feat(agents): 重构智能体系统,引入Context替代Configuration
- 将Configuration重构为Context,支持动态工具配置 - 新增state.py和tools.py模块,优化状态管理和工具处理 - 移除旧的tools_factory.py和registry.py - 更新前端API引用路径,修复brandApi命名错误 - 调整README和更新日志,反映架构变更 - 优化智能体历史记录管理,改进SQLite检查点 - 修复默认智能体加载和工具展示问题
This commit is contained in:
parent
808485aa5e
commit
b524ca687b
@ -29,7 +29,7 @@
|
||||
<a href="https://www.bilibili.com/video/BV1ETedzREgY/?share_source=copy_web&vd_source=37b0bdbf95b72ea38b2dc959cfadc4d8" target="_blank">
|
||||
<img width="3651" height="1933" alt="视频演示缩略图" src="https://github.com/user-attachments/assets/eac4fa89-2176-46ae-a649-45a125cb6ed1" />
|
||||
</a>
|
||||
|
||||
|
||||
<!-- 视频链接文字 -->
|
||||
<p style="margin-top: 12px;">
|
||||
<a href="https://www.bilibili.com/video/BV1ETedzREgY/?share_source=copy_web&vd_source=37b0bdbf95b72ea38b2dc959cfadc4d8" target="_blank" style="text-decoration: none; color: #23ade5; font-weight: 500;">
|
||||
@ -223,7 +223,7 @@ docker compose up paddlex --build
|
||||
|
||||
目前该项目默认集成了三个 Demo 智能体,包含基础智能体、ReAct、DeepResearch 三个案例 Demo,均使用 [LangGraph](https://github.com/langchain-ai/langgraph) 开发。代码位于 [src/agents](src/agents) 目录。在 [src/agents/react/graph.py](src/agents/react/graph.py) 中定义了 `ReActAgent` 示例。
|
||||
|
||||
如果需要自定义智能体应用,实现一个继承于 `BaseAgent` 的类,并实现 `get_graph` 方法返回一个 graph 实例。智能体的 `config_schema` 定义了配置参数,可继承 `Configuration` 定义。
|
||||
如果需要自定义智能体应用,实现一个继承于 `BaseAgent` 的类,并实现 `get_graph` 方法返回一个 graph 实例。智能体的 `context_schema` 定义了配置参数。
|
||||
|
||||
注册智能体的方式请参考已有实现:[src/agents/__init__.py](src/agents/__init__.py)。例如:
|
||||
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
目前已有的开发计划包括:
|
||||
|
||||
💭 **Features Todo**
|
||||
- [ ] LangGraph 升级到 0.6+ 版本,并适配新特性,如 context 等。
|
||||
- [ ] LangGraph 升级到 0.6+ 版本,并适配新特性,如 context 等,添加 MCP 工具的支持。
|
||||
- [x] 支持动态工具配置的同时,将 Configuration替换为 Context 后能够正常使用
|
||||
|
||||
- [ ] 使用其他的聊天记录管理方法,解决两个问题,一个是上下文长度过长,一个是上下文的类型变得更加丰富,比如多模态等等。(现在是基于 LangGraph 的 Memory 实现的,v0.2.3 版本实现),暂定使用 [mem0](github.com/mem0ai/mem0) 来实现。但是目前了解下来,还不是我想要的那种方案。可能会基于这个实现一个 ThreadConvManager 这个类。
|
||||
- [ ] 添加对于上传文件的支持:这里的复杂的地方就在于如何和历史记录结合在一起(v0.2.3 版本实现,放在记忆管理后面)
|
||||
|
||||
@ -9,6 +11,8 @@
|
||||
- [x] LlightRAG 知识库中,点击边,没有显示,但是在全屏的时候却又能够显示出来。
|
||||
- [ ] 部分 doc 格式的文件支持有问题
|
||||
- [ ] 当出现不支持的文件类型的时候,前端没有限制
|
||||
- [ ] 默认智能体设置后,在一些情况下依然仅加载第一个智能体
|
||||
- [ ] 目前只能获取默认的 tools,单个智能体的tools没法展示
|
||||
|
||||
💯 **More**:
|
||||
|
||||
|
||||
@ -4,7 +4,6 @@ from server.routers.auth_router import auth
|
||||
from server.routers.chat_router import chat
|
||||
from server.routers.knowledge_router import knowledge
|
||||
from server.routers.graph_router import graph
|
||||
from server.routers.tool_router import tool
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@ -14,4 +13,3 @@ router.include_router(auth) # /api/auth/*
|
||||
router.include_router(chat) # /api/chat/*
|
||||
router.include_router(knowledge) # /api/knowledge/*
|
||||
router.include_router(graph) # /api/graph/*
|
||||
router.include_router(tool) # /api/tool/*
|
||||
|
||||
@ -14,7 +14,7 @@ from src import executor, config
|
||||
from src.agents import agent_manager
|
||||
from src.models import select_model
|
||||
from src.utils.logging_config import logger
|
||||
from src.agents.tools_factory import get_buildin_tools_info
|
||||
from src.agents.common.tools import get_buildin_tools, gen_tool_info
|
||||
from server.routers.auth_router import get_admin_user
|
||||
from server.utils.auth_middleware import get_required_user, get_db
|
||||
from server.models.user_model import User
|
||||
@ -102,6 +102,8 @@ async def chat_agent(agent_id: str,
|
||||
current_user: User = Depends(get_required_user)):
|
||||
"""使用特定智能体进行对话(需要登录)"""
|
||||
|
||||
logger.info(f"agent_id: {agent_id}, query: {query}, config: {config}, meta: {meta}")
|
||||
|
||||
meta.update({
|
||||
"query": query,
|
||||
"agent_id": agent_id,
|
||||
@ -134,15 +136,13 @@ async def chat_agent(agent_id: str,
|
||||
messages = [{"role": "user", "content": query}]
|
||||
|
||||
# 构造运行时配置,如果没有thread_id则生成一个
|
||||
config["user_id"] = str(current_user.id)
|
||||
if "thread_id" not in config or not config["thread_id"]:
|
||||
config["thread_id"] = str(uuid.uuid4())
|
||||
logger.debug(f"没有thread_id,生成一个: {config['thread_id']=}")
|
||||
user_id = str(current_user.id)
|
||||
thread_id = config.get("thread_id")
|
||||
|
||||
runnable_config = {"configurable": {**config}}
|
||||
input_context = {"user_id": user_id, "thread_id": thread_id}
|
||||
|
||||
try:
|
||||
async for msg, metadata in agent.stream_messages(messages, config_schema=runnable_config):
|
||||
async for msg, metadata in agent.stream_messages(messages, input_context=input_context):
|
||||
# logger.debug(f"msg: {msg.model_dump()}, metadata: {metadata}")
|
||||
if isinstance(msg, AIMessageChunk):
|
||||
yield make_chunk(content=msg.content,
|
||||
@ -179,9 +179,12 @@ async def update_chat_models(model_provider: str, model_names: list[str], curren
|
||||
return {"models": config.model_names[model_provider]["models"]}
|
||||
|
||||
@chat.get("/tools")
|
||||
async def get_tools(current_user: User = Depends(get_admin_user)):
|
||||
async def get_tools(agent_id: str, current_user: User = Depends(get_admin_user)):
|
||||
"""获取所有可用工具(需要登录)"""
|
||||
return {"tools": [t.name for t in get_buildin_tools_info()]}
|
||||
logger.info(f"agent_id: {agent_id}")
|
||||
tools = get_buildin_tools()
|
||||
tools_info = gen_tool_info(tools)
|
||||
return {"tools": {tool["id"]: tool for tool in tools_info}}
|
||||
|
||||
@chat.post("/agent/{agent_id}/config")
|
||||
async def save_agent_config(
|
||||
@ -192,13 +195,11 @@ async def save_agent_config(
|
||||
"""保存智能体配置到YAML文件(需要管理员权限)"""
|
||||
try:
|
||||
# 获取Agent实例和配置类
|
||||
agent = agent_manager.get_agent(agent_id)
|
||||
if not agent:
|
||||
if not (agent := agent_manager.get_agent(agent_id)):
|
||||
raise HTTPException(status_code=404, detail=f"智能体 {agent_id} 不存在")
|
||||
|
||||
# 使用配置类的save_to_file方法保存配置
|
||||
config_cls = agent.config_schema
|
||||
result = config_cls.save_to_file(config, agent.module_name)
|
||||
result = agent.context_schema.save_to_file(config, agent.module_name)
|
||||
|
||||
if result:
|
||||
return {"success": True, "message": f"智能体 {agent.name} 配置已保存"}
|
||||
@ -218,8 +219,7 @@ async def get_agent_history(
|
||||
"""获取智能体历史消息(需要登录)"""
|
||||
try:
|
||||
# 获取Agent实例和配置类
|
||||
agent = agent_manager.get_agent(agent_id)
|
||||
if not agent:
|
||||
if not (agent := agent_manager.get_agent(agent_id)):
|
||||
raise HTTPException(status_code=404, detail=f"智能体 {agent_id} 不存在")
|
||||
|
||||
# 获取历史消息
|
||||
@ -241,7 +241,8 @@ async def get_agent_config(
|
||||
if not (agent := agent_manager.get_agent(agent_id)):
|
||||
raise HTTPException(status_code=404, detail=f"智能体 {agent_id} 不存在")
|
||||
|
||||
config = agent.config_schema.from_runnable_config(config={}, module_name=agent.module_name)
|
||||
config = await agent.get_config()
|
||||
logger.debug(f"config: {config}, ContextClass: {agent.context_schema=}")
|
||||
return {"success": True, "config": config}
|
||||
|
||||
except Exception as e:
|
||||
@ -279,6 +280,7 @@ async def create_thread(
|
||||
):
|
||||
"""创建新对话线程"""
|
||||
thread_id = str(uuid.uuid4())
|
||||
logger.debug(f"thread.agent_id: {thread.agent_id}")
|
||||
|
||||
new_thread = Thread(
|
||||
id=thread_id,
|
||||
@ -305,18 +307,19 @@ async def create_thread(
|
||||
|
||||
@chat.get("/threads", response_model=list[ThreadResponse])
|
||||
async def list_threads(
|
||||
agent_id: str | None = None,
|
||||
agent_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_required_user)
|
||||
):
|
||||
"""获取用户的所有对话线程"""
|
||||
assert agent_id, "agent_id 不能为空"
|
||||
query = db.query(Thread).filter(
|
||||
Thread.user_id == str(current_user.id),
|
||||
Thread.status == 1
|
||||
Thread.status == 1,
|
||||
Thread.agent_id == agent_id,
|
||||
)
|
||||
|
||||
if agent_id:
|
||||
query = query.filter(Thread.agent_id == agent_id)
|
||||
logger.debug(f"agent_id: {agent_id}")
|
||||
|
||||
threads = query.order_by(Thread.update_at.desc()).all()
|
||||
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from src.agents.tools_factory import get_buildin_tools_info
|
||||
from server.models.user_model import User
|
||||
from server.utils.auth_middleware import get_required_user
|
||||
|
||||
tool = chat = APIRouter(prefix="/tool", tags=["tool"])
|
||||
|
||||
@tool.get("/tools")
|
||||
async def get_tools(current_user: User = Depends(get_required_user)):
|
||||
"""获取所有可用工具的信息"""
|
||||
try:
|
||||
tools_info = get_buildin_tools_info()
|
||||
return {"tools": {tool["id"]: tool for tool in tools_info}}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
@ -1,6 +1,6 @@
|
||||
import asyncio
|
||||
|
||||
from .chatbot import ChatbotAgent
|
||||
from .chatbot.graph import ChatbotAgent
|
||||
from .react.graph import ReActAgent
|
||||
|
||||
class AgentManager:
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
from .graph import ChatbotAgent
|
||||
from .configuration import ChatbotConfiguration
|
||||
|
||||
__all__ = ["ChatbotAgent", "ChatbotConfiguration"]
|
||||
__all__ = ["ChatbotAgent"]
|
||||
|
||||
@ -1,43 +0,0 @@
|
||||
import uuid
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from src.agents.registry import Configuration
|
||||
from src.agents.tools_factory import get_buildin_tools
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class ChatbotConfiguration(Configuration):
|
||||
"""Chatbot 的配置
|
||||
|
||||
配置说明:
|
||||
|
||||
metadata 中 configurable 为 True 的配置项可以被用户配置,
|
||||
configurable 为 False 的配置项不能被用户配置,只能由开发者预设。
|
||||
除非显示配置为 False,否则所有配置项都默认可配置。
|
||||
"""
|
||||
|
||||
system_prompt: str = field(
|
||||
default="You are a helpful assistant.",
|
||||
metadata={
|
||||
"name": "系统提示词",
|
||||
"description": "用来描述智能体的角色和行为"
|
||||
},
|
||||
)
|
||||
|
||||
model: str = field(
|
||||
default="siliconflow/Qwen/Qwen3-235B-A22B-Instruct-2507",
|
||||
metadata={
|
||||
"name": "智能体模型",
|
||||
"options": [],
|
||||
"description": "智能体的驱动模型"
|
||||
},
|
||||
)
|
||||
|
||||
tools: list[str] = field(
|
||||
default_factory=list,
|
||||
metadata={
|
||||
"name": "工具",
|
||||
"options": [t.name for t in get_buildin_tools()], # 这里的选择是所有的工具
|
||||
"description": "工具列表"
|
||||
},
|
||||
)
|
||||
26
src/agents/chatbot/context.py
Normal file
26
src/agents/chatbot/context.py
Normal file
@ -0,0 +1,26 @@
|
||||
from typing import Annotated
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from src.agents.common.context import BaseContext
|
||||
from src.agents.common.tools import get_buildin_tools
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Context(BaseContext):
|
||||
|
||||
model: Annotated[str, {"__template_metadata__": {"kind": "llm"}}] = field(
|
||||
default="siliconflow/Qwen/Qwen3-235B-A22B-Instruct-2507",
|
||||
metadata={
|
||||
"name": "智能体模型",
|
||||
"options": [],
|
||||
"description": "智能体的驱动模型"
|
||||
},
|
||||
)
|
||||
|
||||
tools: Annotated[list[str], {"__template_metadata__": {"kind": "tools"}}] = field(
|
||||
default_factory=list,
|
||||
metadata={
|
||||
"name": "工具",
|
||||
"options": [t.name for t in get_buildin_tools()], # 这里的选择是所有的工具
|
||||
"description": "工具列表"
|
||||
},
|
||||
)
|
||||
@ -1,31 +1,38 @@
|
||||
import os
|
||||
import uuid
|
||||
from typing import Any
|
||||
from typing import Any, cast, Annotated
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import sqlite3
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from dataclasses import dataclass, field, fields
|
||||
from langchain_core.messages import AIMessage, ToolMessage
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
from langgraph.runtime import Runtime
|
||||
from langgraph.prebuilt import ToolNode, tools_condition
|
||||
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver, aiosqlite
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
|
||||
from src import config as sys_config
|
||||
from src.utils import logger
|
||||
from src.agents.registry import State, BaseAgent
|
||||
from src.agents.utils import load_chat_model, get_cur_time_with_utc
|
||||
from src.agents.chatbot.configuration import ChatbotConfiguration
|
||||
from src.agents.tools_factory import get_buildin_tools
|
||||
from src.agents.common.utils import get_cur_time_with_utc
|
||||
from src.agents.common.base import BaseAgent
|
||||
from src.agents.common.models import load_chat_model
|
||||
|
||||
from .state import State
|
||||
from .context import Context
|
||||
from .tools import get_tools
|
||||
|
||||
|
||||
|
||||
class ChatbotAgent(BaseAgent):
|
||||
name = "智能体助手"
|
||||
description = "基础的对话机器人,可以回答问题,默认不使用任何工具,可在配置中启用需要的工具。"
|
||||
config_schema = ChatbotConfiguration
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.graph = None
|
||||
self.workdir = Path(sys_config.save_dir) / "agents" / self.id
|
||||
self.context_schema = Context
|
||||
self.workdir = Path(sys_config.save_dir) / "agents" / self.module_name
|
||||
self.workdir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def _get_tools(self, tools: list[str]):
|
||||
@ -33,7 +40,7 @@ class ChatbotAgent(BaseAgent):
|
||||
默认不使用任何工具。
|
||||
如果配置为列表,则使用列表中的工具。
|
||||
"""
|
||||
platform_tools = get_buildin_tools()
|
||||
platform_tools = get_tools()
|
||||
if tools is None or not isinstance(tools, list) or len(tools) == 0:
|
||||
# 默认不使用任何工具
|
||||
logger.info("未配置工具或配置为空,不使用任何工具")
|
||||
@ -44,51 +51,74 @@ class ChatbotAgent(BaseAgent):
|
||||
logger.info(f"使用工具: {[tool.name for tool in tools]}")
|
||||
return tools
|
||||
|
||||
async def llm_call(self, state: State, config: RunnableConfig = None) -> dict[str, Any]:
|
||||
async def llm_call(self, state: State, runtime: Runtime[Context] = None) -> dict[str, Any]:
|
||||
"""调用 llm 模型 - 异步版本以支持异步工具"""
|
||||
conf = self.config_schema.from_runnable_config(config, module_name=self.module_name)
|
||||
system_prompt = f"{runtime.context.system_prompt}. Current time is {get_cur_time_with_utc()}"
|
||||
model = load_chat_model(runtime.context.model)
|
||||
|
||||
system_prompt = f"{conf.system_prompt} Now is {get_cur_time_with_utc()}"
|
||||
model = load_chat_model(conf.model)
|
||||
|
||||
if tools := self._get_tools(conf.tools):
|
||||
# 这里要根据配置动态获取工具
|
||||
if tools := self._get_tools(runtime.context.tools):
|
||||
model = model.bind_tools(tools)
|
||||
|
||||
# 使用异步调用
|
||||
res = await model.ainvoke(
|
||||
[{"role": "system", "content": system_prompt}, *state["messages"]]
|
||||
response = cast(
|
||||
AIMessage,
|
||||
await model.ainvoke(
|
||||
[{"role": "system", "content": system_prompt}, *state.messages]
|
||||
),
|
||||
)
|
||||
return {"messages": [res]}
|
||||
return {"messages": [response]}
|
||||
|
||||
async def get_graph(self, config_schema: RunnableConfig = None, **kwargs):
|
||||
|
||||
async def dynamic_tools_node(
|
||||
self, state: State, runtime: Runtime[Context]
|
||||
) -> dict[str, list[ToolMessage]]:
|
||||
"""Execute tools dynamically based on configuration.
|
||||
|
||||
This function gets the available tools based on the current configuration
|
||||
and executes the requested tool calls from the last message.
|
||||
"""
|
||||
# Get available tools based on configuration
|
||||
available_tools = get_tools()
|
||||
|
||||
# Create a ToolNode with the available tools
|
||||
tool_node = ToolNode(available_tools)
|
||||
|
||||
# Execute the tool node
|
||||
result = await tool_node.ainvoke(state)
|
||||
|
||||
return cast(dict[str, list[ToolMessage]], result)
|
||||
|
||||
async def get_graph(self, **kwargs):
|
||||
"""构建图"""
|
||||
if self.graph:
|
||||
return self.graph
|
||||
|
||||
runnable_tools = get_buildin_tools()
|
||||
runnable_tools = get_tools()
|
||||
logger.debug(f"build graph `{self.id}` with {len(runnable_tools)} tools")
|
||||
|
||||
workflow = StateGraph(State, config_schema=self.config_schema)
|
||||
workflow.add_node("chatbot", self.llm_call)
|
||||
workflow.add_node("tools", ToolNode(tools=runnable_tools))
|
||||
workflow.add_edge(START, "chatbot")
|
||||
workflow.add_conditional_edges(
|
||||
builder = StateGraph(State, context_schema=self.context_schema)
|
||||
builder.add_node("chatbot", self.llm_call)
|
||||
builder.add_node("tools", self.dynamic_tools_node)
|
||||
builder.add_edge(START, "chatbot")
|
||||
builder.add_conditional_edges(
|
||||
"chatbot",
|
||||
tools_condition,
|
||||
)
|
||||
workflow.add_edge("tools", "chatbot")
|
||||
workflow.add_edge("chatbot", END)
|
||||
builder.add_edge("tools", "chatbot")
|
||||
builder.add_edge("chatbot", END)
|
||||
|
||||
# 创建数据库连接并确保设置 checkpointer
|
||||
try:
|
||||
sqlite_checkpointer = AsyncSqliteSaver(await self.get_async_conn())
|
||||
graph = workflow.compile(checkpointer=sqlite_checkpointer)
|
||||
graph = builder.compile(checkpointer=sqlite_checkpointer, name=self.name)
|
||||
self.graph = graph
|
||||
return graph
|
||||
except Exception as e:
|
||||
logger.error(f"构建 Graph 设置 checkpointer 时出错: {e}")
|
||||
logger.error(f"构建 Graph 设置 checkpointer 时出错: {e}, 尝试使用内存存储")
|
||||
# 即使出错也返回一个可用的图实例,只是无法保存历史
|
||||
graph = workflow.compile()
|
||||
checkpointer = InMemorySaver()
|
||||
graph = builder.compile(checkpointer=checkpointer, name=self.name)
|
||||
self.graph = graph
|
||||
return graph
|
||||
|
||||
@ -101,7 +131,7 @@ class ChatbotAgent(BaseAgent):
|
||||
return AsyncSqliteSaver(await self.get_async_conn())
|
||||
|
||||
def main():
|
||||
agent = ChatbotAgent(ChatbotConfiguration())
|
||||
agent = ChatbotAgent(Context)
|
||||
|
||||
thread_id = str(uuid.uuid4())
|
||||
config = {"configurable": {"thread_id": thread_id}}
|
||||
|
||||
22
src/agents/chatbot/state.py
Normal file
22
src/agents/chatbot/state.py
Normal file
@ -0,0 +1,22 @@
|
||||
"""Define the state structures for the agent."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from collections.abc import Sequence
|
||||
|
||||
from langchain_core.messages import AnyMessage
|
||||
from langgraph.graph import add_messages
|
||||
from typing import Annotated
|
||||
|
||||
|
||||
@dataclass
|
||||
class State:
|
||||
"""Defines the input state for the agent, representing a narrower interface to the outside world.
|
||||
|
||||
This class is used to define the initial state and structure of incoming data.
|
||||
"""
|
||||
|
||||
messages: Annotated[Sequence[AnyMessage], add_messages] = field(
|
||||
default_factory=list
|
||||
)
|
||||
34
src/agents/chatbot/tools.py
Normal file
34
src/agents/chatbot/tools.py
Normal file
@ -0,0 +1,34 @@
|
||||
from typing import Any
|
||||
|
||||
from langchain_core.tools import tool
|
||||
|
||||
from src.utils import logger
|
||||
from src.agents.common.tools import get_buildin_tools
|
||||
|
||||
|
||||
@tool
|
||||
def calculator(a: float, b: float, operation: str) -> float:
|
||||
"""Calculate two numbers. operation: add, subtract, multiply, divide"""
|
||||
try:
|
||||
if operation == "add":
|
||||
return a + b
|
||||
elif operation == "subtract":
|
||||
return a - b
|
||||
elif operation == "multiply":
|
||||
return a * b
|
||||
elif operation == "divide":
|
||||
if b == 0:
|
||||
raise ZeroDivisionError("除数不能为零")
|
||||
return a / b
|
||||
else:
|
||||
raise ValueError(f"不支持的运算类型: {operation},仅支持 add, subtract, multiply, divide")
|
||||
except Exception as e:
|
||||
logger.error(f"Calculator error: {e}")
|
||||
raise
|
||||
|
||||
def get_tools() -> dict[str, Any]:
|
||||
"""获取所有可运行的工具(给大模型使用)"""
|
||||
tools = get_buildin_tools()
|
||||
tools.append(calculator)
|
||||
|
||||
return tools
|
||||
102
src/agents/common/base.py
Normal file
102
src/agents/common/base.py
Normal file
@ -0,0 +1,102 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import abstractmethod
|
||||
from langgraph.graph.state import CompiledStateGraph
|
||||
|
||||
from src.utils import logger
|
||||
from src.agents.common.context import BaseContext
|
||||
|
||||
|
||||
class BaseAgent:
|
||||
|
||||
"""
|
||||
定义一个基础 Agent 供 各类 graph 继承
|
||||
"""
|
||||
|
||||
name = "base_agent"
|
||||
description = "base_agent"
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.graph = None # will be covered by get_graph
|
||||
self.context_schema = BaseContext
|
||||
|
||||
@property
|
||||
def module_name(self) -> str:
|
||||
"""Get the module name of the agent class."""
|
||||
return self.__class__.__module__.split('.')[-2]
|
||||
|
||||
@property
|
||||
def id(self) -> str:
|
||||
"""Get the agent's class name."""
|
||||
return self.__class__.__name__
|
||||
|
||||
async def get_info(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name if hasattr(self, "name") else "Unknown",
|
||||
"description": self.description if hasattr(self, "description") else "Unknown",
|
||||
"configurable_items": self.context_schema.get_configurable_items(),
|
||||
"all_tools": self.all_tools if hasattr(self, "all_tools") else [],
|
||||
"has_checkpointer": await self.check_checkpointer(),
|
||||
}
|
||||
|
||||
async def get_config(self):
|
||||
return self.context_schema.from_file(module_name=self.module_name)
|
||||
|
||||
async def stream_values(self, messages: list[str], input_context = None, **kwargs):
|
||||
graph = await self.get_graph()
|
||||
context = self.context_schema.from_file(module_name=self.module_name, input_context=input_context)
|
||||
for event in graph.astream({"messages": messages}, stream_mode="values", context=context):
|
||||
yield event["messages"]
|
||||
|
||||
async def stream_messages(self, messages: list[str], input_context = None, **kwargs):
|
||||
graph = await self.get_graph()
|
||||
logger.debug(f"stream_messages: {input_context}")
|
||||
|
||||
context = self.context_schema.from_file(module_name=self.module_name, input_context=input_context)
|
||||
# TODO 的 Checkpointer 似乎还没有适配最新的 Context API
|
||||
async for msg, metadata in graph.astream({"messages": messages}, stream_mode="messages", context=context, config={"configurable": input_context}):
|
||||
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]:
|
||||
"""获取历史消息"""
|
||||
try:
|
||||
app = await self.get_graph()
|
||||
|
||||
if not await self.check_checkpointer():
|
||||
return []
|
||||
|
||||
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
|
||||
async def get_graph(self, **kwargs) -> CompiledStateGraph:
|
||||
"""
|
||||
获取并编译对话图实例。
|
||||
必须确保在编译时设置 checkpointer,否则将无法获取历史记录。
|
||||
例如: graph = workflow.compile(checkpointer=sqlite_checkpointer)
|
||||
"""
|
||||
pass
|
||||
161
src/agents/common/context.py
Normal file
161
src/agents/common/context.py
Normal file
@ -0,0 +1,161 @@
|
||||
"""Define the configurable parameters for the agent."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import yaml
|
||||
import uuid
|
||||
from dataclasses import dataclass, field, fields, MISSING
|
||||
from pathlib import Path
|
||||
from typing import get_origin, get_args
|
||||
|
||||
from src import config as sys_config
|
||||
from src.utils import logger
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class BaseContext:
|
||||
"""
|
||||
定义一个基础 Context 供 各类 graph 继承
|
||||
|
||||
配置优先级:
|
||||
1. 运行时配置(RunnableConfig):最高优先级,直接从函数参数传入
|
||||
2. 文件配置(config.private.yaml):中等优先级,从文件加载
|
||||
3. 类默认配置:最低优先级,类中定义的默认值
|
||||
"""
|
||||
|
||||
def update(self, data: dict):
|
||||
"""更新配置字段"""
|
||||
for key, value in data.items():
|
||||
if hasattr(self, key):
|
||||
setattr(self, key, value)
|
||||
|
||||
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={
|
||||
"name": "系统提示词",
|
||||
"description": "用来描述智能体的角色和行为"
|
||||
},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, module_name: str, input_context: dict = None) -> BaseContext:
|
||||
"""Load configuration from a YAML file. 用于持久化配置"""
|
||||
|
||||
# 从文件加载配置
|
||||
context = cls()
|
||||
config_file_path = Path(sys_config.save_dir) / "agents" / module_name / "config.yaml"
|
||||
if module_name is not None and os.path.exists(config_file_path):
|
||||
file_config = {}
|
||||
try:
|
||||
with open(config_file_path, encoding='utf-8') as f:
|
||||
file_config = yaml.safe_load(f) or {}
|
||||
except Exception as e:
|
||||
logger.error(f"加载智能体配置文件出错: {e}")
|
||||
|
||||
context.update(file_config)
|
||||
|
||||
if input_context:
|
||||
context.update(input_context)
|
||||
|
||||
return context
|
||||
|
||||
@classmethod
|
||||
def save_to_file(cls, config: dict, module_name: str) -> bool:
|
||||
"""Save configuration to a YAML file 用于持久化配置"""
|
||||
|
||||
configurable_items = cls.get_configurable_items()
|
||||
configurable_config = {}
|
||||
for k, v in config.items():
|
||||
if k in configurable_items:
|
||||
configurable_config[k] = v
|
||||
|
||||
try:
|
||||
config_file_path = Path(sys_config.save_dir) / "agents" / module_name / "config.yaml"
|
||||
# 确保目录存在
|
||||
os.makedirs(os.path.dirname(config_file_path), exist_ok=True)
|
||||
with open(config_file_path, 'w', encoding='utf-8') as f:
|
||||
yaml.dump(configurable_config, f, indent=2, allow_unicode=True)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"保存智能体配置文件出错: {e}")
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def get_configurable_items(cls):
|
||||
"""实现一个可配置的参数列表,在 UI 上配置时使用"""
|
||||
configurable_items = {}
|
||||
for f in fields(cls):
|
||||
if f.init and not f.metadata.get("hide", False):
|
||||
if f.metadata.get("configurable", True):
|
||||
# 处理类型信息
|
||||
field_type = f.type
|
||||
type_name = cls._get_type_name(field_type)
|
||||
|
||||
# 提取 Annotated 的元数据
|
||||
template_metadata = cls._extract_template_metadata(field_type)
|
||||
|
||||
configurable_items[f.name] = {
|
||||
"type": type_name,
|
||||
"name": f.metadata.get("name", f.name),
|
||||
"options": f.metadata.get("options", []),
|
||||
"default": f.default if f.default is not MISSING else f.default_factory() if f.default_factory is not MISSING else None,
|
||||
"description": f.metadata.get("description", ""),
|
||||
"template_metadata": template_metadata, # Annotated 的额外元数据
|
||||
}
|
||||
|
||||
return configurable_items
|
||||
|
||||
@classmethod
|
||||
def _get_type_name(cls, field_type) -> str:
|
||||
"""获取类型名称,处理 Annotated 类型"""
|
||||
# 检查是否是 Annotated 类型
|
||||
if get_origin(field_type) is not None:
|
||||
# 处理泛型类型如 list[str], Annotated[str, {...}]
|
||||
origin = get_origin(field_type)
|
||||
if hasattr(origin, '__name__'):
|
||||
if origin.__name__ == 'Annotated':
|
||||
# Annotated 类型,获取真实类型
|
||||
args = get_args(field_type)
|
||||
if args:
|
||||
return cls._get_type_name(args[0]) # 递归处理真实类型
|
||||
return origin.__name__
|
||||
else:
|
||||
return str(origin)
|
||||
elif hasattr(field_type, '__name__'):
|
||||
return field_type.__name__
|
||||
else:
|
||||
return str(field_type)
|
||||
|
||||
@classmethod
|
||||
def _extract_template_metadata(cls, field_type) -> dict:
|
||||
"""从 Annotated 类型中提取模板元数据"""
|
||||
if get_origin(field_type) is not None:
|
||||
origin = get_origin(field_type)
|
||||
if hasattr(origin, '__name__') and origin.__name__ == 'Annotated':
|
||||
args = get_args(field_type)
|
||||
if len(args) > 1:
|
||||
# 查找包含 __template_metadata__ 的字典
|
||||
for metadata in args[1:]:
|
||||
if isinstance(metadata, dict) and "__template_metadata__" in metadata:
|
||||
return metadata["__template_metadata__"]
|
||||
return {}
|
||||
61
src/agents/common/models.py
Normal file
61
src/agents/common/models.py
Normal file
@ -0,0 +1,61 @@
|
||||
import os
|
||||
import traceback
|
||||
|
||||
from src import config
|
||||
from src.utils import get_docker_safe_url
|
||||
from src.models import get_custom_model
|
||||
from langchain_core.language_models import BaseChatModel
|
||||
from pydantic import SecretStr
|
||||
|
||||
|
||||
|
||||
|
||||
def load_chat_model(fully_specified_name: str, **kwargs) -> BaseChatModel:
|
||||
"""
|
||||
Load a chat model from a fully specified name.
|
||||
"""
|
||||
provider, model = fully_specified_name.split("/", maxsplit=1)
|
||||
|
||||
if provider == "custom":
|
||||
from langchain_openai import ChatOpenAI
|
||||
model_info = get_custom_model(model)
|
||||
api_key = model_info.get("api_key") or "custom_model"
|
||||
base_url = get_docker_safe_url(model_info["api_base"])
|
||||
model_name = model_info.get("name") or "custom_model"
|
||||
return ChatOpenAI(
|
||||
model=model_name,
|
||||
api_key=SecretStr(api_key),
|
||||
base_url=base_url,
|
||||
)
|
||||
|
||||
model_info = config.model_names.get(provider, {})
|
||||
api_key = os.getenv(model_info["env"][0], model_info["env"][0])
|
||||
base_url = get_docker_safe_url(model_info["base_url"])
|
||||
|
||||
if provider in ["deepseek", "dashscope"]:
|
||||
from langchain_deepseek import ChatDeepSeek
|
||||
return ChatDeepSeek(
|
||||
model=model,
|
||||
api_key=SecretStr(api_key),
|
||||
base_url=base_url,
|
||||
api_base=base_url,
|
||||
)
|
||||
|
||||
elif provider == "together":
|
||||
from langchain_together import ChatTogether
|
||||
return ChatTogether(
|
||||
model=model,
|
||||
api_key=SecretStr(api_key),
|
||||
base_url=base_url,
|
||||
)
|
||||
|
||||
else:
|
||||
try: # 其他模型,默认使用OpenAIBase, like openai, zhipuai
|
||||
from langchain_openai import ChatOpenAI
|
||||
return ChatOpenAI(
|
||||
model=model,
|
||||
api_key=SecretStr(api_key),
|
||||
base_url=base_url,
|
||||
)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Model provider {provider} load failed, {e} \n {traceback.format_exc()}")
|
||||
@ -10,6 +10,31 @@ from src import config, graph_base, knowledge_base
|
||||
from src.utils import logger
|
||||
|
||||
|
||||
@tool
|
||||
def query_knowledge_graph(query: Annotated[str, "The keyword to query knowledge graph."]) -> Any:
|
||||
"""Use this to query knowledge graph, which include some food domain knowledge."""
|
||||
try:
|
||||
logger.debug(f"Querying knowledge graph with: {query}")
|
||||
result = graph_base.query_node(query, hops=2, return_format='triples')
|
||||
logger.debug(f"Knowledge graph query returned {len(result.get('triples', [])) if isinstance(result, dict) else 'N/A'} triples")
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Knowledge graph query error: {e}, {traceback.format_exc()}")
|
||||
return f"知识图谱查询失败: {str(e)}"
|
||||
|
||||
def get_static_tools() -> dict[str, Any]:
|
||||
"""注册静态工具"""
|
||||
static_tools = [
|
||||
query_knowledge_graph,
|
||||
]
|
||||
|
||||
# 检查是否启用网页搜索
|
||||
if config.enable_web_search:
|
||||
static_tools.append(TavilySearch(max_results=10))
|
||||
|
||||
return static_tools
|
||||
|
||||
|
||||
class KnowledgeRetrieverModel(BaseModel):
|
||||
query_text: str = Field(
|
||||
description=(
|
||||
@ -19,46 +44,33 @@ class KnowledgeRetrieverModel(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
def _create_retriever_wrapper(db_id: str, retriever_info: dict[str, Any]):
|
||||
"""创建检索器包装函数的工厂函数,避免闭包变量捕获问题"""
|
||||
async def async_retriever_wrapper(query_text: str) -> Any:
|
||||
"""异步检索器包装函数"""
|
||||
retriever = retriever_info["retriever"]
|
||||
try:
|
||||
logger.debug(f"Retrieving from database {db_id} with query: {query_text}")
|
||||
if asyncio.iscoroutinefunction(retriever):
|
||||
result = await retriever(query_text)
|
||||
else:
|
||||
result = retriever(query_text)
|
||||
logger.debug(f"Retrieved {len(result) if isinstance(result, list) else 'N/A'} results from {db_id}")
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Error in retriever {db_id}: {e}")
|
||||
return f"检索失败: {str(e)}"
|
||||
|
||||
return async_retriever_wrapper
|
||||
|
||||
def get_buildin_tools() -> dict[str, Any]:
|
||||
"""获取所有可运行的工具(给大模型使用)"""
|
||||
tools = []
|
||||
|
||||
try:
|
||||
# 获取所有知识库基于的工具
|
||||
tools.extend(get_kb_based_tools())
|
||||
tools.extend(get_static_tools())
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get knowledge base retrievers: {e}")
|
||||
|
||||
logger.info(f"Total tools available: {len(tools)}")
|
||||
return tools
|
||||
|
||||
def get_kb_based_tools() -> dict[str, Any]:
|
||||
"""获取所有知识库基于的工具"""
|
||||
# 获取所有知识库
|
||||
kb_tools = []
|
||||
retrievers = knowledge_base.get_retrievers()
|
||||
logger.debug(f"Found {len(retrievers)} knowledge base retrievers")
|
||||
|
||||
def _create_retriever_wrapper(db_id: str, retriever_info: dict[str, Any]):
|
||||
"""创建检索器包装函数的工厂函数,避免闭包变量捕获问题"""
|
||||
async def async_retriever_wrapper(query_text: str) -> Any:
|
||||
"""异步检索器包装函数"""
|
||||
retriever = retriever_info["retriever"]
|
||||
try:
|
||||
logger.debug(f"Retrieving from database {db_id} with query: {query_text}")
|
||||
if asyncio.iscoroutinefunction(retriever):
|
||||
result = await retriever(query_text)
|
||||
else:
|
||||
result = retriever(query_text)
|
||||
logger.debug(f"Retrieved {len(result) if isinstance(result, list) else 'N/A'} results from {db_id}")
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Error in retriever {db_id}: {e}")
|
||||
return f"检索失败: {str(e)}"
|
||||
|
||||
return async_retriever_wrapper
|
||||
|
||||
|
||||
for db_id, retrieve_info in retrievers.items():
|
||||
try:
|
||||
@ -94,14 +106,28 @@ def get_kb_based_tools() -> dict[str, Any]:
|
||||
|
||||
return kb_tools
|
||||
|
||||
def get_buildin_tools_info() -> dict[str, dict[str, Any]]:
|
||||
|
||||
def get_buildin_tools() -> dict[str, Any]:
|
||||
"""获取所有可运行的工具(给大模型使用)"""
|
||||
tools = []
|
||||
|
||||
try:
|
||||
# 获取所有知识库基于的工具
|
||||
tools.extend(get_kb_based_tools())
|
||||
tools.extend(get_static_tools())
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get knowledge base retrievers: {e}")
|
||||
|
||||
logger.info(f"Total tools available: {len(tools)}")
|
||||
return tools
|
||||
|
||||
|
||||
def gen_tool_info(tools) -> dict[str, dict[str, Any]]:
|
||||
"""获取所有工具的信息(用于前端展示)"""
|
||||
tools_info = []
|
||||
|
||||
try:
|
||||
tools = get_buildin_tools()
|
||||
logger.debug(f"Processing {len(tools)} tools for info extraction")
|
||||
|
||||
# 获取注册的工具信息
|
||||
for tool_obj in tools:
|
||||
try:
|
||||
@ -123,7 +149,7 @@ def get_buildin_tools_info() -> dict[str, dict[str, Any]]:
|
||||
})
|
||||
|
||||
tools_info.append(info)
|
||||
logger.debug(f"Successfully processed tool info for {tool_obj.name}")
|
||||
# logger.debug(f"Successfully processed tool info for {tool_obj.name}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to process tool {tool_obj.name}: {e}")
|
||||
@ -136,50 +162,4 @@ def get_buildin_tools_info() -> dict[str, dict[str, Any]]:
|
||||
logger.info(f"Successfully extracted info for {len(tools_info)} tools")
|
||||
return tools_info
|
||||
|
||||
@tool
|
||||
def calculator(a: float, b: float, operation: str) -> float:
|
||||
"""Calculate two numbers. operation: add, subtract, multiply, divide"""
|
||||
try:
|
||||
if operation == "add":
|
||||
return a + b
|
||||
elif operation == "subtract":
|
||||
return a - b
|
||||
elif operation == "multiply":
|
||||
return a * b
|
||||
elif operation == "divide":
|
||||
if b == 0:
|
||||
raise ZeroDivisionError("除数不能为零")
|
||||
return a / b
|
||||
else:
|
||||
raise ValueError(f"不支持的运算类型: {operation},仅支持 add, subtract, multiply, divide")
|
||||
except Exception as e:
|
||||
logger.error(f"Calculator error: {e}")
|
||||
raise
|
||||
|
||||
|
||||
@tool
|
||||
def query_knowledge_graph(query: Annotated[str, "The keyword to query knowledge graph."]) -> Any:
|
||||
"""Use this to query knowledge graph, which include some food domain knowledge."""
|
||||
try:
|
||||
logger.debug(f"Querying knowledge graph with: {query}")
|
||||
result = graph_base.query_node(query, hops=2, return_format='triples')
|
||||
logger.debug(f"Knowledge graph query returned {len(result.get('triples', [])) if isinstance(result, dict) else 'N/A'} triples")
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Knowledge graph query error: {e}, {traceback.format_exc()}")
|
||||
return f"知识图谱查询失败: {str(e)}"
|
||||
|
||||
def get_static_tools() -> dict[str, Any]:
|
||||
"""注册静态工具"""
|
||||
static_tools = [
|
||||
calculator,
|
||||
query_knowledge_graph,
|
||||
]
|
||||
|
||||
# 检查是否启用网页搜索
|
||||
if config.enable_web_search:
|
||||
static_tools.append(TavilySearch(max_results=10))
|
||||
|
||||
return static_tools
|
||||
|
||||
|
||||
@ -4,9 +4,9 @@ import os
|
||||
import traceback
|
||||
|
||||
from src import config
|
||||
from src.utils import logger, get_docker_safe_url
|
||||
from src.utils import get_docker_safe_url
|
||||
from src.models import get_custom_model
|
||||
from src.agents.registry import BaseAgent
|
||||
from src.agents.common.base import BaseAgent
|
||||
from langchain_core.language_models import BaseChatModel
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langchain_core.messages import AIMessageChunk, ToolMessage
|
||||
@ -1,9 +1,49 @@
|
||||
from src.agents.registry import BaseAgent
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver, aiosqlite
|
||||
from langgraph.prebuilt import create_react_agent
|
||||
from langchain_core.messages import AnyMessage, SystemMessage
|
||||
from langgraph.runtime import get_runtime
|
||||
|
||||
from src import config as sys_config
|
||||
from src.utils import logger
|
||||
from src.agents.common.context import BaseContext
|
||||
from src.agents.common.base import BaseAgent
|
||||
from src.agents.common.models import load_chat_model
|
||||
from src.agents.common.tools import get_buildin_tools
|
||||
|
||||
|
||||
model = load_chat_model("siliconflow/Qwen/Qwen3-235B-A22B-Instruct-2507")
|
||||
|
||||
def prompt(state) -> list[AnyMessage]:
|
||||
runtime = get_runtime(BaseContext)
|
||||
system_msg = SystemMessage(content=runtime.context.system_prompt)
|
||||
return [system_msg] + state["messages"]
|
||||
|
||||
class ReActAgent(BaseAgent):
|
||||
name = "ReAct"
|
||||
name = "ReAct (all tools)"
|
||||
description = "A react agent that can answer questions and help with tasks."
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.graph = None
|
||||
self.workdir = Path(sys_config.save_dir) / "agents" / self.module_name
|
||||
self.workdir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
async def get_graph(self, **kwargs):
|
||||
from .workflows import graph
|
||||
if self.graph:
|
||||
return self.graph
|
||||
|
||||
available_tools = get_buildin_tools()
|
||||
|
||||
sqlite_checkpointer = AsyncSqliteSaver(await aiosqlite.connect(self.workdir / "react_history.db"))
|
||||
graph = create_react_agent(
|
||||
model,
|
||||
tools=available_tools,
|
||||
checkpointer=sqlite_checkpointer,
|
||||
prompt=prompt
|
||||
)
|
||||
self.graph = graph
|
||||
logger.info("ReActAgent使用SQLite checkpointer构建成功")
|
||||
return graph
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
import os
|
||||
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
from langgraph.prebuilt import create_react_agent
|
||||
|
||||
model = ChatOpenAI(model="glm-4-plus",
|
||||
api_key=os.getenv("ZHIPUAI_API_KEY"),
|
||||
base_url="https://open.bigmodel.cn/api/paas/v4/",
|
||||
temperature=0)
|
||||
|
||||
tools = []
|
||||
graph = create_react_agent(model, tools=tools, checkpointer=InMemorySaver())
|
||||
@ -1,249 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import yaml
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Annotated, TypedDict, Optional, Any
|
||||
from abc import abstractmethod
|
||||
from dataclasses import dataclass, fields, field
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langchain_core.messages import BaseMessage
|
||||
from langgraph.graph.state import CompiledStateGraph
|
||||
from langgraph.graph.message import add_messages
|
||||
|
||||
from src.utils import logger
|
||||
|
||||
class State(TypedDict):
|
||||
messages: Annotated[list[BaseMessage], add_messages]
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Configuration(dict):
|
||||
"""
|
||||
定义一个基础 Configuration 供 各类 graph 继承
|
||||
|
||||
配置优先级:
|
||||
1. 运行时配置(RunnableConfig):最高优先级,直接从函数参数传入
|
||||
2. 文件配置(config.private.yaml):中等优先级,从文件加载
|
||||
3. 类默认配置:最低优先级,类中定义的默认值
|
||||
"""
|
||||
|
||||
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": "用来描述智能体的角色和行为"
|
||||
},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_runnable_config(
|
||||
cls, config: RunnableConfig | None = None, module_name: str | None = None
|
||||
) -> Configuration:
|
||||
"""Create a Configuration instance from a RunnableConfig object.
|
||||
|
||||
Args:
|
||||
config: RunnableConfig object with highest priority
|
||||
module_name: Name of the agent to load config file for
|
||||
|
||||
Returns:
|
||||
Configuration instance with merged config values
|
||||
"""
|
||||
# 获取类默认配置:创建一个实例获取所有默认值
|
||||
instance = cls()
|
||||
_fields = {f.name for f in fields(cls) if f.init}
|
||||
|
||||
# 尝试加载文件配置(中等优先级)
|
||||
file_config = {}
|
||||
if module_name:
|
||||
file_config = cls.from_file(module_name)
|
||||
|
||||
# 获取运行时配置(最高优先级)
|
||||
configurable = (config.get("configurable") or {}) if config else {}
|
||||
|
||||
# 合并三级配置,注意优先级
|
||||
merged_config = {}
|
||||
for config_field in _fields:
|
||||
# 1. 默认使用类默认值
|
||||
if hasattr(instance, config_field):
|
||||
merged_config[config_field] = getattr(instance, config_field)
|
||||
|
||||
# 2. 如果文件配置中有此字段,则覆盖
|
||||
if config_field in file_config:
|
||||
merged_config[config_field] = file_config[config_field]
|
||||
|
||||
# 3. 如果运行时配置中有此字段,则覆盖
|
||||
if config_field in configurable:
|
||||
merged_config[config_field] = configurable[config_field]
|
||||
|
||||
# 创建并返回配置实例
|
||||
# logger.debug(f"合并配置: {merged_config}")
|
||||
return cls(**merged_config)
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, module_name: str) -> Configuration:
|
||||
"""从文件加载配置"""
|
||||
config_file_path = Path(f"src/agents/{module_name}/config.private.yaml")
|
||||
file_config = {}
|
||||
if os.path.exists(config_file_path):
|
||||
try:
|
||||
with open(config_file_path, encoding='utf-8') as f:
|
||||
file_config = yaml.safe_load(f) or {}
|
||||
# logger.info(f"从文件加载智能体 {module_name} 配置: {file_config}")
|
||||
except Exception as e:
|
||||
logger.error(f"加载智能体配置文件出错: {e}")
|
||||
|
||||
return file_config
|
||||
|
||||
@classmethod
|
||||
def save_to_file(cls, config: dict, module_name: str) -> bool:
|
||||
"""Save configuration to a YAML file
|
||||
|
||||
Args:
|
||||
config: Configuration dictionary to save
|
||||
module_name: Name of the agent to save config for
|
||||
|
||||
Returns:
|
||||
True if saving was successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
config_file_path = Path(f"src/agents/{module_name}/config.private.yaml")
|
||||
# 确保目录存在
|
||||
os.makedirs(os.path.dirname(config_file_path), exist_ok=True)
|
||||
with open(config_file_path, 'w', encoding='utf-8') as f:
|
||||
yaml.dump(config, f, indent=2, allow_unicode=True)
|
||||
|
||||
# logger.info(f"智能体 {module_name} 配置已保存到 {config_file_path}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"保存智能体配置文件出错: {e}")
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def to_dict(cls):
|
||||
# 创建一个实例来处理 default_factory
|
||||
instance = cls()
|
||||
confs = {}
|
||||
configurable_items = {}
|
||||
for f in fields(cls):
|
||||
if f.init and not f.metadata.get("hide", False):
|
||||
value = getattr(instance, f.name)
|
||||
if callable(value) and hasattr(value, "__call__"):
|
||||
confs[f.name] = value()
|
||||
else:
|
||||
confs[f.name] = value
|
||||
|
||||
if f.metadata.get("configurable", True):
|
||||
configurable_items[f.name] = {
|
||||
"type": f.type.__name__,
|
||||
"name": f.metadata.get("name", f.name),
|
||||
"options": f.metadata.get("options", []),
|
||||
"default": f.default,
|
||||
"description": f.metadata.get("description", ""),
|
||||
}
|
||||
confs["configurable_items"] = configurable_items
|
||||
return confs
|
||||
|
||||
class BaseAgent:
|
||||
|
||||
"""
|
||||
定义一个基础 Agent 供 各类 graph 继承
|
||||
"""
|
||||
|
||||
name = "base_agent"
|
||||
description = "base_agent"
|
||||
config_schema: Configuration = Configuration
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
pass
|
||||
|
||||
@property
|
||||
def module_name(self) -> str:
|
||||
"""Get the module name of the agent class."""
|
||||
return self.__class__.__module__.split('.')[-2]
|
||||
|
||||
@property
|
||||
def id(self) -> str:
|
||||
"""Get the agent's class name."""
|
||||
return self.__class__.__name__
|
||||
|
||||
async def get_info(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"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(),
|
||||
"all_tools": self.all_tools if hasattr(self, "all_tools") else [],
|
||||
"has_checkpointer": await self.check_checkpointer(),
|
||||
}
|
||||
|
||||
|
||||
|
||||
async def stream_values(self, messages: list[str], config_schema: RunnableConfig = None, **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()
|
||||
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]:
|
||||
"""获取历史消息"""
|
||||
try:
|
||||
app = await self.get_graph()
|
||||
|
||||
if not await self.check_checkpointer():
|
||||
return []
|
||||
|
||||
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
|
||||
async def get_graph(self, **kwargs) -> CompiledStateGraph:
|
||||
"""
|
||||
获取并编译对话图实例。
|
||||
必须确保在编译时设置 checkpointer,否则将无法获取历史记录。
|
||||
例如: graph = workflow.compile(checkpointer=sqlite_checkpointer)
|
||||
"""
|
||||
pass
|
||||
@ -28,7 +28,7 @@ class KnowledgeBaseFactory:
|
||||
|
||||
cls._kb_types[kb_type] = kb_class
|
||||
cls._default_configs[kb_type] = default_config or {}
|
||||
logger.info(f"Registered knowledge base type: {kb_type}")
|
||||
# logger.info(f"Registered knowledge base type: {kb_type}")
|
||||
|
||||
@classmethod
|
||||
def create(cls, kb_type: str, work_dir: str, **kwargs) -> KnowledgeBase:
|
||||
|
||||
@ -112,7 +112,7 @@ export const agentApi = {
|
||||
* 获取所有可用工具的信息
|
||||
* @returns {Promise} - 工具信息列表
|
||||
*/
|
||||
getTools: () => apiGet('/api/tool/tools')
|
||||
getTools: (agentId) => apiGet(`/api/chat/tools?agent_id=${agentId}`)
|
||||
}
|
||||
|
||||
|
||||
@ -127,7 +127,7 @@ export const threadApi = {
|
||||
* @returns {Promise} - 对话线程列表
|
||||
*/
|
||||
getThreads: (agentId) => {
|
||||
const url = agentId ? `/api/chat/threads?agent_id=${agentId}` : '/api/chat/threads';
|
||||
const url = `/api/chat/threads?agent_id=${agentId}`;
|
||||
return apiGet(url);
|
||||
},
|
||||
|
||||
@ -7,8 +7,7 @@
|
||||
export * from './system_api' // 系统管理API
|
||||
export * from './knowledge_api' // 知识库管理API
|
||||
export * from './graph_api' // 图谱API
|
||||
export * from './tools.js' // 工具API
|
||||
export * from './agent.js' // 智能体API
|
||||
export * from './agent_api' // 智能体API
|
||||
|
||||
// 导出基础工具函数
|
||||
export { apiGet, apiPost, apiPut, apiDelete,
|
||||
|
||||
@ -68,7 +68,7 @@ export const configApi = {
|
||||
// === 信息管理分组 ===
|
||||
// =============================================================================
|
||||
|
||||
export const brandAPi = {
|
||||
export const brandApi = {
|
||||
/**
|
||||
* 获取系统信息配置(公开接口)
|
||||
* @returns {Promise} - 系统信息配置
|
||||
|
||||
@ -30,7 +30,7 @@
|
||||
|
||||
<a-divider />
|
||||
|
||||
<div v-if="selectedAgentId && configSchema" class="config-form-content">
|
||||
<div v-if="selectedAgentId && configurableItems" class="config-form-content">
|
||||
<!-- 配置表单 -->
|
||||
<a-form :model="agentConfig" layout="vertical" class="config-form">
|
||||
<a-alert
|
||||
@ -57,8 +57,9 @@
|
||||
>
|
||||
<p v-if="value.description" class="config-description">{{ value.description }}</p>
|
||||
|
||||
<!-- <div>{{ value }}</div> -->
|
||||
<!-- 模型选择 -->
|
||||
<div v-if="key === 'model'" class="model-selector">
|
||||
<div v-if="value.template_metadata.kind === 'llm'" class="model-selector">
|
||||
<ModelSelectorComponent
|
||||
@select-model="handleModelChange"
|
||||
:model_name="agentConfig[key] ? agentConfig[key].split('/').slice(1).join('/') : ''"
|
||||
@ -77,7 +78,7 @@
|
||||
/>
|
||||
|
||||
<!-- 工具选择 -->
|
||||
<div v-else-if="key === 'tools'" class="tools-selector">
|
||||
<div v-else-if="value.template_metadata.kind === 'tools'" class="tools-selector">
|
||||
<div class="tools-summary">
|
||||
<div class="tools-summary-info">
|
||||
<span class="tools-count">已选择 {{ getSelectedCount(key) }} 个工具</span>
|
||||
@ -307,7 +308,8 @@ const {
|
||||
availableTools,
|
||||
selectedAgent,
|
||||
selectedAgentId,
|
||||
agentConfig
|
||||
agentConfig,
|
||||
configurableItems
|
||||
} = storeToRefs(agentStore);
|
||||
|
||||
// console.log(availableTools.value)
|
||||
@ -318,21 +320,6 @@ const toolsModalOpen = ref(false);
|
||||
const selectedTools = ref([]);
|
||||
const toolsSearchText = ref('');
|
||||
|
||||
// 计算属性
|
||||
const configSchema = computed(() => selectedAgent.value?.config_schema || {});
|
||||
|
||||
const configurableItems = computed(() => {
|
||||
const items = configSchema.value.configurable_items || {};
|
||||
// 遍历所有的配置项,将所有的 x_oap_ui_config 的层级提升到上一层
|
||||
Object.keys(items).forEach(key => {
|
||||
const item = items[key];
|
||||
if (item.x_oap_ui_config) {
|
||||
items[key] = { ...item, ...item.x_oap_ui_config };
|
||||
delete items[key].x_oap_ui_config;
|
||||
}
|
||||
});
|
||||
return items;
|
||||
});
|
||||
|
||||
const isEmptyConfig = computed(() => {
|
||||
return !selectedAgentId.value || Object.keys(configurableItems.value).length === 0;
|
||||
|
||||
@ -116,7 +116,6 @@ import {
|
||||
} from '@ant-design/icons-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { configApi } from '@/apis/system_api';
|
||||
import { agentApi } from '@/apis/agent';
|
||||
import { checkAdminPermission } from '@/stores/user';
|
||||
|
||||
const configStore = useConfigStore()
|
||||
@ -402,7 +401,6 @@ const printAgentConfig = async () => {
|
||||
console.log('当前选中智能体:', {
|
||||
agent: toRaw(agentStore.selectedAgent),
|
||||
isDefault: agentStore.isDefaultAgent,
|
||||
configSchema: toRaw(agentStore.configSchema),
|
||||
configurableItems: Object.keys(agentStore.configurableItems).length
|
||||
});
|
||||
|
||||
@ -419,7 +417,7 @@ const printAgentConfig = async () => {
|
||||
|
||||
// 线程信息
|
||||
console.log('线程信息:', {
|
||||
currentAgentThreads: agentStore.currentAgentThreads.length,
|
||||
currentAgentThreads: agentStore.currentAgentThreads,
|
||||
currentThread: agentStore.currentThread ? toRaw(agentStore.currentThread) : null,
|
||||
currentThreadMessages: agentStore.currentThreadMessages.length
|
||||
});
|
||||
|
||||
@ -312,11 +312,11 @@ const selectedEdgeData = computed(() => graphStore.selectedEdgeData)
|
||||
// 智能边面板位置 - 确保在紧凑模式下也能正确显示
|
||||
const intelligentEdgePanelPosition = computed(() => {
|
||||
if (!sigmaContainer.value) return edgePanelPosition.value
|
||||
|
||||
|
||||
const containerHeight = sigmaContainer.value.clientHeight
|
||||
const panelHeight = 200 // 估计的面板高度
|
||||
const nodePanelBottom = selectedNodeData.value ? nodePanelPosition.value.y + 200 : 0
|
||||
|
||||
|
||||
// 如果有节点面板显示,将边面板放在节点面板下方
|
||||
if (selectedNodeData.value) {
|
||||
return {
|
||||
@ -324,7 +324,7 @@ const intelligentEdgePanelPosition = computed(() => {
|
||||
y: Math.min(nodePanelPosition.value.y, containerHeight - panelHeight - 20)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 确保边面板在容器范围内
|
||||
return {
|
||||
x: edgePanelPosition.value.x,
|
||||
@ -551,10 +551,10 @@ const registerEvents = () => {
|
||||
// 获取Sigma边的属性,其中包含原始数据
|
||||
const sigmaEdgeData = graph.getEdgeAttributes(edge)
|
||||
console.log('Sigma边属性:', sigmaEdgeData)
|
||||
|
||||
|
||||
// 立即设置选中的边 - 使用Sigma边ID
|
||||
graphStore.setSelectedEdge(edge)
|
||||
|
||||
|
||||
// 确保边面板显示
|
||||
nextTick(() => {
|
||||
if (selectedEdgeData.value) {
|
||||
|
||||
@ -202,7 +202,7 @@ import {
|
||||
} from '@ant-design/icons-vue';
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
import { modelIcons } from '@/utils/modelIcon';
|
||||
import { agentApi } from '@/apis/agent';
|
||||
import { agentApi } from '@/apis/agent_api';
|
||||
|
||||
const configStore = useConfigStore();
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { agentApi, threadApi } from '@/apis/agent';
|
||||
import { agentApi, threadApi } from '@/apis/agent_api';
|
||||
import { MessageProcessor } from '@/utils/messageProcessor';
|
||||
import { handleChatError } from '@/utils/errorHandler';
|
||||
|
||||
@ -46,14 +46,11 @@ export const useAgentStore = defineStore('agent', {
|
||||
defaultAgent: (state) => state.defaultAgentId ? state.agents[state.defaultAgentId] : state.agents[Object.keys(state.agents)[0]],
|
||||
agentsList: (state) => Object.values(state.agents),
|
||||
isDefaultAgent: (state) => state.selectedAgentId === state.defaultAgentId,
|
||||
configSchema: (state) => {
|
||||
const agent = state.selectedAgentId ? state.agents[state.selectedAgentId] : null;
|
||||
return agent?.config_schema || {};
|
||||
},
|
||||
configurableItems: (state) => {
|
||||
const schema = state.configSchema || {};
|
||||
if (!schema || !schema.configurable_items) return {};
|
||||
const items = { ...schema.configurable_items };
|
||||
const agent = state.selectedAgentId ? state.agents[state.selectedAgentId] : null;
|
||||
const agentConfigurableItems = agent.configurable_items || {};
|
||||
if (!agentConfigurableItems) return {};
|
||||
const items = { ...agentConfigurableItems };
|
||||
Object.keys(items).forEach(key => {
|
||||
const item = items[key];
|
||||
if (item && item.x_oap_ui_config) {
|
||||
@ -156,7 +153,7 @@ export const useAgentStore = defineStore('agent', {
|
||||
// 设置默认智能体
|
||||
async setDefaultAgent(agentId) {
|
||||
try {
|
||||
await agentConfigApi.setDefaultAgent(agentId);
|
||||
await agentApi.setDefaultAgent(agentId);
|
||||
this.defaultAgentId = agentId;
|
||||
} catch (error) {
|
||||
console.error('Failed to set default agent:', error);
|
||||
@ -427,7 +424,7 @@ export const useAgentStore = defineStore('agent', {
|
||||
|
||||
try {
|
||||
const response = await agentApi.sendAgentMessage(this.selectedAgentId, requestData);
|
||||
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { brandAPi } from '@/apis/system_api'
|
||||
import { brandApi } from '@/apis/system_api'
|
||||
|
||||
export const useInfoStore = defineStore('info', () => {
|
||||
// 状态
|
||||
@ -49,7 +49,7 @@ export const useInfoStore = defineStore('info', () => {
|
||||
|
||||
try {
|
||||
isLoading.value = true
|
||||
const response = await brandAPi.getInfoConfig()
|
||||
const response = await brandApi.getInfoConfig()
|
||||
|
||||
if (response.success && response.data) {
|
||||
setInfoConfig(response.data)
|
||||
@ -70,7 +70,7 @@ export const useInfoStore = defineStore('info', () => {
|
||||
async function reloadInfoConfig() {
|
||||
try {
|
||||
isLoading.value = true
|
||||
const response = await brandAPi.reloadInfoConfig()
|
||||
const response = await brandApi.reloadInfoConfig()
|
||||
|
||||
if (response.success && response.data) {
|
||||
setInfoConfig(response.data)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user