sofhiaazzhr Claude Opus 4.8 commited on
Commit
66fbce0
Β·
1 Parent(s): d31c608

[KM-667] feat(help): wire help dispatch in chat_handler

Browse files

Replace 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>

Files changed (1) hide show
  1. 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` β†’ placeholder (handler pending).
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
- # help skill not wired yet (LLM skill, deferred). Placeholder keeps
289
- # dispatch exhaustive; real handler lands in a follow-up step.
290
- logger.info("unwired intent stub", intent=intent)
291
- yield {"event": "chunk", "data": _stub_message(intent)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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,