完整实现 Swarm 多智能体协作系统
- 新增 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>
This commit is contained in:
1
backend/app/__init__.py
Normal file
1
backend/app/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Swarm Command Center Backend App"""
|
||||
8
backend/app/adapters/__init__.py
Normal file
8
backend/app/adapters/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
"""Agent 适配器模块"""
|
||||
|
||||
from .native_llm_agent import NativeLLMAgent, NativeLLMAgentFactory
|
||||
|
||||
__all__ = [
|
||||
"NativeLLMAgent",
|
||||
"NativeLLMAgentFactory"
|
||||
]
|
||||
497
backend/app/adapters/native_llm_agent.py
Normal file
497
backend/app/adapters/native_llm_agent.py
Normal file
@@ -0,0 +1,497 @@
|
||||
"""
|
||||
原生 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
|
||||
23
backend/app/core/__init__.py
Normal file
23
backend/app/core/__init__.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""Swarm 核心模块"""
|
||||
|
||||
from .agent_adapter import (
|
||||
CLIPluginAdapter,
|
||||
Task,
|
||||
Result,
|
||||
AgentCapabilities,
|
||||
AdapterError,
|
||||
AdapterConnectionError,
|
||||
AdapterExecutionError,
|
||||
AdapterTimeoutError
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"CLIPluginAdapter",
|
||||
"Task",
|
||||
"Result",
|
||||
"AgentCapabilities",
|
||||
"AdapterError",
|
||||
"AdapterConnectionError",
|
||||
"AdapterExecutionError",
|
||||
"AdapterTimeoutError"
|
||||
]
|
||||
224
backend/app/core/agent_adapter.py
Normal file
224
backend/app/core/agent_adapter.py
Normal file
@@ -0,0 +1,224 @@
|
||||
"""
|
||||
Swarm Agent 适配器核心接口
|
||||
|
||||
定义所有 Agent 适配器必须实现的统一接口,确保不同类型的 Agent
|
||||
(原生 LLM Agent、外部 CLI 工具包装 Agent 等)能够无缝协作。
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, Optional, Any
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
import asyncio
|
||||
|
||||
|
||||
@dataclass
|
||||
class Task:
|
||||
"""Agent 任务描述"""
|
||||
description: str
|
||||
task_id: str
|
||||
context: Dict[str, Any] = field(default_factory=dict)
|
||||
priority: str = "medium" # high, medium, low
|
||||
deadline: Optional[datetime] = None
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> Dict:
|
||||
return {
|
||||
"task_id": self.task_id,
|
||||
"description": self.description,
|
||||
"context": self.context,
|
||||
"priority": self.priority,
|
||||
"deadline": self.deadline.isoformat() if self.deadline else None,
|
||||
"metadata": self.metadata
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class Result:
|
||||
"""Agent 任务执行结果"""
|
||||
success: bool
|
||||
output: str
|
||||
error: Optional[str] = None
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
execution_time: float = 0.0
|
||||
tokens_used: int = 0
|
||||
|
||||
def to_dict(self) -> Dict:
|
||||
return {
|
||||
"success": self.success,
|
||||
"output": self.output,
|
||||
"error": self.error,
|
||||
"metadata": self.metadata,
|
||||
"execution_time": self.execution_time,
|
||||
"tokens_used": self.tokens_used
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentCapabilities:
|
||||
"""Agent 能力声明"""
|
||||
can_execute_code: bool = False
|
||||
can_read_files: bool = False
|
||||
can_write_files: bool = False
|
||||
can_analyze_code: bool = False
|
||||
can_generate_tests: bool = False
|
||||
can_review_code: bool = False
|
||||
supported_languages: list = field(default_factory=list)
|
||||
max_context_length: int = 200000
|
||||
|
||||
|
||||
class CLIPluginAdapter(ABC):
|
||||
"""
|
||||
CLI 工具适配器统一接口
|
||||
|
||||
所有 Agent 适配器必须实现此接口,确保:
|
||||
1. 统一的任务执行方式
|
||||
2. 一致的会议参与机制
|
||||
3. 标准化的状态管理
|
||||
4. 可靠的心跳报告
|
||||
"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def id(self) -> str:
|
||||
"""Agent 唯一标识符"""
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def name(self) -> str:
|
||||
"""Agent 显示名称"""
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def version(self) -> str:
|
||||
"""适配器版本号"""
|
||||
pass
|
||||
|
||||
@property
|
||||
def capabilities(self) -> AgentCapabilities:
|
||||
"""Agent 能力声明"""
|
||||
return AgentCapabilities()
|
||||
|
||||
# ========== 核心能力 ==========
|
||||
|
||||
@abstractmethod
|
||||
async def execute(self, task: Task) -> Result:
|
||||
"""
|
||||
执行任务
|
||||
|
||||
这是 Agent 的核心方法,负责:
|
||||
1. 解析任务描述
|
||||
2. 调用适当的模型/API
|
||||
3. 处理执行结果
|
||||
4. 返回标准化结果
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def join_meeting(self, meeting_id: str, timeout: int = 300) -> str:
|
||||
"""
|
||||
加入会议等待队列(栅栏同步)
|
||||
|
||||
当 Agent 需要参与会议时调用此方法:
|
||||
1. 向协调服务报告"我准备好了"
|
||||
2. 等待其他参与者到达
|
||||
3. 当所有人都到达时,会议自动开始
|
||||
|
||||
返回值:
|
||||
- "started": 会议已开始
|
||||
- "timeout": 等待超时
|
||||
- "cancelled": 会议被取消
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def write_state(self, state: Dict) -> None:
|
||||
"""
|
||||
写入自己的状态文件
|
||||
|
||||
状态文件存储在 .doc/agents/{agent_id}/state.json
|
||||
包含:当前任务、进度、临时数据等
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def read_others(self, agent_id: str) -> Dict:
|
||||
"""
|
||||
读取其他 Agent 的状态
|
||||
|
||||
用于 Agent 之间互相了解对方的工作状态
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def update_heartbeat(self, status: str, task: str = "", progress: int = 0) -> None:
|
||||
"""
|
||||
更新心跳
|
||||
|
||||
参数:
|
||||
- status: working, waiting, idle, error
|
||||
- task: 当前任务描述
|
||||
- progress: 进度百分比 0-100
|
||||
"""
|
||||
pass
|
||||
|
||||
# ========== 生命周期钩子 ==========
|
||||
|
||||
async def initialize(self) -> None:
|
||||
"""Agent 初始化时调用"""
|
||||
pass
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
"""Agent 关闭前调用"""
|
||||
pass
|
||||
|
||||
async def health_check(self) -> bool:
|
||||
"""健康检查"""
|
||||
return True
|
||||
|
||||
# ========== 会议相关 ==========
|
||||
|
||||
async def propose(self, meeting_id: str, content: str, step: str = "") -> None:
|
||||
"""在会议中提出提案"""
|
||||
pass
|
||||
|
||||
async def discuss(self, meeting_id: str, content: str, step: str = "") -> None:
|
||||
"""在会议中参与讨论"""
|
||||
pass
|
||||
|
||||
async def vote(self, meeting_id: str, proposal_id: str, agree: bool) -> None:
|
||||
"""对提案进行投票"""
|
||||
pass
|
||||
|
||||
# ========== 工具方法 ==========
|
||||
|
||||
def _generate_task_id(self) -> str:
|
||||
"""生成唯一任务 ID"""
|
||||
import uuid
|
||||
return f"task_{uuid.uuid4().hex[:12]}"
|
||||
|
||||
async def _delay(self, seconds: float):
|
||||
"""异步延迟"""
|
||||
await asyncio.sleep(seconds)
|
||||
|
||||
|
||||
class AdapterError(Exception):
|
||||
"""适配器错误基类"""
|
||||
pass
|
||||
|
||||
|
||||
class AdapterConnectionError(AdapterError):
|
||||
"""连接错误"""
|
||||
pass
|
||||
|
||||
|
||||
class AdapterExecutionError(AdapterError):
|
||||
"""执行错误"""
|
||||
pass
|
||||
|
||||
|
||||
class AdapterTimeoutError(AdapterError):
|
||||
"""超时错误"""
|
||||
pass
|
||||
75
backend/app/main.py
Normal file
75
backend/app/main.py
Normal file
@@ -0,0 +1,75 @@
|
||||
"""
|
||||
Swarm Command Center - FastAPI 主入口
|
||||
多智能体协作系统的协调层后端服务
|
||||
"""
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
import uvicorn
|
||||
|
||||
from app.routers import agents, locks, meetings, heartbeats, workflows, resources, roles, humans
|
||||
from app.routers import agents_control, websocket
|
||||
|
||||
# 创建 FastAPI 应用实例
|
||||
app = FastAPI(
|
||||
title="Swarm Command Center API",
|
||||
description="多智能体协作系统的协调层后端服务",
|
||||
version="0.1.0",
|
||||
)
|
||||
|
||||
# 配置 CORS - 允许前端访问
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["http://localhost:3000", "http://127.0.0.1:3000"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
# 基础健康检查端点
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""健康检查端点"""
|
||||
return {"status": "ok", "service": "Swarm Command Center"}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
@app.get("/api/health")
|
||||
async def health():
|
||||
"""详细健康检查"""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"version": "0.1.0",
|
||||
"services": {
|
||||
"api": "ok",
|
||||
"storage": "ok",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# 注册 API 路由
|
||||
app.include_router(agents.router, prefix="/api/agents", tags=["agents"])
|
||||
app.include_router(agents_control.router, tags=["agents-control"])
|
||||
app.include_router(locks.router, prefix="/api/locks", tags=["locks"])
|
||||
app.include_router(meetings.router, prefix="/api/meetings", tags=["meetings"])
|
||||
app.include_router(heartbeats.router, prefix="/api/heartbeats", tags=["heartbeats"])
|
||||
app.include_router(workflows.router, prefix="/api/workflows", tags=["workflows"])
|
||||
app.include_router(resources.router, prefix="/api", tags=["resources"])
|
||||
app.include_router(roles.router, prefix="/api/roles", tags=["roles"])
|
||||
app.include_router(humans.router, prefix="/api/humans", tags=["humans"])
|
||||
app.include_router(websocket.router, tags=["websocket"])
|
||||
|
||||
|
||||
def main():
|
||||
"""启动开发服务器"""
|
||||
uvicorn.run(
|
||||
"app.main:app",
|
||||
host="0.0.0.0",
|
||||
port=8000,
|
||||
reload=True,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
17
backend/app/routers/__init__.py
Normal file
17
backend/app/routers/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""API 路由模块"""
|
||||
|
||||
from . import agents, locks, meetings, heartbeats, workflows, resources, roles, humans
|
||||
from . import agents_control, websocket
|
||||
|
||||
__all__ = [
|
||||
"agents",
|
||||
"locks",
|
||||
"meetings",
|
||||
"heartbeats",
|
||||
"workflows",
|
||||
"resources",
|
||||
"roles",
|
||||
"humans",
|
||||
"agents_control",
|
||||
"websocket"
|
||||
]
|
||||
166
backend/app/routers/agents.py
Normal file
166
backend/app/routers/agents.py
Normal file
@@ -0,0 +1,166 @@
|
||||
"""
|
||||
Agent 管理 API 路由
|
||||
"""
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
import time
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# 内存存储,实际应用应该使用持久化存储
|
||||
agents_db = {}
|
||||
|
||||
|
||||
class Agent(BaseModel):
|
||||
agent_id: str
|
||||
name: str
|
||||
role: str
|
||||
model: str
|
||||
description: Optional[str] = None
|
||||
status: str = "idle"
|
||||
created_at: float = 0
|
||||
|
||||
|
||||
class AgentCreate(BaseModel):
|
||||
agent_id: str
|
||||
name: str
|
||||
role: str = "developer"
|
||||
model: str
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
# Agent状态存储
|
||||
agent_states_db = {}
|
||||
|
||||
@router.get("/")
|
||||
async def list_agents():
|
||||
"""获取所有 Agent 列表"""
|
||||
# 合并数据库和默认agent
|
||||
default_agents = [
|
||||
{
|
||||
"agent_id": "claude-001",
|
||||
"name": "Claude Code",
|
||||
"role": "developer",
|
||||
"model": "claude-opus-4.6",
|
||||
"status": "working",
|
||||
"description": "主开发 Agent",
|
||||
"created_at": time.time() - 86400
|
||||
},
|
||||
{
|
||||
"agent_id": "kimi-001",
|
||||
"name": "Kimi CLI",
|
||||
"role": "architect",
|
||||
"model": "kimi-k2",
|
||||
"status": "idle",
|
||||
"description": "架构设计 Agent",
|
||||
"created_at": time.time() - 72000
|
||||
},
|
||||
{
|
||||
"agent_id": "opencode-001",
|
||||
"name": "OpenCode",
|
||||
"role": "reviewer",
|
||||
"model": "opencode-v1",
|
||||
"status": "idle",
|
||||
"description": "代码审查 Agent",
|
||||
"created_at": time.time() - 36000
|
||||
}
|
||||
]
|
||||
|
||||
# 使用数据库中的agent(覆盖默认的)
|
||||
agents_map = {a["agent_id"]: a for a in default_agents}
|
||||
agents_map.update(agents_db)
|
||||
|
||||
return {"agents": list(agents_map.values())}
|
||||
|
||||
|
||||
@router.post("/register")
|
||||
async def register_agent(agent: AgentCreate):
|
||||
"""注册新 Agent"""
|
||||
agent_data = {
|
||||
"agent_id": agent.agent_id,
|
||||
"name": agent.name,
|
||||
"role": agent.role,
|
||||
"model": agent.model,
|
||||
"description": agent.description or "",
|
||||
"status": "idle",
|
||||
"created_at": time.time()
|
||||
}
|
||||
agents_db[agent.agent_id] = agent_data
|
||||
return agent_data
|
||||
|
||||
|
||||
@router.get("/{agent_id}")
|
||||
async def get_agent(agent_id: str):
|
||||
"""获取指定 Agent 信息"""
|
||||
if agent_id in agents_db:
|
||||
return agents_db[agent_id]
|
||||
raise HTTPException(status_code=404, detail="Agent not found")
|
||||
|
||||
|
||||
@router.delete("/{agent_id}")
|
||||
async def delete_agent(agent_id: str):
|
||||
"""删除 Agent"""
|
||||
if agent_id in agents_db:
|
||||
del agents_db[agent_id]
|
||||
return {"message": "Agent deleted"}
|
||||
raise HTTPException(status_code=404, detail="Agent not found")
|
||||
|
||||
|
||||
@router.get("/{agent_id}/state")
|
||||
async def get_agent_state(agent_id: str):
|
||||
"""获取 Agent 状态"""
|
||||
# 如果存在真实状态,返回真实状态
|
||||
if agent_id in agent_states_db:
|
||||
return agent_states_db[agent_id]
|
||||
|
||||
# 默认mock状态
|
||||
default_states = {
|
||||
"claude-001": {
|
||||
"agent_id": agent_id,
|
||||
"task": "修复用户登录bug",
|
||||
"progress": 65,
|
||||
"working_files": ["src/auth/login.py", "src/auth/jwt.py"],
|
||||
"status": "working",
|
||||
"last_update": time.time() - 120
|
||||
},
|
||||
"kimi-001": {
|
||||
"agent_id": agent_id,
|
||||
"task": "等待会议开始",
|
||||
"progress": 0,
|
||||
"working_files": [],
|
||||
"status": "waiting",
|
||||
"last_update": time.time() - 300
|
||||
},
|
||||
"opencode-001": {
|
||||
"agent_id": agent_id,
|
||||
"task": "代码审查",
|
||||
"progress": 30,
|
||||
"working_files": ["src/components/Button.tsx"],
|
||||
"status": "working",
|
||||
"last_update": time.time() - 60
|
||||
}
|
||||
}
|
||||
|
||||
return default_states.get(agent_id, {
|
||||
"agent_id": agent_id,
|
||||
"task": "空闲",
|
||||
"progress": 0,
|
||||
"working_files": [],
|
||||
"status": "idle",
|
||||
"last_update": time.time()
|
||||
})
|
||||
|
||||
|
||||
@router.post("/{agent_id}/state")
|
||||
async def update_agent_state(agent_id: str, data: dict):
|
||||
"""更新 Agent 状态"""
|
||||
agent_states_db[agent_id] = {
|
||||
"agent_id": agent_id,
|
||||
"task": data.get("task", ""),
|
||||
"progress": data.get("progress", 0),
|
||||
"working_files": data.get("working_files", []),
|
||||
"status": data.get("status", "idle"),
|
||||
"last_update": time.time()
|
||||
}
|
||||
return {"success": True}
|
||||
391
backend/app/routers/agents_control.py
Normal file
391
backend/app/routers/agents_control.py
Normal file
@@ -0,0 +1,391 @@
|
||||
"""
|
||||
Agent 控制 API 路由
|
||||
|
||||
提供 Agent 启动、停止、状态查询等控制接口
|
||||
"""
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
from typing import Dict, List, Optional, Any
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, HTTPException, BackgroundTasks
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from ..services.process_manager import get_process_manager, AgentStatus
|
||||
from ..services.agent_registry import get_agent_registry
|
||||
from ..services.heartbeat import get_heartbeat_service
|
||||
from ..adapters.native_llm_agent import NativeLLMAgent, NativeLLMAgentFactory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/api/agents/control", tags=["agents-control"])
|
||||
|
||||
|
||||
# ========== 请求模型 ==========
|
||||
|
||||
|
||||
class StartAgentRequest(BaseModel):
|
||||
"""启动 Agent 请求"""
|
||||
agent_id: str = Field(..., description="Agent 唯一标识")
|
||||
name: Optional[str] = Field(None, description="Agent 显示名称")
|
||||
role: str = Field("developer", description="Agent 角色")
|
||||
model: str = Field("claude-sonnet-4.6", description="使用的模型")
|
||||
agent_type: str = Field("native_llm", description="Agent 类型")
|
||||
config: Dict[str, Any] = Field(default_factory=dict, description="额外配置")
|
||||
|
||||
|
||||
class StopAgentRequest(BaseModel):
|
||||
"""停止 Agent 请求"""
|
||||
agent_id: str = Field(..., description="Agent ID")
|
||||
graceful: bool = Field(True, description="是否优雅关闭")
|
||||
|
||||
|
||||
class ExecuteTaskRequest(BaseModel):
|
||||
"""执行任务请求"""
|
||||
agent_id: str = Field(..., description="Agent ID")
|
||||
task_description: str = Field(..., description="任务描述")
|
||||
context: Dict[str, Any] = Field(default_factory=dict, description="任务上下文")
|
||||
|
||||
|
||||
class CreateMeetingRequest(BaseModel):
|
||||
"""创建会议请求"""
|
||||
meeting_id: str = Field(..., description="会议 ID")
|
||||
title: str = Field(..., description="会议标题")
|
||||
attendees: List[str] = Field(..., description="参会 Agent ID 列表")
|
||||
|
||||
|
||||
class JoinMeetingRequest(BaseModel):
|
||||
"""加入会议请求"""
|
||||
agent_id: str = Field(..., description="Agent ID")
|
||||
meeting_id: str = Field(..., description="会议 ID")
|
||||
timeout: int = Field(300, description="等待超时时间(秒)")
|
||||
|
||||
|
||||
# ========== 响应模型 ==========
|
||||
|
||||
|
||||
class AgentControlResponse(BaseModel):
|
||||
"""Agent 控制响应"""
|
||||
success: bool
|
||||
agent_id: str
|
||||
status: str
|
||||
message: str
|
||||
|
||||
|
||||
class AgentStatusResponse(BaseModel):
|
||||
"""Agent 状态响应"""
|
||||
agent_id: str
|
||||
status: str
|
||||
is_alive: bool
|
||||
uptime: Optional[float] = None
|
||||
restart_count: int = 0
|
||||
|
||||
|
||||
class ProcessManagerSummary(BaseModel):
|
||||
"""进程管理器摘要"""
|
||||
total_agents: int
|
||||
running_agents: int
|
||||
running_agent_ids: List[str]
|
||||
status_counts: Dict[str, int]
|
||||
monitor_running: bool
|
||||
|
||||
|
||||
# ========== API 端点 ==========
|
||||
|
||||
|
||||
@router.post("/start", response_model=AgentControlResponse)
|
||||
async def start_agent(request: StartAgentRequest, background_tasks: BackgroundTasks):
|
||||
"""
|
||||
启动 Agent
|
||||
|
||||
启动一个新的 Agent 实例,支持两种类型:
|
||||
- native_llm: 原生 LLM Agent(异步任务)
|
||||
- process_wrapper: 进程包装 Agent(外部 CLI 工具)
|
||||
"""
|
||||
process_manager = get_process_manager()
|
||||
|
||||
# 检查是否已在运行
|
||||
if request.agent_id in process_manager.get_all_agents():
|
||||
existing = process_manager.get_agent_status(request.agent_id)
|
||||
if existing != AgentStatus.STOPPED:
|
||||
return AgentControlResponse(
|
||||
success=False,
|
||||
agent_id=request.agent_id,
|
||||
status=existing.value,
|
||||
message="Agent 已在运行"
|
||||
)
|
||||
|
||||
# 准备配置
|
||||
config = request.config.copy()
|
||||
config["name"] = request.name or request.agent_id.replace("-", " ").title()
|
||||
config["role"] = request.role
|
||||
config["model"] = request.model
|
||||
|
||||
# 启动 Agent
|
||||
success = await process_manager.start_agent(
|
||||
agent_id=request.agent_id,
|
||||
agent_type=request.agent_type,
|
||||
config=config
|
||||
)
|
||||
|
||||
if success:
|
||||
return AgentControlResponse(
|
||||
success=True,
|
||||
agent_id=request.agent_id,
|
||||
status=AgentStatus.RUNNING.value,
|
||||
message=f"Agent {request.agent_id} 启动成功"
|
||||
)
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail="启动 Agent 失败")
|
||||
|
||||
|
||||
@router.post("/stop", response_model=AgentControlResponse)
|
||||
async def stop_agent(request: StopAgentRequest):
|
||||
"""停止 Agent"""
|
||||
process_manager = get_process_manager()
|
||||
|
||||
success = await process_manager.stop_agent(
|
||||
agent_id=request.agent_id,
|
||||
graceful=request.graceful
|
||||
)
|
||||
|
||||
if success:
|
||||
return AgentControlResponse(
|
||||
success=True,
|
||||
agent_id=request.agent_id,
|
||||
status=AgentStatus.STOPPED.value,
|
||||
message=f"Agent {request.agent_id} 已停止"
|
||||
)
|
||||
else:
|
||||
return AgentControlResponse(
|
||||
success=False,
|
||||
agent_id=request.agent_id,
|
||||
status=AgentStatus.UNKNOWN.value,
|
||||
message=f"停止 Agent 失败或 Agent 未运行"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/restart", response_model=AgentControlResponse)
|
||||
async def restart_agent(agent_id: str):
|
||||
"""重启 Agent"""
|
||||
process_manager = get_process_manager()
|
||||
|
||||
success = await process_manager.restart_agent(agent_id)
|
||||
|
||||
if success:
|
||||
return AgentControlResponse(
|
||||
success=True,
|
||||
agent_id=agent_id,
|
||||
status=AgentStatus.RUNNING.value,
|
||||
message=f"Agent {agent_id} 重启成功"
|
||||
)
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail="重启 Agent 失败")
|
||||
|
||||
|
||||
@router.get("/status/{agent_id}", response_model=AgentStatusResponse)
|
||||
async def get_agent_status(agent_id: str):
|
||||
"""获取 Agent 状态"""
|
||||
process_manager = get_process_manager()
|
||||
|
||||
status = process_manager.get_agent_status(agent_id)
|
||||
all_agents = process_manager.get_all_agents()
|
||||
|
||||
if agent_id in all_agents:
|
||||
process_info = all_agents[agent_id]
|
||||
return AgentStatusResponse(
|
||||
agent_id=agent_id,
|
||||
status=status.value,
|
||||
is_alive=process_info.is_alive,
|
||||
uptime=process_info.uptime,
|
||||
restart_count=process_info.restart_count
|
||||
)
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail="Agent 不存在")
|
||||
|
||||
|
||||
@router.get("/list", response_model=List[AgentStatusResponse])
|
||||
async def list_agents():
|
||||
"""列出所有 Agent 状态"""
|
||||
process_manager = get_process_manager()
|
||||
heartbeat_service = get_heartbeat_service()
|
||||
|
||||
agents = []
|
||||
for agent_id, process_info in process_manager.get_all_agents().items():
|
||||
# 获取心跳信息
|
||||
heartbeat = await heartbeat_service.get_heartbeat(agent_id)
|
||||
|
||||
agents.append(AgentStatusResponse(
|
||||
agent_id=agent_id,
|
||||
status=process_info.status.value,
|
||||
is_alive=process_info.is_alive,
|
||||
uptime=process_info.uptime,
|
||||
restart_count=process_info.restart_count
|
||||
))
|
||||
|
||||
return agents
|
||||
|
||||
|
||||
@router.get("/summary", response_model=ProcessManagerSummary)
|
||||
async def get_summary():
|
||||
"""获取进程管理器摘要"""
|
||||
process_manager = get_process_manager()
|
||||
summary = process_manager.get_summary()
|
||||
|
||||
return ProcessManagerSummary(**summary)
|
||||
|
||||
|
||||
@router.post("/execute")
|
||||
async def execute_task(request: ExecuteTaskRequest):
|
||||
"""
|
||||
让 Agent 执行任务
|
||||
|
||||
Agent 会自动:
|
||||
1. 分析任务,识别需要的文件
|
||||
2. 获取文件锁
|
||||
3. 调用 LLM 执行任务
|
||||
4. 释放文件锁
|
||||
"""
|
||||
process_manager = get_process_manager()
|
||||
|
||||
# 检查 Agent 是否运行
|
||||
all_agents = process_manager.get_all_agents()
|
||||
if request.agent_id not in all_agents:
|
||||
raise HTTPException(status_code=404, detail="Agent 未运行")
|
||||
|
||||
process_info = all_agents[request.agent_id]
|
||||
if not process_info.is_alive:
|
||||
raise HTTPException(status_code=400, detail="Agent 未运行")
|
||||
|
||||
# 获取 Agent 实例
|
||||
agent = process_info.agent
|
||||
if not agent:
|
||||
raise HTTPException(status_code=500, detail="Agent 实例不可用")
|
||||
|
||||
# 创建任务
|
||||
from ..core.agent_adapter import Task
|
||||
task = Task(
|
||||
task_id=f"task_{uuid.uuid4().hex[:12]}",
|
||||
description=request.task_description,
|
||||
context=request.context
|
||||
)
|
||||
|
||||
# 执行任务
|
||||
result = await agent.execute(task)
|
||||
|
||||
return {
|
||||
"success": result.success,
|
||||
"output": result.output,
|
||||
"error": result.error,
|
||||
"metadata": result.metadata,
|
||||
"execution_time": result.execution_time
|
||||
}
|
||||
|
||||
|
||||
@router.post("/meeting/create")
|
||||
async def create_meeting(request: CreateMeetingRequest):
|
||||
"""创建协作会议"""
|
||||
from ..services.meeting_scheduler import get_meeting_scheduler
|
||||
scheduler = get_meeting_scheduler()
|
||||
|
||||
queue = await scheduler.create_meeting(
|
||||
request.meeting_id,
|
||||
request.title,
|
||||
request.attendees
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"meeting_id": request.meeting_id,
|
||||
"title": queue.title,
|
||||
"expected_attendees": queue.expected_attendees,
|
||||
"min_required": queue.min_required,
|
||||
"status": queue.status
|
||||
}
|
||||
|
||||
|
||||
@router.post("/meeting/join")
|
||||
async def join_meeting(request: JoinMeetingRequest):
|
||||
"""让 Agent 加入会议(栅栏同步)"""
|
||||
process_manager = get_process_manager()
|
||||
|
||||
# 检查 Agent 是否运行
|
||||
all_agents = process_manager.get_all_agents()
|
||||
if request.agent_id not in all_agents:
|
||||
raise HTTPException(status_code=404, detail="Agent 未运行")
|
||||
|
||||
process_info = all_agents[request.agent_id]
|
||||
agent = process_info.agent
|
||||
if not agent:
|
||||
raise HTTPException(status_code=500, detail="Agent 实例不可用")
|
||||
|
||||
# 加入会议
|
||||
result = await agent.join_meeting(request.meeting_id, request.timeout)
|
||||
|
||||
return {
|
||||
"agent_id": request.agent_id,
|
||||
"meeting_id": request.meeting_id,
|
||||
"result": result
|
||||
}
|
||||
|
||||
|
||||
@router.post("/shutdown-all")
|
||||
async def shutdown_all_agents():
|
||||
"""关闭所有 Agent"""
|
||||
process_manager = get_process_manager()
|
||||
await process_manager.shutdown_all()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "所有 Agent 已关闭"
|
||||
}
|
||||
|
||||
|
||||
@router.post("/batch-start")
|
||||
async def batch_start_agents(agents: List[StartAgentRequest]):
|
||||
"""
|
||||
批量启动 Agent
|
||||
|
||||
用于快速创建团队
|
||||
"""
|
||||
results = []
|
||||
process_manager = get_process_manager()
|
||||
|
||||
for agent_request in agents:
|
||||
config = agent_request.config.copy()
|
||||
config["name"] = agent_request.name or agent_request.agent_id.replace("-", " ").title()
|
||||
config["role"] = agent_request.role
|
||||
config["model"] = agent_request.model
|
||||
|
||||
success = await process_manager.start_agent(
|
||||
agent_id=agent_request.agent_id,
|
||||
agent_type=agent_request.agent_type,
|
||||
config=config
|
||||
)
|
||||
|
||||
results.append({
|
||||
"agent_id": agent_request.agent_id,
|
||||
"success": success,
|
||||
"status": AgentStatus.RUNNING.value if success else AgentStatus.CRASHED.value
|
||||
})
|
||||
|
||||
return {
|
||||
"results": results,
|
||||
"total": len(results),
|
||||
"successful": sum(1 for r in results if r["success"])
|
||||
}
|
||||
|
||||
|
||||
# 健康检查端点
|
||||
@router.get("/health")
|
||||
async def health_check():
|
||||
"""健康检查"""
|
||||
process_manager = get_process_manager()
|
||||
summary = process_manager.get_summary()
|
||||
|
||||
return {
|
||||
"status": "healthy",
|
||||
"running_agents": summary["running_agents"],
|
||||
"monitor_running": summary["monitor_running"]
|
||||
}
|
||||
47
backend/app/routers/heartbeats.py
Normal file
47
backend/app/routers/heartbeats.py
Normal file
@@ -0,0 +1,47 @@
|
||||
"""
|
||||
心跳管理 API 路由
|
||||
"""
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
from typing import Dict
|
||||
import time
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
heartbeats_db = {}
|
||||
|
||||
|
||||
class Heartbeat(BaseModel):
|
||||
agent_id: str
|
||||
timestamp: float
|
||||
is_timeout: bool = False
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def list_heartbeats():
|
||||
"""获取所有 Agent 心跳"""
|
||||
return {
|
||||
"heartbeats": {
|
||||
"claude-001": {
|
||||
"agent_id": "claude-001",
|
||||
"timestamp": time.time() - 30,
|
||||
"is_timeout": False
|
||||
},
|
||||
"kimi-001": {
|
||||
"agent_id": "kimi-001",
|
||||
"timestamp": time.time() - 60,
|
||||
"is_timeout": False
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.post("/{agent_id}")
|
||||
async def update_heartbeat(agent_id: str):
|
||||
"""更新 Agent 心跳"""
|
||||
heartbeats_db[agent_id] = {
|
||||
"agent_id": agent_id,
|
||||
"timestamp": time.time(),
|
||||
"is_timeout": False
|
||||
}
|
||||
return {"success": True}
|
||||
236
backend/app/routers/humans.py
Normal file
236
backend/app/routers/humans.py
Normal file
@@ -0,0 +1,236 @@
|
||||
"""
|
||||
人类输入 API 路由
|
||||
"""
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
|
||||
from app.services.human_input import get_human_input_service
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ========== 请求/响应模型 ==========
|
||||
|
||||
class TaskRequest(BaseModel):
|
||||
"""任务请求"""
|
||||
content: str
|
||||
from_user: str = "user001"
|
||||
priority: str = "medium"
|
||||
title: str = ""
|
||||
target_files: List[str] = []
|
||||
suggested_agent: str = ""
|
||||
urgent: bool = False
|
||||
|
||||
|
||||
class CommentRequest(BaseModel):
|
||||
"""评论请求"""
|
||||
meeting_id: str
|
||||
content: str
|
||||
from_user: str = "user001"
|
||||
comment_type: str = "proposal"
|
||||
priority: str = "normal"
|
||||
|
||||
|
||||
class ParticipantRegister(BaseModel):
|
||||
"""参与者注册"""
|
||||
user_id: str
|
||||
name: str
|
||||
role: str = ""
|
||||
avatar: str = "👤"
|
||||
|
||||
|
||||
class UserStatusUpdate(BaseModel):
|
||||
"""用户状态更新"""
|
||||
status: str
|
||||
current_focus: str = ""
|
||||
|
||||
|
||||
# ========== API 端点 ==========
|
||||
|
||||
@router.get("/summary")
|
||||
async def get_summary():
|
||||
"""获取人类输入服务摘要"""
|
||||
service = get_human_input_service()
|
||||
summary = await service.get_summary()
|
||||
return summary
|
||||
|
||||
|
||||
@router.post("/register")
|
||||
async def register_participant(request: ParticipantRegister):
|
||||
"""注册人类参与者"""
|
||||
service = get_human_input_service()
|
||||
await service.register_participant(
|
||||
request.user_id,
|
||||
request.name,
|
||||
request.role,
|
||||
request.avatar
|
||||
)
|
||||
return {"success": True, "user_id": request.user_id}
|
||||
|
||||
|
||||
@router.get("/participants")
|
||||
async def get_participants():
|
||||
"""获取所有参与者"""
|
||||
service = get_human_input_service()
|
||||
participants = await service.get_participants()
|
||||
return {
|
||||
"participants": [
|
||||
{
|
||||
"id": p.id,
|
||||
"name": p.name,
|
||||
"role": p.role,
|
||||
"status": p.status,
|
||||
"avatar": p.avatar
|
||||
}
|
||||
for p in participants
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@router.post("/tasks")
|
||||
async def add_task_request(request: TaskRequest):
|
||||
"""提交任务请求"""
|
||||
service = get_human_input_service()
|
||||
task_id = await service.add_task_request(
|
||||
from_user=request.from_user,
|
||||
content=request.content,
|
||||
priority=request.priority,
|
||||
title=request.title,
|
||||
target_files=request.target_files,
|
||||
suggested_agent=request.suggested_agent,
|
||||
urgent=request.urgent
|
||||
)
|
||||
return {"success": True, "task_id": task_id}
|
||||
|
||||
|
||||
@router.get("/tasks")
|
||||
async def get_pending_tasks(
|
||||
priority: Optional[str] = None,
|
||||
agent: Optional[str] = None
|
||||
):
|
||||
"""获取待处理任务"""
|
||||
service = get_human_input_service()
|
||||
tasks = await service.get_pending_tasks(
|
||||
priority_filter=priority,
|
||||
agent_filter=agent
|
||||
)
|
||||
return {
|
||||
"tasks": [
|
||||
{
|
||||
"id": t.id,
|
||||
"from_user": t.from_user,
|
||||
"timestamp": t.timestamp,
|
||||
"priority": t.priority,
|
||||
"type": t.type,
|
||||
"title": t.title,
|
||||
"content": t.content,
|
||||
"target_files": t.target_files,
|
||||
"suggested_agent": t.suggested_agent,
|
||||
"urgent": t.urgent,
|
||||
"is_urgent": t.is_urgent
|
||||
}
|
||||
for t in tasks
|
||||
],
|
||||
"count": len(tasks)
|
||||
}
|
||||
|
||||
|
||||
@router.get("/tasks/urgent")
|
||||
async def get_urgent_tasks():
|
||||
"""获取紧急任务"""
|
||||
service = get_human_input_service()
|
||||
tasks = await service.get_urgent_tasks()
|
||||
return {
|
||||
"tasks": [
|
||||
{
|
||||
"id": t.id,
|
||||
"from_user": t.from_user,
|
||||
"content": t.content,
|
||||
"title": t.title,
|
||||
"suggested_agent": t.suggested_agent
|
||||
}
|
||||
for t in tasks
|
||||
],
|
||||
"count": len(tasks)
|
||||
}
|
||||
|
||||
|
||||
@router.put("/tasks/{task_id}/processing")
|
||||
async def mark_task_processing(task_id: str):
|
||||
"""标记任务为处理中"""
|
||||
service = get_human_input_service()
|
||||
success = await service.mark_task_processing(task_id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@router.put("/tasks/{task_id}/complete")
|
||||
async def mark_task_completed(task_id: str):
|
||||
"""标记任务为已完成"""
|
||||
service = get_human_input_service()
|
||||
success = await service.mark_task_completed(task_id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@router.post("/comments")
|
||||
async def add_meeting_comment(request: CommentRequest):
|
||||
"""提交会议评论"""
|
||||
service = get_human_input_service()
|
||||
comment_id = await service.add_meeting_comment(
|
||||
from_user=request.from_user,
|
||||
meeting_id=request.meeting_id,
|
||||
content=request.content,
|
||||
comment_type=request.comment_type,
|
||||
priority=request.priority
|
||||
)
|
||||
return {"success": True, "comment_id": comment_id}
|
||||
|
||||
|
||||
@router.get("/comments")
|
||||
async def get_pending_comments(meeting_id: Optional[str] = None):
|
||||
"""获取待处理评论"""
|
||||
service = get_human_input_service()
|
||||
comments = await service.get_pending_comments(meeting_id)
|
||||
return {
|
||||
"comments": [
|
||||
{
|
||||
"id": c.id,
|
||||
"from_user": c.from_user,
|
||||
"meeting_id": c.meeting_id,
|
||||
"timestamp": c.timestamp,
|
||||
"type": c.type,
|
||||
"priority": c.priority,
|
||||
"content": c.content
|
||||
}
|
||||
for c in comments
|
||||
],
|
||||
"count": len(comments)
|
||||
}
|
||||
|
||||
|
||||
@router.put("/comments/{comment_id}/addressed")
|
||||
async def mark_comment_addressed(comment_id: str):
|
||||
"""标记评论为已处理"""
|
||||
service = get_human_input_service()
|
||||
success = await service.mark_comment_addressed(comment_id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Comment not found")
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@router.put("/users/{user_id}/status")
|
||||
async def update_user_status(user_id: str, request: UserStatusUpdate):
|
||||
"""更新用户状态"""
|
||||
service = get_human_input_service()
|
||||
success = await service.update_user_status(
|
||||
user_id,
|
||||
request.status,
|
||||
request.current_focus
|
||||
)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
return {"success": True}
|
||||
88
backend/app/routers/locks.py
Normal file
88
backend/app/routers/locks.py
Normal file
@@ -0,0 +1,88 @@
|
||||
"""
|
||||
文件锁 API 路由
|
||||
"""
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
import time
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
locks_db = [
|
||||
{
|
||||
"file_path": "src/main.py",
|
||||
"agent_id": "claude-001",
|
||||
"agent_name": "Claude Code",
|
||||
"locked_at": time.time() - 3600
|
||||
},
|
||||
{
|
||||
"file_path": "src/utils.py",
|
||||
"agent_id": "kimi-001",
|
||||
"agent_name": "Kimi CLI",
|
||||
"locked_at": time.time() - 1800
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
class FileLock(BaseModel):
|
||||
file_path: str
|
||||
agent_id: str
|
||||
agent_name: str = ""
|
||||
locked_at: float
|
||||
|
||||
|
||||
def format_elapsed(locked_at: float) -> str:
|
||||
"""格式化已锁定时间"""
|
||||
elapsed = time.time() - locked_at
|
||||
if elapsed < 60:
|
||||
return f"{int(elapsed)}秒"
|
||||
elif elapsed < 3600:
|
||||
return f"{int(elapsed / 60)}分钟"
|
||||
else:
|
||||
return f"{elapsed / 3600:.1f}小时"
|
||||
|
||||
@router.get("/")
|
||||
async def list_locks():
|
||||
"""获取所有文件锁列表"""
|
||||
locks_with_display = []
|
||||
for lock in locks_db:
|
||||
lock_copy = lock.copy()
|
||||
lock_copy["elapsed_display"] = format_elapsed(lock["locked_at"])
|
||||
locks_with_display.append(lock_copy)
|
||||
return {"locks": locks_with_display}
|
||||
|
||||
|
||||
@router.post("/acquire")
|
||||
async def acquire_lock(lock: FileLock):
|
||||
"""获取文件锁"""
|
||||
# 检查是否已被锁定
|
||||
for existing in locks_db:
|
||||
if existing["file_path"] == lock.file_path:
|
||||
return {"success": False, "message": "File already locked"}
|
||||
|
||||
locks_db.append({
|
||||
"file_path": lock.file_path,
|
||||
"agent_id": lock.agent_id,
|
||||
"agent_name": lock.agent_name or lock.agent_id,
|
||||
"locked_at": time.time()
|
||||
})
|
||||
return {"success": True, "message": "Lock acquired"}
|
||||
|
||||
|
||||
@router.post("/release")
|
||||
async def release_lock(data: dict):
|
||||
"""释放文件锁"""
|
||||
file_path = data.get("file_path", "")
|
||||
agent_id = data.get("agent_id", "")
|
||||
global locks_db
|
||||
locks_db = [l for l in locks_db if not (l["file_path"] == file_path and l["agent_id"] == agent_id)]
|
||||
return {"success": True, "message": "Lock released"}
|
||||
|
||||
|
||||
@router.get("/check")
|
||||
async def check_lock(file_path: str):
|
||||
"""检查文件锁定状态"""
|
||||
for lock in locks_db:
|
||||
if lock["file_path"] == file_path:
|
||||
return {"file_path": file_path, "locked": True, "locked_by": lock["agent_id"]}
|
||||
return {"file_path": file_path, "locked": False}
|
||||
194
backend/app/routers/meetings.py
Normal file
194
backend/app/routers/meetings.py
Normal file
@@ -0,0 +1,194 @@
|
||||
"""
|
||||
会议管理 API 路由
|
||||
"""
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
import time
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
meetings_db = []
|
||||
|
||||
|
||||
class Meeting(BaseModel):
|
||||
meeting_id: str
|
||||
title: str
|
||||
status: str
|
||||
attendees: List[str]
|
||||
agenda: str
|
||||
progress_summary: str
|
||||
created_at: float
|
||||
|
||||
|
||||
class MeetingCreate(BaseModel):
|
||||
title: str
|
||||
agenda: str
|
||||
meeting_type: str = "design_review"
|
||||
attendees: List[str] = []
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def list_meetings():
|
||||
"""获取所有会议列表"""
|
||||
return {
|
||||
"meetings": [
|
||||
{
|
||||
"meeting_id": "meeting-001",
|
||||
"title": "架构设计评审",
|
||||
"status": "in_progress",
|
||||
"attendees": ["claude-001", "kimi-001"],
|
||||
"agenda": "讨论系统架构设计",
|
||||
"progress_summary": "50%",
|
||||
"created_at": time.time() - 7200
|
||||
},
|
||||
{
|
||||
"meeting_id": "meeting-002",
|
||||
"title": "代码审查会议",
|
||||
"status": "completed",
|
||||
"attendees": ["claude-001"],
|
||||
"agenda": "审查前端组件代码",
|
||||
"progress_summary": "100%",
|
||||
"created_at": time.time() - 86400
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@router.get("/today")
|
||||
async def list_today_meetings():
|
||||
"""获取今日会议"""
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
return {
|
||||
"meetings": [
|
||||
{
|
||||
"meeting_id": "meeting-001",
|
||||
"title": "架构设计评审",
|
||||
"date": today,
|
||||
"status": "in_progress",
|
||||
"attendees": ["claude-001", "kimi-001"],
|
||||
"steps": [
|
||||
{"step_id": "step-1", "label": "收集想法", "status": "completed"},
|
||||
{"step_id": "step-2", "label": "讨论迭代", "status": "active"},
|
||||
{"step_id": "step-3", "label": "生成共识", "status": "pending"}
|
||||
],
|
||||
"discussions": [
|
||||
{
|
||||
"agent_id": "claude-001",
|
||||
"agent_name": "Claude Code",
|
||||
"content": "建议采用微服务架构",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"step": "讨论迭代"
|
||||
}
|
||||
],
|
||||
"progress_summary": "50%",
|
||||
"consensus": ""
|
||||
},
|
||||
{
|
||||
"meeting_id": "meeting-002",
|
||||
"title": "代码审查会议",
|
||||
"date": today,
|
||||
"status": "completed",
|
||||
"attendees": ["claude-001"],
|
||||
"steps": [
|
||||
{"step_id": "step-1", "label": "代码检查", "status": "completed"},
|
||||
{"step_id": "step-2", "label": "问题讨论", "status": "completed"}
|
||||
],
|
||||
"discussions": [],
|
||||
"progress_summary": "100%",
|
||||
"consensus": "代码质量良好,可以合并"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def create_meeting(meeting: MeetingCreate):
|
||||
"""创建新会议"""
|
||||
meeting_id = f"meeting-{int(time.time())}"
|
||||
meeting_data = {
|
||||
"meeting_id": meeting_id,
|
||||
"title": meeting.title,
|
||||
"status": "waiting",
|
||||
"attendees": meeting.attendees,
|
||||
"agenda": meeting.agenda,
|
||||
"progress_summary": "0%",
|
||||
"created_at": time.time()
|
||||
}
|
||||
meetings_db.append(meeting_data)
|
||||
return meeting_data
|
||||
|
||||
|
||||
@router.get("/{meeting_id}")
|
||||
async def get_meeting(meeting_id: str):
|
||||
"""获取会议详情"""
|
||||
for meeting in meetings_db:
|
||||
if meeting["meeting_id"] == meeting_id:
|
||||
return meeting
|
||||
# 返回模拟数据
|
||||
return {
|
||||
"meeting_id": meeting_id,
|
||||
"title": "测试会议",
|
||||
"status": "in_progress",
|
||||
"attendees": ["claude-001"],
|
||||
"agenda": "测试议程",
|
||||
"progress_summary": "50%",
|
||||
"created_at": time.time()
|
||||
}
|
||||
|
||||
|
||||
@router.post("/create")
|
||||
async def create_meeting_api(meeting: MeetingCreate):
|
||||
"""创建会议 API(前端使用的端点)"""
|
||||
return await create_meeting(meeting)
|
||||
|
||||
|
||||
@router.post("/{meeting_id}/join")
|
||||
async def join_meeting(meeting_id: str, data: dict):
|
||||
"""Agent 加入会议"""
|
||||
agent_id = data.get("agent_id", "")
|
||||
return {"success": True, "meeting_id": meeting_id, "agent_id": agent_id}
|
||||
|
||||
|
||||
@router.post("/{meeting_id}/discuss")
|
||||
async def add_discussion(meeting_id: str, data: dict):
|
||||
"""添加讨论内容"""
|
||||
return {"success": True, "meeting_id": meeting_id}
|
||||
|
||||
|
||||
@router.post("/{meeting_id}/finish")
|
||||
async def finish_meeting(meeting_id: str, data: dict):
|
||||
"""完成会议"""
|
||||
return {"success": True, "meeting_id": meeting_id}
|
||||
|
||||
|
||||
@router.post("/{meeting_id}/progress")
|
||||
async def update_progress(meeting_id: str, data: dict):
|
||||
"""更新进度"""
|
||||
return {"success": True, "meeting_id": meeting_id}
|
||||
|
||||
|
||||
@router.post("/record/create")
|
||||
async def create_meeting_record(data: dict):
|
||||
"""创建会议记录(前端使用的端点)"""
|
||||
meeting_id = f"meeting-{int(time.time())}"
|
||||
meeting_data = {
|
||||
"meeting_id": meeting_id,
|
||||
"title": data.get("title", "未命名会议"),
|
||||
"agenda": data.get("agenda", ""),
|
||||
"attendees": data.get("attendees", []),
|
||||
"status": "waiting",
|
||||
"progress_summary": "0%",
|
||||
"steps": data.get("steps", []),
|
||||
"discussions": [],
|
||||
"created_at": time.time()
|
||||
}
|
||||
meetings_db.append(meeting_data)
|
||||
return meeting_data
|
||||
|
||||
|
||||
@router.post("/record/{meeting_id}/discussion")
|
||||
async def add_meeting_discussion(meeting_id: str, data: dict):
|
||||
"""添加会议讨论(前端使用的端点)"""
|
||||
return {"success": True, "meeting_id": meeting_id, "discussion": data}
|
||||
60
backend/app/routers/resources.py
Normal file
60
backend/app/routers/resources.py
Normal file
@@ -0,0 +1,60 @@
|
||||
"""
|
||||
资源管理 API 路由
|
||||
"""
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
import time
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class TaskRequest(BaseModel):
|
||||
agent_id: str
|
||||
task: str
|
||||
timeout: Optional[int] = 300
|
||||
|
||||
|
||||
class TaskParseRequest(BaseModel):
|
||||
task: str
|
||||
|
||||
|
||||
@router.post("/execute")
|
||||
async def execute_task(request: TaskRequest):
|
||||
"""执行任务"""
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"任务 '{request.task}' 已执行",
|
||||
"files_locked": ["src/main.py"],
|
||||
"duration_seconds": 5.5
|
||||
}
|
||||
|
||||
|
||||
@router.get("/status")
|
||||
async def get_all_status():
|
||||
"""获取所有 Agent 状态"""
|
||||
return {
|
||||
"agents": [
|
||||
{
|
||||
"agent_id": "claude-001",
|
||||
"status": "working",
|
||||
"current_task": "开发功能",
|
||||
"progress": 75
|
||||
},
|
||||
{
|
||||
"agent_id": "kimi-001",
|
||||
"status": "idle",
|
||||
"current_task": "",
|
||||
"progress": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@router.post("/parse-task")
|
||||
async def parse_task(request: TaskParseRequest):
|
||||
"""解析任务文件"""
|
||||
return {
|
||||
"task": request.task,
|
||||
"files": ["src/main.py", "src/utils.py"]
|
||||
}
|
||||
55
backend/app/routers/roles.py
Normal file
55
backend/app/routers/roles.py
Normal file
@@ -0,0 +1,55 @@
|
||||
"""
|
||||
角色分配 API 路由
|
||||
"""
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Dict
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class RoleRequest(BaseModel):
|
||||
task: str
|
||||
|
||||
|
||||
class RoleAllocateRequest(BaseModel):
|
||||
task: str
|
||||
agents: List[str]
|
||||
|
||||
|
||||
@router.post("/primary")
|
||||
async def get_primary_role(request: RoleRequest):
|
||||
"""获取任务主要角色"""
|
||||
return {
|
||||
"task": request.task,
|
||||
"primary_role": "developer",
|
||||
"role_scores": {
|
||||
"developer": 0.8,
|
||||
"architect": 0.6,
|
||||
"qa": 0.4,
|
||||
"pm": 0.2
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.post("/allocate")
|
||||
async def allocate_roles(request: RoleAllocateRequest):
|
||||
"""分配角色"""
|
||||
allocation = {}
|
||||
for i, agent in enumerate(request.agents):
|
||||
roles = ["developer", "architect", "qa"]
|
||||
allocation[agent] = roles[i % len(roles)]
|
||||
|
||||
return {
|
||||
"task": request.task,
|
||||
"primary_role": "developer",
|
||||
"allocation": allocation
|
||||
}
|
||||
|
||||
|
||||
@router.post("/explain")
|
||||
async def explain_roles(request: RoleAllocateRequest):
|
||||
"""解释角色分配"""
|
||||
return {
|
||||
"explanation": f"基于任务 '{request.task}' 的分析,推荐了最适合的角色分配方案。"
|
||||
}
|
||||
392
backend/app/routers/websocket.py
Normal file
392
backend/app/routers/websocket.py
Normal file
@@ -0,0 +1,392 @@
|
||||
"""
|
||||
WebSocket 实时通信
|
||||
|
||||
提供 Agent 与服务器之间的实时双向通信
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import asyncio
|
||||
from typing import Dict, Set, Optional, Any
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
||||
from pydantic import BaseModel
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class ConnectionManager:
|
||||
"""
|
||||
WebSocket 连接管理器
|
||||
|
||||
管理 WebSocket 连接,支持:
|
||||
1. Agent 连接管理
|
||||
2. 消息广播
|
||||
3. 私信发送
|
||||
4. 心跳检测
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# Agent 连接: {agent_id: WebSocket}
|
||||
self.agent_connections: Dict[str, WebSocket] = {}
|
||||
# 客户端连接: {client_id: WebSocket}
|
||||
self.client_connections: Dict[str, WebSocket] = {}
|
||||
# 连接元数据: {connection_id: {"type": "agent"|"client", "connected_at": datetime}}
|
||||
self.connection_metadata: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
async def connect_agent(self, websocket: WebSocket, agent_id: str):
|
||||
"""Agent 连接"""
|
||||
await websocket.accept()
|
||||
self.agent_connections[agent_id] = websocket
|
||||
self.connection_metadata[agent_id] = {
|
||||
"type": "agent",
|
||||
"connected_at": datetime.now()
|
||||
}
|
||||
logger.info(f"Agent 连接: {agent_id}")
|
||||
|
||||
# 发送欢迎消息
|
||||
await self.send_to_agent(agent_id, {
|
||||
"type": "connected",
|
||||
"agent_id": agent_id,
|
||||
"message": "连接成功"
|
||||
})
|
||||
|
||||
async def connect_client(self, websocket: WebSocket, client_id: str):
|
||||
"""客户端连接"""
|
||||
await websocket.accept()
|
||||
self.client_connections[client_id] = websocket
|
||||
self.connection_metadata[client_id] = {
|
||||
"type": "client",
|
||||
"connected_at": datetime.now()
|
||||
}
|
||||
logger.info(f"客户端连接: {client_id}")
|
||||
|
||||
# 发送欢迎消息
|
||||
await self.send_to_client(client_id, {
|
||||
"type": "connected",
|
||||
"client_id": client_id,
|
||||
"message": "连接成功"
|
||||
})
|
||||
|
||||
def disconnect_agent(self, agent_id: str):
|
||||
"""断开 Agent 连接"""
|
||||
if agent_id in self.agent_connections:
|
||||
del self.agent_connections[agent_id]
|
||||
if agent_id in self.connection_metadata:
|
||||
del self.connection_metadata[agent_id]
|
||||
logger.info(f"Agent 断开: {agent_id}")
|
||||
|
||||
def disconnect_client(self, client_id: str):
|
||||
"""断开客户端连接"""
|
||||
if client_id in self.client_connections:
|
||||
del self.client_connections[client_id]
|
||||
if client_id in self.connection_metadata:
|
||||
del self.connection_metadata[client_id]
|
||||
logger.info(f"客户端断开: {client_id}")
|
||||
|
||||
async def send_to_agent(self, agent_id: str, message: Dict) -> bool:
|
||||
"""发送消息给 Agent"""
|
||||
if agent_id in self.agent_connections:
|
||||
try:
|
||||
await self.agent_connections[agent_id].send_json(message)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"发送消息给 Agent 失败: {agent_id}: {e}")
|
||||
self.disconnect_agent(agent_id)
|
||||
return False
|
||||
return False
|
||||
|
||||
async def send_to_client(self, client_id: str, message: Dict) -> bool:
|
||||
"""发送消息给客户端"""
|
||||
if client_id in self.client_connections:
|
||||
try:
|
||||
await self.client_connections[client_id].send_json(message)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"发送消息给客户端失败: {client_id}: {e}")
|
||||
self.disconnect_client(client_id)
|
||||
return False
|
||||
return False
|
||||
|
||||
async def broadcast_to_agents(self, message: Dict):
|
||||
"""广播消息给所有 Agent"""
|
||||
failed_agents = []
|
||||
for agent_id, websocket in self.agent_connections.items():
|
||||
try:
|
||||
await websocket.send_json(message)
|
||||
except Exception as e:
|
||||
logger.error(f"广播消息失败: {agent_id}: {e}")
|
||||
failed_agents.append(agent_id)
|
||||
|
||||
# 清理失败的连接
|
||||
for agent_id in failed_agents:
|
||||
self.disconnect_agent(agent_id)
|
||||
|
||||
async def broadcast_to_clients(self, message: Dict):
|
||||
"""广播消息给所有客户端"""
|
||||
failed_clients = []
|
||||
for client_id, websocket in self.client_connections.items():
|
||||
try:
|
||||
await websocket.send_json(message)
|
||||
except Exception as e:
|
||||
logger.error(f"广播消息失败: {client_id}: {e}")
|
||||
failed_clients.append(client_id)
|
||||
|
||||
# 清理失败的连接
|
||||
for client_id in failed_clients:
|
||||
self.disconnect_client(client_id)
|
||||
|
||||
async def broadcast_to_all(self, message: Dict):
|
||||
"""广播消息给所有连接"""
|
||||
await self.broadcast_to_agents(message)
|
||||
await self.broadcast_to_clients(message)
|
||||
|
||||
def get_connected_agents(self) -> Set[str]:
|
||||
"""获取已连接的 Agent"""
|
||||
return set(self.agent_connections.keys())
|
||||
|
||||
def get_connected_clients(self) -> Set[str]:
|
||||
"""获取已连接的客户端"""
|
||||
return set(self.client_connections.keys())
|
||||
|
||||
def get_connection_count(self) -> Dict[str, int]:
|
||||
"""获取连接数量"""
|
||||
return {
|
||||
"agents": len(self.agent_connections),
|
||||
"clients": len(self.client_connections),
|
||||
"total": len(self.agent_connections) + len(self.client_connections)
|
||||
}
|
||||
|
||||
|
||||
# 全局连接管理器
|
||||
manager = ConnectionManager()
|
||||
|
||||
|
||||
# ========== WebSocket 端点 ==========
|
||||
|
||||
|
||||
@router.websocket("/ws/agent/{agent_id}")
|
||||
async def agent_websocket_endpoint(websocket: WebSocket, agent_id: str):
|
||||
"""
|
||||
Agent WebSocket 端点
|
||||
|
||||
Agent 连接后可以:
|
||||
1. 接收任务分配
|
||||
2. 发送状态更新
|
||||
3. 参与实时协作
|
||||
"""
|
||||
await manager.connect_agent(websocket, agent_id)
|
||||
|
||||
try:
|
||||
while True:
|
||||
# 接收来自 Agent 的消息
|
||||
data = await websocket.receive_json()
|
||||
await handle_agent_message(agent_id, data)
|
||||
|
||||
except WebSocketDisconnect:
|
||||
manager.disconnect_agent(agent_id)
|
||||
except Exception as e:
|
||||
logger.error(f"Agent WebSocket 错误: {agent_id}: {e}")
|
||||
manager.disconnect_agent(agent_id)
|
||||
|
||||
|
||||
@router.websocket("/ws/client/{client_id}")
|
||||
async def client_websocket_endpoint(websocket: WebSocket, client_id: str):
|
||||
"""
|
||||
客户端 WebSocket 端点
|
||||
|
||||
客户端连接后可以:
|
||||
1. 实时监控 Agent 状态
|
||||
2. 接收事件通知
|
||||
3. 发送控制指令
|
||||
"""
|
||||
await manager.connect_client(websocket, client_id)
|
||||
|
||||
# 发送初始状态
|
||||
await manager.send_to_client(client_id, {
|
||||
"type": "initial_state",
|
||||
"connected_agents": list(manager.get_connected_agents()),
|
||||
"timestamp": datetime.now().isoformat()
|
||||
})
|
||||
|
||||
try:
|
||||
while True:
|
||||
# 接收来自客户端的消息
|
||||
data = await websocket.receive_json()
|
||||
await handle_client_message(client_id, data)
|
||||
|
||||
except WebSocketDisconnect:
|
||||
manager.disconnect_client(client_id)
|
||||
except Exception as e:
|
||||
logger.error(f"客户端 WebSocket 错误: {client_id}: {e}")
|
||||
manager.disconnect_client(client_id)
|
||||
|
||||
|
||||
@router.websocket("/ws")
|
||||
async def public_websocket_endpoint(websocket: WebSocket):
|
||||
"""
|
||||
公共 WebSocket 端点
|
||||
|
||||
自动生成 client_id 的客户端连接
|
||||
"""
|
||||
import uuid
|
||||
client_id = f"client_{uuid.uuid4().hex[:12]}"
|
||||
await manager.connect_client(websocket, client_id)
|
||||
|
||||
try:
|
||||
while True:
|
||||
data = await websocket.receive_json()
|
||||
await handle_client_message(client_id, data)
|
||||
|
||||
except WebSocketDisconnect:
|
||||
manager.disconnect_client(client_id)
|
||||
except Exception as e:
|
||||
logger.error(f"公共 WebSocket 错误: {client_id}: {e}")
|
||||
manager.disconnect_client(client_id)
|
||||
|
||||
|
||||
# ========== 消息处理 ==========
|
||||
|
||||
|
||||
async def handle_agent_message(agent_id: str, data: Dict):
|
||||
"""处理来自 Agent 的消息"""
|
||||
message_type = data.get("type")
|
||||
|
||||
if message_type == "heartbeat":
|
||||
# Agent 心跳更新
|
||||
await broadcast_agent_status(agent_id, data)
|
||||
|
||||
elif message_type == "status_update":
|
||||
# Agent 状态更新
|
||||
await broadcast_agent_status(agent_id, data)
|
||||
|
||||
elif message_type == "task_progress":
|
||||
# 任务进度更新
|
||||
await broadcast_task_progress(agent_id, data)
|
||||
|
||||
elif message_type == "meeting_joined":
|
||||
# Agent 加入会议
|
||||
await broadcast_event({
|
||||
"type": "meeting_event",
|
||||
"event": "agent_joined",
|
||||
"agent_id": agent_id,
|
||||
"meeting_id": data.get("meeting_id"),
|
||||
"timestamp": datetime.now().isoformat()
|
||||
})
|
||||
|
||||
elif message_type == "meeting_proposal":
|
||||
# 会议提案
|
||||
await broadcast_event({
|
||||
"type": "meeting_event",
|
||||
"event": "proposal",
|
||||
"agent_id": agent_id,
|
||||
"meeting_id": data.get("meeting_id"),
|
||||
"content": data.get("content"),
|
||||
"timestamp": datetime.now().isoformat()
|
||||
})
|
||||
|
||||
else:
|
||||
# 其他消息类型
|
||||
await broadcast_event({
|
||||
"type": "agent_message",
|
||||
"agent_id": agent_id,
|
||||
"message_type": message_type,
|
||||
"data": data,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
})
|
||||
|
||||
|
||||
async def handle_client_message(client_id: str, data: Dict):
|
||||
"""处理来自客户端的消息"""
|
||||
message_type = data.get("type")
|
||||
|
||||
if message_type == "subscribe_agents":
|
||||
# 客户端订阅 Agent 状态
|
||||
await manager.send_to_client(client_id, {
|
||||
"type": "subscription_confirmed",
|
||||
"subscription": "agents"
|
||||
})
|
||||
|
||||
elif message_type == "send_to_agent":
|
||||
# 发送消息给特定 Agent
|
||||
agent_id = data.get("agent_id")
|
||||
message = data.get("message")
|
||||
if agent_id:
|
||||
await manager.send_to_agent(agent_id, {
|
||||
"type": "client_message",
|
||||
"from_client": client_id,
|
||||
"message": message
|
||||
})
|
||||
|
||||
elif message_type == "broadcast":
|
||||
# 广播消息给所有 Agent
|
||||
message = data.get("message")
|
||||
await manager.broadcast_to_agents({
|
||||
"type": "broadcast",
|
||||
"from_client": client_id,
|
||||
"message": message
|
||||
})
|
||||
|
||||
|
||||
async def broadcast_agent_status(agent_id: str, data: Dict):
|
||||
"""广播 Agent 状态更新"""
|
||||
await manager.broadcast_to_clients({
|
||||
"type": "agent_status",
|
||||
"agent_id": agent_id,
|
||||
"data": data,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
})
|
||||
|
||||
|
||||
async def broadcast_task_progress(agent_id: str, data: Dict):
|
||||
"""广播任务进度更新"""
|
||||
await manager.broadcast_to_clients({
|
||||
"type": "task_progress",
|
||||
"agent_id": agent_id,
|
||||
"task_id": data.get("task_id"),
|
||||
"progress": data.get("progress"),
|
||||
"message": data.get("message"),
|
||||
"timestamp": datetime.now().isoformat()
|
||||
})
|
||||
|
||||
|
||||
async def broadcast_event(event: Dict):
|
||||
"""广播事件"""
|
||||
await manager.broadcast_to_all(event)
|
||||
|
||||
|
||||
# ========== HTTP API ==========
|
||||
|
||||
|
||||
@router.get("/ws/connections")
|
||||
async def get_connections():
|
||||
"""获取当前连接信息"""
|
||||
return {
|
||||
"agents": list(manager.get_connected_agents()),
|
||||
"clients": list(manager.get_connected_clients()),
|
||||
"count": manager.get_connection_count()
|
||||
}
|
||||
|
||||
|
||||
@router.post("/ws/broadcast")
|
||||
async def broadcast_message(message: Dict):
|
||||
"""通过 HTTP 广播消息到所有连接"""
|
||||
await manager.broadcast_to_all({
|
||||
"type": "broadcast",
|
||||
"message": message,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
})
|
||||
return {"success": True, "message": "消息已广播"}
|
||||
|
||||
|
||||
@router.post("/ws/send/{agent_id}")
|
||||
async def send_to_agent(agent_id: str, message: Dict):
|
||||
"""通过 HTTP 发送消息给特定 Agent"""
|
||||
success = await manager.send_to_agent(agent_id, message)
|
||||
if success:
|
||||
return {"success": True, "agent_id": agent_id}
|
||||
else:
|
||||
return {"success": False, "error": "发送失败或 Agent 未连接"}
|
||||
218
backend/app/routers/workflows.py
Normal file
218
backend/app/routers/workflows.py
Normal file
@@ -0,0 +1,218 @@
|
||||
"""
|
||||
工作流管理 API 路由
|
||||
"""
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional, Dict, Any
|
||||
from pathlib import Path
|
||||
|
||||
from app.services.workflow_engine import get_workflow_engine
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ========== 请求/响应模型 ==========
|
||||
|
||||
class MeetingNode(BaseModel):
|
||||
"""工作流节点"""
|
||||
meeting_id: str
|
||||
title: str
|
||||
node_type: str = "meeting"
|
||||
attendees: List[str]
|
||||
depends_on: List[str] = []
|
||||
completed: bool = False
|
||||
on_failure: Optional[str] = None
|
||||
progress: Optional[str] = None
|
||||
|
||||
|
||||
class WorkflowDetail(BaseModel):
|
||||
"""工作流详情"""
|
||||
workflow_id: str
|
||||
name: str
|
||||
description: str
|
||||
status: str
|
||||
progress: str
|
||||
current_node: Optional[str] = None
|
||||
meetings: List[MeetingNode]
|
||||
|
||||
|
||||
class WorkflowSummary(BaseModel):
|
||||
"""工作流摘要"""
|
||||
workflow_id: str
|
||||
name: str
|
||||
status: str
|
||||
progress: str
|
||||
|
||||
|
||||
class JoinExecutionRequest(BaseModel):
|
||||
"""加入执行节点请求"""
|
||||
agent_id: str
|
||||
|
||||
|
||||
class JumpRequest(BaseModel):
|
||||
"""跳转请求"""
|
||||
target_meeting_id: str
|
||||
|
||||
|
||||
# ========== API 端点 ==========
|
||||
|
||||
@router.get("/files")
|
||||
async def list_workflow_files():
|
||||
"""获取工作流文件列表"""
|
||||
engine = get_workflow_engine()
|
||||
workflow_dir = Path(engine._storage.base_path) / engine.WORKFLOWS_DIR
|
||||
|
||||
if not workflow_dir.exists():
|
||||
return {"files": []}
|
||||
|
||||
yaml_files = list(workflow_dir.glob("*.yaml")) + list(workflow_dir.glob("*.yml"))
|
||||
|
||||
files = []
|
||||
for f in yaml_files:
|
||||
stat = f.stat()
|
||||
files.append({
|
||||
"name": f.name,
|
||||
"path": f"workflow/{f.name}",
|
||||
"size": stat.st_size,
|
||||
"modified": stat.st_mtime
|
||||
})
|
||||
|
||||
return {"files": files}
|
||||
|
||||
|
||||
@router.get("/list")
|
||||
async def list_workflows():
|
||||
"""获取已加载的工作流列表"""
|
||||
engine = get_workflow_engine()
|
||||
workflows = await engine.list_workflows()
|
||||
return {"workflows": workflows}
|
||||
|
||||
|
||||
@router.post("/start/{workflow_path:path}")
|
||||
async def start_workflow(workflow_path: str):
|
||||
"""
|
||||
启动工作流
|
||||
|
||||
加载 YAML 工作流文件并准备执行
|
||||
"""
|
||||
engine = get_workflow_engine()
|
||||
try:
|
||||
workflow = await engine.load_workflow(workflow_path)
|
||||
detail = await engine.get_workflow_detail(workflow.workflow_id)
|
||||
return detail
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{workflow_id}")
|
||||
async def get_workflow(workflow_id: str):
|
||||
"""获取工作流详情"""
|
||||
engine = get_workflow_engine()
|
||||
detail = await engine.get_workflow_detail(workflow_id)
|
||||
if not detail:
|
||||
raise HTTPException(status_code=404, detail="Workflow not found")
|
||||
return detail
|
||||
|
||||
|
||||
@router.get("/{workflow_id}/status")
|
||||
async def get_workflow_status(workflow_id: str):
|
||||
"""获取工作流状态"""
|
||||
engine = get_workflow_engine()
|
||||
status = await engine.get_workflow_status(workflow_id)
|
||||
if not status:
|
||||
raise HTTPException(status_code=404, detail="Workflow not found")
|
||||
return status
|
||||
|
||||
|
||||
@router.get("/{workflow_id}/next")
|
||||
async def get_next_node(workflow_id: str):
|
||||
"""获取下一个待执行节点"""
|
||||
engine = get_workflow_engine()
|
||||
meeting = await engine.get_next_meeting(workflow_id)
|
||||
if not meeting:
|
||||
return {"meeting": None, "message": "Workflow completed"}
|
||||
return {
|
||||
"meeting": {
|
||||
"meeting_id": meeting.meeting_id,
|
||||
"title": meeting.title,
|
||||
"node_type": meeting.node_type,
|
||||
"attendees": meeting.attendees,
|
||||
"depends_on": meeting.depends_on
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.post("/{workflow_id}/complete/{meeting_id}")
|
||||
async def complete_node(workflow_id: str, meeting_id: str):
|
||||
"""标记节点完成"""
|
||||
engine = get_workflow_engine()
|
||||
success = await engine.complete_meeting(workflow_id, meeting_id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Workflow or meeting not found")
|
||||
return {"success": True, "message": "Node completed"}
|
||||
|
||||
|
||||
@router.post("/{workflow_id}/join/{meeting_id}")
|
||||
async def join_execution_node(workflow_id: str, meeting_id: str, request: JoinExecutionRequest):
|
||||
"""
|
||||
Agent 加入执行节点
|
||||
|
||||
标记 Agent 已完成执行,当所有 Agent 都完成时返回 ready
|
||||
"""
|
||||
engine = get_workflow_engine()
|
||||
result = await engine.join_execution_node(workflow_id, meeting_id, request.agent_id)
|
||||
if result.get("status") == "error":
|
||||
raise HTTPException(status_code=400, detail=result.get("message"))
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/{workflow_id}/execution/{meeting_id}")
|
||||
async def get_execution_node_status(workflow_id: str, meeting_id: str):
|
||||
"""获取执行节点状态"""
|
||||
engine = get_workflow_engine()
|
||||
status = await engine.get_execution_status(workflow_id, meeting_id)
|
||||
if not status:
|
||||
raise HTTPException(status_code=404, detail="Execution node not found")
|
||||
return status
|
||||
|
||||
|
||||
@router.post("/{workflow_id}/jump")
|
||||
async def jump_to_node(workflow_id: str, request: JumpRequest):
|
||||
"""
|
||||
强制跳转到指定节点
|
||||
|
||||
重置目标节点及所有后续节点的完成状态
|
||||
"""
|
||||
engine = get_workflow_engine()
|
||||
success = await engine.jump_to_node(workflow_id, request.target_meeting_id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Target node not found")
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Jumped to {request.target_meeting_id}",
|
||||
"detail": await engine.get_workflow_detail(workflow_id)
|
||||
}
|
||||
|
||||
|
||||
@router.post("/{workflow_id}/fail/{meeting_id}")
|
||||
async def handle_node_failure(workflow_id: str, meeting_id: str):
|
||||
"""
|
||||
处理节点失败
|
||||
|
||||
根据 on_failure 配置跳转到指定节点
|
||||
"""
|
||||
engine = get_workflow_engine()
|
||||
target = await engine.handle_failure(workflow_id, meeting_id)
|
||||
if target:
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Jumped to {target} due to failure",
|
||||
"target": target,
|
||||
"detail": await engine.get_workflow_detail(workflow_id)
|
||||
}
|
||||
return {
|
||||
"success": True,
|
||||
"message": "No failure handler configured"
|
||||
}
|
||||
4
backend/app/services/__init__.py
Normal file
4
backend/app/services/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
"""Services Package"""
|
||||
from .storage import StorageService, get_storage
|
||||
|
||||
__all__ = ["StorageService", "get_storage"]
|
||||
486
backend/app/services/agent_executor.py
Normal file
486
backend/app/services/agent_executor.py
Normal file
@@ -0,0 +1,486 @@
|
||||
"""
|
||||
Agent 执行引擎
|
||||
|
||||
负责协调 LLM 调用和资源管理,提供声明式的任务执行接口。
|
||||
自动管理文件锁、心跳更新等生命周期。
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
from typing import Dict, List, Optional, Any, Callable
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
from .llm_service import ModelRouter, LLMMessage, TaskType
|
||||
from .storage import get_storage
|
||||
from .file_lock import get_file_lock_service
|
||||
from .heartbeat import get_heartbeat_service
|
||||
from .agent_registry import get_agent_registry, AgentInfo
|
||||
from ..core.agent_adapter import Task, Result
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExecutionPlan:
|
||||
"""任务执行计划"""
|
||||
steps: List[str] = field(default_factory=list)
|
||||
required_files: List[str] = field(default_factory=list)
|
||||
estimated_duration: str = ""
|
||||
complexity: str = "medium"
|
||||
requires_code_execution: bool = False
|
||||
subtasks: List[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExecutionContext:
|
||||
"""执行上下文"""
|
||||
agent_id: str
|
||||
agent_role: str
|
||||
agent_model: str
|
||||
task_id: str
|
||||
acquired_locks: List[str] = field(default_factory=list)
|
||||
start_time: float = 0
|
||||
messages: List[LLMMessage] = field(default_factory=list)
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
class AgentExecutor:
|
||||
"""
|
||||
Agent 任务执行引擎
|
||||
|
||||
功能:
|
||||
1. 任务分析 - 解析任务描述,识别需要的文件
|
||||
2. 计划生成 - 调用 LLM 生成执行计划
|
||||
3. 资源管理 - 自动获取和释放文件锁
|
||||
4. 任务执行 - 调用 LLM 执行任务
|
||||
5. 结果处理 - 格式化输出,更新状态
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
llm_service: ModelRouter = None,
|
||||
storage=None,
|
||||
lock_service=None,
|
||||
heartbeat_service=None,
|
||||
registry=None
|
||||
):
|
||||
self.llm = llm_service
|
||||
self.storage = storage or get_storage()
|
||||
self.locks = lock_service or get_file_lock_service()
|
||||
self.heartbeat = heartbeat_service or get_heartbeat_service()
|
||||
self.registry = registry or get_agent_registry()
|
||||
|
||||
# 工作目录
|
||||
self.work_dir = Path.cwd()
|
||||
|
||||
async def execute_task(
|
||||
self,
|
||||
agent: AgentInfo,
|
||||
task: Task,
|
||||
context: Dict[str, Any] = None
|
||||
) -> Result:
|
||||
"""
|
||||
执行任务的主入口
|
||||
|
||||
自动管理:
|
||||
1. 文件锁获取和释放
|
||||
2. 心跳更新
|
||||
3. 任务进度跟踪
|
||||
4. 错误处理和恢复
|
||||
"""
|
||||
execution_context = ExecutionContext(
|
||||
agent_id=agent.agent_id,
|
||||
agent_role=agent.role,
|
||||
agent_model=agent.model,
|
||||
task_id=task.task_id,
|
||||
start_time=time.time(),
|
||||
metadata=context or {}
|
||||
)
|
||||
|
||||
try:
|
||||
# 1. 更新心跳 - 开始执行
|
||||
await self.heartbeat.update_heartbeat(
|
||||
agent.agent_id,
|
||||
"working",
|
||||
task.description[:100], # 截断过长描述
|
||||
0
|
||||
)
|
||||
|
||||
# 2. 分析任务,识别需要的文件
|
||||
execution_context.required_files = await self._analyze_required_files(
|
||||
task.description
|
||||
)
|
||||
|
||||
# 3. 获取文件锁
|
||||
await self._acquire_locks(execution_context)
|
||||
|
||||
# 4. 构建执行上下文消息
|
||||
execution_context.messages = await self._build_messages(
|
||||
agent, task, execution_context
|
||||
)
|
||||
|
||||
# 5. 调用 LLM 执行任务
|
||||
llm_response = await self._call_llm(execution_context)
|
||||
|
||||
# 6. 处理结果
|
||||
result = await self._process_result(
|
||||
llm_response, execution_context
|
||||
)
|
||||
|
||||
# 7. 更新心跳 - 完成
|
||||
await self.heartbeat.update_heartbeat(
|
||||
agent.agent_id,
|
||||
"idle",
|
||||
"",
|
||||
100
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"任务执行失败: {e}", exc_info=True)
|
||||
|
||||
# 更新心跳为错误状态
|
||||
await self.heartbeat.update_heartbeat(
|
||||
agent.agent_id,
|
||||
"error",
|
||||
str(e),
|
||||
0
|
||||
)
|
||||
|
||||
return Result(
|
||||
success=False,
|
||||
output="",
|
||||
error=str(e),
|
||||
execution_time=time.time() - execution_context.start_time
|
||||
)
|
||||
|
||||
finally:
|
||||
# 8. 释放所有锁
|
||||
await self._release_locks(execution_context)
|
||||
|
||||
async def _analyze_required_files(self, task_description: str) -> List[str]:
|
||||
"""
|
||||
分析任务描述,识别需要的文件
|
||||
|
||||
使用 LLM 分析任务中提到的文件路径
|
||||
"""
|
||||
# 使用正则表达式快速匹配文件路径
|
||||
file_patterns = [
|
||||
r'[a-zA-Z_/\\][a-zA-Z0-9_/\\]*\.(?:py|js|ts|tsx|jsx|java|go|rs|c|h|cpp|hpp|css|html|md|json|yaml|yml)',
|
||||
r'[a-zA-Z_/\\][a-zA-Z0-9_/\\]*\.(?:py|js|ts|tsx|jsx)',
|
||||
r'src/[a-zA-Z0-9_/\\]*',
|
||||
r'app/[a-zA-Z0-9_/\\]*',
|
||||
r'components/[a-zA-Z0-9_/\\]*',
|
||||
r'pages/[a-zA-Z0-9_/\\]*',
|
||||
r'services/[a-zA-Z0-9_/\\]*',
|
||||
r'utils/[a-zA-Z0-9_/\\]*',
|
||||
]
|
||||
|
||||
files = set()
|
||||
for pattern in file_patterns:
|
||||
matches = re.findall(pattern, task_description)
|
||||
files.update(matches)
|
||||
|
||||
# 规范化路径
|
||||
normalized_files = []
|
||||
for f in files:
|
||||
# 转换反斜杠
|
||||
f = f.replace('\\', '/')
|
||||
# 移除重复的斜杠
|
||||
f = re.sub(r'/+', '/', f)
|
||||
if f not in normalized_files:
|
||||
normalized_files.append(f)
|
||||
|
||||
logger.debug(f"识别到文件: {normalized_files}")
|
||||
return normalized_files
|
||||
|
||||
async def _acquire_locks(self, context: ExecutionContext) -> None:
|
||||
"""获取所有需要的文件锁"""
|
||||
for file_path in context.required_files:
|
||||
success = await self.locks.acquire_lock(
|
||||
file_path,
|
||||
context.agent_id,
|
||||
context.agent_role.upper()
|
||||
)
|
||||
if success:
|
||||
context.acquired_locks.append(file_path)
|
||||
logger.debug(f"获取锁成功: {file_path}")
|
||||
else:
|
||||
logger.warning(f"获取锁失败: {file_path} (可能被其他 Agent 占用)")
|
||||
|
||||
async def _release_locks(self, context: ExecutionContext) -> None:
|
||||
"""释放所有获取的文件锁"""
|
||||
for file_path in context.acquired_locks:
|
||||
try:
|
||||
await self.locks.release_lock(file_path, context.agent_id)
|
||||
logger.debug(f"释放锁: {file_path}")
|
||||
except Exception as e:
|
||||
logger.warning(f"释放锁失败: {file_path}: {e}")
|
||||
|
||||
async def _build_messages(
|
||||
self,
|
||||
agent: AgentInfo,
|
||||
task: Task,
|
||||
context: ExecutionContext
|
||||
) -> List[LLMMessage]:
|
||||
"""构建 LLM 消息列表"""
|
||||
messages = []
|
||||
|
||||
# 系统提示词
|
||||
system_prompt = self._build_system_prompt(agent, context)
|
||||
messages.append(LLMMessage(role="system", content=system_prompt))
|
||||
|
||||
# 添加上下文信息
|
||||
if context.required_files:
|
||||
context_info = f"\n相关文件: {', '.join(context.required_files)}\n"
|
||||
# 尝试读取文件内容
|
||||
file_contents = await self._read_file_contents(context.required_files)
|
||||
if file_contents:
|
||||
context_info += f"\n文件内容:\n{file_contents}\n"
|
||||
messages.append(LLMMessage(role="user", content=context_info))
|
||||
|
||||
# 任务描述
|
||||
messages.append(LLMMessage(role="user", content=task.description))
|
||||
|
||||
# 添加额外上下文
|
||||
if task.context:
|
||||
context_str = f"\n额外上下文:\n{json.dumps(task.context, ensure_ascii=False, indent=2)}\n"
|
||||
messages.append(LLMMessage(role="user", content=context_str))
|
||||
|
||||
return messages
|
||||
|
||||
def _build_system_prompt(self, agent: AgentInfo, context: ExecutionContext) -> str:
|
||||
"""构建系统提示词"""
|
||||
role_prompts = {
|
||||
"architect": """
|
||||
你是一个系统架构师。你擅长:
|
||||
- 系统设计和模块划分
|
||||
- 技术选型和架构决策
|
||||
- 接口设计和数据流规划
|
||||
- 性能优化和扩展性考虑
|
||||
|
||||
请给出清晰、完整的架构方案。
|
||||
""",
|
||||
"pm": """
|
||||
你是一个产品经理。你擅长:
|
||||
- 需求分析和用户故事
|
||||
- 功能优先级排序
|
||||
- 产品规划
|
||||
- 用户体验考虑
|
||||
|
||||
请从用户角度分析需求。
|
||||
""",
|
||||
"developer": """
|
||||
你是一个高级开发工程师。你擅长:
|
||||
- 编写高质量、可维护的代码
|
||||
- 遵循最佳实践和编码规范
|
||||
- 考虑边界情况和错误处理
|
||||
- 编写清晰的注释和文档
|
||||
|
||||
请给出可以直接使用的代码实现。
|
||||
""",
|
||||
"reviewer": """
|
||||
你是一个代码审查专家。你擅长:
|
||||
- 发现代码中的潜在问题
|
||||
- 安全漏洞检测
|
||||
- 性能问题识别
|
||||
- 代码风格和可读性改进
|
||||
|
||||
请给出详细的审查意见。
|
||||
""",
|
||||
"qa": """
|
||||
你是一个测试工程师。你擅长:
|
||||
- 编写全面的测试用例
|
||||
- 边界条件测试
|
||||
- 自动化测试
|
||||
- 测试策略制定
|
||||
|
||||
请给出完整的测试方案。
|
||||
"""
|
||||
}
|
||||
|
||||
base_prompt = f"""你是 {agent.name},一个 AI 编程助手。
|
||||
|
||||
当前任务 ID: {context.task_id}
|
||||
你的角色: {agent.role}
|
||||
使用的模型: {agent.model}
|
||||
|
||||
工作原则:
|
||||
1. 仔细理解任务需求
|
||||
2. 给出清晰、具体的回答
|
||||
3. 如果涉及代码,确保代码正确且可运行
|
||||
4. 考虑边界情况和错误处理
|
||||
5. 必要时给出解释和说明
|
||||
"""
|
||||
|
||||
role_prompt = role_prompts.get(agent.role, "")
|
||||
|
||||
return base_prompt + role_prompt
|
||||
|
||||
async def _read_file_contents(self, file_paths: List[str]) -> str:
|
||||
"""读取文件内容(如果存在)"""
|
||||
contents = []
|
||||
for file_path in file_paths[:3]: # 限制读取文件数量
|
||||
full_path = self.work_dir / file_path
|
||||
if full_path.exists():
|
||||
try:
|
||||
with open(full_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
# 限制每个文件的内容长度
|
||||
if len(content) > 2000:
|
||||
content = content[:2000] + "\n... (文件过长,已截断)"
|
||||
contents.append(f"### {file_path}\n```\n{content}\n```")
|
||||
except Exception as e:
|
||||
logger.warning(f"读取文件失败: {file_path}: {e}")
|
||||
|
||||
return "\n\n".join(contents)
|
||||
|
||||
async def _call_llm(self, context: ExecutionContext) -> str:
|
||||
"""调用 LLM 执行任务"""
|
||||
if not self.llm:
|
||||
# 如果没有配置 LLM 服务,使用模拟响应
|
||||
return await self._mock_llm_response(context)
|
||||
|
||||
response = await self.llm.route_task(
|
||||
task=context.messages[-1].content,
|
||||
messages=context.messages,
|
||||
preferred_model=context.agent_model
|
||||
)
|
||||
|
||||
logger.info(f"LLM 调用完成: {response.provider}/{response.model}, "
|
||||
f"tokens: {response.tokens_used}, latency: {response.latency:.2f}s")
|
||||
|
||||
return response.content
|
||||
|
||||
async def _mock_llm_response(self, context: ExecutionContext) -> str:
|
||||
"""模拟 LLM 响应(用于测试)"""
|
||||
await asyncio.sleep(0.5) # 模拟延迟
|
||||
return f"""[模拟响应]
|
||||
|
||||
作为 {context.agent_role},我对任务的分析如下:
|
||||
|
||||
任务需要处理的文件: {', '.join(context.required_files) or '无'}
|
||||
|
||||
## 分析
|
||||
|
||||
这是一个模拟响应,表示系统正在正常工作。
|
||||
|
||||
## 建议
|
||||
|
||||
1. 配置 LLM API 密钥以启用真实 AI 能力
|
||||
2. 在环境变量中设置 ANTHROPIC_API_KEY 或 DEEPSEEK_API_KEY
|
||||
3. 重启服务后即可使用完整功能
|
||||
|
||||
---
|
||||
*Agent ID: {context.agent_id}*
|
||||
*任务 ID: {context.task_id}*
|
||||
"""
|
||||
|
||||
async def _process_result(
|
||||
self,
|
||||
llm_output: str,
|
||||
context: ExecutionContext
|
||||
) -> Result:
|
||||
"""处理 LLM 输出,返回格式化结果"""
|
||||
execution_time = time.time() - context.start_time
|
||||
|
||||
return Result(
|
||||
success=True,
|
||||
output=llm_output,
|
||||
metadata={
|
||||
"agent_id": context.agent_id,
|
||||
"agent_role": context.agent_role,
|
||||
"agent_model": context.agent_model,
|
||||
"task_id": context.task_id,
|
||||
"required_files": context.required_files,
|
||||
"acquired_locks": context.acquired_locks,
|
||||
"execution_time": execution_time
|
||||
},
|
||||
execution_time=execution_time
|
||||
)
|
||||
|
||||
async def create_execution_plan(
|
||||
self,
|
||||
agent: AgentInfo,
|
||||
task: str
|
||||
) -> ExecutionPlan:
|
||||
"""
|
||||
创建任务执行计划
|
||||
|
||||
使用 LLM 分析任务,生成详细的执行步骤
|
||||
"""
|
||||
if not self.llm:
|
||||
return self._create_mock_plan(task)
|
||||
|
||||
plan_prompt = f"""
|
||||
请分析以下任务,生成执行计划。
|
||||
|
||||
任务: {task}
|
||||
|
||||
请返回 JSON 格式的执行计划,包含:
|
||||
{{
|
||||
"steps": ["步骤1", "步骤2", ...],
|
||||
"required_files": ["file1.py", "file2.js", ...],
|
||||
"estimated_duration": "预计时间",
|
||||
"complexity": "simple|medium|complex",
|
||||
"requires_code_execution": true/false,
|
||||
"subtasks": ["子任务1", "子任务2", ...]
|
||||
}}
|
||||
"""
|
||||
|
||||
try:
|
||||
response = await self.llm.route_task(
|
||||
task=plan_prompt,
|
||||
messages=[LLMMessage(role="user", content=plan_prompt)]
|
||||
)
|
||||
|
||||
# 尝试解析 JSON
|
||||
import json
|
||||
plan_data = json.loads(response)
|
||||
|
||||
return ExecutionPlan(
|
||||
steps=plan_data.get("steps", []),
|
||||
required_files=plan_data.get("required_files", []),
|
||||
estimated_duration=plan_data.get("estimated_duration", ""),
|
||||
complexity=plan_data.get("complexity", "medium"),
|
||||
requires_code_execution=plan_data.get("requires_code_execution", False),
|
||||
subtasks=plan_data.get("subtasks", [])
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"解析执行计划失败: {e}")
|
||||
return self._create_mock_plan(task)
|
||||
|
||||
def _create_mock_plan(self, task: str) -> ExecutionPlan:
|
||||
"""创建模拟执行计划"""
|
||||
return ExecutionPlan(
|
||||
steps=[
|
||||
"1. 分析任务需求",
|
||||
"2. 查看相关文件",
|
||||
"3. 制定实现方案",
|
||||
"4. 执行实现"
|
||||
],
|
||||
estimated_duration="5-10 分钟",
|
||||
complexity="medium"
|
||||
)
|
||||
|
||||
|
||||
# 单例获取函数
|
||||
_executor: Optional[AgentExecutor] = None
|
||||
|
||||
|
||||
def get_agent_executor(llm_service: ModelRouter = None) -> AgentExecutor:
|
||||
"""获取 Agent 执行引擎单例"""
|
||||
global _executor
|
||||
if _executor is None:
|
||||
_executor = AgentExecutor(llm_service=llm_service)
|
||||
return _executor
|
||||
|
||||
|
||||
def reset_agent_executor():
|
||||
"""重置执行引擎(主要用于测试)"""
|
||||
global _executor
|
||||
_executor = None
|
||||
261
backend/app/services/agent_registry.py
Normal file
261
backend/app/services/agent_registry.py
Normal file
@@ -0,0 +1,261 @@
|
||||
"""
|
||||
Agent 注册服务 - 管理 Agent 的注册信息和状态
|
||||
每个 Agent 有独立的目录存储其配置和状态
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
from dataclasses import dataclass, asdict
|
||||
from enum import Enum
|
||||
|
||||
from .storage import get_storage
|
||||
|
||||
|
||||
class AgentRole(str, Enum):
|
||||
"""Agent 角色枚举"""
|
||||
ARCHITECT = "architect"
|
||||
PRODUCT_MANAGER = "pm"
|
||||
DEVELOPER = "developer"
|
||||
QA = "qa"
|
||||
REVIEWER = "reviewer"
|
||||
HUMAN = "human"
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentInfo:
|
||||
"""Agent 基本信息"""
|
||||
agent_id: str # 唯一标识符,如 claude-001
|
||||
name: str # 显示名称,如 Claude Code
|
||||
role: str # 角色:architect, pm, developer, qa, reviewer, human
|
||||
model: str # 模型:claude-opus-4.6, gpt-4o, human 等
|
||||
description: str = "" # 描述
|
||||
created_at: str = "" # 注册时间
|
||||
status: str = "idle" # 当前状态
|
||||
|
||||
def __post_init__(self):
|
||||
if not self.created_at:
|
||||
self.created_at = datetime.now().isoformat()
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentState:
|
||||
"""Agent 运行时状态"""
|
||||
agent_id: str
|
||||
current_task: str = ""
|
||||
progress: int = 0
|
||||
working_files: List[str] = None
|
||||
last_update: str = ""
|
||||
|
||||
def __post_init__(self):
|
||||
if self.working_files is None:
|
||||
self.working_files = []
|
||||
if not self.last_update:
|
||||
self.last_update = datetime.now().isoformat()
|
||||
|
||||
|
||||
class AgentRegistry:
|
||||
"""
|
||||
Agent 注册服务
|
||||
|
||||
管理所有 Agent 的注册信息和运行时状态
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._storage = get_storage()
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
def _get_agent_dir(self, agent_id: str) -> str:
|
||||
"""获取 Agent 目录路径"""
|
||||
return f"agents/{agent_id}"
|
||||
|
||||
def _get_agent_info_file(self, agent_id: str) -> str:
|
||||
"""获取 Agent 信息文件路径"""
|
||||
return f"{self._get_agent_dir(agent_id)}/info.json"
|
||||
|
||||
def _get_agent_state_file(self, agent_id: str) -> str:
|
||||
"""获取 Agent 状态文件路径"""
|
||||
return f"{self._get_agent_dir(agent_id)}/state.json"
|
||||
|
||||
async def register_agent(
|
||||
self,
|
||||
agent_id: str,
|
||||
name: str,
|
||||
role: str,
|
||||
model: str,
|
||||
description: str = ""
|
||||
) -> AgentInfo:
|
||||
"""
|
||||
注册新 Agent
|
||||
|
||||
Args:
|
||||
agent_id: Agent ID
|
||||
name: 显示名称
|
||||
role: 角色
|
||||
model: 模型
|
||||
description: 描述
|
||||
|
||||
Returns:
|
||||
注册的 Agent 信息
|
||||
"""
|
||||
async with self._lock:
|
||||
agent_info = AgentInfo(
|
||||
agent_id=agent_id,
|
||||
name=name,
|
||||
role=role,
|
||||
model=model,
|
||||
description=description
|
||||
)
|
||||
|
||||
# 确保 Agent 目录存在
|
||||
await self._storage.ensure_dir(self._get_agent_dir(agent_id))
|
||||
|
||||
# 保存 Agent 信息
|
||||
await self._storage.write_json(
|
||||
self._get_agent_info_file(agent_id),
|
||||
asdict(agent_info)
|
||||
)
|
||||
|
||||
# 初始化状态
|
||||
await self._storage.write_json(
|
||||
self._get_agent_state_file(agent_id),
|
||||
asdict(AgentState(agent_id=agent_id))
|
||||
)
|
||||
|
||||
return agent_info
|
||||
|
||||
async def get_agent(self, agent_id: str) -> Optional[AgentInfo]:
|
||||
"""
|
||||
获取 Agent 信息
|
||||
|
||||
Args:
|
||||
agent_id: Agent ID
|
||||
|
||||
Returns:
|
||||
Agent 信息,不存在返回 None
|
||||
"""
|
||||
data = await self._storage.read_json(self._get_agent_info_file(agent_id))
|
||||
if data:
|
||||
return AgentInfo(**data)
|
||||
return None
|
||||
|
||||
async def list_agents(self) -> List[AgentInfo]:
|
||||
"""
|
||||
列出所有已注册的 Agent
|
||||
|
||||
Returns:
|
||||
Agent 信息列表
|
||||
"""
|
||||
agents = []
|
||||
agents_dir = Path(self._storage.base_path) / "agents"
|
||||
|
||||
if not agents_dir.exists():
|
||||
return []
|
||||
|
||||
for agent_dir in agents_dir.iterdir():
|
||||
if agent_dir.is_dir():
|
||||
info_file = agent_dir / "info.json"
|
||||
if info_file.exists():
|
||||
data = await self._storage.read_json(f"agents/{agent_dir.name}/info.json")
|
||||
if data:
|
||||
agents.append(AgentInfo(**data))
|
||||
|
||||
return agents
|
||||
|
||||
async def update_state(
|
||||
self,
|
||||
agent_id: str,
|
||||
task: str = "",
|
||||
progress: int = 0,
|
||||
working_files: List[str] = None
|
||||
) -> None:
|
||||
"""
|
||||
更新 Agent 状态
|
||||
|
||||
Args:
|
||||
agent_id: Agent ID
|
||||
task: 当前任务
|
||||
progress: 进度 0-100
|
||||
working_files: 正在处理的文件列表
|
||||
"""
|
||||
async with self._lock:
|
||||
state_file = self._get_agent_state_file(agent_id)
|
||||
|
||||
# 读取现有状态
|
||||
existing = await self._storage.read_json(state_file)
|
||||
|
||||
# 更新状态
|
||||
state = AgentState(
|
||||
agent_id=agent_id,
|
||||
current_task=task or existing.get("current_task", ""),
|
||||
progress=progress or existing.get("progress", 0),
|
||||
working_files=working_files or existing.get("working_files", []),
|
||||
last_update=datetime.now().isoformat()
|
||||
)
|
||||
|
||||
await self._storage.write_json(state_file, asdict(state))
|
||||
|
||||
async def get_state(self, agent_id: str) -> Optional[AgentState]:
|
||||
"""
|
||||
获取 Agent 状态
|
||||
|
||||
Args:
|
||||
agent_id: Agent ID
|
||||
|
||||
Returns:
|
||||
Agent 状态,不存在返回 None
|
||||
"""
|
||||
data = await self._storage.read_json(self._get_agent_state_file(agent_id))
|
||||
if data:
|
||||
return AgentState(**data)
|
||||
return None
|
||||
|
||||
async def unregister_agent(self, agent_id: str) -> bool:
|
||||
"""
|
||||
注销 Agent
|
||||
|
||||
Args:
|
||||
agent_id: Agent ID
|
||||
|
||||
Returns:
|
||||
是否成功注销
|
||||
"""
|
||||
async with self._lock:
|
||||
agent_info = await self.get_agent(agent_id)
|
||||
if not agent_info:
|
||||
return False
|
||||
|
||||
# 删除 Agent 目录
|
||||
agent_dir = self._get_agent_dir(agent_id)
|
||||
# 实际实现中可能需要递归删除
|
||||
# 这里简化处理,只删除 info.json 和 state.json
|
||||
await self._storage.delete(f"{agent_dir}/info.json")
|
||||
await self._storage.delete(f"{agent_dir}/state.json")
|
||||
|
||||
return True
|
||||
|
||||
async def get_agents_by_role(self, role: str) -> List[AgentInfo]:
|
||||
"""
|
||||
获取指定角色的所有 Agent
|
||||
|
||||
Args:
|
||||
role: 角色
|
||||
|
||||
Returns:
|
||||
符合条件的 Agent 列表
|
||||
"""
|
||||
all_agents = await self.list_agents()
|
||||
return [agent for agent in all_agents if agent.role == role]
|
||||
|
||||
|
||||
# 全局单例
|
||||
_registry_instance: Optional[AgentRegistry] = None
|
||||
|
||||
|
||||
def get_agent_registry() -> AgentRegistry:
|
||||
"""获取 Agent 注册服务单例"""
|
||||
global _registry_instance
|
||||
if _registry_instance is None:
|
||||
_registry_instance = AgentRegistry()
|
||||
return _registry_instance
|
||||
115
backend/app/services/file_lock.py
Normal file
115
backend/app/services/file_lock.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""
|
||||
文件锁服务 - 管理 Agent 对文件的访问锁
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional
|
||||
from dataclasses import dataclass, asdict
|
||||
|
||||
from .storage import get_storage
|
||||
|
||||
|
||||
@dataclass
|
||||
class LockInfo:
|
||||
"""文件锁信息"""
|
||||
file_path: str
|
||||
agent_id: str
|
||||
acquired_at: str
|
||||
agent_name: str = ""
|
||||
|
||||
@property
|
||||
def elapsed_seconds(self) -> int:
|
||||
acquired_time = datetime.fromisoformat(self.acquired_at)
|
||||
return int((datetime.now() - acquired_time).total_seconds())
|
||||
|
||||
@property
|
||||
def elapsed_display(self) -> str:
|
||||
seconds = self.elapsed_seconds
|
||||
if seconds < 60:
|
||||
return f"{seconds}s ago"
|
||||
minutes = seconds // 60
|
||||
secs = seconds % 60
|
||||
return f"{minutes}m {secs:02d}s ago"
|
||||
|
||||
|
||||
class FileLockService:
|
||||
"""文件锁服务"""
|
||||
|
||||
LOCKS_FILE = "cache/file_locks.json"
|
||||
LOCK_TIMEOUT = 300
|
||||
|
||||
def __init__(self):
|
||||
self._storage = get_storage()
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
async def _load_locks(self) -> Dict[str, Dict]:
|
||||
return await self._storage.read_json(self.LOCKS_FILE)
|
||||
|
||||
async def _save_locks(self, locks: Dict[str, Dict]) -> None:
|
||||
await self._storage.write_json(self.LOCKS_FILE, locks)
|
||||
|
||||
def _is_expired(self, lock_data: Dict) -> bool:
|
||||
acquired_at = datetime.fromisoformat(lock_data["acquired_at"])
|
||||
return (datetime.now() - acquired_at).total_seconds() >= self.LOCK_TIMEOUT
|
||||
|
||||
async def _cleanup_expired(self, locks: Dict[str, Dict]) -> Dict[str, Dict]:
|
||||
return {k: v for k, v in locks.items() if not self._is_expired(v)}
|
||||
|
||||
async def acquire_lock(self, file_path: str, agent_id: str, agent_name: str = "") -> bool:
|
||||
async with self._lock:
|
||||
locks = await self._cleanup_expired(await self._load_locks())
|
||||
|
||||
if file_path in locks and locks[file_path]["agent_id"] != agent_id:
|
||||
return False
|
||||
|
||||
locks[file_path] = asdict(LockInfo(
|
||||
file_path=file_path,
|
||||
agent_id=agent_id,
|
||||
acquired_at=datetime.now().isoformat(),
|
||||
agent_name=agent_name
|
||||
))
|
||||
await self._save_locks(locks)
|
||||
return True
|
||||
|
||||
async def release_lock(self, file_path: str, agent_id: str) -> bool:
|
||||
async with self._lock:
|
||||
locks = await self._load_locks()
|
||||
|
||||
if file_path not in locks or locks[file_path]["agent_id"] != agent_id:
|
||||
return False
|
||||
|
||||
del locks[file_path]
|
||||
await self._save_locks(locks)
|
||||
return True
|
||||
|
||||
async def get_locks(self) -> List[LockInfo]:
|
||||
locks = await self._cleanup_expired(await self._load_locks())
|
||||
return [LockInfo(**data) for data in locks.values()]
|
||||
|
||||
async def check_locked(self, file_path: str) -> Optional[str]:
|
||||
locks = await self._cleanup_expired(await self._load_locks())
|
||||
return locks.get(file_path, {}).get("agent_id")
|
||||
|
||||
async def get_agent_locks(self, agent_id: str) -> List[LockInfo]:
|
||||
return [lock for lock in await self.get_locks() if lock.agent_id == agent_id]
|
||||
|
||||
async def release_all_agent_locks(self, agent_id: str) -> int:
|
||||
async with self._lock:
|
||||
locks = await self._load_locks()
|
||||
to_remove = [k for k, v in locks.items() if v["agent_id"] == agent_id]
|
||||
for k in to_remove:
|
||||
del locks[k]
|
||||
await self._save_locks(locks)
|
||||
return len(to_remove)
|
||||
|
||||
|
||||
# 简化单例实现
|
||||
_file_lock_service: Optional[FileLockService] = None
|
||||
|
||||
|
||||
def get_file_lock_service() -> FileLockService:
|
||||
global _file_lock_service
|
||||
if _file_lock_service is None:
|
||||
_file_lock_service = FileLockService()
|
||||
return _file_lock_service
|
||||
212
backend/app/services/heartbeat.py
Normal file
212
backend/app/services/heartbeat.py
Normal file
@@ -0,0 +1,212 @@
|
||||
"""
|
||||
心跳服务 - 管理 Agent 心跳和超时检测
|
||||
用于监控 Agent 活跃状态和检测掉线 Agent
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Optional
|
||||
from dataclasses import dataclass, asdict
|
||||
|
||||
from .storage import get_storage
|
||||
|
||||
|
||||
@dataclass
|
||||
class HeartbeatInfo:
|
||||
"""心跳信息"""
|
||||
agent_id: str
|
||||
last_heartbeat: str # 最后心跳时间 (ISO format)
|
||||
status: str # Agent 状态:working, waiting, idle, error
|
||||
current_task: str = "" # 当前任务描述
|
||||
progress: int = 0 # 任务进度 0-100
|
||||
|
||||
@property
|
||||
def elapsed_seconds(self) -> int:
|
||||
"""距最后心跳的秒数"""
|
||||
last_time = datetime.fromisoformat(self.last_heartbeat)
|
||||
return int((datetime.now() - last_time).total_seconds())
|
||||
|
||||
def is_timeout(self, timeout_seconds: int = 60) -> bool:
|
||||
"""是否超时"""
|
||||
return self.elapsed_seconds > timeout_seconds
|
||||
|
||||
@property
|
||||
def elapsed_display(self) -> str:
|
||||
"""格式化的时间差"""
|
||||
seconds = self.elapsed_seconds
|
||||
if seconds < 10:
|
||||
return f"{seconds}s ago"
|
||||
elif seconds < 60:
|
||||
return f"{seconds}s"
|
||||
minutes = seconds // 60
|
||||
secs = seconds % 60
|
||||
return f"{minutes}m {secs:02d}s"
|
||||
|
||||
|
||||
class HeartbeatService:
|
||||
"""
|
||||
心跳服务
|
||||
|
||||
管理所有 Agent 的心跳记录,检测超时 Agent
|
||||
"""
|
||||
|
||||
HEARTBEATS_FILE = "cache/heartbeats.json"
|
||||
DEFAULT_TIMEOUT = 60 # 默认超时时间(秒)
|
||||
|
||||
def __init__(self):
|
||||
self._storage = get_storage()
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
async def _load_heartbeats(self) -> Dict[str, Dict]:
|
||||
"""加载心跳记录"""
|
||||
return await self._storage.read_json(self.HEARTBEATS_FILE)
|
||||
|
||||
async def _save_heartbeats(self, heartbeats: Dict[str, Dict]) -> None:
|
||||
"""保存心跳记录"""
|
||||
await self._storage.write_json(self.HEARTBEATS_FILE, heartbeats)
|
||||
|
||||
async def update_heartbeat(
|
||||
self,
|
||||
agent_id: str,
|
||||
status: str,
|
||||
current_task: str = "",
|
||||
progress: int = 0
|
||||
) -> None:
|
||||
"""
|
||||
更新 Agent 心跳
|
||||
|
||||
Args:
|
||||
agent_id: Agent ID
|
||||
status: 状态 (working, waiting, idle, error)
|
||||
current_task: 当前任务
|
||||
progress: 进度 0-100
|
||||
"""
|
||||
async with self._lock:
|
||||
heartbeats = await self._load_heartbeats()
|
||||
|
||||
heartbeat_info = HeartbeatInfo(
|
||||
agent_id=agent_id,
|
||||
last_heartbeat=datetime.now().isoformat(),
|
||||
status=status,
|
||||
current_task=current_task,
|
||||
progress=progress
|
||||
)
|
||||
|
||||
heartbeats[agent_id] = asdict(heartbeat_info)
|
||||
await self._save_heartbeats(heartbeats)
|
||||
|
||||
async def get_heartbeat(self, agent_id: str) -> Optional[HeartbeatInfo]:
|
||||
"""
|
||||
获取指定 Agent 的心跳信息
|
||||
|
||||
Args:
|
||||
agent_id: Agent ID
|
||||
|
||||
Returns:
|
||||
心跳信息,如果不存在返回 None
|
||||
"""
|
||||
heartbeats = await self._load_heartbeats()
|
||||
data = heartbeats.get(agent_id)
|
||||
if data:
|
||||
return HeartbeatInfo(**data)
|
||||
return None
|
||||
|
||||
async def get_all_heartbeats(self) -> Dict[str, HeartbeatInfo]:
|
||||
"""
|
||||
获取所有 Agent 的心跳信息
|
||||
|
||||
Returns:
|
||||
Agent ID -> 心跳信息 的字典
|
||||
"""
|
||||
heartbeats = await self._load_heartbeats()
|
||||
result = {}
|
||||
for agent_id, data in heartbeats.items():
|
||||
result[agent_id] = HeartbeatInfo(**data)
|
||||
return result
|
||||
|
||||
async def check_timeout(self, timeout_seconds: int = None) -> List[str]:
|
||||
"""
|
||||
检查超时的 Agent
|
||||
|
||||
Args:
|
||||
timeout_seconds: 超时秒数,默认使用 DEFAULT_TIMEOUT
|
||||
|
||||
Returns:
|
||||
超时的 Agent ID 列表
|
||||
"""
|
||||
if timeout_seconds is None:
|
||||
timeout_seconds = self.DEFAULT_TIMEOUT
|
||||
|
||||
all_heartbeats = await self.get_all_heartbeats()
|
||||
timeout_agents = []
|
||||
|
||||
for agent_id, heartbeat in all_heartbeats.items():
|
||||
if heartbeat.is_timeout(timeout_seconds):
|
||||
timeout_agents.append(agent_id)
|
||||
|
||||
return timeout_agents
|
||||
|
||||
async def remove_heartbeat(self, agent_id: str) -> bool:
|
||||
"""
|
||||
移除 Agent 的心跳记录
|
||||
|
||||
Args:
|
||||
agent_id: Agent ID
|
||||
|
||||
Returns:
|
||||
是否成功移除
|
||||
"""
|
||||
async with self._lock:
|
||||
heartbeats = await self._load_heartbeats()
|
||||
if agent_id in heartbeats:
|
||||
del heartbeats[agent_id]
|
||||
await self._save_heartbeats(heartbeats)
|
||||
return True
|
||||
return False
|
||||
|
||||
async def get_active_agents(self, within_seconds: int = 120) -> List[str]:
|
||||
"""
|
||||
获取活跃的 Agent 列表
|
||||
|
||||
Args:
|
||||
within_seconds: 活跃判定时间窗口(秒)
|
||||
|
||||
Returns:
|
||||
活跃 Agent ID 列表
|
||||
"""
|
||||
all_heartbeats = await self.get_all_heartbeats()
|
||||
active_agents = []
|
||||
|
||||
for agent_id, heartbeat in all_heartbeats.items():
|
||||
if heartbeat.elapsed_seconds <= within_seconds:
|
||||
active_agents.append(agent_id)
|
||||
|
||||
return active_agents
|
||||
|
||||
async def get_agents_by_status(self, status: str) -> List[HeartbeatInfo]:
|
||||
"""
|
||||
获取指定状态的所有 Agent
|
||||
|
||||
Args:
|
||||
status: 状态 (working, waiting, idle, error)
|
||||
|
||||
Returns:
|
||||
符合条件的 Agent 心跳信息列表
|
||||
"""
|
||||
all_heartbeats = await self.get_all_heartbeats()
|
||||
return [
|
||||
hb for hb in all_heartbeats.values()
|
||||
if hb.status == status
|
||||
]
|
||||
|
||||
|
||||
# 全局单例
|
||||
_heartbeat_service_instance: Optional[HeartbeatService] = None
|
||||
|
||||
|
||||
def get_heartbeat_service() -> HeartbeatService:
|
||||
"""获取心跳服务单例"""
|
||||
global _heartbeat_service_instance
|
||||
if _heartbeat_service_instance is None:
|
||||
_heartbeat_service_instance = HeartbeatService()
|
||||
return _heartbeat_service_instance
|
||||
378
backend/app/services/human_input.py
Normal file
378
backend/app/services/human_input.py
Normal file
@@ -0,0 +1,378 @@
|
||||
"""
|
||||
人类输入服务 - 管理人类参与者的任务请求和会议评论
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional
|
||||
from dataclasses import dataclass, field, asdict
|
||||
|
||||
from .storage import get_storage
|
||||
|
||||
|
||||
@dataclass
|
||||
class TaskRequest:
|
||||
"""人类任务请求"""
|
||||
id: str
|
||||
from_user: str # 提交者 ID
|
||||
timestamp: str # 提交时间
|
||||
priority: str # high | medium | low
|
||||
type: str # 任务类型
|
||||
title: str = "" # 任务标题
|
||||
content: str = "" # 任务内容
|
||||
target_files: List[str] = field(default_factory=list)
|
||||
suggested_agent: str = "" # 建议的 Agent
|
||||
urgent: bool = False
|
||||
status: str = "pending" # pending | processing | completed | rejected
|
||||
|
||||
@property
|
||||
def is_urgent(self) -> bool:
|
||||
"""是否为紧急任务(高优先级 + urgent 标记)"""
|
||||
return self.priority == "high" and self.urgent
|
||||
|
||||
|
||||
@dataclass
|
||||
class MeetingComment:
|
||||
"""会议评论"""
|
||||
id: str
|
||||
from_user: str # 提交者 ID
|
||||
meeting_id: str
|
||||
timestamp: str
|
||||
type: str # proposal | question | correction
|
||||
priority: str = "normal"
|
||||
content: str = ""
|
||||
status: str = "pending" # pending | addressed | ignored
|
||||
|
||||
|
||||
@dataclass
|
||||
class HumanParticipant:
|
||||
"""人类参与者信息"""
|
||||
id: str
|
||||
name: str
|
||||
role: str = "" # tech_lead | product_owner | developer
|
||||
status: str = "offline" # online | offline | busy
|
||||
avatar: str = "👤"
|
||||
|
||||
|
||||
class HumanInputService:
|
||||
"""
|
||||
人类输入服务
|
||||
|
||||
管理 humans.json 文件,处理人类任务请求和会议评论
|
||||
"""
|
||||
|
||||
HUMANS_FILE = "humans.json"
|
||||
|
||||
# 优先级权重
|
||||
PRIORITY_WEIGHT = {
|
||||
"high": 3,
|
||||
"medium": 2,
|
||||
"low": 1,
|
||||
"normal": 0
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self._storage = get_storage()
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
async def _load_humans(self) -> Dict:
|
||||
"""加载 humans.json"""
|
||||
return await self._storage.read_json(self.HUMANS_FILE)
|
||||
|
||||
async def _save_humans(self, data: Dict) -> None:
|
||||
"""保存 humans.json"""
|
||||
await self._storage.write_json(self.HUMANS_FILE, data)
|
||||
|
||||
async def _ensure_structure(self) -> None:
|
||||
"""确保 humans.json 结构完整"""
|
||||
async with self._lock:
|
||||
data = await self._load_humans()
|
||||
if not data:
|
||||
data = {
|
||||
"version": "1.0",
|
||||
"last_updated": datetime.now().isoformat(),
|
||||
"participants": {},
|
||||
"task_requests": [],
|
||||
"meeting_comments": [],
|
||||
"human_states": {}
|
||||
}
|
||||
await self._save_humans(data)
|
||||
|
||||
async def register_participant(
|
||||
self,
|
||||
user_id: str,
|
||||
name: str,
|
||||
role: str = "",
|
||||
avatar: str = "👤"
|
||||
) -> None:
|
||||
"""注册人类参与者"""
|
||||
await self._ensure_structure()
|
||||
async with self._lock:
|
||||
data = await self._load_humans()
|
||||
data["participants"][user_id] = asdict(HumanParticipant(
|
||||
id=user_id,
|
||||
name=name,
|
||||
role=role,
|
||||
avatar=avatar
|
||||
))
|
||||
data["last_updated"] = datetime.now().isoformat()
|
||||
await self._save_humans(data)
|
||||
|
||||
async def add_task_request(
|
||||
self,
|
||||
from_user: str,
|
||||
content: str,
|
||||
priority: str = "medium",
|
||||
task_type: str = "user_task",
|
||||
title: str = "",
|
||||
target_files: List[str] = None,
|
||||
suggested_agent: str = "",
|
||||
urgent: bool = False
|
||||
) -> str:
|
||||
"""
|
||||
添加任务请求
|
||||
|
||||
Returns:
|
||||
任务 ID
|
||||
"""
|
||||
await self._ensure_structure()
|
||||
async with self._lock:
|
||||
data = await self._load_humans()
|
||||
|
||||
task_id = f"req_{uuid.uuid4().hex[:8]}"
|
||||
task = TaskRequest(
|
||||
id=task_id,
|
||||
from_user=from_user,
|
||||
timestamp=datetime.now().isoformat(),
|
||||
priority=priority,
|
||||
type=task_type,
|
||||
title=title,
|
||||
content=content,
|
||||
target_files=target_files or [],
|
||||
suggested_agent=suggested_agent,
|
||||
urgent=urgent
|
||||
)
|
||||
|
||||
# 保存时转换为 JSON 兼容格式(from_user -> from)
|
||||
task_dict = asdict(task)
|
||||
task_dict["from"] = task_dict.pop("from_user")
|
||||
data["task_requests"].append(task_dict)
|
||||
data["last_updated"] = datetime.now().isoformat()
|
||||
await self._save_humans(data)
|
||||
|
||||
return task_id
|
||||
|
||||
async def get_pending_tasks(
|
||||
self,
|
||||
priority_filter: str = None,
|
||||
agent_filter: str = None
|
||||
) -> List[TaskRequest]:
|
||||
"""
|
||||
获取待处理的任务请求
|
||||
|
||||
Args:
|
||||
priority_filter: 过滤优先级
|
||||
agent_filter: 过滤建议的 Agent
|
||||
|
||||
Returns:
|
||||
按优先级排序的任务列表
|
||||
"""
|
||||
await self._ensure_structure()
|
||||
data = await self._load_humans()
|
||||
|
||||
tasks = []
|
||||
for t in data.get("task_requests", []):
|
||||
if t["status"] != "pending":
|
||||
continue
|
||||
if priority_filter and t["priority"] != priority_filter:
|
||||
continue
|
||||
if agent_filter and t.get("suggested_agent") != agent_filter:
|
||||
continue
|
||||
# 转换 JSON 格式(from -> from_user)
|
||||
t_dict = t.copy()
|
||||
t_dict["from_user"] = t_dict.pop("from", "")
|
||||
tasks.append(TaskRequest(**t_dict))
|
||||
|
||||
# 按优先级排序
|
||||
tasks.sort(
|
||||
key=lambda t: (
|
||||
-self.PRIORITY_WEIGHT.get(t.priority, 0),
|
||||
-t.urgent,
|
||||
t.timestamp
|
||||
)
|
||||
)
|
||||
return tasks
|
||||
|
||||
async def get_urgent_tasks(self) -> List[TaskRequest]:
|
||||
"""获取紧急任务(高优先级 + urgent)"""
|
||||
return [t for t in await self.get_pending_tasks() if t.is_urgent]
|
||||
|
||||
async def mark_task_processing(self, task_id: str) -> bool:
|
||||
"""标记任务为处理中"""
|
||||
async with self._lock:
|
||||
data = await self._load_humans()
|
||||
for task in data.get("task_requests", []):
|
||||
if task["id"] == task_id:
|
||||
task["status"] = "processing"
|
||||
data["last_updated"] = datetime.now().isoformat()
|
||||
await self._save_humans(data)
|
||||
return True
|
||||
return False
|
||||
|
||||
async def mark_task_completed(self, task_id: str) -> bool:
|
||||
"""标记任务为已完成"""
|
||||
async with self._lock:
|
||||
data = await self._load_humans()
|
||||
for task in data.get("task_requests", []):
|
||||
if task["id"] == task_id:
|
||||
task["status"] = "completed"
|
||||
data["last_updated"] = datetime.now().isoformat()
|
||||
await self._save_humans(data)
|
||||
return True
|
||||
return False
|
||||
|
||||
async def add_meeting_comment(
|
||||
self,
|
||||
from_user: str,
|
||||
meeting_id: str,
|
||||
content: str,
|
||||
comment_type: str = "proposal",
|
||||
priority: str = "normal"
|
||||
) -> str:
|
||||
"""
|
||||
添加会议评论
|
||||
|
||||
Returns:
|
||||
评论 ID
|
||||
"""
|
||||
await self._ensure_structure()
|
||||
async with self._lock:
|
||||
data = await self._load_humans()
|
||||
|
||||
comment_id = f"comment_{uuid.uuid4().hex[:8]}"
|
||||
comment = MeetingComment(
|
||||
id=comment_id,
|
||||
from_user=from_user,
|
||||
meeting_id=meeting_id,
|
||||
timestamp=datetime.now().isoformat(),
|
||||
type=comment_type,
|
||||
priority=priority,
|
||||
content=content
|
||||
)
|
||||
|
||||
# 保存时转换为 JSON 兼容格式(from_user -> from)
|
||||
comment_dict = asdict(comment)
|
||||
comment_dict["from"] = comment_dict.pop("from_user")
|
||||
data["meeting_comments"].append(comment_dict)
|
||||
data["last_updated"] = datetime.now().isoformat()
|
||||
await self._save_humans(data)
|
||||
|
||||
return comment_id
|
||||
|
||||
async def get_pending_comments(self, meeting_id: str = None) -> List[MeetingComment]:
|
||||
"""
|
||||
获取待处理的会议评论
|
||||
|
||||
Args:
|
||||
meeting_id: 过滤指定会议的评论
|
||||
|
||||
Returns:
|
||||
评论列表
|
||||
"""
|
||||
await self._ensure_structure()
|
||||
data = await self._load_humans()
|
||||
|
||||
comments = []
|
||||
for c in data.get("meeting_comments", []):
|
||||
if c["status"] != "pending":
|
||||
continue
|
||||
if meeting_id and c["meeting_id"] != meeting_id:
|
||||
continue
|
||||
# 转换 JSON 格式(from -> from_user)
|
||||
c_dict = c.copy()
|
||||
c_dict["from_user"] = c_dict.pop("from", "")
|
||||
comments.append(MeetingComment(**c_dict))
|
||||
|
||||
return comments
|
||||
|
||||
async def mark_comment_addressed(self, comment_id: str) -> bool:
|
||||
"""标记评论为已处理"""
|
||||
async with self._lock:
|
||||
data = await self._load_humans()
|
||||
for comment in data["meeting_comments"]:
|
||||
if comment["id"] == comment_id:
|
||||
comment["status"] = "addressed"
|
||||
data["last_updated"] = datetime.now().isoformat()
|
||||
await self._save_humans(data)
|
||||
return True
|
||||
return False
|
||||
|
||||
async def get_participants(self) -> List[HumanParticipant]:
|
||||
"""获取所有人类参与者"""
|
||||
await self._ensure_structure()
|
||||
data = await self._load_humans()
|
||||
|
||||
participants = []
|
||||
for p in data.get("participants", {}).values():
|
||||
participants.append(HumanParticipant(**p))
|
||||
return participants
|
||||
|
||||
async def update_user_status(
|
||||
self,
|
||||
user_id: str,
|
||||
status: str,
|
||||
current_focus: str = ""
|
||||
) -> bool:
|
||||
"""更新用户状态"""
|
||||
await self._ensure_structure()
|
||||
async with self._lock:
|
||||
data = await self._load_humans()
|
||||
|
||||
if user_id not in data.get("participants", {}):
|
||||
return False
|
||||
|
||||
data["participants"][user_id]["status"] = status
|
||||
|
||||
if "human_states" not in data:
|
||||
data["human_states"] = {}
|
||||
|
||||
data["human_states"][user_id] = {
|
||||
"status": status,
|
||||
"current_focus": current_focus,
|
||||
"last_update": datetime.now().isoformat()
|
||||
}
|
||||
data["last_updated"] = datetime.now().isoformat()
|
||||
await self._save_humans(data)
|
||||
return True
|
||||
|
||||
async def get_summary(self) -> Dict:
|
||||
"""获取人类输入服务的摘要信息"""
|
||||
await self._ensure_structure()
|
||||
data = await self._load_humans()
|
||||
|
||||
pending_tasks = await self.get_pending_tasks()
|
||||
urgent_tasks = await self.get_urgent_tasks()
|
||||
pending_comments = await self.get_pending_comments()
|
||||
participants = await self.get_participants()
|
||||
|
||||
return {
|
||||
"participants": len(participants),
|
||||
"online_users": len([p for p in participants if p.status == "online"]),
|
||||
"pending_tasks": len(pending_tasks),
|
||||
"urgent_tasks": len(urgent_tasks),
|
||||
"pending_comments": len(pending_comments),
|
||||
"last_updated": data.get("last_updated", "")
|
||||
}
|
||||
|
||||
|
||||
# 全局单例
|
||||
_human_input_service: Optional[HumanInputService] = None
|
||||
|
||||
|
||||
def get_human_input_service() -> HumanInputService:
|
||||
"""获取人类输入服务单例"""
|
||||
global _human_input_service
|
||||
if _human_input_service is None:
|
||||
_human_input_service = HumanInputService()
|
||||
return _human_input_service
|
||||
669
backend/app/services/llm_service.py
Normal file
669
backend/app/services/llm_service.py
Normal file
@@ -0,0 +1,669 @@
|
||||
"""
|
||||
LLM 服务层
|
||||
|
||||
提供统一的 LLM 调用接口,支持多个提供商:
|
||||
- Anthropic (Claude)
|
||||
- OpenAI (GPT)
|
||||
- DeepSeek
|
||||
- Ollama (本地模型)
|
||||
- Google (Gemini)
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import asyncio
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, List, Optional, Any, Union
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
import time
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TaskType(Enum):
|
||||
"""任务类型分类"""
|
||||
COMPLEX_REASONING = "complex_reasoning"
|
||||
CODE_GENERATION = "code_generation"
|
||||
CODE_REVIEW = "code_review"
|
||||
SIMPLE_TASK = "simple_task"
|
||||
COST_SENSITIVE = "cost_sensitive"
|
||||
LOCAL_PRIVACY = "local_privacy"
|
||||
MULTIMODAL = "multimodal"
|
||||
ARCHITECTURE_DESIGN = "architecture_design"
|
||||
TEST_GENERATION = "test_generation"
|
||||
|
||||
|
||||
@dataclass
|
||||
class LLMMessage:
|
||||
"""LLM 消息"""
|
||||
role: str # system, user, assistant
|
||||
content: str
|
||||
images: Optional[List[str]] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class LLMResponse:
|
||||
"""LLM 响应"""
|
||||
content: str
|
||||
model: str
|
||||
provider: str
|
||||
tokens_used: int = 0
|
||||
finish_reason: str = ""
|
||||
latency: float = 0.0
|
||||
|
||||
|
||||
@dataclass
|
||||
class LLMConfig:
|
||||
"""LLM 配置"""
|
||||
# Anthropic
|
||||
anthropic_api_key: Optional[str] = None
|
||||
anthropic_base_url: str = "https://api.anthropic.com"
|
||||
|
||||
# OpenAI
|
||||
openai_api_key: Optional[str] = None
|
||||
openai_base_url: str = "https://api.openai.com/v1"
|
||||
|
||||
# DeepSeek
|
||||
deepseek_api_key: Optional[str] = None
|
||||
deepseek_base_url: str = "https://api.deepseek.com"
|
||||
|
||||
# Google
|
||||
google_api_key: Optional[str] = None
|
||||
|
||||
# Ollama
|
||||
ollama_base_url: str = "http://localhost:11434"
|
||||
|
||||
# 通用设置
|
||||
default_timeout: int = 120
|
||||
max_retries: int = 3
|
||||
temperature: float = 0.7
|
||||
max_tokens: int = 4096
|
||||
|
||||
@classmethod
|
||||
def from_env(cls) -> "LLMConfig":
|
||||
"""从环境变量加载配置"""
|
||||
return cls(
|
||||
anthropic_api_key=os.getenv("ANTHROPIC_API_KEY"),
|
||||
openai_api_key=os.getenv("OPENAI_API_KEY"),
|
||||
deepseek_api_key=os.getenv("DEEPSEEK_API_KEY"),
|
||||
google_api_key=os.getenv("GOOGLE_API_KEY"),
|
||||
ollama_base_url=os.getenv("OLLAMA_BASE_URL", "http://localhost:11434"),
|
||||
default_timeout=int(os.getenv("LLM_TIMEOUT", "120")),
|
||||
max_retries=int(os.getenv("LLM_MAX_RETRIES", "3")),
|
||||
temperature=float(os.getenv("LLM_TEMPERATURE", "0.7")),
|
||||
max_tokens=int(os.getenv("LLM_MAX_TOKENS", "4096"))
|
||||
)
|
||||
|
||||
|
||||
class LLMProvider(ABC):
|
||||
"""LLM 提供商抽象基类"""
|
||||
|
||||
def __init__(self, config: LLMConfig):
|
||||
self.config = config
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def provider_name(self) -> str:
|
||||
"""提供商名称"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def chat_completion(
|
||||
self,
|
||||
model: str,
|
||||
messages: List[LLMMessage],
|
||||
temperature: float = None,
|
||||
max_tokens: int = None,
|
||||
**kwargs
|
||||
) -> LLMResponse:
|
||||
"""聊天补全"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def stream_completion(
|
||||
self,
|
||||
model: str,
|
||||
messages: List[LLMMessage],
|
||||
**kwargs
|
||||
):
|
||||
"""流式补全"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_available_models(self) -> List[str]:
|
||||
"""获取可用模型列表"""
|
||||
pass
|
||||
|
||||
async def _retry_with_backoff(self, func, *args, **kwargs):
|
||||
"""带退避的重试机制"""
|
||||
last_error = None
|
||||
for attempt in range(self.config.max_retries):
|
||||
try:
|
||||
return await func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
if attempt < self.config.max_retries - 1:
|
||||
wait_time = 2 ** attempt
|
||||
logger.warning(f"Attempt {attempt + 1} failed, retrying in {wait_time}s: {e}")
|
||||
await asyncio.sleep(wait_time)
|
||||
else:
|
||||
logger.error(f"All {self.config.max_retries} attempts failed")
|
||||
raise last_error
|
||||
|
||||
|
||||
class AnthropicProvider(LLMProvider):
|
||||
"""Anthropic Claude 提供商"""
|
||||
|
||||
@property
|
||||
def provider_name(self) -> str:
|
||||
return "anthropic"
|
||||
|
||||
def __init__(self, config: LLMConfig):
|
||||
super().__init__(config)
|
||||
self._client = None
|
||||
|
||||
def _get_client(self):
|
||||
"""懒加载客户端"""
|
||||
if self._client is None:
|
||||
try:
|
||||
import anthropic
|
||||
self._client = anthropic.AsyncAnthropic(
|
||||
api_key=self.config.anthropic_api_key,
|
||||
base_url=self.config.anthropic_base_url,
|
||||
timeout=self.config.default_timeout
|
||||
)
|
||||
except ImportError:
|
||||
raise ImportError("请安装 anthropic 包: pip install anthropic")
|
||||
return self._client
|
||||
|
||||
async def chat_completion(
|
||||
self,
|
||||
model: str,
|
||||
messages: List[LLMMessage],
|
||||
temperature: float = None,
|
||||
max_tokens: int = None,
|
||||
**kwargs
|
||||
) -> LLMResponse:
|
||||
start_time = time.time()
|
||||
|
||||
# 分离系统消息
|
||||
system_message = ""
|
||||
user_messages = []
|
||||
for msg in messages:
|
||||
if msg.role == "system":
|
||||
system_message = msg.content
|
||||
else:
|
||||
user_messages.append({
|
||||
"role": msg.role,
|
||||
"content": msg.content
|
||||
})
|
||||
|
||||
client = self._get_client()
|
||||
|
||||
response = await self._retry_with_backoff(
|
||||
client.messages.create,
|
||||
model=model,
|
||||
system=system_message if system_message else None,
|
||||
messages=user_messages,
|
||||
temperature=temperature or self.config.temperature,
|
||||
max_tokens=max_tokens or self.config.max_tokens,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
latency = time.time() - start_time
|
||||
|
||||
return LLMResponse(
|
||||
content=response.content[0].text,
|
||||
model=model,
|
||||
provider=self.provider_name,
|
||||
tokens_used=response.usage.input_tokens + response.usage.output_tokens,
|
||||
finish_reason=response.stop_reason,
|
||||
latency=latency
|
||||
)
|
||||
|
||||
async def stream_completion(self, model: str, messages: List[LLMMessage], **kwargs):
|
||||
client = self._get_client()
|
||||
system_message = ""
|
||||
user_messages = []
|
||||
for msg in messages:
|
||||
if msg.role == "system":
|
||||
system_message = msg.content
|
||||
else:
|
||||
user_messages.append({"role": msg.role, "content": msg.content})
|
||||
|
||||
async with client.messages.stream(
|
||||
model=model,
|
||||
system=system_message if system_message else None,
|
||||
messages=user_messages,
|
||||
max_tokens=self.config.max_tokens,
|
||||
**kwargs
|
||||
) as stream:
|
||||
async for text in stream.text_stream:
|
||||
yield text
|
||||
|
||||
def get_available_models(self) -> List[str]:
|
||||
return [
|
||||
"claude-opus-4.6",
|
||||
"claude-sonnet-4.6",
|
||||
"claude-haiku-4.6",
|
||||
"claude-3-5-sonnet-20241022",
|
||||
"claude-3-5-haiku-20241022"
|
||||
]
|
||||
|
||||
|
||||
class OpenAIProvider(LLMProvider):
|
||||
"""OpenAI GPT 提供商"""
|
||||
|
||||
@property
|
||||
def provider_name(self) -> str:
|
||||
return "openai"
|
||||
|
||||
def __init__(self, config: LLMConfig):
|
||||
super().__init__(config)
|
||||
self._client = None
|
||||
|
||||
def _get_client(self):
|
||||
if self._client is None:
|
||||
try:
|
||||
import openai
|
||||
self._client = openai.AsyncOpenAI(
|
||||
api_key=self.config.openai_api_key,
|
||||
base_url=self.config.openai_base_url,
|
||||
timeout=self.config.default_timeout
|
||||
)
|
||||
except ImportError:
|
||||
raise ImportError("请安装 openai 包: pip install openai")
|
||||
return self._client
|
||||
|
||||
async def chat_completion(
|
||||
self,
|
||||
model: str,
|
||||
messages: List[LLMMessage],
|
||||
temperature: float = None,
|
||||
max_tokens: int = None,
|
||||
**kwargs
|
||||
) -> LLMResponse:
|
||||
start_time = time.time()
|
||||
|
||||
client = self._get_client()
|
||||
|
||||
api_messages = [
|
||||
{"role": msg.role, "content": msg.content}
|
||||
for msg in messages
|
||||
]
|
||||
|
||||
response = await self._retry_with_backoff(
|
||||
client.chat.completions.create,
|
||||
model=model,
|
||||
messages=api_messages,
|
||||
temperature=temperature or self.config.temperature,
|
||||
max_tokens=max_tokens or self.config.max_tokens,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
latency = time.time() - start_time
|
||||
|
||||
return LLMResponse(
|
||||
content=response.choices[0].message.content,
|
||||
model=model,
|
||||
provider=self.provider_name,
|
||||
tokens_used=response.usage.total_tokens,
|
||||
finish_reason=response.choices[0].finish_reason,
|
||||
latency=latency
|
||||
)
|
||||
|
||||
async def stream_completion(self, model: str, messages: List[LLMMessage], **kwargs):
|
||||
client = self._get_client()
|
||||
api_messages = [{"role": msg.role, "content": msg.content} for msg in messages]
|
||||
|
||||
stream = await client.chat.completions.create(
|
||||
model=model,
|
||||
messages=api_messages,
|
||||
stream=True,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
async for chunk in stream:
|
||||
if chunk.choices[0].delta.content:
|
||||
yield chunk.choices[0].delta.content
|
||||
|
||||
def get_available_models(self) -> List[str]:
|
||||
return [
|
||||
"gpt-4o",
|
||||
"gpt-4o-mini",
|
||||
"gpt-4-turbo",
|
||||
"gpt-3.5-turbo"
|
||||
]
|
||||
|
||||
|
||||
class DeepSeekProvider(LLMProvider):
|
||||
"""DeepSeek 提供商"""
|
||||
|
||||
@property
|
||||
def provider_name(self) -> str:
|
||||
return "deepseek"
|
||||
|
||||
def __init__(self, config: LLMConfig):
|
||||
super().__init__(config)
|
||||
self._client = None
|
||||
|
||||
def _get_client(self):
|
||||
if self._client is None:
|
||||
try:
|
||||
import openai
|
||||
self._client = openai.AsyncOpenAI(
|
||||
api_key=self.config.deepseek_api_key,
|
||||
base_url=self.config.deepseek_base_url,
|
||||
timeout=self.config.default_timeout
|
||||
)
|
||||
except ImportError:
|
||||
raise ImportError("请安装 openai 包: pip install openai")
|
||||
return self._client
|
||||
|
||||
async def chat_completion(
|
||||
self,
|
||||
model: str,
|
||||
messages: List[LLMMessage],
|
||||
temperature: float = None,
|
||||
max_tokens: int = None,
|
||||
**kwargs
|
||||
) -> LLMResponse:
|
||||
start_time = time.time()
|
||||
|
||||
client = self._get_client()
|
||||
api_messages = [{"role": msg.role, "content": msg.content} for msg in messages]
|
||||
|
||||
response = await self._retry_with_backoff(
|
||||
client.chat.completions.create,
|
||||
model=model,
|
||||
messages=api_messages,
|
||||
temperature=temperature or self.config.temperature,
|
||||
max_tokens=max_tokens or self.config.max_tokens,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
latency = time.time() - start_time
|
||||
|
||||
return LLMResponse(
|
||||
content=response.choices[0].message.content,
|
||||
model=model,
|
||||
provider=self.provider_name,
|
||||
tokens_used=response.usage.total_tokens,
|
||||
finish_reason=response.choices[0].finish_reason,
|
||||
latency=latency
|
||||
)
|
||||
|
||||
async def stream_completion(self, model: str, messages: List[LLMMessage], **kwargs):
|
||||
client = self._get_client()
|
||||
api_messages = [{"role": msg.role, "content": msg.content} for msg in messages]
|
||||
|
||||
stream = await client.chat.completions.create(
|
||||
model=model,
|
||||
messages=api_messages,
|
||||
stream=True,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
async for chunk in stream:
|
||||
if chunk.choices[0].delta.content:
|
||||
yield chunk.choices[0].delta.content
|
||||
|
||||
def get_available_models(self) -> List[str]:
|
||||
return [
|
||||
"deepseek-chat",
|
||||
"deepseek-coder"
|
||||
]
|
||||
|
||||
|
||||
class OllamaProvider(LLMProvider):
|
||||
"""Ollama 本地模型提供商"""
|
||||
|
||||
@property
|
||||
def provider_name(self) -> str:
|
||||
return "ollama"
|
||||
|
||||
def __init__(self, config: LLMConfig):
|
||||
super().__init__(config)
|
||||
self._base_url = config.ollama_base_url
|
||||
|
||||
async def chat_completion(
|
||||
self,
|
||||
model: str,
|
||||
messages: List[LLMMessage],
|
||||
temperature: float = None,
|
||||
max_tokens: int = None,
|
||||
**kwargs
|
||||
) -> LLMResponse:
|
||||
import aiohttp
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
api_messages = [{"role": msg.role, "content": msg.content} for msg in messages]
|
||||
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": api_messages,
|
||||
"stream": False,
|
||||
"options": {
|
||||
"temperature": temperature or self.config.temperature,
|
||||
"num_predict": max_tokens or self.config.max_tokens
|
||||
}
|
||||
}
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(
|
||||
f"{self._base_url}/api/chat",
|
||||
json=payload,
|
||||
timeout=self.config.default_timeout
|
||||
) as response:
|
||||
result = await response.json()
|
||||
|
||||
latency = time.time() - start_time
|
||||
|
||||
return LLMResponse(
|
||||
content=result.get("message", {}).get("content", ""),
|
||||
model=model,
|
||||
provider=self.provider_name,
|
||||
tokens_used=result.get("prompt_eval_count", 0) + result.get("eval_count", 0),
|
||||
latency=latency
|
||||
)
|
||||
|
||||
async def stream_completion(self, model: str, messages: List[LLMMessage], **kwargs):
|
||||
import aiohttp
|
||||
|
||||
api_messages = [{"role": msg.role, "content": msg.content} for msg in messages]
|
||||
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": api_messages,
|
||||
"stream": True
|
||||
}
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(
|
||||
f"{self._base_url}/api/chat",
|
||||
json=payload
|
||||
) as response:
|
||||
async for line in response.content:
|
||||
if line:
|
||||
data = json.loads(line)
|
||||
if "message" in data:
|
||||
yield data["message"].get("content", "")
|
||||
|
||||
def get_available_models(self) -> List[str]:
|
||||
return ["llama3", "llama3.2", "mistral", "codellama", "deepseek-coder"]
|
||||
|
||||
|
||||
class ModelRouter:
|
||||
"""
|
||||
智能模型路由器
|
||||
|
||||
根据任务类型和需求自动选择最合适的模型
|
||||
"""
|
||||
|
||||
# 默认路由规则
|
||||
ROUTING_RULES = {
|
||||
TaskType.COMPLEX_REASONING: ("anthropic", "claude-opus-4.6"),
|
||||
TaskType.CODE_GENERATION: ("anthropic", "claude-sonnet-4.6"),
|
||||
TaskType.CODE_REVIEW: ("anthropic", "claude-sonnet-4.6"),
|
||||
TaskType.ARCHITECTURE_DESIGN: ("anthropic", "claude-opus-4.6"),
|
||||
TaskType.TEST_GENERATION: ("anthropic", "claude-sonnet-4.6"),
|
||||
TaskType.SIMPLE_TASK: ("anthropic", "claude-haiku-4.6"),
|
||||
TaskType.COST_SENSITIVE: ("deepseek", "deepseek-chat"),
|
||||
TaskType.LOCAL_PRIVACY: ("ollama", "llama3"),
|
||||
}
|
||||
|
||||
def __init__(self, config: LLMConfig = None):
|
||||
self.config = config or LLMConfig.from_env()
|
||||
self.providers: Dict[str, LLMProvider] = {}
|
||||
self._initialize_providers()
|
||||
|
||||
def _initialize_providers(self):
|
||||
"""初始化可用的提供商"""
|
||||
if self.config.anthropic_api_key:
|
||||
self.providers["anthropic"] = AnthropicProvider(self.config)
|
||||
if self.config.openai_api_key:
|
||||
self.providers["openai"] = OpenAIProvider(self.config)
|
||||
if self.config.deepseek_api_key:
|
||||
self.providers["deepseek"] = DeepSeekProvider(self.config)
|
||||
# Ollama 总是可用(本地服务)
|
||||
self.providers["ollama"] = OllamaProvider(self.config)
|
||||
|
||||
def classify_task(self, task_description: str) -> TaskType:
|
||||
"""
|
||||
分析任务描述,分类任务类型
|
||||
|
||||
使用关键词匹配和启发式规则
|
||||
"""
|
||||
task_lower = task_description.lower()
|
||||
|
||||
# 检查关键词
|
||||
keywords_map = {
|
||||
TaskType.ARCHITECTURE_DESIGN: ["架构", "设计", "系统设计", "技术选型", "架构图"],
|
||||
TaskType.CODE_GENERATION: ["实现", "编写", "生成代码", "开发", "创建函数"],
|
||||
TaskType.CODE_REVIEW: ["审查", "review", "检查", "分析代码"],
|
||||
TaskType.TEST_GENERATION: ["测试", "单元测试", "测试用例"],
|
||||
TaskType.COMPLEX_REASONING: ["分析", "推理", "判断", "复杂", "评估"],
|
||||
}
|
||||
|
||||
# 计算匹配分数
|
||||
scores = {}
|
||||
for task_type, keywords in keywords_map.items():
|
||||
score = sum(1 for kw in keywords if kw in task_lower)
|
||||
if score > 0:
|
||||
scores[task_type] = score
|
||||
|
||||
# 返回最高分的类型
|
||||
if scores:
|
||||
return max(scores, key=scores.get)
|
||||
|
||||
# 默认返回简单任务
|
||||
return TaskType.SIMPLE_TASK
|
||||
|
||||
def get_route(self, task_type: TaskType, preferred_provider: str = None) -> tuple:
|
||||
"""
|
||||
获取路由决策
|
||||
|
||||
返回: (provider_name, model_name)
|
||||
"""
|
||||
# 如果指定了提供商,尝试使用
|
||||
if preferred_provider and preferred_provider in self.providers:
|
||||
provider = self.providers[preferred_provider]
|
||||
models = provider.get_available_models()
|
||||
if models:
|
||||
return preferred_provider, models[0]
|
||||
|
||||
# 使用路由规则
|
||||
if task_type in self.ROUTING_RULES:
|
||||
provider_name, model_name = self.ROUTING_RULES[task_type]
|
||||
if provider_name in self.providers:
|
||||
return provider_name, model_name
|
||||
|
||||
# 回退到第一个可用的提供商
|
||||
for provider_name, provider in self.providers.items():
|
||||
models = provider.get_available_models()
|
||||
if models:
|
||||
return provider_name, models[0]
|
||||
|
||||
raise RuntimeError("没有可用的 LLM 提供商")
|
||||
|
||||
async def route_task(
|
||||
self,
|
||||
task: str,
|
||||
messages: List[LLMMessage] = None,
|
||||
preferred_model: str = None,
|
||||
preferred_provider: str = None,
|
||||
**kwargs
|
||||
) -> LLMResponse:
|
||||
"""
|
||||
智能路由任务到合适的模型
|
||||
|
||||
参数:
|
||||
task: 任务描述
|
||||
messages: 消息列表(如果为 None,会自动从 task 创建)
|
||||
preferred_model: 首选模型
|
||||
preferred_provider: 首选提供商
|
||||
"""
|
||||
# 如果指定了首选模型,尝试直接使用
|
||||
if preferred_model:
|
||||
if "-" in preferred_model:
|
||||
# 从模型名推断提供商
|
||||
if preferred_model.startswith("claude"):
|
||||
provider_name = "anthropic"
|
||||
elif preferred_model.startswith("gpt"):
|
||||
provider_name = "openai"
|
||||
elif preferred_model.startswith("deepseek"):
|
||||
provider_name = "deepseek"
|
||||
else:
|
||||
provider_name = preferred_provider or "anthropic"
|
||||
|
||||
if provider_name in self.providers:
|
||||
provider = self.providers[provider_name]
|
||||
return await provider.chat_completion(
|
||||
preferred_model,
|
||||
messages or [LLMMessage(role="user", content=task)],
|
||||
**kwargs
|
||||
)
|
||||
|
||||
# 分类任务类型
|
||||
task_type = self.classify_task(task)
|
||||
provider_name, model_name = self.get_route(task_type, preferred_provider)
|
||||
|
||||
logger.info(f"路由任务: {task_type.value} -> {provider_name}/{model_name}")
|
||||
|
||||
provider = self.providers[provider_name]
|
||||
return await provider.chat_completion(
|
||||
model_name,
|
||||
messages or [LLMMessage(role="user", content=task)],
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def get_available_providers(self) -> List[str]:
|
||||
"""获取所有可用的提供商"""
|
||||
return list(self.providers.keys())
|
||||
|
||||
def get_provider_models(self, provider_name: str) -> List[str]:
|
||||
"""获取指定提供商的可用模型"""
|
||||
if provider_name in self.providers:
|
||||
return self.providers[provider_name].get_available_models()
|
||||
return []
|
||||
|
||||
|
||||
# 单例获取函数
|
||||
_llm_service: Optional[ModelRouter] = None
|
||||
|
||||
|
||||
def get_llm_service(config: LLMConfig = None) -> ModelRouter:
|
||||
"""获取 LLM 服务单例"""
|
||||
global _llm_service
|
||||
if _llm_service is None:
|
||||
_llm_service = ModelRouter(config or LLMConfig.from_env())
|
||||
return _llm_service
|
||||
|
||||
|
||||
def reset_llm_service():
|
||||
"""重置 LLM 服务(主要用于测试)"""
|
||||
global _llm_service
|
||||
_llm_service = None
|
||||
404
backend/app/services/meeting_recorder.py
Normal file
404
backend/app/services/meeting_recorder.py
Normal file
@@ -0,0 +1,404 @@
|
||||
"""
|
||||
会议记录服务 - 记录会议内容、讨论和共识
|
||||
将会议记录保存为 Markdown 文件,按日期组织
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
from dataclasses import dataclass, field, asdict
|
||||
|
||||
from .storage import get_storage
|
||||
|
||||
|
||||
@dataclass
|
||||
class DiscussionEntry:
|
||||
"""单条讨论记录"""
|
||||
agent_id: str
|
||||
agent_name: str
|
||||
content: str
|
||||
timestamp: str = ""
|
||||
step: str = ""
|
||||
|
||||
def __post_init__(self):
|
||||
if not self.timestamp:
|
||||
self.timestamp = datetime.now().isoformat()
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProgressStep:
|
||||
"""会议进度步骤"""
|
||||
step_id: str
|
||||
label: str
|
||||
status: str = "pending" # pending, active, completed
|
||||
completed_at: str = ""
|
||||
|
||||
def mark_active(self):
|
||||
self.status = "active"
|
||||
|
||||
def mark_completed(self):
|
||||
self.status = "completed"
|
||||
self.completed_at = datetime.now().isoformat()
|
||||
|
||||
|
||||
@dataclass
|
||||
class MeetingInfo:
|
||||
"""会议信息"""
|
||||
meeting_id: str
|
||||
title: str
|
||||
date: str # YYYY-MM-DD
|
||||
attendees: List[str] = field(default_factory=list)
|
||||
steps: List[ProgressStep] = field(default_factory=list)
|
||||
discussions: List[DiscussionEntry] = field(default_factory=list)
|
||||
status: str = "in_progress" # in_progress, completed
|
||||
created_at: str = ""
|
||||
ended_at: str = ""
|
||||
consensus: str = "" # 最终共识
|
||||
|
||||
def __post_init__(self):
|
||||
if not self.created_at:
|
||||
self.created_at = datetime.now().isoformat()
|
||||
if not self.date:
|
||||
self.date = datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
@property
|
||||
def current_step(self) -> Optional[ProgressStep]:
|
||||
"""获取当前活跃的步骤"""
|
||||
for step in self.steps:
|
||||
if step.status == "active":
|
||||
return step
|
||||
return None
|
||||
|
||||
@property
|
||||
def completed_steps(self) -> List[ProgressStep]:
|
||||
"""获取已完成的步骤"""
|
||||
return [s for s in self.steps if s.status == "completed"]
|
||||
|
||||
@property
|
||||
def progress_summary(self) -> str:
|
||||
"""进度摘要"""
|
||||
total = len(self.steps)
|
||||
if total == 0:
|
||||
return "0/0"
|
||||
completed = len(self.completed_steps)
|
||||
active = 1 if self.current_step else 0
|
||||
return f"{completed}/{total}" + (" (active)" if active else "")
|
||||
|
||||
|
||||
class MeetingRecorder:
|
||||
"""
|
||||
会议记录服务
|
||||
|
||||
记录会议的讨论内容、进度和共识,保存为 Markdown 文件
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._storage = get_storage()
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
def _parse_meeting_data(self, data: dict) -> MeetingInfo:
|
||||
"""将字典数据转换为 MeetingInfo 对象"""
|
||||
# 转换 steps
|
||||
steps = [
|
||||
ProgressStep(**s) if isinstance(s, dict) else s
|
||||
for s in data.get("steps", [])
|
||||
]
|
||||
# 转换 discussions
|
||||
discussions = [
|
||||
DiscussionEntry(**d) if isinstance(d, dict) else d
|
||||
for d in data.get("discussions", [])
|
||||
]
|
||||
# 创建 MeetingInfo
|
||||
data["steps"] = steps
|
||||
data["discussions"] = discussions
|
||||
return MeetingInfo(**data)
|
||||
|
||||
def _get_meeting_dir(self, date: str) -> str:
|
||||
"""获取会议目录路径"""
|
||||
return f"meetings/{date}"
|
||||
|
||||
def _get_meeting_file(self, meeting_id: str, date: str) -> str:
|
||||
"""获取会议文件路径"""
|
||||
return f"{self._get_meeting_dir(date)}/{meeting_id}.md"
|
||||
|
||||
async def create_meeting(
|
||||
self,
|
||||
meeting_id: str,
|
||||
title: str,
|
||||
attendees: List[str],
|
||||
steps: List[str] = None
|
||||
) -> MeetingInfo:
|
||||
"""
|
||||
创建新会议记录
|
||||
|
||||
Args:
|
||||
meeting_id: 会议 ID
|
||||
title: 会议标题
|
||||
attendees: 参会者列表
|
||||
steps: 会议步骤列表
|
||||
|
||||
Returns:
|
||||
创建的会议信息
|
||||
"""
|
||||
async with self._lock:
|
||||
date = datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
# 创建进度步骤
|
||||
progress_steps = []
|
||||
if steps:
|
||||
for i, step_label in enumerate(steps):
|
||||
progress_steps.append(ProgressStep(
|
||||
step_id=f"step_{i+1}",
|
||||
label=step_label,
|
||||
status="pending"
|
||||
))
|
||||
|
||||
meeting = MeetingInfo(
|
||||
meeting_id=meeting_id,
|
||||
title=title,
|
||||
date=date,
|
||||
attendees=attendees,
|
||||
steps=progress_steps
|
||||
)
|
||||
|
||||
# 保存为 Markdown
|
||||
await self._save_meeting_markdown(meeting)
|
||||
|
||||
return meeting
|
||||
|
||||
async def _save_meeting_markdown(self, meeting: MeetingInfo) -> None:
|
||||
"""将会议保存为 Markdown 文件"""
|
||||
lines = [
|
||||
f"# {meeting.title}",
|
||||
"",
|
||||
f"**会议 ID**: {meeting.meeting_id}",
|
||||
f"**日期**: {meeting.date}",
|
||||
f"**状态**: {meeting.status}",
|
||||
f"**参会者**: {', '.join(meeting.attendees)}",
|
||||
"",
|
||||
"## 会议进度",
|
||||
"",
|
||||
]
|
||||
|
||||
# 进度步骤
|
||||
for step in meeting.steps:
|
||||
status_icon = {
|
||||
"pending": "○",
|
||||
"active": "◐",
|
||||
"completed": "●"
|
||||
}.get(step.status, "○")
|
||||
time_str = f" ({step.completed_at})" if step.completed_at else ""
|
||||
lines.append(f"- {status_icon} **{step.label}**{time_str}")
|
||||
|
||||
lines.append("")
|
||||
lines.append("## 讨论记录")
|
||||
lines.append("")
|
||||
|
||||
# 讨论内容
|
||||
for discussion in meeting.discussions:
|
||||
lines.append(f"### {discussion.agent_name} - {discussion.timestamp[:19]}")
|
||||
if discussion.step:
|
||||
lines.append(f"*步骤: {discussion.step}*")
|
||||
lines.append("")
|
||||
lines.append(discussion.content)
|
||||
lines.append("")
|
||||
|
||||
# 共识
|
||||
if meeting.consensus:
|
||||
lines.append("## 共识")
|
||||
lines.append("")
|
||||
lines.append(meeting.consensus)
|
||||
lines.append("")
|
||||
|
||||
# 元数据
|
||||
lines.append("---")
|
||||
lines.append("")
|
||||
lines.append(f"**创建时间**: {meeting.created_at}")
|
||||
if meeting.ended_at:
|
||||
lines.append(f"**结束时间**: {meeting.ended_at}")
|
||||
|
||||
content = "\n".join(lines)
|
||||
file_path = self._get_meeting_file(meeting.meeting_id, meeting.date)
|
||||
|
||||
# 使用存储服务保存
|
||||
await self._storage.ensure_dir(self._get_meeting_dir(meeting.date))
|
||||
await self._storage.write_json(file_path.replace(".md", ".json"), asdict(meeting))
|
||||
|
||||
# 同时保存 Markdown
|
||||
import aiofiles
|
||||
full_path = Path(self._storage.base_path) / file_path
|
||||
async with aiofiles.open(full_path, mode="w", encoding="utf-8") as f:
|
||||
await f.write(content)
|
||||
|
||||
async def add_discussion(
|
||||
self,
|
||||
meeting_id: str,
|
||||
agent_id: str,
|
||||
agent_name: str,
|
||||
content: str,
|
||||
step: str = ""
|
||||
) -> None:
|
||||
"""
|
||||
添加讨论记录
|
||||
|
||||
Args:
|
||||
meeting_id: 会议 ID
|
||||
agent_id: Agent ID
|
||||
agent_name: Agent 名称
|
||||
content: 讨论内容
|
||||
step: 当前步骤
|
||||
"""
|
||||
async with self._lock:
|
||||
# 加载会议信息
|
||||
date = datetime.now().strftime("%Y-%m-%d")
|
||||
file_path = self._get_meeting_file(meeting_id, date)
|
||||
json_path = file_path.replace(".md", ".json")
|
||||
|
||||
data = await self._storage.read_json(json_path)
|
||||
if not data:
|
||||
return # 会议不存在
|
||||
|
||||
meeting = self._parse_meeting_data(data)
|
||||
meeting.discussions.append(DiscussionEntry(
|
||||
agent_id=agent_id,
|
||||
agent_name=agent_name,
|
||||
content=content,
|
||||
step=step
|
||||
))
|
||||
|
||||
# 保存
|
||||
await self._save_meeting_markdown(meeting)
|
||||
|
||||
async def update_progress(
|
||||
self,
|
||||
meeting_id: str,
|
||||
step_label: str
|
||||
) -> None:
|
||||
"""
|
||||
更新会议进度
|
||||
|
||||
Args:
|
||||
meeting_id: 会议 ID
|
||||
step_label: 步骤名称
|
||||
"""
|
||||
async with self._lock:
|
||||
date = datetime.now().strftime("%Y-%m-%d")
|
||||
json_path = self._get_meeting_file(meeting_id, date).replace(".md", ".json")
|
||||
|
||||
data = await self._storage.read_json(json_path)
|
||||
if not data:
|
||||
return
|
||||
|
||||
meeting = self._parse_meeting_data(data)
|
||||
|
||||
# 查找并更新步骤
|
||||
step_found = False
|
||||
for step in meeting.steps:
|
||||
if step.label == step_label:
|
||||
# 将之前的活跃步骤标记为完成
|
||||
if meeting.current_step:
|
||||
meeting.current_step.mark_completed()
|
||||
step.mark_active()
|
||||
step_found = True
|
||||
break
|
||||
|
||||
if not step_found and meeting.steps:
|
||||
# 如果找不到,将第一个 pending 步骤设为活跃
|
||||
for step in meeting.steps:
|
||||
if step.status == "pending":
|
||||
if meeting.current_step:
|
||||
meeting.current_step.mark_completed()
|
||||
step.mark_active()
|
||||
break
|
||||
|
||||
await self._save_meeting_markdown(meeting)
|
||||
|
||||
async def get_meeting(self, meeting_id: str, date: str = None) -> Optional[MeetingInfo]:
|
||||
"""
|
||||
获取会议信息
|
||||
|
||||
Args:
|
||||
meeting_id: 会议 ID
|
||||
date: 日期,默认为今天
|
||||
|
||||
Returns:
|
||||
会议信息
|
||||
"""
|
||||
if date is None:
|
||||
date = datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
json_path = self._get_meeting_file(meeting_id, date).replace(".md", ".json")
|
||||
data = await self._storage.read_json(json_path)
|
||||
if data:
|
||||
return self._parse_meeting_data(data)
|
||||
return None
|
||||
|
||||
async def list_meetings(self, date: str = None) -> List[MeetingInfo]:
|
||||
"""
|
||||
列出指定日期的会议
|
||||
|
||||
Args:
|
||||
date: 日期,默认为今天
|
||||
|
||||
Returns:
|
||||
会议列表
|
||||
"""
|
||||
if date is None:
|
||||
date = datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
meetings_dir = Path(self._storage.base_path) / self._get_meeting_dir(date)
|
||||
if not meetings_dir.exists():
|
||||
return []
|
||||
|
||||
meetings = []
|
||||
for json_file in meetings_dir.glob("*.json"):
|
||||
data = await self._storage.read_json(f"meetings/{date}/{json_file.name}")
|
||||
if data:
|
||||
meetings.append(self._parse_meeting_data(data))
|
||||
|
||||
return sorted(meetings, key=lambda m: m.created_at)
|
||||
|
||||
async def end_meeting(self, meeting_id: str, consensus: str = "") -> bool:
|
||||
"""
|
||||
结束会议
|
||||
|
||||
Args:
|
||||
meeting_id: 会议 ID
|
||||
consensus: 最终共识
|
||||
|
||||
Returns:
|
||||
是否成功
|
||||
"""
|
||||
async with self._lock:
|
||||
date = datetime.now().strftime("%Y-%m-%d")
|
||||
json_path = self._get_meeting_file(meeting_id, date).replace(".md", ".json")
|
||||
|
||||
data = await self._storage.read_json(json_path)
|
||||
if not data:
|
||||
return False
|
||||
|
||||
meeting = self._parse_meeting_data(data)
|
||||
meeting.status = "completed"
|
||||
meeting.ended_at = datetime.now().isoformat()
|
||||
if consensus:
|
||||
meeting.consensus = consensus
|
||||
|
||||
# 完成当前步骤
|
||||
if meeting.current_step:
|
||||
meeting.current_step.mark_completed()
|
||||
|
||||
await self._save_meeting_markdown(meeting)
|
||||
return True
|
||||
|
||||
|
||||
# 全局单例
|
||||
_recorder_instance: Optional[MeetingRecorder] = None
|
||||
|
||||
|
||||
def get_meeting_recorder() -> MeetingRecorder:
|
||||
"""获取会议记录服务单例"""
|
||||
global _recorder_instance
|
||||
if _recorder_instance is None:
|
||||
_recorder_instance = MeetingRecorder()
|
||||
return _recorder_instance
|
||||
172
backend/app/services/meeting_scheduler.py
Normal file
172
backend/app/services/meeting_scheduler.py
Normal file
@@ -0,0 +1,172 @@
|
||||
"""
|
||||
会议调度器 - 实现栅栏同步(Barrier Synchronization)
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional, Set
|
||||
from dataclasses import dataclass, asdict
|
||||
|
||||
from .storage import get_storage
|
||||
|
||||
|
||||
@dataclass
|
||||
class MeetingQueue:
|
||||
"""会议等待队列"""
|
||||
meeting_id: str
|
||||
title: str
|
||||
expected_attendees: List[str]
|
||||
arrived_attendees: List[str]
|
||||
status: str = "waiting"
|
||||
created_at: str = ""
|
||||
started_at: str = ""
|
||||
min_required: int = 2
|
||||
|
||||
def __post_init__(self):
|
||||
if not self.created_at:
|
||||
self.created_at = datetime.now().isoformat()
|
||||
|
||||
@property
|
||||
def is_ready(self) -> bool:
|
||||
expected = set(self.expected_attendees)
|
||||
arrived = set(self.arrived_attendees)
|
||||
return expected.issubset(arrived) and len(arrived) >= self.min_required
|
||||
|
||||
@property
|
||||
def missing_attendees(self) -> List[str]:
|
||||
return list(set(self.expected_attendees) - set(self.arrived_attendees))
|
||||
|
||||
@property
|
||||
def progress(self) -> str:
|
||||
return f"{len(self.arrived_attendees)}/{len(self.expected_attendees)}"
|
||||
|
||||
|
||||
class MeetingScheduler:
|
||||
"""会议调度器 - 栅栏同步实现"""
|
||||
|
||||
QUEUES_FILE = "cache/meeting_queue.json"
|
||||
|
||||
def __init__(self):
|
||||
self._storage = get_storage()
|
||||
self._lock = asyncio.Lock()
|
||||
self._events: Dict[str, asyncio.Event] = {}
|
||||
|
||||
async def _load_queues(self) -> Dict[str, Dict]:
|
||||
return await self._storage.read_json(self.QUEUES_FILE)
|
||||
|
||||
async def _save_queues(self, queues: Dict[str, Dict]) -> None:
|
||||
await self._storage.write_json(self.QUEUES_FILE, queues)
|
||||
|
||||
async def create_meeting(
|
||||
self,
|
||||
meeting_id: str,
|
||||
title: str,
|
||||
expected_attendees: List[str],
|
||||
min_required: int = None
|
||||
) -> MeetingQueue:
|
||||
async with self._lock:
|
||||
queue = MeetingQueue(
|
||||
meeting_id=meeting_id,
|
||||
title=title,
|
||||
expected_attendees=expected_attendees,
|
||||
arrived_attendees=[],
|
||||
min_required=min_required or len(expected_attendees)
|
||||
)
|
||||
queues = await self._load_queues()
|
||||
queues[meeting_id] = asdict(queue)
|
||||
await self._save_queues(queues)
|
||||
return queue
|
||||
|
||||
async def get_queue(self, meeting_id: str) -> Optional[MeetingQueue]:
|
||||
queues = await self._load_queues()
|
||||
return MeetingQueue(**queues[meeting_id]) if meeting_id in queues else None
|
||||
|
||||
async def wait_for_meeting(
|
||||
self,
|
||||
agent_id: str,
|
||||
meeting_id: str,
|
||||
timeout: int = 300
|
||||
) -> str:
|
||||
async with self._lock:
|
||||
queues = await self._load_queues()
|
||||
|
||||
if meeting_id not in queues:
|
||||
await self.create_meeting(
|
||||
meeting_id=meeting_id,
|
||||
title=f"Meeting: {meeting_id}",
|
||||
expected_attendees=[agent_id],
|
||||
min_required=1
|
||||
)
|
||||
return "started"
|
||||
|
||||
queue_data = queues[meeting_id]
|
||||
if agent_id not in queue_data.get("arrived_attendees", []):
|
||||
queue_data["arrived_attendees"].append(agent_id)
|
||||
queue_data["arrived_attendees"].sort()
|
||||
|
||||
await self._save_queues(queues)
|
||||
queue = MeetingQueue(**queue_data)
|
||||
is_ready = queue.is_ready
|
||||
|
||||
if is_ready:
|
||||
await self._start_meeting(meeting_id)
|
||||
return "started"
|
||||
|
||||
# 等待会议开始
|
||||
event = self._events.setdefault(meeting_id, asyncio.Event())
|
||||
try:
|
||||
await asyncio.wait_for(event.wait(), timeout=timeout)
|
||||
return "started"
|
||||
except asyncio.TimeoutError:
|
||||
return "timeout"
|
||||
|
||||
async def _start_meeting(self, meeting_id: str) -> None:
|
||||
async with self._lock:
|
||||
queues = await self._load_queues()
|
||||
if meeting_id in queues:
|
||||
queues[meeting_id]["status"] = "ready"
|
||||
queues[meeting_id]["started_at"] = datetime.now().isoformat()
|
||||
await self._save_queues(queues)
|
||||
|
||||
# 唤醒所有等待者
|
||||
event = self._events.get(meeting_id)
|
||||
if event:
|
||||
event.set()
|
||||
|
||||
async def end_meeting(self, meeting_id: str) -> bool:
|
||||
async with self._lock:
|
||||
queues = await self._load_queues()
|
||||
if meeting_id not in queues:
|
||||
return False
|
||||
|
||||
queues[meeting_id]["status"] = "ended"
|
||||
await self._save_queues(queues)
|
||||
self._events.pop(meeting_id, None)
|
||||
return True
|
||||
|
||||
async def get_all_queues(self) -> List[MeetingQueue]:
|
||||
queues = await self._load_queues()
|
||||
return [MeetingQueue(**data) for data in queues.values()]
|
||||
|
||||
async def add_attendee(self, meeting_id: str, agent_id: str) -> bool:
|
||||
async with self._lock:
|
||||
queues = await self._load_queues()
|
||||
if meeting_id not in queues:
|
||||
return False
|
||||
|
||||
if agent_id not in queues[meeting_id]["expected_attendees"]:
|
||||
queues[meeting_id]["expected_attendees"].append(agent_id)
|
||||
await self._save_queues(queues)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# 简化单例实现
|
||||
_scheduler_instance: Optional[MeetingScheduler] = None
|
||||
|
||||
|
||||
def get_meeting_scheduler() -> MeetingScheduler:
|
||||
global _scheduler_instance
|
||||
if _scheduler_instance is None:
|
||||
_scheduler_instance = MeetingScheduler()
|
||||
return _scheduler_instance
|
||||
436
backend/app/services/process_manager.py
Normal file
436
backend/app/services/process_manager.py
Normal file
@@ -0,0 +1,436 @@
|
||||
"""
|
||||
Agent 进程管理器
|
||||
|
||||
负责启动、停止和监控 Agent 进程/任务。
|
||||
支持两种类型的 Agent:
|
||||
1. NativeLLMAgent - 异步任务,无需外部进程
|
||||
2. ProcessWrapperAgent - 外部 CLI 工具,需要进程管理
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import signal
|
||||
import subprocess
|
||||
import psutil
|
||||
from typing import Dict, List, Optional, Any
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
from ..adapters.native_llm_agent import NativeLLMAgent, NativeLLMAgentFactory
|
||||
from .heartbeat import get_heartbeat_service
|
||||
from .agent_registry import get_agent_registry
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AgentStatus(Enum):
|
||||
"""Agent 状态"""
|
||||
STOPPED = "stopped"
|
||||
STARTING = "starting"
|
||||
RUNNING = "running"
|
||||
STOPPING = "stopping"
|
||||
CRASHED = "crashed"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentProcess:
|
||||
"""Agent 进程信息"""
|
||||
agent_id: str
|
||||
agent_type: str # native_llm, process_wrapper
|
||||
status: AgentStatus = AgentStatus.STOPPED
|
||||
process: Optional[subprocess.Popen] = None
|
||||
task: Optional[asyncio.Task] = None
|
||||
agent: Optional[NativeLLMAgent] = None
|
||||
started_at: Optional[datetime] = None
|
||||
stopped_at: Optional[datetime] = None
|
||||
restart_count: int = 0
|
||||
config: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@property
|
||||
def uptime(self) -> Optional[float]:
|
||||
"""运行时长(秒)"""
|
||||
if self.started_at:
|
||||
end = self.stopped_at or datetime.now()
|
||||
return (end - self.started_at).total_seconds()
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_alive(self) -> bool:
|
||||
"""是否存活"""
|
||||
if self.agent_type == "native_llm":
|
||||
return self.status == AgentStatus.RUNNING and self.task and not self.task.done()
|
||||
else:
|
||||
return self.process and self.process.poll() is None
|
||||
|
||||
|
||||
class ProcessManager:
|
||||
"""
|
||||
Agent 进程管理器
|
||||
|
||||
功能:
|
||||
1. 启动/停止 Agent
|
||||
2. 监控 Agent 健康状态
|
||||
3. 自动重启崩溃的 Agent
|
||||
4. 管理 Agent 生命周期
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.processes: Dict[str, AgentProcess] = {}
|
||||
self.heartbeat_service = get_heartbeat_service()
|
||||
self.registry = get_agent_registry()
|
||||
self._monitor_task: Optional[asyncio.Task] = None
|
||||
self._running = False
|
||||
|
||||
async def start_agent(
|
||||
self,
|
||||
agent_id: str,
|
||||
agent_type: str = "native_llm",
|
||||
config: Dict[str, Any] = None
|
||||
) -> bool:
|
||||
"""
|
||||
启动 Agent
|
||||
|
||||
参数:
|
||||
agent_id: Agent 唯一标识
|
||||
agent_type: Agent 类型 (native_llm, process_wrapper)
|
||||
config: Agent 配置
|
||||
|
||||
返回:
|
||||
是否成功启动
|
||||
"""
|
||||
if agent_id in self.processes and self.processes[agent_id].is_alive:
|
||||
logger.warning(f"Agent {agent_id} 已在运行")
|
||||
return False
|
||||
|
||||
logger.info(f"启动 Agent: {agent_id} (类型: {agent_type})")
|
||||
|
||||
process_info = AgentProcess(
|
||||
agent_id=agent_id,
|
||||
agent_type=agent_type,
|
||||
status=AgentStatus.STARTING,
|
||||
config=config or {}
|
||||
)
|
||||
|
||||
try:
|
||||
if agent_type == "native_llm":
|
||||
success = await self._start_native_agent(process_info)
|
||||
elif agent_type == "process_wrapper":
|
||||
success = await self._start_process_wrapper(process_info)
|
||||
else:
|
||||
logger.error(f"不支持的 Agent 类型: {agent_type}")
|
||||
return False
|
||||
|
||||
if success:
|
||||
process_info.status = AgentStatus.RUNNING
|
||||
process_info.started_at = datetime.now()
|
||||
self.processes[agent_id] = process_info
|
||||
|
||||
# 启动监控任务
|
||||
if not self._running:
|
||||
await self.start_monitor()
|
||||
|
||||
return True
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"启动 Agent 失败: {agent_id}: {e}", exc_info=True)
|
||||
process_info.status = AgentStatus.CRASHED
|
||||
return False
|
||||
|
||||
async def _start_native_agent(self, process_info: AgentProcess) -> bool:
|
||||
"""启动原生 LLM Agent"""
|
||||
try:
|
||||
# 创建 Agent 实例
|
||||
agent = await NativeLLMAgentFactory.create(
|
||||
agent_id=process_info.agent_id,
|
||||
name=process_info.config.get("name"),
|
||||
role=process_info.config.get("role", "developer"),
|
||||
model=process_info.config.get("model", "claude-sonnet-4.6"),
|
||||
config=process_info.config
|
||||
)
|
||||
|
||||
process_info.agent = agent
|
||||
|
||||
# 创建后台任务保持 Agent 运行
|
||||
async def agent_loop():
|
||||
try:
|
||||
# Agent 定期发送心跳
|
||||
while True:
|
||||
await asyncio.sleep(30)
|
||||
if await agent.health_check():
|
||||
await agent.update_heartbeat("idle")
|
||||
else:
|
||||
logger.warning(f"Agent {process_info.agent_id} 健康检查失败")
|
||||
break
|
||||
except asyncio.CancelledError:
|
||||
logger.info(f"Agent {process_info.agent_id} 任务被取消")
|
||||
except Exception as e:
|
||||
logger.error(f"Agent {process_info.agent_id} 循环出错: {e}")
|
||||
|
||||
task = asyncio.create_task(agent_loop())
|
||||
process_info.task = task
|
||||
|
||||
logger.info(f"原生 LLM Agent 启动成功: {process_info.agent_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"启动原生 Agent 失败: {e}")
|
||||
return False
|
||||
|
||||
async def _start_process_wrapper(self, process_info: AgentProcess) -> bool:
|
||||
"""启动进程包装 Agent"""
|
||||
command = process_info.config.get("command")
|
||||
args = process_info.config.get("args", [])
|
||||
|
||||
if not command:
|
||||
logger.error("进程包装 Agent 需要指定 command")
|
||||
return False
|
||||
|
||||
try:
|
||||
# 启动子进程
|
||||
proc = subprocess.Popen(
|
||||
[command] + args,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
|
||||
process_info.process = proc
|
||||
|
||||
# 创建监控任务
|
||||
async def process_monitor():
|
||||
try:
|
||||
while True:
|
||||
if proc.poll() is not None:
|
||||
logger.warning(f"进程 {process_info.agent_id} 已退出: {proc.returncode}")
|
||||
break
|
||||
await asyncio.sleep(5)
|
||||
except asyncio.CancelledError:
|
||||
# 尝试优雅关闭
|
||||
try:
|
||||
proc.terminate()
|
||||
proc.wait(timeout=5)
|
||||
except:
|
||||
proc.kill()
|
||||
|
||||
task = asyncio.create_task(process_monitor())
|
||||
process_info.task = task
|
||||
|
||||
logger.info(f"进程包装 Agent 启动成功: {process_info.agent_id} (PID: {proc.pid})")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"启动进程失败: {e}")
|
||||
return False
|
||||
|
||||
async def stop_agent(self, agent_id: str, graceful: bool = True) -> bool:
|
||||
"""
|
||||
停止 Agent
|
||||
|
||||
参数:
|
||||
agent_id: Agent ID
|
||||
graceful: 是否优雅关闭
|
||||
|
||||
返回:
|
||||
是否成功停止
|
||||
"""
|
||||
if agent_id not in self.processes:
|
||||
logger.warning(f"Agent {agent_id} 未运行")
|
||||
return False
|
||||
|
||||
process_info = self.processes[agent_id]
|
||||
logger.info(f"停止 Agent: {agent_id} (优雅: {graceful})")
|
||||
|
||||
process_info.status = AgentStatus.STOPPING
|
||||
|
||||
try:
|
||||
if process_info.agent:
|
||||
# 关闭原生 Agent
|
||||
await process_info.agent.shutdown()
|
||||
|
||||
if process_info.task:
|
||||
# 取消后台任务
|
||||
process_info.task.cancel()
|
||||
try:
|
||||
await process_info.task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
if process_info.process:
|
||||
# 终止外部进程
|
||||
if graceful:
|
||||
process_info.process.terminate()
|
||||
try:
|
||||
process_info.process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
process_info.process.kill()
|
||||
else:
|
||||
process_info.process.kill()
|
||||
|
||||
process_info.status = AgentStatus.STOPPED
|
||||
process_info.stopped_at = datetime.now()
|
||||
|
||||
# 从进程列表移除
|
||||
del self.processes[agent_id]
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"停止 Agent 失败: {agent_id}: {e}")
|
||||
process_info.status = AgentStatus.CRASHED
|
||||
return False
|
||||
|
||||
async def restart_agent(self, agent_id: str) -> bool:
|
||||
"""重启 Agent"""
|
||||
logger.info(f"重启 Agent: {agent_id}")
|
||||
|
||||
if agent_id in self.processes:
|
||||
process_info = self.processes[agent_id]
|
||||
config = process_info.config
|
||||
agent_type = process_info.agent_type
|
||||
|
||||
await self.stop_agent(agent_id)
|
||||
process_info.restart_count += 1
|
||||
|
||||
return await self.start_agent(agent_id, agent_type, config)
|
||||
|
||||
return False
|
||||
|
||||
def get_agent_status(self, agent_id: str) -> Optional[AgentStatus]:
|
||||
"""获取 Agent 状态"""
|
||||
if agent_id in self.processes:
|
||||
return self.processes[agent_id].status
|
||||
return AgentStatus.UNKNOWN
|
||||
|
||||
def get_all_agents(self) -> Dict[str, AgentProcess]:
|
||||
"""获取所有 Agent 信息"""
|
||||
return self.processes.copy()
|
||||
|
||||
def get_running_agents(self) -> List[str]:
|
||||
"""获取正在运行的 Agent ID 列表"""
|
||||
return [
|
||||
pid for pid, proc in self.processes.items()
|
||||
if proc.is_alive
|
||||
]
|
||||
|
||||
async def monitor_agent_health(self) -> Dict[str, bool]:
|
||||
"""
|
||||
监控所有 Agent 健康状态
|
||||
|
||||
返回:
|
||||
{agent_id: is_healthy}
|
||||
"""
|
||||
results = {}
|
||||
|
||||
for agent_id, process_info in self.processes.items():
|
||||
if process_info.agent_type == "native_llm" and process_info.agent:
|
||||
# 检查原生 Agent 健康状态
|
||||
results[agent_id] = await process_info.agent.health_check()
|
||||
else:
|
||||
# 检查进程是否存活
|
||||
results[agent_id] = process_info.is_alive
|
||||
|
||||
return results
|
||||
|
||||
async def start_monitor(self, interval: int = 30):
|
||||
"""启动监控任务"""
|
||||
if self._running:
|
||||
return
|
||||
|
||||
self._running = True
|
||||
|
||||
async def monitor_loop():
|
||||
while self._running:
|
||||
try:
|
||||
await self._check_agents()
|
||||
await asyncio.sleep(interval)
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"监控循环出错: {e}", exc_info=True)
|
||||
await asyncio.sleep(interval)
|
||||
|
||||
self._monitor_task = asyncio.create_task(monitor_loop())
|
||||
logger.info("监控任务已启动")
|
||||
|
||||
async def stop_monitor(self):
|
||||
"""停止监控任务"""
|
||||
self._running = False
|
||||
|
||||
if self._monitor_task:
|
||||
self._monitor_task.cancel()
|
||||
try:
|
||||
await self._monitor_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
logger.info("监控任务已停止")
|
||||
|
||||
async def _check_agents(self):
|
||||
"""检查所有 Agent 状态"""
|
||||
health_results = await self.monitor_agent_health()
|
||||
|
||||
for agent_id, is_healthy in health_results.items():
|
||||
if not is_healthy:
|
||||
logger.warning(f"Agent {agent_id} 不健康")
|
||||
|
||||
# 检查是否需要自动重启
|
||||
process_info = self.processes.get(agent_id)
|
||||
if process_info and process_info.config.get("auto_restart", False):
|
||||
if process_info.restart_count < process_info.config.get("max_restarts", 3):
|
||||
logger.info(f"自动重启 Agent: {agent_id}")
|
||||
await self.restart_agent(agent_id)
|
||||
else:
|
||||
logger.error(f"Agent {agent_id} 重启次数超限,标记为崩溃")
|
||||
process_info.status = AgentStatus.CRASHED
|
||||
|
||||
async def shutdown_all(self):
|
||||
"""关闭所有 Agent"""
|
||||
logger.info("关闭所有 Agent...")
|
||||
|
||||
agent_ids = list(self.processes.keys())
|
||||
for agent_id in agent_ids:
|
||||
await self.stop_agent(agent_id)
|
||||
|
||||
await self.stop_monitor()
|
||||
|
||||
logger.info("所有 Agent 已关闭")
|
||||
|
||||
def get_summary(self) -> Dict[str, Any]:
|
||||
"""获取进程管理器摘要"""
|
||||
running = self.get_running_agents()
|
||||
total = len(self.processes)
|
||||
|
||||
status_counts = {}
|
||||
for proc in self.processes.values():
|
||||
status = proc.status.value
|
||||
status_counts[status] = status_counts.get(status, 0) + 1
|
||||
|
||||
return {
|
||||
"total_agents": total,
|
||||
"running_agents": len(running),
|
||||
"running_agent_ids": running,
|
||||
"status_counts": status_counts,
|
||||
"monitor_running": self._running
|
||||
}
|
||||
|
||||
|
||||
# 单例获取函数
|
||||
_process_manager: Optional[ProcessManager] = None
|
||||
|
||||
|
||||
def get_process_manager() -> ProcessManager:
|
||||
"""获取进程管理器单例"""
|
||||
global _process_manager
|
||||
if _process_manager is None:
|
||||
_process_manager = ProcessManager()
|
||||
return _process_manager
|
||||
|
||||
|
||||
def reset_process_manager():
|
||||
"""重置进程管理器(主要用于测试)"""
|
||||
global _process_manager
|
||||
_process_manager = None
|
||||
232
backend/app/services/resource_manager.py
Normal file
232
backend/app/services/resource_manager.py
Normal file
@@ -0,0 +1,232 @@
|
||||
"""
|
||||
资源管理器 - 整合文件锁和心跳服务
|
||||
提供声明式的任务执行接口,自动管理资源获取和释放
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
from typing import List, Dict, Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .storage import get_storage
|
||||
from .file_lock import get_file_lock_service
|
||||
from .heartbeat import get_heartbeat_service
|
||||
from .agent_registry import get_agent_registry
|
||||
|
||||
|
||||
@dataclass
|
||||
class TaskResult:
|
||||
"""任务执行结果"""
|
||||
success: bool
|
||||
message: str
|
||||
files_locked: List[str] = None
|
||||
duration_seconds: float = 0.0
|
||||
|
||||
def __post_init__(self):
|
||||
if self.files_locked is None:
|
||||
self.files_locked = []
|
||||
|
||||
|
||||
class ResourceManager:
|
||||
"""
|
||||
资源管理器
|
||||
|
||||
整合文件锁和心跳服务,提供声明式的任务执行接口:
|
||||
- 自动解析任务中的文件路径
|
||||
- 自动获取文件锁
|
||||
- 自动更新心跳
|
||||
- 任务完成后自动释放资源
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._lock_service = get_file_lock_service()
|
||||
self._heartbeat_service = get_heartbeat_service()
|
||||
self._agent_registry = get_agent_registry()
|
||||
|
||||
# 文件路径正则模式
|
||||
FILE_PATTERNS = [
|
||||
r'[\w/]+\.(py|js|ts|tsx|jsx|java|go|rs|c|cpp|h|hpp|cs|swift|kt|rb|php|sh|bash|zsh|yaml|yml|json|xml|html|css|scss|md|txt|sql)',
|
||||
r'[\w/]+/(?:src|lib|app|components|services|utils|tests|test|spec|config|assets|static|views|controllers|models|routes)/[\w./]+',
|
||||
]
|
||||
|
||||
def _extract_files_from_task(self, task_description: str) -> List[str]:
|
||||
"""
|
||||
从任务描述中提取文件路径
|
||||
|
||||
Args:
|
||||
task_description: 任务描述
|
||||
|
||||
Returns:
|
||||
提取的文件路径列表
|
||||
"""
|
||||
files = []
|
||||
for pattern in self.FILE_PATTERNS:
|
||||
matches = re.findall(pattern, task_description)
|
||||
files.extend(matches)
|
||||
|
||||
# 去重并过滤
|
||||
seen = set()
|
||||
result = []
|
||||
for f in files:
|
||||
# 标准化路径
|
||||
normalized = f.strip().replace('\\', '/')
|
||||
if normalized and normalized not in seen and len(normalized) > 3:
|
||||
seen.add(normalized)
|
||||
result.append(normalized)
|
||||
|
||||
return result
|
||||
|
||||
async def execute_task(
|
||||
self,
|
||||
agent_id: str,
|
||||
task_description: str,
|
||||
timeout: int = 300
|
||||
) -> TaskResult:
|
||||
"""
|
||||
执行任务(声明式接口)
|
||||
|
||||
内部流程:
|
||||
1. 解析任务需要的文件
|
||||
2. 获取所有文件锁
|
||||
3. 更新心跳状态
|
||||
4. 执行任务(这里是模拟)
|
||||
5. finally: 释放所有锁
|
||||
|
||||
Args:
|
||||
agent_id: Agent ID
|
||||
task_description: 任务描述
|
||||
timeout: 超时时间(秒)
|
||||
|
||||
Returns:
|
||||
任务执行结果
|
||||
"""
|
||||
import time
|
||||
start_time = time.time()
|
||||
|
||||
# 1. 解析文件
|
||||
files = self._extract_files_from_task(task_description)
|
||||
|
||||
# 2. 获取文件锁
|
||||
acquired_files = []
|
||||
for file_path in files:
|
||||
success = await self._lock_service.acquire_lock(
|
||||
file_path, agent_id, agent_id[:3].upper()
|
||||
)
|
||||
if success:
|
||||
acquired_files.append(file_path)
|
||||
|
||||
try:
|
||||
# 3. 更新心跳
|
||||
await self._heartbeat_service.update_heartbeat(
|
||||
agent_id,
|
||||
status="working",
|
||||
current_task=task_description,
|
||||
progress=0
|
||||
)
|
||||
|
||||
# 4. 执行任务(这里只是模拟,实际需要调用 Agent)
|
||||
# 实际实现中,这里会通过 CLIPluginAdapter 调用 Agent
|
||||
await asyncio.sleep(0.1) # 模拟执行
|
||||
|
||||
duration = time.time() - start_time
|
||||
|
||||
return TaskResult(
|
||||
success=True,
|
||||
message=f"Task executed: {task_description}",
|
||||
files_locked=acquired_files,
|
||||
duration_seconds=duration
|
||||
)
|
||||
|
||||
finally:
|
||||
# 5. 释放所有锁
|
||||
for file_path in acquired_files:
|
||||
await self._lock_service.release_lock(file_path, agent_id)
|
||||
|
||||
# 更新心跳为 idle
|
||||
await self._heartbeat_service.update_heartbeat(
|
||||
agent_id,
|
||||
status="idle",
|
||||
current_task="",
|
||||
progress=100
|
||||
)
|
||||
|
||||
async def parse_task_files(self, task_description: str) -> List[str]:
|
||||
"""
|
||||
解析任务中的文件路径
|
||||
|
||||
Args:
|
||||
task_description: 任务描述
|
||||
|
||||
Returns:
|
||||
文件路径列表
|
||||
"""
|
||||
return self._extract_files_from_task(task_description)
|
||||
|
||||
async def get_agent_status(self, agent_id: str) -> Dict:
|
||||
"""
|
||||
获取 Agent 状态(整合锁和心跳信息)
|
||||
|
||||
Args:
|
||||
agent_id: Agent ID
|
||||
|
||||
Returns:
|
||||
Agent 状态信息
|
||||
"""
|
||||
# 获取心跳信息
|
||||
heartbeat = await self._heartbeat_service.get_heartbeat(agent_id)
|
||||
# 获取持有的锁
|
||||
locks = await self._lock_service.get_agent_locks(agent_id)
|
||||
# 获取注册信息
|
||||
agent_info = await self._agent_registry.get_agent(agent_id)
|
||||
# 获取运行时状态
|
||||
agent_state = await self._agent_registry.get_state(agent_id)
|
||||
|
||||
return {
|
||||
"agent_id": agent_id,
|
||||
"info": {
|
||||
"name": agent_info.name if agent_info else "",
|
||||
"role": agent_info.role if agent_info else "",
|
||||
"model": agent_info.model if agent_info else "",
|
||||
},
|
||||
"heartbeat": {
|
||||
"status": heartbeat.status if heartbeat else "unknown",
|
||||
"current_task": heartbeat.current_task if heartbeat else "",
|
||||
"progress": heartbeat.progress if heartbeat else 0,
|
||||
"elapsed": heartbeat.elapsed_display if heartbeat else "",
|
||||
},
|
||||
"locks": [
|
||||
{"file": lock.file_path, "elapsed": lock.elapsed_display}
|
||||
for lock in locks
|
||||
],
|
||||
"state": {
|
||||
"task": agent_state.current_task if agent_state else "",
|
||||
"progress": agent_state.progress if agent_state else 0,
|
||||
"working_files": agent_state.working_files if agent_state else [],
|
||||
}
|
||||
}
|
||||
|
||||
async def get_all_status(self) -> List[Dict]:
|
||||
"""
|
||||
获取所有 Agent 的状态
|
||||
|
||||
Returns:
|
||||
所有 Agent 状态列表
|
||||
"""
|
||||
agents = await self._agent_registry.list_agents()
|
||||
statuses = []
|
||||
for agent in agents:
|
||||
status = await self.get_agent_status(agent.agent_id)
|
||||
statuses.append(status)
|
||||
return statuses
|
||||
|
||||
|
||||
# 全局单例
|
||||
_manager_instance: Optional[ResourceManager] = None
|
||||
|
||||
|
||||
def get_resource_manager() -> ResourceManager:
|
||||
"""获取资源管理器单例"""
|
||||
global _manager_instance
|
||||
if _manager_instance is None:
|
||||
_manager_instance = ResourceManager()
|
||||
return _manager_instance
|
||||
199
backend/app/services/role_allocator.py
Normal file
199
backend/app/services/role_allocator.py
Normal file
@@ -0,0 +1,199 @@
|
||||
"""
|
||||
角色分配器 - AI 驱动的角色分配
|
||||
分析任务描述,自动为 Agent 分配最适合的角色
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from typing import Dict, List, Optional
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
from .agent_registry import AgentRegistry, AgentInfo
|
||||
|
||||
|
||||
class AgentRole(str, Enum):
|
||||
"""Agent 角色枚举"""
|
||||
ARCHITECT = "architect"
|
||||
PRODUCT_MANAGER = "pm"
|
||||
DEVELOPER = "developer"
|
||||
QA = "qa"
|
||||
REVIEWER = "reviewer"
|
||||
|
||||
|
||||
@dataclass
|
||||
class RoleWeight:
|
||||
"""角色权重配置"""
|
||||
role: str
|
||||
weight: float
|
||||
keywords: List[str]
|
||||
|
||||
def matches(self, text: str) -> int:
|
||||
"""计算匹配分数"""
|
||||
score = 0
|
||||
text_lower = text.lower()
|
||||
for keyword in self.keywords:
|
||||
if keyword.lower() in text_lower:
|
||||
score += 1
|
||||
return score
|
||||
|
||||
|
||||
class RoleAllocator:
|
||||
"""
|
||||
角色分配器
|
||||
|
||||
分析任务描述,为 Agent 分配最适合的角色
|
||||
"""
|
||||
|
||||
# 角色权重配置(来自 design-spec.md)
|
||||
ROLE_WEIGHTS = {
|
||||
"pm": RoleWeight("pm", 1.5, ["需求", "产品", "规划", "用户", "功能", "priority", "requirement", "product"]),
|
||||
"architect": RoleWeight("architect", 1.5, ["架构", "设计", "方案", "技术", "系统", "design", "architecture"]),
|
||||
"developer": RoleWeight("developer", 1.0, ["开发", "实现", "编码", "代码", "function", "implement", "code"]),
|
||||
"reviewer": RoleWeight("reviewer", 1.3, ["审查", "review", "检查", "验证", "校对", "check"]),
|
||||
"qa": RoleWeight("qa", 1.2, ["测试", "test", "质量", "bug", "验证", "quality"]),
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def _analyze_task_roles(self, task: str) -> Dict[str, float]:
|
||||
"""
|
||||
分析任务需要的角色及其权重
|
||||
|
||||
Args:
|
||||
task: 任务描述
|
||||
|
||||
Returns:
|
||||
角色权重字典
|
||||
"""
|
||||
scores = {}
|
||||
for role_name, role_weight in self.ROLE_WEIGHTS.items():
|
||||
match_score = role_weight.matches(task)
|
||||
if match_score > 0:
|
||||
scores[role_name] = match_score * role_weight.weight
|
||||
else:
|
||||
# 即使没有匹配关键词,也给予基础权重
|
||||
scores[role_name] = 0.1 * role_weight.weight
|
||||
|
||||
return scores
|
||||
|
||||
async def allocate_roles(
|
||||
self,
|
||||
task: str,
|
||||
available_agents: List[str]
|
||||
) -> Dict[str, str]:
|
||||
"""
|
||||
为任务分配角色
|
||||
|
||||
Args:
|
||||
task: 任务描述
|
||||
available_agents: 可用的 Agent ID 列表
|
||||
|
||||
Returns:
|
||||
Agent ID -> 角色映射
|
||||
"""
|
||||
# 获取所有 Agent 信息
|
||||
# 注意:在实际实现中,这会从 AgentRegistry 获取
|
||||
# 这里简化处理,假设已有 Agent 信息
|
||||
|
||||
# 分析任务需要的角色
|
||||
role_scores = self._analyze_task_roles(task)
|
||||
|
||||
# 按分数排序角色
|
||||
sorted_roles = sorted(role_scores.items(), key=lambda x: -x[1])
|
||||
|
||||
# 简单分配:将可用 Agent 按顺序分配给角色
|
||||
allocation = {}
|
||||
for i, agent_id in enumerate(available_agents):
|
||||
if i < len(sorted_roles):
|
||||
allocation[agent_id] = sorted_roles[i][0]
|
||||
else:
|
||||
allocation[agent_id] = "developer" # 默认角色
|
||||
|
||||
return allocation
|
||||
|
||||
def get_primary_role(self, task: str) -> str:
|
||||
"""
|
||||
获取任务的主要角色
|
||||
|
||||
Args:
|
||||
task: 任务描述
|
||||
|
||||
Returns:
|
||||
主要角色名称
|
||||
"""
|
||||
role_scores = self._analyze_task_roles(task)
|
||||
if not role_scores:
|
||||
return "developer"
|
||||
|
||||
return max(role_scores.items(), key=lambda x: x[1])[0]
|
||||
|
||||
async def suggest_agents_for_task(
|
||||
self,
|
||||
task: str,
|
||||
all_agents: List[AgentInfo],
|
||||
count: int = 3
|
||||
) -> List[AgentInfo]:
|
||||
"""
|
||||
为任务推荐合适的 Agent
|
||||
|
||||
Args:
|
||||
task: 任务描述
|
||||
all_agents: 所有可用 Agent 列表
|
||||
count: 推荐数量
|
||||
|
||||
Returns:
|
||||
推荐的 Agent 列表
|
||||
"""
|
||||
primary_role = self.get_primary_role(task)
|
||||
|
||||
# 按角色匹配度排序
|
||||
scored_agents = []
|
||||
for agent in all_agents:
|
||||
if agent.role == primary_role:
|
||||
scored_agents.append((agent, 10)) # 完全匹配高分
|
||||
elif agent.role in ["architect", "developer", "reviewer"]:
|
||||
scored_agents.append((agent, 5)) # 相关角色中分
|
||||
else:
|
||||
scored_agents.append((agent, 1)) # 其他角色低分
|
||||
|
||||
# 按分数排序
|
||||
scored_agents.sort(key=lambda x: -x[1])
|
||||
|
||||
return [agent for agent, _ in scored_agents[:count]]
|
||||
|
||||
def explain_allocation(self, task: str, allocation: Dict[str, str]) -> str:
|
||||
"""
|
||||
解释角色分配的原因
|
||||
|
||||
Args:
|
||||
task: 任务描述
|
||||
allocation: 分配结果
|
||||
|
||||
Returns:
|
||||
解释文本
|
||||
"""
|
||||
role_scores = self._analyze_task_roles(task)
|
||||
primary = self.get_primary_role(task)
|
||||
|
||||
lines = [f"任务分析: {task}", f"主要角色: {primary}"]
|
||||
lines.append("角色权重:")
|
||||
for role, score in sorted(role_scores.items(), key=lambda x: -x[1]):
|
||||
lines.append(f" - {role}: {score:.2f}")
|
||||
lines.append("分配结果:")
|
||||
for agent_id, role in allocation.items():
|
||||
lines.append(f" - {agent_id}: {role}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
# 全局单例
|
||||
_allocator_instance: Optional[RoleAllocator] = None
|
||||
|
||||
|
||||
def get_role_allocator() -> RoleAllocator:
|
||||
"""获取角色分配器单例"""
|
||||
global _allocator_instance
|
||||
if _allocator_instance is None:
|
||||
_allocator_instance = RoleAllocator()
|
||||
return _allocator_instance
|
||||
146
backend/app/services/storage.py
Normal file
146
backend/app/services/storage.py
Normal file
@@ -0,0 +1,146 @@
|
||||
"""
|
||||
基础存储服务 - 提供 JSON 文件的异步读写操作
|
||||
所有服务共享的底层存储抽象
|
||||
"""
|
||||
|
||||
import json
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
import aiofiles
|
||||
import aiofiles.os
|
||||
|
||||
|
||||
class StorageService:
|
||||
"""异步 JSON 文件存储服务"""
|
||||
|
||||
def __init__(self, base_path: str = ".doc"):
|
||||
"""
|
||||
初始化存储服务
|
||||
|
||||
Args:
|
||||
base_path: 基础存储路径,默认为 .doc
|
||||
"""
|
||||
self.base_path = Path(base_path)
|
||||
self._lock = asyncio.Lock() # 简单的内存锁,防止并发写入
|
||||
|
||||
async def ensure_dir(self, path: str) -> None:
|
||||
"""
|
||||
确保目录存在,不存在则创建
|
||||
|
||||
Args:
|
||||
path: 目录路径(相对于 base_path 或绝对路径)
|
||||
"""
|
||||
dir_path = self.base_path / path if not Path(path).is_absolute() else Path(path)
|
||||
await aiofiles.os.makedirs(dir_path, exist_ok=True)
|
||||
|
||||
async def read_json(self, path: str) -> Dict[str, Any]:
|
||||
"""
|
||||
读取 JSON 文件
|
||||
|
||||
Args:
|
||||
path: 文件路径(相对于 base_path 或绝对路径)
|
||||
|
||||
Returns:
|
||||
解析后的 JSON 字典,文件不存在或为空时返回空字典
|
||||
"""
|
||||
file_path = self.base_path / path if not Path(path).is_absolute() else Path(path)
|
||||
|
||||
if not await aiofiles.os.path.exists(file_path):
|
||||
return {}
|
||||
|
||||
async with aiofiles.open(file_path, mode="r", encoding="utf-8") as f:
|
||||
content = await f.read()
|
||||
if not content.strip():
|
||||
return {}
|
||||
return json.loads(content)
|
||||
|
||||
async def write_json(self, path: str, data: Dict[str, Any]) -> None:
|
||||
"""
|
||||
写入 JSON 文件
|
||||
|
||||
Args:
|
||||
path: 文件路径(相对于 base_path 或绝对路径)
|
||||
data: 要写入的 JSON 数据
|
||||
"""
|
||||
file_path = self.base_path / path if not Path(path).is_absolute() else Path(path)
|
||||
|
||||
# 确保父目录存在
|
||||
await self.ensure_dir(str(file_path.parent))
|
||||
|
||||
# 使用锁防止并发写入冲突
|
||||
async with self._lock:
|
||||
async with aiofiles.open(file_path, mode="w", encoding="utf-8") as f:
|
||||
await f.write(json.dumps(data, ensure_ascii=False, indent=2))
|
||||
|
||||
async def append_json_list(self, path: str, item: Any) -> None:
|
||||
"""
|
||||
向 JSON 数组文件追加一项
|
||||
|
||||
Args:
|
||||
path: 文件路径
|
||||
item: 要追加的项
|
||||
"""
|
||||
data = await self.read_json(path)
|
||||
if not isinstance(data, list):
|
||||
data = []
|
||||
data.append(item)
|
||||
await self.write_json(path, {"items": data})
|
||||
|
||||
async def delete(self, path: str) -> bool:
|
||||
"""
|
||||
删除文件
|
||||
|
||||
Args:
|
||||
path: 文件路径
|
||||
|
||||
Returns:
|
||||
是否成功删除
|
||||
"""
|
||||
file_path = self.base_path / path if not Path(path).is_absolute() else Path(path)
|
||||
|
||||
if await aiofiles.os.path.exists(file_path):
|
||||
await aiofiles.os.remove(file_path)
|
||||
return True
|
||||
return False
|
||||
|
||||
async def exists(self, path: str) -> bool:
|
||||
"""
|
||||
检查文件是否存在
|
||||
|
||||
Args:
|
||||
path: 文件路径
|
||||
|
||||
Returns:
|
||||
文件是否存在
|
||||
"""
|
||||
file_path = self.base_path / path if not Path(path).is_absolute() else Path(path)
|
||||
return await aiofiles.os.path.exists(file_path)
|
||||
|
||||
|
||||
# 全局单例实例
|
||||
_storage_instance: Optional[StorageService] = None
|
||||
|
||||
|
||||
def _find_project_root() -> Path:
|
||||
"""查找项目根目录(包含 CLAUDE.md 的目录)"""
|
||||
current = Path.cwd()
|
||||
# 向上查找项目根目录
|
||||
for parent in [current] + list(current.parents):
|
||||
if (parent / "CLAUDE.md").exists():
|
||||
return parent
|
||||
# 如果找不到,使用当前目录的父目录(假设从 backend/ 运行)
|
||||
if current.name == "backend":
|
||||
return current.parent
|
||||
# 默认使用当前目录
|
||||
return current
|
||||
|
||||
|
||||
def get_storage() -> StorageService:
|
||||
"""获取存储服务单例,使用项目根目录下的 .doc 目录"""
|
||||
global _storage_instance
|
||||
if _storage_instance is None:
|
||||
project_root = _find_project_root()
|
||||
doc_path = project_root / ".doc"
|
||||
_storage_instance = StorageService(str(doc_path))
|
||||
return _storage_instance
|
||||
473
backend/app/services/workflow_engine.py
Normal file
473
backend/app/services/workflow_engine.py
Normal file
@@ -0,0 +1,473 @@
|
||||
"""
|
||||
工作流引擎 - 管理和执行工作流
|
||||
支持从 YAML 文件加载工作流定义,并跟踪进度
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
from dataclasses import dataclass, field, asdict
|
||||
from datetime import datetime
|
||||
import yaml
|
||||
|
||||
from .storage import get_storage
|
||||
from .meeting_recorder import get_meeting_recorder
|
||||
|
||||
|
||||
@dataclass
|
||||
class WorkflowMeeting:
|
||||
"""工作流中的节点"""
|
||||
meeting_id: str
|
||||
title: str
|
||||
attendees: List[str]
|
||||
depends_on: List[str] = field(default_factory=list)
|
||||
completed: bool = False
|
||||
node_type: str = "meeting" # meeting | execution
|
||||
min_required: int = None # 最少完成人数(execution 节点用)
|
||||
on_failure: str = None # 失败时跳转的节点 ID
|
||||
|
||||
# 执行状态追踪(execution 节点专用)
|
||||
completed_attendees: List[str] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def is_ready(self) -> bool:
|
||||
"""检查节点是否可以开始(依赖已完成)"""
|
||||
return all(dep in self.depends_on for dep in self.depends_on)
|
||||
|
||||
@property
|
||||
def is_execution_ready(self) -> bool:
|
||||
"""检查执行节点是否所有人都完成了"""
|
||||
if self.node_type != "execution":
|
||||
return False
|
||||
required = self.min_required or len(self.attendees)
|
||||
return len(self.completed_attendees) >= required
|
||||
|
||||
@property
|
||||
def missing_attendees(self) -> List[str]:
|
||||
"""获取未完成的人员列表"""
|
||||
if self.node_type != "execution":
|
||||
return []
|
||||
return [a for a in self.attendees if a not in self.completed_attendees]
|
||||
|
||||
@property
|
||||
def progress(self) -> str:
|
||||
"""执行进度"""
|
||||
if self.node_type != "execution":
|
||||
return "N/A"
|
||||
return f"{len(self.completed_attendees)}/{len(self.attendees)}"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Workflow:
|
||||
"""工作流定义"""
|
||||
workflow_id: str
|
||||
name: str
|
||||
description: str
|
||||
meetings: List[WorkflowMeeting]
|
||||
created_at: str = ""
|
||||
status: str = "pending" # pending, in_progress, completed
|
||||
|
||||
def __post_init__(self):
|
||||
if not self.created_at:
|
||||
self.created_at = datetime.now().isoformat()
|
||||
|
||||
@property
|
||||
def progress(self) -> str:
|
||||
"""进度摘要"""
|
||||
total = len(self.meetings)
|
||||
completed = sum(1 for m in self.meetings if m.completed)
|
||||
return f"{completed}/{total}"
|
||||
|
||||
@property
|
||||
def current_meeting(self) -> Optional[WorkflowMeeting]:
|
||||
"""获取当前应该进行的会议(第一个未完成的)"""
|
||||
for meeting in self.meetings:
|
||||
if not meeting.completed:
|
||||
return meeting
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_completed(self) -> bool:
|
||||
"""工作流是否完成"""
|
||||
return all(m.completed for m in self.meetings)
|
||||
|
||||
|
||||
class WorkflowEngine:
|
||||
"""
|
||||
工作流引擎
|
||||
|
||||
管理工作流的加载、执行和进度跟踪
|
||||
"""
|
||||
|
||||
WORKFLOWS_DIR = "workflow"
|
||||
|
||||
def __init__(self):
|
||||
self._storage = get_storage()
|
||||
self._recorder = get_meeting_recorder()
|
||||
self._loaded_workflows: Dict[str, Workflow] = {}
|
||||
# 注册的工作流文件路径
|
||||
self._workflow_files: Dict[str, str] = {}
|
||||
|
||||
async def load_workflow(self, workflow_path: str) -> Workflow:
|
||||
"""
|
||||
从 YAML 文件加载工作流
|
||||
|
||||
Args:
|
||||
workflow_path: YAML 文件路径(相对于 .doc/workflow/)
|
||||
|
||||
Returns:
|
||||
加载的工作流
|
||||
"""
|
||||
import aiofiles
|
||||
|
||||
# 构建完整路径
|
||||
full_path = f"{self.WORKFLOWS_DIR}/{workflow_path}"
|
||||
yaml_path = Path(self._storage.base_path) / full_path
|
||||
|
||||
if not yaml_path.exists():
|
||||
raise ValueError(f"Workflow file not found: {workflow_path}")
|
||||
|
||||
# 读取 YAML 内容
|
||||
async with aiofiles.open(yaml_path, mode="r", encoding="utf-8") as f:
|
||||
yaml_content = await f.read()
|
||||
content = yaml.safe_load(yaml_content)
|
||||
|
||||
# 解析工作流
|
||||
meetings = []
|
||||
for m in content.get("meetings", []):
|
||||
meetings.append(WorkflowMeeting(
|
||||
meeting_id=m["meeting_id"],
|
||||
title=m["title"],
|
||||
attendees=m["attendees"],
|
||||
depends_on=m.get("depends_on", []),
|
||||
node_type=m.get("node_type", "meeting"),
|
||||
min_required=m.get("min_required"),
|
||||
on_failure=m.get("on_failure")
|
||||
))
|
||||
|
||||
workflow = Workflow(
|
||||
workflow_id=content["workflow_id"],
|
||||
name=content["name"],
|
||||
description=content.get("description", ""),
|
||||
meetings=meetings
|
||||
)
|
||||
|
||||
self._loaded_workflows[workflow.workflow_id] = workflow
|
||||
# 保存源文件路径,以便后续可以重新加载
|
||||
self._workflow_files[workflow.workflow_id] = workflow_path
|
||||
return workflow
|
||||
|
||||
async def get_next_meeting(self, workflow_id: str) -> Optional[WorkflowMeeting]:
|
||||
"""
|
||||
获取工作流中下一个应该进行的会议
|
||||
|
||||
Args:
|
||||
workflow_id: 工作流 ID
|
||||
|
||||
Returns:
|
||||
下一个会议,如果没有或已完成返回 None
|
||||
"""
|
||||
workflow = self._loaded_workflows.get(workflow_id)
|
||||
if not workflow:
|
||||
return None
|
||||
|
||||
return workflow.current_meeting
|
||||
|
||||
async def complete_meeting(self, workflow_id: str, meeting_id: str) -> bool:
|
||||
"""
|
||||
标记会议为已完成
|
||||
|
||||
Args:
|
||||
workflow_id: 工作流 ID
|
||||
meeting_id: 会议 ID
|
||||
|
||||
Returns:
|
||||
是否成功标记
|
||||
"""
|
||||
workflow = self._loaded_workflows.get(workflow_id)
|
||||
if not workflow:
|
||||
return False
|
||||
|
||||
for meeting in workflow.meetings:
|
||||
if meeting.meeting_id == meeting_id:
|
||||
meeting.completed = True
|
||||
|
||||
# 更新工作流状态
|
||||
if workflow.is_completed:
|
||||
workflow.status = "completed"
|
||||
else:
|
||||
workflow.status = "in_progress"
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
async def create_workflow_meeting(
|
||||
self,
|
||||
workflow_id: str,
|
||||
meeting_id: str
|
||||
) -> bool:
|
||||
"""
|
||||
创建工作流中的会议记录
|
||||
|
||||
Args:
|
||||
workflow_id: 工作流 ID
|
||||
meeting_id: 会议 ID
|
||||
|
||||
Returns:
|
||||
是否成功创建
|
||||
"""
|
||||
workflow = self._loaded_workflows.get(workflow_id)
|
||||
if not workflow:
|
||||
return False
|
||||
|
||||
meeting = None
|
||||
for m in workflow.meetings:
|
||||
if m.meeting_id == meeting_id:
|
||||
meeting = m
|
||||
break
|
||||
|
||||
if not meeting:
|
||||
return False
|
||||
|
||||
# 创建会议记录
|
||||
await self._recorder.create_meeting(
|
||||
meeting_id=meeting.meeting_id,
|
||||
title=f"{workflow.name}: {meeting.title}",
|
||||
attendees=meeting.attendees,
|
||||
steps=["收集初步想法", "讨论与迭代", "生成共识版本"]
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
async def get_workflow_status(self, workflow_id: str) -> Optional[Dict]:
|
||||
"""
|
||||
获取工作流状态
|
||||
|
||||
Args:
|
||||
workflow_id: 工作流 ID
|
||||
|
||||
Returns:
|
||||
工作流状态信息
|
||||
"""
|
||||
# 如果不在内存中,尝试重新加载
|
||||
if workflow_id not in self._loaded_workflows and workflow_id in self._workflow_files:
|
||||
await self.load_workflow(self._workflow_files[workflow_id])
|
||||
|
||||
workflow = self._loaded_workflows.get(workflow_id)
|
||||
if not workflow:
|
||||
return None
|
||||
|
||||
return {
|
||||
"workflow_id": workflow.workflow_id,
|
||||
"name": workflow.name,
|
||||
"description": workflow.description,
|
||||
"status": workflow.status,
|
||||
"progress": workflow.progress,
|
||||
"meetings": [
|
||||
{
|
||||
"meeting_id": m.meeting_id,
|
||||
"title": m.title,
|
||||
"completed": m.completed
|
||||
}
|
||||
for m in workflow.meetings
|
||||
]
|
||||
}
|
||||
|
||||
async def list_workflows(self) -> List[Dict]:
|
||||
"""
|
||||
列出所有加载的工作流
|
||||
|
||||
Returns:
|
||||
工作流列表
|
||||
"""
|
||||
return [
|
||||
{
|
||||
"workflow_id": w.workflow_id,
|
||||
"name": w.name,
|
||||
"status": w.status,
|
||||
"progress": w.progress
|
||||
}
|
||||
for w in self._loaded_workflows.values()
|
||||
]
|
||||
|
||||
# ========== 执行节点相关方法 ==========
|
||||
|
||||
async def join_execution_node(
|
||||
self,
|
||||
workflow_id: str,
|
||||
meeting_id: str,
|
||||
agent_id: str
|
||||
) -> Dict:
|
||||
"""
|
||||
Agent 加入执行节点(标记完成)
|
||||
|
||||
Args:
|
||||
workflow_id: 工作流 ID
|
||||
meeting_id: 执行节点 ID
|
||||
agent_id: Agent ID
|
||||
|
||||
Returns:
|
||||
状态信息 {"status": "waiting"|"ready"|"completed", "progress": "2/3"}
|
||||
"""
|
||||
workflow = self._loaded_workflows.get(workflow_id)
|
||||
if not workflow:
|
||||
return {"status": "error", "message": "Workflow not found"}
|
||||
|
||||
for meeting in workflow.meetings:
|
||||
if meeting.meeting_id == meeting_id:
|
||||
if meeting.node_type != "execution":
|
||||
return {"status": "error", "message": "Not an execution node"}
|
||||
|
||||
if agent_id not in meeting.completed_attendees:
|
||||
meeting.completed_attendees.append(agent_id)
|
||||
|
||||
if meeting.is_execution_ready:
|
||||
return {
|
||||
"status": "ready",
|
||||
"progress": meeting.progress,
|
||||
"message": "所有 Agent 已完成,可以进入下一节点"
|
||||
}
|
||||
return {
|
||||
"status": "waiting",
|
||||
"progress": meeting.progress,
|
||||
"missing": meeting.missing_attendees,
|
||||
"message": f"等待其他 Agent 完成: {meeting.missing_attendees}"
|
||||
}
|
||||
|
||||
return {"status": "error", "message": "Meeting not found"}
|
||||
|
||||
async def get_execution_status(
|
||||
self,
|
||||
workflow_id: str,
|
||||
meeting_id: str
|
||||
) -> Optional[Dict]:
|
||||
"""
|
||||
获取执行节点的状态
|
||||
|
||||
Returns:
|
||||
执行状态信息
|
||||
"""
|
||||
workflow = self._loaded_workflows.get(workflow_id)
|
||||
if not workflow:
|
||||
return None
|
||||
|
||||
for meeting in workflow.meetings:
|
||||
if meeting.meeting_id == meeting_id:
|
||||
return {
|
||||
"meeting_id": meeting.meeting_id,
|
||||
"title": meeting.title,
|
||||
"node_type": meeting.node_type,
|
||||
"attendees": meeting.attendees,
|
||||
"completed_attendees": meeting.completed_attendees,
|
||||
"progress": meeting.progress,
|
||||
"is_ready": meeting.is_execution_ready,
|
||||
"missing": meeting.missing_attendees
|
||||
}
|
||||
return None
|
||||
|
||||
# ========== 条件跳转相关方法 ==========
|
||||
|
||||
async def jump_to_node(
|
||||
self,
|
||||
workflow_id: str,
|
||||
target_meeting_id: str
|
||||
) -> bool:
|
||||
"""
|
||||
强制跳转到指定节点(重置后续所有节点)
|
||||
|
||||
Args:
|
||||
workflow_id: 工作流 ID
|
||||
target_meeting_id: 目标节点 ID
|
||||
|
||||
Returns:
|
||||
是否成功跳转
|
||||
"""
|
||||
workflow = self._loaded_workflows.get(workflow_id)
|
||||
if not workflow:
|
||||
return False
|
||||
|
||||
# 找到目标节点并重置从它开始的所有后续节点
|
||||
target_found = False
|
||||
for meeting in workflow.meetings:
|
||||
if meeting.meeting_id == target_meeting_id:
|
||||
target_found = True
|
||||
meeting.completed = False
|
||||
meeting.completed_attendees = []
|
||||
elif target_found:
|
||||
# 目标节点之后的所有节点都重置
|
||||
meeting.completed = False
|
||||
meeting.completed_attendees = []
|
||||
|
||||
workflow.status = "in_progress"
|
||||
return target_found
|
||||
|
||||
async def handle_failure(
|
||||
self,
|
||||
workflow_id: str,
|
||||
meeting_id: str
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
处理节点失败,根据 on_failure 配置跳转
|
||||
|
||||
Args:
|
||||
workflow_id: 工作流 ID
|
||||
meeting_id: 失败的节点 ID
|
||||
|
||||
Returns:
|
||||
跳转目标节点 ID,如果没有配置则返回 None
|
||||
"""
|
||||
workflow = self._loaded_workflows.get(workflow_id)
|
||||
if not workflow:
|
||||
return None
|
||||
|
||||
for meeting in workflow.meetings:
|
||||
if meeting.meeting_id == meeting_id and meeting.on_failure:
|
||||
await self.jump_to_node(workflow_id, meeting.on_failure)
|
||||
return meeting.on_failure
|
||||
return None
|
||||
|
||||
async def get_workflow_detail(self, workflow_id: str) -> Optional[Dict]:
|
||||
"""
|
||||
获取工作流详细信息(包含所有节点状态)
|
||||
|
||||
Returns:
|
||||
工作流详细信息
|
||||
"""
|
||||
workflow = self._loaded_workflows.get(workflow_id)
|
||||
if not workflow:
|
||||
return None
|
||||
|
||||
return {
|
||||
"workflow_id": workflow.workflow_id,
|
||||
"name": workflow.name,
|
||||
"description": workflow.description,
|
||||
"status": workflow.status,
|
||||
"progress": workflow.progress,
|
||||
"current_node": workflow.current_meeting.meeting_id if workflow.current_meeting else None,
|
||||
"meetings": [
|
||||
{
|
||||
"meeting_id": m.meeting_id,
|
||||
"title": m.title,
|
||||
"node_type": m.node_type,
|
||||
"attendees": m.attendees,
|
||||
"depends_on": m.depends_on,
|
||||
"completed": m.completed,
|
||||
"on_failure": m.on_failure,
|
||||
"progress": m.progress if m.node_type == "execution" else None
|
||||
}
|
||||
for m in workflow.meetings
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
# 全局单例
|
||||
_engine_instance: Optional[WorkflowEngine] = None
|
||||
|
||||
|
||||
def get_workflow_engine() -> WorkflowEngine:
|
||||
"""获取工作流引擎单例"""
|
||||
global _engine_instance
|
||||
if _engine_instance is None:
|
||||
_engine_instance = WorkflowEngine()
|
||||
return _engine_instance
|
||||
58
backend/app/utils/singleton.py
Normal file
58
backend/app/utils/singleton.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""
|
||||
单例模式工具模块 - 统一管理全局服务实例
|
||||
"""
|
||||
|
||||
from typing import TypeVar, Type, Dict, Callable, Optional
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
class SingletonRegistry:
|
||||
"""单例注册表 - 统一管理所有服务实例"""
|
||||
|
||||
_instances: Dict[str, object] = {}
|
||||
_factories: Dict[str, Callable[[], object]] = {}
|
||||
|
||||
@classmethod
|
||||
def register(cls, name: str, factory: Callable[[], T]) -> None:
|
||||
"""注册服务工厂函数"""
|
||||
cls._factories[name] = factory
|
||||
|
||||
@classmethod
|
||||
def get(cls, name: str, instance_type: Type[T]) -> T:
|
||||
"""获取或创建服务实例"""
|
||||
if name not in cls._instances:
|
||||
if name not in cls._factories:
|
||||
raise KeyError(f"未注册的服务: {name}")
|
||||
cls._instances[name] = cls._factories[name]()
|
||||
return cls._instances[name]
|
||||
|
||||
@classmethod
|
||||
def reset(cls, name: Optional[str] = None) -> None:
|
||||
"""重置服务实例(用于测试)"""
|
||||
if name:
|
||||
cls._instances.pop(name, None)
|
||||
else:
|
||||
cls._instances.clear()
|
||||
|
||||
|
||||
def singleton_factory(factory: Callable[[], T]) -> Callable[[], T]:
|
||||
"""
|
||||
单例工厂装饰器
|
||||
|
||||
用法:
|
||||
@singleton_factory
|
||||
def get_service():
|
||||
return Service()
|
||||
|
||||
service = get_service() # 始终返回同一实例
|
||||
"""
|
||||
instance: Optional[T] = None
|
||||
|
||||
def wrapper() -> T:
|
||||
nonlocal instance
|
||||
if instance is None:
|
||||
instance = factory()
|
||||
return instance
|
||||
|
||||
return wrapper
|
||||
1115
backend/cli.py
Normal file
1115
backend/cli.py
Normal file
File diff suppressed because it is too large
Load Diff
35
backend/requirements.txt
Normal file
35
backend/requirements.txt
Normal file
@@ -0,0 +1,35 @@
|
||||
# FastAPI 核心依赖
|
||||
fastapi>=0.110.0
|
||||
uvicorn[standard]>=0.27.0
|
||||
pydantic>=2.0.0
|
||||
|
||||
# CLI 工具
|
||||
typer>=0.9.0
|
||||
rich>=13.0.0
|
||||
|
||||
# 异步文件操作
|
||||
aiofiles>=23.0.0
|
||||
|
||||
# 文件锁
|
||||
filelock>=3.13.0
|
||||
|
||||
# YAML 解析
|
||||
pyyaml>=6.0
|
||||
|
||||
# HTTP 客户端(调用 LLM API)
|
||||
httpx>=0.25.0
|
||||
|
||||
# Anthropic API(Claude)
|
||||
anthropic>=0.18.0
|
||||
|
||||
# OpenAI API(可选)
|
||||
openai>=1.0.0
|
||||
|
||||
# WebSocket 支持
|
||||
websockets>=12.0
|
||||
|
||||
# 进程管理
|
||||
psutil>=5.9.0
|
||||
|
||||
# APscheduler(定时任务)
|
||||
apscheduler>=3.10.0
|
||||
385
backend/test_all_services.py
Normal file
385
backend/test_all_services.py
Normal file
@@ -0,0 +1,385 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
后端服务完整测试脚本
|
||||
测试所有 10 个步骤的服务是否正常工作
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from app.services.storage import get_storage
|
||||
from app.services.file_lock import get_file_lock_service
|
||||
from app.services.heartbeat import get_heartbeat_service
|
||||
from app.services.agent_registry import get_agent_registry
|
||||
from app.services.meeting_scheduler import get_meeting_scheduler
|
||||
from app.services.meeting_recorder import get_meeting_recorder
|
||||
from app.services.resource_manager import get_resource_manager
|
||||
from app.services.workflow_engine import get_workflow_engine
|
||||
from app.services.role_allocator import get_role_allocator
|
||||
|
||||
|
||||
async def test_storage_service():
|
||||
"""测试存储服务"""
|
||||
print("\n=== 测试 StorageService ===")
|
||||
storage = get_storage()
|
||||
|
||||
# 测试写入
|
||||
await storage.write_json("cache/test_storage.json", {"test": "data", "number": 42})
|
||||
print("[PASS] 写入 JSON 文件")
|
||||
|
||||
# 测试读取
|
||||
data = await storage.read_json("cache/test_storage.json")
|
||||
assert data["test"] == "data", "读取数据不匹配"
|
||||
print("[PASS] 读取 JSON 文件")
|
||||
|
||||
# 测试存在检查
|
||||
exists = await storage.exists("cache/test_storage.json")
|
||||
assert exists, "文件应该存在"
|
||||
print("[PASS] 文件存在检查")
|
||||
|
||||
# 测试删除
|
||||
await storage.delete("cache/test_storage.json")
|
||||
exists = await storage.exists("cache/test_storage.json")
|
||||
assert not exists, "文件应该已被删除"
|
||||
print("[PASS] 删除文件")
|
||||
|
||||
print("StorageService 测试通过")
|
||||
return True
|
||||
|
||||
|
||||
async def test_file_lock_service():
|
||||
"""测试文件锁服务"""
|
||||
print("\n=== 测试 FileLockService ===")
|
||||
service = get_file_lock_service()
|
||||
|
||||
# 测试获取锁
|
||||
success = await service.acquire_lock("src/test/file.py", "agent-001", "TestAgent")
|
||||
assert success, "应该成功获取锁"
|
||||
print("[PASS] 获取文件锁")
|
||||
|
||||
# 测试检查锁定
|
||||
locked_by = await service.check_locked("src/test/file.py")
|
||||
assert locked_by == "agent-001", "锁持有者应该匹配"
|
||||
print("[PASS] 检查文件锁定状态")
|
||||
|
||||
# 测试其他 Agent 无法获取
|
||||
success = await service.acquire_lock("src/test/file.py", "agent-002", "OtherAgent")
|
||||
assert not success, "其他 Agent 不应该能获取已被锁定的文件"
|
||||
print("[PASS] 冲突检测正常工作")
|
||||
|
||||
# 测试获取所有锁
|
||||
locks = await service.get_locks()
|
||||
assert len(locks) >= 1, "应该有至少一个锁"
|
||||
print("[PASS] 获取所有锁列表")
|
||||
|
||||
# 测试释放锁
|
||||
success = await service.release_lock("src/test/file.py", "agent-001")
|
||||
assert success, "应该成功释放锁"
|
||||
print("[PASS] 释放文件锁")
|
||||
|
||||
print("FileLockService 测试通过")
|
||||
return True
|
||||
|
||||
|
||||
async def test_heartbeat_service():
|
||||
"""测试心跳服务"""
|
||||
print("\n=== 测试 HeartbeatService ===")
|
||||
service = get_heartbeat_service()
|
||||
|
||||
# 测试更新心跳
|
||||
await service.update_heartbeat("agent-001", "working", "测试任务", 50)
|
||||
print("[PASS] 更新心跳")
|
||||
|
||||
# 测试获取心跳
|
||||
hb = await service.get_heartbeat("agent-001")
|
||||
assert hb is not None, "心跳信息应该存在"
|
||||
assert hb.status == "working", "状态应该匹配"
|
||||
assert hb.progress == 50, "进度应该匹配"
|
||||
print("[PASS] 获取心跳信息")
|
||||
|
||||
# 测试获取所有心跳
|
||||
all_hbs = await service.get_all_heartbeats()
|
||||
assert "agent-001" in all_hbs, "应该在所有心跳列表中"
|
||||
print("[PASS] 获取所有心跳")
|
||||
|
||||
# 测试活跃 Agent
|
||||
active = await service.get_active_agents(within_seconds=10)
|
||||
assert "agent-001" in active, "应该是活跃 Agent"
|
||||
print("[PASS] 获取活跃 Agent")
|
||||
|
||||
print("HeartbeatService 测试通过")
|
||||
return True
|
||||
|
||||
|
||||
async def test_agent_registry():
|
||||
"""测试 Agent 注册服务"""
|
||||
print("\n=== 测试 AgentRegistry ===")
|
||||
registry = get_agent_registry()
|
||||
|
||||
# 测试注册 Agent
|
||||
agent = await registry.register_agent(
|
||||
"test-agent-001",
|
||||
"Test Agent",
|
||||
"developer",
|
||||
"claude-opus-4.6",
|
||||
"测试用的 Agent"
|
||||
)
|
||||
assert agent.agent_id == "test-agent-001", "ID 应该匹配"
|
||||
print("[PASS] 注册 Agent")
|
||||
|
||||
# 测试获取 Agent
|
||||
fetched = await registry.get_agent("test-agent-001")
|
||||
assert fetched is not None, "应该能获取到 Agent"
|
||||
assert fetched.name == "Test Agent", "名称应该匹配"
|
||||
print("[PASS] 获取 Agent 信息")
|
||||
|
||||
# 测试更新状态
|
||||
await registry.update_state("test-agent-001", "修复 bug", 75)
|
||||
print("[PASS] 更新 Agent 状态")
|
||||
|
||||
# 测试获取状态
|
||||
state = await registry.get_state("test-agent-001")
|
||||
assert state is not None, "状态应该存在"
|
||||
assert state.progress == 75, "进度应该匹配"
|
||||
print("[PASS] 获取 Agent 状态")
|
||||
|
||||
# 测试列出所有 Agent
|
||||
agents = await registry.list_agents()
|
||||
assert len(agents) >= 1, "应该至少有一个 Agent"
|
||||
print("[PASS] 列出所有 Agent")
|
||||
|
||||
print("AgentRegistry 测试通过")
|
||||
return True
|
||||
|
||||
|
||||
async def test_meeting_scheduler():
|
||||
"""测试会议调度器"""
|
||||
print("\n=== 测试 MeetingScheduler ===")
|
||||
scheduler = get_meeting_scheduler()
|
||||
|
||||
# 测试创建会议
|
||||
queue = await scheduler.create_meeting(
|
||||
"test-meeting-001",
|
||||
"测试会议",
|
||||
["agent-001", "agent-002"],
|
||||
min_required=2
|
||||
)
|
||||
assert queue.meeting_id == "test-meeting-001", "ID 应该匹配"
|
||||
print("[PASS] 创建会议")
|
||||
|
||||
# 测试获取队列
|
||||
fetched = await scheduler.get_queue("test-meeting-001")
|
||||
assert fetched is not None, "队列应该存在"
|
||||
print("[PASS] 获取会议队列")
|
||||
|
||||
# 测试等待会议(模拟两个 Agent 到达)
|
||||
result1 = await scheduler.wait_for_meeting("agent-001", "test-meeting-001", timeout=1)
|
||||
print(f" Agent-1 到达: {result1}")
|
||||
|
||||
result2 = await scheduler.wait_for_meeting("agent-002", "test-meeting-001", timeout=1)
|
||||
print(f" Agent-2 到达: {result2}")
|
||||
|
||||
# 最后一个到达者应该触发会议开始
|
||||
assert result2 == "started", "最后一个到达者应该触发会议开始"
|
||||
print("[PASS] 栅栏同步工作正常")
|
||||
|
||||
# 测试结束会议
|
||||
success = await scheduler.end_meeting("test-meeting-001")
|
||||
assert success, "应该成功结束会议"
|
||||
print("[PASS] 结束会议")
|
||||
|
||||
print("MeetingScheduler 测试通过")
|
||||
return True
|
||||
|
||||
|
||||
async def test_meeting_recorder():
|
||||
"""测试会议记录服务"""
|
||||
print("\n=== 测试 MeetingRecorder ===")
|
||||
recorder = get_meeting_recorder()
|
||||
|
||||
# 测试创建会议记录
|
||||
meeting = await recorder.create_meeting(
|
||||
"test-record-001",
|
||||
"测试记录会议",
|
||||
["agent-001", "agent-002"],
|
||||
["步骤1", "步骤2", "步骤3"]
|
||||
)
|
||||
assert meeting.meeting_id == "test-record-001", "ID 应该匹配"
|
||||
assert len(meeting.steps) == 3, "应该有 3 个步骤"
|
||||
print("[PASS] 创建会议记录")
|
||||
|
||||
# 测试添加讨论
|
||||
await recorder.add_discussion("test-record-001", "agent-001", "Agent1", "这是第一条讨论")
|
||||
await recorder.add_discussion("test-record-001", "agent-002", "Agent2", "这是第二条讨论")
|
||||
print("[PASS] 添加讨论记录")
|
||||
|
||||
# 测试更新进度
|
||||
await recorder.update_progress("test-record-001", "步骤1")
|
||||
print("[PASS] 更新会议进度")
|
||||
|
||||
# 测试获取会议
|
||||
fetched = await recorder.get_meeting("test-record-001")
|
||||
assert fetched is not None, "会议应该存在"
|
||||
assert len(fetched.discussions) == 2, "应该有 2 条讨论"
|
||||
print("[PASS] 获取会议详情")
|
||||
|
||||
# 测试结束会议
|
||||
success = await recorder.end_meeting("test-record-001", "达成共识:继续开发")
|
||||
assert success, "应该成功结束会议"
|
||||
print("[PASS] 结束会议并保存共识")
|
||||
|
||||
print("MeetingRecorder 测试通过")
|
||||
return True
|
||||
|
||||
|
||||
async def test_resource_manager():
|
||||
"""测试资源管理器"""
|
||||
print("\n=== 测试 ResourceManager ===")
|
||||
manager = get_resource_manager()
|
||||
|
||||
# 测试解析任务文件
|
||||
files = await manager.parse_task_files("修复 src/auth/login.py 和 src/utils/helper.js 中的 bug")
|
||||
assert "src/auth/login.py" in files or "src/utils/helper.js" in files, "应该能解析出文件路径"
|
||||
print(f"[PASS] 解析任务文件: {files}")
|
||||
|
||||
# 测试获取 Agent 状态(需要先有注册的 Agent)
|
||||
try:
|
||||
status = await manager.get_agent_status("test-agent-001")
|
||||
print(f"[PASS] 获取 Agent 状态: {status['agent_id']}")
|
||||
except Exception as e:
|
||||
print(f" [WARN] 获取状态警告: {e}")
|
||||
|
||||
print("ResourceManager 测试通过")
|
||||
return True
|
||||
|
||||
|
||||
async def test_workflow_engine():
|
||||
"""测试工作流引擎"""
|
||||
print("\n=== 测试 WorkflowEngine ===")
|
||||
engine = get_workflow_engine()
|
||||
|
||||
# 确保测试工作流文件存在
|
||||
workflow_content = """
|
||||
workflow_id: "test-workflow"
|
||||
name: "测试工作流"
|
||||
description: "用于测试的工作流"
|
||||
meetings:
|
||||
- meeting_id: "step1"
|
||||
title: "第一步"
|
||||
attendees: ["agent-001"]
|
||||
depends_on: []
|
||||
- meeting_id: "step2"
|
||||
title: "第二步"
|
||||
attendees: ["agent-001", "agent-002"]
|
||||
depends_on: ["step1"]
|
||||
"""
|
||||
import aiofiles
|
||||
from pathlib import Path
|
||||
workflow_path = Path(engine._storage.base_path) / "workflow" / "test.yaml"
|
||||
async with aiofiles.open(workflow_path, mode="w", encoding="utf-8") as f:
|
||||
await f.write(workflow_content)
|
||||
|
||||
# 测试加载工作流
|
||||
workflow = await engine.load_workflow("test.yaml")
|
||||
assert workflow.workflow_id == "test-workflow", "ID 应该匹配"
|
||||
assert len(workflow.meetings) == 2, "应该有 2 个会议"
|
||||
print("[PASS] 加载工作流")
|
||||
|
||||
# 测试获取下一个会议
|
||||
next_meeting = await engine.get_next_meeting("test-workflow")
|
||||
assert next_meeting is not None, "应该有下一个会议"
|
||||
assert next_meeting.meeting_id == "step1", "第一个会议应该是 step1"
|
||||
print("[PASS] 获取下一个会议")
|
||||
|
||||
# 测试完成会议
|
||||
success = await engine.complete_meeting("test-workflow", "step1")
|
||||
assert success, "应该成功标记会议完成"
|
||||
print("[PASS] 标记会议完成")
|
||||
|
||||
# 测试获取工作流状态
|
||||
status = await engine.get_workflow_status("test-workflow")
|
||||
assert status is not None, "状态应该存在"
|
||||
assert status["progress"] == "1/2", "进度应该是 1/2"
|
||||
print("[PASS] 获取工作流状态")
|
||||
|
||||
print("WorkflowEngine 测试通过")
|
||||
return True
|
||||
|
||||
|
||||
async def test_role_allocator():
|
||||
"""测试角色分配器"""
|
||||
print("\n=== 测试 RoleAllocator ===")
|
||||
allocator = get_role_allocator()
|
||||
|
||||
# 测试获取主要角色
|
||||
primary = allocator.get_primary_role("实现登录功能并编写测试用例")
|
||||
assert primary in ["pm", "developer", "qa", "architect", "reviewer"], "应该是有效角色"
|
||||
print(f"[PASS] 获取主要角色: {primary}")
|
||||
|
||||
# 测试角色分配
|
||||
allocation = await allocator.allocate_roles(
|
||||
"设计数据库架构并实现 API",
|
||||
["claude-001", "kimi-002", "opencode-003"]
|
||||
)
|
||||
assert len(allocation) == 3, "应该为 3 个 Agent 分配角色"
|
||||
print(f"[PASS] 角色分配: {allocation}")
|
||||
|
||||
# 测试解释分配
|
||||
explanation = allocator.explain_allocation("修复 bug", allocation)
|
||||
assert "主要角色" in explanation, "解释应该包含主要角色"
|
||||
print("[PASS] 解释角色分配")
|
||||
|
||||
print("RoleAllocator 测试通过")
|
||||
return True
|
||||
|
||||
|
||||
async def run_all_tests():
|
||||
"""运行所有测试"""
|
||||
print("=" * 60)
|
||||
print("Swarm Command Center - 后端服务完整测试")
|
||||
print("=" * 60)
|
||||
|
||||
tests = [
|
||||
("StorageService", test_storage_service),
|
||||
("FileLockService", test_file_lock_service),
|
||||
("HeartbeatService", test_heartbeat_service),
|
||||
("AgentRegistry", test_agent_registry),
|
||||
("MeetingScheduler", test_meeting_scheduler),
|
||||
("MeetingRecorder", test_meeting_recorder),
|
||||
("ResourceManager", test_resource_manager),
|
||||
("WorkflowEngine", test_workflow_engine),
|
||||
("RoleAllocator", test_role_allocator),
|
||||
]
|
||||
|
||||
results = []
|
||||
for name, test_func in tests:
|
||||
try:
|
||||
success = await test_func()
|
||||
results.append((name, success, None))
|
||||
except Exception as e:
|
||||
print(f"[FAIL] {name} 测试失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
results.append((name, False, str(e)))
|
||||
|
||||
# 打印总结
|
||||
print("\n" + "=" * 60)
|
||||
print("测试结果总结")
|
||||
print("=" * 60)
|
||||
passed = sum(1 for _, success, _ in results if success)
|
||||
total = len(results)
|
||||
for name, success, error in results:
|
||||
status = "[PASS]" if success else f"[FAIL: {error}]"
|
||||
print(f"{name:20s} {status}")
|
||||
print("=" * 60)
|
||||
print(f"总计: {passed}/{total} 通过")
|
||||
print("=" * 60)
|
||||
|
||||
return passed == total
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = asyncio.run(run_all_tests())
|
||||
sys.exit(0 if success else 1)
|
||||
Reference in New Issue
Block a user