Spaces:
Running
Running
import { convertToCoreMessages, streamText as _streamText, type CoreMessage } from 'ai'; | |
import fs from 'fs'; | |
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); | |
const CACHE_CONTROL_METADATA = { | |
experimental_providerMetadata: { | |
anthropic: { cacheControl: { type: 'ephemeral' } }, | |
}, | |
}; | |
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 persistMessages(messages: CoreMessage[]) { | |
try { | |
const messagesFilePath = 'messages.json'; | |
fs.writeFileSync(messagesFilePath, JSON.stringify(messages, null, 2), 'utf8'); | |
} catch (error) { | |
console.error('Error writing messages to file:', error); | |
} | |
} | |
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; | |
isPromptCachingEnabled?: boolean; | |
}) { | |
const { | |
messages, | |
env: serverEnv, | |
options, | |
apiKeys, | |
files, | |
providerSettings, | |
promptId, | |
contextOptimization, | |
isPromptCachingEnabled, | |
} = props; | |
let currentModel = DEFAULT_MODEL; | |
let currentProvider = DEFAULT_PROVIDER.name; | |
const processedMessages = messages.map((message, idx) => { | |
if (message.role === 'user') { | |
const { model, provider, content } = extractPropertiesFromMessage(message); | |
currentModel = model; | |
currentProvider = provider; | |
const putCacheControl = isPromptCachingEnabled && idx >= messages?.length - 4; | |
return { | |
...message, | |
content, | |
...(putCacheControl && CACHE_CONTROL_METADATA), | |
}; | |
} 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}`); | |
if (isPromptCachingEnabled) { | |
const messages = [ | |
{ | |
role: 'system', | |
content: systemPrompt, | |
experimental_providerMetadata: { | |
anthropic: { cacheControl: { type: 'ephemeral' } }, | |
}, | |
}, | |
...processedMessages, | |
] as CoreMessage[]; | |
persistMessages(messages); | |
return _streamText({ | |
model: provider.getModelInstance({ | |
model: currentModel, | |
serverEnv, | |
apiKeys, | |
providerSettings, | |
}), | |
maxTokens: dynamicMaxTokens, | |
messages, | |
...options, | |
}); | |
} | |
return _streamText({ | |
model: provider.getModelInstance({ | |
model: currentModel, | |
serverEnv, | |
apiKeys, | |
providerSettings, | |
}), | |
system: systemPrompt, | |
maxTokens: dynamicMaxTokens, | |
messages: convertToCoreMessages(processedMessages as any), | |
...options, | |
}); | |
} | |