@librechat/agents 2.2.1 → 2.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/cjs/graphs/Graph.cjs +56 -19
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/main.cjs +18 -8
  4. package/dist/cjs/main.cjs.map +1 -1
  5. package/dist/cjs/{messages.cjs → messages/core.cjs} +2 -2
  6. package/dist/cjs/messages/core.cjs.map +1 -0
  7. package/dist/cjs/messages/format.cjs +334 -0
  8. package/dist/cjs/messages/format.cjs.map +1 -0
  9. package/dist/cjs/messages/prune.cjs +124 -0
  10. package/dist/cjs/messages/prune.cjs.map +1 -0
  11. package/dist/cjs/run.cjs +24 -0
  12. package/dist/cjs/run.cjs.map +1 -1
  13. package/dist/cjs/utils/tokens.cjs +64 -0
  14. package/dist/cjs/utils/tokens.cjs.map +1 -0
  15. package/dist/esm/graphs/Graph.mjs +51 -14
  16. package/dist/esm/graphs/Graph.mjs.map +1 -1
  17. package/dist/esm/main.mjs +3 -1
  18. package/dist/esm/main.mjs.map +1 -1
  19. package/dist/esm/{messages.mjs → messages/core.mjs} +2 -2
  20. package/dist/esm/messages/core.mjs.map +1 -0
  21. package/dist/esm/messages/format.mjs +326 -0
  22. package/dist/esm/messages/format.mjs.map +1 -0
  23. package/dist/esm/messages/prune.mjs +122 -0
  24. package/dist/esm/messages/prune.mjs.map +1 -0
  25. package/dist/esm/run.mjs +24 -0
  26. package/dist/esm/run.mjs.map +1 -1
  27. package/dist/esm/utils/tokens.mjs +62 -0
  28. package/dist/esm/utils/tokens.mjs.map +1 -0
  29. package/dist/types/graphs/Graph.d.ts +8 -1
  30. package/dist/types/messages/format.d.ts +120 -0
  31. package/dist/types/messages/index.d.ts +3 -0
  32. package/dist/types/messages/prune.d.ts +16 -0
  33. package/dist/types/types/run.d.ts +4 -0
  34. package/dist/types/utils/tokens.d.ts +2 -0
  35. package/package.json +1 -1
  36. package/src/graphs/Graph.ts +54 -16
  37. package/src/messages/format.ts +460 -0
  38. package/src/messages/formatAgentMessages.test.ts +628 -0
  39. package/src/messages/formatMessage.test.ts +277 -0
  40. package/src/messages/index.ts +3 -0
  41. package/src/messages/prune.ts +167 -0
  42. package/src/messages/shiftIndexTokenCountMap.test.ts +81 -0
  43. package/src/run.ts +26 -0
  44. package/src/scripts/code_exec_simple.ts +21 -8
  45. package/src/specs/prune.test.ts +444 -0
  46. package/src/types/run.ts +5 -0
  47. package/src/utils/tokens.ts +70 -0
  48. package/dist/cjs/messages.cjs.map +0 -1
  49. package/dist/esm/messages.mjs.map +0 -1
  50. /package/dist/types/{messages.d.ts → messages/core.d.ts} +0 -0
  51. /package/src/{messages.ts → messages/core.ts} +0 -0
