mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-16 12:34:42 +00:00
feat(core): add tavily web search (#42591)
This commit is contained in:
parent
51f0b499f9
commit
3d3bbd48f9
6 changed files with 165 additions and 2 deletions
|
|
@ -1,5 +1,11 @@
|
|||
import { WebSearchExa } from "./exa.js"
|
||||
import { WebSearchFirecrawl } from "./firecrawl.js"
|
||||
import { WebSearchParallel } from "./parallel.js"
|
||||
import { WebSearchTavily } from "./tavily.js"
|
||||
|
||||
export const WebSearchPlugins = [WebSearchExa.Plugin, WebSearchFirecrawl.Plugin, WebSearchParallel.Plugin] as const
|
||||
export const WebSearchPlugins = [
|
||||
WebSearchExa.Plugin,
|
||||
WebSearchFirecrawl.Plugin,
|
||||
WebSearchParallel.Plugin,
|
||||
WebSearchTavily.Plugin,
|
||||
] as const
|
||||
|
|
|
|||
85
packages/core/src/plugin/websearch/tavily.ts
Normal file
85
packages/core/src/plugin/websearch/tavily.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
export * as WebSearchTavily from "./tavily.js"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { Duration, Effect, Schema, Scope } from "effect"
|
||||
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
||||
import { App } from "../../app.js"
|
||||
|
||||
export const endpoint = "https://api.tavily.com/search"
|
||||
|
||||
const SearchRequest = Schema.Struct({
|
||||
query: Schema.String,
|
||||
search_depth: Schema.Literal("basic"),
|
||||
chunks_per_source: Schema.Number,
|
||||
max_results: Schema.Number,
|
||||
})
|
||||
|
||||
const SearchResponse = Schema.Struct({
|
||||
results: Schema.Array(
|
||||
Schema.Struct({
|
||||
title: Schema.String,
|
||||
url: Schema.String,
|
||||
content: Schema.String,
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
export const Plugin = define<HttpClient.HttpClient | Scope.Scope>({
|
||||
id: "opencode.websearch.tavily",
|
||||
effect: Effect.fn("WebSearchTavily.Plugin")(function* (ctx) {
|
||||
const http = yield* HttpClient.HttpClient
|
||||
yield* ctx.integration.transform((draft) => {
|
||||
draft.update("tavily", (integration) => (integration.name = "Tavily"))
|
||||
draft.method.update({
|
||||
integrationID: "tavily",
|
||||
method: { type: "key" },
|
||||
})
|
||||
draft.method.update({
|
||||
integrationID: "tavily",
|
||||
method: { type: "env", names: ["TAVILY_API_KEY"] },
|
||||
})
|
||||
})
|
||||
yield* ctx.websearch.transform((draft) => {
|
||||
draft.add({
|
||||
id: "tavily",
|
||||
name: "Tavily",
|
||||
execute: (input) =>
|
||||
Effect.gen(function* () {
|
||||
const connection = yield* ctx.integration.connection.active("tavily")
|
||||
const credential = connection ? yield* ctx.integration.connection.resolve(connection) : undefined
|
||||
const request = yield* HttpClientRequest.post(endpoint).pipe(
|
||||
HttpClientRequest.acceptJson,
|
||||
HttpClientRequest.setHeaders({
|
||||
"User-Agent": App.useragent(ctx.app),
|
||||
"X-Client-Name": "opencode2",
|
||||
...(credential?.type === "key"
|
||||
? { Authorization: `Bearer ${credential.key}` }
|
||||
: { "X-Tavily-Access-Mode": "keyless" }),
|
||||
}),
|
||||
HttpClientRequest.schemaBodyJson(SearchRequest)({
|
||||
query: input.query,
|
||||
search_depth: "basic",
|
||||
chunks_per_source: 3,
|
||||
max_results: 8,
|
||||
}),
|
||||
)
|
||||
const response = yield* Effect.gen(function* () {
|
||||
const httpResponse = yield* HttpClient.filterStatusOk(http).execute(request)
|
||||
return yield* HttpClientResponse.schemaBodyJson(SearchResponse)(httpResponse)
|
||||
}).pipe(
|
||||
Effect.timeoutOrElse({
|
||||
duration: Duration.seconds(25),
|
||||
orElse: () => Effect.fail(new Error("Tavily web search request timed out")),
|
||||
}),
|
||||
)
|
||||
return response.results.map((item) => ({
|
||||
url: item.url,
|
||||
title: item.title,
|
||||
...(item.content ? { content: item.content } : {}),
|
||||
time: {},
|
||||
}))
|
||||
}),
|
||||
})
|
||||
})
|
||||
}),
|
||||
})
|
||||
|
|
@ -5,6 +5,7 @@ import { WebSearch } from "@opencode-ai/core/websearch"
|
|||
import { WebSearchExa } from "@opencode-ai/core/plugin/websearch/exa"
|
||||
import { WebSearchFirecrawl } from "@opencode-ai/core/plugin/websearch/firecrawl"
|
||||
import { WebSearchParallel } from "@opencode-ai/core/plugin/websearch/parallel"
|
||||
import { WebSearchTavily } from "@opencode-ai/core/plugin/websearch/tavily"
|
||||
import { host, integrationHost, webSearchHost } from "./host"
|
||||
import { requests, resetWebSearchFixture, webSearchIntegrationTest } from "./websearch-fixture"
|
||||
|
||||
|
|
@ -190,4 +191,71 @@ describe("built-in web search providers", () => {
|
|||
expect(JSON.stringify(output)).not.toContain("parallel-secret")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("registers Tavily with keyless and keyed Search API access", () =>
|
||||
Effect.gen(function* () {
|
||||
resetWebSearchFixture(
|
||||
JSON.stringify({
|
||||
query: "effect typescript",
|
||||
results: [
|
||||
{
|
||||
url: "https://effect.website",
|
||||
title: "Effect",
|
||||
content: "Effect documentation",
|
||||
score: 0.99,
|
||||
},
|
||||
],
|
||||
}),
|
||||
)
|
||||
const integrations = yield* Integration.Service
|
||||
const websearch = yield* WebSearch.Service
|
||||
yield* WebSearchTavily.Plugin.effect(
|
||||
host({ integration: integrationHost(integrations), websearch: webSearchHost(websearch) }),
|
||||
)
|
||||
|
||||
expect(yield* integrations.get(Integration.ID.make("tavily"))).toMatchObject({
|
||||
id: "tavily",
|
||||
name: "Tavily",
|
||||
methods: [{ type: "key" }, { type: "env", names: ["TAVILY_API_KEY"] }],
|
||||
})
|
||||
const query = {
|
||||
query: "effect typescript",
|
||||
providerID: WebSearch.ID.make("tavily"),
|
||||
}
|
||||
expect(yield* websearch.query(query)).toEqual(
|
||||
new WebSearch.Response({
|
||||
providerID: WebSearch.ID.make("tavily"),
|
||||
results: [
|
||||
{
|
||||
url: "https://effect.website",
|
||||
title: "Effect",
|
||||
content: "Effect documentation",
|
||||
time: {},
|
||||
},
|
||||
],
|
||||
}),
|
||||
)
|
||||
expect(requests[0]).toMatchObject({
|
||||
url: WebSearchTavily.endpoint,
|
||||
headers: { "x-client-name": "opencode2", "x-tavily-access-mode": "keyless" },
|
||||
body: {
|
||||
query: "effect typescript",
|
||||
search_depth: "basic",
|
||||
chunks_per_source: 3,
|
||||
max_results: 8,
|
||||
},
|
||||
})
|
||||
expect(requests[0]?.headers.authorization).toBeUndefined()
|
||||
|
||||
yield* integrations.connection.key({
|
||||
integrationID: Integration.ID.make("tavily"),
|
||||
key: "tavily-secret",
|
||||
})
|
||||
yield* websearch.query(query)
|
||||
expect(requests[1]).toMatchObject({
|
||||
headers: { authorization: "Bearer tavily-secret", "x-client-name": "opencode2" },
|
||||
})
|
||||
expect(requests[1]?.headers["x-tavily-access-mode"]).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -467,7 +467,9 @@ function webSearchProviderLabel(provider: unknown, i18n: ReturnType<typeof useI1
|
|||
? "Exa"
|
||||
: provider === "firecrawl"
|
||||
? "Firecrawl"
|
||||
: undefined
|
||||
: provider === "tavily"
|
||||
? "Tavily"
|
||||
: undefined
|
||||
if (name) return i18n.t("ui.tool.websearch.provider", { provider: name })
|
||||
return i18n.t("ui.tool.websearch")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export function webSearchProviderLabel(provider: unknown) {
|
|||
if (provider === "parallel") return "Parallel Web Search"
|
||||
if (provider === "exa") return "Exa Web Search"
|
||||
if (provider === "firecrawl") return "Firecrawl Web Search"
|
||||
if (provider === "tavily") return "Tavily Web Search"
|
||||
return "Web Search"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ describe("webSearchProviderLabel", () => {
|
|||
test("labels known providers", () => {
|
||||
expect(webSearchProviderLabel("parallel")).toBe("Parallel Web Search")
|
||||
expect(webSearchProviderLabel("exa")).toBe("Exa Web Search")
|
||||
expect(webSearchProviderLabel("tavily")).toBe("Tavily Web Search")
|
||||
})
|
||||
|
||||
for (const [name, provider] of [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue