import { generateText, type CoreTool, type GenerateTextResult, type Message } from 'ai'; import ignore from 'ignore'; import type { IProviderSetting } from '~/types/model'; import { IGNORE_PATTERNS, type FileMap } from './constants'; import { DEFAULT_MODEL, DEFAULT_PROVIDER, PROVIDER_LIST } from '~/utils/constants'; import { createFilesContext, extractCurrentContext, extractPropertiesFromMessage, simplifyBoltActions } from './utils'; import { createScopedLogger } from '~/utils/logger'; import { LLMManager } from '~/lib/modules/llm/manager'; // Common patterns to ignore, similar to .gitignore const ig = ignore().add(IGNORE_PATTERNS); const logger = createScopedLogger('select-context'); export async function selectContext(props: { messages: Message[]; env?: Env; apiKeys?: Record; files: FileMap; providerSettings?: Record; promptId?: string; contextOptimization?: boolean; summary: string; onFinish?: (resp: GenerateTextResult>, never>) => void; }) { const { messages, env: serverEnv, apiKeys, files, providerSettings, contextOptimization, summary, onFinish } = props; let currentModel = DEFAULT_MODEL; let currentProvider = DEFAULT_PROVIDER.name; const processedMessages = messages.map((message) => { if (message.role === 'user') { const { model, provider, content } = extractPropertiesFromMessage(message); currentModel = model; currentProvider = provider; return { ...message, content }; } else if (message.role == 'assistant') { let content = message.content; if (contextOptimization) { content = simplifyBoltActions(content); } return { ...message, content }; } return message; }); const provider = PROVIDER_LIST.find((p) => p.name === currentProvider) || DEFAULT_PROVIDER; const staticModels = LLMManager.getInstance().getStaticModelListFromProvider(provider); let modelDetails = staticModels.find((m) => m.name === currentModel); if (!modelDetails) { const modelsList = [ ...(provider.staticModels || []), ...(await LLMManager.getInstance().getModelListFromProvider(provider, { apiKeys, providerSettings, serverEnv: serverEnv as any, })), ]; if (!modelsList.length) { throw new Error(`No models found for provider ${provider.name}`); } modelDetails = modelsList.find((m) => m.name === currentModel); if (!modelDetails) { // Fallback to first model logger.warn( `MODEL [${currentModel}] not found in provider [${provider.name}]. Falling back to first model. ${modelsList[0].name}`, ); modelDetails = modelsList[0]; } } const { codeContext } = extractCurrentContext(processedMessages); let filePaths = getFilePaths(files || {}); filePaths = filePaths.filter((x) => { const relPath = x.replace('/home/project/', ''); return !ig.ignores(relPath); }); let context = ''; const currrentFiles: string[] = []; const contextFiles: FileMap = {}; if (codeContext?.type === 'codeContext') { const codeContextFiles: string[] = codeContext.files; Object.keys(files || {}).forEach((path) => { let relativePath = path; if (path.startsWith('/home/project/')) { relativePath = path.replace('/home/project/', ''); } if (codeContextFiles.includes(relativePath)) { contextFiles[relativePath] = files[path]; currrentFiles.push(relativePath); } }); context = createFilesContext(contextFiles); } const summaryText = `Here is the summary of the chat till now: ${summary}`; const extractTextContent = (message: Message) => Array.isArray(message.content) ? (message.content.find((item) => item.type === 'text')?.text as string) || '' : message.content; const lastUserMessage = processedMessages.filter((x) => x.role == 'user').pop(); if (!lastUserMessage) { throw new Error('No user message found'); } // select files from the list of code file from the project that might be useful for the current request from the user const resp = await generateText({ system: ` You are a software engineer. You are working on a project. You have access to the following files: AVAILABLE FILES PATHS --- ${filePaths.map((path) => `- ${path}`).join('\n')} --- You have following code loaded in the context buffer that you can refer to: CURRENT CONTEXT BUFFER --- ${context} --- Now, you are given a task. You need to select the files that are relevant to the task from the list of files above. RESPONSE FORMAT: your response shoudl be in following format: --- --- * Your should start with and end with . * You can include multiple and tags in the response. * You should not include any other text in the response. * You should not include any file that is not in the list of files above. * You should not include any file that is already in the context buffer. * If no changes are needed, you can leave the response empty updateContextBuffer tag. `, prompt: ` ${summaryText} Users Question: ${extractTextContent(lastUserMessage)} update the context buffer with the files that are relevant to the task from the list of files above. CRITICAL RULES: * Only include relevant files in the context buffer. * context buffer should not include any file that is not in the list of files above. * context buffer is extremlly expensive, so only include files that are absolutely necessary. * If no changes are needed, you can leave the response empty updateContextBuffer tag. * Only 5 files can be placed in the context buffer at a time. * if the buffer is full, you need to exclude files that is not needed and include files that is relevent. `, model: provider.getModelInstance({ model: currentModel, serverEnv, apiKeys, providerSettings, }), }); const response = resp.text; const updateContextBuffer = response.match(/([\s\S]*?)<\/updateContextBuffer>/); if (!updateContextBuffer) { throw new Error('Invalid response. Please follow the response format'); } const includeFiles = updateContextBuffer[1] .match(/ x.replace(' x.replace(' { delete contextFiles[path]; }); includeFiles.forEach((path) => { let fullPath = path; if (!path.startsWith('/home/project/')) { fullPath = `/home/project/${path}`; } if (!filePaths.includes(fullPath)) { throw new Error(`File ${path} is not in the list of files above.`); } if (currrentFiles.includes(path)) { return; } filteredFiles[path] = files[fullPath]; }); if (onFinish) { onFinish(resp); } return filteredFiles; // generateText({ } export function getFilePaths(files: FileMap) { let filePaths = Object.keys(files); filePaths = filePaths.filter((x) => { const relPath = x.replace('/home/project/', ''); return !ig.ignores(relPath); }); return filePaths; }