pipeline_core/pipeline_core/agent_config.py

522 lines
23 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.

"""
pipeline-core: Agent 能力定义层
对照 Hermes Agent 的 config.yaml + system prompt builder + tool registry:
在此定义每个产线的 Agent 配置——模型、工具、技能、记忆、上下文压缩。
每个产线(pipeline)可以有不同的 AgentConfig,通过 sd_org_settings 或
pipelines 表的 agent_config 字段存储。pipeline-service 执行时读取此配置。
"""
import json
import os
from dataclasses import dataclass, field
from typing import Dict, List, Optional
# ═══════════════════════════════════════════════════════════
# 数据模型
# ═══════════════════════════════════════════════════════════
@dataclass
class ToolDefinition:
"""工具定义 — 等价于 HA 的 tool schema"""
name: str
description: str
parameters: dict = field(default_factory=dict) # JSON Schema for params
enabled: bool = True
category: str = "general" # project / task / repo / shell / agent
requires_confirmation: bool = False # 是否需要用户确认
@dataclass
class CompressionConfig:
"""上下文压缩配置"""
enabled: bool = True
threshold: float = 0.50 # 达到上下文窗口的 50% 时触发压缩
target_ratio: float = 0.20 # 压缩到 20%
keep_recent: int = 8 # 保留最近 N 轮
@dataclass
class MemoryConfig:
"""记忆系统配置"""
enabled: bool = True
user_profile_enabled: bool = True # 用户偏好
cross_session_enabled: bool = True # 跨会话事实
max_entries: int = 50 # 最多保留条数
@dataclass
class SkillConfig:
"""技能配置 — 六级隔离:global→org→pipeline→role→project→user"""
enabled: bool = True
base_dir: str = "skills" # 技能根目录,其下为 global/pipelines/projects/orgs/users/
max_skills_per_turn: int = 5
enable_global: bool = True # 启用全局技能
enable_org: bool = True # 启用组织技能
enable_pipeline: bool = True # 启用产线技能
enable_role: bool = True # 启用角色技能
enable_project: bool = True # 启用项目/会话技能
enable_user: bool = True # 启用用户技能
@dataclass
class AgentConfig:
"""Agent 完整配置 — 一个产线一个配置"""
# ── 模型 ──
model_name: str = ""
temperature: float = 0.4
max_turns: int = 30 # 最大 tool-calling 轮次(替代硬编码 10)
# ── 系统提示词 ──
system_prompt: str = "" # 产线专属系统提示词
personality: str = "" # 人设描述
# ── 上下文 ──
compression: CompressionConfig = field(default_factory=CompressionConfig)
context_limit: int = 64000 # token 上限估计
# ── 记忆 ──
memory: MemoryConfig = field(default_factory=MemoryConfig)
# ── 技能 ──
skills: SkillConfig = field(default_factory=SkillConfig)
# ── 工具 ──
tools: List[ToolDefinition] = field(default_factory=list)
# ── 会话 ──
session_isolation: str = "project" # "project" | "user" | "none"
history_limit: int = 50 # 加载历史消息数
# ── 安全 ──
require_approval: bool = False # 危险命令需确认
allowed_workdirs: List[str] = field(default_factory=list)
def to_dict(self) -> dict:
"""序列化为 JSON"""
return {
"model_name": self.model_name,
"temperature": self.temperature,
"max_turns": self.max_turns,
"system_prompt": self.system_prompt,
"personality": self.personality,
"compression": {
"enabled": self.compression.enabled,
"threshold": self.compression.threshold,
"target_ratio": self.compression.target_ratio,
"keep_recent": self.compression.keep_recent,
},
"memory": {
"enabled": self.memory.enabled,
"user_profile_enabled": self.memory.user_profile_enabled,
"cross_session_enabled": self.memory.cross_session_enabled,
"max_entries": self.memory.max_entries,
},
"skills": {
"enabled": self.skills.enabled,
"max_skills_per_turn": self.skills.max_skills_per_turn,
"base_dir": self.skills.base_dir,
"enable_global": self.skills.enable_global,
"enable_org": self.skills.enable_org,
"enable_pipeline": self.skills.enable_pipeline,
"enable_role": self.skills.enable_role,
"enable_project": self.skills.enable_project,
"enable_user": self.skills.enable_user,
},
"tools": [
{
"name": t.name,
"description": t.description,
"parameters": t.parameters,
"enabled": t.enabled,
"category": t.category,
}
for t in self.tools
],
"session_isolation": self.session_isolation,
"history_limit": self.history_limit,
"context_limit": self.context_limit,
}
@classmethod
def from_dict(cls, data: dict) -> "AgentConfig":
"""从 JSON 反序列化"""
comp = data.get("compression", {})
mem = data.get("memory", {})
sk = data.get("skills", {})
return cls(
model_name=data.get("model_name", ""),
temperature=data.get("temperature", 0.4),
max_turns=data.get("max_turns", 30),
system_prompt=data.get("system_prompt", ""),
personality=data.get("personality", ""),
compression=CompressionConfig(
enabled=comp.get("enabled", True),
threshold=comp.get("threshold", 0.50),
target_ratio=comp.get("target_ratio", 0.20),
keep_recent=comp.get("keep_recent", 8),
),
memory=MemoryConfig(
enabled=mem.get("enabled", True),
user_profile_enabled=mem.get("user_profile_enabled", True),
cross_session_enabled=mem.get("cross_session_enabled", True),
max_entries=mem.get("max_entries", 50),
),
skills=SkillConfig(
enabled=sk.get("enabled", True),
base_dir=sk.get("base_dir", "skills"),
max_skills_per_turn=sk.get("max_skills_per_turn", 5),
enable_global=sk.get("enable_global", True),
enable_org=sk.get("enable_org", True),
enable_pipeline=sk.get("enable_pipeline", True),
enable_role=sk.get("enable_role", True),
enable_project=sk.get("enable_project", True),
enable_user=sk.get("enable_user", True),
),
tools=[ToolDefinition(**t) if isinstance(t, dict) else t for t in data.get("tools", [])],
session_isolation=data.get("session_isolation", "project"),
history_limit=data.get("history_limit", 50),
context_limit=data.get("context_limit", 64000),
)
# ═══════════════════════════════════════════════════════════
# 通用 agent 内核(产线无关)
#
# GENERAL_TOOLS:任何产线 agent 都具备的基础能力
# (项目管理/终端/文件/搜索/会话/规划/澄清/委派)。
# 产线专属能力(如 SDLC 的 create_task/diagnose_project)以「能力包」形式
# 独立注册,见 pipeline_service.sdlc_ability,通过 PipelineAbility 挂载。
# ═══════════════════════════════════════════════════════════
GENERAL_TOOLS = [
ToolDefinition(
name="switch_project",
description="切换到指定项目。用户说「切换到XXX」时调用。",
parameters={"project_name": "项目名称"},
category="project",
),
ToolDefinition(
name="create_project",
description="创建新的软件项目",
parameters={"name": "项目名称", "description": "项目描述"},
category="project",
),
# ── 项目管理(终止/删除类仅项目人类 owner 可执行,代码层校验)──
ToolDefinition(
name="project_info",
description="查看指定项目的详情(状态/描述/产线/创建时间)。用户问「项目信息/状态」时调用。仅项目 owner 可查。",
parameters={"project_name": "项目名称(可选,缺省当前项目)"},
category="project",
),
ToolDefinition(
name="list_my_projects",
description="列出我创建的项目(可按状态过滤)。用户问「我的项目/项目列表」时调用。",
parameters={"status": "状态过滤(可选: active/paused/completed/archived/draft)"},
category="project",
),
ToolDefinition(
name="pause_project",
description="终止/暂停项目:项目置为 paused,agent 不再推进。用户明确说「暂停/停止/终止项目」时调用。仅项目人类 owner 可执行,系统会校验你的身份。",
parameters={"project_name": "项目名称(可选,缺省当前项目)"},
category="project",
requires_confirmation=True,
),
ToolDefinition(
name="resume_project",
description="恢复已暂停的项目(暂停中 → 还原为暂停前的状态)。用户说「恢复/继续项目」时调用。仅项目人类 owner 可执行。",
parameters={"project_name": "项目名称(可选,缺省当前项目)"},
category="project",
requires_confirmation=True,
),
ToolDefinition(
name="delete_project",
description="删除项目:先自动归档备份(数据库记录+工作目录打包),再删除全部关联数据与工作目录,不可撤销。用户明确要求「删除项目」时直接调用本工具(confirm=true),平台确认门会自动弹窗向用户最终确认——你不需要也不应该在调用前反复向用户复述确认,那会造成确认死循环。仅项目人类 owner 可执行。",
parameters={"project_name": "项目名称(可选,缺省当前项目)", "confirm": "固定传 true(平台确认门负责向用户最终确认)"},
category="project",
requires_confirmation=True,
),
ToolDefinition(
name="run_command",
description="在工作空间中执行shell命令",
parameters={"command": "命令"},
category="shell",
requires_confirmation=True,
),
# ── v2 新增 ──
ToolDefinition(
name="ask_user",
description="向用户提问澄清意图(不确定时使用)",
parameters={"question": "问题"},
category="agent",
),
ToolDefinition(
name="delegate_subtask",
description="派生子agent调查子任务(并行执行)",
parameters={"goal": "子任务目标", "context": "背景信息"},
category="agent",
),
# ── 通用工具集(Hermes CLI 能力子集:文件/搜索/会话/规划)──
ToolDefinition(
name="read_file",
description="读取工作空间中的文件内容。支持 docx/txt/md/json 等文本格式,docx 会自动解析提取正文文本,直接调用即可读取 docx 内容。单次最多 30000 字符;返回带截断提示时,用 offset 参数分段读后文",
parameters={"path": "相对路径", "offset": "可选:从第几个字符开始读(分段读大文件)"},
category="file",
),
ToolDefinition(
name="load_skill",
description="加载指定技能的完整内容(具体步骤/规范/陷阱)。先在系统提示的『可用技能』目录里找到技能名,需要时再调用本工具加载正文。技能带子文件(模板库/参考文档)时,正文末尾会列出可用子文件,用 file_path 参数逐个加载",
parameters={"name": "技能名称", "file_path": "可选:子文件相对路径(如 references/uapi-templates.md),只允许 references/scripts/templates/assets 下的文件"},
category="skill",
),
ToolDefinition(
name="list_packs",
description="列出可安装的技能集(如 ocai-h5-dev 开发技能集等)。用户说「安装/导入 XX 技能/技能集」时,先调用本工具查看有哪些技能集可选",
parameters={},
category="skill",
),
ToolDefinition(
name="install_pack",
description="安装技能集到当前机构。用户说「导入/安装 ocai 开发技能」等时调用,pack 参数填技能集名(如 ocai-h5-dev)",
parameters={"pack": "技能集名称(如 ocai-h5-dev)"},
category="skill",
),
ToolDefinition(
name="propose_skill",
description="自省提出技能化建议:把值得沉淀的流程/经验/坑写成技能草稿提交审核(不自动生效)。用户要求沉淀经验、总结技能,或你发现反复出现的流程/坑/规范时调用",
parameters={"name": "技能名", "description": "技能描述", "content": "SKILL.md 草稿内容"},
category="skill",
),
ToolDefinition(
name="write_file",
description="写入文件到工作空间(自动创建父目录)",
parameters={"path": "相对路径", "content": "文件内容"},
category="file",
),
ToolDefinition(
name="list_files",
description="列出工作空间目录内容",
parameters={"path": "相对路径(可选,默认工作空间根)"},
category="file",
),
ToolDefinition(
name="search_files",
description="在工作空间中搜索文件内容(grep)",
parameters={"pattern": "搜索关键词或正则", "path": "相对路径(可选,默认整个工作空间)"},
category="file",
),
ToolDefinition(
name="session_search",
description="搜索本项目的会话历史记录,找回之前讨论过的内容",
parameters={"query": "搜索关键词"},
category="memory",
),
ToolDefinition(
name="todo",
description="管理当前会话的任务清单(list/add/done)",
parameters={"action": "list|add|done", "content": "任务内容(add时必填)"},
category="agent",
),
]
DEFAULT_AGENT_CONFIG = AgentConfig(
model_name="deepseek-v4-pro",
temperature=0.4,
max_turns=30,
system_prompt="""你是一个通用 agent,具备通用推理与判断能力,并根据当前产线配备了对应的工具。像一名有经验的负责人那样思考:先理解意图,再拆解问题,判断自己能否解决,必要时澄清或诚实说明。
## 工作原则
1. 先理解用户意图,再决定行动。意图模糊时用 ask_user 澄清,不要臆测。
2. 用工具是手段,不是目的。工具能解决就用工具,用工具只是为了把事做成。
3. 能力自省与诚实降级:如果用户的请求超出你的工具能力,不要硬套一个不相关的工具。你必须:
a) 明确告诉用户:你做不到、缺什么能力、为什么;
b) 给出你能做到的替代方案;
c) 必要时 ask_user 让用户拍板。
4. 发现异常/卡点时,主动定位根因并推动解决,而不是只列清单。
5. 每轮输出 tool_call / reply / ask_user 三者之一,根据实际情况选择,没有任何强制。
## 工具集
{tools_description}
""",
tools=GENERAL_TOOLS,
session_isolation="project",
history_limit=50,
compression=CompressionConfig(enabled=True, threshold=0.50, target_ratio=0.20, keep_recent=6),
memory=MemoryConfig(enabled=True),
skills=SkillConfig(enabled=True, base_dir="skills"),
)
# 兼容别名(过渡期):SDLC 能力已迁移到 pipeline_service.sdlc_ability 能力包,
# 此处保留别名以免破坏既有引用,后续可删除。
SDLC_DEFAULT_TOOLS = GENERAL_TOOLS
SDLC_DEFAULT_CONFIG = DEFAULT_AGENT_CONFIG
# 默认产线:pipeline_id 为空时回退的能力包(对应 pipelines 表的「通用软件开发产线」)
DEFAULT_ABILITY_ID = "sdlc_general"
# ═══════════════════════════════════════════════════════════
# 配置加载器
# ═══════════════════════════════════════════════════════════
async def load_agent_config(pipeline_id: str = None, project_id: str = None, generic: bool = False) -> AgentConfig:
"""加载产线的 Agent 配置(通用内核 + 可插拔产线能力)。
组装顺序:
1. 基础 = DEFAULT_AGENT_CONFIG(通用心智 + GENERAL_TOOLS)
2. 产线能力 = get_ability(pipeline_id) 动态挂载(工具 + prompt 片段)
—— pipeline_id 为空时回退 DEFAULT_ABILITY_ID(默认产线)
—— generic=True 时不挂载任何产线能力(纯通用,只用 GENERAL_TOOLS + 通用心智)
3. DB 覆盖:sd_org_settings.agent_config / pipelines.agent_config
"""
from .ability import get_ability
# 1. 基础通用配置
base = DEFAULT_AGENT_CONFIG
# 2. 产线能力挂载(generic 时不挂载任何产线插件)
if generic:
ability = None
else:
ability = get_ability(pipeline_id) if pipeline_id else get_ability(DEFAULT_ABILITY_ID)
tools = list(GENERAL_TOOLS)
prompt = base.system_prompt
if ability:
tools = _merge_tools(ability.tools, tools)
if ability.system_prompt:
prompt += "\n\n## 产线专属能力\n" + ability.system_prompt
cfg = AgentConfig(
model_name=base.model_name,
temperature=base.temperature,
max_turns=base.max_turns,
system_prompt=prompt,
tools=tools,
session_isolation=base.session_isolation,
history_limit=base.history_limit,
compression=base.compression,
memory=base.memory,
skills=base.skills,
)
# 3. DB 覆盖(项目级 / 产线级)
try:
from sqlor.dbpools import DBPools
db = DBPools()
async with db.sqlorContext("pipeline") as sor:
# 项目级配置:sd_org_settings 无 project_id 列,需先由 sd_projects 解析 org_id 再查
if project_id:
org_id = ""
proj_recs = await sor.sqlExe(
"SELECT org_id FROM sd_projects WHERE id=${pid}$", {"pid": project_id})
if proj_recs:
org_id = getattr(proj_recs[0], "org_id", "") or ""
if org_id:
recs = await sor.sqlExe(
"SELECT agent_config FROM sd_org_settings WHERE org_id=${oid}$",
{"oid": org_id},
)
if recs:
raw = getattr(recs[0], "agent_config", "")
if raw:
try:
data = json.loads(raw) if isinstance(raw, str) else raw
data["tools"] = _merge_tools(data.get("tools", []), tools)
return AgentConfig.from_dict(data)
except (json.JSONDecodeError, TypeError):
pass
# 产线级配置 + 产线缺省模型
if pipeline_id:
recs = await sor.sqlExe(
"SELECT agent_config, default_model FROM pipelines WHERE id=${pid}$",
{"pid": pipeline_id},
)
if recs:
raw = getattr(recs[0], "agent_config", "") or ""
default_model = getattr(recs[0], "default_model", "") or ""
if raw:
try:
data = json.loads(raw) if isinstance(raw, str) else raw
data["tools"] = _merge_tools(data.get("tools", []), tools)
if not data.get("model_name") and default_model:
data["model_name"] = default_model
return AgentConfig.from_dict(data)
except (json.JSONDecodeError, TypeError):
pass
elif default_model:
cfg.model_name = default_model
except Exception:
pass
return cfg
def _merge_tools(custom_tools: list, default_tools: list) -> list:
"""合并自定义工具和默认工具。自定义覆盖同名工具。"""
merged = {t["name"] if isinstance(t, dict) else t.name: t for t in default_tools}
for t in custom_tools:
name = t["name"] if isinstance(t, dict) else t.name
merged[name] = t
return list(merged.values())
async def save_agent_config(project_id: str, config: AgentConfig):
"""保存项目级 Agent 配置到 sd_org_settings(按 org_id 组织)"""
from sqlor.dbpools import DBPools
db = DBPools()
async with db.sqlorContext("pipeline") as sor:
# sd_org_settings 以 org_id 为键,需先由项目解析 org_id
org_id = ""
ws = ""
proj_recs = await sor.sqlExe(
"SELECT org_id, workspace_dir FROM sd_projects WHERE id=${pid}$", {"pid": project_id})
if proj_recs:
org_id = getattr(proj_recs[0], "org_id", "") or ""
ws = getattr(proj_recs[0], "workspace_dir", "") or ""
if not org_id:
return
config_json = json.dumps(config.to_dict(), ensure_ascii=False)
# UPDATE-first 防止竞态
await sor.sqlExe(
"UPDATE sd_org_settings SET agent_config=${cfg}$ WHERE org_id=${oid}$",
{"cfg": config_json, "oid": org_id},
)
existing = await sor.sqlExe(
"SELECT id FROM sd_org_settings WHERE org_id=${oid}$", {"oid": org_id},
)
if not existing:
from appPublic.uniqueID import getID
await sor.C("sd_org_settings", {
"id": getID(),
"org_id": org_id,
"workspace_root": ws or "/tmp/ws",
"agent_config": config_json,
})
# ═══════════════════════════════════════════════════════════
# 模块加载
# ═══════════════════════════════════════════════════════════
def load_pipeline_core():
"""注册 Agent 配置管理函数到 ServerEnv"""
from ahserver.serverenv import ServerEnv
env = ServerEnv()
env.load_agent_config = load_agent_config
env.save_agent_config = save_agent_config
env.AgentConfig = AgentConfig
env.SDLC_DEFAULT_CONFIG = SDLC_DEFAULT_CONFIG
MODULE_NAME = "pipeline_core"