diff --git a/pyproject.toml b/pyproject.toml index 3022e841..9cb7faeb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,7 @@ dependencies = [ "chromadb>=1.0.0,<2.0.0", "qdrant-client (>=1.15.1,<2.0.0)", "pyseekdb==1.1.0.post3", - "langbot-plugin==0.3.6", + "langbot-plugin==0.3.7", "asyncpg>=0.30.0", "line-bot-sdk>=3.19.0", "tboxsdk>=0.0.10", diff --git a/src/langbot/pkg/api/http/controller/groups/resources/tools.py b/src/langbot/pkg/api/http/controller/groups/resources/tools.py new file mode 100644 index 00000000..de827e54 --- /dev/null +++ b/src/langbot/pkg/api/http/controller/groups/resources/tools.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +from ... import group + + +@group.group_class('tools', '/api/v1/tools') +class ToolsRouterGroup(group.RouterGroup): + async def initialize(self) -> None: + @self.route('', methods=['GET'], auth_type=group.AuthType.USER_TOKEN) + async def _() -> str: + """获取所有可用工具列表""" + tools = await self.ap.tool_mgr.get_all_tools() + + tool_list = [] + for tool in tools: + tool_list.append( + { + 'name': tool.name, + 'description': tool.description, + 'human_desc': tool.human_desc, + 'parameters': tool.parameters, + } + ) + + return self.success(data={'tools': tool_list}) + + @self.route('/', methods=['GET'], auth_type=group.AuthType.USER_TOKEN) + async def _(tool_name: str) -> str: + """获取特定工具详情""" + tools = await self.ap.tool_mgr.get_all_tools() + + for tool in tools: + if tool.name == tool_name: + return self.success( + data={ + 'tool': { + 'name': tool.name, + 'description': tool.description, + 'human_desc': tool.human_desc, + 'parameters': tool.parameters, + } + } + ) + + return self.http_status(404, -1, f'Tool not found: {tool_name}') diff --git a/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx b/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx index bf53cbbb..b6b21aab 100644 --- a/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx +++ b/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx @@ -249,6 +249,9 @@ export default function DynamicFormComponent({ case 'bot-selector': fieldSchema = z.string(); break; + case 'tools-selector': + fieldSchema = z.array(z.string()); + break; case 'model-fallback-selector': fieldSchema = z.object({ primary: z.string(), diff --git a/web/src/app/home/components/dynamic-form/DynamicFormItemComponent.tsx b/web/src/app/home/components/dynamic-form/DynamicFormItemComponent.tsx index 6a6f8707..7b574033 100644 --- a/web/src/app/home/components/dynamic-form/DynamicFormItemComponent.tsx +++ b/web/src/app/home/components/dynamic-form/DynamicFormItemComponent.tsx @@ -23,6 +23,7 @@ import { Bot, KnowledgeBase, EmbeddingModel, + PluginTool, } from '@/app/infra/entities/api'; import { toast } from 'sonner'; import { useTranslation } from 'react-i18next'; @@ -75,9 +76,14 @@ export default function DynamicFormItemComponent({ const [embeddingModels, setEmbeddingModels] = useState([]); const [knowledgeBases, setKnowledgeBases] = useState([]); const [bots, setBots] = useState([]); + const [tools, setTools] = useState([]); const [uploading, setUploading] = useState(false); const [kbDialogOpen, setKbDialogOpen] = useState(false); const [tempSelectedKBIds, setTempSelectedKBIds] = useState([]); + const [toolsDialogOpen, setToolsDialogOpen] = useState(false); + const [tempSelectedToolNames, setTempSelectedToolNames] = useState( + [], + ); const { t } = useTranslation(); const [modelsDialogOpen, setModelsDialogOpen] = useState(false); @@ -209,6 +215,21 @@ export default function DynamicFormItemComponent({ } }, [config.type]); + useEffect(() => { + if (config.type === DynamicFormItemType.TOOLS_SELECTOR) { + httpClient + .getTools() + .then((resp) => { + setTools(resp.tools); + }) + .catch((err) => { + toast.error( + t('tools.getToolListError', 'Failed to get tools: ') + err.msg, + ); + }); + } + }, [config.type]); + switch (config.type) { case DynamicFormItemType.INT: case DynamicFormItemType.FLOAT: @@ -1161,6 +1182,139 @@ export default function DynamicFormItemComponent({ ); + case DynamicFormItemType.TOOLS_SELECTOR: + return ( + <> +
+ {field.value && field.value.length > 0 ? ( +
+ {field.value.map((toolName: string) => { + const currentTool = tools.find( + (tool) => tool.name === toolName, + ); + + return ( +
+
+ +
+
{toolName}
+ {currentTool?.human_desc && ( +
+ {currentTool.human_desc} +
+ )} +
+
+ +
+ ); + })} +
+ ) : ( +
+

+ {t('tools.noToolSelected', 'No tools selected')} +

+
+ )} +
+ + + + + + + + {t('tools.selectTools', 'Select Tools')} + + +
+ {tools.map((tool) => { + const isSelected = tempSelectedToolNames.includes(tool.name); + return ( +
{ + setTempSelectedToolNames((prev) => + prev.includes(tool.name) + ? prev.filter((name) => name !== tool.name) + : [...prev, tool.name], + ); + }} + > + + +
+
{tool.name}
+ {tool.human_desc && ( +
+ {tool.human_desc} +
+ )} +
+
+ ); + })} + {tools.length === 0 && ( +
+

+ {t('tools.noToolsAvailable', 'No tools available')} +

+
+ )} +
+ + + + +
+
+ + ); + case DynamicFormItemType.PROMPT_EDITOR: { // Guard: field.value may be undefined when the form resets or // initialValues haven't propagated yet. Fall back to a default diff --git a/web/src/app/infra/entities/api/index.ts b/web/src/app/infra/entities/api/index.ts index 56b79888..42285221 100644 --- a/web/src/app/infra/entities/api/index.ts +++ b/web/src/app/infra/entities/api/index.ts @@ -465,3 +465,18 @@ export interface MCPTool { description: string; parameters?: object; } + +export interface PluginTool { + name: string; + description: string; + human_desc: string; + parameters: object; +} + +export interface ApiRespTools { + tools: PluginTool[]; +} + +export interface ApiRespToolDetail { + tool: PluginTool; +} diff --git a/web/src/app/infra/entities/form/dynamic.ts b/web/src/app/infra/entities/form/dynamic.ts index f13871eb..f44ced4a 100644 --- a/web/src/app/infra/entities/form/dynamic.ts +++ b/web/src/app/infra/entities/form/dynamic.ts @@ -42,6 +42,7 @@ export enum DynamicFormItemType { KNOWLEDGE_BASE_MULTI_SELECTOR = 'knowledge-base-multi-selector', PLUGIN_SELECTOR = 'plugin-selector', BOT_SELECTOR = 'bot-selector', + TOOLS_SELECTOR = 'tools-selector', WEBHOOK_URL = 'webhook-url', } diff --git a/web/src/app/infra/http/BackendClient.ts b/web/src/app/infra/http/BackendClient.ts index fb8648b5..ecc0cce3 100644 --- a/web/src/app/infra/http/BackendClient.ts +++ b/web/src/app/infra/http/BackendClient.ts @@ -41,6 +41,8 @@ import { ApiRespKnowledgeEngines, ApiRespParsers, RagMigrationStatusResp, + ApiRespTools, + ApiRespToolDetail, } from '@/app/infra/entities/api'; import { Plugin } from '@/app/infra/entities/plugin'; import { GetBotLogsRequest } from '@/app/infra/http/requestParam/bots/GetBotLogsRequest'; @@ -649,6 +651,16 @@ export class BackendClient extends BaseHttpClient { return this.get('/api/v1/mcp/servers'); } + // ========== Tools ========== + + public getTools(): Promise { + return this.get('/api/v1/tools'); + } + + public getToolDetail(toolName: string): Promise { + return this.get(`/api/v1/tools/${toolName}`); + } + public getMCPServer(serverName: string): Promise { return this.get(`/api/v1/mcp/servers/${serverName}`); }