Skip to content

Commit 8c4397a

Browse files
authored
Merge pull request #578 from thecodacus/context-optimization
feat(context optimization): Optimize LLM Context Management and File Handling
2 parents 1e04ab3 + da37d94 commit 8c4397a

File tree

5 files changed

+119
-10
lines changed

5 files changed

+119
-10
lines changed

app/components/chat/Chat.client.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export const ChatImpl = memo(
9292
const [chatStarted, setChatStarted] = useState(initialMessages.length > 0);
9393
const [uploadedFiles, setUploadedFiles] = useState<File[]>([]); // Move here
9494
const [imageDataList, setImageDataList] = useState<string[]>([]); // Move here
95+
const files = useStore(workbenchStore.files);
9596
const { activeProviders } = useSettings();
9697

9798
const [model, setModel] = useState(() => {
@@ -113,6 +114,7 @@ export const ChatImpl = memo(
113114
api: '/api/chat',
114115
body: {
115116
apiKeys,
117+
files,
116118
},
117119
onError: (error) => {
118120
logger.error('Request failed\n\n', error);

app/lib/.server/llm/stream-text.ts

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { getModel } from '~/lib/.server/llm/model';
33
import { MAX_TOKENS } from './constants';
44
import { getSystemPrompt } from './prompts';
55
import { DEFAULT_MODEL, DEFAULT_PROVIDER, getModelList, MODEL_REGEX, PROVIDER_REGEX } from '~/utils/constants';
6+
import ignore from 'ignore';
67
import type { IProviderSetting } from '~/types/model';
78

89
interface ToolResult<Name extends string, Args, Result> {
@@ -23,6 +24,78 @@ export type Messages = Message[];
2324

2425
export type StreamingOptions = Omit<Parameters<typeof _streamText>[0], 'model'>;
2526

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+
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+
2699
function extractPropertiesFromMessage(message: Message): { model: string; provider: string; content: string } {
27100
const textContent = Array.isArray(message.content)
28101
? message.content.find((item) => item.type === 'text')?.text || ''
@@ -64,9 +137,10 @@ export async function streamText(props: {
64137
env: Env;
65138
options?: StreamingOptions;
66139
apiKeys?: Record<string, string>;
140+
files?: FileMap;
67141
providerSettings?: Record<string, IProviderSetting>;
68142
}) {
69-
const { messages, env, options, apiKeys, providerSettings } = props;
143+
const { messages, env, options, apiKeys, files, providerSettings } = props;
70144
let currentModel = DEFAULT_MODEL;
71145
let currentProvider = DEFAULT_PROVIDER.name;
72146
const MODEL_LIST = await getModelList(apiKeys || {}, providerSettings);
@@ -80,6 +154,11 @@ export async function streamText(props: {
80154

81155
currentProvider = provider;
82156

157+
return { ...message, content };
158+
} else if (message.role == 'assistant') {
159+
let content = message.content;
160+
content = simplifyBoltActions(content);
161+
83162
return { ...message, content };
84163
}
85164

@@ -90,9 +169,17 @@ export async function streamText(props: {
90169

91170
const dynamicMaxTokens = modelDetails && modelDetails.maxTokenAllowed ? modelDetails.maxTokenAllowed : MAX_TOKENS;
92171

172+
let systemPrompt = getSystemPrompt();
173+
let codeContext = '';
174+
175+
if (files) {
176+
codeContext = createFilesContext(files);
177+
systemPrompt = `${systemPrompt}\n\n ${codeContext}`;
178+
}
179+
93180
return _streamText({
94181
model: getModel(currentProvider, currentModel, env, apiKeys, providerSettings) as any,
95-
system: getSystemPrompt(),
182+
system: systemPrompt,
96183
maxTokens: dynamicMaxTokens,
97184
messages: convertToCoreMessages(processedMessages as any),
98185
...options,

app/lib/hooks/useMessageParser.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,14 @@ const messageParser = new StreamingMessageParser({
2323
logger.trace('onActionOpen', data.action);
2424

2525
// we only add shell actions when when the close tag got parsed because only then we have the content
26-
if (data.action.type !== 'shell') {
26+
if (data.action.type === 'file') {
2727
workbenchStore.addAction(data);
2828
}
2929
},
3030
onActionClose: (data) => {
3131
logger.trace('onActionClose', data.action);
3232

33-
if (data.action.type === 'shell') {
33+
if (data.action.type !== 'file') {
3434
workbenchStore.addAction(data);
3535
}
3636

app/lib/stores/workbench.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,9 +262,9 @@ export class WorkbenchStore {
262262
this.artifacts.setKey(messageId, { ...artifact, ...state });
263263
}
264264
addAction(data: ActionCallbackData) {
265-
this._addAction(data);
265+
// this._addAction(data);
266266

267-
// this.addToExecutionQueue(()=>this._addAction(data))
267+
this.addToExecutionQueue(() => this._addAction(data));
268268
}
269269
async _addAction(data: ActionCallbackData) {
270270
const { messageId } = data;
@@ -294,6 +294,12 @@ export class WorkbenchStore {
294294
unreachable('Artifact not found');
295295
}
296296

297+
const action = artifact.runner.actions.get()[data.actionId];
298+
299+
if (action.executed) {
300+
return;
301+
}
302+
297303
if (data.action.type === 'file') {
298304
const wc = await webcontainer;
299305
const fullPath = nodePath.join(wc.workdir, data.action.filePath);

app/routes/api.chat.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ function parseCookies(cookieHeader: string) {
3030
}
3131

3232
async function chatAction({ context, request }: ActionFunctionArgs) {
33-
const { messages } = await request.json<{
33+
const { messages, files } = await request.json<{
3434
messages: Messages;
35-
model: string;
35+
files: any;
3636
}>();
3737

3838
const cookieHeader = request.headers.get('Cookie');
@@ -64,13 +64,27 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
6464
messages.push({ role: 'assistant', content });
6565
messages.push({ role: 'user', content: CONTINUE_PROMPT });
6666

67-
const result = await streamText({ messages, env: context.cloudflare.env, options, apiKeys, providerSettings });
67+
const result = await streamText({
68+
messages,
69+
env: context.cloudflare.env,
70+
options,
71+
apiKeys,
72+
files,
73+
providerSettings,
74+
});
6875

6976
return stream.switchSource(result.toAIStream());
7077
},
7178
};
7279

73-
const result = await streamText({ messages, env: context.cloudflare.env, options, apiKeys, providerSettings });
80+
const result = await streamText({
81+
messages,
82+
env: context.cloudflare.env,
83+
options,
84+
apiKeys,
85+
files,
86+
providerSettings,
87+
});
7488

7589
stream.switchSource(result.toAIStream());
7690

0 commit comments

Comments
 (0)