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
# 如果没有设置默认智能体,尝试获取第一个可用的智能体
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:
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 (仅管理员)"""
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]
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")
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}
@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())
try:
agent = agent_manager.get_runnable_agent(agent_name)
agent = agent_manager.get_agent(agent_name)
except Exception as e:
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")
@ -284,7 +285,7 @@ async def get_agent_history(
"""获取智能体历史消息(需要登录)"""
try:
# 获取Agent实例和配置类
agent = agent_manager.get_runnable_agent(agent_name)
agent = agent_manager.get_agent(agent_name)
if not agent:
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.react import ReActAgent
class AgentManager:
def __init__(self):
self.agents = {}
self.agent_instances = {} # 存储已创建的 agent 实例
self._classes = {}
self._instances = {} # 存储已创建的 agent 实例
def add_agent(self, agent_id, agent_class):
self.agents[agent_id] = agent_class
def register_agent(self, 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 的实例
if agent_id not in self.agent_instances:
agent_class = self.get_agent(agent_id)
self.agent_instances[agent_id] = agent_class()
return self.agent_instances[agent_id]
if agent_id not in self._instances:
agent_class = self._classes[agent_id]
self._instances[agent_id] = agent_class()
def get_agent(self, agent_id):
return self.agents[agent_id]
return self._instances[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.add_agent("chatbot", ChatbotAgent)
# agent_manager.add_agent("react", ReActAgent) # 暂时屏蔽 ReActAgent
agent_manager.register_agent("chatbot", ChatbotAgent)
agent_manager.register_agent("react", ReActAgent) # 暂时屏蔽 ReActAgent
agent_manager.init_all_agents()
__all__ = ["agent_manager"]
if __name__ == "__main__":
agent = agent_manager.get_agent("chatbot")
conf = agent_manager.get_configuration("chatbot")
agent_info = {
"name": agent.name,
"description": agent.description,
}
print(agent_info)
pass

View File

@ -15,24 +15,6 @@ class ChatbotConfiguration(Configuration):
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(
default="You are a helpful assistant.",
metadata={

View File

@ -75,11 +75,18 @@ class ChatbotAgent(BaseAgent):
workflow.add_edge("tools", "chatbot")
workflow.add_edge("chatbot", END)
# 创建数据库连接
sqlite_checkpointer = AsyncSqliteSaver(await self.get_async_conn())
graph = workflow.compile(checkpointer=sqlite_checkpointer)
self.graph = graph
return graph
# 创建数据库连接并确保设置 checkpointer
try:
sqlite_checkpointer = AsyncSqliteSaver(await self.get_async_conn())
graph = workflow.compile(checkpointer=sqlite_checkpointer)
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:
"""获取异步数据库连接"""

View File

@ -1,36 +1,12 @@
from dataclasses import dataclass, field
from datetime import datetime, timezone
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from dataclasses import dataclass
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)
class ReActConfiguration(Configuration):
"""ReAct 的配置"""
"""配置"""
system_prompt: str = field(
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."
},
)
pass

View File

@ -2,28 +2,22 @@ import asyncio
import uuid
from src.utils import logger
from src.agents.registry import BaseAgent
from src.agents.react.configuration import ReActConfiguration
class ReActAgent(BaseAgent):
name = "react"
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
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__":
main()
# asyncio.run(main())
pass

View File

@ -1,8 +1,7 @@
import os
from langchain_openai import ChatOpenAI
from src import graph_base
from langgraph.checkpoint.memory import InMemorySaver
model = ChatOpenAI(model="glm-4-plus",
api_key=os.getenv("ZHIPUAI_API_KEY"),
@ -23,4 +22,4 @@ tools = []
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 yaml
import uuid
from pathlib import Path
from typing import Type, Annotated, Optional, TypedDict
from enum import Enum
@ -139,6 +140,23 @@ class Configuration(dict):
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():
@ -146,66 +164,85 @@ class BaseAgent():
定义一个基础 Agent 各类 graph 继承
"""
name: str = field(default="base_agent")
description: str = field(default="base_agent")
name = "base_agent"
description = "base_agent"
config_schema: Configuration = Configuration
requirements: list[str]
def __init__(self, **kwargs):
self.check_requirements()
@classmethod
def get_info(cls):
async def get_info(self):
return {
"name": cls.name,
"description": cls.description,
"config_schema": cls.config_schema.to_dict(),
"requirements": cls.requirements if hasattr(cls, "requirements") else [],
"all_tools": cls.all_tools if hasattr(cls, "all_tools") else [],
"name": self.name if hasattr(self, "name") else "Unknown",
"description": self.description if hasattr(self, "description") else "Unknown",
"config_schema": self.config_schema.to_dict(),
"requirements": self.requirements if hasattr(self, "requirements") 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):
if not hasattr(self, "requirements") or not self.requirements:
return
return True
for requirement in self.requirements:
if requirement not in os.environ:
raise ValueError(f"没有配置{requirement} 环境变量,请在 src/.env 文件中配置,并重新启动服务")
return True
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}")
for event in graph.astream({"messages": messages}, stream_mode="values", config=config_schema):
yield event["messages"]
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}")
async for msg, metadata in graph.astream({"messages": messages}, stream_mode="messages", config=config_schema):
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]:
"""获取历史消息"""
# 获取LangGraph应用实例
app = await self.get_graph()
# 构建配置信息
config = {"configurable": {"thread_id": thread_id, "user_id": user_id}}
# 获取状态
state = await app.aget_state(config)
try:
app = await self.get_graph()
result = []
if state:
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)
if not await self.check_checkpointer():
return []
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
def get_graph(self, **kwargs) -> CompiledStateGraph:
async def get_graph(self, **kwargs) -> CompiledStateGraph:
"""
获取并编译对话图实例
必须确保在编译时设置 checkpointer否则将无法获取历史记录
例如: graph = workflow.compile(checkpointer=sqlite_checkpointer)
"""
pass

View File

@ -44,7 +44,7 @@
<span>正在加载历史记录...</span>
</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>
<p>{{ currentAgent ? currentAgent.description : '不同的智能体有不同的专长和能力' }}</p>
</div>

View File

@ -35,7 +35,7 @@ import { useRouter } from 'vue-router';
import { useUserStore } from '@/stores/user';
import { UserOutlined, LogoutOutlined } from '@ant-design/icons-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 userStore = useUserStore();

View File

@ -59,14 +59,14 @@
<div class="sidebar" :class="{ 'is-open': state.agentSiderbarConfigOpen }">
<div class="agent-info">
<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">
<!-- 配置表单 -->
<a-form :model="agentConfig" layout="vertical">
<div class="empty-config" v-if="state.isEmptyConfig">
<a-alert type="warning" message="该智能体没有配置项" show-icon/>
</div>
<a-alert v-if="state.isEmptyConfig" 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/>
<!-- 统一显示所有配置项 -->
<template v-for="(value, key) in configurableItems" :key="key">
<a-form-item
@ -185,7 +185,9 @@ const state = reactive({
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 || {});
//
@ -617,6 +619,11 @@ const toggleSidebar = () => {
.config-modal-content {
max-height: 70vh;
overflow-y: auto;
user-select: text;
div[role="alert"] {
margin-bottom: 10px;
}
.description {
font-size: 12px;