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">
|
<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" />
|
<img width="3651" height="1933" alt="视频演示缩略图" src="https://github.com/user-attachments/assets/eac4fa89-2176-46ae-a649-45a125cb6ed1" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- 视频链接文字 -->
|
<!-- 视频链接文字 -->
|
||||||
<p style="margin-top: 12px;">
|
<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;">
|
<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` 示例。
|
目前该项目默认集成了三个 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)。例如:
|
注册智能体的方式请参考已有实现:[src/agents/__init__.py](src/agents/__init__.py)。例如:
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
目前已有的开发计划包括:
|
目前已有的开发计划包括:
|
||||||
|
|
||||||
💭 **Features Todo**
|
💭 **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 这个类。
|
- [ ] 使用其他的聊天记录管理方法,解决两个问题,一个是上下文长度过长,一个是上下文的类型变得更加丰富,比如多模态等等。(现在是基于 LangGraph 的 Memory 实现的,v0.2.3 版本实现),暂定使用 [mem0](github.com/mem0ai/mem0) 来实现。但是目前了解下来,还不是我想要的那种方案。可能会基于这个实现一个 ThreadConvManager 这个类。
|
||||||
- [ ] 添加对于上传文件的支持:这里的复杂的地方就在于如何和历史记录结合在一起(v0.2.3 版本实现,放在记忆管理后面)
|
- [ ] 添加对于上传文件的支持:这里的复杂的地方就在于如何和历史记录结合在一起(v0.2.3 版本实现,放在记忆管理后面)
|
||||||
|
|
||||||
@ -9,6 +11,8 @@
|
|||||||
- [x] LlightRAG 知识库中,点击边,没有显示,但是在全屏的时候却又能够显示出来。
|
- [x] LlightRAG 知识库中,点击边,没有显示,但是在全屏的时候却又能够显示出来。
|
||||||
- [ ] 部分 doc 格式的文件支持有问题
|
- [ ] 部分 doc 格式的文件支持有问题
|
||||||
- [ ] 当出现不支持的文件类型的时候,前端没有限制
|
- [ ] 当出现不支持的文件类型的时候,前端没有限制
|
||||||
|
- [ ] 默认智能体设置后,在一些情况下依然仅加载第一个智能体
|
||||||
|
- [ ] 目前只能获取默认的 tools,单个智能体的tools没法展示
|
||||||
|
|
||||||
💯 **More**:
|
💯 **More**:
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,6 @@ from server.routers.auth_router import auth
|
|||||||
from server.routers.chat_router import chat
|
from server.routers.chat_router import chat
|
||||||
from server.routers.knowledge_router import knowledge
|
from server.routers.knowledge_router import knowledge
|
||||||
from server.routers.graph_router import graph
|
from server.routers.graph_router import graph
|
||||||
from server.routers.tool_router import tool
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@ -14,4 +13,3 @@ router.include_router(auth) # /api/auth/*
|
|||||||
router.include_router(chat) # /api/chat/*
|
router.include_router(chat) # /api/chat/*
|
||||||
router.include_router(knowledge) # /api/knowledge/*
|
router.include_router(knowledge) # /api/knowledge/*
|
||||||
router.include_router(graph) # /api/graph/*
|
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.agents import agent_manager
|
||||||
from src.models import select_model
|
from src.models import select_model
|
||||||
from src.utils.logging_config import logger
|
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.routers.auth_router import get_admin_user
|
||||||
from server.utils.auth_middleware import get_required_user, get_db
|
from server.utils.auth_middleware import get_required_user, get_db
|
||||||
from server.models.user_model import User
|
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)):
|
current_user: User = Depends(get_required_user)):
|
||||||
"""使用特定智能体进行对话(需要登录)"""
|
"""使用特定智能体进行对话(需要登录)"""
|
||||||
|
|
||||||
|
logger.info(f"agent_id: {agent_id}, query: {query}, config: {config}, meta: {meta}")
|
||||||
|
|
||||||
meta.update({
|
meta.update({
|
||||||
"query": query,
|
"query": query,
|
||||||
"agent_id": agent_id,
|
"agent_id": agent_id,
|
||||||
@ -134,15 +136,13 @@ async def chat_agent(agent_id: str,
|
|||||||
messages = [{"role": "user", "content": query}]
|
messages = [{"role": "user", "content": query}]
|
||||||
|
|
||||||
# 构造运行时配置,如果没有thread_id则生成一个
|
# 构造运行时配置,如果没有thread_id则生成一个
|
||||||
config["user_id"] = str(current_user.id)
|
user_id = str(current_user.id)
|
||||||
if "thread_id" not in config or not config["thread_id"]:
|
thread_id = config.get("thread_id")
|
||||||
config["thread_id"] = str(uuid.uuid4())
|
|
||||||
logger.debug(f"没有thread_id,生成一个: {config['thread_id']=}")
|
|
||||||
|
|
||||||
runnable_config = {"configurable": {**config}}
|
input_context = {"user_id": user_id, "thread_id": thread_id}
|
||||||
|
|
||||||
try:
|
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}")
|
# logger.debug(f"msg: {msg.model_dump()}, metadata: {metadata}")
|
||||||
if isinstance(msg, AIMessageChunk):
|
if isinstance(msg, AIMessageChunk):
|
||||||
yield make_chunk(content=msg.content,
|
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"]}
|
return {"models": config.model_names[model_provider]["models"]}
|
||||||
|
|
||||||
@chat.get("/tools")
|
@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")
|
@chat.post("/agent/{agent_id}/config")
|
||||||
async def save_agent_config(
|
async def save_agent_config(
|
||||||
@ -192,13 +195,11 @@ async def save_agent_config(
|
|||||||
"""保存智能体配置到YAML文件(需要管理员权限)"""
|
"""保存智能体配置到YAML文件(需要管理员权限)"""
|
||||||
try:
|
try:
|
||||||
# 获取Agent实例和配置类
|
# 获取Agent实例和配置类
|
||||||
agent = agent_manager.get_agent(agent_id)
|
if not (agent := agent_manager.get_agent(agent_id)):
|
||||||
if not agent:
|
|
||||||
raise HTTPException(status_code=404, detail=f"智能体 {agent_id} 不存在")
|
raise HTTPException(status_code=404, detail=f"智能体 {agent_id} 不存在")
|
||||||
|
|
||||||
# 使用配置类的save_to_file方法保存配置
|
# 使用配置类的save_to_file方法保存配置
|
||||||
config_cls = agent.config_schema
|
result = agent.context_schema.save_to_file(config, agent.module_name)
|
||||||
result = config_cls.save_to_file(config, agent.module_name)
|
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
return {"success": True, "message": f"智能体 {agent.name} 配置已保存"}
|
return {"success": True, "message": f"智能体 {agent.name} 配置已保存"}
|
||||||
@ -218,8 +219,7 @@ async def get_agent_history(
|
|||||||
"""获取智能体历史消息(需要登录)"""
|
"""获取智能体历史消息(需要登录)"""
|
||||||
try:
|
try:
|
||||||
# 获取Agent实例和配置类
|
# 获取Agent实例和配置类
|
||||||
agent = agent_manager.get_agent(agent_id)
|
if not (agent := agent_manager.get_agent(agent_id)):
|
||||||
if not agent:
|
|
||||||
raise HTTPException(status_code=404, detail=f"智能体 {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)):
|
if not (agent := agent_manager.get_agent(agent_id)):
|
||||||
raise HTTPException(status_code=404, detail=f"智能体 {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}
|
return {"success": True, "config": config}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -279,6 +280,7 @@ async def create_thread(
|
|||||||
):
|
):
|
||||||
"""创建新对话线程"""
|
"""创建新对话线程"""
|
||||||
thread_id = str(uuid.uuid4())
|
thread_id = str(uuid.uuid4())
|
||||||
|
logger.debug(f"thread.agent_id: {thread.agent_id}")
|
||||||
|
|
||||||
new_thread = Thread(
|
new_thread = Thread(
|
||||||
id=thread_id,
|
id=thread_id,
|
||||||
@ -305,18 +307,19 @@ async def create_thread(
|
|||||||
|
|
||||||
@chat.get("/threads", response_model=list[ThreadResponse])
|
@chat.get("/threads", response_model=list[ThreadResponse])
|
||||||
async def list_threads(
|
async def list_threads(
|
||||||
agent_id: str | None = None,
|
agent_id: str,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(get_required_user)
|
current_user: User = Depends(get_required_user)
|
||||||
):
|
):
|
||||||
"""获取用户的所有对话线程"""
|
"""获取用户的所有对话线程"""
|
||||||
|
assert agent_id, "agent_id 不能为空"
|
||||||
query = db.query(Thread).filter(
|
query = db.query(Thread).filter(
|
||||||
Thread.user_id == str(current_user.id),
|
Thread.user_id == str(current_user.id),
|
||||||
Thread.status == 1
|
Thread.status == 1,
|
||||||
|
Thread.agent_id == agent_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
if agent_id:
|
logger.debug(f"agent_id: {agent_id}")
|
||||||
query = query.filter(Thread.agent_id == agent_id)
|
|
||||||
|
|
||||||
threads = query.order_by(Thread.update_at.desc()).all()
|
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
|
import asyncio
|
||||||
|
|
||||||
from .chatbot import ChatbotAgent
|
from .chatbot.graph import ChatbotAgent
|
||||||
from .react.graph import ReActAgent
|
from .react.graph import ReActAgent
|
||||||
|
|
||||||
class AgentManager:
|
class AgentManager:
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
from .graph import ChatbotAgent
|
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 os
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Any
|
from typing import Any, cast, Annotated
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
import sqlite3
|
from dataclasses import dataclass, field, fields
|
||||||
from langchain_core.runnables import RunnableConfig
|
from langchain_core.messages import AIMessage, ToolMessage
|
||||||
from langgraph.graph import StateGraph, START, END
|
from langgraph.graph import StateGraph, START, END
|
||||||
|
from langgraph.runtime import Runtime
|
||||||
from langgraph.prebuilt import ToolNode, tools_condition
|
from langgraph.prebuilt import ToolNode, tools_condition
|
||||||
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver, aiosqlite
|
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver, aiosqlite
|
||||||
|
from langgraph.checkpoint.memory import InMemorySaver
|
||||||
|
|
||||||
from src import config as sys_config
|
from src import config as sys_config
|
||||||
from src.utils import logger
|
from src.utils import logger
|
||||||
from src.agents.registry import State, BaseAgent
|
from src.agents.common.utils import get_cur_time_with_utc
|
||||||
from src.agents.utils import load_chat_model, get_cur_time_with_utc
|
from src.agents.common.base import BaseAgent
|
||||||
from src.agents.chatbot.configuration import ChatbotConfiguration
|
from src.agents.common.models import load_chat_model
|
||||||
from src.agents.tools_factory import get_buildin_tools
|
|
||||||
|
from .state import State
|
||||||
|
from .context import Context
|
||||||
|
from .tools import get_tools
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ChatbotAgent(BaseAgent):
|
class ChatbotAgent(BaseAgent):
|
||||||
name = "智能体助手"
|
name = "智能体助手"
|
||||||
description = "基础的对话机器人,可以回答问题,默认不使用任何工具,可在配置中启用需要的工具。"
|
description = "基础的对话机器人,可以回答问题,默认不使用任何工具,可在配置中启用需要的工具。"
|
||||||
config_schema = ChatbotConfiguration
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.graph = None
|
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)
|
self.workdir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
def _get_tools(self, tools: list[str]):
|
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:
|
if tools is None or not isinstance(tools, list) or len(tools) == 0:
|
||||||
# 默认不使用任何工具
|
# 默认不使用任何工具
|
||||||
logger.info("未配置工具或配置为空,不使用任何工具")
|
logger.info("未配置工具或配置为空,不使用任何工具")
|
||||||
@ -44,51 +51,74 @@ class ChatbotAgent(BaseAgent):
|
|||||||
logger.info(f"使用工具: {[tool.name for tool in tools]}")
|
logger.info(f"使用工具: {[tool.name for tool in tools]}")
|
||||||
return 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 模型 - 异步版本以支持异步工具"""
|
"""调用 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(runtime.context.tools):
|
||||||
|
|
||||||
if tools := self._get_tools(conf.tools):
|
|
||||||
model = model.bind_tools(tools)
|
model = model.bind_tools(tools)
|
||||||
|
|
||||||
# 使用异步调用
|
# 使用异步调用
|
||||||
res = await model.ainvoke(
|
response = cast(
|
||||||
[{"role": "system", "content": system_prompt}, *state["messages"]]
|
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:
|
if self.graph:
|
||||||
return 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")
|
logger.debug(f"build graph `{self.id}` with {len(runnable_tools)} tools")
|
||||||
|
|
||||||
workflow = StateGraph(State, config_schema=self.config_schema)
|
builder = StateGraph(State, context_schema=self.context_schema)
|
||||||
workflow.add_node("chatbot", self.llm_call)
|
builder.add_node("chatbot", self.llm_call)
|
||||||
workflow.add_node("tools", ToolNode(tools=runnable_tools))
|
builder.add_node("tools", self.dynamic_tools_node)
|
||||||
workflow.add_edge(START, "chatbot")
|
builder.add_edge(START, "chatbot")
|
||||||
workflow.add_conditional_edges(
|
builder.add_conditional_edges(
|
||||||
"chatbot",
|
"chatbot",
|
||||||
tools_condition,
|
tools_condition,
|
||||||
)
|
)
|
||||||
workflow.add_edge("tools", "chatbot")
|
builder.add_edge("tools", "chatbot")
|
||||||
workflow.add_edge("chatbot", END)
|
builder.add_edge("chatbot", END)
|
||||||
|
|
||||||
# 创建数据库连接并确保设置 checkpointer
|
# 创建数据库连接并确保设置 checkpointer
|
||||||
try:
|
try:
|
||||||
sqlite_checkpointer = AsyncSqliteSaver(await self.get_async_conn())
|
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
|
self.graph = graph
|
||||||
return graph
|
return graph
|
||||||
except Exception as e:
|
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
|
self.graph = graph
|
||||||
return graph
|
return graph
|
||||||
|
|
||||||
@ -101,7 +131,7 @@ class ChatbotAgent(BaseAgent):
|
|||||||
return AsyncSqliteSaver(await self.get_async_conn())
|
return AsyncSqliteSaver(await self.get_async_conn())
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
agent = ChatbotAgent(ChatbotConfiguration())
|
agent = ChatbotAgent(Context)
|
||||||
|
|
||||||
thread_id = str(uuid.uuid4())
|
thread_id = str(uuid.uuid4())
|
||||||
config = {"configurable": {"thread_id": thread_id}}
|
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
|
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):
|
class KnowledgeRetrieverModel(BaseModel):
|
||||||
query_text: str = Field(
|
query_text: str = Field(
|
||||||
description=(
|
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]:
|
def get_kb_based_tools() -> dict[str, Any]:
|
||||||
"""获取所有知识库基于的工具"""
|
"""获取所有知识库基于的工具"""
|
||||||
# 获取所有知识库
|
# 获取所有知识库
|
||||||
kb_tools = []
|
kb_tools = []
|
||||||
retrievers = knowledge_base.get_retrievers()
|
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():
|
for db_id, retrieve_info in retrievers.items():
|
||||||
try:
|
try:
|
||||||
@ -94,14 +106,28 @@ def get_kb_based_tools() -> dict[str, Any]:
|
|||||||
|
|
||||||
return kb_tools
|
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 = []
|
tools_info = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
tools = get_buildin_tools()
|
|
||||||
logger.debug(f"Processing {len(tools)} tools for info extraction")
|
|
||||||
|
|
||||||
# 获取注册的工具信息
|
# 获取注册的工具信息
|
||||||
for tool_obj in tools:
|
for tool_obj in tools:
|
||||||
try:
|
try:
|
||||||
@ -123,7 +149,7 @@ def get_buildin_tools_info() -> dict[str, dict[str, Any]]:
|
|||||||
})
|
})
|
||||||
|
|
||||||
tools_info.append(info)
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Failed to process tool {tool_obj.name}: {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")
|
logger.info(f"Successfully extracted info for {len(tools_info)} tools")
|
||||||
return tools_info
|
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
|
import traceback
|
||||||
|
|
||||||
from src import config
|
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.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.language_models import BaseChatModel
|
||||||
from langchain_core.runnables import RunnableConfig
|
from langchain_core.runnables import RunnableConfig
|
||||||
from langchain_core.messages import AIMessageChunk, ToolMessage
|
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):
|
class ReActAgent(BaseAgent):
|
||||||
name = "ReAct"
|
name = "ReAct (all tools)"
|
||||||
description = "A react agent that can answer questions and help with tasks."
|
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):
|
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
|
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._kb_types[kb_type] = kb_class
|
||||||
cls._default_configs[kb_type] = default_config or {}
|
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
|
@classmethod
|
||||||
def create(cls, kb_type: str, work_dir: str, **kwargs) -> KnowledgeBase:
|
def create(cls, kb_type: str, work_dir: str, **kwargs) -> KnowledgeBase:
|
||||||
|
|||||||
@ -112,7 +112,7 @@ export const agentApi = {
|
|||||||
* 获取所有可用工具的信息
|
* 获取所有可用工具的信息
|
||||||
* @returns {Promise} - 工具信息列表
|
* @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} - 对话线程列表
|
* @returns {Promise} - 对话线程列表
|
||||||
*/
|
*/
|
||||||
getThreads: (agentId) => {
|
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);
|
return apiGet(url);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -7,8 +7,7 @@
|
|||||||
export * from './system_api' // 系统管理API
|
export * from './system_api' // 系统管理API
|
||||||
export * from './knowledge_api' // 知识库管理API
|
export * from './knowledge_api' // 知识库管理API
|
||||||
export * from './graph_api' // 图谱API
|
export * from './graph_api' // 图谱API
|
||||||
export * from './tools.js' // 工具API
|
export * from './agent_api' // 智能体API
|
||||||
export * from './agent.js' // 智能体API
|
|
||||||
|
|
||||||
// 导出基础工具函数
|
// 导出基础工具函数
|
||||||
export { apiGet, apiPost, apiPut, apiDelete,
|
export { apiGet, apiPost, apiPut, apiDelete,
|
||||||
|
|||||||
@ -68,7 +68,7 @@ export const configApi = {
|
|||||||
// === 信息管理分组 ===
|
// === 信息管理分组 ===
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
export const brandAPi = {
|
export const brandApi = {
|
||||||
/**
|
/**
|
||||||
* 获取系统信息配置(公开接口)
|
* 获取系统信息配置(公开接口)
|
||||||
* @returns {Promise} - 系统信息配置
|
* @returns {Promise} - 系统信息配置
|
||||||
|
|||||||
@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
<a-divider />
|
<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-form :model="agentConfig" layout="vertical" class="config-form">
|
||||||
<a-alert
|
<a-alert
|
||||||
@ -57,8 +57,9 @@
|
|||||||
>
|
>
|
||||||
<p v-if="value.description" class="config-description">{{ value.description }}</p>
|
<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
|
<ModelSelectorComponent
|
||||||
@select-model="handleModelChange"
|
@select-model="handleModelChange"
|
||||||
:model_name="agentConfig[key] ? agentConfig[key].split('/').slice(1).join('/') : ''"
|
: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">
|
||||||
<div class="tools-summary-info">
|
<div class="tools-summary-info">
|
||||||
<span class="tools-count">已选择 {{ getSelectedCount(key) }} 个工具</span>
|
<span class="tools-count">已选择 {{ getSelectedCount(key) }} 个工具</span>
|
||||||
@ -307,7 +308,8 @@ const {
|
|||||||
availableTools,
|
availableTools,
|
||||||
selectedAgent,
|
selectedAgent,
|
||||||
selectedAgentId,
|
selectedAgentId,
|
||||||
agentConfig
|
agentConfig,
|
||||||
|
configurableItems
|
||||||
} = storeToRefs(agentStore);
|
} = storeToRefs(agentStore);
|
||||||
|
|
||||||
// console.log(availableTools.value)
|
// console.log(availableTools.value)
|
||||||
@ -318,21 +320,6 @@ const toolsModalOpen = ref(false);
|
|||||||
const selectedTools = ref([]);
|
const selectedTools = ref([]);
|
||||||
const toolsSearchText = 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(() => {
|
const isEmptyConfig = computed(() => {
|
||||||
return !selectedAgentId.value || Object.keys(configurableItems.value).length === 0;
|
return !selectedAgentId.value || Object.keys(configurableItems.value).length === 0;
|
||||||
|
|||||||
@ -116,7 +116,6 @@ import {
|
|||||||
} from '@ant-design/icons-vue';
|
} from '@ant-design/icons-vue';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { configApi } from '@/apis/system_api';
|
import { configApi } from '@/apis/system_api';
|
||||||
import { agentApi } from '@/apis/agent';
|
|
||||||
import { checkAdminPermission } from '@/stores/user';
|
import { checkAdminPermission } from '@/stores/user';
|
||||||
|
|
||||||
const configStore = useConfigStore()
|
const configStore = useConfigStore()
|
||||||
@ -402,7 +401,6 @@ const printAgentConfig = async () => {
|
|||||||
console.log('当前选中智能体:', {
|
console.log('当前选中智能体:', {
|
||||||
agent: toRaw(agentStore.selectedAgent),
|
agent: toRaw(agentStore.selectedAgent),
|
||||||
isDefault: agentStore.isDefaultAgent,
|
isDefault: agentStore.isDefaultAgent,
|
||||||
configSchema: toRaw(agentStore.configSchema),
|
|
||||||
configurableItems: Object.keys(agentStore.configurableItems).length
|
configurableItems: Object.keys(agentStore.configurableItems).length
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -419,7 +417,7 @@ const printAgentConfig = async () => {
|
|||||||
|
|
||||||
// 线程信息
|
// 线程信息
|
||||||
console.log('线程信息:', {
|
console.log('线程信息:', {
|
||||||
currentAgentThreads: agentStore.currentAgentThreads.length,
|
currentAgentThreads: agentStore.currentAgentThreads,
|
||||||
currentThread: agentStore.currentThread ? toRaw(agentStore.currentThread) : null,
|
currentThread: agentStore.currentThread ? toRaw(agentStore.currentThread) : null,
|
||||||
currentThreadMessages: agentStore.currentThreadMessages.length
|
currentThreadMessages: agentStore.currentThreadMessages.length
|
||||||
});
|
});
|
||||||
|
|||||||
@ -312,11 +312,11 @@ const selectedEdgeData = computed(() => graphStore.selectedEdgeData)
|
|||||||
// 智能边面板位置 - 确保在紧凑模式下也能正确显示
|
// 智能边面板位置 - 确保在紧凑模式下也能正确显示
|
||||||
const intelligentEdgePanelPosition = computed(() => {
|
const intelligentEdgePanelPosition = computed(() => {
|
||||||
if (!sigmaContainer.value) return edgePanelPosition.value
|
if (!sigmaContainer.value) return edgePanelPosition.value
|
||||||
|
|
||||||
const containerHeight = sigmaContainer.value.clientHeight
|
const containerHeight = sigmaContainer.value.clientHeight
|
||||||
const panelHeight = 200 // 估计的面板高度
|
const panelHeight = 200 // 估计的面板高度
|
||||||
const nodePanelBottom = selectedNodeData.value ? nodePanelPosition.value.y + 200 : 0
|
const nodePanelBottom = selectedNodeData.value ? nodePanelPosition.value.y + 200 : 0
|
||||||
|
|
||||||
// 如果有节点面板显示,将边面板放在节点面板下方
|
// 如果有节点面板显示,将边面板放在节点面板下方
|
||||||
if (selectedNodeData.value) {
|
if (selectedNodeData.value) {
|
||||||
return {
|
return {
|
||||||
@ -324,7 +324,7 @@ const intelligentEdgePanelPosition = computed(() => {
|
|||||||
y: Math.min(nodePanelPosition.value.y, containerHeight - panelHeight - 20)
|
y: Math.min(nodePanelPosition.value.y, containerHeight - panelHeight - 20)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保边面板在容器范围内
|
// 确保边面板在容器范围内
|
||||||
return {
|
return {
|
||||||
x: edgePanelPosition.value.x,
|
x: edgePanelPosition.value.x,
|
||||||
@ -551,10 +551,10 @@ const registerEvents = () => {
|
|||||||
// 获取Sigma边的属性,其中包含原始数据
|
// 获取Sigma边的属性,其中包含原始数据
|
||||||
const sigmaEdgeData = graph.getEdgeAttributes(edge)
|
const sigmaEdgeData = graph.getEdgeAttributes(edge)
|
||||||
console.log('Sigma边属性:', sigmaEdgeData)
|
console.log('Sigma边属性:', sigmaEdgeData)
|
||||||
|
|
||||||
// 立即设置选中的边 - 使用Sigma边ID
|
// 立即设置选中的边 - 使用Sigma边ID
|
||||||
graphStore.setSelectedEdge(edge)
|
graphStore.setSelectedEdge(edge)
|
||||||
|
|
||||||
// 确保边面板显示
|
// 确保边面板显示
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (selectedEdgeData.value) {
|
if (selectedEdgeData.value) {
|
||||||
|
|||||||
@ -202,7 +202,7 @@ import {
|
|||||||
} from '@ant-design/icons-vue';
|
} from '@ant-design/icons-vue';
|
||||||
import { useConfigStore } from '@/stores/config';
|
import { useConfigStore } from '@/stores/config';
|
||||||
import { modelIcons } from '@/utils/modelIcon';
|
import { modelIcons } from '@/utils/modelIcon';
|
||||||
import { agentApi } from '@/apis/agent';
|
import { agentApi } from '@/apis/agent_api';
|
||||||
|
|
||||||
const configStore = useConfigStore();
|
const configStore = useConfigStore();
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { agentApi, threadApi } from '@/apis/agent';
|
import { agentApi, threadApi } from '@/apis/agent_api';
|
||||||
import { MessageProcessor } from '@/utils/messageProcessor';
|
import { MessageProcessor } from '@/utils/messageProcessor';
|
||||||
import { handleChatError } from '@/utils/errorHandler';
|
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]],
|
defaultAgent: (state) => state.defaultAgentId ? state.agents[state.defaultAgentId] : state.agents[Object.keys(state.agents)[0]],
|
||||||
agentsList: (state) => Object.values(state.agents),
|
agentsList: (state) => Object.values(state.agents),
|
||||||
isDefaultAgent: (state) => state.selectedAgentId === state.defaultAgentId,
|
isDefaultAgent: (state) => state.selectedAgentId === state.defaultAgentId,
|
||||||
configSchema: (state) => {
|
|
||||||
const agent = state.selectedAgentId ? state.agents[state.selectedAgentId] : null;
|
|
||||||
return agent?.config_schema || {};
|
|
||||||
},
|
|
||||||
configurableItems: (state) => {
|
configurableItems: (state) => {
|
||||||
const schema = state.configSchema || {};
|
const agent = state.selectedAgentId ? state.agents[state.selectedAgentId] : null;
|
||||||
if (!schema || !schema.configurable_items) return {};
|
const agentConfigurableItems = agent.configurable_items || {};
|
||||||
const items = { ...schema.configurable_items };
|
if (!agentConfigurableItems) return {};
|
||||||
|
const items = { ...agentConfigurableItems };
|
||||||
Object.keys(items).forEach(key => {
|
Object.keys(items).forEach(key => {
|
||||||
const item = items[key];
|
const item = items[key];
|
||||||
if (item && item.x_oap_ui_config) {
|
if (item && item.x_oap_ui_config) {
|
||||||
@ -156,7 +153,7 @@ export const useAgentStore = defineStore('agent', {
|
|||||||
// 设置默认智能体
|
// 设置默认智能体
|
||||||
async setDefaultAgent(agentId) {
|
async setDefaultAgent(agentId) {
|
||||||
try {
|
try {
|
||||||
await agentConfigApi.setDefaultAgent(agentId);
|
await agentApi.setDefaultAgent(agentId);
|
||||||
this.defaultAgentId = agentId;
|
this.defaultAgentId = agentId;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to set default agent:', error);
|
console.error('Failed to set default agent:', error);
|
||||||
@ -427,7 +424,7 @@ export const useAgentStore = defineStore('agent', {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await agentApi.sendAgentMessage(this.selectedAgentId, requestData);
|
const response = await agentApi.sendAgentMessage(this.selectedAgentId, requestData);
|
||||||
|
|
||||||
const reader = response.body.getReader();
|
const reader = response.body.getReader();
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
let buffer = '';
|
let buffer = '';
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { brandAPi } from '@/apis/system_api'
|
import { brandApi } from '@/apis/system_api'
|
||||||
|
|
||||||
export const useInfoStore = defineStore('info', () => {
|
export const useInfoStore = defineStore('info', () => {
|
||||||
// 状态
|
// 状态
|
||||||
@ -49,7 +49,7 @@ export const useInfoStore = defineStore('info', () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
const response = await brandAPi.getInfoConfig()
|
const response = await brandApi.getInfoConfig()
|
||||||
|
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
setInfoConfig(response.data)
|
setInfoConfig(response.data)
|
||||||
@ -70,7 +70,7 @@ export const useInfoStore = defineStore('info', () => {
|
|||||||
async function reloadInfoConfig() {
|
async function reloadInfoConfig() {
|
||||||
try {
|
try {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
const response = await brandAPi.reloadInfoConfig()
|
const response = await brandApi.reloadInfoConfig()
|
||||||
|
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
setInfoConfig(response.data)
|
setInfoConfig(response.data)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user