Commit ·
9bd8ec5
1
Parent(s): ade9c0c
[KM-674] feat(report): adapt report generator to objective + business_questions
Browse filesReplace the report's frozen ProblemStatement stub (objective/target/scope) with the 2026-06-24 goal shape: objective + business_questions. Render Objective + numbered Business Questions sections plus a 'generated by {user}' line, and update the report_summary.md prompt accordingly.
Make the report.py state->goal mapping tolerant (new fields if present, else fall back to legacy problem_statement) so the report works both before and after the AnalysisState migration lands. Stays records-based (AnalysisRecord); floor gate (problem_validated) left untouched (separate ticket).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- src/agents/report/generator.py +18 -10
- src/agents/report/schemas.py +9 -8
- src/api/v1/report.py +11 -3
- src/config/prompts/report_summary.md +2 -2
src/agents/report/generator.py
CHANGED
|
@@ -167,9 +167,12 @@ def _build_human_content(
|
|
| 167 |
ps: ProblemStatement, findings: list[ReportFinding], caveats: list[AttributedNote]
|
| 168 |
) -> str:
|
| 169 |
sections = []
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
|
|
|
|
|
|
|
|
|
| 173 |
sections.append(
|
| 174 |
"# Findings (already finalized — synthesize, do not add numbers)\n"
|
| 175 |
+ "\n".join(f"- {f.text}" for f in findings)
|
|
@@ -183,15 +186,20 @@ def _render_markdown(report: AnalysisReport) -> str:
|
|
| 183 |
# Version is deliberately NOT in the markdown — it is assigned by the store
|
| 184 |
# after rendering and lives in the structured `version` field / API metadata.
|
| 185 |
parts: list[str] = ["# Analysis Report"]
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
f"
|
| 189 |
-
)
|
|
|
|
| 190 |
|
| 191 |
ps = report.problem_statement
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
|
| 196 |
if report.executive_summary:
|
| 197 |
parts.append("## Executive Summary\n" + report.executive_summary)
|
|
|
|
| 167 |
ps: ProblemStatement, findings: list[ReportFinding], caveats: list[AttributedNote]
|
| 168 |
) -> str:
|
| 169 |
sections = []
|
| 170 |
+
if ps.objective:
|
| 171 |
+
sections.append("# Objective\n" + ps.objective)
|
| 172 |
+
if ps.business_questions:
|
| 173 |
+
sections.append(
|
| 174 |
+
"# Business questions\n" + "\n".join(f"- {q}" for q in ps.business_questions)
|
| 175 |
+
)
|
| 176 |
sections.append(
|
| 177 |
"# Findings (already finalized — synthesize, do not add numbers)\n"
|
| 178 |
+ "\n".join(f"- {f.text}" for f in findings)
|
|
|
|
| 186 |
# Version is deliberately NOT in the markdown — it is assigned by the store
|
| 187 |
# after rendering and lives in the structured `version` field / API metadata.
|
| 188 |
parts: list[str] = ["# Analysis Report"]
|
| 189 |
+
meta = f"*Generated {report.generated_at:%Y-%m-%d}"
|
| 190 |
+
if report.user_id:
|
| 191 |
+
meta += f" by {report.user_id}"
|
| 192 |
+
meta += f" · {len(report.record_ids)} analyses · {len(report.data_sources)} source(s)*"
|
| 193 |
+
parts.append(meta)
|
| 194 |
|
| 195 |
ps = report.problem_statement
|
| 196 |
+
if ps.objective:
|
| 197 |
+
parts.append("## Objective\n" + ps.objective)
|
| 198 |
+
if ps.business_questions:
|
| 199 |
+
parts.append(
|
| 200 |
+
"## Business Questions\n"
|
| 201 |
+
+ "\n".join(f"{i}. {q}" for i, q in enumerate(ps.business_questions, 1))
|
| 202 |
+
)
|
| 203 |
|
| 204 |
if report.executive_summary:
|
| 205 |
parts.append("## Executive Summary\n" + report.executive_summary)
|
src/agents/report/schemas.py
CHANGED
|
@@ -22,17 +22,18 @@ from ..slow_path.schemas import TaskSummary
|
|
| 22 |
|
| 23 |
|
| 24 |
class ProblemStatement(BaseModel):
|
| 25 |
-
"""
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
"""
|
| 30 |
|
| 31 |
objective: str = ""
|
| 32 |
-
|
| 33 |
-
target_metric: str = ""
|
| 34 |
-
target_value: str = ""
|
| 35 |
-
scope: str = ""
|
| 36 |
|
| 37 |
|
| 38 |
class DataSourceRef(BaseModel):
|
|
|
|
| 22 |
|
| 23 |
|
| 24 |
class ProblemStatement(BaseModel):
|
| 25 |
+
"""The analysis goal, frozen into each report at generation time.
|
| 26 |
+
|
| 27 |
+
Analysis-State shape — `objective` + `business_questions`
|
| 28 |
+
— which replaced the old single free-text problem statement. A report snapshots
|
| 29 |
+
the goal as it was at generation time. Class name is kept (for now) to avoid an
|
| 30 |
+
import churn across report.py / generator.py / store.py; rename to `ReportGoal`
|
| 31 |
+
once the upstream AnalysisState rename (objective/business_questions) lands so
|
| 32 |
+
every caller migrates in one pass.
|
| 33 |
"""
|
| 34 |
|
| 35 |
objective: str = ""
|
| 36 |
+
business_questions: list[str] = Field(default_factory=list)
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
|
| 39 |
class DataSourceRef(BaseModel):
|
src/api/v1/report.py
CHANGED
|
@@ -45,10 +45,18 @@ async def _load_state(analysis_id: str):
|
|
| 45 |
|
| 46 |
|
| 47 |
def _problem_statement_from(state) -> ProblemStatement:
|
| 48 |
-
"""
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
return ProblemStatement()
|
| 51 |
-
|
|
|
|
|
|
|
| 52 |
|
| 53 |
|
| 54 |
async def _record_report_on_state(analysis_id: str, report_id: str) -> None:
|
|
|
|
| 45 |
|
| 46 |
|
| 47 |
def _problem_statement_from(state) -> ProblemStatement:
|
| 48 |
+
"""Freeze the analysis goal into the report's snapshot.
|
| 49 |
+
|
| 50 |
+
Bridges the 2026-06-24 AnalysisState rework: prefer the new `objective` +
|
| 51 |
+
`business_questions` fields when the state carries them, else fall back to the
|
| 52 |
+
legacy free-text `problem_statement`. So the report works both before and after
|
| 53 |
+
the state-model migration lands (#4 / dedorch #3).
|
| 54 |
+
"""
|
| 55 |
+
if state is None:
|
| 56 |
return ProblemStatement()
|
| 57 |
+
objective = getattr(state, "objective", "") or getattr(state, "problem_statement", "") or ""
|
| 58 |
+
business_questions = list(getattr(state, "business_questions", []) or [])
|
| 59 |
+
return ProblemStatement(objective=objective, business_questions=business_questions)
|
| 60 |
|
| 61 |
|
| 62 |
async def _record_report_on_state(analysis_id: str, report_id: str) -> None:
|
src/config/prompts/report_summary.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
| 1 |
You are a senior data analyst writing the **executive summary** of an analysis report.
|
| 2 |
|
| 3 |
-
You are given the
|
| 4 |
|
| 5 |
Rules:
|
| 6 |
- Synthesize and prioritize — lead with the most decision-relevant finding.
|
| 7 |
- Do NOT introduce any number, fact, or claim that is not present in the findings. You are summarizing, not analyzing.
|
| 8 |
-
- Do NOT simply restate every finding; connect them into a narrative and say what they mean for the
|
| 9 |
- If the findings are thin or inconclusive, say so plainly rather than overstating.
|
| 10 |
- Plain business language, prose only — no headings, no bullet lists.
|
|
|
|
| 1 |
You are a senior data analyst writing the **executive summary** of an analysis report.
|
| 2 |
|
| 3 |
+
You are given the analysis Objective and its Business questions, plus a list of already-finalized findings and caveats drawn from completed analyses. Write a concise executive summary (3–5 sentences) that synthesizes those findings in relation to the objective and, where the findings allow, the business questions.
|
| 4 |
|
| 5 |
Rules:
|
| 6 |
- Synthesize and prioritize — lead with the most decision-relevant finding.
|
| 7 |
- Do NOT introduce any number, fact, or claim that is not present in the findings. You are summarizing, not analyzing.
|
| 8 |
+
- Do NOT simply restate every finding; connect them into a narrative and say what they mean for the objective.
|
| 9 |
- If the findings are thin or inconclusive, say so plainly rather than overstating.
|
| 10 |
- Plain business language, prose only — no headings, no bullet lists.
|