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 +48 -15
- src/agents/report/readiness.py +2 -2
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(
|
|
|
|
|
|
|
| 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 |
-
|
| 213 |
-
lines.append(f"{i}. {f.text}{cite}")
|
| 214 |
parts.append("\n".join(lines))
|
| 215 |
|
|
|
|
|
|
|
|
|
|
| 216 |
if report.data_sources:
|
| 217 |
-
lines = ["##
|
| 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'])}
|
| 227 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 234 |
-
lines.append(f"- {n.text}{cite}")
|
| 235 |
for n in report.open_questions:
|
| 236 |
-
|
| 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})"
|
|
|
|
| 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
|
| 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
|
| 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",
|