feat(router): 添加工具路由,更新聊天和智能体相关功能,优化工具获取逻辑

This commit is contained in:
Wenjie Zhang 2025-07-24 00:45:47 +08:00
parent 770d098f05
commit 75c52a3855
8 changed files with 401 additions and 48 deletions

View File

@ -4,6 +4,7 @@ from server.routers.auth_router import auth
from server.routers.chat_router import chat
from server.routers.knowledge_router import knowledge
from server.routers.graph_router import graph
from server.routers.tool_router import tool
router = APIRouter()
@ -13,3 +14,4 @@ router.include_router(auth) # /api/auth/*
router.include_router(chat) # /api/chat/*
router.include_router(knowledge) # /api/knowledge/*
router.include_router(graph) # /api/graph/*
router.include_router(tool) # /api/tool/*

View File

@ -15,7 +15,7 @@ from src.knowledge import HistoryManager
from src.agents import agent_manager
from src.models import select_model
from src.utils.logging_config import logger
from src.agents.tools_factory import get_all_tools
from src.agents.tools_factory import get_runnable_tools
from server.routers.auth_router import get_admin_user
from server.utils.auth_middleware import get_required_user, get_db
from server.models.user_model import User
@ -44,9 +44,13 @@ async def get_default_agent(current_user: User = Depends(get_required_user)):
raise HTTPException(status_code=500, detail=f"获取默认智能体出错: {str(e)}")
@chat.post("/set_default_agent")
async def set_default_agent(agent_id: str = Body(..., embed=True), current_user = Depends(get_admin_user)):
async def set_default_agent(request_data: dict = Body(...), current_user = Depends(get_admin_user)):
"""设置默认智能体ID (仅管理员)"""
try:
agent_id = request_data.get("agent_id")
if not agent_id:
raise HTTPException(status_code=422, detail="缺少必需的 agent_id 字段")
# 验证智能体是否存在
agents = await agent_manager.get_agents_info()
agent_ids = [agent.get("id", "") for agent in agents]
@ -70,11 +74,6 @@ async def set_default_agent(agent_id: str = Body(..., embed=True), current_user
# > === 对话分组 ===
# =============================================================================
@chat.get("/")
async def chat_get(current_user: User = Depends(get_required_user)):
"""聊天服务健康检查(需要登录)"""
return "Chat Get!"
@chat.post("/call")
async def call(query: str = Body(...), meta: dict = Body(None), current_user: User = Depends(get_required_user)):
"""调用模型进行简单问答(需要登录)"""
@ -183,7 +182,7 @@ async def update_chat_models(model_provider: str, model_names: list[str], curren
@chat.get("/tools")
async def get_tools(current_user: User = Depends(get_admin_user)):
"""获取所有可用工具(需要登录)"""
return {"tools": list(get_all_tools().keys())}
return {"tools": list(get_runnable_tools().keys())}
@chat.post("/agent/{agent_id}/config")
async def save_agent_config(

View File

@ -0,0 +1,15 @@
from fastapi import APIRouter, Depends
from src.agents.tools_factory import get_all_tools_info
from server.models.user_model import User
from server.utils.auth_middleware import get_required_user
tool = chat = APIRouter(prefix="/tool", tags=["tool"])
@tool.get("/tools")
async def get_tools(current_user: User = Depends(get_required_user)):
"""获取所有可用工具的信息"""
try:
tools_info = get_all_tools_info()
return {"tools": tools_info}
except Exception as e:
return {"error": str(e)}

View File

@ -3,7 +3,7 @@ import uuid
from dataclasses import dataclass, field
from src.agents.registry import Configuration
from src.agents.tools_factory import get_all_tools
from src.agents.tools_factory import get_runnable_tools
@dataclass(kw_only=True)
class ChatbotConfiguration(Configuration):
@ -37,7 +37,7 @@ class ChatbotConfiguration(Configuration):
default_factory=list,
metadata={
"name": "工具",
"options": list(get_all_tools().keys()), # 这里的选择是所有的工具
"options": list(get_runnable_tools().keys()), # 这里的选择是所有的工具
"description": "工具列表"
},
)

View File

@ -15,12 +15,11 @@ from src.utils import logger
from src.agents.registry import State, BaseAgent
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 get_all_tools
from src.agents.tools_factory import get_runnable_tools
class ChatbotAgent(BaseAgent):
name = "对话机器人Chatbot"
description = "基础的对话机器人,可以回答问题,默认不使用任何工具,可在配置中启用需要的工具。"
requirements = ["TAVILY_API_KEY", "ZHIPUAI_API_KEY"]
config_schema = ChatbotConfiguration
def __init__(self, **kwargs):
@ -34,7 +33,7 @@ class ChatbotAgent(BaseAgent):
默认不使用任何工具
如果配置为列表则使用列表中的工具
"""
platform_tools = get_all_tools()
platform_tools = get_runnable_tools()
if tools is None or not isinstance(tools, list) or len(tools) == 0:
# 默认不使用任何工具
logger.info("未配置工具或配置为空,不使用任何工具")
@ -68,7 +67,7 @@ class ChatbotAgent(BaseAgent):
workflow = StateGraph(State, config_schema=self.config_schema)
workflow.add_node("chatbot", self.llm_call)
workflow.add_node("tools", ToolNode(tools=list(get_all_tools().values())))
workflow.add_node("tools", ToolNode(tools=list(get_runnable_tools().values())))
workflow.add_edge(START, "chatbot")
workflow.add_conditional_edges(
"chatbot",

View File

@ -8,6 +8,7 @@ export * from './system_api' // 系统管理API
export * from './knowledge_api' // 知识库管理API
export * from './auth_api' // 认证API
export * from './graph_api' // 图谱API
export * from './tools.js' // 工具API
// 导出基础工具函数
export { apiRequest, apiGet, apiPost, apiPut, apiDelete } from './base'
@ -29,5 +30,8 @@ export { apiRequest, apiGet, apiPost, apiPut, apiDelete } from './base'
* 4. graph_api.js: 图谱API
* - 知识图谱相关功能
*
* 5. tools.js: 工具API
* - 工具信息获取
*
* 注意API模块已处理权限验证和请求头使用时无需再手动添加认证头
*/

18
web/src/apis/tools.js Normal file
View File

@ -0,0 +1,18 @@
import { apiGet } from './base'
/**
* 工具管理API模块
* 包含工具信息获取等功能
*/
// =============================================================================
// === 工具信息分组 ===
// =============================================================================
export const toolsApi = {
/**
* 获取所有可用工具的信息
* @returns {Promise} - 工具信息列表
*/
getTools: () => apiGet('/api/tool/tools', {}, true)
}

View File

@ -69,20 +69,10 @@
>
<div class="conf-content">
<div class="agent-info">
<h3>详细配置信<span @click="toggleDebugMode"></span></h3>
<p>详细配置信<span @click="toggleDebugMode"></span></p>
<p>{{ selectedAgent.description }}</p>
<pre v-if="state.debug_mode">{{ selectedAgent }}</pre>
<!-- 添加requirements显示部分 -->
<div v-if="agents[selectedAgentId]?.requirements && agents[selectedAgentId]?.requirements.length > 0" class="info-section">
<h3>所需环境变量:</h3>
<div class="requirements-list">
<a-tag v-for="req in agents[selectedAgentId].requirements" :key="req">
{{ req }}
</a-tag>
</div>
</div>
<a-divider />
<div v-if="selectedAgentId && configSchema" class="config-modal-content">
@ -111,7 +101,7 @@
<a-textarea
v-else-if="key === 'system_prompt'"
v-model:value="agentConfig[key]"
:rows="4"
:rows="2"
:placeholder="getPlaceholder(key, value)"
/>
@ -129,6 +119,25 @@
{{ option.label || option }}
</a-select-option>
</a-select>
<!-- 工具选择特殊处理 -->
<div v-else-if="key === 'tools'" class="tools-selector">
<div class="tools-summary">
<div class="tools-summary-left">
<span class="tools-count">已选择 {{ getSelectedCount(key) }} 个工具</span>
<a-button type="link" size="small" @click="clearSelection(key)" v-if="getSelectedCount(key) > 0">
清空
</a-button>
</div>
<a-button type="primary" @click="openToolsModal" class="select-tools-btn">
选择工具
</a-button>
</div>
<div v-if="getSelectedCount(key) > 0" class="selected-tools-preview">
<a-tag v-for="toolId in agentConfig[key]" :key="toolId" closable @close="removeSelectedTool(toolId)">
{{ getToolNameById(toolId) }}
</a-tag>
</div>
</div>
<!-- 多选 -->
<div v-else-if="value?.options && value?.type === 'list'" class="multi-select-cards">
<div class="multi-select-label">
@ -149,7 +158,7 @@
@click="toggleOption(key, option)"
>
<div class="option-content">
<span class="option-text">{{ option }}</span>
<span class="option-text">{{ getToolNameById(toolId) }}</span>
<div class="option-indicator">
<CheckCircleOutlined v-if="isOptionSelected(key, option)" />
<PlusCircleOutlined v-else />
@ -198,6 +207,56 @@
</div>
</a-modal>
<!-- 工具选择弹窗 -->
<a-modal
v-model:open="state.toolsModalOpen"
title="选择工具"
:width="800"
:footer="null"
:maskClosable="false"
class="tools-modal"
>
<div class="tools-modal-content">
<div class="tools-search">
<a-input
v-model:value="toolsSearchText"
placeholder="搜索工具..."
allow-clear
/>
</div>
<div class="tools-list">
<div
v-for="tool in filteredTools"
:key="tool.id"
class="tool-item"
:class="{ 'selected': selectedTools.includes(tool.id) }"
@click="toggleToolSelection(tool.id)"
>
<div class="tool-content">
<div class="tool-header">
<span class="tool-name">{{ tool.name }}</span>
<div class="tool-indicator">
<CheckCircleOutlined v-if="selectedTools.includes(tool.id)" />
<PlusCircleOutlined v-else />
</div>
</div>
<div class="tool-description">{{ tool.description }}</div>
</div>
</div>
</div>
<div class="tools-modal-footer">
<div class="selected-count">
已选择 {{ selectedTools.length }} 个工具
</div>
<div class="modal-actions">
<a-button @click="cancelToolsSelection">取消</a-button>
<a-button type="primary" @click="confirmToolsSelection">确认</a-button>
</div>
</div>
</div>
</a-modal>
<!-- 中间内容区域 -->
<div class="content">
<AgentChatComponent
@ -230,6 +289,7 @@ import ModelSelectorComponent from '@/components/ModelSelectorComponent.vue';
import { useUserStore } from '@/stores/user';
import { chatApi } from '@/apis/auth_api';
import { agentConfigApi } from '@/apis/system_api';
import { toolsApi } from '@/apis/tools';
//
const router = useRouter();
@ -242,12 +302,36 @@ const defaultAgentId = ref(null); // 存储默认智能体ID
const state = reactive({
agentConfOpen: false,
debug_mode: false,
toolsModalOpen: false,
isEmptyConfig: computed(() =>
!selectedAgentId.value ||
Object.keys(configurableItems.value).length === 0
)
});
//
const availableTools = ref([]);
const selectedTools = ref([]);
const toolsSearchText = ref('');
//
const filteredTools = computed(() => {
if (!toolsSearchText.value) {
return availableTools.value;
}
const searchLower = toolsSearchText.value.toLowerCase();
return availableTools.value.filter(tool =>
tool.name.toLowerCase().includes(searchLower) ||
tool.description.toLowerCase().includes(searchLower)
);
});
// ID
const getToolNameById = (toolId) => {
const tool = availableTools.value.find(t => t.id === toolId);
return tool ? tool.name : toolId;
};
const selectedAgent = computed(() => agents.value[selectedAgentId.value] || {});
const configSchema = computed(() => selectedAgent.value.config_schema || {});
const configurableItems = computed(() => {
@ -321,6 +405,53 @@ const clearSelection = (key) => {
agentConfig.value[key] = [];
};
//
const openToolsModal = async () => {
try {
//
const response = await toolsApi.getTools();
// idname
availableTools.value = Object.values(response.tools || {});
//
selectedTools.value = [...(agentConfig.value.tools || [])];
//
state.toolsModalOpen = true;
} catch (error) {
console.error('加载工具列表失败:', error);
message.error('加载工具列表失败');
}
};
const toggleToolSelection = (toolId) => {
const index = selectedTools.value.indexOf(toolId);
if (index > -1) {
selectedTools.value.splice(index, 1);
} else {
selectedTools.value.push(toolId);
}
};
const removeSelectedTool = (toolId) => {
const index = agentConfig.value.tools.indexOf(toolId);
if (index > -1) {
agentConfig.value.tools.splice(index, 1);
}
};
const confirmToolsSelection = () => {
agentConfig.value.tools = [...selectedTools.value];
state.toolsModalOpen = false;
toolsSearchText.value = '';
};
const cancelToolsSelection = () => {
state.toolsModalOpen = false;
toolsSearchText.value = '';
selectedTools.value = [];
};
// ID
const fetchDefaultAgent = async () => {
try {
@ -536,7 +667,7 @@ const toggleConf = () => {
width: 100%;
height: 100vh;
overflow: hidden;
--agent-view-header-height: 60px;
--agent-view-header-height: 45px;
}
.agent-view-header {
@ -597,27 +728,7 @@ const toggleConf = () => {
}
}
// requirements
.info-section {
margin-top: 16px;
padding-top: 12px;
h3 {
font-size: 14px;
margin-bottom: 8px;
font-weight: 500;
}
}
.requirements-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
.ant-tag {
user-select: all;
}
}
.agent-model {
width: 100%;
@ -712,6 +823,211 @@ const toggleConf = () => {
margin-left: 4px;
}
}
//
.tools-selector {
.tools-summary {
display: flex;
justify-content: space-between;
align-items: center;
// margin-bottom: 8px;
padding: 8px 12px;
background: var(--gray-50);
border-radius: 8px;
border: 1px solid var(--gray-200);
font-size: 14px;
color: var(--gray-700);
transition: border-color 0.2s ease;
.tools-summary-left {
display: flex;
align-items: center;
gap: 8px;
.tools-count {
color: var(--gray-900);
}
}
.select-tools-btn {
background: var(--main-color);
border: none;
color: #fff;
border-radius: 6px;
padding: 4px 12px;
font-size: 13px;
font-weight: 500;
height: 28px;
transition: all 0.2s ease;
&:hover {
background: var(--main-600);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
&:active {
transform: translateY(0);
}
}
}
.selected-tools-preview {
display: flex;
flex-wrap: wrap;
gap: 6px;
padding: 8px 0;
background: none;
border: none;
min-height: 32px;
:deep(.ant-tag) {
margin: 0;
padding: 4px 10px;
border-radius: 6px;
background: var(--gray-100);
border: 1px solid var(--gray-300);
color: var(--gray-900);
font-size: 13px;
font-weight: 400;
.anticon-close {
color: var(--gray-600);
margin-left: 4px;
&:hover {
color: var(--gray-900);
}
}
}
}
}
//
.tools-modal {
:deep(.ant-modal-content) {
border-radius: 8px;
box-shadow: 0 4px 24px rgba(0,0,0,0.08);
overflow: hidden;
}
:deep(.ant-modal-header) {
background: #fff;
border-bottom: 1px solid var(--gray-200);
padding: 16px 20px;
.ant-modal-title {
font-size: 16px;
font-weight: 600;
color: var(--gray-900);
}
}
:deep(.ant-modal-body) {
padding: 20px;
background: #fff;
}
.tools-modal-content {
.tools-search {
margin-bottom: 16px;
:deep(.ant-input) {
border-radius: 8px;
border: 1px solid var(--gray-300);
padding: 8px 12px;
font-size: 14px;
&:focus {
border-color: var(--main-color);
box-shadow: none;
}
}
}
.tools-list {
max-height: 350px;
overflow-y: auto;
border: 1px solid var(--gray-200);
border-radius: 8px;
margin-bottom: 16px;
background: #fff;
.tool-item {
padding: 14px 16px;
border-bottom: 1px solid var(--gray-100);
cursor: pointer;
transition: background 0.2s, border 0.2s;
border-left: 3px solid transparent;
&:last-child { border-bottom: none; }
&:hover {
background: var(--gray-50);
}
&.selected {
background: var(--main-10);
border-left: 3px solid var(--main-color);
}
.tool-content {
.tool-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
.tool-name {
font-weight: 500;
color: var(--gray-900);
font-size: 14px;
}
.tool-indicator { display: none; }
}
.tool-description {
font-size: 13px;
color: var(--gray-700);
margin-bottom: 6px;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
.tools-modal-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0 0 0;
border-top: 1px solid var(--gray-200);
.selected-count {
font-size: 13px;
color: var(--gray-700);
background: none;
padding: 0;
border: none;
}
.modal-actions {
display: flex;
gap: 10px;
:deep(.ant-btn) {
border-radius: 8px;
font-weight: 500;
padding: 6px 18px;
height: 36px;
font-size: 14px;
&.ant-btn-default {
border: 1px solid var(--gray-300);
color: var(--gray-900);
background: #fff;
&:hover {
border-color: var(--main-color);
color: var(--main-color);
background: var(--main-10);
}
}
&.ant-btn-primary {
background: var(--main-color);
border: none;
color: #fff;
&:hover {
background: var(--main-600);
}
}
}
}
}
}
}
//
.multi-select-cards {