From d110031fc075ba33246afd82c7bf999d4f758ee4 Mon Sep 17 00:00:00 2001 From: Marc McIntosh Date: Wed, 22 May 2024 14:10:05 +0200 Subject: [PATCH] wip: render lsp functions --- src/__fixtures__/chat.ts | 372 +++++++++++++++++- src/__fixtures__/index.ts | 2 +- .../ChatContent/ChatContent.stories.tsx | 9 +- src/components/ChatContent/ChatContent.tsx | 52 ++- src/services/refact.ts | 57 ++- 5 files changed, 479 insertions(+), 13 deletions(-) diff --git a/src/__fixtures__/chat.ts b/src/__fixtures__/chat.ts index 06a12c39..00667afa 100644 --- a/src/__fixtures__/chat.ts +++ b/src/__fixtures__/chat.ts @@ -1,4 +1,12 @@ -import { ChatThread } from "../events"; +import { + ChatMessage, + ChatMessages, + ChatThread, + ToolCall, + ToolCallsMessage, + ToolMessage, + ToolResult, +} from "../events"; export const MARS_ROVER_CHAT: ChatThread = { id: "9afd6fef-3e49-40df-8aca-688af3621514", @@ -58,3 +66,365 @@ export const MARS_ROVER_CHAT: ChatThread = { // createdAt: "2023-12-21T17:32:50.186Z", // lastUpdated: "2023-12-21T17:33:22.209Z", }; + +const chatWithFunctionsRaw = [ + { + role: "system", + content: + '\nYou are a search agent. You need to actively search for the answer yourself, don\'t ask the user to do anything. The answer is most likely in the files and databases accessible using tool calls, not on the internet.\n\nWhen responding to a query, first provide a very brief explanation of your plan to use tools in parallel to answer the question, and then make several tool calls to gather more details.\n\nMinimize the number of steps, call up to 15 tools in parallel when exploring.\n\nIT IS FORBIDDEN TO JUST CALL TOOLS WITHOUT EXPLAINING. EXPLAIN FIRST!\n\nWhen user corrects you, acknowledge the correction, write "I will make a note to remember this" and use note_to_self call.\n\n\nExample 1\n\nUser: "What is the weather like today in Paris and London?"\nAssistant: "Must be sunny in Paris and foggy in London."\nUser: "don\'t hallucinate, use the tools"\nAssistant: "Sorry for the confusion, you are right, weather is real-time, and my best shot is to use the weather tool. I will make a note to remember this. My original instruction was to fetch weather for Paris and London. I will use 2 calls in parallel."\n[Call note_to_self "Weather" "For weather, use tools to get real-time information"]\n[Call weather "London"]\n[Call weather "Paris"]\n\n\nExample 2\n\nUser: "What is MyClass"\nAssistant: "Let me find it first."\n[Call ls "."]\nTool: subdir1, subdir2, subdir3\nAssistant: "I see 3 subdirs, will make 3 calls in parallel to check what\'s inside."\n[Call ls "subdir1"]\n[Call ls "subdir2"]\n[Call ls "subdir3"]\nTool: ...\nTool: ...\nTool: ...\nAssistant: "I give up, I can\'t find a file relevant for MyClass \ud83d\ude15"\nUser: "Look, it\'s my_class.cpp"\nAssistant: "Sorry for the confusion, there is in fact a file named `my_class.cpp` in `subdir2` that must be relevant for MyClass. I will make a note to remember this. My original instruction was to describe MyClass."\n[Call note_to_self "searching, browsing filesystem, MyClass" "Try to lowercase the name user asked about, convert to snake case, and guess the right file this way."]\n[Call cat "subdir2/my_class.cpp"]\nTool: ...\nAssistant: "MyClass does this and this"\n\n\n', + tool_calls: null, + finish_reason: "", + tool_call_id: "", + }, + { + role: "user", + content: "Explain what Frog is", + tool_calls: null, + finish_reason: "", + tool_call_id: "", + }, + { + role: "assistant", + content: + "Let me find the relevant information about Frog first. I will check the contents of files or directories that might contain information about Frog. I will start by listing the files in the current directory to see if there are any relevant files or directories.", + tool_calls: [ + { + id: "call_WOyQ1sykVGppzWjjUu1drk6L", + function: { + arguments: '{"dirpath":"."}', + name: "ls", + }, + type: "function", + }, + ], + finish_reason: "tool_calls", + tool_call_id: "", + }, + { + role: "tool", + content: + "Listing directory .\n 2260 file Cargo.toml\n 1530 file LICENSE\n 224 dir target\n 1198 file mycaps_te3.json\n 416 dir tests\n 152298 file Cargo.lock\n 757 file mycaps_openai.json\n 61 file build.rs\n 1264 file mycaps_gte.json\n 1598 file _video\n 3548 file README.md\n 768 dir examples\n 219 file _backtrace\n 1665 file _video2\n 141 file a.sh\n 139 file _help\n 992 dir src\n", + tool_calls: null, + finish_reason: "call_worked", + tool_call_id: "call_WOyQ1sykVGppzWjjUu1drk6L", + }, + { + role: "assistant", + content: null, + tool_calls: [ + { + id: "call_IYK970zyp9vZ36m7emzmNDC9", + function: { + arguments: '{"filepath": "README.md"}', + name: "cat", + }, + type: "function", + }, + { + id: "call_8jTn7oj8tfctEnqgKQRBJH0w", + function: { + arguments: '{"filepath": "Cargo.toml"}', + name: "cat", + }, + type: "function", + }, + { + id: "call_Ql7xrkn5BqtjVSHHAnNksFis", + function: { + arguments: '{"filepath": "Cargo.lock"}', + name: "cat", + }, + type: "function", + }, + ], + finish_reason: "tool_calls", + tool_call_id: "", + }, + { + role: "tool", + content: + 'File README.md:50-99\n``` "temperature": 0.1,\n "max_new_tokens": 20\n }\n}\'\n```\n\nOutput is `[{"code_completion": "\\n return \\"Hello World!\\"\\n"}]`.\n\n[LSP example](examples/lsp_completion.py)\n\n\n## Telemetry\n\nThe flags `--basic-telemetry` and `--snippet-telemetry` control what telemetry is sent. To be clear: without\nthese flags, no telemetry is sent. Those flags are typically controlled from IDE plugin settings.\n\nBasic telemetry means counters and error messages without information about you or your code. It is "compressed"\ninto `.cache/refact/telemetry/compressed` folder, then from time to time it\'s sent and moved\nto `.cache/refact/telemetry/sent` folder.\n\n"Compressed" means similar records are joined together, increasing the counter. "Sent" means the rust binary\ncommunicates with a HTTP endpoint specified in caps (see Caps section below) and sends .json file exactly how\nyou see it in `.cache/refact/telemetry`. The files are human-readable.\n\nWhen using Refact self-hosted server, telemetry goes to the self-hosted server, not to the cloud.\n\n\n## Caps File\n\nThe `--address-url` parameter controls the behavior of this program by a lot. The address is first used\nto construct `$URL/coding_assistant_caps.json` address to fetch the caps file. Furthermore, there are\ncompiled-in caps you can use by magic addresses "Refact" and "HF".\n\nThe caps file describes which models are running, default models for completion and chat,\nwhere to send the telemetry, how to download a\ntokenizer, where is the endpoint to access actual language models. To read more, check out\ncompiled-in caps in [caps.rs](src/caps.rs).\n\n\n## Tests\n\nThe one to run often is [test_edge_cases.py](tests/test_edge_cases.py).\n\nYou can also run [measure_humaneval_fim.py](tests/measure_humaneval_fim.py) for your favorite model.\n\n\n## Credits\n\nThe initial version of this project was written by looking at llm-ls by [@McPatate](https://github.com/McPatate). He\'s a Rust fan who inspired this project!\n```', + tool_calls: null, + finish_reason: "call_worked", + tool_call_id: "call_IYK970zyp9vZ36m7emzmNDC9", + }, + { + role: "tool", + content: + 'File Cargo.toml:39-88\n```futures-util = "0.3"\nasync-stream = "0.3.5"\nchrono = "0.4.31"\nregex = "1.9.5"\nasync-trait = "0.1.73"\nsimilar = "2.3.0"\naxum = "0.6.20"\nuuid = { version = "1", features = ["v4"] }\nlazy_static = "1.4.0"\n\nregex-automata = { version = "0.1.10", features = ["transducer"] }\nsorted-vec = "0.8.3"\ntree-sitter = "0.20"\ntree-sitter-cpp = "0.20"\n#tree-sitter-c-sharp = "0.20"\ntree-sitter-java = "0.20"\ntree-sitter-javascript = "0.20"\n#tree-sitter-kotlin = "0.3.1"\ntree-sitter-python = "0.20"\ntree-sitter-rust = "0.20"\ntree-sitter-typescript = "0.20"\n\narrow = "47.0.0"\narrow-array = "47.0.0"\narrow-schema= "47.0.0"\nasync_once= "0.2.6"\nasync-process = "2.0.1"\nitertools = "0.11.0"\nlance = "=0.9.0"\nlance-linalg = "=0.9.0"\nlance-index = "=0.9.0"\nlog = "0.4.20"\nmd5 = "0.7"\nmockito = "0.28.0"\nnotify = { version = "6.1.1", features = ["serde"] }\nparking_lot = { version = "0.12.1", features = ["serde"] }\nrusqlite = { version = "0.30.0", features = ["bundled"] }\ntempfile = "3.8.1"\ntime = "0.3.30"\ntokio-rusqlite = "0.5.0"\nvectordb = "=0.4.0"\nwalkdir = "2.3"\nwhich = "5.0.0"\nstrsim = "0.8.0"\ntypetag = "0.2"\ndyn_partial_eq = "=0.1.2"\nrayon = "1.8.0"\nbacktrace = "0.3.71"\nrand = "0.8.5"\n```', + tool_calls: null, + finish_reason: "call_worked", + tool_call_id: "call_8jTn7oj8tfctEnqgKQRBJH0w", + }, + { + role: "tool", + content: + 'File Cargo.lock:6265-6314\n```]\n\n[[package]]\nname = "zstd"\nversion = "0.11.2+zstd.1.5.2"\nsource = "registry+https://github.com/rust-lang/crates.io-index"\nchecksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4"\ndependencies = [\n "zstd-safe 5.0.2+zstd.1.5.2",\n]\n\n[[package]]\nname = "zstd"\nversion = "0.12.4"\nsource = "registry+https://github.com/rust-lang/crates.io-index"\nchecksum = "1a27595e173641171fc74a1232b7b1c7a7cb6e18222c11e9dfb9888fa424c53c"\ndependencies = [\n "zstd-safe 6.0.6",\n]\n\n[[package]]\nname = "zstd-safe"\nversion = "5.0.2+zstd.1.5.2"\nsource = "registry+https://github.com/rust-lang/crates.io-index"\nchecksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db"\ndependencies = [\n "libc",\n "zstd-sys",\n]\n\n[[package]]\nname = "zstd-safe"\nversion = "6.0.6"\nsource = "registry+https://github.com/rust-lang/crates.io-index"\nchecksum = "ee98ffd0b48ee95e6c5168188e44a54550b1564d9d530ee21d5f0eaed1069581"\ndependencies = [\n "libc",\n "zstd-sys",\n]\n\n[[package]]\nname = "zstd-sys"\nversion = "2.0.9+zstd.1.5.5"\nsource = "registry+https://github.com/rust-lang/crates.io-index"\nchecksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656"\ndependencies = [\n "cc",\n "pkg-config",\n]\n```', + tool_calls: null, + finish_reason: "call_worked", + tool_call_id: "call_Ql7xrkn5BqtjVSHHAnNksFis", + }, + { + role: "assistant", + content: + "Based on the information found in the files:\n\n- Frog seems to be a project related to coding assistance and telemetry.\n- Telemetry is sent based on flags like `--basic-telemetry` and `--snippet-telemetry`.\n- The `Caps File` contains information about models, default models for completion and chat, telemetry destinations, tokenizers, and language models.\n- Tests related to the project include `test_edge_cases.py` and `measure_humaneval_fim.py`.\n- The project credits [@McPatate](https://github.com/McPatate) for the initial version, who is a Rust fan.\n\nFor more specific details about Frog, further exploration into the project's source code or documentation may be needed.", + tool_calls: null, + finish_reason: "stop", + tool_call_id: "", + }, + { + role: "user", + content: "continue searching for the Frog in tests", + tool_calls: null, + finish_reason: "", + tool_call_id: "", + }, + { + role: "assistant", + content: null, + tool_calls: [ + { + id: "call_KKgK2Ki3bpvCk2jCtDhQxfpw", + function: { + arguments: '{"dirpath":"tests"}', + name: "ls", + }, + type: "function", + }, + ], + finish_reason: "tool_calls", + tool_call_id: "", + }, + { + role: "tool", + content: + "Listing directory tests\n 2438 file test_hf_endpoint.py\n 3021 file lsp_connect.py\n 678 file lsp_completion.py\n 0 file __init__.py\n 96 dir __pycache__/\n 1340 file test_at_completion.py\n 3926 file post_doc_info.py\n 224 dir emergency_frog_situation/\n 4107 file test_edge_cases.py\n 2081 file code_completion_with_rag.py\n 2866 file measure_humaneval_fim.py\n", + tool_calls: null, + finish_reason: "call_worked", + tool_call_id: "call_KKgK2Ki3bpvCk2jCtDhQxfpw", + }, + { + role: "assistant", + content: null, + tool_calls: [ + { + id: "call_dT0OdIL7JLYJ7Fxk72MvmuRA", + function: { + arguments: '{"filepath":"tests/emergency_frog_situation/"}', + name: "cat", + }, + type: "function", + }, + ], + finish_reason: "tool_calls", + tool_call_id: "", + }, + { + role: "tool", + content: + "ERROR: [Errno 21] Is a directory: './tests/emergency_frog_situation'", + tool_calls: null, + finish_reason: "call_failed", + tool_call_id: "call_dT0OdIL7JLYJ7Fxk72MvmuRA", + }, + { + role: "assistant", + content: null, + tool_calls: [ + { + id: "call_C5uTWek5PUKmaTfe7u0TypZL", + function: { + arguments: '{"filepath":"tests/emergency_frog_situation"}', + name: "cat", + }, + type: "function", + }, + ], + finish_reason: "tool_calls", + tool_call_id: "", + }, + { + role: "tool", + content: + "ERROR: [Errno 21] Is a directory: './tests/emergency_frog_situation'", + tool_calls: null, + finish_reason: "call_failed", + tool_call_id: "call_C5uTWek5PUKmaTfe7u0TypZL", + }, + { + role: "assistant", + content: null, + tool_calls: [ + { + id: "call_9vYcfrFCiUhy8g6bNn0WJn5p", + function: { + arguments: '{"filepath":"tests/emergency_frog_situation/"}', + name: "cat", + }, + type: "function", + }, + ], + finish_reason: "tool_calls", + tool_call_id: "", + }, + { + role: "tool", + content: + "ERROR: [Errno 21] Is a directory: './tests/emergency_frog_situation'", + tool_calls: null, + finish_reason: "call_failed", + tool_call_id: "call_9vYcfrFCiUhy8g6bNn0WJn5p", + }, + { + role: "assistant", + content: null, + tool_calls: [ + { + id: "call_6Cg6UfAvNTgEt96EQrHz16W6", + function: { + arguments: '{"filepath":"tests/emergency_frog_situation"}', + name: "cat", + }, + type: "function", + }, + ], + finish_reason: "tool_calls", + tool_call_id: "", + }, + { + role: "tool", + content: + "ERROR: [Errno 21] Is a directory: './tests/emergency_frog_situation'", + tool_calls: null, + finish_reason: "call_failed", + tool_call_id: "call_6Cg6UfAvNTgEt96EQrHz16W6", + }, + { + role: "assistant", + content: + "I couldn't retrieve specific information about Frog in the tests directory. If there are any other locations or specific files you suspect might contain details about Frog, please let me know.", + tool_calls: null, + finish_reason: "stop", + tool_call_id: "", + }, + { + role: "user", + content: "use ls, don't be stupid", + tool_calls: null, + finish_reason: "", + tool_call_id: "", + }, + { + role: "assistant", + content: null, + tool_calls: [ + { + id: "call_UoHvkwbPq6LMAKRM0iblVkSB", + function: { + arguments: '{"dirpath":"tests/emergency_frog_situation"}', + name: "ls", + }, + type: "function", + }, + ], + finish_reason: "tool_calls", + tool_call_id: "", + }, + { + role: "tool", + content: + "Listing directory tests/emergency_frog_situation\n 1516 file jump_to_conclusions.py\n 695 file set_as_avatar.py\n 96 dir __pycache__/\n 777 file frog.py\n 249 file work_day.py\n", + tool_calls: null, + finish_reason: "call_worked", + tool_call_id: "call_UoHvkwbPq6LMAKRM0iblVkSB", + }, + { + role: "assistant", + content: + "In the `tests/emergency_frog_situation` directory, the file `frog.py` seems to be relevant to Frog. Let's check the content of `frog.py` to find out more about Frog.", + tool_calls: [ + { + id: "call_spx7e7LMfw97BmmzojQQf0rO", + function: { + arguments: '{"filepath":"tests/emergency_frog_situation/frog.py"}', + name: "cat", + }, + type: "function", + }, + ], + finish_reason: "tool_calls", + tool_call_id: "", + }, + { + role: "tool", + content: + "File tests/emergency_frog_situation/frog.py:1-29\n```import numpy as np\n\nDT = 0.01\n\nclass Frog:\n def __init__(self, x, y, vx, vy):\n self.x = x\n self.y = y\n self.vx = vx\n self.vy = vy\n\n def bounce_off_banks(self, pond_width, pond_height):\n if self.x < 0:\n self.vx = np.abs(self.vx)\n elif self.x > pond_width:\n self.vx = -np.abs(self.vx)\n if self.y < 0:\n self.vy = np.abs(self.vy)\n elif self.y > pond_height:\n self.vy = -np.abs(self.vy)\n\n def jump(self, pond_width, pond_height):\n self.x += self.vx * DT\n self.y += self.vy * DT\n self.bounce_off_banks(pond_width, pond_height)\n self.x = np.clip(self.x, 0, pond_width)\n self.y = np.clip(self.y, 0, pond_height)\n\n```", + tool_calls: null, + finish_reason: "call_worked", + tool_call_id: "call_spx7e7LMfw97BmmzojQQf0rO", + }, + { + role: "assistant", + content: + "The file `frog.py` in the `tests/emergency_frog_situation` directory contains a Python class `Frog` with methods related to simulating the movement of a frog in a pond. The class includes methods for initialization, bouncing off banks, and jumping within the pond. The `Frog` class has attributes like position (`x`, `y`) and velocity (`vx`, `vy`), along with methods to handle movement and boundary conditions.\n\nThis information suggests that Frog in this context is related to a simulation or program involving a frog's behavior in a pond.", + tool_calls: null, + finish_reason: "stop", + tool_call_id: "", + }, +]; + +export const CHAT_FUNCTIONS_MESSAGES: ChatMessages = + chatWithFunctionsRaw.reduce((acc, message) => { + if (message.role === "assistant" && message.content !== null) { + const next: ChatMessages = [[message.role, message.content]]; + + if (message.tool_calls !== null) { + const toolCalls: ToolCall[] = message.tool_calls.map((toolCall) => { + let args: ToolCall["function"]["arguments"] = {}; + try { + args = JSON.parse( + toolCall.function.arguments, + ) as ToolCall["function"]["arguments"]; + } catch (e) { + args = {}; + } + return { + id: toolCall.id, + type: toolCall.type, + function: { + ...toolCall.function, + arguments: args, + }, + }; + }); + const msg: ToolCallsMessage = ["tool_calls", toolCalls]; + next.push(msg); + } + + return [...acc, ...next]; + } + + if (message.role === "system" && message.content !== null) { + const msg: ChatMessage = [message.role, message.content]; + return [...acc, msg]; + } + + if (message.role === "user" && message.content !== null) { + return [...acc, [message.role, message.content]]; + } + + if (message.role === "tool" && message.content !== null) { + const content: ToolResult = { + tool_call_id: message.tool_call_id, + content: message.content, + finish_reason: message.finish_reason, + }; + const msg: ToolMessage = [message.role, content]; + + return [...acc, msg]; + } + + // if(message.role === "context_file"){} + + return acc; + }, []); diff --git a/src/__fixtures__/index.ts b/src/__fixtures__/index.ts index 7380477b..2e844d14 100644 --- a/src/__fixtures__/index.ts +++ b/src/__fixtures__/index.ts @@ -1,5 +1,5 @@ export { STUB_CAPS_RESPONSE } from "./caps"; -export { MARS_ROVER_CHAT } from "./chat"; +export { MARS_ROVER_CHAT, CHAT_FUNCTIONS_MESSAGES } from "./chat"; export { TABLE } from "./table"; export * from "./context_files"; export * from "./prompts"; diff --git a/src/components/ChatContent/ChatContent.stories.tsx b/src/components/ChatContent/ChatContent.stories.tsx index 153506f6..210edd23 100644 --- a/src/components/ChatContent/ChatContent.stories.tsx +++ b/src/components/ChatContent/ChatContent.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; import { ChatContent } from "."; -import { MARS_ROVER_CHAT } from "../../__fixtures__"; +import { MARS_ROVER_CHAT, CHAT_FUNCTIONS_MESSAGES } from "../../__fixtures__"; const noop = () => ({}); @@ -23,3 +23,10 @@ export default meta; type Story = StoryObj; export const Primary: Story = {}; + +export const WithFunctions: Story = { + args: { + ...meta.args, + messages: CHAT_FUNCTIONS_MESSAGES, + }, +}; diff --git a/src/components/ChatContent/ChatContent.tsx b/src/components/ChatContent/ChatContent.tsx index 5c661ab3..315d116b 100644 --- a/src/components/ChatContent/ChatContent.tsx +++ b/src/components/ChatContent/ChatContent.tsx @@ -1,6 +1,10 @@ import React, { useEffect, useImperativeHandle } from "react"; -import { ChatMessages, isChatContextFileMessage } from "../../services/refact"; -import { Markdown, MarkdownProps } from "../Markdown"; +import { + ChatMessages, + ToolCall, + isChatContextFileMessage, +} from "../../services/refact"; +import type { MarkdownProps } from "../Markdown"; import { UserInput } from "./UserInput"; import { ScrollArea } from "../ScrollArea"; import { Spinner } from "../Spinner"; @@ -43,6 +47,24 @@ export const ChatContent = React.forwardRef( innerRef.current.scrollIntoView({ behavior: "instant", block: "end" }); }, [messages]); + const toolCallsMap = React.useMemo( + () => + messages.reduce>((acc, message) => { + if (message[0] === "tool_calls") { + const toolCals = message[1].reduce>( + (calls, toolCall) => { + calls[toolCall.id] = toolCall; + return calls; + }, + {}, + ); + return { ...acc, ...toolCals }; + } + return acc; + }, {}), + [messages], + ); + return ( @@ -54,6 +76,7 @@ export const ChatContent = React.forwardRef( } const [role, text] = message; + // store tool_calls data if (role === "user") { const handleRetry = (question: string) => { @@ -83,11 +106,28 @@ export const ChatContent = React.forwardRef( ); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - } else if (role === "system") { - return null; - // return {text}; + } else if (role === "tool" && toolCallsMap[text.tool_call_id]) { + // get tool_calls data + // render somthing nice + const toolCallData = toolCallsMap[text.tool_call_id]; + const toolArgs = Object.entries( + toolCallData.function.arguments, + ).map(([key, value]) => `${key}=${value}`); + + return ( +
+
Tool
+
+ Command: {toolCallData.function.name}, Args: {toolArgs} +
+
Finish reason: {text.finish_reason}
+
Result: {text.content}
+
{text.content}
+
+ ); } else { - return {text}; + return null; + // return {text}; } })} {isWaiting && } diff --git a/src/services/refact.ts b/src/services/refact.ts index c206b9ed..c195ddf9 100644 --- a/src/services/refact.ts +++ b/src/services/refact.ts @@ -6,7 +6,13 @@ const AT_COMMAND_COMPLETION = "/v1/at-command-completion"; const AT_COMMAND_PREVIEW = "/v1/at-command-preview"; const CUSTOM_PROMPTS_URL = "/v1/customization"; -export type ChatRole = "user" | "assistant" | "context_file" | "system"; +export type ChatRole = + | "user" + | "assistant" + | "context_file" + | "system" + | "tool_calls" + | "tool"; export type ChatContextFile = { file_name: string; @@ -18,9 +24,27 @@ export type ChatContextFile = { usefullness?: number; }; -interface BaseMessage extends Array { +export type ToolCall = { + id: string; + function: { + arguments: Record; + name: string; + }; + type: string; +}; + +export type ToolResult = { + tool_call_id: string; + finish_reason: string; // "call_failed" | "call_worked"; + content: string; +}; + +// tool call mesage + +interface BaseMessage + extends Array { 0: ChatRole; - 1: string | ChatContextFile[]; + 1: string | ChatContextFile[] | ToolCall[] | ToolResult; } export interface ChatContextFileMessage extends BaseMessage { @@ -43,6 +67,16 @@ export interface SystemMessage extends BaseMessage { 1: string; } +export interface ToolCallsMessage extends BaseMessage { + 0: "tool_calls"; + 1: ToolCall[]; +} + +export interface ToolMessage extends BaseMessage { + 0: "tool"; + 1: ToolResult; +} + export function isUserMessage(message: ChatMessage): message is UserMessage { return message[0] === "user"; } @@ -51,7 +85,9 @@ export type ChatMessage = | UserMessage | AssistantMessage | ChatContextFileMessage - | SystemMessage; + | SystemMessage + | ToolCallsMessage + | ToolMessage; export type ChatMessages = ChatMessage[]; @@ -93,6 +129,10 @@ export type ChatUserMessageResponse = { content: string; }; +export type ToolResponse = { + role: "tool"; +} & ToolResult; + export function isChatUserMessageResponse( json: unknown, ): json is ChatUserMessageResponse { @@ -104,6 +144,15 @@ export function isChatUserMessageResponse( return json.role === "user" || json.role === "context_file"; } +export function isToolMessage(json: unknown): json is ToolResponse { + if (!json) return false; + if (typeof json !== "object") return false; + if (!("id" in json)) return false; + if (!("content" in json)) return false; + if (!("role" in json)) return false; + return json.role === "tool"; +} + export type ChatResponse = | { choices: ChatChoice[];