refactor: 重构智能体管理和基础代理类

- 将 AgentManager 类中的方法重命名为更具描述性的名称,并优化了代理的注册和实例化逻辑。
- 添加了线程ID和用户ID字段到基础代理类,并改进了获取历史消息的逻辑,增加了错误处理。
- 在 ChatbotAgent 中确保创建图时设置 checkpointer,并处理可能的异常。
This commit is contained in:
Wenjie Zhang 2025-05-20 22:21:51 +08:00
parent 44fad724bf
commit 6475170c4b
11 changed files with 133 additions and 125 deletions

View File

@ -31,7 +31,7 @@ async def get_default_agent(current_user: User = Depends(get_required_user)):
default_agent_id = config.default_agent_id default_agent_id = config.default_agent_id
# 如果没有设置默认智能体,尝试获取第一个可用的智能体 # 如果没有设置默认智能体,尝试获取第一个可用的智能体
if not default_agent_id: if not default_agent_id:
agents = [agent.get_info() for agent in agent_manager.agents.values()] agents = await agent_manager.get_agents_info()
if agents: if agents:
default_agent_id = agents[0].get("name", "") default_agent_id = agents[0].get("name", "")
@ -45,7 +45,7 @@ async def set_default_agent(agent_id: str = Body(..., embed=True), current_user
"""设置默认智能体ID (仅管理员)""" """设置默认智能体ID (仅管理员)"""
try: try:
# 验证智能体是否存在 # 验证智能体是否存在
agents = [agent.get_info() for agent in agent_manager.agents.values()] agents = await agent_manager.get_agents_info()
agent_ids = [agent.get("name", "") for agent in agents] agent_ids = [agent.get("name", "") for agent in agents]
if agent_id not in agent_ids: if agent_id not in agent_ids:
@ -161,7 +161,8 @@ async def call(query: str = Body(...), meta: dict = Body(None), current_user: Us
@chat.get("/agent") @chat.get("/agent")
async def get_agent(current_user: User = Depends(get_required_user)): async def get_agent(current_user: User = Depends(get_required_user)):
"""获取所有可用智能体(需要登录)""" """获取所有可用智能体(需要登录)"""
agents = [agent.get_info() for agent in agent_manager.agents.values()] agents = await agent_manager.get_agents_info()
# logger.debug(f"agents: {agents}")
return {"agents": agents} return {"agents": agents}
@chat.post("/agent/{agent_name}") @chat.post("/agent/{agent_name}")
@ -195,7 +196,7 @@ async def chat_agent(agent_name: str,
yield make_chunk(status="init", meta=meta, msg=HumanMessage(content=query).model_dump()) yield make_chunk(status="init", meta=meta, msg=HumanMessage(content=query).model_dump())
try: try:
agent = agent_manager.get_runnable_agent(agent_name) agent = agent_manager.get_agent(agent_name)
except Exception as e: except Exception as e:
logger.error(f"Error getting agent {agent_name}: {e}, {traceback.format_exc()}") logger.error(f"Error getting agent {agent_name}: {e}, {traceback.format_exc()}")
yield make_chunk(message=f"Error getting agent {agent_name}: {e}", status="error") yield make_chunk(message=f"Error getting agent {agent_name}: {e}", status="error")
@ -284,7 +285,7 @@ async def get_agent_history(
"""获取智能体历史消息(需要登录)""" """获取智能体历史消息(需要登录)"""
try: try:
# 获取Agent实例和配置类 # 获取Agent实例和配置类
agent = agent_manager.get_runnable_agent(agent_name) agent = agent_manager.get_agent(agent_name)
if not agent: if not agent:
raise HTTPException(status_code=404, detail=f"智能体 {agent_name} 不存在") raise HTTPException(status_code=404, detail=f"智能体 {agent_name} 不存在")

View File

@ -1,37 +1,42 @@
import asyncio
from src.agents.chatbot import ChatbotAgent from src.agents.chatbot import ChatbotAgent
from src.agents.react import ReActAgent from src.agents.react import ReActAgent
class AgentManager: class AgentManager:
def __init__(self): def __init__(self):
self.agents = {} self._classes = {}
self.agent_instances = {} # 存储已创建的 agent 实例 self._instances = {} # 存储已创建的 agent 实例
def add_agent(self, agent_id, agent_class): def register_agent(self, agent_id, agent_class):
self.agents[agent_id] = agent_class self._classes[agent_id] = agent_class
def get_runnable_agent(self, agent_id, **kwargs): def init_all_agents(self):
for agent_id, agent_class in self._classes.items():
self.get_agent(agent_id)
def get_agent(self, agent_id, **kwargs):
# 检查是否已经创建了该 agent 的实例 # 检查是否已经创建了该 agent 的实例
if agent_id not in self.agent_instances: if agent_id not in self._instances:
agent_class = self.get_agent(agent_id) agent_class = self._classes[agent_id]
self.agent_instances[agent_id] = agent_class() self._instances[agent_id] = agent_class()
return self.agent_instances[agent_id]
def get_agent(self, agent_id): return self._instances[agent_id]
return self.agents[agent_id]
def get_agents(self):
return list(self._instances.values())
async def get_agents_info(self):
agents = self.get_agents()
return await asyncio.gather(*[a.get_info() for a in agents])
agent_manager = AgentManager() agent_manager = AgentManager()
agent_manager.add_agent("chatbot", ChatbotAgent) agent_manager.register_agent("chatbot", ChatbotAgent)
# agent_manager.add_agent("react", ReActAgent) # 暂时屏蔽 ReActAgent agent_manager.register_agent("react", ReActAgent) # 暂时屏蔽 ReActAgent
agent_manager.init_all_agents()
__all__ = ["agent_manager"] __all__ = ["agent_manager"]
if __name__ == "__main__": if __name__ == "__main__":
agent = agent_manager.get_agent("chatbot") pass
conf = agent_manager.get_configuration("chatbot")
agent_info = {
"name": agent.name,
"description": agent.description,
}
print(agent_info)

View File

@ -15,24 +15,6 @@ class ChatbotConfiguration(Configuration):
configurable False 的配置项不能被用户配置只能由开发者预设 configurable False 的配置项不能被用户配置只能由开发者预设
""" """
thread_id: str = field(
default_factory=lambda: str(uuid.uuid4()),
metadata={
"name": "线程ID",
"configurable": False,
"description": "用来描述智能体的角色和行为"
},
)
user_id: str = field(
default_factory=lambda: str(uuid.uuid4()),
metadata={
"name": "用户ID",
"configurable": False,
"description": "用来描述智能体的角色和行为"
},
)
system_prompt: str = field( system_prompt: str = field(
default="You are a helpful assistant.", default="You are a helpful assistant.",
metadata={ metadata={

View File

@ -75,11 +75,18 @@ class ChatbotAgent(BaseAgent):
workflow.add_edge("tools", "chatbot") workflow.add_edge("tools", "chatbot")
workflow.add_edge("chatbot", END) workflow.add_edge("chatbot", END)
# 创建数据库连接 # 创建数据库连接并确保设置 checkpointer
sqlite_checkpointer = AsyncSqliteSaver(await self.get_async_conn()) try:
graph = workflow.compile(checkpointer=sqlite_checkpointer) sqlite_checkpointer = AsyncSqliteSaver(await self.get_async_conn())
self.graph = graph graph = workflow.compile(checkpointer=sqlite_checkpointer)
return graph self.graph = graph
return graph
except Exception as e:
logger.error(f"构建 Graph 设置 checkpointer 时出错: {e}")
# 即使出错也返回一个可用的图实例,只是无法保存历史
graph = workflow.compile()
self.graph = graph
return graph
async def get_async_conn(self) -> aiosqlite.Connection: async def get_async_conn(self) -> aiosqlite.Connection:
"""获取异步数据库连接""" """获取异步数据库连接"""

View File

@ -1,36 +1,12 @@
from dataclasses import dataclass, field
from datetime import datetime, timezone
from langchain_core.tools import tool from dataclasses import dataclass
from langchain_openai import ChatOpenAI
from src.agents.registry import Configuration from src.agents.registry import Configuration
def get_default_requirements():
return ["TAVILY_API_KEY"]
@tool
def multiply(first_int: int, second_int: int) -> int:
"""Multiply two integers together."""
return first_int * second_int
@dataclass(kw_only=True) @dataclass(kw_only=True)
class ReActConfiguration(Configuration): class ReActConfiguration(Configuration):
"""ReAct 的配置""" """配置"""
system_prompt: str = field( pass
default=f"You are a helpful assistant. Now is {datetime.now(tz=timezone.utc).isoformat()}",
metadata={
"description": "The system prompt to use for the agent's interactions. "
"This prompt sets the context and behavior for the agent."
},
)
model: str = field(
default="zhipu/glm-4-plus",
metadata={
"description": "The name of the language model to use for the agent's main interactions. "
"Should be in the form: provider/model-name."
},
)

View File

@ -2,28 +2,22 @@ import asyncio
import uuid import uuid
from src.utils import logger
from src.agents.registry import BaseAgent from src.agents.registry import BaseAgent
from src.agents.react.configuration import ReActConfiguration from src.agents.react.configuration import ReActConfiguration
class ReActAgent(BaseAgent): class ReActAgent(BaseAgent):
name = "react" name = "react"
description = "A react agent that can answer questions and help with tasks." description = "A react agent that can answer questions and help with tasks."
config_schema = ReActConfiguration
def get_graph(self, **kwargs): async def get_graph(self, **kwargs):
"""构建图"""
from .workflows import graph from .workflows import graph
return graph return graph
def main():
agent = ReActAgent(ReActConfiguration())
thread_id = str(uuid.uuid4())
config = {"configurable": {"thread_id": thread_id}}
from src.agents.utils import agent_cli
asyncio.run(agent_cli(agent, config))
if __name__ == "__main__": if __name__ == "__main__":
main() pass
# asyncio.run(main())

View File

@ -1,8 +1,7 @@
import os import os
from langchain_openai import ChatOpenAI from langchain_openai import ChatOpenAI
from langgraph.checkpoint.memory import InMemorySaver
from src import graph_base
model = ChatOpenAI(model="glm-4-plus", model = ChatOpenAI(model="glm-4-plus",
api_key=os.getenv("ZHIPUAI_API_KEY"), api_key=os.getenv("ZHIPUAI_API_KEY"),
@ -23,4 +22,4 @@ tools = []
from langgraph.prebuilt import create_react_agent from langgraph.prebuilt import create_react_agent
graph = create_react_agent(model, tools=tools) graph = create_react_agent(model, tools=tools, checkpointer=InMemorySaver())

View File

@ -2,6 +2,7 @@ from __future__ import annotations
import os import os
import yaml import yaml
import uuid
from pathlib import Path from pathlib import Path
from typing import Type, Annotated, Optional, TypedDict from typing import Type, Annotated, Optional, TypedDict
from enum import Enum from enum import Enum
@ -139,6 +140,23 @@ class Configuration(dict):
return confs return confs
thread_id: str = field(
default_factory=lambda: str(uuid.uuid4()),
metadata={
"name": "线程ID",
"configurable": False,
"description": "用来描述智能体的角色和行为"
},
)
user_id: str = field(
default_factory=lambda: str(uuid.uuid4()),
metadata={
"name": "用户ID",
"configurable": False,
"description": "用来描述智能体的角色和行为"
},
)
class BaseAgent(): class BaseAgent():
@ -146,66 +164,85 @@ class BaseAgent():
定义一个基础 Agent 各类 graph 继承 定义一个基础 Agent 各类 graph 继承
""" """
name: str = field(default="base_agent") name = "base_agent"
description: str = field(default="base_agent") description = "base_agent"
config_schema: Configuration = Configuration config_schema: Configuration = Configuration
requirements: list[str] requirements: list[str]
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.check_requirements() self.check_requirements()
@classmethod async def get_info(self):
def get_info(cls):
return { return {
"name": cls.name, "name": self.name if hasattr(self, "name") else "Unknown",
"description": cls.description, "description": self.description if hasattr(self, "description") else "Unknown",
"config_schema": cls.config_schema.to_dict(), "config_schema": self.config_schema.to_dict(),
"requirements": cls.requirements if hasattr(cls, "requirements") else [], "requirements": self.requirements if hasattr(self, "requirements") else [],
"all_tools": cls.all_tools if hasattr(cls, "all_tools") else [], "all_tools": self.all_tools if hasattr(self, "all_tools") else [],
"has_checkpointer": await self.check_checkpointer(),
"met_requirements": self.check_requirements(),
} }
def check_requirements(self): def check_requirements(self):
if not hasattr(self, "requirements") or not self.requirements: if not hasattr(self, "requirements") or not self.requirements:
return return True
for requirement in self.requirements: for requirement in self.requirements:
if requirement not in os.environ: if requirement not in os.environ:
raise ValueError(f"没有配置{requirement} 环境变量,请在 src/.env 文件中配置,并重新启动服务") raise ValueError(f"没有配置{requirement} 环境变量,请在 src/.env 文件中配置,并重新启动服务")
return True
async def stream_values(self, messages: list[str], config_schema: RunnableConfig = None, **kwargs): async def stream_values(self, messages: list[str], config_schema: RunnableConfig = None, **kwargs):
graph = await self.get_graph(config_schema=config_schema, **kwargs) graph = await self.get_graph()
logger.debug(f"stream_values: {config_schema}") logger.debug(f"stream_values: {config_schema}")
for event in graph.astream({"messages": messages}, stream_mode="values", config=config_schema): for event in graph.astream({"messages": messages}, stream_mode="values", config=config_schema):
yield event["messages"] yield event["messages"]
async def stream_messages(self, messages: list[str], config_schema: RunnableConfig = None, **kwargs): async def stream_messages(self, messages: list[str], config_schema: RunnableConfig = None, **kwargs):
graph = await self.get_graph(config_schema=config_schema, **kwargs) graph = await self.get_graph()
logger.debug(f"stream_messages: {config_schema}") logger.debug(f"stream_messages: {config_schema}")
async for msg, metadata in graph.astream({"messages": messages}, stream_mode="messages", config=config_schema): async for msg, metadata in graph.astream({"messages": messages}, stream_mode="messages", config=config_schema):
yield msg, metadata yield msg, metadata
async def check_checkpointer(self):
app = await self.get_graph()
if not hasattr(app, "checkpointer") or app.checkpointer is None:
logger.warning(f"智能体 {self.name} 的 Graph 未配置 checkpointer无法获取历史记录")
return False
return True
async def get_history(self, user_id, thread_id) -> list[dict]: async def get_history(self, user_id, thread_id) -> list[dict]:
"""获取历史消息""" """获取历史消息"""
# 获取LangGraph应用实例 try:
app = await self.get_graph() app = await self.get_graph()
# 构建配置信息
config = {"configurable": {"thread_id": thread_id, "user_id": user_id}}
# 获取状态
state = await app.aget_state(config)
result = [] if not await self.check_checkpointer():
if state: return []
messages = state.values.get('messages', [])
for msg in messages:
if hasattr(msg, 'model_dump'):
msg_dict = msg.model_dump() # 转换成字典
else:
# 如果消息没有model_dump方法尝试转成dict
msg_dict = dict(msg) if hasattr(msg, '__dict__') else {"content": str(msg)}
result.append(msg_dict)
return result config = {"configurable": {"thread_id": thread_id, "user_id": user_id}}
state = await app.aget_state(config)
result = []
if state:
messages = state.values.get('messages', [])
for msg in messages:
if hasattr(msg, 'model_dump'):
msg_dict = msg.model_dump() # 转换成字典
else:
msg_dict = dict(msg) if hasattr(msg, '__dict__') else {"content": str(msg)}
result.append(msg_dict)
return result
except Exception as e:
logger.error(f"获取智能体 {self.name} 历史消息出错: {e}")
return []
@abstractmethod @abstractmethod
def get_graph(self, **kwargs) -> CompiledStateGraph: async def get_graph(self, **kwargs) -> CompiledStateGraph:
"""
获取并编译对话图实例
必须确保在编译时设置 checkpointer否则将无法获取历史记录
例如: graph = workflow.compile(checkpointer=sqlite_checkpointer)
"""
pass pass

View File

@ -44,7 +44,7 @@
<span>正在加载历史记录...</span> <span>正在加载历史记录...</span>
</div> </div>
<div v-else-if="convs.length === 0" class="chat-examples"> <div v-else-if="convs.length === 0 && !onGoingConv.messages.length" class="chat-examples">
<h1>{{ currentAgent ? currentAgent.name : '请选择一个智能体开始对话' }}</h1> <h1>{{ currentAgent ? currentAgent.name : '请选择一个智能体开始对话' }}</h1>
<p>{{ currentAgent ? currentAgent.description : '不同的智能体有不同的专长和能力' }}</p> <p>{{ currentAgent ? currentAgent.description : '不同的智能体有不同的专长和能力' }}</p>
</div> </div>

View File

@ -35,7 +35,7 @@ import { useRouter } from 'vue-router';
import { useUserStore } from '@/stores/user'; import { useUserStore } from '@/stores/user';
import { UserOutlined, LogoutOutlined } from '@ant-design/icons-vue'; import { UserOutlined, LogoutOutlined } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { CircleUser, LogOut } from 'lucide-vue-next'; import { CircleUser, UserRoundCheck } from 'lucide-vue-next';
const router = useRouter(); const router = useRouter();
const userStore = useUserStore(); const userStore = useUserStore();

View File

@ -59,14 +59,14 @@
<div class="sidebar" :class="{ 'is-open': state.agentSiderbarConfigOpen }"> <div class="sidebar" :class="{ 'is-open': state.agentSiderbarConfigOpen }">
<div class="agent-info"> <div class="agent-info">
<h3 @click="toggleDebugMode">描述</h3> <h3 @click="toggleDebugMode">描述</h3>
<p>{{ agents[selectedAgentId]?.description }}</p> <p>{{ selectedAgent.description }}</p>
<pre v-if="state.debug_mode">{{ selectedAgent }}</pre>
<div v-if="selectedAgentId && configSchema" class="config-modal-content"> <div v-if="selectedAgentId && configSchema" class="config-modal-content">
<!-- 配置表单 --> <!-- 配置表单 -->
<a-form :model="agentConfig" layout="vertical"> <a-form :model="agentConfig" layout="vertical">
<div class="empty-config" v-if="state.isEmptyConfig"> <a-alert v-if="state.isEmptyConfig" type="warning" message="该智能体没有配置项" show-icon/>
<a-alert type="warning" message="该智能体没有配置项" show-icon/> <a-alert v-if="!selectedAgent.has_checkpointer" type="error" message="该智能体没有配置 Checkpointer功能无法正常使用参考https://langchain-ai.github.io/langgraph/concepts/persistence/" show-icon/>
</div>
<!-- 统一显示所有配置项 --> <!-- 统一显示所有配置项 -->
<template v-for="(value, key) in configurableItems" :key="key"> <template v-for="(value, key) in configurableItems" :key="key">
<a-form-item <a-form-item
@ -185,7 +185,9 @@ const state = reactive({
Object.keys(configurableItems.value).length === 0 Object.keys(configurableItems.value).length === 0
) )
}); });
const configSchema = computed(() => agents.value[selectedAgentId.value]?.config_schema || {});
const selectedAgent = computed(() => agents.value[selectedAgentId.value] || {});
const configSchema = computed(() => selectedAgent.value.config_schema || {});
const configurableItems = computed(() => configSchema.value.configurable_items || {}); const configurableItems = computed(() => configSchema.value.configurable_items || {});
// //
@ -617,6 +619,11 @@ const toggleSidebar = () => {
.config-modal-content { .config-modal-content {
max-height: 70vh; max-height: 70vh;
overflow-y: auto; overflow-y: auto;
user-select: text;
div[role="alert"] {
margin-bottom: 10px;
}
.description { .description {
font-size: 12px; font-size: 12px;