Files
windmill/frontend/src/lib/components/copilot/chat/shared.ts
T
63c0062283 nit(aichat): delete old messages when conversation becomes too long (#6258)
* delete old messages

* nit

* feat(aichat): add recursion bounds and improve token estimation

- Add maxDepth parameter (default 10) to deleteOldestMessage to prevent infinite recursion
- Enhance estimateTokenUsage to handle tool calls, content arrays, and function names
- Improves stability and accuracy of message cleanup when conversations become too long

Co-authored-by: centdix <centdix@users.noreply.github.com>

* nit

* cleaning

* better logic

* fix logic

* fix

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: centdix <centdix@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2025-07-23 16:36:41 +02:00

200 lines
4.9 KiB
TypeScript

import type {
ChatCompletionMessageParam,
ChatCompletionMessageToolCall,
ChatCompletionTool
} from 'openai/resources/chat/completions.mjs'
import { get } from 'svelte/store'
import type { ContextElement } from './context'
import { workspaceStore } from '$lib/stores'
import type { ExtendedOpenFlow } from '$lib/components/flows/types'
import type { FunctionParameters } from 'openai/resources/shared.mjs'
import { zodToJsonSchema } from 'zod-to-json-schema'
import { z } from 'zod'
import { ScriptService } from '$lib/gen'
type BaseDisplayMessage = {
content: string
contextElements?: ContextElement[]
snapshot?: ExtendedOpenFlow
}
export type UserDisplayMessage = BaseDisplayMessage & {
role: 'user'
index: number // Used to match index with actual chat messages
error?: boolean
}
export type ToolDisplayMessage = {
role: 'tool'
tool_call_id: string
content: string
}
export type AssistantDisplayMessage = BaseDisplayMessage & {
role: 'assistant'
}
export type DisplayMessage = UserDisplayMessage | ToolDisplayMessage | AssistantDisplayMessage
async function callTool<T>({
tools,
functionName,
args,
workspace,
helpers,
toolCallbacks,
toolId
}: {
tools: Tool<T>[]
functionName: string
args: any
workspace: string
helpers: T
toolCallbacks: ToolCallbacks
toolId: string
}): Promise<string> {
const tool = tools.find((t) => t.def.function.name === functionName)
if (!tool) {
throw new Error(`Unknown tool call: ${functionName}`)
}
return tool.fn({ args, workspace, helpers, toolCallbacks, toolId })
}
export async function processToolCall<T>({
tools,
toolCall,
messages,
helpers,
toolCallbacks
}: {
tools: Tool<T>[]
toolCall: ChatCompletionMessageToolCall
messages: ChatCompletionMessageParam[]
helpers: T
toolCallbacks: ToolCallbacks
}): Promise<ChatCompletionMessageParam> {
try {
const args = JSON.parse(toolCall.function.arguments || '{}')
let result = ''
try {
result = await callTool({
tools,
functionName: toolCall.function.name,
args,
workspace: get(workspaceStore) ?? '',
helpers,
toolCallbacks,
toolId: toolCall.id
})
} catch (err) {
console.error(err)
result =
'Error while calling tool, MUST tell the user to check the browser console for more details, and then respond as much as possible to the original request'
}
const toAdd = {
role: 'tool' as const,
tool_call_id: toolCall.id,
content: result
}
return toAdd
} catch (err) {
console.error(err)
return {
role: 'tool' as const,
tool_call_id: toolCall.id,
content:
'Error while calling tool, MUST tell the user to check the browser console for more details, and then respond as much as possible to the original request'
}
}
}
export interface Tool<T> {
def: ChatCompletionTool
fn: (p: {
args: any
workspace: string
helpers: T
toolCallbacks: ToolCallbacks
toolId: string
}) => Promise<string>
preAction?: (p: { toolCallbacks: ToolCallbacks; toolId: string }) => void
}
export interface ToolCallbacks {
setToolStatus: (id: string, content: string) => void
}
export function createToolDef(
zodSchema: z.ZodSchema,
name: string,
description: string
): ChatCompletionTool {
const schema = zodToJsonSchema(zodSchema, {
name,
target: 'openAi'
})
let parameters = schema.definitions![name] as FunctionParameters
parameters = {
...parameters,
required: parameters.required ?? []
}
return {
type: 'function',
function: {
strict: true,
name,
description,
parameters
}
}
}
const searchHubScriptsSchema = z.object({
query: z
.string()
.describe('The query to search for, e.g. send email, list stripe invoices, etc..')
})
const searchHubScriptsToolDef = createToolDef(
searchHubScriptsSchema,
'search_hub_scripts',
'Search for scripts in the hub'
)
export const createSearchHubScriptsTool = (withContent: boolean = false) => ({
def: searchHubScriptsToolDef,
fn: async ({ args, toolId, toolCallbacks }) => {
toolCallbacks.setToolStatus(
toolId,
'Searching for hub scripts related to "' + args.query + '"...'
)
const parsedArgs = searchHubScriptsSchema.parse(args)
const scripts = await ScriptService.queryHubScripts({
text: parsedArgs.query,
kind: 'script'
})
toolCallbacks.setToolStatus(
toolId,
'Found ' + scripts.length + ' scripts in the hub related to "' + args.query + '"'
)
// if withContent, fetch scripts with their content, limit to 3 results
const results = await Promise.all(
scripts.slice(0, withContent ? 3 : undefined).map(async (s) => {
let content = ''
if (withContent) {
content = await ScriptService.getHubScriptContentByPath({
path: `hub/${s.version_id}/${s.app}/${s.summary.toLowerCase().replaceAll(/\s+/g, '_')}`
})
}
return {
path: `hub/${s.version_id}/${s.app}/${s.summary.toLowerCase().replaceAll(/\s+/g, '_')}`,
summary: s.summary,
...(withContent ? { content } : {})
}
})
)
return JSON.stringify(results)
}
})