"""Run SP generate + coverage evaluation for D2 and D4 messy notes.""" from __future__ import annotations import asyncio import json import sys from datetime import datetime, timezone from pathlib import Path _ROOT = Path(__file__).resolve().parents[1] if str(_ROOT) not in sys.path: sys.path.insert(0, str(_ROOT)) from backend.evaluation import evaluate_report, evaluation_manifest_path from backend.evaluation.models import SectionEvalInput from backend.models.report import GeneratedSection, ReportResult from scripts.test_sp_decompose_generate import ( CASES, DEFAULT_TENANT, _case_for, ) from backend.domain import template_discoverer from backend.standard_paragraphs.generate import generate_from_standard_paragraphs from backend.standard_paragraphs.note_issues_manifest import record_note_issues from backend.storage import retrieval_manifest DRAFT_ID = ( "sp-e2e-d2-d4-" + datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S") ) SECTIONS = ("D2", "D4") _STYLE_SAMPLES = "--style-samples" in sys.argv def _run_section(sid: str) -> dict: case = _case_for(sid) title = case["section_title"] observations = list(case["observations"]) schema = template_discoverer.ensure_canonical_schema(DEFAULT_TENANT) text, hits, messages, guidance, issues, llm_usage, style_sample_count = ( generate_from_standard_paragraphs( tenant_id=DEFAULT_TENANT, schema=schema, section_id=sid, section_title=title, observations=observations, candidate_ids=[sid], force_decompose=True, force_decompose_llm=True, use_all_section_sps=False, style_samples_enabled=True if _STYLE_SAMPLES else None, ) ) record_note_issues( DEFAULT_TENANT, DRAFT_ID, section_id=sid, section_title=title, observations=observations, issues=list(issues), source="e2e_d2_d4_eval", used_llm=True, ) retrieval_manifest.record_section_retrieval( DEFAULT_TENANT, DRAFT_ID, section_id=sid, section_title=title, observations=observations, baseline_text=guidance, hits=hits, status="MAPPED" if text.strip() else "NO_RAG_MATCH", prompt_messages=messages, retrieved_count=len(hits), prompt_chunk_count=len(hits), knowledge_source="standard_paragraph", generated_text=text, retrieval_issues=list(issues), llm_usage=llm_usage, style_sample_count=style_sample_count, ) return { "section_id": sid, "title": title, "observations": observations, "findings": list(issues), "hit_count": len(hits), "generated_text": text, "llm_usage": llm_usage, "style_sample_count": style_sample_count, } async def _evaluate(results: list[dict]): # Prefer decomposed findings for coverage (atomic notes); fall back to blob. section_inputs = [] sections = [] for r in results: obs = r["findings"] or r["observations"] sections.append( GeneratedSection( section_id=r["section_id"], title=r["title"], text=r["generated_text"] or "", ) ) section_inputs.append( SectionEvalInput( section_id=r["section_id"], title=r["title"], observations=obs, generated_text=r["generated_text"] or "", baseline_text="", ) ) report = ReportResult( tenant_id=DEFAULT_TENANT, schema_version=2, sections=sections, ) return await evaluate_report( report, report_id=DRAFT_ID, section_inputs=section_inputs, ) def _pct(v: float | None) -> str: return "n/a" if v is None else f"{v * 100:.1f}%" async def main() -> int: print("draft=", DRAFT_ID) print("tenant=", DEFAULT_TENANT) print("style_samples=", _STYLE_SAMPLES) results = [] for sid in SECTIONS: print(f"\n========== GENERATE {sid} ==========") r = _run_section(sid) results.append(r) print("findings=", len(r["findings"])) for i, f in enumerate(r["findings"], 1): print(f" {i}. {f}") print("retrieved_hits=", r["hit_count"]) print("style_sample_count=", r.get("style_sample_count")) print("llm_usage=", r.get("llm_usage")) print("generated_text=\n", r["generated_text"]) print("\n========== EVALUATION ==========") evaluation = await _evaluate(results) if evaluation is None: print("evaluation disabled") return 1 print("status=", evaluation.status) print("coverage_rate=", _pct(evaluation.coverage_rate)) print("covered/total=", evaluation.covered_note_atoms, "/", evaluation.total_note_atoms) print("faithfulness=", _pct(evaluation.faithfulness_score)) print("model=", evaluation.model) print("error=", evaluation.error) for sec in evaluation.sections: print(f"\n--- {sec.section_id} coverage={_pct(sec.coverage_rate)} ---") print("observations judged=", len(sec.observations)) for j in sec.note_judgments or []: print(f" [{j.status}] {j.note}") if j.evidence: print(f" evidence: {j.evidence[:120]}") if sec.missing_facts: print(" missing_facts=", sec.missing_facts) if sec.error: print(" error=", sec.error) path = evaluation_manifest_path(DEFAULT_TENANT, DRAFT_ID) print("\nevaluation_manifest=", path) print("retrieval_manifest=", retrieval_manifest.retrieval_manifest_path(DEFAULT_TENANT, DRAFT_ID)) summary = { "draft_id": DRAFT_ID, "sections": { r["section_id"]: { "findings": r["findings"], "hit_count": r["hit_count"], "generated_text": r["generated_text"], } for r in results }, "evaluation": { "status": evaluation.status, "coverage_rate": evaluation.coverage_rate, "covered_note_atoms": evaluation.covered_note_atoms, "total_note_atoms": evaluation.total_note_atoms, "faithfulness_score": evaluation.faithfulness_score, "per_section": { s.section_id: { "coverage_rate": s.coverage_rate, "judgments": [ {"note": j.note, "status": j.status, "evidence": j.evidence} for j in (s.note_judgments or []) ], "missing_facts": s.missing_facts, } for s in evaluation.sections }, }, } out = ( _ROOT / "backend" / "standard_paragraphs" / "samples" / f"{DRAFT_ID}.e2e_eval.json" ) out.write_text(json.dumps(summary, indent=2, ensure_ascii=False), encoding="utf-8") print("sample copy=", out) return 0 if __name__ == "__main__": # Ensure CASES module is loaded (D2/D4 notes updated in test_sp_decompose_notes). _ = CASES raise SystemExit(asyncio.run(main()))