Rifqi Hafizuddin commited on
Commit
b1b1017
Β·
1 Parent(s): 079782d

/fix report: remove record_id hash citations, EDA (for future tool) change to data source, transform to mdx-safe

Browse files
src/agents/report/generator.py CHANGED
@@ -25,6 +25,7 @@ from src.middlewares.logging import get_logger
25
 
26
  from ..slow_path.schemas import AnalysisRecord, TaskSummary
27
  from .errors import ReportError
 
28
  from .schemas import (
29
  AnalysisReport,
30
  AttributedNote,
@@ -46,6 +47,13 @@ _STAGE_LABELS: list[tuple[str, str]] = [
46
  ("evaluation", "Evaluation"),
47
  ]
48
 
 
 
 
 
 
 
 
49
  _PROMPT_PATH = (
50
  Path(__file__).resolve().parent.parent.parent / "config" / "prompts" / "report_summary.md"
51
  )
@@ -182,6 +190,23 @@ def _build_human_content(
182
  return "\n\n".join(sections)
183
 
184
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  def _render_markdown(report: AnalysisReport) -> str:
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.
@@ -196,11 +221,13 @@ def _render_markdown(report: AnalysisReport) -> str:
196
 
197
  ps = report.problem_statement
198
  if ps.objective:
199
- parts.append("## Objective\n" + ps.objective)
200
  if ps.business_questions:
201
  parts.append(
202
  "## Business Questions\n"
203
- + "\n".join(f"{i}. {q}" for i, q in enumerate(ps.business_questions, 1))
 
 
204
  )
205
 
206
  if report.executive_summary:
@@ -209,32 +236,33 @@ def _render_markdown(report: AnalysisReport) -> str:
209
  if report.findings:
210
  lines = ["## Key Findings"]
211
  for i, f in enumerate(report.findings, 1):
212
- cite = f" *({', '.join(f.record_ids)})*" if f.record_ids else ""
213
- lines.append(f"{i}. {f.text}{cite}")
214
  parts.append("\n".join(lines))
215
 
 
 
 
216
  if report.data_sources:
217
- lines = ["## EDA", "| source | type | detail |", "|---|---|---|"]
218
  for ds in report.data_sources:
219
  d = ds.detail
220
  bits = []
221
  if d.get("tables"):
222
- bits.append("tables: " + ", ".join(d["tables"]))
223
- if d.get("row_count"):
224
- bits.append(f"{d['row_count']} rows")
225
  if d.get("columns"):
226
- bits.append(f"{len(d['columns'])} cols")
227
- lines.append(f"| {ds.name} | {ds.source_type or 'β€”'} | {' Β· '.join(bits) or 'β€”'} |")
 
 
 
228
  parts.append("\n".join(lines))
229
 
230
  if report.caveats or report.open_questions:
231
  lines = ["## Notes & Limitations"]
232
  for n in report.caveats:
233
- cite = f" *({', '.join(n.record_ids)})*" if n.record_ids else ""
234
- lines.append(f"- {n.text}{cite}")
235
  for n in report.open_questions:
236
- cite = f" *({', '.join(n.record_ids)})*" if n.record_ids else ""
237
- lines.append(f"- Open: {n.text}{cite}")
238
  parts.append("\n".join(lines))
239
 
240
  if report.method_steps:
@@ -244,7 +272,8 @@ def _render_markdown(report: AnalysisReport) -> str:
244
  if not steps:
245
  continue
246
  rendered = "; ".join(
247
- f"{', '.join(s.tools_used) or 'β€”'} ({s.status})" for s in steps
 
248
  )
249
  lines.append(f"**{label}** β€” {rendered}")
250
  parts.append("\n".join(lines))
@@ -299,6 +328,10 @@ class ReportGenerator:
299
  user_name: str | None = None,
300
  ) -> AnalysisReport:
301
  records = await self._ensure_record_store().list_for_analysis(analysis_id)
 
 
 
 
302
  if not records:
303
  raise ReportError(f"no analyses recorded for {analysis_id!r} yet")
304
 
 
25
 
26
  from ..slow_path.schemas import AnalysisRecord, TaskSummary
27
  from .errors import ReportError
28
+ from .readiness import has_successful_analysis
29
  from .schemas import (
30
  AnalysisReport,
31
  AttributedNote,
 
47
  ("evaluation", "Evaluation"),
48
  ]
49
 
50
+ # Friendly labels for the catalog's internal source_type enum, shown in Data Sources.
51
+ _SOURCE_TYPE_LABELS: dict[str, str] = {
52
+ "schema": "Database",
53
+ "tabular": "Tabular file",
54
+ "unstructured": "Documents",
55
+ }
56
+
57
  _PROMPT_PATH = (
58
  Path(__file__).resolve().parent.parent.parent / "config" / "prompts" / "report_summary.md"
59
  )
 
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:
211
  # Version is deliberately NOT in the markdown β€” it is assigned by the store
212
  # after rendering and lives in the structured `version` field / API metadata.
 
221
 
222
  ps = report.problem_statement
223
  if ps.objective:
224
+ parts.append("## Objective\n" + _mdx_escape(ps.objective))
225
  if ps.business_questions:
226
  parts.append(
227
  "## Business Questions\n"
228
+ + "\n".join(
229
+ f"{i}. {_mdx_escape(q)}" for i, q in enumerate(ps.business_questions, 1)
230
+ )
231
  )
232
 
233
  if report.executive_summary:
 
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.
244
+
245
  if report.data_sources:
246
+ lines = ["## Data Sources", "| source | type | detail |", "|---|---|---|"]
247
  for ds in report.data_sources:
248
  d = ds.detail
249
  bits = []
250
  if d.get("tables"):
251
+ bits.append("tables: " + ", ".join(_mdx_escape(t) for t in d["tables"]))
 
 
252
  if d.get("columns"):
253
+ bits.append(f"{len(d['columns'])} columns")
254
+ type_label = _SOURCE_TYPE_LABELS.get(ds.source_type, ds.source_type or "β€”")
255
+ lines.append(
256
+ f"| {_mdx_escape(ds.name)} | {type_label} | {' Β· '.join(bits) or 'β€”'} |"
257
+ )
258
  parts.append("\n".join(lines))
259
 
260
  if report.caveats or report.open_questions:
261
  lines = ["## Notes & Limitations"]
262
  for n in report.caveats:
263
+ lines.append(f"- {_mdx_escape(n.text)}")
 
264
  for n in report.open_questions:
265
+ lines.append(f"- Open: {_mdx_escape(n.text)}")
 
266
  parts.append("\n".join(lines))
267
 
268
  if report.method_steps:
 
272
  if not steps:
273
  continue
274
  rendered = "; ".join(
275
+ f"{', '.join(_mdx_escape(t) for t in s.tools_used) or 'β€”'} ({s.status})"
276
+ for s in steps
277
  )
278
  lines.append(f"**{label}** β€” {rendered}")
279
  parts.append("\n".join(lines))
 
328
  user_name: str | None = None,
329
  ) -> AnalysisReport:
