pipeline_core/pipeline_core/agent_config.py

466 lines
19 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",
),
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"