sofhiaazzhr Claude Opus 4.8 commited on
Commit
9bd8ec5
·
1 Parent(s): ade9c0c

[KM-674] feat(report): adapt report generator to objective + business_questions

Browse files

Replace 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 CHANGED
@@ -167,9 +167,12 @@ def _build_human_content(
167
  ps: ProblemStatement, findings: list[ReportFinding], caveats: list[AttributedNote]
168
  ) -> str:
169
  sections = []
170
- ps_lines = [v for v in (ps.objective, ps.target_value, ps.scope) if v]
171
- if ps_lines:
172
- sections.append("# Problem Statement\n" + "\n".join(ps_lines))
 
 
 
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
- parts.append(
187
- f"*Generated {report.generated_at:%Y-%m-%d} · "
188
- f"{len(report.record_ids)} analyses · {len(report.data_sources)} source(s)*"
189
- )
 
190
 
191
  ps = report.problem_statement
192
- ps_lines = [v for v in (ps.objective, ps.target_value, ps.scope) if v]
193
- if ps_lines:
194
- parts.append("## Problem Statement\n" + " ".join(ps_lines))
 
 
 
 
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
- """Minimal stub of Harry's Problem Statement, frozen into each report.
26
-
27
- Loose on purpose until the real PS template lands (Analysis State, upstream).
28
- A report snapshots the PS as it was at generation time.
 
 
 
 
29
  """
30
 
31
  objective: str = ""
32
- metric_direction: str = "" # "increase" | "decrease"
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
- """Map the analysis's free-text problem statement into the report's structured PS."""
49
- if state is None or not state.problem_statement:
 
 
 
 
 
 
50
  return ProblemStatement()
51
- return ProblemStatement(objective=state.problem_statement)
 
 
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 Problem Statement and 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 stated goal.
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 goal.
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.