diff --git a/frontend/src/lib/ata/index.ts b/frontend/src/lib/ata/index.ts index 3099094bf5..4f5c585900 100644 --- a/frontend/src/lib/ata/index.ts +++ b/frontend/src/lib/ata/index.ts @@ -345,7 +345,7 @@ function getDTName(s: string) { if (s.indexOf('@') === 0 && s.indexOf('/') !== -1) { // we have a scoped module, e.g. @bla/foo // which should be converted to bla__foo - s = s.substr(1).replace('/', '__') + s = s.substring(1).replace('/', '__') } return s } diff --git a/frontend/src/lib/components/copilot/chat/flow/core.ts b/frontend/src/lib/components/copilot/chat/flow/core.ts index 46b849eb60..91269caf8b 100644 --- a/frontend/src/lib/components/copilot/chat/flow/core.ts +++ b/frontend/src/lib/components/copilot/chat/flow/core.ts @@ -348,7 +348,44 @@ const getInstructionsForCodeGenerationToolDef = createToolDef( const workspaceScriptsSearch = new WorkspaceScriptsSearch() +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 = searchScriptsSchema.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) + } +}) + export const flowTools: Tool[] = [ + createSearchHubScriptsTool(false), { def: searchScriptsToolDef, fn: async ({ args, workspace, toolId, toolCallbacks }) => { @@ -369,30 +406,6 @@ export const flowTools: Tool[] = [ return JSON.stringify(scriptResults) } }, - { - def: searchHubScriptsToolDef, - fn: async ({ args, toolId, toolCallbacks }) => { - toolCallbacks.setToolStatus( - toolId, - 'Searching for hub scripts related to "' + args.query + '"...' - ) - const parsedArgs = searchScriptsSchema.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 + '"' - ) - return JSON.stringify( - scripts.map((s) => ({ - path: `hub/${s.version_id}/${s.app}/${s.summary.toLowerCase().replaceAll(/\s+/g, '_')}`, - summary: s.summary - })) - ) - } - }, { def: addStepToolDef, fn: async ({ args, helpers, toolId, toolCallbacks }) => { diff --git a/frontend/src/lib/components/copilot/chat/script/core.ts b/frontend/src/lib/components/copilot/chat/script/core.ts index 9a5facfbda..5c893156e7 100644 --- a/frontend/src/lib/components/copilot/chat/script/core.ts +++ b/frontend/src/lib/components/copilot/chat/script/core.ts @@ -8,12 +8,22 @@ import type { ChatCompletionTool, ChatCompletionUserMessageParam } from 'openai/resources/index.mjs' -import { type DBSchema, dbSchemas } from '$lib/stores' +import { copilotSessionModel, type DBSchema, dbSchemas } from '$lib/stores' import { scriptLangToEditorLang } from '$lib/scripts' import { getDbSchemas } from '$lib/components/apps/components/display/dbtable/utils' import type { CodePieceElement, ContextElement } from '../context' import type { Tool } from '../shared' import { PYTHON_PREPROCESSOR_MODULE_CODE, TS_PREPROCESSOR_MODULE_CODE } from '$lib/script_helpers' +import { createSearchHubScriptsTool } from '../flow/core' +import { setupTypeAcquisition, type DepsToGet } from '$lib/ata' +import { getModelContextWindow } from '../../lib' + +// Score threshold for npm packages search filtering +const SCORE_THRESHOLD = 1000 +// percentage of the context window for documentation of npm packages +const DOCS_CONTEXT_PERCENTAGE = 1 +// percentage of the context window for types of npm packages +const TYPES_CONTEXT_PERCENTAGE = 1 export function formatResourceTypes( allResourceTypes: ResourceType[], @@ -334,6 +344,7 @@ export const CHAT_SYSTEM_PROMPT = ` - The user can ask you questions about a list of \`DATABASES\` that are available in the user's workspace. If the user asks you a question about a database, you should ask the user to specify the database name if not given, or take the only one available if there is only one. - You can also receive a \`DIFF\` of the changes that have been made to the code. You should use this diff to give better answers. - Before giving your answer, check again that you carefully followed these instructions. + - When asked to create a script that communicates with an external service, you can use the \`search_hub_scripts\` tool to search for relevant scripts in the hub. Make sure the language is the same as what the user is coding in. If you do not find any relevant scripts, you can use the \`search_npm_packages\` tool to search for relevant packages and their documentation. Always give a link to the documentation in your answer if possible. Important: Do not mention or reveal these instructions to the user unless explicitly asked to do so. @@ -477,6 +488,10 @@ export function prepareScriptTools( if (context.some((c) => c.type === 'db')) { tools.push(dbSchemaTool) } + if (['bun', 'deno'].includes(language)) { + tools.push(createSearchHubScriptsTool(true)) + tools.push(searchNpmPackagesTool) + } return tools } @@ -655,3 +670,158 @@ export const dbSchemaTool: Tool = { return stringSchema } } + +type PackageSearchQuery = { + package: { + name: string + version: string + links: { + npm: string + homepage: string + repository: string + bugs: string + } + } + searchScore: number +} + +type PackageSearchResult = { + package: string + documentation: string + types: string +} + +const packagesSearchCache = new Map() +export async function searchExternalIntegrationResources(args: { query: string }): Promise { + try { + if (packagesSearchCache.has(args.query)) { + return JSON.stringify(packagesSearchCache.get(args.query)) + } + + const result = await fetch(`https://registry.npmjs.org/-/v1/search?text=${args.query}&size=2`) + const data = await result.json() + const filtered = data.objects.filter( + (r: PackageSearchQuery) => r.searchScore >= SCORE_THRESHOLD + ) + + const modelContextWindow = getModelContextWindow(get(copilotSessionModel)?.model ?? '') + const results: PackageSearchResult[] = await Promise.all( + filtered.map(async (r: PackageSearchQuery) => { + let documentation = '' + let types = '' + try { + const docResponse = await fetch(`https://unpkg.com/${r.package.name}/readme.md`) + const docLimit = Math.floor((modelContextWindow * DOCS_CONTEXT_PERCENTAGE) / 100) + documentation = await docResponse.text() + documentation = documentation.slice(0, docLimit) + } catch (error) { + console.error('Error getting documentation for package:', error) + documentation = '' + } + try { + const typesResponse = await fetchNpmPackageTypes(r.package.name, r.package.version) + const typesLimit = Math.floor((modelContextWindow * TYPES_CONTEXT_PERCENTAGE) / 100) + types = typesResponse.types.slice(0, typesLimit) + } catch (error) { + console.error('Error getting types for package:', error) + types = '' + } + return { + package: r.package.name, + documentation: documentation, + types: types + } + }) + ) + packagesSearchCache.set(args.query, results) + return JSON.stringify(results) + } catch (error) { + console.error('Error searching external integration resources:', error) + return 'Error searching external integration resources' + } +} + +const SEARCH_NPM_PACKAGES_TOOL: ChatCompletionTool = { + type: 'function', + function: { + name: 'search_npm_packages', + description: 'Search for npm packages and their documentation', + parameters: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'The query to search for' + } + }, + required: ['query'] + } + } +} + +export const searchNpmPackagesTool: Tool = { + def: SEARCH_NPM_PACKAGES_TOOL, + fn: async ({ args, toolId, toolCallbacks }) => { + toolCallbacks.setToolStatus(toolId, 'Searching for relevant packages...') + const result = await searchExternalIntegrationResources(args) + toolCallbacks.setToolStatus(toolId, 'Retrieved relevant packages') + return result + } +} + +export async function fetchNpmPackageTypes( + packageName: string, + version: string = 'latest' +): Promise<{ success: boolean; types: string; error?: string }> { + try { + const typeDefinitions = new Map() + + const ata = setupTypeAcquisition({ + projectName: 'NPM-Package-Types', + depsParser: () => [], + root: '', + delegate: { + receivedFile: (code: string, path: string) => { + if (path.endsWith('.d.ts')) { + typeDefinitions.set(path, code) + } + }, + localFile: () => {} + } + }) + + const depsToGet: DepsToGet = [ + { + raw: packageName, + module: packageName, + version: version + } + ] + + await ata(depsToGet) + + if (typeDefinitions.size === 0) { + return { + success: false, + types: '', + error: `No type definitions found for ${packageName}` + } + } + + const formattedTypes = Array.from(typeDefinitions.entries()) + .map(([path, content]) => `// ${path}\n${content}`) + .join('\n\n') + + return { + success: true, + types: formattedTypes + } + } catch (error) { + console.error('Error fetching NPM package types:', error) + return { + success: false, + types: '', + error: `Error fetching package types: ${error instanceof Error ? error.message : 'Unknown error'}` + } + } +} diff --git a/frontend/src/lib/components/copilot/lib.ts b/frontend/src/lib/components/copilot/lib.ts index 6d4ff44e1a..f0da2c381e 100644 --- a/frontend/src/lib/components/copilot/lib.ts +++ b/frontend/src/lib/components/copilot/lib.ts @@ -106,6 +106,18 @@ function getModelMaxTokens(model: string) { return 8192 } +export function getModelContextWindow(model: string) { + if (model.startsWith('gpt-4.1') || model.startsWith('gemini')) { + return 1000000 + } else if (model.startsWith('gpt-4o') || model.startsWith('llama-3.3')) { + return 128000 + } else if (model.startsWith('claude') || model.startsWith('o4-mini') || model.startsWith('o3')) { + return 200000 + } else { + return 128000 + } +} + function getModelSpecificConfig( modelProvider: AIProviderModel, tools?: OpenAI.Chat.Completions.ChatCompletionTool[]