@librechat/agents 2.2.5 → 2.2.6
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.
- package/dist/cjs/messages/format.cjs +161 -80
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/esm/messages/format.mjs +162 -81
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/types/messages/format.d.ts +6 -15
- package/dist/types/types/stream.d.ts +30 -1
- package/dist/types/types/tools.d.ts +1 -5
- package/package.json +1 -1
- package/src/messages/format.ts +180 -111
- package/src/messages/formatAgentMessages.test.ts +2 -1
- package/src/messages/formatAgentMessages.tools.test.ts +349 -0
- package/src/types/stream.ts +36 -2
- package/src/types/tools.ts +0 -5
package/src/messages/format.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { ToolMessage, BaseMessage } from '@langchain/core/messages';
|
|
2
|
-
import { HumanMessage, AIMessage, SystemMessage } from '@langchain/core/messages';
|
|
3
|
-
import { MessageContentImageUrl } from '@langchain/core/messages';
|
|
2
|
+
import { HumanMessage, AIMessage, SystemMessage, getBufferString } from '@langchain/core/messages';
|
|
3
|
+
import type { MessageContentImageUrl } from '@langchain/core/messages';
|
|
4
4
|
import type { ToolCall } from '@langchain/core/messages/tool';
|
|
5
|
-
import type { MessageContentComplex } from '@/types';
|
|
5
|
+
import type { MessageContentComplex, ToolCallPart, TPayload, TMessage } from '@/types';
|
|
6
6
|
import { Providers, ContentTypes } from '@/common';
|
|
7
7
|
|
|
8
8
|
interface VisionMessageParams {
|
|
@@ -201,39 +201,118 @@ export const formatFromLangChain = (message: LangChainMessage): Record<string, a
|
|
|
201
201
|
};
|
|
202
202
|
};
|
|
203
203
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
204
|
+
/**
|
|
205
|
+
* Helper function to format an assistant message
|
|
206
|
+
* @param message The message to format
|
|
207
|
+
* @returns Array of formatted messages
|
|
208
|
+
*/
|
|
209
|
+
function formatAssistantMessage(message: Partial<TMessage>): Array<AIMessage | ToolMessage> {
|
|
210
|
+
const formattedMessages: Array<AIMessage | ToolMessage> = [];
|
|
211
|
+
let currentContent: MessageContentComplex[] = [];
|
|
212
|
+
let lastAIMessage: AIMessage | null = null;
|
|
213
|
+
let hasReasoning = false;
|
|
214
|
+
|
|
215
|
+
if (Array.isArray(message.content)) {
|
|
216
|
+
for (const part of message.content) {
|
|
217
|
+
if (part.type === ContentTypes.TEXT && part.tool_call_ids) {
|
|
218
|
+
/*
|
|
219
|
+
If there's pending content, it needs to be aggregated as a single string to prepare for tool calls.
|
|
220
|
+
For Anthropic models, the "tool_calls" field on a message is only respected if content is a string.
|
|
221
|
+
*/
|
|
222
|
+
if (currentContent.length > 0) {
|
|
223
|
+
let content = currentContent.reduce((acc, curr) => {
|
|
224
|
+
if (curr.type === ContentTypes.TEXT) {
|
|
225
|
+
return `${acc}${curr[ContentTypes.TEXT] || ''}\n`;
|
|
226
|
+
}
|
|
227
|
+
return acc;
|
|
228
|
+
}, '');
|
|
229
|
+
content = `${content}\n${part[ContentTypes.TEXT] ?? part.text ?? ''}`.trim();
|
|
230
|
+
lastAIMessage = new AIMessage({ content });
|
|
231
|
+
formattedMessages.push(lastAIMessage);
|
|
232
|
+
currentContent = [];
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
// Create a new AIMessage with this text and prepare for tool calls
|
|
236
|
+
lastAIMessage = new AIMessage({
|
|
237
|
+
content: part.text || '',
|
|
238
|
+
});
|
|
239
|
+
formattedMessages.push(lastAIMessage);
|
|
240
|
+
} else if (part?.type === ContentTypes.TOOL_CALL) {
|
|
241
|
+
if (!lastAIMessage) {
|
|
242
|
+
throw new Error('Invalid tool call structure: No preceding AIMessage with tool_call_ids');
|
|
243
|
+
}
|
|
215
244
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
245
|
+
// Note: `tool_calls` list is defined when constructed by `AIMessage` class, and outputs should be excluded from it
|
|
246
|
+
const { output, args: _args, ..._tool_call } = (part.tool_call as ToolCallPart);
|
|
247
|
+
const tool_call: ToolCallPart = _tool_call;
|
|
248
|
+
// TODO: investigate; args as dictionary may need to be providers-or-tool-specific
|
|
249
|
+
let args: any = _args;
|
|
250
|
+
try {
|
|
251
|
+
if (typeof _args === 'string') {
|
|
252
|
+
args = JSON.parse(_args);
|
|
253
|
+
}
|
|
254
|
+
} catch (e) {
|
|
255
|
+
if (typeof _args === 'string') {
|
|
256
|
+
args = { input: _args };
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
tool_call.args = args;
|
|
261
|
+
if (!lastAIMessage.tool_calls) {
|
|
262
|
+
lastAIMessage.tool_calls = [];
|
|
263
|
+
}
|
|
264
|
+
lastAIMessage.tool_calls.push(tool_call as ToolCall);
|
|
265
|
+
|
|
266
|
+
formattedMessages.push(
|
|
267
|
+
new ToolMessage({
|
|
268
|
+
tool_call_id: tool_call.id ?? '',
|
|
269
|
+
name: tool_call.name,
|
|
270
|
+
content: output || '',
|
|
271
|
+
}),
|
|
272
|
+
);
|
|
273
|
+
} else if (part.type === ContentTypes.THINK) {
|
|
274
|
+
hasReasoning = true;
|
|
275
|
+
continue;
|
|
276
|
+
} else if (part.type === ContentTypes.ERROR || part.type === ContentTypes.AGENT_UPDATE) {
|
|
277
|
+
continue;
|
|
278
|
+
} else {
|
|
279
|
+
currentContent.push(part);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (hasReasoning && currentContent.length > 0) {
|
|
285
|
+
const content = currentContent
|
|
286
|
+
.reduce((acc, curr) => {
|
|
287
|
+
if (curr.type === ContentTypes.TEXT) {
|
|
288
|
+
return `${acc}${curr[ContentTypes.TEXT] || ''}\n`;
|
|
289
|
+
}
|
|
290
|
+
return acc;
|
|
291
|
+
}, '')
|
|
292
|
+
.trim();
|
|
293
|
+
|
|
294
|
+
if (content) {
|
|
295
|
+
formattedMessages.push(new AIMessage({ content }));
|
|
296
|
+
}
|
|
297
|
+
} else if (currentContent.length > 0) {
|
|
298
|
+
formattedMessages.push(new AIMessage({ content: currentContent }));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return formattedMessages;
|
|
225
302
|
}
|
|
226
303
|
|
|
227
304
|
/**
|
|
228
305
|
* Formats an array of messages for LangChain, handling tool calls and creating ToolMessage instances.
|
|
229
306
|
*
|
|
230
|
-
* @param {
|
|
307
|
+
* @param {TPayload} payload - The array of messages to format.
|
|
231
308
|
* @param {Record<number, number>} [indexTokenCountMap] - Optional map of message indices to token counts.
|
|
309
|
+
* @param {Set<string>} [tools] - Optional set of tool names that are allowed in the request.
|
|
232
310
|
* @returns {Object} - Object containing formatted messages and updated indexTokenCountMap if provided.
|
|
233
311
|
*/
|
|
234
312
|
export const formatAgentMessages = (
|
|
235
|
-
payload:
|
|
236
|
-
indexTokenCountMap?: Record<number, number
|
|
313
|
+
payload: TPayload,
|
|
314
|
+
indexTokenCountMap?: Record<number, number>,
|
|
315
|
+
tools?: Set<string>
|
|
237
316
|
): {
|
|
238
317
|
messages: Array<HumanMessage | AIMessage | SystemMessage | ToolMessage>;
|
|
239
318
|
indexTokenCountMap?: Record<number, number>;
|
|
@@ -244,6 +323,7 @@ export const formatAgentMessages = (
|
|
|
244
323
|
// Keep track of the mapping from original payload indices to result indices
|
|
245
324
|
const indexMapping: Record<number, number[]> = {};
|
|
246
325
|
|
|
326
|
+
// Process messages with tool conversion if tools set is provided
|
|
247
327
|
for (let i = 0; i < payload.length; i++) {
|
|
248
328
|
const message = payload[i];
|
|
249
329
|
// Q: Store the current length of messages to track where this payload message starts in the result?
|
|
@@ -265,97 +345,88 @@ export const formatAgentMessages = (
|
|
|
265
345
|
// For assistant messages, track the starting index before processing
|
|
266
346
|
const startMessageIndex = messages.length;
|
|
267
347
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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);
|
|
348
|
+
// If tools set is provided, we need to check if we need to convert tool messages to a string
|
|
349
|
+
if (tools) {
|
|
350
|
+
// First, check if this message contains tool calls
|
|
351
|
+
let hasToolCalls = false;
|
|
352
|
+
let hasInvalidTool = false;
|
|
353
|
+
let toolNames: string[] = [];
|
|
354
|
+
|
|
355
|
+
const content = message.content;
|
|
356
|
+
if (content && Array.isArray(content)) {
|
|
357
|
+
for (const part of content) {
|
|
358
|
+
if (part?.type === ContentTypes.TOOL_CALL) {
|
|
359
|
+
hasToolCalls = true;
|
|
360
|
+
if (tools.size === 0) {
|
|
361
|
+
hasInvalidTool = true;
|
|
362
|
+
break;
|
|
311
363
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
364
|
+
const toolName = part.tool_call.name;
|
|
365
|
+
toolNames.push(toolName);
|
|
366
|
+
if (!tools.has(toolName)) {
|
|
367
|
+
hasInvalidTool = true;
|
|
315
368
|
}
|
|
316
369
|
}
|
|
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
370
|
}
|
|
340
371
|
}
|
|
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
372
|
|
|
353
|
-
|
|
354
|
-
|
|
373
|
+
// If this message has tool calls and at least one is invalid, we need to convert it
|
|
374
|
+
if (hasToolCalls && hasInvalidTool) {
|
|
375
|
+
// We need to collect all related messages (this message and any subsequent tool messages)
|
|
376
|
+
const toolSequence: BaseMessage[] = [];
|
|
377
|
+
let sequenceEndIndex = i;
|
|
378
|
+
|
|
379
|
+
// Process the current assistant message to get the AIMessage with tool calls
|
|
380
|
+
const formattedMessages = formatAssistantMessage(message);
|
|
381
|
+
toolSequence.push(...formattedMessages);
|
|
382
|
+
|
|
383
|
+
// Look ahead for any subsequent assistant messages that might be part of this tool sequence
|
|
384
|
+
let j = i + 1;
|
|
385
|
+
while (j < payload.length && payload[j].role === 'assistant') {
|
|
386
|
+
// Check if this is a continuation of the tool sequence
|
|
387
|
+
let isToolResponse = false;
|
|
388
|
+
const content = payload[j].content;
|
|
389
|
+
if (content && Array.isArray(content)) {
|
|
390
|
+
for (const part of content) {
|
|
391
|
+
if (part?.type === ContentTypes.TOOL_CALL) {
|
|
392
|
+
isToolResponse = true;
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (isToolResponse) {
|
|
399
|
+
// This is part of the tool sequence, add it
|
|
400
|
+
const nextMessages = formatAssistantMessage(payload[j]);
|
|
401
|
+
toolSequence.push(...nextMessages);
|
|
402
|
+
sequenceEndIndex = j;
|
|
403
|
+
j++;
|
|
404
|
+
} else {
|
|
405
|
+
// This is not part of the tool sequence, stop looking
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Convert the sequence to a string
|
|
411
|
+
const bufferString = getBufferString(toolSequence);
|
|
412
|
+
messages.push(new AIMessage({ content: bufferString }));
|
|
413
|
+
|
|
414
|
+
// Skip the messages we've already processed
|
|
415
|
+
i = sequenceEndIndex;
|
|
416
|
+
|
|
417
|
+
// Update the index mapping for this sequence
|
|
418
|
+
const resultIndices = [messages.length - 1];
|
|
419
|
+
for (let k = i; k >= i && k <= sequenceEndIndex; k++) {
|
|
420
|
+
indexMapping[k] = resultIndices;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
continue;
|
|
355
424
|
}
|
|
356
|
-
} else if (currentContent.length > 0) {
|
|
357
|
-
messages.push(new AIMessage({ content: currentContent }));
|
|
358
425
|
}
|
|
426
|
+
|
|
427
|
+
// Process the assistant message using the helper function
|
|
428
|
+
const formattedMessages = formatAssistantMessage(message);
|
|
429
|
+
messages.push(...formattedMessages);
|
|
359
430
|
|
|
360
431
|
// Update the index mapping for this assistant message
|
|
361
432
|
// Store all indices that were created from this original message
|
|
@@ -446,8 +517,6 @@ export function shiftIndexTokenCountMap(
|
|
|
446
517
|
): Record<number, number> {
|
|
447
518
|
// Create a new map to avoid modifying the original
|
|
448
519
|
const shiftedMap: Record<number, number> = {};
|
|
449
|
-
|
|
450
|
-
// Add the system message token count at index 0
|
|
451
520
|
shiftedMap[0] = instructionsTokenCount;
|
|
452
521
|
|
|
453
522
|
// Shift all existing indices by 1
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { HumanMessage, AIMessage, SystemMessage, ToolMessage } from '@langchain/core/messages';
|
|
2
|
+
import type { TPayload } from '@/types';
|
|
2
3
|
import { formatAgentMessages } from './format';
|
|
3
4
|
import { ContentTypes } from '@/common';
|
|
4
5
|
|
|
5
6
|
describe('formatAgentMessages', () => {
|
|
6
7
|
it('should format simple user and AI messages', () => {
|
|
7
|
-
const payload = [
|
|
8
|
+
const payload: TPayload = [
|
|
8
9
|
{ role: 'user', content: 'Hello' },
|
|
9
10
|
{ role: 'assistant', content: 'Hi there!' },
|
|
10
11
|
];
|