@stackfactor/agent-utils 1.2.20 → 1.3.1
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 +68 -40
- package/dist/cjs/index.d.ts +2 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/langChain.d.ts +5 -62
- package/dist/cjs/langChain.d.ts.map +1 -1
- package/dist/cjs/langChain.js +50 -287
- package/dist/cjs/tavily.d.ts +91 -0
- package/dist/cjs/tavily.d.ts.map +1 -0
- package/dist/cjs/tavily.js +404 -0
- package/dist/esm/index.d.ts +2 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/langChain.d.ts +5 -62
- package/dist/esm/langChain.d.ts.map +1 -1
- package/dist/esm/langChain.js +50 -287
- package/dist/esm/tavily.d.ts +91 -0
- package/dist/esm/tavily.d.ts.map +1 -0
- package/dist/esm/tavily.js +395 -0
- package/package.json +1 -1
package/dist/esm/langChain.js
CHANGED
|
@@ -10,6 +10,7 @@ import logger from "./logger.js";
|
|
|
10
10
|
import { z } from "zod";
|
|
11
11
|
import { transformJSONSchema } from "@anthropic-ai/sdk/lib/transform-json-schema";
|
|
12
12
|
import { getAbortSignal } from "./runtimeContext.js";
|
|
13
|
+
import { assertTavilyUsable, buildTavilyTools, getTavilyOptions, } from "./tavily.js";
|
|
13
14
|
const JSON_ESCAPE_INSTRUCTION = `
|
|
14
15
|
CRITICAL - Your response must be valid JSON. Escape ALL special characters in string values:
|
|
15
16
|
- Newlines → \\n
|
|
@@ -603,114 +604,12 @@ const resolveTemperatureSetting = (modelName, config) => {
|
|
|
603
604
|
}
|
|
604
605
|
return { temperature: clamped };
|
|
605
606
|
};
|
|
606
|
-
/**
|
|
607
|
-
* Whether a Claude model can run web search through dynamic filtering, where
|
|
608
|
-
* Claude writes and runs code that filters the search results before they reach
|
|
609
|
-
* the context window instead of loading every result into it. Requires Claude
|
|
610
|
-
* 4.6 or later (the models with programmatic tool calling); on anything earlier
|
|
611
|
-
* the filtering tool versions return a 400 unless search is pinned to
|
|
612
|
-
* `allowed_callers: ["direct"]`.
|
|
613
|
-
* @param modelName - The Claude model identifier being routed
|
|
614
|
-
* @returns `true` when the model supports dynamic filtering
|
|
615
|
-
*/
|
|
616
|
-
const supportsAnthropicDynamicFiltering = (modelName) => /^claude-(opus|sonnet|haiku)-4-(?:[6-9]|\d\d)\b/.test(modelName) ||
|
|
617
|
-
/^claude-(opus|sonnet|haiku|fable|mythos)-(?:[5-9]|\d\d)\b/.test(modelName);
|
|
618
|
-
/**
|
|
619
|
-
* Normalizes `config.webSearch` into an options object, returning `null` when
|
|
620
|
-
* web search is off so callers can use it as the single enablement gate.
|
|
621
|
-
*/
|
|
622
|
-
const getWebSearchOptions = (config) => {
|
|
623
|
-
const webSearch = config?.webSearch;
|
|
624
|
-
if (!webSearch)
|
|
625
|
-
return null;
|
|
626
|
-
return webSearch === true ? {} : webSearch;
|
|
627
|
-
};
|
|
628
|
-
/**
|
|
629
|
-
* Builds the provider-native web search tool definition for a model:
|
|
630
|
-
* - `claude-` → Anthropic's `web_search` server tool, executed by the Messages
|
|
631
|
-
* API within a single request and answered with citations. Defaults to the
|
|
632
|
-
* dynamic-filtering tool version on models that support it, so search results
|
|
633
|
-
* are filtered by code before they reach the context window.
|
|
634
|
-
* - `gpt-` → OpenAI's hosted `web_search` tool (Responses API). Context spend
|
|
635
|
-
* is governed by `search_context_size` (OpenAI defaults to `medium`).
|
|
636
|
-
* - `gemini-` → Google's `googleSearch` grounding tool. Google exposes no
|
|
637
|
-
* result-filtering or context-size control; leaving `searchTypes` unset keeps
|
|
638
|
-
* grounding on text-only web results rather than image bytes.
|
|
639
|
-
* Returns `null` for providers with no native web search (DeepSeek, Kimi, GLM),
|
|
640
|
-
* warning instead of throwing so one config can be pointed at any model.
|
|
641
|
-
* @param modelName - The model identifier being routed
|
|
642
|
-
* @param options - Normalized options from `getWebSearchOptions`
|
|
643
|
-
* @returns The provider's tool definition, or `null` when unsupported
|
|
644
|
-
*/
|
|
645
|
-
const buildWebSearchTool = (modelName, options) => {
|
|
646
|
-
const { allowedDomains, blockedDomains, userLocation } = options;
|
|
647
|
-
if (modelName.startsWith("claude-")) {
|
|
648
|
-
// The API returns a 400 when both filters are present, so fail locally
|
|
649
|
-
// rather than paying for the round trip.
|
|
650
|
-
if (allowedDomains && blockedDomains) {
|
|
651
|
-
throw errorHandlingHelper.create(constants.HTTP_CODES.BAD_REQUEST, "Anthropic web search accepts allowedDomains or blockedDomains, not both.");
|
|
652
|
-
}
|
|
653
|
-
// Prefer the newest tool version the model can actually run. Basic search
|
|
654
|
-
// loads every result into the context window; from `web_search_20260209`
|
|
655
|
-
// Claude filters them with code first, and `web_search_20260318` can also
|
|
656
|
-
// keep the consumed results out of the response.
|
|
657
|
-
const canFilter = supportsAnthropicDynamicFiltering(modelName);
|
|
658
|
-
const type = options.toolVersion ||
|
|
659
|
-
(canFilter ? "web_search_20260318" : "web_search_20250305");
|
|
660
|
-
const version = Number(type.slice(-8));
|
|
661
|
-
// Filtering versions default to running search from inside code execution.
|
|
662
|
-
// Say so explicitly when it is not wanted (or not possible), which is what
|
|
663
|
-
// the API requires from models without programmatic tool calling.
|
|
664
|
-
const directOnly = version >= 20260209 && (options.dynamicFiltering === false || !canFilter);
|
|
665
|
-
return {
|
|
666
|
-
type,
|
|
667
|
-
name: "web_search",
|
|
668
|
-
...(options.maxUses ? { max_uses: options.maxUses } : {}),
|
|
669
|
-
...(allowedDomains ? { allowed_domains: allowedDomains } : {}),
|
|
670
|
-
...(blockedDomains ? { blocked_domains: blockedDomains } : {}),
|
|
671
|
-
...(userLocation
|
|
672
|
-
? { user_location: { type: "approximate", ...userLocation } }
|
|
673
|
-
: {}),
|
|
674
|
-
...(directOnly ? { allowed_callers: ["direct"] } : {}),
|
|
675
|
-
...(version >= 20260318
|
|
676
|
-
? { response_inclusion: options.responseInclusion || "excluded" }
|
|
677
|
-
: {}),
|
|
678
|
-
};
|
|
679
|
-
}
|
|
680
|
-
if (modelName.startsWith("gpt-")) {
|
|
681
|
-
return {
|
|
682
|
-
type: "web_search",
|
|
683
|
-
...(allowedDomains
|
|
684
|
-
? { filters: { allowed_domains: allowedDomains } }
|
|
685
|
-
: {}),
|
|
686
|
-
...(userLocation
|
|
687
|
-
? { user_location: { type: "approximate", ...userLocation } }
|
|
688
|
-
: {}),
|
|
689
|
-
...(options.searchContextSize
|
|
690
|
-
? { search_context_size: options.searchContextSize }
|
|
691
|
-
: {}),
|
|
692
|
-
};
|
|
693
|
-
}
|
|
694
|
-
if (modelName.startsWith("gemini-")) {
|
|
695
|
-
// `timeRangeFilter` is the only filter the Gemini API exposes;
|
|
696
|
-
// `excludeDomains` is a Vertex AI field and is rejected here, so domain
|
|
697
|
-
// filters are deliberately not mapped for Google.
|
|
698
|
-
return {
|
|
699
|
-
googleSearch: options.timeRange
|
|
700
|
-
? { timeRangeFilter: options.timeRange }
|
|
701
|
-
: {},
|
|
702
|
-
};
|
|
703
|
-
}
|
|
704
|
-
logger.log(null, logger.levels.warn, `Model "${modelName}" has no native web search tool; ignoring the configured webSearch options.`);
|
|
705
|
-
return null;
|
|
706
|
-
};
|
|
707
607
|
/**
|
|
708
608
|
* Flattens LangChain message content into plain text. Content is a string for
|
|
709
|
-
* ordinary completions, but
|
|
710
|
-
*
|
|
711
|
-
*
|
|
712
|
-
*
|
|
713
|
-
* would receive a non-string and every web-search call would fail.
|
|
609
|
+
* ordinary completions, but providers switch to an array of blocks whenever the
|
|
610
|
+
* turn carries more than prose — tool-use blocks in agentic runs, or Anthropic's
|
|
611
|
+
* thinking blocks — so without this the JSON parse pipeline would receive a
|
|
612
|
+
* non-string and the call would fail.
|
|
714
613
|
* @param content - A message's `content` field, or a raw string
|
|
715
614
|
* @returns The concatenated text of all text blocks
|
|
716
615
|
*/
|
|
@@ -729,108 +628,6 @@ const extractTextContent = (content) => {
|
|
|
729
628
|
}
|
|
730
629
|
return text;
|
|
731
630
|
};
|
|
732
|
-
const createWebSearchUsage = () => ({
|
|
733
|
-
reportedRequests: 0,
|
|
734
|
-
callIds: new Set(),
|
|
735
|
-
grounded: false,
|
|
736
|
-
sources: new Map(),
|
|
737
|
-
});
|
|
738
|
-
/**
|
|
739
|
-
* Folds one message — or one streaming chunk — into a `WebSearchUsage`. Safe to
|
|
740
|
-
* call on every chunk of a stream and on messages that involved no search.
|
|
741
|
-
*/
|
|
742
|
-
const collectWebSearchUsage = (payload, usage) => {
|
|
743
|
-
if (!payload)
|
|
744
|
-
return;
|
|
745
|
-
const addSource = (url, title) => {
|
|
746
|
-
if (typeof url === "string" && url && !usage.sources.has(url)) {
|
|
747
|
-
usage.sources.set(url, {
|
|
748
|
-
url,
|
|
749
|
-
...(typeof title === "string" ? { title } : {}),
|
|
750
|
-
});
|
|
751
|
-
}
|
|
752
|
-
};
|
|
753
|
-
if (Array.isArray(payload.content)) {
|
|
754
|
-
for (const block of payload.content) {
|
|
755
|
-
if (!block || typeof block !== "object")
|
|
756
|
-
continue;
|
|
757
|
-
// Anthropic: results of a search the API executed server-side.
|
|
758
|
-
if (block.type === "web_search_tool_result" &&
|
|
759
|
-
Array.isArray(block.content)) {
|
|
760
|
-
for (const result of block.content) {
|
|
761
|
-
addSource(result?.url, result?.title);
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
// Anthropic: citations attached to the answer's text blocks.
|
|
765
|
-
if (Array.isArray(block.citations)) {
|
|
766
|
-
for (const citation of block.citations) {
|
|
767
|
-
addSource(citation?.url, citation?.title);
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
// OpenAI Responses API: one block per executed search, plus url citations.
|
|
771
|
-
if (block.type === "web_search_call" && block.id) {
|
|
772
|
-
usage.callIds.add(block.id);
|
|
773
|
-
}
|
|
774
|
-
if (Array.isArray(block.annotations)) {
|
|
775
|
-
for (const annotation of block.annotations) {
|
|
776
|
-
if (annotation?.type === "url_citation") {
|
|
777
|
-
addSource(annotation.url, annotation.title);
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
const metadata = payload.response_metadata;
|
|
784
|
-
if (!metadata)
|
|
785
|
-
return;
|
|
786
|
-
const requests = metadata.usage?.server_tool_use?.web_search_requests;
|
|
787
|
-
if (typeof requests === "number" && requests > usage.reportedRequests) {
|
|
788
|
-
usage.reportedRequests = requests;
|
|
789
|
-
}
|
|
790
|
-
const grounding = metadata.groundingMetadata;
|
|
791
|
-
if (grounding) {
|
|
792
|
-
usage.grounded = true;
|
|
793
|
-
for (const chunk of grounding.groundingChunks || []) {
|
|
794
|
-
addSource(chunk?.web?.uri, chunk?.web?.title);
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
};
|
|
798
|
-
/**
|
|
799
|
-
* Adds a call's web-search usage to the caller-supplied tracker. Searches are
|
|
800
|
-
* billed per request rather than per token (Anthropic charges $10 per 1,000
|
|
801
|
-
* searches; Google charges per grounded request), so the rate is read from the
|
|
802
|
-
* `<model>-web-search-costs` constant expressed in USD per 1,000 searches and
|
|
803
|
-
* accumulated under `<model>_webSearches`. Sources are appended to
|
|
804
|
-
* `tracker.webSearchSources`, deduplicated by URL across the whole run.
|
|
805
|
-
*/
|
|
806
|
-
const updateWebSearchUsageTracker = (tracker, modelName, usage, config) => {
|
|
807
|
-
if (!tracker || !modelName)
|
|
808
|
-
return;
|
|
809
|
-
// The three signals describe the same searches from different providers, so
|
|
810
|
-
// the largest one is the count rather than their sum.
|
|
811
|
-
const searches = Math.max(usage.reportedRequests, usage.callIds.size, usage.grounded ? 1 : 0);
|
|
812
|
-
if (!searches && usage.sources.size === 0)
|
|
813
|
-
return;
|
|
814
|
-
if (typeof tracker.cost !== "number")
|
|
815
|
-
tracker.cost = 0;
|
|
816
|
-
if (!tracker.tokens || typeof tracker.tokens !== "object")
|
|
817
|
-
tracker.tokens = {};
|
|
818
|
-
if (searches > 0) {
|
|
819
|
-
const addedCost = (searches / 1_000) * getModelRate(modelName, config, "web-search");
|
|
820
|
-
if (Number.isFinite(addedCost) && addedCost > 0)
|
|
821
|
-
tracker.cost += addedCost;
|
|
822
|
-
const key = `${modelName}_webSearches`;
|
|
823
|
-
tracker.tokens[key] = (tracker.tokens[key] || 0) + searches;
|
|
824
|
-
}
|
|
825
|
-
if (usage.sources.size > 0) {
|
|
826
|
-
const sources = tracker.webSearchSources || (tracker.webSearchSources = []);
|
|
827
|
-
for (const source of usage.sources.values()) {
|
|
828
|
-
if (!sources.some((existing) => existing.url === source.url)) {
|
|
829
|
-
sources.push(source);
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
};
|
|
834
631
|
/**
|
|
835
632
|
* Instantiates and returns the appropriate LangChain chat model based on the model
|
|
836
633
|
* name prefix. `claude-` maps to `ChatAnthropic`, `gemini-` maps to
|
|
@@ -857,13 +654,6 @@ const getLLMModel = (modelName, config, schema = null) => {
|
|
|
857
654
|
// Resolve `temperature` with presence/support/range handling (see
|
|
858
655
|
// resolveTemperatureSetting). Applied uniformly to every provider below.
|
|
859
656
|
const modelSettings = resolveTemperatureSetting(modelName, config);
|
|
860
|
-
// Native web search (see buildWebSearchTool). The tool is bound to the model
|
|
861
|
-
// so both `.invoke()` and `.stream()` pick it up; the provider runs the search
|
|
862
|
-
// server-side within the same request, so no client-side agent loop is needed.
|
|
863
|
-
const webSearchOptions = getWebSearchOptions(config);
|
|
864
|
-
const webSearchTool = webSearchOptions
|
|
865
|
-
? buildWebSearchTool(modelName, webSearchOptions)
|
|
866
|
-
: null;
|
|
867
657
|
// Claude models (Anthropic)
|
|
868
658
|
if (modelName.startsWith("claude-")) {
|
|
869
659
|
// Anthropic's SDK rejects non-streamed requests when max_tokens is large
|
|
@@ -897,7 +687,7 @@ const getLLMModel = (modelName, config, schema = null) => {
|
|
|
897
687
|
...outputConfig,
|
|
898
688
|
...modelSettings,
|
|
899
689
|
});
|
|
900
|
-
return
|
|
690
|
+
return model;
|
|
901
691
|
}
|
|
902
692
|
// Gemini models (Google)
|
|
903
693
|
else if (modelName.startsWith("gemini-")) {
|
|
@@ -910,11 +700,6 @@ const getLLMModel = (modelName, config, schema = null) => {
|
|
|
910
700
|
...(schema ? { json: true } : {}),
|
|
911
701
|
...modelSettings,
|
|
912
702
|
});
|
|
913
|
-
// Combining grounding with structured output requires Gemini 3 or later;
|
|
914
|
-
// Gemini 1.5/2.x reject `responseSchema` alongside `googleSearch` with a 400.
|
|
915
|
-
const bound = webSearchTool
|
|
916
|
-
? model.bindTools([webSearchTool])
|
|
917
|
-
: model;
|
|
918
703
|
// `responseSchema` additionally constrains the output shape. It is a
|
|
919
704
|
// call-time option (not a constructor field), so it is bound onto the model
|
|
920
705
|
// via `withConfig`. Gemini's schema is an OpenAPI 3.0 subset, so
|
|
@@ -922,9 +707,9 @@ const getLLMModel = (modelName, config, schema = null) => {
|
|
|
922
707
|
// parse/validate pipeline is unchanged.
|
|
923
708
|
if (schema) {
|
|
924
709
|
const jsonSchema = sanitizeGeminiSchema(buildJsonSchema(schema));
|
|
925
|
-
return
|
|
710
|
+
return model.withConfig({ responseSchema: jsonSchema });
|
|
926
711
|
}
|
|
927
|
-
return
|
|
712
|
+
return model;
|
|
928
713
|
}
|
|
929
714
|
// GPT models (OpenAI)
|
|
930
715
|
else if (modelName.startsWith("gpt-")) {
|
|
@@ -932,41 +717,24 @@ const getLLMModel = (modelName, config, schema = null) => {
|
|
|
932
717
|
apiKey: config.openAIAPIKey,
|
|
933
718
|
max_tokens: config.maxTokens || 200000,
|
|
934
719
|
modelName: modelName,
|
|
935
|
-
// `web_search` is a hosted Responses API tool, so the request has to go to
|
|
936
|
-
// `/v1/responses` rather than `/v1/chat/completions`.
|
|
937
|
-
...(webSearchTool ? { useResponsesApi: true } : {}),
|
|
938
720
|
...modelSettings,
|
|
939
721
|
};
|
|
940
|
-
// Use native structured output with a JSON schema.
|
|
941
|
-
//
|
|
942
|
-
// the Responses API takes `text.format` with the schema flattened one level
|
|
943
|
-
// — and `modelKwargs` is spread verbatim into whichever request is built.
|
|
722
|
+
// Use native structured output with a JSON schema. `modelKwargs` is spread
|
|
723
|
+
// verbatim into the Chat Completions request.
|
|
944
724
|
if (schema) {
|
|
945
725
|
const jsonSchema = strictifyJsonSchema(buildJsonSchema(schema));
|
|
946
|
-
openAISettings.modelKwargs =
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
schema: jsonSchema,
|
|
954
|
-
},
|
|
726
|
+
openAISettings.modelKwargs = {
|
|
727
|
+
response_format: {
|
|
728
|
+
type: "json_schema",
|
|
729
|
+
json_schema: {
|
|
730
|
+
name: "response_schema",
|
|
731
|
+
strict: true,
|
|
732
|
+
schema: jsonSchema,
|
|
955
733
|
},
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
response_format: {
|
|
959
|
-
type: "json_schema",
|
|
960
|
-
json_schema: {
|
|
961
|
-
name: "response_schema",
|
|
962
|
-
strict: true,
|
|
963
|
-
schema: jsonSchema,
|
|
964
|
-
},
|
|
965
|
-
},
|
|
966
|
-
};
|
|
734
|
+
},
|
|
735
|
+
};
|
|
967
736
|
}
|
|
968
|
-
|
|
969
|
-
return webSearchTool ? model.bindTools([webSearchTool]) : model;
|
|
737
|
+
return new ChatOpenAI(openAISettings);
|
|
970
738
|
}
|
|
971
739
|
// OpenAI-compatible providers: DeepSeek, Kimi (Moonshot), GLM (Zhipu)
|
|
972
740
|
const openAICompatible = getOpenAICompatibleProvider(modelName, config);
|
|
@@ -991,22 +759,28 @@ const getLLMModel = (modelName, config, schema = null) => {
|
|
|
991
759
|
* an empty array
|
|
992
760
|
* @param responseFormat - Optional structured response format descriptor passed to the
|
|
993
761
|
* LangChain agent constructor
|
|
994
|
-
* @param config - Configuration object forwarded to `getLLMModel` (API keys, temperature,
|
|
762
|
+
* @param config - Configuration object forwarded to `getLLMModel` (API keys, temperature,
|
|
763
|
+
* etc.), plus optional `tavily` (`true` or a `TavilyConfig`) to append the web tools
|
|
764
|
+
* @param usageTracker - Optional accumulator the Tavily tools bill their credits and
|
|
765
|
+
* record their sources into; the LLM's own token usage is tracked by `runAgent`
|
|
995
766
|
* @returns A configured LangChain agent instance ready to be run with `runAgent`
|
|
996
767
|
*/
|
|
997
|
-
const createAgent = (name, modelName, systemPrompt, tools = [], responseFormat, config) => {
|
|
998
|
-
//
|
|
999
|
-
// `getLLMModel`:
|
|
1000
|
-
//
|
|
1001
|
-
|
|
1002
|
-
const
|
|
1003
|
-
|
|
1004
|
-
|
|
768
|
+
const createAgent = (name, modelName, systemPrompt, tools = [], responseFormat, config, usageTracker = null) => {
|
|
769
|
+
// The Tavily tools join the agent's tool list rather than being bound inside
|
|
770
|
+
// `getLLMModel`: they run client-side, so only the agent loop can execute
|
|
771
|
+
// them, and the agent binds its own tools to the model anyway — anything
|
|
772
|
+
// bound there would be dropped.
|
|
773
|
+
const tavilyOptions = getTavilyOptions(config);
|
|
774
|
+
let tavilyTools = [];
|
|
775
|
+
if (tavilyOptions) {
|
|
776
|
+
assertTavilyUsable(config, tavilyOptions);
|
|
777
|
+
tavilyTools = buildTavilyTools(config, tavilyOptions, usageTracker);
|
|
778
|
+
}
|
|
1005
779
|
const agent = createLangChainAgent({
|
|
1006
780
|
name: name,
|
|
1007
|
-
model: getLLMModel(modelName,
|
|
781
|
+
model: getLLMModel(modelName, config),
|
|
1008
782
|
systemPrompt: systemPrompt.trim(),
|
|
1009
|
-
tools:
|
|
783
|
+
tools: tavilyTools.length > 0 ? [...tools, ...tavilyTools] : tools,
|
|
1010
784
|
...(responseFormat ? { responseFormat: responseFormat } : {}),
|
|
1011
785
|
});
|
|
1012
786
|
return agent;
|
|
@@ -1093,13 +867,6 @@ const runAgent = async (agent, prompt, config, onProgress = null, usageTracker =
|
|
|
1093
867
|
}
|
|
1094
868
|
}
|
|
1095
869
|
updateUsageTracker(usageTracker, modelName, sumAgentResponseUsage(response), config);
|
|
1096
|
-
// Web-search activity is spread across the agent's messages — one search may
|
|
1097
|
-
// be reported by the message that ran it and cited by a later one.
|
|
1098
|
-
const webSearchUsage = createWebSearchUsage();
|
|
1099
|
-
for (const message of response?.messages || []) {
|
|
1100
|
-
collectWebSearchUsage(message, webSearchUsage);
|
|
1101
|
-
}
|
|
1102
|
-
updateWebSearchUsageTracker(usageTracker, modelName, webSearchUsage, config);
|
|
1103
870
|
const endTime = Date.now();
|
|
1104
871
|
const duration = endTime - startTime;
|
|
1105
872
|
logger.log(null, logger.levels.info, `Agent "${agent.options?.name}" completed in ${Math.round(duration / 1000)} seconds.`);
|
|
@@ -1337,15 +1104,15 @@ const buildValidationRetryMessages = (priorMessages, rawContent, validationError
|
|
|
1337
1104
|
* When `expectsJsonResponse` is `true`, JSON escape instructions are prepended to the
|
|
1338
1105
|
* system prompt and the parsed result is optionally validated against `schema`.
|
|
1339
1106
|
*
|
|
1340
|
-
* Setting `config.
|
|
1341
|
-
*
|
|
1342
|
-
*
|
|
1343
|
-
*
|
|
1107
|
+
* Setting `config.tavily` adds the Tavily web tools (`web_search`, `web_extract`, and
|
|
1108
|
+
* optionally `web_map` / `web_crawl`). They execute in this process, so they require
|
|
1109
|
+
* `config.agentic` to be `true`; the credits spent and the URLs returned are recorded
|
|
1110
|
+
* on `usageTracker`.
|
|
1344
1111
|
* @param modelName - The model identifier, e.g. `"gpt-4o"`, `"claude-3-5-sonnet"`,
|
|
1345
1112
|
* `"gemini-1.5-pro"`
|
|
1346
1113
|
* @param config - Configuration object with API keys, `temperature`, optional `agentic`
|
|
1347
|
-
* flag, optional `recursionLimit`, and optional `
|
|
1348
|
-
* `
|
|
1114
|
+
* flag, optional `recursionLimit`, and optional `tavily` (`true` or a
|
|
1115
|
+
* `TavilyConfig`)
|
|
1349
1116
|
* @param prompt - The prompt to send; either a plain string (user message only) or an
|
|
1350
1117
|
* array of `{ role, content }` message objects
|
|
1351
1118
|
* @param onProgressReport - Optional async callback invoked with `{ message, progress }`
|
|
@@ -1396,9 +1163,11 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1396
1163
|
? `${JSON_ESCAPE_INSTRUCTION}\n\n---\n\n${systemPrompt}`
|
|
1397
1164
|
: JSON_ESCAPE_INSTRUCTION;
|
|
1398
1165
|
}
|
|
1399
|
-
// Create the agent with tools
|
|
1166
|
+
// Create the agent with tools. The tracker is handed over at construction
|
|
1167
|
+
// because the Tavily tools bill their own credits as they run, rather than
|
|
1168
|
+
// being reconciled from the response afterwards.
|
|
1400
1169
|
const agent = createAgent(agentName, modelName, systemPrompt, [...tools], null, // responseFormat
|
|
1401
|
-
config);
|
|
1170
|
+
config, usageTracker);
|
|
1402
1171
|
// Run the agent with progress callback
|
|
1403
1172
|
const response = await runAgent(agent, userPrompt, config, onProgressReport || null, usageTracker);
|
|
1404
1173
|
// Extract content from agent response
|
|
@@ -1531,7 +1300,6 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1531
1300
|
output_tokens: 0,
|
|
1532
1301
|
total_tokens: 0,
|
|
1533
1302
|
};
|
|
1534
|
-
let webSearchUsage = createWebSearchUsage();
|
|
1535
1303
|
// Inner loop: wait + retry on 429 around stream setup and consumption.
|
|
1536
1304
|
// Usage is only recorded on a successful stream — partial streams that
|
|
1537
1305
|
// error out with a rate limit are not counted. A 429 fired mid-stream
|
|
@@ -1541,7 +1309,6 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1541
1309
|
rawContent = "";
|
|
1542
1310
|
chunkCount = 0;
|
|
1543
1311
|
streamUsage = { input_tokens: 0, output_tokens: 0, total_tokens: 0 };
|
|
1544
|
-
webSearchUsage = createWebSearchUsage();
|
|
1545
1312
|
try {
|
|
1546
1313
|
// Honour caller cancellation: passing the signal tears down the
|
|
1547
1314
|
// upstream HTTP request so a cancelled call stops billing tokens.
|
|
@@ -1554,9 +1321,8 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1554
1321
|
throw errorHandlingHelper.create(constants.HTTP_CODES.BAD_REQUEST, "Request cancelled by caller");
|
|
1555
1322
|
}
|
|
1556
1323
|
accumulateChunkUsage(streamUsage, chunk);
|
|
1557
|
-
collectWebSearchUsage(chunk, webSearchUsage);
|
|
1558
1324
|
// Counting every chunk (not just the ones carrying text) keeps
|
|
1559
|
-
// progress ticking through
|
|
1325
|
+
// progress ticking through pauses in the stream.
|
|
1560
1326
|
chunkCount++;
|
|
1561
1327
|
rawContent += extractTextContent(chunk?.content ?? chunk);
|
|
1562
1328
|
if (chunkCount % progressReportInterval === 0) {
|
|
@@ -1586,7 +1352,6 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1586
1352
|
}
|
|
1587
1353
|
}
|
|
1588
1354
|
updateUsageTracker(usageTracker, modelName, streamUsage, config);
|
|
1589
|
-
updateWebSearchUsageTracker(usageTracker, modelName, webSearchUsage, config);
|
|
1590
1355
|
if (!rawContent) {
|
|
1591
1356
|
throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, "LLM returned no content");
|
|
1592
1357
|
}
|
|
@@ -1691,10 +1456,8 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1691
1456
|
}
|
|
1692
1457
|
}
|
|
1693
1458
|
updateUsageTracker(usageTracker, modelName, extractUsageFromInvoke(response), config);
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
updateWebSearchUsageTracker(usageTracker, modelName, webSearchUsage, config);
|
|
1697
|
-
// Flattened because a server-tool turn returns content blocks, not a string.
|
|
1459
|
+
// Flattened because providers return content blocks, not a string, once
|
|
1460
|
+
// the turn carries anything besides prose.
|
|
1698
1461
|
const rawContent = extractTextContent(response?.content ?? response);
|
|
1699
1462
|
// If not expecting JSON, return raw content directly
|
|
1700
1463
|
if (!expectsJsonResponse) {
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { UsageTracker } from "./langChain.js";
|
|
2
|
+
export type TavilyToolName = "search" | "extract" | "crawl" | "map";
|
|
3
|
+
/**
|
|
4
|
+
* Caller-facing options for the Tavily tools. Enable them by setting
|
|
5
|
+
* `config.tavily` to `true` (defaults for every field) or to one of these
|
|
6
|
+
* objects, and supply `config.tavilyAPIKey`.
|
|
7
|
+
*
|
|
8
|
+
* Tavily runs client-side: the model emits a tool call, this process performs
|
|
9
|
+
* the HTTP request, and the result is appended to the conversation. That only
|
|
10
|
+
* happens inside an agent loop, so `config.agentic` must be `true` — see
|
|
11
|
+
* `assertTavilyUsable`.
|
|
12
|
+
*/
|
|
13
|
+
export type TavilyConfig = {
|
|
14
|
+
/**
|
|
15
|
+
* Which tools to expose. Every enabled tool spends its description tokens on
|
|
16
|
+
* every request, so this defaults to `["search", "extract"]`. Add `"map"` to
|
|
17
|
+
* let the model enumerate a site's URLs and `"crawl"` to let it read a whole
|
|
18
|
+
* section — `crawl` returns full content for every page it visits, which is
|
|
19
|
+
* the easiest way to exhaust a context window here.
|
|
20
|
+
*/
|
|
21
|
+
tools?: TavilyToolName[];
|
|
22
|
+
/** Passed through to `/search`. Tavily defaults to `"basic"`. */
|
|
23
|
+
searchDepth?: "basic" | "advanced" | "fast" | "ultra-fast";
|
|
24
|
+
/** `"advanced"` also pulls tables and embedded content, at double the credits. */
|
|
25
|
+
extractDepth?: "basic" | "advanced";
|
|
26
|
+
/** Results per search. Defaults to `5`. */
|
|
27
|
+
maxResults?: number;
|
|
28
|
+
/**
|
|
29
|
+
* Relevant chunks returned per URL when the model supplies a `query` to
|
|
30
|
+
* `web_extract`. Defaults to `3`; ignored for whole-page extraction.
|
|
31
|
+
*/
|
|
32
|
+
chunksPerSource?: number;
|
|
33
|
+
/** Per-URL character cap before truncation. Defaults to `60000`. */
|
|
34
|
+
maxCharsPerUrl?: number;
|
|
35
|
+
/** Character cap across a single tool call. Defaults to `150000`. */
|
|
36
|
+
maxCharsTotal?: number;
|
|
37
|
+
/** Restricts `/search` to these domains. */
|
|
38
|
+
includeDomains?: string[];
|
|
39
|
+
/** Excludes these domains from `/search`. */
|
|
40
|
+
excludeDomains?: string[];
|
|
41
|
+
/** Maximum pages a single `web_crawl` or `web_map` call may return. */
|
|
42
|
+
crawlLimit?: number;
|
|
43
|
+
/**
|
|
44
|
+
* Returns markdown (the default) or flattened text. Markdown preserves table
|
|
45
|
+
* and heading structure, which costs fewer tokens than the prose equivalent
|
|
46
|
+
* for the tabular documents this is usually pointed at.
|
|
47
|
+
*/
|
|
48
|
+
format?: "markdown" | "text";
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Normalizes `config.tavily` into an options object, returning `null` when the
|
|
52
|
+
* Tavily tools are off so callers can use it as the single enablement gate.
|
|
53
|
+
* @param config - The integration config object
|
|
54
|
+
* @returns Normalized options, or `null` when Tavily is not enabled
|
|
55
|
+
*/
|
|
56
|
+
export declare const getTavilyOptions: (config: any) => TavilyConfig | null;
|
|
57
|
+
/**
|
|
58
|
+
* Fails fast on the two ways a Tavily config cannot work. Tavily tools are
|
|
59
|
+
* executed by the agent loop in this process, so outside agentic mode the model
|
|
60
|
+
* would be handed tools whose calls nothing answers: the turn ends silently and
|
|
61
|
+
* looks like the model ignored them. Called once at config time rather than
|
|
62
|
+
* discovered per request.
|
|
63
|
+
* @param config - The integration config object
|
|
64
|
+
* @param options - Normalized options from `getTavilyOptions`
|
|
65
|
+
*/
|
|
66
|
+
export declare const assertTavilyUsable: (config: any, options: TavilyConfig) => void;
|
|
67
|
+
/**
|
|
68
|
+
* Builds the LangChain tools for the Tavily endpoints named in
|
|
69
|
+
* `options.tools`, closed over the run's config and usage tracker. Tools are
|
|
70
|
+
* created per run rather than shared, because each one bills into that run's
|
|
71
|
+
* tracker.
|
|
72
|
+
*
|
|
73
|
+
* `web_extract` covers both of the reading modes Tavily supports through one
|
|
74
|
+
* tool: with a `query` the API returns only the matching chunks of each page,
|
|
75
|
+
* and without one it returns the whole document. Splitting that into two tools
|
|
76
|
+
* would duplicate the description tokens on every request to express a
|
|
77
|
+
* distinction the API already makes with one optional field.
|
|
78
|
+
*
|
|
79
|
+
* @param config - Config object carrying `tavilyAPIKey` and cost constants
|
|
80
|
+
* @param options - Normalized options from `getTavilyOptions`
|
|
81
|
+
* @param usageTracker - Optional accumulator billed for every Tavily call
|
|
82
|
+
* @returns LangChain tool instances to append to the agent's tool list
|
|
83
|
+
*/
|
|
84
|
+
export declare const buildTavilyTools: (config: any, options: TavilyConfig, usageTracker?: UsageTracker | null) => any[];
|
|
85
|
+
declare const _default: {
|
|
86
|
+
assertTavilyUsable: (config: any, options: TavilyConfig) => void;
|
|
87
|
+
buildTavilyTools: (config: any, options: TavilyConfig, usageTracker?: UsageTracker | null) => any[];
|
|
88
|
+
getTavilyOptions: (config: any) => TavilyConfig | null;
|
|
89
|
+
};
|
|
90
|
+
export default _default;
|
|
91
|
+
//# sourceMappingURL=tavily.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tavily.d.ts","sourceRoot":"","sources":["../../src/tavily.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AA6BnD,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;CAC9B,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,IAsBvE,CAAC;AAqLF;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,gBAAgB,GAC3B,QAAQ,GAAG,EACX,SAAS,YAAY,EACrB,eAAc,YAAY,GAAG,IAAW,KACvC,GAAG,EA2PL,CAAC;;iCA3dyC,GAAG,WAAW,YAAY,KAAG,IAAI;+BA6NlE,GAAG,WACF,YAAY,iBACP,YAAY,GAAG,IAAI,KAChC,GAAG,EAAE;+BA/OiC,GAAG,KAAG,YAAY,GAAG,IAAI;;AA4elE,wBAIE"}
|