feat(toolbox): F1 拆书 teardown 落库成项目 rules
- teardown_spec.writes=["rules"](只声明 tier,入库仍经白名单 gate,不变量 #3) - toolbox_registry teardown entry 加 ingest=IngestSpec(table="rules") - routers/toolbox.py 通用 ingest dispatcher 加 _ingest_rules: BookTeardownResult(themes/archetypes/structure/hooks) 拍平为可读 rules 条目, 落项目级(无 continuity 预检,仿 _ingest_outline 无 409) - schemas/toolbox.py 加 TeardownIngestView + ToolIngestRequest.teardown - 单测:teardown ingest→rules 真落行 + 预览(generate)不写库 后端门禁绿:ruff/format/mypy clean · alembic no drift · pytest 644 passed
This commit is contained in:
@@ -24,6 +24,7 @@ from ww_core.domain.chapter_repo import ChapterRepo
|
||||
from ww_core.domain.outline_write_repo import OutlineWriteRepo
|
||||
from ww_core.domain.project_repo import ProjectRepo
|
||||
from ww_core.domain.repositories import MemoryRepos, OutlineRepo
|
||||
from ww_core.domain.rule_repo import RuleWriteRepo
|
||||
from ww_core.domain.world_entity_repo import WorldEntityWriteRepo
|
||||
from ww_core.orchestrator import run_generator
|
||||
from ww_db import get_session
|
||||
@@ -37,6 +38,7 @@ from ww_api.routers.generation import _project_context, _world_context
|
||||
from ww_api.schemas.generation import IngestConflictView, WorldEntityCardView
|
||||
from ww_api.schemas.toolbox import (
|
||||
OutlineSceneIngestView,
|
||||
TeardownIngestView,
|
||||
ToolboxListResponse,
|
||||
ToolDescriptorView,
|
||||
ToolGeneratePreviewResponse,
|
||||
@@ -53,6 +55,7 @@ from ww_api.services.project_deps import (
|
||||
get_outline_read_repo,
|
||||
get_outline_write_repo,
|
||||
get_project_repo,
|
||||
get_rule_write_repo,
|
||||
get_tier_gateway_builder,
|
||||
get_world_entity_write_repo,
|
||||
)
|
||||
@@ -79,6 +82,7 @@ OutlineReadRepoDep = Annotated[OutlineRepo, Depends(get_outline_read_repo)]
|
||||
ChapterRepoDep = Annotated[ChapterRepo, Depends(get_chapter_repo)]
|
||||
WorldWriteRepoDep = Annotated[WorldEntityWriteRepo, Depends(get_world_entity_write_repo)]
|
||||
OutlineWriteRepoDep = Annotated[OutlineWriteRepo, Depends(get_outline_write_repo)]
|
||||
RuleWriteRepoDep = Annotated[RuleWriteRepo, Depends(get_rule_write_repo)]
|
||||
GatewayBuilderDep = Annotated[TierGatewayBuilder, Depends(get_tier_gateway_builder)]
|
||||
SessionDep = Annotated[AsyncSession, Depends(get_session)]
|
||||
|
||||
@@ -316,6 +320,7 @@ async def ingest_with_tool(
|
||||
memory: MemoryReposDep,
|
||||
world_write_repo: WorldWriteRepoDep,
|
||||
outline_write_repo: OutlineWriteRepoDep,
|
||||
rule_write_repo: RuleWriteRepoDep,
|
||||
build_gateway: GatewayBuilderDep,
|
||||
session: SessionDep,
|
||||
) -> ToolIngestResponse:
|
||||
@@ -362,6 +367,16 @@ async def ingest_with_tool(
|
||||
request_id=request_id,
|
||||
tool_key=tool_key,
|
||||
)
|
||||
if table == "rules":
|
||||
return await _ingest_rules(
|
||||
spec,
|
||||
body=body,
|
||||
project_id=project_id,
|
||||
rule_write_repo=rule_write_repo,
|
||||
session=session,
|
||||
request_id=request_id,
|
||||
tool_key=tool_key,
|
||||
)
|
||||
# 描述符的 IngestSpec.table 已限白名单;其余表本期通用入库未实现。
|
||||
raise AppError(ErrorCode.VALIDATION, f"入库表 {table} 暂未支持通用入库", {"table": table})
|
||||
|
||||
@@ -493,3 +508,72 @@ async def _ingest_outline(
|
||||
created_count=len(created),
|
||||
)
|
||||
return ToolIngestResponse(table="outline", created=created, rejected_tables=rejected)
|
||||
|
||||
|
||||
# 拆书入库的 rules 级别(meta 规则非设定卡,落项目级;单用户原型 owner stub)。
|
||||
_TEARDOWN_RULE_LEVEL = "project"
|
||||
|
||||
|
||||
def _flatten_teardown_rules(teardown: TeardownIngestView) -> list[tuple[str, str]]:
|
||||
"""把拆书结论拍平成可读 rules 条目:返回 (label, content) 列表(确定性顺序,跳过空字段)。
|
||||
|
||||
主题/原型/钩子各一条(清单拼成可读文本);结构一条。空清单/空串的字段跳过,不落空规则。
|
||||
"""
|
||||
entries: list[tuple[str, str]] = []
|
||||
if teardown.themes:
|
||||
entries.append(("themes", f"核心主题:{';'.join(teardown.themes)}"))
|
||||
if teardown.archetypes:
|
||||
entries.append(("archetypes", f"人物原型:{';'.join(teardown.archetypes)}"))
|
||||
if teardown.structure.strip():
|
||||
entries.append(("structure", f"叙事结构:{teardown.structure.strip()}"))
|
||||
if teardown.hooks:
|
||||
entries.append(("hooks", f"钩子套路:{';'.join(teardown.hooks)}"))
|
||||
return entries
|
||||
|
||||
|
||||
async def _ingest_rules(
|
||||
spec: AgentSpec,
|
||||
*,
|
||||
body: ToolIngestRequest,
|
||||
project_id: uuid.UUID,
|
||||
rule_write_repo: RuleWriteRepo,
|
||||
session: AsyncSession,
|
||||
request_id: str | None,
|
||||
tool_key: str,
|
||||
) -> ToolIngestResponse:
|
||||
"""rules 入库(拆书):白名单 → 把拆书结论拍平为可读 rules 条目写入项目级 rules。
|
||||
|
||||
meta 规则非设定卡,无需 continuity 预检(仿 outline 无 409 路径);仍过 partition_writes
|
||||
白名单(teardown spec 声明 writes=["rules"])。无 teardown 产物 → VALIDATION。
|
||||
"""
|
||||
if body.teardown is None:
|
||||
raise AppError(ErrorCode.VALIDATION, "拆书入库需提供 teardown 产物", {"tool_key": tool_key})
|
||||
|
||||
entries = _flatten_teardown_rules(body.teardown)
|
||||
|
||||
# gate:权限白名单(teardown spec 声明 writes=["rules"])。
|
||||
allowed, rejected = partition_writes(spec, {"rules": entries})
|
||||
if rejected:
|
||||
log.warning(
|
||||
"toolbox_ingest_rejected_over_permission",
|
||||
tool_key=tool_key,
|
||||
project_id=str(project_id),
|
||||
request_id=request_id,
|
||||
rejected_tables=rejected,
|
||||
)
|
||||
|
||||
created: list[str] = []
|
||||
for label, content in allowed.get("rules", []):
|
||||
await rule_write_repo.create(project_id, level=_TEARDOWN_RULE_LEVEL, content=content)
|
||||
created.append(label)
|
||||
|
||||
await session.commit()
|
||||
log.info(
|
||||
"toolbox_ingest_done",
|
||||
tool_key=tool_key,
|
||||
project_id=str(project_id),
|
||||
request_id=request_id,
|
||||
table="rules",
|
||||
created_count=len(created),
|
||||
)
|
||||
return ToolIngestResponse(table="rules", created=created, rejected_tables=rejected)
|
||||
|
||||
@@ -105,12 +105,22 @@ class OutlineSceneIngestView(BaseModel):
|
||||
hook: str = ""
|
||||
|
||||
|
||||
class TeardownIngestView(BaseModel):
|
||||
"""拆书入库产物(贴 ww_agents.BookTeardownResult;落 rules 表)。F1。"""
|
||||
|
||||
themes: list[str] = Field(default_factory=list, description="核心主题/立意清单")
|
||||
archetypes: list[str] = Field(default_factory=list, description="人物原型/角色模板清单")
|
||||
structure: str = Field(default="", description="叙事结构概述")
|
||||
hooks: list[str] = Field(default_factory=list, description="抓人钩子/爽点套路清单")
|
||||
|
||||
|
||||
class ToolIngestRequest(BaseModel):
|
||||
"""通用入库请求:待持久化的产物 + 冲突确认。
|
||||
|
||||
仅声明了 `ingest` 的工具可用。按入库表取用对应字段:
|
||||
- world_entities(金手指/词条)→ `world_entities`(贴 WorldEntityCardView:type/name/rules);
|
||||
- outline(细纲)→ `chapter_no` + `scenes`(贴 OutlineSceneIngestView)。
|
||||
- outline(细纲)→ `chapter_no` + `scenes`(贴 OutlineSceneIngestView);
|
||||
- rules(拆书)→ `teardown`(贴 TeardownIngestView:themes/archetypes/structure/hooks)。
|
||||
`acknowledge_conflicts`:作者已查看并接受 continuity 预检冲突时置 true 放行(仿 accept gate)。
|
||||
"""
|
||||
|
||||
@@ -123,6 +133,9 @@ class ToolIngestRequest(BaseModel):
|
||||
scenes: list[OutlineSceneIngestView] = Field(
|
||||
default_factory=list, description="outline 入库的场景行(细纲)"
|
||||
)
|
||||
teardown: TeardownIngestView | None = Field(
|
||||
default=None, description="rules 入库的拆书结论(F1;拍平为可读规则条目)"
|
||||
)
|
||||
acknowledge_conflicts: bool = Field(
|
||||
default=False, description="作者已知悉并接受 continuity 冲突 → 放行入库"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user