330
  records = await self._ensure_record_store().list_for_analysis(analysis_id)
331
+ # The report reflects only substantive runs β€” those with a successful
332
+ # analysis step (the same set the report floor validates). Fully-failed runs
333
+ # are dropped so their failure narration can't contradict the real findings.
334
+ records = [r for r in records if has_successful_analysis(r)]
335
  if not records:
336
  raise ReportError(f"no analyses recorded for {analysis_id!r} yet")
337
 
src/agents/report/readiness.py CHANGED
@@ -71,7 +71,7 @@ def _is_newer(a: datetime, b: datetime) -> bool:
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
@@ -113,7 +113,7 @@ async def report_floor(
113
  try:
114
  store = record_store or _default_record_store()
115
  records = await store.list_for_analysis(analysis_id)
116
- substantive = [r for r in records if _has_successful_analysis(r)]
117
  except Exception as exc: # noqa: BLE001 β€” never-throw; fail closed to not-ready
118
  logger.warning(
119
  "report_floor: record store read failed β€” not ready",
 
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
 
113
  try:
114
  store = record_store or _default_record_store()
115
  records = await store.list_for_analysis(analysis_id)
116
+ substantive = [r for r in records if has_successful_analysis(r)]
117
  except Exception as exc: # noqa: BLE001 β€” never-throw; fail closed to not-ready
118
  logger.warning(
119
  "report_floor: record store read failed β€” not ready",