一、整体架构
CoPaw 是一个支持多渠道通信的个人 AI 助手系统,采用分层架构设计:
架构层次:
- 用户层 - Console Web UI、钉钉、飞书、QQ、Discord、iMessage 等多渠道
- 应用层 - FastAPI 应用,包含动态路由和多 Agent 管理
- 核心 Agent 层 - CoPawAgent(内置工具、技能、记忆管理、安全拦截)
- 支撑服务层 - 模型提供商、技能管理、安全模块、Token 追踪等
- 基础设施层 - AgentScope Runtime、FastAPI、Redis、文件系统
二、核心模块
1. agents/ - Agent 核心实现
- react_agent.py - CoPawAgent 主类,继承 ReActAgent + ToolGuardMixin
- skills_manager.py - 技能管理器,动态加载技能
- tools/ - 内置工具(shell、文件读写、浏览器控制等)
- skills/ - 内置技能(定时任务、文档处理、新闻摘要等)
- memory/ - 记忆管理(向量搜索 + 全文搜索)
2. app/ - FastAPI 应用
- _app.py - 应用入口,动态路由到不同的 Workspace
- workspace.py - 独立工作空间,每个 Workspace 是完整的 Agent 实例
- channels/ - 渠道管理(钉钉、飞书、Discord、QQ 等)
- crons/ - 定时任务管理
- routers/ - API 路由(agent、config、providers、skills)
3. providers/ - 模型提供商管理
- 支持多种提供商:ModelScope、OpenAI、Anthropic、Ollama 、自定义等
- provider_manager.py - 统一管理不同提供商
4. security/ - 安全模块
- tool_guard/ - 工具守卫规则引擎,拦截危险操作
- skill_scanner/ - 技能扫描器,安全检查技能代码
5. console/ - 前端应用
- React 18 + TypeScript + Vite
- 主要页面:聊天、Agent 配置、控制面板、设置
6. cli/-命令行
- main.py/ - 定义命令行指令功能,定义了chat、cron、skills、model等多个指令
- 指令均使用click包装
用户要求我用中文讲解这个文件的功能和逻辑。这是一个 FastAPI 应用的核心文件,让我分析一下它的主要功能和逻辑。
让我先阅读这个文件,然后给出详细的讲解。我来详细讲解这个文件的功能和逻辑。
三、后端服务管理
_app.py 是 CoPaw 项目的 FastAPI 应用核心文件,负责初始化和配置整个 Web 服务器,包括多 Agent 管理、路由配置、静态文件服务等。
1. DynamicMultiAgentRunner 类
这是核心的多 Agent 路由机制:
- 作用: 动态将请求路由到正确的 Agent 工作区运行器
- 原理: 通过检查请求中的 X-Agent-Id header 来确定使用哪个 Agent
- 关键方法:
- _get_workspace_runner() - 根据 agent_id 获取对应的工作区运行器
- stream_query() - 路由流式查询请求
- query_handler() - 路由查询处理请求
这个设计允许单个 FastAPI 应用同时服务多个 Agent 实例。
2. AgentApp 初始化
agent_app = AgentApp( app_name="Friday", app_description="A helpful assistant", runner=runner, )
创建 AgentScope 运行时应用实例,使用我们自定义的 DynamicMultiAgentRunner 作为运行器。
3. Lifespan 生命周期管理
应用启动和关闭时的关键操作:
启动阶段:
- 设置日志文件处理器
- 迁移和初始化: 检查旧配置并迁移到默认 Agent
- 多 Agent 管理器初始化: 创建 MultiAgentManager 并启动所有配置的 Agent
- Provider 管理: 初始化模型提供者管理器(单例)
- 连接组件: 将 DynamicMultiAgentRunner 连接到 MultiAgentManager
- 审批服务: 使用默认 Agent 的 channel_manager 设置审批服务
关闭阶段: - 停止所有 Agent 及其组件
- 清理资源
4. FastAPI 应用配置
app = FastAPI( lifespan=lifespan, docs_url="/docs" if DOCS_ENABLED else None, ... )
创建 FastAPI 应用实例,配置生命周期管理和文档路由。
5. 中间件配置
AgentContextMiddleware:
app.add_middleware(AgentContextMiddleware)
为 Agent 范围的路由添加上下文中间件,用于提取和管理 Agent ID。
CORS 中间件:
if CORS_ORIGINS: app.add_middleware(CORSMiddleware, ...)
根据配置添加跨域资源共享支持。
6. 路由配置
主要路由:
- 根路由: GET / - 服务 Web Console 的 index.html
- API 路由: /api/* - 通用 API 端点
- Agent 范围路由: /api/agents/{agentId}/* - 特定 Agent 的聊天、控制等
- Agent 应用路由: /api/agent/* - AgentScope 的查询接口
- 语音路由: /voice/* - Twilio 集成的语音端点(POST、WebSocket)
- 版本路由: /api/version - 返回 CoPaw 版本
核心设计思想
- 多 Agent 架构: 通过 DynamicMultiAgentRunner 实现单个应用服务多个 Agent
- 动态路由: 基于 HTTP header 动态选择 Agent,无需为每个 Agent 创建独立实例
- 生命周期管理: 统一管理所有 Agent 的启动、运行和关闭
- 渐进式迁移: 支持从旧版本配置平滑迁移到新的多 Agent 架构
- 灵活性: 通过环境变量和配置文件控制各种功能(CORS、文档、静态文件路径等)
数据流
客户端请求 → AgentContextMiddleware(提取 Agent ID) → DynamicMultiAgentRunner(路由到对应 Agent) → 具体的工作区运行器处理请求 → 返回响应
这个文件是整个 CoPaw 服务的入口点,协调了所有核心组件的初始化和交互。
记忆模块
模型使用的库是阿里开源的reme,在copaw中做了包装使用
python
class Workspace:
"""Single agent workspace with complete runtime components.
Each Workspace is an independent agent instance with its own:
- Runner: Processes agent requests
- ChannelManager: Manages communication channels
- MemoryManager: Manages conversation memory
- MCPClientManager: Manages MCP tool clients
- CronManager: Manages scheduled tasks
All components use existing single-agent code without modification.
"""
def __init__(self, agent_id: str, workspace_dir: str):
"""Initialize agent instance.
Args:
agent_id: Unique agent identifier
workspace_dir: Path to agent's workspace directory
"""
self.agent_id = agent_id
self.workspace_dir = Path(workspace_dir).expanduser()
self.workspace_dir.mkdir(parents=True, exist_ok=True)
# All components are None until start() is called (lazy loading)
self._runner: Optional[AgentRunner] = None
self._channel_manager: Optional["BaseChannel"] = None
# MemoryManager集成至ReMeLight
self._memory_manager: Optional[MemoryManager] = None
self._mcp_manager: Optional[MCPClientManager] = None
self._cron_manager: Optional["CronManager"] = None
self._chat_manager = None
self._config = None
self._config_watcher = None
self._mcp_config_watcher = None
self._started = False
ReMe 是一个专为 AI 智能体 打造的记忆管理框架,同时提供基于文件系统 和基于向量库的记忆系统。看copaw的代码使用了文件系统做存储,注册了文件读、写、编辑工具。
python
class MemoryManager(ReMeLight):
self.summary_toolkit = Toolkit()
self.summary_toolkit.register_tool_function(read_file)
self.summary_toolkit.register_tool_function(write_file)
self.summary_toolkit.register_tool_function(edit_file)
AgentRunner集成至agentscope_runtime的Runner,让 Agent 动起来、干活、执行思考循环的核心组件, 执行 Agent 的主循环(think → act → observe)。
python
class AgentRunner(Runner):
def __init__(
self,
agent_id: str = "default",
workspace_dir: Path | None = None,
) -> None:
super().__init__()
self.framework_type = "agentscope"
self.agent_id = agent_id # Store agent_id for config loading
self.workspace_dir = (
workspace_dir # Store workspace_dir for prompt building
)
self._chat_manager = None # Store chat_manager reference
self._mcp_manager = None # MCP client manager for hot-reload
self.memory_manager: MemoryManager | None = None
async def query_handler() # 处理输入提示词语llm请求
......
if not approval_consumed and query and _is_command(query):
logger.info("Command path: %s", query.strip()[:50])
# 判断是否为"/"指令
async for msg, last in run_command_path(request, msgs, self):
yield msg, last
return
......
# Get MCP clients from manager (hot-reloadable)
mcp_clients = []
if self._mcp_manager is not None:
mcp_clients = await self._mcp_manager.get_clients()
# 构建copawagent,继承ReActAgent功能,CoPaw Agent内根据运营商创建聊天实例,
# 并整合integrated tools, skills, and memory management.
# src/copaw/agents/react_agent.py 内copawagent类
agent = CoPawAgent(
env_context=env_context,
mcp_clients=mcp_clients,
memory_manager=self.memory_manager,
request_context={
"session_id": session_id,
"user_id": user_id,
"channel": channel,
"agent_id": self.agent_id,
},
max_iters=max_iters,
max_input_length=max_input_length,
memory_compact_threshold=(
running_config.memory_compact_threshold
),
memory_compact_reserve=running_config.memory_compact_reserve,
enable_tool_result_compact=(
running_config.enable_tool_result_compact
),
tool_result_compact_keep_n=(
running_config.tool_result_compact_keep_n
),
language=language,
workspace_dir=self.workspace_dir,
)
await agent.register_mcp_clients()
AgentRunner集成至agentscope_runtime的Runner,让 Agent 动起来、干活、执行思考循环的核心组件, 执行 Agent 的主循环(think → act → observe)。
python
class AgentRunner(Runner):
def __init__(
self,
agent_id: str = "default",
workspace_dir: Path | None = None,
) -> None:
super().__init__()
self.framework_type = "agentscope"
self.agent_id = agent_id # Store agent_id for config loading
self.workspace_dir = (
workspace_dir # Store workspace_dir for prompt building
)
self._chat_manager = None # Store chat_manager reference
self._mcp_manager = None # MCP client manager for hot-reload
self.memory_manager: MemoryManager | None = None
async def query_handler() # 处理输入提示词语llm请求
......
if not approval_consumed and query and _is_command(query):
logger.info("Command path: %s", query.strip()[:50])
# 判断是否为"/"指令
async for msg, last in run_command_path(request, msgs, self):
yield msg, last
return
......
# Get MCP clients from manager (hot-reloadable)
mcp_clients = []
if self._mcp_manager is not None:
mcp_clients = await self._mcp_manager.get_clients()
# 构建copawagent,继承ReActAgent功能,CoPaw Agent内根据运营商创建聊天实例,
# 并整合integrated tools, skills, and memory management.
# src/copaw/agents/react_agent.py 内copawagent类
agent = CoPawAgent(
env_context=env_context,
mcp_clients=mcp_clients,
memory_manager=self.memory_manager,
request_context={
"session_id": session_id,
"user_id": user_id,
"channel": channel,
"agent_id": self.agent_id,
},
max_iters=max_iters,
max_input_length=max_input_length,
memory_compact_threshold=(
running_config.memory_compact_threshold
),
memory_compact_reserve=running_config.memory_compact_reserve,
enable_tool_result_compact=(
running_config.enable_tool_result_compact
),
tool_result_compact_keep_n=(
running_config.tool_result_compact_keep_n
),
language=language,
workspace_dir=self.workspace_dir,
)
await agent.register_mcp_clients()
def list_available_skills(workspace_dir: Path) -> list[str]:
"""
List all available skills in active_skills directory.
Args:
workspace_dir: Workspace directory path.
Returns:
List of skill names.
skills列表获取是从存储的本地目录中获取,并确认SKILL.md存在
"""
active_skills = get_active_skills_dir(workspace_dir)
if not active_skills.exists():
return []
return [
d.name
for d in active_skills.iterdir()
if d.is_dir() and (d / "SKILL.md").exists()
]
class Toolkit(StateModule):
def register_agent_skill(......):
# 在注册skills时,把SKILL.md文件的name、description 、目录记录下来:
with open(path_skill_md, "r", encoding="utf-8") as f:
post = frontmatter.load(f)
name = post.get("name", None)
description = post.get("description", None)
if not name or not description:
raise ValueError(
f"The SKILL.md file in '{skill_dir}' must have a YAML Front "
"Matter including `name` and `description` fields.",
)
name, description = str(name), str(description)
if name in self.skills:
raise ValueError(
f"An agent skill with name '{name}' is already registered "
"in the toolkit.",
)
self.skills[name] = AgentSkill(
name=name,
description=description,
dir=skill_dir,
)
copaw agent 执行之后主要模型逻辑:
python
class ReActAgent(ReActAgentBase):
async def reply(
self,
msg: Msg | list[Msg] | None = None,
structured_model: Type[BaseModel] | None = None,
) -> Msg:
......
# 一个有限定次数的循环执行模型思考、工具调用、观察
for _ in range(self.max_iters):
# -------------- Memory compression --------------
await self._compress_memory_if_needed()
# -------------- The reasoning process --------------
msg_reasoning = await self._reasoning(tool_choice)
# -------------- The acting process --------------
futures = [
self._acting(tool_call)
for tool_call in msg_reasoning.get_content_blocks(
"tool_use",
)
]
# Parallel tool calls or not
if self.parallel_tool_calls:
structured_outputs = await asyncio.gather(*futures)
else:
# Sequential tool calls
structured_outputs = [await _ for _ in futures]
# -------------- Check for exit condition --------------
# If structured output is still not satisfied
if self._required_structured_model:
# Remove None results
structured_outputs = [_ for _ in structured_outputs if _]
msg_hint = None
# If the acting step generates structured outputs
if structured_outputs:
# Cache the structured output data
structured_output = structured_outputs[-1]
# Prepare textual response
if msg_reasoning.has_content_blocks("text"):
# Re-use the existing text response if any to avoid
# duplicate text generation
reply_msg = Msg(
self.name,
msg_reasoning.get_content_blocks("text"),
"assistant",
metadata=structured_output,
)
break
# Generate a textual response in the next iteration
msg_hint = Msg(
"user",
"<system-hint>Now generate a text "
"response based on your current situation"
"</system-hint>",
"user",
)
await self.memory.add(
msg_hint,
marks=_MemoryMark.HINT,
)
# Just generate text response in the next reasoning step
tool_choice = "none"
# The structured output is generated successfully
self._required_structured_model = None
elif not msg_reasoning.has_content_blocks("tool_use"):
# If structured output is required but no tool call is
# made, remind the llm to go on the task
msg_hint = Msg(
"user",
"<system-hint>Structured output is "
f"required, go on to finish your task or call "
f"'{self.finish_function_name}' to generate the "
f"required structured output.</system-hint>",
"user",
)
await self.memory.add(msg_hint, marks=_MemoryMark.HINT)
# Require tool call in the next reasoning step
tool_choice = "required"
if msg_hint and self.print_hint_msg:
await self.print(msg_hint)
elif not msg_reasoning.has_content_blocks("tool_use"):
# Exit the loop when no structured output is required (or
# already satisfied) and only text response is generated
msg_reasoning.metadata = structured_output
reply_msg = msg_reasoning
break
#模型思考
async def _reasoning(
self,
tool_choice: Literal["auto", "none", "required"] | None = None,
) -> Msg:
......
# 模型附带工具调用
res = await self.model(
prompt,
tools=self.toolkit.get_json_schemas(),
tool_choice=tool_choice,
)
......
# 根据模型返回处理,如果有工具则做调用
async def _acting(self, tool_call: ToolUseBlock) -> dict | None:
tool_res_msg = Msg(
"system",
[
ToolResultBlock(
type="tool_result",
id=tool_call["id"],
name=tool_call["name"],
output=[],
),
],
"system",
)
try:
# Execute the tool call
tool_res = await self.toolkit.call_tool_function(tool_call)
......
channel频道管理支持的内建:
python
_BUILTIN_SPECS: dict[str, tuple[str, str]] = {
"imessage": (".imessage", "IMessageChannel"),
"discord": (".discord_", "DiscordChannel"),
"dingtalk": (".dingtalk", "DingTalkChannel"),
"feishu": (".feishu", "FeishuChannel"),
"qq": (".qq", "QQChannel"),
"telegram": (".telegram", "TelegramChannel"),
"mattermost": (".mattermost", "MattermostChannel"),
"mqtt": (".mqtt", "MQTTChannel"),
"console": (".console", "ConsoleChannel"),
"matrix": (".matrix", "MatrixChannel"),
"voice": (".voice", "VoiceChannel"),
"wecom": (".wecom", "WecomChannel"),
"xiaoyi": (".xiaoyi", "XiaoYiChannel"),
}