@stackfactor/agent-utils 1.11.0 → 1.12.0
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/README.md +24 -0
- package/dist/cjs/index.d.ts +2 -2
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +3 -2
- package/dist/cjs/langChain.d.ts +8 -0
- package/dist/cjs/langChain.d.ts.map +1 -1
- package/dist/cjs/langChain.js +3 -2
- package/dist/cjs/referenceContext.d.ts +62 -15
- package/dist/cjs/referenceContext.d.ts.map +1 -1
- package/dist/cjs/referenceContext.js +134 -34
- package/dist/cjs/serve.d.ts.map +1 -1
- package/dist/cjs/serve.js +7 -0
- package/dist/cjs/stackfactorKnowledge.d.ts +9 -4
- package/dist/cjs/stackfactorKnowledge.d.ts.map +1 -1
- package/dist/cjs/stackfactorKnowledge.js +30 -6
- package/dist/cjs/tavily.d.ts +8 -0
- package/dist/cjs/tavily.d.ts.map +1 -1
- package/dist/cjs/tavily.js +73 -4
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +2 -2
- package/dist/esm/langChain.d.ts +8 -0
- package/dist/esm/langChain.d.ts.map +1 -1
- package/dist/esm/langChain.js +3 -2
- package/dist/esm/referenceContext.d.ts +62 -15
- package/dist/esm/referenceContext.d.ts.map +1 -1
- package/dist/esm/referenceContext.js +132 -33
- package/dist/esm/serve.d.ts.map +1 -1
- package/dist/esm/serve.js +7 -0
- package/dist/esm/stackfactorKnowledge.d.ts +9 -4
- package/dist/esm/stackfactorKnowledge.d.ts.map +1 -1
- package/dist/esm/stackfactorKnowledge.js +30 -6
- package/dist/esm/tavily.d.ts +8 -0
- package/dist/esm/tavily.d.ts.map +1 -1
- package/dist/esm/tavily.js +73 -4
- package/package.json +1 -1
|
@@ -11,6 +11,12 @@
|
|
|
11
11
|
// references cite, searched under the run's own token, so a run reads only
|
|
12
12
|
// what the person it acts for may open.
|
|
13
13
|
//
|
|
14
|
+
// A cited knowledge base that cannot be read stops the run (Candin,
|
|
15
|
+
// 2026-10-13): with no token, a refusal or a failed search, the gathering
|
|
16
|
+
// throws a KnowledgeAccessError naming the documents, and the agent answers
|
|
17
|
+
// with it rather than writing as if the content cited nothing. A search that
|
|
18
|
+
// works and finds nothing is not an error.
|
|
19
|
+
//
|
|
14
20
|
// A reference is cited by its short id `r<N>`, N being its index in the
|
|
15
21
|
// supplied list, whatever its kind.
|
|
16
22
|
//
|
|
@@ -18,9 +24,43 @@ import logger from "./logger.js";
|
|
|
18
24
|
import { STACKFACTOR_REFERENCE, retrievePassages, sourcesFromReferences, } from "./stackfactorKnowledge.js";
|
|
19
25
|
/** How many passages one search asks for when the caller does not say. */
|
|
20
26
|
const DEFAULT_REFERENCE_TOP_K = 5;
|
|
27
|
+
/** How many passages a gathering keeps when the caller does not say: the best-ranked. */
|
|
28
|
+
const DEFAULT_MAX_PASSAGES = 40;
|
|
21
29
|
/** The prompt key the passages are handed to the model under. */
|
|
22
30
|
export const REFERENCE_PASSAGES_KEY = "Knowledge Base Passages";
|
|
23
31
|
const text = (value) => (typeof value === "string" ? value.trim() : "");
|
|
32
|
+
/**
|
|
33
|
+
* The knowledge base documents a run's references cite could not be read: the
|
|
34
|
+
* run has no token, the person may not open a cited knowledge base (401, 403,
|
|
35
|
+
* 404), or its search failed. The agent answers with it — `code` and `message`
|
|
36
|
+
* are what the person sees — and generates nothing.
|
|
37
|
+
*/
|
|
38
|
+
export class KnowledgeAccessError extends Error {
|
|
39
|
+
/** 403 when every skipped knowledge base refused the person (or there was no token), else 502. */
|
|
40
|
+
code;
|
|
41
|
+
/** The knowledge bases that could not be searched, and why; empty when there was no token. */
|
|
42
|
+
skipped;
|
|
43
|
+
/** The titles of the STACKFACTOR references citing those knowledge bases, each once. */
|
|
44
|
+
documents;
|
|
45
|
+
/**
|
|
46
|
+
* @param details `skipped` (from the search) and `documents` (titles); the
|
|
47
|
+
* code and the message follow from them
|
|
48
|
+
*/
|
|
49
|
+
constructor(details = {}) {
|
|
50
|
+
const skipped = Array.isArray(details.skipped) ? details.skipped : [];
|
|
51
|
+
const documents = [...new Set((Array.isArray(details.documents) ? details.documents : []).map(text).filter(Boolean))];
|
|
52
|
+
const code = skipped.every((source) => source?.noAccess) ? 403 : 502;
|
|
53
|
+
const titles = documents.map((title) => `"${title}"`).join(", ");
|
|
54
|
+
super(code === 403
|
|
55
|
+
? `The knowledge base documents this content cites could not be read with your access${titles ? `: ${titles}` : ""}.`
|
|
56
|
+
: `The knowledge base documents this content cites could not be read: the knowledge base search failed${titles ? ` for ${titles}` : ""}.`);
|
|
57
|
+
this.name = "KnowledgeAccessError";
|
|
58
|
+
this.code = code;
|
|
59
|
+
this.skipped = skipped;
|
|
60
|
+
this.documents = documents;
|
|
61
|
+
Object.setPrototypeOf(this, KnowledgeAccessError.prototype);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
24
64
|
const id = (value) => typeof value === "string" ? value.trim() : value != null && typeof value === "object" ? String(value) : "";
|
|
25
65
|
const shortId = (index) => `r${index}`;
|
|
26
66
|
/** The document a STACKFACTOR reference cites, or "" when it cites none. */
|
|
@@ -29,6 +69,14 @@ const documentOf = (reference) => {
|
|
|
29
69
|
return (id(reference.knowledgeBase?.documentId) ||
|
|
30
70
|
(target.elementType === STACKFACTOR_REFERENCE.TARGET_ELEMENT_TYPE ? id(target.elementId) : ""));
|
|
31
71
|
};
|
|
72
|
+
/** The knowledge base a STACKFACTOR reference's document is in, or "" when it cites no document. */
|
|
73
|
+
const knowledgeBaseOf = (reference) => {
|
|
74
|
+
const target = reference.target ?? {};
|
|
75
|
+
return (id(reference.knowledgeBase?.knowledgeBaseId) ||
|
|
76
|
+
(target.elementType === STACKFACTOR_REFERENCE.TARGET_ELEMENT_TYPE ? id(target.parentId) : ""));
|
|
77
|
+
};
|
|
78
|
+
/** A cap the caller passed (a whole number, 0 or more), else `fallback`. */
|
|
79
|
+
const capOf = (value, fallback) => typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : fallback;
|
|
32
80
|
/** The kinds the platform retired; it reads them as PUBLICATION. */
|
|
33
81
|
const RETIRED_KINDS = new Set(["DOCUMENT", "KNOWLEDGE_BASE", "NOTE"]);
|
|
34
82
|
/** A reference's kind as the platform reads it: missing or retired is PUBLICATION. */
|
|
@@ -54,39 +102,71 @@ const pagesOf = (references) => {
|
|
|
54
102
|
});
|
|
55
103
|
return pages;
|
|
56
104
|
};
|
|
57
|
-
/**
|
|
58
|
-
|
|
59
|
-
|
|
105
|
+
/**
|
|
106
|
+
* The short ids of the STACKFACTOR references citing each document, and of
|
|
107
|
+
* those citing each chunk (`knowledgeBase.chunkId`) of a document.
|
|
108
|
+
*/
|
|
109
|
+
const citersOf = (references) => {
|
|
110
|
+
const byDocument = new Map();
|
|
111
|
+
const byChunk = new Map();
|
|
60
112
|
references.forEach((reference, index) => {
|
|
61
113
|
if (reference?.kind !== STACKFACTOR_REFERENCE.KIND)
|
|
62
114
|
return;
|
|
63
115
|
const documentId = documentOf(reference);
|
|
64
|
-
if (documentId)
|
|
65
|
-
|
|
116
|
+
if (!documentId)
|
|
117
|
+
return;
|
|
118
|
+
byDocument.set(documentId, [...(byDocument.get(documentId) ?? []), shortId(index)]);
|
|
119
|
+
const chunkId = id(reference.knowledgeBase?.chunkId);
|
|
120
|
+
if (chunkId)
|
|
121
|
+
byChunk.set(chunkId, [...(byChunk.get(chunkId) ?? []), shortId(index)]);
|
|
66
122
|
});
|
|
67
|
-
return
|
|
123
|
+
return { byChunk, byDocument };
|
|
68
124
|
};
|
|
125
|
+
/**
|
|
126
|
+
* The error for cited knowledge bases that could not be read: it names the
|
|
127
|
+
* documents of the STACKFACTOR references citing them.
|
|
128
|
+
*/
|
|
129
|
+
const accessError = (references, unread, skipped) => new KnowledgeAccessError({
|
|
130
|
+
documents: references
|
|
131
|
+
.filter((reference) => reference?.kind === STACKFACTOR_REFERENCE.KIND && documentOf(reference) && unread.has(knowledgeBaseOf(reference)))
|
|
132
|
+
.map((reference) => reference.title),
|
|
133
|
+
skipped,
|
|
134
|
+
});
|
|
69
135
|
/**
|
|
70
136
|
* What a list of references points at: its pages, and the passages of the
|
|
71
137
|
* documents its STACKFACTOR references cite. One search per knowledge base per
|
|
72
|
-
* query (per batch of 500 documents), however many references there are.
|
|
73
|
-
*
|
|
74
|
-
*
|
|
138
|
+
* query (per batch of 500 documents), however many references there are.
|
|
139
|
+
*
|
|
140
|
+
* References that cite a knowledge base the run cannot read stop it: with no
|
|
141
|
+
* token, or when any cited knowledge base is skipped (refused or failed), it
|
|
142
|
+
* throws a KnowledgeAccessError naming the documents. A search that works and
|
|
143
|
+
* finds nothing is not an error. With no knowledge cited, no token is needed.
|
|
144
|
+
*
|
|
145
|
+
* A passage is attributed to the references citing its chunk, else to every
|
|
146
|
+
* reference citing its document. The best-ranked `maxPassages` are kept, and
|
|
147
|
+
* the first `maxPages` pages in reference order.
|
|
75
148
|
* @param options `references` (a skill's, a microskill's…), `queries` to
|
|
76
149
|
* search the cited documents with, `search` (client-api
|
|
77
150
|
* `knowledgeBaseAgent.search`), `token` (the run's AGENT token), `topK` per
|
|
78
|
-
* search (default 5)
|
|
79
|
-
* @returns `{ pages, passages, skipped, citesKnowledge }`
|
|
151
|
+
* search (default 5), `maxPassages` (default 40), `maxPages` (default: all)
|
|
152
|
+
* @returns `{ pages, passages, skipped, citesKnowledge }`; `skipped` is always
|
|
153
|
+
* empty, since a skip throws
|
|
154
|
+
* @throws KnowledgeAccessError When cited knowledge bases cannot be read
|
|
80
155
|
*/
|
|
81
156
|
export const gatherReferenceContext = async (options) => {
|
|
82
157
|
const references = Array.isArray(options.references) ? options.references : [];
|
|
83
158
|
const sources = sourcesFromReferences(references);
|
|
84
159
|
const citesKnowledge = sources.length > 0;
|
|
85
|
-
const pages = pagesOf(references);
|
|
86
|
-
if (!
|
|
87
|
-
logContext(pages.length, 0, []
|
|
160
|
+
const pages = pagesOf(references).slice(0, capOf(options.maxPages, Infinity));
|
|
161
|
+
if (!citesKnowledge) {
|
|
162
|
+
logContext(pages.length, 0, []);
|
|
88
163
|
return { citesKnowledge, pages, passages: [], skipped: [] };
|
|
89
164
|
}
|
|
165
|
+
if (!options.token) {
|
|
166
|
+
const error = accessError(references, new Set(sources.map((source) => source.knowledgeBaseId)), []);
|
|
167
|
+
logContext(pages.length, 0, [], { error, noToken: true });
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
90
170
|
const found = await retrievePassages({
|
|
91
171
|
queries: options.queries,
|
|
92
172
|
search: options.search,
|
|
@@ -94,30 +174,44 @@ export const gatherReferenceContext = async (options) => {
|
|
|
94
174
|
token: options.token,
|
|
95
175
|
topK: options.topK ?? DEFAULT_REFERENCE_TOP_K,
|
|
96
176
|
});
|
|
97
|
-
|
|
177
|
+
if (found.skipped.length > 0) {
|
|
178
|
+
const unread = new Set(found.skipped.map((source) => source.knowledgeBaseId));
|
|
179
|
+
const error = accessError(references, unread, found.skipped);
|
|
180
|
+
logContext(pages.length, 0, found.skipped, { error });
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
const citers = citersOf(references);
|
|
98
184
|
const passages = found.passages
|
|
99
|
-
.filter((passage) => citers.has(passage.documentId))
|
|
185
|
+
.filter((passage) => citers.byDocument.has(passage.documentId))
|
|
186
|
+
.slice(0, capOf(options.maxPassages, DEFAULT_MAX_PASSAGES))
|
|
100
187
|
.map((passage) => ({
|
|
101
188
|
chunkId: passage.chunkId,
|
|
102
189
|
document: passage.documentName,
|
|
103
190
|
documentId: passage.documentId,
|
|
104
191
|
knowledgeBaseId: passage.knowledgeBaseId,
|
|
105
|
-
references: citers.get(passage.documentId),
|
|
192
|
+
references: citers.byChunk.get(passage.chunkId) ?? citers.byDocument.get(passage.documentId),
|
|
106
193
|
section: passage.sectionTitle,
|
|
107
194
|
text: passage.text,
|
|
108
195
|
}));
|
|
109
|
-
logContext(pages.length, passages.length,
|
|
110
|
-
return { citesKnowledge, pages, passages, skipped:
|
|
196
|
+
logContext(pages.length, passages.length, []);
|
|
197
|
+
return { citesKnowledge, pages, passages, skipped: [] };
|
|
111
198
|
};
|
|
112
|
-
/**
|
|
113
|
-
|
|
199
|
+
/**
|
|
200
|
+
* The one summary line of a gathering, for the logs: a warning when it was
|
|
201
|
+
* refused. Document titles stay out of the logs.
|
|
202
|
+
*/
|
|
203
|
+
const logContext = (pages, passages, skipped, refusal) => {
|
|
114
204
|
const parts = [`${pages} page${pages === 1 ? "" : "s"}`, `${passages} passage${passages === 1 ? "" : "s"}`];
|
|
115
205
|
if (skipped.length > 0) {
|
|
116
206
|
parts.push(`skipped knowledge bases: ${skipped.map((s) => `${s.knowledgeBaseId} (noAccess: ${s.noAccess})`).join(", ")}`);
|
|
117
207
|
}
|
|
118
|
-
if (noToken)
|
|
119
|
-
parts.push("no token
|
|
120
|
-
|
|
208
|
+
if (refusal?.noToken)
|
|
209
|
+
parts.push("no token");
|
|
210
|
+
if (refusal) {
|
|
211
|
+
const count = refusal.error.documents.length;
|
|
212
|
+
parts.push(`refused (${refusal.error.code}): ${count} document${count === 1 ? "" : "s"} unreadable`);
|
|
213
|
+
}
|
|
214
|
+
logger.log(null, refusal ? logger.levels.warn : logger.levels.info, `Reference context: ${parts.join("; ")}`);
|
|
121
215
|
};
|
|
122
216
|
/**
|
|
123
217
|
* The passages as a prompt shows them: who cites each, where it came from, and
|
|
@@ -133,19 +227,23 @@ export const referencePassagesForPrompt = (context) => context.passages.map((pas
|
|
|
133
227
|
}));
|
|
134
228
|
/**
|
|
135
229
|
* The tools a run is given. Never `search`, `map` or `crawl`: a run reads what
|
|
136
|
-
* its references point at and discovers nothing. `extract` only,
|
|
137
|
-
* there are pages to open
|
|
230
|
+
* its references point at and discovers nothing. `extract` only, only when
|
|
231
|
+
* there are pages to open, and kept to those pages: any other url the model
|
|
232
|
+
* asks for is refused (`allowedUrls`).
|
|
138
233
|
* @param context From `gatherReferenceContext`
|
|
139
|
-
* @returns `{ tavily: { tools: ["extract"] } }` with pages, else
|
|
140
|
-
* pages it returns `{}`, so a caller spreading it over a config
|
|
141
|
-
* has a `tavily` key keeps that key (and `tavily: true` means
|
|
142
|
-
* extract): callers must not carry a base `tavily` key.
|
|
234
|
+
* @returns `{ tavily: { tools: ["extract"], allowedUrls } }` with pages, else
|
|
235
|
+
* `{}`. With no pages it returns `{}`, so a caller spreading it over a config
|
|
236
|
+
* that already has a `tavily` key keeps that key (and `tavily: true` means
|
|
237
|
+
* search plus extract): callers must not carry a base `tavily` key.
|
|
143
238
|
*/
|
|
144
|
-
export const toolsFor = (context) => context.pages.length > 0
|
|
239
|
+
export const toolsFor = (context) => context.pages.length > 0
|
|
240
|
+
? { tavily: { tools: ["extract"], allowedUrls: context.pages.map((page) => page.url) } }
|
|
241
|
+
: {};
|
|
145
242
|
/**
|
|
146
243
|
* The stage prompt followed by the guidance for each kind of source the run
|
|
147
|
-
* has: pages, then passages; when it has neither
|
|
148
|
-
* guidance adds
|
|
244
|
+
* has: pages, then passages; when it has neither (knowledge cited and readable
|
|
245
|
+
* but no passage found included), the `none` guidance. An empty guidance adds
|
|
246
|
+
* nothing.
|
|
149
247
|
* @param stagePrompt The stage's own prompt
|
|
150
248
|
* @param context From `gatherReferenceContext`
|
|
151
249
|
* @param guidance From `sourceGuidanceFrom`
|
|
@@ -271,6 +369,7 @@ export const sourceGuidanceFrom = (config) => ({
|
|
|
271
369
|
none: text(config?.noReferencesGuidance),
|
|
272
370
|
});
|
|
273
371
|
export default {
|
|
372
|
+
KnowledgeAccessError,
|
|
274
373
|
REFERENCE_PASSAGES_KEY,
|
|
275
374
|
citedReferences,
|
|
276
375
|
composeSourceInstructions,
|
package/dist/esm/serve.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AAWtC,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,YAAY,CAAC;AAwPpB;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;AAE/D;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,CAC1B,OAAO,EAAE,GAAG,KACT,OAAO,CAAC;IAAE,IAAI,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,CAAC,CAAC;AAEzC,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wFAAwF;IACxF,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,uEAAuE;IACvE,aAAa,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;IACpE,sEAAsE;IACtE,SAAS,CAAC,EAAE,YAAY,CAAC;IACzB,iEAAiE;IACjE,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9B,mFAAmF;IACnF,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,mFAAmF;IACnF,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACxD,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,MAAM,EAAE,CACN,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,MAAM,KACV,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CACpC;AAKD;;;;;;GAMG;AACH,eAAO,MAAM,aAAa;;;;;CAKhB,CAAC;AAEX,mFAAmF;AACnF,eAAO,MAAM,kBAAkB,GAAI,MAAK,MAAM,CAAC,UAAwB,KAAG,OACJ,CAAC;AAKvE;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qBAAqB,GAAI,OAAO,GAAG,EAAE,SAAS,GAAG,KAAG,OAEhB,CAAC;AAKlD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,cAAc,GAAI,OAAO,GAAG,EAAE,SAAS,GAAG,KAAG,OAGN,CAAC;
|
|
1
|
+
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AAWtC,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,YAAY,CAAC;AAwPpB;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;AAE/D;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,CAC1B,OAAO,EAAE,GAAG,KACT,OAAO,CAAC;IAAE,IAAI,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,CAAC,CAAC;AAEzC,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wFAAwF;IACxF,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,uEAAuE;IACvE,aAAa,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;IACpE,sEAAsE;IACtE,SAAS,CAAC,EAAE,YAAY,CAAC;IACzB,iEAAiE;IACjE,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9B,mFAAmF;IACnF,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,mFAAmF;IACnF,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACxD,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,MAAM,EAAE,CACN,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,MAAM,KACV,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CACpC;AAKD;;;;;;GAMG;AACH,eAAO,MAAM,aAAa;;;;;CAKhB,CAAC;AAEX,mFAAmF;AACnF,eAAO,MAAM,kBAAkB,GAAI,MAAK,MAAM,CAAC,UAAwB,KAAG,OACJ,CAAC;AAKvE;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qBAAqB,GAAI,OAAO,GAAG,EAAE,SAAS,GAAG,KAAG,OAEhB,CAAC;AAKlD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,cAAc,GAAI,OAAO,GAAG,EAAE,SAAS,GAAG,KAAG,OAGN,CAAC;AAmKrD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,OAAO,GAClB,MAAM,SAAS,EACf,UAAS,YAAiB,KACzB,OAAO,CAAC,IAAI,CA8Id,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,KAAK,GAChB,MAAM,SAAS,EACf,UAAS,YAAiB,KACzB,IAAI,CAAC,MAyOP,CAAC"}
|
package/dist/esm/serve.js
CHANGED
|
@@ -339,8 +339,15 @@ const safeParse = (value, fallback) => {
|
|
|
339
339
|
/**
|
|
340
340
|
* Agents that fail throw `new Error(JSON.stringify({ code, message }))`. Unwrap
|
|
341
341
|
* that back into a structured gRPC error frame; fall back to a 500 otherwise.
|
|
342
|
+
* A KnowledgeAccessError an agent did not catch (referenceContext) answers
|
|
343
|
+
* with its own code (403, 502) and message, which names the documents that
|
|
344
|
+
* could not be read. It is known by its name, so one thrown by another copy of
|
|
345
|
+
* this package in the agent's dependencies is known too.
|
|
342
346
|
*/
|
|
343
347
|
const toErrorFrame = (error) => {
|
|
348
|
+
if (error?.name === "KnowledgeAccessError" && typeof error.code === "number") {
|
|
349
|
+
return { code: error.code, message: String(error.message ?? "") };
|
|
350
|
+
}
|
|
344
351
|
try {
|
|
345
352
|
const parsed = JSON.parse(error?.message);
|
|
346
353
|
if (parsed && typeof parsed.code === "number") {
|
|
@@ -67,7 +67,10 @@ export interface SkippedSource {
|
|
|
67
67
|
knowledgeBaseId: string;
|
|
68
68
|
/** The HTTP status, when the platform answered one. */
|
|
69
69
|
status: number | null;
|
|
70
|
-
/**
|
|
70
|
+
/**
|
|
71
|
+
* True when the platform refused the person (401, 403) or does not have the
|
|
72
|
+
* knowledge base (404); false when the search failed otherwise.
|
|
73
|
+
*/
|
|
71
74
|
noAccess: boolean;
|
|
72
75
|
message: string;
|
|
73
76
|
}
|
|
@@ -116,9 +119,11 @@ export declare const sourcesFromCoverage: (coverage: unknown) => KnowledgeSource
|
|
|
116
119
|
* model to cite. A source with documents is searched within them (in batches of
|
|
117
120
|
* SEARCH_DOCUMENT_IDS_MAX); one without, whole. A source the person may not
|
|
118
121
|
* read (401, 403, 404) is skipped: none of its passages are used and no more of
|
|
119
|
-
* its searches are made. A search that fails otherwise is
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
+
* its searches are made. A search that fails otherwise is tried once more; when
|
|
123
|
+
* that fails too it is reported and the passages its other searches found
|
|
124
|
+
* stand. A refusal is never tried again. Either way it is in `skipped`, once
|
|
125
|
+
* per knowledge base, and the passages of the others stand; whether a skip is
|
|
126
|
+
* an error is the caller's to say.
|
|
122
127
|
* @param options `sources`, `queries`, `search` (client-api
|
|
123
128
|
* `knowledgeBaseAgent.search`), `token` (the run's AGENT token), `topK`
|
|
124
129
|
* per search (default DEFAULT_TOP_K)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stackfactorKnowledge.d.ts","sourceRoot":"","sources":["../../src/stackfactorKnowledge.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"stackfactorKnowledge.d.ts","sourceRoot":"","sources":["../../src/stackfactorKnowledge.ts"],"names":[],"mappings":"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"}
|
|
@@ -14,8 +14,11 @@
|
|
|
14
14
|
// microskill;
|
|
15
15
|
// - access: the platform's search judges the person the run acts for, so a
|
|
16
16
|
// run reads only what that person may open. A knowledge base the person
|
|
17
|
-
// cannot read is skipped and reported
|
|
18
|
-
// generates without it.
|
|
17
|
+
// cannot read is skipped and reported (`retrievePassages`): grounding from
|
|
18
|
+
// skill coverage generates without it. A knowledge base that references
|
|
19
|
+
// cite is another matter (Candin, 2026-10-13): the references reader
|
|
20
|
+
// (referenceContext `gatherReferenceContext`) stops the run with a
|
|
21
|
+
// KnowledgeAccessError instead.
|
|
19
22
|
//
|
|
20
23
|
// A reference points at the content and never copies it: the passage is
|
|
21
24
|
// searched again, under the reader's own access, whenever it is used. Its
|
|
@@ -46,6 +49,8 @@ export const DEFAULT_TOP_K = 8;
|
|
|
46
49
|
const CONCURRENCY = 4;
|
|
47
50
|
/** Statuses that mean the person may not read the knowledge base, or it is gone. */
|
|
48
51
|
const NO_ACCESS = new Set([401, 403, 404]);
|
|
52
|
+
/** How long a search that failed for another reason waits before it is tried once more. */
|
|
53
|
+
const RETRY_DELAY_MS = 200;
|
|
49
54
|
const text = (value) => (typeof value === "string" ? value.trim() : "");
|
|
50
55
|
/** An http(s) address, or null: a reference's url lands in an `href`. */
|
|
51
56
|
const webUrl = (value) => (/^https?:\/\//i.test(text(value)) ? text(value) : null);
|
|
@@ -125,15 +130,18 @@ const statusOf = (error) => {
|
|
|
125
130
|
const status = error?.response?.status ?? error?.status;
|
|
126
131
|
return typeof status === "number" ? status : null;
|
|
127
132
|
};
|
|
133
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
128
134
|
/**
|
|
129
135
|
* Searches every source for every query, under the person's access, and hands
|
|
130
136
|
* back the passages found, best first, each once, tagged "k1", "k2"… for the
|
|
131
137
|
* model to cite. A source with documents is searched within them (in batches of
|
|
132
138
|
* SEARCH_DOCUMENT_IDS_MAX); one without, whole. A source the person may not
|
|
133
139
|
* read (401, 403, 404) is skipped: none of its passages are used and no more of
|
|
134
|
-
* its searches are made. A search that fails otherwise is
|
|
135
|
-
*
|
|
136
|
-
*
|
|
140
|
+
* its searches are made. A search that fails otherwise is tried once more; when
|
|
141
|
+
* that fails too it is reported and the passages its other searches found
|
|
142
|
+
* stand. A refusal is never tried again. Either way it is in `skipped`, once
|
|
143
|
+
* per knowledge base, and the passages of the others stand; whether a skip is
|
|
144
|
+
* an error is the caller's to say.
|
|
137
145
|
* @param options `sources`, `queries`, `search` (client-api
|
|
138
146
|
* `knowledgeBaseAgent.search`), `token` (the run's AGENT token), `topK`
|
|
139
147
|
* per search (default DEFAULT_TOP_K)
|
|
@@ -162,8 +170,24 @@ export const retrievePassages = async (options) => {
|
|
|
162
170
|
await inPool(searches, CONCURRENCY, async ({ source, query, documentIds, order }) => {
|
|
163
171
|
if (skipped.get(source.knowledgeBaseId)?.noAccess)
|
|
164
172
|
return;
|
|
173
|
+
const searchOnce = () => options.search(source.knowledgeBaseId, query, documentIds ? { topK, documentIds } : { topK }, options.token);
|
|
165
174
|
try {
|
|
166
|
-
|
|
175
|
+
let answer;
|
|
176
|
+
try {
|
|
177
|
+
answer = await searchOnce();
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
// A refusal is the platform's answer and is not asked again. Any other
|
|
181
|
+
// failure may pass (a dropped connection, a 5xx), and one failed search
|
|
182
|
+
// would skip its whole knowledge base, so it is tried once more —
|
|
183
|
+
// unless the base refused the person meanwhile.
|
|
184
|
+
if (NO_ACCESS.has(statusOf(error)))
|
|
185
|
+
throw error;
|
|
186
|
+
await delay(RETRY_DELAY_MS);
|
|
187
|
+
if (skipped.get(source.knowledgeBaseId)?.noAccess)
|
|
188
|
+
return;
|
|
189
|
+
answer = await searchOnce();
|
|
190
|
+
}
|
|
167
191
|
(Array.isArray(answer?.chunks) ? answer.chunks : []).forEach((chunk, rank) => {
|
|
168
192
|
const chunkId = id(chunk?._id);
|
|
169
193
|
if (!chunkId || !id(chunk?.documentId))
|
package/dist/esm/tavily.d.ts
CHANGED
|
@@ -46,6 +46,14 @@ export type TavilyConfig = {
|
|
|
46
46
|
* for the tabular documents this is usually pointed at.
|
|
47
47
|
*/
|
|
48
48
|
format?: "markdown" | "text";
|
|
49
|
+
/**
|
|
50
|
+
* The only URLs `web_extract` may read (matched trimmed, without case or a
|
|
51
|
+
* trailing slash, and sent as listed). Any other URL is never sent to
|
|
52
|
+
* Tavily: the tool answers the model with an error line for it and never
|
|
53
|
+
* throws. Unset, any URL may be read. Only `web_extract` reads it, so a run
|
|
54
|
+
* kept to its pages enables `extract` alone (referenceContext `toolsFor`).
|
|
55
|
+
*/
|
|
56
|
+
allowedUrls?: string[];
|
|
49
57
|
};
|
|
50
58
|
/**
|
|
51
59
|
* Normalizes `config.tavily` into an options object, returning `null` when the
|
package/dist/esm/tavily.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tavily.d.ts","sourceRoot":"","sources":["../../src/tavily.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AA0BnD,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,GAAG,KAAK,CAAC;AAEpE;;;;;;;;;GASG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,cAAc,EAAE,CAAC;IACzB,iEAAiE;IACjE,WAAW,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,MAAM,GAAG,YAAY,CAAC;IAC3D,kFAAkF;IAClF,YAAY,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC;IACpC,2CAA2C;IAC3C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oEAAoE;IACpE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qEAAqE;IACrE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,4CAA4C;IAC5C,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,6CAA6C;IAC7C,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,MAAM,CAAC,EAAE,UAAU,GAAG,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"tavily.d.ts","sourceRoot":"","sources":["../../src/tavily.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AA0BnD,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,GAAG,KAAK,CAAC;AAEpE;;;;;;;;;GASG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,cAAc,EAAE,CAAC;IACzB,iEAAiE;IACjE,WAAW,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,MAAM,GAAG,YAAY,CAAC;IAC3D,kFAAkF;IAClF,YAAY,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC;IACpC,2CAA2C;IAC3C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oEAAoE;IACpE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qEAAqE;IACrE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,4CAA4C;IAC5C,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,6CAA6C;IAC7C,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,MAAM,CAAC,EAAE,UAAU,GAAG,MAAM,CAAC;IAC7B;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,GAAI,QAAQ,GAAG,KAAG,YAAY,GAAG,IAI7D,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAAI,QAAQ,GAAG,EAAE,SAAS,YAAY,KAAG,IA+BvE,CAAC;AA0OF;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,gBAAgB,GAC3B,QAAQ,GAAG,EACX,SAAS,YAAY,EACrB,eAAc,YAAY,GAAG,IAAW,KACvC,GAAG,EAmRL,CAAC;AAiBF;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,eAAO,MAAM,kBAAkB,GAC7B,MAAM,SAAS,MAAM,EAAE,EACvB,QAAQ,GAAG,EACX,eAAc,YAAY,GAAG,IAAW,KACvC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CA6DrB,CAAC;;iCAhqByC,GAAG,WAAW,YAAY,KAAG,IAAI;+BA2RlE,GAAG,WACF,YAAY,iBACP,YAAY,GAAG,IAAI,KAChC,GAAG,EAAE;+BAkUA,SAAS,MAAM,EAAE,UACf,GAAG,iBACG,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;+BAlnBkB,GAAG,KAAG,YAAY,GAAG,IAAI;iCA+IvD,YAAY,GAAG,IAAI,GAAG,SAAS,WAC/B,GAAG,WACH,MAAM,WACN;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,KACzC,IAAI;;AA8hBP,wBAME"}
|
package/dist/esm/tavily.js
CHANGED
|
@@ -53,6 +53,10 @@ export const assertTavilyUsable = (config, options) => {
|
|
|
53
53
|
if (!config?.tavilyAPIKey) {
|
|
54
54
|
throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, "Tavily tools are enabled but config.tavilyAPIKey is missing.");
|
|
55
55
|
}
|
|
56
|
+
if (options.allowedUrls !== undefined &&
|
|
57
|
+
!(Array.isArray(options.allowedUrls) && options.allowedUrls.every((url) => typeof url === "string"))) {
|
|
58
|
+
throw errorHandlingHelper.create(constants.HTTP_CODES.BAD_REQUEST, "config.tavily.allowedUrls must be a list of URLs.");
|
|
59
|
+
}
|
|
56
60
|
const unknown = (options.tools || []).filter((name) => !["search", "extract", "crawl", "map"].includes(name));
|
|
57
61
|
if (unknown.length > 0) {
|
|
58
62
|
throw errorHandlingHelper.create(constants.HTTP_CODES.BAD_REQUEST, `Unknown Tavily tool(s): ${unknown.join(", ")}. Valid values are search, extract, crawl, map.`);
|
|
@@ -160,6 +164,52 @@ const recordTavilyUsage = (tracker, _config, credits, sources) => {
|
|
|
160
164
|
}
|
|
161
165
|
}
|
|
162
166
|
};
|
|
167
|
+
/**
|
|
168
|
+
* Adds the URLs `web_extract` is about to ask Tavily for to
|
|
169
|
+
* `tracker.attemptedUrls`, each once across the run. Recorded before the call,
|
|
170
|
+
* so a URL the call then fails on is still there.
|
|
171
|
+
* @param tracker - The caller's usage accumulator, when it passed one
|
|
172
|
+
* @param urls - The URLs sent
|
|
173
|
+
*/
|
|
174
|
+
const recordAttemptedUrls = (tracker, urls) => {
|
|
175
|
+
if (!tracker || urls.length === 0)
|
|
176
|
+
return;
|
|
177
|
+
const attempted = tracker.attemptedUrls || (tracker.attemptedUrls = []);
|
|
178
|
+
for (const url of urls) {
|
|
179
|
+
if (!attempted.includes(url))
|
|
180
|
+
attempted.push(url);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
/**
|
|
184
|
+
* How a URL is matched, against Tavily's answer or `allowedUrls`: trimmed,
|
|
185
|
+
* lowercased, no trailing slash.
|
|
186
|
+
*/
|
|
187
|
+
const urlMatchKey = (url) => url.trim().replace(/\/+$/, "").toLowerCase();
|
|
188
|
+
/** What `web_extract` answers for each URL `allowedUrls` refused. */
|
|
189
|
+
const REFUSED_HEADING = "Refused, not read (only the referenced pages may be read):";
|
|
190
|
+
/**
|
|
191
|
+
* Splits the URLs the model asked `web_extract` for into those it may read —
|
|
192
|
+
* as `allowedUrls` lists them — and those it may not, each once. With no
|
|
193
|
+
* `allowedUrls`, every URL may be read, as asked.
|
|
194
|
+
* @param urls - The URLs the model asked for
|
|
195
|
+
* @param allowed - `allowedUrls` by match key, or `null` when unset
|
|
196
|
+
*/
|
|
197
|
+
const screenUrls = (urls, allowed) => {
|
|
198
|
+
if (!allowed)
|
|
199
|
+
return { read: urls, refused: [] };
|
|
200
|
+
const read = [];
|
|
201
|
+
const refused = [];
|
|
202
|
+
for (const url of urls) {
|
|
203
|
+
const listed = allowed.get(urlMatchKey(url));
|
|
204
|
+
if (listed === undefined) {
|
|
205
|
+
if (!refused.includes(url))
|
|
206
|
+
refused.push(url);
|
|
207
|
+
}
|
|
208
|
+
else if (!read.includes(listed))
|
|
209
|
+
read.push(listed);
|
|
210
|
+
}
|
|
211
|
+
return { read, refused };
|
|
212
|
+
};
|
|
163
213
|
/**
|
|
164
214
|
* Trims one document to whatever the per-URL and per-call budgets allow,
|
|
165
215
|
* appending a note when anything was cut.
|
|
@@ -230,6 +280,9 @@ export const buildTavilyTools = (config, options, usageTracker = null) => {
|
|
|
230
280
|
const maxCharsPerUrl = options.maxCharsPerUrl ?? DEFAULT_MAX_CHARS_PER_URL;
|
|
231
281
|
const maxCharsTotal = options.maxCharsTotal ?? DEFAULT_MAX_CHARS_TOTAL;
|
|
232
282
|
const crawlLimit = options.crawlLimit ?? DEFAULT_CRAWL_LIMIT;
|
|
283
|
+
const allowed = Array.isArray(options.allowedUrls)
|
|
284
|
+
? new Map(options.allowedUrls.map((url) => [urlMatchKey(url), url.trim()]))
|
|
285
|
+
: null;
|
|
233
286
|
const newBudget = () => ({
|
|
234
287
|
remaining: maxCharsTotal,
|
|
235
288
|
perUrl: maxCharsPerUrl,
|
|
@@ -277,8 +330,22 @@ export const buildTavilyTools = (config, options, usageTracker = null) => {
|
|
|
277
330
|
}
|
|
278
331
|
if (enabled.includes("extract")) {
|
|
279
332
|
tools.push(tool(async ({ urls, query }) => {
|
|
333
|
+
// Outside `allowedUrls` nothing is sent: the model is told per URL,
|
|
334
|
+
// and the run goes on with what it may read.
|
|
335
|
+
const { read, refused } = screenUrls(urls, allowed);
|
|
336
|
+
const refusal = refused.length
|
|
337
|
+
? `${REFUSED_HEADING}\n${refused
|
|
338
|
+
.map((url) => `- ${url}: not one of the referenced pages`)
|
|
339
|
+
.join("\n")}`
|
|
340
|
+
: "";
|
|
341
|
+
if (refused.length > 0) {
|
|
342
|
+
logger.log(null, logger.levels.warn, `Tavily extract refused ${refused.length} URL(s) not among the allowed pages: ${refused.join(", ")}`);
|
|
343
|
+
}
|
|
344
|
+
if (read.length === 0)
|
|
345
|
+
return refusal;
|
|
346
|
+
recordAttemptedUrls(usageTracker, read);
|
|
280
347
|
const data = await callTavily("extract", {
|
|
281
|
-
urls,
|
|
348
|
+
urls: read,
|
|
282
349
|
extract_depth: options.extractDepth,
|
|
283
350
|
format,
|
|
284
351
|
// `query` switches the API from whole-document extraction to
|
|
@@ -309,10 +376,14 @@ export const buildTavilyTools = (config, options, usageTracker = null) => {
|
|
|
309
376
|
.join("\n");
|
|
310
377
|
rendered += `\n\n---\n\nFailed to extract:\n${lines}`;
|
|
311
378
|
}
|
|
379
|
+
if (refusal)
|
|
380
|
+
rendered += `\n\n---\n\n${refusal}`;
|
|
312
381
|
return rendered;
|
|
313
382
|
}, {
|
|
314
383
|
name: "web_extract",
|
|
315
|
-
description:
|
|
384
|
+
description: `Read the content of specific URLs. Pass a query to get back only the passages relevant to it — much cheaper and usually sufficient. Omit the query to retrieve the whole document, which is appropriate when you need its full structure. Long documents are truncated, and the result says so when that happens.${allowed
|
|
385
|
+
? " Only the referenced pages can be read; any other URL is refused."
|
|
386
|
+
: ""}`,
|
|
316
387
|
schema: z.object({
|
|
317
388
|
urls: z
|
|
318
389
|
.array(z.string())
|
|
@@ -390,8 +461,6 @@ export const buildTavilyTools = (config, options, usageTracker = null) => {
|
|
|
390
461
|
const EXTRACT_MAX_URLS = 20;
|
|
391
462
|
/** Seconds. A reachability check needs the page fetched, not rendered at length. */
|
|
392
463
|
const REACHABILITY_TIMEOUT_SECONDS = 20;
|
|
393
|
-
/** How a URL is matched against Tavily's answer: trimmed, lowercased, no trailing slash. */
|
|
394
|
-
const urlMatchKey = (url) => url.trim().replace(/\/+$/, "").toLowerCase();
|
|
395
464
|
/** True for an `http:` or `https:` URL; false for anything else, unparsable included. */
|
|
396
465
|
const isWebUrl = (url) => {
|
|
397
466
|
try {
|