diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index b96e128f1..f0c577c75 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -241,49 +241,16 @@ export class ChatGPTApi implements LLMApi { ); } if (shouldStream) { - const [tools1, funcs2] = usePluginStore + const [tools, funcs] = usePluginStore .getState() .getAsTools(useChatStore.getState().currentSession().mask?.plugin); - console.log("getAsTools", tools1, funcs2); - // return - // TODO mock tools and funcs - const tools = [ - { - type: "function", - function: { - name: "get_current_weather", - description: "Get the current weather", - parameters: { - type: "object", - properties: { - location: { - type: "string", - description: "The city and country, eg. San Francisco, USA", - }, - format: { - type: "string", - enum: ["celsius", "fahrenheit"], - }, - }, - required: ["location", "format"], - }, - }, - }, - ]; - const funcs = { - get_current_weather: (args: any) => { - console.log("call get_current_weather", args); - return new Promise((resolve) => { - setTimeout(() => resolve("30"), 3000); - }); - }, - }; + console.log("getAsTools", tools, funcs); stream( chatPath, requestPayload, getHeaders(), - tools1, - funcs2, + tools, + funcs, controller, // parseSSE (text: string, runTools: ChatMessageTool[]) => { diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 9d3b86f4e..0e2aa3e2f 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -442,70 +442,6 @@ export function ChatActions(props: { const navigate = useNavigate(); const chatStore = useChatStore(); const pluginStore = usePluginStore(); - console.log("pluginStore", pluginStore.getAll()); - // test - if (pluginStore.getAll().length == 0) { - pluginStore.create({ - title: "Pet API", - version: "1.0.0", - content: `{ - "openapi": "3.0.2", - "info": { - "title": "Pet API", - "version": "1.0.0" - }, - "paths": { - "/api/pets": { - "get": { - "operationId": "getPets", - "description": "Returns all pets from the system that the user has access to", - "responses": { - "200": { - "description": "List of Pets", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Pet" - } - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "Pet": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "cat", - "dog" - ] - }, - "name": { - "type": "string" - } - }, - "required": [ - "id", - "type" - ] - } - } - } -}`, - }); - } // switch themes const theme = config.theme; @@ -805,12 +741,10 @@ export function ChatActions(props: { value: Plugin.Artifacts, }, ].concat( - pluginStore - .getAll() - .map((item) => ({ - title: `${item.title}@${item.version}`, - value: item.id, - })), + pluginStore.getAll().map((item) => ({ + title: `${item.title}@${item.version}`, + value: item.id, + })), )} onClose={() => setShowPluginSelector(false)} onSelection={(s) => { diff --git a/app/components/plugin.module.scss b/app/components/plugin.module.scss new file mode 100644 index 000000000..53c632468 --- /dev/null +++ b/app/components/plugin.module.scss @@ -0,0 +1,15 @@ +.plugin-title { + font-weight: bolder; + font-size: 16px; + margin: 10px 0; +} +.plugin-content { + font-size: 14px; + font-family: inherit; + pre code { + max-height: 240px; + overflow-y: auto; + white-space: pre-wrap; + } +} + diff --git a/app/components/plugin.tsx b/app/components/plugin.tsx index 769e02f2a..247cba257 100644 --- a/app/components/plugin.tsx +++ b/app/components/plugin.tsx @@ -1,7 +1,11 @@ +import { useDebouncedCallback } from "use-debounce"; +import OpenAPIClientAxios from "openapi-client-axios"; +import yaml from "js-yaml"; import { IconButton } from "./button"; import { ErrorBoundary } from "./error"; import styles from "./mask.module.scss"; +import pluginStyles from "./plugin.module.scss"; import DownloadIcon from "../icons/download.svg"; import EditIcon from "../icons/edit.svg"; @@ -11,7 +15,7 @@ import DeleteIcon from "../icons/delete.svg"; import EyeIcon from "../icons/eye.svg"; import CopyIcon from "../icons/copy.svg"; -import { Plugin, usePluginStore } from "../store/plugin"; +import { Plugin, usePluginStore, FunctionToolService } from "../store/plugin"; import { Input, List, @@ -20,7 +24,9 @@ import { Popover, Select, showConfirm, + showToast, } from "./ui-lib"; +import { downloadAs } from "../utils"; import Locale from "../locales"; import { useNavigate } from "react-router-dom"; import { useEffect, useState } from "react"; @@ -30,12 +36,56 @@ import { nanoid } from "nanoid"; export function PluginPage() { const navigate = useNavigate(); const pluginStore = usePluginStore(); - const plugins = pluginStore.getAll(); + + const allPlugins = pluginStore.getAll(); + const [searchPlugins, setSearchPlugins] = useState([]); + const [searchText, setSearchText] = useState(""); + const plugins = searchText.length > 0 ? searchPlugins : allPlugins; + + // refactored already, now it accurate + const onSearch = (text: string) => { + setSearchText(text); + if (text.length > 0) { + const result = allPlugins.filter((m) => + m.title.toLowerCase().includes(text.toLowerCase()), + ); + setSearchPlugins(result); + } else { + setSearchPlugins(allPlugins); + } + }; const [editingPluginId, setEditingPluginId] = useState(); const editingPlugin = pluginStore.get(editingPluginId); + const editingPluginTool = FunctionToolService.get(editingPlugin?.id); const closePluginModal = () => setEditingPluginId(undefined); + const onChangePlugin = useDebouncedCallback((editingPlugin, e) => { + const content = e.target.innerText; + try { + const api = new OpenAPIClientAxios({ definition: yaml.load(content) }); + api + .init() + .then(() => { + if (content != editingPlugin.content) { + pluginStore.updatePlugin(editingPlugin.id, (plugin) => { + plugin.content = content; + const tool = FunctionToolService.add(plugin, true); + plugin.title = tool.api.definition.info.title; + plugin.version = tool.api.definition.info.version; + }); + } + }) + .catch((e) => { + console.error(e); + showToast(Locale.Plugin.EditModal.Error); + }); + } catch (e) { + console.error(e); + showToast(Locale.Plugin.EditModal.Error); + } + }, 100).bind(null, editingPlugin); + return (
@@ -61,6 +111,27 @@ export function PluginPage() {
+
+ onSearch(e.currentTarget.value)} + /> + + } + text={Locale.Plugin.Page.Create} + bordered + onClick={() => { + const createdPlugin = pluginStore.create(); + setEditingPluginId(createdPlugin.id); + }} + /> +
+
{plugins.map((m) => (
@@ -71,7 +142,9 @@ export function PluginPage() { {m.title}@{m.version}
- {`${Locale.Plugin.Item.Info(m.content.length)} / / `} + {Locale.Plugin.Item.Info( + FunctionToolService.add(m).length, + )}
@@ -123,24 +196,48 @@ export function PluginPage() { onClick={() => downloadAs( JSON.stringify(editingPlugin), - `${editingPlugin.name}.json`, + `${editingPlugin.title}@${editingPlugin.version}.json`, ) } />, - } - bordered - text={Locale.Plugin.EditModal.Clone} - onClick={() => { - navigate(Path.Plugins); - pluginStore.create(editingPlugin); - setEditingPluginId(undefined); - }} - />, ]} > - PluginConfig +
+
+ {Locale.Plugin.EditModal.Content} +
+
+
+                  
+                
+
+
+ {Locale.Plugin.EditModal.Method} +
+
+ {editingPluginTool?.tools.map((tool, index) => ( +
+
+
+
+ {tool?.function?.name} +
+
+ {tool?.function?.description} +
+
+
+
+ ))} +
+
)} diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 9a3227d68..f0ff705c1 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -509,10 +509,6 @@ const cn = { Clear: "上下文已清除", Revert: "恢复上下文", }, - Plugin: { - Name: "插件", - Artifacts: "Artifacts", - }, Discovery: { Name: "发现", }, @@ -534,6 +530,30 @@ const cn = { View: "查看", }, }, + Plugin: { + Name: "插件", + Artifacts: "Artifacts", + Page: { + Title: "插件", + SubTitle: (count: number) => `${count} 个插件`, + Search: "搜索插件", + Create: "新建", + }, + Item: { + Info: (count: number) => `${count} 方法`, + View: "查看", + Edit: "编辑", + Delete: "删除", + DeleteConfirm: "确认删除?", + }, + EditModal: { + Title: (readonly: boolean) => `编辑插件 ${readonly ? "(只读)" : ""}`, + Download: "下载", + Content: "OpenAPI Schema", + Method: "方法", + Error: "格式错误", + }, + }, Mask: { Name: "面具", Page: { diff --git a/app/locales/en.ts b/app/locales/en.ts index ea098c0f3..15db8190a 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -517,10 +517,6 @@ const en: LocaleType = { Clear: "Context Cleared", Revert: "Revert", }, - Plugin: { - Name: "Plugin", - Artifacts: "Artifacts", - }, Discovery: { Name: "Discovery", }, @@ -544,6 +540,7 @@ const en: LocaleType = { }, Plugin: { Name: "Plugin", + Artifacts: "Artifacts", Page: { Title: "Plugins", SubTitle: (count: number) => `${count} plugins`, @@ -551,8 +548,7 @@ const en: LocaleType = { Create: "Create", }, Item: { - Info: (count: number) => `${count} plugins`, - Chat: "Chat", + Info: (count: number) => `${count} method`, View: "View", Edit: "Edit", Delete: "Delete", @@ -562,25 +558,9 @@ const en: LocaleType = { Title: (readonly: boolean) => `Edit Plugin ${readonly ? "(readonly)" : ""}`, Download: "Download", - Clone: "Clone", - }, - Config: { - Avatar: "Bot Avatar", - Name: "Bot Name", - Sync: { - Title: "Use Global Config", - SubTitle: "Use global config in this chat", - Confirm: "Confirm to override custom config with global config?", - }, - HideContext: { - Title: "Hide Context Prompts", - SubTitle: "Do not show in-context prompts in chat", - }, - Share: { - Title: "Share This Plugin", - SubTitle: "Generate a link to this mask", - Action: "Copy Link", - }, + Content: "OpenAPI Schema", + Method: "Method", + Error: "OpenAPI Schema Error", }, }, Mask: { diff --git a/app/store/plugin.ts b/app/store/plugin.ts index d93044c4d..b25b162a3 100644 --- a/app/store/plugin.ts +++ b/app/store/plugin.ts @@ -10,16 +10,91 @@ export type Plugin = { createdAt: number; title: string; version: string; - context: string; + content: string; builtin: boolean; }; +export type FunctionToolItem = { + type: string; + function: { + name: string; + description?: string; + parameters: Object; + }; +}; + +type FunctionToolServiceItem = { + api: OpenAPIClientAxios; + tools: FunctionToolItem[]; + funcs: Function[]; +}; + +export const FunctionToolService = { + tools: {} as Record, + add(plugin: Plugin, replace = false) { + if (!replace && this.tools[plugin.id]) return this.tools[plugin.id]; + const api = new OpenAPIClientAxios({ + definition: yaml.load(plugin.content), + }); + console.log("add", plugin, api); + try { + api.initSync(); + } catch (e) {} + const operations = api.getOperations(); + return (this.tools[plugin.id] = { + api, + length: operations.length, + tools: operations.map((o) => { + const parameters = o?.requestBody?.content["application/json"] + ?.schema || { + type: "object", + properties: {}, + }; + if (!parameters["required"]) { + parameters["required"] = []; + } + if (o.parameters instanceof Array) { + o.parameters.forEach((p) => { + if (p.in == "query" || p.in == "path") { + // const name = `${p.in}__${p.name}` + const name = p.name; + console.log("p", p, p.schema); + parameters["properties"][name] = { + type: p.schema.type, + description: p.description, + }; + if (p.required) { + parameters["required"].push(name); + } + } + }); + } + return { + type: "function", + function: { + name: o.operationId, + description: o.description, + parameters: parameters, + }, + }; + }), + funcs: operations.reduce((s, o) => { + s[o.operationId] = api.client[o.operationId]; + return s; + }, {}), + }); + }, + get(id) { + return this.tools[id]; + }, +}; + export const createEmptyPlugin = () => ({ id: nanoid(), title: "", - version: "", - context: "", + version: "1.0.0", + content: "", builtin: false, createdAt: Date.now(), }) as Plugin; @@ -69,46 +144,10 @@ export const usePluginStore = createPersistStore( const selected = ids .map((id) => plugins[id]) .filter((i) => i) - .map((i) => [ - i, - new OpenAPIClientAxios({ definition: yaml.load(i.content) }), - ]) - .map(([item, api]) => { - api.initSync(); - const operations = api.getOperations().map((o) => { - const parameters = o.parameters; - return [ - { - type: "function", - function: { - name: o.operationId, - description: o.description, - parameters: o.parameters, - }, - }, - api.client[o.operationId], - ]; - // return [{ - // }, function(arg) { - // const args = [] - // for (const p in parameters) { - // if (p.type === "object") { - // const a = {} - // for (const n of p.) - // } - // } - // }] - }); - return [item, api, operations]; - }); - console.log("selected", selected); - const result = selected.reduce((s, i) => s.concat(i[2]), []); + .map((p) => FunctionToolService.add(p)); return [ - result.map(([t, _]) => t), - result.reduce((s, i) => { - s[i[0].function.name] = i[1]; - return s; - }, {}), + selected.reduce((s, i) => s.concat(i.tools), []), + selected.reduce((s, i) => Object.assign(s, i.funcs), {}), ]; }, get(id?: string) {