- 新增 CLIPluginAdapter 统一接口 (backend/app/core/agent_adapter.py) - 新增 LLM 服务层,支持 Anthropic/OpenAI/DeepSeek/Ollama (backend/app/services/llm_service.py) - 新增 Agent 执行引擎,支持文件锁自动管理 (backend/app/services/agent_executor.py) - 新增 NativeLLMAgent 原生 LLM 适配器 (backend/app/adapters/native_llm_agent.py) - 新增进程管理器 (backend/app/services/process_manager.py) - 新增 Agent 控制 API (backend/app/routers/agents_control.py) - 新增 WebSocket 实时通信 (backend/app/routers/websocket.py) - 更新前端 AgentsPage,支持启动/停止 Agent - 测试通过:Agent 启动、批量操作、栅栏同步 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
498 lines
14 KiB
Python
498 lines
14 KiB
Python
"""
|
||
原生 LLM Agent 适配器
|
||
|
||
直接调用 LLM API 实现 Agent,不需要外部进程。
|
||
这是主要使用的适配器类型。
|
||
"""
|
||
|
||
import asyncio
|
||
import logging
|
||
from typing import Dict, Optional, Any
|
||
from datetime import datetime
|
||
|
||
from ..core.agent_adapter import (
|
||
CLIPluginAdapter,
|
||
Task,
|
||
Result,
|
||
AgentCapabilities
|
||
)
|
||
from ..services.llm_service import ModelRouter, get_llm_service
|
||
from ..services.agent_executor import AgentExecutor, get_agent_executor
|
||
from ..services.meeting_scheduler import get_meeting_scheduler
|
||
from ..services.agent_registry import get_agent_registry
|
||
from ..services.heartbeat import get_heartbeat_service
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class NativeLLMAgent(CLIPluginAdapter):
|
||
"""
|
||
原生 LLM Agent
|
||
|
||
直接通过 LLM API 实现的 Agent,特点:
|
||
1. 无需外部进程,完全异步
|
||
2. 支持所有主流 LLM 提供商
|
||
3. 自动管理资源(文件锁、心跳)
|
||
4. 支持会议协作
|
||
"""
|
||
|
||
def __init__(
|
||
self,
|
||
agent_id: str,
|
||
name: str,
|
||
role: str,
|
||
model: str,
|
||
config: Dict[str, Any] = None,
|
||
llm_service: ModelRouter = None,
|
||
executor: AgentExecutor = None
|
||
):
|
||
self._id = agent_id
|
||
self._name = name
|
||
self._role = role
|
||
self._model = model
|
||
self.config = config or {}
|
||
self._version = "1.0.0"
|
||
|
||
# 获取服务
|
||
self.llm_service = llm_service or get_llm_service()
|
||
self.executor = executor or get_agent_executor(self.llm_service)
|
||
self.scheduler = get_meeting_scheduler()
|
||
self.registry = get_agent_registry()
|
||
self.heartbeat_service = get_heartbeat_service()
|
||
|
||
# 状态
|
||
self._is_running = False
|
||
self._current_task: Optional[Task] = None
|
||
|
||
logger.info(f"NativeLLMAgent 初始化: {self._id} ({self._name})")
|
||
|
||
# ========== 属性 ==========
|
||
|
||
@property
|
||
def id(self) -> str:
|
||
return self._id
|
||
|
||
@property
|
||
def name(self) -> str:
|
||
return self._name
|
||
|
||
@property
|
||
def version(self) -> str:
|
||
return self._version
|
||
|
||
@property
|
||
def role(self) -> str:
|
||
return self._role
|
||
|
||
@property
|
||
def model(self) -> str:
|
||
return self._model
|
||
|
||
@property
|
||
def capabilities(self) -> AgentCapabilities:
|
||
"""返回 Agent 能力声明"""
|
||
return AgentCapabilities(
|
||
can_execute_code=True,
|
||
can_read_files=True,
|
||
can_write_files=True,
|
||
can_analyze_code=True,
|
||
can_generate_tests=self._role in ["developer", "qa"],
|
||
can_review_code=self._role == "reviewer",
|
||
supported_languages=["Python", "JavaScript", "TypeScript", "Java", "Go"],
|
||
max_context_length=200000
|
||
)
|
||
|
||
@property
|
||
def is_running(self) -> bool:
|
||
return self._is_running
|
||
|
||
# ========== 核心能力 ==========
|
||
|
||
async def execute(self, task: Task) -> Result:
|
||
"""
|
||
执行任务
|
||
|
||
通过 AgentExecutor 协调 LLM 调用和资源管理
|
||
"""
|
||
self._current_task = task
|
||
|
||
try:
|
||
# 获取或注册 Agent 信息
|
||
agent_info = await self._ensure_registered()
|
||
|
||
# 使用执行引擎执行任务
|
||
result = await self.executor.execute_task(
|
||
agent_info,
|
||
task,
|
||
context=self.config.get("context", {})
|
||
)
|
||
|
||
logger.info(f"任务执行完成: {task.task_id} -> {result.success}")
|
||
return result
|
||
|
||
except Exception as e:
|
||
logger.error(f"任务执行失败: {task.task_id}: {e}", exc_info=True)
|
||
return Result(
|
||
success=False,
|
||
output="",
|
||
error=str(e)
|
||
)
|
||
finally:
|
||
self._current_task = None
|
||
|
||
async def join_meeting(self, meeting_id: str, timeout: int = 300) -> str:
|
||
"""
|
||
加入会议等待队列(栅栏同步)
|
||
|
||
当最后一个参与者到达时,会议自动开始
|
||
"""
|
||
logger.info(f"Agent {self._id} 等待会议: {meeting_id}")
|
||
|
||
# 更新心跳为等待状态
|
||
await self.update_heartbeat("waiting", f"等待会议: {meeting_id}", 0)
|
||
|
||
# 调用会议调度器
|
||
result = await self.scheduler.wait_for_meeting(
|
||
self._id,
|
||
meeting_id,
|
||
timeout=timeout
|
||
)
|
||
|
||
logger.info(f"会议 {meeting_id} 结果: {result}")
|
||
return result
|
||
|
||
async def write_state(self, state: Dict) -> None:
|
||
"""
|
||
写入状态到注册表
|
||
|
||
状态包含:当前任务、进度、临时数据等
|
||
"""
|
||
task = state.get("task", "")
|
||
progress = state.get("progress", 0)
|
||
|
||
await self.registry.update_state(self._id, task, progress)
|
||
logger.debug(f"状态已更新: {self._id} -> {progress}%")
|
||
|
||
async def read_others(self, agent_id: str) -> Dict:
|
||
"""
|
||
读取其他 Agent 的状态
|
||
|
||
用于 Agent 之间互相了解工作状态
|
||
"""
|
||
agent = await self.registry.get_agent(agent_id)
|
||
if not agent:
|
||
return {"error": f"Agent {agent_id} 不存在"}
|
||
|
||
state = await self.registry.get_state(agent_id)
|
||
heartbeat = await self.heartbeat_service.get_heartbeat(agent_id)
|
||
|
||
return {
|
||
"agent": {
|
||
"agent_id": agent.agent_id,
|
||
"name": agent.name,
|
||
"role": agent.role,
|
||
"model": agent.model,
|
||
"status": agent.status
|
||
},
|
||
"state": {
|
||
"current_task": state.current_task if state else None,
|
||
"progress": state.progress if state else 0
|
||
} if state else None,
|
||
"heartbeat": {
|
||
"status": heartbeat.status if heartbeat else None,
|
||
"last_seen": heartbeat.last_seen.isoformat() if heartbeat else None,
|
||
"is_alive": heartbeat.is_alive() if heartbeat else False
|
||
} if heartbeat else None
|
||
}
|
||
|
||
async def update_heartbeat(self, status: str, task: str = "", progress: int = 0) -> None:
|
||
"""
|
||
更新心跳
|
||
|
||
参数:
|
||
status: working, waiting, idle, error
|
||
task: 当前任务描述
|
||
progress: 进度 0-100
|
||
"""
|
||
await self.heartbeat_service.update_heartbeat(
|
||
self._id,
|
||
status,
|
||
task,
|
||
progress
|
||
)
|
||
|
||
# ========== 生命周期 ==========
|
||
|
||
async def initialize(self) -> None:
|
||
"""Agent 初始化"""
|
||
logger.info(f"Agent 初始化: {self._id}")
|
||
|
||
# 确保已注册
|
||
await self._ensure_registered()
|
||
|
||
# 发送初始心跳
|
||
await self.update_heartbeat("idle", "", 0)
|
||
|
||
self._is_running = True
|
||
|
||
async def shutdown(self) -> None:
|
||
"""Agent 关闭"""
|
||
logger.info(f"Agent 关闭: {self._id}")
|
||
|
||
# 更新状态为离线
|
||
await self.update_heartbeat("offline", "", 0)
|
||
|
||
self._is_running = False
|
||
|
||
async def health_check(self) -> bool:
|
||
"""健康检查"""
|
||
try:
|
||
heartbeat = await self.heartbeat_service.get_heartbeat(self._id)
|
||
return heartbeat is not None and heartbeat.is_alive()
|
||
except Exception:
|
||
return False
|
||
|
||
# ========== 会议相关 ==========
|
||
|
||
async def propose(self, meeting_id: str, content: str, step: str = "") -> None:
|
||
"""在会议中提出提案"""
|
||
from ..services.meeting_recorder import get_meeting_recorder
|
||
recorder = get_meeting_recorder()
|
||
|
||
await recorder.add_discussion(
|
||
meeting_id,
|
||
self._id,
|
||
self._role.upper(),
|
||
content,
|
||
step
|
||
)
|
||
|
||
logger.debug(f"提案已添加: {self._id} -> {meeting_id}")
|
||
|
||
async def discuss(self, meeting_id: str, content: str, step: str = "") -> None:
|
||
"""在会议中参与讨论"""
|
||
await self.propose(meeting_id, content, step)
|
||
|
||
async def vote(self, meeting_id: str, proposal_id: str, agree: bool) -> None:
|
||
"""对提案进行投票"""
|
||
# TODO: 实现投票机制
|
||
logger.debug(f"投票: {self._id} -> {proposal_id}: {agree}")
|
||
|
||
# ========== 私有方法 ==========
|
||
|
||
async def _ensure_registered(self):
|
||
"""确保 Agent 已注册"""
|
||
agent = await self.registry.get_agent(self._id)
|
||
|
||
if agent is None:
|
||
# 注册 Agent
|
||
agent = await self.registry.register_agent(
|
||
self._id,
|
||
self._name,
|
||
self._role,
|
||
self._model,
|
||
self.config.get("description", f"{self._name} - {self._role}")
|
||
)
|
||
logger.info(f"Agent 已注册: {self._id}")
|
||
|
||
return agent
|
||
|
||
# ========== 高级功能 ==========
|
||
|
||
async def collaborate_with(
|
||
self,
|
||
other_agent_ids: list,
|
||
task: str,
|
||
meeting_id: str = None
|
||
) -> Dict:
|
||
"""
|
||
与其他 Agent 协作完成任务
|
||
|
||
流程:
|
||
1. 创建或加入会议
|
||
2. 等待所有 Agent 到达
|
||
3. 讨论和分工
|
||
4. 执行分配的任务
|
||
5. 汇总结果
|
||
"""
|
||
if not meeting_id:
|
||
# 生成会议 ID
|
||
import uuid
|
||
meeting_id = f"meeting_{uuid.uuid4().hex[:12]}"
|
||
|
||
# 创建会议
|
||
await self.scheduler.create_meeting(
|
||
meeting_id,
|
||
f"协作任务: {task[:50]}",
|
||
[self._id] + other_agent_ids
|
||
)
|
||
|
||
# 更新心跳
|
||
await self.update_heartbeat("waiting", f"等待协作会议: {meeting_id}", 0)
|
||
|
||
# 提出初始提案
|
||
await self.propose(meeting_id, f"任务: {task}")
|
||
|
||
return {
|
||
"meeting_id": meeting_id,
|
||
"status": "waiting",
|
||
"participants": [self._id] + other_agent_ids
|
||
}
|
||
|
||
async def start_collaboration_loop(
|
||
self,
|
||
meeting_id: str,
|
||
max_iterations: int = 5
|
||
) -> Dict:
|
||
"""
|
||
启动协作循环
|
||
|
||
持续参与会议讨论,直到达成共识或达到最大迭代次数
|
||
"""
|
||
from ..services.meeting_recorder import get_meeting_recorder
|
||
recorder = get_meeting_recorder()
|
||
|
||
for iteration in range(max_iterations):
|
||
# 等待会议开始
|
||
result = await self.join_meeting(meeting_id)
|
||
|
||
if result == "started":
|
||
# 获取会议信息
|
||
meeting = await recorder.get_meeting(meeting_id)
|
||
|
||
if meeting and meeting.status == "completed":
|
||
return {
|
||
"status": "consensus_reached",
|
||
"consensus": meeting.consensus,
|
||
"iterations": iteration + 1
|
||
}
|
||
|
||
# 分析当前讨论,提出自己的观点
|
||
await self._analyze_and_respond(meeting_id)
|
||
|
||
elif result == "timeout":
|
||
return {
|
||
"status": "timeout",
|
||
"iterations": iteration + 1
|
||
}
|
||
|
||
return {
|
||
"status": "max_iterations_reached",
|
||
"iterations": max_iterations
|
||
}
|
||
|
||
async def _analyze_and_respond(self, meeting_id: str) -> None:
|
||
"""分析会议讨论并响应"""
|
||
from ..services.meeting_recorder import get_meeting_recorder
|
||
recorder = get_meeting_recorder()
|
||
|
||
meeting = await recorder.get_meeting(meeting_id)
|
||
if not meeting:
|
||
return
|
||
|
||
# 获取最近的讨论
|
||
recent_discussions = meeting.discussions[-5:] if meeting.discussions else []
|
||
|
||
# 构建分析提示
|
||
discussion_summary = "\n".join([
|
||
f"{d.agent} ({d.timestamp}): {d.content}"
|
||
for d in recent_discussions
|
||
])
|
||
|
||
response_prompt = f"""
|
||
你是 {self._name} ({self._role})。
|
||
|
||
以下是会议讨论的摘要:
|
||
{discussion_summary}
|
||
|
||
请基于你的角色 ({self._role}),给出你的回应:
|
||
- 如果你不同意之前的提案,说明理由
|
||
- 如果你有更好的建议,提出新方案
|
||
- 如果你同意,可以表示支持或补充细节
|
||
|
||
保持简洁,直接回应。
|
||
"""
|
||
|
||
# 调用 LLM 生成响应
|
||
if self.llm_service:
|
||
try:
|
||
llm_response = await self.llm_service.route_task(
|
||
task=response_prompt,
|
||
messages=[
|
||
{"role": "system", "content": f"你是 {self._name},一个 {self._role}。"},
|
||
{"role": "user", "content": response_prompt}
|
||
]
|
||
)
|
||
|
||
# 发送响应到会议
|
||
await self.discuss(meeting_id, llm_response.content)
|
||
|
||
except Exception as e:
|
||
logger.error(f"生成会议响应失败: {e}")
|
||
|
||
|
||
class NativeLLMAgentFactory:
|
||
"""原生 LLM Agent 工厂类"""
|
||
|
||
@staticmethod
|
||
async def create(
|
||
agent_id: str,
|
||
name: str = None,
|
||
role: str = "developer",
|
||
model: str = "claude-sonnet-4.6",
|
||
config: Dict = None
|
||
) -> NativeLLMAgent:
|
||
"""
|
||
创建并初始化一个 Agent
|
||
|
||
参数:
|
||
agent_id: Agent 唯一标识
|
||
name: 显示名称(默认从 agent_id 生成)
|
||
role: 角色 (architect, pm, developer, qa, reviewer)
|
||
model: 使用的模型
|
||
config: 额外配置
|
||
"""
|
||
if name is None:
|
||
name = agent_id.replace("-", " ").title()
|
||
|
||
agent = NativeLLMAgent(
|
||
agent_id=agent_id,
|
||
name=name,
|
||
role=role,
|
||
model=model,
|
||
config=config
|
||
)
|
||
|
||
# 初始化 Agent
|
||
await agent.initialize()
|
||
|
||
return agent
|
||
|
||
@staticmethod
|
||
async def create_team(team_config: Dict) -> Dict[str, NativeLLMAgent]:
|
||
"""
|
||
创建一个 Agent 团队
|
||
|
||
配置格式:
|
||
{
|
||
"team_id": "dev-team-1",
|
||
"agents": [
|
||
{"id": "arch-001", "role": "architect", "model": "claude-opus-4.6"},
|
||
{"id": "dev-001", "role": "developer", "model": "claude-sonnet-4.6"},
|
||
{"id": "qa-001", "role": "qa", "model": "claude-haiku-4.6"}
|
||
]
|
||
}
|
||
"""
|
||
agents = {}
|
||
|
||
for agent_config in team_config.get("agents", []):
|
||
agent = await NativeLLMAgentFactory.create(
|
||
agent_id=agent_config["id"],
|
||
role=agent_config.get("role", "developer"),
|
||
model=agent_config.get("model", "claude-sonnet-4.6"),
|
||
config=agent_config.get("config", {})
|
||
)
|
||
agents[agent.id] = agent
|
||
|
||
return agents
|