mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-03-09 21:50:36 +00:00
* fix: enhance prompt fix * fix: added error capture on api error * fix: replaced error with log for wrong files selected by bolt
245 lines
7.9 KiB
TypeScript
245 lines
7.9 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, 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;
|
|
}
|