diff --git a/src/agents/__init__.py b/src/agents/__init__.py index 0ce6de15..6add01ba 100644 --- a/src/agents/__init__.py +++ b/src/agents/__init__.py @@ -1,53 +1,39 @@ -from src.agents.registry import BaseAgent -from src.agents.chatbot.graph import ChatbotAgent -from langchain_core.runnables import RunnableConfig -from langchain_core.messages import AIMessageChunk, ToolMessage - +from src.agents.chatbot import ChatbotAgent, ChatbotConfiguration class AgentManager: def __init__(self): self.agents = {} - def add_agent(self, agent_id, agent): + def add_agent(self, agent_id, agent_class, configuration_class): self.agents[agent_id] = { - "agent": agent, - "configuration": agent.configuration + "agent_class": agent_class, + "configuration_class": configuration_class } -def agent_cli(agent: BaseAgent, config: RunnableConfig = None): - config = config or {} - if "configurable" not in config: - config["configurable"] = {} + def get_runnable_agent(self, agent_id, **kwargs): + agent_class = self.get_agent(agent_id) + configuration_class = self.get_configuration(agent_id) + configuration = configuration_class(**kwargs) + return agent_class(configuration) - while True: - user_input = input("\nUser: ") - if user_input.lower() in ["quit", "exit", "q"]: - print("Goodbye!") - break + def get_agent(self, agent_id): + return self.agents[agent_id]["agent_class"] - stream_flag = False - for msg, metadata in agent.stream_messages([{"role": "user", "content": user_input}], config): - if isinstance(msg, AIMessageChunk): - content = msg.content or msg.tool_calls + def get_configuration(self, agent_id): + return self.agents[agent_id]["configuration_class"] - if not content: - if stream_flag == True: - print() - stream_flag = False - continue - if stream_flag == False and content: - print(f"AI: {content}", end="", flush=True) - stream_flag = True - continue +agent_manager = AgentManager() +agent_manager.add_agent("chatbot", ChatbotAgent, ChatbotConfiguration) - elif content: - print(f"{content}", end="", flush=True) +__all__ = ["agent_manager"] - if isinstance(msg, ToolMessage): - print(f"Tool: {msg.content}") -def get_agents(): - agent_manager = AgentManager() - agent_manager.add_agent("chatbot", ChatbotAgent()) - return agent_manager.agents +if __name__ == "__main__": + agent = agent_manager.get_agent("chatbot") + conf = agent_manager.get_configuration("chatbot") + agent_info = { + "name": agent.name, + "description": agent.description, + } + print(agent_info) diff --git a/src/agents/chatbot/__init__.py b/src/agents/chatbot/__init__.py index d6f7031e..8ebda7da 100644 --- a/src/agents/chatbot/__init__.py +++ b/src/agents/chatbot/__init__.py @@ -1,3 +1,4 @@ from .graph import ChatbotAgent +from .configuration import ChatbotConfiguration -__all__ = ["ChatbotAgent"] \ No newline at end of file +__all__ = ["ChatbotAgent", "ChatbotConfiguration"] \ No newline at end of file diff --git a/src/agents/chatbot/configuration.py b/src/agents/chatbot/configuration.py index 65d09178..573e8435 100644 --- a/src/agents/chatbot/configuration.py +++ b/src/agents/chatbot/configuration.py @@ -1,7 +1,5 @@ from dataclasses import dataclass, field -from typing import List -from langchain_core.messages import SystemMessage from langchain_core.tools import Tool from langchain_openai import ChatOpenAI @@ -9,20 +7,19 @@ from src import config from src.models import select_model from src.agents.registry import Configuration -def get_default_llm(): - return select_model(config).chat_open_ai - -def get_default_tools(): - from langchain_community.tools.tavily_search import TavilySearchResults - return [TavilySearchResults(max_results=10)] - def get_default_requirements(): return ["TAVILY_API_KEY"] @dataclass(kw_only=True) class ChatbotConfiguration(Configuration): - name: str = "chatbot" - description: str = "A chatbot that can answer questions and help with tasks." requirements: list[str] = field(default_factory=get_default_requirements) - tools: list[Tool] = field(default_factory=get_default_tools) - llm: ChatOpenAI = field(default_factory=get_default_llm) + llm: ChatOpenAI | None = None + model_provider: str = "siliconflow" + model_name: str = "Qwen/Qwen2.5-72B-Instruct" + + def __post_init__(self): + # TODO 需要确保这里的模型是支持 tools 的 + if self.llm is None: + self.llm = select_model(config=config, + model_provider=self.model_provider, + model_name=self.model_name).chat_open_ai diff --git a/src/agents/chatbot/graph.py b/src/agents/chatbot/graph.py index 7ec47f45..a5c9a8fc 100644 --- a/src/agents/chatbot/graph.py +++ b/src/agents/chatbot/graph.py @@ -3,7 +3,6 @@ import uuid from typing import Any from datetime import datetime -from langchain_core.messages import AIMessageChunk, ToolMessage from langchain_core.runnables import RunnableConfig from langgraph.graph import StateGraph, START, END from langgraph.prebuilt import ToolNode, tools_condition @@ -13,53 +12,56 @@ from src.agents.registry import State, BaseAgent from src.agents.chatbot.configuration import ChatbotConfiguration class ChatbotAgent(BaseAgent): + name = "chatbot" + description = "A chatbot that can answer questions and help with tasks." _graph_cache = None def __init__(self, configuration: ChatbotConfiguration = None): super().__init__(configuration) self.configuration = configuration or ChatbotConfiguration() - self.llm = configuration.llm + self.llm = self.configuration.llm - def llm_call(self, state: State) -> dict[str, Any]: - tools = self.configuration.tools - system_prompt = { - "role": "system", - "content": ( - f"Current time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n" - ) - } - messages = [system_prompt] + state["messages"] - model = self.llm.bind_tools(tools) + def _get_tools(self, config: RunnableConfig): + """根据配置获取工具""" + tools = [] + if not config: + return tools - res = model.invoke(messages) + if config.get("configurable", {}).get("use_web", None): + from langchain_community.tools.tavily_search import TavilySearchResults + tools.append(TavilySearchResults(max_results=10)) + + return tools + + def llm_call(self, state: State, config: RunnableConfig) -> dict[str, Any]: + model = self.llm.bind_tools(self._get_tools(config)) + + res = model.invoke(state["messages"]) return {"messages": [res]} - def get_graph(self): + def get_graph(self, config: RunnableConfig = None): """构建图""" - if ChatbotAgent._graph_cache is None: - workflow = StateGraph(State) - workflow.add_node("chatbot", self.llm_call) - workflow.add_node("tools", ToolNode(tools=self.configuration.tools)) - workflow.add_edge(START, "chatbot") - workflow.add_conditional_edges( - "chatbot", - tools_condition, - ) - workflow.add_edge("tools", "chatbot") - workflow.add_edge("chatbot", END) + workflow = StateGraph(State) + workflow.add_node("chatbot", self.llm_call) + workflow.add_node("tools", ToolNode(tools=self._get_tools(config))) + workflow.add_edge(START, "chatbot") + workflow.add_conditional_edges( + "chatbot", + tools_condition, + ) + workflow.add_edge("tools", "chatbot") + workflow.add_edge("chatbot", END) - graph = workflow.compile(checkpointer=MemorySaver()) - ChatbotAgent._graph_cache = graph - - return ChatbotAgent._graph_cache + graph = workflow.compile(checkpointer=MemorySaver()) + return graph def stream_values(self, messages: list[str], config: RunnableConfig = None): - graph = self.get_graph() + graph = self.get_graph(config) for event in graph.stream({"messages": messages}, stream_mode="values", config=config): yield event["messages"] def stream_messages(self, messages: list[str], config: RunnableConfig = None): - graph = self.get_graph() + graph = self.get_graph(config) for msg, metadata in graph.stream({"messages": messages}, stream_mode="messages", config=config): msg_type = msg.type @@ -67,14 +69,13 @@ class ChatbotAgent(BaseAgent): if not return_keys or msg_type in return_keys: yield msg, metadata - def main(): agent = ChatbotAgent(ChatbotConfiguration()) thread_id = str(uuid.uuid4()) config = {"configurable": {"thread_id": thread_id}} - from src.agents import agent_cli + from src.agents.utils import agent_cli agent_cli(agent, config) diff --git a/src/agents/registry.py b/src/agents/registry.py index 04915770..39f0838f 100644 --- a/src/agents/registry.py +++ b/src/agents/registry.py @@ -1,12 +1,11 @@ from typing import Type, Annotated, Optional, TypedDict from abc import abstractmethod -from langchain_core.tools import BaseTool from langchain_openai import ChatOpenAI -from langgraph.checkpoint.base import BaseCheckpointSaver -from langgraph.graph.message import add_messages -from langchain_core.messages import BaseMessage, ToolMessage, AIMessage, filter_messages + +from langchain_core.messages import BaseMessage from langgraph.graph.state import CompiledStateGraph +from langgraph.graph.message import add_messages from dataclasses import dataclass @@ -26,7 +25,8 @@ class Configuration: """ 定义一个基础 Configuration 供 各类 graph 继承 """ - llm: ChatOpenAI + pass + class BaseAgent(): diff --git a/src/agents/utils.py b/src/agents/utils.py new file mode 100644 index 00000000..4f2bb20b --- /dev/null +++ b/src/agents/utils.py @@ -0,0 +1,37 @@ +from src.agents.registry import BaseAgent +from langchain_core.runnables import RunnableConfig +from langchain_core.messages import AIMessageChunk, ToolMessage + + +def agent_cli(agent: BaseAgent, config: RunnableConfig = None): + config = config or {} + if "configurable" not in config: + config["configurable"] = {} + + while True: + user_input = input("\nUser: ") + if user_input.lower() in ["quit", "exit", "q"]: + print("Goodbye!") + break + + stream_flag = False + for msg, metadata in agent.stream_messages([{"role": "user", "content": user_input}], config): + if isinstance(msg, AIMessageChunk): + content = msg.content or msg.tool_calls + + if not content: + if stream_flag == True: + print() + stream_flag = False + continue + + if stream_flag == False and content: + print(f"AI: {content}", end="", flush=True) + stream_flag = True + continue + + elif content: + print(f"{content}", end="", flush=True) + + if isinstance(msg, ToolMessage): + print(f"Tool: {msg.content}") diff --git a/src/routers/chat_router.py b/src/routers/chat_router.py index b898a714..1f2b8811 100644 --- a/src/routers/chat_router.py +++ b/src/routers/chat_router.py @@ -1,10 +1,14 @@ import json import asyncio import traceback +import uuid from fastapi import APIRouter, Body -from fastapi.responses import StreamingResponse, Response -from src.core import HistoryManager +from fastapi.responses import StreamingResponse +from langchain_core.messages import AIMessageChunk + from src import executor, config, retriever +from src.core import HistoryManager +from src.agents import agent_manager from src.models import select_model from src.utils.logging_config import logger @@ -20,8 +24,8 @@ async def chat_get(): def chat_post( query: str = Body(...), meta: dict = Body(None), - history: list = Body(...), - cur_res_id: str = Body(...)): + history: list | None = Body(None), + thread_id: str | None = Body(None)): model = select_model(config) meta["server_model_name"] = model.model_name @@ -31,7 +35,6 @@ def chat_post( def make_chunk(content=None, **kwargs): return json.dumps({ "response": content, - "model_name": config.model_name, "meta": meta, **kwargs }, ensure_ascii=False).encode('utf-8') + b"\n" @@ -76,7 +79,7 @@ def chat_post( else: content += delta.content or "" - chunk = make_chunk(content=content, status="loading") + chunk = make_chunk(content=delta.content, status="loading") yield chunk logger.debug(f"Final response: {content}") @@ -117,4 +120,62 @@ async def call(query: str = Body(...), meta: dict = Body(None)): response = await predict_async(query) logger.debug({"query": query, "response": response.content}) - return {"response": response.content} \ No newline at end of file + return {"response": response.content} + +@chat.get("/agent") +async def get_agent(): + agents = [{ + "name": agent["agent_class"].name, + "description": agent["agent_class"].description + } for agent in agent_manager.agents.values()] + return {"agents": agents} + +@chat.post("/agent/{agent_name}") +def chat_agent(agent_name: str, + query: str = Body(...), + meta: dict = Body({}), + history: list = Body(...), + thread_id: str | None = Body(None)): + + meta["server_model_name"] = agent_name + agent = agent_manager.get_runnable_agent(agent_name) + + history_manager = HistoryManager(history) + messages = history_manager.get_history_with_msg(query, max_rounds=meta.get('history_round')) + history_manager.add_user(query) # 注意这里使用原始查询 + + runnable_config = { + "configurable": { + "thread_id": thread_id or str(uuid.uuid4()), + "use_web": meta.get("use_web", False), + "return_keys": [] + } + } + + def make_chunk(content=None, **kwargs): + return json.dumps({ + "response": content, + "model_name": agent_name, + "meta": meta, + **kwargs + }, ensure_ascii=False).encode('utf-8') + b"\n" + + def stream_messages(): + content = "" + yield make_chunk(status="waiting") + for msg, metadata in agent.stream_messages(messages, runnable_config): + # logger.debug(f"msg: {msg.model_dump()}, {metadata=}") + if isinstance(msg, AIMessageChunk) and msg.content != "": + content += msg.content + yield make_chunk(content=msg.content, + msg=msg.model_dump(), + metadata=metadata, + status="loading") + else: + yield make_chunk(msg=msg.model_dump(), + metadata=metadata, + status="loading") + + yield make_chunk(status="finished", history=history_manager.update_ai(content)) + + return StreamingResponse(stream_messages(), media_type='application/json') diff --git a/web/src/components/ChatComponent.vue b/web/src/components/ChatComponent.vue index 8897f71d..8e33ff47 100644 --- a/web/src/components/ChatComponent.vue +++ b/web/src/components/ChatComponent.vue @@ -370,7 +370,7 @@ const updateMessage = (info) => { try { // 只有在 text 不为空时更新 if (info.text !== null && info.text !== undefined && info.text !== '') { - msg.text = info.text; + msg.text += info.text; } if (info.reasoning_content !== null && info.reasoning_content !== undefined && info.reasoning_content !== '') { @@ -463,6 +463,7 @@ const fetchChatResponse = (user_input, cur_res_id) => { history: conv.value.history, meta: meta, cur_res_id: cur_res_id, + thread_id: conv.value.id.toString(), }), headers: { 'Content-Type': 'application/json' @@ -639,6 +640,12 @@ watch( display: flex; align-items: center; } + + .header__left { + .close { + margin-right: 12px; + } + } } .nav-btn { @@ -652,6 +659,7 @@ watch( font-size: 1rem; width: auto; padding: 0.5rem 1rem; + transition: background-color 0.3s; .text { margin-left: 10px; @@ -678,6 +686,7 @@ watch( padding: 12px; z-index: 11; width: 280px; + transition: transform 0.3s ease, opacity 0.3s ease; .flex-center { display: flex; @@ -1051,9 +1060,27 @@ watch( @keyframes loading {0%,80%,100%{transform:scale(0.5);}40%{transform:scale(1);}} .slide-out-left{-webkit-animation:slide-out-left .2s cubic-bezier(.55,.085,.68,.53) both;animation:slide-out-left .5s cubic-bezier(.55,.085,.68,.53) both} -.swing-in-top-fwd{-webkit-animation:swing-in-top-fwd .2s ease-out both;animation:swing-in-top-fwd .2s ease-out both} -@-webkit-keyframes swing-in-top-fwd{0%{-webkit-transform:rotateX(-100deg);transform:rotateX(-100deg);-webkit-transform-origin:top;transform-origin:top;opacity:0}100%{-webkit-transform:rotateX(0deg);transform:rotateX(0deg);-webkit-transform-origin:top;transform-origin:top;opacity:1}}@keyframes swing-in-top-fwd{0%{-webkit-transform:rotateX(-100deg);transform:rotateX(-100deg);-webkit-transform-origin:top;transform-origin:top;opacity:0}100%{-webkit-transform:rotateX(0deg);transform:rotateX(0deg);-webkit-transform-origin:top;transform-origin:top;opacity:1}} -@-webkit-keyframes slide-out-left{0%{-webkit-transform:translateX(0);transform:translateX(0);opacity:1}100%{-webkit-transform:translateX(-1000px);transform:translateX(-1000px);opacity:0}}@keyframes slide-out-left{0%{-webkit-transform:translateX(0);transform:translateX(0);opacity:1}100%{-webkit-transform:translateX(-1000px);transform:translateX(-1000px);opacity:0}} +.swing-in-top-fwd { + -webkit-animation: swing-in-top-fwd 0.3s cubic-bezier(0.175, 0.885, 0.320, 1.275) both; + animation: swing-in-top-fwd 0.3s cubic-bezier(0.175, 0.885, 0.320, 1.275) both; +} + +@keyframes swing-in-top-fwd { + 0% { + -webkit-transform: rotateX(-100deg); + transform: rotateX(-100deg); + -webkit-transform-origin: top; + transform-origin: top; + opacity: 0; + } + 100% { + -webkit-transform: rotateX(0deg); + transform: rotateX(0deg); + -webkit-transform-origin: top; + transform-origin: top; + opacity: 1; + } +} @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes slideInUp { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } diff --git a/web/src/layouts/AppLayout.vue b/web/src/layouts/AppLayout.vue index 4ff28fd6..55fddc7d 100644 --- a/web/src/layouts/AppLayout.vue +++ b/web/src/layouts/AppLayout.vue @@ -21,6 +21,8 @@ import { StarFilled, StarOutlined, ExclamationCircleOutlined, + RobotOutlined, + RobotFilled, } from '@ant-design/icons-vue' import { themeConfig } from '@/assets/theme' import { useConfigStore } from '@/stores/config' @@ -73,6 +75,37 @@ onMounted(() => { // 打印当前页面的路由信息,使用 vue3 的 setup composition API const route = useRoute() console.log(route) + +// 下面是导航菜单部分,添加智能体项 +const mainList = [{ + name: '对话', + path: '/chat', + icon: MessageOutlined, + activeIcon: MessageFilled, + }, { + name: '智能体', + path: '/agent', + icon: RobotOutlined, + activeIcon: RobotFilled, + }, { + name: '图谱', + path: '/graph', + icon: ProjectOutlined, + activeIcon: ProjectFilled, + // hidden: !configStore.config.enable_knowledge_graph, + }, { + name: '知识库', + path: '/database', + icon: BookOutlined, + activeIcon: BookFilled, + // hidden: !configStore.config.enable_knowledge_base, + }, { + name: '工具', + path: '/tools', + icon: ToolOutlined, + activeIcon: ToolFilled, + } +]