Spaces:
Running
Running
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<string, string>; | |
files: FileMap; | |
providerSettings?: Record<string, IProviderSetting>; | |
promptId?: string; | |
contextOptimization?: boolean; | |
summary: string; | |
onFinish?: (resp: GenerateTextResult<Record<string, CoreTool<any, any>>, never>) => void; | |
}) { | |
const { messages, env: serverEnv, apiKeys, files, providerSettings, 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; | |
content = simplifyBoltActions(content); | |
content = content.replace(/<div class=\\"__boltThought__\\">.*?<\/div>/s, ''); | |
content = content.replace(/<think>.*?<\/think>/s, ''); | |
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 should be in following format: | |
--- | |
<updateContextBuffer> | |
<includeFile path="path/to/file"/> | |
<excludeFile path="path/to/file"/> | |
</updateContextBuffer> | |
--- | |
* Your should start with <updateContextBuffer> and end with </updateContextBuffer>. | |
* You can include multiple <includeFile> and <excludeFile> 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(/<updateContextBuffer>([\s\S]*?)<\/updateContextBuffer>/); | |
if (!updateContextBuffer) { | |
throw new Error('Invalid response. Please follow the response format'); | |
} | |
const includeFiles = | |
updateContextBuffer[1] | |
.match(/<includeFile path="(.*?)"/gm) | |
?.map((x) => x.replace('<includeFile path="', '').replace('"', '')) || []; | |
const excludeFiles = | |
updateContextBuffer[1] | |
.match(/<excludeFile path="(.*?)"/gm) | |
?.map((x) => x.replace('<excludeFile path="', '').replace('"', '')) || []; | |
const filteredFiles: FileMap = {}; | |
excludeFiles.forEach((path) => { | |
delete contextFiles[path]; | |
}); | |
includeFiles.forEach((path) => { | |
let fullPath = path; | |
if (!path.startsWith('/home/project/')) { | |
fullPath = `/home/project/${path}`; | |
} | |
if (!filePaths.includes(fullPath)) { | |
logger.error(`File ${path} is not in the list of files above.`); | |
return; | |
// 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); | |
} | |
const totalFiles = Object.keys(filteredFiles).length; | |
logger.info(`Total files: ${totalFiles}`); | |
if (totalFiles == 0) { | |
throw new Error(`Bolt failed to select files`); | |
} | |
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; | |
} | |