Files
windmill/frontend/src/lib/components/copilot/chat/api/apiTools.ts
T
centdix 4bb9be09af feat(aichat): cache prompts when using anthropic models (#6489)
* working draft

* add caching

* add anthropic.ts

* cleaning

* better typing

* nit

* use anthropic sdk

* draft

* draft parser

* cleaning

* update openai sdk + use stream method

* fix anthropic base url

* cleaning

* better error handling + cleaning

* rollback to old parsing

* nit

* set strict to false for mistral
2025-09-01 08:55:15 +00:00

235 lines
6.6 KiB
TypeScript

import type { ChatCompletionFunctionTool } from 'openai/resources/index.mjs'
import type { Tool } from '../shared'
import { get } from 'svelte/store'
import { workspaceStore } from '$lib/stores'
import type { EndpointTool } from '$lib/gen/types.gen'
import { McpService } from '$lib/gen/services.gen'
function buildApiCallTool(endpointTool: EndpointTool): ChatCompletionFunctionTool {
// Build the parameters schema for OpenAI function calling
const parameters: Record<string, any> = {
type: 'object',
properties: {},
required: []
}
// Add path parameters
if (endpointTool.path_params_schema?.properties) {
for (const [key, schema] of Object.entries(endpointTool.path_params_schema.properties)) {
// Skip workspace parameter as it's auto-filled
if (key === 'workspace') continue
parameters.properties[key] = schema
if (
Array.isArray(endpointTool.path_params_schema.required) &&
endpointTool.path_params_schema.required.includes(key)
) {
parameters.required.push(key)
}
}
}
// Add query parameters
if (endpointTool.query_params_schema?.properties) {
for (const [key, schema] of Object.entries(endpointTool.query_params_schema.properties)) {
parameters.properties[key] = schema
if (
Array.isArray(endpointTool.query_params_schema.required) &&
endpointTool.query_params_schema.required.includes(key)
) {
parameters.required.push(key)
}
}
}
// Add body parameters
if (endpointTool.body_schema?.properties) {
// For body params, we wrap them in a 'body' object
parameters.properties.body = {
type: 'object',
description: 'Request body',
properties: endpointTool.body_schema.properties,
required: endpointTool.body_schema.required || []
}
if (
Array.isArray(endpointTool.body_schema.required) &&
endpointTool.body_schema.required.length > 0
) {
parameters.required.push('body')
}
}
return {
type: 'function',
function: {
name: endpointTool.name,
description: endpointTool.instructions || endpointTool.description,
parameters
}
}
}
function buildToolsFromEndpoints(endpointTools: EndpointTool[]): {
tools: ChatCompletionFunctionTool[]
endpointMap: Record<string, { method: string; path: string }>
} {
const tools: ChatCompletionFunctionTool[] = []
const endpointMap: Record<string, { method: string; path: string }> = {}
for (const endpointTool of endpointTools) {
const tool = buildApiCallTool(endpointTool)
tools.push(tool)
// Store the endpoint info in the map
endpointMap[endpointTool.name] = {
method: endpointTool.method,
path: endpointTool.path
}
}
return { tools, endpointMap }
}
export function createApiTools(
chatTools: ChatCompletionFunctionTool[],
endpointMap: Record<string, { method: string; path: string }> = {}
): Tool<{}>[] {
return chatTools.map((chatTool) => {
const toolName = chatTool.function.name
const endpoint = endpointMap[toolName]
const method = endpoint?.method?.toUpperCase() || 'GET'
// Determine if tool needs confirmation based on method
const needsConfirmation = ['DELETE', 'POST', 'PUT', 'PATCH'].includes(method)
return {
def: chatTool,
requiresConfirmation: needsConfirmation,
confirmationMessage: `Run ${toolName}`,
showDetails: true,
fn: async ({ args, toolId, toolCallbacks }) => {
const toolName = chatTool.function.name
const endpoint = endpointMap[toolName]
if (!endpoint) {
throw new Error(`No endpoint mapping found for tool ${toolName}`)
}
try {
const workspace = get(workspaceStore) as string
let path = endpoint.path.replace('{workspace}', workspace)
// Build URL with path parameters
let url = `/api${path}`
const queryParams: Record<string, string> = {}
let requestBody: any = undefined
// Process arguments
for (const [key, value] of Object.entries(args)) {
if (key === 'body') {
requestBody = value
continue
}
// Check if this is a path parameter
if (url.includes(`{${key}}`)) {
url = url.replace(`{${key}}`, encodeURIComponent(String(value)))
} else {
// Assume it's a query parameter
queryParams[key] = String(value)
}
}
// Add query parameters to URL if needed
if (Object.keys(queryParams).length > 0) {
const searchParams = new URLSearchParams()
for (const [key, value] of Object.entries(queryParams)) {
searchParams.append(key, value)
}
url += `?${searchParams.toString()}`
}
toolCallbacks.setToolStatus(toolId, {
content: `Calling ${toolName}...`
})
const fetchOptions: RequestInit = {
method: endpoint.method
}
// Add request body for POST/PUT/PATCH methods
if (requestBody && ['POST', 'PUT', 'PATCH'].includes(endpoint.method.toUpperCase())) {
fetchOptions.headers = {
'Content-Type': 'application/json'
}
fetchOptions.body = JSON.stringify(requestBody)
}
const response = await fetch(url, fetchOptions)
if (response.ok) {
let result = ''
if (response.headers.get('content-type')?.includes('application/json')) {
result = await response.json()
} else {
result = await response.text()
}
const jsonResult = JSON.stringify({
success: true,
data: result
})
toolCallbacks.setToolStatus(toolId, {
content: `Call to ${toolName} completed`,
result: jsonResult
})
return jsonResult
} else {
const text = await response.text()
const jsonResult = JSON.stringify({
success: false,
error: text,
status: response.status
})
toolCallbacks.setToolStatus(toolId, {
content: `Call to ${toolName} failed`,
result: jsonResult,
error: `HTTP ${response.status}: ${text}`
})
return jsonResult
}
} catch (error) {
const errorMessage = `Error calling API: ${error instanceof Error ? error.message : String(error)}`
toolCallbacks.setToolStatus(toolId, {
content: `Call to ${toolName} failed`,
error: errorMessage
})
console.error(`Error calling API:`, error)
return errorMessage
}
}
}
})
}
export async function loadApiTools(): Promise<Tool<{}>[]> {
try {
// Fetch the list of available MCP tools from the backend
const endpointTools = await McpService.listMcpTools({
workspace: get(workspaceStore) as string
})
// Build tools from the endpoint definitions
const { tools: apiTools, endpointMap } = buildToolsFromEndpoints(endpointTools)
// Create executable tools
const executableApiTools = createApiTools(apiTools, endpointMap)
return executableApiTools
} catch (error) {
console.error('Failed to load API tools:', error)
return []
}
}