feat: NCT orchestrator support + Home Assistant webhook
nextcloud_talk.py: - Fix missing import hmac / import hashlib (NameError bug in _verify_signature) - Add orchestrator routing when channels.json "tools": true — sends "⏳ Working on it…" immediately, then runs the full tool loop and replies with the result; checkpoint case gets a web UI confirmation note - Read tier and role from channel config (defaults: default_tier / "chat") - Pass cfg through to _process_message homeassistant.py (new): - POST /webhook/ha/{username}/{webhook_id} - Auth: webhook_id path segment matched against channels.json - Accepts JSON or form-encoded body from HA automations - Builds natural-language task from payload (uses "message" key if present, otherwise serialises full body as context) - Same orchestrator/direct dispatch as NCT - Delivers response via notify() — NC Talk, web push, or configured channel - Session key: ha_{username} for continuity across HA events - Registered in main.py; /webhook/ prefix already public in auth_middleware channels.json schema addition: "homeassistant": { "webhook_id": "your-secret-id", "persona": "inara", "tier": 2, "role": "chat", "tools": false } Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
import asyncio
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, HTTPException, Request, Response
|
||||
|
||||
from auth_utils import get_user_channels
|
||||
from auth_utils import get_user_channels, get_user_gemini_key, get_user_role, get_tool_policy
|
||||
from context_loader import load_context
|
||||
from llm_client import complete
|
||||
from notification import _send_nct_message
|
||||
@@ -13,6 +15,9 @@ from session_logger import log_turn
|
||||
from session_store import load as load_session, save as save_session
|
||||
from config import settings
|
||||
import event_bus
|
||||
import model_registry
|
||||
import orchestrator_engine
|
||||
import openai_orchestrator
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
@@ -50,15 +55,19 @@ async def _process_message(
|
||||
nextcloud_url: str,
|
||||
secret: str,
|
||||
timeout: int,
|
||||
cfg: dict,
|
||||
) -> None:
|
||||
logger.info("NCT process: token=%s user=%s text=%r", conversation_token, actor_name, user_text)
|
||||
|
||||
set_context(username, persona_name)
|
||||
|
||||
session_id = f"nct_{username}_{conversation_token}"
|
||||
system_prompt = load_context(settings.default_tier)
|
||||
history = load_session(session_id)
|
||||
history.append({"role": "user", "content": user_text})
|
||||
tier = cfg.get("tier") or settings.default_tier
|
||||
role = cfg.get("role", "chat")
|
||||
use_tools = cfg.get("tools", False)
|
||||
|
||||
session_id = f"nct_{username}_{conversation_token}"
|
||||
history = load_session(session_id)
|
||||
session_msgs = list(history) # snapshot before we append
|
||||
|
||||
await event_bus.publish({
|
||||
"type": "nct_message",
|
||||
@@ -68,11 +77,69 @@ async def _process_message(
|
||||
"actor": actor_name,
|
||||
})
|
||||
|
||||
backend = "unknown"
|
||||
try:
|
||||
response_text, backend = await asyncio.wait_for(
|
||||
complete(system_prompt=system_prompt, messages=history),
|
||||
timeout=timeout,
|
||||
)
|
||||
if use_tools:
|
||||
await _send_reply(conversation_token, "⏳ Working on it…", nextcloud_url, secret)
|
||||
|
||||
role_cfg = model_registry.get_role_config(username, role)
|
||||
system_prompt = load_context(
|
||||
tier,
|
||||
role_append=role_cfg.get("system_append", ""),
|
||||
inject_datetime=role_cfg.get("inject_datetime", True),
|
||||
inject_mode=role_cfg.get("inject_mode", True),
|
||||
)
|
||||
orch_model = model_registry.get_model_for_role(username, "orchestrator")
|
||||
user_role_val = get_user_role(username)
|
||||
tool_list = role_cfg.get("tools")
|
||||
policy = get_tool_policy(username)
|
||||
c_allow = set(policy.get("allow", []))
|
||||
c_deny = set(policy.get("deny", []))
|
||||
|
||||
if orch_model and orch_model.get("type") == "local_openai":
|
||||
result = await openai_orchestrator.run(
|
||||
task=user_text,
|
||||
system_prompt=system_prompt,
|
||||
session_messages=session_msgs or None,
|
||||
model_cfg=orch_model,
|
||||
user_role=user_role_val,
|
||||
tool_list=tool_list,
|
||||
confirm_allow=c_allow,
|
||||
confirm_deny=c_deny,
|
||||
)
|
||||
else:
|
||||
gemini_key = (
|
||||
(orch_model.get("api_key") if orch_model else None)
|
||||
or get_user_gemini_key(username)
|
||||
)
|
||||
result = await orchestrator_engine.run(
|
||||
task=user_text,
|
||||
system_prompt=system_prompt,
|
||||
session_messages=session_msgs or None,
|
||||
respond_with_claude=True,
|
||||
gemini_api_key=gemini_key,
|
||||
model_name=orch_model.get("model_name") if orch_model else None,
|
||||
response_role=role,
|
||||
user_role=user_role_val,
|
||||
tool_list=tool_list,
|
||||
confirm_allow=c_allow,
|
||||
confirm_deny=c_deny,
|
||||
)
|
||||
|
||||
response_text = result.response
|
||||
backend = result.backend
|
||||
|
||||
if result.checkpoint:
|
||||
response_text += "\n\n_(This action requires confirmation — use the web UI to approve or deny.)_"
|
||||
|
||||
else:
|
||||
system_prompt = load_context(tier)
|
||||
history_for_llm = list(session_msgs) + [{"role": "user", "content": user_text}]
|
||||
response_text, backend = await asyncio.wait_for(
|
||||
complete(system_prompt=system_prompt, messages=history_for_llm),
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning("NCT timeout for %s", conversation_token)
|
||||
await _send_reply(conversation_token, "⏳ Still thinking — this is taking longer than usual.", nextcloud_url, secret)
|
||||
@@ -83,6 +150,8 @@ async def _process_message(
|
||||
return
|
||||
|
||||
logger.info("NCT LLM responded via %s (%d chars)", backend, len(response_text))
|
||||
|
||||
history.append({"role": "user", "content": user_text})
|
||||
history.append({"role": "assistant", "content": response_text})
|
||||
save_session(session_id, history)
|
||||
log_turn(session_id, user_text, response_text)
|
||||
@@ -163,6 +232,6 @@ async def nextcloud_talk_webhook(username: str, request: Request, background_tas
|
||||
background_tasks.add_task(
|
||||
_process_message,
|
||||
conversation_token, user_text, actor_name,
|
||||
username, persona_name, nextcloud_url, secret, timeout,
|
||||
username, persona_name, nextcloud_url, secret, timeout, cfg,
|
||||
)
|
||||
return Response(status_code=200)
|
||||
|
||||
Reference in New Issue
Block a user