#!/usr/bin/env python3 """用 prompt 管理 writer 角色生成第 1 章基线候选。 第 1 章没有合法 asOf,不能构造完整 WriterContext,因此本脚本直接把冻结的 WriterCreativeInput 交给 muse_role。角色 prompt、schema、预算、上下文上限和模型策略由 RoleExecutionProfile 绑定;调用回执和候选仍落既有证据表。第 1 章不伪造机械/语义门通过, 正式采纳仍受 write_canonical 的实时状态门约束。 """ import hashlib import json import sys import uuid from datetime import datetime from decimal import Decimal from pathlib import Path SCRIPT_DIR = Path(__file__).resolve().parent AGENT_ROOT = SCRIPT_DIR.parents[1] SKILLS = AGENT_ROOT / ".agent" / "skills" for sub in ( "write-next-chapter/scripts", "decide-candidate/scripts", "assemble-context/scripts", ): p = str(SKILLS / sub) if p not in sys.path: sys.path.insert(0, p) from muse_db import connect # noqa: E402 from muse_role import run_role # noqa: E402 from run_writer import build_writer_execution_profile # noqa: E402 from writer_contract import han_count, normalize_text, validate_writer_draft # noqa: E402 from write_canonical import accept # noqa: E402 WORK_ID = 12 TARGET_CHAPTER = 1 # 冻结 writer 提示词:从 replay-eval config 读(不复制粘贴,sha256 可与配置核对) GATE_A_CONFIG_PATH = ( SKILLS / "replay-writer-gate" / "configs" / "writer-gate-a-deep-space-v1.json" ) GATE_A_WRITER_PROFILE = json.loads(GATE_A_CONFIG_PATH.read_text(encoding="utf-8"))[ "executionProfiles"]["writer"] SYSTEM_PROMPT = GATE_A_WRITER_PROFILE["systemPrompt"] SYSTEM_PROMPT_ID = GATE_A_WRITER_PROFILE["systemPromptId"] SYSTEM_PROMPT_SHA256 = "sha256:" + hashlib.sha256(SYSTEM_PROMPT.encode("utf-8")).hexdigest() assert SYSTEM_PROMPT_SHA256 == GATE_A_WRITER_PROFILE["systemPromptSha256"], \ "config 里的 systemPrompt 与其冻结 sha256 不一致,停止(事实来源已漂移)" ARTIFACTS_DIR = SCRIPT_DIR / "artifacts" # 第 1 章细纲硬骨架(与库内 planning section id=5 confirmed payload 同源) HARD_CONSTRAINTS = [ "主角是天才机师,初次驾驶生物机甲。", "深渊异族(克苏鲁式)首次出现,营造微恐/精神压迫。", "建立黑暗森林/舰队/巨舰的世界观基调。", "章末钩子:主角觉醒生物机甲的代价(精神污染的预兆)。", ] ADJUSTABLE_BEATS = [ "开场节奏(侦察船日常 → 异常信号 → 异族现身)可调整。", "精神压迫的描写强度可调整(微恐基调)。", ] DECLARED_NEW_FACTS = [ {"factId": "fact-ch1-1", "text": "主角林深:天才机师。"}, {"factId": "fact-ch1-2", "text": "生物机甲:以异种核心驱动,机师与机甲精神链接。"}, {"factId": "fact-ch1-3", "text": "深渊异族:克苏鲁式,造成精神污染。"}, ] # 篇幅合同:细纲显式 targetChars=4000,区间按生产算法 = 目标值 ±30%(硬边界 2000-10000) LENGTH_CONTRACT = {"targetChars": 4000, "minChars": 2800, "maxChars": 5200, "frontmatterRequired": False} # 文风约束:投影自设定包 style 字段(planning section id=3)。第 1 章无历史原文摘录, # 文风画像是唯一文风锚点;生产投影从第 2 章起有 proseEvidence,本字段可为空。 STYLE_TEXT = ("冷峻、克制、微恐。战斗场面宏大(舰队/巨舰/机甲),心理描写细腻" "(克苏鲁式精神压迫)。句式偏短,节奏紧。") # 创作输入——严格按 build_writer_creative_input 的生产投影形状 CREATIVE_INPUT = { "fineOutline": { "hardConstraints": HARD_CONSTRAINTS, "adjustableBeats": ADJUSTABLE_BEATS, "declaredNewFacts": DECLARED_NEW_FACTS, }, "narrativeState": { "time": "故事开篇", "location": "深渊星域,人类舰队侦察船", "characterPositions": {"林深": "侦察船"}, "immediateSituation": "侦察船在深渊星域捕获异常信号,未知存在逼近。", }, "factConstraints": [], "proseExcerpts": [], "patternReferences": [], "lengthContract": LENGTH_CONTRACT, "styleConstraints": [{"styleId": "style-baseline-v1", "text": STYLE_TEXT}], } def main(): run_id = f"run-prod-work12-ch1-{uuid.uuid4().hex[:8]}" ARTIFACTS_DIR.mkdir(exist_ok=True) (ARTIFACTS_DIR / f"{run_id}-creative-input.json").write_text( json.dumps(CREATIVE_INPUT, ensure_ascii=False, indent=1), encoding="utf-8") profile = build_writer_execution_profile( max_budget_usd_per_call=Decimal("5.0"), timeout_seconds=1200, max_context_chars=200000, system_prompt=SYSTEM_PROMPT, system_prompt_id=SYSTEM_PROMPT_ID, ) print( f"writer 角色调用开始: run_id={run_id}, 模型策略={profile.model_alias}, " f"提示词={SYSTEM_PROMPT_ID}" ) started = datetime.now() invocation = run_role( profile, CREATIVE_INPUT, business_validator=validate_writer_draft, run_id=run_id, caller="writer", ) finished = datetime.now() receipt = invocation.receipt body = normalize_text(invocation.structured_output["candidateBody"]) actual_han = han_count(body) candidate_sha256 = hashlib.sha256(body.encode("utf-8")).hexdigest() print("── 回执 ──") print(f" actual_model_id: {receipt.actual_model_id} model_match: {receipt.model_match}") print(f" terminal_reason: {receipt.terminal_reason} stop_reason: {receipt.stop_reason} " f"is_error: {receipt.is_error} exit_code: {receipt.exit_code}") print(f" total_cost_usd: {receipt.total_cost_usd} usage: " f"{json.dumps(dict(receipt.usage or {}), ensure_ascii=False)[:160]}") print(f"── 候选 ──\n 汉字数: {actual_han}(合同 {LENGTH_CONTRACT['minChars']}" f"-{LENGTH_CONTRACT['maxChars']},目标 {LENGTH_CONTRACT['targetChars']})") print(f" sha256: {candidate_sha256[:16]}...") print(f" 正文前 120 字: {body[:120]}") # 落库前硬门:篇幅不出界才落库(3 字级垃圾不许入库污染看板) if not LENGTH_CONTRACT["minChars"] <= actual_han <= LENGTH_CONTRACT["maxChars"]: print(f"[拒绝落库] 汉字数 {actual_han} 超出篇幅合同,候选只留档 artifacts 不入库") raise SystemExit(1) if not receipt.model_match: print("[拒绝落库] 实际模型不属于冻结治理策略") raise SystemExit(1) # ── 落库:run + 候选 + 回执 + 实跑长度检测(同事务)── with connect() as conn: conn.execute( "INSERT INTO example_run(run_id, work_id, target_chapter, trigger_source, " "trigger_detail, started_at, finished_at, terminal_state, creator) " "VALUES (%s,%s,%s,'user',%s,%s,%s,'completed','writer')", (run_id, WORK_ID, TARGET_CHAPTER, json.dumps({"note": "第1章基线:writer 受治理角色调用;asOf 合同不适用," "未走生产组装与门", "systemPromptId": SYSTEM_PROMPT_ID, "systemPromptSha256": SYSTEM_PROMPT_SHA256, "executionProfileSha256": receipt.execution_profile_sha256}, ensure_ascii=False), started, finished)) cand_id = conn.execute( "INSERT INTO example_candidate(work_id, target_chapter, run_id, attempt, run_type, " "candidate_version, candidate_sha256, candidate_body, state, mode, source_role, " "acceptance_eligible, creator) " "VALUES (%s,%s,%s,1,'production','1',%s,%s,'passed','continuation','writer',true," "'writer') RETURNING id", (WORK_ID, TARGET_CHAPTER, run_id, candidate_sha256, body)).fetchone()[0] receipt_id = conn.execute( "INSERT INTO example_run_receipt(run_id, sample_id, revision, adapter_role, " "stage_kind, attempt, candidate_version, candidate_sha256, requested_model_id, " "actual_model_id, model_match, effort, total_cost_usd, usage, stop_reason, " "terminal_reason, is_error, creator) " "VALUES (%s,'writer-ch1',1,'writer','generation',1,'1',%s,%s,%s,%s,%s,%s," "%s::jsonb,%s,%s,%s,'writer') RETURNING id", (run_id, candidate_sha256, receipt.requested_model_id, receipt.actual_model_id, receipt.model_match, receipt.effort, str(receipt.total_cost_usd), json.dumps(dict(receipt.usage or {}), ensure_ascii=False), receipt.stop_reason, receipt.terminal_reason, bool(receipt.is_error))).fetchone()[0] # 长度检测是脚本真跑过的机械检查,如实落账;语义门对基线章不适用,不伪造通过。 conn.execute( "INSERT INTO example_quality_result(run_id, receipt_id, candidate_sha256, " "judge_kind, dimension, scale_version, conclusion, detail, creator) " "VALUES (%s,%s,%s,'detection','length','writer-production-v1','pass',%s::jsonb," "'writer')", (run_id, receipt_id, candidate_sha256, json.dumps({"actualHanChars": actual_han, **LENGTH_CONTRACT, "note": "基线章:仅篇幅机械检查;无 WriterContext,语义门不适用"}, ensure_ascii=False))) conn.commit() print(f"候选落库: candidate_id={cand_id}, run_id={run_id}") # 接受为正式正文(confirm 唯一通道)——第 1 章无正文块,expected_revision=0 accept_result = accept( cand_id, decided_by="1", rationale=("开篇基线章:writer 受治理角色调用产出,联合回执校验与篇幅" "机械检查通过。第 1 章无前文,asOf 合同无解,生产组装与机械/语义门不适用;" "文风锚定设定包 style。"), basis_ref="细纲@v1 (planning_section id=5)", expected_revision=0, command_id=f"accept-work12-ch1-{run_id}") print(f"接受结果: {json.dumps(accept_result, ensure_ascii=False)}") print(f"\nCANDIDATE_ID={cand_id}\nRUN_ID={run_id}") if __name__ == "__main__": main()