Merge branch 'main' into code-streaming
This commit is contained in:
@@ -2,12 +2,18 @@
|
||||
// Preventing TS checks with files presented in the video for a better presentation.
|
||||
import { env } from 'node:process';
|
||||
|
||||
export function getAPIKey(cloudflareEnv: Env, provider: string) {
|
||||
export function getAPIKey(cloudflareEnv: Env, provider: string, userApiKeys?: Record<string, string>) {
|
||||
/**
|
||||
* The `cloudflareEnv` is only used when deployed or when previewing locally.
|
||||
* In development the environment variables are available through `env`.
|
||||
*/
|
||||
|
||||
// First check user-provided API keys
|
||||
if (userApiKeys?.[provider]) {
|
||||
return userApiKeys[provider];
|
||||
}
|
||||
|
||||
// Fall back to environment variables
|
||||
switch (provider) {
|
||||
case 'Anthropic':
|
||||
return env.ANTHROPIC_API_KEY || cloudflareEnv.ANTHROPIC_API_KEY;
|
||||
@@ -36,6 +42,8 @@ export function getBaseURL(cloudflareEnv: Env, provider: string) {
|
||||
switch (provider) {
|
||||
case 'OpenAILike':
|
||||
return env.OPENAI_LIKE_API_BASE_URL || cloudflareEnv.OPENAI_LIKE_API_BASE_URL;
|
||||
case 'LMStudio':
|
||||
return env.LMSTUDIO_API_BASE_URL || cloudflareEnv.LMSTUDIO_API_BASE_URL || "http://localhost:1234";
|
||||
case 'Ollama':
|
||||
let baseUrl = env.OLLAMA_API_BASE_URL || cloudflareEnv.OLLAMA_API_BASE_URL || "http://localhost:11434";
|
||||
if (env.RUNNING_IN_DOCKER === 'true') {
|
||||
|
||||
@@ -83,6 +83,15 @@ export function getOpenRouterModel(apiKey: string, model: string) {
|
||||
return openRouter.chat(model);
|
||||
}
|
||||
|
||||
export function getLMStudioModel(baseURL: string, model: string) {
|
||||
const lmstudio = createOpenAI({
|
||||
baseUrl: `${baseURL}/v1`,
|
||||
apiKey: "",
|
||||
});
|
||||
|
||||
return lmstudio(model);
|
||||
}
|
||||
|
||||
export function getXAIModel(apiKey: string, model: string) {
|
||||
const openai = createOpenAI({
|
||||
baseURL: 'https://api.x.ai/v1',
|
||||
@@ -91,9 +100,8 @@ export function getXAIModel(apiKey: string, model: string) {
|
||||
|
||||
return openai(model);
|
||||
}
|
||||
|
||||
export function getModel(provider: string, model: string, env: Env) {
|
||||
const apiKey = getAPIKey(env, provider);
|
||||
export function getModel(provider: string, model: string, env: Env, apiKeys?: Record<string, string>) {
|
||||
const apiKey = getAPIKey(env, provider, apiKeys);
|
||||
const baseURL = getBaseURL(env, provider);
|
||||
|
||||
switch (provider) {
|
||||
@@ -106,13 +114,15 @@ export function getModel(provider: string, model: string, env: Env) {
|
||||
case 'OpenRouter':
|
||||
return getOpenRouterModel(apiKey, model);
|
||||
case 'Google':
|
||||
return getGoogleModel(apiKey, model)
|
||||
return getGoogleModel(apiKey, model);
|
||||
case 'OpenAILike':
|
||||
return getOpenAILikeModel(baseURL,apiKey, model);
|
||||
case 'Deepseek':
|
||||
return getDeepseekModel(apiKey, model)
|
||||
return getDeepseekModel(apiKey, model);
|
||||
case 'Mistral':
|
||||
return getMistralModel(apiKey, model);
|
||||
case 'LMStudio':
|
||||
return getLMStudioModel(baseURL, model);
|
||||
case 'xAI':
|
||||
return getXAIModel(apiKey, model);
|
||||
default:
|
||||
|
||||
@@ -174,10 +174,16 @@ You are Bolt, an expert AI assistant and exceptional senior software developer w
|
||||
|
||||
- When Using \`npx\`, ALWAYS provide the \`--yes\` flag.
|
||||
- When running multiple shell commands, use \`&&\` to run them sequentially.
|
||||
- ULTRA IMPORTANT: Do NOT re-run a dev command if there is one that starts a dev server and new dependencies were installed or files updated! If a dev server has started already, assume that installing dependencies will be executed in a different process and will be picked up by the dev server.
|
||||
- ULTRA IMPORTANT: Do NOT re-run a dev command with shell action use dev action to run dev commands
|
||||
|
||||
- file: For writing new files or updating existing files. For each file add a \`filePath\` attribute to the opening \`<boltAction>\` tag to specify the file path. The content of the file artifact is the file contents. All file paths MUST BE relative to the current working directory.
|
||||
|
||||
- start: For starting development server.
|
||||
- Use to start application if not already started or NEW dependencies added
|
||||
- Only use this action when you need to run a dev server or start the application
|
||||
- ULTRA IMORTANT: do NOT re-run a dev server if files updated, existing dev server can autometically detect changes and executes the file changes
|
||||
|
||||
|
||||
9. The order of the actions is VERY IMPORTANT. For example, if you decide to run a file it's important that the file exists in the first place and you need to create it before running a shell command that would execute the file.
|
||||
|
||||
10. ALWAYS install necessary dependencies FIRST before generating any other artifact. If that requires a \`package.json\` then you should create that first!
|
||||
@@ -265,7 +271,7 @@ Here are some examples of correct usage of artifacts:
|
||||
...
|
||||
</boltAction>
|
||||
|
||||
<boltAction type="shell">
|
||||
<boltAction type="start">
|
||||
npm run dev
|
||||
</boltAction>
|
||||
</boltArtifact>
|
||||
@@ -322,7 +328,7 @@ Here are some examples of correct usage of artifacts:
|
||||
...
|
||||
</boltAction>
|
||||
|
||||
<boltAction type="shell">
|
||||
<boltAction type="start">
|
||||
npm run dev
|
||||
</boltAction>
|
||||
</boltArtifact>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { streamText as _streamText, convertToCoreMessages } from 'ai';
|
||||
import { getModel } from '~/lib/.server/llm/model';
|
||||
import { MAX_TOKENS } from './constants';
|
||||
import { getSystemPrompt } from './prompts';
|
||||
import { MODEL_LIST, DEFAULT_MODEL, DEFAULT_PROVIDER } from '~/utils/constants';
|
||||
import { MODEL_LIST, DEFAULT_MODEL, DEFAULT_PROVIDER, MODEL_REGEX, PROVIDER_REGEX } from '~/utils/constants';
|
||||
|
||||
interface ToolResult<Name extends string, Args, Result> {
|
||||
toolCallId: string;
|
||||
@@ -24,42 +24,53 @@ export type Messages = Message[];
|
||||
|
||||
export type StreamingOptions = Omit<Parameters<typeof _streamText>[0], 'model'>;
|
||||
|
||||
function extractModelFromMessage(message: Message): { model: string; content: string } {
|
||||
const modelRegex = /^\[Model: (.*?)\]\n\n/;
|
||||
const match = message.content.match(modelRegex);
|
||||
function extractPropertiesFromMessage(message: Message): { model: string; provider: string; content: string } {
|
||||
// Extract model
|
||||
const modelMatch = message.content.match(MODEL_REGEX);
|
||||
const model = modelMatch ? modelMatch[1] : DEFAULT_MODEL;
|
||||
|
||||
if (match) {
|
||||
const model = match[1];
|
||||
const content = message.content.replace(modelRegex, '');
|
||||
return { model, content };
|
||||
}
|
||||
// Extract provider
|
||||
const providerMatch = message.content.match(PROVIDER_REGEX);
|
||||
const provider = providerMatch ? providerMatch[1] : DEFAULT_PROVIDER;
|
||||
|
||||
// Default model if not specified
|
||||
return { model: DEFAULT_MODEL, content: message.content };
|
||||
// Remove model and provider lines from content
|
||||
const cleanedContent = message.content
|
||||
.replace(MODEL_REGEX, '')
|
||||
.replace(PROVIDER_REGEX, '')
|
||||
.trim();
|
||||
|
||||
return { model, provider, content: cleanedContent };
|
||||
}
|
||||
|
||||
export function streamText(messages: Messages, env: Env, options?: StreamingOptions) {
|
||||
export function streamText(
|
||||
messages: Messages,
|
||||
env: Env,
|
||||
options?: StreamingOptions,
|
||||
apiKeys?: Record<string, string>
|
||||
) {
|
||||
let currentModel = DEFAULT_MODEL;
|
||||
let currentProvider = DEFAULT_PROVIDER;
|
||||
|
||||
const processedMessages = messages.map((message) => {
|
||||
if (message.role === 'user') {
|
||||
const { model, content } = extractModelFromMessage(message);
|
||||
if (model && MODEL_LIST.find((m) => m.name === model)) {
|
||||
currentModel = model; // Update the current model
|
||||
const { model, provider, content } = extractPropertiesFromMessage(message);
|
||||
|
||||
if (MODEL_LIST.find((m) => m.name === model)) {
|
||||
currentModel = model;
|
||||
}
|
||||
|
||||
currentProvider = provider;
|
||||
|
||||
return { ...message, content };
|
||||
}
|
||||
return message;
|
||||
|
||||
return message; // No changes for non-user messages
|
||||
});
|
||||
|
||||
const provider = MODEL_LIST.find((model) => model.name === currentModel)?.provider || DEFAULT_PROVIDER;
|
||||
|
||||
return _streamText({
|
||||
model: getModel(provider, currentModel, env),
|
||||
model: getModel(currentProvider, currentModel, env, apiKeys),
|
||||
system: getSystemPrompt(),
|
||||
maxTokens: MAX_TOKENS,
|
||||
// headers: {
|
||||
// 'anthropic-beta': 'max-tokens-3-5-sonnet-2024-07-15',
|
||||
// },
|
||||
messages: convertToCoreMessages(processedMessages),
|
||||
...options,
|
||||
});
|
||||
|
||||
@@ -12,41 +12,55 @@ export function usePromptEnhancer() {
|
||||
setPromptEnhanced(false);
|
||||
};
|
||||
|
||||
const enhancePrompt = async (input: string, setInput: (value: string) => void) => {
|
||||
const enhancePrompt = async (
|
||||
input: string,
|
||||
setInput: (value: string) => void,
|
||||
model: string,
|
||||
provider: string,
|
||||
apiKeys?: Record<string, string>
|
||||
) => {
|
||||
setEnhancingPrompt(true);
|
||||
setPromptEnhanced(false);
|
||||
|
||||
|
||||
const requestBody: any = {
|
||||
message: input,
|
||||
model,
|
||||
provider,
|
||||
};
|
||||
|
||||
if (apiKeys) {
|
||||
requestBody.apiKeys = apiKeys;
|
||||
}
|
||||
|
||||
const response = await fetch('/api/enhancer', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
message: input,
|
||||
}),
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
|
||||
|
||||
const originalInput = input;
|
||||
|
||||
|
||||
if (reader) {
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
|
||||
let _input = '';
|
||||
let _error;
|
||||
|
||||
|
||||
try {
|
||||
setInput('');
|
||||
|
||||
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
|
||||
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
_input += decoder.decode(value);
|
||||
|
||||
|
||||
logger.trace('Set input', _input);
|
||||
|
||||
|
||||
setInput(_input);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -56,10 +70,10 @@ export function usePromptEnhancer() {
|
||||
if (_error) {
|
||||
logger.error(_error);
|
||||
}
|
||||
|
||||
|
||||
setEnhancingPrompt(false);
|
||||
setPromptEnhanced(true);
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
setInput(_input);
|
||||
});
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { WebContainer } from '@webcontainer/api';
|
||||
import { map, type MapStore } from 'nanostores';
|
||||
import { WebContainer, type WebContainerProcess } from '@webcontainer/api';
|
||||
import { atom, map, type MapStore } from 'nanostores';
|
||||
import * as nodePath from 'node:path';
|
||||
import type { BoltAction } from '~/types/actions';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
import { unreachable } from '~/utils/unreachable';
|
||||
import type { ActionCallbackData } from './message-parser';
|
||||
import type { ITerminal } from '~/types/terminal';
|
||||
import type { BoltShell } from '~/utils/shell';
|
||||
|
||||
const logger = createScopedLogger('ActionRunner');
|
||||
|
||||
@@ -36,11 +38,14 @@ type ActionsMap = MapStore<Record<string, ActionState>>;
|
||||
export class ActionRunner {
|
||||
#webcontainer: Promise<WebContainer>;
|
||||
#currentExecutionPromise: Promise<void> = Promise.resolve();
|
||||
|
||||
#shellTerminal: () => BoltShell;
|
||||
runnerId = atom<string>(`${Date.now()}`);
|
||||
actions: ActionsMap = map({});
|
||||
|
||||
constructor(webcontainerPromise: Promise<WebContainer>) {
|
||||
constructor(webcontainerPromise: Promise<WebContainer>, getShellTerminal: () => BoltShell) {
|
||||
this.#webcontainer = webcontainerPromise;
|
||||
this.#shellTerminal = getShellTerminal;
|
||||
|
||||
}
|
||||
|
||||
addAction(data: ActionCallbackData) {
|
||||
@@ -113,11 +118,16 @@ export class ActionRunner {
|
||||
await this.#runFileAction(action);
|
||||
break;
|
||||
}
|
||||
case 'start': {
|
||||
await this.#runStartAction(action)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.#updateAction(actionId, { status: isStreaming ? 'running' : action.abortSignal.aborted ? 'aborted' : 'complete' });
|
||||
} catch (error) {
|
||||
this.#updateAction(actionId, { status: 'failed', error: 'Action failed' });
|
||||
logger.error(`[${action.type}]:Action failed\n\n`, error);
|
||||
|
||||
// re-throw the error to be caught in the promise chain
|
||||
throw error;
|
||||
@@ -128,28 +138,38 @@ export class ActionRunner {
|
||||
if (action.type !== 'shell') {
|
||||
unreachable('Expected shell action');
|
||||
}
|
||||
const shell = this.#shellTerminal()
|
||||
await shell.ready()
|
||||
if (!shell || !shell.terminal || !shell.process) {
|
||||
unreachable('Shell terminal not found');
|
||||
}
|
||||
const resp = await shell.executeCommand(this.runnerId.get(), action.content)
|
||||
logger.debug(`${action.type} Shell Response: [exit code:${resp?.exitCode}]`)
|
||||
if (resp?.exitCode != 0) {
|
||||
throw new Error("Failed To Execute Shell Command");
|
||||
|
||||
const webcontainer = await this.#webcontainer;
|
||||
}
|
||||
}
|
||||
|
||||
const process = await webcontainer.spawn('jsh', ['-c', action.content], {
|
||||
env: { npm_config_yes: true },
|
||||
});
|
||||
async #runStartAction(action: ActionState) {
|
||||
if (action.type !== 'start') {
|
||||
unreachable('Expected shell action');
|
||||
}
|
||||
if (!this.#shellTerminal) {
|
||||
unreachable('Shell terminal not found');
|
||||
}
|
||||
const shell = this.#shellTerminal()
|
||||
await shell.ready()
|
||||
if (!shell || !shell.terminal || !shell.process) {
|
||||
unreachable('Shell terminal not found');
|
||||
}
|
||||
const resp = await shell.executeCommand(this.runnerId.get(), action.content)
|
||||
logger.debug(`${action.type} Shell Response: [exit code:${resp?.exitCode}]`)
|
||||
|
||||
action.abortSignal.addEventListener('abort', () => {
|
||||
process.kill();
|
||||
});
|
||||
|
||||
process.output.pipeTo(
|
||||
new WritableStream({
|
||||
write(data) {
|
||||
console.log(data);
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const exitCode = await process.exit;
|
||||
|
||||
logger.debug(`Process terminated with code ${exitCode}`);
|
||||
if (resp?.exitCode != 0) {
|
||||
throw new Error("Failed To Start Application");
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
async #runFileAction(action: ActionState) {
|
||||
@@ -180,7 +200,6 @@ export class ActionRunner {
|
||||
logger.error('Failed to write file\n\n', error);
|
||||
}
|
||||
}
|
||||
|
||||
#updateAction(id: string, newState: ActionStateUpdate) {
|
||||
const actions = this.actions.get();
|
||||
|
||||
|
||||
@@ -272,7 +272,7 @@ export class StreamingMessageParser {
|
||||
}
|
||||
|
||||
(actionAttributes as FileAction).filePath = filePath;
|
||||
} else if (actionType !== 'shell') {
|
||||
} else if (!(['shell', 'start'].includes(actionType))) {
|
||||
logger.warn(`Unknown action type '${actionType}'`);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import type { WebContainer, WebContainerProcess } from '@webcontainer/api';
|
||||
import { atom, type WritableAtom } from 'nanostores';
|
||||
import type { ITerminal } from '~/types/terminal';
|
||||
import { newShellProcess } from '~/utils/shell';
|
||||
import { newBoltShellProcess, newShellProcess } from '~/utils/shell';
|
||||
import { coloredText } from '~/utils/terminal';
|
||||
|
||||
export class TerminalStore {
|
||||
#webcontainer: Promise<WebContainer>;
|
||||
#terminals: Array<{ terminal: ITerminal; process: WebContainerProcess }> = [];
|
||||
#boltTerminal = newBoltShellProcess()
|
||||
|
||||
showTerminal: WritableAtom<boolean> = import.meta.hot?.data.showTerminal ?? atom(false);
|
||||
showTerminal: WritableAtom<boolean> = import.meta.hot?.data.showTerminal ?? atom(true);
|
||||
|
||||
constructor(webcontainerPromise: Promise<WebContainer>) {
|
||||
this.#webcontainer = webcontainerPromise;
|
||||
@@ -17,10 +18,22 @@ export class TerminalStore {
|
||||
import.meta.hot.data.showTerminal = this.showTerminal;
|
||||
}
|
||||
}
|
||||
get boltTerminal() {
|
||||
return this.#boltTerminal;
|
||||
}
|
||||
|
||||
toggleTerminal(value?: boolean) {
|
||||
this.showTerminal.set(value !== undefined ? value : !this.showTerminal.get());
|
||||
}
|
||||
async attachBoltTerminal(terminal: ITerminal) {
|
||||
try {
|
||||
let wc = await this.#webcontainer
|
||||
await this.#boltTerminal.init(wc, terminal)
|
||||
} catch (error: any) {
|
||||
terminal.write(coloredText.red('Failed to spawn bolt shell\n\n') + error.message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async attachTerminal(terminal: ITerminal) {
|
||||
try {
|
||||
|
||||
@@ -13,6 +13,7 @@ import JSZip from 'jszip';
|
||||
import { saveAs } from 'file-saver';
|
||||
import { Octokit } from "@octokit/rest";
|
||||
import * as nodePath from 'node:path';
|
||||
import type { WebContainerProcess } from '@webcontainer/api';
|
||||
|
||||
export interface ArtifactState {
|
||||
id: string;
|
||||
@@ -40,6 +41,7 @@ export class WorkbenchStore {
|
||||
unsavedFiles: WritableAtom<Set<string>> = import.meta.hot?.data.unsavedFiles ?? atom(new Set<string>());
|
||||
modifiedFiles = new Set<string>();
|
||||
artifactIdList: string[] = [];
|
||||
#boltTerminal: { terminal: ITerminal; process: WebContainerProcess } | undefined;
|
||||
|
||||
constructor() {
|
||||
if (import.meta.hot) {
|
||||
@@ -77,6 +79,9 @@ export class WorkbenchStore {
|
||||
get showTerminal() {
|
||||
return this.#terminalStore.showTerminal;
|
||||
}
|
||||
get boltTerminal() {
|
||||
return this.#terminalStore.boltTerminal;
|
||||
}
|
||||
|
||||
toggleTerminal(value?: boolean) {
|
||||
this.#terminalStore.toggleTerminal(value);
|
||||
@@ -85,6 +90,10 @@ export class WorkbenchStore {
|
||||
attachTerminal(terminal: ITerminal) {
|
||||
this.#terminalStore.attachTerminal(terminal);
|
||||
}
|
||||
attachBoltTerminal(terminal: ITerminal) {
|
||||
|
||||
this.#terminalStore.attachBoltTerminal(terminal);
|
||||
}
|
||||
|
||||
onTerminalResize(cols: number, rows: number) {
|
||||
this.#terminalStore.onTerminalResize(cols, rows);
|
||||
@@ -233,7 +242,7 @@ export class WorkbenchStore {
|
||||
id,
|
||||
title,
|
||||
closed: false,
|
||||
runner: new ActionRunner(webcontainer),
|
||||
runner: new ActionRunner(webcontainer, () => this.boltTerminal),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user