diff --git a/docker/api.Dockerfile b/docker/api.Dockerfile index 068bdc63..2bd86612 100644 --- a/docker/api.Dockerfile +++ b/docker/api.Dockerfile @@ -16,4 +16,5 @@ RUN apt-get update && apt-get install ffmpeg libsm6 libxext6 -y # 复制代码到容器中 COPY ../src /app/src +COPY ../server /app/server diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index dfd09a6f..28cab182 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -6,6 +6,7 @@ services: container_name: api-dev working_dir: /app volumes: + - ../server:/app/server - ../src:/app/src - ../saves_dev:/app/saves # - ${MODEL_DIR}:/models # 如果出现 undefined volume MODEL_DIR: invalid compose project,请添加此环境变量或者注释此行 diff --git a/server/db_manager.py b/server/db_manager.py new file mode 100644 index 00000000..0a05bcb9 --- /dev/null +++ b/server/db_manager.py @@ -0,0 +1,40 @@ +import os +import sqlite3 +import pathlib +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from sqlalchemy.ext.declarative import declarative_base + +from server.models.token_model import Base, AgentToken + +class DBManager: + """数据库管理器""" + + def __init__(self): + self.db_path = os.path.join("saves", "data", "server.db") + self.ensure_db_dir() + + # 创建SQLAlchemy引擎 + self.engine = create_engine(f"sqlite:///{self.db_path}") + + # 创建会话工厂 + self.Session = sessionmaker(bind=self.engine) + + # 确保表存在 + self.create_tables() + + def ensure_db_dir(self): + """确保数据库目录存在""" + db_dir = os.path.dirname(self.db_path) + pathlib.Path(db_dir).mkdir(parents=True, exist_ok=True) + + def create_tables(self): + """创建数据库表""" + Base.metadata.create_all(self.engine) + + def get_session(self): + """获取数据库会话""" + return self.Session() + +# 创建全局数据库管理器实例 +db_manager = DBManager() \ No newline at end of file diff --git a/src/main.py b/server/main.py similarity index 92% rename from src/main.py rename to server/main.py index 8280996d..1d144459 100644 --- a/src/main.py +++ b/server/main.py @@ -2,7 +2,7 @@ import uvicorn from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from src.routers import router +from server.routers import router from src.utils.logging_config import logger diff --git a/server/models/token_model.py b/server/models/token_model.py new file mode 100644 index 00000000..74ee1d59 --- /dev/null +++ b/server/models/token_model.py @@ -0,0 +1,24 @@ +from sqlalchemy import Column, Integer, String, DateTime, ForeignKey +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.sql import func + +Base = declarative_base() + +class AgentToken(Base): + """智能体访问令牌模型""" + __tablename__ = 'agent_tokens' + + id = Column(Integer, primary_key=True, autoincrement=True) + agent_id = Column(String, nullable=False, index=True) # 智能体ID + name = Column(String, nullable=False) # 令牌名称 + token = Column(String, nullable=False, unique=True) # 令牌值 + created_at = Column(DateTime, default=func.now()) # 创建时间 + + def to_dict(self): + return { + "id": self.id, + "agent_id": self.agent_id, + "name": self.name, + "token": self.token, + "created_at": self.created_at.isoformat() if self.created_at else None + } \ No newline at end of file diff --git a/server/routers/__init__.py b/server/routers/__init__.py new file mode 100644 index 00000000..6878d934 --- /dev/null +++ b/server/routers/__init__.py @@ -0,0 +1,13 @@ +from fastapi import APIRouter +from server.routers.chat_router import chat +from server.routers.data_router import data +from server.routers.base_router import base +from server.routers.tool_router import tool +from server.routers.admin_router import admin + +router = APIRouter() +router.include_router(base) +router.include_router(chat) +router.include_router(data) +router.include_router(tool) +router.include_router(admin) diff --git a/server/routers/admin_router.py b/server/routers/admin_router.py new file mode 100644 index 00000000..9471b6bc --- /dev/null +++ b/server/routers/admin_router.py @@ -0,0 +1,103 @@ +import secrets +import string +from fastapi import APIRouter, Depends, HTTPException, Query +from pydantic import BaseModel +from typing import List, Optional +from sqlalchemy.orm import Session + +from server.db_manager import db_manager +from server.models.token_model import AgentToken + +admin = APIRouter(prefix="/admin", tags=["admin"]) + +# 依赖项:获取数据库会话 +def get_db(): + db = db_manager.get_session() + try: + yield db + finally: + db.close() + +# 请求和响应模型 +class TokenCreate(BaseModel): + agent_id: str + name: str + +class TokenVerify(BaseModel): + agent_id: str + token: str + +class TokenResponse(BaseModel): + id: int + agent_id: str + name: str + token: str + created_at: str + +# 生成随机token +def generate_token(length=32): + alphabet = string.ascii_letters + string.digits + return ''.join(secrets.choice(alphabet) for _ in range(length)) + +@admin.get("/tokens", response_model=List[TokenResponse]) +async def get_agent_tokens( + agent_id: Optional[str] = Query(None), + db: Session = Depends(get_db) +): + """获取智能体的token列表""" + query = db.query(AgentToken) + if agent_id: + query = query.filter(AgentToken.agent_id == agent_id) + tokens = query.all() + return [token.to_dict() for token in tokens] + +@admin.post("/tokens", response_model=TokenResponse) +async def create_token( + token_data: TokenCreate, + db: Session = Depends(get_db) +): + """创建新的token""" + # 生成随机token + token_value = generate_token() + + # 创建token记录 + new_token = AgentToken( + agent_id=token_data.agent_id, + name=token_data.name, + token=token_value + ) + + # 保存到数据库 + db.add(new_token) + db.commit() + db.refresh(new_token) + + return new_token.to_dict() + +@admin.delete("/tokens/{token_id}", response_model=dict) +async def delete_token(token_id: int, db: Session = Depends(get_db)): + """删除token""" + token = db.query(AgentToken).filter(AgentToken.id == token_id).first() + if not token: + raise HTTPException(status_code=404, detail="Token not found") + + db.delete(token) + db.commit() + + return {"success": True, "message": "Token deleted"} + +@admin.post("/verify_token") +async def verify_agent_token( + token_data: TokenVerify, + db: Session = Depends(get_db) +): + """验证智能体访问令牌""" + token = db.query(AgentToken).filter( + AgentToken.agent_id == token_data.agent_id, + AgentToken.token == token_data.token + ).first() + + if not token: + raise HTTPException(status_code=401, detail="Invalid token") + + return {"success": True, "message": "Token verified"} \ No newline at end of file diff --git a/src/routers/base_router.py b/server/routers/base_router.py similarity index 100% rename from src/routers/base_router.py rename to server/routers/base_router.py diff --git a/src/routers/chat_router.py b/server/routers/chat_router.py similarity index 70% rename from src/routers/chat_router.py rename to server/routers/chat_router.py index 4bf81093..55959402 100644 --- a/src/routers/chat_router.py +++ b/server/routers/chat_router.py @@ -1,8 +1,9 @@ +import os import json import asyncio import traceback import uuid -from fastapi import APIRouter, Body +from fastapi import APIRouter, Body, Depends, HTTPException from fastapi.responses import StreamingResponse from langchain_core.messages import AIMessageChunk @@ -14,8 +15,6 @@ from src.utils.logging_config import logger chat = APIRouter(prefix="/chat") - - @chat.get("/") async def chat_get(): return "Chat Get!" @@ -123,60 +122,77 @@ async def call(query: str = Body(...), meta: dict = Body(None)): @chat.get("/agent") async def get_agent(): - agents = [{ - "name": agent.name, - "description": agent.description, - "config_schema": agent.config_schema - } for agent in agent_manager.agents.values()] + agents = [agent.get_info() 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)): + config: dict = Body({}), + meta: dict = Body({})): - 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": [] - } - } + meta.update({ + "query": query, + "agent_name": agent_name, + "server_model_name": config.get("model", agent_name) , + "thread_id": config.get("thread_id"), + }) + # 将meta和thread_id整合到config中 def make_chunk(content=None, **kwargs): + return json.dumps({ + "request_id": meta.get("request_id"), "response": content, - "model_name": agent_name, - "meta": meta, **kwargs }, ensure_ascii=False).encode('utf-8') + b"\n" + try: + agent = agent_manager.get_runnable_agent(agent_name) + except Exception as e: + logger.error(f"Error getting agent {agent_name}: {e}") + return StreamingResponse(make_chunk(message=f"Error getting agent {agent_name}: {e}", status="error"), media_type='application/json') + + # 从config中获取history_round + history_round = config.get("history_round") + history_manager = HistoryManager(history) + messages = history_manager.get_history_with_msg(query, max_rounds=history_round) + history_manager.add_user(query) + + # 如果没有thread_id则生成一个 + if "thread_id" not in config or not config["thread_id"]: + config["thread_id"] = str(uuid.uuid4()) + + # 构造运行时配置 + runnable_config = { + "configurable": { + **config + } + } + 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="init", meta=meta) + try: + for msg, metadata in agent.stream_messages(messages, config_schema=runnable_config): + 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)) + yield make_chunk(status="finished", + history=history_manager.update_ai(content), + meta=meta) + except Exception as e: + logger.error(f"Error streaming messages: {e}") + yield make_chunk(message=f"Error streaming messages: {e}", status="error") return StreamingResponse(stream_messages(), media_type='application/json') diff --git a/src/routers/data_router.py b/server/routers/data_router.py similarity index 98% rename from src/routers/data_router.py rename to server/routers/data_router.py index c2a19aa5..27bb04ca 100644 --- a/src/routers/data_router.py +++ b/server/routers/data_router.py @@ -72,7 +72,7 @@ async def create_document_by_file(db_id: str = Body(...), files: List[str] = Bod @data.post("/add-by-chunks") async def add_by_chunks(db_id: str = Body(...), file_chunks: dict = Body(...)): - logger.debug(f"Add chunks in {db_id}: {file_chunks}") + # logger.debug(f"Add chunks in {db_id}: {len(file_chunks)} chunks") try: loop = asyncio.get_event_loop() await loop.run_in_executor( diff --git a/src/routers/tool_router.py b/server/routers/tool_router.py similarity index 97% rename from src/routers/tool_router.py rename to server/routers/tool_router.py index 8a44616b..73c6d071 100644 --- a/src/routers/tool_router.py +++ b/server/routers/tool_router.py @@ -50,7 +50,7 @@ async def route_index(): description=agent.description, url=f"/agent/{agent.name}", method="POST", - metadata=agent.config_schema, + metadata=agent.config_schema.to_dict(), ) ) diff --git a/src/agents/chatbot/configuration.py b/src/agents/chatbot/configuration.py index 2296f354..fbdb68bc 100644 --- a/src/agents/chatbot/configuration.py +++ b/src/agents/chatbot/configuration.py @@ -1,36 +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 ChatbotConfiguration(Configuration): - """Chatbot 的配置""" + """Chatbot 的配置 + + 配置说明: + + metadata 中 configurable 为 True 的配置项可以被用户配置, + configurable 为 False 的配置项不能被用户配置,只能由开发者预设。 + """ system_prompt: str = field( - default=f"You are a helpful assistant. Now is {datetime.now(tz=timezone.utc).isoformat()}", + default="You are a helpful assistant.", metadata={ - "description": "The system prompt to use for the agent's interactions. " - "This prompt sets the context and behavior for the agent." + "name": "系统提示词", + "configurable": True, + "description": "用来描述智能体的角色和行为" }, ) 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." + "name": "智能体模型", + "configurable": True, + "options": [ + "zhipu/glm-4-plus", + "siliconflow/Qwen/QwQ-32B", + "siliconflow/deepseek-ai/DeepSeek-V3", + ], + "description": "智能体的驱动模型" }, ) - diff --git a/src/agents/chatbot/graph.py b/src/agents/chatbot/graph.py index 51c362d9..1d4162ce 100644 --- a/src/agents/chatbot/graph.py +++ b/src/agents/chatbot/graph.py @@ -1,48 +1,62 @@ import asyncio import uuid from typing import Any -from datetime import datetime +from datetime import datetime, timezone 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 langgraph.checkpoint.memory import MemorySaver # 实际上没有起作用 +from src.utils import logger from src.agents.registry import State, BaseAgent -from src.agents.utils import load_chat_model -from src.agents.chatbot.configuration import ChatbotConfiguration, multiply +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 _TOOLS_REGISTRY class ChatbotAgent(BaseAgent): name = "chatbot" description = "A chatbot that can answer questions and help with tasks." - _graph_cache = None - config_schema = ChatbotConfiguration.to_dict() + requirements = ["TAVILY_API_KEY", "ZHIPUAI_API_KEY"] + all_tools = ["TavilySearchResults", "multiply", "add", "subtract", "divide"] + config_schema = ChatbotConfiguration def __init__(self, **kwargs): super().__init__(**kwargs) def _get_tools(self, config_schema: RunnableConfig): - """根据配置获取工具""" - tools = [multiply, TavilySearchResults(max_results=10)] - return tools + """根据配置获取工具,如果配置为空,则使用所有工具,如果配置为列表,则使用列表中的工具, + 如果配置为其他类型,则抛出错误""" + conf_tools = config_schema.get("tools") + if conf_tools == None: + tool_names = self.all_tools + elif isinstance(conf_tools, list): + tool_names = [tool for tool in self.all_tools if tool in conf_tools] + else: + raise ValueError(f"tools 配置错误: {conf_tools}") + + logger.info(f"Tools: {tool_names}") + return [_TOOLS_REGISTRY[tool] for tool in tool_names] def llm_call(self, state: State, config: RunnableConfig = None) -> dict[str, Any]: """调用 llm 模型""" config_schema = config or {} - conf = ChatbotConfiguration.from_runnable_config(config_schema) + conf = self.config_schema.from_runnable_config(config_schema) + + system_prompt = f"{conf.system_prompt} Now is {get_cur_time_with_utc()}" model = load_chat_model(conf.model) model_with_tools = model.bind_tools(self._get_tools(config_schema)) + logger.info(f"llm_call with config: {conf}, {conf.model}") res = model_with_tools.invoke( - [{"role": "system", "content": conf.system_prompt}, *state["messages"]] + [{"role": "system", "content": system_prompt}, *state["messages"]] ) return {"messages": [res]} - def get_graph(self, config_schema: RunnableConfig = None): + def get_graph(self, config_schema: RunnableConfig = None, **kwargs): """构建图""" - workflow = StateGraph(State, config_schema=ChatbotConfiguration) + workflow = StateGraph(State, config_schema=self.config_schema) workflow.add_node("chatbot", self.llm_call) workflow.add_node("tools", ToolNode(tools=self._get_tools(config_schema))) workflow.add_edge(START, "chatbot") @@ -56,20 +70,6 @@ class ChatbotAgent(BaseAgent): 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 = ChatbotConfiguration.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 = ChatbotAgent(ChatbotConfiguration()) diff --git a/src/agents/react/graph.py b/src/agents/react/graph.py index 02650c83..f67b1854 100644 --- a/src/agents/react/graph.py +++ b/src/agents/react/graph.py @@ -17,60 +17,12 @@ 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): + def get_graph(self, **kwargs): """构建图""" - 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()) + from .workflows import graph 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()) diff --git a/src/agents/react/workflows.py b/src/agents/react/workflows.py new file mode 100644 index 00000000..1337d133 --- /dev/null +++ b/src/agents/react/workflows.py @@ -0,0 +1,36 @@ +import os + +from langchain_openai import ChatOpenAI + +model = ChatOpenAI(model="glm-4-plus", + api_key=os.getenv("ZHIPUAI_API_KEY"), + base_url="https://open.bigmodel.cn/api/paas/v4/", + temperature=0) + + +# For this tutorial we will use custom tool that returns pre-defined values for weather in two cities (NYC & SF) + +from typing import Literal + +from langchain_core.tools import tool + + +@tool +def get_weather(city: Literal["nyc", "sf"]): + """Use this to get weather information.""" + if city == "nyc": + return "It might be cloudy in nyc" + elif city == "sf": + return "It's always sunny in sf" + else: + raise AssertionError("Unknown city") + + +tools = [get_weather] + + +# Define the graph + +from langgraph.prebuilt import create_react_agent + +graph = create_react_agent(model, tools=tools) diff --git a/src/agents/registry.py b/src/agents/registry.py index 7c365ed2..dc974bd7 100644 --- a/src/agents/registry.py +++ b/src/agents/registry.py @@ -1,5 +1,7 @@ from __future__ import annotations +import os + from typing import Type, Annotated, Optional, TypedDict from abc import abstractmethod from dataclasses import dataclass, fields, field @@ -10,7 +12,7 @@ from langgraph.graph.state import CompiledStateGraph from langgraph.graph.message import add_messages from src.config import SimpleConfig - +from src.utils import logger class State(TypedDict): """ @@ -24,7 +26,7 @@ class State(TypedDict): @dataclass(kw_only=True) -class Configuration(SimpleConfig): +class Configuration(dict): """ 定义一个基础 Configuration 供 各类 graph 继承 """ @@ -40,15 +42,73 @@ class Configuration(SimpleConfig): @classmethod def to_dict(cls): - return {f.name: getattr(cls, f.name) for f in fields(cls) if f.init} + # 创建一个实例来处理 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"): + 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: str = field(default="base_agent") + description: str = field(default="base_agent") + config_schema: Configuration = Configuration + requirements: list[str] + def __init__(self, **kwargs): - pass + self.check_requirements() + + @classmethod + def get_info(cls): + return { + "name": cls.name, + "description": cls.description, + "config_schema": cls.config_schema.to_dict(), + "requirements": cls.requirements if hasattr(cls, "requirements") else [], + "all_tools": cls.all_tools if hasattr(cls, "all_tools") else [], + } + + def check_requirements(self): + if not hasattr(self, "requirements") or not self.requirements: + return + for requirement in self.requirements: + if requirement not in os.environ: + raise ValueError(f"{requirement} is not set") + + def stream_values(self, messages: list[str], config_schema: RunnableConfig = None, **kwargs): + graph = self.get_graph(config_schema=config_schema, **kwargs) + 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, **kwargs): + graph = self.get_graph(config_schema=config_schema, **kwargs) + + for msg, metadata in graph.stream({"messages": messages}, stream_mode="messages", config=config_schema): + yield msg, metadata @abstractmethod - def get_graph(self) -> CompiledStateGraph: + def get_graph(self, **kwargs) -> CompiledStateGraph: pass \ No newline at end of file diff --git a/src/agents/tools_factory.py b/src/agents/tools_factory.py index 95d04426..61369af4 100644 --- a/src/agents/tools_factory.py +++ b/src/agents/tools_factory.py @@ -5,11 +5,8 @@ from typing import Any, Callable, Optional, Type, Union from pydantic import BaseModel, Field -from langchain.agents import tool -from langchain_core.tools import Tool, BaseTool - - -_TOOLS_REGISTRY = {} +from langchain_core.tools import tool, BaseTool +from langchain_community.tools.tavily_search import TavilySearchResults # refs https://github.com/chatchat-space/LangGraph-Chatchat chatchat-server/chatchat/server/agent/tools_factory/tools_registry.py def regist_tool( @@ -95,3 +92,33 @@ class BaseToolOutput: else: return str(self.data) + + +@tool +def multiply(first_int: int, second_int: int) -> int: + """Multiply two integers together.""" + return first_int * second_int + +@tool +def add(first_int: int, second_int: int) -> int: + """Add two integers together.""" + return first_int + second_int + +@tool +def subtract(first_int: int, second_int: int) -> int: + """Subtract two integers.""" + return first_int - second_int + +@tool +def divide(first_int: int, second_int: int) -> int: + """Divide two integers.""" + return first_int / second_int + + +_TOOLS_REGISTRY = { + "multiply": multiply, + "add": add, + "subtract": subtract, + "divide": divide, + "TavilySearchResults": TavilySearchResults(max_results=10), +} diff --git a/src/agents/utils.py b/src/agents/utils.py index 9da3ee33..1d08636d 100644 --- a/src/agents/utils.py +++ b/src/agents/utils.py @@ -1,3 +1,5 @@ +from datetime import datetime, timezone + from src.models import select_model from src.agents.registry import BaseAgent from langchain_core.language_models import BaseChatModel @@ -7,14 +9,17 @@ from langchain_core.messages import AIMessageChunk, ToolMessage -def load_chat_model(fully_specified_name: str) -> BaseChatModel: +def load_chat_model(fully_specified_name: str, **kwargs) -> BaseChatModel: """Load a chat model from a fully specified name. Args: fully_specified_name (str): String in the format 'provider/model'. + **kwargs: Additional parameters to pass to the model. """ provider, model = fully_specified_name.split("/", maxsplit=1) - return select_model(model_name=model, model_provider=provider).chat_open_ai + model_instance = select_model(model_name=model, model_provider=provider) + + return model_instance.chat_open_ai def agent_cli(agent: BaseAgent, config: RunnableConfig = None): @@ -49,3 +54,7 @@ def agent_cli(agent: BaseAgent, config: RunnableConfig = None): if isinstance(msg, ToolMessage): print(f"Tool: {msg.content}") + +def get_cur_time_with_utc(): + return datetime.now(tz=timezone.utc).isoformat() + diff --git a/src/config/__init__.py b/src/config/__init__.py index 372e6305..b9fc17e2 100644 --- a/src/config/__init__.py +++ b/src/config/__init__.py @@ -28,7 +28,7 @@ class SimpleConfig(dict): return self.get(self.__key(key)) def __getitem__(self, key): - return super().get(self.__key(key)) + return self.get(self.__key(key)) def __setitem__(self, key, value): return super().__setitem__(self.__key(key), value) diff --git a/src/core/retriever.py b/src/core/retriever.py index ee52d915..871997ed 100644 --- a/src/core/retriever.py +++ b/src/core/retriever.py @@ -231,7 +231,7 @@ class Retriever: return formatted_results def format_query_results(self, results): - logger.debug(f"Graph Query Results: {results}") + # logger.debug(f"Graph Query Results: {results}") formatted_results = {"nodes": [], "edges": []} node_dict = {} diff --git a/src/models/chat_model.py b/src/models/chat_model.py index f71289e5..065d9e36 100644 --- a/src/models/chat_model.py +++ b/src/models/chat_model.py @@ -11,7 +11,8 @@ class OpenAIBase(): self.model_name = model_name self.chat_open_ai = ChatOpenAI(model=model_name, api_key=api_key, - base_url=base_url) + base_url=base_url, + temperature=0.7) def predict(self, message, stream=False): if isinstance(message, str): diff --git a/src/routers/__init__.py b/src/routers/__init__.py deleted file mode 100644 index c39810cd..00000000 --- a/src/routers/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from fastapi import APIRouter -from src.routers.chat_router import chat -from src.routers.data_router import data -from src.routers.base_router import base -from src.routers.tool_router import tool - -router = APIRouter() -router.include_router(base) -router.include_router(chat) -router.include_router(data) -router.include_router(tool) diff --git a/web/src/assets/markdown.css b/web/src/assets/markdown.css index ffaf9991..7acc8984 100644 --- a/web/src/assets/markdown.css +++ b/web/src/assets/markdown.css @@ -10,12 +10,17 @@ padding: 1rem; } -.message-md pre:has(code.hljs) { +.message-md pre:has(code) { padding: 0; } -.message-md pre code.hljs { - font-size: 0.8rem; +.message-md pre code { + font-size: 13px; + font-family: 'Menlo', 'Monaco', 'Consolas', 'PingFang SC', 'Microsoft YaHei', 'Hiragino Sans GB', 'Source Han Sans CN', 'Courier New', monospace; + line-height: 1.5; + letter-spacing: 0.025em; + tab-size: 4; + -moz-tab-size: 4; background-color: var(--gray-100); } diff --git a/web/src/components/AgentChatComponent.vue b/web/src/components/AgentChatComponent.vue new file mode 100644 index 00000000..d28caca3 --- /dev/null +++ b/web/src/components/AgentChatComponent.vue @@ -0,0 +1,1272 @@ + + + + + diff --git a/web/src/components/AgentSingleViewComponent.vue b/web/src/components/AgentSingleViewComponent.vue deleted file mode 100644 index e69de29b..00000000 diff --git a/web/src/components/MessageComponent.vue b/web/src/components/MessageComponent.vue index be18d974..53acf069 100644 --- a/web/src/components/MessageComponent.vue +++ b/web/src/components/MessageComponent.vue @@ -6,6 +6,9 @@
+

+ {{ message.status }} +