Skip to content

Commit f0355df

Browse files
authored
feat(provider): add anysearch (oomol-lab#157)
oomol-lab#152
1 parent da1fe8b commit f0355df

5 files changed

Lines changed: 704 additions & 0 deletions

File tree

src/providers/anysearch/actions.ts

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import type { ActionDefinition } from "../../core/types.ts";
2+
3+
import { s } from "../../core/json-schema.ts";
4+
import { defineProviderAction } from "../../core/provider-definition.ts";
5+
6+
const service = "anysearch";
7+
8+
const restDomains = [
9+
"general",
10+
"code",
11+
"tech",
12+
"fashion",
13+
"travel",
14+
"home",
15+
"ecommerce",
16+
"gaming",
17+
"film",
18+
"music",
19+
"finance",
20+
"academic",
21+
"legal",
22+
"business",
23+
"ip",
24+
"security",
25+
"education",
26+
"health",
27+
"religion",
28+
"geo",
29+
"environment",
30+
"energy",
31+
];
32+
33+
const mcpDomains = [
34+
"general",
35+
"resource",
36+
"social_media",
37+
"finance",
38+
"academic",
39+
"legal",
40+
"health",
41+
"business",
42+
"security",
43+
"ip",
44+
"code",
45+
"energy",
46+
"environment",
47+
"agriculture",
48+
"travel",
49+
"film",
50+
"gaming",
51+
];
52+
53+
const searchResultSchema = s.looseRequiredObject("One source returned by AnySearch.", {
54+
title: s.string("The source title."),
55+
url: s.url("The original source URL."),
56+
snippet: s.string("A short summary of the source."),
57+
content: s.string("The cleaned source content."),
58+
});
59+
60+
const searchMetadataSchema = s.looseRequiredObject("Metadata describing the AnySearch request.", {
61+
total_results: s.nonNegativeInteger("The number of results returned by AnySearch."),
62+
search_time_ms: s.nonNegativeInteger("The end-to-end search latency reported by AnySearch in milliseconds."),
63+
});
64+
65+
const mcpSearchItemSchema = s.object(
66+
"One query in an AnySearch batch search request.",
67+
{
68+
query: s.nonEmptyString("The search query with one intent."),
69+
domain: s.stringEnum("The vertical domain selected after calling get_sub_domains.", mcpDomains),
70+
sub_domain: s.nonEmptyString("The sub-domain routing key returned by get_sub_domains."),
71+
sub_domain_params: s.record(
72+
"Structured parameters returned for the selected sub-domain.",
73+
s.unknown("A sub-domain-specific parameter value."),
74+
),
75+
max_results: s.integer("The maximum number of results for this query.", {
76+
minimum: 1,
77+
maximum: 10,
78+
}),
79+
},
80+
{ optional: ["domain", "sub_domain", "sub_domain_params", "max_results"] },
81+
);
82+
83+
export const anySearchActions: ActionDefinition[] = [
84+
defineProviderAction(service, {
85+
name: "search",
86+
description: "Search AnySearch's automatically routed data sources and return structured source results.",
87+
inputSchema: s.object(
88+
"The input payload for an AnySearch unified REST search request.",
89+
{
90+
query: s.nonEmptyString("The search query to execute."),
91+
max_results: s.integer("The maximum number of search results to return.", {
92+
minimum: 1,
93+
maximum: 100,
94+
}),
95+
domain: s.stringEnum("The top-level domain used to route the search.", restDomains),
96+
tag: s.nonEmptyString(
97+
"A sub-domain capability tag, such as code.doc. Use a sub-domain returned by get_sub_domains when possible.",
98+
),
99+
content_types: s.stringArray("Content types to include, such as web, news, or doc.", {
100+
minItems: 1,
101+
itemDescription: "One AnySearch content type.",
102+
}),
103+
zone: s.stringEnum("The regional search zone used by AnySearch.", ["cn", "intl"]),
104+
language: s.nonEmptyString("The preferred result language, such as zh-CN or en."),
105+
params: s.record(
106+
"Parameters required by the selected tag. Use values returned by get_sub_domains when possible.",
107+
s.unknown("A tag-specific parameter value."),
108+
),
109+
},
110+
{ optional: ["max_results", "domain", "tag", "content_types", "zone", "language", "params"] },
111+
),
112+
outputSchema: s.looseRequiredObject("The normalized AnySearch search response.", {
113+
results: s.array("The structured sources returned by AnySearch.", searchResultSchema),
114+
metadata: searchMetadataSchema,
115+
request_id: s.nonEmptyString("The AnySearch request identifier used for tracing."),
116+
}),
117+
}),
118+
defineProviderAction(service, {
119+
name: "get_sub_domains",
120+
description:
121+
"Discover AnySearch vertical sub-domains and their required parameters before running a specialized search.",
122+
inputSchema: s.oneOf(
123+
[
124+
s.object("Discover capabilities for one AnySearch domain.", {
125+
domain: s.stringEnum("The domain whose sub-domains should be returned.", mcpDomains),
126+
}),
127+
s.object("Discover capabilities for multiple AnySearch domains.", {
128+
domains: s.array(
129+
"The domains whose sub-domains should be returned.",
130+
s.stringEnum("One AnySearch vertical domain.", mcpDomains),
131+
{ minItems: 1, maxItems: 5 },
132+
),
133+
}),
134+
],
135+
{ description: "One domain or a batch of up to five domains to discover." },
136+
),
137+
outputSchema: s.object("The current AnySearch vertical capability directory.", {
138+
content: s.nonEmptyString("Markdown describing matching sub-domains and their parameters."),
139+
request_id: s.nonEmptyString("The AnySearch request identifier used for tracing."),
140+
}),
141+
}),
142+
defineProviderAction(service, {
143+
name: "batch_search",
144+
description:
145+
"Run up to five independent general or vertical AnySearch queries in parallel and return agent-ready Markdown.",
146+
inputSchema: s.object("The parallel AnySearch query batch.", {
147+
queries: s.array("The search queries to execute in parallel.", mcpSearchItemSchema, {
148+
minItems: 1,
149+
maxItems: 5,
150+
}),
151+
}),
152+
outputSchema: s.object("The combined AnySearch batch search result.", {
153+
content: s.nonEmptyString("Markdown containing the result of every query in the batch."),
154+
request_ids: s.stringArray("The AnySearch request identifiers associated with the batch.", {
155+
minItems: 1,
156+
maxItems: 5,
157+
itemDescription: "One AnySearch request identifier.",
158+
}),
159+
}),
160+
}),
161+
defineProviderAction(service, {
162+
name: "extract",
163+
description: "Fetch an HTML page through AnySearch and return its cleaned content as Markdown.",
164+
inputSchema: s.object("The page to extract through AnySearch.", {
165+
url: s.url("The HTTP or HTTPS page URL to fetch and convert to Markdown."),
166+
}),
167+
outputSchema: s.object("The cleaned page content returned by AnySearch.", {
168+
content: s.nonEmptyString("The extracted page content as Markdown."),
169+
request_id: s.nonEmptyString("The AnySearch request identifier used for tracing."),
170+
}),
171+
}),
172+
];
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { ProviderDefinition } from "../../core/types.ts";
2+
3+
import { anySearchActions } from "./actions.ts";
4+
5+
const service = "anysearch";
6+
7+
export const provider: ProviderDefinition = {
8+
service,
9+
displayName: "AnySearch",
10+
description: "Real-time web and vertical-domain search, batch search, and page extraction for AI agents.",
11+
categories: ["AI", "Data"],
12+
authTypes: ["api_key"],
13+
auth: [
14+
{
15+
type: "api_key",
16+
label: "API Key",
17+
placeholder: "Enter your AnySearch API key",
18+
description:
19+
"AnySearch API key sent as a Bearer token. Create and manage keys in the AnySearch console: https://www.anysearch.com/console/api-keys.",
20+
},
21+
],
22+
homepageUrl: "https://anysearch.com/home",
23+
actions: anySearchActions,
24+
};
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { CredentialValidators, ProviderExecutors, ProviderProxyExecutor } from "../../core/types.ts";
2+
3+
import { defineApiKeyProviderExecutors, defineProviderProxy } from "../provider-runtime.ts";
4+
import { anySearchActionHandlers, anySearchApiBaseUrl, validateAnySearchApiKey } from "./runtime.ts";
5+
6+
const service = "anysearch";
7+
8+
export const executors: ProviderExecutors = defineApiKeyProviderExecutors(service, anySearchActionHandlers);
9+
10+
export const proxy: ProviderProxyExecutor = defineProviderProxy({
11+
service,
12+
baseUrl: anySearchApiBaseUrl,
13+
auth: { type: "api_key_authorization", prefix: "Bearer " },
14+
customizeRequest({ headers }) {
15+
headers.set("accept", "application/json");
16+
},
17+
});
18+
19+
export const credentialValidators: CredentialValidators = {
20+
apiKey(input, { fetcher, signal }) {
21+
return validateAnySearchApiKey(input.apiKey, fetcher, signal);
22+
},
23+
};
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import type { ApiKeyProviderContext, ProviderFetch } from "../provider-runtime.ts";
2+
3+
import { describe, expect, it } from "vitest";
4+
import { anySearchActionHandlers, validateAnySearchApiKey } from "./runtime.ts";
5+
6+
describe("AnySearch runtime", () => {
7+
it("preserves the status and body of a non-JSON error response", async () => {
8+
const context = createContext(new Response("rate limit exceeded", { status: 429 }));
9+
10+
await expect(anySearchActionHandlers.search!({ query: "connector" }, context)).rejects.toMatchObject({
11+
status: 429,
12+
message: "rate limit exceeded",
13+
details: { message: "rate limit exceeded" },
14+
});
15+
});
16+
17+
it("rejects a non-JSON successful response as an upstream failure", async () => {
18+
const fetcher: ProviderFetch = async () => new Response("not json", { status: 200 });
19+
20+
await expect(validateAnySearchApiKey("test-api-key", fetcher)).rejects.toMatchObject({
21+
status: 502,
22+
message: "AnySearch returned invalid JSON",
23+
});
24+
});
25+
});
26+
27+
function createContext(response: Response): ApiKeyProviderContext {
28+
return {
29+
apiKey: "test-api-key",
30+
fetcher: async () => response,
31+
};
32+
}

0 commit comments

Comments
 (0)