bolt.diy/app/lib/.server/llm/select-context.ts
2025-01-28 22:57:06 +01:00

234 lines
7.6 KiB
TypeScript

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, 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 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)) {
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;
}