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 / 打包冒烟)
This commit is contained in:
@@ -27,8 +27,8 @@ def _spec(*, reads: list[str], writes: list[str], scope: str = "custom") -> Agen
|
||||
name="custom_skill",
|
||||
tier="writer",
|
||||
system_prompt="x",
|
||||
reads=reads,
|
||||
writes=writes,
|
||||
reads=tuple(reads),
|
||||
writes=tuple(writes),
|
||||
scope=scope,
|
||||
)
|
||||
|
||||
|
||||
@@ -20,16 +20,16 @@ def _record(
|
||||
name: str,
|
||||
*,
|
||||
scope: str = "custom",
|
||||
reads: list[str] | None = None,
|
||||
writes: list[str] | None = None,
|
||||
reads: tuple[str, ...] = (),
|
||||
writes: tuple[str, ...] = (),
|
||||
) -> SkillRecord:
|
||||
return SkillRecord(
|
||||
name=name,
|
||||
scope=scope,
|
||||
tier="writer",
|
||||
system_prompt=f"prompt for {name}",
|
||||
reads=reads or [],
|
||||
writes=writes or [],
|
||||
reads=reads,
|
||||
writes=writes,
|
||||
genre=None,
|
||||
)
|
||||
|
||||
@@ -49,10 +49,10 @@ async def test_load_builds_specs_keyed_by_name() -> None:
|
||||
_record(
|
||||
"worldgen",
|
||||
scope="builtin",
|
||||
reads=["world_entities"],
|
||||
writes=["world_entities"],
|
||||
reads=("world_entities",),
|
||||
writes=("world_entities",),
|
||||
),
|
||||
_record("cpgen", scope="custom", reads=["characters"]),
|
||||
_record("cpgen", scope="custom", reads=("characters",)),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -62,7 +62,7 @@ async def test_load_builds_specs_keyed_by_name() -> None:
|
||||
spec = registry.get("worldgen")
|
||||
assert isinstance(spec, AgentSpec)
|
||||
assert spec.tier == "writer"
|
||||
assert spec.reads == ["world_entities"]
|
||||
assert spec.reads == ("world_entities",)
|
||||
assert spec.scope == "builtin"
|
||||
|
||||
|
||||
@@ -95,9 +95,54 @@ async def test_list_scope_filters_by_scope() -> None:
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_rejects_over_permission_skill() -> None:
|
||||
# 越权声明(reads 指向未知表)→ 加载即拒绝(守 §5.6),不静默入册。
|
||||
repo = _FakeSkillRepo([_record("evil", reads=["secret_table"])])
|
||||
repo = _FakeSkillRepo([_record("evil", reads=("secret_table",))])
|
||||
|
||||
with pytest.raises(AppError) as exc:
|
||||
await SkillRegistry.load(repo)
|
||||
|
||||
assert exc.value.code is ErrorCode.VALIDATION
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_rejects_skill_colliding_with_reserved_review_name() -> None:
|
||||
# 用户 skill 偷用四审保留名(continuity)→ 入库即拒(守卫前移,不变量 #3)。
|
||||
repo = _FakeSkillRepo([_record("continuity", reads=("chapter_digests",))])
|
||||
|
||||
with pytest.raises(AppError) as exc:
|
||||
await SkillRegistry.load(repo)
|
||||
|
||||
assert exc.value.code is ErrorCode.VALIDATION
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_rejects_skill_colliding_with_builtin_name() -> None:
|
||||
# 用户 skill 冒用非四审的内置 name(brainstorm)→ 入库即拒。
|
||||
repo = _FakeSkillRepo([_record("brainstorm", reads=("projects",))])
|
||||
|
||||
with pytest.raises(AppError) as exc:
|
||||
await SkillRegistry.load(repo)
|
||||
|
||||
assert exc.value.code is ErrorCode.VALIDATION
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolver_build_does_not_raise_on_reserved_collision() -> None:
|
||||
# 守卫在入库期(load),resolver.build 纯合并不校验——不得因同名抛错。
|
||||
from ww_skills import SpecResolver
|
||||
|
||||
empty = await SkillRegistry.load(_FakeSkillRepo([]))
|
||||
|
||||
resolver = SpecResolver.build(empty) # 不抛
|
||||
|
||||
assert resolver.get("continuity") is not None
|
||||
|
||||
|
||||
def test_skill_record_reads_writes_are_immutable_tuples() -> None:
|
||||
# frozen 只防整字段重绑,不防 list 原地变异——reads/writes 必须是 tuple(无 append)。
|
||||
record = _record("immut", reads=("characters",), writes=("world_entities",))
|
||||
|
||||
assert isinstance(record.reads, tuple)
|
||||
assert isinstance(record.writes, tuple)
|
||||
assert record.reads == ("characters",)
|
||||
assert record.writes == ("world_entities",)
|
||||
assert not hasattr(record.reads, "append")
|
||||
|
||||
149
packages/skills/tests/test_spec_resolver.py
Normal file
149
packages/skills/tests/test_spec_resolver.py
Normal file
@@ -0,0 +1,149 @@
|
||||
"""SpecResolver 单测(Prompt 外置方案A · 步4)。
|
||||
|
||||
`SpecResolver` 统一内置(`SPECS`,纯内存、零 DB)与用户 skill(`SkillRegistry`,DB)
|
||||
的只读解析入口。核心约束(不变量 #3 / 评审 HIGH):
|
||||
|
||||
- `get(name)` 先查内置 `SPECS`(纯内存),命中内置 name **绝不触发任何 DB/registry 调用**;
|
||||
- 未命中内置才查 `SkillRegistry`;都无 → `AppError(NOT_FOUND)`;
|
||||
- `output_schema_for`:内置 → `SCHEMA_CATALOG[name]` 真类;纯用户 skill → None;
|
||||
name 精确字符串相等(无大小写/连字符归一),拼错近似 name → None 不报错;
|
||||
- `resolver.build` 纯合并,**不**做保留名冲突校验(守卫前移至 SkillRegistry 入库)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from ww_agents import (
|
||||
SCHEMA_CATALOG,
|
||||
SPECS,
|
||||
AgentSpec,
|
||||
)
|
||||
from ww_shared import AppError, ErrorCode
|
||||
from ww_skills import SpecResolver
|
||||
|
||||
|
||||
class _FakeRegistry:
|
||||
"""断言用 fake:记录 get/names 是否被调用(内置 get 必须零触达)。"""
|
||||
|
||||
def __init__(self, specs: dict[str, AgentSpec] | None = None) -> None:
|
||||
self._specs = dict(specs or {})
|
||||
self.get_calls: list[str] = []
|
||||
self.names_calls: int = 0
|
||||
|
||||
def get(self, name: str) -> AgentSpec:
|
||||
self.get_calls.append(name)
|
||||
spec = self._specs.get(name)
|
||||
if spec is None:
|
||||
raise AppError(ErrorCode.NOT_FOUND, f"skill not found: {name}")
|
||||
return spec
|
||||
|
||||
def names(self) -> list[str]:
|
||||
self.names_calls += 1
|
||||
return sorted(self._specs)
|
||||
|
||||
def list_scope(self, scope: str) -> list[AgentSpec]:
|
||||
return [s for s in sorted(self._specs.values(), key=lambda s: s.name) if s.scope == scope]
|
||||
|
||||
|
||||
def _user_spec(name: str, scope: str = "custom") -> AgentSpec:
|
||||
return AgentSpec(
|
||||
name=name,
|
||||
tier="writer",
|
||||
system_prompt=f"prompt for {name}",
|
||||
input_schema=None,
|
||||
output_schema=None,
|
||||
reads=(),
|
||||
writes=(),
|
||||
scope=scope,
|
||||
)
|
||||
|
||||
|
||||
# ---- #7 resolver 内置/用户对齐 + 零 DB ----
|
||||
|
||||
|
||||
def test_get_builtin_returns_specs_instance_without_touching_registry() -> None:
|
||||
# Arrange:fake registry 本身有同名条目也无所谓——内置命中必须零触达。
|
||||
fake = _FakeRegistry({"continuity": _user_spec("continuity")})
|
||||
resolver = SpecResolver.build(fake)
|
||||
|
||||
# Act
|
||||
spec = resolver.get("continuity")
|
||||
|
||||
# Assert:拿到的是内置同一实例,且 registry.get 从未被调用(零 DB)。
|
||||
assert spec is SPECS["continuity"]
|
||||
assert fake.get_calls == []
|
||||
|
||||
|
||||
def test_get_user_skill_falls_through_to_registry() -> None:
|
||||
fake = _FakeRegistry({"my-skill": _user_spec("my-skill")})
|
||||
resolver = SpecResolver.build(fake)
|
||||
|
||||
spec = resolver.get("my-skill")
|
||||
|
||||
assert spec.name == "my-skill"
|
||||
assert fake.get_calls == ["my-skill"]
|
||||
|
||||
|
||||
def test_get_unknown_everywhere_raises_not_found() -> None:
|
||||
fake = _FakeRegistry()
|
||||
resolver = SpecResolver.build(fake)
|
||||
|
||||
with pytest.raises(AppError) as exc:
|
||||
resolver.get("nope-not-here")
|
||||
|
||||
assert exc.value.code is ErrorCode.NOT_FOUND
|
||||
assert fake.get_calls == ["nope-not-here"]
|
||||
|
||||
|
||||
def test_names_merges_builtin_and_user() -> None:
|
||||
fake = _FakeRegistry({"my-skill": _user_spec("my-skill")})
|
||||
resolver = SpecResolver.build(fake)
|
||||
|
||||
names = resolver.names()
|
||||
|
||||
assert "continuity" in names
|
||||
assert "my-skill" in names
|
||||
|
||||
|
||||
def test_list_scope_filters_builtin_and_user() -> None:
|
||||
fake = _FakeRegistry({"my-skill": _user_spec("my-skill", scope="custom")})
|
||||
resolver = SpecResolver.build(fake)
|
||||
|
||||
builtin = resolver.list_scope("builtin")
|
||||
custom = resolver.list_scope("custom")
|
||||
|
||||
assert all(s.scope == "builtin" for s in builtin)
|
||||
assert {s.name for s in custom} == {"my-skill"}
|
||||
|
||||
|
||||
# ---- #9 output_schema_for ----
|
||||
|
||||
|
||||
def test_output_schema_for_builtin_returns_real_type() -> None:
|
||||
fake = _FakeRegistry()
|
||||
resolver = SpecResolver.build(fake)
|
||||
|
||||
assert resolver.output_schema_for("continuity") is SCHEMA_CATALOG["continuity"]
|
||||
|
||||
|
||||
def test_output_schema_for_refiner_is_none() -> None:
|
||||
fake = _FakeRegistry()
|
||||
resolver = SpecResolver.build(fake)
|
||||
|
||||
assert resolver.output_schema_for("refiner") is None
|
||||
|
||||
|
||||
def test_output_schema_for_pure_user_skill_is_none() -> None:
|
||||
fake = _FakeRegistry({"my-skill": _user_spec("my-skill")})
|
||||
resolver = SpecResolver.build(fake)
|
||||
|
||||
assert resolver.output_schema_for("my-skill") is None
|
||||
|
||||
|
||||
def test_output_schema_for_misspelled_name_is_none_no_fuzzy_match() -> None:
|
||||
# 拼错近似 name(下划线)不得误命中连字符内置 "character-gen"(精确字符串相等)。
|
||||
fake = _FakeRegistry()
|
||||
resolver = SpecResolver.build(fake)
|
||||
|
||||
assert resolver.output_schema_for("character_gen") is None
|
||||
assert resolver.output_schema_for("character-gen") is not None
|
||||
@@ -37,8 +37,8 @@ def _spec() -> AgentSpec:
|
||||
tier="light",
|
||||
system_prompt="x",
|
||||
output_schema=_FakeOut,
|
||||
reads=["projects"],
|
||||
writes=[],
|
||||
reads=("projects",),
|
||||
writes=(),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ww_agents import SPECS
|
||||
from ww_skills import TOOLBOX, get_tool
|
||||
|
||||
_LEGACY_KEYS = {"worldbuilding", "character", "outline"}
|
||||
@@ -88,6 +89,18 @@ def test_every_tool_has_brief_field_where_applicable() -> None:
|
||||
assert "brief" in field_names, key
|
||||
|
||||
|
||||
def test_new_tools_spec_is_registry_instance() -> None:
|
||||
# 桥接后:新工具 spec 经 SPECS[key] 解析,是注册表同一实例(非直接 import)。
|
||||
for key in _NEW_KEYS:
|
||||
tool = TOOLBOX[key]
|
||||
assert tool.spec is SPECS[key], key
|
||||
|
||||
|
||||
def test_legacy_tools_spec_is_none_unchanged() -> None:
|
||||
for key in _LEGACY_KEYS:
|
||||
assert TOOLBOX[key].spec is None, key
|
||||
|
||||
|
||||
def test_get_tool_resolves_known_and_unknown() -> None:
|
||||
assert get_tool("brainstorm") is TOOLBOX["brainstorm"]
|
||||
assert get_tool("worldbuilding") is TOOLBOX["worldbuilding"]
|
||||
|
||||
@@ -22,6 +22,7 @@ from ww_skills.skill_registry import (
|
||||
SkillRepo,
|
||||
SqlSkillRepo,
|
||||
)
|
||||
from ww_skills.spec_resolver import SpecResolver
|
||||
from ww_skills.toolbox import (
|
||||
ContextStrategy,
|
||||
GeneratorTool,
|
||||
@@ -38,6 +39,7 @@ __all__ = [
|
||||
"SkillRecord",
|
||||
"SkillRegistry",
|
||||
"SkillRepo",
|
||||
"SpecResolver",
|
||||
"SqlSkillRepo",
|
||||
"ContextStrategy",
|
||||
"GeneratorTool",
|
||||
|
||||
@@ -18,7 +18,7 @@ from typing import Protocol, cast, get_args
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from ww_agents import AgentSpec
|
||||
from ww_agents import REVIEW_RESERVED_NAMES, SPECS, AgentSpec
|
||||
from ww_db.models import Skill
|
||||
from ww_llm_gateway.types import Tier
|
||||
from ww_shared import AppError, ErrorCode
|
||||
@@ -27,6 +27,20 @@ from ww_skills.skill_permissions import validate_declaration
|
||||
|
||||
_VALID_TIERS: frozenset[str] = frozenset(get_args(Tier))
|
||||
|
||||
# 内置保留命名空间:四审受信名(REVIEW_RESERVED_NAMES)∪ 全部内置 SPECS name。
|
||||
# 用户 skill 不可冒用这些 name(守卫前移在入库期,呼应不变量 #3:四审 prompt 不被偷换)。
|
||||
_RESERVED_NAMES: frozenset[str] = REVIEW_RESERVED_NAMES | frozenset(SPECS)
|
||||
|
||||
|
||||
def _reject_reserved_name(spec: AgentSpec) -> None:
|
||||
"""用户 skill 冒用内置/保留名 → AppError(VALIDATION)(不可信声明的第一道闸)。"""
|
||||
if spec.name in _RESERVED_NAMES:
|
||||
raise AppError(
|
||||
ErrorCode.VALIDATION,
|
||||
f"Skill「{spec.name}」与内置保留名冲突,不可覆盖",
|
||||
{"skill": spec.name},
|
||||
)
|
||||
|
||||
|
||||
class SkillRecord(BaseModel):
|
||||
"""`skills` 表一行的声明式快照(frozen;snake_case)。
|
||||
@@ -42,8 +56,10 @@ class SkillRecord(BaseModel):
|
||||
scope: str # builtin / custom / community
|
||||
tier: Tier
|
||||
system_prompt: str
|
||||
reads: list[str] = []
|
||||
writes: list[str] = []
|
||||
# 不可变序列:frozen 只防整字段重绑,不防 list 原地变异(record.reads.append 可旁路);
|
||||
# tuple 无 append/clear,对齐 AgentSpec 同字段语义。Pydantic v2 coerce list→tuple。
|
||||
reads: tuple[str, ...] = ()
|
||||
writes: tuple[str, ...] = ()
|
||||
genre: str | None = None
|
||||
|
||||
|
||||
@@ -54,8 +70,8 @@ def _to_spec(record: SkillRecord) -> AgentSpec:
|
||||
system_prompt=record.system_prompt,
|
||||
input_schema=None,
|
||||
output_schema=None,
|
||||
reads=list(record.reads),
|
||||
writes=list(record.writes),
|
||||
reads=record.reads,
|
||||
writes=record.writes,
|
||||
genre=record.genre,
|
||||
scope=record.scope,
|
||||
)
|
||||
@@ -67,6 +83,20 @@ class SkillRepo(Protocol):
|
||||
async def list_all(self) -> list[SkillRecord]: ...
|
||||
|
||||
|
||||
class SkillRegistryLike(Protocol):
|
||||
"""`SpecResolver` 消费的 registry 只读契约(结构子集;测试注 duck-type fake)。
|
||||
|
||||
SpecResolver 只用 `.get` / `.names` / `.list_scope` 三个方法——以 Protocol 精确捕获该
|
||||
契约,避免依赖具体 `SkillRegistry` 类逼测试 fake 加 `# type: ignore`。
|
||||
"""
|
||||
|
||||
def get(self, name: str) -> AgentSpec: ...
|
||||
|
||||
def names(self) -> list[str]: ...
|
||||
|
||||
def list_scope(self, scope: str) -> list[AgentSpec]: ...
|
||||
|
||||
|
||||
class SkillRegistry:
|
||||
"""加载后的只读 skill 注册表(name → AgentSpec)。"""
|
||||
|
||||
@@ -82,6 +112,7 @@ class SkillRegistry:
|
||||
specs: dict[str, AgentSpec] = {}
|
||||
for record in await repo.list_all():
|
||||
spec = _to_spec(record)
|
||||
_reject_reserved_name(spec) # 冒用内置/保留名 → 抛 VALIDATION
|
||||
validate_declaration(spec) # 越权 → 抛 VALIDATION
|
||||
specs[spec.name] = spec
|
||||
return cls(specs)
|
||||
@@ -114,8 +145,8 @@ def _row_to_record(row: Skill) -> SkillRecord:
|
||||
scope=row.scope,
|
||||
tier=cast(Tier, row.tier),
|
||||
system_prompt=row.system_prompt,
|
||||
reads=[str(t) for t in (row.reads or [])],
|
||||
writes=[str(t) for t in (row.writes or [])],
|
||||
reads=tuple(str(t) for t in (row.reads or [])),
|
||||
writes=tuple(str(t) for t in (row.writes or [])),
|
||||
genre=row.genre,
|
||||
)
|
||||
|
||||
|
||||
66
packages/skills/ww_skills/spec_resolver.py
Normal file
66
packages/skills/ww_skills/spec_resolver.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""`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,纯内存)+ 用户 skill(SkillRegistry,DB)的统一只读解析入口。"""
|
||||
|
||||
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)
|
||||
@@ -16,20 +16,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ww_agents import (
|
||||
blurb_spec,
|
||||
book_title_spec,
|
||||
brainstorm_spec,
|
||||
continue_spec,
|
||||
de_ai_spec,
|
||||
expand_spec,
|
||||
fine_outline_spec,
|
||||
glossary_spec,
|
||||
golden_finger_spec,
|
||||
name_spec,
|
||||
opening_spec,
|
||||
teardown_spec,
|
||||
)
|
||||
from ww_agents import SPECS
|
||||
from ww_agents.schemas import (
|
||||
BlurbResult,
|
||||
BookTeardownResult,
|
||||
@@ -117,7 +104,7 @@ TOOLBOX: dict[str, GeneratorTool] = {
|
||||
key="brainstorm",
|
||||
title="脑洞生成器",
|
||||
subtitle="突破想象,脑洞大开",
|
||||
spec=brainstorm_spec,
|
||||
spec=SPECS["brainstorm"],
|
||||
output_schema=IdeaListResult,
|
||||
context_strategy="brief_only",
|
||||
input_fields=[_BRIEF_FIELD],
|
||||
@@ -126,7 +113,7 @@ TOOLBOX: dict[str, GeneratorTool] = {
|
||||
key="book-title",
|
||||
title="书名生成器",
|
||||
subtitle="一秒生成抓人书名",
|
||||
spec=book_title_spec,
|
||||
spec=SPECS["book-title"],
|
||||
output_schema=TitleListResult,
|
||||
context_strategy="brief_only",
|
||||
input_fields=[_BRIEF_FIELD],
|
||||
@@ -135,7 +122,7 @@ TOOLBOX: dict[str, GeneratorTool] = {
|
||||
key="blurb",
|
||||
title="简介生成器",
|
||||
subtitle="多版差异化书页文案",
|
||||
spec=blurb_spec,
|
||||
spec=SPECS["blurb"],
|
||||
output_schema=BlurbResult,
|
||||
context_strategy="with_project",
|
||||
input_fields=[_BRIEF_FIELD],
|
||||
@@ -144,7 +131,7 @@ TOOLBOX: dict[str, GeneratorTool] = {
|
||||
key="name",
|
||||
title="名字生成器",
|
||||
subtitle="契合世界观的人/物/地命名",
|
||||
spec=name_spec,
|
||||
spec=SPECS["name"],
|
||||
output_schema=NameListResult,
|
||||
context_strategy="with_world",
|
||||
input_fields=[
|
||||
@@ -162,7 +149,7 @@ TOOLBOX: dict[str, GeneratorTool] = {
|
||||
key="golden-finger",
|
||||
title="金手指生成器",
|
||||
subtitle="自洽机制 + 成长 + 限制代价",
|
||||
spec=golden_finger_spec,
|
||||
spec=SPECS["golden-finger"],
|
||||
output_schema=GoldenFingerResult,
|
||||
context_strategy="with_world",
|
||||
input_fields=[_BRIEF_FIELD],
|
||||
@@ -172,7 +159,7 @@ TOOLBOX: dict[str, GeneratorTool] = {
|
||||
key="glossary",
|
||||
title="词条生成器",
|
||||
subtitle="带硬规则的世界观术语表",
|
||||
spec=glossary_spec,
|
||||
spec=SPECS["glossary"],
|
||||
output_schema=GlossaryResult,
|
||||
context_strategy="with_world",
|
||||
input_fields=[_BRIEF_FIELD],
|
||||
@@ -182,7 +169,7 @@ TOOLBOX: dict[str, GeneratorTool] = {
|
||||
key="opening",
|
||||
title="黄金开篇生成器",
|
||||
subtitle="多版高代入感开篇正文",
|
||||
spec=opening_spec,
|
||||
spec=SPECS["opening"],
|
||||
output_schema=OpeningResult,
|
||||
context_strategy="with_outline_chapter",
|
||||
input_fields=[_chapter_no_field(), _BRIEF_FIELD],
|
||||
@@ -191,7 +178,7 @@ TOOLBOX: dict[str, GeneratorTool] = {
|
||||
key="fine-outline",
|
||||
title="细纲生成器",
|
||||
subtitle="把章节粗节拍展开为场景序列",
|
||||
spec=fine_outline_spec,
|
||||
spec=SPECS["fine-outline"],
|
||||
output_schema=DetailedOutlineResult,
|
||||
context_strategy="with_outline_chapter",
|
||||
input_fields=[_chapter_no_field(), _BRIEF_FIELD],
|
||||
@@ -202,7 +189,7 @@ TOOLBOX: dict[str, GeneratorTool] = {
|
||||
key="continue",
|
||||
title="续写生成器",
|
||||
subtitle="承接前文,无缝续写下文",
|
||||
spec=continue_spec,
|
||||
spec=SPECS["continue"],
|
||||
output_schema=ContinuationResult,
|
||||
context_strategy="with_prior_chapter",
|
||||
input_fields=[
|
||||
@@ -220,7 +207,7 @@ TOOLBOX: dict[str, GeneratorTool] = {
|
||||
key="expand",
|
||||
title="扩写生成器",
|
||||
subtitle="在原文基础上丰富细节铺陈",
|
||||
spec=expand_spec,
|
||||
spec=SPECS["expand"],
|
||||
output_schema=PolishResult,
|
||||
context_strategy="text_input",
|
||||
input_fields=[_SOURCE_TEXT_FIELD, _BRIEF_FIELD],
|
||||
@@ -229,7 +216,7 @@ TOOLBOX: dict[str, GeneratorTool] = {
|
||||
key="de-ai",
|
||||
title="降 AI 率生成器",
|
||||
subtitle="去机翻腔,更自然的人写质感",
|
||||
spec=de_ai_spec,
|
||||
spec=SPECS["de-ai"],
|
||||
output_schema=DeAiResult,
|
||||
context_strategy="text_input",
|
||||
input_fields=[_SOURCE_TEXT_FIELD, _BRIEF_FIELD],
|
||||
@@ -238,7 +225,7 @@ TOOLBOX: dict[str, GeneratorTool] = {
|
||||
key="teardown",
|
||||
title="拆书生成器",
|
||||
subtitle="拆解主题/原型/结构/钩子套路",
|
||||
spec=teardown_spec,
|
||||
spec=SPECS["teardown"],
|
||||
output_schema=BookTeardownResult,
|
||||
context_strategy="text_input",
|
||||
input_fields=[
|
||||
|
||||
Reference in New Issue
Block a user