Files
sanguo_moziplus_v2/src/daemon/prompt_composer.py
T
cfdaily c89863a288
CI / lint (pull_request) Failing after 7s
CI / test (pull_request) Has been skipped
CI / notify-on-failure (pull_request) Successful in 1s
feat: §17 ToolchainHandler 强约束实现(Step 1-4)
Step 1: 基础设施
- prompt_composer.py: PromptContext 新增 action_type + action_steps 字段
- spawner.py: handler 路径提取 action_type/action_steps 传入 PromptContext
- db.py: comments CHECK 约束加入 action_report

Step 2: ToolchainHandler 强化
- ToolchainContextSection: 加 steps 渲染 + action_hint(按 action_type)
- ToolchainApiSection: 改为 action_report 提交指引 + Gitea 协作指引
- ToolchainConstraintsSection: 5 条强约束 + Red Flags 防self-rationalization
- verify_completion: action_report → output → comment 三层 fallback
  - review_merged 始终通过(纯通知)
  - infrastructure_failure 始终通过(防递归)
  - 修复 LENGTH(content) → LENGTH(body) bug
- on_failure 三分路: 业务→Gitea PR comment / 系统→Gitea Issue / 基础设施→toolchain task

Step 3: toolchain_routes 改造
- 新增 _toolchain_db_path() + _send_toolchain_task()
- 所有 8 个 handler 改为 _send_toolchain_task
- _send_mail 保留但不再被 toolchain handler 调用
- _send_deploy_failure_mail → _send_deploy_failure_task

Step 4: 测试
- 29 个单元测试全部通过
- 全量 456 passed, 3 skipped, 0 failures
2026-06-13 23:36:44 +08:00

130 lines
4.2 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.
"""
prompt_composer.py — PromptSection Protocol + PromptContext + PromptComposer
拼装器:有序管理 prompt 段落,按优先级排序后合并为最终 prompt。
"""
import logging
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Protocol, runtime_checkable
logger = logging.getLogger("moziplus-v2.prompt_composer")
# ---------------------------------------------------------------------------
# Section 优先级范围约定
# ---------------------------------------------------------------------------
PRIORITY_CONTEXT = 10 # 任务上下文
PRIORITY_PRIOR = 20 # 前序信息
PRIORITY_ROLE = 30 # 角色规范
PRIORITY_API = 40 # API 操作指令
PRIORITY_CONSTRAINTS = 50 # 硬约束
PRIORITY_EXTENSION = 60 # 扩展段
# ---------------------------------------------------------------------------
# PromptSection Protocol
# ---------------------------------------------------------------------------
@runtime_checkable
class PromptSection(Protocol):
"""一个 prompt 段"""
name: str # 段名(去重用,同名覆盖)
priority: int # 排序优先级(小数字=靠前)
def render(self, context: "PromptContext") -> str:
"""渲染此段的文本内容。返回空字符串表示不注入。"""
...
def should_include(self, context: "PromptContext") -> bool:
"""是否注入此段(默认 True,条件段可覆盖)。"""
...
# ---------------------------------------------------------------------------
# PromptContext 数据对象
# ---------------------------------------------------------------------------
@dataclass
class PromptContext:
"""Prompt 渲染的统一上下文"""
task_id: str
title: str
description: str
must_haves: str
project_id: str
agent_id: str
task: Optional[Dict] = None
role: str = "executor"
spawn_type: str = "executor"
# mail 专用
from_agent: str = ""
mail_type: str = "" # inform / request
# toolchain 专用
event_type: str = "" # ci_failure / review_request / ...
event_data: Dict = field(default_factory=dict)
action_type: str = "" # 动作分类(review_result / ci_failure / ...
action_steps: list = field(default_factory=list) # 结构化编号步骤列表
# 前序产出
depends_on_outputs: Optional[List] = None
# ---------------------------------------------------------------------------
# PromptComposer 拼装器
# ---------------------------------------------------------------------------
class PromptComposer:
"""有序拼装 prompt sections"""
SEPARATOR = "\n\n---\n\n"
TOKEN_BUDGET_WARN = 800 # token 预算警告阈值
CHARS_PER_TOKEN = 3.5 # 估算比率
def __init__(self) -> None:
self._sections: List[PromptSection] = []
def add(self, section: PromptSection) -> None:
"""添加一个 section(同名覆盖)"""
self._sections = [s for s in self._sections if s.name != section.name]
self._sections.append(section)
def add_many(self, sections: List[PromptSection]) -> None:
"""批量添加"""
for s in sections:
self.add(s)
def compose(self, context: PromptContext) -> str:
"""拼装最终 prompt
1. 过滤 should_include=False 的段
2. 按 priority 排序
3. 逐段 render
4. 过滤空段
5. 用分隔符连接
6. Token 预算警告(不截断)
"""
active = [s for s in self._sections if s.should_include(context)]
active.sort(key=lambda s: s.priority)
parts = [s.render(context) for s in active]
parts = [p for p in parts if p.strip()]
result = self.SEPARATOR.join(parts)
# Token 估算
tokens = max(1, int(len(result) / self.CHARS_PER_TOKEN))
logger.debug(
"Composed prompt from %d sections, %d tokens",
len(parts), tokens,
)
if tokens > self.TOKEN_BUDGET_WARN:
logger.warning(
"Prompt exceeds %d token budget: %d tokens (task_id=%s)",
self.TOKEN_BUDGET_WARN, tokens, context.task_id,
)
return result