Merge pull request #120 from xerrors/agent-update

Agent 部分更新(20%)
This commit is contained in:
Wenjie Zhang 2025-04-05 02:29:42 +08:00 committed by GitHub
commit 5fd20b9bbd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 2730 additions and 1224 deletions

View File

@ -16,4 +16,5 @@ RUN apt-get update && apt-get install ffmpeg libsm6 libxext6 -y
# 复制代码到容器中
COPY ../src /app/src
COPY ../server /app/server

View File

@ -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请添加此环境变量或者注释此行

40
server/db_manager.py Normal file
View File

@ -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()

View File

@ -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

View File

@ -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
}

View File

@ -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)

View File

@ -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"}

View File

@ -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 != "<tool_call>":
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 != "<tool_call>":
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')

View File

@ -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(

View File

@ -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(),
)
)

View File

@ -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": "智能体的驱动模型"
},
)

View File

@ -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())

View File

@ -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())

View File

@ -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)

View File

@ -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

View File

@ -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),
}

View File

@ -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()

View File

@ -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)

View File

@ -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 = {}

View File

@ -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):

View File

@ -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)

View File

@ -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);
}

File diff suppressed because it is too large Load Diff

View File

@ -6,6 +6,9 @@
<!-- 助手消息 -->
<div v-else-if="message.role === 'assistant' || message.role === 'received'" class="assistant-message">
<!-- 推理过程 (ChatComponent特有) -->
<p v-if="debugMode">
{{ message.status }}
</p>
<div v-if="message.reasoning_content" class="reasoning-box">
<a-collapse v-model:activeKey="reasoningActiveKey" :bordered="false">
<template #expandIcon="{ isActive }">
@ -50,7 +53,7 @@
<slot name="tool-calls"></slot>
<div v-if="(message.role=='received' || message.role=='assistant') && message.status=='finished' && showRefs">
<RefsComponent :message="message" @retry="emit('retry')" />
<RefsComponent :message="message" :show-refs="showRefs" @retry="emit('retry')" />
</div>
<!-- 错误消息 -->
</div>
@ -77,11 +80,6 @@ const props = defineProps({
type: Object,
required: true
},
// HTML
contentHtml: {
type: String,
default: ''
},
//
isProcessing: {
type: Boolean,
@ -94,9 +92,13 @@ const props = defineProps({
},
//
showRefs: {
type: [Array, Boolean],
default: () => false
},
debugMode: {
type: Boolean,
default: false
}
},
});
const statusDefination = {

View File

@ -4,19 +4,23 @@
<!-- <span class="item btn" @click="likeThisResponse(msg)"><LikeOutlined /></span> -->
<!-- <span class="item btn" @click="dislikeThisResponse(msg)"><DislikeOutlined /></span> -->
<span v-if="msg.meta?.server_model_name" class="item"><BulbOutlined /> {{ msg.meta.server_model_name }}</span>
<span class="item btn" @click="copyText(msg.content)" title="复制"><CopyOutlined /></span>
<span class="item btn" @click="regenerateMessage()" title="重新生成"><ReloadOutlined /></span>
<span
v-if="showKey('copy')"
class="item btn" @click="copyText(msg.content)" title="复制"><CopyOutlined /></span>
<span
v-if="showKey('regenerate')"
class="item btn" @click="regenerateMessage()" title="重新生成"><ReloadOutlined /></span>
<span
v-if="showKey('subGraph') && hasSubGraphData(msg)"
class="item btn"
@click="openSubGraph(msg)"
v-if="hasSubGraphData(msg)"
>
<DeploymentUnitOutlined /> 关系图
</span>
<span
class="item btn"
@click="showWebResult(msg)"
v-if="msg.refs?.web_search.results.length > 0"
v-if="showKey('webSearch') && msg.refs?.web_search.results.length > 0"
>
<GlobalOutlined /> 网页搜索 {{ msg.refs.web_search?.results.length }}
</span>
@ -120,6 +124,10 @@ import GraphContainer from './GraphContainer.vue' // 导入 GraphContainer 组
const emit = defineEmits(['retry']);
const props = defineProps({
message: Object,
showRefs: {
type: [Array, Boolean],
default: () => false
}
})
const msg = ref(props.message)
@ -127,6 +135,13 @@ const msg = ref(props.message)
// 使 useClipboard
const { copy, isSupported } = useClipboard()
const showKey = (key) => {
if (props.showRefs === true) {
return true
}
return props.showRefs.includes(key)
}
// copy
const copyText = async (text) => {
if (isSupported) {

View File

@ -0,0 +1,258 @@
<template>
<div class="token-manager">
<div class="token-tools">
<a-button type="primary" size="small" @click="showAddTokenModal">
<PlusOutlined /> 创建 Token
</a-button>
</div>
<!-- 令牌列表 -->
<div class="token-list" v-if="tokens.length > 0">
<a-spin :spinning="loading">
<a-list size="small">
<a-list-item v-for="token in tokens" :key="token.id">
<div class="token-item">
<div class="token-info">
<div class="token-name">{{ token.name }}</div>
<div class="token-value">
<code>{{ token.token }}</code>
<a-button type="link" size="small" @click="copyToken(token.token)">
<CopyOutlined />
</a-button>
</div>
<div class="token-time">创建时间: {{ formatDate(token.created_at) }}</div>
</div>
<div class="token-actions">
<a-popconfirm
title="确定要删除这个令牌吗?"
ok-text="确定"
cancel-text="取消"
@confirm="deleteToken(token.id)"
>
<a-button type="text" danger size="small">
<DeleteOutlined />
</a-button>
</a-popconfirm>
</div>
</div>
</a-list-item>
</a-list>
</a-spin>
</div>
<a-empty v-else description="暂无访问令牌" :image="Empty.PRESENTED_IMAGE_SIMPLE" />
<!-- 添加令牌弹窗 -->
<a-modal
v-model:open="addTokenModalVisible"
title="添加访问令牌"
ok-text="创建"
cancel-text="取消"
@ok="createToken"
>
<a-form :model="newToken" layout="vertical">
<a-form-item label="令牌名称" name="name">
<a-input v-model:value="newToken.name" placeholder="请输入令牌名称" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue';
import { message, Empty } from 'ant-design-vue';
import { PlusOutlined, DeleteOutlined, CopyOutlined } from '@ant-design/icons-vue';
const props = defineProps({
agentId: {
type: String,
required: true
}
});
//
const tokens = ref([]);
const loading = ref(false);
const addTokenModalVisible = ref(false);
const newToken = ref({
name: ''
});
//
const fetchTokens = async () => {
loading.value = true;
try {
const response = await fetch(`/api/admin/tokens?agent_id=${props.agentId}`);
if (response.ok) {
const data = await response.json();
tokens.value = data;
} else {
message.error('获取令牌列表失败');
}
} catch (error) {
console.error('获取令牌列表出错:', error);
message.error('获取令牌列表出错');
} finally {
loading.value = false;
}
};
//
const createToken = async () => {
if (!newToken.value.name.trim()) {
message.warning('请输入令牌名称');
return;
}
try {
const response = await fetch('/api/admin/tokens', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
agent_id: props.agentId,
name: newToken.value.name
})
});
if (response.ok) {
const data = await response.json();
tokens.value.push(data);
message.success('令牌创建成功');
addTokenModalVisible.value = false;
newToken.value.name = '';
} else {
message.error('创建令牌失败');
}
} catch (error) {
console.error('创建令牌出错:', error);
message.error('创建令牌出错');
}
};
//
const deleteToken = async (tokenId) => {
try {
const response = await fetch(`/api/admin/tokens/${tokenId}`, {
method: 'DELETE'
});
if (response.ok) {
tokens.value = tokens.value.filter(token => token.id !== tokenId);
message.success('令牌已删除');
} else {
message.error('删除令牌失败');
}
} catch (error) {
console.error('删除令牌出错:', error);
message.error('删除令牌出错');
}
};
//
const copyToken = (token) => {
navigator.clipboard.writeText(token).then(() => {
message.success('令牌已复制到剪贴板');
});
};
//
const showAddTokenModal = () => {
newToken.value.name = '';
addTokenModalVisible.value = true;
};
//
const formatDate = (dateString) => {
if (!dateString) return '';
const date = new Date(dateString);
return date.toLocaleString();
};
// agentId
watch(() => props.agentId, (newAgentId) => {
if (newAgentId) {
fetchTokens();
} else {
tokens.value = [];
}
}, { immediate: true });
//
onMounted(() => {
if (props.agentId) {
fetchTokens();
}
});
</script>
<style lang="less" scoped>
.token-manager {
margin-top: 1rem;
// padding: 0 0.5rem;
}
.manager-title {
font-size: 1rem;
margin-bottom: 1rem;
}
.token-tools {
margin-bottom: 1rem;
display: flex;
justify-content: flex-end;
}
.token-list {
max-height: calc(100vh - 400px);
overflow-y: auto;
li.ant-list-item {
background-color: var(--gray-100);
border-radius: 0.5rem;
padding: 0.5rem;
margin-bottom: 0.5rem;
}
}
.token-item {
display: flex;
justify-content: space-between;
width: 100%;
}
.token-info {
flex: 1;
}
.token-name {
font-weight: 500;
margin-bottom: 0.25rem;
}
.token-value {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.8rem;
background-color: var(--main-light-4);
padding: 0.25rem 0.5rem;
border-radius: 4px;
margin-bottom: 0.25rem;
overflow-x: auto;
user-select: all;
code {
flex: 1;
}
}
.token-time {
font-size: 0.75rem;
color: var(--gray-500);
}
.token-actions {
display: flex;
align-items: center;
}
</style>

