Files
pp-qa-km/tools/selfcheck_ingest.py

145 lines
6.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Phase 3 自檢:以假 LLM + 沙盒 git repo 驗證攝入管線的完整 plumbing——
分支建立、頁面產出、frontmatter 合規、index/log 更新、冪等跳過、就地編輯合併,
以及 kb.llm_json 的重試與 fallback 切換。不需要 Ollama 在線。
執行:.venv/Scripts/python tools/selfcheck_ingest.py
"""
import hashlib
import pathlib
import shutil
import subprocess
import sys
import tempfile
sys.path.insert(0, str(pathlib.Path(__file__).parent))
import kb
import ingest
CFG = """ollama: {base_url: "http://localhost:11434", timeout_seconds: 5}
tasks:
ingest: {model: "fake:test", temperature: 0.2, max_retries: 2}
lint: {model: "fake:test", temperature: 0.1, max_retries: 2}
fallback: {model: "fake:fb"}
limits: {mcp_response_max_tokens: 2000, ingest_chunk_max_chars: 200}
"""
def fake_llm(cfg, task, prompt, validate):
if prompt.startswith("以下是一份長文件的第"):
obj = {"summary_markdown": "- 分段重點"}
elif '"updated_markdown"' in prompt:
obj = {"updated_markdown": "- 壓測逾時\n- 新來源事實",
"updated_description": "支付閘道模組(已更新)"}
else:
obj = {"title": "支付閘道壓測報告", "description": "壓測發現逾時缺陷",
"slug": "gateway-load-test", "tags": ["load-test", "gateway"],
"summary_markdown": "- 逾時缺陷於壓測重現",
"knowledge_items": [{"type": "entity", "slug": "payment-gateway",
"title": "支付閘道", "description": "支付閘道模組",
"tags": ["gateway"], "facts_markdown": "- 壓測逾時"}]}
err = validate(obj) # 假輸出也要通過真 validator確保契約一致
assert err is None, err
return obj, "fake:test"
def git(root, *args):
return subprocess.run(["git", *args], cwd=root, check=True, text=True,
encoding="utf-8", capture_output=True).stdout.strip()
def add_doc(root, name, text):
p = root / "raw" / "converted" / name
p.write_text(text, encoding="utf-8")
sha = hashlib.sha256(text.encode("utf-8")).hexdigest()
import json
mp = root / "raw" / "manifest.json"
m = json.loads(mp.read_text(encoding="utf-8"))
m["files"][f"raw/converted/{name}"] = {
"kind": "converted", "sha256": sha, "original_path": f"raw/originals/{name}",
"original_sha256": sha, "converter": "from_docx", "converted_at": "2026-07-14T00:00:00"}
mp.write_text(json.dumps(m, ensure_ascii=False, indent=2), encoding="utf-8")
return sha
def main():
tmp = pathlib.Path(tempfile.mkdtemp(prefix="ppqa-ingest-"))
real_llm, real_chat = kb.llm_json, kb.ollama_chat
try:
root = tmp / "repo"
for sub in ("config", "raw/originals", "raw/converted",
"wiki/summaries", "wiki/entities", "wiki/concepts"):
(root / sub).mkdir(parents=True)
(root / "config" / "models.yaml").write_text(CFG, encoding="utf-8")
(root / "raw" / "manifest.json").write_text('{"version": 1, "files": {}}',
encoding="utf-8")
(root / "index.md").write_text("# 知識庫索引\n", encoding="utf-8")
(root / "log.md").write_text("# 操作日誌\n", encoding="utf-8")
git(root, "init", "-q", "-b", "main")
git(root, "config", "user.name", "selfcheck")
git(root, "config", "user.email", "selfcheck@local")
text = "支付閘道於壓力測試下出現逾時缺陷,交易峰值時重試機制未生效。\n\n" * 12
add_doc(root, "doc1.md", text) # > 200 chars → 走分段路徑
git(root, "add", "-A")
git(root, "commit", "-q", "-m", "init")
# 1) 攝入建分支、產頁、commit、回到 main
kb.llm_json = fake_llm
ingest.main(["raw/converted/doc1.md", "--root", str(root)])
assert git(root, "branch", "--show-current") == "main"
branch = git(root, "branch", "--list", "ingest/*").strip("* ").strip()
assert branch, "未建立 ingest 分支"
assert not git(root, "status", "--porcelain"), "工作區不乾淨"
git(root, "checkout", "-q", branch)
summary = list((root / "wiki/summaries").glob("*.md"))
entity = root / "wiki/entities/payment-gateway.md"
assert len(summary) == 1 and entity.is_file()
meta, _ = kb.parse_page(entity.read_text(encoding="utf-8"))
assert kb.validate_meta(meta) == [], kb.validate_meta(meta)
assert "payment-gateway" in (root / "index.md").read_text(encoding="utf-8")
assert "] ingest |" in (root / "log.md").read_text(encoding="utf-8")
print("PASS: 攝入 → 分支 + summary/entity 頁 + index/log")
# 2) 模擬 PR 合併後:冪等跳過;新來源觸發既有頁就地編輯
git(root, "checkout", "-q", "main")
git(root, "merge", "-q", branch)
ingest.main(["raw/converted/doc1.md", "--root", str(root)]) # 應 skip
assert git(root, "branch", "--show-current") == "main"
add_doc(root, "doc2.md", "第二份文件:支付閘道逾時已修復並通過回歸測試。")
git(root, "add", "-A")
git(root, "commit", "-q", "-m", "doc2")
ingest.main(["raw/converted/doc2.md", "--root", str(root)])
b2 = [b.strip("* ").strip() for b in
git(root, "branch", "--list", "ingest/*").splitlines() if branch not in b]
git(root, "checkout", "-q", b2[0])
meta, body = kb.parse_page(entity.read_text(encoding="utf-8"))
assert len(meta["sources"]) == 2 and "已更新" in meta["description"], meta
assert "新來源事實" in body
git(root, "checkout", "-q", "main")
print("PASS: 冪等跳過 + 既有 entity 頁就地編輯sources 追加)")
# 3) kb.llm_json重試後切 fallback全失敗拋錯
kb.llm_json = real_llm
cfg = kb.load_config(root)
calls = {"n": 0}
def flaky(cfg_, model, prompt, temperature, json_format=False):
calls["n"] += 1
return "not-json" if calls["n"] < 3 else '{"summary_markdown": "ok"}'
kb.ollama_chat = flaky
obj, model = kb.llm_json(cfg, "ingest", "p", ingest.validate_chunk)
assert obj["summary_markdown"] == "ok" and model == "fake:fb" and calls["n"] == 3
kb.ollama_chat = lambda *a, **k: "junk"
try:
kb.llm_json(cfg, "ingest", "p", ingest.validate_chunk)
raise AssertionError("應拋 RuntimeError")
except RuntimeError as e:
assert "fallback" in str(e)
print("PASS: 重試 + fallback 切換 + 全失敗報錯")
print("ALL PASS")
finally:
kb.llm_json, kb.ollama_chat = real_llm, real_chat
shutil.rmtree(tmp, ignore_errors=True)
if __name__ == "__main__":
main()