Files
writer-work-flow/packages/skills/ww_skills/spec_resolver.py
Yaojia Wang dca4d45d4e refactor(agents): prompt 外置方案A — 21 prompt 散文外迁 .md + SpecResolver
把 21 个内置 agent 的 system_prompt 从 specs.py 的 Python 常量外置为
prompts/<spec.name>.md,import 期由 load_prompt 确定性加载;建立 SPECS 名册
+ SCHEMA_CATALOG(Pydantic 类型留 Python)+ 统一只读解析入口 SpecResolver。
纯重构、零功能/schema 变更,缓存断点前块字节级不变(不变量 #9)。

@llm packages/agents(步骤1-3)
- spec_model.py:抽出 AgentSpec(frozen,字段不变)
- prompt_loader.py:load_prompt = utf-8-sig 去BOM → LF 归一 → NFC → rstrip尾LF,
  内存缓存 + fail-fast(PromptNotFoundError),import 期确定性
- schema_catalog.py:SCHEMA_CATALOG[name]→output type 唯一真相源(refiner=None)
- prompts/*.md ×21:取常量「运行时值」程序化外迁(反斜杠折行已塌缩,
  物理换行≡运行时换行);文件名按 spec.name 连字符(style.md/character-gen.md 等)
- specs.py:删 21 常量 + AgentSpec 类;system_prompt=load_prompt(name)、
  output_schema=SCHEMA_CATALOG[name];建 SPECS + REVIEW_RESERVED_NAMES;
  *_spec 兼容期保留且 SPECS[name] is *_spec(同一实例)。804→337 行
- __init__.py:显式 __all__ 重导出(避 F401)

@backend packages/skills(步骤4-5)
- SpecResolver:内置 SPECS(纯内存、零 DB)+ 用户 SkillRegistry 统一 get;
  内置 name 永不触发 DB;output_schema_for 精确匹配
- skill_registry:保留命名空间守卫前移至入库校验,拒同名内置 → VALIDATION
- toolbox_registry:GeneratorTool.spec 改走 SPECS[...],删 12 个 *_spec 直接 import

@devops repo-root
- .gitattributes:prompts/*.md text eol=lf(修正:须用完整嵌套路径才匹配)
- packages/agents/pyproject:hatchling artifacts 纳入 prompts/*.md 随 wheel/sdist 分发
- ci.yml:新增 build wheel → 裸装 → import ww_agents.SPECS 冒烟

TDD 全程 mock 网关;门禁绿:ruff/format clean · mypy 209 files · pytest 744 passed
(含金标准 sha256 回归 / md↔spec↔catalog 一一对应 / fail-fast / BOM+NFC /
内置守卫 / 同一实例 / 编排器无回归 / apps/api import-smoke / 打包冒烟)
2026-06-24 04:49:44 +02:00

67 lines
3.0 KiB
Python
Raw Permalink 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.

"""`SpecResolver`:内置 + 用户 skill 的统一只读解析入口Prompt 外置方案A · 步4
内置 Agent`SPECS`,纯内存)与用户 Skill`SkillRegistry`DB二者都产出 `AgentSpec`。
resolver 提供统一 `.get(name)`
- **内置优先、零 DB**:先查内置 `SPECS`(纯内存查表);命中内置 name 时**绝不触发**任何
`SkillRegistry` 调用(评审 HIGH / 不变量 #3四审 prompt 不被用户 skill 偷换的安全边界,
其守卫前移在 `SkillRegistry` 入库期,故读路径对内置永远是确定性纯内存)。
- 未命中内置才查 `SkillRegistry`;都无 → `AppError(NOT_FOUND)`。
`build` 是**纯合并**,不做保留名冲突校验(守卫前移至 `SkillRegistry.load`)。
`output_schema_for`:内置 → `SCHEMA_CATALOG[name]` 真类;纯用户 skill → None。
name 语义锁死为**精确字符串相等**(无大小写折叠、无连字符归一、无模糊匹配)。
不可变:`build` 用 `dict(SPECS)` 复制一份内置快照resolver 不改入参。
"""
from __future__ import annotations
from pydantic import BaseModel
from ww_agents import SCHEMA_CATALOG, SPECS, AgentSpec
from ww_skills.skill_registry import SkillRegistryLike
class SpecResolver:
"""内置SPECS纯内存+ 用户 skillSkillRegistryDB的统一只读解析入口。"""
def __init__(self, builtin: dict[str, AgentSpec], skills: SkillRegistryLike) -> None:
self._builtin = builtin
self._skills = skills
@classmethod
def build(cls, skills: SkillRegistryLike) -> SpecResolver:
"""纯合并:拷贝内置 SPECS 快照 + 持有 SkillRegistry不做冲突校验守卫已前移"""
return cls(builtin=dict(SPECS), skills=skills)
def get(self, name: str) -> AgentSpec:
"""先查内置(纯内存、零 DB未命中才查 SkillRegistry都无 → NOT_FOUND。
命中内置 name 时**绝不触发** SkillRegistry 调用(确定性 + 安全边界)。
"""
builtin = self._builtin.get(name)
if builtin is not None:
return builtin
return self._skills.get(name)
def output_schema_for(self, name: str) -> type[BaseModel] | None:
"""命中内置 → SCHEMA_CATALOG[name];纯用户 skill / 未知 → None。
精确字符串相等命中——拼错近似 name如 `character_gen` vs `character-gen`)→ None
不报错(预期行为,非 bug
"""
if name in self._builtin:
return SCHEMA_CATALOG[name]
return None
def names(self) -> list[str]:
"""内置 + 用户 skill 的全部 name去重、排序"""
return sorted(set(self._builtin) | set(self._skills.names()))
def list_scope(self, scope: str) -> list[AgentSpec]:
"""按 scope 过滤内置 + 用户 skill按 name 排序。"""
merged = [s for s in self._builtin.values() if s.scope == scope]
merged.extend(self._skills.list_scope(scope))
return sorted(merged, key=lambda s: s.name)