View File

@ -31,29 +31,29 @@ const router = createRouter({
}
]
},
{
path: '/agent',
name: 'agent',
component: AppLayout,
children: [
{
path: '',
name: 'AgentMain',
component: () => import('../views/AgentView.vue'),
meta: { keepAlive: true }
},
{
path: ':agent_id',
name: 'AgentSinglePage',
component: () => import('../components/AgentSingleViewComponent.vue'),
meta: { keepAlive: false }
}
]
},
// {
// path: '/agent',
// name: 'agent',
// component: AppLayout,
// children: [
// {
// path: '',
// name: 'AgentMain',
// component: () => import('../views/AgentView.vue'),
// meta: { keepAlive: true }
// },
// {
// path: ':agent_id',
// name: 'AgentSinglePage',
// component: () => import('../components/AgentSingleViewComponent.vue'),
// meta: { keepAlive: false }
// }
// ]
// },
{
path: '/agent/:agent_id',
name: 'AgentSinglePage',
component: () => import('../views/AgentView.vue'),
component: () => import('../views/AgentSingleView.vue'),
},
{
path: '/graph',

View File

@ -0,0 +1,163 @@
<template>
<div class="agent-single-view">
<!-- Token验证弹窗 -->
<a-modal
v-model:open="tokenModalVisible"
title="访问验证"
:closable="false"
:maskClosable="false"
:keyboard="false"
:footer="null"
width="500px"
>
<div class="token-verify-form">
<p>需要输入访问令牌才能使用该智能体</p>
<a-input-password
v-model:value="tokenInput"
placeholder="请输入访问令牌"
@pressEnter="verifyToken"
/>
<div class="error-message" v-if="errorMessage">{{ errorMessage }}</div>
<div class="token-actions">
<a-button type="primary" :loading="verifying" @click="verifyToken">验证</a-button>
</div>
</div>
</a-modal>
<!-- 智能体聊天界面 -->
<AgentChatComponent v-if="isVerified" :agent-id="agentId" />
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import AgentChatComponent from '@/components/AgentChatComponent.vue';
import { message } from 'ant-design-vue';
const route = useRoute();
const router = useRouter();
const agentId = computed(() => route.params.agent_id);
// Token
const tokenModalVisible = ref(false);
const tokenInput = ref('');
const isVerified = ref(false);
const verifying = ref(false);
const errorMessage = ref('');
// Token
const verifyToken = async () => {
if (!tokenInput.value) {
errorMessage.value = '请输入访问令牌';
return;
}
verifying.value = true;
errorMessage.value = '';
try {
const response = await fetch('/api/admin/verify_token', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
agent_id: agentId.value,
token: tokenInput.value
})
});
if (response.ok) {
//
isVerified.value = true;
tokenModalVisible.value = false;
// localStorage
localStorage.setItem(`agent-token-${agentId.value}`, tokenInput.value);
} else {
//
const data = await response.json();
errorMessage.value = data.detail || '令牌验证失败';
}
} catch (error) {
console.error('验证令牌出错:', error);
errorMessage.value = '验证令牌时发生错误';
} finally {
verifying.value = false;
}
};
//
const checkVerification = async () => {
const savedToken = localStorage.getItem(`agent-token-${agentId.value}`);
if (savedToken) {
// 使
verifying.value = true;
try {
const response = await fetch('/api/admin/verify_token', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
agent_id: agentId.value,
token: savedToken
})
});
if (response.ok) {
//
isVerified.value = true;
tokenInput.value = savedToken; //
} else {
//
localStorage.removeItem(`agent-token-${agentId.value}`);
tokenModalVisible.value = true;
}
} catch (error) {
console.error('验证令牌出错:', error);
tokenModalVisible.value = true;
} finally {
verifying.value = false;
}
} else {
//
tokenModalVisible.value = true;
}
};
//
onMounted(() => {
checkVerification();
});
</script>
<style lang="less" scoped>
.agent-single-view {
width: 100%;
height: 100vh;
overflow: hidden;
}
.token-verify-form {
display: flex;
flex-direction: column;
gap: 1rem;
.error-message {
color: #ff4d4f;
font-size: 0.85rem;
}
.token-actions {
display: flex;
justify-content: space-between;
margin-top: 0.5rem;
}
}
</style>

File diff suppressed because it is too large Load Diff