Rifqi Hafizuddin Claude Opus 4.8 commited on
Commit
029b488
·
1 Parent(s): aacd4d1

/fix report: group findings by analysis, dedup, code-span-safe MDX escaping

Browse files

- Key Findings now grouped under each analysis's restated question so
results from different questions don't read as one contradictory list
- dedup exact-duplicate findings within a record
- _mdx_escape leaves inline code spans verbatim (`product_id` no longer
renders as `product\_id`)

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

src/agents/report/generator.py CHANGED
@@ -13,6 +13,7 @@ Chain construction mirrors `agents/slow_path/assembler.py`.
13
 
14
  from __future__ import annotations
15
 
 
16
  from datetime import UTC, datetime
17
  from pathlib import Path
18
 
@@ -98,12 +99,18 @@ def _get_default_chain() -> Runnable:
98
 
99
 
100
  def _collect_findings(records: list[AnalysisRecord]) -> list[ReportFinding]:
101
- # Findings are distinct insights not deduped; each traces to its record.
102
- return [
103
- ReportFinding(text=text, record_ids=[rec.record_id])
104
- for rec in records
105
- for text in rec.findings
106
- ]
 
 
 
 
 
 
107
 
108
 
109
  def _collect_notes(records: list[AnalysisRecord], field: str) -> list[AttributedNote]:
