@librechat/agents 3.0.0-rc9 → 3.0.1
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/agents/AgentContext.cjs +6 -2
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +23 -2
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +5 -5
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/instrumentation.cjs +21 -0
- package/dist/cjs/instrumentation.cjs.map +1 -0
- package/dist/cjs/llm/anthropic/index.cjs +21 -2
- package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
- package/dist/cjs/llm/google/index.cjs +3 -0
- package/dist/cjs/llm/google/index.cjs.map +1 -1
- package/dist/cjs/llm/google/utils/common.cjs +13 -0
- package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
- package/dist/cjs/llm/ollama/index.cjs +3 -0
- package/dist/cjs/llm/ollama/index.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +18 -3
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/llm/openai/utils/index.cjs +6 -1
- package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
- package/dist/cjs/llm/openrouter/index.cjs +5 -1
- package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
- package/dist/cjs/llm/vertexai/index.cjs +1 -1
- package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +8 -2
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/messages/cache.cjs +49 -0
- package/dist/cjs/messages/cache.cjs.map +1 -0
- package/dist/cjs/messages/content.cjs +53 -0
- package/dist/cjs/messages/content.cjs.map +1 -0
- package/dist/cjs/messages/core.cjs +5 -1
- package/dist/cjs/messages/core.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +50 -59
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/messages/prune.cjs +28 -0
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/run.cjs +57 -5
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/stream.cjs +7 -0
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +2 -0
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/search/firecrawl.cjs +3 -1
- package/dist/cjs/tools/search/firecrawl.cjs.map +1 -1
- package/dist/cjs/tools/search/rerankers.cjs +8 -6
- package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
- package/dist/cjs/tools/search/search.cjs +5 -5
- package/dist/cjs/tools/search/search.cjs.map +1 -1
- package/dist/cjs/tools/search/serper-scraper.cjs +132 -0
- package/dist/cjs/tools/search/serper-scraper.cjs.map +1 -0
- package/dist/cjs/tools/search/tool.cjs +46 -9
- package/dist/cjs/tools/search/tool.cjs.map +1 -1
- package/dist/cjs/utils/handlers.cjs +70 -0
- package/dist/cjs/utils/handlers.cjs.map +1 -0
- package/dist/cjs/utils/misc.cjs +8 -1
- package/dist/cjs/utils/misc.cjs.map +1 -1
- package/dist/cjs/utils/title.cjs +54 -25
- package/dist/cjs/utils/title.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +6 -2
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +23 -2
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +5 -5
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/instrumentation.mjs +19 -0
- package/dist/esm/instrumentation.mjs.map +1 -0
- package/dist/esm/llm/anthropic/index.mjs +21 -2
- package/dist/esm/llm/anthropic/index.mjs.map +1 -1
- package/dist/esm/llm/google/index.mjs +3 -0
- package/dist/esm/llm/google/index.mjs.map +1 -1
- package/dist/esm/llm/google/utils/common.mjs +13 -0
- package/dist/esm/llm/google/utils/common.mjs.map +1 -1
- package/dist/esm/llm/ollama/index.mjs +3 -0
- package/dist/esm/llm/ollama/index.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +18 -3
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/llm/openai/utils/index.mjs +6 -1
- package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
- package/dist/esm/llm/openrouter/index.mjs +5 -1
- package/dist/esm/llm/openrouter/index.mjs.map +1 -1
- package/dist/esm/llm/vertexai/index.mjs +1 -1
- package/dist/esm/llm/vertexai/index.mjs.map +1 -1
- package/dist/esm/main.mjs +5 -2
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/messages/cache.mjs +47 -0
- package/dist/esm/messages/cache.mjs.map +1 -0
- package/dist/esm/messages/content.mjs +51 -0
- package/dist/esm/messages/content.mjs.map +1 -0
- package/dist/esm/messages/core.mjs +5 -1
- package/dist/esm/messages/core.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +50 -58
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/messages/prune.mjs +28 -0
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/run.mjs +57 -5
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/stream.mjs +7 -0
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +2 -0
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/search/firecrawl.mjs +3 -1
- package/dist/esm/tools/search/firecrawl.mjs.map +1 -1
- package/dist/esm/tools/search/rerankers.mjs +8 -6
- package/dist/esm/tools/search/rerankers.mjs.map +1 -1
- package/dist/esm/tools/search/search.mjs +5 -5
- package/dist/esm/tools/search/search.mjs.map +1 -1
- package/dist/esm/tools/search/serper-scraper.mjs +129 -0
- package/dist/esm/tools/search/serper-scraper.mjs.map +1 -0
- package/dist/esm/tools/search/tool.mjs +46 -9
- package/dist/esm/tools/search/tool.mjs.map +1 -1
- package/dist/esm/utils/handlers.mjs +68 -0
- package/dist/esm/utils/handlers.mjs.map +1 -0
- package/dist/esm/utils/misc.mjs +8 -2
- package/dist/esm/utils/misc.mjs.map +1 -1
- package/dist/esm/utils/title.mjs +54 -25
- package/dist/esm/utils/title.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +4 -1
- package/dist/types/instrumentation.d.ts +1 -0
- package/dist/types/llm/anthropic/index.d.ts +3 -0
- package/dist/types/llm/google/index.d.ts +1 -0
- package/dist/types/llm/ollama/index.d.ts +1 -0
- package/dist/types/llm/openai/index.d.ts +4 -0
- package/dist/types/llm/openrouter/index.d.ts +4 -2
- package/dist/types/llm/vertexai/index.d.ts +1 -1
- package/dist/types/messages/cache.d.ts +8 -0
- package/dist/types/messages/content.d.ts +7 -0
- package/dist/types/messages/format.d.ts +22 -25
- package/dist/types/messages/index.d.ts +2 -0
- package/dist/types/run.d.ts +2 -1
- package/dist/types/tools/search/firecrawl.d.ts +2 -1
- package/dist/types/tools/search/rerankers.d.ts +4 -1
- package/dist/types/tools/search/search.d.ts +1 -2
- package/dist/types/tools/search/serper-scraper.d.ts +59 -0
- package/dist/types/tools/search/tool.d.ts +25 -4
- package/dist/types/tools/search/types.d.ts +31 -1
- package/dist/types/types/graph.d.ts +3 -1
- package/dist/types/types/messages.d.ts +4 -0
- package/dist/types/utils/handlers.d.ts +34 -0
- package/dist/types/utils/index.d.ts +1 -0
- package/dist/types/utils/misc.d.ts +1 -0
- package/package.json +7 -3
- package/src/agents/AgentContext.ts +8 -0
- package/src/graphs/Graph.ts +31 -2
- package/src/graphs/MultiAgentGraph.ts +5 -5
- package/src/instrumentation.ts +22 -0
- package/src/llm/anthropic/index.ts +23 -2
- package/src/llm/anthropic/llm.spec.ts +1 -1
- package/src/llm/google/index.ts +4 -0
- package/src/llm/google/utils/common.ts +14 -0
- package/src/llm/ollama/index.ts +3 -0
- package/src/llm/openai/index.ts +17 -4
- package/src/llm/openai/utils/index.ts +7 -1
- package/src/llm/openrouter/index.ts +15 -6
- package/src/llm/vertexai/index.ts +2 -2
- package/src/messages/cache.test.ts +262 -0
- package/src/messages/cache.ts +56 -0
- package/src/messages/content.test.ts +362 -0
- package/src/messages/content.ts +63 -0
- package/src/messages/core.ts +5 -2
- package/src/messages/format.ts +65 -71
- package/src/messages/formatMessage.test.ts +418 -2
- package/src/messages/index.ts +2 -0
- package/src/messages/prune.ts +51 -0
- package/src/run.ts +82 -10
- package/src/scripts/ant_web_search.ts +1 -1
- package/src/scripts/handoff-test.ts +1 -1
- package/src/scripts/multi-agent-chain.ts +4 -4
- package/src/scripts/multi-agent-conditional.ts +4 -4
- package/src/scripts/multi-agent-document-review-chain.ts +4 -4
- package/src/scripts/multi-agent-parallel.ts +10 -8
- package/src/scripts/multi-agent-sequence.ts +3 -3
- package/src/scripts/multi-agent-supervisor.ts +5 -3
- package/src/scripts/multi-agent-test.ts +2 -2
- package/src/scripts/search.ts +5 -1
- package/src/scripts/simple.ts +8 -0
- package/src/scripts/test-custom-prompt-key.ts +4 -4
- package/src/scripts/test-handoff-input.ts +3 -3
- package/src/scripts/test-multi-agent-list-handoff.ts +2 -2
- package/src/scripts/tools.ts +4 -1
- package/src/specs/agent-handoffs.test.ts +889 -0
- package/src/stream.ts +9 -2
- package/src/tools/search/firecrawl.ts +5 -2
- package/src/tools/search/jina-reranker.test.ts +126 -0
- package/src/tools/search/rerankers.ts +11 -5
- package/src/tools/search/search.ts +6 -8
- package/src/tools/search/serper-scraper.ts +155 -0
- package/src/tools/search/tool.ts +49 -8
- package/src/tools/search/types.ts +46 -0
- package/src/types/graph.ts +6 -1
- package/src/types/messages.ts +4 -0
- package/src/utils/handlers.ts +107 -0
- package/src/utils/index.ts +2 -1
- package/src/utils/llmConfig.ts +35 -1
- package/src/utils/misc.ts +33 -21
- package/src/utils/title.ts +80 -40
|
@@ -1,8 +1,43 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
HumanMessage,
|
|
3
|
+
AIMessage,
|
|
4
|
+
SystemMessage,
|
|
5
|
+
} from '@langchain/core/messages';
|
|
6
|
+
import type { MessageContentComplex } from '@/types';
|
|
7
|
+
import {
|
|
8
|
+
formatMessage,
|
|
9
|
+
formatLangChainMessages,
|
|
10
|
+
formatFromLangChain,
|
|
11
|
+
formatMediaMessage,
|
|
12
|
+
} from './format';
|
|
13
|
+
import { Providers } from '@/common';
|
|
3
14
|
|
|
4
15
|
const NO_PARENT = '00000000-0000-0000-0000-000000000000';
|
|
5
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Type for formatted message results with media content
|
|
19
|
+
*/
|
|
20
|
+
interface FormattedMediaMessage {
|
|
21
|
+
role: string;
|
|
22
|
+
content: MessageContentComplex[];
|
|
23
|
+
name?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Type guard to check if result is a FormattedMediaMessage
|
|
28
|
+
*/
|
|
29
|
+
function isFormattedMediaMessage(
|
|
30
|
+
result: unknown
|
|
31
|
+
): result is FormattedMediaMessage {
|
|
32
|
+
return (
|
|
33
|
+
typeof result === 'object' &&
|
|
34
|
+
result !== null &&
|
|
35
|
+
'role' in result &&
|
|
36
|
+
'content' in result &&
|
|
37
|
+
Array.isArray((result as FormattedMediaMessage).content)
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
6
41
|
describe('formatMessage', () => {
|
|
7
42
|
it('formats user message', () => {
|
|
8
43
|
const input = {
|
|
@@ -187,6 +222,387 @@ describe('formatMessage', () => {
|
|
|
187
222
|
});
|
|
188
223
|
});
|
|
189
224
|
|
|
225
|
+
describe('formatMediaMessage', () => {
|
|
226
|
+
it('formats message with images for default provider', () => {
|
|
227
|
+
const message = {
|
|
228
|
+
role: 'user',
|
|
229
|
+
content: 'Check out this image',
|
|
230
|
+
name: 'John',
|
|
231
|
+
};
|
|
232
|
+
const mediaParts = [
|
|
233
|
+
{
|
|
234
|
+
type: 'image_url',
|
|
235
|
+
image_url: { url: 'https://example.com/image1.jpg' },
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
type: 'image_url',
|
|
239
|
+
image_url: { url: 'https://example.com/image2.jpg' },
|
|
240
|
+
},
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
const result = formatMediaMessage({ message, mediaParts });
|
|
244
|
+
|
|
245
|
+
expect(result.role).toBe('user');
|
|
246
|
+
expect(result.name).toBe('John');
|
|
247
|
+
expect(Array.isArray(result.content)).toBe(true);
|
|
248
|
+
expect(result.content).toHaveLength(3);
|
|
249
|
+
expect(result.content[0]).toEqual({
|
|
250
|
+
type: 'text',
|
|
251
|
+
text: 'Check out this image',
|
|
252
|
+
});
|
|
253
|
+
expect(result.content[1]).toEqual(mediaParts[0]);
|
|
254
|
+
expect(result.content[2]).toEqual(mediaParts[1]);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('formats message with images for Anthropic (media first)', () => {
|
|
258
|
+
const message = {
|
|
259
|
+
role: 'user',
|
|
260
|
+
content: 'Check out this image',
|
|
261
|
+
};
|
|
262
|
+
const mediaParts = [
|
|
263
|
+
{
|
|
264
|
+
type: 'image_url',
|
|
265
|
+
image_url: { url: 'https://example.com/image.jpg' },
|
|
266
|
+
},
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
const result = formatMediaMessage({
|
|
270
|
+
message,
|
|
271
|
+
mediaParts,
|
|
272
|
+
endpoint: Providers.ANTHROPIC,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
expect(result.content).toHaveLength(2);
|
|
276
|
+
expect(result.content[0]).toEqual(mediaParts[0]);
|
|
277
|
+
expect(result.content[1]).toEqual({
|
|
278
|
+
type: 'text',
|
|
279
|
+
text: 'Check out this image',
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('formats message with multiple media types', () => {
|
|
284
|
+
const message = {
|
|
285
|
+
role: 'user',
|
|
286
|
+
content: 'Check out these files',
|
|
287
|
+
};
|
|
288
|
+
const mediaParts = [
|
|
289
|
+
{ type: 'document', document: { url: 'https://example.com/doc.pdf' } },
|
|
290
|
+
{ type: 'video', video: { url: 'https://example.com/video.mp4' } },
|
|
291
|
+
{ type: 'audio', audio: { url: 'https://example.com/audio.mp3' } },
|
|
292
|
+
{
|
|
293
|
+
type: 'image_url',
|
|
294
|
+
image_url: { url: 'https://example.com/image.jpg' },
|
|
295
|
+
},
|
|
296
|
+
];
|
|
297
|
+
|
|
298
|
+
const result = formatMediaMessage({ message, mediaParts });
|
|
299
|
+
|
|
300
|
+
expect(result.content).toHaveLength(5);
|
|
301
|
+
expect(result.content[0]).toEqual({
|
|
302
|
+
type: 'text',
|
|
303
|
+
text: 'Check out these files',
|
|
304
|
+
});
|
|
305
|
+
expect(result.content[1]).toEqual(mediaParts[0]);
|
|
306
|
+
expect(result.content[2]).toEqual(mediaParts[1]);
|
|
307
|
+
expect(result.content[3]).toEqual(mediaParts[2]);
|
|
308
|
+
expect(result.content[4]).toEqual(mediaParts[3]);
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
describe('formatMessage with media', () => {
|
|
313
|
+
it('formats user message with image_urls (backward compatibility)', () => {
|
|
314
|
+
const input = {
|
|
315
|
+
message: {
|
|
316
|
+
sender: 'user',
|
|
317
|
+
text: 'Check out this image',
|
|
318
|
+
image_urls: [
|
|
319
|
+
{
|
|
320
|
+
type: 'image_url' as const,
|
|
321
|
+
image_url: { url: 'https://example.com/image.jpg' },
|
|
322
|
+
},
|
|
323
|
+
],
|
|
324
|
+
},
|
|
325
|
+
userName: 'John',
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const result = formatMessage(input);
|
|
329
|
+
|
|
330
|
+
expect(isFormattedMediaMessage(result)).toBe(true);
|
|
331
|
+
if (isFormattedMediaMessage(result)) {
|
|
332
|
+
expect(result.role).toBe('user');
|
|
333
|
+
expect(result.name).toBe('John');
|
|
334
|
+
expect(Array.isArray(result.content)).toBe(true);
|
|
335
|
+
expect(result.content).toHaveLength(2);
|
|
336
|
+
expect(result.content[0]).toEqual({
|
|
337
|
+
type: 'text',
|
|
338
|
+
text: 'Check out this image',
|
|
339
|
+
});
|
|
340
|
+
expect(result.content[1]).toEqual(input.message.image_urls[0]);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('formats user message with documents', () => {
|
|
345
|
+
const input = {
|
|
346
|
+
message: {
|
|
347
|
+
role: 'user',
|
|
348
|
+
content: 'Review this document',
|
|
349
|
+
documents: [
|
|
350
|
+
{
|
|
351
|
+
type: 'document',
|
|
352
|
+
document: { url: 'https://example.com/report.pdf' },
|
|
353
|
+
},
|
|
354
|
+
],
|
|
355
|
+
},
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const result = formatMessage(input);
|
|
359
|
+
|
|
360
|
+
expect(isFormattedMediaMessage(result)).toBe(true);
|
|
361
|
+
if (isFormattedMediaMessage(result)) {
|
|
362
|
+
expect(result.role).toBe('user');
|
|
363
|
+
expect(Array.isArray(result.content)).toBe(true);
|
|
364
|
+
expect(result.content).toHaveLength(2);
|
|
365
|
+
expect(result.content[0]).toEqual({
|
|
366
|
+
type: 'text',
|
|
367
|
+
text: 'Review this document',
|
|
368
|
+
});
|
|
369
|
+
expect(result.content[1]).toEqual(input.message.documents[0]);
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('formats user message with videos', () => {
|
|
374
|
+
const input = {
|
|
375
|
+
message: {
|
|
376
|
+
role: 'user',
|
|
377
|
+
content: 'Watch this video',
|
|
378
|
+
videos: [
|
|
379
|
+
{ type: 'video', video: { url: 'https://example.com/demo.mp4' } },
|
|
380
|
+
],
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const result = formatMessage(input);
|
|
385
|
+
|
|
386
|
+
expect(isFormattedMediaMessage(result)).toBe(true);
|
|
387
|
+
if (isFormattedMediaMessage(result)) {
|
|
388
|
+
expect(result.role).toBe('user');
|
|
389
|
+
expect(Array.isArray(result.content)).toBe(true);
|
|
390
|
+
expect(result.content).toHaveLength(2);
|
|
391
|
+
expect(result.content[0]).toEqual({
|
|
392
|
+
type: 'text',
|
|
393
|
+
text: 'Watch this video',
|
|
394
|
+
});
|
|
395
|
+
expect(result.content[1]).toEqual(input.message.videos[0]);
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('formats user message with audios', () => {
|
|
400
|
+
const input = {
|
|
401
|
+
message: {
|
|
402
|
+
role: 'user',
|
|
403
|
+
content: 'Listen to this',
|
|
404
|
+
audios: [
|
|
405
|
+
{ type: 'audio', audio: { url: 'https://example.com/podcast.mp3' } },
|
|
406
|
+
],
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
const result = formatMessage(input);
|
|
411
|
+
|
|
412
|
+
expect(isFormattedMediaMessage(result)).toBe(true);
|
|
413
|
+
if (isFormattedMediaMessage(result)) {
|
|
414
|
+
expect(result.role).toBe('user');
|
|
415
|
+
expect(Array.isArray(result.content)).toBe(true);
|
|
416
|
+
expect(result.content).toHaveLength(2);
|
|
417
|
+
expect(result.content[0]).toEqual({
|
|
418
|
+
type: 'text',
|
|
419
|
+
text: 'Listen to this',
|
|
420
|
+
});
|
|
421
|
+
expect(result.content[1]).toEqual(input.message.audios[0]);
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('formats user message with all media types in correct order', () => {
|
|
426
|
+
const input = {
|
|
427
|
+
message: {
|
|
428
|
+
role: 'user',
|
|
429
|
+
content: 'Check out all these files',
|
|
430
|
+
documents: [
|
|
431
|
+
{
|
|
432
|
+
type: 'document',
|
|
433
|
+
document: { url: 'https://example.com/doc.pdf' },
|
|
434
|
+
},
|
|
435
|
+
],
|
|
436
|
+
videos: [
|
|
437
|
+
{ type: 'video', video: { url: 'https://example.com/video.mp4' } },
|
|
438
|
+
],
|
|
439
|
+
audios: [
|
|
440
|
+
{ type: 'audio', audio: { url: 'https://example.com/audio.mp3' } },
|
|
441
|
+
],
|
|
442
|
+
image_urls: [
|
|
443
|
+
{
|
|
444
|
+
type: 'image_url' as const,
|
|
445
|
+
image_url: { url: 'https://example.com/image.jpg' },
|
|
446
|
+
},
|
|
447
|
+
],
|
|
448
|
+
},
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const result = formatMessage(input);
|
|
452
|
+
|
|
453
|
+
expect(isFormattedMediaMessage(result)).toBe(true);
|
|
454
|
+
if (isFormattedMediaMessage(result)) {
|
|
455
|
+
expect(result.role).toBe('user');
|
|
456
|
+
expect(Array.isArray(result.content)).toBe(true);
|
|
457
|
+
expect(result.content).toHaveLength(5);
|
|
458
|
+
// Text first
|
|
459
|
+
expect(result.content[0]).toEqual({
|
|
460
|
+
type: 'text',
|
|
461
|
+
text: 'Check out all these files',
|
|
462
|
+
});
|
|
463
|
+
// Then documents, videos, audios, images
|
|
464
|
+
expect(result.content[1]).toEqual(input.message.documents[0]);
|
|
465
|
+
expect(result.content[2]).toEqual(input.message.videos[0]);
|
|
466
|
+
expect(result.content[3]).toEqual(input.message.audios[0]);
|
|
467
|
+
expect(result.content[4]).toEqual(input.message.image_urls[0]);
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it('formats user message with multiple files of the same type', () => {
|
|
472
|
+
const input = {
|
|
473
|
+
message: {
|
|
474
|
+
role: 'user',
|
|
475
|
+
content: 'Review these documents',
|
|
476
|
+
documents: [
|
|
477
|
+
{
|
|
478
|
+
type: 'document',
|
|
479
|
+
document: { url: 'https://example.com/doc1.pdf' },
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
type: 'document',
|
|
483
|
+
document: { url: 'https://example.com/doc2.pdf' },
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
type: 'document',
|
|
487
|
+
document: { url: 'https://example.com/doc3.pdf' },
|
|
488
|
+
},
|
|
489
|
+
],
|
|
490
|
+
},
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
const result = formatMessage(input);
|
|
494
|
+
|
|
495
|
+
expect(isFormattedMediaMessage(result)).toBe(true);
|
|
496
|
+
if (isFormattedMediaMessage(result)) {
|
|
497
|
+
expect(result.content).toHaveLength(4);
|
|
498
|
+
expect(result.content[0].type).toBe('text');
|
|
499
|
+
expect(result.content[1]).toEqual(input.message.documents[0]);
|
|
500
|
+
expect(result.content[2]).toEqual(input.message.documents[1]);
|
|
501
|
+
expect(result.content[3]).toEqual(input.message.documents[2]);
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it('respects Anthropic provider ordering (media before text)', () => {
|
|
506
|
+
const input = {
|
|
507
|
+
message: {
|
|
508
|
+
role: 'user',
|
|
509
|
+
content: 'Check this out',
|
|
510
|
+
documents: [
|
|
511
|
+
{
|
|
512
|
+
type: 'document',
|
|
513
|
+
document: { url: 'https://example.com/doc.pdf' },
|
|
514
|
+
},
|
|
515
|
+
],
|
|
516
|
+
image_urls: [
|
|
517
|
+
{
|
|
518
|
+
type: 'image_url' as const,
|
|
519
|
+
image_url: { url: 'https://example.com/image.jpg' },
|
|
520
|
+
},
|
|
521
|
+
],
|
|
522
|
+
},
|
|
523
|
+
endpoint: Providers.ANTHROPIC,
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
const result = formatMessage(input);
|
|
527
|
+
|
|
528
|
+
expect(isFormattedMediaMessage(result)).toBe(true);
|
|
529
|
+
if (isFormattedMediaMessage(result)) {
|
|
530
|
+
expect(result.content).toHaveLength(3);
|
|
531
|
+
// Media first for Anthropic
|
|
532
|
+
expect(result.content[0]).toEqual(input.message.documents[0]);
|
|
533
|
+
expect(result.content[1]).toEqual(input.message.image_urls[0]);
|
|
534
|
+
expect(result.content[2]).toEqual({
|
|
535
|
+
type: 'text',
|
|
536
|
+
text: 'Check this out',
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
it('does not format media for assistant messages', () => {
|
|
542
|
+
const input = {
|
|
543
|
+
message: {
|
|
544
|
+
role: 'assistant',
|
|
545
|
+
content: 'Here is a response',
|
|
546
|
+
documents: [
|
|
547
|
+
{
|
|
548
|
+
type: 'document',
|
|
549
|
+
document: { url: 'https://example.com/doc.pdf' },
|
|
550
|
+
},
|
|
551
|
+
],
|
|
552
|
+
},
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
const result = formatMessage(input);
|
|
556
|
+
|
|
557
|
+
expect(result).toMatchObject({
|
|
558
|
+
role: 'assistant',
|
|
559
|
+
content: 'Here is a response',
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it('handles empty media arrays gracefully', () => {
|
|
564
|
+
const input = {
|
|
565
|
+
message: {
|
|
566
|
+
role: 'user',
|
|
567
|
+
content: 'Just text',
|
|
568
|
+
documents: [],
|
|
569
|
+
videos: [],
|
|
570
|
+
audios: [],
|
|
571
|
+
image_urls: [],
|
|
572
|
+
},
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
const result = formatMessage(input);
|
|
576
|
+
|
|
577
|
+
expect(result).toMatchObject({
|
|
578
|
+
role: 'user',
|
|
579
|
+
content: 'Just text',
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it('formats media with langChain flag', () => {
|
|
584
|
+
const input = {
|
|
585
|
+
message: {
|
|
586
|
+
role: 'user',
|
|
587
|
+
content: 'Check this image',
|
|
588
|
+
image_urls: [
|
|
589
|
+
{
|
|
590
|
+
type: 'image_url' as const,
|
|
591
|
+
image_url: { url: 'https://example.com/image.jpg' },
|
|
592
|
+
},
|
|
593
|
+
],
|
|
594
|
+
},
|
|
595
|
+
langChain: true,
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
const result = formatMessage(input);
|
|
599
|
+
|
|
600
|
+
expect(result).toBeInstanceOf(HumanMessage);
|
|
601
|
+
expect(Array.isArray(result.lc_kwargs.content)).toBe(true);
|
|
602
|
+
expect(result.lc_kwargs.content).toHaveLength(2);
|
|
603
|
+
});
|
|
604
|
+
});
|
|
605
|
+
|
|
190
606
|
describe('formatLangChainMessages', () => {
|
|
191
607
|
it('formats an array of messages for LangChain', () => {
|
|
192
608
|
const messages = [
|
package/src/messages/index.ts
CHANGED
package/src/messages/prune.ts
CHANGED
|
@@ -389,6 +389,14 @@ export function checkValidNumber(value: unknown): value is number {
|
|
|
389
389
|
return typeof value === 'number' && !isNaN(value) && value > 0;
|
|
390
390
|
}
|
|
391
391
|
|
|
392
|
+
type ThinkingBlocks = {
|
|
393
|
+
thinking_blocks?: Array<{
|
|
394
|
+
type: 'thinking';
|
|
395
|
+
thinking: string;
|
|
396
|
+
signature: string;
|
|
397
|
+
}>;
|
|
398
|
+
};
|
|
399
|
+
|
|
392
400
|
export function createPruneMessages(factoryParams: PruneMessagesFactoryParams) {
|
|
393
401
|
const indexTokenCountMap = { ...factoryParams.indexTokenCountMap };
|
|
394
402
|
let lastTurnStartIndex = factoryParams.startIndex;
|
|
@@ -402,6 +410,49 @@ export function createPruneMessages(factoryParams: PruneMessagesFactoryParams) {
|
|
|
402
410
|
context: BaseMessage[];
|
|
403
411
|
indexTokenCountMap: Record<string, number | undefined>;
|
|
404
412
|
} {
|
|
413
|
+
if (
|
|
414
|
+
factoryParams.provider === Providers.OPENAI &&
|
|
415
|
+
factoryParams.thinkingEnabled === true
|
|
416
|
+
) {
|
|
417
|
+
for (let i = lastTurnStartIndex; i < params.messages.length; i++) {
|
|
418
|
+
const m = params.messages[i];
|
|
419
|
+
if (
|
|
420
|
+
m.getType() === 'ai' &&
|
|
421
|
+
typeof m.additional_kwargs.reasoning_content === 'string' &&
|
|
422
|
+
Array.isArray(
|
|
423
|
+
(
|
|
424
|
+
m.additional_kwargs.provider_specific_fields as
|
|
425
|
+
| ThinkingBlocks
|
|
426
|
+
| undefined
|
|
427
|
+
)?.thinking_blocks
|
|
428
|
+
) &&
|
|
429
|
+
(m as AIMessage).tool_calls &&
|
|
430
|
+
((m as AIMessage).tool_calls?.length ?? 0) > 0
|
|
431
|
+
) {
|
|
432
|
+
const message = m as AIMessage;
|
|
433
|
+
const thinkingBlocks = (
|
|
434
|
+
message.additional_kwargs.provider_specific_fields as ThinkingBlocks
|
|
435
|
+
).thinking_blocks;
|
|
436
|
+
const signature =
|
|
437
|
+
thinkingBlocks?.[thinkingBlocks.length - 1].signature;
|
|
438
|
+
const thinkingBlock: ThinkingContentText = {
|
|
439
|
+
signature,
|
|
440
|
+
type: ContentTypes.THINKING,
|
|
441
|
+
thinking: message.additional_kwargs.reasoning_content as string,
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
params.messages[i] = new AIMessage({
|
|
445
|
+
...message,
|
|
446
|
+
content: [thinkingBlock],
|
|
447
|
+
additional_kwargs: {
|
|
448
|
+
...message.additional_kwargs,
|
|
449
|
+
reasoning_content: undefined,
|
|
450
|
+
},
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
405
456
|
let currentUsage: UsageMetadata | undefined;
|
|
406
457
|
if (
|
|
407
458
|
params.usageMetadata &&
|
package/src/run.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
// src/run.ts
|
|
2
|
+
import './instrumentation';
|
|
3
|
+
import { CallbackHandler } from '@langfuse/langchain';
|
|
2
4
|
import { PromptTemplate } from '@langchain/core/prompts';
|
|
5
|
+
import { RunnableLambda } from '@langchain/core/runnables';
|
|
3
6
|
import { AzureChatOpenAI, ChatOpenAI } from '@langchain/openai';
|
|
4
7
|
import type {
|
|
5
|
-
BaseMessage,
|
|
6
8
|
MessageContentComplex,
|
|
9
|
+
BaseMessage,
|
|
7
10
|
} from '@langchain/core/messages';
|
|
11
|
+
import type { StringPromptValue } from '@langchain/core/prompt_values';
|
|
8
12
|
import type { RunnableConfig } from '@langchain/core/runnables';
|
|
9
13
|
import type * as t from '@/types';
|
|
10
14
|
import {
|
|
@@ -17,6 +21,7 @@ import { createTokenCounter } from '@/utils/tokens';
|
|
|
17
21
|
import { StandardGraph } from '@/graphs/Graph';
|
|
18
22
|
import { HandlerRegistry } from '@/events';
|
|
19
23
|
import { isOpenAILike } from '@/utils/llm';
|
|
24
|
+
import { isPresent } from '@/utils/misc';
|
|
20
25
|
|
|
21
26
|
export const defaultOmitOptions = new Set([
|
|
22
27
|
'stream',
|
|
@@ -239,6 +244,27 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
239
244
|
[Callback.CUSTOM_EVENT]: customEventCallback,
|
|
240
245
|
});
|
|
241
246
|
|
|
247
|
+
if (
|
|
248
|
+
isPresent(process.env.LANGFUSE_SECRET_KEY) &&
|
|
249
|
+
isPresent(process.env.LANGFUSE_PUBLIC_KEY) &&
|
|
250
|
+
isPresent(process.env.LANGFUSE_BASE_URL)
|
|
251
|
+
) {
|
|
252
|
+
const userId = config.configurable?.user_id;
|
|
253
|
+
const sessionId = config.configurable?.thread_id;
|
|
254
|
+
const traceMetadata = {
|
|
255
|
+
messageId: this.id,
|
|
256
|
+
parentMessageId: config.configurable?.requestBody?.parentMessageId,
|
|
257
|
+
};
|
|
258
|
+
const handler = new CallbackHandler({
|
|
259
|
+
userId,
|
|
260
|
+
sessionId,
|
|
261
|
+
traceMetadata,
|
|
262
|
+
});
|
|
263
|
+
config.callbacks = (
|
|
264
|
+
(config.callbacks as t.ProvidedCallbacks) ?? []
|
|
265
|
+
).concat([handler]);
|
|
266
|
+
}
|
|
267
|
+
|
|
242
268
|
if (!this.id) {
|
|
243
269
|
throw new Error('Run ID not provided');
|
|
244
270
|
}
|
|
@@ -313,18 +339,38 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
313
339
|
titleMethod = TitleMethod.COMPLETION,
|
|
314
340
|
titlePromptTemplate,
|
|
315
341
|
}: t.RunTitleOptions): Promise<{ language?: string; title?: string }> {
|
|
342
|
+
if (
|
|
343
|
+
chainOptions != null &&
|
|
344
|
+
isPresent(process.env.LANGFUSE_SECRET_KEY) &&
|
|
345
|
+
isPresent(process.env.LANGFUSE_PUBLIC_KEY) &&
|
|
346
|
+
isPresent(process.env.LANGFUSE_BASE_URL)
|
|
347
|
+
) {
|
|
348
|
+
const userId = chainOptions.configurable?.user_id;
|
|
349
|
+
const sessionId = chainOptions.configurable?.thread_id;
|
|
350
|
+
const traceMetadata = {
|
|
351
|
+
messageId: 'title-' + this.id,
|
|
352
|
+
};
|
|
353
|
+
const handler = new CallbackHandler({
|
|
354
|
+
userId,
|
|
355
|
+
sessionId,
|
|
356
|
+
traceMetadata,
|
|
357
|
+
});
|
|
358
|
+
chainOptions.callbacks = (
|
|
359
|
+
(chainOptions.callbacks as t.ProvidedCallbacks) ?? []
|
|
360
|
+
).concat([handler]);
|
|
361
|
+
}
|
|
362
|
+
|
|
316
363
|
const convoTemplate = PromptTemplate.fromTemplate(
|
|
317
364
|
titlePromptTemplate ?? 'User: {input}\nAI: {output}'
|
|
318
365
|
);
|
|
366
|
+
|
|
319
367
|
const response = contentParts
|
|
320
368
|
.map((part) => {
|
|
321
369
|
if (part?.type === 'text') return part.text;
|
|
322
370
|
return '';
|
|
323
371
|
})
|
|
324
372
|
.join('\n');
|
|
325
|
-
|
|
326
|
-
await convoTemplate.invoke({ input: inputText, output: response })
|
|
327
|
-
).value;
|
|
373
|
+
|
|
328
374
|
const model = this.Graph?.getNewModel({
|
|
329
375
|
provider,
|
|
330
376
|
clientOptions,
|
|
@@ -349,25 +395,51 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
349
395
|
model.n = (clientOptions as t.OpenAIClientOptions | undefined)
|
|
350
396
|
?.n as number;
|
|
351
397
|
}
|
|
352
|
-
|
|
398
|
+
|
|
399
|
+
const convoToTitleInput = new RunnableLambda({
|
|
400
|
+
func: (
|
|
401
|
+
promptValue: StringPromptValue
|
|
402
|
+
): { convo: string; inputText: string; skipLanguage?: boolean } => ({
|
|
403
|
+
convo: promptValue.value,
|
|
404
|
+
inputText,
|
|
405
|
+
skipLanguage,
|
|
406
|
+
}),
|
|
407
|
+
}).withConfig({ runName: 'ConvoTransform' });
|
|
408
|
+
|
|
409
|
+
const titleChain =
|
|
353
410
|
titleMethod === TitleMethod.COMPLETION
|
|
354
411
|
? await createCompletionTitleRunnable(model, titlePrompt)
|
|
355
412
|
: await createTitleRunnable(model, titlePrompt);
|
|
413
|
+
|
|
414
|
+
/** Pipes `convoTemplate` -> `transformer` -> `titleChain` */
|
|
415
|
+
const fullChain = convoTemplate
|
|
416
|
+
.withConfig({ runName: 'ConvoTemplate' })
|
|
417
|
+
.pipe(convoToTitleInput)
|
|
418
|
+
.pipe(titleChain)
|
|
419
|
+
.withConfig({ runName: 'TitleChain' });
|
|
420
|
+
|
|
356
421
|
const invokeConfig = Object.assign({}, chainOptions, {
|
|
357
422
|
run_id: this.id,
|
|
358
423
|
runId: this.id,
|
|
359
424
|
});
|
|
425
|
+
|
|
360
426
|
try {
|
|
361
|
-
return await
|
|
362
|
-
{
|
|
427
|
+
return await fullChain.invoke(
|
|
428
|
+
{ input: inputText, output: response },
|
|
363
429
|
invokeConfig
|
|
364
430
|
);
|
|
365
431
|
} catch (_e) {
|
|
366
432
|
// Fallback: strip callbacks to avoid EventStream tracer errors in certain environments
|
|
433
|
+
// But preserve langfuse handler if it exists
|
|
434
|
+
const langfuseHandler = (invokeConfig.callbacks as t.ProvidedCallbacks)?.find(
|
|
435
|
+
(cb) => cb instanceof CallbackHandler
|
|
436
|
+
);
|
|
367
437
|
const { callbacks: _cb, ...rest } = invokeConfig;
|
|
368
|
-
const safeConfig = Object.assign({}, rest, {
|
|
369
|
-
|
|
370
|
-
|
|
438
|
+
const safeConfig = Object.assign({}, rest, {
|
|
439
|
+
callbacks: langfuseHandler ? [langfuseHandler] : [],
|
|
440
|
+
});
|
|
441
|
+
return await fullChain.invoke(
|
|
442
|
+
{ input: inputText, output: response },
|
|
371
443
|
safeConfig as Partial<RunnableConfig>
|
|
372
444
|
);
|
|
373
445
|
}
|
|
@@ -80,7 +80,7 @@ async function testStandardStreaming(): Promise<void> {
|
|
|
80
80
|
const llmConfig = getLLMConfig(
|
|
81
81
|
Providers.ANTHROPIC
|
|
82
82
|
) as t.AnthropicClientOptions & t.SharedLLMConfig;
|
|
83
|
-
llmConfig.model = 'claude-
|
|
83
|
+
llmConfig.model = 'claude-haiku-4-5';
|
|
84
84
|
|
|
85
85
|
const run = await Run.create<t.IState>({
|
|
86
86
|
runId: 'test-run-id',
|