diff --git a/modules/tool/packages/chatBI/config.ts b/modules/tool/packages/chatBI/config.ts new file mode 100644 index 00000000..5d769e32 --- /dev/null +++ b/modules/tool/packages/chatBI/config.ts @@ -0,0 +1,88 @@ +import { defineTool } from '@tool/type'; +import { + FlowNodeInputTypeEnum, + FlowNodeOutputTypeEnum, + WorkflowIOValueTypeEnum +} from '@tool/type/fastgpt'; +import { ToolTypeEnum } from '@tool/type/tool'; + +export default defineTool({ + name: { + 'zh-CN': 'chatBI', + en: 'Template tool' + }, + type: ToolTypeEnum.tools, + description: { + 'zh-CN': 'chatBI 工具', + en: 'chatBI Tool' + }, + toolDescription: + 'send user natural language instructions to ChatBI application for execution, sse stream interface returns results', + secretInputConfig: [ + { + key: 'chatBIUrl', + label: 'chatBI 服务根地址', + description: '根地址,例如:http://example.com', + required: true, + inputType: 'secret' + }, + { + key: 'sysAccessKey', + label: 'chatBI 系统AccessKey', + description: '系统AccessKey', + required: true, + inputType: 'secret' + }, + { + key: 'corpId', + label: 'chatBI 企业ID', + description: '企业ID', + required: true, + inputType: 'secret' + } + ], + versionList: [ + { + value: '0.1.0', + description: 'Default version', + inputs: [ + { + key: 'query', + label: '用户提问内容', + renderTypeList: [FlowNodeInputTypeEnum.input, FlowNodeInputTypeEnum.reference], + valueType: WorkflowIOValueTypeEnum.string, + required: true + }, + { + key: 'appId', + label: 'chatBI 应用ID', + renderTypeList: [FlowNodeInputTypeEnum.input, FlowNodeInputTypeEnum.reference], + valueType: WorkflowIOValueTypeEnum.string, + required: true + }, + { + key: 'appAccessKey', + label: 'chatBI 应用AccessKey', + renderTypeList: [FlowNodeInputTypeEnum.input, FlowNodeInputTypeEnum.reference], + valueType: WorkflowIOValueTypeEnum.string, + required: true + }, + { + key: 'sessionId', + label: '会话id', + renderTypeList: [FlowNodeInputTypeEnum.input, FlowNodeInputTypeEnum.reference], + valueType: WorkflowIOValueTypeEnum.string, + required: false + } + ], + outputs: [ + { + valueType: WorkflowIOValueTypeEnum.arrayAny, + key: 'displayContentList', + label: '展示内容列表', + description: 'ChatBI返回的核心展示内容,包含文本、图表、表格等多种类型' + } + ] + } + ] +}); diff --git a/modules/tool/packages/chatBI/index.ts b/modules/tool/packages/chatBI/index.ts new file mode 100644 index 00000000..d698ed48 --- /dev/null +++ b/modules/tool/packages/chatBI/index.ts @@ -0,0 +1,10 @@ +import config from './config'; +import { InputType, OutputType, tool as toolCb } from './src'; +import { exportTool } from '@tool/utils/tool'; + +export default exportTool({ + toolCb, + InputType, + OutputType, + config +}); diff --git a/modules/tool/packages/chatBI/logo.svg b/modules/tool/packages/chatBI/logo.svg new file mode 100644 index 00000000..ca10cec3 --- /dev/null +++ b/modules/tool/packages/chatBI/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/modules/tool/packages/chatBI/package.json b/modules/tool/packages/chatBI/package.json new file mode 100644 index 00000000..a0d4ee76 --- /dev/null +++ b/modules/tool/packages/chatBI/package.json @@ -0,0 +1,17 @@ +{ + "name": "@fastgpt-plugins/tool-chat-bi", + "module": "index.ts", + "type": "module", + "scripts": { + "build": "bun ../../../../scripts/build.ts" + }, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "zod": "^3.24.2" + } +} \ No newline at end of file diff --git a/modules/tool/packages/chatBI/src/index.ts b/modules/tool/packages/chatBI/src/index.ts new file mode 100644 index 00000000..b61ed69b --- /dev/null +++ b/modules/tool/packages/chatBI/src/index.ts @@ -0,0 +1,163 @@ +import { z } from 'zod'; +import type { RunToolSecondParamsType } from '@tool/type/tool'; +import { StreamDataAnswerTypeEnum } from '@tool/type/tool'; +import { getErrText } from '@tool/utils/err'; + +type DataType = { + isFinished: boolean; + isSuccess: boolean; + errorMessage?: string; + processList: unknown[]; + displayContentList: ContentType[]; + streamData: string; +}; + +enum ContentTypeEnum { + text = 'TEXT' +} +type ContentType = { + content: string; + type: ContentTypeEnum; +}; + +export const InputType = z.object({ + query: z.string(), + appId: z.string(), + appAccessKey: z.string(), + sessionId: z.string().optional(), + chatBIUrl: z.string(), + sysAccessKey: z.string(), + corpId: z.string() +}); + +export const OutputType = z.object({ + // displayContentList: z.array(z.any()) +}); + +export async function tool( + { + query, + appId, + appAccessKey, + sessionId, + chatBIUrl, + sysAccessKey, + corpId + }: z.infer, + { systemVar, streamResponse }: RunToolSecondParamsType +): Promise> { + try { + const url = new URL('/v2/open/api/common/stream/chatbi/chatNew', chatBIUrl); + + // userID get from systemVar + const userId = systemVar.user.username.split('-')[1]; + + if (!sessionId) { + sessionId = userId; + } + + const response = await fetch(url.toString(), { + method: 'POST', + headers: { + Accept: 'text/event-stream', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + sysAccessKey, + corpId, + appId, + appAccessKey, + userId, + query, + sessionId + }) + }); + + if (!response.ok) { + return Promise.reject(`HTTP ${response.status}: ${response.statusText}`); + } + + const reader = response.body?.getReader(); + + if (!reader) { + return Promise.reject('无法获取响应流'); + } + + const decoder = new TextDecoder(); + let sentListLen = 0; + let buffer = ''; + let isFinished = false; + + while (!isFinished) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + + const events = buffer.split('\n\n'); + + buffer = events.pop() || ''; + + for (const event of events) { + if (!event.trim()) continue; + + const lines = event.split('\n'); + let eventData = ''; + + for (const line of lines) { + if (line.startsWith('data:')) { + // remove "data:" and trim + eventData = line.slice(5).trim(); + break; + } + } + + if (eventData && eventData !== '') { + try { + const data: DataType = JSON.parse(eventData); + + if (data.displayContentList && data.displayContentList.length > sentListLen) { + // only send the last items + const sendList = data.displayContentList.slice(sentListLen); + const texts = sendList + .filter((item) => item.type === 'TEXT') + .map((item) => item.content) + .join('\n'); + const unTexts = sendList.filter((item) => item.type !== 'TEXT'); + const content = + texts + + '\n' + + (unTexts.length > 0 ? `\`\`\`RENDER\n${JSON.stringify(unTexts)}\n\`\`\`\n` : ''); + + streamResponse({ + content, + type: StreamDataAnswerTypeEnum.answer + }); + sentListLen = data.displayContentList.length; + } + + if (data.streamData) { + streamResponse({ + content: data.streamData, + type: StreamDataAnswerTypeEnum.answer + }); + } + + if (data.isFinished) { + isFinished = true; + break; + } + } catch (e) { + continue; + } + } + } + } + + return { + // displayContentList + }; + } catch (error) { + return Promise.reject(getErrText(error)); + } +} diff --git a/modules/tool/packages/chatBI/test/index.test.ts b/modules/tool/packages/chatBI/test/index.test.ts new file mode 100644 index 00000000..b70e289f --- /dev/null +++ b/modules/tool/packages/chatBI/test/index.test.ts @@ -0,0 +1,8 @@ +import { expect, test } from 'vitest'; +import tool from '..'; + +test(async () => { + expect(tool.name).toBeDefined(); + expect(tool.description).toBeDefined(); + expect(tool.cb).toBeDefined(); +});