diff --git a/.vscode/nextapi.code-snippets b/.vscode/nextapi.code-snippets index 701732b0e..fbd95e57c 100644 --- a/.vscode/nextapi.code-snippets +++ b/.vscode/nextapi.code-snippets @@ -35,7 +35,7 @@ "scope": "typescriptreact", "prefix": "context", "body": [ - "import React, { ReactNode } from 'react';", + "import React, { type ReactNode } from 'react';", "import { createContext } from 'use-context-selector';", "", "type ContextType = {$1};", diff --git a/packages/global/core/chat/adapt.ts b/packages/global/core/chat/adapt.ts index 1e6850913..686ac2362 100644 --- a/packages/global/core/chat/adapt.ts +++ b/packages/global/core/chat/adapt.ts @@ -4,9 +4,10 @@ import type { ChatItemValueItemType, RuntimeUserPromptType, SystemChatItemValueItemType, + UserChatItemFileItemType, UserChatItemType, UserChatItemValueItemType -} from '../../core/chat/type.d'; +} from './type'; import { ChatFileTypeEnum, ChatRoleEnum } from '../../core/chat/constants'; import type { ChatCompletionContentPart, @@ -18,7 +19,7 @@ import type { } from '../../core/ai/type.d'; import { ChatCompletionRequestMessageRoleEnum } from '../../core/ai/constants'; -const GPT2Chat = { +export const GPT2Chat = { [ChatCompletionRequestMessageRoleEnum.System]: ChatRoleEnum.System, [ChatCompletionRequestMessageRoleEnum.User]: ChatRoleEnum.Human, [ChatCompletionRequestMessageRoleEnum.Assistant]: ChatRoleEnum.AI, @@ -385,9 +386,10 @@ export const chatValue2RuntimePrompt = (value: ChatItemValueItemType[]): Runtime return prompt; }; -export const runtimePrompt2ChatsValue = ( - prompt: RuntimeUserPromptType -): UserChatItemType['value'] => { +export const runtimePrompt2ChatsValue = (prompt: { + files?: UserChatItemFileItemType[]; + text?: string; +}): UserChatItemType['value'] => { const value: UserChatItemType['value'] = []; if (prompt.files) { prompt.files.forEach((file) => { diff --git a/packages/global/core/chat/helperBot/adaptor.ts b/packages/global/core/chat/helperBot/adaptor.ts new file mode 100644 index 000000000..e32785215 --- /dev/null +++ b/packages/global/core/chat/helperBot/adaptor.ts @@ -0,0 +1,132 @@ +import { ChatCompletionRequestMessageRoleEnum } from 'core/ai/constants'; +import type { + ChatCompletionContentPart, + ChatCompletionFunctionMessageParam, + ChatCompletionMessageFunctionCall, + ChatCompletionMessageParam, + ChatCompletionMessageToolCall, + ChatCompletionToolMessageParam +} from '../../ai/type'; +import { ChatFileTypeEnum, ChatRoleEnum } from '../constants'; +import type { HelperBotChatItemType } from './type'; +import { GPT2Chat, simpleUserContentPart } from '../adapt'; +import type { + AIChatItemValueItemType, + SystemChatItemValueItemType, + UserChatItemValueItemType +} from '../type'; + +export const helperChats2GPTMessages = ({ + messages, + reserveTool = false +}: { + messages: HelperBotChatItemType[]; + reserveTool?: boolean; +}): ChatCompletionMessageParam[] => { + let results: ChatCompletionMessageParam[] = []; + + messages.forEach((item) => { + if (item.obj === ChatRoleEnum.System) { + const content = item.value?.[0]?.text?.content; + if (content) { + results.push({ + role: ChatCompletionRequestMessageRoleEnum.System, + content + }); + } + } else if (item.obj === ChatRoleEnum.Human) { + const value = item.value + .map((item) => { + if (item.text) { + return { + type: 'text', + text: item.text?.content || '' + }; + } + if (item.file) { + if (item.file?.type === ChatFileTypeEnum.image) { + return { + type: 'image_url', + key: item.file.key, + image_url: { + url: item.file.url + } + }; + } else if (item.file?.type === ChatFileTypeEnum.file) { + return { + type: 'file_url', + name: item.file?.name || '', + url: item.file.url, + key: item.file.key + }; + } + } + }) + .filter(Boolean) as ChatCompletionContentPart[]; + + results.push({ + role: ChatCompletionRequestMessageRoleEnum.User, + content: simpleUserContentPart(value) + }); + } else { + const aiResults: ChatCompletionMessageParam[] = []; + + //AI: 只需要把根节点转化即可 + item.value.forEach((value, i) => { + if ('tool' in value && reserveTool) { + const tool_calls: ChatCompletionMessageToolCall[] = [ + { + id: value.tool.id, + type: 'function', + function: { + name: value.tool.functionName, + arguments: value.tool.params + } + } + ]; + const toolResponse: ChatCompletionToolMessageParam[] = [ + { + tool_call_id: value.tool.id, + role: ChatCompletionRequestMessageRoleEnum.Tool, + content: value.tool.response + } + ]; + aiResults.push({ + role: ChatCompletionRequestMessageRoleEnum.Assistant, + tool_calls + }); + aiResults.push(...toolResponse); + } else if ('text' in value && value.text?.content === 'string') { + if (!value.text.content && item.value.length > 1) { + return; + } + // Concat text + const lastValue = item.value[i - 1]; + const lastResult = aiResults[aiResults.length - 1]; + if (lastValue && typeof lastResult?.content === 'string') { + lastResult.content += value.text.content; + } else { + aiResults.push({ + role: ChatCompletionRequestMessageRoleEnum.Assistant, + content: value.text.content + }); + } + } + }); + + // Auto add empty assistant message + results = results.concat( + aiResults.length > 0 + ? aiResults + : [ + { + role: ChatCompletionRequestMessageRoleEnum.Assistant, + content: '' + } + ] + ); + } + }); + + return results; +}; diff --git a/packages/global/core/chat/helperBot/type.ts b/packages/global/core/chat/helperBot/type.ts new file mode 100644 index 000000000..498742cb7 --- /dev/null +++ b/packages/global/core/chat/helperBot/type.ts @@ -0,0 +1,85 @@ +import { ObjectIdSchema } from '../../../common/type/mongo'; +import { z } from 'zod'; +import { ChatRoleEnum } from '../constants'; +import { + UserChatItemSchema, + SystemChatItemSchema, + type ChatItemObjItemType, + type ChatItemValueItemType, + ToolModuleResponseItemSchema +} from '../type'; + +export enum HelperBotTypeEnum { + topAgent = 'topAgent' +} +export const HelperBotTypeEnumSchema = z.enum(Object.values(HelperBotTypeEnum)); +export type HelperBotTypeEnumType = z.infer; + +export const HelperBotChatSchema = z.object({ + _id: ObjectIdSchema, + chatId: z.string(), + type: HelperBotTypeEnum, + userId: z.string(), + createTime: z.date(), + updateTime: z.date() +}); +export type HelperBotChatType = z.infer; + +// AI schema +const AIChatItemValueItemSchema = z.union([ + z.object({ + text: z.object({ + content: z.string() + }) + }), + z.object({ + reasoning: z.object({ + content: z.string() + }) + }), + z.object({ + tool: ToolModuleResponseItemSchema + }) +]); +const AIChatItemSchema = z.object({ + obj: z.literal(ChatRoleEnum.AI), + value: z.array(AIChatItemValueItemSchema) +}); + +const HelperBotChatRoleSchema = z.union([ + UserChatItemSchema, + SystemChatItemSchema, + AIChatItemSchema +]); +export const HelperBotChatItemSchema = z + .object({ + _id: ObjectIdSchema, + userId: z.string(), + chatId: z.string(), + dataId: z.string(), + createTime: z.date(), + memories: z.record(z.string(), z.any()).nullish() + }) + .and(HelperBotChatRoleSchema); +export type HelperBotChatItemType = z.infer; + +/* 客户端 UI 展示的类型 */ +export const HelperBotChatItemSiteSchema = z + .object({ + _id: ObjectIdSchema, + dataId: z.string(), + createTime: z.date() + }) + .and(HelperBotChatRoleSchema); +export type HelperBotChatItemSiteType = z.infer; + +/* 具体的 bot 的特有参数 */ + +export const topAgentParamsSchema = z.object({ + role: z.string().nullish(), + taskObject: z.string().nullish(), + selectedTools: z.array(z.string()).nullish(), + selectedDatasets: z.array(z.string()).nullish(), + fileUpload: z.boolean().nullish() +}); +export type TopAgentParamsType = z.infer; diff --git a/packages/global/core/chat/type.d.ts b/packages/global/core/chat/type.ts similarity index 62% rename from packages/global/core/chat/type.d.ts rename to packages/global/core/chat/type.ts index bac927298..f9d619511 100644 --- a/packages/global/core/chat/type.d.ts +++ b/packages/global/core/chat/type.ts @@ -1,20 +1,29 @@ -import { ClassifyQuestionAgentItemType } from '../workflow/template/system/classifyQuestion/type'; import type { SearchDataResponseItemType } from '../dataset/type'; -import type { ChatFileTypeEnum, ChatRoleEnum, ChatSourceEnum, ChatStatusEnum } from './constants'; +import type { ChatSourceEnum, ChatStatusEnum } from './constants'; +import { ChatFileTypeEnum, ChatRoleEnum } from './constants'; import type { FlowNodeTypeEnum } from '../workflow/node/constant'; -import type { NodeInputKeyEnum, NodeOutputKeyEnum } from '../workflow/constants'; +import { NodeOutputKeyEnum } from '../workflow/constants'; import type { DispatchNodeResponseKeyEnum } from '../workflow/runtime/constants'; import type { AppSchema, VariableItemType } from '../app/type'; -import { AppChatConfigType } from '../app/type'; -import type { AppSchema as AppType } from '@fastgpt/global/core/app/type.d'; -import { DatasetSearchModeEnum } from '../dataset/constants'; -import type { DispatchNodeResponseType } from '../workflow/runtime/type.d'; +import type { DispatchNodeResponseType } from '../workflow/runtime/type'; import type { ChatBoxInputType } from '../../../../projects/app/src/components/core/chat/ChatContainer/ChatBox/type'; import type { WorkflowInteractiveResponseType } from '../workflow/template/system/interactive/type'; import type { FlowNodeInputItemType } from '../workflow/type/io'; -import type { FlowNodeTemplateType } from '../workflow/type/node.d'; -import { ChatCompletionMessageParam } from '../ai/type'; import type { RequireOnlyOne } from '../../common/type/utils'; +import z from 'zod'; + +/* One tool run response */ +export type ToolRunResponseItemType = any; +/* tool module response */ +export const ToolModuleResponseItemSchema = z.object({ + id: z.string(), + toolName: z.string(), + toolAvatar: z.string(), + params: z.string(), + response: z.string(), + functionName: z.string() +}); +export type ToolModuleResponseItemType = z.infer; /* --------- chat ---------- */ export type ChatSchemaType = { @@ -47,33 +56,57 @@ export type ChatWithAppSchema = Omit & { }; /* --------- chat item ---------- */ -export type UserChatItemFileItemType = { - type: `${ChatFileTypeEnum}`; - name?: string; - key?: string; - url: string; -}; -export type UserChatItemValueItemType = { - text?: { - content: string; - }; - file?: UserChatItemFileItemType; -}; -export type UserChatItemType = { - obj: ChatRoleEnum.Human; - value: UserChatItemValueItemType[]; - hideInUI?: boolean; -}; +// User +export const UserChatItemFileItemSchema = z.object({ + type: z.enum(Object.values(ChatFileTypeEnum)), + name: z.string().optional(), + key: z.string().optional(), + url: z.string() +}); +export type UserChatItemFileItemType = z.infer; -export type SystemChatItemValueItemType = { - text?: { - content: string; - }; -}; -export type SystemChatItemType = { - obj: ChatRoleEnum.System; - value: SystemChatItemValueItemType[]; -}; +export const UserChatItemValueItemSchema = z.object({ + text: z + .object({ + content: z.string() + }) + .optional(), + file: UserChatItemFileItemSchema.optional() +}); +export type UserChatItemValueItemType = z.infer; + +export const UserChatItemSchema = z.object({ + obj: z.literal(ChatRoleEnum.Human), + value: z.array(UserChatItemValueItemSchema), + hideInUI: z.boolean().optional() +}); +export type UserChatItemType = z.infer; + +// System +export const SystemChatItemValueItemSchema = z.object({ + text: z + .object({ + content: z.string() + }) + .nullish() +}); +export type SystemChatItemValueItemType = z.infer; + +export const SystemChatItemSchema = z.object({ + obj: z.literal(ChatRoleEnum.System), + value: z.array(SystemChatItemValueItemSchema) +}); +export type SystemChatItemType = z.infer; + +// AI +export const AdminFbkSchema = z.object({ + feedbackDataId: z.string(), + datasetId: z.string(), + collectionId: z.string(), + q: z.string(), + a: z.string().optional() +}); +export type AdminFbkType = z.infer; export type AIChatItemValueItemType = { id?: string; @@ -136,14 +169,6 @@ export type ChatItemSchema = ChatItemObjItemType & { time: Date; }; -export type AdminFbkType = { - feedbackDataId: string; - datasetId: string; - collectionId: string; - q: string; - a?: string; -}; - export type ResponseTagItemType = { totalQuoteList?: SearchDataResponseItemType[]; llmModuleAccount?: number; @@ -180,8 +205,8 @@ export type ChatItemResponseSchemaType = { /* --------- team chat --------- */ export type ChatAppListSchema = { - apps: AppType[]; - teamInfo: teamInfoSchema; + apps: AppSchema[]; + teamInfo: any; uid?: string; }; @@ -206,30 +231,22 @@ export type ChatHistoryItemResType = DispatchNodeResponseType & { }; /* ---------- node outputs ------------ */ -export type NodeOutputItemType = { - nodeId: string; - key: NodeOutputKeyEnum; - value: any; -}; +export const NodeOutputItemSchema = z.object({ + nodeId: z.string(), + key: z.enum(Object.values(NodeOutputKeyEnum)), + value: z.any() +}); +export type NodeOutputItemType = z.infer; -/* One tool run response */ -export type ToolRunResponseItemType = any; -/* tool module response */ -export type ToolModuleResponseItemType = { - id: string; - toolName: string; // tool name - toolAvatar: string; - params: string; // tool params - response: string; - functionName: string; -}; +export const ToolCiteLinksSchema = z.object({ + name: z.string(), + url: z.string() +}); +export type ToolCiteLinksType = z.infer; -export type ToolCiteLinksType = { - name: string; - url: string; -}; /* dispatch run time */ -export type RuntimeUserPromptType = { - files: UserChatItemValueItemType['file'][]; - text: string; -}; +export const RuntimeUserPromptSchema = z.object({ + files: z.array(UserChatItemFileItemSchema), + text: z.string() +}); +export type RuntimeUserPromptType = z.infer; diff --git a/packages/global/core/chat/utils.ts b/packages/global/core/chat/utils.ts index 7987d533b..c051ba0ca 100644 --- a/packages/global/core/chat/utils.ts +++ b/packages/global/core/chat/utils.ts @@ -6,7 +6,7 @@ import { type ChatHistoryItemResType, type ChatItemType, type UserChatItemValueItemType -} from './type.d'; +} from './type'; import { sliceStrStartEnd } from '../../common/string/tools'; import { PublishChannelEnum } from '../../support/outLink/constant'; import { removeDatasetCiteText } from '../ai/llm/utils'; diff --git a/packages/global/openapi/core/chat/helperBot/api.ts b/packages/global/openapi/core/chat/helperBot/api.ts new file mode 100644 index 000000000..9d537d0c2 --- /dev/null +++ b/packages/global/openapi/core/chat/helperBot/api.ts @@ -0,0 +1,62 @@ +import { PaginationPropsSchema, PaginationResponseSchema } from '../../../type'; +import { + type HelperBotChatItemSiteType, + HelperBotTypeEnumSchema, + topAgentParamsSchema +} from '../../../../core/chat/helperBot/type'; +import { z } from 'zod'; +import type { PaginationResponse } from '../../../../../web/common/fetch/type'; + +// 分页获取记录 +export const GetHelperBotChatRecordsParamsSchema = z + .object({ + type: HelperBotTypeEnumSchema, + chatId: z.string() + }) + .and(PaginationPropsSchema); +export type GetHelperBotChatRecordsParamsType = z.infer; +export type GetHelperBotChatRecordsResponseType = PaginationResponse; + +// 删除单组对话 +export const DeleteHelperBotChatParamsSchema = z.object({ + type: HelperBotTypeEnumSchema, + chatId: z.string(), + chatItemId: z.string() +}); +export type DeleteHelperBotChatParamsType = z.infer; + +// 获取文件上传签名 +export const GetHelperBotFilePresignParamsSchema = z.object({ + type: HelperBotTypeEnumSchema, + chatId: z.string(), + filename: z.string() +}); +export type GetHelperBotFilePresignParamsType = z.infer; + +// 获取文件预览链接 +export const GetHelperBotFilePreviewParamsSchema = z.object({ + key: z.string().min(1) +}); +export type GetHelperBotFilePreviewParamsType = z.infer; +export const GetHelperBotFilePreviewResponseSchema = z.string(); + +export const HelperBotCompletionsParamsSchema = z.object({ + chatId: z.string(), + chatItemId: z.string(), + query: z.string(), + files: z.array( + z.object({ + type: z.enum(['image', 'file']), + key: z.string(), + url: z.string().optional(), + name: z.string() + }) + ), + metadata: z.discriminatedUnion('type', [ + z.object({ + type: z.literal('topAgent'), + data: topAgentParamsSchema + }) + ]) +}); +export type HelperBotCompletionsParamsType = z.infer; diff --git a/packages/global/openapi/core/chat/helperBot/index.ts b/packages/global/openapi/core/chat/helperBot/index.ts new file mode 100644 index 000000000..2077a47ed --- /dev/null +++ b/packages/global/openapi/core/chat/helperBot/index.ts @@ -0,0 +1,79 @@ +import { z } from 'zod'; +import type { OpenAPIPath } from '../../../type'; +import { + DeleteHelperBotChatParamsSchema, + GetHelperBotChatRecordsParamsSchema, + HelperBotCompletionsParamsSchema +} from './api'; +import { TagsMap } from '../../../tag'; + +export const HelperBotPath: OpenAPIPath = { + '/core/chat/helperBot/getRecords': { + get: { + summary: '分页获取记录', + description: '分页获取记录', + tags: [TagsMap.helperBot], + requestParams: { + query: GetHelperBotChatRecordsParamsSchema + }, + responses: { + 200: { + description: '成功返回记录列表', + content: { + 'application/json': { + schema: z.array(z.any()) + } + } + } + } + } + }, + '/core/chat/helperBot/deleteRecord': { + delete: { + summary: '删除单组对话', + description: '删除单组对话', + tags: [TagsMap.helperBot], + requestBody: { + content: { + 'application/json': { + schema: DeleteHelperBotChatParamsSchema + } + } + }, + responses: { + 200: { + description: '成功删除记录', + content: { + 'application/json': { + schema: z.any() + } + } + } + } + } + }, + '/core/chat/helperBot/completions': { + post: { + summary: '辅助助手对话接口', + description: '辅助助手对话接口', + tags: [TagsMap.helperBot], + requestBody: { + content: { + 'application/json': { + schema: HelperBotCompletionsParamsSchema + } + } + }, + responses: { + 200: { + description: '成功返回处理结果', + content: { + 'application/stream+json': { + schema: z.any() + } + } + } + } + } + } +}; diff --git a/packages/global/openapi/core/chat/index.ts b/packages/global/openapi/core/chat/index.ts index b97786982..c6ef5354a 100644 --- a/packages/global/openapi/core/chat/index.ts +++ b/packages/global/openapi/core/chat/index.ts @@ -5,11 +5,12 @@ import { z } from 'zod'; import { CreatePostPresignedUrlResultSchema } from '../../../../service/common/s3/type'; import { PresignChatFileGetUrlSchema, PresignChatFilePostUrlSchema } from '../../../core/chat/api'; import { TagsMap } from '../../tag'; +import { HelperBotPath } from './helperBot'; export const ChatPath: OpenAPIPath = { ...ChatSettingPath, ...ChatFavouriteAppPath, - + ...HelperBotPath, '/core/chat/presignChatFileGetUrl': { post: { summary: '获取对话文件预签名 URL', diff --git a/packages/global/openapi/index.ts b/packages/global/openapi/index.ts index 323e3148f..df2119f26 100644 --- a/packages/global/openapi/index.ts +++ b/packages/global/openapi/index.ts @@ -33,6 +33,10 @@ export const openAPIDocument = createDocument({ { name: 'ApiKey', tags: [TagsMap.apiKey] + }, + { + name: '系统接口', + tags: [TagsMap.helperBot] } ] }); diff --git a/packages/global/openapi/tag.ts b/packages/global/openapi/tag.ts index ddc4d8ce6..4f927dc32 100644 --- a/packages/global/openapi/tag.ts +++ b/packages/global/openapi/tag.ts @@ -6,5 +6,6 @@ export const TagsMap = { pluginAdmin: '管理员插件管理', pluginToolAdmin: '管理员系统工具管理', pluginTeam: '团队插件管理', - apiKey: 'APIKey' + apiKey: 'APIKey', + helperBot: '辅助助手' }; diff --git a/packages/global/openapi/type.ts b/packages/global/openapi/type.ts index 29829055c..ac1ac0429 100644 --- a/packages/global/openapi/type.ts +++ b/packages/global/openapi/type.ts @@ -27,3 +27,26 @@ export const formatSuccessResponse = (data: T) => { data }); }; + +export const PaginationPropsSchema = z + .object({ + pageSize: z.union([z.number(), z.string()]), + // offset 和 pageNum 只能传其一 + offset: z.union([z.number(), z.string()]).optional(), + pageNum: z.union([z.number(), z.string()]).optional() + }) + .refine( + (data) => (typeof data.offset !== 'undefined') !== (typeof data.pageNum !== 'undefined'), + { message: 'offset 和 pageNum 必须且只能传一个' } + ); +export type PaginationPropsType = z.infer; + +export const PaginationResponseSchema = (item: T) => + z.object({ + total: z.number(), + list: z.array(item) + }); +export type PaginationResponseType = { + total: number; + list: T[]; +}; diff --git a/packages/global/support/outLink/api.d.ts b/packages/global/support/outLink/api.d.ts index 141254577..77c378edf 100644 --- a/packages/global/support/outLink/api.d.ts +++ b/packages/global/support/outLink/api.d.ts @@ -1,4 +1,4 @@ -import type { HistoryItemType } from '../../core/chat/type.d'; +import type { HistoryItemType } from '../../core/chat/type'; import type { OutLinkSchema } from './type.d'; export type AuthOutLinkInitProps = { diff --git a/packages/service/common/s3/sources/helperbot/index.ts b/packages/service/common/s3/sources/helperbot/index.ts new file mode 100644 index 000000000..805a9cafe --- /dev/null +++ b/packages/service/common/s3/sources/helperbot/index.ts @@ -0,0 +1,121 @@ +import { parseFileExtensionFromUrl } from '@fastgpt/global/common/string/tools'; +import { S3PrivateBucket } from '../../buckets/private'; +import { S3Sources } from '../../type'; +import { + type CheckHelperBotFileKeys, + type DelChatFileByPrefixParams, + DelChatFileByPrefixSchema, + HelperBotFileUploadSchema +} from './type'; +import { differenceInHours } from 'date-fns'; +import { S3Buckets } from '../../constants'; +import path from 'path'; +import { getFileS3Key } from '../../utils'; + +export class S3HelperBotSource { + private bucket: S3PrivateBucket; + private static instance: S3HelperBotSource; + + constructor() { + this.bucket = new S3PrivateBucket(); + } + + static getInstance() { + return (this.instance ??= new S3HelperBotSource()); + } + + static parseFileUrl(url: string | URL) { + try { + const parseUrl = new URL(url); + const pathname = decodeURIComponent(parseUrl.pathname); + // 非 S3 key + if (!pathname.startsWith(`/${S3Buckets.private}/${S3Sources.helperBot}/`)) { + return { + filename: '', + extension: '', + imageParsePrefix: '' + }; + } + + const filename = pathname.split('/').pop() || 'file'; + const extension = path.extname(filename); + + return { + filename, + extension: extension.replace('.', ''), + imageParsePrefix: `${pathname.replace(`/${S3Buckets.private}/`, '').replace(extension, '')}-parsed` + }; + } catch (error) { + return { + filename: '', + extension: '', + imageParsePrefix: '' + }; + } + } + + parseKey(key: string) { + const [type, chatId, userId, filename] = key.split('/'); + return { type, chatId, userId, filename }; + } + + // 获取文件流 + getFileStream(key: string) { + return this.bucket.getObject(key); + } + + // 获取文件状态 + getFileStat(key: string) { + return this.bucket.statObject(key); + } + + // 获取文件元数据 + async getFileMetadata(key: string) { + const stat = await this.getFileStat(key); + if (!stat) return { filename: '', extension: '', contentLength: 0, contentType: '' }; + + const contentLength = stat.size; + const filename: string = decodeURIComponent(stat.metaData['origin-filename']); + const extension = parseFileExtensionFromUrl(filename); + const contentType: string = stat.metaData['content-type']; + return { + filename, + extension, + contentType, + contentLength + }; + } + + async createGetFileURL(params: { key: string; expiredHours?: number; external: boolean }) { + const { key, expiredHours = 1, external = false } = params; // 默认一个小时 + + if (external) { + return await this.bucket.createExternalUrl({ key, expiredHours }); + } + return await this.bucket.createPreviewUrl({ key, expiredHours }); + } + + async createUploadFileURL(params: CheckHelperBotFileKeys) { + const { type, chatId, userId, filename, expiredTime } = HelperBotFileUploadSchema.parse(params); + const { fileKey } = getFileS3Key.helperBot({ type, chatId, userId, filename }); + return await this.bucket.createPostPresignedUrl( + { rawKey: fileKey, filename }, + { expiredHours: expiredTime ? differenceInHours(new Date(), expiredTime) : 24 } + ); + } + + deleteFilesByPrefix(params: DelChatFileByPrefixParams) { + const { type, chatId, userId } = DelChatFileByPrefixSchema.parse(params); + + const prefix = [S3Sources.helperBot, type, userId, chatId].filter(Boolean).join('/'); + return this.bucket.addDeleteJob({ prefix }); + } + + deleteFileByKey(key: string) { + return this.bucket.addDeleteJob({ key }); + } +} + +export function getS3HelperBotSource() { + return S3HelperBotSource.getInstance(); +} diff --git a/packages/service/common/s3/sources/helperbot/type.ts b/packages/service/common/s3/sources/helperbot/type.ts new file mode 100644 index 000000000..58f351017 --- /dev/null +++ b/packages/service/common/s3/sources/helperbot/type.ts @@ -0,0 +1,18 @@ +import { z } from 'zod'; +import { HelperBotTypeEnumSchema } from '@fastgpt/global/core/chat/helperBot/type'; + +export const HelperBotFileUploadSchema = z.object({ + type: HelperBotTypeEnumSchema, + chatId: z.string().nonempty(), + userId: z.string().nonempty(), + filename: z.string().nonempty(), + expiredTime: z.date().optional() +}); +export type CheckHelperBotFileKeys = z.infer; + +export const DelChatFileByPrefixSchema = z.object({ + type: HelperBotTypeEnumSchema, + chatId: z.string().nonempty().optional(), + userId: z.string().nonempty().optional() +}); +export type DelChatFileByPrefixParams = z.infer; diff --git a/packages/service/common/s3/type.ts b/packages/service/common/s3/type.ts index 3d3f34975..4f834b17f 100644 --- a/packages/service/common/s3/type.ts +++ b/packages/service/common/s3/type.ts @@ -17,7 +17,7 @@ export type ExtensionType = keyof typeof Mimes; export type S3OptionsType = typeof defaultS3Options; -export const S3SourcesSchema = z.enum(['avatar', 'chat', 'dataset', 'temp']); +export const S3SourcesSchema = z.enum(['avatar', 'chat', 'dataset', 'temp', 'helperBot']); export const S3Sources = S3SourcesSchema.enum; export type S3SourceType = z.infer; diff --git a/packages/service/common/s3/utils.ts b/packages/service/common/s3/utils.ts index ecb706f9c..02ff0461a 100644 --- a/packages/service/common/s3/utils.ts +++ b/packages/service/common/s3/utils.ts @@ -11,6 +11,7 @@ import { getNanoid } from '@fastgpt/global/common/string/tools'; import path from 'node:path'; import type { ParsedFileContentS3KeyParams } from './sources/dataset/type'; import { EndpointUrl } from '@fastgpt/global/common/file/constants'; +import type { HelperBotTypeEnumType } from '@fastgpt/global/core/chat/helperBot/type'; // S3文件名最大长度配置 export const S3_FILENAME_MAX_LENGTH = 50; @@ -194,6 +195,25 @@ export const getFileS3Key = { }; }, + helperBot: ({ + type, + chatId, + userId, + filename + }: { + type: HelperBotTypeEnumType; + chatId: string; + userId: string; + filename: string; + }) => { + const { formatedFilename, extension } = getFormatedFilename(filename); + const basePrefix = [S3Sources.helperBot, type, userId, chatId].filter(Boolean).join('/'); + return { + fileKey: [basePrefix, `${formatedFilename}${extension ? `.${extension}` : ''}`].join('/'), + fileParsedPrefix: [basePrefix, `${formatedFilename}-parsed`].join('/') + }; + }, + // 上传数据集的文件的解析结果的图片的 Key dataset: (params: ParsedFileContentS3KeyParams) => { const { datasetId, filename } = params; diff --git a/packages/service/core/chat/HelperBot/chatItemSchema.ts b/packages/service/core/chat/HelperBot/chatItemSchema.ts new file mode 100644 index 000000000..c7d749df3 --- /dev/null +++ b/packages/service/core/chat/HelperBot/chatItemSchema.ts @@ -0,0 +1,42 @@ +import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; +import { connectionMongo, getMongoModel } from '../../../common/mongo'; +const { Schema } = connectionMongo; +import { helperBotChatItemCollectionName } from './constants'; +import { HelperBotTypeEnum } from '@fastgpt/global/core/chat/helperBot/type'; +import type { HelperBotChatItemType } from '@fastgpt/global/core/chat/helperBot/type'; + +const HelperBotChatItemSchema = new Schema({ + userId: { + type: String, + require: true + }, + chatId: { + type: String, + require: true + }, + dataId: { + type: String, + require: true + }, + createTime: { + type: Date, + default: () => new Date() + }, + obj: { + type: String, + require: true, + enum: Object.values(ChatRoleEnum) + }, + value: { + type: Array, + require: true + }, + memories: Object +}); + +HelperBotChatItemSchema.index({ userId: 1, chatId: 1, dataId: 1, obj: 1 }, { unique: true }); + +export const MongoHelperBotChatItem = getMongoModel( + helperBotChatItemCollectionName, + HelperBotChatItemSchema +); diff --git a/packages/service/core/chat/HelperBot/chatSchema.ts b/packages/service/core/chat/HelperBot/chatSchema.ts new file mode 100644 index 000000000..1ba519cd0 --- /dev/null +++ b/packages/service/core/chat/HelperBot/chatSchema.ts @@ -0,0 +1,36 @@ +import { connectionMongo, getMongoModel } from '../../../common/mongo'; +const { Schema } = connectionMongo; +import { helperBotChatCollectionName } from './constants'; +import { HelperBotTypeEnum } from '@fastgpt/global/core/chat/helperBot/type'; +import type { HelperBotChatType } from '../../../../global/core/chat/helperBot/type'; + +const HelperBotChatSchema = new Schema({ + type: { + type: String, + required: true, + enum: Object.values(HelperBotTypeEnum) + }, + userId: { + type: String, + require: true + }, + chatId: { + type: String, + require: true + }, + createTime: { + type: Date, + default: () => new Date() + }, + updateTime: { + type: Date, + default: () => new Date() + } +}); + +HelperBotChatSchema.index({ type: 1, userId: 1, chatId: 1 }, { unique: true }); + +export const MongoHelperBotChat = getMongoModel( + helperBotChatCollectionName, + HelperBotChatSchema +); diff --git a/packages/service/core/chat/HelperBot/constants.ts b/packages/service/core/chat/HelperBot/constants.ts new file mode 100644 index 000000000..a0d4a00d0 --- /dev/null +++ b/packages/service/core/chat/HelperBot/constants.ts @@ -0,0 +1,2 @@ +export const helperBotChatCollectionName = 'helper_bot_chats'; +export const helperBotChatItemCollectionName = 'helper_bot_chat_items'; diff --git a/packages/service/core/chat/HelperBot/dispatch/index.ts b/packages/service/core/chat/HelperBot/dispatch/index.ts new file mode 100644 index 000000000..8f5719ba4 --- /dev/null +++ b/packages/service/core/chat/HelperBot/dispatch/index.ts @@ -0,0 +1,6 @@ +import { HelperBotTypeEnum } from '@fastgpt/global/core/chat/helperBot/type'; +import { dispatchTopAgent } from './topAgent'; + +export const dispatchMap = { + [HelperBotTypeEnum.topAgent]: dispatchTopAgent +}; diff --git a/packages/service/core/chat/HelperBot/dispatch/topAgent/index.ts b/packages/service/core/chat/HelperBot/dispatch/topAgent/index.ts new file mode 100644 index 000000000..65d735671 --- /dev/null +++ b/packages/service/core/chat/HelperBot/dispatch/topAgent/index.ts @@ -0,0 +1,10 @@ +import type { HelperBotDispatchParamsType } from '../type'; +import { helperChats2GPTMessages } from '@fastgpt/global/core/chat/helperBot/adaptor'; + +export const dispatchTopAgent = async (props: HelperBotDispatchParamsType) => { + const { query, files, metadata, histories } = props; + const messages = helperChats2GPTMessages({ + messages: histories, + reserveTool: false + }); +}; diff --git a/packages/service/core/chat/HelperBot/dispatch/topAgent/prompt.ts b/packages/service/core/chat/HelperBot/dispatch/topAgent/prompt.ts new file mode 100644 index 000000000..2baadc002 --- /dev/null +++ b/packages/service/core/chat/HelperBot/dispatch/topAgent/prompt.ts @@ -0,0 +1,413 @@ +export const getPrompt = ({ resourceList }: { resourceList: string }) => { + return ` + + +你是一个专业的**流程架构师**和**智能化搭建专家**,专门帮助搭建者设计可复用的Agent执行流程模板。 + +**核心价值**:让搭建者能够快速创建高质量的执行流程,为后续用户提供标准化的问题解决方案。 + +**核心能力**: +- 流程抽象化:将具体需求抽象为通用流程模板 +- 参数化设计:识别可变参数和固定逻辑 +- 能力边界识别:严格基于系统现有工具、知识库、文件处理等能力进行规划 +- 复用性优化:确保模板在不同场景下的适应性 + + + +**核心目标**:为搭建者设计可复用的执行流程,包含: +1. 明确的步骤序列 +2. 标准化的工具调用 +3. 合理的决策点设计 +4. 100%基于系统能力的可行性保证 + +**输出价值**: +- 搭建者可以直接使用或参考这个流程设计 +- 最终用户可以通过这个流程解决相关问题 +- 系统可以保证完全的可执行性 + + + +当处于信息收集阶段时: + +**重要前提**:你需要为搭建者设计可复用的执行流程模板,而不仅仅是解决单个问题。 + +**信息收集目标**:收集设计高质量流程模板所必需的核心信息,包括: + +1. **任务类型与场景识别**(首要任务) + - 通过开放式提问,了解用户想要实现的具体功能 + - 基于用户描述,识别和归纳出任务所属的场景类型 + - 理解任务的核心特征、目标和定位 + - 为后续信息收集确定方向和重点 + +2. **能力边界确认**(最关键,必须优先确认) + - **基于实际工具列表**确认系统能力: + * 需要使用哪些核心工具(从可用工具列表中选择) + * 这些工具的具体功能和限制条件 + * 工具之间的组合使用方式 + - **明确不支持的功能范围**(重点): + * 哪些功能系统当前无法实现 + * 哪些操作没有对应的工具支持 + * 用户可能期望但实际不可行的需求 + - **技术限制和约束条件**: + * 数据格式、大小、性能等限制 + * 第三方服务的可用性和配置需求 + * 用户权限和资源约束 + +3. **流程定位信息** + - 目标用户群体和典型使用场景 + - 解决问题的具体类型和适用范围 + - 流程的核心价值和预期效果 + +4. **输入输出规范** + - 流程的输入参数类型、格式和来源 + - 输出结果的规范、格式和目标 + - 参数约束条件(必选/可选/默认值/取值范围) + +5. **可变逻辑识别** + - 哪些步骤需要根据参数动态调整 + - 决策点的判断条件和分支逻辑 + - 可配置的工具选项和参数映射关系 + +**关键原则**: +- **能力边界优先**:必须先确认系统能做什么,再设计流程细节 +- **工具列表约束**:严格基于可用工具列表,不假设任何未提供的能力 +- **场景分类明确**:首先明确任务类型,指导后续信息收集方向 +- **问题精准聚焦**:每个问题都直接服务于输出准确的信息和工具列表 +- **明确不可行项**:重点确认哪些功能不能做,避免后续生成无法执行的流程 + +**信息收集顺序建议**: +1. 先问任务类型/场景(确定大方向,对应输出type字段) +2. 再问能力边界(确认可行性,识别可用工具) +3. 然后问流程定位(明确具体目标) +4. 最后问输入输出、可变逻辑(完善设计细节) + + +**输出格式**: + +**重要:信息收集阶段的所有回复必须使用JSON格式,包含推理过程** + +直接输出以下格式的JSON(不要添加代码块标记): +{ + "reasoning": "为什么问这个问题的推理过程:基于什么考虑、希望收集什么信息、对后续有什么帮助", + "question": "实际向用户提出的问题内容" +} + +问题内容可以是开放式问题,也可以包含选项: + +开放式问题示例: +{ + "reasoning": "需要首先了解任务的基本定位和目标场景,这将决定后续需要确认的工具类型和能力边界", + "question": "我想了解一下您希望这个流程模板实现什么功能?能否详细描述一下具体要处理什么样的任务或问题?" +} + +选择题示例: +{ + "reasoning": "需要确认参数化设计的重点方向,这将影响流程模板的灵活性设计", + "question": "关于流程的参数化设计,用户最需要调整的是:\\nA. 输入数据源(不同类型的数据库/文件)\\nB. 处理参数(阈值、过滤条件、算法选择)\\nC. 输出格式(报告类型、文件格式、目标系统)\\nD. 执行环境(触发方式、频率、并发度)\\n\\n请选择最符合的选项,或输入您的详细回答:" +} + +选项设计原则: +1. 选项要覆盖主要可能性(3-4个为佳) +2. 包含"其他"选项让用户可以自由回答 +3. 选项要简洁明了,便于快速理解 +4. 当问题涉及量化、分类、优先级时优先使用选择题 + +适合选择题的场景: +- 经验水平判断(初学者/有经验/熟练/专家) +- 优先级排序(时间/质量/成本/创新) +- 任务类型分类(分析/设计/开发/测试) +- 满意度评估(非常满意/满意/一般/不满意) +- 复杂度判断(简单/中等/复杂/极复杂) + +避免的行为: +- 不要为所有问题都强制提供选项 +- 选项之间要有明显的区分度 +- 不要使用过于技术化的术语 + + + +**系统能力边界确认**: + +**动态约束原则**: +1. **只规划现有能力**:只能使用系统当前提供的工具和功能 +2. **基于实际能力判断**:如果系统有编程工具,就可以规划编程任务 +3. **能力适配规划**:根据可用工具库的能力边界来设计流程 +4. **避免能力假设**:不能假设系统有未明确提供的能力 + +**规划前自我检查**: +- 这个步骤需要什么具体能力? +- 当前系统中是否有对应的工具提供这种能力? +- 用户是否具备使用该工具的条件? +- 如果没有合适的工具,能否用现有能力组合实现? + +**能力发现机制**: +- 优先使用系统中明确提供的工具 +- 探索现有工具的组合能力 +- 基于实际可用能力设计解决方案 +- 避免依赖系统中不存在的能力 + +**重要提醒**:请基于下面提供的可用工具列表,仔细分析系统能力边界,确保规划的每个步骤都有对应的工具支持。 + + + +当处于计划生成阶段时: + + +**系统资源定义**(重要:理解三类资源的本质区别) + +**工具 (Tools)**: +- 定义:可以执行特定功能的能力模块 +- 功能:执行操作、调用API、处理数据、生成内容等 +- 特点:主动执行,产生结果或副作用 +- 示例:搜索引擎、数据库操作、邮件发送、内容生成 + +**知识库 (Knowledges)**: +- 定义:系统上已经搭建好的文件存储系统,包含特定领域的结构化信息 +- 功能:存储和检索信息,提供领域知识查询 +- 特点:被动查询,返回已存储的信息 +- 示例:产品文档库、技术手册、行业知识库 + +**系统功能 (System Features)**: +- 定义:平台级的功能开关,控制执行流程的特殊能力 +- 功能:影响任务执行方式的系统级配置 +- 特点:开关控制,改变交互模式 +- 示例:文件上传、用户交互、实时数据流 + +**关键区别**: +- 工具 = "做事情"(执行动作、调用服务、处理数据) +- 知识库 = "查信息"(检索已有知识、获取领域信息) +- 系统功能 = "改变模式"(启用特殊交互方式、系统级能力) + +**选择建议**: +- 需要执行操作(搜索、发送、计算、转换)→ 选择工具 +- 需要查询特定领域的信息(产品资料、技术文档、行业知识)→ 选择知识库 +- 需要用户提供文件/特殊交互方式 → 启用系统功能 +- 三者可以配合使用:例如用搜索工具获取实时信息,用知识库补充领域知识,启用文件上传让用户提供私有数据 + + +**可用资源列表**: +""" +${resourceList} +""" + +**计划生成要求**: +1. 严格按照JSON格式输出 +2. **严格确保所有引用的资源都在可用资源列表中** - 这是硬性要求 +3. 考虑搭建者的实际约束条件(时间、资源、技能等) +4. **绝不要使用任何不在可用资源列表中的资源** - 违背此项将导致计划被拒绝 + +**🚨 资源使用严格限制(极其重要)**: + +**资源识别规则**: +1. 在上面的"## 可用资源列表"中查找所有可用资源 +2. 每个资源ID后面都有标签:[工具] 或 [知识库] +3. 输出时必须根据标签确定 type 值: + - 标签是 [工具] → "type": "tool" + - 标签是 [知识库] → "type": "knowledge" + +**输出格式要求**: +- ✅ 必须使用对象数组格式:[{"id": "...", "type": "..."}] +- ✅ 资源ID必须完全匹配列表中的ID(包括大小写、特殊字符) +- ❌ 不要使用字符串数组格式:["...", "..."] +- ❌ 不要猜测 type 值,必须根据列表中的标签确定 + +**输出前的自我检查步骤**: +1. 查看你选择的每个资源ID,它在列表中的标签是什么? +2. 如果标签是 [工具] → 设置 "type": "tool" +3. 如果标签是 [知识库] → 设置 "type": "knowledge" +4. 确保每个资源都有 id 和 type 两个字段 + +**常见错误避免**: +- ❌ 不要凭空想象资源名称 +- ❌ 不要使用通用描述如"数据库工具"而不指定具体ID +- ❌ 不要引用"可能"存在但未在列表中明确的资源 +- ❌ 不要输出字符串数组,必须是对象数组 +- ❌ 不要把 [知识库] 标签的资源设置为 type: "tool" +- ✅ 必须根据列表中的标签准确设置 type 值 +- ✅ 基于实际可用的资源进行规划 + +**深度分析框架**(内部思考过程,不输出): +🔍 第一层:任务本质分析 +- 识别用户的核心目标和真实意图 +- 分析任务的复杂度、范围和关键约束 +- 确定主要的功能需求和预期成果 + +📋 第二层:资源需求识别 +根据任务特点,识别需要的三类资源: +- 需要哪些工具来执行操作?(搜索、计算、生成、发送等) +- 需要哪些知识库来获取领域知识?(产品资料、技术文档等) +- 需要哪些系统功能来改变交互模式?(是否需要用户上传文件?) + +🎯 第三层:精确资源匹配 +从可用资源列表中选择最合适的资源: +- 工具选择:基于任务细节选择功能最匹配的工具 +- 知识库选择:基于领域需求选择相关知识库 +- 系统功能判断: + * 是否需要用户的私有文件?→ 启用 file_upload + * 数据能否通过工具获取?→ 不需要 file_upload + +🔧 第四层:资源整合 +- 收集所有需要的工具、知识库和系统功能 +- 去除重复项 +- 确保所有工具和知识库ID都在可用列表中 +- 形成完整的 resources 配置 + +**输出要求**: +**重要:只输出JSON,不要添加任何解释文字、代码块标记或其他内容!** + +直接输出以下格式的JSON: +{ + "task_analysis": { + "goal": "任务的核心目标描述", + "role": "该流程的角色信息", + "key_features": "收集到的信息,对任务的深度理解和定位" + }, + "reasoning": "详细说明所有资源的选择理由:工具、知识库和系统功能如何协同工作来完成任务目标", + "resources": { + "tools": [ + {"id": "工具ID", "type": "tool"} + ], + "knowledges": [ + {"id": "知识库ID", "type": "knowledge"} + ], + "system_features": { + "file_upload": { + "enabled": true/false, + "purpose": "说明原因(enabled=true时必填)", + "file_types": ["可选的文件类型"] + } + } + } +} + +**字段说明**: +- task_analysis: 提供对任务的深度理解和角色定义 +- reasoning: 说明所有资源(工具+知识库+系统功能)的选择理由和协同关系 +- resources: 资源配置对象,包含三类资源 + * tools: 工具数组,每个对象包含 id 和 type(值为"tool") + * knowledges: 知识库数组,每个对象包含 id 和 type(值为"knowledge") + * system_features: 系统功能配置对象 + - file_upload.enabled: 是否需要文件上传(必填) + - file_upload.purpose: 为什么需要(enabled=true时必填) + - file_upload.file_types: 建议的文件类型(可选),如["pdf", "xlsx"] + +**✅ 正确示例1**(需要文件上传): +{ + "task_analysis": { + "goal": "分析用户的财务报表数据", + "role": "财务数据分析专家" + }, + "reasoning": "使用数据分析工具处理Excel数据,需要用户上传自己的财务报表文件", + "resources": { + "tools": [ + {"id": "data_analysis/tool", "type": "tool"} + ], + "knowledges": [], + "system_features": { + "file_upload": { + "enabled": true, + "purpose": "需要您上传财务报表文件(Excel或PDF格式)进行数据提取和分析", + "file_types": ["xlsx", "xls", "pdf"] + } + } + } +} + +**✅ 正确示例2**(不需要文件上传): +{ + "reasoning": "使用搜索工具获取实时信息,结合知识库的专业知识", + "resources": { + "tools": [ + {"id": "metaso/metasoSearch", "type": "tool"} + ], + "knowledges": [ + {"id": "travel_kb", "type": "knowledge"} + ], + "system_features": { + "file_upload": { + "enabled": false + } + } + } +} + +**❌ 错误示例1**(使用旧格式): +{ + "tools": [...] // ❌ 错误:应该使用 resources.tools +} + +**❌ 错误示例2**(system_features 中的配置错误): +{ + "resources": { + "system_features": { + "file_upload": { + "enabled": true + // ❌ 错误:启用时缺少 purpose 字段 + } + } + } +} + +**严格输出规则**: +- ❌ 不要使用 \`\`\`json 或其他代码块标记 +- ❌ 不要使用旧格式的 tools 字段,必须使用 resources 结构 +- ❌ 不要添加任何解释性文字或前言后语 +- ✅ 必须使用 resources 对象,包含 tools、knowledges、system_features +- ✅ file_upload.enabled=true 时必须提供 purpose 字段 +- ✅ knowledges 或 tools 可以为空数组(如果不需要) +- ✅ 直接、纯净地输出JSON内容 + +质量要求: +1. 任务理解深度:确保分析基于对用户需求的深度理解 +2. 资源匹配精度:每个资源的选择都要有明确的理由 +3. 资源完整性:确保所有必需的资源都包含在 resources 配置中 +4. 输出格式规范:严格遵循 resources 结构要求 +5. 资源去重:同一个资源在 tools 或 knowledges 数组中只出现一次 +6. type准确性:工具的type为"tool",知识库的type为"knowledge" +7. 系统功能配置正确:file_upload.enabled=true时必须提供purpose字段 +8. 输出纯净性:只输出JSON,不包含任何其他内容 + + + +**信息收集完成条件**(满足任一即可切换到计划生成): +- 已收集到明确的目标描述、主要约束条件和成功标准 +- 对话轮次达到6轮 +- 用户表示信息足够完整 +- 当前收集的信息已足够制定可行的计划 + +**重要:切换标识符要求** +当判断「信息收集已完成」,准备切换到计划生成阶段时,**必须**在回复的开始处包含以下标识符: +\`\`\` +「信息收集已完成」 +\`\`\` + +然后继续说: +"基于我们刚才的交流,我已经收集到足够的信息来为您制定执行计划。现在让我根据您的需求和实际情况,生成详细的任务分解方案。" + +**示例格式**: +\`\`\` +「信息收集已完成」 + +基于我们刚才的交流,我已经收集到足够的信息来为您制定执行计划... +\`\`\` + +这样系统会自动检测到标识符并切换到计划生成阶段。 + + + +**回复格式**: +- 信息收集阶段:专业、友善、充满好奇心,每轮只问1-2个核心问题 +- 计划生成阶段:直接输出JSON格式的执行计划 + +**特殊命令处理**: +- 如果搭建者明确要求"直接生成流程",可以开始规划 +- 如果需要补充信息,可以回到信息收集 +- 避免过度询问,保持高效(3-4轮完成信息收集) + +**质量保证**: +- 收集的信息要具体、准确、可验证 +- 生成的计划要基于收集到的信息 +- 确保计划中的每个步骤都是可执行的 +- 严格基于系统能力边界进行规划 +`; +}; diff --git a/packages/service/core/chat/HelperBot/dispatch/type.ts b/packages/service/core/chat/HelperBot/dispatch/type.ts new file mode 100644 index 000000000..1e37c5217 --- /dev/null +++ b/packages/service/core/chat/HelperBot/dispatch/type.ts @@ -0,0 +1,16 @@ +import { z } from 'zod'; +import { HelperBotCompletionsParamsSchema } from '../../../../../global/openapi/core/chat/helperBot/api'; +import { HelperBotChatItemSchema } from '@fastgpt/global/core/chat/helperBot/type'; +import { WorkflowResponseFnSchema } from '../../../workflow/dispatch/type'; + +export const HelperBotDispatchParamsSchema = z.object({ + query: z.string(), + files: HelperBotCompletionsParamsSchema.shape.files, + metadata: HelperBotCompletionsParamsSchema.shape.metadata, + histories: z.array(HelperBotChatItemSchema), + workflowResponseWrite: WorkflowResponseFnSchema +}); +export type HelperBotDispatchParamsType = z.infer; + +export const HelperBotDispatchResponseSchema = z.object({}); +export type HelperBotDispatchResponseType = z.infer; diff --git a/packages/service/core/chat/chatSchema.ts b/packages/service/core/chat/chatSchema.ts index 8ca948960..8ba8f9de7 100644 --- a/packages/service/core/chat/chatSchema.ts +++ b/packages/service/core/chat/chatSchema.ts @@ -1,6 +1,6 @@ import { connectionMongo, getMongoModel } from '../../common/mongo'; const { Schema } = connectionMongo; -import { type ChatSchemaType } from '@fastgpt/global/core/chat/type.d'; +import { type ChatSchemaType } from '@fastgpt/global/core/chat/type'; import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants'; import { TeamCollectionName, diff --git a/packages/service/core/chat/saveChat.ts b/packages/service/core/chat/saveChat.ts index 60f0ccc5b..31a374f29 100644 --- a/packages/service/core/chat/saveChat.ts +++ b/packages/service/core/chat/saveChat.ts @@ -2,7 +2,7 @@ import type { AIChatItemType, ChatHistoryItemResType, UserChatItemType -} from '@fastgpt/global/core/chat/type.d'; +} from '@fastgpt/global/core/chat/type'; import { MongoApp } from '../app/schema'; import type { ChatSourceEnum } from '@fastgpt/global/core/chat/constants'; import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; diff --git a/packages/service/core/workflow/dispatch/abandoned/runApp.ts b/packages/service/core/workflow/dispatch/abandoned/runApp.ts index b30d444f9..8305e0672 100644 --- a/packages/service/core/workflow/dispatch/abandoned/runApp.ts +++ b/packages/service/core/workflow/dispatch/abandoned/runApp.ts @@ -1,5 +1,5 @@ /* Abandoned */ -import type { ChatItemType } from '@fastgpt/global/core/chat/type.d'; +import type { ChatItemType } from '@fastgpt/global/core/chat/type'; import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type'; import { type SelectAppItemType } from '@fastgpt/global/core/workflow/template/system/abandoned/runApp/type'; import { runWorkflow } from '../index'; diff --git a/packages/service/core/workflow/dispatch/ai/chat.ts b/packages/service/core/workflow/dispatch/ai/chat.ts index 8c41a0103..45a7560d8 100644 --- a/packages/service/core/workflow/dispatch/ai/chat.ts +++ b/packages/service/core/workflow/dispatch/ai/chat.ts @@ -1,5 +1,5 @@ import { filterGPTMessageByMaxContext } from '../../../ai/llm/utils'; -import type { ChatItemType, UserChatItemValueItemType } from '@fastgpt/global/core/chat/type.d'; +import type { ChatItemType, UserChatItemFileItemType } from '@fastgpt/global/core/chat/type'; import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { textAdaptGptResponse } from '@fastgpt/global/core/workflow/runtime/utils'; @@ -319,7 +319,7 @@ async function getMultiInput({ runningUserInfo }: { histories: ChatItemType[]; - inputFiles: UserChatItemValueItemType['file'][]; + inputFiles: UserChatItemFileItemType[]; fileLinks?: string[]; stringQuoteText?: string; // file quote requestOrigin?: string; @@ -371,7 +371,9 @@ async function getMultiInput({ return { documentQuoteText: text, - userFiles: fileLinks.map((url) => parseUrlToFileType(url)).filter(Boolean) + userFiles: fileLinks + .map((url) => parseUrlToFileType(url)) + .filter(Boolean) as UserChatItemFileItemType[] }; } @@ -402,7 +404,7 @@ async function getChatMessages({ systemPrompt: string; userChatInput: string; - userFiles: UserChatItemValueItemType['file'][]; + userFiles: UserChatItemFileItemType[]; documentQuoteText?: string; // document quote }) { // Dataset prompt ====> diff --git a/packages/service/core/workflow/dispatch/ai/classifyQuestion.ts b/packages/service/core/workflow/dispatch/ai/classifyQuestion.ts index 1e58ada8f..1716f1978 100644 --- a/packages/service/core/workflow/dispatch/ai/classifyQuestion.ts +++ b/packages/service/core/workflow/dispatch/ai/classifyQuestion.ts @@ -1,5 +1,5 @@ import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt'; -import type { ChatItemType } from '@fastgpt/global/core/chat/type.d'; +import type { ChatItemType } from '@fastgpt/global/core/chat/type'; import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import type { ClassifyQuestionAgentItemType } from '@fastgpt/global/core/workflow/template/system/classifyQuestion/type'; import type { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; diff --git a/packages/service/core/workflow/dispatch/ai/extract.ts b/packages/service/core/workflow/dispatch/ai/extract.ts index f25073ae2..8951bf6c3 100644 --- a/packages/service/core/workflow/dispatch/ai/extract.ts +++ b/packages/service/core/workflow/dispatch/ai/extract.ts @@ -1,6 +1,6 @@ import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt'; import { filterGPTMessageByMaxContext } from '../../../ai/llm/utils'; -import type { ChatItemType } from '@fastgpt/global/core/chat/type.d'; +import type { ChatItemType } from '@fastgpt/global/core/chat/type'; import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import type { ContextExtractAgentItemType } from '@fastgpt/global/core/workflow/template/system/contextExtract/type'; import type { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; diff --git a/packages/service/core/workflow/dispatch/ai/tool/index.ts b/packages/service/core/workflow/dispatch/ai/tool/index.ts index 8b019dd3c..08d3de542 100644 --- a/packages/service/core/workflow/dispatch/ai/tool/index.ts +++ b/packages/service/core/workflow/dispatch/ai/tool/index.ts @@ -9,7 +9,11 @@ import { getLLMModel } from '../../../../ai/model'; import { filterToolNodeIdByEdges, getNodeErrResponse, getHistories } from '../../utils'; import { runToolCall } from './toolCall'; import { type DispatchToolModuleProps, type ToolNodeItemType } from './type'; -import { type ChatItemType, type UserChatItemValueItemType } from '@fastgpt/global/core/chat/type'; +import type { + UserChatItemFileItemType, + ChatItemType, + UserChatItemValueItemType +} from '@fastgpt/global/core/chat/type'; import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import { GPTMessages2Chats, @@ -121,10 +125,7 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise< fileLinks, inputFiles: globalFiles, hasReadFilesTool, - usageId, - appId: props.runningAppInfo.id, - chatId: props.chatId, - uId: props.uid + usageId }); const concatenateSystemPrompt = [ @@ -284,10 +285,7 @@ const getMultiInput = async ({ customPdfParse, inputFiles, hasReadFilesTool, - usageId, - appId, - chatId, - uId + usageId }: { runningUserInfo: ChatDispatchProps['runningUserInfo']; histories: ChatItemType[]; @@ -295,12 +293,9 @@ const getMultiInput = async ({ requestOrigin?: string; maxFiles: number; customPdfParse?: boolean; - inputFiles: UserChatItemValueItemType['file'][]; + inputFiles: UserChatItemFileItemType[]; hasReadFilesTool: boolean; usageId?: string; - appId: string; - chatId?: string; - uId: string; }) => { // Not file quote if (!fileLinks || hasReadFilesTool) { @@ -334,7 +329,9 @@ const getMultiInput = async ({ return { documentQuoteText: text, - userFiles: fileLinks.map((url) => parseUrlToFileType(url)).filter(Boolean) + userFiles: fileLinks + .map((url) => parseUrlToFileType(url)) + .filter(Boolean) as UserChatItemFileItemType[] }; }; diff --git a/packages/service/core/workflow/dispatch/child/runApp.ts b/packages/service/core/workflow/dispatch/child/runApp.ts index 4c2d4773d..9d1c0c856 100644 --- a/packages/service/core/workflow/dispatch/child/runApp.ts +++ b/packages/service/core/workflow/dispatch/child/runApp.ts @@ -1,4 +1,4 @@ -import type { ChatItemType } from '@fastgpt/global/core/chat/type.d'; +import type { ChatItemType } from '@fastgpt/global/core/chat/type'; import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type'; import { runWorkflow } from '../index'; import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; diff --git a/packages/service/core/workflow/dispatch/index.ts b/packages/service/core/workflow/dispatch/index.ts index 57a1839f8..53bebf02d 100644 --- a/packages/service/core/workflow/dispatch/index.ts +++ b/packages/service/core/workflow/dispatch/index.ts @@ -5,7 +5,7 @@ import type { ChatHistoryItemResType, NodeOutputItemType, ToolRunResponseItemType -} from '@fastgpt/global/core/chat/type.d'; +} from '@fastgpt/global/core/chat/type'; import type { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { NodeInputKeyEnum, VariableInputEnum } from '@fastgpt/global/core/workflow/constants'; import { diff --git a/packages/service/core/workflow/dispatch/tools/queryExternsion.ts b/packages/service/core/workflow/dispatch/tools/queryExternsion.ts index c06a61c6a..f87008278 100644 --- a/packages/service/core/workflow/dispatch/tools/queryExternsion.ts +++ b/packages/service/core/workflow/dispatch/tools/queryExternsion.ts @@ -1,4 +1,4 @@ -import type { ChatItemType } from '@fastgpt/global/core/chat/type.d'; +import type { ChatItemType } from '@fastgpt/global/core/chat/type'; import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type'; import type { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants'; diff --git a/packages/service/core/workflow/dispatch/type.d.ts b/packages/service/core/workflow/dispatch/type.ts similarity index 74% rename from packages/service/core/workflow/dispatch/type.d.ts rename to packages/service/core/workflow/dispatch/type.ts index b0f327de6..fb05c0051 100644 --- a/packages/service/core/workflow/dispatch/type.d.ts +++ b/packages/service/core/workflow/dispatch/type.ts @@ -3,15 +3,18 @@ import type { ChatHistoryItemResType, ToolRunResponseItemType } from '@fastgpt/global/core/chat/type'; -import { ChatItemValueItemType } from '@fastgpt/global/core/chat/type'; import type { DispatchNodeResponseKeyEnum, SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import type { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type'; -import type { WorkflowInteractiveResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type'; +import type { + InteractiveNodeResponseType, + WorkflowInteractiveResponseType +} from '@fastgpt/global/core/workflow/template/system/interactive/type'; import type { RuntimeEdgeItemType } from '@fastgpt/global/core/workflow/type/edge'; import type { ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type'; +import z from 'zod'; export type WorkflowDebugResponse = { memoryEdges: RuntimeEdgeItemType[]; @@ -41,10 +44,16 @@ export type DispatchFlowResponse = { durationSeconds: number; }; -export type WorkflowResponseType = (e: { - id?: string; - stepId?: string; +export const WorkflowResponseFnSchema = z.function({ + input: z.tuple([ + z.object({ + id: z.string().optional(), + stepId: z.string().optional(), + event: z.custom(), + data: z.record(z.string(), z.any()) + }) + ]), + output: z.void() +}); - event: SseResponseEventEnum; - data: Record; -}) => void; +export type WorkflowResponseType = z.infer; diff --git a/packages/service/core/workflow/dispatch/utils.ts b/packages/service/core/workflow/dispatch/utils.ts index b6ffea6c6..e52443d8c 100644 --- a/packages/service/core/workflow/dispatch/utils.ts +++ b/packages/service/core/workflow/dispatch/utils.ts @@ -1,6 +1,6 @@ import { getErrText } from '@fastgpt/global/common/error/utils'; import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; -import type { ChatItemType, UserChatItemFileItemType } from '@fastgpt/global/core/chat/type.d'; +import type { ChatItemType, UserChatItemFileItemType } from '@fastgpt/global/core/chat/type'; import { NodeOutputKeyEnum, VariableInputEnum } from '@fastgpt/global/core/workflow/constants'; import type { VariableItemType } from '@fastgpt/global/core/app/type'; import { encryptSecret } from '../../../common/secret/aes256gcm'; diff --git a/packages/web/common/fetch/type.d.ts b/packages/web/common/fetch/type.d.ts index 58e55bca4..9d730c921 100644 --- a/packages/web/common/fetch/type.d.ts +++ b/packages/web/common/fetch/type.d.ts @@ -1,13 +1,13 @@ import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils'; -type PaginationProps = T & { +export type PaginationProps = T & { pageSize: number | string; } & RequireOnlyOne<{ offset: number | string; pageNum: number | string; }>; -type PaginationResponse = { +export type PaginationResponse = { total: number; list: T[]; }; diff --git a/packages/web/hooks/useScrollPagination.tsx b/packages/web/hooks/useScrollPagination.tsx index 708423b2c..6ae7f6714 100644 --- a/packages/web/hooks/useScrollPagination.tsx +++ b/packages/web/hooks/useScrollPagination.tsx @@ -2,7 +2,6 @@ import React, { type ReactNode, type RefObject, useMemo, useRef, useState } from import { Box, type BoxProps } from '@chakra-ui/react'; import { useToast } from './useToast'; import { getErrText } from '@fastgpt/global/common/error/utils'; -import { type PaginationProps, type PaginationResponse } from '../common/fetch/type'; import { useBoolean, useLockFn, @@ -15,6 +14,7 @@ import { import MyBox from '../components/common/MyBox'; import { useTranslation } from 'next-i18next'; import { useRequest2 } from './useRequest'; +import type { PaginationPropsType, PaginationResponseType } from '@fastgpt/global/openapi/type'; type ItemHeight = (index: number, data: T) => number; const thresholdVal = 100; @@ -31,8 +31,8 @@ export type ScrollListType = ({ } & BoxProps) => React.JSX.Element; export function useVirtualScrollPagination< - TParams extends PaginationProps, - TData extends PaginationResponse + TParams extends PaginationPropsType, + TData extends PaginationResponseType >( api: (data: TParams) => Promise, { @@ -179,8 +179,8 @@ export function useVirtualScrollPagination< } export function useScrollPagination< - TParams extends PaginationProps, - TData extends PaginationResponse + TParams extends PaginationPropsType, + TData extends PaginationResponseType >( api: (data: TParams) => Promise, { diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/ChatItem.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/ChatItem.tsx index 566596841..207ca2c1c 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/ChatItem.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/ChatItem.tsx @@ -239,7 +239,7 @@ const ChatItem = ({ hasPlanCheck, ...props }: Props) => { return []; }, [chat.obj, chat.value, isChatting]); - console.log(chat.value, splitAiResponseResults, 232); + const setCiteModalData = useContextSelector(ChatItemContext, (v) => v.setCiteModalData); const onOpenCiteModal = useMemoizedFn( (item?: { diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/SelectMarkCollection.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/SelectMarkCollection.tsx index 507d2d3c9..3df4b6184 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/SelectMarkCollection.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/SelectMarkCollection.tsx @@ -6,7 +6,7 @@ import MyIcon from '@fastgpt/web/components/common/Icon'; import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants'; import DatasetSelectModal, { useDatasetSelect } from '@/components/core/dataset/SelectModal'; import dynamic from 'next/dynamic'; -import { type AdminFbkType } from '@fastgpt/global/core/chat/type.d'; +import { type AdminFbkType } from '@fastgpt/global/core/chat/type'; import SelectCollections from '@/web/core/dataset/components/SelectCollections'; import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx index 989e87ec8..70b02a3b9 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx @@ -11,7 +11,7 @@ import type { AIChatItemValueItemType, ChatSiteItemType, UserChatItemValueItemType -} from '@fastgpt/global/core/chat/type.d'; +} from '@fastgpt/global/core/chat/type'; import { useToast } from '@fastgpt/web/hooks/useToast'; import { getErrText } from '@fastgpt/global/common/error/utils'; import { Box, Button, Checkbox, Flex } from '@chakra-ui/react'; diff --git a/projects/app/src/components/core/chat/HelperBot/Chatinput.tsx b/projects/app/src/components/core/chat/HelperBot/Chatinput.tsx new file mode 100644 index 000000000..0c5c54cab --- /dev/null +++ b/projects/app/src/components/core/chat/HelperBot/Chatinput.tsx @@ -0,0 +1,400 @@ +import type { FlexProps } from '@chakra-ui/react'; +import { Box, Flex, Textarea, useBoolean } from '@chakra-ui/react'; +import React, { useRef, useCallback, useMemo } from 'react'; +import { useTranslation } from 'next-i18next'; +import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import { + type ChatBoxInputFormType, + type ChatBoxInputType, + type SendPromptFnType +} from '../ChatContainer/ChatBox/type'; +import { textareaMinH } from '../ChatContainer/ChatBox/constants'; +import { useFieldArray, type UseFormReturn } from 'react-hook-form'; +import { ChatBoxContext } from '../ChatContainer/ChatBox/Provider'; +import dynamic from 'next/dynamic'; +import { useContextSelector } from 'use-context-selector'; +import { WorkflowRuntimeContext } from '../ChatContainer/context/workflowRuntimeContext'; +import { useSystem } from '@fastgpt/web/hooks/useSystem'; +import { documentFileType } from '@fastgpt/global/common/file/constants'; +import FilePreview from '../ChatContainer/components/FilePreview'; +import { useFileUpload } from './hooks/useFileUpload'; +import ComplianceTip from '@/components/common/ComplianceTip/index'; +import { useToast } from '@fastgpt/web/hooks/useToast'; +import { HelperBotContext } from './context'; +import type { onSendMessageFnType } from './type'; + +const fileTypeFilter = (file: File) => { + return ( + file.type.includes('image') || + documentFileType.split(',').some((type) => file.name.endsWith(type.trim())) + ); +}; + +const ChatInput = ({ + chatId, + onSendMessage, + onStop, + TextareaDom, + chatForm, + isChatting +}: { + chatId: string; + onSendMessage: onSendMessageFnType; + onStop: () => void; + TextareaDom: React.MutableRefObject; + chatForm: UseFormReturn; + isChatting: boolean; +}) => { + const { t } = useTranslation(); + const { toast } = useToast(); + const { isPc } = useSystem(); + + const { setValue, watch, control } = chatForm; + const inputValue = watch('input'); + + const type = useContextSelector(HelperBotContext, (v) => v.type); + const fileSelectConfig = useContextSelector(HelperBotContext, (v) => v.fileSelectConfig); + + const [focusing, { on: onFocus, off: offFocus }] = useBoolean(); + + const fileCtrl = useFieldArray({ + control, + name: 'files' + }); + const { + File, + onOpenSelectFile, + fileList, + onSelectFile, + selectFileIcon, + selectFileLabel, + showSelectFile, + showSelectImg, + showSelectVideo, + showSelectAudio, + showSelectCustomFileExtension, + removeFiles, + replaceFiles, + hasFileUploading + } = useFileUpload({ + fileSelectConfig, + fileCtrl, + type, + chatId + }); + const havInput = !!inputValue || fileList.length > 0; + const canSendMessage = havInput && !hasFileUploading; + const canUploadFile = + showSelectFile || + showSelectImg || + showSelectVideo || + showSelectAudio || + showSelectCustomFileExtension; + + /* on send */ + const handleSend = useCallback( + async (val?: string) => { + if (!canSendMessage) return; + const textareaValue = val || TextareaDom.current?.value || ''; + + onSendMessage({ + query: textareaValue.trim(), + files: fileList + }); + replaceFiles([]); + }, + [TextareaDom, canSendMessage, fileList, onSendMessage, replaceFiles] + ); + + const RenderTextarea = useMemo( + () => ( + 0 ? 1 : 0}> + {/* Textarea */} + + {/* Prompt Container */} +