Merge branch 'main' into feat/image-select-merge
Browse files- app/commit.json +1 -1
- app/components/chat/Chat.client.tsx +2 -0
- app/lib/.server/llm/stream-text.ts +90 -2
- app/lib/hooks/useMessageParser.ts +2 -2
- app/lib/stores/workbench.ts +8 -2
- app/routes/api.chat.ts +18 -4
- app/utils/constants.ts +0 -1
app/commit.json
CHANGED
|
@@ -1 +1 @@
|
|
| 1 |
-
{ "commit": "
|
|
|
|
| 1 |
+
{ "commit": "4cfabd94ee8ab91a1466cf644dbf9c74ab1324d7" }
|
app/components/chat/Chat.client.tsx
CHANGED
|
@@ -92,6 +92,7 @@ export const ChatImpl = memo(
|
|
| 92 |
const [chatStarted, setChatStarted] = useState(initialMessages.length > 0);
|
| 93 |
const [uploadedFiles, setUploadedFiles] = useState<File[]>([]); // Move here
|
| 94 |
const [imageDataList, setImageDataList] = useState<string[]>([]); // Move here
|
|
|
|
| 95 |
const { activeProviders } = useSettings();
|
| 96 |
|
| 97 |
const [model, setModel] = useState(() => {
|
|
@@ -113,6 +114,7 @@ export const ChatImpl = memo(
|
|
| 113 |
api: '/api/chat',
|
| 114 |
body: {
|
| 115 |
apiKeys,
|
|
|
|
| 116 |
},
|
| 117 |
onError: (error) => {
|
| 118 |
logger.error('Request failed\n\n', error);
|
|
|
|
| 92 |
const [chatStarted, setChatStarted] = useState(initialMessages.length > 0);
|
| 93 |
const [uploadedFiles, setUploadedFiles] = useState<File[]>([]); // Move here
|
| 94 |
const [imageDataList, setImageDataList] = useState<string[]>([]); // Move here
|
| 95 |
+
const files = useStore(workbenchStore.files);
|
| 96 |
const { activeProviders } = useSettings();
|
| 97 |
|
| 98 |
const [model, setModel] = useState(() => {
|
|
|
|
| 114 |
api: '/api/chat',
|
| 115 |
body: {
|
| 116 |
apiKeys,
|
| 117 |
+
files,
|
| 118 |
},
|
| 119 |
onError: (error) => {
|
| 120 |
logger.error('Request failed\n\n', error);
|
app/lib/.server/llm/stream-text.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { getModel } from '~/lib/.server/llm/model';
|
|
| 3 |
import { MAX_TOKENS } from './constants';
|
| 4 |
import { getSystemPrompt } from './prompts';
|
| 5 |
import { DEFAULT_MODEL, DEFAULT_PROVIDER, getModelList, MODEL_REGEX, PROVIDER_REGEX } from '~/utils/constants';
|
|
|
|
| 6 |
import type { IProviderSetting } from '~/types/model';
|
| 7 |
|
| 8 |
interface ToolResult<Name extends string, Args, Result> {
|
|
@@ -23,6 +24,78 @@ export type Messages = Message[];
|
|
| 23 |
|
| 24 |
export type StreamingOptions = Omit<Parameters<typeof _streamText>[0], 'model'>;
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
function extractPropertiesFromMessage(message: Message): { model: string; provider: string; content: string } {
|
| 27 |
const textContent = Array.isArray(message.content)
|
| 28 |
? message.content.find((item) => item.type === 'text')?.text || ''
|
|
@@ -64,9 +137,10 @@ export async function streamText(props: {
|
|
| 64 |
env: Env;
|
| 65 |
options?: StreamingOptions;
|
| 66 |
apiKeys?: Record<string, string>;
|
|
|
|
| 67 |
providerSettings?: Record<string, IProviderSetting>;
|
| 68 |
}) {
|
| 69 |
-
const { messages, env, options, apiKeys, providerSettings } = props;
|
| 70 |
let currentModel = DEFAULT_MODEL;
|
| 71 |
let currentProvider = DEFAULT_PROVIDER.name;
|
| 72 |
const MODEL_LIST = await getModelList(apiKeys || {}, providerSettings);
|
|
@@ -80,6 +154,12 @@ export async function streamText(props: {
|
|
| 80 |
|
| 81 |
currentProvider = provider;
|
| 82 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
return { ...message, content };
|
| 84 |
}
|
| 85 |
|
|
@@ -90,9 +170,17 @@ export async function streamText(props: {
|
|
| 90 |
|
| 91 |
const dynamicMaxTokens = modelDetails && modelDetails.maxTokenAllowed ? modelDetails.maxTokenAllowed : MAX_TOKENS;
|
| 92 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
return _streamText({
|
| 94 |
model: getModel(currentProvider, currentModel, env, apiKeys, providerSettings) as any,
|
| 95 |
-
system:
|
| 96 |
maxTokens: dynamicMaxTokens,
|
| 97 |
messages: convertToCoreMessages(processedMessages as any),
|
| 98 |
...options,
|
|
|
|
| 3 |
import { MAX_TOKENS } from './constants';
|
| 4 |
import { getSystemPrompt } from './prompts';
|
| 5 |
import { DEFAULT_MODEL, DEFAULT_PROVIDER, getModelList, MODEL_REGEX, PROVIDER_REGEX } from '~/utils/constants';
|
| 6 |
+
import ignore from 'ignore';
|
| 7 |
import type { IProviderSetting } from '~/types/model';
|
| 8 |
|
| 9 |
interface ToolResult<Name extends string, Args, Result> {
|
|
|
|
| 24 |
|
| 25 |
export type StreamingOptions = Omit<Parameters<typeof _streamText>[0], 'model'>;
|
| 26 |
|
| 27 |
+
export interface File {
|
| 28 |
+
type: 'file';
|
| 29 |
+
content: string;
|
| 30 |
+
isBinary: boolean;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
export interface Folder {
|
| 34 |
+
type: 'folder';
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
type Dirent = File | Folder;
|
| 38 |
+
|
| 39 |
+
export type FileMap = Record<string, Dirent | undefined>;
|
| 40 |
+
|
| 41 |
+
export function simplifyBoltActions(input: string): string {
|
| 42 |
+
// Using regex to match boltAction tags that have type="file"
|
| 43 |
+
const regex = /(<boltAction[^>]*type="file"[^>]*>)([\s\S]*?)(<\/boltAction>)/g;
|
| 44 |
+
|
| 45 |
+
// Replace each matching occurrence
|
| 46 |
+
return input.replace(regex, (_0, openingTag, _2, closingTag) => {
|
| 47 |
+
return `${openingTag}\n ...\n ${closingTag}`;
|
| 48 |
+
});
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
// Common patterns to ignore, similar to .gitignore
|
| 52 |
+
const IGNORE_PATTERNS = [
|
| 53 |
+
'node_modules/**',
|
| 54 |
+
'.git/**',
|
| 55 |
+
'dist/**',
|
| 56 |
+
'build/**',
|
| 57 |
+
'.next/**',
|
| 58 |
+
'coverage/**',
|
| 59 |
+
'.cache/**',
|
| 60 |
+
'.vscode/**',
|
| 61 |
+
'.idea/**',
|
| 62 |
+
'**/*.log',
|
| 63 |
+
'**/.DS_Store',
|
| 64 |
+
'**/npm-debug.log*',
|
| 65 |
+
'**/yarn-debug.log*',
|
| 66 |
+
'**/yarn-error.log*',
|
| 67 |
+
'**/*lock.json',
|
| 68 |
+
'**/*lock.yml',
|
| 69 |
+
];
|
| 70 |
+
const ig = ignore().add(IGNORE_PATTERNS);
|
| 71 |
+
|
| 72 |
+
function createFilesContext(files: FileMap) {
|
| 73 |
+
let filePaths = Object.keys(files);
|
| 74 |
+
filePaths = filePaths.filter((x) => {
|
| 75 |
+
const relPath = x.replace('/home/project/', '');
|
| 76 |
+
return !ig.ignores(relPath);
|
| 77 |
+
});
|
| 78 |
+
|
| 79 |
+
const fileContexts = filePaths
|
| 80 |
+
.filter((x) => files[x] && files[x].type == 'file')
|
| 81 |
+
.map((path) => {
|
| 82 |
+
const dirent = files[path];
|
| 83 |
+
|
| 84 |
+
if (!dirent || dirent.type == 'folder') {
|
| 85 |
+
return '';
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
const codeWithLinesNumbers = dirent.content
|
| 89 |
+
.split('\n')
|
| 90 |
+
.map((v, i) => `${i + 1}|${v}`)
|
| 91 |
+
.join('\n');
|
| 92 |
+
|
| 93 |
+
return `<file path="${path}">\n${codeWithLinesNumbers}\n</file>`;
|
| 94 |
+
});
|
| 95 |
+
|
| 96 |
+
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>`;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
function extractPropertiesFromMessage(message: Message): { model: string; provider: string; content: string } {
|
| 100 |
const textContent = Array.isArray(message.content)
|
| 101 |
? message.content.find((item) => item.type === 'text')?.text || ''
|
|
|
|
| 137 |
env: Env;
|
| 138 |
options?: StreamingOptions;
|
| 139 |
apiKeys?: Record<string, string>;
|
| 140 |
+
files?: FileMap;
|
| 141 |
providerSettings?: Record<string, IProviderSetting>;
|
| 142 |
}) {
|
| 143 |
+
const { messages, env, options, apiKeys, files, providerSettings } = props;
|
| 144 |
let currentModel = DEFAULT_MODEL;
|
| 145 |
let currentProvider = DEFAULT_PROVIDER.name;
|
| 146 |
const MODEL_LIST = await getModelList(apiKeys || {}, providerSettings);
|
|
|
|
| 154 |
|
| 155 |
currentProvider = provider;
|
| 156 |
|
| 157 |
+
return { ...message, content };
|
| 158 |
+
} else if (message.role == 'assistant') {
|
| 159 |
+
const content = message.content;
|
| 160 |
+
|
| 161 |
+
// content = simplifyBoltActions(content);
|
| 162 |
+
|
| 163 |
return { ...message, content };
|
| 164 |
}
|
| 165 |
|
|
|
|
| 170 |
|
| 171 |
const dynamicMaxTokens = modelDetails && modelDetails.maxTokenAllowed ? modelDetails.maxTokenAllowed : MAX_TOKENS;
|
| 172 |
|
| 173 |
+
let systemPrompt = getSystemPrompt();
|
| 174 |
+
let codeContext = '';
|
| 175 |
+
|
| 176 |
+
if (files) {
|
| 177 |
+
codeContext = createFilesContext(files);
|
| 178 |
+
systemPrompt = `${systemPrompt}\n\n ${codeContext}`;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
return _streamText({
|
| 182 |
model: getModel(currentProvider, currentModel, env, apiKeys, providerSettings) as any,
|
| 183 |
+
system: systemPrompt,
|
| 184 |
maxTokens: dynamicMaxTokens,
|
| 185 |
messages: convertToCoreMessages(processedMessages as any),
|
| 186 |
...options,
|
app/lib/hooks/useMessageParser.ts
CHANGED
|
@@ -23,14 +23,14 @@ const messageParser = new StreamingMessageParser({
|
|
| 23 |
logger.trace('onActionOpen', data.action);
|
| 24 |
|
| 25 |
// we only add shell actions when when the close tag got parsed because only then we have the content
|
| 26 |
-
if (data.action.type
|
| 27 |
workbenchStore.addAction(data);
|
| 28 |
}
|
| 29 |
},
|
| 30 |
onActionClose: (data) => {
|
| 31 |
logger.trace('onActionClose', data.action);
|
| 32 |
|
| 33 |
-
if (data.action.type
|
| 34 |
workbenchStore.addAction(data);
|
| 35 |
}
|
| 36 |
|
|
|
|
| 23 |
logger.trace('onActionOpen', data.action);
|
| 24 |
|
| 25 |
// we only add shell actions when when the close tag got parsed because only then we have the content
|
| 26 |
+
if (data.action.type === 'file') {
|
| 27 |
workbenchStore.addAction(data);
|
| 28 |
}
|
| 29 |
},
|
| 30 |
onActionClose: (data) => {
|
| 31 |
logger.trace('onActionClose', data.action);
|
| 32 |
|
| 33 |
+
if (data.action.type !== 'file') {
|
| 34 |
workbenchStore.addAction(data);
|
| 35 |
}
|
| 36 |
|
app/lib/stores/workbench.ts
CHANGED
|
@@ -262,9 +262,9 @@ export class WorkbenchStore {
|
|
| 262 |
this.artifacts.setKey(messageId, { ...artifact, ...state });
|
| 263 |
}
|
| 264 |
addAction(data: ActionCallbackData) {
|
| 265 |
-
this._addAction(data);
|
| 266 |
|
| 267 |
-
|
| 268 |
}
|
| 269 |
async _addAction(data: ActionCallbackData) {
|
| 270 |
const { messageId } = data;
|
|
@@ -294,6 +294,12 @@ export class WorkbenchStore {
|
|
| 294 |
unreachable('Artifact not found');
|
| 295 |
}
|
| 296 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
if (data.action.type === 'file') {
|
| 298 |
const wc = await webcontainer;
|
| 299 |
const fullPath = nodePath.join(wc.workdir, data.action.filePath);
|
|
|
|
| 262 |
this.artifacts.setKey(messageId, { ...artifact, ...state });
|
| 263 |
}
|
| 264 |
addAction(data: ActionCallbackData) {
|
| 265 |
+
// this._addAction(data);
|
| 266 |
|
| 267 |
+
this.addToExecutionQueue(() => this._addAction(data));
|
| 268 |
}
|
| 269 |
async _addAction(data: ActionCallbackData) {
|
| 270 |
const { messageId } = data;
|
|
|
|
| 294 |
unreachable('Artifact not found');
|
| 295 |
}
|
| 296 |
|
| 297 |
+
const action = artifact.runner.actions.get()[data.actionId];
|
| 298 |
+
|
| 299 |
+
if (action.executed) {
|
| 300 |
+
return;
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
if (data.action.type === 'file') {
|
| 304 |
const wc = await webcontainer;
|
| 305 |
const fullPath = nodePath.join(wc.workdir, data.action.filePath);
|
app/routes/api.chat.ts
CHANGED
|
@@ -30,9 +30,9 @@ function parseCookies(cookieHeader: string) {
|
|
| 30 |
}
|
| 31 |
|
| 32 |
async function chatAction({ context, request }: ActionFunctionArgs) {
|
| 33 |
-
const { messages } = await request.json<{
|
| 34 |
messages: Messages;
|
| 35 |
-
|
| 36 |
}>();
|
| 37 |
|
| 38 |
const cookieHeader = request.headers.get('Cookie');
|
|
@@ -64,13 +64,27 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
|
|
| 64 |
messages.push({ role: 'assistant', content });
|
| 65 |
messages.push({ role: 'user', content: CONTINUE_PROMPT });
|
| 66 |
|
| 67 |
-
const result = await streamText({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
|
| 69 |
return stream.switchSource(result.toAIStream());
|
| 70 |
},
|
| 71 |
};
|
| 72 |
|
| 73 |
-
const result = await streamText({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
|
| 75 |
stream.switchSource(result.toAIStream());
|
| 76 |
|
|
|
|
| 30 |
}
|
| 31 |
|
| 32 |
async function chatAction({ context, request }: ActionFunctionArgs) {
|
| 33 |
+
const { messages, files } = await request.json<{
|
| 34 |
messages: Messages;
|
| 35 |
+
files: any;
|
| 36 |
}>();
|
| 37 |
|
| 38 |
const cookieHeader = request.headers.get('Cookie');
|
|
|
|
| 64 |
messages.push({ role: 'assistant', content });
|
| 65 |
messages.push({ role: 'user', content: CONTINUE_PROMPT });
|
| 66 |
|
| 67 |
+
const result = await streamText({
|
| 68 |
+
messages,
|
| 69 |
+
env: context.cloudflare.env,
|
| 70 |
+
options,
|
| 71 |
+
apiKeys,
|
| 72 |
+
files,
|
| 73 |
+
providerSettings,
|
| 74 |
+
});
|
| 75 |
|
| 76 |
return stream.switchSource(result.toAIStream());
|
| 77 |
},
|
| 78 |
};
|
| 79 |
|
| 80 |
+
const result = await streamText({
|
| 81 |
+
messages,
|
| 82 |
+
env: context.cloudflare.env,
|
| 83 |
+
options,
|
| 84 |
+
apiKeys,
|
| 85 |
+
files,
|
| 86 |
+
providerSettings,
|
| 87 |
+
});
|
| 88 |
|
| 89 |
stream.switchSource(result.toAIStream());
|
| 90 |
|
app/utils/constants.ts
CHANGED
|
@@ -462,7 +462,6 @@ async function getOpenRouterModels(): Promise<ModelInfo[]> {
|
|
| 462 |
}
|
| 463 |
|
| 464 |
async function getLMStudioModels(_apiKeys?: Record<string, string>, settings?: IProviderSetting): Promise<ModelInfo[]> {
|
| 465 |
-
|
| 466 |
try {
|
| 467 |
const baseUrl = settings?.baseUrl || import.meta.env.LMSTUDIO_API_BASE_URL || 'http://localhost:1234';
|
| 468 |
const response = await fetch(`${baseUrl}/v1/models`);
|
|
|
|
| 462 |
}
|
| 463 |
|
| 464 |
async function getLMStudioModels(_apiKeys?: Record<string, string>, settings?: IProviderSetting): Promise<ModelInfo[]> {
|
|
|
|
| 465 |
try {
|
| 466 |
const baseUrl = settings?.baseUrl || import.meta.env.LMSTUDIO_API_BASE_URL || 'http://localhost:1234';
|
| 467 |
const response = await fetch(`${baseUrl}/v1/models`);
|