@@ -0,0 +1,460 @@
1
+ import { ToolMessage, BaseMessage } from '@langchain/core/messages';
2
+ import { HumanMessage, AIMessage, SystemMessage } from '@langchain/core/messages';
3
+ import { MessageContentImageUrl } from '@langchain/core/messages';
4
+ import type { ToolCall } from '@langchain/core/messages/tool';
5
+ import type { MessageContentComplex } from '@/types';
6
+ import { Providers, ContentTypes } from '@/common';
7
+
8
+ interface VisionMessageParams {
9
+ message: {
10
+ role: string;
11
+ content: string;
12
+ name?: string;
13
+ [key: string]: any;
14
+ };
15
+ image_urls: MessageContentImageUrl[];
16
+ endpoint?: Providers;
17
+ }
18
+
19
+ /**
20
+ * Formats a message to OpenAI Vision API payload format.
21
+ *
22
+ * @param {VisionMessageParams} params - The parameters for formatting.
23
+ * @returns {Object} - The formatted message.
24
+ */
25
+ export const formatVisionMessage = ({ message, image_urls, endpoint }: VisionMessageParams): {
26
+ role: string;
27
+ content: MessageContentComplex[];
28
+ name?: string;
29
+ [key: string]: any;
30
+ } => {
31
+ // Create a new object to avoid mutating the input
32
+ const result: {
33
+ role: string;
34
+ content: MessageContentComplex[];
35
+ name?: string;
36
+ [key: string]: any;
37
+ } = {
38
+ ...message,
39
+ content: [] as MessageContentComplex[]
40
+ };
41
+
42
+ if (endpoint === Providers.ANTHROPIC) {
43
+ result.content = [
44
+ ...image_urls,
45
+ { type: ContentTypes.TEXT, text: message.content }
46
+ ] as MessageContentComplex[];
47
+ return result;
48
+ }
49
+
50
+ result.content = [
51
+ { type: ContentTypes.TEXT, text: message.content },
52
+ ...image_urls
53
+ ] as MessageContentComplex[];
54
+
55
+ return result;
56
+ };
57
+
58
+ interface MessageInput {
59
+ role?: string;
60
+ _name?: string;
61
+ sender?: string;
62
+ text?: string;
63
+ content?: string | MessageContentComplex[];
64
+ image_urls?: MessageContentImageUrl[];
65
+ lc_id?: string[];
66
+ [key: string]: any;
67
+ }
68
+
69
+ interface FormatMessageParams {
70
+ message: MessageInput;
71
+ userName?: string;
72
+ assistantName?: string;
73
+ endpoint?: Providers;
74
+ langChain?: boolean;
75
+ }
76
+
77
+ interface FormattedMessage {
78
+ role: string;
79
+ content: string | MessageContentComplex[];
80
+ name?: string;
81
+ [key: string]: any;
82
+ }
83
+
84
+ /**
85
+ * Formats a message to OpenAI payload format based on the provided options.
86
+ *
87
+ * @param {FormatMessageParams} params - The parameters for formatting.
88
+ * @returns {FormattedMessage | HumanMessage | AIMessage | SystemMessage} - The formatted message.
89
+ */
90
+ export const formatMessage = ({
91
+ message,
92
+ userName,
93
+ assistantName,
94
+ endpoint,
95
+ langChain = false
96
+ }: FormatMessageParams): FormattedMessage | HumanMessage | AIMessage | SystemMessage => {
97
+ let { role: _role, _name, sender, text, content: _content, lc_id } = message;
98
+ if (lc_id && lc_id[2] && !langChain) {
99
+ const roleMapping: Record<string, string> = {
100
+ SystemMessage: 'system',
101
+ HumanMessage: 'user',
102
+ AIMessage: 'assistant',
103
+ };
104
+ _role = roleMapping[lc_id[2]] || _role;
105
+ }
106
+ const role = _role ?? (sender && sender?.toLowerCase() === 'user' ? 'user' : 'assistant');
107
+ const content = _content ?? text ?? '';
108
+ const formattedMessage: FormattedMessage = {
109
+ role,
110
+ content,
111
+ };
112
+
113
+ const { image_urls } = message;
114
+ if (Array.isArray(image_urls) && image_urls.length > 0 && role === 'user') {
115
+ return formatVisionMessage({
116
+ message: {
117
+ ...formattedMessage,
118
+ content: typeof formattedMessage.content === 'string' ? formattedMessage.content : ''
119
+ },
120
+ image_urls,
121
+ endpoint,
122
+ });
123
+ }
124
+
125
+ if (_name) {
126
+ formattedMessage.name = _name;
127
+ }
128
+
129
+ if (userName && formattedMessage.role === 'user') {
130
+ formattedMessage.name = userName;
131
+ }
132
+
133
+ if (assistantName && formattedMessage.role === 'assistant') {
134
+ formattedMessage.name = assistantName;
135
+ }
136
+
137
+ if (formattedMessage.name) {
138
+ // Conform to API regex: ^[a-zA-Z0-9_-]{1,64}$
139
+ // https://community.openai.com/t/the-format-of-the-name-field-in-the-documentation-is-incorrect/175684/2
140
+ formattedMessage.name = formattedMessage.name.replace(/[^a-zA-Z0-9_-]/g, '_');
141
+
142
+ if (formattedMessage.name.length > 64) {
143
+ formattedMessage.name = formattedMessage.name.substring(0, 64);
144
+ }
145
+ }
146
+
147
+ if (!langChain) {
148
+ return formattedMessage;
149
+ }
150
+
151
+ if (role === 'user') {
152
+ return new HumanMessage(formattedMessage);
153
+ } else if (role === 'assistant') {
154
+ return new AIMessage(formattedMessage);
155
+ } else {
156
+ return new SystemMessage(formattedMessage);
157
+ }
158
+ };
159
+
160
+ /**
161
+ * Formats an array of messages for LangChain.
162
+ *
163
+ * @param {Array<MessageInput>} messages - The array of messages to format.
164
+ * @param {Omit<FormatMessageParams, 'message' | 'langChain'>} formatOptions - The options for formatting each message.
165
+ * @returns {Array<HumanMessage | AIMessage | SystemMessage>} - The array of formatted LangChain messages.
166
+ */
167
+ export const formatLangChainMessages = (
168
+ messages: Array<MessageInput>,
169
+ formatOptions: Omit<FormatMessageParams, 'message' | 'langChain'>
170
+ ): Array<HumanMessage | AIMessage | SystemMessage> => {
171
+ return messages.map((msg) => {
172
+ const formatted = formatMessage({ ...formatOptions, message: msg, langChain: true });
173
+ return formatted as HumanMessage | AIMessage | SystemMessage;
174
+ });
175
+ };
176
+
177
+ interface LangChainMessage {
178
+ lc_kwargs?: {
179
+ additional_kwargs?: Record<string, any>;
180
+ [key: string]: any;
181
+ };
182
+ kwargs?: {
183
+ additional_kwargs?: Record<string, any>;
184
+ [key: string]: any;
185
+ };
186
+ [key: string]: any;
187
+ }
188
+
189
+ /**
190
+ * Formats a LangChain message object by merging properties from `lc_kwargs` or `kwargs` and `additional_kwargs`.
191
+ *
192
+ * @param {LangChainMessage} message - The message object to format.
193
+ * @returns {Record<string, any>} The formatted LangChain message.
194
+ */
195
+ export const formatFromLangChain = (message: LangChainMessage): Record<string, any> => {
196
+ const kwargs = message.lc_kwargs ?? message.kwargs ?? {};
197
+ const { additional_kwargs = {}, ...message_kwargs } = kwargs;
198
+ return {
199
+ ...message_kwargs,
200
+ ...additional_kwargs,
201
+ };
202
+ };
203
+
204
+ interface TMessage {
205
+ role?: string;
206
+ content?: string | Array<{
207
+ type: ContentTypes;
208
+ [ContentTypes.TEXT]?: string;
209
+ text?: string;
210
+ tool_call_ids?: string[];
211
+ [key: string]: any;
212
+ }>;
213
+ [key: string]: any;
214
+ }
215
+
216
+ interface ToolCallPart {
217
+ type: ContentTypes.TOOL_CALL;
218
+ tool_call: {
219
+ id: string;
220
+ name: string;
221
+ args: string | Record<string, unknown>;
222
+ output?: string;
223
+ [key: string]: any;
224
+ };
225
+ }
226
+
227
+ /**
228
+ * Formats an array of messages for LangChain, handling tool calls and creating ToolMessage instances.
229
+ *
230
+ * @param {Array<Partial<TMessage>>} payload - The array of messages to format.
231
+ * @param {Record<number, number>} [indexTokenCountMap] - Optional map of message indices to token counts.
232
+ * @returns {Object} - Object containing formatted messages and updated indexTokenCountMap if provided.
233
+ */
234
+ export const formatAgentMessages = (
235
+ payload: Array<Partial<TMessage>>,
236
+ indexTokenCountMap?: Record<number, number>
237
+ ): {
238
+ messages: Array<HumanMessage | AIMessage | SystemMessage | ToolMessage>;
239
+ indexTokenCountMap?: Record<number, number>;
240
+ } => {
241
+ const messages: Array<HumanMessage | AIMessage | SystemMessage | ToolMessage> = [];
242
+ // If indexTokenCountMap is provided, create a new map to track the updated indices
243
+ const updatedIndexTokenCountMap: Record<number, number> = {};
244
+ // Keep track of the mapping from original payload indices to result indices
245
+ const indexMapping: Record<number, number[]> = {};
246
+
247
+ for (let i = 0; i < payload.length; i++) {
248
+ const message = payload[i];
249
+ // Q: Store the current length of messages to track where this payload message starts in the result?
250
+ // const startIndex = messages.length;
251
+ if (typeof message.content === 'string') {
252
+ message.content = [{ type: ContentTypes.TEXT, [ContentTypes.TEXT]: message.content }];
253
+ }
254
+ if (message.role !== 'assistant') {
255
+ messages.push(formatMessage({
256
+ message: message as MessageInput,
257
+ langChain: true
258
+ }) as HumanMessage | AIMessage | SystemMessage);
259
+
260
+ // Update the index mapping for this message
261
+ indexMapping[i] = [messages.length - 1];
262
+ continue;
263
+ }
264
+
265
+ // For assistant messages, track the starting index before processing
266
+ const startMessageIndex = messages.length;
267
+
268
+ let currentContent: any[] = [];
269
+ let lastAIMessage: AIMessage | null = null;
270
+
271
+ let hasReasoning = false;
272
+ if (Array.isArray(message.content)) {
273
+ for (const part of message.content) {
274
+ if (part.type === ContentTypes.TEXT && part.tool_call_ids) {
275
+ /*
276
+ If there's pending content, it needs to be aggregated as a single string to prepare for tool calls.
277
+ For Anthropic models, the "tool_calls" field on a message is only respected if content is a string.
278
+ */
279
+ if (currentContent.length > 0) {
280
+ let content = currentContent.reduce((acc, curr) => {
281
+ if (curr.type === ContentTypes.TEXT) {
282
+ return `${acc}${curr[ContentTypes.TEXT] || ''}\n`;
283
+ }
284
+ return acc;
285
+ }, '');
286
+ content = `${content}\n${part[ContentTypes.TEXT] ?? part.text ?? ''}`.trim();
287
+ lastAIMessage = new AIMessage({ content });
288
+ messages.push(lastAIMessage);
289
+ currentContent = [];
290
+ continue;
291
+ }
292
+
293
+ // Create a new AIMessage with this text and prepare for tool calls
294
+ lastAIMessage = new AIMessage({
295
+ content: part.text || '',
296
+ });
297
+
298
+ messages.push(lastAIMessage);
299
+ } else if (part.type === ContentTypes.TOOL_CALL) {
300
+ if (!lastAIMessage) {
301
+ throw new Error('Invalid tool call structure: No preceding AIMessage with tool_call_ids');
302
+ }
303
+
304
+ // Note: `tool_calls` list is defined when constructed by `AIMessage` class, and outputs should be excluded from it
305
+ const { output, args: _args, ...tool_call } = (part.tool_call as any);
306
+ // TODO: investigate; args as dictionary may need to be providers-or-tool-specific
307
+ let args: any = _args;
308
+ try {
309
+ if (typeof _args === 'string') {
310
+ args = JSON.parse(_args);
311
+ }
312
+ } catch (e) {
313
+ if (typeof _args === 'string') {
314
+ args = { input: _args };
315
+ }
316
+ }
317
+
318
+ tool_call.args = args;
319
+ if (!lastAIMessage.tool_calls) {
320
+ lastAIMessage.tool_calls = [];
321
+ }
322
+ lastAIMessage.tool_calls.push(tool_call as ToolCall);
323
+
324
+ // Add the corresponding ToolMessage
325
+ messages.push(
326
+ new ToolMessage({
327
+ tool_call_id: tool_call.id,
328
+ name: tool_call.name,
329
+ content: output || '',
330
+ }),
331
+ );
332
+ } else if (part.type === ContentTypes.THINK) {
333
+ hasReasoning = true;
334
+ continue;
335
+ } else if (part.type === ContentTypes.ERROR || part.type === ContentTypes.AGENT_UPDATE) {
336
+ continue;
337
+ } else {
338
+ currentContent.push(part);
339
+ }
340
+ }
341
+ }
342
+
343
+ if (hasReasoning && currentContent.length > 0) {
344
+ const content = currentContent
345
+ .reduce((acc, curr) => {
346
+ if (curr.type === ContentTypes.TEXT) {
347
+ return `${acc}${curr[ContentTypes.TEXT] || ''}\n`;
348
+ }
349
+ return acc;
350
+ }, '')
351
+ .trim();
352
+
353
+ if (content) {
354
+ messages.push(new AIMessage({ content }));
355
+ }
356
+ } else if (currentContent.length > 0) {
357
+ messages.push(new AIMessage({ content: currentContent }));
358
+ }
359
+
360
+ // Update the index mapping for this assistant message
361
+ // Store all indices that were created from this original message
362
+ const endMessageIndex = messages.length;
363
+ const resultIndices = [];
364
+ for (let j = startMessageIndex; j < endMessageIndex; j++) {
365
+ resultIndices.push(j);
366
+ }
367
+ indexMapping[i] = resultIndices;
368
+ }
369
+
370
+ // Update the token count map if it was provided
371
+ if (indexTokenCountMap) {
372
+ for (let originalIndex = 0; originalIndex < payload.length; originalIndex++) {
373
+ const resultIndices = indexMapping[originalIndex] || [];
374
+ const tokenCount = indexTokenCountMap[originalIndex];
375
+
376
+ if (tokenCount !== undefined) {
377
+ if (resultIndices.length === 1) {
378
+ // Simple 1:1 mapping
379
+ updatedIndexTokenCountMap[resultIndices[0]] = tokenCount;
380
+ } else if (resultIndices.length > 1) {
381
+ // If one message was split into multiple, distribute the token count
382
+ // This is a simplification - in reality, you might want a more sophisticated distribution
383
+ const countPerMessage = Math.floor(tokenCount / resultIndices.length);
384
+ resultIndices.forEach((resultIndex, idx) => {
385
+ if (idx === resultIndices.length - 1) {
386
+ // Give any remainder to the last message
387
+ updatedIndexTokenCountMap[resultIndex] = tokenCount - (countPerMessage * (resultIndices.length - 1));
388
+ } else {
389
+ updatedIndexTokenCountMap[resultIndex] = countPerMessage;
390
+ }
391
+ });
392
+ }
393
+ }
394
+ }
395
+ }
396
+
397
+ return {
398
+ messages,
399
+ indexTokenCountMap: indexTokenCountMap ? updatedIndexTokenCountMap : undefined
400
+ };
401
+ };
402
+
403
+ /**
404
+ * Formats an array of messages for LangChain, making sure all content fields are strings
405
+ * @param {Array<HumanMessage | AIMessage | SystemMessage | ToolMessage>} payload - The array of messages to format.
406
+ * @returns {Array<HumanMessage | AIMessage | SystemMessage | ToolMessage>} - The array of formatted LangChain messages, including ToolMessages for tool calls.
407
+ */
408
+ export const formatContentStrings = (payload: Array<BaseMessage>): Array<BaseMessage> => {
409
+ // Create a copy of the payload to avoid modifying the original
410
+ const result = [...payload];
411
+
412
+ for (const message of result) {
413
+ if (typeof message.content === 'string') {
414
+ continue;
415
+ }
416
+
417
+ if (!Array.isArray(message.content)) {
418
+ continue;
419
+ }
420
+
421
+ // Reduce text types to a single string, ignore all other types
422
+ const content = message.content.reduce((acc, curr) => {
423
+ if (curr.type === ContentTypes.TEXT) {
424
+ return `${acc}${curr[ContentTypes.TEXT] || ''}\n`;
425
+ }
426
+ return acc;
427
+ }, '');
428
+
429
+ message.content = content.trim();
430
+ }
431
+
432
+ return result;
433
+ };
434
+
435
+ /**
436
+ * Adds a value at key 0 for system messages and shifts all key indices by one in an indexTokenCountMap.
437
+ * This is useful when adding a system message at the beginning of a conversation.
438
+ *
439
+ * @param indexTokenCountMap - The original map of message indices to token counts
440
+ * @param instructionsTokenCount - The token count for the system message to add at index 0
441
+ * @returns A new map with the system message at index 0 and all other indices shifted by 1
442
+ */
443
+ export function shiftIndexTokenCountMap(
444
+ indexTokenCountMap: Record<number, number>,
445
+ instructionsTokenCount: number
446
+ ): Record<number, number> {
447
+ // Create a new map to avoid modifying the original
448
+ const shiftedMap: Record<number, number> = {};
449
+
450
+ // Add the system message token count at index 0
451
+ shiftedMap[0] = instructionsTokenCount;
452
+
453
+ // Shift all existing indices by 1
454
+ for (const [indexStr, tokenCount] of Object.entries(indexTokenCountMap)) {
455
+ const index = Number(indexStr);
456
+ shiftedMap[index + 1] = tokenCount;
457
+ }
458
+
459
+ return shiftedMap;
460
+ }