Coverage for src\genesis\qa\qa_loop.py: 95%
53 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-26 14:20 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-26 14:20 +0800
1"""QA 闭环:生成 → 校验 → 仅重生成失败章 → 复校验(Phase 5)。"""
2from __future__ import annotations
4from pathlib import Path
6from genesis.inference.factory import build_inference_engine
7from genesis.inference.prompt_registry import PromptRegistry
8from genesis.qa.guardrails import DEFAULT_MAX_QA_ROUNDS, QALoopController
9from genesis.qa.report import QAReport
10from genesis.qa.validator import QAValidator
11from genesis.writer.context_builder import build_contexts
12from genesis.writer.docx_injector import Block, DocxInjector
13from genesis.writer.models import ChapterContent
14from genesis.writer.orchestrator import _section_id_of, _warn_unanchored
15from genesis.writer.renderer import render_chapter_blocks
16from genesis.writer.writer_agent import WriterAgent
17from genesis.writer.writer_state import WriterState
20class QALoop:
21 def __init__(self, max_rounds: int = DEFAULT_MAX_QA_ROUNDS) -> None:
22 self.controller = QALoopController(max_rounds=max_rounds)
24 def _build(self, structured_source, samples_dir, engine, prompt_registry, template_path, output_path, session_id, only_ids=None, prev=None, output_language: str = "auto"):
25 ctxs = build_contexts(structured_source, samples_dir, output_language=output_language)
26 _warn_unanchored(ctxs)
27 state = WriterState([c.chapter_id for c in ctxs])
28 agent = WriterAgent(session_id=session_id, engine=engine, prompt_registry=prompt_registry, state=state)
29 contents_map = dict(prev) if prev else {}
30 order = [c.chapter_id for c in ctxs]
31 sections: dict[str, list[Block]] = {}
32 for ctx in ctxs:
33 if only_ids is not None and ctx.chapter_id not in only_ids and ctx.chapter_id in contents_map:
34 content = contents_map[ctx.chapter_id]
35 else:
36 content = agent.generate_chapter(ctx)
37 contents_map[ctx.chapter_id] = content
38 blocks = render_chapter_blocks(content)
39 sec_id = _section_id_of(ctx.template_marker.section_placeholder)
40 if sec_id: 40 ↛ 32line 40 didn't jump to line 32 because the condition on line 40 was always true
41 sections[sec_id] = blocks
42 tpl = template_path or getattr(structured_source.template, "file_name", None)
43 if not tpl: 43 ↛ 44line 43 didn't jump to line 44 because the condition on line 43 was never true
44 raise ValueError("template_path 必须提供")
45 Path(output_path).parent.mkdir(parents=True, exist_ok=True)
46 doc = DocxInjector(tpl).inject(sections, meta={})
47 doc.save(output_path)
48 return [contents_map[cid] for cid in order]
50 def run(self, structured_source, output_path, session_id="writer", samples_dir="sample", engine=None, prompt_registry=None, template_path=None, output_language: str = "auto") -> QAReport:
51 engine = engine or build_inference_engine()
52 prompt_registry = prompt_registry or PromptRegistry()
53 validator = QAValidator()
54 # auto 不可推导期望语言 → 语言维度记满分(unverifiable);zh/ja 显式强制
55 expected = output_language if output_language in ("zh", "ja") else ""
56 contents = self._build(structured_source, samples_dir, engine, prompt_registry, template_path, output_path, session_id, output_language=output_language)
57 report = validator.validate_doc(contents, structured_source, expected_language=expected)
58 while self.controller.can_continue() and report.failed_chapters:
59 self.controller.advance()
60 contents = self._build(
61 structured_source,
62 samples_dir,
63 engine,
64 prompt_registry,
65 template_path,
66 output_path,
67 session_id,
68 only_ids=set(report.failed_chapters),
69 prev={c.chapter_id: c for c in contents},
70 output_language=output_language,
71 )
72 report = validator.validate_doc(contents, structured_source, expected_language=expected)
73 return report