pipeline_core/pipeline_core/agent_config.py

438 lines
16 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:
"""技能配置 — 三级隔离:全局 + 组织 + 用户"""
enabled: bool = True
base_dir: str = "skills" # 技能根目录,其下为 global/orgs/users/
max_skills_per_turn: int = 5
enable_global: bool = True # 启用全局技能
enable_org: 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_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_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),
)
# ═══════════════════════════════════════════════════════════
# SDLC 默认配置
# ═══════════════════════════════════════════════════════════
SDLC_DEFAULT_TOOLS = [
ToolDefinition(
name="switch_project",
description="切换到指定项目。用户说「切换到XXX」时调用。",
parameters={"project_name": "项目名称"},
category="project",
),
ToolDefinition(
name="create_project",
description="创建新的软件项目",
parameters={"name": "项目名称", "description": "项目描述"},
category="project",
),
ToolDefinition(
name="create_task",
description="创建开发任务分配给角色agent执行",
parameters={
"title": "任务标题",
"role": "目标角色(requirement/design/develop/test/deploy)",
"description": "任务详细描述",
},
category="task",
),
ToolDefinition(
name="list_tasks",
description="查看当前项目的任务列表",
parameters={"role": "按角色筛选(可选)", "state": "按状态筛选(可选)"},
category="task",
),
ToolDefinition(
name="task_detail",
description="查看任务详情",
parameters={"task_id": "任务ID"},
category="task",
),
ToolDefinition(
name="start_agents",
description="启动角色agent执行已提交的任务",
parameters={},
category="agent",
),
ToolDefinition(
name="diagnose_project",
description="诊断项目:汇总卡点/失败/问题/运行中任务",
parameters={},
category="agent",
),
ToolDefinition(
name="list_deliverables",
description="查看交付件列表",
parameters={"role": "按角色筛选(可选)"},
category="task",
),
ToolDefinition(
name="view_deliverable",
description="查看交付件详细内容",
parameters={"deliverable_id": "交付件ID"},
category="task",
),
ToolDefinition(
name="list_questions",
description="查看待回答的问题",
parameters={},
category="agent",
),
ToolDefinition(
name="answer_question",
description="回答agent提出的问题",
parameters={"question_id": "问题ID", "answer": "回答内容"},
category="agent",
),
ToolDefinition(
name="add_repo",
description="添加项目关联的Git仓库",
parameters={"repo_url": "仓库URL", "repo_name": "仓库名称", "default_branch": "默认分支(默认main)"},
category="repo",
),
ToolDefinition(
name="list_repos",
description="查看项目关联的仓库列表",
parameters={},
category="repo",
),
ToolDefinition(
name="run_command",
description="在工作空间中执行shell命令",
parameters={"command": "命令"},
category="shell",
requires_confirmation=True,
),
ToolDefinition(
name="check_progress",
description="查看项目整体进展",
parameters={},
category="agent",
),
ToolDefinition(
name="add_bug",
description="提交Bug",
parameters={"title": "Bug标题", "description": "描述", "severity": "严重程度"},
category="task",
),
# ── v2 新增 ──
ToolDefinition(
name="ask_user",
description="向用户提问澄清意图(不确定时使用)",
parameters={"question": "问题"},
category="agent",
),
ToolDefinition(
name="delegate_subtask",
description="派生子agent调查子任务并行执行",
parameters={"goal": "子任务目标", "context": "背景信息"},
category="agent",
),
]
SDLC_DEFAULT_CONFIG = AgentConfig(
model_name="deepseek-v4-pro",
temperature=0.4,
max_turns=30,
system_prompt="""你是 SDLC 开发产线的 AI 驾驶舱助理。
## 核心原则
- 说做就做:承诺调用工具时立刻调,不要只描述计划
- 每次只输出一个 JSON
- 工具调用后等待结果,再决定下一步
- 不确定用户意图时,使用 ask_user 工具澄清
## 项目上下文
当前项目:{current_project}
{project_context}
## 可用工具
{tools_description}
## 输出格式
每次只输出一个 JSON 对象:
- 回复用户:{"action":"reply","message":"回复内容"}
- 调用工具:{"action":"tool_call","tool":"工具名","params":{}}
- 提问澄清:{"action":"tool_call","tool":"ask_user","params":{"question":"问题"}}
""",
tools=SDLC_DEFAULT_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"),
)
# ═══════════════════════════════════════════════════════════
# 配置加载器
# ═══════════════════════════════════════════════════════════
async def load_agent_config(pipeline_id: str = None, project_id: str = None) -> AgentConfig:
"""加载产线的 Agent 配置。
优先级:
1. 项目级 sd_org_settings.agent_config
2. 产线级 pipelines.agent_config
3. SDLC_DEFAULT_CONFIG
"""
try:
from sqlor.dbpools import DBPools
db = DBPools()
async with db.sqlorContext("pipeline") as sor:
# 项目级配置
if project_id:
recs = await sor.sqlExe(
"SELECT agent_config FROM sd_org_settings WHERE project_id=${pid}$",
{"pid": project_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", []), SDLC_DEFAULT_TOOLS)
return AgentConfig.from_dict(data)
except (json.JSONDecodeError, TypeError):
pass
# 产线级配置
if pipeline_id:
recs = await sor.sqlExe(
"SELECT agent_config FROM pipelines WHERE id=${pid}$",
{"pid": pipeline_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", []), SDLC_DEFAULT_TOOLS)
return AgentConfig.from_dict(data)
except (json.JSONDecodeError, TypeError):
pass
except Exception:
pass
return SDLC_DEFAULT_CONFIG
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"""
from sqlor.dbpools import DBPools
db = DBPools()
async with db.sqlorContext("pipeline") as sor:
config_json = json.dumps(config.to_dict(), ensure_ascii=False)
# UPDATE-first 防止竞态
await sor.sqlExe(
"UPDATE sd_org_settings SET agent_config=${cfg}$ WHERE project_id=${pid}$",
{"cfg": config_json, "pid": project_id},
)
existing = await sor.sqlExe(
"SELECT id FROM sd_org_settings WHERE project_id=${pid}$",
{"pid": project_id},
)
if not existing:
from appPublic.uniqueID import getID
await sor.C("sd_org_settings", {
"id": getID(),
"project_id": project_id,
"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"