mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-15 12:41:37 +03:00
feat(core): add TinyFish web search (#48561)
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
9f3ba44c4a
commit
3f579f53f4
@@ -2,10 +2,12 @@ import { WebSearchExa } from "./exa.js"
|
||||
import { WebSearchFirecrawl } from "./firecrawl.js"
|
||||
import { WebSearchParallel } from "./parallel.js"
|
||||
import { WebSearchTavily } from "./tavily.js"
|
||||
import { WebSearchTinyFish } from "./tinyfish.js"
|
||||
|
||||
export const WebSearchPlugins = [
|
||||
WebSearchExa.Plugin,
|
||||
WebSearchFirecrawl.Plugin,
|
||||
WebSearchParallel.Plugin,
|
||||
WebSearchTavily.Plugin,
|
||||
WebSearchTinyFish.Plugin,
|
||||
] as const
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
export * as WebSearchTinyFish from "./tinyfish.js"
|
||||
|
||||
import { define } from "@opencode/plugin/effect/plugin"
|
||||
import { Effect, Option, Schema, Scope } from "effect"
|
||||
import { HttpClient } from "effect/unstable/http"
|
||||
import { App } from "../../app.js"
|
||||
import { WebSearchMcp } from "./mcp.js"
|
||||
|
||||
export const endpoint = "https://agent.tinyfish.ai/mcp"
|
||||
|
||||
const McpInput = Schema.Struct({
|
||||
query: Schema.String,
|
||||
})
|
||||
|
||||
const McpOutput = Schema.Struct({
|
||||
content: Schema.Array(Schema.Struct({ type: Schema.Literal("text"), text: Schema.String })),
|
||||
})
|
||||
|
||||
const SearchResponse = Schema.fromJsonString(
|
||||
Schema.Struct({
|
||||
results: Schema.Array(
|
||||
Schema.Struct({
|
||||
url: Schema.String,
|
||||
title: Schema.String,
|
||||
snippet: Schema.String,
|
||||
}),
|
||||
),
|
||||
}),
|
||||
)
|
||||
const decodeSearchResponse = Schema.decodeUnknownOption(SearchResponse)
|
||||
|
||||
export const Plugin = define<HttpClient.HttpClient | Scope.Scope>({
|
||||
id: "opencode.websearch.tinyfish",
|
||||
effect: Effect.fn("WebSearchTinyFish.Plugin")(function* (ctx) {
|
||||
const http = yield* HttpClient.HttpClient
|
||||
yield* ctx.integration.transform((editor) => {
|
||||
editor.update("tinyfish", (integration) => (integration.name = "TinyFish"))
|
||||
editor.method.update({
|
||||
integrationID: "tinyfish",
|
||||
method: { type: "key" },
|
||||
})
|
||||
editor.method.update({
|
||||
integrationID: "tinyfish",
|
||||
method: { type: "env", names: ["TINYFISH_API_KEY"] },
|
||||
})
|
||||
})
|
||||
yield* ctx.websearch.transform((editor) => {
|
||||
editor.add({
|
||||
id: "tinyfish",
|
||||
name: "TinyFish",
|
||||
execute: (input) =>
|
||||
Effect.gen(function* () {
|
||||
const connection = yield* ctx.integration.connection.active("tinyfish")
|
||||
const credential = connection ? yield* ctx.integration.connection.resolve(connection) : undefined
|
||||
const result = yield* WebSearchMcp.call(
|
||||
http,
|
||||
endpoint,
|
||||
"search",
|
||||
{ input: McpInput, output: McpOutput },
|
||||
{ query: input.query },
|
||||
{
|
||||
"User-Agent": App.useragent(ctx.app),
|
||||
...(credential?.type === "key"
|
||||
? { "X-API-Key": credential.key }
|
||||
: { "X-TinyFish-Access-Mode": "keyless" }),
|
||||
},
|
||||
)
|
||||
const content = result?.content.find((item) => item.text)
|
||||
const response = content ? Option.getOrUndefined(decodeSearchResponse(content.text)) : undefined
|
||||
return (
|
||||
response?.results.map((item) => ({
|
||||
url: item.url,
|
||||
title: item.title,
|
||||
...(item.snippet ? { content: item.snippet } : {}),
|
||||
time: {},
|
||||
})) ?? []
|
||||
)
|
||||
}),
|
||||
})
|
||||
})
|
||||
}),
|
||||
})
|
||||
@@ -6,6 +6,7 @@ import { WebSearchExa } from "@opencode/core/plugin/websearch/exa"
|
||||
import { WebSearchFirecrawl } from "@opencode/core/plugin/websearch/firecrawl"
|
||||
import { WebSearchParallel } from "@opencode/core/plugin/websearch/parallel"
|
||||
import { WebSearchTavily } from "@opencode/core/plugin/websearch/tavily"
|
||||
import { WebSearchTinyFish } from "@opencode/core/plugin/websearch/tinyfish"
|
||||
import { host, integrationHost, webSearchHost } from "./host"
|
||||
import { requests, signals, resetWebSearchFixture, webSearchIntegrationTest } from "./websearch-fixture"
|
||||
|
||||
@@ -30,24 +31,26 @@ beforeEach(() => {
|
||||
const it = webSearchIntegrationTest
|
||||
|
||||
describe("built-in web search providers", () => {
|
||||
;[WebSearchExa.Plugin, WebSearchParallel.Plugin, WebSearchFirecrawl.Plugin, WebSearchTavily.Plugin].forEach(
|
||||
(plugin) => {
|
||||
it.effect(`releases rate-limited HTTP requests for ${plugin.id} before caching their errors`, () =>
|
||||
Effect.gen(function* () {
|
||||
resetWebSearchFixture("Rate limited", 429)
|
||||
const integrations = yield* Integration.Service
|
||||
const websearch = yield* WebSearch.Service
|
||||
yield* plugin.effect(
|
||||
host({ integration: integrationHost(integrations), websearch: webSearchHost(websearch) }),
|
||||
)
|
||||
yield* websearch.select("random")
|
||||
expect(yield* websearch.query({ query: "limited" }).pipe(Effect.flip)).toBeInstanceOf(WebSearch.RequestError)
|
||||
expect(signals).toHaveLength(1)
|
||||
expect(signals[0]?.aborted).toBe(true)
|
||||
}),
|
||||
)
|
||||
},
|
||||
)
|
||||
;[
|
||||
WebSearchExa.Plugin,
|
||||
WebSearchParallel.Plugin,
|
||||
WebSearchFirecrawl.Plugin,
|
||||
WebSearchTavily.Plugin,
|
||||
WebSearchTinyFish.Plugin,
|
||||
].forEach((plugin) => {
|
||||
it.effect(`releases rate-limited HTTP requests for ${plugin.id} before caching their errors`, () =>
|
||||
Effect.gen(function* () {
|
||||
resetWebSearchFixture("Rate limited", 429)
|
||||
const integrations = yield* Integration.Service
|
||||
const websearch = yield* WebSearch.Service
|
||||
yield* plugin.effect(host({ integration: integrationHost(integrations), websearch: webSearchHost(websearch) }))
|
||||
yield* websearch.select("random")
|
||||
expect(yield* websearch.query({ query: "limited" }).pipe(Effect.flip)).toBeInstanceOf(WebSearch.RequestError)
|
||||
expect(signals).toHaveLength(1)
|
||||
expect(signals[0]?.aborted).toBe(true)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it.effect("registers a provider without an integration", () =>
|
||||
Effect.gen(function* () {
|
||||
@@ -277,4 +280,84 @@ describe("built-in web search providers", () => {
|
||||
expect(requests[1]?.headers["x-tavily-access-mode"]).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("registers TinyFish with keyless and keyed MCP search access", () =>
|
||||
Effect.gen(function* () {
|
||||
resetWebSearchFixture(
|
||||
JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
id: 1,
|
||||
result: {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
query: "effect typescript",
|
||||
results: [
|
||||
{
|
||||
position: 1,
|
||||
site_name: "effect.website",
|
||||
snippet: "Effect documentation",
|
||||
title: "Effect",
|
||||
url: "https://effect.website",
|
||||
},
|
||||
],
|
||||
total_results: 1,
|
||||
page: 0,
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
)
|
||||
const integrations = yield* Integration.Service
|
||||
const websearch = yield* WebSearch.Service
|
||||
yield* WebSearchTinyFish.Plugin.effect(
|
||||
host({ integration: integrationHost(integrations), websearch: webSearchHost(websearch) }),
|
||||
)
|
||||
|
||||
expect(yield* integrations.get(Integration.ID.make("tinyfish"))).toMatchObject({
|
||||
id: "tinyfish",
|
||||
name: "TinyFish",
|
||||
methods: [{ type: "key" }, { type: "env", names: ["TINYFISH_API_KEY"] }],
|
||||
})
|
||||
expect(yield* websearch.query({ query: "effect typescript", providerID: WebSearch.ID.make("tinyfish") })).toEqual(
|
||||
new WebSearch.Response({
|
||||
providerID: WebSearch.ID.make("tinyfish"),
|
||||
results: [
|
||||
{
|
||||
url: "https://effect.website",
|
||||
title: "Effect",
|
||||
content: "Effect documentation",
|
||||
time: {},
|
||||
},
|
||||
],
|
||||
}),
|
||||
)
|
||||
expect(requests[0]).toMatchObject({
|
||||
url: WebSearchTinyFish.endpoint,
|
||||
headers: { "x-tinyfish-access-mode": "keyless" },
|
||||
body: {
|
||||
jsonrpc: "2.0",
|
||||
id: 1,
|
||||
method: "tools/call",
|
||||
params: {
|
||||
name: "search",
|
||||
arguments: { query: "effect typescript" },
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(requests[0]?.headers.authorization).toBeUndefined()
|
||||
|
||||
yield* integrations.connection.key({
|
||||
integrationID: Integration.ID.make("tinyfish"),
|
||||
key: "tinyfish-secret",
|
||||
})
|
||||
yield* websearch.query({ query: "effect typescript", providerID: WebSearch.ID.make("tinyfish") })
|
||||
expect(requests[1]).toMatchObject({
|
||||
headers: { "x-api-key": "tinyfish-secret" },
|
||||
})
|
||||
expect(requests[1]?.headers["x-tinyfish-access-mode"]).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user