mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-01-23 19:27:04 +00:00
6494f5ac2e
* fix: updated logger and model caching * usage token stream issue fix * minor changes * updated starter template change to fix the app title * starter template bigfix * fixed hydretion errors and raw logs * removed raw log * made auto select template false by default * more cleaner logs and updated logic to call dynamicModels only if not found in static models * updated starter template instructions * browser console log improved for firefox * provider icons fix icons
242 lines
6.8 KiB
TypeScript
242 lines
6.8 KiB
TypeScript
import { convertToCoreMessages, streamText as _streamText } from 'ai';
|
|
import { MAX_TOKENS } from './constants';
|
|
import { getSystemPrompt } from '~/lib/common/prompts/prompts';
|
|
import {
|
|
DEFAULT_MODEL,
|
|
DEFAULT_PROVIDER,
|
|
MODEL_REGEX,
|
|
MODIFICATIONS_TAG_NAME,
|
|
PROVIDER_LIST,
|
|
PROVIDER_REGEX,
|
|
WORK_DIR,
|
|
} from '~/utils/constants';
|
|
import ignore from 'ignore';
|
|
import type { IProviderSetting } from '~/types/model';
|
|
import { PromptLibrary } from '~/lib/common/prompt-library';
|
|
import { allowedHTMLElements } from '~/utils/markdown';
|
|
import { LLMManager } from '~/lib/modules/llm/manager';
|
|
import { createScopedLogger } from '~/utils/logger';
|
|
|
|
interface ToolResult<Name extends string, Args, Result> {
|
|
toolCallId: string;
|
|
toolName: Name;
|
|
args: Args;
|
|
result: Result;
|
|
}
|
|
|
|
interface Message {
|
|
role: 'user' | 'assistant';
|
|
content: string;
|
|
toolInvocations?: ToolResult<string, unknown, unknown>[];
|
|
model?: string;
|
|
}
|
|
|
|
export type Messages = Message[];
|
|
|
|
export type StreamingOptions = Omit<Parameters<typeof _streamText>[0], 'model'>;
|
|
|
|
export interface File {
|
|
type: 'file';
|
|
content: string;
|
|
isBinary: boolean;
|
|
}
|
|
|
|
export interface Folder {
|
|
type: 'folder';
|
|
}
|
|
|
|
type Dirent = File | Folder;
|
|
|
|
export type FileMap = Record<string, Dirent | undefined>;
|
|
|
|
export function simplifyBoltActions(input: string): string {
|
|
// Using regex to match boltAction tags that have type="file"
|
|
const regex = /(<boltAction[^>]*type="file"[^>]*>)([\s\S]*?)(<\/boltAction>)/g;
|
|
|
|
// Replace each matching occurrence
|
|
return input.replace(regex, (_0, openingTag, _2, closingTag) => {
|
|
return `${openingTag}\n ...\n ${closingTag}`;
|
|
});
|
|
}
|
|
|
|
// Common patterns to ignore, similar to .gitignore
|
|
const IGNORE_PATTERNS = [
|
|
'node_modules/**',
|
|
'.git/**',
|
|
'dist/**',
|
|
'build/**',
|
|
'.next/**',
|
|
'coverage/**',
|
|
'.cache/**',
|
|
'.vscode/**',
|
|
'.idea/**',
|
|
'**/*.log',
|
|
'**/.DS_Store',
|
|
'**/npm-debug.log*',
|
|
'**/yarn-debug.log*',
|
|
'**/yarn-error.log*',
|
|
'**/*lock.json',
|
|
'**/*lock.yml',
|
|
];
|
|
const ig = ignore().add(IGNORE_PATTERNS);
|
|
|
|
function createFilesContext(files: FileMap) {
|
|
let filePaths = Object.keys(files);
|
|
filePaths = filePaths.filter((x) => {
|
|
const relPath = x.replace('/home/project/', '');
|
|
return !ig.ignores(relPath);
|
|
});
|
|
|
|
const fileContexts = filePaths
|
|
.filter((x) => files[x] && files[x].type == 'file')
|
|
.map((path) => {
|
|
const dirent = files[path];
|
|
|
|
if (!dirent || dirent.type == 'folder') {
|
|
return '';
|
|
}
|
|
|
|
const codeWithLinesNumbers = dirent.content
|
|
.split('\n')
|
|
.map((v, i) => `${i + 1}|${v}`)
|
|
.join('\n');
|
|
|
|
return `<file path="${path}">\n${codeWithLinesNumbers}\n</file>`;
|
|
});
|
|
|
|
return `Below are the code files present in the webcontainer:\ncode format:\n<line number>|<line content>\n <codebase>${fileContexts.join('\n\n')}\n\n</codebase>`;
|
|
}
|
|
|
|
function extractPropertiesFromMessage(message: Message): { model: string; provider: string; content: string } {
|
|
const textContent = Array.isArray(message.content)
|
|
? message.content.find((item) => item.type === 'text')?.text || ''
|
|
: message.content;
|
|
|
|
const modelMatch = textContent.match(MODEL_REGEX);
|
|
const providerMatch = textContent.match(PROVIDER_REGEX);
|
|
|
|
/*
|
|
* Extract model
|
|
* const modelMatch = message.content.match(MODEL_REGEX);
|
|
*/
|
|
const model = modelMatch ? modelMatch[1] : DEFAULT_MODEL;
|
|
|
|
/*
|
|
* Extract provider
|
|
* const providerMatch = message.content.match(PROVIDER_REGEX);
|
|
*/
|
|
const provider = providerMatch ? providerMatch[1] : DEFAULT_PROVIDER.name;
|
|
|
|
const cleanedContent = Array.isArray(message.content)
|
|
? message.content.map((item) => {
|
|
if (item.type === 'text') {
|
|
return {
|
|
type: 'text',
|
|
text: item.text?.replace(MODEL_REGEX, '').replace(PROVIDER_REGEX, ''),
|
|
};
|
|
}
|
|
|
|
return item; // Preserve image_url and other types as is
|
|
})
|
|
: textContent.replace(MODEL_REGEX, '').replace(PROVIDER_REGEX, '');
|
|
|
|
return { model, provider, content: cleanedContent };
|
|
}
|
|
|
|
const logger = createScopedLogger('stream-text');
|
|
|
|
export async function streamText(props: {
|
|
messages: Messages;
|
|
env: Env;
|
|
options?: StreamingOptions;
|
|
apiKeys?: Record<string, string>;
|
|
files?: FileMap;
|
|
providerSettings?: Record<string, IProviderSetting>;
|
|
promptId?: string;
|
|
contextOptimization?: boolean;
|
|
}) {
|
|
const { messages, env: serverEnv, options, apiKeys, files, providerSettings, promptId, contextOptimization } = props;
|
|
|
|
// console.log({serverEnv});
|
|
|
|
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 dynamicMaxTokens = modelDetails && modelDetails.maxTokenAllowed ? modelDetails.maxTokenAllowed : MAX_TOKENS;
|
|
|
|
let systemPrompt =
|
|
PromptLibrary.getPropmtFromLibrary(promptId || 'default', {
|
|
cwd: WORK_DIR,
|
|
allowedHtmlElements: allowedHTMLElements,
|
|
modificationTagName: MODIFICATIONS_TAG_NAME,
|
|
}) ?? getSystemPrompt();
|
|
|
|
if (files && contextOptimization) {
|
|
const codeContext = createFilesContext(files);
|
|
systemPrompt = `${systemPrompt}\n\n ${codeContext}`;
|
|
}
|
|
|
|
logger.info(`Sending llm call to ${provider.name} with model ${modelDetails.name}`);
|
|
|
|
return _streamText({
|
|
model: provider.getModelInstance({
|
|
model: currentModel,
|
|
serverEnv,
|
|
apiKeys,
|
|
providerSettings,
|
|
}),
|
|
system: systemPrompt,
|
|
maxTokens: dynamicMaxTokens,
|
|
messages: convertToCoreMessages(processedMessages as any),
|
|
...options,
|
|
});
|
|
}
|