feat(skills): Scope B A2 — 4 竞品生成器注册表+上下文分派+端点接线
注册表 TOOLBOX +4 entry(continue/expand/de-ai/teardown,全 preview-only writes=[]); ContextStrategy +with_prior_chapter/text_input;ToolGenerateRequest +text 字段。 toolbox_context 分派新策略(续写→build_continuation_context、原文→build_text_input_context); 路由续写经 chapter_repo 读最新 accepted/draft 正文注入 builder(core 不 import apps/api,仿 accept_op)。 预览仅 commit ledger 不写业务表(#3);只声明 tier(#2);system_prompt 进缓存前缀(#9)。 单测:4 生成器 generate 预览 + 续写读前文/草稿路由 + 扩写/降AI text 路由 + 拆书结构化; 更新 registry/strategy 计数测试(15 工具 / 6 策略)。门禁绿:ruff/format/mypy 196 files/pytest 168(api)+32(skills)。
This commit is contained in:
@@ -20,6 +20,7 @@ from typing import Annotated, Any
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from ww_agents import AgentSpec, Conflict, ContinuityReview, continuity_spec
|
||||
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
|
||||
@@ -47,6 +48,7 @@ from ww_api.schemas.toolbox import (
|
||||
from ww_api.services.credentials import STUB_OWNER_ID
|
||||
from ww_api.services.project_deps import (
|
||||
TierGatewayBuilder,
|
||||
get_chapter_repo,
|
||||
get_memory_repos,
|
||||
get_outline_read_repo,
|
||||
get_outline_write_repo,
|
||||
@@ -74,6 +76,7 @@ _INGEST_ERRORS: dict[int | str, dict[str, Any]] = {
|
||||
ProjectRepoDep = Annotated[ProjectRepo, Depends(get_project_repo)]
|
||||
MemoryReposDep = Annotated[MemoryRepos, Depends(get_memory_repos)]
|
||||
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)]
|
||||
GatewayBuilderDep = Annotated[TierGatewayBuilder, Depends(get_tier_gateway_builder)]
|
||||
@@ -138,6 +141,23 @@ async def _chapter_beats(
|
||||
return list(raw.get("beats", [])) if isinstance(raw, dict) else []
|
||||
|
||||
|
||||
async def _prior_chapter_text(
|
||||
chapter_repo: ChapterRepo, project_id: uuid.UUID, chapter_no: int
|
||||
) -> str:
|
||||
"""读续写承接的前文正文:优先该章最新 accepted,无则草稿,皆无 → 空串(端点降级)。
|
||||
|
||||
core 的 builder 不 import apps/api——正文在端点读 chapter 领域表后经参数注入
|
||||
(仿链 accept_op 的注入模式),守不变量 #1(DB 为真相源)。
|
||||
"""
|
||||
accepted = await chapter_repo.latest_accepted(project_id, chapter_no)
|
||||
if accepted is not None and accepted.content.strip():
|
||||
return accepted.content
|
||||
draft = await chapter_repo.get_draft(project_id, chapter_no)
|
||||
if draft is not None and draft.content.strip():
|
||||
return draft.content
|
||||
return ""
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/skills/{tool_key}/generate", responses=_TOOL_ERRORS)
|
||||
async def generate_with_tool(
|
||||
project_id: uuid.UUID,
|
||||
@@ -147,6 +167,7 @@ async def generate_with_tool(
|
||||
project_repo: ProjectRepoDep,
|
||||
memory: MemoryReposDep,
|
||||
outline_repo: OutlineReadRepoDep,
|
||||
chapter_repo: ChapterRepoDep,
|
||||
build_gateway: GatewayBuilderDep,
|
||||
session: SessionDep,
|
||||
) -> ToolGeneratePreviewResponse:
|
||||
@@ -170,11 +191,17 @@ async def generate_with_tool(
|
||||
|
||||
world_context = ""
|
||||
beats: list[str] = []
|
||||
prior_text = ""
|
||||
if tool.context_strategy == "with_world":
|
||||
world_context = await _world_context(memory, project_id)
|
||||
elif tool.context_strategy == "with_outline_chapter":
|
||||
chapter_no = body.chapter_no or 1
|
||||
beats = await _chapter_beats(outline_repo, project_id, chapter_no)
|
||||
elif tool.context_strategy == "with_prior_chapter":
|
||||
# 续写:端点先读该章最新 accepted/draft 正文(DB 为真相源,#1)+ 该章节拍,注入 builder。
|
||||
chapter_no = body.chapter_no or 1
|
||||
prior_text = await _prior_chapter_text(chapter_repo, project_id, chapter_no)
|
||||
beats = await _chapter_beats(outline_repo, project_id, chapter_no)
|
||||
|
||||
context = build_toolbox_context(
|
||||
tool.context_strategy,
|
||||
@@ -183,6 +210,8 @@ async def generate_with_tool(
|
||||
world_context=world_context,
|
||||
chapter_no=body.chapter_no,
|
||||
beats=beats,
|
||||
prior_text=prior_text,
|
||||
text=body.text or "",
|
||||
)
|
||||
|
||||
gateway = await build_gateway(spec.tier)
|
||||
|
||||
@@ -64,8 +64,14 @@ class ToolGenerateRequest(BaseModel):
|
||||
"""通用生成请求(覆盖全部工具的可选入参;按工具 input_fields 取用,snake_case)。"""
|
||||
|
||||
brief: str = Field(default="", description="一句话需求/方向(可空,空则按题材发散)")
|
||||
text: str | None = Field(
|
||||
default=None,
|
||||
description="原文输入(扩写/降AI 的原文、拆书的样本;text_input 策略工具用)",
|
||||
)
|
||||
chapter_no: int | None = Field(
|
||||
default=None, ge=MIN_CHAPTER_NO, description="按章展开类工具的章号(开篇/细纲)"
|
||||
default=None,
|
||||
ge=MIN_CHAPTER_NO,
|
||||
description="按章展开/续写类工具的章号(开篇/细纲/续写)",
|
||||
)
|
||||
count: int | None = Field(default=None, ge=1, le=12, description="生成数量(部分工具可用)")
|
||||
kind: str | None = Field(default=None, description="命名对象类别等(部分工具可用)")
|
||||
|
||||
@@ -11,14 +11,21 @@ IO(读 projects/world_entities/outline)发生在端点;本模块只负责
|
||||
- `brief_only` :作品设定(仅标题/题材级)+ 一句话需求;
|
||||
- `with_project` :作品设定(含前提/主题)+ 一句话需求;
|
||||
- `with_world` :作品设定 + world_entities 硬规则卡 + 一句话需求;
|
||||
- `with_outline_chapter`:作品设定 + 指定章大纲节拍(缺章/缺大纲 → 空节拍,不报错)。
|
||||
- `with_outline_chapter`:作品设定 + 指定章大纲节拍(缺章/缺大纲 → 空节拍,不报错);
|
||||
- `with_prior_chapter` :作品设定 + 最新已写正文(端点先读 accepted/draft)+ 可选节拍;
|
||||
- `text_input` :作品设定 + 作者提供的原文(扩写/降AI/拆书样本)+ 一句话需求。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from ww_core.orchestrator import build_brief_context, build_outline_chapter_context
|
||||
from ww_core.orchestrator import (
|
||||
build_brief_context,
|
||||
build_continuation_context,
|
||||
build_outline_chapter_context,
|
||||
build_text_input_context,
|
||||
)
|
||||
from ww_skills import ContextStrategy
|
||||
|
||||
|
||||
@@ -30,16 +37,30 @@ def build_toolbox_context(
|
||||
world_context: str = "",
|
||||
chapter_no: int | None = None,
|
||||
beats: Sequence[str] = (),
|
||||
prior_text: str = "",
|
||||
text: str = "",
|
||||
) -> str:
|
||||
"""据注入策略组装喂网关的文本(PURE,确定性)。
|
||||
|
||||
`project_context` 由调用方按策略序列化(brief_only 给精简设定、with_project 给完整设定);
|
||||
`world_context` 仅 with_world 用(已序列化的硬规则卡);`chapter_no`/`beats` 仅
|
||||
with_outline_chapter 用(缺章/缺大纲时 beats 为空,降级到空节拍而非报错)。
|
||||
with_outline_chapter 用(缺章/缺大纲时 beats 为空,降级到空节拍而非报错);
|
||||
`prior_text` 仅 with_prior_chapter 用(端点先读最新 accepted/draft 正文注入);
|
||||
`text` 仅 text_input 用(作者提供的原文样本)。
|
||||
"""
|
||||
if strategy in ("brief_only", "with_project"):
|
||||
return build_brief_context(brief=brief, project_context=project_context)
|
||||
|
||||
if strategy == "with_prior_chapter":
|
||||
return build_continuation_context(
|
||||
project_context=project_context,
|
||||
prior_text=prior_text,
|
||||
beats=list(beats) if beats else None,
|
||||
)
|
||||
|
||||
if strategy == "text_input":
|
||||
return build_text_input_context(project_context=project_context, text=text, brief=brief)
|
||||
|
||||
if strategy == "with_world":
|
||||
base = build_brief_context(brief=brief, project_context=project_context)
|
||||
world_block = world_context.strip() or "(暂无世界观设定)"
|
||||
|
||||
Reference in New Issue
Block a user