Rifqi Hafizuddin Claude Opus 4.8 commited on
Commit
2218f33
Β·
1 Parent(s): 8612bdd

[KM-652] fix(readiness,ps): substantive = successful analysis; PS needs explicit goal

Browse files

T1 (is_report_ready): a record is "substantive" only when an analysis task
(analyze_*) SUCCEEDED β€” not when it merely has findings. A failed run still
writes findings (narrating the failure) and its data-access tasks succeed, so
the old "has findings" check marked failed analyses as report-ready. Fixes the
e2e case where analyze_aggregate failed yet the floor would have said ready.

T3a (problem_statement): require an explicitly-stated objective + metric before
validating. New `missing` field on the draft; is_valid = statement present AND
nothing missing. Prompt hardened ("explicitly stated", not "implied") with a
negative few-shot: a bare data question yields missing=[objective,metric] and
does NOT validate, so the gate stops being a formality. The robust gate (user
confirmation) is T3b, with the frontend.

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

src/agents/handlers/problem_statement.py CHANGED
@@ -1,13 +1,17 @@
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.
4
- An LLM drafts/refines the statement from the analysis title + the user's message;
5
- a **deterministic** check decides whether it's complete enough to validate
6
- (`problem_statement` + `objective` + `metric` all present) β€” so the pass is a rule,
7
- not an LLM judgment. On a valid draft it persists `problem_statement` +
8
- `problem_validated=True` to the analysis state; otherwise it streams guidance and
 
9
  leaves the analysis un-validated.
10
 
 
 
 
11
  See `ORCHESTRATOR_REWORK_PLAN.md` Β§4 and the 2026-06-18 checkpoint.
12
  """
13
 
@@ -44,10 +48,17 @@ class ProblemStatementDraft(BaseModel):
44
  ..., description="The refined, standalone problem statement (never empty)."
45
  )
46
  objective: str = Field(
47
- "", description="What success looks like; empty string if not yet clear."
 
48
  )
49
  metric: str = Field(
50
- "", description="The KPI to move/investigate; empty string if not yet clear."
 
 
 
 
 
 
51
  )
52
  feedback: str = Field(
53
  ...,
@@ -56,12 +67,13 @@ class ProblemStatementDraft(BaseModel):
56
 
57
 
58
  def is_valid(draft: ProblemStatementDraft) -> bool:
59
- """Deterministic completeness check β€” the pass is a rule, not LLM judgment."""
60
- return bool(
61
- draft.problem_statement.strip()
62
- and draft.objective.strip()
63
- and draft.metric.strip()
64
- )
 
65
 
66
 
67
  def _load_prompt_text() -> str:
 
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.
4
+ An LLM drafts/refines the statement from the analysis title + the user's message and
5
+ declares what's still `missing`; a check validates only when nothing is missing. The
6
+ model is instructed to fill `objective`/`metric` ONLY from what the user explicitly
7
+ stated β€” a bare data question ("which X has the most Y?") leaves them in `missing`, so
8
+ it does not auto-validate (the gate stays meaningful). On a valid draft it persists
9
+ `problem_statement` + `problem_validated=True`; otherwise it streams guidance and
10
  leaves the analysis un-validated.
11
 
12
+ NOTE: completeness is still a (hardened) LLM judgment β€” the truly deterministic gate
13
+ is an explicit user confirmation, planned with the frontend (see T3b / #11).
14
+
15
  See `ORCHESTRATOR_REWORK_PLAN.md` Β§4 and the 2026-06-18 checkpoint.
16
  """
17
 
 
48
  ..., description="The refined, standalone problem statement (never empty)."
49
  )
50
  objective: str = Field(
51
+ "", description="What success looks like β€” fill ONLY when the user explicitly "
52
+ "stated it; never inferred from a data question. Empty otherwise."
53
  )
