diff --git a/apps/api/migrations/adr094_hermes_dispatch_log.sql b/apps/api/migrations/adr094_hermes_dispatch_log.sql new file mode 100644 index 00000000..dee4a0be --- /dev/null +++ b/apps/api/migrations/adr094_hermes_dispatch_log.sql @@ -0,0 +1,26 @@ +-- ADR-094: Hermes NL Dispatch Audit Log +-- 每次 @mention 觸發 → 記錄派發決策供 P95 latency 監控與幻覺追蹤 +-- 2026-04-25 ogt + Claude Sonnet 4.6 + +CREATE TABLE IF NOT EXISTS hermes_dispatch_log ( + id BIGSERIAL PRIMARY KEY, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + chat_id VARCHAR(32) NOT NULL, + user_id BIGINT NOT NULL, + username VARCHAR(100), + agent_name VARCHAR(64) NOT NULL, + input_preview VARCHAR(200), -- 前 200 字,不存完整輸入(隱私) + latency_ms INTEGER, + success BOOLEAN NOT NULL DEFAULT TRUE, + error_type VARCHAR(64), + budget_usd NUMERIC(8, 5) +); + +CREATE INDEX IF NOT EXISTS idx_hermes_dispatch_created ON hermes_dispatch_log(created_at DESC); +CREATE INDEX IF NOT EXISTS idx_hermes_dispatch_agent ON hermes_dispatch_log(agent_name); +CREATE INDEX IF NOT EXISTS idx_hermes_dispatch_user ON hermes_dispatch_log(user_id); + +GRANT SELECT, INSERT ON hermes_dispatch_log TO awoooi; +GRANT USAGE, SELECT ON SEQUENCE hermes_dispatch_log_id_seq TO awoooi; + +COMMENT ON TABLE hermes_dispatch_log IS 'ADR-094: Hermes NL 派發審計日誌(P95 latency 監控 + 幻覺追蹤)'; diff --git a/apps/api/src/hermes/nl_gateway.py b/apps/api/src/hermes/nl_gateway.py index 3541d600..6cec65ab 100644 --- a/apps/api/src/hermes/nl_gateway.py +++ b/apps/api/src/hermes/nl_gateway.py @@ -6,6 +6,7 @@ Layer 1 意圖路由(關鍵字正則)→ Claude Agent SDK 呼叫 → Telegra """ from __future__ import annotations import re +import time import structlog from claude_agent_sdk import query, ClaudeAgentOptions @@ -93,15 +94,10 @@ async def process_nl_message( agent_name = DEFAULT_AGENT system_prompt = get_agent_system_prompt(agent_name) or "" - logger.info( - "hermes_nl_dispatch", - agent=agent_name, - user_id=user_id, - chat_id=chat_id, - username=username, - ) + t0 = time.monotonic() # 呼叫 Claude Agent SDK + success = False try: options = ClaudeAgentOptions( system_prompt=system_prompt, @@ -117,6 +113,7 @@ async def process_nl_message( if not result_text: result_text = "_Agent 回應為空,請稍後再試。_" + success = True except Exception as exc: logger.error( @@ -127,6 +124,17 @@ async def process_nl_message( ) result_text = f"_Hermes 暫時無法連線({type(exc).__name__}),請稍後再試。_" + latency_ms = int((time.monotonic() - t0) * 1000) + logger.info( + "hermes_nl_dispatch", + agent=agent_name, + user_id=user_id, + chat_id=chat_id, + username=username, + latency_ms=latency_ms, + success=success, + ) + header = format_response_header(agent_name) # Telegram 訊息上限 4096 字元,超過截斷 body = result_text[:3800]