Commit Β·
66fbce0
1
Parent(s): d31c608
[KM-667] feat(help): wire help dispatch in chat_handler
Browse filesReplace the help placeholder with the real handler. _load_analysis_state
fail-closes to a not-validated stub (legacy room / read error); help_agent
injectable; streams chunk/done with masked callbacks. Remove dead _stub_message
(help was the last unwired intent).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- src/agents/chat_handler.py +49 -10
src/agents/chat_handler.py
CHANGED
|
@@ -10,7 +10,7 @@ End-to-end flow per user message:
|
|
| 10 |
list[DocumentChunk].
|
| 11 |
- `check` β check_data / check_knowledge tool β rendered table.
|
| 12 |
- `problem_statement` β PS skill: draft + validate β write analysis state.
|
| 13 |
-
- `help` β
|
| 14 |
3. `ChatbotAgent.astream` β yield text tokens.
|
| 15 |
4. Wrap each step into an SSE-style event dict so the API endpoint can
|
| 16 |
stream them as Server-Sent Events.
|
|
@@ -38,6 +38,7 @@ from src.retrieval.base import RetrievalResult
|
|
| 38 |
|
| 39 |
from .chatbot import ChatbotAgent, DocumentChunk
|
| 40 |
from .handlers.check import run_check
|
|
|
|
| 41 |
from .handlers.problem_statement import ProblemStatementAgent, run_problem_statement
|
| 42 |
from .orchestration import OrchestratorAgent
|
| 43 |
|
|
@@ -45,6 +46,7 @@ if TYPE_CHECKING:
|
|
| 45 |
from ..catalog.reader import CatalogReader
|
| 46 |
from ..query.service import QueryService
|
| 47 |
from ..retrieval.router import RetrievalRouter
|
|
|
|
| 48 |
from .slow_path.coordinator import SlowPathCoordinator
|
| 49 |
from .slow_path.store import AnalysisStore
|
| 50 |
|
|
@@ -79,6 +81,7 @@ class ChatHandler:
|
|
| 79 |
analysis_store: AnalysisStore | None = None,
|
| 80 |
check_invoker_factory: Callable[[str], Any] | None = None,
|
| 81 |
ps_agent: ProblemStatementAgent | None = None,
|
|
|
|
| 82 |
state_store: Any | None = None,
|
| 83 |
enable_gate: bool = False,
|
| 84 |
enable_tracing: bool = False,
|
|
@@ -104,6 +107,8 @@ class ChatHandler:
|
|
| 104 |
# `problem_statement` skill: LLM drafter + the Analysis State store it writes
|
| 105 |
# `problem_validated` to. Both injectable for tests.
|
| 106 |
self._ps_agent = ps_agent
|
|
|
|
|
|
|
| 107 |
self._state_store = state_store
|
| 108 |
# Deterministic gate: redirect structured_flow -> problem_statement until the
|
| 109 |
# analysis is validated. OFF by default (legacy rooms have no state row).
|
|
@@ -158,6 +163,11 @@ class ChatHandler:
|
|
| 158 |
self._ps_agent = ProblemStatementAgent()
|
| 159 |
return self._ps_agent
|
| 160 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
def _get_state_store(self) -> Any:
|
| 162 |
if self._state_store is None:
|
| 163 |
from .state_store import AnalysisStateStore
|
|
@@ -165,6 +175,23 @@ class ChatHandler:
|
|
| 165 |
self._state_store = AnalysisStateStore()
|
| 166 |
return self._state_store
|
| 167 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
# ------------------------------------------------------------------
|
| 169 |
# Public entry
|
| 170 |
# ------------------------------------------------------------------
|
|
@@ -285,10 +312,27 @@ class ChatHandler:
|
|
| 285 |
yield {"event": "done", "data": ""}
|
| 286 |
return
|
| 287 |
elif intent == "help":
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
yield {"event": "done", "data": ""}
|
| 293 |
return
|
| 294 |
# else: chat path β no context
|
|
@@ -480,11 +524,6 @@ def _intent_to_source_hint(intent: str) -> str:
|
|
| 480 |
return "chat"
|
| 481 |
|
| 482 |
|
| 483 |
-
def _stub_message(intent: str) -> str:
|
| 484 |
-
"""Placeholder reply for intents whose handlers are not wired yet."""
|
| 485 |
-
return f"The '{intent}' route is recognized but not wired up yet."
|
| 486 |
-
|
| 487 |
-
|
| 488 |
def _build_sources(
|
| 489 |
intent: str,
|
| 490 |
user_id: str,
|
|
|
|
| 10 |
list[DocumentChunk].
|
| 11 |
- `check` β check_data / check_knowledge tool β rendered table.
|
| 12 |
- `problem_statement` β PS skill: draft + validate β write analysis state.
|
| 13 |
+
- `help` β Help skill: analysis state + history β streamed guidance.
|
| 14 |
3. `ChatbotAgent.astream` β yield text tokens.
|
| 15 |
4. Wrap each step into an SSE-style event dict so the API endpoint can
|
| 16 |
stream them as Server-Sent Events.
|
|
|
|
| 38 |
|
| 39 |
from .chatbot import ChatbotAgent, DocumentChunk
|
| 40 |
from .handlers.check import run_check
|
| 41 |
+
from .handlers.help import HelpAgent
|
| 42 |
from .handlers.problem_statement import ProblemStatementAgent, run_problem_statement
|
| 43 |
from .orchestration import OrchestratorAgent
|
| 44 |
|
|
|
|
| 46 |
from ..catalog.reader import CatalogReader
|
| 47 |
from ..query.service import QueryService
|
| 48 |
from ..retrieval.router import RetrievalRouter
|
| 49 |
+
from .gate import AnalysisState
|
| 50 |
from .slow_path.coordinator import SlowPathCoordinator
|
| 51 |
from .slow_path.store import AnalysisStore
|
| 52 |
|
|
|
|
| 81 |
analysis_store: AnalysisStore | None = None,
|
| 82 |
check_invoker_factory: Callable[[str], Any] | None = None,
|
| 83 |
ps_agent: ProblemStatementAgent | None = None,
|
| 84 |
+
help_agent: HelpAgent | None = None,
|
| 85 |
state_store: Any | None = None,
|
| 86 |
enable_gate: bool = False,
|
| 87 |
enable_tracing: bool = False,
|
|
|
|
| 107 |
# `problem_statement` skill: LLM drafter + the Analysis State store it writes
|
| 108 |
# `problem_validated` to. Both injectable for tests.
|
| 109 |
self._ps_agent = ps_agent
|
| 110 |
+
# `help` skill: LLM guide that reads the Analysis State + chat history.
|
| 111 |
+
self._help_agent = help_agent
|
| 112 |
self._state_store = state_store
|
| 113 |
# Deterministic gate: redirect structured_flow -> problem_statement until the
|
| 114 |
# analysis is validated. OFF by default (legacy rooms have no state row).
|
|
|
|
| 163 |
self._ps_agent = ProblemStatementAgent()
|
| 164 |
return self._ps_agent
|
| 165 |
|
| 166 |
+
def _get_help_agent(self) -> HelpAgent:
|
| 167 |
+
if self._help_agent is None:
|
| 168 |
+
self._help_agent = HelpAgent()
|
| 169 |
+
return self._help_agent
|
| 170 |
+
|
| 171 |
def _get_state_store(self) -> Any:
|
| 172 |
if self._state_store is None:
|
| 173 |
from .state_store import AnalysisStateStore
|
|
|
|
| 175 |
self._state_store = AnalysisStateStore()
|
| 176 |
return self._state_store
|
| 177 |
|
| 178 |
+
async def _load_analysis_state(self, analysis_id: str | None) -> AnalysisState:
|
| 179 |
+
"""Load Analysis State for the Help skill; fail closed to a not-validated stub.
|
| 180 |
+
|
| 181 |
+
Mirrors the gate's never-throw fallback so Help degrades gracefully on a
|
| 182 |
+
missing row, a read error, or a legacy room with no `analysis_id`.
|
| 183 |
+
"""
|
| 184 |
+
from .gate import stub_analysis_state
|
| 185 |
+
|
| 186 |
+
if not analysis_id:
|
| 187 |
+
return stub_analysis_state(problem_validated=False)
|
| 188 |
+
try:
|
| 189 |
+
state = await self._get_state_store().get(analysis_id)
|
| 190 |
+
except Exception as e:
|
| 191 |
+
logger.warning("help state read failed β not-validated", error=str(e))
|
| 192 |
+
state = None
|
| 193 |
+
return state if state is not None else stub_analysis_state(problem_validated=False)
|
| 194 |
+
|
| 195 |
# ------------------------------------------------------------------
|
| 196 |
# Public entry
|
| 197 |
# ------------------------------------------------------------------
|
|
|
|
| 312 |
yield {"event": "done", "data": ""}
|
| 313 |
return
|
| 314 |
elif intent == "help":
|
| 315 |
+
try:
|
| 316 |
+
state = await self._load_analysis_state(analysis_id)
|
| 317 |
+
except Exception as e:
|
| 318 |
+
logger.error("help route failed", user_id=user_id, error=str(e))
|
| 319 |
+
yield {"event": "error", "data": f"Help failed: {e}"}
|
| 320 |
+
return
|
| 321 |
+
# report_ready is the deterministic readiness signal (seam #5). Until
|
| 322 |
+
# `is_report_ready` lands, the default (not ready) degrades safely β Help
|
| 323 |
+
# won't suggest generating a report. The prompt sees chat history -> masked.
|
| 324 |
+
hc = tracer.callbacks(masked=True)
|
| 325 |
+
hkw = {"callbacks": hc} if hc else {}
|
| 326 |
+
try:
|
| 327 |
+
async for token in self._get_help_agent().astream(
|
| 328 |
+
state, history=history, message=message, **hkw
|
| 329 |
+
):
|
| 330 |
+
yield {"event": "chunk", "data": token}
|
| 331 |
+
except Exception as e:
|
| 332 |
+
logger.error("help streaming failed", user_id=user_id, error=str(e))
|
| 333 |
+
yield {"event": "error", "data": f"Help generation failed: {e}"}
|
| 334 |
+
return
|
| 335 |
+
tracer.end()
|
| 336 |
yield {"event": "done", "data": ""}
|
| 337 |
return
|
| 338 |
# else: chat path β no context
|
|
|
|
| 524 |
return "chat"
|
| 525 |
|
| 526 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 527 |
def _build_sources(
|
| 528 |
intent: str,
|
| 529 |
user_id: str,
|