54
  metric: str = Field(
55
+ "", description="The KPI to move/investigate β€” fill ONLY when the user "
56
+ "explicitly stated it; never inferred from a data question. Empty otherwise."
57
+ )
58
+ missing: list[str] = Field(
59
+ default_factory=list,
60
+ description="Which of 'objective' / 'metric' the user has NOT explicitly stated "
61
+ "yet. A bare data question leaves both here. Empty list = complete.",
62
  )
63
  feedback: str = Field(
64
  ...,
 
67
 
68
 
69
  def is_valid(draft: ProblemStatementDraft) -> bool:
70
+ """Complete iff there's a statement and the model flagged nothing missing.
71
+
72
+ Keying on the model's explicit `missing` list (rather than 'are objective/metric
73
+ non-empty') is what stops a bare data question from auto-validating: the hardened
74
+ prompt puts the un-stated parts in `missing`, so this returns False for it.
75
+ """
76
+ return bool(draft.problem_statement.strip()) and not draft.missing
77
 
78
 
79
  def _load_prompt_text() -> str:
src/agents/report/readiness.py CHANGED
@@ -9,9 +9,12 @@ The rule mirrors what makes a real report non-empty and worth generating, so Hel
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 that actually
13
- produced findings. A failed/empty run still persists a record (failure tasks, no
14
- findings); it must not count. We count *results*, not chat turns.
 
 
 
15
  3. delta-since-report β€” if a report already exists (`state.report_id`), only ready
16
  when there's a substantive analysis newer than the latest report; otherwise the
17
  new report would be identical.
@@ -68,6 +71,20 @@ def _is_newer(a: datetime, b: datetime) -> bool:
68
  return a > b
69
 
70
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  async def is_report_ready(
72
  analysis_id: str | None,
73
  state: AnalysisState,
@@ -89,7 +106,7 @@ async def is_report_ready(
89
  try:
90
  store = record_store or _default_record_store()
91
  records = await store.list_for_analysis(analysis_id)
92
- substantive = [r for r in records if r.findings]
93
  except Exception as exc: # noqa: BLE001 β€” never-throw; fail closed to not-ready
94
  logger.warning(
95
  "is_report_ready: record store read failed β€” not ready",
 
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
15
+ when the analysis fails β€” so neither "has findings" nor "any task succeeded" is
16
+ enough. We require a genuine analysis tool (analyze_*) to have completed. We count
17
+ *results*, not chat turns.
18
  3. delta-since-report β€” if a report already exists (`state.report_id`), only ready
19
  when there's a substantive analysis newer than the latest report; otherwise the
20
  new report would be identical.
 
71
  return a > b
72
 
73
 
74
+ def _has_successful_analysis(record) -> bool:
75
+ """True if the record has at least one *analysis* task that succeeded.
76
+
77
+ A failed run still writes findings (narrating the failure) and its data-access
78
+ tasks (check_/retrieve_) succeed, so we can't key on findings or on "any task
79
+ succeeded". An analysis tool (analyze_*) completing is the real "we produced a
80
+ result" signal.
81
+ """
82
+ return any(
83
+ t.status == "success" and any(tool.startswith("analyze") for tool in t.tools_used)
84
+ for t in record.tasks_run
85
+ )
86
+
87
+
88
  async def is_report_ready(
89
  analysis_id: str | None,
90
  state: AnalysisState,
 
106
  try:
107
  store = record_store or _default_record_store()
108
  records = await store.list_for_analysis(analysis_id)
109
+ substantive = [r for r in records if _has_successful_analysis(r)]
110
  except Exception as exc: # noqa: BLE001 β€” never-throw; fail closed to not-ready
111
  logger.warning(
112
  "is_report_ready: record store read failed β€” not ready",
src/config/prompts/problem_statement.md CHANGED
@@ -8,18 +8,34 @@ You do not run analysis. You only shape the problem statement and decide whether
8
  2. **objective** β€” what success looks like (e.g. "reduce churn", "grow north-region revenue", "understand drivers of retention").
9
  3. **metric** β€” the concrete measure to move or investigate (e.g. "churn rate", "monthly revenue", "retention score").
10
 
11
- A statement is **complete** only when all three are present and concrete. If the user hasn't given enough to fill `objective` or `metric`, leave them empty and ask for what's missing in `feedback`.
 
 
12
 
13
  ## Output (structured)
14
 
15
  - **`problem_statement`** β€” your best refined version so far (never empty; use the title if that's all you have).
16
- - **`objective`** β€” filled only when clear; otherwise empty string.
17
- - **`metric`** β€” filled only when clear; otherwise empty string.
18
- - **`feedback`** β€” a short, friendly message to the user. If incomplete: explain what's missing and ask one focused question. If complete: confirm the problem statement back to them and say they can now start analyzing.
 
19
 
20
  ## Rules
21
 
22
  - Be concise and concrete. One focused follow-up question at a time β€” don't interrogate.
23
- - Don't invent an objective or metric the user hasn't implied. Empty is correct when unknown.
24
  - Keep `problem_statement` decision-oriented, not a restatement of the data.
25
  - Match the user's language (English / Indonesian).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  2. **objective** β€” what success looks like (e.g. "reduce churn", "grow north-region revenue", "understand drivers of retention").
9
  3. **metric** β€” the concrete measure to move or investigate (e.g. "churn rate", "monthly revenue", "retention score").
10
 
11
+ A statement is **complete** only when all three are present and concrete, and **the user explicitly stated the objective and the metric in their own words**. If they haven't, leave that field empty, list it in `missing`, and ask for it in `feedback`.
12
+
13
+ **A bare data question is NOT a complete problem statement.** Questions like "which product category has the most revenue?", "what's our top region?", "how many orders last month?" only tell you *what to compute* β€” they do not state a business objective or a target metric to move. Do **not** infer `objective`/`metric` from such a question. Put both in `missing` and ask the user for the actual goal.
14
 
15
  ## Output (structured)
16
 
17
  - **`problem_statement`** β€” your best refined version so far (never empty; use the title if that's all you have).
18
+ - **`objective`** β€” filled ONLY when the user explicitly stated it; otherwise empty string.
19
+ - **`metric`** β€” filled ONLY when the user explicitly stated it; otherwise empty string.
20
+ - **`missing`** β€” the list of which fields among `objective` / `metric` the user has not yet explicitly stated. Empty list means the statement is complete and will be validated. A bare data question must yield `missing: ["objective", "metric"]`.
21
+ - **`feedback`** β€” a short, friendly message. If `missing` is non-empty: explain what's missing and ask one focused question. If complete: confirm the problem statement back and say they can start analyzing.
22
 
23
  ## Rules
24
 
25
  - Be concise and concrete. One focused follow-up question at a time β€” don't interrogate.
26
+ - Only fill `objective`/`metric` from what the user **explicitly stated**, never from what a question merely implies. Empty + listed in `missing` is correct when the user hasn't said it.
27
  - Keep `problem_statement` decision-oriented, not a restatement of the data.
28
  - Match the user's language (English / Indonesian).
29
+
30
+ ## Examples
31
+
32
+ **Incomplete β€” a bare data question (do NOT validate):**
33
+ User: "Which product category generates the most total revenue?"
34
+ β†’ `problem_statement`: "Identify which product category drives the most total revenue."
35
+ β†’ `objective`: "" Β· `metric`: "" Β· `missing`: ["objective", "metric"]
36
+ β†’ `feedback`: "Good starting question. To set this up as an analysis goal: what business outcome are you trying to drive (e.g. grow revenue, cut cost), and which metric should we track (e.g. total revenue per category)?"
37
+
38
+ **Complete β€” the user stated the goal + metric:**
39
+ User: "Goal: grow total revenue by focusing marketing on the top categories. Metric: total revenue per category."
40
+ β†’ `objective`: "grow total revenue by focusing on the top categories" Β· `metric`: "total revenue per category" Β· `missing`: []
41
+ β†’ `feedback`: "Your problem statement is complete β€” you can start the analysis."