添加自定义工具功能,给未来的 MCP 打一下基础

This commit is contained in:
Wenjie Zhang 2025-04-05 17:27:52 +08:00
parent 0bd862069c
commit 693d05d808
10 changed files with 209 additions and 88 deletions

View File

@ -28,7 +28,7 @@ async def update_config(key = Body(...), value = Body(...)):
@base.post("/restart") @base.post("/restart")
async def restart(): async def restart():
knowledge_base.restart() knowledge_base.restart()
graph_base.restart() graph_base.start()
retriever.restart() retriever.restart()
return {"message": "Restarted!"} return {"message": "Restarted!"}

View File

@ -12,6 +12,7 @@ from src.core import HistoryManager
from src.agents import agent_manager from src.agents import agent_manager
from src.models import select_model from src.models import select_model
from src.utils.logging_config import logger from src.utils.logging_config import logger
from src.agents.tools_factory import get_all_tools
chat = APIRouter(prefix="/chat") chat = APIRouter(prefix="/chat")
@ -201,3 +202,8 @@ async def get_chat_models(model_provider: str):
"""获取指定模型提供商的模型列表""" """获取指定模型提供商的模型列表"""
model = select_model(model_provider=model_provider) model = select_model(model_provider=model_provider)
return {"models": model.get_models()} return {"models": model.get_models()}
@chat.get("/tools")
async def get_tools():
"""获取所有工具"""
return {"tools": list(get_all_tools().keys())}

View File

@ -34,3 +34,12 @@ class ChatbotConfiguration(Configuration):
"description": "智能体的驱动模型" "description": "智能体的驱动模型"
}, },
) )
tools: list[str] = field(
default_factory=list,
metadata={
"name": "工具",
"configurable": False,
"description": "工具列表"
},
)

View File

