From 81660d563f55cb7346d0e580c0dd5cead109f718 Mon Sep 17 00:00:00 2001 From: heimoshuiyu Date: Thu, 9 Nov 2023 17:04:27 +0800 Subject: [PATCH 01/11] add support for tool call (function call) --- src/app.tsx | 7 ++++++- src/chatbox.tsx | 9 ++++++--- src/chatgpt.ts | 49 +++++++++++++++++++++++++++++++++++++++--------- src/message.tsx | 15 +++++++++++++++ src/settings.tsx | 7 ++++++- 5 files changed, 73 insertions(+), 14 deletions(-) diff --git a/src/app.tsx b/src/app.tsx index d12c633..f850864 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -21,9 +21,11 @@ export interface TemplateAPI { key: string; endpoint: string; } + export interface ChatStore { chatgpt_api_web_version: string; systemMessageContent: string; + toolsString: string; history: ChatStoreMessage[]; postBeginIndex: number; tokenMargin: number; @@ -67,11 +69,13 @@ export const newChatStore = ( tts_api = "", tts_key = "", tts_speed = 1.0, - tts_speed_enabled = false + tts_speed_enabled = false, + toolsString = "" ): ChatStore => { return { chatgpt_api_web_version: CHATGPT_API_WEB_VERSION, systemMessageContent: getDefaultParams("sys", systemMessageContent), + toolsString, history: [], postBeginIndex: 0, tokenMargin: 1024, @@ -173,6 +177,7 @@ export function App() { if (ret.maxGenTokens_enabled === undefined) ret.maxGenTokens_enabled = true; if (ret.model === undefined) ret.model = "gpt-3.5-turbo"; if (ret.responseModelName === undefined) ret.responseModelName = ""; + if (ret.toolsString === undefined) ret.toolsString = ""; if (ret.chatgpt_api_web_version === undefined) // this is from old version becasue it is undefined, // so no higher than v1.3.0 diff --git a/src/chatbox.tsx b/src/chatbox.tsx index 057f207..61524c7 100644 --- a/src/chatbox.tsx +++ b/src/chatbox.tsx @@ -127,7 +127,7 @@ export default function ChatBOX(props: { chatStore.cost += cost; addTotalCost(cost); } - const content = client.processFetchResponse(data); + const msg = client.processFetchResponse(data); // estimate user's input message token let aboveToken = 0; @@ -147,9 +147,11 @@ export default function ChatBOX(props: { chatStore.history.push({ role: "assistant", - content, + content: msg.content, + tool_calls: msg.tool_calls, hide: false, - token: data.usage.completion_tokens ?? calculate_token_length(content), + token: + data.usage.completion_tokens ?? calculate_token_length(msg.content), example: false, }); setShowGenerating(false); @@ -160,6 +162,7 @@ export default function ChatBOX(props: { // manually copy status from chatStore to client client.apiEndpoint = chatStore.apiEndpoint; client.sysMessageContent = chatStore.systemMessageContent; + client.toolsString = chatStore.toolsString; client.tokens_margin = chatStore.tokenMargin; client.temperature = chatStore.temperature; client.enable_temperature = chatStore.temperature_enabled; diff --git a/src/chatgpt.ts b/src/chatgpt.ts index 1f604d7..d89b0fe 100644 --- a/src/chatgpt.ts +++ b/src/chatgpt.ts @@ -12,6 +12,11 @@ export interface Message { role: "system" | "user" | "assistant" | "function"; content: string | MessageDetail[]; name?: "example_user" | "example_assistant"; + tool_calls?: { + id: string; + type: string; + function: any; + }[]; } export const getMessageText = (message: Message): string => { if (typeof message.content === "string") { @@ -78,6 +83,7 @@ class Chat { OPENAI_API_KEY: string; messages: Message[]; sysMessageContent: string; + toolsString: string; total_tokens: number; max_tokens: number; max_gen_tokens: number; @@ -96,6 +102,7 @@ class Chat { OPENAI_API_KEY: string | undefined, { systemMessage = "", + toolsString = "", max_tokens = 4096, max_gen_tokens = 2048, enable_max_gen_tokens = true, @@ -121,6 +128,7 @@ class Chat { this.enable_max_gen_tokens = enable_max_gen_tokens; this.tokens_margin = tokens_margin; this.sysMessageContent = systemMessage; + this.toolsString = toolsString; this.apiEndpoint = apiEndPoint; this.model = model; this.temperature = temperature; @@ -178,6 +186,25 @@ class Chat { body["max_tokens"] = this.max_gen_tokens; } + // parse toolsString to function call format + const ts = this.toolsString.trim(); + if (ts) { + try { + const fcList: any[] = JSON.parse(ts); + body["tools"] = fcList.map((fc) => { + return { + type: "function", + function: fc, + }; + }); + } catch (e) { + console.log("toolsString parse error"); + throw ( + "Function call toolsString parse error, not a valied json list: " + e + ); + } + } + return fetch(this.apiEndpoint, { method: "POST", headers: { @@ -234,7 +261,7 @@ class Chat { } } - processFetchResponse(resp: FetchResponse): string { + processFetchResponse(resp: FetchResponse): Message { if (resp.error !== undefined) { throw JSON.stringify(resp.error); } @@ -249,15 +276,19 @@ class Chat { this.forgetSomeMessages(); } - return ( - (resp?.choices[0]?.message?.content as string) ?? - `Error: ${JSON.stringify(resp)}` - ); - } + let content = ""; + if ( + !resp.choices[0]?.message?.content && + !resp.choices[0]?.message?.tool_calls + ) { + content = `Unparsed response: ${JSON.stringify(resp)}`; + } - async complete(): Promise { - const resp = await this.fetch(); - return this.processFetchResponse(resp); + return { + role: "assistant", + content, + tool_calls: resp?.choices[0]?.message?.tool_calls, + }; } completeWithSteam() { diff --git a/src/message.tsx b/src/message.tsx index 3fdb679..b4ff996 100644 --- a/src/message.tsx +++ b/src/message.tsx @@ -288,6 +288,21 @@ export default function Message(props: Props) { : "bg-green-400" } ${chat.hide ? "opacity-50" : ""}`} > + {chat.tool_calls && chat.hide ? ( +
Tool Call
+ ) : ( +
+
+ {chat.tool_calls?.map((tool_call) => ( +
+ Tool Call ID: {tool_call?.id} +

Type: {tool_call?.type}

+

Function: {JSON.stringify(tool_call?.function)}

+
+ ))} +
+
+ )}

{typeof chat.content !== "string" ? ( // render for multiple messages diff --git a/src/settings.tsx b/src/settings.tsx index 1aa901a..e8acda3 100644 --- a/src/settings.tsx +++ b/src/settings.tsx @@ -60,7 +60,7 @@ const SelectModel = (props: { const LongInput = (props: { chatStore: ChatStore; setChatStore: (cs: ChatStore) => void; - field: "systemMessageContent"; + field: "systemMessageContent" | "toolsString"; help: string; }) => { return ( @@ -373,6 +373,11 @@ export default (props: { help="系统消息,用于指示ChatGPT的角色和一些前置条件,例如“你是一个有帮助的人工智能助理”,或者“你是一个专业英语翻译,把我的话全部翻译成英语”,详情参考 OPEAN AI API 文档" {...props} /> + Date: Thu, 9 Nov 2023 17:48:04 +0800 Subject: [PATCH 02/11] tool call WIP --- src/chatbox.tsx | 112 ++++++++++++++++++++++++++++++++++++++++++++---- src/chatgpt.ts | 3 +- 2 files changed, 105 insertions(+), 10 deletions(-) diff --git a/src/chatbox.tsx b/src/chatbox.tsx index 61524c7..d57279c 100644 --- a/src/chatbox.tsx +++ b/src/chatbox.tsx @@ -13,6 +13,7 @@ import ChatGPT, { calculate_token_length, ChunkMessage, FetchResponse, + Message as MessageType, MessageDetail, } from "./chatgpt"; import Message from "./message"; @@ -41,6 +42,9 @@ export default function ChatBOX(props: { const [generatingMessage, setGeneratingMessage] = useState(""); const [showRetry, setShowRetry] = useState(false); const [isRecording, setIsRecording] = useState("Mic"); + const [showAddToolMsg, setShowAddToolMsg] = useState(false); + const [newToolCallID, setNewToolCallID] = useState(""); + const [newToolContent, setNewToolContent] = useState(""); const mediaRef = createRef(); const messagesEndRef = createRef(); @@ -175,18 +179,21 @@ export default function ChatBOX(props: { .filter(({ hide }) => !hide) .slice(chatStore.postBeginIndex) // only copy content and role attribute to client for posting - .map(({ content, role, example }) => { - if (example) { - return { - content, - role: "system", - name: role === "assistant" ? "example_assistant" : "example_user", - }; - } - return { + .map(({ content, role, example, tool_call_id }) => { + const ret: MessageType = { content, role, }; + + if (example) { + ret.name = + ret.role === "assistant" ? "example_assistant" : "example_user"; + ret.role = "system"; + } + + if (tool_call_id) ret.tool_call_id = tool_call_id; + + return ret; }); client.model = chatStore.model; client.max_tokens = chatStore.maxTokens; @@ -946,6 +953,93 @@ export default function ChatBOX(props: { {Tr("User")} )} + {chatStore.develop_mode && ( + + )} + {showAddToolMsg && ( +

{ + setShowAddToolMsg(false); + }} + > +
{ + event.stopPropagation(); + }} + > +

Add Tool Message

+
+ + + + setNewToolCallID(event.target.value) + } + /> + + + + + + + + + +
+
+ )} ); diff --git a/src/chatgpt.ts b/src/chatgpt.ts index d89b0fe..f8c3f59 100644 --- a/src/chatgpt.ts +++ b/src/chatgpt.ts @@ -9,7 +9,7 @@ export interface MessageDetail { image_url?: ImageURL; } export interface Message { - role: "system" | "user" | "assistant" | "function"; + role: "system" | "user" | "assistant" | "tool"; content: string | MessageDetail[]; name?: "example_user" | "example_assistant"; tool_calls?: { @@ -17,6 +17,7 @@ export interface Message { type: string; function: any; }[]; + tool_call_id?: string; } export const getMessageText = (message: Message): string => { if (typeof message.content === "string") { From 6aaf7beb5b7a3a27f9103731e5d2252deac14304 Mon Sep 17 00:00:00 2001 From: heimoshuiyu Date: Fri, 10 Nov 2023 11:17:22 +0800 Subject: [PATCH 03/11] add
above tool_call msg --- src/message.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/message.tsx b/src/message.tsx index b4ff996..b692b03 100644 --- a/src/message.tsx +++ b/src/message.tsx @@ -294,11 +294,14 @@ export default function Message(props: Props) {
{chat.tool_calls?.map((tool_call) => ( -
- Tool Call ID: {tool_call?.id} -

Type: {tool_call?.type}

-

Function: {JSON.stringify(tool_call?.function)}

-
+ <> +
+
+ Tool Call ID: {tool_call?.id} +

Type: {tool_call?.type}

+

Function: {JSON.stringify(tool_call?.function)}

+
+ ))}
From 856976c03c44afb9fa92ac8b0df34b78f7883dce Mon Sep 17 00:00:00 2001 From: heimoshuiyu Date: Fri, 10 Nov 2023 11:29:18 +0800 Subject: [PATCH 04/11] fix fetch resp msg --- src/chatgpt.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chatgpt.ts b/src/chatgpt.ts index f8c3f59..9affb8d 100644 --- a/src/chatgpt.ts +++ b/src/chatgpt.ts @@ -277,7 +277,7 @@ class Chat { this.forgetSomeMessages(); } - let content = ""; + let content = resp.choices[0].message?.content ?? ""; if ( !resp.choices[0]?.message?.content && !resp.choices[0]?.message?.tool_calls From 626e7780f824c53fa8c5533758793dd888a2062d Mon Sep 17 00:00:00 2001 From: heimoshuiyu Date: Fri, 10 Nov 2023 12:22:28 +0800 Subject: [PATCH 05/11] fix tool-call streaming mode --- src/chatbox.tsx | 40 +++++++++++++++++++++++++++++++++++++++- src/chatgpt.ts | 38 ++++++++++++++++++++++++++++++++------ src/message.tsx | 3 ++- 3 files changed, 73 insertions(+), 8 deletions(-) diff --git a/src/chatbox.tsx b/src/chatbox.tsx index d57279c..6893630 100644 --- a/src/chatbox.tsx +++ b/src/chatbox.tsx @@ -15,6 +15,7 @@ import ChatGPT, { FetchResponse, Message as MessageType, MessageDetail, + ToolCall, } from "./chatgpt"; import Message from "./message"; import models from "./models"; @@ -71,12 +72,48 @@ export default function ChatBOX(props: { let responseTokenCount = 0; chatStore.streamMode = true; const allChunkMessage: string[] = []; + const allChunkTool: ToolCall[] = []; setShowGenerating(true); for await (const i of client.processStreamResponse(response)) { chatStore.responseModelName = i.model; responseTokenCount += 1; allChunkMessage.push(i.choices[0].delta.content ?? ""); - setGeneratingMessage(allChunkMessage.join("")); + const tool_calls = i.choices[0].delta.tool_calls; + if (tool_calls) { + for (const tool_call of tool_calls) { + // init + if (tool_call.id) { + allChunkTool.push({ + id: tool_call.id, + type: tool_call.type, + index: tool_call.index, + function: { + name: tool_call.function.name, + arguments: "", + }, + }); + continue; + } + + // update tool call arguments + const tool = allChunkTool.find( + (tool) => tool.index === tool_call.index + ); + + if (!tool) { + console.log("tool (by index) not found", tool_call.index); + continue; + } + + tool.function.arguments += tool_call.function.arguments; + } + } + setGeneratingMessage( + allChunkMessage.join("") + + allChunkTool.map((tool) => { + return `Tool Call ID: ${tool.id}\nType: ${tool.type}\nFunction: ${tool.function.name}\nArguments: ${tool.function.arguments}`; + }) + ); } setShowGenerating(false); const content = allChunkMessage.join(""); @@ -103,6 +140,7 @@ export default function ChatBOX(props: { chatStore.history.push({ role: "assistant", content, + tool_calls: allChunkTool, hide: false, token: responseTokenCount, example: false, diff --git a/src/chatgpt.ts b/src/chatgpt.ts index 9affb8d..6704c46 100644 --- a/src/chatgpt.ts +++ b/src/chatgpt.ts @@ -8,17 +8,43 @@ export interface MessageDetail { text?: string; image_url?: ImageURL; } +export interface ToolCall { + index: number; + id?: string; + type: string; + function: { + name: string; + arguments: string; + }; +} export interface Message { role: "system" | "user" | "assistant" | "tool"; content: string | MessageDetail[]; name?: "example_user" | "example_assistant"; - tool_calls?: { - id: string; - type: string; - function: any; - }[]; + tool_calls?: ToolCall[]; tool_call_id?: string; } + +interface Delta { + role?: string; + content?: string; + tool_calls?: ToolCall[]; +} + +interface Choices { + index: number; + delta: Delta; + finish_reason: string | null; +} + +export interface StreamingResponseChunk { + id: string; + object: string; + created: number; + model: string; + system_fingerprint: string; + choices: Choices[]; +} export const getMessageText = (message: Message): string => { if (typeof message.content === "string") { return message.content; @@ -252,7 +278,7 @@ class Chat { console.log("line", line); try { const jsonStr = line.slice("data:".length).trim(); - const json = JSON.parse(jsonStr); + const json = JSON.parse(jsonStr) as StreamingResponseChunk; yield json; } catch (e) { console.log(`Chunk parse error at: ${line}`); diff --git a/src/message.tsx b/src/message.tsx index b692b03..b7f8f1e 100644 --- a/src/message.tsx +++ b/src/message.tsx @@ -299,7 +299,8 @@ export default function Message(props: Props) {
Tool Call ID: {tool_call?.id}

Type: {tool_call?.type}

-

Function: {JSON.stringify(tool_call?.function)}

+

Function: {tool_call.function.name}

+

Arguments: {tool_call.function.arguments}

))} From 33f4ab7b42aa4040587e395b9cfe3575c2d8a2f2 Mon Sep 17 00:00:00 2001 From: heimoshuiyu Date: Fri, 10 Nov 2023 17:12:57 +0800 Subject: [PATCH 06/11] fix copy render msg --- src/chatgpt.ts | 8 ++++ src/global.css | 2 +- src/message.tsx | 120 ++++++++++++++++++++++++++++++------------------ 3 files changed, 85 insertions(+), 45 deletions(-) diff --git a/src/chatgpt.ts b/src/chatgpt.ts index 6704c46..748476a 100644 --- a/src/chatgpt.ts +++ b/src/chatgpt.ts @@ -47,6 +47,14 @@ export interface StreamingResponseChunk { } export const getMessageText = (message: Message): string => { if (typeof message.content === "string") { + // function call message + if (message.tool_calls) { + return message.tool_calls + .map((tc) => { + return `Tool Call ID: ${tc.id}\nType: ${tc.type}\nFunction: ${tc.function.name}\nArguments: ${tc.function.arguments}}`; + }) + .join("\n"); + } return message.content; } return message.content diff --git a/src/global.css b/src/global.css index f67213a..c951762 100644 --- a/src/global.css +++ b/src/global.css @@ -28,6 +28,6 @@ body::-webkit-scrollbar { display: none; } -p.message-content { +.message-content { white-space: pre-wrap; } diff --git a/src/message.tsx b/src/message.tsx index b7f8f1e..94c2a4b 100644 --- a/src/message.tsx +++ b/src/message.tsx @@ -245,14 +245,18 @@ export default function Message(props: Props) { ); - const CopyIcon = () => { + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text); + setShowCopiedHint(true); + setTimeout(() => setShowCopiedHint(false), 1000); + }; + + const CopyIcon = ({ textToCopy }: { textToCopy: string }) => { return ( <>