@stackfactor/agent-utils 1.2.19 → 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 +2 -2
package/dist/cjs/langChain.js
CHANGED
|
@@ -15,6 +15,7 @@ const logger_js_1 = __importDefault(require("./logger.js"));
|
|
|
15
15
|
const zod_1 = require("zod");
|
|
16
16
|
const transform_json_schema_1 = require("@anthropic-ai/sdk/lib/transform-json-schema");
|
|
17
17
|
const runtimeContext_js_1 = require("./runtimeContext.js");
|
|
18
|
+
const tavily_js_1 = require("./tavily.js");
|
|
18
19
|
const JSON_ESCAPE_INSTRUCTION = `
|
|
19
20
|
CRITICAL - Your response must be valid JSON. Escape ALL special characters in string values:
|
|
20
21
|
- Newlines → \\n
|
|
@@ -608,114 +609,12 @@ const resolveTemperatureSetting = (modelName, config) => {
|
|
|
608
609
|
}
|
|
609
610
|
return { temperature: clamped };
|
|
610
611
|
};
|
|
611
|
-
/**
|
|
612
|
-
* Whether a Claude model can run web search through dynamic filtering, where
|
|
613
|
-
* Claude writes and runs code that filters the search results before they reach
|
|
614
|
-
* the context window instead of loading every result into it. Requires Claude
|
|
615
|
-
* 4.6 or later (the models with programmatic tool calling); on anything earlier
|
|
616
|
-
* the filtering tool versions return a 400 unless search is pinned to
|
|
617
|
-
* `allowed_callers: ["direct"]`.
|
|
618
|
-
* @param modelName - The Claude model identifier being routed
|
|
619
|
-
* @returns `true` when the model supports dynamic filtering
|
|
620
|
-
*/
|
|
621
|
-
const supportsAnthropicDynamicFiltering = (modelName) => /^claude-(opus|sonnet|haiku)-4-(?:[6-9]|\d\d)\b/.test(modelName) ||
|
|
622
|
-
/^claude-(opus|sonnet|haiku|fable|mythos)-(?:[5-9]|\d\d)\b/.test(modelName);
|
|
623
|
-
/**
|
|
624
|
-
* Normalizes `config.webSearch` into an options object, returning `null` when
|
|
625
|
-
* web search is off so callers can use it as the single enablement gate.
|
|
626
|
-
*/
|
|
627
|
-
const getWebSearchOptions = (config) => {
|
|
628
|
-
const webSearch = config?.webSearch;
|
|
629
|
-
if (!webSearch)
|
|
630
|
-
return null;
|
|
631
|
-
return webSearch === true ? {} : webSearch;
|
|
632
|
-
};
|
|
633
|
-
/**
|
|
634
|
-
* Builds the provider-native web search tool definition for a model:
|
|
635
|
-
* - `claude-` → Anthropic's `web_search` server tool, executed by the Messages
|
|
636
|
-
* API within a single request and answered with citations. Defaults to the
|
|
637
|
-
* dynamic-filtering tool version on models that support it, so search results
|
|
638
|
-
* are filtered by code before they reach the context window.
|
|
639
|
-
* - `gpt-` → OpenAI's hosted `web_search` tool (Responses API). Context spend
|
|
640
|
-
* is governed by `search_context_size` (OpenAI defaults to `medium`).
|
|
641
|
-
* - `gemini-` → Google's `googleSearch` grounding tool. Google exposes no
|
|
642
|
-
* result-filtering or context-size control; leaving `searchTypes` unset keeps
|
|
643
|
-
* grounding on text-only web results rather than image bytes.
|
|
644
|
-
* Returns `null` for providers with no native web search (DeepSeek, Kimi, GLM),
|
|
645
|
-
* warning instead of throwing so one config can be pointed at any model.
|
|
646
|
-
* @param modelName - The model identifier being routed
|
|
647
|
-
* @param options - Normalized options from `getWebSearchOptions`
|
|
648
|
-
* @returns The provider's tool definition, or `null` when unsupported
|
|
649
|
-
*/
|
|
650
|
-
const buildWebSearchTool = (modelName, options) => {
|
|
651
|
-
const { allowedDomains, blockedDomains, userLocation } = options;
|
|
652
|
-
if (modelName.startsWith("claude-")) {
|
|
653
|
-
// The API returns a 400 when both filters are present, so fail locally
|
|
654
|
-
// rather than paying for the round trip.
|
|
655
|
-
if (allowedDomains && blockedDomains) {
|
|
656
|
-
throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.BAD_REQUEST, "Anthropic web search accepts allowedDomains or blockedDomains, not both.");
|
|
657
|
-
}
|
|
658
|
-
// Prefer the newest tool version the model can actually run. Basic search
|
|
659
|
-
// loads every result into the context window; from `web_search_20260209`
|
|
660
|
-
// Claude filters them with code first, and `web_search_20260318` can also
|
|
661
|
-
// keep the consumed results out of the response.
|
|
662
|
-
const canFilter = supportsAnthropicDynamicFiltering(modelName);
|
|
663
|
-
const type = options.toolVersion ||
|
|
664
|
-
(canFilter ? "web_search_20260318" : "web_search_20250305");
|
|
665
|
-
const version = Number(type.slice(-8));
|
|
666
|
-
// Filtering versions default to running search from inside code execution.
|
|
667
|
-
// Say so explicitly when it is not wanted (or not possible), which is what
|
|
668
|
-
// the API requires from models without programmatic tool calling.
|
|
669
|
-
const directOnly = version >= 20260209 && (options.dynamicFiltering === false || !canFilter);
|
|
670
|
-
return {
|
|
671
|
-
type,
|
|
672
|
-
name: "web_search",
|
|
673
|
-
...(options.maxUses ? { max_uses: options.maxUses } : {}),
|
|
674
|
-
...(allowedDomains ? { allowed_domains: allowedDomains } : {}),
|
|
675
|
-
...(blockedDomains ? { blocked_domains: blockedDomains } : {}),
|
|
676
|
-
...(userLocation
|
|
677
|
-
? { user_location: { type: "approximate", ...userLocation } }
|
|
678
|
-
: {}),
|
|
679
|
-
...(directOnly ? { allowed_callers: ["direct"] } : {}),
|
|
680
|
-
...(version >= 20260318
|
|
681
|
-
? { response_inclusion: options.responseInclusion || "excluded" }
|
|
682
|
-
: {}),
|
|
683
|
-
};
|
|
684
|
-
}
|
|
685
|
-
if (modelName.startsWith("gpt-")) {
|
|
686
|
-
return {
|
|
687
|
-
type: "web_search",
|
|
688
|
-
...(allowedDomains
|
|
689
|
-
? { filters: { allowed_domains: allowedDomains } }
|
|
690
|
-
: {}),
|
|
691
|
-
...(userLocation
|
|
692
|
-
? { user_location: { type: "approximate", ...userLocation } }
|
|
693
|
-
: {}),
|
|
694
|
-
...(options.searchContextSize
|
|
695
|
-
? { search_context_size: options.searchContextSize }
|
|
696
|
-
: {}),
|
|
697
|
-
};
|
|
698
|
-
}
|
|
699
|
-
if (modelName.startsWith("gemini-")) {
|
|
700
|
-
// `timeRangeFilter` is the only filter the Gemini API exposes;
|
|
701
|
-
// `excludeDomains` is a Vertex AI field and is rejected here, so domain
|
|
702
|
-
// filters are deliberately not mapped for Google.
|
|
703
|
-
return {
|
|
704
|
-
googleSearch: options.timeRange
|
|
705
|
-
? { timeRangeFilter: options.timeRange }
|
|
706
|
-
: {},
|
|
707
|
-
};
|
|
708
|
-
}
|
|
709
|
-
logger_js_1.default.log(null, logger_js_1.default.levels.warn, `Model "${modelName}" has no native web search tool; ignoring the configured webSearch options.`);
|
|
710
|
-
return null;
|
|
711
|
-
};
|
|
712
612
|
/**
|
|
713
613
|
* Flattens LangChain message content into plain text. Content is a string for
|
|
714
|
-
* ordinary completions, but
|
|
715
|
-
*
|
|
716
|
-
*
|
|
717
|
-
*
|
|
718
|
-
* would receive a non-string and every web-search call would fail.
|
|
614
|
+
* ordinary completions, but providers switch to an array of blocks whenever the
|
|
615
|
+
* turn carries more than prose — tool-use blocks in agentic runs, or Anthropic's
|
|
616
|
+
* thinking blocks — so without this the JSON parse pipeline would receive a
|
|
617
|
+
* non-string and the call would fail.
|
|
719
618
|
* @param content - A message's `content` field, or a raw string
|
|
720
619
|
* @returns The concatenated text of all text blocks
|
|
721
620
|
*/
|
|
@@ -734,108 +633,6 @@ const extractTextContent = (content) => {
|
|
|
734
633
|
}
|
|
735
634
|
return text;
|
|
736
635
|
};
|
|
737
|
-
const createWebSearchUsage = () => ({
|
|
738
|
-
reportedRequests: 0,
|
|
739
|
-
callIds: new Set(),
|
|
740
|
-
grounded: false,
|
|
741
|
-
sources: new Map(),
|
|
742
|
-
});
|
|
743
|
-
/**
|
|
744
|
-
* Folds one message — or one streaming chunk — into a `WebSearchUsage`. Safe to
|
|
745
|
-
* call on every chunk of a stream and on messages that involved no search.
|
|
746
|
-
*/
|
|
747
|
-
const collectWebSearchUsage = (payload, usage) => {
|
|
748
|
-
if (!payload)
|
|
749
|
-
return;
|
|
750
|
-
const addSource = (url, title) => {
|
|
751
|
-
if (typeof url === "string" && url && !usage.sources.has(url)) {
|
|
752
|
-
usage.sources.set(url, {
|
|
753
|
-
url,
|
|
754
|
-
...(typeof title === "string" ? { title } : {}),
|
|
755
|
-
});
|
|
756
|
-
}
|
|
757
|
-
};
|
|
758
|
-
if (Array.isArray(payload.content)) {
|
|
759
|
-
for (const block of payload.content) {
|
|
760
|
-
if (!block || typeof block !== "object")
|
|
761
|
-
continue;
|
|
762
|
-
// Anthropic: results of a search the API executed server-side.
|
|
763
|
-
if (block.type === "web_search_tool_result" &&
|
|
764
|
-
Array.isArray(block.content)) {
|
|
765
|
-
for (const result of block.content) {
|
|
766
|
-
addSource(result?.url, result?.title);
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
// Anthropic: citations attached to the answer's text blocks.
|
|
770
|
-
if (Array.isArray(block.citations)) {
|
|
771
|
-
for (const citation of block.citations) {
|
|
772
|
-
addSource(citation?.url, citation?.title);
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
// OpenAI Responses API: one block per executed search, plus url citations.
|
|
776
|
-
if (block.type === "web_search_call" && block.id) {
|
|
777
|
-
usage.callIds.add(block.id);
|
|
778
|
-
}
|
|
779
|
-
if (Array.isArray(block.annotations)) {
|
|
780
|
-
for (const annotation of block.annotations) {
|
|
781
|
-
if (annotation?.type === "url_citation") {
|
|
782
|
-
addSource(annotation.url, annotation.title);
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
const metadata = payload.response_metadata;
|
|
789
|
-
if (!metadata)
|
|
790
|
-
return;
|
|
791
|
-
const requests = metadata.usage?.server_tool_use?.web_search_requests;
|
|
792
|
-
if (typeof requests === "number" && requests > usage.reportedRequests) {
|
|
793
|
-
usage.reportedRequests = requests;
|
|
794
|
-
}
|
|
795
|
-
const grounding = metadata.groundingMetadata;
|
|
796
|
-
if (grounding) {
|
|
797
|
-
usage.grounded = true;
|
|
798
|
-
for (const chunk of grounding.groundingChunks || []) {
|
|
799
|
-
addSource(chunk?.web?.uri, chunk?.web?.title);
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
};
|
|
803
|
-
/**
|
|
804
|
-
* Adds a call's web-search usage to the caller-supplied tracker. Searches are
|
|
805
|
-
* billed per request rather than per token (Anthropic charges $10 per 1,000
|
|
806
|
-
* searches; Google charges per grounded request), so the rate is read from the
|
|
807
|
-
* `<model>-web-search-costs` constant expressed in USD per 1,000 searches and
|
|
808
|
-
* accumulated under `<model>_webSearches`. Sources are appended to
|
|
809
|
-
* `tracker.webSearchSources`, deduplicated by URL across the whole run.
|
|
810
|
-
*/
|
|
811
|
-
const updateWebSearchUsageTracker = (tracker, modelName, usage, config) => {
|
|
812
|
-
if (!tracker || !modelName)
|
|
813
|
-
return;
|
|
814
|
-
// The three signals describe the same searches from different providers, so
|
|
815
|
-
// the largest one is the count rather than their sum.
|
|
816
|
-
const searches = Math.max(usage.reportedRequests, usage.callIds.size, usage.grounded ? 1 : 0);
|
|
817
|
-
if (!searches && usage.sources.size === 0)
|
|
818
|
-
return;
|
|
819
|
-
if (typeof tracker.cost !== "number")
|
|
820
|
-
tracker.cost = 0;
|
|
821
|
-
if (!tracker.tokens || typeof tracker.tokens !== "object")
|
|
822
|
-
tracker.tokens = {};
|
|
823
|
-
if (searches > 0) {
|
|
824
|
-
const addedCost = (searches / 1_000) * getModelRate(modelName, config, "web-search");
|
|
825
|
-
if (Number.isFinite(addedCost) && addedCost > 0)
|
|
826
|
-
tracker.cost += addedCost;
|
|
827
|
-
const key = `${modelName}_webSearches`;
|
|
828
|
-
tracker.tokens[key] = (tracker.tokens[key] || 0) + searches;
|
|
829
|
-
}
|
|
830
|
-
if (usage.sources.size > 0) {
|
|
831
|
-
const sources = tracker.webSearchSources || (tracker.webSearchSources = []);
|
|
832
|
-
for (const source of usage.sources.values()) {
|
|
833
|
-
if (!sources.some((existing) => existing.url === source.url)) {
|
|
834
|
-
sources.push(source);
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
};
|
|
839
636
|
/**
|
|
840
637
|
* Instantiates and returns the appropriate LangChain chat model based on the model
|
|
841
638
|
* name prefix. `claude-` maps to `ChatAnthropic`, `gemini-` maps to
|
|
@@ -862,13 +659,6 @@ const getLLMModel = (modelName, config, schema = null) => {
|
|
|
862
659
|
// Resolve `temperature` with presence/support/range handling (see
|
|
863
660
|
// resolveTemperatureSetting). Applied uniformly to every provider below.
|
|
864
661
|
const modelSettings = resolveTemperatureSetting(modelName, config);
|
|
865
|
-
// Native web search (see buildWebSearchTool). The tool is bound to the model
|
|
866
|
-
// so both `.invoke()` and `.stream()` pick it up; the provider runs the search
|
|
867
|
-
// server-side within the same request, so no client-side agent loop is needed.
|
|
868
|
-
const webSearchOptions = getWebSearchOptions(config);
|
|
869
|
-
const webSearchTool = webSearchOptions
|
|
870
|
-
? buildWebSearchTool(modelName, webSearchOptions)
|
|
871
|
-
: null;
|
|
872
662
|
// Claude models (Anthropic)
|
|
873
663
|
if (modelName.startsWith("claude-")) {
|
|
874
664
|
// Anthropic's SDK rejects non-streamed requests when max_tokens is large
|
|
@@ -902,7 +692,7 @@ const getLLMModel = (modelName, config, schema = null) => {
|
|
|
902
692
|
...outputConfig,
|
|
903
693
|
...modelSettings,
|
|
904
694
|
});
|
|
905
|
-
return
|
|
695
|
+
return model;
|
|
906
696
|
}
|
|
907
697
|
// Gemini models (Google)
|
|
908
698
|
else if (modelName.startsWith("gemini-")) {
|
|
@@ -915,11 +705,6 @@ const getLLMModel = (modelName, config, schema = null) => {
|
|
|
915
705
|
...(schema ? { json: true } : {}),
|
|
916
706
|
...modelSettings,
|
|
917
707
|
});
|
|
918
|
-
// Combining grounding with structured output requires Gemini 3 or later;
|
|
919
|
-
// Gemini 1.5/2.x reject `responseSchema` alongside `googleSearch` with a 400.
|
|
920
|
-
const bound = webSearchTool
|
|
921
|
-
? model.bindTools([webSearchTool])
|
|
922
|
-
: model;
|
|
923
708
|
// `responseSchema` additionally constrains the output shape. It is a
|
|
924
709
|
// call-time option (not a constructor field), so it is bound onto the model
|
|
925
710
|
// via `withConfig`. Gemini's schema is an OpenAPI 3.0 subset, so
|
|
@@ -927,9 +712,9 @@ const getLLMModel = (modelName, config, schema = null) => {
|
|
|
927
712
|
// parse/validate pipeline is unchanged.
|
|
928
713
|
if (schema) {
|
|
929
714
|
const jsonSchema = sanitizeGeminiSchema(buildJsonSchema(schema));
|
|
930
|
-
return
|
|
715
|
+
return model.withConfig({ responseSchema: jsonSchema });
|
|
931
716
|
}
|
|
932
|
-
return
|
|
717
|
+
return model;
|
|
933
718
|
}
|
|
934
719
|
// GPT models (OpenAI)
|
|
935
720
|
else if (modelName.startsWith("gpt-")) {
|
|
@@ -937,41 +722,24 @@ const getLLMModel = (modelName, config, schema = null) => {
|
|
|
937
722
|
apiKey: config.openAIAPIKey,
|
|
938
723
|
max_tokens: config.maxTokens || 200000,
|
|
939
724
|
modelName: modelName,
|
|
940
|
-
// `web_search` is a hosted Responses API tool, so the request has to go to
|
|
941
|
-
// `/v1/responses` rather than `/v1/chat/completions`.
|
|
942
|
-
...(webSearchTool ? { useResponsesApi: true } : {}),
|
|
943
725
|
...modelSettings,
|
|
944
726
|
};
|
|
945
|
-
// Use native structured output with a JSON schema.
|
|
946
|
-
//
|
|
947
|
-
// the Responses API takes `text.format` with the schema flattened one level
|
|
948
|
-
// — and `modelKwargs` is spread verbatim into whichever request is built.
|
|
727
|
+
// Use native structured output with a JSON schema. `modelKwargs` is spread
|
|
728
|
+
// verbatim into the Chat Completions request.
|
|
949
729
|
if (schema) {
|
|
950
730
|
const jsonSchema = strictifyJsonSchema(buildJsonSchema(schema));
|
|
951
|
-
openAISettings.modelKwargs =
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
schema: jsonSchema,
|
|
959
|
-
},
|
|
731
|
+
openAISettings.modelKwargs = {
|
|
732
|
+
response_format: {
|
|
733
|
+
type: "json_schema",
|
|
734
|
+
json_schema: {
|
|
735
|
+
name: "response_schema",
|
|
736
|
+
strict: true,
|
|
737
|
+
schema: jsonSchema,
|
|
960
738
|
},
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
response_format: {
|
|
964
|
-
type: "json_schema",
|
|
965
|
-
json_schema: {
|
|
966
|
-
name: "response_schema",
|
|
967
|
-
strict: true,
|
|
968
|
-
schema: jsonSchema,
|
|
969
|
-
},
|
|
970
|
-
},
|
|
971
|
-
};
|
|
739
|
+
},
|
|
740
|
+
};
|
|
972
741
|
}
|
|
973
|
-
|
|
974
|
-
return webSearchTool ? model.bindTools([webSearchTool]) : model;
|
|
742
|
+
return new openai_1.ChatOpenAI(openAISettings);
|
|
975
743
|
}
|
|
976
744
|
// OpenAI-compatible providers: DeepSeek, Kimi (Moonshot), GLM (Zhipu)
|
|
977
745
|
const openAICompatible = getOpenAICompatibleProvider(modelName, config);
|
|
@@ -996,22 +764,28 @@ const getLLMModel = (modelName, config, schema = null) => {
|
|
|
996
764
|
* an empty array
|
|
997
765
|
* @param responseFormat - Optional structured response format descriptor passed to the
|
|
998
766
|
* LangChain agent constructor
|
|
999
|
-
* @param config - Configuration object forwarded to `getLLMModel` (API keys, temperature,
|
|
767
|
+
* @param config - Configuration object forwarded to `getLLMModel` (API keys, temperature,
|
|
768
|
+
* etc.), plus optional `tavily` (`true` or a `TavilyConfig`) to append the web tools
|
|
769
|
+
* @param usageTracker - Optional accumulator the Tavily tools bill their credits and
|
|
770
|
+
* record their sources into; the LLM's own token usage is tracked by `runAgent`
|
|
1000
771
|
* @returns A configured LangChain agent instance ready to be run with `runAgent`
|
|
1001
772
|
*/
|
|
1002
|
-
const createAgent = (name, modelName, systemPrompt, tools = [], responseFormat, config) => {
|
|
1003
|
-
//
|
|
1004
|
-
// `getLLMModel`:
|
|
1005
|
-
//
|
|
1006
|
-
|
|
1007
|
-
const
|
|
1008
|
-
|
|
1009
|
-
|
|
773
|
+
const createAgent = (name, modelName, systemPrompt, tools = [], responseFormat, config, usageTracker = null) => {
|
|
774
|
+
// The Tavily tools join the agent's tool list rather than being bound inside
|
|
775
|
+
// `getLLMModel`: they run client-side, so only the agent loop can execute
|
|
776
|
+
// them, and the agent binds its own tools to the model anyway — anything
|
|
777
|
+
// bound there would be dropped.
|
|
778
|
+
const tavilyOptions = (0, tavily_js_1.getTavilyOptions)(config);
|
|
779
|
+
let tavilyTools = [];
|
|
780
|
+
if (tavilyOptions) {
|
|
781
|
+
(0, tavily_js_1.assertTavilyUsable)(config, tavilyOptions);
|
|
782
|
+
tavilyTools = (0, tavily_js_1.buildTavilyTools)(config, tavilyOptions, usageTracker);
|
|
783
|
+
}
|
|
1010
784
|
const agent = (0, langchain_1.createAgent)({
|
|
1011
785
|
name: name,
|
|
1012
|
-
model: getLLMModel(modelName,
|
|
786
|
+
model: getLLMModel(modelName, config),
|
|
1013
787
|
systemPrompt: systemPrompt.trim(),
|
|
1014
|
-
tools:
|
|
788
|
+
tools: tavilyTools.length > 0 ? [...tools, ...tavilyTools] : tools,
|
|
1015
789
|
...(responseFormat ? { responseFormat: responseFormat } : {}),
|
|
1016
790
|
});
|
|
1017
791
|
return agent;
|
|
@@ -1098,13 +872,6 @@ const runAgent = async (agent, prompt, config, onProgress = null, usageTracker =
|
|
|
1098
872
|
}
|
|
1099
873
|
}
|
|
1100
874
|
updateUsageTracker(usageTracker, modelName, sumAgentResponseUsage(response), config);
|
|
1101
|
-
// Web-search activity is spread across the agent's messages — one search may
|
|
1102
|
-
// be reported by the message that ran it and cited by a later one.
|
|
1103
|
-
const webSearchUsage = createWebSearchUsage();
|
|
1104
|
-
for (const message of response?.messages || []) {
|
|
1105
|
-
collectWebSearchUsage(message, webSearchUsage);
|
|
1106
|
-
}
|
|
1107
|
-
updateWebSearchUsageTracker(usageTracker, modelName, webSearchUsage, config);
|
|
1108
875
|
const endTime = Date.now();
|
|
1109
876
|
const duration = endTime - startTime;
|
|
1110
877
|
logger_js_1.default.log(null, logger_js_1.default.levels.info, `Agent "${agent.options?.name}" completed in ${Math.round(duration / 1000)} seconds.`);
|
|
@@ -1342,15 +1109,15 @@ const buildValidationRetryMessages = (priorMessages, rawContent, validationError
|
|
|
1342
1109
|
* When `expectsJsonResponse` is `true`, JSON escape instructions are prepended to the
|
|
1343
1110
|
* system prompt and the parsed result is optionally validated against `schema`.
|
|
1344
1111
|
*
|
|
1345
|
-
* Setting `config.
|
|
1346
|
-
*
|
|
1347
|
-
*
|
|
1348
|
-
*
|
|
1112
|
+
* Setting `config.tavily` adds the Tavily web tools (`web_search`, `web_extract`, and
|
|
1113
|
+
* optionally `web_map` / `web_crawl`). They execute in this process, so they require
|
|
1114
|
+
* `config.agentic` to be `true`; the credits spent and the URLs returned are recorded
|
|
1115
|
+
* on `usageTracker`.
|
|
1349
1116
|
* @param modelName - The model identifier, e.g. `"gpt-4o"`, `"claude-3-5-sonnet"`,
|
|
1350
1117
|
* `"gemini-1.5-pro"`
|
|
1351
1118
|
* @param config - Configuration object with API keys, `temperature`, optional `agentic`
|
|
1352
|
-
* flag, optional `recursionLimit`, and optional `
|
|
1353
|
-
* `
|
|
1119
|
+
* flag, optional `recursionLimit`, and optional `tavily` (`true` or a
|
|
1120
|
+
* `TavilyConfig`)
|
|
1354
1121
|
* @param prompt - The prompt to send; either a plain string (user message only) or an
|
|
1355
1122
|
* array of `{ role, content }` message objects
|
|
1356
1123
|
* @param onProgressReport - Optional async callback invoked with `{ message, progress }`
|
|
@@ -1401,9 +1168,11 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1401
1168
|
? `${JSON_ESCAPE_INSTRUCTION}\n\n---\n\n${systemPrompt}`
|
|
1402
1169
|
: JSON_ESCAPE_INSTRUCTION;
|
|
1403
1170
|
}
|
|
1404
|
-
// Create the agent with tools
|
|
1171
|
+
// Create the agent with tools. The tracker is handed over at construction
|
|
1172
|
+
// because the Tavily tools bill their own credits as they run, rather than
|
|
1173
|
+
// being reconciled from the response afterwards.
|
|
1405
1174
|
const agent = createAgent(agentName, modelName, systemPrompt, [...tools], null, // responseFormat
|
|
1406
|
-
config);
|
|
1175
|
+
config, usageTracker);
|
|
1407
1176
|
// Run the agent with progress callback
|
|
1408
1177
|
const response = await runAgent(agent, userPrompt, config, onProgressReport || null, usageTracker);
|
|
1409
1178
|
// Extract content from agent response
|
|
@@ -1536,7 +1305,6 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1536
1305
|
output_tokens: 0,
|
|
1537
1306
|
total_tokens: 0,
|
|
1538
1307
|
};
|
|
1539
|
-
let webSearchUsage = createWebSearchUsage();
|
|
1540
1308
|
// Inner loop: wait + retry on 429 around stream setup and consumption.
|
|
1541
1309
|
// Usage is only recorded on a successful stream — partial streams that
|
|
1542
1310
|
// error out with a rate limit are not counted. A 429 fired mid-stream
|
|
@@ -1546,7 +1314,6 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1546
1314
|
rawContent = "";
|
|
1547
1315
|
chunkCount = 0;
|
|
1548
1316
|
streamUsage = { input_tokens: 0, output_tokens: 0, total_tokens: 0 };
|
|
1549
|
-
webSearchUsage = createWebSearchUsage();
|
|
1550
1317
|
try {
|
|
1551
1318
|
// Honour caller cancellation: passing the signal tears down the
|
|
1552
1319
|
// upstream HTTP request so a cancelled call stops billing tokens.
|
|
@@ -1559,9 +1326,8 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1559
1326
|
throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.BAD_REQUEST, "Request cancelled by caller");
|
|
1560
1327
|
}
|
|
1561
1328
|
accumulateChunkUsage(streamUsage, chunk);
|
|
1562
|
-
collectWebSearchUsage(chunk, webSearchUsage);
|
|
1563
1329
|
// Counting every chunk (not just the ones carrying text) keeps
|
|
1564
|
-
// progress ticking through
|
|
1330
|
+
// progress ticking through pauses in the stream.
|
|
1565
1331
|
chunkCount++;
|
|
1566
1332
|
rawContent += extractTextContent(chunk?.content ?? chunk);
|
|
1567
1333
|
if (chunkCount % progressReportInterval === 0) {
|
|
@@ -1591,7 +1357,6 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1591
1357
|
}
|
|
1592
1358
|
}
|
|
1593
1359
|
updateUsageTracker(usageTracker, modelName, streamUsage, config);
|
|
1594
|
-
updateWebSearchUsageTracker(usageTracker, modelName, webSearchUsage, config);
|
|
1595
1360
|
if (!rawContent) {
|
|
1596
1361
|
throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, "LLM returned no content");
|
|
1597
1362
|
}
|
|
@@ -1696,10 +1461,8 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1696
1461
|
}
|
|
1697
1462
|
}
|
|
1698
1463
|
updateUsageTracker(usageTracker, modelName, extractUsageFromInvoke(response), config);
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
updateWebSearchUsageTracker(usageTracker, modelName, webSearchUsage, config);
|
|
1702
|
-
// Flattened because a server-tool turn returns content blocks, not a string.
|
|
1464
|
+
// Flattened because providers return content blocks, not a string, once
|
|
1465
|
+
// the turn carries anything besides prose.
|
|
1703
1466
|
const rawContent = extractTextContent(response?.content ?? response);
|
|
1704
1467
|
// If not expecting JSON, return raw content directly
|
|
1705
1468
|
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"}
|