v0.21 alpha add research brief and compressed findings
This commit is contained in:
+109
-3
@@ -41,8 +41,41 @@ def validate_chapter_brief(brief: dict) -> None:
|
||||
raise ValueError("chapter brief requires counter_evidence")
|
||||
|
||||
|
||||
def validate_compressed_finding(finding: dict) -> None:
|
||||
required = {
|
||||
"chapter_id",
|
||||
"chapter_title",
|
||||
"packet_ids",
|
||||
"chapter_thesis",
|
||||
"key_findings",
|
||||
"evidence_landings",
|
||||
"counter_evidence",
|
||||
"source_ids",
|
||||
"open_questions",
|
||||
"writing_plan",
|
||||
}
|
||||
missing = sorted(required - set(finding))
|
||||
if missing:
|
||||
raise ValueError(f"compressed finding missing fields: {missing}")
|
||||
if not finding["chapter_id"]:
|
||||
raise ValueError("chapter_id required")
|
||||
if not finding["packet_ids"]:
|
||||
raise ValueError("compressed finding requires packet_ids")
|
||||
if not finding["chapter_thesis"]:
|
||||
raise ValueError("compressed finding requires chapter_thesis")
|
||||
if not finding["key_findings"]:
|
||||
raise ValueError("compressed finding requires key_findings")
|
||||
if not finding["evidence_landings"]:
|
||||
raise ValueError("compressed finding requires evidence_landings")
|
||||
if not finding["counter_evidence"]:
|
||||
raise ValueError("compressed finding requires counter_evidence")
|
||||
|
||||
|
||||
def validate_chapter_markdown_citations(markdown: str, brief: dict) -> None:
|
||||
validate_chapter_brief(brief)
|
||||
if "key_findings" in brief:
|
||||
validate_compressed_finding(brief)
|
||||
else:
|
||||
validate_chapter_brief(brief)
|
||||
cited = set(re.findall(r"\[(src_[A-Za-z0-9_-]+)\]", markdown))
|
||||
allowed = set(brief.get("source_ids") or [])
|
||||
unknown = sorted(cited - allowed)
|
||||
@@ -103,9 +136,79 @@ def build_chapter_briefs(project_root: Path) -> list[dict]:
|
||||
return briefs
|
||||
|
||||
|
||||
def _source_ids_from_item(item: dict) -> list[str]:
|
||||
if item.get("source_ids"):
|
||||
return list(item.get("source_ids") or [])
|
||||
if item.get("source_id"):
|
||||
return [item["source_id"]]
|
||||
return []
|
||||
|
||||
|
||||
def build_compressed_findings(project_root: Path) -> list[dict]:
|
||||
"""Compress packet-level evidence into chapter-level writing inputs.
|
||||
|
||||
This is intentionally deterministic: it does not invent a better narrative,
|
||||
but it forces a chapter-level evidence map before any model writes prose.
|
||||
"""
|
||||
brief_dir = project_root / "phase2" / "chapter_briefs"
|
||||
if not brief_dir.exists() or not list(brief_dir.glob("ch*.json")):
|
||||
briefs = build_chapter_briefs(project_root)
|
||||
else:
|
||||
briefs = [
|
||||
json.loads(path.read_text(encoding="utf-8"))
|
||||
for path in sorted(brief_dir.glob("ch*.json"))
|
||||
]
|
||||
out_dir = project_root / "phase2" / "compressed_findings"
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
findings: list[dict] = []
|
||||
for brief in briefs:
|
||||
validate_chapter_brief(brief)
|
||||
core_claims = brief.get("core_claims") or []
|
||||
evidence_items = brief.get("evidence_items") or []
|
||||
first_claim = core_claims[0] if core_claims else {}
|
||||
chapter_thesis = first_claim.get("claim") or f"{brief['chapter_title']} 需要以证据为中心重写。"
|
||||
finding = {
|
||||
"chapter_id": brief["chapter_id"],
|
||||
"chapter_title": brief["chapter_title"],
|
||||
"packet_ids": brief["packet_ids"],
|
||||
"chapter_thesis": chapter_thesis,
|
||||
"key_findings": [
|
||||
{
|
||||
"finding": claim.get("claim") or claim.get("summary") or str(claim),
|
||||
"source_ids": _source_ids_from_item(claim),
|
||||
"confidence": claim.get("confidence", "medium"),
|
||||
}
|
||||
for claim in core_claims
|
||||
],
|
||||
"evidence_landings": [
|
||||
{
|
||||
"evidence": item.get("summary") or item.get("finding") or item.get("quote") or str(item),
|
||||
"source_ids": _source_ids_from_item(item),
|
||||
"landing_hint": item.get("landing_hint", "用于支撑本章关键判断或整改动作。"),
|
||||
}
|
||||
for item in evidence_items
|
||||
],
|
||||
"counter_evidence": brief["counter_evidence"],
|
||||
"source_ids": brief["source_ids"],
|
||||
"open_questions": brief["open_questions"],
|
||||
"writing_plan": [
|
||||
"先写本章判断,不按 packet 顺序堆砌。",
|
||||
"每个二级小节至少落下具体审计发现、法规要求、记录/参数或整改证据。",
|
||||
"正文末尾必须保留“证据落点与待补证据”表。",
|
||||
],
|
||||
}
|
||||
validate_compressed_finding(finding)
|
||||
(out_dir / f"{brief['chapter_id']}.json").write_text(
|
||||
json.dumps(finding, ensure_ascii=False, indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
findings.append(finding)
|
||||
return findings
|
||||
|
||||
|
||||
def build_chapter_user_prompt(brief: dict) -> str:
|
||||
return (
|
||||
"请根据以下 chapter brief 写一章正式中文 Markdown 正文。\n"
|
||||
"请根据以下 compressed finding / chapter brief 写一章正式中文 Markdown 正文。\n"
|
||||
"目标是形成一个完整章节,而不是 packet 摘要。避免碎片化,按金字塔结构组织:章首先给结论,再用证据支撑。\n"
|
||||
"要求:标题必须是观点型判断;每个数字和事实保留 [src_xxx];纳入反方证据;不要出现调度元数据。\n"
|
||||
"禁止写空泛咨询腔。每个二级小节都必须至少落下 2 个具体审计发现、法规要求、SOP/记录/参数/现场观察或整改证据;不要只写原则。\n"
|
||||
@@ -142,7 +245,10 @@ class ChapterAssemblyWorker:
|
||||
)
|
||||
|
||||
def write_chapter(self, *, project_root: Path, brief: dict) -> Path:
|
||||
validate_chapter_brief(brief)
|
||||
if "key_findings" in brief:
|
||||
validate_compressed_finding(brief)
|
||||
else:
|
||||
validate_chapter_brief(brief)
|
||||
markdown = self.client.chat_complete(
|
||||
model=self.role.model,
|
||||
system=self._system_prompt(),
|
||||
|
||||
Reference in New Issue
Block a user