@librechat/agents 3.7.9 → 3.7.11
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/graphs/Graph.cjs +11 -1
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +36 -3
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/langfuse.cjs +6 -2
- package/dist/cjs/langfuse.cjs.map +1 -1
- package/dist/cjs/run.cjs +5 -1
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +12 -2
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- 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/graphs/Graph.mjs +11 -1
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +36 -3
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/langfuse.mjs +6 -2
- package/dist/esm/langfuse.mjs.map +1 -1
- package/dist/esm/run.mjs +5 -1
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +12 -2
- package/dist/esm/tools/ToolNode.mjs.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/langfuse.d.ts +5 -1
- package/dist/types/tools/ToolNode.d.ts +4 -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/dist/types/types/tools.d.ts +5 -0
- package/package.json +1 -1
- package/src/graphs/Graph.ts +10 -0
- package/src/graphs/MultiAgentGraph.ts +71 -2
- package/src/langfuse.ts +12 -0
- package/src/run.ts +4 -0
- package/src/tools/ToolNode.ts +19 -0
- 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
- package/src/types/tools.ts +5 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import type * as t from './types';
|
|
2
|
+
|
|
3
|
+
/** Distinct failure reasons kept per phase before the tail folds into
|
|
4
|
+
* `other`: enough to characterize a mixed failure without letting a
|
|
5
|
+
* pathological run grow one map entry per source. */
|
|
6
|
+
const MAX_REASON_KEYS = 6;
|
|
7
|
+
/** Failing hosts named in a summary before the rest are counted only. */
|
|
8
|
+
const MAX_FAILURE_SAMPLES = 3;
|
|
9
|
+
/** Cap on the raw provider message carried alongside a summary. */
|
|
10
|
+
const MAX_DETAIL_LENGTH = 200;
|
|
11
|
+
/** Cap on a provider-supplied label placed inside a summary line, so a
|
|
12
|
+
* malformed response cannot stretch a fixed-width line without bound. */
|
|
13
|
+
const MAX_LABEL_LENGTH = 48;
|
|
14
|
+
const OTHER_REASON = 'other';
|
|
15
|
+
|
|
16
|
+
const HTTP_STATUS_REGEX = /\b(?:status(?:\scode)?|http)\D{0,3}(\d{3})\b/i;
|
|
17
|
+
|
|
18
|
+
/** Free-form provider messages have unbounded cardinality; the aggregate only
|
|
19
|
+
* keeps a normalized class so the reason map stays small and comparable. */
|
|
20
|
+
const REASON_PATTERNS: ReadonlyArray<readonly [RegExp, string]> = [
|
|
21
|
+
[/ECONNABORTED|ETIMEDOUT|timeout|timed out/i, 'timeout'],
|
|
22
|
+
[/ENOTFOUND|EAI_AGAIN|getaddrinfo/i, 'dns'],
|
|
23
|
+
[/ECONNREFUSED|ECONNRESET|EPIPE|socket hang up/i, 'connection'],
|
|
24
|
+
[/CERT_|SSL|TLS|self[- ]signed/i, 'tls'],
|
|
25
|
+
[/abort|cancel/i, 'aborted'],
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export const classifyFailure = (message?: string): string => {
|
|
29
|
+
if (message == null || message === '') {
|
|
30
|
+
return 'unknown';
|
|
31
|
+
}
|
|
32
|
+
const status = HTTP_STATUS_REGEX.exec(message);
|
|
33
|
+
if (status != null) {
|
|
34
|
+
return `http_${status[1]}`;
|
|
35
|
+
}
|
|
36
|
+
for (const [pattern, reason] of REASON_PATTERNS) {
|
|
37
|
+
if (pattern.test(message)) {
|
|
38
|
+
return reason;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return OTHER_REASON;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const bump = (counts: Map<string, number>, rawKey: string): void => {
|
|
45
|
+
const key = label(rawKey);
|
|
46
|
+
const existing = counts.get(key);
|
|
47
|
+
if (existing != null) {
|
|
48
|
+
counts.set(key, existing + 1);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (counts.size >= MAX_REASON_KEYS) {
|
|
52
|
+
counts.set(OTHER_REASON, (counts.get(OTHER_REASON) ?? 0) + 1);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
counts.set(key, 1);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const hostOf = (url: string): string => {
|
|
59
|
+
try {
|
|
60
|
+
return new URL(url).hostname.replace(/^www\./, '');
|
|
61
|
+
} catch {
|
|
62
|
+
return url.slice(0, 60);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const formatCount = (value: number): string =>
|
|
67
|
+
value < 10000 ? String(value) : `${(value / 1000).toFixed(1)}k`;
|
|
68
|
+
|
|
69
|
+
const formatMs = (ms: number): string =>
|
|
70
|
+
ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`;
|
|
71
|
+
|
|
72
|
+
const formatMap = (counts: Map<string, number>): string => {
|
|
73
|
+
let out = '';
|
|
74
|
+
for (const [key, count] of counts) {
|
|
75
|
+
out += `${out === '' ? '' : ','}${key}:${count}`;
|
|
76
|
+
}
|
|
77
|
+
return `{${out}}`;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const truncate = (value: string, max: number): string =>
|
|
81
|
+
value.length <= max ? value : `${value.slice(0, max)}…`;
|
|
82
|
+
|
|
83
|
+
/** Newlines, bidi overrides, and other non-printing characters let a remote
|
|
84
|
+
* payload split one summary into several physical lines or forge a second
|
|
85
|
+
* apparent entry, so they never survive into a log message. */
|
|
86
|
+
const UNPRINTABLE = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}"]+/gu;
|
|
87
|
+
|
|
88
|
+
const sanitize = (value: string): string => value.replace(UNPRINTABLE, ' ');
|
|
89
|
+
|
|
90
|
+
/** Every provider-supplied string that reaches a summary line or a map key
|
|
91
|
+
* goes through here: the phases promise one bounded line, and neither a
|
|
92
|
+
* remote payload nor an external caller's label is safe on its own. */
|
|
93
|
+
const label = (value: string): string =>
|
|
94
|
+
truncate(sanitize(value), MAX_LABEL_LENGTH);
|
|
95
|
+
|
|
96
|
+
const detailOf = (value: string): string =>
|
|
97
|
+
truncate(sanitize(value), MAX_DETAIL_LENGTH);
|
|
98
|
+
|
|
99
|
+
/** A phase carries one provider label. Nothing in this package mixes them,
|
|
100
|
+
* but a wrong label is worse than an honest one if that ever changes. */
|
|
101
|
+
const mergeProvider = (current: string, incoming: string): string =>
|
|
102
|
+
current === incoming ? current : 'mixed';
|
|
103
|
+
|
|
104
|
+
interface SearchPhase {
|
|
105
|
+
provider: string;
|
|
106
|
+
queries: number;
|
|
107
|
+
failed: number;
|
|
108
|
+
errors: number;
|
|
109
|
+
durationMs: number;
|
|
110
|
+
types: Map<string, number>;
|
|
111
|
+
reasons: Map<string, number>;
|
|
112
|
+
detail?: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
interface ScrapePhase {
|
|
116
|
+
links: number;
|
|
117
|
+
ok: number;
|
|
118
|
+
empty: number;
|
|
119
|
+
chars: number;
|
|
120
|
+
highlights: number;
|
|
121
|
+
failed: number;
|
|
122
|
+
reasons: Map<string, number>;
|
|
123
|
+
samples: string[];
|
|
124
|
+
detail?: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
interface RerankPhase {
|
|
128
|
+
provider: string;
|
|
129
|
+
model?: string;
|
|
130
|
+
calls: number;
|
|
131
|
+
fallbacks: number;
|
|
132
|
+
errors: number;
|
|
133
|
+
chunks: number;
|
|
134
|
+
maxChunks: number;
|
|
135
|
+
dropped: number;
|
|
136
|
+
topK?: number;
|
|
137
|
+
topKLimit?: number;
|
|
138
|
+
results: number;
|
|
139
|
+
units: number;
|
|
140
|
+
durationMs: number;
|
|
141
|
+
maxDurationMs: number;
|
|
142
|
+
reasons: Map<string, number>;
|
|
143
|
+
error?: t.SafeErrorLog;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Creates the counter set for one `web_search` call.
|
|
148
|
+
*
|
|
149
|
+
* The pipeline reranks once per scraped source and scrapes once per link, so
|
|
150
|
+
* logging at those points scales the log with the result count. Every phase
|
|
151
|
+
* records into fixed-width counters instead and {@link t.SearchMetrics.flush}
|
|
152
|
+
* emits one line per phase, at the highest severity that phase reached.
|
|
153
|
+
*
|
|
154
|
+
* @param autoFlush emit on every record, for a caller with no enclosing search
|
|
155
|
+
* to fold into (a reranker used directly). Recording is synchronous, so the
|
|
156
|
+
* record/flush/reset cycle can never interleave with a concurrent call.
|
|
157
|
+
*/
|
|
158
|
+
export const createSearchMetrics = (
|
|
159
|
+
logger: t.Logger,
|
|
160
|
+
autoFlush = false
|
|
161
|
+
): t.SearchMetrics => {
|
|
162
|
+
let search: SearchPhase | undefined;
|
|
163
|
+
let scrape: ScrapePhase | undefined;
|
|
164
|
+
let rerank: RerankPhase | undefined;
|
|
165
|
+
|
|
166
|
+
const flushSearch = (phase: SearchPhase): void => {
|
|
167
|
+
let line =
|
|
168
|
+
`[web_search] search=${label(phase.provider)}` +
|
|
169
|
+
` queries=${phase.queries}`;
|
|
170
|
+
if (phase.types.size > 0) {
|
|
171
|
+
line += ` results=${formatMap(phase.types)}`;
|
|
172
|
+
}
|
|
173
|
+
line += ` dur=${formatMs(phase.durationMs)}`;
|
|
174
|
+
if (phase.failed === 0) {
|
|
175
|
+
logger.debug(line);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
line += ` failed=${phase.failed} reasons=${formatMap(phase.reasons)}`;
|
|
179
|
+
/** A rejected query was an error-level event before it was aggregated;
|
|
180
|
+
* a provider that merely reported failure was not. */
|
|
181
|
+
if (phase.errors === 0) {
|
|
182
|
+
logger.warn(line);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (phase.detail == null) {
|
|
186
|
+
logger.error(line);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
logger.error(line, phase.detail);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const flushScrape = (phase: ScrapePhase): void => {
|
|
193
|
+
let line =
|
|
194
|
+
`[web_search] scrape links=${phase.links} ok=${phase.ok}` +
|
|
195
|
+
` chars=${formatCount(phase.chars)} highlights=${phase.highlights}`;
|
|
196
|
+
if (phase.empty > 0) {
|
|
197
|
+
line += ` empty=${phase.empty}`;
|
|
198
|
+
}
|
|
199
|
+
if (phase.failed === 0) {
|
|
200
|
+
logger.debug(line);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
line += ` failed=${phase.failed} reasons=${formatMap(phase.reasons)}`;
|
|
204
|
+
if (phase.samples.length > 0) {
|
|
205
|
+
line += ` sample=${phase.samples.join(',')}`;
|
|
206
|
+
}
|
|
207
|
+
if (phase.detail == null) {
|
|
208
|
+
logger.error(line);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
logger.error(line, phase.detail);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const flushRerank = (phase: RerankPhase): void => {
|
|
215
|
+
let line =
|
|
216
|
+
`[web_search] rerank=${label(phase.provider)} calls=${phase.calls}` +
|
|
217
|
+
` chunks=${phase.chunks} maxChunks=${phase.maxChunks}` +
|
|
218
|
+
` results=${phase.results}` +
|
|
219
|
+
` dur=${formatMs(phase.durationMs)}` +
|
|
220
|
+
` maxDur=${formatMs(phase.maxDurationMs)}`;
|
|
221
|
+
if (phase.model != null) {
|
|
222
|
+
/** The only free-form remote value in the line, so it is the one field
|
|
223
|
+
* that gets delimited — a truncated payload cannot then read as
|
|
224
|
+
* further fields. */
|
|
225
|
+
line += ` model="${label(phase.model)}"`;
|
|
226
|
+
}
|
|
227
|
+
if (phase.units > 0) {
|
|
228
|
+
line += ` units=${phase.units}`;
|
|
229
|
+
}
|
|
230
|
+
if (phase.dropped > 0) {
|
|
231
|
+
line += ` dropped=${phase.dropped}`;
|
|
232
|
+
}
|
|
233
|
+
if (phase.topKLimit != null) {
|
|
234
|
+
line += ` topK=${phase.topK} topKLimit=${phase.topKLimit}`;
|
|
235
|
+
}
|
|
236
|
+
if (phase.fallbacks === 0) {
|
|
237
|
+
logger.debug(line);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
line += ` fallbacks=${phase.fallbacks} reasons=${formatMap(phase.reasons)}`;
|
|
241
|
+
/** A placeholder reranker returns input order by design, so a phase whose
|
|
242
|
+
* only fallbacks are placeholders is the configuration working — it stays
|
|
243
|
+
* at the debug level it had before these counters existed. */
|
|
244
|
+
const degraded = phase.fallbacks - (phase.reasons.get('placeholder') ?? 0);
|
|
245
|
+
if (degraded === 0 && phase.errors === 0) {
|
|
246
|
+
logger.debug(line);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (phase.errors === 0) {
|
|
250
|
+
logger.warn(line);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (phase.error == null) {
|
|
254
|
+
logger.error(line);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
logger.error(line, phase.error);
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const flush = (): void => {
|
|
261
|
+
if (search != null) {
|
|
262
|
+
flushSearch(search);
|
|
263
|
+
search = undefined;
|
|
264
|
+
}
|
|
265
|
+
if (scrape != null) {
|
|
266
|
+
flushScrape(scrape);
|
|
267
|
+
scrape = undefined;
|
|
268
|
+
}
|
|
269
|
+
if (rerank != null) {
|
|
270
|
+
flushRerank(rerank);
|
|
271
|
+
rerank = undefined;
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const recordSearch = (observation: t.SearchObservation): void => {
|
|
276
|
+
search ??= {
|
|
277
|
+
provider: observation.provider,
|
|
278
|
+
queries: 0,
|
|
279
|
+
failed: 0,
|
|
280
|
+
errors: 0,
|
|
281
|
+
durationMs: 0,
|
|
282
|
+
types: new Map(),
|
|
283
|
+
reasons: new Map(),
|
|
284
|
+
};
|
|
285
|
+
search.provider = mergeProvider(search.provider, observation.provider);
|
|
286
|
+
search.queries += 1;
|
|
287
|
+
/** Sub-searches run concurrently, so the phase lasts as long as its
|
|
288
|
+
* slowest query rather than the sum of all of them. */
|
|
289
|
+
search.durationMs = Math.max(search.durationMs, observation.durationMs);
|
|
290
|
+
if (observation.error != null) {
|
|
291
|
+
search.failed += 1;
|
|
292
|
+
/** Provider messages are prose: classify before counting, or equivalent
|
|
293
|
+
* failures never collapse and the raw text lands in the summary. */
|
|
294
|
+
bump(
|
|
295
|
+
search.reasons,
|
|
296
|
+
`${observation.type}:${classifyFailure(observation.error)}`
|
|
297
|
+
);
|
|
298
|
+
/** `detail` is only ever logged on the error path, so it is captured
|
|
299
|
+
* only from a thrown observation — otherwise an earlier soft failure's
|
|
300
|
+
* message would be attached to an exception it has nothing to do
|
|
301
|
+
* with, and the real one dropped. */
|
|
302
|
+
if (observation.thrown === true) {
|
|
303
|
+
search.errors += 1;
|
|
304
|
+
search.detail ??= detailOf(observation.error);
|
|
305
|
+
}
|
|
306
|
+
} else if (observation.results > 0) {
|
|
307
|
+
const type = label(observation.type);
|
|
308
|
+
const seen = search.types.get(type) ?? 0;
|
|
309
|
+
search.types.set(type, seen + observation.results);
|
|
310
|
+
}
|
|
311
|
+
if (autoFlush) {
|
|
312
|
+
flush();
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const recordScrape = (observation: t.ScrapeObservation): void => {
|
|
317
|
+
scrape ??= {
|
|
318
|
+
links: 0,
|
|
319
|
+
ok: 0,
|
|
320
|
+
empty: 0,
|
|
321
|
+
chars: 0,
|
|
322
|
+
highlights: 0,
|
|
323
|
+
failed: 0,
|
|
324
|
+
reasons: new Map(),
|
|
325
|
+
samples: [],
|
|
326
|
+
detail: undefined,
|
|
327
|
+
};
|
|
328
|
+
scrape.links += 1;
|
|
329
|
+
if (observation.error != null) {
|
|
330
|
+
scrape.failed += 1;
|
|
331
|
+
const reason = classifyFailure(observation.error);
|
|
332
|
+
bump(scrape.reasons, reason);
|
|
333
|
+
if (scrape.samples.length < MAX_FAILURE_SAMPLES) {
|
|
334
|
+
scrape.samples.push(label(`${hostOf(observation.url)}:${reason}`));
|
|
335
|
+
}
|
|
336
|
+
scrape.detail ??= detailOf(observation.error);
|
|
337
|
+
} else {
|
|
338
|
+
scrape.ok += 1;
|
|
339
|
+
const chars = observation.chars ?? 0;
|
|
340
|
+
scrape.chars += chars;
|
|
341
|
+
scrape.highlights += observation.highlights ?? 0;
|
|
342
|
+
if (chars === 0) {
|
|
343
|
+
scrape.empty += 1;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (autoFlush) {
|
|
347
|
+
flush();
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const recordRerank = (observation: t.RerankObservation): void => {
|
|
352
|
+
rerank ??= {
|
|
353
|
+
provider: observation.provider,
|
|
354
|
+
calls: 0,
|
|
355
|
+
fallbacks: 0,
|
|
356
|
+
errors: 0,
|
|
357
|
+
chunks: 0,
|
|
358
|
+
maxChunks: 0,
|
|
359
|
+
dropped: 0,
|
|
360
|
+
results: 0,
|
|
361
|
+
units: 0,
|
|
362
|
+
durationMs: 0,
|
|
363
|
+
maxDurationMs: 0,
|
|
364
|
+
reasons: new Map(),
|
|
365
|
+
};
|
|
366
|
+
rerank.provider = mergeProvider(rerank.provider, observation.provider);
|
|
367
|
+
rerank.calls += 1;
|
|
368
|
+
rerank.chunks += observation.chunks;
|
|
369
|
+
rerank.maxChunks = Math.max(rerank.maxChunks, observation.chunks);
|
|
370
|
+
rerank.results += observation.results;
|
|
371
|
+
rerank.dropped += observation.dropped ?? 0;
|
|
372
|
+
rerank.topK ??= observation.topK;
|
|
373
|
+
rerank.topKLimit ??= observation.topKLimit;
|
|
374
|
+
rerank.units += observation.units ?? 0;
|
|
375
|
+
rerank.durationMs += observation.durationMs;
|
|
376
|
+
rerank.maxDurationMs = Math.max(
|
|
377
|
+
rerank.maxDurationMs,
|
|
378
|
+
observation.durationMs
|
|
379
|
+
);
|
|
380
|
+
if (observation.model != null) {
|
|
381
|
+
rerank.model =
|
|
382
|
+
rerank.model == null
|
|
383
|
+
? observation.model
|
|
384
|
+
: mergeProvider(rerank.model, observation.model);
|
|
385
|
+
}
|
|
386
|
+
if (observation.reason != null) {
|
|
387
|
+
rerank.fallbacks += 1;
|
|
388
|
+
bump(rerank.reasons, observation.reason);
|
|
389
|
+
}
|
|
390
|
+
if (observation.error != null) {
|
|
391
|
+
rerank.errors += 1;
|
|
392
|
+
rerank.error ??= observation.error;
|
|
393
|
+
}
|
|
394
|
+
if (autoFlush) {
|
|
395
|
+
flush();
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
return { recordSearch, recordScrape, recordRerank, flush };
|
|
400
|
+
};
|