Rifqi Hafizuddin Claude Opus 4.8 commited on
Commit
057e0b2
Β·
1 Parent(s): 4835fb8

[KM-652] refactor: remove problem_statement skill + problem_validated gate

Browse files

2026-06-24 pivot: the analysis goal is now two user-entered fields (objective + business_questions) captured at onboarding with no agent validation, so the problem_statement skill and the problem_validated gate are removed (comment, don't delete).

- Router: drop problem_statement from the Intent literal + intent_router.md (6->5 intents); comment the dispatch branch in chat_handler; remove /problem-statement from list_tools. problem_statement.py kept intact but unwired.
- Gate: gate() neutered to a no-op; call site commented in chat_handler; enable_gate marked deprecated. report_floor drops the problem_validated precondition -> floor is now >=1 completed analysis only.
- Docs: REPO_STATUS.md + DEV_PLAN.md updated.
- Tests (local, gitignored): test_gate / test_chat_handler / test_readiness updated. Full suite 284 passed, 7 skipped; ruff clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

DEV_PLAN.md CHANGED
@@ -82,8 +82,8 @@ Status legend: ⬜ not started Β· πŸ”„ in progress Β· βœ… done Β· β›” blocked Β·
82
 
83
  | # | Task | Owner | Status | Note |
84
  |---|---|---|---|---|
85
- | 1 | Comment out `problem_statement` skill **+ `Intent` literal + router prompt + gate redirect target**; remove `/problem-statement` from `list_tools` | Rifqi | ⬜ | Do with #2 β€” 4 places name it |
86
- | 2 | Drop `problem_validated`: gate β†’ identity/presence-check (or remove); `is_report_ready` β†’ presence(`objective` + β‰₯1 `business_question`) + β‰₯1 analysis, no-LLM | Rifqi | ⬜ | Merges Sofhia 2+3. Make reads tolerant (getattr) |
87
  | 3 | dedorch `analysis` migration: drop `problem_statement`/`problem_validated`, add `objective` + `business_questions`, rename `owner_id`β†’`user_id` | Harry | ⬜ | Blocks #4 |
88
  | 4 | Update Python `analysis` model + `state_store` + `analysis.py` to match #3; `owner_id`β†’`user_id` | Rifqi/Sofhia | β›” after #3 | getattr-tolerant so deploy order isn't a hard blocker |
89
  | 5 | Report generator β†’ `objective`+`business_questions`, "generated by {user}", formal outline | Sofhia | πŸ”„ pushed (KM-674) | Goal-shape done. Open: author name source (#19); background/EDA/insights mapping |
@@ -101,6 +101,7 @@ Status legend: ⬜ not started Β· πŸ”„ in progress Β· βœ… done Β· β›” blocked Β·
101
  | 17 | Reconcile report contract with records-based: remove/flag `chat_history` | Sofhia/Rifqi | ⬜ new | Resolves #5 ↔ #6 contradiction |
102
  | 18 | Confirm Go owns ALL analysis-state writes + both creation gates; make Python `state_store.ensure` read-only | Rifqi + Harry | ⬜ new | Extends #7 to the full decision #4 (G5) |
103
  | 19 | Decide report author display-name source (`users.fullname` vs Go-passed name) | Sofhia | ⬜ new | Raw `user_id` in a formal report looks unprofessional |
 
104
 
105
  ## 5. Critical path & sequencing
106
 
 
82
 
83
  | # | Task | Owner | Status | Note |
84
  |---|---|---|---|---|
85
+ | 1 | Comment out `problem_statement` skill **+ `Intent` literal + router prompt + gate redirect target**; remove `/problem-statement` from `list_tools` | Rifqi | βœ… | Done 2026-06-25 (one commit w/ #2). Unwired in `orchestration.py`, `intent_router.md`, `chat_handler.py`, `tools.py`; `problem_statement.py` kept intact |
86
+ | 2 | Drop `problem_validated`: gate neutered; `is_report_ready`/`report_floor` β†’ **β‰₯1 completed analysis** only, no-LLM | Rifqi | βœ… | Done 2026-06-25. `gate.py` no-op, gate call site commented in `chat_handler.py`, `report_floor` drops the goal check. Tests updated (`test_gate`/`test_chat_handler`/`test_readiness`). Suite: **284 passed, 7 skipped**; ruff clean |
87
  | 3 | dedorch `analysis` migration: drop `problem_statement`/`problem_validated`, add `objective` + `business_questions`, rename `owner_id`β†’`user_id` | Harry | ⬜ | Blocks #4 |
88
  | 4 | Update Python `analysis` model + `state_store` + `analysis.py` to match #3; `owner_id`β†’`user_id` | Rifqi/Sofhia | β›” after #3 | getattr-tolerant so deploy order isn't a hard blocker |
89
  | 5 | Report generator β†’ `objective`+`business_questions`, "generated by {user}", formal outline | Sofhia | πŸ”„ pushed (KM-674) | Goal-shape done. Open: author name source (#19); background/EDA/insights mapping |
 
101
  | 17 | Reconcile report contract with records-based: remove/flag `chat_history` | Sofhia/Rifqi | ⬜ new | Resolves #5 ↔ #6 contradiction |
102
  | 18 | Confirm Go owns ALL analysis-state writes + both creation gates; make Python `state_store.ensure` read-only | Rifqi + Harry | ⬜ new | Extends #7 to the full decision #4 (G5) |
103
  | 19 | Decide report author display-name source (`users.fullname` vs Go-passed name) | Sofhia | ⬜ new | Raw `user_id` in a formal report looks unprofessional |
104
+ | 20 | **Help handoff:** update `handlers/help.py` + `help.md` β€” drop the `problem_validated` tier + `define_problem_statement` action (the skill it points at is gone as of #1) | Sofhia | ⬜ new | Created by #1/#2 (2026-06-25). Help currently still steers users to a removed skill |
105
 
106
  ## 5. Critical path & sequencing
107
 
REPO_STATUS.md CHANGED
@@ -1,8 +1,8 @@
1
  # Data Eyond β€” Python Agentic Service: Current Status
2
 
3
  **Audience:** teammates onboarding onto the Python repo (`Agentic-Service-Data-Eyond-Catalog`).
4
- **Scope:** what the code does **right now** (branch `pr/3`, ticket KM-652). Describes current state only β€” no roadmap or to-dos.
5
- **Snapshot date:** 2026-06-24.
6
 
7
  > This file is grounded in the source, not the older design docs. Where the two
8
  > disagree, the code wins β€” see [Β§11 Doc-vs-code](#11-where-the-older-docs-are-stale).
@@ -68,19 +68,21 @@ POST /chat/stream { user_id, room_id, message }
68
  └─ ChatHandler.handle:
69
  1. classify β†’ RouterDecision [1 GPT-4o call]
70
  2. ensure analysis-state row (get-or-create, idempotent)
71
- 3. gate (if ENABLE_GATE): structured_flow + not validated β†’ problem_statement
72
- 4. emit `intent` (internal; gates caching), then dispatch:
73
  chat β†’ ChatbotAgent β†’ SSE
74
  help β†’ HelpAgent (state + history + readiness) β†’ SSE
75
  check β†’ check_data/check_knowledge tool β†’ rendered table [no LLM]
76
- problem_statement β†’ PS draft + validate β†’ write state
77
  unstructured_flow β†’ DocumentRetriever (PGVector RAG) β†’ ChatbotAgent β†’ SSE
78
  structured_flow β†’ CatalogReader β†’ (slow path | QueryService) β†’ SSE
79
- 5. SSE events: intent (internal), sources, chunk, status, done | error
80
  ```
81
 
82
  Only the `chat` intent is cached (stateless). Messages persist on `done`.
83
 
 
 
 
 
84
  ---
85
 
86
  ## 5. Report lifecycle
@@ -90,7 +92,7 @@ The report is a **dedicated API, not a chat route** (`src/api/v1/report.py`):
90
  ```
91
  POST /report?analysis_id&user_id
92
  β”œβ”€ load analysis state; enforce the report FLOOR
93
- β”‚ (validated goal + β‰₯1 substantive analyze_* success) β†’ else 409
94
  β”œβ”€ ReportGenerator.generate (src/agents/report/generator.py):
95
  β”‚ read persisted AnalysisRecords (list_for_analysis)
96
  β”‚ deterministically assemble findings / caveats / open-questions /
@@ -115,9 +117,8 @@ Two facts to internalise:
115
 
116
  ## 6. Feature list (what's built)
117
 
118
- - **6-intent handler router** with history-aware query rewriting (EN/ID).
119
- - **Deterministic gate** β€” `problem_validated` precondition for analytical queries.
120
- - **Skills:** `help` (LLM, state-aware next-step guidance), `check` (no-LLM data/document inventory), `problem_statement` (LLM drafter + completeness validator).
121
  - **Slow analytical path:** Planner β†’ TaskRunner β†’ Assembler (static plan, degrade-and-continue, 3 LLM calls fixed).
122
  - **Structured query engine:** catalog-driven JSON IR β†’ deterministic SQL/pandas compiler β†’ read-only executor, with **single-level FK joins** (DB sources only).
123
  - **Unstructured RAG** over PGVector.
@@ -132,7 +133,7 @@ Two facts to internalise:
132
 
133
  | Endpoint | Purpose | Caller |
134
  |---|---|---|
135
- | `POST /chat/stream` | Main chat SSE (router → gate → dispatch) | FE → Go → Python (the only FE→Python call today) |
136
  | `DELETE /chat/cache` Β· `/chat/cache/room/{id}` Β· `/retrieval/cache/{user_id}` | Cache management | internal / ops |
137
  | `POST /analysis/create` Β· `GET /analysis` Β· `GET /analysis/{id}` | Analysis-session CRUD (state + room + bindings created atomically) | intended FE β†’ Go |
138
  | `POST /report` Β· `GET /report/{id}` Β· `GET /report/{id}/{ver}` | Report generate / list / fetch | FE β†’ Go (report button) |
@@ -171,16 +172,18 @@ Joins are single-level equi-joins to a related table **in the same source**, FK-
171
 
172
  ### Router β€” `src/agents/orchestration.py`
173
  One GPT-4o structured-output call β†’ `RouterDecision{intent, rewritten_query, confidence}`,
174
- `intent ∈ {chat, help, problem_statement, check, unstructured_flow, structured_flow}`. It's a
 
175
  *handler* classifier: `structured_flow` = slow path, `unstructured_flow` = fast RAG; the
176
  data-modality mix on the slow path is the Planner's job. Prompt: `src/config/prompts/intent_router.md`.
177
 
178
  ### Gate β€” `src/agents/gate.py`
179
- Pure function, no I/O. One rule: `structured_flow` + `not problem_validated` β†’ redirect to
180
- `problem_statement`; everything else passes. Fails **closed** (not-validated stub) on a missing
181
- row / read error. `AnalysisState` is the locked contract (id, analysis_title, problem_statement,
182
- problem_validated, owner_id, report_id, created_at, updated_at). Applied in `chat_handler.handle`
183
- only when `ENABLE_GATE` and `analysis_id` are present.
 
184
 
185
  ### Skills β€” `src/agents/handlers/`
186
  - `help.py` β€” LLM (streamed). A consistency guard derives the *allowed* actions from state
@@ -188,9 +191,8 @@ only when `ENABLE_GATE` and `analysis_id` are present.
188
  isn't validated or there's nothing to report. Consumes a deterministic readiness signal.
189
  - `check.py` β€” **no LLM.** Keyword cues route to `check_data`, `check_knowledge`, or both
190
  (helicopter view, concurrent). Renders tool tables to markdown.
191
- - `problem_statement.py` β€” LLM drafts `{problem_statement, objective, metric, missing[], feedback}`;
192
- valid iff a statement exists and `missing` is empty (a bare data question stays incomplete). On a
193
- valid draft, writes `problem_statement` + `problem_validated=True`.
194
 
195
  ### Slow path β€” `src/agents/slow_path/` + `src/agents/planner/`
196
  - **Planner** (`planner/service.py`) β€” 1 LLM call β†’ `TaskList` (DAG of tool-call chains). 8-check
@@ -231,8 +233,9 @@ real `analytics_registry()`.
231
  ### Report β€” `src/agents/report/`
232
  `generator.py` reads records, deterministically assembles structured fields, 1 LLM call for the
233
  executive summary; `store.py` versions under an advisory lock and persists markdown to dedorch
234
- `reports`; `readiness.py` defines the **report floor** (validated goal + β‰₯1 successful `analyze_*`)
235
- shared by the report API and the Help readiness signal so the two can't disagree.
 
236
 
237
  ### Observability β€” Langfuse
238
  The endpoint's `ChatHandler` runs with `enable_tracing=True`. One trace per request groups
@@ -247,7 +250,7 @@ only.
247
  | Flag | Where | Default | Effect |
248
  |---|---|---|---|
249
  | `ENABLE_SLOW_PATH` | `settings.enable_slow_path` | **off** | Route `structured_flow` through Planner/TaskRunner/Assembler (vs single-query `QueryService`). Records persist only on the slow path β†’ reports require this on. |
250
- | `ENABLE_GATE` | `settings.enable_gate` | **off** | Apply the `problem_validated` redirect. Off because legacy rooms have no state row. |
251
  | `SKIP_INIT_DB` | env, `main.py` | off | Skip `create_all` on startup β€” the dedorch cutover switch (Go owns dedorch migrations). |
252
  | `enable_tracing` | hardcoded `True` in `chat.py` | on (endpoint) | Langfuse tracing. |
253
 
@@ -260,7 +263,7 @@ copies disagree with the current code on:
260
 
261
  | Topic | Old docs | Current code |
262
  |---|---|---|
263
- | Router | 3-way `source_hint` (chat/unstructured/structured) | Flat **6-intent** `RouterDecision` |
264
  | Joins in IR | "single-table only; deferred" | **Single-level FK-backed joins** (DB sources only) |
265
  | Analysis / report / gate / slow path | "Phase 2 spine only" | All built and present |
266
  | `analysis_id` | open question | resolved: **`analysis_id == room_id`** |
 
1
  # Data Eyond β€” Python Agentic Service: Current Status
2
 
3
  **Audience:** teammates onboarding onto the Python repo (`Agentic-Service-Data-Eyond-Catalog`).
4
+ **Scope:** what the code does **right now** (branch `pr/4`, ticket KM-652). Describes current state only β€” no roadmap or to-dos.
5
+ **Snapshot date:** 2026-06-25.
6
 
7
  > This file is grounded in the source, not the older design docs. Where the two
8
  > disagree, the code wins β€” see [Β§11 Doc-vs-code](#11-where-the-older-docs-are-stale).
 
68
  └─ ChatHandler.handle:
69
  1. classify β†’ RouterDecision [1 GPT-4o call]
70
  2. ensure analysis-state row (get-or-create, idempotent)
71
+ 3. emit `intent` (internal; gates caching), then dispatch:
 
72
  chat β†’ ChatbotAgent β†’ SSE
73
  help β†’ HelpAgent (state + history + readiness) β†’ SSE
74
  check β†’ check_data/check_knowledge tool β†’ rendered table [no LLM]
 
75
  unstructured_flow β†’ DocumentRetriever (PGVector RAG) β†’ ChatbotAgent β†’ SSE
76
  structured_flow β†’ CatalogReader β†’ (slow path | QueryService) β†’ SSE
77
+ 4. SSE events: intent (internal), sources, chunk, status, done | error
78
  ```
79
 
80
  Only the `chat` intent is cached (stateless). Messages persist on `done`.
81
 
82
+ > The router emits **5 intents** now. The `problem_statement` skill and the `problem_validated`
83
+ > gate were removed 2026-06-25 (KM-652) β€” the analysis goal is two user-entered fields
84
+ > (`objective` + `business_questions`) captured at onboarding, with no agent validation.
85
+
86
  ---
87
 
88
  ## 5. Report lifecycle
 
92
  ```
93
  POST /report?analysis_id&user_id
94
  β”œβ”€ load analysis state; enforce the report FLOOR
95
+ β”‚ (β‰₯1 substantive analyze_* success) β†’ else 409
96
  β”œβ”€ ReportGenerator.generate (src/agents/report/generator.py):
97
  β”‚ read persisted AnalysisRecords (list_for_analysis)
98
  β”‚ deterministically assemble findings / caveats / open-questions /
 
117
 
118
  ## 6. Feature list (what's built)
119
 
120
+ - **5-intent handler router** (`chat`/`help`/`check`/`unstructured_flow`/`structured_flow`) with history-aware query rewriting (EN/ID).
121
+ - **Skills:** `help` (LLM, state-aware next-step guidance), `check` (no-LLM data/document inventory). *(The `problem_statement` skill and the `problem_validated` gate were removed 2026-06-25 β€” KM-652; `gate.py` kept as a no-op seam, `problem_statement.py` kept but unwired.)*
 
122
  - **Slow analytical path:** Planner β†’ TaskRunner β†’ Assembler (static plan, degrade-and-continue, 3 LLM calls fixed).
123
  - **Structured query engine:** catalog-driven JSON IR β†’ deterministic SQL/pandas compiler β†’ read-only executor, with **single-level FK joins** (DB sources only).
124
  - **Unstructured RAG** over PGVector.
 
133
 
134
  | Endpoint | Purpose | Caller |
135
  |---|---|---|
136
+ | `POST /chat/stream` | Main chat SSE (router → dispatch) | FE → Go → Python (the only FE→Python call today) |
137
  | `DELETE /chat/cache` Β· `/chat/cache/room/{id}` Β· `/retrieval/cache/{user_id}` | Cache management | internal / ops |
138
  | `POST /analysis/create` Β· `GET /analysis` Β· `GET /analysis/{id}` | Analysis-session CRUD (state + room + bindings created atomically) | intended FE β†’ Go |
139
  | `POST /report` Β· `GET /report/{id}` Β· `GET /report/{id}/{ver}` | Report generate / list / fetch | FE β†’ Go (report button) |
 
172
 
173
  ### Router β€” `src/agents/orchestration.py`
174
  One GPT-4o structured-output call β†’ `RouterDecision{intent, rewritten_query, confidence}`,
175
+ `intent ∈ {chat, help, check, unstructured_flow, structured_flow}` (`problem_statement` removed
176
+ 2026-06-25). It's a
177
  *handler* classifier: `structured_flow` = slow path, `unstructured_flow` = fast RAG; the
178
  data-modality mix on the slow path is the Planner's job. Prompt: `src/config/prompts/intent_router.md`.
179
 
180
  ### Gate β€” `src/agents/gate.py`
181
+ **Neutered 2026-06-25 (KM-652):** `gate()` now passes every intent through unchanged β€” the
182
+ `problem_validated` redirect was removed (the goal is user-entered, no agent validation). The
183
+ function + `AnalysisState` contract are kept as a no-op seam; the call site in
184
+ `chat_handler.handle` is commented out. `AnalysisState` still carries (id, analysis_title,
185
+ problem_statement, problem_validated, owner_id, report_id, created_at, updated_at) until the
186
+ dedorch state migration (#3/#4) renames it.
187
 
188
  ### Skills β€” `src/agents/handlers/`
189
  - `help.py` β€” LLM (streamed). A consistency guard derives the *allowed* actions from state
 
191
  isn't validated or there's nothing to report. Consumes a deterministic readiness signal.
192
  - `check.py` β€” **no LLM.** Keyword cues route to `check_data`, `check_knowledge`, or both
193
  (helicopter view, concurrent). Renders tool tables to markdown.
194
+ - `problem_statement.py` β€” **unwired 2026-06-25** (no longer routed to; file kept intact). Was an
195
+ LLM drafter that validated a goal and wrote `problem_validated`.
 
196
 
197
  ### Slow path β€” `src/agents/slow_path/` + `src/agents/planner/`
198
  - **Planner** (`planner/service.py`) β€” 1 LLM call β†’ `TaskList` (DAG of tool-call chains). 8-check
 
233
  ### Report β€” `src/agents/report/`
234
  `generator.py` reads records, deterministically assembles structured fields, 1 LLM call for the
235
  executive summary; `store.py` versions under an advisory lock and persists markdown to dedorch
236
+ `reports`; `readiness.py` defines the **report floor** (β‰₯1 successful `analyze_*`; the
237
+ `problem_validated` precondition was dropped 2026-06-25) shared by the report API and the Help
238
+ readiness signal so the two can't disagree.
239
 
240
  ### Observability β€” Langfuse
241
  The endpoint's `ChatHandler` runs with `enable_tracing=True`. One trace per request groups
 
250
  | Flag | Where | Default | Effect |
251
  |---|---|---|---|
252
  | `ENABLE_SLOW_PATH` | `settings.enable_slow_path` | **off** | Route `structured_flow` through Planner/TaskRunner/Assembler (vs single-query `QueryService`). Records persist only on the slow path β†’ reports require this on. |
253
+ | `ENABLE_GATE` | `settings.enable_gate` | **off** | **Deprecated 2026-06-25** β€” gate neutered; the flag has no effect. Kept to avoid `.env` churn. |
254
  | `SKIP_INIT_DB` | env, `main.py` | off | Skip `create_all` on startup β€” the dedorch cutover switch (Go owns dedorch migrations). |
255
  | `enable_tracing` | hardcoded `True` in `chat.py` | on (endpoint) | Langfuse tracing. |
256
 
 
263
 
264
  | Topic | Old docs | Current code |
265
  |---|---|---|
266
+ | Router | 3-way `source_hint` (chat/unstructured/structured) | Flat **5-intent** `RouterDecision` (was 6; `problem_statement` removed 2026-06-25) |
267
  | Joins in IR | "single-table only; deferred" | **Single-level FK-backed joins** (DB sources only) |
268
  | Analysis / report / gate / slow path | "Phase 2 spine only" | All built and present |
269
  | `analysis_id` | open question | resolved: **`analysis_id == room_id`** |
src/agents/chat_handler.py CHANGED
@@ -9,8 +9,10 @@ End-to-end flow per user message:
9
  - `unstructured_flow` β†’ DocumentRetriever (RAG over PGVector) β†’
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.
@@ -39,7 +41,9 @@ from src.retrieval.base import RetrievalResult
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
 
45
  if TYPE_CHECKING:
@@ -114,8 +118,8 @@ class ChatHandler:
114
  # `#10` data-source binding: scopes structured_flow's catalog to the sources
115
  # the analysis is bound to. Injectable for tests; fail-open when absent.
116
  self._binding_store = binding_store
117
- # Deterministic gate: redirect structured_flow -> problem_statement until the
118
- # analysis is validated. OFF by default (legacy rooms have no state row).
119
  self._enable_gate = enable_gate
120
 
121
  # ------------------------------------------------------------------
@@ -244,9 +248,8 @@ class ChatHandler:
244
 
245
  intent = decision.intent
246
  # ---- 1a. Ensure session state row (T-A) ----------------------
247
- # Rooms created via /room/create have no `analysis_states` row. Without one
248
- # the gate redirect-loops and problem_statement / report_id writes silently
249
- # no-op. Lazily get-or-create it (idempotent) so any session is gate-ready.
250
  analysis_state: AnalysisState | None = None
251
  if analysis_id:
252
  try:
@@ -256,18 +259,20 @@ class ChatHandler:
256
  "analysis state ensure failed", analysis_id=analysis_id, error=str(e)
257
  )
258
 
259
- # ---- 1b. Gate (deterministic, post-router) -------------------
260
- # Redirect structured_flow -> problem_statement until the analysis is
261
- # validated. Fails closed (not-validated) when the state row is unavailable.
262
- if self._enable_gate and analysis_id:
263
- from .gate import gate, stub_analysis_state
264
-
265
- intent = gate(
266
- intent,
267
- analysis_state
268
- if analysis_state is not None
269
- else stub_analysis_state(problem_validated=False),
270
- )
 
 
271
 
272
  # The `intent` event is consumed by the endpoint (it gates response caching
273
  # on the effective intent) and is NOT forwarded to the frontend. We emit the
@@ -337,22 +342,24 @@ class ChatHandler:
337
  yield {"event": "chunk", "data": text}
338
  yield {"event": "done", "data": ""}
339
  return
340
- elif intent == "problem_statement":
341
- try:
342
- text = await run_problem_statement(
343
- message,
344
- analysis_id,
345
- agent=self._get_ps_agent(),
346
- store=self._get_state_store(),
347
- history=history,
348
- )
349
- except Exception as e:
350
- logger.error("problem_statement route failed", user_id=user_id, error=str(e))
351
- yield {"event": "error", "data": f"Problem statement failed: {e}"}
352
- return
353
- yield {"event": "chunk", "data": text}
354
- yield {"event": "done", "data": ""}
355
- return
 
 
356
  elif intent == "help":
357
  try:
358
  state = analysis_state or await self._load_analysis_state(analysis_id)
 
9
  - `unstructured_flow` β†’ DocumentRetriever (RAG over PGVector) β†’
10
  list[DocumentChunk].
11
  - `check` β†’ check_data / check_knowledge tool β†’ rendered table.
 
12
  - `help` β†’ Help skill: analysis state + history β†’ streamed guidance.
13
+
14
+ (`problem_statement` was removed 2026-06-24 β€” the goal is now user-entered
15
+ `objective` + `business_questions` captured at onboarding, with no agent skill.)
16
  3. `ChatbotAgent.astream` β†’ yield text tokens.
17
  4. Wrap each step into an SSE-style event dict so the API endpoint can
18
  stream them as Server-Sent Events.
 
41
  from .chatbot import ChatbotAgent, DocumentChunk
42
  from .handlers.check import run_check
43
  from .handlers.help import HelpAgent
44
+ # `run_problem_statement` unwired 2026-06-24 (problem_statement removed from the router).
45
+ # `ProblemStatementAgent` kept β€” still referenced by the constructor + _get_ps_agent.
46
+ from .handlers.problem_statement import ProblemStatementAgent
47
  from .orchestration import OrchestratorAgent
48
 
49
  if TYPE_CHECKING:
 
118
  # `#10` data-source binding: scopes structured_flow's catalog to the sources
119
  # the analysis is bound to. Injectable for tests; fail-open when absent.
120
  self._binding_store = binding_store
121
+ # Deterministic gate β€” DEPRECATED 2026-06-24 (problem_validated gate removed).
122
+ # Unused flag; the gate call site in handle() is commented out.
123
  self._enable_gate = enable_gate
124
 
125
  # ------------------------------------------------------------------
 
248
 
249
  intent = decision.intent
250
  # ---- 1a. Ensure session state row (T-A) ----------------------
251
+ # Rooms created via /room/create have no `analysis` row. Without one, Help and
252
+ # the report_id write-back silently no-op. Lazily get-or-create it (idempotent).
 
253
  analysis_state: AnalysisState | None = None
254
  if analysis_id:
255
  try:
 
259
  "analysis state ensure failed", analysis_id=analysis_id, error=str(e)
260
  )
261
 
262
+ # ---- 1b. Gate (REMOVED 2026-06-24) ---------------------------
263
+ # The problem_validated gate was dropped: structured_flow is no longer
264
+ # redirected to problem_statement (the goal is now user-entered objective +
265
+ # business_questions, no agent validation). `gate()` is neutered to a no-op; the
266
+ # call site is left commented for restorability.
267
+ # if self._enable_gate and analysis_id:
268
+ # from .gate import gate, stub_analysis_state
269
+ #
270
+ # intent = gate(
271
+ # intent,
272
+ # analysis_state
273
+ # if analysis_state is not None
274
+ # else stub_analysis_state(problem_validated=False),
275
+ # )
276
 
277
  # The `intent` event is consumed by the endpoint (it gates response caching
278
  # on the effective intent) and is NOT forwarded to the frontend. We emit the
 
342
  yield {"event": "chunk", "data": text}
343
  yield {"event": "done", "data": ""}
344
  return
345
+ # problem_statement dispatch removed 2026-06-24 (skill unwired; intent no longer
346
+ # emitted by the router). Branch kept commented for restorability.
347
+ # elif intent == "problem_statement":
348
+ # try:
349
+ # text = await run_problem_statement(
350
+ # message,
351
+ # analysis_id,
352
+ # agent=self._get_ps_agent(),
353
+ # store=self._get_state_store(),
354
+ # history=history,
355
+ # )
356
+ # except Exception as e:
357
+ # logger.error("problem_statement route failed", user_id=user_id, error=str(e))
358
+ # yield {"event": "error", "data": f"Problem statement failed: {e}"}
359
+ # return
360
+ # yield {"event": "chunk", "data": text}
361
+ # yield {"event": "done", "data": ""}
362
+ # return
363
  elif intent == "help":
364
  try:
365
  state = analysis_state or await self._load_analysis_state(analysis_id)
src/agents/gate.py CHANGED
@@ -47,19 +47,22 @@ class AnalysisState(BaseModel):
47
 
48
 
49
  def gate(intent: Intent, state: AnalysisState) -> Intent:
50
- """Return the effective intent after applying the deterministic gate policy.
51
 
52
- `structured_flow` requires `problem_validated is True`; otherwise redirect to
53
- `problem_statement`. All other intents pass through unchanged.
 
 
54
  """
55
- if intent == "structured_flow" and not state.problem_validated:
56
- logger.info(
57
- "gate redirect",
58
- requested=intent,
59
- effective="problem_statement",
60
- reason="problem_not_validated",
61
- )
62
- return "problem_statement"
 
63
  return intent
64
 
65
 
 
47
 
48
 
49
  def gate(intent: Intent, state: AnalysisState) -> Intent:
50
+ """Return the effective intent (NEUTERED 2026-06-24 β€” passes everything through).
51
 
52
+ The `problem_validated` gate was removed: analysis is no longer gated on a validated
53
+ problem statement (the goal is now two user-entered fields, `objective` +
54
+ `business_questions`, captured at onboarding with no agent validation). Kept as a
55
+ no-op seam so gating can be restored without re-threading call sites.
56
  """
57
+ # Pre-2026-06-24 policy: redirect analytical requests until the goal was validated.
58
+ # if intent == "structured_flow" and not state.problem_validated:
59
+ # logger.info(
60
+ # "gate redirect",
61
+ # requested=intent,
62
+ # effective="problem_statement",
63
+ # reason="problem_not_validated",
64
+ # )
65
+ # return "problem_statement"
66
  return intent
67
 
68
 
src/agents/handlers/problem_statement.py CHANGED
@@ -1,3 +1,7 @@
 
 
 
 
1
  """Problem Statement skill β€” guide the user to a usable problem statement.
2
 
3
  Routed by the orchestrator (intent `problem_statement`) and callable as a skill.
 
1
+ # UNWIRED 2026-06-24: the problem_statement skill is no longer routed to β€” it was removed
2
+ # from the 6-intent router and the gate (the goal is now user-entered objective +
3
+ # business_questions, no agent validation). File kept intact (comment, don't delete) so
4
+ # the skill can be restored if needed. See DEV_PLAN.md #1.
5
  """Problem Statement skill β€” guide the user to a usable problem statement.
6
 
7
  Routed by the orchestrator (intent `problem_statement`) and callable as a skill.
src/agents/orchestration.py CHANGED
@@ -32,7 +32,9 @@ logger = get_logger("orchestrator")
32
  Intent = Literal[
33
  "chat",
34
  "help",
35
- "problem_statement",
 
 
36
  "check",
37
  "unstructured_flow",
38
  "structured_flow",
@@ -53,10 +55,10 @@ class RouterDecision(BaseModel):
53
  ...,
54
  description=(
55
  "Handler route for this message: 'chat' (conversational, no data), "
56
- "'help' (what-to-do-next guidance), 'problem_statement' (define or "
57
- "refine the analysis goal), 'check' (inventory: what data/documents "
58
- "exist), 'unstructured_flow' (answer from documents, fast RAG), or "
59
- "'structured_flow' (analytical question over data, slow Planner path)."
60
  ),
61
  )
62
  rewritten_query: str | None = Field(
 
32
  Intent = Literal[
33
  "chat",
34
  "help",
35
+ # "problem_statement", # removed 2026-06-24 β€” the analysis goal is now two
36
+ # # user-entered fields (objective + business_questions),
37
+ # # captured at onboarding with no agent validation.
38
  "check",
39
  "unstructured_flow",
40
  "structured_flow",
 
55
  ...,
56
  description=(
57
  "Handler route for this message: 'chat' (conversational, no data), "
58
+ "'help' (what-to-do-next guidance), 'check' (inventory: what "
59
+ "data/documents exist), 'unstructured_flow' (answer from documents, fast "
60
+ "RAG), or 'structured_flow' (analytical question over data, slow Planner "
61
+ "path)."
62
  ),
63
  )
64
  rewritten_query: str | None = Field(
src/agents/report/readiness.py CHANGED
@@ -7,8 +7,8 @@ not a judgement.
7
 
8
  The rule mirrors what makes a real report non-empty and worth generating, so Help can
9
  never suggest an action that would 409 or produce a duplicate:
10
- 1. `problem_validated` β€” the gate's own precondition (no validated goal, no
11
- analysis worth reporting). Same rule `gate.gate` applies to `structured_flow`.
12
  2. at least one **substantive** persisted `AnalysisRecord` β€” a record whose
13
  *analysis* task succeeded. A failed run still persists a record WITH findings
14
  (they narrate the failure), and data-access tasks (check_/retrieve_) succeed even
@@ -45,7 +45,7 @@ if TYPE_CHECKING:
45
  logger = get_logger("report_readiness")
46
 
47
  # Human-readable gaps surfaced to the user via Help (kept stable for the prompt).
48
- _MISSING_PROBLEM = "a validated problem statement"
49
  _MISSING_ANALYSIS = "at least one completed analysis"
50
  _MISSING_DELTA = "a new analysis since the last report"
51
 
@@ -91,18 +91,22 @@ async def report_floor(
91
  *,
92
  record_store=None,
93
  ) -> tuple[list[str], list]:
94
- """The report **floor**: a validated goal + β‰₯1 substantive analysis.
95
 
96
  Returns `(missing, substantive_records)`. This is the shared gate both the Help
97
  readiness signal AND the report API enforce, so the button and Help can't drift
98
- (T-D / T11). It deliberately excludes the delta-since-report check β€” that is
99
- advisory and lives only in `is_report_ready`; the report button is always allowed
100
- to cut a new version (decision 4A). Fails closed (counts as missing analysis) on
101
- a record-store read error. `record_store` is injectable for tests.
 
 
 
 
 
 
102
  """
103
  missing: list[str] = []
104
- if not state.problem_validated:
105
- missing.append(_MISSING_PROBLEM)
106
 
107
  substantive: list = []
108
  if analysis_id:
@@ -116,7 +120,7 @@ async def report_floor(
116
  analysis_id=analysis_id,
117
  error=str(exc),
118
  )
119
- return [*missing, _MISSING_ANALYSIS], []
120
 
121
  if not substantive:
122
  missing.append(_MISSING_ANALYSIS)
 
7
 
8
  The rule mirrors what makes a real report non-empty and worth generating, so Help can
9
  never suggest an action that would 409 or produce a duplicate:
10
+ 1. (removed 2026-06-24) a validated problem statement β€” the report no longer gates on
11
+ the goal (now user-entered `objective` + `business_questions`, no agent validation).
12
  2. at least one **substantive** persisted `AnalysisRecord` β€” a record whose
13
  *analysis* task succeeded. A failed run still persists a record WITH findings
14
  (they narrate the failure), and data-access tasks (check_/retrieve_) succeed even
 
45
  logger = get_logger("report_readiness")
46
 
47
  # Human-readable gaps surfaced to the user via Help (kept stable for the prompt).
48
+ # _MISSING_PROBLEM retired 2026-06-24 β€” the report no longer gates on a validated goal.
49
  _MISSING_ANALYSIS = "at least one completed analysis"
50
  _MISSING_DELTA = "a new analysis since the last report"
51
 
 
91
  *,
92
  record_store=None,
93
  ) -> tuple[list[str], list]:
94
+ """The report **floor**: β‰₯1 substantive analysis.
95
 
96
  Returns `(missing, substantive_records)`. This is the shared gate both the Help
97
  readiness signal AND the report API enforce, so the button and Help can't drift
98
+ (T-D / T11).
99
+
100
+ CHANGED 2026-06-24: the `problem_validated` precondition was dropped β€” analysis is no
101
+ longer gated on a validated goal (now user-entered `objective` + `business_questions`,
102
+ no agent validation), so the only floor is "is there anything worth reporting". The
103
+ delta-since-report check stays advisory and lives only in `is_report_ready`; the
104
+ report button is always allowed to cut a new version (decision 4A). Fails closed
105
+ (counts as missing analysis) on a record-store read error. `record_store` is
106
+ injectable for tests. `state` stays in the signature (callers + the `is_report_ready`
107
+ delta check use it).
108
  """
109
  missing: list[str] = []
 
 
110
 
111
  substantive: list = []
112
  if analysis_id:
 
120
  analysis_id=analysis_id,
121
  error=str(exc),
122
  )
123
+ return [_MISSING_ANALYSIS], []
124
 
125
  if not substantive:
126
  missing.append(_MISSING_ANALYSIS)
src/api/v1/tools.py CHANGED
@@ -56,13 +56,16 @@ _COMMAND_CATALOG: list[CommandResponse] = [
56
  type="skill",
57
  description="Show what the assistant can do and guide your next step.",
58
  ),
59
- CommandResponse(
60
- command="/problem-statement",
61
- name="problem_statement",
62
- type="skill",
63
- description="Define and validate your analysis goal (objective + metric) "
64
- "before exploring data.",
65
- ),
 
 
 
66
  CommandResponse(
67
  command="/analyze-descriptive",
68
  name="analyze_descriptive",
 
56
  type="skill",
57
  description="Show what the assistant can do and guide your next step.",
58
  ),
59
+ # /problem-statement removed 2026-06-24 β€” replaced by user-entered objective +
60
+ # business_questions captured at onboarding (no agent skill). Kept commented for
61
+ # restorability.
62
+ # CommandResponse(
63
+ # command="/problem-statement",
64
+ # name="problem_statement",
65
+ # type="skill",
66
+ # description="Define and validate your analysis goal (objective + metric) "
67
+ # "before exploring data.",
68
+ # ),
69
  CommandResponse(
70
  command="/analyze-descriptive",
71
  name="analyze_descriptive",
src/config/prompts/intent_router.md CHANGED
@@ -7,7 +7,7 @@ Return three fields:
7
  - **`intent`** β€” exactly one of:
8
  - `chat` β€” conversational, no data needed: greetings, farewells, thanks, "how are you", "what can you do", small talk.
9
  - `help` β€” the user wants to know **what to do next** or how the process works ("what's the next step?", "how do I start?", "what should I do now?").
10
- - `problem_statement` β€” the user wants to **define or refine the analysis goal**: the business problem, objectives, what to increase/decrease, targets/success metrics β€” or is answering questions about the goal.
11
  - `check` β€” the user wants an **inventory** of what they have: "what data do I have?", "what columns are in this table?", "what documents did I upload?", "describe my dataset". This is metadata/listing, not analysis.
12
  - `unstructured_flow` β€” the user asks about a **topic, concept, feature, explanation, or factual knowledge** that may live in uploaded documents (PDF/DOCX/TXT). Pure document Q&A. The user need not mention a document.
13
  - `structured_flow` β€” the user asks an **analytical question over their data**: counts, sums, top-N, filters, comparisons, trends, correlations, segments, share-of-total, joins across structured sources. This routes to the slow analytical path.
@@ -18,16 +18,14 @@ Return three fields:
18
 
19
  1. Pure greeting / farewell / thanks / "what can you do" / compliment with no task β†’ `chat`.
20
  2. "What do I do next / how do I proceed / where do I start" β†’ `help`.
21
- 3. The user states or refines a goal, objective, target, or success metric, or answers a goal-defining question β†’ `problem_statement`.
22
- 4. "What data / columns / tables / documents do I have", "describe my data", inventory or metadata requests β†’ `check`.
23
- 5. A question answerable from document prose β€” a topic, concept, feature, explanation, summary, or factual knowledge, even without naming a document β†’ `unstructured_flow`.
24
- 6. An analytical question answerable by computing over tabular/DB data (counts, sums, top-N, filters, comparisons, trends, correlations, segments) β†’ `structured_flow`.
25
 
26
  ## Disambiguation (the boundaries that matter)
27
 
28
  - **`check` vs `structured_flow`** β€” "what do I have / describe it" β†’ `check`; "analyze / compute / trend / correlate / compare it" β†’ `structured_flow`.
29
  - **`unstructured_flow` vs `structured_flow`** β€” pure document/concept Q&A β†’ `unstructured_flow`; anything needing computation over tabular/DB data β†’ `structured_flow`. **When in doubt between "analytical AND also needs document context" β†’ `structured_flow`** (the analytical path can pull document context itself). Only choose `unstructured_flow` for *pure* document questions with no computation.
30
- - **`help` vs `problem_statement`** β€” "what's next?" β†’ `help`; "here is my goal / let's define the objective" β†’ `problem_statement`.
31
  - **`chat` vs everything else** β€” only use `chat` when there is no task and no data question at all.
32
 
33
  ## Rewriting follow-ups
@@ -58,16 +56,6 @@ User: "Okay I uploaded my data, what do I do next?"
58
  User: "How does this work? Where should I start?"
59
  β†’ intent="help", rewritten_query=null, confidence=0.9
60
 
61
- User: "I want to reduce customer churn next quarter, target under 5%."
62
- β†’ intent="problem_statement",
63
- rewritten_query="Define the analysis goal: reduce customer churn next quarter to under 5%.",
64
- confidence=0.9
65
-
66
- User: "My goal is to grow revenue in the north region."
67
- β†’ intent="problem_statement",
68
- rewritten_query="Define the analysis goal: grow revenue in the north region.",
69
- confidence=0.88
70
-
71
  User: "What data do I have?"
72
  β†’ intent="check", rewritten_query="What data sources do I have?", confidence=0.95
73
 
@@ -113,7 +101,7 @@ User: "And in March?"
113
 
114
  ## Constraints
115
 
116
- - Pick exactly one `intent`. Do not invent values outside the six listed.
117
  - Prefer `unstructured_flow` over `structured_flow` only for pure knowledge/document questions; prefer `structured_flow` whenever computation over data is involved.
118
  - Do not refuse β€” refusal happens later in guardrails. Just classify.
119
  - One JSON object as output; no prose, no markdown.
 
7
  - **`intent`** β€” exactly one of:
8
  - `chat` β€” conversational, no data needed: greetings, farewells, thanks, "how are you", "what can you do", small talk.
9
  - `help` β€” the user wants to know **what to do next** or how the process works ("what's the next step?", "how do I start?", "what should I do now?").
10
+ <!-- `problem_statement` intent removed 2026-06-24 β€” the analysis goal is now two user-entered fields (objective + business_questions) captured at onboarding, with no agent validation. -->
11
  - `check` β€” the user wants an **inventory** of what they have: "what data do I have?", "what columns are in this table?", "what documents did I upload?", "describe my dataset". This is metadata/listing, not analysis.
12
  - `unstructured_flow` β€” the user asks about a **topic, concept, feature, explanation, or factual knowledge** that may live in uploaded documents (PDF/DOCX/TXT). Pure document Q&A. The user need not mention a document.
13
  - `structured_flow` β€” the user asks an **analytical question over their data**: counts, sums, top-N, filters, comparisons, trends, correlations, segments, share-of-total, joins across structured sources. This routes to the slow analytical path.
 
18
 
19
  1. Pure greeting / farewell / thanks / "what can you do" / compliment with no task β†’ `chat`.
20
  2. "What do I do next / how do I proceed / where do I start" β†’ `help`.
21
+ 3. "What data / columns / tables / documents do I have", "describe my data", inventory or metadata requests β†’ `check`.
22
+ 4. A question answerable from document prose β€” a topic, concept, feature, explanation, summary, or factual knowledge, even without naming a document β†’ `unstructured_flow`.
23
+ 5. An analytical question answerable by computing over tabular/DB data (counts, sums, top-N, filters, comparisons, trends, correlations, segments) β†’ `structured_flow`.
 
24
 
25
  ## Disambiguation (the boundaries that matter)
26
 
27
  - **`check` vs `structured_flow`** β€” "what do I have / describe it" β†’ `check`; "analyze / compute / trend / correlate / compare it" β†’ `structured_flow`.
28
  - **`unstructured_flow` vs `structured_flow`** β€” pure document/concept Q&A β†’ `unstructured_flow`; anything needing computation over tabular/DB data β†’ `structured_flow`. **When in doubt between "analytical AND also needs document context" β†’ `structured_flow`** (the analytical path can pull document context itself). Only choose `unstructured_flow` for *pure* document questions with no computation.
 
29
  - **`chat` vs everything else** β€” only use `chat` when there is no task and no data question at all.
30
 
31
  ## Rewriting follow-ups
 
56
  User: "How does this work? Where should I start?"
57
  β†’ intent="help", rewritten_query=null, confidence=0.9
58
 
 
 
 
 
 
 
 
 
 
 
59
  User: "What data do I have?"
60
  β†’ intent="check", rewritten_query="What data sources do I have?", confidence=0.95
61
 
 
101
 
102
  ## Constraints
103
 
104
+ - Pick exactly one `intent`. Do not invent values outside the five listed.
105
  - Prefer `unstructured_flow` over `structured_flow` only for pure knowledge/document questions; prefer `structured_flow` whenever computation over data is involved.
106
  - Do not refuse β€” refusal happens later in guardrails. Just classify.
107
  - One JSON object as output; no prose, no markdown.
src/config/settings.py CHANGED
@@ -24,11 +24,10 @@ class Settings(BaseSettings):
24
  # real source lands, so this stays opt-in.
25
  enable_slow_path: bool = Field(alias="enable_slow_path", default=False)
26
 
27
- # Apply the deterministic gate (problem_validated) before dispatch: redirect
28
- # `structured_flow` to `problem_statement` until the analysis is validated. Off
29
- # by default β€” legacy `rooms` have no `analysis_states` row, so it would gate
30
- # everything. Flip ENABLE_GATE=true once the frontend creates analyses via
31
- # /analysis/create.
32
  enable_gate: bool = Field(alias="enable_gate", default=False)
33
 
34
  # Database
 
24
  # real source lands, so this stays opt-in.
25
  enable_slow_path: bool = Field(alias="enable_slow_path", default=False)
26
 
27
+ # DEPRECATED 2026-06-24: the problem_validated gate was removed (the goal is now
28
+ # user-entered objective + business_questions, no agent validation). This flag no
29
+ # longer has any effect β€” the gate call site in ChatHandler is commented out. Kept
30
+ # to avoid .env churn; remove once no environment references it.
 
31
  enable_gate: bool = Field(alias="enable_gate", default=False)
32
 
33
  # Database