v0.20 alpha skill-driven python core
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(REPO_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(REPO_ROOT))
|
||||
|
||||
from scripts.runtime.assembly import (
|
||||
ChapterAssemblyWorker,
|
||||
build_chapter_briefs,
|
||||
build_chapter_user_prompt,
|
||||
run_chapter_assembly_workers,
|
||||
validate_chapter_markdown_citations,
|
||||
validate_chapter_brief,
|
||||
)
|
||||
from scripts.runtime.roles import resolve_runtime_profile
|
||||
|
||||
|
||||
class FakeClient:
|
||||
def __init__(self, response: str) -> None:
|
||||
self.response = response
|
||||
self.calls: list[dict[str, object]] = []
|
||||
|
||||
def chat_complete(self, **kwargs) -> str:
|
||||
self.calls.append(kwargs)
|
||||
return self.response
|
||||
|
||||
|
||||
class TaggedClient:
|
||||
def __init__(self, responses_by_tag: dict[str, str]) -> None:
|
||||
self.responses_by_tag = responses_by_tag
|
||||
self.calls: list[dict[str, object]] = []
|
||||
|
||||
def chat_complete(self, **kwargs) -> str:
|
||||
self.calls.append(kwargs)
|
||||
return self.responses_by_tag[str(kwargs["tag"])]
|
||||
|
||||
|
||||
def chapter_brief(chapter_id: str, source_ids: list[str]) -> dict:
|
||||
return {
|
||||
"chapter_id": chapter_id,
|
||||
"chapter_title": "临床证据正在重塑需求判断",
|
||||
"packet_ids": [f"{chapter_id}-clinical"],
|
||||
"core_claims": [{"claim": "临床证据支持核心判断", "source_ids": source_ids[:1]}],
|
||||
"evidence_items": [{"source_id": source_ids[0], "summary": "III 期数据支持主要终点"}],
|
||||
"counter_evidence": [{"claim": "长期安全性仍需随访", "source_ids": source_ids[-1:]}],
|
||||
"source_ids": source_ids,
|
||||
"open_questions": [],
|
||||
"assembly_notes": ["按金字塔结构组织。"],
|
||||
}
|
||||
|
||||
|
||||
def write_packet(path: Path, task_id: str, claim: str, source_id: str) -> None:
|
||||
packet = {
|
||||
"task_id": task_id,
|
||||
"claims": [{"claim": claim, "source_ids": [source_id]}],
|
||||
"evidence_items": [{"source_id": source_id, "summary": f"{claim} 的证据"}],
|
||||
"counter_evidence": [{"claim": "仍需关注样本量和外推限制", "source_ids": ["src_counter"]}],
|
||||
"source_ids": [source_id, "src_counter"],
|
||||
"source_quality_notes": [f"{source_id} Tier 1"],
|
||||
"open_questions": ["还需要补充中国市场数据"],
|
||||
"raw_quotes_or_notes": ["English note can remain as source material."],
|
||||
}
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(packet, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def test_build_chapter_briefs_aggregates_packets_by_chapter(tmp_path: Path) -> None:
|
||||
project = tmp_path / "project"
|
||||
cards = [
|
||||
{
|
||||
"task_id": "ch01-clinical",
|
||||
"chapter_ids": ["ch01"],
|
||||
"topic_axis": "clinical",
|
||||
"questions": ["q"],
|
||||
"search_routes": ["scholar"],
|
||||
"output_packet": "phase2/packets/ch01-clinical.json",
|
||||
},
|
||||
{
|
||||
"task_id": "ch01-market",
|
||||
"chapter_ids": ["ch01"],
|
||||
"topic_axis": "market",
|
||||
"questions": ["q"],
|
||||
"search_routes": ["news"],
|
||||
"output_packet": "phase2/packets/ch01-market.json",
|
||||
},
|
||||
]
|
||||
(project / "phase2").mkdir(parents=True)
|
||||
(project / "phase2" / "task_cards.json").write_text(json.dumps(cards, ensure_ascii=False), encoding="utf-8")
|
||||
write_packet(project / "phase2/packets/ch01-clinical.json", "ch01-clinical", "临床证据支持核心判断", "src_001")
|
||||
write_packet(project / "phase2/packets/ch01-market.json", "ch01-market", "市场数据支持需求增长", "src_002")
|
||||
|
||||
briefs = build_chapter_briefs(project)
|
||||
|
||||
assert len(briefs) == 1
|
||||
brief = briefs[0]
|
||||
validate_chapter_brief(brief)
|
||||
assert brief["chapter_id"] == "ch01"
|
||||
assert brief["packet_ids"] == ["ch01-clinical", "ch01-market"]
|
||||
assert "src_001" in brief["source_ids"]
|
||||
assert "src_002" in brief["source_ids"]
|
||||
assert (project / "phase2/chapter_briefs/ch01.json").exists()
|
||||
|
||||
|
||||
def test_chapter_prompt_contains_brief_and_fragmentation_guard() -> None:
|
||||
brief = {
|
||||
"chapter_id": "ch01",
|
||||
"chapter_title": "临床证据正在重塑需求判断",
|
||||
"packet_ids": ["ch01-clinical"],
|
||||
"core_claims": [{"claim": "临床证据支持核心判断", "source_ids": ["src_001"]}],
|
||||
"evidence_items": [{"source_id": "src_001", "summary": "III 期数据支持主要终点"}],
|
||||
"counter_evidence": [{"claim": "长期安全性仍需随访", "source_ids": ["src_002"]}],
|
||||
"source_ids": ["src_001", "src_002"],
|
||||
"open_questions": [],
|
||||
"assembly_notes": ["避免重复 packet 原文;按金字塔结构组织。"],
|
||||
}
|
||||
|
||||
prompt = build_chapter_user_prompt(brief)
|
||||
|
||||
assert "临床证据正在重塑需求判断" in prompt
|
||||
assert "避免碎片化" in prompt
|
||||
assert "只输出 Markdown" in prompt
|
||||
|
||||
|
||||
def test_chapter_assembly_worker_writes_markdown(tmp_path: Path) -> None:
|
||||
runtime = resolve_runtime_profile(profile="medium")
|
||||
role = runtime.role_for_task("chapter_assembly")
|
||||
fake = FakeClient("# 第1章 临床证据正在重塑需求判断\n\n结论先行。[src_001]\n")
|
||||
worker = ChapterAssemblyWorker(role=role, client=fake)
|
||||
brief = {
|
||||
"chapter_id": "ch01",
|
||||
"chapter_title": "临床证据正在重塑需求判断",
|
||||
"packet_ids": ["ch01-clinical"],
|
||||
"core_claims": [{"claim": "临床证据支持核心判断", "source_ids": ["src_001"]}],
|
||||
"evidence_items": [{"source_id": "src_001", "summary": "III 期数据支持主要终点"}],
|
||||
"counter_evidence": [{"claim": "长期安全性仍需随访", "source_ids": ["src_002"]}],
|
||||
"source_ids": ["src_001", "src_002"],
|
||||
"open_questions": [],
|
||||
"assembly_notes": ["按金字塔结构组织。"],
|
||||
}
|
||||
|
||||
output = worker.write_chapter(project_root=tmp_path, brief=brief)
|
||||
|
||||
assert output == tmp_path / "phase2/drafts/ch01.md"
|
||||
assert "结论先行" in output.read_text(encoding="utf-8")
|
||||
assert fake.calls[0]["model"] == role.model
|
||||
|
||||
|
||||
def test_validate_chapter_markdown_rejects_unknown_source_ids() -> None:
|
||||
brief = {
|
||||
"chapter_id": "ch01",
|
||||
"chapter_title": "临床证据正在重塑需求判断",
|
||||
"packet_ids": ["ch01-clinical"],
|
||||
"core_claims": [{"claim": "临床证据支持核心判断", "source_ids": ["src_001"]}],
|
||||
"evidence_items": [{"source_id": "src_001", "summary": "III 期数据支持主要终点"}],
|
||||
"counter_evidence": [{"claim": "长期安全性仍需随访", "source_ids": ["src_002"]}],
|
||||
"source_ids": ["src_001", "src_002"],
|
||||
"open_questions": [],
|
||||
"assembly_notes": ["按金字塔结构组织。"],
|
||||
}
|
||||
|
||||
try:
|
||||
validate_chapter_markdown_citations("结论引用了不存在的来源。[src_fake]", brief)
|
||||
except ValueError as exc:
|
||||
assert "unknown citation ids" in str(exc)
|
||||
assert "src_fake" in str(exc)
|
||||
else:
|
||||
raise AssertionError("unknown source id should fail validation")
|
||||
|
||||
|
||||
def test_chapter_assembly_worker_refuses_to_write_unknown_citations(tmp_path: Path) -> None:
|
||||
runtime = resolve_runtime_profile(profile="medium")
|
||||
role = runtime.role_for_task("chapter_assembly")
|
||||
fake = FakeClient("# 第1章 临床证据正在重塑需求判断\n\n结论先行。[src_fake]\n")
|
||||
worker = ChapterAssemblyWorker(role=role, client=fake)
|
||||
brief = {
|
||||
"chapter_id": "ch01",
|
||||
"chapter_title": "临床证据正在重塑需求判断",
|
||||
"packet_ids": ["ch01-clinical"],
|
||||
"core_claims": [{"claim": "临床证据支持核心判断", "source_ids": ["src_001"]}],
|
||||
"evidence_items": [{"source_id": "src_001", "summary": "III 期数据支持主要终点"}],
|
||||
"counter_evidence": [{"claim": "长期安全性仍需随访", "source_ids": ["src_002"]}],
|
||||
"source_ids": ["src_001", "src_002"],
|
||||
"open_questions": [],
|
||||
"assembly_notes": ["按金字塔结构组织。"],
|
||||
}
|
||||
|
||||
try:
|
||||
worker.write_chapter(project_root=tmp_path, brief=brief)
|
||||
except ValueError as exc:
|
||||
assert "unknown citation ids" in str(exc)
|
||||
else:
|
||||
raise AssertionError("chapter with unknown citation should not be written")
|
||||
assert not (tmp_path / "phase2/drafts/ch01.md").exists()
|
||||
|
||||
|
||||
def test_run_chapter_assembly_workers_records_errors_without_aborting_batch(tmp_path: Path) -> None:
|
||||
runtime = resolve_runtime_profile(profile="medium")
|
||||
fake = TaggedClient(
|
||||
{
|
||||
"chapter:ch01": "# 第1章 临床证据正在重塑需求判断\n\n结论先行。[src_001]\n",
|
||||
"chapter:ch02": "# 第2章 临床证据存在不确定性\n\n错误引用。[src_fake]\n",
|
||||
}
|
||||
)
|
||||
|
||||
count = run_chapter_assembly_workers(
|
||||
project_root=tmp_path,
|
||||
briefs=[chapter_brief("ch01", ["src_001", "src_002"]), chapter_brief("ch02", ["src_003", "src_004"])],
|
||||
runtime=runtime,
|
||||
client_factory=lambda _role: fake,
|
||||
workers=2,
|
||||
)
|
||||
|
||||
assert count == 1
|
||||
assert (tmp_path / "phase2/drafts/ch01.md").exists()
|
||||
assert not (tmp_path / "phase2/drafts/ch02.md").exists()
|
||||
error_path = tmp_path / "phase2/chapter_errors/ch02.json"
|
||||
assert error_path.exists()
|
||||
error = json.loads(error_path.read_text(encoding="utf-8"))
|
||||
assert error["chapter_id"] == "ch02"
|
||||
assert error["status"] == "failed"
|
||||
assert "src_fake" in error["error"]
|
||||
Reference in New Issue
Block a user