"""阶段 4:汇总报告 + 落盘。 审查结论在阶段 3 已是结构化对象(`{summary, issues[]}`), 因此这里**不再**需要第二次 LLM 调用来抽取结构。 """ import json from datetime import datetime from pathlib import Path from .. import config from ..findings import flatten_issues _SUMMARY_SYSTEM = ( "你是图纸审查负责人。请把下面各图框的审查意见汇总为一份结构化的审查报告:" "先给整体结论,再按图框列出问题,最后给出整改建议。语言简洁、条目化。" ) def format_finding(finding): """把一个 findings 元素渲染成给汇总 LLM 的文本块。""" lines = [f"【图框 {finding.get('frame')}(图幅 {finding.get('sheet')})】"] summary = finding.get("summary") if summary: lines.append(str(summary)) for issue in finding.get("issues") or []: tag = "确定性" if issue.get("source") == "deterministic" else "LLM" flag = "" if issue.get("verified", True) else "(未在证据中定位,仅供参考)" handle = issue.get("handle") where = f" handle={handle}" if handle else "" lines.append( f"- [{issue.get('severity', '?')}][{tag}] {issue.get('message', '')}{where}{flag}" ) return "\n".join(lines) def summarize(ctx, findings, log=print): """用 LLM 把各图框结论汇总为总报告。""" if not findings: return "未识别到可审查的图框,或所有图框均无有效包围盒。" report_input = "\n\n".join(format_finding(f) for f in findings) try: return ctx.llm.chat_text(_SUMMARY_SYSTEM, report_input) except Exception as exc: # noqa: BLE001 log(f"[汇总失败] {exc}") return report_input def save_report(markdown, findings, plan, issues=None, log=print): """落盘 Markdown + JSON 报告,返回两个路径。 `issues` 省略时从 findings 里拍平派生(供批注回写)。 """ if issues is None: issues = flatten_issues(findings) out_dir = Path(config.REPORT_DIR) out_dir.mkdir(parents=True, exist_ok=True) stamp = datetime.now().strftime("%Y%m%d_%H%M%S") md_path = out_dir / f"review_{stamp}.md" json_path = out_dir / f"review_{stamp}.json" md_path.write_text(markdown, encoding="utf-8") json_path.write_text( json.dumps( { "generated_at": stamp, "findings": findings, "issues": issues, "frames": plan.get("frames"), }, ensure_ascii=False, indent=2, ), encoding="utf-8", ) log(f"[报告] 已写入 {md_path} 与 {json_path}") return md_path, json_path