@@ -190,21 +197,39 @@ def _build_human_content(
190
  return "\n\n".join(sections)
191
 
192
 
 
 
 
 
 
 
193
  def _mdx_escape(text: str) -> str:
194
  """Escape Markdown/MDX syntax characters in a dynamic value.
195
 
196
  Applied only to values authored outside the renderer (LLM findings/caveats,
197
  user objective/questions, catalog source & table names, tool names) so that
198
  identifiers like ``XL_S_129`` don't italicize and stray ``<``/``{`` don't break
199
- MDX compilation. NOT applied to the executive summary (its prompt allows light
200
- inline emphasis) nor to the structural markdown the renderer emits itself.
 
201
  """
202
  if not text:
203
  return text
204
- out = text.replace("\\", "\\\\")
205
- for ch in ("<", "{", "|", "_", "*"):
206
- out = out.replace(ch, "\\" + ch)
207
- return out
 
 
 
 
 
 
 
 
 
 
 
208
 
209
 
210
  def _render_markdown(report: AnalysisReport) -> str:
@@ -234,10 +259,28 @@ def _render_markdown(report: AnalysisReport) -> str:
234
  parts.append("## Executive Summary\n" + report.executive_summary)
235
 
236
  if report.findings:
237
- lines = ["## Key Findings"]
238
- for i, f in enumerate(report.findings, 1):
239
- lines.append(f"{i}. {_mdx_escape(f.text)}")
240
- parts.append("\n".join(lines))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
 
242
  # ## EDA — reserved for future exploratory visuals; charts wrapped as MDX
243
  # components will render here. Emitted only when it has content; omitted today.
@@ -354,6 +397,7 @@ class ReportGenerator:
354
  generated_at=datetime.now(UTC),
355
  problem_statement=ps,
356
  record_ids=[r.record_id for r in records],
 
357
  executive_summary=executive_summary,
358
  findings=findings,
359
  caveats=caveats,
 
13
 
14
  from __future__ import annotations
15
 
16
+ import re
17
  from datetime import UTC, datetime
18
  from pathlib import Path
19
 
 
99
 
100
 
101
  def _collect_findings(records: list[AnalysisRecord]) -> list[ReportFinding]:
102
+ # Each finding traces to its record. Deduped *within* a record (an Assembler run
103
+ # occasionally repeats a line); kept across records so the grouped render can show
104
+ # each analysis's own findings under its own question.
105
+ out: list[ReportFinding] = []
106
+ for rec in records:
107
+ seen: set[str] = set()
108
+ for text in rec.findings:
109
+ if text in seen:
110
+ continue
111
+ seen.add(text)
112
+ out.append(ReportFinding(text=text, record_ids=[rec.record_id]))
113
+ return out
114
 
115
 
116
  def _collect_notes(records: list[AnalysisRecord], field: str) -> list[AttributedNote]:
 
197
  return "\n\n".join(sections)
198
 
199
 
200
+ # Inline code spans (one or more backticks). Content inside is already literal in
201
+ # Markdown/MDX, so escaping within them would only surface a visible backslash
202
+ # (e.g. `product\_id`). We keep code spans verbatim and escape only around them.
203
+ _CODE_SPAN_RE = re.compile(r"`+[^`]*`+")
204
+
205
+
206
  def _mdx_escape(text: str) -> str:
207
  """Escape Markdown/MDX syntax characters in a dynamic value.
208
 
209
  Applied only to values authored outside the renderer (LLM findings/caveats,
210
  user objective/questions, catalog source & table names, tool names) so that
211
  identifiers like ``XL_S_129`` don't italicize and stray ``<``/``{`` don't break
212
+ MDX compilation. Content inside inline code spans is left verbatim (already
213
+ literal). NOT applied to the executive summary (its prompt allows light inline
214
+ emphasis) nor to the structural markdown the renderer emits itself.
215
  """
216
  if not text:
217
  return text
218
+
219
+ def _esc(s: str) -> str:
220
+ s = s.replace("\\", "\\\\")
221
+ for ch in ("<", "{", "|", "_", "*"):
222
+ s = s.replace(ch, "\\" + ch)
223
+ return s
224
+
225
+ out: list[str] = []
226
+ pos = 0
227
+ for m in _CODE_SPAN_RE.finditer(text):
228
+ out.append(_esc(text[pos : m.start()]))
229
+ out.append(m.group(0)) # keep the code span verbatim
230
+ pos = m.end()
231
+ out.append(_esc(text[pos:]))
232
+ return "".join(out)
233
 
234
 
235
  def _render_markdown(report: AnalysisReport) -> str:
 
259
  parts.append("## Executive Summary\n" + report.executive_summary)
260
 
261
  if report.findings:
262
+ # Group findings by their originating analysis (record) so results from
263
+ # different questions read as separate analyses, not one flat, seemingly
264
+ # contradictory list. Subheadings (the restated question) appear only when
265
+ # more than one analysis contributed.
266
+ by_record: dict[str, list[ReportFinding]] = {}
267
+ for f in report.findings:
268
+ by_record.setdefault(f.record_ids[0] if f.record_ids else "", []).append(f)
269
+ ordered = [rid for rid in report.record_goals if rid in by_record]
270
+ ordered += [rid for rid in by_record if rid not in ordered]
271
+ grouped = sum(1 for rid in ordered if by_record.get(rid)) > 1
272
+
273
+ blocks = ["## Key Findings"]
274
+ for rid in ordered:
275
+ group = by_record.get(rid)
276
+ if not group:
277
+ continue
278
+ block: list[str] = []
279
+ if grouped:
280
+ block.append(f"### {_mdx_escape(report.record_goals.get(rid) or 'Analysis')}")
281
+ block.extend(f"{i}. {_mdx_escape(f.text)}" for i, f in enumerate(group, 1))
282
+ blocks.append("\n".join(block))
283
+ parts.append("\n\n".join(blocks))
284
 
285
  # ## EDA — reserved for future exploratory visuals; charts wrapped as MDX
286
  # components will render here. Emitted only when it has content; omitted today.
 
397
  generated_at=datetime.now(UTC),
398
  problem_statement=ps,
399
  record_ids=[r.record_id for r in records],
400
+ record_goals={r.record_id: r.goal_restated for r in records},
401
  executive_summary=executive_summary,
402
  findings=findings,
403
  caveats=caveats,
src/agents/report/schemas.py CHANGED
@@ -82,6 +82,10 @@ class AnalysisReport(BaseModel):
82
  # Frozen snapshots.
83
  problem_statement: ProblemStatement = Field(default_factory=ProblemStatement)
84
  record_ids: list[str] = Field(default_factory=list) # records used (snapshot)
 
 
 
 
85
  # LLM-authored.
86
  executive_summary: str = ""
87
  # Deterministic pass-through from records.
 
82
  # Frozen snapshots.
83
  problem_statement: ProblemStatement = Field(default_factory=ProblemStatement)
84
  record_ids: list[str] = Field(default_factory=list) # records used (snapshot)
85
+ # record_id -> restated question, in record order. Lets the renderer group Key
86
+ # Findings per analysis so results from different questions aren't a flat list
87
+ # that reads as contradictory. Rebuilt-empty on dedorch read-back (markdown only).
88
+ record_goals: dict[str, str] = Field(default_factory=dict)
89
  # LLM-authored.
90
  executive_summary: str = ""
91
  # Deterministic pass-through from records.