@stackfactor/agent-utils 1.10.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 +3 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +4 -1
- 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 +238 -0
- package/dist/cjs/referenceContext.d.ts.map +1 -0
- package/dist/cjs/referenceContext.js +399 -0
- 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 +3 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +2 -1
- 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 +238 -0
- package/dist/esm/referenceContext.d.ts.map +1 -0
- package/dist/esm/referenceContext.js +383 -0
- 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
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 {
|