[KM-652] fix(readiness,ps): substantive = successful analysis; PS needs explicit goal
Browse filesT1 (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>
|
@@ -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 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
|
|
|
| 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
|
|
|
|
| 48 |
)
|
| 49 |
metric: str = Field(
|
| 50 |
-
"", description="The KPI to move/investigate
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
)
|
| 52 |
feedback: str = Field(
|
| 53 |
...,
|
|
@@ -56,12 +67,13 @@ class ProblemStatementDraft(BaseModel):
|
|
| 56 |
|
| 57 |
|
| 58 |
def is_valid(draft: ProblemStatementDraft) -> bool:
|
| 59 |
-
"""
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 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:
|
|
@@ -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
|
| 13 |
-
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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",
|
|
@@ -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
|
|
|
|
|
|
|
| 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
|
| 17 |
-
- **`metric`** β filled
|
| 18 |
-
- **`
|
|
|
|
| 19 |
|
| 20 |
## Rules
|
| 21 |
|
| 22 |
- Be concise and concrete. One focused follow-up question at a time β don't interrogate.
|
| 23 |
-
-
|
| 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."
|