@triplef/agent 0.1.10 → 0.1.12
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 +1 -1
- package/dist/schemas/index.d.ts +37 -36
- package/dist/tools/index.d.ts +5 -0
- package/dist/tools/index.mjs +296 -200
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ The tripleF (3F) agent domain — structured-output schemas, prompt builders, an
|
|
|
16
16
|
|
|
17
17
|
## Subpath exports
|
|
18
18
|
|
|
19
|
-
- `@triplef/agent/schemas` — Zod schemas and `z.infer` types for intent classification, response templates, and memory extraction/consolidation/profile, plus URL-trust and Zod-shape helpers (`formatZodShape`, `deriveSchemaKeys`) and the `
|
|
19
|
+
- `@triplef/agent/schemas` — Zod schemas and `z.infer` types for intent classification, response templates, and memory extraction/consolidation/profile, plus URL-trust and Zod-shape helpers (`formatZodShape`, `deriveSchemaKeys`) and the `EncyclopediaSelectInput`/`EncyclopediaSelectResult` retrieval-selection contract.
|
|
20
20
|
- `@triplef/agent/prompts` — harness and memory prompt builders, the snippet system, and `buildStructuredPrompt` (renders a schema's JSON shape into a prompt template).
|
|
21
21
|
- `@triplef/agent/tools` — search/tool factories (Serper, Bright Data, YouTube, web-fetch, image-variants, memory) with a decoupled `ToolDependencies` contract.
|
|
22
22
|
|
package/dist/schemas/index.d.ts
CHANGED
|
@@ -6,6 +6,42 @@ declare const BLOCKED_IMAGE_HOSTS: Set<string>;
|
|
|
6
6
|
declare const BLOCKED_URL_HOSTS: Set<string>;
|
|
7
7
|
declare const NON_PAGE_EXTENSIONS: RegExp;
|
|
8
8
|
|
|
9
|
+
interface EncyclopediaSourceDocument {
|
|
10
|
+
url?: string;
|
|
11
|
+
title?: string;
|
|
12
|
+
content: string;
|
|
13
|
+
}
|
|
14
|
+
interface EncyclopediaSelectedChunk {
|
|
15
|
+
url?: string;
|
|
16
|
+
title?: string;
|
|
17
|
+
content: string;
|
|
18
|
+
score: number;
|
|
19
|
+
sourceType?: 'content' | 'result';
|
|
20
|
+
}
|
|
21
|
+
interface EncyclopediaSearchResult {
|
|
22
|
+
url: string;
|
|
23
|
+
title?: string;
|
|
24
|
+
snippet: string;
|
|
25
|
+
}
|
|
26
|
+
interface EncyclopediaSelectInput {
|
|
27
|
+
query: string;
|
|
28
|
+
documents: EncyclopediaSourceDocument[];
|
|
29
|
+
searchResults?: EncyclopediaSearchResult[];
|
|
30
|
+
budgetChars?: number;
|
|
31
|
+
partitionScope?: string;
|
|
32
|
+
model?: string;
|
|
33
|
+
}
|
|
34
|
+
interface EncyclopediaSelectResult {
|
|
35
|
+
chunks: EncyclopediaSelectedChunk[];
|
|
36
|
+
consideredChunks: number;
|
|
37
|
+
selectedChunks: number;
|
|
38
|
+
droppedByThreshold: number;
|
|
39
|
+
inputChunksDropped: number;
|
|
40
|
+
pastChunks?: EncyclopediaSelectedChunk[];
|
|
41
|
+
reusedDocs?: number;
|
|
42
|
+
storedDocs?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
9
45
|
declare function categorizeTools(toolNames: readonly string[]): Record<string, string[]>;
|
|
10
46
|
|
|
11
47
|
declare const VARIANT_NAMES: readonly ["grayscale", "denoised", "sharpened", "clahe"];
|
|
@@ -139,41 +175,6 @@ declare function deriveSchemaKeys(schema: z.ZodType): {
|
|
|
139
175
|
|
|
140
176
|
declare function formatZodIssues(issues: z.ZodIssue[]): string;
|
|
141
177
|
|
|
142
|
-
interface LexiconSourceDocument {
|
|
143
|
-
url?: string;
|
|
144
|
-
title?: string;
|
|
145
|
-
content: string;
|
|
146
|
-
}
|
|
147
|
-
interface LexiconSelectedChunk {
|
|
148
|
-
url?: string;
|
|
149
|
-
title?: string;
|
|
150
|
-
content: string;
|
|
151
|
-
score: number;
|
|
152
|
-
sourceType?: 'content' | 'result';
|
|
153
|
-
}
|
|
154
|
-
interface LexiconSearchResult {
|
|
155
|
-
url: string;
|
|
156
|
-
title?: string;
|
|
157
|
-
snippet: string;
|
|
158
|
-
}
|
|
159
|
-
interface LexiconSelectInput {
|
|
160
|
-
query: string;
|
|
161
|
-
documents: LexiconSourceDocument[];
|
|
162
|
-
searchResults?: LexiconSearchResult[];
|
|
163
|
-
budgetChars?: number;
|
|
164
|
-
partitionScope?: string;
|
|
165
|
-
}
|
|
166
|
-
interface LexiconSelectResult {
|
|
167
|
-
chunks: LexiconSelectedChunk[];
|
|
168
|
-
consideredChunks: number;
|
|
169
|
-
selectedChunks: number;
|
|
170
|
-
droppedByThreshold: number;
|
|
171
|
-
inputChunksDropped: number;
|
|
172
|
-
pastChunks?: LexiconSelectedChunk[];
|
|
173
|
-
reusedDocs?: number;
|
|
174
|
-
storedDocs?: number;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
178
|
declare const ConsolidationVerdictSchema: z.ZodObject<{
|
|
178
179
|
verdict: z.ZodEnum<{
|
|
179
180
|
merge: "merge";
|
|
@@ -908,4 +909,4 @@ declare const videolistSchema: z.ZodObject<{
|
|
|
908
909
|
}, z.core.$strip>>>;
|
|
909
910
|
}, z.core.$strip>;
|
|
910
911
|
|
|
911
|
-
export { BLOCKED_IMAGE_HOSTS, BLOCKED_URL_HOSTS, BROWSER_TOOL_NAMES, COGNITION_LIMIT_DEFAULT, COGNITION_LIMIT_MAX, COGNITION_LIMIT_MIN, COGNITION_PURGE_BATCH, type ConsolidationVerdict, ConsolidationVerdictSchema, EPISODE_PROBE_LIMIT_DEFAULT, EPISODE_PROBE_LIMIT_MIN, EPISODE_RECENCY_MIDPOINT_DEFAULT, EPISODE_RECENCY_MIDPOINT_MAX, EPISODE_RECENCY_MIDPOINT_MIN, EPISODE_RECENCY_SCALE_SECONDS_DEFAULT, EPISODE_RECENCY_SCALE_SECONDS_MAX, EPISODE_RECENCY_SCALE_SECONDS_MIN, EPISODE_RECENCY_WEIGHT_DEFAULT, EPISODE_RECENCY_WEIGHT_MAX, EPISODE_RECENCY_WEIGHT_MIN, EPISODE_SCORE_THRESHOLD_DEFAULT, EPISODE_SCORE_THRESHOLD_MAX, EPISODE_SCORE_THRESHOLD_MIN, EPISODE_TAGS, EPISODE_TEXT_LIMIT,
|
|
912
|
+
export { BLOCKED_IMAGE_HOSTS, BLOCKED_URL_HOSTS, BROWSER_TOOL_NAMES, COGNITION_LIMIT_DEFAULT, COGNITION_LIMIT_MAX, COGNITION_LIMIT_MIN, COGNITION_PURGE_BATCH, type ConsolidationVerdict, ConsolidationVerdictSchema, EPISODE_PROBE_LIMIT_DEFAULT, EPISODE_PROBE_LIMIT_MIN, EPISODE_RECENCY_MIDPOINT_DEFAULT, EPISODE_RECENCY_MIDPOINT_MAX, EPISODE_RECENCY_MIDPOINT_MIN, EPISODE_RECENCY_SCALE_SECONDS_DEFAULT, EPISODE_RECENCY_SCALE_SECONDS_MAX, EPISODE_RECENCY_SCALE_SECONDS_MIN, EPISODE_RECENCY_WEIGHT_DEFAULT, EPISODE_RECENCY_WEIGHT_MAX, EPISODE_RECENCY_WEIGHT_MIN, EPISODE_SCORE_THRESHOLD_DEFAULT, EPISODE_SCORE_THRESHOLD_MAX, EPISODE_SCORE_THRESHOLD_MIN, EPISODE_TAGS, EPISODE_TEXT_LIMIT, type EncyclopediaSearchResult, type EncyclopediaSelectInput, type EncyclopediaSelectResult, type EncyclopediaSelectedChunk, type EncyclopediaSourceDocument, ExtractionSchema, HERO_VIDEO_TITLE_ISSUE, INSIGHTS_MAX_PER_TURN, INSIGHT_TAGS, INSIGHT_TEXT_LIMIT, MEMORY_TOOL_NAMES, type MemoryCognitionProfile, type MemoryEnrichment, MemoryEnrichmentSchema, type MemoryExtraction, type MemoryProfileInsight, type MemoryProfileResponse, NON_PAGE_EXTENSIONS, type ProviderConfig, TOOL_DESCRIPTIONS, TOOL_NAMES, type ToolName, VARIANT_NAMES, type VariantName, cardSchema, categorizeTools, clampCognitionLimit, clampEpisodeProbeLimit, clampEpisodeRecencyMidpoint, clampEpisodeRecencyScaleSeconds, clampEpisodeRecencyWeight, clampEpisodeScoreThreshold, compareSchema, createTextItemSchema, deriveSchemaKeys, describeSchema, discardedReferenceSchema, formatZodIssues, galleryItemSchema, hasBlockedImageHost, heroVideoHasTitle, imagelistSchema, internationalCoverageSchema, isAllFieldsNullWipe, isPrivateOrLocalhost, isTrustedImageUrl, isTrustedUrl, markerSchema, memoryProfileResponseSchema, mergeCognitionProfiles, normalizeInsightPath, ocrSchema, parseStoredProfile, productSchema, referenceGalleryItemSchema, referenceLineSchema, relatedStorySchema, safeMediaUrl, safeMediaUrlOrEmpty, safeUrl, safeVideoUrl, safeVideoUrlOrEmpty, shoplistSchema, sourceSchema, stockmarketItemSchema, stockmarketListSchema, summarySchema, videoGalleryItemSchema, videolistSchema };
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -401,6 +401,11 @@ interface MemoryPoint {
|
|
|
401
401
|
path?: string;
|
|
402
402
|
createdAt: string;
|
|
403
403
|
score?: number;
|
|
404
|
+
isConsolidated?: boolean;
|
|
405
|
+
isReflected?: boolean;
|
|
406
|
+
isFriction?: boolean;
|
|
407
|
+
superseded?: boolean;
|
|
408
|
+
supersededBy?: string;
|
|
404
409
|
}
|
|
405
410
|
|
|
406
411
|
interface MemoryPartitionRecallDeps {
|
package/dist/tools/index.mjs
CHANGED
|
@@ -261,6 +261,21 @@ function tbsSizeLabelForPixels(pixels) {
|
|
|
261
261
|
return selected.label;
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
+
// src/tools/bright-data/helpers/map-bright-data-image-result.helper.ts
|
|
265
|
+
function mapBrightDataImageResult(r) {
|
|
266
|
+
return {
|
|
267
|
+
title: r.title || "",
|
|
268
|
+
// Prefer the real image URL; `image` is a base64 thumbnail data
|
|
269
|
+
// URI that our trust rules reject.
|
|
270
|
+
imageUrl: r.original_image || r.image_url || r.imageUrl || r.link || "",
|
|
271
|
+
sourcePageUrl: r.source_link || r.link || "",
|
|
272
|
+
width: r.width,
|
|
273
|
+
height: r.height,
|
|
274
|
+
source: r.source || "",
|
|
275
|
+
domain: ""
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
264
279
|
// src/tools/bright-data/image-search.tool.ts
|
|
265
280
|
function createBrightDataImageSearch(deps) {
|
|
266
281
|
return tool({
|
|
@@ -304,17 +319,7 @@ function createBrightDataImageSearch(deps) {
|
|
|
304
319
|
deps.logger.warn(`Bright Data image search returned 0 results for "${query}"`);
|
|
305
320
|
return { results: [] };
|
|
306
321
|
}
|
|
307
|
-
const results = images.map((r) =>
|
|
308
|
-
title: r.title || "",
|
|
309
|
-
// Prefer the real image URL; `image` is a base64 thumbnail data
|
|
310
|
-
// URI that our trust rules reject.
|
|
311
|
-
imageUrl: r.original_image || r.image_url || r.imageUrl || r.link || "",
|
|
312
|
-
sourcePageUrl: r.source_link || r.link || "",
|
|
313
|
-
width: r.width,
|
|
314
|
-
height: r.height,
|
|
315
|
-
source: r.source || "",
|
|
316
|
-
domain: ""
|
|
317
|
-
})).filter((r) => {
|
|
322
|
+
const results = images.map(mapBrightDataImageResult).filter((r) => {
|
|
318
323
|
if (!isTrustedImageUrl(r.imageUrl)) return false;
|
|
319
324
|
const w = r.width ?? 0;
|
|
320
325
|
const h = r.height ?? 0;
|
|
@@ -336,6 +341,20 @@ var brightDataNewsSearchSchema = z.object({
|
|
|
336
341
|
recency: z.enum(["day", "week", "month", "year"]).optional().describe(RECENCY_DESCRIPTION),
|
|
337
342
|
lang: z.string().optional().describe("Two-letter ISO language code for result preference (e.g. en, de, ja)")
|
|
338
343
|
});
|
|
344
|
+
|
|
345
|
+
// src/tools/bright-data/helpers/map-bright-data-news-result.helper.ts
|
|
346
|
+
function mapBrightDataNewsResult(r) {
|
|
347
|
+
return {
|
|
348
|
+
title: r.title,
|
|
349
|
+
snippet: r.description || "",
|
|
350
|
+
url: r.link,
|
|
351
|
+
source: r.source || SOURCE,
|
|
352
|
+
date: r.date || "",
|
|
353
|
+
imageUrl: r.image_url || ""
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// src/tools/bright-data/news-search.tool.ts
|
|
339
358
|
function createBrightDataNewsSearch(deps) {
|
|
340
359
|
return tool({
|
|
341
360
|
description: 'Search the latest news using Bright Data SERP API. Returns headlines, sources, dates, and snippets. Pass recency ("day"|"week"|"month"|"year") to restrict to a recent period. ' + STANDALONE_QUERY_TOOL_CLAUSE,
|
|
@@ -361,14 +380,7 @@ function createBrightDataNewsSearch(deps) {
|
|
|
361
380
|
});
|
|
362
381
|
const news = data.news ?? [];
|
|
363
382
|
if (!news.length) return { results: [] };
|
|
364
|
-
const results = news.map(
|
|
365
|
-
title: r.title,
|
|
366
|
-
snippet: r.description || "",
|
|
367
|
-
url: r.link,
|
|
368
|
-
source: r.source || SOURCE,
|
|
369
|
-
date: r.date || "",
|
|
370
|
-
imageUrl: r.image_url || ""
|
|
371
|
-
}));
|
|
383
|
+
const results = news.map(mapBrightDataNewsResult);
|
|
372
384
|
deps.logger.log(`Bright Data news returned ${results.length} results for "${query}"`);
|
|
373
385
|
return { results };
|
|
374
386
|
} catch (err) {
|
|
@@ -385,6 +397,23 @@ var brightDataPlacesSearchSchema = z.object({
|
|
|
385
397
|
count: z.number().optional().describe("Number of results (max 100)"),
|
|
386
398
|
lang: z.string().optional().describe("Two-letter ISO language code for result preference (e.g. en, de, ja)")
|
|
387
399
|
});
|
|
400
|
+
|
|
401
|
+
// src/tools/bright-data/helpers/map-bright-data-place-result.helper.ts
|
|
402
|
+
function mapBrightDataPlaceResult(r) {
|
|
403
|
+
return {
|
|
404
|
+
title: r.title || "",
|
|
405
|
+
address: r.address || "",
|
|
406
|
+
phoneNumber: r.phone || "",
|
|
407
|
+
latitude: r.latitude,
|
|
408
|
+
longitude: r.longitude,
|
|
409
|
+
rating: r.rating,
|
|
410
|
+
ratingCount: r.reviews_cnt,
|
|
411
|
+
type: r.type || "",
|
|
412
|
+
website: r.website || ""
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// src/tools/bright-data/places-search.tool.ts
|
|
388
417
|
function createBrightDataPlacesSearch(deps) {
|
|
389
418
|
return tool({
|
|
390
419
|
description: 'Search for places and businesses using Bright Data SERP API (Google Maps local results). Returns names, addresses, ratings, review counts, and coordinates. Phrase the query like a Google Maps search: a business name, a business type, or a business type plus location (e.g. "MediaMarkt Berlin", "coffee shops in Munich").',
|
|
@@ -415,17 +444,7 @@ function createBrightDataPlacesSearch(deps) {
|
|
|
415
444
|
deps.logger.warn(`Bright Data places returned 0 results for "${query}"`);
|
|
416
445
|
return { results: [] };
|
|
417
446
|
}
|
|
418
|
-
const results = places.map(
|
|
419
|
-
title: r.title || "",
|
|
420
|
-
address: r.address || "",
|
|
421
|
-
phoneNumber: r.phone || "",
|
|
422
|
-
latitude: r.latitude,
|
|
423
|
-
longitude: r.longitude,
|
|
424
|
-
rating: r.rating,
|
|
425
|
-
ratingCount: r.reviews_cnt,
|
|
426
|
-
type: r.type || "",
|
|
427
|
-
website: r.website || ""
|
|
428
|
-
}));
|
|
447
|
+
const results = places.map(mapBrightDataPlaceResult);
|
|
429
448
|
return { results };
|
|
430
449
|
} catch (err) {
|
|
431
450
|
deps.logger.warn(`Bright Data places search failed for "${query}": ${String(err)}`);
|
|
@@ -439,6 +458,22 @@ var brightDataShoppingSearchSchema = z.object({
|
|
|
439
458
|
count: z.number().optional().describe("Number of results (max 100)"),
|
|
440
459
|
lang: z.string().optional().describe("Two-letter ISO language code for result preference (e.g. en, de, ja)")
|
|
441
460
|
});
|
|
461
|
+
|
|
462
|
+
// src/tools/bright-data/helpers/map-bright-data-shopping-result.helper.ts
|
|
463
|
+
function mapBrightDataShoppingResult(r) {
|
|
464
|
+
return {
|
|
465
|
+
title: r.title || "",
|
|
466
|
+
price: r.price || "",
|
|
467
|
+
link: r.link || "",
|
|
468
|
+
source: r.source || "",
|
|
469
|
+
imageUrl: r.image_url || r.image || "",
|
|
470
|
+
delivery: r.delivery || "",
|
|
471
|
+
rating: r.rating,
|
|
472
|
+
ratingCount: r.rating_count
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// src/tools/bright-data/shopping-search.tool.ts
|
|
442
477
|
function createBrightDataShoppingSearch(deps) {
|
|
443
478
|
return tool({
|
|
444
479
|
description: 'Search for products using Bright Data SERP API (Google Shopping). Returns prices, sellers, images, and ratings. Phrase the query as the bare product name with model number (e.g. "Sony WH-1000XM5") \u2014 do NOT add words like "review", "test", or long descriptive sentences.',
|
|
@@ -469,16 +504,7 @@ function createBrightDataShoppingSearch(deps) {
|
|
|
469
504
|
deps.logger.warn(`Bright Data shopping returned 0 results for "${query}"`);
|
|
470
505
|
return { results: [] };
|
|
471
506
|
}
|
|
472
|
-
const results = shopping.map(
|
|
473
|
-
title: r.title || "",
|
|
474
|
-
price: r.price || "",
|
|
475
|
-
link: r.link || "",
|
|
476
|
-
source: r.source || "",
|
|
477
|
-
imageUrl: r.image_url || r.image || "",
|
|
478
|
-
delivery: r.delivery || "",
|
|
479
|
-
rating: r.rating,
|
|
480
|
-
ratingCount: r.rating_count
|
|
481
|
-
}));
|
|
507
|
+
const results = shopping.map(mapBrightDataShoppingResult);
|
|
482
508
|
return { results };
|
|
483
509
|
} catch (err) {
|
|
484
510
|
deps.logger.warn(`Bright Data shopping search failed for "${query}": ${String(err)}`);
|
|
@@ -494,6 +520,24 @@ var brightDataVideoSearchSchema = z.object({
|
|
|
494
520
|
lang: z.string().optional().describe("Two-letter ISO language code for result preference (e.g. en, de, ja)")
|
|
495
521
|
});
|
|
496
522
|
|
|
523
|
+
// src/tools/helpers/localized-query-suffix.helper.ts
|
|
524
|
+
function localizedQuerySuffix(lang) {
|
|
525
|
+
if (!lang) return "";
|
|
526
|
+
let code;
|
|
527
|
+
try {
|
|
528
|
+
code = new Intl.Locale(lang).language;
|
|
529
|
+
} catch {
|
|
530
|
+
return "";
|
|
531
|
+
}
|
|
532
|
+
if (!code || code === "en") return "";
|
|
533
|
+
try {
|
|
534
|
+
const name = new Intl.DisplayNames([code], { type: "language" }).of(code);
|
|
535
|
+
return !name || name === code ? "" : name;
|
|
536
|
+
} catch {
|
|
537
|
+
return "";
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
497
541
|
// src/tools/helpers/build-youtube-thumbnail-url.helper.ts
|
|
498
542
|
var YOUTUBE_ID_PATTERN = /^[A-Za-z0-9_-]{11}$/;
|
|
499
543
|
function extractYoutubeVideoId(url) {
|
|
@@ -521,22 +565,21 @@ function buildYoutubeThumbnailUrl(url) {
|
|
|
521
565
|
return id ? `https://i.ytimg.com/vi/${id}/maxresdefault.jpg` : void 0;
|
|
522
566
|
}
|
|
523
567
|
|
|
524
|
-
// src/tools/helpers/
|
|
525
|
-
function
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
}
|
|
568
|
+
// src/tools/bright-data/helpers/map-bright-data-video-result.helper.ts
|
|
569
|
+
function mapBrightDataVideoResult(r) {
|
|
570
|
+
return {
|
|
571
|
+
title: r.title || "",
|
|
572
|
+
link: r.link || "",
|
|
573
|
+
snippet: r.description || "",
|
|
574
|
+
channel: "",
|
|
575
|
+
duration: r.duration || "",
|
|
576
|
+
date: "",
|
|
577
|
+
// `image` is an embedded base64 thumbnail — derive a direct YouTube
|
|
578
|
+
// thumbnail from the link instead.
|
|
579
|
+
thumbnailUrl: buildYoutubeThumbnailUrl(r.link || "") ?? "",
|
|
580
|
+
source: SOURCE,
|
|
581
|
+
views: 0
|
|
582
|
+
};
|
|
540
583
|
}
|
|
541
584
|
|
|
542
585
|
// src/tools/bright-data/video-search.tool.ts
|
|
@@ -571,19 +614,7 @@ function createBrightDataVideoSearch(deps) {
|
|
|
571
614
|
});
|
|
572
615
|
const videos = data.organic ?? [];
|
|
573
616
|
if (!videos.length) return { results: [] };
|
|
574
|
-
const results = videos.map(
|
|
575
|
-
title: r.title || "",
|
|
576
|
-
link: r.link || "",
|
|
577
|
-
snippet: r.description || "",
|
|
578
|
-
channel: "",
|
|
579
|
-
duration: r.duration || "",
|
|
580
|
-
date: "",
|
|
581
|
-
// `image` is an embedded base64 thumbnail — derive a direct YouTube
|
|
582
|
-
// thumbnail from the link instead.
|
|
583
|
-
thumbnailUrl: buildYoutubeThumbnailUrl(r.link || "") ?? "",
|
|
584
|
-
source: SOURCE,
|
|
585
|
-
views: 0
|
|
586
|
-
}));
|
|
617
|
+
const results = videos.map(mapBrightDataVideoResult);
|
|
587
618
|
deps.logger.log(`Bright Data video search returned ${results.length} results for "${searchQuery}"`);
|
|
588
619
|
return { results };
|
|
589
620
|
} catch (err) {
|
|
@@ -598,6 +629,18 @@ var brightDataWebSearchSchema = z.object({
|
|
|
598
629
|
recency: z.enum(["day", "week", "month", "year"]).optional().describe(RECENCY_DESCRIPTION),
|
|
599
630
|
lang: z.string().optional().describe("Two-letter ISO language code for result preference (e.g. en, de, ja)")
|
|
600
631
|
});
|
|
632
|
+
|
|
633
|
+
// src/tools/bright-data/helpers/map-bright-data-web-result.helper.ts
|
|
634
|
+
function mapBrightDataWebResult(r) {
|
|
635
|
+
return {
|
|
636
|
+
title: r.title,
|
|
637
|
+
snippet: r.description || "",
|
|
638
|
+
url: r.link,
|
|
639
|
+
source: SOURCE
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// src/tools/bright-data/web-search.tool.ts
|
|
601
644
|
function createBrightDataWebSearch(deps) {
|
|
602
645
|
return tool({
|
|
603
646
|
description: 'Search the web using Bright Data SERP API (Google). Returns organic results with titles, snippets, and links. Pass recency ("day"|"week"|"month"|"year") to restrict to fresh results. ' + STANDALONE_QUERY_TOOL_CLAUSE,
|
|
@@ -625,12 +668,7 @@ function createBrightDataWebSearch(deps) {
|
|
|
625
668
|
deps.logger.warn(`Bright Data returned 0 results for "${query}"`);
|
|
626
669
|
return { results: [] };
|
|
627
670
|
}
|
|
628
|
-
const results = organic.map(
|
|
629
|
-
title: r.title,
|
|
630
|
-
snippet: r.description || "",
|
|
631
|
-
url: r.link,
|
|
632
|
-
source: SOURCE
|
|
633
|
-
}));
|
|
671
|
+
const results = organic.map(mapBrightDataWebResult);
|
|
634
672
|
deps.logger.log(`Bright Data returned ${results.length} results for "${query}"`);
|
|
635
673
|
return { results };
|
|
636
674
|
} catch (err) {
|
|
@@ -705,6 +743,16 @@ function pickMerchantResult(results, storeToken) {
|
|
|
705
743
|
return void 0;
|
|
706
744
|
}
|
|
707
745
|
|
|
746
|
+
// src/tools/helpers/apply-resolved-merchant-url.helper.ts
|
|
747
|
+
function applyResolvedMerchantUrl(offer, merchantUrlByLink) {
|
|
748
|
+
return merchantUrlByLink.has(offer.link) ? { ...offer, link: merchantUrlByLink.get(offer.link) } : offer;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// src/tools/helpers/map-organic-entry-to-url.helper.ts
|
|
752
|
+
function mapOrganicEntryToUrl(entry) {
|
|
753
|
+
return { url: entry.link };
|
|
754
|
+
}
|
|
755
|
+
|
|
708
756
|
// src/tools/helpers/store-host-token.helper.ts
|
|
709
757
|
var NON_IDENTITY_TOKENS = /* @__PURE__ */ new Set([
|
|
710
758
|
// company/legal forms
|
|
@@ -772,30 +820,9 @@ function storeHostToken(source) {
|
|
|
772
820
|
return source.toLowerCase().split(/[^a-z0-9]+/).filter((token) => token.length > 0 && !NON_IDENTITY_TOKENS.has(token)).join("");
|
|
773
821
|
}
|
|
774
822
|
|
|
775
|
-
// src/tools/helpers/
|
|
823
|
+
// src/tools/helpers/resolve-offer-merchant-url.helper.ts
|
|
776
824
|
var SERPER_SEARCH_URL = "https://google.serper.dev/search";
|
|
777
|
-
var MAX_LINK_RESOLUTIONS = 12;
|
|
778
825
|
var RESOLUTION_RESULT_COUNT = 3;
|
|
779
|
-
async function resolveSerperShopOfferLinks(offers, deps) {
|
|
780
|
-
const googleLinked = offers.filter((offer) => offer.link && isGoogleHostUrl(offer.link));
|
|
781
|
-
if (googleLinked.length === 0) return offers;
|
|
782
|
-
const resolvable = googleLinked.slice(0, MAX_LINK_RESOLUTIONS);
|
|
783
|
-
const resolutions = await Promise.all(
|
|
784
|
-
resolvable.map(async (offer) => ({
|
|
785
|
-
offer,
|
|
786
|
-
merchantUrl: await fetchMerchantUrl(offer, deps)
|
|
787
|
-
}))
|
|
788
|
-
);
|
|
789
|
-
const merchantUrlByLink = /* @__PURE__ */ new Map();
|
|
790
|
-
for (const { offer, merchantUrl } of resolutions) {
|
|
791
|
-
if (merchantUrl) merchantUrlByLink.set(offer.link, merchantUrl);
|
|
792
|
-
}
|
|
793
|
-
if (merchantUrlByLink.size === 0) return offers;
|
|
794
|
-
deps.logger.log(`Resolved ${merchantUrlByLink.size}/${googleLinked.length} Google shop link(s) to merchant URLs`);
|
|
795
|
-
return offers.map(
|
|
796
|
-
(offer) => merchantUrlByLink.has(offer.link) ? { ...offer, link: merchantUrlByLink.get(offer.link) } : offer
|
|
797
|
-
);
|
|
798
|
-
}
|
|
799
826
|
async function fetchMerchantUrl(offer, deps) {
|
|
800
827
|
const token = storeHostToken(offer.source ?? "");
|
|
801
828
|
if (!token || !offer.title) return void 0;
|
|
@@ -819,14 +846,33 @@ async function fetchMerchantUrl(offer, deps) {
|
|
|
819
846
|
);
|
|
820
847
|
if (!res.ok) return void 0;
|
|
821
848
|
const data = await res.json();
|
|
822
|
-
return pickMerchantResult(
|
|
823
|
-
(data.organic ?? []).map((entry) => ({ url: entry.link })),
|
|
824
|
-
token
|
|
825
|
-
);
|
|
849
|
+
return pickMerchantResult((data.organic ?? []).map(mapOrganicEntryToUrl), token);
|
|
826
850
|
} catch {
|
|
827
851
|
return void 0;
|
|
828
852
|
}
|
|
829
853
|
}
|
|
854
|
+
async function resolveOfferMerchantUrl(offer, deps) {
|
|
855
|
+
return {
|
|
856
|
+
offer,
|
|
857
|
+
merchantUrl: await fetchMerchantUrl(offer, deps)
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// src/tools/helpers/serper-shop-links.ts
|
|
862
|
+
var MAX_LINK_RESOLUTIONS = 12;
|
|
863
|
+
async function resolveSerperShopOfferLinks(offers, deps) {
|
|
864
|
+
const googleLinked = offers.filter((offer) => offer.link && isGoogleHostUrl(offer.link));
|
|
865
|
+
if (googleLinked.length === 0) return offers;
|
|
866
|
+
const resolvable = googleLinked.slice(0, MAX_LINK_RESOLUTIONS);
|
|
867
|
+
const resolutions = await Promise.all(resolvable.map((offer) => resolveOfferMerchantUrl(offer, deps)));
|
|
868
|
+
const merchantUrlByLink = /* @__PURE__ */ new Map();
|
|
869
|
+
for (const { offer, merchantUrl } of resolutions) {
|
|
870
|
+
if (merchantUrl) merchantUrlByLink.set(offer.link, merchantUrl);
|
|
871
|
+
}
|
|
872
|
+
if (merchantUrlByLink.size === 0) return offers;
|
|
873
|
+
deps.logger.log(`Resolved ${merchantUrlByLink.size}/${googleLinked.length} Google shop link(s) to merchant URLs`);
|
|
874
|
+
return offers.map((offer) => applyResolvedMerchantUrl(offer, merchantUrlByLink));
|
|
875
|
+
}
|
|
830
876
|
var variantRequestSchema = z.object({});
|
|
831
877
|
function createVariantRequestTool(variant) {
|
|
832
878
|
const descriptions = {
|
|
@@ -950,7 +996,8 @@ function createMemoryPartitionRecallTool(deps) {
|
|
|
950
996
|
const lines = hits.map((hit) => {
|
|
951
997
|
const who = hit.role === "user" ? "the user" : "you (assistant)";
|
|
952
998
|
const when = hit.createdAt ? ` on ${new Date(hit.createdAt).toISOString().slice(0, 10)}` : "";
|
|
953
|
-
|
|
999
|
+
const contested = hit.isFriction ? " \u2014 \u26A0 CONTESTED (an open conflict exists; treat with caution)" : "";
|
|
1000
|
+
return `- "${hit.text}" \u2014 stated by ${who}${when}${contested}`;
|
|
954
1001
|
});
|
|
955
1002
|
return `YOUR MEMORY OF THIS USER (trusted statements they said or asked you to remember \u2014 answer from them and attribute them to the user; never present them as public web knowledge):
|
|
956
1003
|
${lines.join("\n")}`;
|
|
@@ -1000,6 +1047,18 @@ var serperBusinessReviewsSearchSchema = z.object({
|
|
|
1000
1047
|
lang: z.string().optional().describe("Two-letter ISO language code for result preference (e.g. en, de, ja)")
|
|
1001
1048
|
});
|
|
1002
1049
|
|
|
1050
|
+
// src/tools/serper/helpers/map-serper-review-result.helper.ts
|
|
1051
|
+
function mapSerperReviewResult(r, placeName) {
|
|
1052
|
+
return {
|
|
1053
|
+
author: r.user?.name || "",
|
|
1054
|
+
snippet: r.snippet || "",
|
|
1055
|
+
rating: r.rating,
|
|
1056
|
+
date: r.isoDate || r.date || "",
|
|
1057
|
+
likes: r.likes ?? 0,
|
|
1058
|
+
place: placeName
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1003
1062
|
// src/tools/serper/serper.constants.ts
|
|
1004
1063
|
var HEADERS = (apiKey) => ({
|
|
1005
1064
|
"X-API-KEY": apiKey,
|
|
@@ -1053,14 +1112,7 @@ function createSerperBusinessReviewsSearch(deps) {
|
|
|
1053
1112
|
return { results: [] };
|
|
1054
1113
|
}
|
|
1055
1114
|
const placeName = data.placeInfo?.title || "";
|
|
1056
|
-
const results = data.reviews.map((r) => (
|
|
1057
|
-
author: r.user?.name || "",
|
|
1058
|
-
snippet: r.snippet || "",
|
|
1059
|
-
rating: r.rating,
|
|
1060
|
-
date: r.isoDate || r.date || "",
|
|
1061
|
-
likes: r.likes ?? 0,
|
|
1062
|
-
place: placeName
|
|
1063
|
-
}));
|
|
1115
|
+
const results = data.reviews.map((r) => mapSerperReviewResult(r, placeName));
|
|
1064
1116
|
return {
|
|
1065
1117
|
results,
|
|
1066
1118
|
place: data.placeInfo ? {
|
|
@@ -1085,6 +1137,21 @@ var serperImageSearchSchema = z.object({
|
|
|
1085
1137
|
lang: z.string().optional().describe("Two-letter ISO language code for result preference (e.g. en, de, ja)"),
|
|
1086
1138
|
recency: z.enum(["day", "week", "month", "year"]).optional().describe(RECENCY_DESCRIPTION)
|
|
1087
1139
|
});
|
|
1140
|
+
|
|
1141
|
+
// src/tools/serper/helpers/map-serper-image-result.helper.ts
|
|
1142
|
+
function mapSerperImageResult(r) {
|
|
1143
|
+
return {
|
|
1144
|
+
title: r.title || "",
|
|
1145
|
+
imageUrl: r.imageUrl || r.image || "",
|
|
1146
|
+
sourcePageUrl: r.link || "",
|
|
1147
|
+
width: r.imageWidth ?? r.width,
|
|
1148
|
+
height: r.imageHeight ?? r.height,
|
|
1149
|
+
source: r.source || "",
|
|
1150
|
+
domain: r.domain || ""
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// src/tools/serper/image-search.tool.ts
|
|
1088
1155
|
function createSerperImageSearch(deps) {
|
|
1089
1156
|
return tool({
|
|
1090
1157
|
description: 'Search for images using Serper.dev (Google Images). Returns image URLs, thumbnails, source pages, and dimensions. The tool prefers 2560\xD71440 (1440p) images and always enforces a minimum of 1280\xD7720 (720p). It passes the appropriate Google Images `tbs=isz:lt,islt:<bucket>` size filter server-side, drops any returned images whose dimensions are below 1280\xD7720, and rejects untrusted domains such as Google thumbnail proxies (encrypted-tbn*.gstatic.com, t*.gstatic.com), data URIs, localhost, and private IPs. You do not need to pass minWidth/minHeight for the default 720p floor. If the user asks for a higher resolution, pass minWidth/minHeight and the tool will pick the smallest Google bucket that can satisfy the requested area. Common reference: 1280\xD7720 (720p) ~0.9 MP, 1920\xD71080 (1080p) ~2 MP, 2560\xD71440 (1440p) ~3.7 MP, 3840\xD72160 (4K) ~8.3 MP. Pass recency ("day"|"week"|"month"|"year") to restrict to recently published images. ' + STANDALONE_QUERY_TOOL_CLAUSE,
|
|
@@ -1131,15 +1198,7 @@ function createSerperImageSearch(deps) {
|
|
|
1131
1198
|
deps.logger.warn(`Serper.dev Image Search returned 0 results for "${query}"`);
|
|
1132
1199
|
return { results: [] };
|
|
1133
1200
|
}
|
|
1134
|
-
const results = data.images.map((r) =>
|
|
1135
|
-
title: r.title || "",
|
|
1136
|
-
imageUrl: r.imageUrl || r.image || "",
|
|
1137
|
-
sourcePageUrl: r.link || "",
|
|
1138
|
-
width: r.imageWidth ?? r.width,
|
|
1139
|
-
height: r.imageHeight ?? r.height,
|
|
1140
|
-
source: r.source || "",
|
|
1141
|
-
domain: r.domain || ""
|
|
1142
|
-
})).filter((r) => {
|
|
1201
|
+
const results = data.images.map(mapSerperImageResult).filter((r) => {
|
|
1143
1202
|
if (!isTrustedImageUrl(r.imageUrl)) return false;
|
|
1144
1203
|
const w = r.width ?? 0;
|
|
1145
1204
|
const h = r.height ?? 0;
|
|
@@ -1157,6 +1216,20 @@ var serperNewsSearchSchema = z.object({
|
|
|
1157
1216
|
recency: z.enum(["day", "week", "month", "year"]).optional().describe(RECENCY_DESCRIPTION),
|
|
1158
1217
|
lang: z.string().optional().describe("Two-letter ISO language code for result preference (e.g. en, de, ja)")
|
|
1159
1218
|
});
|
|
1219
|
+
|
|
1220
|
+
// src/tools/serper/helpers/map-serper-news-result.helper.ts
|
|
1221
|
+
function mapSerperNewsResult(r) {
|
|
1222
|
+
return {
|
|
1223
|
+
title: r.title,
|
|
1224
|
+
snippet: r.snippet || "",
|
|
1225
|
+
url: r.link,
|
|
1226
|
+
source: r.source || "",
|
|
1227
|
+
date: r.date || "",
|
|
1228
|
+
imageUrl: r.imageUrl || ""
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// src/tools/serper/news-search.tool.ts
|
|
1160
1233
|
function createSerperNewsSearch(deps) {
|
|
1161
1234
|
return tool({
|
|
1162
1235
|
description: 'Search the latest news using Serper.dev. Returns headlines, sources, dates, and snippets. Pass recency ("day"|"week"|"month"|"year") to restrict to a recent period. ' + STANDALONE_QUERY_TOOL_CLAUSE,
|
|
@@ -1186,14 +1259,7 @@ function createSerperNewsSearch(deps) {
|
|
|
1186
1259
|
if (!res.ok) return { results: [] };
|
|
1187
1260
|
const data = await res.json();
|
|
1188
1261
|
if (!data.news?.length) return { results: [] };
|
|
1189
|
-
const results = data.news.map(
|
|
1190
|
-
title: r.title,
|
|
1191
|
-
snippet: r.snippet || "",
|
|
1192
|
-
url: r.link,
|
|
1193
|
-
source: r.source || "",
|
|
1194
|
-
date: r.date || "",
|
|
1195
|
-
imageUrl: r.imageUrl || ""
|
|
1196
|
-
}));
|
|
1262
|
+
const results = data.news.map(mapSerperNewsResult);
|
|
1197
1263
|
return { results };
|
|
1198
1264
|
}
|
|
1199
1265
|
});
|
|
@@ -1205,6 +1271,24 @@ var serperPlacesSearchSchema = z.object({
|
|
|
1205
1271
|
count: z.number().optional().describe("Number of results (max 100)"),
|
|
1206
1272
|
lang: z.string().optional().describe("Two-letter ISO language code for result preference (e.g. en, de, ja)")
|
|
1207
1273
|
});
|
|
1274
|
+
|
|
1275
|
+
// src/tools/serper/helpers/map-serper-place-result.helper.ts
|
|
1276
|
+
function mapSerperPlaceResult(r) {
|
|
1277
|
+
return {
|
|
1278
|
+
title: r.title,
|
|
1279
|
+
address: r.address || "",
|
|
1280
|
+
phoneNumber: r.phoneNumber || "",
|
|
1281
|
+
latitude: r.latitude,
|
|
1282
|
+
longitude: r.longitude,
|
|
1283
|
+
rating: r.rating,
|
|
1284
|
+
ratingCount: r.ratingCount,
|
|
1285
|
+
type: r.type || "",
|
|
1286
|
+
website: r.website || "",
|
|
1287
|
+
cid: r.cid || ""
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
// src/tools/serper/places-search.tool.ts
|
|
1208
1292
|
function createSerperPlacesSearch(deps) {
|
|
1209
1293
|
return tool({
|
|
1210
1294
|
description: 'Search for places and businesses using Serper.dev (Google Maps). Returns addresses, phone numbers, ratings, review counts, and coordinates. Phrase the query like a Google Maps search: a business name, a business type, or a business type plus location (e.g. "MediaMarkt Berlin", "coffee shops in Munich").',
|
|
@@ -1235,18 +1319,7 @@ function createSerperPlacesSearch(deps) {
|
|
|
1235
1319
|
deps.logger.warn(`Serper.dev Places returned 0 results for "${query}"`);
|
|
1236
1320
|
return { results: [] };
|
|
1237
1321
|
}
|
|
1238
|
-
const results = data.places.map(
|
|
1239
|
-
title: r.title,
|
|
1240
|
-
address: r.address || "",
|
|
1241
|
-
phoneNumber: r.phoneNumber || "",
|
|
1242
|
-
latitude: r.latitude,
|
|
1243
|
-
longitude: r.longitude,
|
|
1244
|
-
rating: r.rating,
|
|
1245
|
-
ratingCount: r.ratingCount,
|
|
1246
|
-
type: r.type || "",
|
|
1247
|
-
website: r.website || "",
|
|
1248
|
-
cid: r.cid || ""
|
|
1249
|
-
}));
|
|
1322
|
+
const results = data.places.map(mapSerperPlaceResult);
|
|
1250
1323
|
return { results };
|
|
1251
1324
|
}
|
|
1252
1325
|
});
|
|
@@ -1258,6 +1331,22 @@ var serperShoppingSearchSchema = z.object({
|
|
|
1258
1331
|
count: z.number().optional().describe("Number of results (max 100)"),
|
|
1259
1332
|
lang: z.string().optional().describe("Two-letter ISO language code for result preference (e.g. en, de, ja)")
|
|
1260
1333
|
});
|
|
1334
|
+
|
|
1335
|
+
// src/tools/serper/helpers/map-serper-shopping-result.helper.ts
|
|
1336
|
+
function mapSerperShoppingResult(r) {
|
|
1337
|
+
return {
|
|
1338
|
+
title: r.title,
|
|
1339
|
+
price: r.price || "",
|
|
1340
|
+
link: r.link || "",
|
|
1341
|
+
source: r.source || "",
|
|
1342
|
+
imageUrl: r.imageUrl || "",
|
|
1343
|
+
delivery: r.delivery || "",
|
|
1344
|
+
rating: r.rating,
|
|
1345
|
+
ratingCount: r.ratingCount
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// src/tools/serper/shopping-search.tool.ts
|
|
1261
1350
|
function createSerperShoppingSearch(deps) {
|
|
1262
1351
|
return tool({
|
|
1263
1352
|
description: 'Search for products using Serper.dev (Google Shopping). Returns prices, sellers, delivery info, images, and per-offer ratings. Phrase the query as the bare product name with model number (e.g. "Sony WH-1000XM5") \u2014 do NOT add words like "review", "test", or long descriptive sentences, they hurt shopping result quality.',
|
|
@@ -1290,16 +1379,7 @@ function createSerperShoppingSearch(deps) {
|
|
|
1290
1379
|
deps.logger.warn(`Serper.dev Shopping returned 0 results for "${query}"`);
|
|
1291
1380
|
return { results: [] };
|
|
1292
1381
|
}
|
|
1293
|
-
const results = data.shopping.map(
|
|
1294
|
-
title: r.title,
|
|
1295
|
-
price: r.price || "",
|
|
1296
|
-
link: r.link || "",
|
|
1297
|
-
source: r.source || "",
|
|
1298
|
-
imageUrl: r.imageUrl || "",
|
|
1299
|
-
delivery: r.delivery || "",
|
|
1300
|
-
rating: r.rating,
|
|
1301
|
-
ratingCount: r.ratingCount
|
|
1302
|
-
}));
|
|
1382
|
+
const results = data.shopping.map(mapSerperShoppingResult);
|
|
1303
1383
|
return {
|
|
1304
1384
|
results: await resolveSerperShopOfferLinks(results, {
|
|
1305
1385
|
apiKey: cfg.apiKey,
|
|
@@ -1333,6 +1413,29 @@ function repairVideoLink(link) {
|
|
|
1333
1413
|
return CONTAMINATION_PATTERN.test(link) ? void 0 : link;
|
|
1334
1414
|
}
|
|
1335
1415
|
|
|
1416
|
+
// src/tools/serper/helpers/map-serper-video-result.helper.ts
|
|
1417
|
+
function mapSerperVideoResult(input) {
|
|
1418
|
+
const { r, link } = input;
|
|
1419
|
+
return {
|
|
1420
|
+
title: r.title,
|
|
1421
|
+
link,
|
|
1422
|
+
// Keep the provider's original link when it had to be repaired —
|
|
1423
|
+
// auditability for provider-side payload corruption.
|
|
1424
|
+
...link !== r.link ? { originalLink: r.link } : {},
|
|
1425
|
+
snippet: r.snippet || "",
|
|
1426
|
+
channel: r.channel || "",
|
|
1427
|
+
duration: r.duration || "",
|
|
1428
|
+
date: r.date || "",
|
|
1429
|
+
// Serper thumbnails are Google proxy images (blocked by our image
|
|
1430
|
+
// trust rules) — derive a direct thumbnail for YouTube instead.
|
|
1431
|
+
// maxresdefault is not guaranteed to exist; consumers degrade to
|
|
1432
|
+
// hqdefault/mqdefault on failure.
|
|
1433
|
+
thumbnailUrl: buildYoutubeThumbnailUrl(link) ?? "",
|
|
1434
|
+
source: r.source || "",
|
|
1435
|
+
views: r.views ?? 0
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1336
1439
|
// src/tools/serper/video-search.tool.ts
|
|
1337
1440
|
function createSerperVideoSearch(deps) {
|
|
1338
1441
|
return tool({
|
|
@@ -1368,24 +1471,7 @@ function createSerperVideoSearch(deps) {
|
|
|
1368
1471
|
const results = data.videos.flatMap((r) => {
|
|
1369
1472
|
const link = repairVideoLink(r.link);
|
|
1370
1473
|
return link ? [{ r, link }] : [];
|
|
1371
|
-
}).slice(0, num).map(
|
|
1372
|
-
title: r.title,
|
|
1373
|
-
link,
|
|
1374
|
-
// Keep the provider's original link when it had to be repaired —
|
|
1375
|
-
// auditability for provider-side payload corruption.
|
|
1376
|
-
...link !== r.link ? { originalLink: r.link } : {},
|
|
1377
|
-
snippet: r.snippet || "",
|
|
1378
|
-
channel: r.channel || "",
|
|
1379
|
-
duration: r.duration || "",
|
|
1380
|
-
date: r.date || "",
|
|
1381
|
-
// Serper thumbnails are Google proxy images (blocked by our image
|
|
1382
|
-
// trust rules) — derive a direct thumbnail for YouTube instead.
|
|
1383
|
-
// maxresdefault is not guaranteed to exist; consumers degrade to
|
|
1384
|
-
// hqdefault/mqdefault on failure.
|
|
1385
|
-
thumbnailUrl: buildYoutubeThumbnailUrl(link) ?? "",
|
|
1386
|
-
source: r.source || "",
|
|
1387
|
-
views: r.views ?? 0
|
|
1388
|
-
}));
|
|
1474
|
+
}).slice(0, num).map(mapSerperVideoResult);
|
|
1389
1475
|
deps.logger.log(`Serper.dev Video search returned ${results.length} results for "${searchQuery}"`);
|
|
1390
1476
|
return { results };
|
|
1391
1477
|
}
|
|
@@ -1396,6 +1482,18 @@ var serperWebSearchSchema = z.object({
|
|
|
1396
1482
|
recency: z.enum(["day", "week", "month", "year"]).optional().describe(RECENCY_DESCRIPTION),
|
|
1397
1483
|
lang: z.string().optional().describe("Two-letter ISO language code for result preference (e.g. en, de, ja)")
|
|
1398
1484
|
});
|
|
1485
|
+
|
|
1486
|
+
// src/tools/serper/helpers/map-serper-web-result.helper.ts
|
|
1487
|
+
function mapSerperWebResult(r) {
|
|
1488
|
+
return {
|
|
1489
|
+
title: r.title,
|
|
1490
|
+
snippet: r.snippet || "",
|
|
1491
|
+
url: r.link,
|
|
1492
|
+
source: "serper"
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
// src/tools/serper/web-search.tool.ts
|
|
1399
1497
|
function createSerperWebSearch(deps) {
|
|
1400
1498
|
return tool({
|
|
1401
1499
|
description: 'Search the web using Serper.dev (Google results). Returns organic results with titles, snippets, and links. Pass recency ("day"|"week"|"month"|"year") to restrict to fresh results. ' + STANDALONE_QUERY_TOOL_CLAUSE,
|
|
@@ -1430,12 +1528,7 @@ function createSerperWebSearch(deps) {
|
|
|
1430
1528
|
deps.logger.warn(`Serper.dev returned 0 results for "${query}"`);
|
|
1431
1529
|
return { results: [] };
|
|
1432
1530
|
}
|
|
1433
|
-
const results = data.organic.map(
|
|
1434
|
-
title: r.title,
|
|
1435
|
-
snippet: r.snippet || "",
|
|
1436
|
-
url: r.link,
|
|
1437
|
-
source: "serper"
|
|
1438
|
-
}));
|
|
1531
|
+
const results = data.organic.map(mapSerperWebResult);
|
|
1439
1532
|
deps.logger.log(`Serper.dev returned ${results.length} results for "${query}"`);
|
|
1440
1533
|
return { results };
|
|
1441
1534
|
}
|
|
@@ -1550,6 +1643,35 @@ function createWebFetchTool() {
|
|
|
1550
1643
|
}
|
|
1551
1644
|
});
|
|
1552
1645
|
}
|
|
1646
|
+
|
|
1647
|
+
// src/tools/youtube/helpers/map-youtube-video-result.helper.ts
|
|
1648
|
+
function formatIsoDuration(iso) {
|
|
1649
|
+
const match = iso?.match(/^PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?$/);
|
|
1650
|
+
if (!match) return "";
|
|
1651
|
+
const hours = Number(match[1] ?? 0);
|
|
1652
|
+
const minutes = Number(match[2] ?? 0);
|
|
1653
|
+
const seconds = Number(match[3] ?? 0);
|
|
1654
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1655
|
+
return hours ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${minutes}:${pad(seconds)}`;
|
|
1656
|
+
}
|
|
1657
|
+
function mapYoutubeVideoResult(item, stats) {
|
|
1658
|
+
const snippet = item.snippet ?? {};
|
|
1659
|
+
const id = item.id.videoId;
|
|
1660
|
+
const detail = stats.get(id);
|
|
1661
|
+
const thumbs = snippet.thumbnails ?? {};
|
|
1662
|
+
return {
|
|
1663
|
+
title: snippet.title ?? "",
|
|
1664
|
+
link: `https://www.youtube.com/watch?v=${id}`,
|
|
1665
|
+
snippet: snippet.description ?? "",
|
|
1666
|
+
channel: snippet.channelTitle ?? "",
|
|
1667
|
+
duration: formatIsoDuration(detail?.duration),
|
|
1668
|
+
date: snippet.publishedAt ?? "",
|
|
1669
|
+
thumbnailUrl: thumbs.maxres?.url ?? thumbs.high?.url ?? thumbs.medium?.url ?? "",
|
|
1670
|
+
source: "youtube",
|
|
1671
|
+
views: detail?.viewCount ?? 0,
|
|
1672
|
+
lang: detail?.lang
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1553
1675
|
var youtubeVideoSearchSchema = z.object({
|
|
1554
1676
|
query: z.string().describe(`${STANDALONE_QUERY_DESCRIPTION} Add the video type (e.g. review, trailer, tutorial, gameplay).`),
|
|
1555
1677
|
count: z.number().optional().describe("Number of results (max 50)"),
|
|
@@ -1580,15 +1702,6 @@ async function readErrorReason(res) {
|
|
|
1580
1702
|
return void 0;
|
|
1581
1703
|
}
|
|
1582
1704
|
}
|
|
1583
|
-
function formatIsoDuration(iso) {
|
|
1584
|
-
const match = iso?.match(/^PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?$/);
|
|
1585
|
-
if (!match) return "";
|
|
1586
|
-
const hours = Number(match[1] ?? 0);
|
|
1587
|
-
const minutes = Number(match[2] ?? 0);
|
|
1588
|
-
const seconds = Number(match[3] ?? 0);
|
|
1589
|
-
const pad = (n) => String(n).padStart(2, "0");
|
|
1590
|
-
return hours ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${minutes}:${pad(seconds)}`;
|
|
1591
|
-
}
|
|
1592
1705
|
async function fetchYoutubeVideoStats(videoIds, apiKey, logger) {
|
|
1593
1706
|
const params = new URLSearchParams({
|
|
1594
1707
|
part: "statistics,contentDetails,snippet",
|
|
@@ -1668,24 +1781,7 @@ function createYoutubeVideoSearch(deps) {
|
|
|
1668
1781
|
cfg.apiKey,
|
|
1669
1782
|
deps.logger
|
|
1670
1783
|
);
|
|
1671
|
-
const results = items.map((item) =>
|
|
1672
|
-
const snippet = item.snippet ?? {};
|
|
1673
|
-
const id = item.id.videoId;
|
|
1674
|
-
const detail = stats.get(id);
|
|
1675
|
-
const thumbs = snippet.thumbnails ?? {};
|
|
1676
|
-
return {
|
|
1677
|
-
title: snippet.title ?? "",
|
|
1678
|
-
link: `https://www.youtube.com/watch?v=${id}`,
|
|
1679
|
-
snippet: snippet.description ?? "",
|
|
1680
|
-
channel: snippet.channelTitle ?? "",
|
|
1681
|
-
duration: formatIsoDuration(detail?.duration),
|
|
1682
|
-
date: snippet.publishedAt ?? "",
|
|
1683
|
-
thumbnailUrl: thumbs.maxres?.url ?? thumbs.high?.url ?? thumbs.medium?.url ?? "",
|
|
1684
|
-
source: "youtube",
|
|
1685
|
-
views: detail?.viewCount ?? 0,
|
|
1686
|
-
lang: detail?.lang
|
|
1687
|
-
};
|
|
1688
|
-
});
|
|
1784
|
+
const results = items.map((item) => mapYoutubeVideoResult(item, stats));
|
|
1689
1785
|
deps.logger.log(`YouTube video search returned ${results.length} results for "${query}"`);
|
|
1690
1786
|
return { results };
|
|
1691
1787
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@triplef/agent",
|
|
3
3
|
"description": "tripleF (3F) agent domain — structured-output schemas, prompt builders, and model tools shared across the apps.",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.12",
|
|
5
5
|
"packageManager": "pnpm@11.25.0",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"turndown": "^7.2.4"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
63
|
-
"ai": "^7.0.
|
|
63
|
+
"ai": "^7.0.87",
|
|
64
64
|
"zod": "^4.5.4"
|
|
65
65
|
},
|
|
66
66
|
"peerDependenciesMeta": {
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
"ts-unused-exports": "^11.0.1",
|
|
93
93
|
"tsup": "^8.5.1",
|
|
94
94
|
"typescript": "^5.9.3",
|
|
95
|
-
"typescript-eslint": "^8.
|
|
95
|
+
"typescript-eslint": "^8.69.0",
|
|
96
96
|
"vitest": "^4.1.11"
|
|
97
97
|
}
|
|
98
98
|
}
|