@ -13,31 +13,32 @@ from src.utils import logger
from src.agents.registry import State, BaseAgent from src.agents.registry import State, BaseAgent
from src.agents.utils import load_chat_model, get_cur_time_with_utc from src.agents.utils import load_chat_model, get_cur_time_with_utc
from src.agents.chatbot.configuration import ChatbotConfiguration from src.agents.chatbot.configuration import ChatbotConfiguration
from src.agents.tools_factory import _TOOLS_REGISTRY from src.agents.tools_factory import get_all_tools
class ChatbotAgent(BaseAgent): class ChatbotAgent(BaseAgent):
name = "chatbot" name = "chatbot"
description = "A chatbot that can answer questions and help with tasks." description = "基础的对话机器人,可以回答问题,默认不使用任何工具,可在配置中启用需要的工具。"
requirements = ["TAVILY_API_KEY", "ZHIPUAI_API_KEY"] requirements = ["TAVILY_API_KEY", "ZHIPUAI_API_KEY"]
all_tools = ["TavilySearchResults", "multiply", "add", "subtract", "divide"]
config_schema = ChatbotConfiguration config_schema = ChatbotConfiguration
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
def _get_tools(self, config_schema: RunnableConfig): def _get_tools(self, tools: list[str]):
"""根据配置获取工具,如果配置为空,则使用所有工具,如果配置为列表,则使用列表中的工具, """根据配置获取工具。
如果配置为其他类型则抛出错误""" 默认不使用任何工具
conf_tools = config_schema.get("tools") 如果配置为列表则使用列表中的工具
if conf_tools == None: """
tool_names = self.all_tools platform_tools = get_all_tools()
elif isinstance(conf_tools, list): if tools is None or not isinstance(tools, list) or len(tools) == 0:
tool_names = [tool for tool in self.all_tools if tool in conf_tools] # 默认不使用任何工具
logger.info("未配置工具或配置为空,不使用任何工具")
return []
else: else:
raise ValueError(f"tools 配置错误: {conf_tools}") # 使用配置中指定的工具
tool_names = [tool for tool in platform_tools.keys() if tool in tools]
logger.info(f"Tools: {tool_names}") logger.info(f"使用工具: {tool_names}")
return [_TOOLS_REGISTRY[tool] for tool in tool_names] return [platform_tools[tool] for tool in tool_names]
def llm_call(self, state: State, config: RunnableConfig = None) -> dict[str, Any]: def llm_call(self, state: State, config: RunnableConfig = None) -> dict[str, Any]:
"""调用 llm 模型""" """调用 llm 模型"""
@ -46,7 +47,7 @@ class ChatbotAgent(BaseAgent):
system_prompt = f"{conf.system_prompt} Now is {get_cur_time_with_utc()}" system_prompt = f"{conf.system_prompt} Now is {get_cur_time_with_utc()}"
model = load_chat_model(conf.model) model = load_chat_model(conf.model)
model_with_tools = model.bind_tools(self._get_tools(config_schema)) model_with_tools = model.bind_tools(self._get_tools(conf.tools))
logger.info(f"llm_call with config: {conf}, {conf.model}") logger.info(f"llm_call with config: {conf}, {conf.model}")
res = model_with_tools.invoke( res = model_with_tools.invoke(
@ -56,9 +57,10 @@ class ChatbotAgent(BaseAgent):
def get_graph(self, config_schema: RunnableConfig = None, **kwargs): def get_graph(self, config_schema: RunnableConfig = None, **kwargs):
"""构建图""" """构建图"""
conf = self.config_schema.from_runnable_config(config_schema)
workflow = StateGraph(State, config_schema=self.config_schema) workflow = StateGraph(State, config_schema=self.config_schema)
workflow.add_node("chatbot", self.llm_call) workflow.add_node("chatbot", self.llm_call)
workflow.add_node("tools", ToolNode(tools=self._get_tools(config_schema))) workflow.add_node("tools", ToolNode(tools=self._get_tools(conf.tools)))
workflow.add_edge(START, "chatbot") workflow.add_edge(START, "chatbot")
workflow.add_conditional_edges( workflow.add_conditional_edges(
"chatbot", "chatbot",

View File

@ -2,6 +2,8 @@ import os
from langchain_openai import ChatOpenAI from langchain_openai import ChatOpenAI
from src import graph_base
model = ChatOpenAI(model="glm-4-plus", model = ChatOpenAI(model="glm-4-plus",
api_key=os.getenv("ZHIPUAI_API_KEY"), api_key=os.getenv("ZHIPUAI_API_KEY"),
base_url="https://open.bigmodel.cn/api/paas/v4/", base_url="https://open.bigmodel.cn/api/paas/v4/",
@ -10,26 +12,14 @@ model = ChatOpenAI(model="glm-4-plus",
# For this tutorial we will use custom tool that returns pre-defined values for weather in two cities (NYC & SF) # 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 typing import Literal, Annotated
from langchain_core.tools import tool from langchain_core.tools import tool, StructuredTool
@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 = []
tools = [get_weather]
# Define the graph
from langgraph.prebuilt import create_react_agent from langgraph.prebuilt import create_react_agent

View File

@ -1,13 +1,15 @@
import json import json
import re import re
import os import os
from typing import Any, Callable, Optional, Type, Union from typing import Any, Callable, Optional, Type, Union, Annotated
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from langchain_core.tools import tool, BaseTool from langchain_core.tools import tool, BaseTool, StructuredTool
from langchain_community.tools.tavily_search import TavilySearchResults from langchain_community.tools.tavily_search import TavilySearchResults
from src import graph_base, knowledge_base
# refs https://github.com/chatchat-space/LangGraph-Chatchat chatchat-server/chatchat/server/agent/tools_factory/tools_registry.py # refs https://github.com/chatchat-space/LangGraph-Chatchat chatchat-server/chatchat/server/agent/tools_factory/tools_registry.py
def regist_tool( def regist_tool(
*args: Any, *args: Any,
@ -63,6 +65,24 @@ def regist_tool(
return t return t
class KnowledgeRetrieverModel(BaseModel):
query: str = Field(description="The query to get knowledge graph.")
def get_all_tools():
"""获取所有工具"""
tools = _TOOLS_REGISTRY.copy()
for db_Id, retrieve_info in knowledge_base.get_retrievers().items():
name = f"retrieve_{retrieve_info['name']}"
tools[name] = StructuredTool.from_function(
retrieve_info["retriever"],
name=name,
description=retrieve_info["description"],
args_schema=KnowledgeRetrieverModel)
return tools
class BaseToolOutput: class BaseToolOutput:
""" """
LLM 要求 Tool 的输出为 str Tool 用在别处时希望它正常返回结构化数据 LLM 要求 Tool 的输出为 str Tool 用在别处时希望它正常返回结构化数据
@ -92,33 +112,30 @@ class BaseToolOutput:
else: else:
return str(self.data) return str(self.data)
@tool
def calculator(a: float, b: float, operation: str) -> float:
"""Calculate two numbers."""
if operation == "add":
return a + b
elif operation == "subtract":
return a - b
elif operation == "multiply":
return a * b
elif operation == "divide":
return a / b
else:
raise ValueError(f"Invalid operation: {operation}, only support add, subtract, multiply, divide")
@tool @tool
def multiply(first_int: int, second_int: int) -> int: def get_knowledge_graph(query: Annotated[str, "The query to get knowledge graph."]):
"""Multiply two integers together.""" """Use this to get knowledge graph."""
return first_int * second_int return graph_base.query_node(query, hops=2)
@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 = { _TOOLS_REGISTRY = {
"multiply": multiply, "calculator": calculator,
"add": add,
"subtract": subtract,
"divide": divide,
"TavilySearchResults": TavilySearchResults(max_results=10), "TavilySearchResults": TavilySearchResults(max_results=10),
"get_knowledge_graph": get_knowledge_graph,
} }

View File

@ -209,6 +209,9 @@ class GraphDatabase:
def query_node(self, entity_name, hops=2, **kwargs): def query_node(self, entity_name, hops=2, **kwargs):
# TODO 添加判断节点数量为 0 停止检索 # TODO 添加判断节点数量为 0 停止检索
# 判断是否启动
if not self.is_running():
raise Exception("图数据库未启动")
logger.debug(f"Query graph node {entity_name} with {hops=}") logger.debug(f"Query graph node {entity_name} with {hops=}")
if kwargs.get("exact_match"): if kwargs.get("exact_match"):
@ -266,7 +269,7 @@ class GraphDatabase:
def query(tx, entity_name, hops): def query(tx, entity_name, hops):
result = tx.run(f""" result = tx.run(f"""
MATCH (n {{name: $entity_name}})-[r*1..{hops}]-(m) MATCH (n {{name: $entity_name}})-[r*1..{hops}]-(m)
RETURN n, r, m RETURN n {{.*, embedding: null}} AS n, r, m {{.*, embedding: null}} AS m
""", entity_name=entity_name) """, entity_name=entity_name)
return result.values() return result.values()
@ -279,7 +282,7 @@ class GraphDatabase:
def query(tx, hops): def query(tx, hops):
result = tx.run(f""" result = tx.run(f"""
MATCH (n)-[r*1..{hops}]->(m) MATCH (n)-[r*1..{hops}]->(m)
RETURN n, r, m RETURN n {{.*, embedding: null}} AS n, r, m {{.*, embedding: null}} AS m
""") """)
return result.values() return result.values()
@ -292,7 +295,7 @@ class GraphDatabase:
def query(tx, relationship_type, hops): def query(tx, relationship_type, hops):
result = tx.run(f""" result = tx.run(f"""
MATCH (n)-[r:`{relationship_type}`*1..{hops}]->(m) MATCH (n)-[r:`{relationship_type}`*1..{hops}]->(m)
RETURN n, r, m RETURN n {{.*, embedding: null}} AS n, r, m {{.*, embedding: null}} AS m
""") """)
return result.values() return result.values()
@ -307,7 +310,7 @@ class GraphDatabase:
MATCH (n:Entity) MATCH (n:Entity)
WHERE n.name CONTAINS $keyword WHERE n.name CONTAINS $keyword
MATCH (n)-[r*1..{hops}]->(m) MATCH (n)-[r*1..{hops}]->(m)
RETURN n, r, m RETURN n {{.*, embedding: null}} AS n, r, m {{.*, embedding: null}} AS m
""", keyword=keyword) """, keyword=keyword)
return result.values() return result.values()
@ -321,7 +324,7 @@ class GraphDatabase:
result = tx.run(f""" result = tx.run(f"""
MATCH (n {{name: $node_name}}) MATCH (n {{name: $node_name}})
OPTIONAL MATCH (n)-[r*1..{hops}]->(m) OPTIONAL MATCH (n)-[r*1..{hops}]->(m)
RETURN n, r, m RETURN n {{.*, embedding: null}} AS n, r, m {{.*, embedding: null}} AS m
""", node_name=node_name) """, node_name=node_name)
return result.values() return result.values()

View File

@ -344,7 +344,7 @@ class KnowledgeBase:
"all_results": all_db_result, "all_results": all_db_result,
} }
def get_retriever(self, db_id): def get_retriever_by_db_id(self, db_id):
retriever_params = { retriever_params = {
"distance_threshold": self.default_distance_threshold, "distance_threshold": self.default_distance_threshold,
"rerank_threshold": self.default_rerank_threshold, "rerank_threshold": self.default_rerank_threshold,
@ -358,6 +358,16 @@ class KnowledgeBase:
return retriever return retriever
def get_retrievers(self):
retrievers = {}
for db in self.db_manager.get_all_databases():
retrievers[db["db_id"]] = {
"name": db["name"],
"description": db["description"],
"retriever": self.get_retriever_by_db_id(db["db_id"]),
}
return retrievers
################################ ################################
#* Below is the code for milvus # #* Below is the code for milvus #
################################ ################################

View File

@ -360,6 +360,8 @@ const sendMessageWithText = async (text) => {
body: JSON.stringify(requestData) body: JSON.stringify(requestData)
}); });
console.log("requestData", requestData);
if (!response.ok) { if (!response.ok) {
throw new Error('请求失败'); throw new Error('请求失败');
} }
@ -554,7 +556,7 @@ const handleFinished = async (data) => {
const handleMessageById = async (data) => { const handleMessageById = async (data) => {
const msgId = data.msg.id; const msgId = data.msg.id;
const msgType = data.msg.type; const msgType = data.msg.type;
console.log("data", data); // console.log("data", data);
// //
const existingMsgIndex = messageMap.value.get(msgId); const existingMsgIndex = messageMap.value.get(msgId);

View File

@ -163,6 +163,23 @@
</a-form-item> </a-form-item>
</template> </template>
<!-- 添加工具选择部分 -->
<a-form-item label="可用工具" name="tools" class="config-item">
<p class="description">选择要启用的工具</p>
<a-form-item-rest>
<div class="tools-switches">
<div v-for="tool in availableTools" :key="tool" class="tool-switch-item">
<span class="tool-name">{{ tool }}</span>
<a-switch
size="small"
:checked="isToolActive(tool)"
@change="(checked) => toggleTool(tool, checked)"
/>
</div>
</div>
</a-form-item-rest>
</a-form-item>
<!-- 弹窗底部按钮 --> <!-- 弹窗底部按钮 -->
<div class="form-actions" v-if="!state.isEmptyConfig"> <div class="form-actions" v-if="!state.isEmptyConfig">
<a-button type="primary" @click="saveConfig">保存配置</a-button> <a-button type="primary" @click="saveConfig">保存配置</a-button>
@ -208,6 +225,7 @@ const router = useRouter();
// //
const agents = ref({}); const agents = ref({});
const selectedAgentId = ref(null); const selectedAgentId = ref(null);
const availableTools = ref([]); //
const state = reactive({ const state = reactive({
debug_mode: false, debug_mode: false,
isSidebarOpen: JSON.parse(localStorage.getItem('agent-sidebar-open') || 'true'), isSidebarOpen: JSON.parse(localStorage.getItem('agent-sidebar-open') || 'true'),
@ -250,6 +268,47 @@ const closeTokenModal = () => {
state.tokenModalVisible = false; state.tokenModalVisible = false;
}; };
//
const fetchAgents = async () => {
try {
const response = await fetch('/api/chat/agent');
if (response.ok) {
const data = await response.json();
//
agents.value = data.agents.reduce((acc, agent) => {
acc[agent.name] = agent;
return acc;
}, {});
// console.log("agents", agents.value);
//
if (selectedAgentId.value) {
loadAgentConfig();
}
} else {
console.error('获取智能体失败');
}
} catch (error) {
console.error('获取智能体错误:', error);
}
};
//
const fetchTools = async () => {
try {
const response = await fetch('/api/chat/tools');
if (response.ok) {
const data = await response.json();
availableTools.value = data.tools;
console.log("Available tools:", availableTools.value);
} else {
console.error('获取工具列表失败');
}
} catch (error) {
console.error('获取工具列表错误:', error);
}
};
// //
const loadAgentConfig = () => { const loadAgentConfig = () => {
// BUG: // BUG:
@ -271,6 +330,10 @@ const loadAgentConfig = () => {
agentConfig.value.model = schema.model; agentConfig.value.model = schema.model;
} }
if (schema.tools) {
agentConfig.value.tools = schema.tools;
}
// //
Object.keys(items).forEach(key => { Object.keys(items).forEach(key => {
const item = items[key]; const item = items[key];
@ -303,6 +366,7 @@ const saveConfig = () => {
// //
message.success('配置已保存'); message.success('配置已保存');
console.log("agentConfig.value", agentConfig.value);
closeConfigModal(); closeConfigModal();
}; };
@ -331,31 +395,6 @@ watch(
} }
); );
//
const fetchAgents = async () => {
try {
const response = await fetch('/api/chat/agent');
if (response.ok) {
const data = await response.json();
//
agents.value = data.agents.reduce((acc, agent) => {
acc[agent.name] = agent;
return acc;
}, {});
// console.log("agents", agents.value);
//
if (selectedAgentId.value) {
loadAgentConfig();
}
} else {
console.error('获取智能体失败');
}
} catch (error) {
console.error('获取智能体错误:', error);
}
};
// //
const toggleSidebar = () => { const toggleSidebar = () => {
state.isSidebarOpen = !state.isSidebarOpen; state.isSidebarOpen = !state.isSidebarOpen;
@ -383,6 +422,8 @@ const selectAgent = (agentId) => {
onMounted(async () => { onMounted(async () => {
// //
await fetchAgents(); await fetchAgents();
//
await fetchTools();
// //
const lastSelectedAgent = localStorage.getItem('last-selected-agent'); const lastSelectedAgent = localStorage.getItem('last-selected-agent');
@ -418,6 +459,31 @@ const goToAgentPage = () => {
window.open(`/agent/${selectedAgentId.value}`, '_blank'); window.open(`/agent/${selectedAgentId.value}`, '_blank');
} }
}; };
//
const isToolActive = (tool) => {
if (!agentConfig.value.tools) {
agentConfig.value.tools = [];
}
return agentConfig.value.tools.includes(tool);
};
//
const toggleTool = (tool, checked) => {
if (!agentConfig.value.tools) {
agentConfig.value.tools = [];
}
if (checked) {
//
if (!agentConfig.value.tools.includes(tool)) {
agentConfig.value.tools.push(tool);
}
} else {
//
agentConfig.value.tools = agentConfig.value.tools.filter(item => item !== tool);
}
};
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@ -689,6 +755,22 @@ const goToAgentPage = () => {
margin-right: 8px; margin-right: 8px;
} }
} }
.tools-switches {
display: flex;
flex-direction: column;
gap: 12px;
.tool-switch-item {
display: flex;
align-items: center;
justify-content: space-between;
.tool-name {
margin-left: 10px;
}
}
}
</style> </style>