@librechat/agents 3.7.9 → 3.7.10
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/tools/search/metrics.cjs +234 -0
- package/dist/cjs/tools/search/metrics.cjs.map +1 -0
- package/dist/cjs/tools/search/rerankers.cjs +80 -68
- package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
- package/dist/cjs/tools/search/search.cjs +95 -45
- package/dist/cjs/tools/search/search.cjs.map +1 -1
- package/dist/cjs/tools/search/tool.cjs +77 -49
- package/dist/cjs/tools/search/tool.cjs.map +1 -1
- package/dist/cjs/tools/search/utils.cjs.map +1 -1
- package/dist/esm/tools/search/metrics.mjs +234 -0
- package/dist/esm/tools/search/metrics.mjs.map +1 -0
- package/dist/esm/tools/search/rerankers.mjs +80 -68
- package/dist/esm/tools/search/rerankers.mjs.map +1 -1
- package/dist/esm/tools/search/search.mjs +96 -46
- package/dist/esm/tools/search/search.mjs.map +1 -1
- package/dist/esm/tools/search/tool.mjs +77 -49
- package/dist/esm/tools/search/tool.mjs.map +1 -1
- package/dist/esm/tools/search/utils.mjs.map +1 -1
- package/dist/types/tools/search/metrics.d.ts +15 -0
- package/dist/types/tools/search/rerankers.d.ts +30 -5
- package/dist/types/tools/search/tool.d.ts +7 -1
- package/dist/types/tools/search/types.d.ts +88 -4
- package/dist/types/tools/search/utils.d.ts +2 -10
- package/package.json +1 -1
- package/src/tools/search/metrics.ts +400 -0
- package/src/tools/search/rerankers.ts +160 -97
- package/src/tools/search/search.ts +139 -56
- package/src/tools/search/tool.ts +126 -62
- package/src/tools/search/types.ts +104 -4
- package/src/tools/search/utils.ts +2 -10
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
2
|
import type * as t from './types';
|
|
3
3
|
import { createDefaultLogger, formatErrorForLog } from './utils';
|
|
4
|
+
import { createSearchMetrics } from './metrics';
|
|
4
5
|
|
|
5
6
|
const DEFAULT_JINA_API_URL = 'https://api.jina.ai/v1/rerank';
|
|
6
7
|
|
|
@@ -17,16 +18,27 @@ const getDefaultJinaApiUrl = (): string =>
|
|
|
17
18
|
export abstract class BaseReranker {
|
|
18
19
|
protected apiKey: string | undefined;
|
|
19
20
|
protected logger: t.Logger;
|
|
21
|
+
/** Public so a caller that fails before reaching `rerank` can still
|
|
22
|
+
* attribute the attempt to the configured reranker. */
|
|
23
|
+
abstract readonly provider: string;
|
|
24
|
+
private ownMetrics?: t.SearchMetrics;
|
|
20
25
|
|
|
21
26
|
constructor(logger?: t.Logger) {
|
|
22
27
|
// Each specific reranker will set its API key
|
|
23
28
|
this.logger = logger || createDefaultLogger();
|
|
24
29
|
}
|
|
25
30
|
|
|
31
|
+
/**
|
|
32
|
+
* A search reranks once per scraped source, so nothing here logs per call:
|
|
33
|
+
* every exit records one observation through `metrics` and the enclosing
|
|
34
|
+
* search emits a single summary. `metrics` is optional only for direct use
|
|
35
|
+
* of a reranker, which falls back to an auto-flushing collector.
|
|
36
|
+
*/
|
|
26
37
|
abstract rerank(
|
|
27
38
|
query: string,
|
|
28
39
|
documents: string[],
|
|
29
|
-
topK?: number
|
|
40
|
+
topK?: number,
|
|
41
|
+
metrics?: t.SearchMetrics
|
|
30
42
|
): Promise<t.Highlight[]>;
|
|
31
43
|
|
|
32
44
|
protected getDefaultRanking(
|
|
@@ -37,9 +49,76 @@ export abstract class BaseReranker {
|
|
|
37
49
|
.slice(0, Math.min(topK, documents.length))
|
|
38
50
|
.map((doc) => ({ text: doc, score: 0 }));
|
|
39
51
|
}
|
|
52
|
+
|
|
53
|
+
/** A direct caller has no enclosing search to fold into, so one
|
|
54
|
+
* auto-flushing collector per instance emits that call's summary on its
|
|
55
|
+
* own; record and flush run synchronously, so concurrent calls on the same
|
|
56
|
+
* instance cannot share a total. */
|
|
57
|
+
private localMetrics(): t.SearchMetrics {
|
|
58
|
+
this.ownMetrics ??= createSearchMetrics(this.logger, true);
|
|
59
|
+
return this.ownMetrics;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Opens the per-call state every exit path records against. */
|
|
63
|
+
protected beginRerank(
|
|
64
|
+
documents: string[],
|
|
65
|
+
topK: number,
|
|
66
|
+
metrics?: t.SearchMetrics
|
|
67
|
+
): t.RerankRun {
|
|
68
|
+
return {
|
|
69
|
+
metrics: metrics ?? this.localMetrics(),
|
|
70
|
+
documents,
|
|
71
|
+
topK,
|
|
72
|
+
startedAt: Date.now(),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private record(
|
|
77
|
+
run: t.RerankRun,
|
|
78
|
+
highlights: t.Highlight[],
|
|
79
|
+
reason?: t.RerankFallback,
|
|
80
|
+
error?: t.SafeErrorLog
|
|
81
|
+
): t.Highlight[] {
|
|
82
|
+
run.metrics.recordRerank({
|
|
83
|
+
provider: this.provider,
|
|
84
|
+
chunks: run.documents.length,
|
|
85
|
+
results: highlights.length,
|
|
86
|
+
durationMs: Date.now() - run.startedAt,
|
|
87
|
+
model: run.model,
|
|
88
|
+
units: run.units,
|
|
89
|
+
dropped: run.dropped,
|
|
90
|
+
topK: run.topKLimit != null ? run.topK : undefined,
|
|
91
|
+
topKLimit: run.topKLimit,
|
|
92
|
+
reason,
|
|
93
|
+
error,
|
|
94
|
+
});
|
|
95
|
+
return highlights;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
protected complete(
|
|
99
|
+
run: t.RerankRun,
|
|
100
|
+
highlights: t.Highlight[]
|
|
101
|
+
): t.Highlight[] {
|
|
102
|
+
return this.record(run, highlights);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Records the call as a fallback and returns the candidates' input order. */
|
|
106
|
+
protected fallback(
|
|
107
|
+
run: t.RerankRun,
|
|
108
|
+
reason: t.RerankFallback,
|
|
109
|
+
error?: t.SafeErrorLog
|
|
110
|
+
): t.Highlight[] {
|
|
111
|
+
return this.record(
|
|
112
|
+
run,
|
|
113
|
+
this.getDefaultRanking(run.documents, run.topK),
|
|
114
|
+
reason,
|
|
115
|
+
error
|
|
116
|
+
);
|
|
117
|
+
}
|
|
40
118
|
}
|
|
41
119
|
|
|
42
120
|
export class JinaReranker extends BaseReranker {
|
|
121
|
+
readonly provider = 'jina';
|
|
43
122
|
private apiUrl: string;
|
|
44
123
|
private timeout: number;
|
|
45
124
|
private httpAgent?: t.HttpAgent;
|
|
@@ -69,16 +148,14 @@ export class JinaReranker extends BaseReranker {
|
|
|
69
148
|
async rerank(
|
|
70
149
|
query: string,
|
|
71
150
|
documents: string[],
|
|
72
|
-
topK: number = 5
|
|
151
|
+
topK: number = 5,
|
|
152
|
+
metrics?: t.SearchMetrics
|
|
73
153
|
): Promise<t.Highlight[]> {
|
|
74
|
-
this.
|
|
75
|
-
`Reranking ${documents.length} chunks with Jina using API URL: ${this.apiUrl}`
|
|
76
|
-
);
|
|
154
|
+
const run = this.beginRerank(documents, topK, metrics);
|
|
77
155
|
|
|
78
156
|
try {
|
|
79
157
|
if (this.apiKey == null || this.apiKey === '') {
|
|
80
|
-
this.
|
|
81
|
-
return this.getDefaultRanking(documents, topK);
|
|
158
|
+
return this.fallback(run, 'no_api_key');
|
|
82
159
|
}
|
|
83
160
|
|
|
84
161
|
const requestData = {
|
|
@@ -103,11 +180,17 @@ export class JinaReranker extends BaseReranker {
|
|
|
103
180
|
}
|
|
104
181
|
);
|
|
105
182
|
|
|
106
|
-
|
|
107
|
-
|
|
183
|
+
run.model = response.data?.model;
|
|
184
|
+
run.units = response.data?.usage?.total_tokens;
|
|
108
185
|
|
|
109
|
-
|
|
110
|
-
|
|
186
|
+
const results = response.data?.results;
|
|
187
|
+
if (!Array.isArray(results) || results.length === 0) {
|
|
188
|
+
return this.fallback(run, 'bad_response');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return this.complete(
|
|
192
|
+
run,
|
|
193
|
+
results.map((result) => {
|
|
111
194
|
const docIndex = result.index;
|
|
112
195
|
const score = result.relevance_score;
|
|
113
196
|
let text = '';
|
|
@@ -126,22 +209,16 @@ export class JinaReranker extends BaseReranker {
|
|
|
126
209
|
}
|
|
127
210
|
|
|
128
211
|
return { text, score };
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
this.logger.warn(
|
|
132
|
-
'Unexpected response format from Jina API. Using default ranking.'
|
|
133
|
-
);
|
|
134
|
-
return this.getDefaultRanking(documents, topK);
|
|
135
|
-
}
|
|
212
|
+
})
|
|
213
|
+
);
|
|
136
214
|
} catch (error) {
|
|
137
|
-
this.
|
|
138
|
-
// Fallback to default ranking on error
|
|
139
|
-
return this.getDefaultRanking(documents, topK);
|
|
215
|
+
return this.fallback(run, 'error', formatErrorForLog(error));
|
|
140
216
|
}
|
|
141
217
|
}
|
|
142
218
|
}
|
|
143
219
|
|
|
144
220
|
export class CohereReranker extends BaseReranker {
|
|
221
|
+
readonly provider = 'cohere';
|
|
145
222
|
private timeout: number;
|
|
146
223
|
private httpAgent?: t.HttpAgent;
|
|
147
224
|
private httpsAgent?: t.HttpsAgent;
|
|
@@ -167,18 +244,19 @@ export class CohereReranker extends BaseReranker {
|
|
|
167
244
|
async rerank(
|
|
168
245
|
query: string,
|
|
169
246
|
documents: string[],
|
|
170
|
-
topK: number = 5
|
|
247
|
+
topK: number = 5,
|
|
248
|
+
metrics?: t.SearchMetrics
|
|
171
249
|
): Promise<t.Highlight[]> {
|
|
172
|
-
this.
|
|
250
|
+
const run = this.beginRerank(documents, topK, metrics);
|
|
173
251
|
|
|
174
252
|
try {
|
|
175
253
|
if (this.apiKey == null || this.apiKey === '') {
|
|
176
|
-
this.
|
|
177
|
-
return this.getDefaultRanking(documents, topK);
|
|
254
|
+
return this.fallback(run, 'no_api_key');
|
|
178
255
|
}
|
|
179
256
|
|
|
257
|
+
const model = 'rerank-v3.5';
|
|
180
258
|
const requestData = {
|
|
181
|
-
model
|
|
259
|
+
model,
|
|
182
260
|
query: query,
|
|
183
261
|
top_n: topK,
|
|
184
262
|
documents: documents,
|
|
@@ -198,29 +276,25 @@ export class CohereReranker extends BaseReranker {
|
|
|
198
276
|
}
|
|
199
277
|
);
|
|
200
278
|
|
|
201
|
-
|
|
202
|
-
|
|
279
|
+
run.model = model;
|
|
280
|
+
run.units = response.data?.meta?.billed_units?.search_units;
|
|
203
281
|
|
|
204
|
-
|
|
205
|
-
|
|
282
|
+
const results = response.data?.results;
|
|
283
|
+
if (!Array.isArray(results) || results.length === 0) {
|
|
284
|
+
return this.fallback(run, 'bad_response');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return this.complete(
|
|
288
|
+
run,
|
|
289
|
+
results.map((result) => {
|
|
206
290
|
const docIndex = result.index;
|
|
207
291
|
const score = result.relevance_score;
|
|
208
292
|
const text = documents[docIndex];
|
|
209
293
|
return { text, score };
|
|
210
|
-
})
|
|
211
|
-
} else {
|
|
212
|
-
this.logger.warn(
|
|
213
|
-
'Unexpected response format from Cohere API. Using default ranking.'
|
|
214
|
-
);
|
|
215
|
-
return this.getDefaultRanking(documents, topK);
|
|
216
|
-
}
|
|
217
|
-
} catch (error) {
|
|
218
|
-
this.logger.error(
|
|
219
|
-
'Error using Cohere reranker',
|
|
220
|
-
formatErrorForLog(error)
|
|
294
|
+
})
|
|
221
295
|
);
|
|
222
|
-
|
|
223
|
-
return this.
|
|
296
|
+
} catch (error) {
|
|
297
|
+
return this.fallback(run, 'error', formatErrorForLog(error));
|
|
224
298
|
}
|
|
225
299
|
}
|
|
226
300
|
}
|
|
@@ -244,27 +318,23 @@ const toRagApiCandidate = (
|
|
|
244
318
|
|
|
245
319
|
/** A single source split into overlapping chunks routinely exceeds the
|
|
246
320
|
* contract limit, so documents are capped before any candidate is built:
|
|
247
|
-
* only the submittable window is ever allocated.
|
|
321
|
+
* only the submittable window is ever allocated. The overflow is reported
|
|
322
|
+
* through the run's `dropped` counter rather than a per-call log. */
|
|
248
323
|
const buildRagApiCandidates = (
|
|
249
|
-
documents: string[]
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
const clampRagApiTopN = (topN: number, logger: t.Logger): number => {
|
|
324
|
+
documents: string[]
|
|
325
|
+
): t.RagApiRerankCandidate[] =>
|
|
326
|
+
documents.length <= RAG_API_MAX_CANDIDATES
|
|
327
|
+
? documents.map(toRagApiCandidate)
|
|
328
|
+
: documents.slice(0, RAG_API_MAX_CANDIDATES).map(toRagApiCandidate);
|
|
329
|
+
|
|
330
|
+
/** Reports the clamp on `run` rather than logging it per call: a search
|
|
331
|
+
* configured above the contract limit returns fewer highlights than asked
|
|
332
|
+
* for, and the summary is the only place that now says so. */
|
|
333
|
+
const clampRagApiTopN = (topN: number, run: t.RerankRun): number => {
|
|
262
334
|
if (topN <= RAG_API_MAX_TOP_N) {
|
|
263
335
|
return topN;
|
|
264
336
|
}
|
|
265
|
-
|
|
266
|
-
`rag_api fast-v1 accepts top_n <= ${RAG_API_MAX_TOP_N}; clamping ${topN} to ${RAG_API_MAX_TOP_N}.`
|
|
267
|
-
);
|
|
337
|
+
run.topKLimit = RAG_API_MAX_TOP_N;
|
|
268
338
|
return RAG_API_MAX_TOP_N;
|
|
269
339
|
};
|
|
270
340
|
|
|
@@ -349,6 +419,7 @@ const withRerankDeadline = <T>(
|
|
|
349
419
|
* to the candidates' original order via {@link BaseReranker.getDefaultRanking}.
|
|
350
420
|
*/
|
|
351
421
|
export class RagApiReranker extends BaseReranker {
|
|
422
|
+
readonly provider = 'rag-api';
|
|
352
423
|
private baseUrl?: string;
|
|
353
424
|
private tokenSupplier?: t.RagApiTokenSupplier;
|
|
354
425
|
private profile: string;
|
|
@@ -383,32 +454,28 @@ export class RagApiReranker extends BaseReranker {
|
|
|
383
454
|
async rerank(
|
|
384
455
|
query: string,
|
|
385
456
|
documents: string[],
|
|
386
|
-
topK: number = 5
|
|
457
|
+
topK: number = 5,
|
|
458
|
+
metrics?: t.SearchMetrics
|
|
387
459
|
): Promise<t.Highlight[]> {
|
|
388
460
|
if (documents.length === 0) {
|
|
389
461
|
return [];
|
|
390
462
|
}
|
|
391
463
|
|
|
392
|
-
this.
|
|
393
|
-
`Reranking ${documents.length} chunks with rag_api (${this.profile}) using base URL: ${this.baseUrl}`
|
|
394
|
-
);
|
|
464
|
+
const run = this.beginRerank(documents, topK, metrics);
|
|
395
465
|
|
|
396
466
|
const baseUrl = this.baseUrl;
|
|
397
467
|
if (baseUrl == null || baseUrl === '') {
|
|
398
|
-
this.
|
|
399
|
-
return this.getDefaultRanking(documents, topK);
|
|
468
|
+
return this.fallback(run, 'no_base_url');
|
|
400
469
|
}
|
|
401
470
|
|
|
402
471
|
const tokenSupplier = this.tokenSupplier;
|
|
403
472
|
if (tokenSupplier == null) {
|
|
404
|
-
this.
|
|
405
|
-
'No rag_api token supplier configured. Using default ranking.'
|
|
406
|
-
);
|
|
407
|
-
return this.getDefaultRanking(documents, topK);
|
|
473
|
+
return this.fallback(run, 'no_token_supplier');
|
|
408
474
|
}
|
|
409
475
|
|
|
410
|
-
const candidates = buildRagApiCandidates(documents
|
|
411
|
-
const topN = clampRagApiTopN(Math.max(0, topK),
|
|
476
|
+
const candidates = buildRagApiCandidates(documents);
|
|
477
|
+
const topN = clampRagApiTopN(Math.max(0, topK), run);
|
|
478
|
+
run.dropped = documents.length - candidates.length;
|
|
412
479
|
const requestData: t.RagApiRerankRequestBody = {
|
|
413
480
|
profile: this.profile,
|
|
414
481
|
query,
|
|
@@ -436,40 +503,35 @@ export class RagApiReranker extends BaseReranker {
|
|
|
436
503
|
return response.data;
|
|
437
504
|
}, this.timeout);
|
|
438
505
|
|
|
439
|
-
|
|
506
|
+
run.model = data?.model;
|
|
440
507
|
|
|
441
508
|
const rawResults = data?.results;
|
|
442
509
|
if (!Array.isArray(rawResults) || rawResults.length === 0) {
|
|
443
|
-
this.
|
|
444
|
-
'Unexpected response format from rag_api rerank. Using default ranking.'
|
|
445
|
-
);
|
|
446
|
-
return this.getDefaultRanking(documents, topK);
|
|
510
|
+
return this.fallback(run, 'bad_response');
|
|
447
511
|
}
|
|
448
512
|
|
|
449
513
|
if (!isValidRagApiBatch(rawResults, candidates.length)) {
|
|
450
|
-
this.
|
|
451
|
-
'rag_api rerank response contained no valid results. Using default ranking.'
|
|
452
|
-
);
|
|
453
|
-
return this.getDefaultRanking(documents, topK);
|
|
514
|
+
return this.fallback(run, 'invalid_results');
|
|
454
515
|
}
|
|
455
516
|
|
|
456
|
-
return
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
'Error using rag_api reranker',
|
|
465
|
-
formatErrorForLog(error)
|
|
517
|
+
return this.complete(
|
|
518
|
+
run,
|
|
519
|
+
sortRagApiResults(rawResults)
|
|
520
|
+
.slice(0, topN)
|
|
521
|
+
.map((result) => ({
|
|
522
|
+
text: documents[result.index],
|
|
523
|
+
score: result.score,
|
|
524
|
+
}))
|
|
466
525
|
);
|
|
467
|
-
|
|
526
|
+
} catch (error) {
|
|
527
|
+
return this.fallback(run, 'error', formatErrorForLog(error));
|
|
468
528
|
}
|
|
469
529
|
}
|
|
470
530
|
}
|
|
471
531
|
|
|
472
532
|
export class InfinityReranker extends BaseReranker {
|
|
533
|
+
readonly provider = 'infinity';
|
|
534
|
+
|
|
473
535
|
constructor(logger?: t.Logger) {
|
|
474
536
|
super(logger);
|
|
475
537
|
// No API key needed for the placeholder implementation
|
|
@@ -478,13 +540,14 @@ export class InfinityReranker extends BaseReranker {
|
|
|
478
540
|
async rerank(
|
|
479
541
|
query: string,
|
|
480
542
|
documents: string[],
|
|
481
|
-
topK: number = 5
|
|
543
|
+
topK: number = 5,
|
|
544
|
+
metrics?: t.SearchMetrics
|
|
482
545
|
): Promise<t.Highlight[]> {
|
|
483
|
-
this.logger.debug(
|
|
484
|
-
`Reranking ${documents.length} chunks with Infinity (placeholder)`
|
|
485
|
-
);
|
|
486
546
|
// This would be replaced with actual Infinity reranker implementation
|
|
487
|
-
return this.
|
|
547
|
+
return this.fallback(
|
|
548
|
+
this.beginRerank(documents, topK, metrics),
|
|
549
|
+
'placeholder'
|
|
550
|
+
);
|
|
488
551
|
}
|
|
489
552
|
}
|
|
490
553
|
|