muse-agent-example/src/muse/编排/生成正文.py
zizi 9e6f1c4481 R2 改造交付:新版模块化单体全量成果
- src/muse 新版全模块(装配/共享/上下文/任务运行/作品规划/故事世界/正文写作/审校修订/知识方法/作者经验/效果评测/交付连载/资料研究/正式变更/元数据/接入/基础设施/编排)+ 测试树(单元/契约/集成/架构/迁移/端到端/夹具)
- 129 项功能全部实现与自动验证(功能覆盖.json/矩阵),含 W31 补齐的规则与代价/节奏安排/伏笔与承诺
- 旧实现按处置清单退出(702 条中 324 删,保护合同与未迁移条目留存有据);web/app.py 旧工作台退役,新工作台为唯一写入口
- 数据库/旧库迁移:真实旧库内容批次迁移链(端点守卫/PG作品正文映射/质量资产缺省投影)
- 运行手册 docs/运行手册.md;W30 本机服务阶段一已运行(infra PG 为正式内容权威)
- R2 执行证据与私有运行材料在 .agents.local/改造/R2-20260909/(不入库)
2026-09-15 12:47:42 +08:00

640 lines
25 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.

"""生成正文旅程:预组装、受限探索、冻结回放、无工具写作与检查入候选。
只调用各模块公开用例并按授权安排步骤;处理器在此登记为实际 Python 实现,
配置(配置/流程模板/生成正文.yaml)只选择稳定身份与版本。
"""
from __future__ import annotations
import asyncio
import hashlib
import json
from dataclasses import asdict, replace
from datetime import UTC, datetime, timedelta
from functools import partial
from typing import Any
from uuid import uuid4
from muse.上下文.接口 import (
依赖重验检查,
保护模型发送,
回放结果,
探索条目,
来源绑定,
核对依赖,
核对范围声音,
装配材料,
记录探索,
)
from muse.任务运行.接口 import (
事件类型,
任务请求,
回合记录,
工具回传,
执行上下文,
模型请求,
步骤处理器,
步骤结果,
步骤计划,
组装角色请求,
角色策略目录,
请求字节,
配置版本管理,
)
from muse.共享.调用身份 import 内容用途
from muse.共享.错误 import Muse错误
from muse.基础设施.数据库.连接 import 数据库工厂
from muse.审校修订.接口 import 审校服务
from muse.正文写作.接口 import (
写作输出合同,
写入模型候选,
创建写作任务,
文本节点,
正文草稿,
段落,
生成正文模板,
读取当前正文依据,
)
from muse.编排.接口 import 流程登记
from muse.编排.流程版本 import 流程定义
_写作输出合同 = 写作输出合同()
流程身份 = "生成正文"
流程版本 = "1.0.0"
# 探索阶段允许的具名只读工具;与冻结范围安装的工具一致,写作阶段为空(禁工具)。
探索工具集 = ("query_work_plan", "query_facts_as_of", "read_history_document")
处理器版本集 = {
"ctx.prep": "1",
"ctx.explore": "1",
"ctx.freeze": "1",
"ctx.write": "1",
"ctx.check": "1",
}
_探索输出合同 = {"type": "object"}
class 生成正文错误(Muse错误):
"""旅程编排内的明确失败;错误说明面向作者呈现。"""
错误码 = "WRITING_FLOW_INVALID"
def _工厂(装配) -> 数据库工厂:
return 数据库工厂(装配.配置.数据库, 装配.配置.运行用途)
def 前步检查点(上下文: 执行上下文, 步骤ID: str) -> dict[str, Any]:
"""读取已提交前置步骤的检查点;缺失即旅程不完整,明确失败。"""
for 项 in 上下文.任务.步骤:
if 项.get("step_id") == 步骤ID:
检查点 = 项.get("checkpoint") or {}
if not 检查点:
raise 生成正文错误(f"前置步骤 {步骤ID} 没有可用检查点")
return 检查点
raise 生成正文错误(f"前置步骤 {步骤ID} 尚未执行")
def _探索清单从检查点(数据: list[dict[str, Any]]) -> tuple[探索条目, ...]:
return tuple(
探索条目(
调用ID=项["调用ID"],
工具名=项["工具名"],
顺序=项["顺序"],
参数=项["参数"],
来源引用=tuple(来源绑定(**绑) for 绑 in 项["来源引用"]),
消费哈希=项["消费哈希"],
)
for 项 in 数据
)
def _原文授权(装配, 任务, 请求: 模型请求, *, 方式: str) -> str:
"""为一次模型调用取得作者批准的原文授权。
探索回合走持久证据(无租约);生成回合走临时租约,字节只进受控暂存。
"""
原文 = 装配.要求原文()
哈希 = hashlib.sha256(请求字节(请求)).hexdigest()
return 原文.批准保留(
任务.任务ID,
任务.作者,
"raw-" + 请求.调用ID,
来源版本="生成正文任务",
哈希=(哈希,),
内容用途="generation",
方式=方式,
有效期=datetime.now(UTC) + timedelta(minutes=20),
调用ID=请求.调用ID,
调用请求哈希=哈希,
)
def _写手请求(
装配,
上下文: 执行上下文,
*,
阶段: str,
系统提示: str,
用户输入: str,
工具: tuple[dict[str, Any], ...] = (),
历史: tuple[回合记录, ...] = (),
) -> 模型请求:
任务 = 上下文.任务
版本 = 配置版本管理(_工厂(装配)).读取任务绑定(任务.任务ID)
声明 = 版本.内容.角色配置["writer"]
请求 = 模型请求(
调用ID=str(uuid4()),
provider=声明["provider"],
model=声明["model"],
thinking=声明.get("thinking"),
系统提示=系统提示,
用户输入=用户输入,
输出合同=_写作输出合同 if 阶段 == "生成" else _探索输出合同,
最大输出token=int(任务.冻结输入["冻结上下文"]["budget"].get("最大输出token", 8192)),
总期限秒=600.0,
工具=工具,
历史=历史,
)
return 组装角色请求(角色策略目录.从发布包(), "writer", 请求)
def 登记生成正文(登记: 流程登记, 装配, 计价) -> None:
"""登记生成正文流程的处理器、类型与恢复检查;装配提供全部实现。"""
服务 = 装配.要求上下文()
登记.登记类型(流程身份, 必需保护=("冻结上下文",))
登记.登记恢复检查(流程身份, 流程版本, 依赖重验检查(服务))
def 预组装(上下文: 执行上下文) -> 步骤结果:
范围 = 服务.恢复冻结范围(上下文.任务)
写作 = 上下文.任务.冻结输入["输入"]["写作任务"]
if 写作.get("chapter_id") != (范围.get("target_plan") or {}).get("chapter_id"):
raise 生成正文错误(f"写作任务章位 {写作.get('chapter_id')} 与冻结细纲章位不一致")
if "budget" not in 上下文.任务.冻结输入["冻结上下文"]:
raise 生成正文错误("任务缺少预算冻结输入")
return 步骤结果(
输出={"work_id": 范围["work_id"], "chapter_id": 写作["chapter_id"]},
检查点={
"work_id": 范围["work_id"],
"预组装": {"as_of": 范围["as_of"], "来源数": len(范围["source_ids"])},
},
)
def 受限探索(上下文: 执行上下文) -> 步骤结果:
任务 = 上下文.任务
工具 = 装配.要求只读工具(上下文.领取)
清单: tuple[探索条目, ...] = ()
绑定集: dict[str, 来源绑定] = {}
历史: list[回合记录] = []
轮次上限 = int(
任务.冻结输入["冻结上下文"].get("stop_conditions", {}).get("探索轮数上限", 6)
)
工具描述 = tuple(
{"name": d.名称, "description": d.说明, "parameters": d.参数合同}
for d in 工具.工具.values()
)
探索提示 = (
"你是本作品的写手,当前处于受限探索阶段。"
"使用提供的只读工具了解目标章细纲、截止时点事实与历史正文,"
"为写作收集必要材料;不要输出正文,探索完成后输出一个空 JSON 对象。"
)
for _ in range(轮次上限):
请求 = _写手请求(
装配,
上下文,
阶段="探索",
系统提示=探索提示,
用户输入=json.dumps(
{
"粒度": 任务.冻结输入["输入"]["写作任务"]["粒度"],
"用途说明": 任务.冻结输入["输入"]["写作任务"].get("用途说明", ""),
"材料需求": ["目标章细纲", "时点事实", "历史正文"],
},
ensure_ascii=False,
),
工具=工具描述,
历史=tuple(历史),
)
执行器 = 装配.要求模型执行器(上下文, 计价)
授权 = _原文授权(装配, 任务, 请求, 方式="persistent")
回合 = asyncio.run(执行器.执行回合(上下文, 请求, 阶段="探索", 原文授权ID=授权))
if 回合.结果.状态 == "tool_calls":
回传: list[工具回传] = []
for 调用 in 回合.结果.工具调用:
结果 = 工具.调用(调用)
清单 = 记录探索(清单, 调用.调用ID, 调用.名称, 调用.参数, 结果)
for 源 in 结果.来源:
绑定集.setdefault(
源.来源ID,
来源绑定(源.来源ID, 源.数据版本, 源.结构哈希, 源.投影版本),
)
回传.append(工具回传(调用, json.dumps(结果.内容, ensure_ascii=False)))
历史.append(回合记录(回合.结果, tuple(回传)))
continue
if 回合.结果.状态 != "completed":
raise 生成正文错误(f"探索回合未完成:{回合.结果.状态}")
break
else:
raise 生成正文错误("探索轮数超出停止条件上限")
if not 清单:
raise 生成正文错误("写手没有进行任何受限探索读取")
return 步骤结果(
输出={"清单长度": len(清单)},
检查点={
"清单": [asdict(项) for 项 in 清单],
"绑定": [asdict(绑) for 绑 in 绑定集.values()],
},
)
def 冻结回放(上下文: 执行上下文) -> 步骤结果:
任务 = 上下文.任务
前置 = 前步检查点(上下文, "受限探索")
清单 = _探索清单从检查点(前置["清单"])
绑定集 = {绑["来源ID"]: 来源绑定(**绑) for 绑 in 前置["绑定"]}
回放: 回放结果 = 服务.回放探索(任务.任务ID, 清单)
材料: dict[str, str] = {}
细纲投影: dict[str, Any] | None = None
事实: list[dict[str, Any]] = []
for 项 in 回放.逐项:
if 项.工具名 == "query_work_plan":
细纲投影 = 项.内容["content"]
材料["目标章细纲"] = json.dumps(项.内容["content"], ensure_ascii=False)
elif 项.工具名 == "query_facts_as_of":
事实 = list(项.内容.get("facts", []))
for 实体 in 事实:
名称 = (实体.get("content") or {}).get("名称")
if 名称:
材料["事实:" + 实体["object_id"]] = str(名称)
elif 项.工具名 == "read_history_document":
材料["历史:" + 项.调用ID] = 项.内容.get("text", "")
if 细纲投影 is None:
raise 生成正文错误("回放清单中没有目标章细纲,不能进入写作")
# 固定绑定方法参与预算取舍;入选只表示准备完成,不等同于模型消费。
from muse.上下文.接口 import 偏好绑定, 核对范围偏好, 核对范围方法
范围 = 服务.恢复冻结范围(任务)
with _工厂(装配).连接(只读=True) as 连:
声音 = 核对范围声音(连, 范围)
方法材料 = 核对范围方法(连, 范围)
偏好材料 = 核对范围偏好(连, 范围)
必读 = ("目标章细纲",)
for 条 in 偏好材料:
材料[条["材料键"]] = 条["材料"]
必读 += (条["材料键"],)
绑定集[条["preference_id"]] = 偏好绑定(条)
if 声音 is not None:
材料[声音["材料键"]] = 声音["材料"]
必读 += (声音["材料键"],)
绑定集[声音["version_id"]] = 来源绑定(
声音["version_id"],
str(声音["revision"]),
声音["schema_hash"],
声音["projection_version"],
)
方法清单 = []
for 条 in 方法材料:
键 = 条["材料键"]
材料[键] = 条["材料"]
方法清单.append({**条, "材料键": 键})
装配结果 = 装配材料(
材料,
必读,
字节上限=int(任务.冻结输入["冻结上下文"]["budget"]["context_bytes"]),
)
# 超出预算而省略的材料未进入快照,不得登记为消费。
方法清单 = [条 for 条 in 方法清单 if 条["材料键"] in 装配结果.入选来源]
for 条 in 方法清单:
绑定集[条["binding_id"]] = 来源绑定(
条["binding_id"], str(条["version"]), 条["schema_hash"], 条["projection_version"]
)
回执 = 服务.冻结上下文(任务.任务ID, 装配结果, tuple(绑定集.values()))
return 步骤结果(
输出={"snapshot_id": 回执.快照ID, "字节数": 装配结果.字节数},
检查点={
"快照ID": 回执.快照ID,
"细纲投影": 细纲投影,
"事实": 事实,
"入选": list(装配结果.入选来源),
"方法材料": [
{键: 条[键] for 键 in ("version_id", "材料键", "材料哈希")} for 条 in 方法清单
],
},
)
def 无工具写作(上下文: 执行上下文) -> 步骤结果:
任务 = 上下文.任务
前置 = 前步检查点(上下文, "冻结回放")
快照 = 服务.读取快照(前置["快照ID"])
if 快照.失效说明:
raise 生成正文错误(f"上下文快照已失效:{快照.失效说明}")
with _工厂(装配).连接(只读=True) as 连:
状态 = 核对依赖(连, 服务.恢复冻结范围(任务), 服务.规划引用解析)
if not 状态.有效:
raise 生成正文错误("冻结来源失效:" + ";".join(状态.变化项))
模板, 模板哈希 = 生成正文模板()
写作 = 任务.冻结输入["输入"]["写作任务"]
请求 = _写手请求(
装配,
上下文,
阶段="生成",
系统提示=模板,
用户输入=json.dumps(
{
"粒度": 写作["粒度"],
"用途说明": 写作.get("用途说明", ""),
"上下文快照ID": 快照.快照ID,
"装配材料": 快照.装配["文本"],
"选区": 写作.get("选区", []),
"保护区": 写作.get("保护区", []),
},
ensure_ascii=False,
),
)
执行器 = 装配.要求模型执行器(上下文, 计价)
授权 = _原文授权(装配, 任务, 请求, 方式="temporary")
交付 = asyncio.run(
执行器.执行(
上下文,
请求,
阶段="生成",
原文授权ID=授权,
临时租约ID=_租约(装配, 任务, 授权, 请求.调用ID),
发送复检=partial(
保护模型发送,
执行用途=_工厂(装配).用途,
作者=任务.作者,
任务ID=任务.任务ID,
快照ID=快照.快照ID,
模型输入=请求.用户输入,
),
)
)
# 只有真实写作交付后才登记消费,并关联模型调用;冻结不自动产生消费。
from muse.知识方法.接口 import 消费输入, 记录消费
with _工厂(装配).连接() as 连:
for 条 in 前置.get("方法材料", []):
记录消费(
连,
任务.作者,
消费输入(
method_version_id=条["version_id"],
kind="generation",
context_ref=str(前置["快照ID"]),
fragment_hash=条["材料哈希"],
result_ref="model-call:" + str(交付.调用ID),
task_id=str(任务.任务ID),
),
模型输入=请求.用户输入,
执行用途=_工厂(装配).用途,
)
段落集 = 交付.内容["paragraphs"]
for 段 in 段落集:
装配.任务运行.追加事件(
上下文.领取,
事件类型.文本片段,
{"文本": 段["text"], "模板哈希": 模板哈希},
事件ID=str(uuid4()),
)
return 步骤结果(
输出={"段落数": len(段落集)},
检查点={
"段落": [段["text"] for 段 in 段落集],
"调用ID": 交付.调用ID,
"证据回执": 交付.证据回执,
"模板哈希": 模板哈希,
},
)
def 检查与候选(上下文: 执行上下文) -> 步骤结果:
任务 = 上下文.任务
冻结点 = 前步检查点(上下文, "冻结回放")
写作点 = 前步检查点(上下文, "无工具写作")
写作 = 任务.冻结输入["输入"]["写作任务"]
草稿 = 正文草稿(
tuple(
段落(f"gen-{序}", (文本节点(文本),))
for 序, 文本 in enumerate(写作点["段落"], start=1)
)
)
出场名映射: dict[str, str] = {}
for 实体 in 冻结点.get("事实", []):
名称 = (实体.get("content") or {}).get("名称")
if 名称:
出场名映射.setdefault(实体["object_id"], str(名称))
for 身份 in 冻结点["细纲投影"].get("mustAppearEntities") or []:
出场名映射.setdefault(str(身份), str(身份))
with _工厂(装配).连接() as 连, 连.transaction():
候选 = 写入模型候选(
连,
任务.作者,
任务.任务ID,
冻结点["快照ID"],
写作["chapter_id"],
写作.get("branch_id", "main"),
int(写作["基线版本"]),
草稿,
流程版本=流程版本,
处理器版本=处理器版本集,
模型证据=[写作点["调用ID"], 写作点["证据回执"]],
提示词哈希={"生成正文": 写作点["模板哈希"]},
)
报告 = 审校服务(_工厂(装配)).检查并保存(
连,
草稿,
冻结点["细纲投影"],
出场名映射,
candidate_id=候选["candidate_id"],
candidate_revision=int(候选["revision"]),
snapshot_id=冻结点["快照ID"],
原始交付=json.dumps(
{"paragraphs": [{"text": t} for t in 写作点["段落"]]},
ensure_ascii=False,
),
)
for 类型, 载荷 in (
(
事件类型.候选保存,
{"candidate_id": 候选["candidate_id"], "revision": 候选["revision"]},
),
(事件类型.检查完成, {"verdict": 报告["verdict"], "report_id": str(报告["report_id"])}),
):
装配.任务运行.追加事件(上下文.领取, 类型, 载荷, 事件ID=str(uuid4()))
return 步骤结果(
输出={
"candidate_id": 候选["candidate_id"],
"revision": 候选["revision"],
"candidate_hash": 候选["candidate_hash"],
"verdict": 报告["verdict"],
},
检查点={"候选": 候选, "报告": str(报告["report_id"]), "verdict": 报告["verdict"]},
)
登记.登记处理器(步骤处理器("ctx.prep", "1", 预组装, "writing-task", "frozen-scope"))
登记.登记处理器(
步骤处理器(
"ctx.explore",
"1",
受限探索,
"frozen-scope",
"exploration-manifest",
角色="writer",
允许工具=探索工具集,
)
)
登记.登记处理器(
步骤处理器(
"ctx.freeze",
"1",
冻结回放,
"exploration-manifest",
"context-snapshot",
保护职责="冻结上下文",
)
)
登记.登记处理器(
步骤处理器(
"ctx.write",
"1",
无工具写作,
"context-snapshot",
"generated-draft",
角色="writer",
)
)
登记.登记处理器(步骤处理器("ctx.check", "1", 检查与候选, "generated-draft", "model-candidate"))
def _租约(装配, 任务, 授权ID: str, 调用ID: str) -> str:
原文 = 装配.要求原文()
return 原文.创建租约(
任务.任务ID,
授权ID,
"lease-" + 调用ID,
datetime.now(UTC) + timedelta(minutes=19),
最少剩余秒=0,
)
def 生成正文步骤() -> tuple[步骤计划, ...]:
return (
步骤计划("预组装", "ctx.prep", "1"),
步骤计划("受限探索", "ctx.explore", "1", 依赖=("预组装",), 角色="writer", 工具=探索工具集),
步骤计划("冻结回放", "ctx.freeze", "1", 依赖=("受限探索",)),
步骤计划("无工具写作", "ctx.write", "1", 依赖=("冻结回放",), 角色="writer"),
步骤计划("检查与候选", "ctx.check", "1", 依赖=("无工具写作",)),
)
def 流程定义_生成正文() -> 流程定义:
return 流程定义(流程身份, 流程版本, 生成正文步骤())
def 发起生成正文(
装配,
身份,
命令ID: str,
粒度: str,
*,
work_id: str,
chapter_id: str,
分支: str = "main",
用途说明: str = "",
预算: dict[str, Any] | None = None,
配置ID: str,
) -> dict[str, str]:
"""冻结写作任务与来源范围后创建生成正文任务;运行配置由作者明确选择。"""
身份 = replace(身份, 内容用途=内容用途.生成)
if 粒度 != "new_chapter":
raise 生成正文错误("扩写、压缩、润色和重写须使用带确切正文与圈选的选段修订入口")
from muse.作品规划.接口 import 读取作品范围, 读取章节归属
with _工厂(装配).连接(只读=True) as 连:
章节 = 读取章节归属(连, 身份.作者, chapter_id)
目录 = 读取作品范围(连, 身份.作者, work_id)
前文 = []
for 行 in 目录["chapters"]:
if 行["position"] >= 章节["position"]:
continue
文稿 = 读取当前正文依据(连, 身份.作者, 行["chapter_id"], 分支=分支)
if 文稿:
前文.append(
{
"chapter_id": 行["chapter_id"],
"revision": 文稿["revision"],
"branch_id": 分支,
}
)
任务合同 = 创建写作任务(
连,
身份,
命令ID,
粒度,
work_id=work_id,
chapter_id=chapter_id,
分支=分支,
用途说明=用途说明,
预算=预算,
)
范围 = 装配.要求上下文().授权任务范围(
身份,
work_id,
int(章节["position"]) - 1,
tuple(前文),
目标章ID=chapter_id,
含事实=True,
含方法=True,
含声音=True,
含偏好=True,
)
冻结上下文 = {
"source_scope": 范围,
"schema_versions": {"context": "w14", "review": "w15"},
"authorization": {
"authorized_by": 范围["authorized_by"],
"run_purpose": 范围["run_purpose"],
"content_purpose": 范围["content_purpose"],
},
"budget": 预算 or {"context_bytes": 65536, "最大输出token": 8192},
"stop_conditions": {"探索轮数上限": 6},
}
策略 = 角色策略目录.从发布包()
请求 = 任务请求(
任务类型=流程身份,
命令ID=命令ID,
作者=身份.作者,
执行用途=装配.配置.运行用途,
内容用途=身份.内容用途,
输入={"写作任务": asdict(任务合同)},
角色策略版本=策略.定义["version"],
资源发布身份=装配.配置.资源发布身份,
冻结上下文=冻结上下文,
作品ID=work_id,
)
任务ID = 装配.任务运行.创建受控任务(请求, 流程身份, 流程版本, 配置ID=配置ID)
return {"task_id": 任务ID, "writing_task_id": 任务合同.任务ID}
__all__ = [
"登记生成正文",
"发起生成正文",
"流程定义_生成正文",
"生成正文步骤",
"生成正文模板",
"流程身份",
"流程版本",
"探索工具集",
"生成正文错误",
"处理器版本集",
]