codacus commited on
Commit
8c4397a
·
unverified ·
2 Parent(s): 1e04ab3 da37d94

Merge pull request #578 from thecodacus/context-optimization

Browse files

feat(context optimization): Optimize LLM Context Management and File Handling

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,11 @@ export async function streamText(props: {
80
 
81
  currentProvider = provider;
82
 
 
 
 
 
 
83
  return { ...message, content };
84
  }
85
 
@@ -90,9 +169,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: getSystemPrompt(),
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
+ 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
+ let content = message.content;
160
+ content = simplifyBoltActions(content);
161
+
162
  return { ...message, content };
163
  }
164
 
 
169
 
170
  const dynamicMaxTokens = modelDetails && modelDetails.maxTokenAllowed ? modelDetails.maxTokenAllowed : MAX_TOKENS;
171
 
172
+ let systemPrompt = getSystemPrompt();
173
+ let codeContext = '';
174
+
175
+ if (files) {
176
+ codeContext = createFilesContext(files);
177
+ systemPrompt = `${systemPrompt}\n\n ${codeContext}`;
178
+ }
179
+
180
  return _streamText({
181
  model: getModel(currentProvider, currentModel, env, apiKeys, providerSettings) as any,
182
+ system: systemPrompt,
183
  maxTokens: dynamicMaxTokens,
184
  messages: convertToCoreMessages(processedMessages as any),
185
  ...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 !== 'shell') {
27
  workbenchStore.addAction(data);
28
  }
29
  },
30
  onActionClose: (data) => {
31
  logger.trace('onActionClose', data.action);
32
 
33
- if (data.action.type === 'shell') {
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
- // this.addToExecutionQueue(()=>this._addAction(data))
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
- model: string;
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({ messages, env: context.cloudflare.env, options, apiKeys, providerSettings });
 
 
 
 
 
 
 
68
 
69
  return stream.switchSource(result.toAIStream());
70
  },
71
  };
72
 
73
- const result = await streamText({ messages, env: context.cloudflare.env, options, apiKeys, providerSettings });
 
 
 
 
 
 
 
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