Skip to content

Commit 32b8ad0

Browse files
lakshyaag-tavilygumadeiras
authored andcommitted
feat(tavily): add Tavily plugin with search and extract capabilities
1 parent 914fc26 commit 32b8ad0

15 files changed

Lines changed: 705 additions & 4 deletions

extensions/tavily/index.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { describe, expect, it } from "vitest";
2+
import plugin from "./index.js";
3+
4+
describe("tavily plugin", () => {
5+
it("exports a valid plugin entry with correct id and name", () => {
6+
expect(plugin.id).toBe("tavily");
7+
expect(plugin.name).toBe("Tavily Plugin");
8+
expect(typeof plugin.register).toBe("function");
9+
});
10+
11+
it("registers web search provider and two tools", () => {
12+
const registrations: {
13+
webSearchProviders: unknown[];
14+
tools: unknown[];
15+
} = { webSearchProviders: [], tools: [] };
16+
17+
const mockApi = {
18+
registerWebSearchProvider(provider: unknown) {
19+
registrations.webSearchProviders.push(provider);
20+
},
21+
registerTool(tool: unknown) {
22+
registrations.tools.push(tool);
23+
},
24+
config: {},
25+
};
26+
27+
plugin.register(mockApi as never);
28+
29+
expect(registrations.webSearchProviders).toHaveLength(1);
30+
expect(registrations.tools).toHaveLength(2);
31+
32+
const provider = registrations.webSearchProviders[0] as Record<string, unknown>;
33+
expect(provider.id).toBe("tavily");
34+
expect(provider.autoDetectOrder).toBe(55);
35+
expect(provider.envVars).toEqual(["TAVILY_API_KEY"]);
36+
37+
const toolNames = registrations.tools.map((t) => (t as Record<string, unknown>).name);
38+
expect(toolNames).toContain("tavily_search");
39+
expect(toolNames).toContain("tavily_extract");
40+
});
41+
});

extensions/tavily/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { definePluginEntry, type AnyAgentTool } from "openclaw/plugin-sdk/core";
2+
import { createTavilyExtractTool } from "./src/tavily-extract-tool.js";
3+
import { createTavilyWebSearchProvider } from "./src/tavily-search-provider.js";
4+
import { createTavilySearchTool } from "./src/tavily-search-tool.js";
5+
6+
export default definePluginEntry({
7+
id: "tavily",
8+
name: "Tavily Plugin",
9+
description: "Bundled Tavily search and extract plugin",
10+
register(api) {
11+
api.registerWebSearchProvider(createTavilyWebSearchProvider());
12+
api.registerTool(createTavilySearchTool(api) as AnyAgentTool);
13+
api.registerTool(createTavilyExtractTool(api) as AnyAgentTool);
14+
},
15+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"id": "tavily",
3+
"configSchema": {
4+
"type": "object",
5+
"additionalProperties": false,
6+
"properties": {
7+
"apiKey": { "type": "string" },
8+
"baseUrl": { "type": "string" }
9+
}
10+
}
11+
}

extensions/tavily/package.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "@openclaw/tavily-plugin",
3+
"version": "2026.3.17",
4+
"private": true,
5+
"description": "OpenClaw Tavily plugin",
6+
"type": "module",
7+
"openclaw": {
8+
"extensions": [
9+
"./index.ts"
10+
]
11+
}
12+
}

extensions/tavily/src/config.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
2+
import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/config-runtime";
3+
import { normalizeSecretInput } from "openclaw/plugin-sdk/provider-auth";
4+
5+
export const DEFAULT_TAVILY_BASE_URL = "https://api.tavily.com";
6+
export const DEFAULT_TAVILY_SEARCH_TIMEOUT_SECONDS = 30;
7+
export const DEFAULT_TAVILY_EXTRACT_TIMEOUT_SECONDS = 60;
8+
9+
type WebSearchConfig = NonNullable<OpenClawConfig["tools"]>["web"] extends infer Web
10+
? Web extends { search?: infer Search }
11+
? Search
12+
: undefined
13+
: undefined;
14+
15+
type TavilySearchConfig =
16+
| {
17+
apiKey?: unknown;
18+
baseUrl?: string;
19+
}
20+
| undefined;
21+
22+
function resolveSearchConfig(cfg?: OpenClawConfig): WebSearchConfig {
23+
const search = cfg?.tools?.web?.search;
24+
if (!search || typeof search !== "object") {
25+
return undefined;
26+
}
27+
return search as WebSearchConfig;
28+
}
29+
30+
export function resolveTavilySearchConfig(cfg?: OpenClawConfig): TavilySearchConfig {
31+
const search = resolveSearchConfig(cfg);
32+
if (!search || typeof search !== "object") {
33+
return undefined;
34+
}
35+
const tavily = "tavily" in search ? search.tavily : undefined;
36+
if (!tavily || typeof tavily !== "object") {
37+
return undefined;
38+
}
39+
return tavily as TavilySearchConfig;
40+
}
41+
42+
function normalizeConfiguredSecret(value: unknown, path: string): string | undefined {
43+
return normalizeSecretInput(
44+
normalizeResolvedSecretInputString({
45+
value,
46+
path,
47+
}),
48+
);
49+
}
50+
51+
export function resolveTavilyApiKey(cfg?: OpenClawConfig): string | undefined {
52+
const search = resolveTavilySearchConfig(cfg);
53+
return (
54+
normalizeConfiguredSecret(search?.apiKey, "tools.web.search.tavily.apiKey") ||
55+
normalizeSecretInput(process.env.TAVILY_API_KEY) ||
56+
undefined
57+
);
58+
}
59+
60+
export function resolveTavilyBaseUrl(cfg?: OpenClawConfig): string {
61+
const search = resolveTavilySearchConfig(cfg);
62+
const configured =
63+
(typeof search?.baseUrl === "string" ? search.baseUrl.trim() : "") ||
64+
normalizeSecretInput(process.env.TAVILY_BASE_URL) ||
65+
"";
66+
return configured || DEFAULT_TAVILY_BASE_URL;
67+
}
68+
69+
export function resolveTavilySearchTimeoutSeconds(override?: number): number {
70+
if (typeof override === "number" && Number.isFinite(override) && override > 0) {
71+
return Math.floor(override);
72+
}
73+
return DEFAULT_TAVILY_SEARCH_TIMEOUT_SECONDS;
74+
}
75+
76+
export function resolveTavilyExtractTimeoutSeconds(override?: number): number {
77+
if (typeof override === "number" && Number.isFinite(override) && override > 0) {
78+
return Math.floor(override);
79+
}
80+
return DEFAULT_TAVILY_EXTRACT_TIMEOUT_SECONDS;
81+
}

0 commit comments

Comments
 (0)