@librechat/agents 3.2.56 → 3.2.58
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 +7 -1
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +7 -2
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/langfuseToolOutputTracing.cjs +4 -0
- package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
- package/dist/cjs/langfuseTraceShaping.cjs +172 -0
- package/dist/cjs/langfuseTraceShaping.cjs.map +1 -0
- package/dist/cjs/run.cjs +6 -2
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/stream.cjs +3 -2
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +19 -5
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/search/keenable-search.cjs +68 -0
- package/dist/cjs/tools/search/keenable-search.cjs.map +1 -0
- package/dist/cjs/tools/search/rerankers.cjs +28 -11
- package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
- package/dist/cjs/tools/search/search.cjs +30 -4
- package/dist/cjs/tools/search/search.cjs.map +1 -1
- package/dist/cjs/tools/search/tool.cjs +14 -6
- package/dist/cjs/tools/search/tool.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +12 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/cjs/utils/title.cjs +9 -9
- package/dist/cjs/utils/title.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +7 -1
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +7 -2
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/langfuseToolOutputTracing.mjs +4 -0
- package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
- package/dist/esm/langfuseTraceShaping.mjs +171 -0
- package/dist/esm/langfuseTraceShaping.mjs.map +1 -0
- package/dist/esm/run.mjs +6 -2
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/stream.mjs +3 -2
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +19 -5
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/search/keenable-search.mjs +66 -0
- package/dist/esm/tools/search/keenable-search.mjs.map +1 -0
- package/dist/esm/tools/search/rerankers.mjs +28 -11
- package/dist/esm/tools/search/rerankers.mjs.map +1 -1
- package/dist/esm/tools/search/search.mjs +30 -4
- package/dist/esm/tools/search/search.mjs.map +1 -1
- package/dist/esm/tools/search/tool.mjs +14 -6
- package/dist/esm/tools/search/tool.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +12 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/esm/utils/title.mjs +9 -9
- package/dist/esm/utils/title.mjs.map +1 -1
- package/dist/types/graphs/Graph.d.ts +1 -0
- package/dist/types/langfuseTraceShaping.d.ts +20 -0
- package/dist/types/run.d.ts +1 -0
- package/dist/types/tools/ToolNode.d.ts +11 -1
- package/dist/types/tools/search/keenable-search.d.ts +4 -0
- package/dist/types/tools/search/rerankers.d.ts +7 -2
- package/dist/types/tools/search/types.d.ts +38 -1
- package/dist/types/types/graph.d.ts +22 -0
- package/dist/types/types/run.d.ts +9 -0
- package/dist/types/types/tools.d.ts +6 -0
- package/package.json +1 -1
- package/src/__tests__/stream.eagerEventExecution.test.ts +44 -0
- package/src/agents/AgentContext.ts +9 -0
- package/src/agents/__tests__/AgentContext.test.ts +40 -0
- package/src/graphs/Graph.ts +31 -3
- package/src/graphs/__tests__/composition.smoke.test.ts +53 -0
- package/src/langfuseToolOutputTracing.ts +11 -0
- package/src/langfuseTraceShaping.ts +280 -0
- package/src/llm/anthropic/inherited-stream-events.spec.ts +6 -3
- package/src/run.ts +7 -3
- package/src/specs/langfuse-routing.integration.test.ts +1 -1
- package/src/specs/langfuse-trace-shaping.test.ts +194 -0
- package/src/stream.ts +10 -2
- package/src/tools/ToolNode.ts +28 -4
- package/src/tools/__tests__/SubagentExecutor.test.ts +44 -0
- package/src/tools/__tests__/ToolNode.session.test.ts +160 -0
- package/src/tools/__tests__/hitl.test.ts +85 -0
- package/src/tools/search/jina-reranker.test.ts +70 -1
- package/src/tools/search/keenable-search.ts +98 -0
- package/src/tools/search/keenable.test.ts +183 -0
- package/src/tools/search/rerankers.ts +41 -4
- package/src/tools/search/search.ts +58 -2
- package/src/tools/search/source-processing.test.ts +86 -0
- package/src/tools/search/tool.ts +29 -4
- package/src/tools/search/types.ts +42 -1
- package/src/tools/subagent/SubagentExecutor.ts +11 -0
- package/src/types/graph.ts +22 -0
- package/src/types/run.ts +9 -0
- package/src/types/tools.ts +6 -0
- package/src/utils/title.ts +9 -9
|
@@ -4,6 +4,11 @@ import { createDefaultLogger, formatErrorForLog } from './utils';
|
|
|
4
4
|
|
|
5
5
|
const DEFAULT_JINA_API_URL = 'https://api.jina.ai/v1/rerank';
|
|
6
6
|
|
|
7
|
+
/** Every other network call in the search pipeline is bounded (scrapers,
|
|
8
|
+
* search providers); rerank requests must be too, or a hung rerank API
|
|
9
|
+
* stalls the whole tool. */
|
|
10
|
+
const DEFAULT_RERANKER_TIMEOUT = 10000;
|
|
11
|
+
|
|
7
12
|
const getDefaultJinaApiUrl = (): string =>
|
|
8
13
|
process.env.JINA_API_URL != null && process.env.JINA_API_URL !== ''
|
|
9
14
|
? process.env.JINA_API_URL
|
|
@@ -36,19 +41,23 @@ export abstract class BaseReranker {
|
|
|
36
41
|
|
|
37
42
|
export class JinaReranker extends BaseReranker {
|
|
38
43
|
private apiUrl: string;
|
|
44
|
+
private timeout: number;
|
|
39
45
|
|
|
40
46
|
constructor({
|
|
41
47
|
apiKey = process.env.JINA_API_KEY,
|
|
42
48
|
apiUrl = getDefaultJinaApiUrl(),
|
|
49
|
+
timeout = DEFAULT_RERANKER_TIMEOUT,
|
|
43
50
|
logger,
|
|
44
51
|
}: {
|
|
45
52
|
apiKey?: string;
|
|
46
53
|
apiUrl?: string;
|
|
54
|
+
timeout?: number;
|
|
47
55
|
logger?: t.Logger;
|
|
48
56
|
}) {
|
|
49
57
|
super(logger);
|
|
50
58
|
this.apiKey = apiKey;
|
|
51
59
|
this.apiUrl = apiUrl;
|
|
60
|
+
this.timeout = timeout;
|
|
52
61
|
}
|
|
53
62
|
|
|
54
63
|
async rerank(
|
|
@@ -56,7 +65,9 @@ export class JinaReranker extends BaseReranker {
|
|
|
56
65
|
documents: string[],
|
|
57
66
|
topK: number = 5
|
|
58
67
|
): Promise<t.Highlight[]> {
|
|
59
|
-
this.logger.debug(
|
|
68
|
+
this.logger.debug(
|
|
69
|
+
`Reranking ${documents.length} chunks with Jina using API URL: ${this.apiUrl}`
|
|
70
|
+
);
|
|
60
71
|
|
|
61
72
|
try {
|
|
62
73
|
if (this.apiKey == null || this.apiKey === '') {
|
|
@@ -80,6 +91,7 @@ export class JinaReranker extends BaseReranker {
|
|
|
80
91
|
'Content-Type': 'application/json',
|
|
81
92
|
Authorization: `Bearer ${this.apiKey}`,
|
|
82
93
|
},
|
|
94
|
+
timeout: this.timeout,
|
|
83
95
|
}
|
|
84
96
|
);
|
|
85
97
|
|
|
@@ -122,15 +134,20 @@ export class JinaReranker extends BaseReranker {
|
|
|
122
134
|
}
|
|
123
135
|
|
|
124
136
|
export class CohereReranker extends BaseReranker {
|
|
137
|
+
private timeout: number;
|
|
138
|
+
|
|
125
139
|
constructor({
|
|
126
140
|
apiKey = process.env.COHERE_API_KEY,
|
|
141
|
+
timeout = DEFAULT_RERANKER_TIMEOUT,
|
|
127
142
|
logger,
|
|
128
143
|
}: {
|
|
129
144
|
apiKey?: string;
|
|
145
|
+
timeout?: number;
|
|
130
146
|
logger?: t.Logger;
|
|
131
147
|
}) {
|
|
132
148
|
super(logger);
|
|
133
149
|
this.apiKey = apiKey;
|
|
150
|
+
this.timeout = timeout;
|
|
134
151
|
}
|
|
135
152
|
|
|
136
153
|
async rerank(
|
|
@@ -161,6 +178,7 @@ export class CohereReranker extends BaseReranker {
|
|
|
161
178
|
'Content-Type': 'application/json',
|
|
162
179
|
Authorization: `Bearer ${this.apiKey}`,
|
|
163
180
|
},
|
|
181
|
+
timeout: this.timeout,
|
|
164
182
|
}
|
|
165
183
|
);
|
|
166
184
|
|
|
@@ -218,19 +236,33 @@ export const createReranker = (config: {
|
|
|
218
236
|
jinaApiKey?: string;
|
|
219
237
|
jinaApiUrl?: string;
|
|
220
238
|
cohereApiKey?: string;
|
|
239
|
+
rerankerTimeout?: number;
|
|
221
240
|
logger?: t.Logger;
|
|
222
241
|
}): BaseReranker | undefined => {
|
|
223
|
-
const {
|
|
242
|
+
const {
|
|
243
|
+
rerankerType,
|
|
244
|
+
jinaApiKey,
|
|
245
|
+
jinaApiUrl,
|
|
246
|
+
cohereApiKey,
|
|
247
|
+
rerankerTimeout,
|
|
248
|
+
logger,
|
|
249
|
+
} = config;
|
|
224
250
|
|
|
225
251
|
// Create a default logger if none is provided
|
|
226
252
|
const defaultLogger = logger || createDefaultLogger();
|
|
227
253
|
|
|
228
254
|
switch (rerankerType.toLowerCase()) {
|
|
229
255
|
case 'jina':
|
|
230
|
-
return new JinaReranker({
|
|
256
|
+
return new JinaReranker({
|
|
257
|
+
apiKey: jinaApiKey,
|
|
258
|
+
apiUrl: jinaApiUrl,
|
|
259
|
+
timeout: rerankerTimeout,
|
|
260
|
+
logger: defaultLogger,
|
|
261
|
+
});
|
|
231
262
|
case 'cohere':
|
|
232
263
|
return new CohereReranker({
|
|
233
264
|
apiKey: cohereApiKey,
|
|
265
|
+
timeout: rerankerTimeout,
|
|
234
266
|
logger: defaultLogger,
|
|
235
267
|
});
|
|
236
268
|
case 'infinity':
|
|
@@ -242,7 +274,12 @@ export const createReranker = (config: {
|
|
|
242
274
|
defaultLogger.warn(
|
|
243
275
|
`Unknown reranker type: ${rerankerType}. Defaulting to InfinityReranker.`
|
|
244
276
|
);
|
|
245
|
-
return new JinaReranker({
|
|
277
|
+
return new JinaReranker({
|
|
278
|
+
apiKey: jinaApiKey,
|
|
279
|
+
apiUrl: jinaApiUrl,
|
|
280
|
+
timeout: rerankerTimeout,
|
|
281
|
+
logger: defaultLogger,
|
|
282
|
+
});
|
|
246
283
|
}
|
|
247
284
|
};
|
|
248
285
|
|
|
@@ -2,6 +2,7 @@ import axios from 'axios';
|
|
|
2
2
|
import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters';
|
|
3
3
|
import type * as t from './types';
|
|
4
4
|
import { getAttribution, createDefaultLogger } from './utils';
|
|
5
|
+
import { createKeenableAPI } from './keenable-search';
|
|
5
6
|
import { createTavilyAPI } from './tavily-search';
|
|
6
7
|
import { BaseReranker } from './rerankers';
|
|
7
8
|
|
|
@@ -67,6 +68,43 @@ const chunker = {
|
|
|
67
68
|
};
|
|
68
69
|
|
|
69
70
|
const DEFAULT_MAX_CONTENT_LENGTH = 50000;
|
|
71
|
+
const DEFAULT_CHUNK_SIZE = 150;
|
|
72
|
+
const DEFAULT_CHUNK_OVERLAP = 50;
|
|
73
|
+
|
|
74
|
+
/** Resolves reranker chunking from config, the `SEARCH_CHUNK_SIZE` /
|
|
75
|
+
* `SEARCH_CHUNK_OVERLAP` env vars, or the defaults (150 / 50 chars). The
|
|
76
|
+
* overlap is clamped below the chunk size — `RecursiveCharacterTextSplitter`
|
|
77
|
+
* throws when overlap >= size. */
|
|
78
|
+
function resolveChunkOptions(
|
|
79
|
+
chunkSize?: number,
|
|
80
|
+
chunkOverlap?: number
|
|
81
|
+
): { chunkSize: number; chunkOverlap: number } {
|
|
82
|
+
const resolve = (
|
|
83
|
+
configValue: number | undefined,
|
|
84
|
+
envVar: string,
|
|
85
|
+
fallback: number
|
|
86
|
+
): number => {
|
|
87
|
+
if (configValue != null && configValue > 0) {
|
|
88
|
+
return configValue;
|
|
89
|
+
}
|
|
90
|
+
const envValue = Number(process.env[envVar]);
|
|
91
|
+
if (Number.isFinite(envValue) && envValue > 0) {
|
|
92
|
+
return envValue;
|
|
93
|
+
}
|
|
94
|
+
return fallback;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const size = resolve(chunkSize, 'SEARCH_CHUNK_SIZE', DEFAULT_CHUNK_SIZE);
|
|
98
|
+
let overlap = resolve(
|
|
99
|
+
chunkOverlap,
|
|
100
|
+
'SEARCH_CHUNK_OVERLAP',
|
|
101
|
+
DEFAULT_CHUNK_OVERLAP
|
|
102
|
+
);
|
|
103
|
+
if (overlap >= size) {
|
|
104
|
+
overlap = Math.floor(size / 3);
|
|
105
|
+
}
|
|
106
|
+
return { chunkSize: size, chunkOverlap: overlap };
|
|
107
|
+
}
|
|
70
108
|
|
|
71
109
|
/** Resolves the per-source scraped content cap from config, the
|
|
72
110
|
* `SEARCH_MAX_CONTENT_LENGTH` env var, or the default (50,000 chars) */
|
|
@@ -103,6 +141,7 @@ const getHighlights = async ({
|
|
|
103
141
|
reranker,
|
|
104
142
|
topResults = 5,
|
|
105
143
|
maxContentLength = DEFAULT_MAX_CONTENT_LENGTH,
|
|
144
|
+
chunkOptions,
|
|
106
145
|
logger,
|
|
107
146
|
}: {
|
|
108
147
|
content: string;
|
|
@@ -110,6 +149,7 @@ const getHighlights = async ({
|
|
|
110
149
|
reranker?: BaseReranker;
|
|
111
150
|
topResults?: number;
|
|
112
151
|
maxContentLength?: number;
|
|
152
|
+
chunkOptions?: { chunkSize: number; chunkOverlap: number };
|
|
113
153
|
logger?: t.Logger;
|
|
114
154
|
}): Promise<t.Highlight[] | undefined> => {
|
|
115
155
|
const logger_ = logger || createDefaultLogger();
|
|
@@ -125,7 +165,8 @@ const getHighlights = async ({
|
|
|
125
165
|
|
|
126
166
|
try {
|
|
127
167
|
const documents = await chunker.splitText(
|
|
128
|
-
truncateContent(content, maxContentLength)
|
|
168
|
+
truncateContent(content, maxContentLength),
|
|
169
|
+
chunkOptions
|
|
129
170
|
);
|
|
130
171
|
if (Array.isArray(documents)) {
|
|
131
172
|
return await reranker.rerank(query, documents, topResults);
|
|
@@ -445,6 +486,9 @@ export const createSearchAPI = (
|
|
|
445
486
|
tavilyApiKey,
|
|
446
487
|
tavilySearchUrl,
|
|
447
488
|
tavilySearchOptions,
|
|
489
|
+
keenableApiKey,
|
|
490
|
+
keenableApiUrl,
|
|
491
|
+
keenableSearchOptions,
|
|
448
492
|
} = config;
|
|
449
493
|
|
|
450
494
|
if (searchProvider.toLowerCase() === 'serper') {
|
|
@@ -453,9 +497,15 @@ export const createSearchAPI = (
|
|
|
453
497
|
return createSearXNGAPI(searxngInstanceUrl, searxngApiKey);
|
|
454
498
|
} else if (searchProvider.toLowerCase() === 'tavily') {
|
|
455
499
|
return createTavilyAPI(tavilyApiKey, tavilySearchUrl, tavilySearchOptions);
|
|
500
|
+
} else if (searchProvider.toLowerCase() === 'keenable') {
|
|
501
|
+
return createKeenableAPI(
|
|
502
|
+
keenableApiKey,
|
|
503
|
+
keenableApiUrl,
|
|
504
|
+
keenableSearchOptions
|
|
505
|
+
);
|
|
456
506
|
} else {
|
|
457
507
|
throw new Error(
|
|
458
|
-
`Invalid search provider: ${searchProvider}. Must be 'serper', 'searxng', or '
|
|
508
|
+
`Invalid search provider: ${searchProvider}. Must be 'serper', 'searxng', 'tavily', or 'keenable'`
|
|
459
509
|
);
|
|
460
510
|
}
|
|
461
511
|
};
|
|
@@ -481,6 +531,10 @@ export const createSourceProcessor = (
|
|
|
481
531
|
} = config;
|
|
482
532
|
|
|
483
533
|
const maxContentLength = resolveMaxContentLength(config.maxContentLength);
|
|
534
|
+
const chunkOptions = resolveChunkOptions(
|
|
535
|
+
config.chunkSize,
|
|
536
|
+
config.chunkOverlap
|
|
537
|
+
);
|
|
484
538
|
const logger_ = logger || createDefaultLogger();
|
|
485
539
|
const scraper = scraperInstance;
|
|
486
540
|
|
|
@@ -521,8 +575,10 @@ export const createSourceProcessor = (
|
|
|
521
575
|
const highlights = await getHighlights({
|
|
522
576
|
query,
|
|
523
577
|
reranker,
|
|
578
|
+
topResults,
|
|
524
579
|
content: result.content,
|
|
525
580
|
maxContentLength,
|
|
581
|
+
chunkOptions,
|
|
526
582
|
logger: logger_,
|
|
527
583
|
});
|
|
528
584
|
if (onGetHighlights) {
|
|
@@ -14,6 +14,7 @@ const silentLogger = {
|
|
|
14
14
|
|
|
15
15
|
class RecordingReranker extends BaseReranker {
|
|
16
16
|
public rerankCalls: string[][] = [];
|
|
17
|
+
public topKCalls: number[] = [];
|
|
17
18
|
|
|
18
19
|
constructor() {
|
|
19
20
|
super(silentLogger);
|
|
@@ -25,6 +26,7 @@ class RecordingReranker extends BaseReranker {
|
|
|
25
26
|
topK: number = 5
|
|
26
27
|
): Promise<t.Highlight[]> {
|
|
27
28
|
this.rerankCalls.push(documents);
|
|
29
|
+
this.topKCalls.push(topK);
|
|
28
30
|
return this.getDefaultRanking(documents, topK);
|
|
29
31
|
}
|
|
30
32
|
}
|
|
@@ -212,6 +214,90 @@ describe('createSourceProcessor content capping', () => {
|
|
|
212
214
|
|
|
213
215
|
expect(data.organic?.[0].content?.length).toBe(50000);
|
|
214
216
|
});
|
|
217
|
+
|
|
218
|
+
test('passes configured topResults through to the reranker', async () => {
|
|
219
|
+
const reranker = new RecordingReranker();
|
|
220
|
+
const scraper = createFakeScraper({ [link]: makeLongContent(3000) });
|
|
221
|
+
const processor = createSourceProcessor(
|
|
222
|
+
{ reranker, topResults: 2, logger: silentLogger },
|
|
223
|
+
scraper
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
await processor.processSources({
|
|
227
|
+
...baseFields,
|
|
228
|
+
news: false,
|
|
229
|
+
numElements: 5,
|
|
230
|
+
result: { success: true, data: { organic: [makeOrganic(link)] } },
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
expect(reranker.topKCalls).toEqual([2]);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe('createSourceProcessor reranker chunking', () => {
|
|
238
|
+
const link = 'https://a.com';
|
|
239
|
+
const baseFields = {
|
|
240
|
+
query: 'test query',
|
|
241
|
+
proMode: true,
|
|
242
|
+
onGetHighlights: undefined,
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const runWithConfig = async (
|
|
246
|
+
config: Partial<t.ProcessSourcesConfig>
|
|
247
|
+
): Promise<string[]> => {
|
|
248
|
+
const reranker = new RecordingReranker();
|
|
249
|
+
const scraper = createFakeScraper({ [link]: makeLongContent(10000) });
|
|
250
|
+
const processor = createSourceProcessor(
|
|
251
|
+
{ reranker, logger: silentLogger, ...config },
|
|
252
|
+
scraper
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
await processor.processSources({
|
|
256
|
+
...baseFields,
|
|
257
|
+
news: false,
|
|
258
|
+
numElements: 5,
|
|
259
|
+
result: { success: true, data: { organic: [makeOrganic(link)] } },
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
return reranker.rerankCalls[0] ?? [];
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
test('configured chunkSize produces fewer, larger chunks', async () => {
|
|
266
|
+
const defaultDocs = await runWithConfig({});
|
|
267
|
+
const largeDocs = await runWithConfig({
|
|
268
|
+
chunkSize: 500,
|
|
269
|
+
chunkOverlap: 100,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
expect(defaultDocs.length).toBeGreaterThan(0);
|
|
273
|
+
expect(largeDocs.length).toBeGreaterThan(0);
|
|
274
|
+
expect(largeDocs.length).toBeLessThan(defaultDocs.length);
|
|
275
|
+
expect(Math.max(...defaultDocs.map((d) => d.length))).toBeLessThanOrEqual(
|
|
276
|
+
150
|
|
277
|
+
);
|
|
278
|
+
expect(Math.max(...largeDocs.map((d) => d.length))).toBeLessThanOrEqual(
|
|
279
|
+
500
|
|
280
|
+
);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test('respects SEARCH_CHUNK_SIZE env vars when config is not set', async () => {
|
|
284
|
+
process.env.SEARCH_CHUNK_SIZE = '500';
|
|
285
|
+
process.env.SEARCH_CHUNK_OVERLAP = '100';
|
|
286
|
+
try {
|
|
287
|
+
const docs = await runWithConfig({});
|
|
288
|
+
expect(docs.length).toBeGreaterThan(0);
|
|
289
|
+
expect(Math.max(...docs.map((d) => d.length))).toBeLessThanOrEqual(500);
|
|
290
|
+
} finally {
|
|
291
|
+
delete process.env.SEARCH_CHUNK_SIZE;
|
|
292
|
+
delete process.env.SEARCH_CHUNK_OVERLAP;
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test('clamps overlap below chunk size instead of throwing', async () => {
|
|
297
|
+
const docs = await runWithConfig({ chunkSize: 200, chunkOverlap: 300 });
|
|
298
|
+
expect(docs.length).toBeGreaterThan(0);
|
|
299
|
+
expect(Math.max(...docs.map((d) => d.length))).toBeLessThanOrEqual(200);
|
|
300
|
+
});
|
|
215
301
|
});
|
|
216
302
|
|
|
217
303
|
describe('createSourceProcessor topStories capping', () => {
|
package/src/tools/search/tool.ts
CHANGED
|
@@ -197,13 +197,17 @@ export async function executeParallelSearches({
|
|
|
197
197
|
function createSearchProcessor({
|
|
198
198
|
searchAPI,
|
|
199
199
|
safeSearch,
|
|
200
|
+
supportsImages,
|
|
200
201
|
supportsVideos,
|
|
202
|
+
supportsNews,
|
|
201
203
|
sourceProcessor,
|
|
202
204
|
onGetHighlights,
|
|
203
205
|
logger,
|
|
204
206
|
}: {
|
|
205
207
|
safeSearch: t.SearchToolConfig['safeSearch'];
|
|
208
|
+
supportsImages: boolean;
|
|
206
209
|
supportsVideos: boolean;
|
|
210
|
+
supportsNews: boolean;
|
|
207
211
|
searchAPI: ReturnType<typeof createSearchAPI>;
|
|
208
212
|
sourceProcessor: ReturnType<typeof createSourceProcessor>;
|
|
209
213
|
onGetHighlights: t.SearchToolConfig['onGetHighlights'];
|
|
@@ -238,9 +242,9 @@ function createSearchProcessor({
|
|
|
238
242
|
date,
|
|
239
243
|
country,
|
|
240
244
|
safeSearch,
|
|
241
|
-
images,
|
|
245
|
+
images: supportsImages && images,
|
|
242
246
|
videos: supportsVideos && videos,
|
|
243
|
-
news,
|
|
247
|
+
news: supportsNews && news,
|
|
244
248
|
logger,
|
|
245
249
|
});
|
|
246
250
|
|
|
@@ -315,7 +319,11 @@ function createTool({
|
|
|
315
319
|
}),
|
|
316
320
|
});
|
|
317
321
|
const turn = runnableConfig.toolCall?.turn ?? 0;
|
|
318
|
-
const { output, references } = formatResultsForLLM(
|
|
322
|
+
const { output, references } = formatResultsForLLM(
|
|
323
|
+
turn,
|
|
324
|
+
searchResult,
|
|
325
|
+
maxOutputChars
|
|
326
|
+
);
|
|
319
327
|
const data: t.SearchResultData = { turn, ...searchResult, references };
|
|
320
328
|
return [output, { [Constants.WEB_SEARCH]: data }];
|
|
321
329
|
},
|
|
@@ -358,9 +366,15 @@ export const createSearchTool = (
|
|
|
358
366
|
tavilySearchUrl,
|
|
359
367
|
tavilyExtractUrl,
|
|
360
368
|
tavilySearchOptions,
|
|
369
|
+
keenableApiKey,
|
|
370
|
+
keenableApiUrl,
|
|
371
|
+
keenableSearchOptions,
|
|
361
372
|
rerankerType = 'cohere',
|
|
373
|
+
rerankerTimeout,
|
|
362
374
|
topResults = 5,
|
|
363
375
|
maxContentLength,
|
|
376
|
+
chunkSize,
|
|
377
|
+
chunkOverlap,
|
|
364
378
|
maxOutputChars,
|
|
365
379
|
strategies = ['no_extraction'],
|
|
366
380
|
filterContent = true,
|
|
@@ -415,6 +429,9 @@ export const createSearchTool = (
|
|
|
415
429
|
tavilyApiKey,
|
|
416
430
|
tavilySearchUrl,
|
|
417
431
|
tavilySearchOptions: effectiveTavilySearchOptions,
|
|
432
|
+
keenableApiKey,
|
|
433
|
+
keenableApiUrl,
|
|
434
|
+
keenableSearchOptions,
|
|
418
435
|
});
|
|
419
436
|
|
|
420
437
|
/** Create scraper based on scraperProvider */
|
|
@@ -455,6 +472,7 @@ export const createSearchTool = (
|
|
|
455
472
|
jinaApiKey,
|
|
456
473
|
jinaApiUrl,
|
|
457
474
|
cohereApiKey,
|
|
475
|
+
rerankerTimeout,
|
|
458
476
|
logger,
|
|
459
477
|
});
|
|
460
478
|
|
|
@@ -467,6 +485,8 @@ export const createSearchTool = (
|
|
|
467
485
|
reranker: selectedReranker,
|
|
468
486
|
topResults,
|
|
469
487
|
maxContentLength,
|
|
488
|
+
chunkSize,
|
|
489
|
+
chunkOverlap,
|
|
470
490
|
strategies,
|
|
471
491
|
filterContent,
|
|
472
492
|
logger,
|
|
@@ -477,7 +497,12 @@ export const createSearchTool = (
|
|
|
477
497
|
const search = createSearchProcessor({
|
|
478
498
|
searchAPI,
|
|
479
499
|
safeSearch,
|
|
480
|
-
|
|
500
|
+
// Keenable is organic-only: its API ignores `type`, so image/news
|
|
501
|
+
// sub-searches would spend rate limit and merge nothing.
|
|
502
|
+
supportsImages: searchProvider !== 'keenable',
|
|
503
|
+
supportsVideos:
|
|
504
|
+
searchProvider !== 'tavily' && searchProvider !== 'keenable',
|
|
505
|
+
supportsNews: searchProvider !== 'keenable',
|
|
481
506
|
sourceProcessor,
|
|
482
507
|
onGetHighlights,
|
|
483
508
|
logger,
|
|
@@ -3,7 +3,7 @@ import type { Logger as WinstonLogger } from 'winston';
|
|
|
3
3
|
import type { BaseReranker } from './rerankers';
|
|
4
4
|
import { DATE_RANGE } from './schema';
|
|
5
5
|
|
|
6
|
-
export type SearchProvider = 'serper' | 'searxng' | 'tavily';
|
|
6
|
+
export type SearchProvider = 'serper' | 'searxng' | 'tavily' | 'keenable';
|
|
7
7
|
export type ScraperProvider = 'firecrawl' | 'serper' | 'tavily';
|
|
8
8
|
export type RerankerType = 'infinity' | 'jina' | 'cohere' | 'none';
|
|
9
9
|
|
|
@@ -115,6 +115,36 @@ export interface SearchConfig {
|
|
|
115
115
|
tavilySearchUrl?: string;
|
|
116
116
|
tavilyExtractUrl?: string;
|
|
117
117
|
tavilySearchOptions?: TavilySearchOptions;
|
|
118
|
+
keenableApiKey?: string;
|
|
119
|
+
keenableApiUrl?: string;
|
|
120
|
+
keenableSearchOptions?: KeenableSearchOptions;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface KeenableSearchOptions {
|
|
124
|
+
maxResults?: number;
|
|
125
|
+
/** Restrict results to a single domain, e.g. "github.com". */
|
|
126
|
+
site?: string;
|
|
127
|
+
/** Sent as the X-Keenable-Title attribution header. Defaults to "LibreChat". */
|
|
128
|
+
attributionTitle?: string;
|
|
129
|
+
timeout?: number;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface KeenableSearchPayload {
|
|
133
|
+
query: string;
|
|
134
|
+
site?: string;
|
|
135
|
+
published_after?: string;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface KeenableSearchResult {
|
|
139
|
+
title?: string;
|
|
140
|
+
url?: string;
|
|
141
|
+
description?: string;
|
|
142
|
+
snippet?: string;
|
|
143
|
+
published_at?: string;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface KeenableSearchResponse {
|
|
147
|
+
results?: KeenableSearchResult[];
|
|
118
148
|
}
|
|
119
149
|
|
|
120
150
|
export type References = {
|
|
@@ -137,6 +167,15 @@ export interface ProcessSourcesConfig {
|
|
|
137
167
|
* chunker/reranker. Defaults to 50,000; also configurable via the
|
|
138
168
|
* `SEARCH_MAX_CONTENT_LENGTH` env var. */
|
|
139
169
|
maxContentLength?: number;
|
|
170
|
+
/** Chunk size (chars) for splitting scraped content before reranking.
|
|
171
|
+
* Defaults to 150; also configurable via the `SEARCH_CHUNK_SIZE` env var.
|
|
172
|
+
* Larger chunks send fewer documents to the reranker (lower cost/latency);
|
|
173
|
+
* highlights are expanded ±300-450 chars around each hit either way. */
|
|
174
|
+
chunkSize?: number;
|
|
175
|
+
/** Overlap (chars) between adjacent chunks. Defaults to 50; also
|
|
176
|
+
* configurable via the `SEARCH_CHUNK_OVERLAP` env var. Clamped below
|
|
177
|
+
* `chunkSize`. */
|
|
178
|
+
chunkOverlap?: number;
|
|
140
179
|
strategies?: string[];
|
|
141
180
|
filterContent?: boolean;
|
|
142
181
|
reranker?: BaseReranker;
|
|
@@ -231,6 +270,8 @@ export interface SearchToolConfig
|
|
|
231
270
|
jinaApiUrl?: string;
|
|
232
271
|
cohereApiKey?: string;
|
|
233
272
|
rerankerType?: RerankerType;
|
|
273
|
+
/** Timeout (ms) for rerank API requests. Defaults to 10,000. */
|
|
274
|
+
rerankerTimeout?: number;
|
|
234
275
|
scraperProvider?: ScraperProvider;
|
|
235
276
|
scraperTimeout?: number;
|
|
236
277
|
serperScraperOptions?: SerperScraperConfig;
|
|
@@ -1422,6 +1422,17 @@ export function buildChildInputs(
|
|
|
1422
1422
|
*/
|
|
1423
1423
|
initialSummary: undefined,
|
|
1424
1424
|
discoveredTools: undefined,
|
|
1425
|
+
/**
|
|
1426
|
+
* Host-supplied direct tools are scrubbed from INHERITED configs only.
|
|
1427
|
+
* A self-spawn config's `agentInputs` is a shallow spread of the parent's
|
|
1428
|
+
* `_sourceInputs`, so without this a parent-scoped graph tool (e.g. an
|
|
1429
|
+
* interrupt-raising ask_user_question, which needs the parent's
|
|
1430
|
+
* checkpointer — child graphs compile without one) would silently leak
|
|
1431
|
+
* into the child and deterministically throw `No checkpointer set`. An
|
|
1432
|
+
* EXPLICIT child config that lists its own `graphTools` is a deliberate
|
|
1433
|
+
* host choice and keeps them (Codex #289 P2).
|
|
1434
|
+
*/
|
|
1435
|
+
graphTools: config.self === true ? undefined : agentInputs.graphTools,
|
|
1425
1436
|
};
|
|
1426
1437
|
|
|
1427
1438
|
if (config.allowNested === true) {
|
package/src/types/graph.ts
CHANGED
|
@@ -610,6 +610,28 @@ export interface AgentInputs {
|
|
|
610
610
|
subagentConfigs?: SubagentConfig[];
|
|
611
611
|
/** Maximum subagent nesting depth. Default 1 means top-level agents can spawn subagents but subagents cannot nest further. */
|
|
612
612
|
maxSubagentDepth?: number;
|
|
613
|
+
/**
|
|
614
|
+
* Host-supplied tool instances that must execute IN-PROCESS inside the graph's
|
|
615
|
+
* ToolNode even when the run is event-driven (`toolDefinitions` non-empty). Each
|
|
616
|
+
* instance is bound to the model alongside the schema-only event tools and its
|
|
617
|
+
* name is marked direct, so calls bypass ON_TOOL_EXECUTE dispatch and run inside
|
|
618
|
+
* the Pregel task frame. This is the only execution mode where a tool body may
|
|
619
|
+
* raise a LangGraph `interrupt()` (e.g. a tool built on `askUserQuestion()`) —
|
|
620
|
+
* the host-side event handler runs outside the graph task, where `interrupt()`
|
|
621
|
+
* throws. Do NOT also list these tools in `toolDefinitions` (they would be bound
|
|
622
|
+
* twice). NOT inherited by SELF-SPAWNED subagent children (their config is a
|
|
623
|
+
* shallow spread of the parent's inputs, and child graphs compile without a
|
|
624
|
+
* checkpointer, so an interrupt-capable tool could never pause there) —
|
|
625
|
+
* `buildChildInputs` scrubs the inherited copy; an EXPLICIT child config that
|
|
626
|
+
* lists its own `graphTools` keeps them.
|
|
627
|
+
*
|
|
628
|
+
* Deliberately `GenericTool[]`, not `GraphTools`: the wider union admits
|
|
629
|
+
* schema-only shapes (OpenAI `BindToolsInput`, Google tool objects) that
|
|
630
|
+
* `initializeTools` cannot register in the ToolNode direct map — the model
|
|
631
|
+
* would bind a tool the SDK advertised as in-process but cannot execute.
|
|
632
|
+
* Every entry must be a real executable tool instance with a `name`.
|
|
633
|
+
*/
|
|
634
|
+
graphTools?: GenericTool[];
|
|
613
635
|
}
|
|
614
636
|
|
|
615
637
|
export interface ContextPruningConfig {
|
package/src/types/run.ts
CHANGED
|
@@ -184,6 +184,15 @@ export type RunConfig = {
|
|
|
184
184
|
* eager dispatch unsafe.
|
|
185
185
|
*/
|
|
186
186
|
eagerEventToolExecution?: EagerEventToolExecutionConfig;
|
|
187
|
+
/**
|
|
188
|
+
* Names of host tools that write to the code-execution sandbox but are not
|
|
189
|
+
* built-in `CODE_EXECUTION_TOOLS` (e.g. LibreChat's create_file/edit_file).
|
|
190
|
+
* Their successful results fold the returned exec `session_id` into the
|
|
191
|
+
* shared code session, so a file such a tool writes is visible to later
|
|
192
|
+
* bash_tool/execute_code calls running in the same sandbox. Host-declared so
|
|
193
|
+
* the SDK stays name-agnostic.
|
|
194
|
+
*/
|
|
195
|
+
codeSessionToolNames?: string[];
|
|
187
196
|
/**
|
|
188
197
|
* Selects the execution backend for built-in code tools. Omit this to keep
|
|
189
198
|
* the remote LibreChat Code API sandbox. Set `{ engine: 'local' }` to run
|
package/src/types/tools.ts
CHANGED
|
@@ -110,6 +110,12 @@ export type ToolNodeOptions = {
|
|
|
110
110
|
directToolNames?: Set<string>;
|
|
111
111
|
/** Opt-in eager execution for event-driven tool calls. */
|
|
112
112
|
eagerEventToolExecution?: EagerEventToolExecutionConfig;
|
|
113
|
+
/**
|
|
114
|
+
* Host tool names that write to the code-execution sandbox but are not
|
|
115
|
+
* built-in `CODE_EXECUTION_TOOLS`. Their exec `session_id` is folded into the
|
|
116
|
+
* shared code session so later bash_tool/execute_code calls see written files.
|
|
117
|
+
*/
|
|
118
|
+
codeSessionToolNames?: string[];
|
|
113
119
|
/** Shared per-run eager execution registry populated by the stream handler. */
|
|
114
120
|
eagerEventToolExecutions?: Map<string, EagerEventToolExecution>;
|
|
115
121
|
/** Shared per-run per-tool turn counter used by eager and normal event dispatch. */
|
package/src/utils/title.ts
CHANGED
|
@@ -53,7 +53,7 @@ export const createTitleRunnable = async (
|
|
|
53
53
|
|
|
54
54
|
const titlePrompt = ChatPromptTemplate.fromTemplate(
|
|
55
55
|
_titlePrompt ?? defaultTitlePrompt
|
|
56
|
-
).withConfig({ runName: '
|
|
56
|
+
).withConfig({ runName: 'BuildTitlePrompt' });
|
|
57
57
|
|
|
58
58
|
const titleOnlyInnerChain = RunnableSequence.from([titlePrompt, titleLLM]);
|
|
59
59
|
const combinedInnerChain = RunnableSequence.from([titlePrompt, combinedLLM]);
|
|
@@ -67,7 +67,7 @@ export const createTitleRunnable = async (
|
|
|
67
67
|
const result = await titleOnlyInnerChain.invoke(input, config);
|
|
68
68
|
return result as { title: string };
|
|
69
69
|
},
|
|
70
|
-
}).withConfig({ runName: '
|
|
70
|
+
}).withConfig({ runName: 'GenerateTitleOnly' });
|
|
71
71
|
|
|
72
72
|
/** Wrap combinedChain in RunnableLambda to create parent span */
|
|
73
73
|
const combinedChain = new RunnableLambda({
|
|
@@ -78,7 +78,7 @@ export const createTitleRunnable = async (
|
|
|
78
78
|
const result = await combinedInnerChain.invoke(input, config);
|
|
79
79
|
return result as { language: string; title: string };
|
|
80
80
|
},
|
|
81
|
-
}).withConfig({ runName: '
|
|
81
|
+
}).withConfig({ runName: 'GenerateTitleAndDetectLanguage' });
|
|
82
82
|
|
|
83
83
|
/** Runnable to add default values if needed */
|
|
84
84
|
const addDefaults = new RunnableLambda({
|
|
@@ -88,7 +88,7 @@ export const createTitleRunnable = async (
|
|
|
88
88
|
language: result?.language ?? 'English',
|
|
89
89
|
title: result?.title ?? '',
|
|
90
90
|
}),
|
|
91
|
-
}).withConfig({ runName: '
|
|
91
|
+
}).withConfig({ runName: 'ApplyTitleDefaults' });
|
|
92
92
|
|
|
93
93
|
const combinedChainInner = RunnableSequence.from([
|
|
94
94
|
combinedChain,
|
|
@@ -103,7 +103,7 @@ export const createTitleRunnable = async (
|
|
|
103
103
|
): Promise<{ language: string; title: string }> => {
|
|
104
104
|
return await combinedChainInner.invoke(input, config);
|
|
105
105
|
},
|
|
106
|
-
}).withConfig({ runName: '
|
|
106
|
+
}).withConfig({ runName: 'GenerateTitleAndDetectLanguageWithDefaults' });
|
|
107
107
|
|
|
108
108
|
return new RunnableLambda({
|
|
109
109
|
func: async (
|
|
@@ -124,7 +124,7 @@ export const createTitleRunnable = async (
|
|
|
124
124
|
|
|
125
125
|
return await combinedChainWithDefaults.invoke(invokeInput, config);
|
|
126
126
|
},
|
|
127
|
-
}).withConfig({ runName: '
|
|
127
|
+
}).withConfig({ runName: 'GenerateTitle' });
|
|
128
128
|
};
|
|
129
129
|
|
|
130
130
|
const defaultCompletionPrompt = `Provide a concise, 5-word-or-less title for the conversation, using title case conventions. Only return the title itself.
|
|
@@ -138,7 +138,7 @@ export const createCompletionTitleRunnable = async (
|
|
|
138
138
|
): Promise<Runnable> => {
|
|
139
139
|
const completionPrompt = ChatPromptTemplate.fromTemplate(
|
|
140
140
|
titlePrompt ?? defaultCompletionPrompt
|
|
141
|
-
).withConfig({ runName: '
|
|
141
|
+
).withConfig({ runName: 'BuildTitlePrompt' });
|
|
142
142
|
|
|
143
143
|
/** Runnable to extract content from model response */
|
|
144
144
|
const extractContent = new RunnableLambda({
|
|
@@ -157,7 +157,7 @@ export const createCompletionTitleRunnable = async (
|
|
|
157
157
|
}
|
|
158
158
|
return { title: content.trim() };
|
|
159
159
|
},
|
|
160
|
-
}).withConfig({ runName: '
|
|
160
|
+
}).withConfig({ runName: 'ParseTitleFromResponse' });
|
|
161
161
|
|
|
162
162
|
const innerChain = RunnableSequence.from([
|
|
163
163
|
completionPrompt,
|
|
@@ -173,5 +173,5 @@ export const createCompletionTitleRunnable = async (
|
|
|
173
173
|
): Promise<{ title: string }> => {
|
|
174
174
|
return await innerChain.invoke(input, config);
|
|
175
175
|
},
|
|
176
|
-
}).withConfig({ runName: '
|
|
176
|
+
}).withConfig({ runName: 'GenerateTitle' });
|
|
177
177
|
};
|