diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index fb9f65c956e..b72d7521e4f 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -104,6 +104,51 @@ export class ChatGPTApi implements LLMApi { } } + /** Function Calling + * Author : @H0llyW00dzZ + * Todo : Function Calling which user can customize it easily + * This method should be a member of the ChatGPTApi class, not nested inside another method + **/ + private getFunctionCalling( + model: string, + istools: boolean, + tools?: any + ): { isGPTModel: boolean; tools?: any } { + const isGPTModel = model.includes("-1106") && istools === true; + if (isGPTModel) { + return { + /** + * Note : This just Sample Payload + **/ + tools: tools || [ + { + type: "function", + function: { + name: "get_current_weather", + description: "Get the current weather in a given location", + parameters: { + type: "object", + properties: { + location: { + type: "string", + description: "The state and country, e.g. Bali, Indonesia", + }, + unit: { type: "string", enum: ["celsius", "fahrenheit"] }, + }, + required: ["location"], + }, + }, + }, + ], + isGPTModel: true, + }; + } else { + return { + isGPTModel: false, + }; + } + } + async chat(options: ChatOptions) { const textmoderation = useAppConfig.getState().textmoderation; const latest = OpenaiPath.TextModerationModels.latest; @@ -193,6 +238,11 @@ export class ChatGPTApi implements LLMApi { modelConfig.system_fingerprint ); + const { isGPTModel, tools } = this.getFunctionCalling( + defaultModel, + modelConfig.istools ?? false, + ); + const requestPayloads = { chat: { messages, @@ -207,6 +257,10 @@ export class ChatGPTApi implements LLMApi { ...{ max_tokens }, // Spread the max_tokens value // not yet ready //...{ system_fingerprint }, // Spread the system_fingerprint value + // bug there is no response in text/event-stream from assistant after answer + // "Could you please provide me with the specific state and country for which you would like to get the current weather information?" + // might cause still beta + //...{ tools }, // Spread the tools payload }, image: { model: actualModel, @@ -222,6 +276,7 @@ export class ChatGPTApi implements LLMApi { * Author : @H0llyW00dzZ **/ const magicPayload = this.getNewStuff(defaultModel); + const toolsPayload = this.getFunctionCalling(defaultModel, tools); if (defaultModel.startsWith("dall-e")) { console.log("[Request] openai payload: ", { @@ -420,7 +475,7 @@ export class ChatGPTApi implements LLMApi { try { const resJson = await res.clone().json(); extraInfo = prettyObject(resJson); - } catch {} + } catch { } if (res.status === 401) { responseTexts.push(Locale.Error.Unauthorized); diff --git a/app/components/settings.tsx b/app/components/settings.tsx index 7f875c6824a..c326f42ddcc 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -66,6 +66,7 @@ import { UPDATE_URL, } from "../constant"; import { Prompt, SearchService, usePromptStore } from "../store/prompt"; +import { Tool, useToolStore, ToolSearchService } from "../store/tools"; import { ErrorBoundary } from "./error"; import { InputRange } from "./input-range"; import { useNavigate } from "react-router-dom"; diff --git a/app/constant.ts b/app/constant.ts index 06f12e91209..0ed40078210 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -32,20 +32,22 @@ export enum SlotID { AppBody = "app-body", CustomModel = "custom-model", } -// This will automatically generate JSON files without the need to include the ".json" extension. + export enum FileName { Masks = "masks.json", Prompts = "prompts.json", + Tool = "tools.json", } export enum StoreKey { - Chat = "chat-next-web-store", - Access = "access-control", - Config = "app-config", - Mask = "mask-store", - Prompt = "prompt-store", - Update = "chat-update", - Sync = "sync", + Chat = "chat-next-web-store", + Access = "access-control", + Config = "app-config", + Mask = "mask-store", + Prompt = "prompt-store", + Update = "chat-update", + Sync = "sync", + Tool = "tool-store", } export const DEFAULT_SIDEBAR_WIDTH = 300; diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 4f21ca594f1..29a0a76c02c 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -479,6 +479,24 @@ const cn = { SubTitle: "生成图像的风格\n必须是生动或自然之一\n此配置仅适用于dall-e-3", }, + FunctionCall: { + Title: "工具,又称函数调用", + Subtitle: "一种将大型语言模型与外部工具连接的工具,也称为函数调用。", + PropsContent: { + List: "工具列表", + ListCount: (builtin: number, custom: number) => + `内置${builtin}个,自定义${custom}个`, + Edit: "编辑", + Modal: { + Title: "工具列表", + Add: "添加一个", + Search: "搜索工具", + }, + EditModal: { + Title: "编辑工具", + }, + }, + }, }, Store: { DefaultTopic: "新的聊天", diff --git a/app/locales/en.ts b/app/locales/en.ts index f7a102aae7b..35397aa8fc6 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -487,6 +487,24 @@ const en: LocaleType = { SubTitle: "A style of the generated images\nMust be one of vivid or natural\nThis Configuration is only supported for dall-e-3", }, + FunctionCall: { + Title: "Tools aka Function Calling", + Subtitle: "A tools aka function calling that connect large language models to external tools.", + PropsContent: { + List: "Tools List", + ListCount: (builtin: number, custom: number) => + `${builtin} built-in, ${custom} user-defined`, + Edit: "Edit", + Modal: { + Title: "Tools List", + Add: "Add One", + Search: "Search Tools", + }, + EditModal: { + Title: "Edit Tools", + }, + }, + }, }, Store: { DefaultTopic: "New Conversation", diff --git a/app/locales/id.ts b/app/locales/id.ts index 62310ed85de..e752696576a 100644 --- a/app/locales/id.ts +++ b/app/locales/id.ts @@ -418,6 +418,24 @@ const id: PartialLocaleType = { SubTitle: "Gaya gambar yang dihasilkan\nHarus menjadi salah satu dari cerah atau alami\nKonfigurasi ini hanya didukung untuk dall-e-3", }, + FunctionCall: { + Title: "Pemanggilan Fungsi Alat", + Subtitle: "Sebuah alat pemanggilan fungsi yang menghubungkan model bahasa besar ke alat eksternal.", + PropsContent: { + List: "Daftar Alat", + ListCount: (builtin: number, custom: number) => + `${builtin} bawaan, ${custom} buatan pengguna`, + Edit: "Edit", + Modal: { + Title: "Daftar Alat", + Add: "Tambah Satu", + Search: "Cari Alat", + }, + EditModal: { + Title: "Edit Alat", + }, + }, + }, }, Store: { DefaultTopic: "Percakapan Baru", diff --git a/app/store/config.ts b/app/store/config.ts index 44199a68f16..9c72ab0ae45 100644 --- a/app/store/config.ts +++ b/app/store/config.ts @@ -78,6 +78,10 @@ export const DEFAULT_CONFIG = { */ style: "vivid", // Only DALL·E-3 for DALL·E-2 not not really needed system_fingerprint: "", + /** Tools Aka Function Call OpenAI + * Author: @H0llyW00dzZ + **/ + istools: false, sendMemory: true, historyMessageCount: 4, compressMessageLengthThreshold: 1000, @@ -182,7 +186,7 @@ export const useAppConfig = createPersistStore( }), { name: StoreKey.Config, - version: 4.2, // DALL·E Models switching version to 4.1 because in 4.0 @Yidadaa using it. + version: 4.3, // DALL·E Models switching version to 4.1 because in 4.0 @Yidadaa using it. migrate(persistedState, version) { const state = persistedState as ChatConfig; @@ -236,6 +240,13 @@ export const useAppConfig = createPersistStore( }; } + if (version < 4.3) { + state.modelConfig = { + ...state.modelConfig, + istools: false, + }; + } + return state as any; }, }, diff --git a/app/store/tools.ts b/app/store/tools.ts new file mode 100644 index 00000000000..39a6b293212 --- /dev/null +++ b/app/store/tools.ts @@ -0,0 +1,186 @@ +import Fuse from "fuse.js"; +import { getLang } from "../locales"; +import { StoreKey } from "../constant"; +import { nanoid } from "nanoid"; +import { createPersistStore } from "../utils/store"; + +export interface ToolParameter { + type: string; + description: string; +} + +export interface ToolFunction { + name: string; + description: string; + parameters: Record; +} + +export interface Tool { + id: string; + isUser?: boolean; + createdAt: number; + type: string; + function: ToolFunction; +} + +export const ToolSearchService = { + ready: false, + builtinEngine: new Fuse([], { keys: ["function.name"] }), + userEngine: new Fuse([], { keys: ["function.name"] }), + count: { + builtin: 0, + }, + allTools: [] as Tool[], + builtinTools: [] as Tool[], + + init(builtinTools: Tool[], userTools: Tool[]) { + if (this.ready) { + return; + } + this.allTools = userTools.concat(builtinTools); + this.builtinTools = builtinTools.slice(); + this.builtinEngine.setCollection(builtinTools); + this.userEngine.setCollection(userTools); + this.ready = true; + }, + + remove(name: string) { + this.userEngine.remove((doc) => doc.function.name === name); + }, + + add(tool: Tool) { + this.userEngine.add(tool); + }, + + search(text: string) { + const userResults = this.userEngine.search(text); + const builtinResults = this.builtinEngine.search(text); + return userResults.concat(builtinResults).map((v) => v.item); + }, +}; + +export const useToolStore = createPersistStore( + { + counter: 0, + tools: {} as Record, + }, + + (set, get) => ({ + add(tool: Tool) { + const tools = get().tools; + tool.id = nanoid(); + tools[tool.id] = tool; + + set(() => ({ + tools: tools, + })); + + return tool.id!; + }, + + get(id: string) { + return get().tools[id]; + }, + + remove(id: string) { + const tools = get().tools; + delete tools[id]; + + set(() => ({ + tools, + counter: get().counter + 1, + })); + }, + + getUserTools() { + return Object.values(get().tools ?? {}); + }, + + updateTool(id: string, updater: (tool: Tool) => void) { + const tool = get().tools[id] ?? { + id: "", + isUser: false, + createdAt: Date.now(), + type: "", + function: { + name: "", + description: "", + parameters: {}, + }, + }; + + ToolSearchService.remove(id); + updater(tool); + const tools = get().tools; + tools[id] = tool; + set(() => ({ tools })); + ToolSearchService.add(tool); + }, + + search(text: string) { + if (text.length === 0) { + // Return all tools + return this.getUserTools().concat(ToolSearchService.builtinTools); + } + return ToolSearchService.search(text) as Tool[]; + }, + }), + + { + name: StoreKey.Tool, + version: 1.1, + + migrate(state, version) { + const newState = JSON.parse(JSON.stringify(state)) as { + tools: Record; + }; + + if (version < 1.1) { + Object.values(newState.tools).forEach((tool) => { + if (!tool.id) { + tool.id = nanoid(); + } + }); + } + + return newState as any; + }, + + onRehydrateStorage(state) { + const TOOL_URL = "./tools.json"; + + fetch(TOOL_URL) + .then((res) => res.json()) + .then((res) => { + const lang = getLang(); + let fetchTools: Tool[]; + + switch (lang) { + case "cn": + fetchTools = res.cn; + break; + case "id": + fetchTools = res.id; + break; + // Add cases for other languages here + default: + fetchTools = res.en; + break; + } + + const builtinTools = fetchTools.map((tool) => ({ + ...tool, + id: nanoid(), + isUser: false, + }) as Tool, + ); + + const userTools = useToolStore.getState().getUserTools() ?? []; + + const allToolsForSearch = [...builtinTools, ...userTools].filter((v) => !!v.function.name && !!v.function.description); + ToolSearchService.count.builtin = fetchTools.length; + ToolSearchService.init(allToolsForSearch, userTools); + }); + }, + }, +); diff --git a/public/tools.json b/public/tools.json new file mode 100644 index 00000000000..7ecd96fd321 --- /dev/null +++ b/public/tools.json @@ -0,0 +1,40 @@ +{ + "en": [ + { + "type": "function", + "function": { + "name": "get_current_weather", + "description": "Get the current weather in a given location", + "parameters": { + "location": { + "type": "string", + "description": "The state and country, e.g. Bali, Indonesia" + }, + "unit": { + "type": "string", + "enum": ["celsius", "fahrenheit"] + } + } + } + } + ], + "cn": [ + { + "type": "function", + "function": { + "name": "get_current_weather", + "description": "Get the current weather in a given location", + "parameters": { + "location": { + "type": "string", + "description": "The state and country, e.g. Bali, Indonesia" + }, + "unit": { + "type": "string", + "enum": ["celsius", "fahrenheit"] + } + } + } + } + ] + }