diff --git a/src/agents/__init__.py b/src/agents/__init__.py index adec7d99..cda5e89f 100644 --- a/src/agents/__init__.py +++ b/src/agents/__init__.py @@ -1,4 +1,5 @@ -from src.agents.chatbot import ChatbotAgent, ChatbotConfiguration +from src.agents.chatbot import ChatbotAgent +from src.agents.react import ReActAgent class AgentManager: def __init__(self): @@ -17,6 +18,7 @@ class AgentManager: agent_manager = AgentManager() agent_manager.add_agent("chatbot", ChatbotAgent) +agent_manager.add_agent("react", ReActAgent) __all__ = ["agent_manager"] diff --git a/src/agents/react/__init__.py b/src/agents/react/__init__.py new file mode 100644 index 00000000..3d2b2444 --- /dev/null +++ b/src/agents/react/__init__.py @@ -0,0 +1,4 @@ +from .graph import ReActAgent +from .configuration import ReActConfiguration + +__all__ = ["ReActAgent", "ReActConfiguration"] \ No newline at end of file diff --git a/src/agents/react/configuration.py b/src/agents/react/configuration.py new file mode 100644 index 00000000..22986169 --- /dev/null +++ b/src/agents/react/configuration.py @@ -0,0 +1,36 @@ +from dataclasses import dataclass, field +from datetime import datetime, timezone + +from langchain_core.tools import tool +from langchain_openai import ChatOpenAI + +from src.agents.registry import Configuration + +def get_default_requirements(): + return ["TAVILY_API_KEY"] + +@tool +def multiply(first_int: int, second_int: int) -> int: + """Multiply two integers together.""" + return first_int * second_int + +@dataclass(kw_only=True) +class ReActConfiguration(Configuration): + """ReAct 的配置""" + + system_prompt: str = field( + default=f"You are a helpful assistant. Now is {datetime.now(tz=timezone.utc).isoformat()}", + metadata={ + "description": "The system prompt to use for the agent's interactions. " + "This prompt sets the context and behavior for the agent." + }, + ) + + model: str = field( + default="zhipu/glm-4-plus", + metadata={ + "description": "The name of the language model to use for the agent's main interactions. " + "Should be in the form: provider/model-name." + }, + ) + diff --git a/src/agents/react/graph.py b/src/agents/react/graph.py new file mode 100644 index 00000000..02650c83 --- /dev/null +++ b/src/agents/react/graph.py @@ -0,0 +1,86 @@ +import asyncio +import uuid +from typing import Any +from datetime import datetime + +from langchain_core.runnables import RunnableConfig +from langgraph.graph import StateGraph, START, END +from langgraph.prebuilt import ToolNode, tools_condition +from langgraph.checkpoint.memory import MemorySaver +from langchain_community.tools.tavily_search import TavilySearchResults + + +from src.agents.registry import State, BaseAgent +from src.agents.utils import load_chat_model +from src.agents.react.configuration import ReActConfiguration, multiply + +class ReActAgent(BaseAgent): + name = "react" + description = "A react agent that can answer questions and help with tasks." + _graph_cache = None + config_schema = ReActConfiguration.to_dict() + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + def _get_tools(self, config_schema: RunnableConfig): + """根据配置获取工具""" + tools = [multiply, TavilySearchResults(max_results=10)] + return tools + + def llm_call(self, state: State, config: RunnableConfig = None) -> dict[str, Any]: + """调用 llm 模型""" + config_schema = config or {} + conf = ReActConfiguration.from_runnable_config(config_schema) + model = load_chat_model(conf.model) + model_with_tools = model.bind_tools(self._get_tools(config_schema)) + + res = model_with_tools.invoke( + [{"role": "system", "content": conf.system_prompt}, *state["messages"]] + ) + return {"messages": [res]} + + def get_graph(self, config_schema: RunnableConfig = None): + """构建图""" + workflow = StateGraph(State, config_schema=ReActConfiguration) + workflow.add_node("react", self.llm_call) + workflow.add_node("tools", ToolNode(tools=self._get_tools(config_schema))) + workflow.add_edge(START, "react") + workflow.add_conditional_edges( + "react", + tools_condition, + ) + workflow.add_edge("tools", "react") + workflow.add_edge("react", END) + + graph = workflow.compile(checkpointer=MemorySaver()) + return graph + + def stream_values(self, messages: list[str], config_schema: RunnableConfig = None): + graph = self.get_graph(config_schema) + for event in graph.stream({"messages": messages}, stream_mode="values", config=config_schema): + yield event["messages"] + + def stream_messages(self, messages: list[str], config_schema: RunnableConfig = None): + graph = self.get_graph(config_schema) + conf = ReActConfiguration.from_runnable_config(config_schema) + for msg, metadata in graph.stream({"messages": messages}, stream_mode="messages", config=config_schema): + msg_type = msg.type + + return_keys =conf.return_keys + if not return_keys or msg_type in return_keys: + yield msg, metadata + +def main(): + agent = ReActAgent(ReActConfiguration()) + + thread_id = str(uuid.uuid4()) + config = {"configurable": {"thread_id": thread_id}} + + from src.agents.utils import agent_cli + agent_cli(agent, config) + + +if __name__ == "__main__": + main() + # asyncio.run(main()) diff --git a/src/routers/tool_router.py b/src/routers/tool_router.py index ace06b65..8a44616b 100644 --- a/src/routers/tool_router.py +++ b/src/routers/tool_router.py @@ -3,7 +3,7 @@ from fastapi import APIRouter, Body from pydantic import BaseModel from typing import List, Dict, Any, Optional -from src.utils import logger +from src.agents import agent_manager tool = APIRouter(prefix="/tool") @@ -15,6 +15,7 @@ class Tool(BaseModel): url: str method: Optional[str] = "POST" params: Optional[Dict[str, Any]] = None + metadata: Optional[Dict[str, Any]] = None @tool.get("/", response_model=List[Tool]) async def route_index(): @@ -41,6 +42,18 @@ async def route_index(): ) ] + for agent in agent_manager.agents.values(): + tools.append( + Tool( + name=agent.name, + title=agent.name, + description=agent.description, + url=f"/agent/{agent.name}", + method="POST", + metadata=agent.config_schema, + ) + ) + return tools @tool.post("/text-chunking")