diff --git a/README.md b/README.md index d33d086f..2a4d9faa 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ 视频演示缩略图 - +

@@ -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)。例如: diff --git a/docs/changelog/update.md b/docs/changelog/update.md index 05c495e2..6100609c 100644 --- a/docs/changelog/update.md +++ b/docs/changelog/update.md @@ -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**: diff --git a/server/routers/__init__.py b/server/routers/__init__.py index d192b751..48dd6cd6 100644 --- a/server/routers/__init__.py +++ b/server/routers/__init__.py @@ -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/* diff --git a/server/routers/chat_router.py b/server/routers/chat_router.py index 0b0d9be9..08caa184 100644 --- a/server/routers/chat_router.py +++ b/server/routers/chat_router.py @@ -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() diff --git a/server/routers/tool_router.py b/server/routers/tool_router.py deleted file mode 100644 index 5d732980..00000000 --- a/server/routers/tool_router.py +++ /dev/null @@ -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)} diff --git a/src/agents/__init__.py b/src/agents/__init__.py index 09b75c34..3598fba7 100644 --- a/src/agents/__init__.py +++ b/src/agents/__init__.py @@ -1,6 +1,6 @@ import asyncio -from .chatbot import ChatbotAgent +from .chatbot.graph import ChatbotAgent from .react.graph import ReActAgent class AgentManager: diff --git a/src/agents/chatbot/__init__.py b/src/agents/chatbot/__init__.py index ff0e6f84..180012be 100644 --- a/src/agents/chatbot/__init__.py +++ b/src/agents/chatbot/__init__.py @@ -1,4 +1,3 @@ from .graph import ChatbotAgent -from .configuration import ChatbotConfiguration -__all__ = ["ChatbotAgent", "ChatbotConfiguration"] +__all__ = ["ChatbotAgent"] diff --git a/src/agents/chatbot/configuration.py b/src/agents/chatbot/configuration.py deleted file mode 100644 index 58b43ac9..00000000 --- a/src/agents/chatbot/configuration.py +++ /dev/null @@ -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": "工具列表" - }, - ) diff --git a/src/agents/chatbot/context.py b/src/agents/chatbot/context.py new file mode 100644 index 00000000..a056d935 --- /dev/null +++ b/src/agents/chatbot/context.py @@ -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": "工具列表" + }, + ) diff --git a/src/agents/chatbot/graph.py b/src/agents/chatbot/graph.py index 70a21b37..7aa04470 100644 --- a/src/agents/chatbot/graph.py +++ b/src/agents/chatbot/graph.py @@ -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}} diff --git a/src/agents/chatbot/state.py b/src/agents/chatbot/state.py new file mode 100644 index 00000000..c37963cf --- /dev/null +++ b/src/agents/chatbot/state.py @@ -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 + ) diff --git a/src/agents/chatbot/tools.py b/src/agents/chatbot/tools.py new file mode 100644 index 00000000..49d59c04 --- /dev/null +++ b/src/agents/chatbot/tools.py @@ -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 diff --git a/src/agents/common/base.py b/src/agents/common/base.py new file mode 100644 index 00000000..c4a079a6 --- /dev/null +++ b/src/agents/common/base.py @@ -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 diff --git a/src/agents/common/context.py b/src/agents/common/context.py new file mode 100644 index 00000000..e4dc1123 --- /dev/null +++ b/src/agents/common/context.py @@ -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 {} diff --git a/src/agents/common/models.py b/src/agents/common/models.py new file mode 100644 index 00000000..7fe7fe02 --- /dev/null +++ b/src/agents/common/models.py @@ -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()}") diff --git a/src/agents/tools_factory.py b/src/agents/common/tools.py similarity index 70% rename from src/agents/tools_factory.py rename to src/agents/common/tools.py index 7916de2b..69532ada 100644 --- a/src/agents/tools_factory.py +++ b/src/agents/common/tools.py @@ -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 - diff --git a/src/agents/utils.py b/src/agents/common/utils.py similarity index 97% rename from src/agents/utils.py rename to src/agents/common/utils.py index 44b65c64..ced1f75b 100644 --- a/src/agents/utils.py +++ b/src/agents/common/utils.py @@ -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 diff --git a/src/agents/react/graph.py b/src/agents/react/graph.py index 43fc96ef..ab7b966f 100644 --- a/src/agents/react/graph.py +++ b/src/agents/react/graph.py @@ -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 diff --git a/src/agents/react/workflows.py b/src/agents/react/workflows.py deleted file mode 100644 index 3356cc1c..00000000 --- a/src/agents/react/workflows.py +++ /dev/null @@ -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()) diff --git a/src/agents/registry.py b/src/agents/registry.py deleted file mode 100644 index 92cb0714..00000000 --- a/src/agents/registry.py +++ /dev/null @@ -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 diff --git a/src/knowledge/kb_factory.py b/src/knowledge/kb_factory.py index 3fe20982..b1e4dbc1 100644 --- a/src/knowledge/kb_factory.py +++ b/src/knowledge/kb_factory.py @@ -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: diff --git a/web/src/apis/agent.js b/web/src/apis/agent_api.js similarity index 97% rename from web/src/apis/agent.js rename to web/src/apis/agent_api.js index 7168963f..cd0aed5d 100644 --- a/web/src/apis/agent.js +++ b/web/src/apis/agent_api.js @@ -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); }, diff --git a/web/src/apis/index.js b/web/src/apis/index.js index df531e16..2131228f 100644 --- a/web/src/apis/index.js +++ b/web/src/apis/index.js @@ -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, diff --git a/web/src/apis/system_api.js b/web/src/apis/system_api.js index b9ecd401..a9d191cd 100644 --- a/web/src/apis/system_api.js +++ b/web/src/apis/system_api.js @@ -68,7 +68,7 @@ export const configApi = { // === 信息管理分组 === // ============================================================================= -export const brandAPi = { +export const brandApi = { /** * 获取系统信息配置(公开接口) * @returns {Promise} - 系统信息配置 diff --git a/web/src/components/AgentConfigSidebar.vue b/web/src/components/AgentConfigSidebar.vue index f3f1903f..875980cb 100644 --- a/web/src/components/AgentConfigSidebar.vue +++ b/web/src/components/AgentConfigSidebar.vue @@ -30,7 +30,7 @@ -

+

{{ value.description }}

+ -
+
-
+
已选择 {{ getSelectedCount(key) }} 个工具 @@ -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; diff --git a/web/src/components/DebugComponent.vue b/web/src/components/DebugComponent.vue index 39e10a72..ff3cfb18 100644 --- a/web/src/components/DebugComponent.vue +++ b/web/src/components/DebugComponent.vue @@ -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 }); diff --git a/web/src/components/KnowledgeGraphViewer.vue b/web/src/components/KnowledgeGraphViewer.vue index 5ab90153..e2e4daad 100644 --- a/web/src/components/KnowledgeGraphViewer.vue +++ b/web/src/components/KnowledgeGraphViewer.vue @@ -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) { diff --git a/web/src/components/ModelProvidersComponent.vue b/web/src/components/ModelProvidersComponent.vue index 74424798..006ac4a5 100644 --- a/web/src/components/ModelProvidersComponent.vue +++ b/web/src/components/ModelProvidersComponent.vue @@ -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(); diff --git a/web/src/stores/agent.js b/web/src/stores/agent.js index a7e0da9a..258a6b22 100644 --- a/web/src/stores/agent.js +++ b/web/src/stores/agent.js @@ -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 = ''; diff --git a/web/src/stores/info.js b/web/src/stores/info.js index f647ce9b..536e99a7 100644 --- a/web/src/stores/info.js +++ b/web/src/stores/info.js @@ -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)