refactor(search): isolate provider schemas
This commit is contained in:
parent
228db3dd9a
commit
97aa6713b5
14 changed files with 151 additions and 227 deletions
|
|
@ -7,12 +7,19 @@ import { SearchMcp } from "./mcp"
|
|||
|
||||
export const endpoint = "https://mcp.exa.ai/mcp"
|
||||
|
||||
const Args = Schema.Struct({
|
||||
const Input = Schema.Struct({
|
||||
query: Schema.String,
|
||||
type: Schema.String,
|
||||
numResults: Schema.Number,
|
||||
livecrawl: Schema.String,
|
||||
contextMaxCharacters: Schema.optional(Schema.Number),
|
||||
numResults: Schema.Number.pipe(Schema.optional),
|
||||
})
|
||||
|
||||
const Output = Schema.Struct({
|
||||
content: Schema.Array(
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("text"),
|
||||
text: Schema.String,
|
||||
_meta: Schema.Struct({ searchTime: Schema.Number }).pipe(Schema.optional),
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
export const Plugin = define<HttpClient.HttpClient | Scope.Scope>({
|
||||
|
|
@ -31,13 +38,21 @@ export const Plugin = define<HttpClient.HttpClient | Scope.Scope>({
|
|||
execute: (input, context) => {
|
||||
const url = new URL(endpoint)
|
||||
if (context.credential?.type === "key") url.searchParams.set("exaApiKey", context.credential.key)
|
||||
return SearchMcp.call(http, url.toString(), "web_search_exa", Args, {
|
||||
query: input.query,
|
||||
type: input.type ?? "auto",
|
||||
numResults: input.numResults ?? 8,
|
||||
livecrawl: input.livecrawl ?? "fallback",
|
||||
contextMaxCharacters: input.contextMaxCharacters,
|
||||
}).pipe(Effect.map((text) => ({ text: text ?? "" })))
|
||||
return SearchMcp.call(
|
||||
http,
|
||||
url.toString(),
|
||||
"web_search_exa",
|
||||
{ input: Input, output: Output },
|
||||
{ query: input.query },
|
||||
).pipe(
|
||||
Effect.map((result) => {
|
||||
const content = result?.content.find((item) => item.text)
|
||||
return {
|
||||
text: content?.text ?? "",
|
||||
...(content?._meta ? { metadata: content._meta } : {}),
|
||||
}
|
||||
}),
|
||||
)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -6,30 +6,24 @@ import { collectBoundedResponseBody } from "../../tool/http-body"
|
|||
|
||||
export const MAX_RESPONSE_BYTES = 256 * 1024
|
||||
|
||||
const Result = Schema.Struct({
|
||||
result: Schema.Struct({
|
||||
content: Schema.Array(Schema.Struct({ type: Schema.String, text: Schema.String })),
|
||||
}),
|
||||
})
|
||||
const decodeResult = Schema.decodeUnknownEffect(Schema.fromJsonString(Result))
|
||||
|
||||
const parsePayload = (payload: string) =>
|
||||
Effect.gen(function* () {
|
||||
export const parseResponse = <F extends Schema.Struct.Fields>(body: string, result: Schema.Struct<F>) => {
|
||||
const decode = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Struct({ result })))
|
||||
const parse = (payload: string) => {
|
||||
const trimmed = payload.trim()
|
||||
if (!trimmed.startsWith("{")) return undefined
|
||||
return (yield* decodeResult(trimmed)).result.content.find((item) => item.text)?.text
|
||||
})
|
||||
|
||||
export const parseResponse = Effect.fn("SearchMcp.parseResponse")(function* (body: string) {
|
||||
const trimmed = body.trim()
|
||||
const direct = trimmed ? yield* parsePayload(trimmed) : undefined
|
||||
if (direct) return direct
|
||||
for (const line of body.split("\n")) {
|
||||
if (!line.startsWith("data: ")) continue
|
||||
const data = yield* parsePayload(line.substring(6))
|
||||
if (data) return data
|
||||
if (!trimmed.startsWith("{")) return Effect.succeed(undefined)
|
||||
return decode(trimmed).pipe(Effect.map((response) => response.result))
|
||||
}
|
||||
})
|
||||
return Effect.gen(function* () {
|
||||
const trimmed = body.trim()
|
||||
const direct = trimmed ? yield* parse(trimmed) : undefined
|
||||
if (direct) return direct
|
||||
for (const line of body.split("\n")) {
|
||||
if (!line.startsWith("data: ")) continue
|
||||
const data = yield* parse(line.substring(6))
|
||||
if (data) return data
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const Request = <F extends Schema.Struct.Fields>(args: Schema.Struct<F>) =>
|
||||
Schema.Struct({
|
||||
|
|
@ -39,11 +33,11 @@ const Request = <F extends Schema.Struct.Fields>(args: Schema.Struct<F>) =>
|
|||
params: Schema.Struct({ name: Schema.String, arguments: args }),
|
||||
})
|
||||
|
||||
export const call = <F extends Schema.Struct.Fields>(
|
||||
export const call = <F extends Schema.Struct.Fields, R extends Schema.Struct.Fields>(
|
||||
http: HttpClient.HttpClient,
|
||||
url: string,
|
||||
tool: string,
|
||||
args: Schema.Struct<F>,
|
||||
schema: { readonly input: Schema.Struct<F>; readonly output: Schema.Struct<R> },
|
||||
value: Schema.Struct.Type<F>,
|
||||
headers: Record<string, string> = {},
|
||||
) =>
|
||||
|
|
@ -51,7 +45,7 @@ export const call = <F extends Schema.Struct.Fields>(
|
|||
const request = yield* HttpClientRequest.post(url).pipe(
|
||||
HttpClientRequest.accept("application/json, text/event-stream"),
|
||||
HttpClientRequest.setHeaders(headers),
|
||||
HttpClientRequest.schemaBodyJson(Request(args))({
|
||||
HttpClientRequest.schemaBodyJson(Request(schema.input))({
|
||||
jsonrpc: "2.0" as const,
|
||||
id: 1 as const,
|
||||
method: "tools/call" as const,
|
||||
|
|
@ -65,7 +59,7 @@ export const call = <F extends Schema.Struct.Fields>(
|
|||
MAX_RESPONSE_BYTES,
|
||||
() => new Error(`${tool} response exceeded ${MAX_RESPONSE_BYTES} bytes`),
|
||||
)
|
||||
return yield* parseResponse(body.toString("utf8"))
|
||||
return yield* parseResponse(body.toString("utf8"), schema.output)
|
||||
}).pipe(
|
||||
Effect.timeoutOrElse({
|
||||
duration: Duration.seconds(25),
|
||||
|
|
|
|||
|
|
@ -8,11 +8,46 @@ import { SearchMcp } from "./mcp"
|
|||
|
||||
export const endpoint = "https://search.parallel.ai/mcp"
|
||||
|
||||
const Args = Schema.Struct({
|
||||
const Input = Schema.Struct({
|
||||
objective: Schema.String,
|
||||
search_queries: Schema.Array(Schema.String),
|
||||
session_id: Schema.String.check(Schema.isMaxLength(100)).pipe(Schema.optional),
|
||||
model_name: Schema.String.check(Schema.isMaxLength(100)).pipe(Schema.optional),
|
||||
})
|
||||
|
||||
const Metadata = Schema.Struct({
|
||||
search_id: Schema.String,
|
||||
results: Schema.Array(
|
||||
Schema.Struct({
|
||||
url: Schema.String,
|
||||
title: Schema.NullOr(Schema.String).pipe(Schema.optional),
|
||||
publish_date: Schema.NullOr(Schema.String).pipe(Schema.optional),
|
||||
excerpts: Schema.Array(Schema.String),
|
||||
}),
|
||||
),
|
||||
warnings: Schema.NullOr(
|
||||
Schema.Array(
|
||||
Schema.Struct({
|
||||
type: Schema.Literals(["spec_validation_warning", "input_validation_warning", "warning"]),
|
||||
message: Schema.String,
|
||||
detail: Schema.NullOr(Schema.Record(Schema.String, Schema.Json)).pipe(Schema.optional),
|
||||
}),
|
||||
),
|
||||
).pipe(Schema.optional),
|
||||
usage: Schema.NullOr(
|
||||
Schema.Array(
|
||||
Schema.Struct({
|
||||
name: Schema.String,
|
||||
count: Schema.Int,
|
||||
}),
|
||||
),
|
||||
).pipe(Schema.optional),
|
||||
session_id: Schema.String,
|
||||
})
|
||||
const Output = Schema.Struct({
|
||||
content: Schema.Array(Schema.Struct({ type: Schema.Literal("text"), text: Schema.String })),
|
||||
structuredContent: Metadata,
|
||||
})
|
||||
|
||||
export const Plugin = define<HttpClient.HttpClient | Scope.Scope>({
|
||||
id: "opencode.search.parallel",
|
||||
|
|
@ -32,17 +67,25 @@ export const Plugin = define<HttpClient.HttpClient | Scope.Scope>({
|
|||
http,
|
||||
endpoint,
|
||||
"web_search",
|
||||
Args,
|
||||
{ input: Input, output: Output },
|
||||
{
|
||||
objective: input.query,
|
||||
search_queries: [input.query],
|
||||
session_id: context.sessionID ?? "opencode",
|
||||
...(context.sessionID ? { session_id: context.sessionID } : {}),
|
||||
},
|
||||
{
|
||||
"User-Agent": `opencode/${InstallationVersion}`,
|
||||
...(context.credential?.type === "key" ? { Authorization: `Bearer ${context.credential.key}` } : {}),
|
||||
},
|
||||
).pipe(Effect.map((text) => ({ text: text ?? "" }))),
|
||||
).pipe(
|
||||
Effect.map((result) => {
|
||||
const content = result?.content.find((item) => item.text)
|
||||
return {
|
||||
text: content?.text ?? "",
|
||||
...(result ? { metadata: result.structuredContent } : {}),
|
||||
}
|
||||
}),
|
||||
),
|
||||
},
|
||||
})
|
||||
}),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue