@radaros/core 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +133 -1
- package/dist/index.js +500 -12
- package/package.json +6 -2
- package/src/index.ts +11 -0
- package/src/toolkits/base.ts +15 -0
- package/src/toolkits/gmail.ts +226 -0
- package/src/toolkits/hackernews.ts +121 -0
- package/src/toolkits/websearch.ts +158 -0
- package/src/toolkits/whatsapp.ts +209 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import type { ToolDef } from "../tools/types.js";
|
|
4
|
+
import type { RunContext } from "../agent/run-context.js";
|
|
5
|
+
import { Toolkit } from "./base.js";
|
|
6
|
+
|
|
7
|
+
const _require = createRequire(import.meta.url);
|
|
8
|
+
|
|
9
|
+
export interface GmailConfig {
|
|
10
|
+
/** Path to OAuth2 credentials JSON file. Falls back to GMAIL_CREDENTIALS_PATH env var. */
|
|
11
|
+
credentialsPath?: string;
|
|
12
|
+
/** Path to saved token JSON file. Falls back to GMAIL_TOKEN_PATH env var. */
|
|
13
|
+
tokenPath?: string;
|
|
14
|
+
/** Pre-authenticated OAuth2 client (if you handle auth yourself). */
|
|
15
|
+
authClient?: any;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Gmail Toolkit — send, search, and read emails from your agent.
|
|
20
|
+
*
|
|
21
|
+
* Requires: npm install googleapis
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* const gmail = new GmailToolkit({ credentialsPath: "./credentials.json", tokenPath: "./token.json" });
|
|
26
|
+
* const agent = new Agent({ tools: [...gmail.getTools()] });
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export class GmailToolkit extends Toolkit {
|
|
30
|
+
readonly name = "gmail";
|
|
31
|
+
private config: GmailConfig;
|
|
32
|
+
private gmail: any = null;
|
|
33
|
+
|
|
34
|
+
constructor(config: GmailConfig = {}) {
|
|
35
|
+
super();
|
|
36
|
+
this.config = config;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private async getGmailClient(): Promise<any> {
|
|
40
|
+
if (this.gmail) return this.gmail;
|
|
41
|
+
|
|
42
|
+
if (this.config.authClient) {
|
|
43
|
+
const { google } = _require("googleapis");
|
|
44
|
+
this.gmail = google.gmail({ version: "v1", auth: this.config.authClient });
|
|
45
|
+
return this.gmail;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const credPath =
|
|
49
|
+
this.config.credentialsPath ?? process.env.GMAIL_CREDENTIALS_PATH;
|
|
50
|
+
const tokenPath =
|
|
51
|
+
this.config.tokenPath ?? process.env.GMAIL_TOKEN_PATH;
|
|
52
|
+
|
|
53
|
+
if (!credPath || !tokenPath) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
"GmailToolkit: Provide credentialsPath + tokenPath, or an authClient. " +
|
|
56
|
+
"Set GMAIL_CREDENTIALS_PATH and GMAIL_TOKEN_PATH env vars, or pass them in config."
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const { google } = _require("googleapis");
|
|
61
|
+
const fs = await import("node:fs");
|
|
62
|
+
const creds = JSON.parse(fs.readFileSync(credPath, "utf-8"));
|
|
63
|
+
const token = JSON.parse(fs.readFileSync(tokenPath, "utf-8"));
|
|
64
|
+
|
|
65
|
+
const { client_id, client_secret, redirect_uris } =
|
|
66
|
+
creds.installed || creds.web;
|
|
67
|
+
const oAuth2 = new google.auth.OAuth2(
|
|
68
|
+
client_id,
|
|
69
|
+
client_secret,
|
|
70
|
+
redirect_uris?.[0]
|
|
71
|
+
);
|
|
72
|
+
oAuth2.setCredentials(token);
|
|
73
|
+
|
|
74
|
+
this.gmail = google.gmail({ version: "v1", auth: oAuth2 });
|
|
75
|
+
return this.gmail;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
getTools(): ToolDef[] {
|
|
79
|
+
const self = this;
|
|
80
|
+
|
|
81
|
+
return [
|
|
82
|
+
{
|
|
83
|
+
name: "gmail_send",
|
|
84
|
+
description:
|
|
85
|
+
"Send an email via Gmail. Provide recipient, subject, and body.",
|
|
86
|
+
parameters: z.object({
|
|
87
|
+
to: z.string().describe("Recipient email address"),
|
|
88
|
+
subject: z.string().describe("Email subject line"),
|
|
89
|
+
body: z.string().describe("Email body (plain text)"),
|
|
90
|
+
}),
|
|
91
|
+
execute: async (
|
|
92
|
+
args: Record<string, unknown>,
|
|
93
|
+
_ctx: RunContext
|
|
94
|
+
): Promise<string> => {
|
|
95
|
+
const gmail = await self.getGmailClient();
|
|
96
|
+
const rawMessage = [
|
|
97
|
+
`To: ${args.to}`,
|
|
98
|
+
`Subject: ${args.subject}`,
|
|
99
|
+
"Content-Type: text/plain; charset=utf-8",
|
|
100
|
+
"",
|
|
101
|
+
args.body,
|
|
102
|
+
].join("\n");
|
|
103
|
+
|
|
104
|
+
const encoded = Buffer.from(rawMessage)
|
|
105
|
+
.toString("base64")
|
|
106
|
+
.replace(/\+/g, "-")
|
|
107
|
+
.replace(/\//g, "_")
|
|
108
|
+
.replace(/=+$/, "");
|
|
109
|
+
|
|
110
|
+
const res = await gmail.users.messages.send({
|
|
111
|
+
userId: "me",
|
|
112
|
+
requestBody: { raw: encoded },
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return `Email sent successfully. Message ID: ${res.data.id}`;
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: "gmail_search",
|
|
120
|
+
description:
|
|
121
|
+
"Search emails in Gmail. Returns subject, from, date, and snippet for matching messages.",
|
|
122
|
+
parameters: z.object({
|
|
123
|
+
query: z
|
|
124
|
+
.string()
|
|
125
|
+
.describe(
|
|
126
|
+
'Gmail search query (e.g. "from:john subject:meeting is:unread")'
|
|
127
|
+
),
|
|
128
|
+
maxResults: z
|
|
129
|
+
.number()
|
|
130
|
+
.optional()
|
|
131
|
+
.describe("Maximum number of results (default 10)"),
|
|
132
|
+
}),
|
|
133
|
+
execute: async (
|
|
134
|
+
args: Record<string, unknown>,
|
|
135
|
+
_ctx: RunContext
|
|
136
|
+
): Promise<string> => {
|
|
137
|
+
const gmail = await self.getGmailClient();
|
|
138
|
+
const max = (args.maxResults as number) ?? 10;
|
|
139
|
+
|
|
140
|
+
const list = await gmail.users.messages.list({
|
|
141
|
+
userId: "me",
|
|
142
|
+
q: args.query as string,
|
|
143
|
+
maxResults: max,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const messages = list.data.messages ?? [];
|
|
147
|
+
if (messages.length === 0) return "No emails found.";
|
|
148
|
+
|
|
149
|
+
const results: string[] = [];
|
|
150
|
+
for (const msg of messages) {
|
|
151
|
+
const detail = await gmail.users.messages.get({
|
|
152
|
+
userId: "me",
|
|
153
|
+
id: msg.id,
|
|
154
|
+
format: "metadata",
|
|
155
|
+
metadataHeaders: ["From", "Subject", "Date"],
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const headers = detail.data.payload?.headers ?? [];
|
|
159
|
+
const getHeader = (name: string) =>
|
|
160
|
+
headers.find(
|
|
161
|
+
(h: any) => h.name.toLowerCase() === name.toLowerCase()
|
|
162
|
+
)?.value ?? "";
|
|
163
|
+
|
|
164
|
+
results.push(
|
|
165
|
+
`ID: ${msg.id}\nFrom: ${getHeader("From")}\nSubject: ${getHeader("Subject")}\nDate: ${getHeader("Date")}\nSnippet: ${detail.data.snippet ?? ""}`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return results.join("\n\n---\n\n");
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: "gmail_read",
|
|
174
|
+
description:
|
|
175
|
+
"Read the full content of an email by its message ID.",
|
|
176
|
+
parameters: z.object({
|
|
177
|
+
messageId: z.string().describe("The Gmail message ID to read"),
|
|
178
|
+
}),
|
|
179
|
+
execute: async (
|
|
180
|
+
args: Record<string, unknown>,
|
|
181
|
+
_ctx: RunContext
|
|
182
|
+
): Promise<string> => {
|
|
183
|
+
const gmail = await self.getGmailClient();
|
|
184
|
+
|
|
185
|
+
const detail = await gmail.users.messages.get({
|
|
186
|
+
userId: "me",
|
|
187
|
+
id: args.messageId as string,
|
|
188
|
+
format: "full",
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const headers = detail.data.payload?.headers ?? [];
|
|
192
|
+
const getHeader = (name: string) =>
|
|
193
|
+
headers.find(
|
|
194
|
+
(h: any) => h.name.toLowerCase() === name.toLowerCase()
|
|
195
|
+
)?.value ?? "";
|
|
196
|
+
|
|
197
|
+
let body = "";
|
|
198
|
+
const payload = detail.data.payload;
|
|
199
|
+
if (payload?.body?.data) {
|
|
200
|
+
body = Buffer.from(payload.body.data, "base64").toString("utf-8");
|
|
201
|
+
} else if (payload?.parts) {
|
|
202
|
+
const textPart = payload.parts.find(
|
|
203
|
+
(p: any) => p.mimeType === "text/plain"
|
|
204
|
+
);
|
|
205
|
+
if (textPart?.body?.data) {
|
|
206
|
+
body = Buffer.from(textPart.body.data, "base64").toString(
|
|
207
|
+
"utf-8"
|
|
208
|
+
);
|
|
209
|
+
} else {
|
|
210
|
+
const htmlPart = payload.parts.find(
|
|
211
|
+
(p: any) => p.mimeType === "text/html"
|
|
212
|
+
);
|
|
213
|
+
if (htmlPart?.body?.data) {
|
|
214
|
+
body = Buffer.from(htmlPart.body.data, "base64").toString(
|
|
215
|
+
"utf-8"
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return `From: ${getHeader("From")}\nTo: ${getHeader("To")}\nSubject: ${getHeader("Subject")}\nDate: ${getHeader("Date")}\n\n${body || "(no body)"}`;
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
];
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { ToolDef } from "../tools/types.js";
|
|
3
|
+
import type { RunContext } from "../agent/run-context.js";
|
|
4
|
+
import { Toolkit } from "./base.js";
|
|
5
|
+
|
|
6
|
+
export interface HackerNewsConfig {
|
|
7
|
+
/** Enable fetching top stories (default true). */
|
|
8
|
+
enableGetTopStories?: boolean;
|
|
9
|
+
/** Enable fetching user details (default true). */
|
|
10
|
+
enableGetUserDetails?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Hacker News Toolkit — search top stories and user details from HN.
|
|
15
|
+
*
|
|
16
|
+
* No API key required — uses the public Hacker News API.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* const hn = new HackerNewsToolkit();
|
|
21
|
+
* const agent = new Agent({ tools: [...hn.getTools()] });
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export class HackerNewsToolkit extends Toolkit {
|
|
25
|
+
readonly name = "hackernews";
|
|
26
|
+
private config: HackerNewsConfig;
|
|
27
|
+
|
|
28
|
+
constructor(config: HackerNewsConfig = {}) {
|
|
29
|
+
super();
|
|
30
|
+
this.config = {
|
|
31
|
+
enableGetTopStories: config.enableGetTopStories ?? true,
|
|
32
|
+
enableGetUserDetails: config.enableGetUserDetails ?? true,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
getTools(): ToolDef[] {
|
|
37
|
+
const tools: ToolDef[] = [];
|
|
38
|
+
|
|
39
|
+
if (this.config.enableGetTopStories) {
|
|
40
|
+
tools.push({
|
|
41
|
+
name: "hackernews_top_stories",
|
|
42
|
+
description:
|
|
43
|
+
"Get the top stories from Hacker News. Returns title, URL, score, author, and comment count.",
|
|
44
|
+
parameters: z.object({
|
|
45
|
+
numStories: z
|
|
46
|
+
.number()
|
|
47
|
+
.optional()
|
|
48
|
+
.describe("Number of top stories to fetch (default 10, max 30)"),
|
|
49
|
+
}),
|
|
50
|
+
execute: async (
|
|
51
|
+
args: Record<string, unknown>,
|
|
52
|
+
_ctx: RunContext
|
|
53
|
+
): Promise<string> => {
|
|
54
|
+
const num = Math.min((args.numStories as number) ?? 10, 30);
|
|
55
|
+
|
|
56
|
+
const idsRes = await fetch(
|
|
57
|
+
"https://hacker-news.firebaseio.com/v0/topstories.json"
|
|
58
|
+
);
|
|
59
|
+
if (!idsRes.ok) throw new Error(`HN API failed: ${idsRes.status}`);
|
|
60
|
+
const ids = (await idsRes.json()) as number[];
|
|
61
|
+
|
|
62
|
+
const stories = await Promise.all(
|
|
63
|
+
ids.slice(0, num).map(async (id) => {
|
|
64
|
+
const res = await fetch(
|
|
65
|
+
`https://hacker-news.firebaseio.com/v0/item/${id}.json`
|
|
66
|
+
);
|
|
67
|
+
return res.json() as Promise<any>;
|
|
68
|
+
})
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
return stories
|
|
72
|
+
.map(
|
|
73
|
+
(s, i) =>
|
|
74
|
+
`${i + 1}. ${s.title}\n URL: ${s.url ?? `https://news.ycombinator.com/item?id=${s.id}`}\n Score: ${s.score} | By: ${s.by} | Comments: ${s.descendants ?? 0}`
|
|
75
|
+
)
|
|
76
|
+
.join("\n\n");
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (this.config.enableGetUserDetails) {
|
|
82
|
+
tools.push({
|
|
83
|
+
name: "hackernews_user",
|
|
84
|
+
description:
|
|
85
|
+
"Get details about a Hacker News user by username. Returns karma, about, and account creation date.",
|
|
86
|
+
parameters: z.object({
|
|
87
|
+
username: z.string().describe("The HN username to look up"),
|
|
88
|
+
}),
|
|
89
|
+
execute: async (
|
|
90
|
+
args: Record<string, unknown>,
|
|
91
|
+
_ctx: RunContext
|
|
92
|
+
): Promise<string> => {
|
|
93
|
+
const username = args.username as string;
|
|
94
|
+
const res = await fetch(
|
|
95
|
+
`https://hacker-news.firebaseio.com/v0/user/${username}.json`
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
if (!res.ok)
|
|
99
|
+
throw new Error(`HN user API failed: ${res.status}`);
|
|
100
|
+
|
|
101
|
+
const user = (await res.json()) as any;
|
|
102
|
+
if (!user) return `User "${username}" not found.`;
|
|
103
|
+
|
|
104
|
+
const created = new Date(user.created * 1000).toISOString().split("T")[0];
|
|
105
|
+
|
|
106
|
+
return [
|
|
107
|
+
`Username: ${user.id}`,
|
|
108
|
+
`Karma: ${user.karma}`,
|
|
109
|
+
`Created: ${created}`,
|
|
110
|
+
user.about ? `About: ${user.about}` : null,
|
|
111
|
+
`Submitted: ${user.submitted?.length ?? 0} items`,
|
|
112
|
+
]
|
|
113
|
+
.filter(Boolean)
|
|
114
|
+
.join("\n");
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return tools;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { ToolDef } from "../tools/types.js";
|
|
3
|
+
import type { RunContext } from "../agent/run-context.js";
|
|
4
|
+
import { Toolkit } from "./base.js";
|
|
5
|
+
|
|
6
|
+
export interface WebSearchConfig {
|
|
7
|
+
/** Search provider: "tavily" or "serpapi". */
|
|
8
|
+
provider: "tavily" | "serpapi";
|
|
9
|
+
/** API key for the search provider. Falls back to TAVILY_API_KEY or SERPAPI_API_KEY env vars. */
|
|
10
|
+
apiKey?: string;
|
|
11
|
+
/** Max results to return per search (default 5). */
|
|
12
|
+
maxResults?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Web Search Toolkit — search the web from your agent.
|
|
17
|
+
*
|
|
18
|
+
* Supports Tavily and SerpAPI backends.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* const search = new WebSearchToolkit({ provider: "tavily" });
|
|
23
|
+
* const agent = new Agent({ tools: [...search.getTools()] });
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export class WebSearchToolkit extends Toolkit {
|
|
27
|
+
readonly name = "websearch";
|
|
28
|
+
private config: WebSearchConfig;
|
|
29
|
+
|
|
30
|
+
constructor(config: WebSearchConfig) {
|
|
31
|
+
super();
|
|
32
|
+
this.config = config;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private getApiKey(): string {
|
|
36
|
+
if (this.config.apiKey) return this.config.apiKey;
|
|
37
|
+
|
|
38
|
+
const envKey =
|
|
39
|
+
this.config.provider === "tavily"
|
|
40
|
+
? process.env.TAVILY_API_KEY
|
|
41
|
+
: process.env.SERPAPI_API_KEY;
|
|
42
|
+
|
|
43
|
+
if (!envKey) {
|
|
44
|
+
const envName =
|
|
45
|
+
this.config.provider === "tavily"
|
|
46
|
+
? "TAVILY_API_KEY"
|
|
47
|
+
: "SERPAPI_API_KEY";
|
|
48
|
+
throw new Error(
|
|
49
|
+
`WebSearchToolkit: No API key provided. Set ${envName} env var or pass apiKey in config.`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return envKey;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
getTools(): ToolDef[] {
|
|
56
|
+
const self = this;
|
|
57
|
+
|
|
58
|
+
return [
|
|
59
|
+
{
|
|
60
|
+
name: "web_search",
|
|
61
|
+
description:
|
|
62
|
+
"Search the web for current information. Returns titles, URLs, and snippets from search results.",
|
|
63
|
+
parameters: z.object({
|
|
64
|
+
query: z.string().describe("The search query"),
|
|
65
|
+
maxResults: z
|
|
66
|
+
.number()
|
|
67
|
+
.optional()
|
|
68
|
+
.describe("Maximum number of results (default 5)"),
|
|
69
|
+
}),
|
|
70
|
+
execute: async (
|
|
71
|
+
args: Record<string, unknown>,
|
|
72
|
+
_ctx: RunContext
|
|
73
|
+
): Promise<string> => {
|
|
74
|
+
const query = args.query as string;
|
|
75
|
+
const max =
|
|
76
|
+
(args.maxResults as number) ?? self.config.maxResults ?? 5;
|
|
77
|
+
|
|
78
|
+
if (self.config.provider === "tavily") {
|
|
79
|
+
return self.searchTavily(query, max);
|
|
80
|
+
}
|
|
81
|
+
return self.searchSerpApi(query, max);
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private async searchTavily(
|
|
88
|
+
query: string,
|
|
89
|
+
maxResults: number
|
|
90
|
+
): Promise<string> {
|
|
91
|
+
const apiKey = this.getApiKey();
|
|
92
|
+
const res = await fetch("https://api.tavily.com/search", {
|
|
93
|
+
method: "POST",
|
|
94
|
+
headers: { "Content-Type": "application/json" },
|
|
95
|
+
body: JSON.stringify({
|
|
96
|
+
api_key: apiKey,
|
|
97
|
+
query,
|
|
98
|
+
max_results: maxResults,
|
|
99
|
+
include_answer: true,
|
|
100
|
+
}),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (!res.ok) {
|
|
104
|
+
throw new Error(`Tavily search failed: ${res.status} ${res.statusText}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const data = (await res.json()) as any;
|
|
108
|
+
const results: string[] = [];
|
|
109
|
+
|
|
110
|
+
if (data.answer) {
|
|
111
|
+
results.push(`Answer: ${data.answer}\n`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (const r of data.results ?? []) {
|
|
115
|
+
results.push(`Title: ${r.title}\nURL: ${r.url}\nSnippet: ${r.content}\n`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return results.join("\n---\n") || "No results found.";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private async searchSerpApi(
|
|
122
|
+
query: string,
|
|
123
|
+
maxResults: number
|
|
124
|
+
): Promise<string> {
|
|
125
|
+
const apiKey = this.getApiKey();
|
|
126
|
+
const params = new URLSearchParams({
|
|
127
|
+
q: query,
|
|
128
|
+
api_key: apiKey,
|
|
129
|
+
engine: "google",
|
|
130
|
+
num: String(maxResults),
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const res = await fetch(
|
|
134
|
+
`https://serpapi.com/search.json?${params.toString()}`
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
if (!res.ok) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
`SerpAPI search failed: ${res.status} ${res.statusText}`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const data = (await res.json()) as any;
|
|
144
|
+
const results: string[] = [];
|
|
145
|
+
|
|
146
|
+
if (data.answer_box?.answer) {
|
|
147
|
+
results.push(`Answer: ${data.answer_box.answer}\n`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
for (const r of (data.organic_results ?? []).slice(0, maxResults)) {
|
|
151
|
+
results.push(
|
|
152
|
+
`Title: ${r.title}\nURL: ${r.link}\nSnippet: ${r.snippet ?? ""}\n`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return results.join("\n---\n") || "No results found.";
|
|
157
|
+
}
|
|
158
|
+
}
|