forked from anomalyco/opencode
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathwebsearch.ts
More file actions
143 lines (129 loc) · 5.07 KB
/
websearch.ts
File metadata and controls
143 lines (129 loc) · 5.07 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
import { Effect, Schema } from "effect"
import { HttpClient } from "effect/unstable/http"
import * as Tool from "./tool"
import * as McpWebSearch from "./mcp-websearch"
import DESCRIPTION from "./websearch.txt"
import { checksum } from "@opencode-ai/core/util/encode"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { RuntimeFlags } from "@/effect/runtime-flags"
export const Parameters = Schema.Struct({
query: Schema.String.annotate({ description: "Websearch query" }),
numResults: Schema.optional(Schema.Number).annotate({
description: "Number of search results to return (default: 8)",
}),
livecrawl: Schema.optional(Schema.Literals(["fallback", "preferred"])).annotate({
description:
"Live crawl mode - 'fallback': use live crawling as backup if cached content unavailable, 'preferred': prioritize live crawling (default: 'fallback')",
}),
type: Schema.optional(Schema.Literals(["auto", "fast", "deep"])).annotate({
description: "Search type - 'auto': balanced search (default), 'fast': quick results, 'deep': comprehensive search",
}),
contextMaxCharacters: Schema.optional(Schema.Number).annotate({
description: "Maximum characters for context string optimized for LLMs (default: 10000)",
}),
})
const WebSearchProviderSchema = Schema.Literals(["exa", "parallel"])
export type WebSearchProvider = Schema.Schema.Type<typeof WebSearchProviderSchema>
export function selectWebSearchProvider(sessionID: string, flags = { exa: false, parallel: false }): WebSearchProvider {
const override = process.env.OPENCODE_WEBSEARCH_PROVIDER
if (override === "exa" || override === "parallel") return override
if (flags.parallel) return "parallel"
if (flags.exa) return "exa"
return Number.parseInt(checksum(sessionID) ?? "0", 36) % 2 === 0 ? "exa" : "parallel"
}
export function webSearchProviderLabel(provider: unknown) {
if (provider === "parallel") return "Parallel Web Search"
if (provider === "exa") return "Exa Web Search"
return "Web Search"
}
export function webSearchModelName(extra: Tool.Context["extra"]) {
const model = extra?.model
if (!model || typeof model !== "object") return undefined
const api = "api" in model && model.api && typeof model.api === "object" ? model.api : undefined
const apiID = api && "id" in api && typeof api.id === "string" ? api.id : undefined
const id = "id" in model && typeof model.id === "string" ? model.id : undefined
return (apiID ?? id)?.slice(0, 100)
}
function parallelAuthHeaders() {
const headers = { "User-Agent": `opencode/${InstallationVersion}` }
if (!process.env.PARALLEL_API_KEY) return headers
return { ...headers, Authorization: `Bearer ${process.env.PARALLEL_API_KEY}` }
}
function callProvider(
http: HttpClient.HttpClient,
provider: WebSearchProvider,
params: Schema.Schema.Type<typeof Parameters>,
ctx: Tool.Context,
) {
if (provider === "parallel") {
return McpWebSearch.call(
http,
McpWebSearch.PARALLEL_URL,
"web_search",
McpWebSearch.ParallelSearchArgs,
{
objective: params.query,
search_queries: [params.query],
session_id: ctx.sessionID,
model_name: webSearchModelName(ctx.extra),
},
"25 seconds",
parallelAuthHeaders(),
)
}
return McpWebSearch.call(
http,
McpWebSearch.EXA_URL,
"web_search_exa",
McpWebSearch.SearchArgs,
{
query: params.query,
type: params.type || "auto",
numResults: params.numResults || 8,
livecrawl: params.livecrawl || "fallback",
contextMaxCharacters: params.contextMaxCharacters,
},
"25 seconds",
)
}
export const WebSearchTool = Tool.define(
"websearch",
Effect.gen(function* () {
const http = yield* HttpClient.HttpClient
const flags = yield* RuntimeFlags.Service
return {
get description() {
return DESCRIPTION.replace("{{year}}", new Date().getFullYear().toString())
},
parameters: Parameters,
execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context) =>
Effect.gen(function* () {
const provider = selectWebSearchProvider(ctx.sessionID, {
exa: flags.enableExa,
parallel: flags.enableParallel,
})
const title = webSearchProviderLabel(provider)
yield* ctx.metadata({ title: `${title} "${params.query}"`, metadata: { provider } })
yield* ctx.ask({
permission: "websearch",
patterns: [params.query],
always: ["*"],
metadata: {
query: params.query,
numResults: params.numResults,
livecrawl: params.livecrawl,
type: params.type,
contextMaxCharacters: params.contextMaxCharacters,
provider,
},
})
const result = yield* callProvider(http, provider, params, ctx)
return {
output: result ?? "No search results found. Please try a different query.",
title: `${title}: ${params.query}`,
metadata: { provider },
}
}).pipe(Effect.orDie),
}
}),
)