466 lines
19 KiB
Python
466 lines
19 KiB
Python
"""
|
||
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",
|
||
),
|
||
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 内容",
|
||
parameters={"path": "相对路径"},
|
||
category="file",
|
||
),
|
||
ToolDefinition(
|
||
name="load_skill",
|
||
description="加载指定技能的完整内容(具体步骤/规范/陷阱)。先在系统提示的『可用技能』目录里找到技能名,需要时再调用本工具加载正文",
|
||
parameters={"name": "技能名称"},
|
||
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) -> AgentConfig:
|
||
"""加载产线的 Agent 配置(通用内核 + 可插拔产线能力)。
|
||
|
||
组装顺序:
|
||
1. 基础 = DEFAULT_AGENT_CONFIG(通用心智 + GENERAL_TOOLS)
|
||
2. 产线能力 = get_ability(pipeline_id) 动态挂载(工具 + prompt 片段)
|
||
—— pipeline_id 为空时回退 DEFAULT_ABILITY_ID(默认产线)
|
||
3. DB 覆盖:sd_org_settings.agent_config / pipelines.agent_config
|
||
"""
|
||
from .ability import get_ability
|
||
|
||
# 1. 基础通用配置
|
||
base = DEFAULT_AGENT_CONFIG
|
||
|
||
# 2. 产线能力挂载
|
||
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"
|