Skip to content

Commit 764de8f

Browse files
khaliqgantclaude
andcommitted
agents(weekly-digest): wire Brave for web search + Reddit fallback
- searchWeb: live Brave query for "proactive agents", past-week freshness - searchReddit: tries the JSON endpoint first (with a distinctive UA), falls back to Brave site:reddit.com on 4xx/5xx/empty - braveSearch helper centralises auth + error handling; reads BRAVE_API_KEY from env, warns and returns [] if missing Free tier covers our cadence with room (2k q/mo; we use ~12 q/mo). External API calls are now real; only clustering + GitHub upsert remain TODO. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 8e02e56 commit 764de8f

7 files changed

Lines changed: 777 additions & 44 deletions

File tree

agents/weekly-digest/agent.ts

Lines changed: 106 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -108,43 +108,114 @@ async function fetchAllSources(ctx: Context): Promise<Mention[]> {
108108
);
109109
}
110110

111-
async function searchWeb(_ctx: Context): Promise<Mention[]> {
112-
// TODO: wire to Tavily — https://tavily.com (good free tier, returns clean
113-
// JSON tuned for LLM consumption). Set `TAVILY_API_KEY` via `relay providers
114-
// connect tavily` once the runtime supports custom providers; for now the
115-
// env var is read at agent boot.
116-
//
117-
// const res = await fetch("https://api.tavily.com/search", {
118-
// method: "POST",
119-
// headers: { "content-type": "application/json" },
120-
// body: JSON.stringify({
121-
// api_key: process.env.TAVILY_API_KEY,
122-
// query: "proactive agents",
123-
// time_range: "week",
124-
// max_results: 20,
125-
// }),
126-
// signal: _ctx.signal,
127-
// });
128-
// const data = (await res.json()) as { results: { url: string; title: string; content: string; published_date: string }[] };
129-
// return data.results.map((r) => ({ source: "web", url: r.url, title: r.title, excerpt: r.content.slice(0, 280), publishedAt: r.published_date }));
130-
return [];
111+
async function searchWeb(ctx: Context): Promise<Mention[]> {
112+
const results = await braveSearch(ctx, '"proactive agents"', { freshness: "pw", count: 20 });
113+
return results.map((r) => ({
114+
source: "web",
115+
url: r.url,
116+
title: r.title,
117+
excerpt: r.description.slice(0, 280),
118+
publishedAt: r.age ?? new Date().toISOString(),
119+
}));
120+
}
121+
122+
async function searchReddit(ctx: Context, subreddit: string): Promise<Mention[]> {
123+
// Try Reddit's JSON endpoint first. Distinctive UA matters more than rate
124+
// here — generic UAs get blocked first; weekly cadence is well under any
125+
// limit. If we get 4xx/5xx or empty, fall back to Brave site:reddit.com.
126+
try {
127+
const res = await fetch(
128+
`https://www.reddit.com/r/${subreddit}/search.json?q=${encodeURIComponent('"proactive agents"')}&sort=new&t=week&restrict_sr=1`,
129+
{
130+
headers: { "user-agent": "proactive-agents-digest/0.1 (by /u/khaliqgant)" },
131+
signal: ctx.signal,
132+
},
133+
);
134+
if (!res.ok) throw new Error(`reddit ${res.status}`);
135+
const data = (await res.json()) as {
136+
data: {
137+
children: {
138+
data: { url: string; title: string; selftext: string; created_utc: number };
139+
}[];
140+
};
141+
};
142+
if (data.data.children.length === 0) throw new Error("reddit empty");
143+
return data.data.children.map((c) => ({
144+
source: `reddit:${subreddit}` as Mention["source"],
145+
url: c.data.url,
146+
title: c.data.title,
147+
excerpt: c.data.selftext.slice(0, 280),
148+
publishedAt: new Date(c.data.created_utc * 1000).toISOString(),
149+
}));
150+
} catch (err) {
151+
ctx.logger.warn("reddit failed, falling back to brave", {
152+
subreddit,
153+
reason: String(err),
154+
});
155+
const results = await braveSearch(
156+
ctx,
157+
`site:reddit.com/r/${subreddit} "proactive agents"`,
158+
{ freshness: "pw", count: 10 },
159+
);
160+
return results.map((r) => ({
161+
source: `reddit:${subreddit}` as Mention["source"],
162+
url: r.url,
163+
title: r.title,
164+
excerpt: r.description.slice(0, 280),
165+
publishedAt: r.age ?? new Date().toISOString(),
166+
}));
167+
}
131168
}
132169

133-
async function searchReddit(_ctx: Context, subreddit: string): Promise<Mention[]> {
134-
// Reddit's JSON endpoint requires no auth for read; UA must be set.
135-
// const res = await fetch(
136-
// `https://www.reddit.com/r/${subreddit}/search.json?q=${encodeURIComponent('"proactive agents"')}&sort=new&t=week&restrict_sr=1`,
137-
// { headers: { "user-agent": "proactive-agents-digest/0.1 (by /u/khaliqgant)" }, signal: _ctx.signal },
138-
// );
139-
// const data = (await res.json()) as { data: { children: { data: { url: string; title: string; selftext: string; created_utc: number } }[] } };
140-
// return data.data.children.map((c) => ({
141-
// source: `reddit:${subreddit}` as Mention["source"],
142-
// url: c.data.url,
143-
// title: c.data.title,
144-
// excerpt: c.data.selftext.slice(0, 280),
145-
// publishedAt: new Date(c.data.created_utc * 1000).toISOString(),
146-
// }));
147-
return [];
170+
// ─────────────────────────────────────────────────────────────────────────────
171+
// Brave Search
172+
173+
type BraveResult = {
174+
url: string;
175+
title: string;
176+
description: string;
177+
age?: string; // ISO 8601 when present
178+
};
179+
180+
/**
181+
* Brave Search Web API. Free tier: 2,000 queries/month, 1 q/sec. We use
182+
* one query per source per week — well under the limit.
183+
*
184+
* Auth: read `BRAVE_API_KEY` from env. When the proactive runtime supports
185+
* provider connections, swap to `relay providers connect brave` and the SDK
186+
* will inject the key at boot.
187+
*/
188+
async function braveSearch(
189+
ctx: Context,
190+
query: string,
191+
opts: { freshness?: "pd" | "pw" | "pm" | "py"; count?: number } = {},
192+
): Promise<BraveResult[]> {
193+
const apiKey = process.env.BRAVE_API_KEY;
194+
if (!apiKey) {
195+
ctx.logger.warn("BRAVE_API_KEY missing — skipping web search");
196+
return [];
197+
}
198+
const params = new URLSearchParams({
199+
q: query,
200+
count: String(opts.count ?? 20),
201+
text_decorations: "false",
202+
});
203+
if (opts.freshness) params.set("freshness", opts.freshness);
204+
205+
const res = await fetch(`https://api.search.brave.com/res/v1/web/search?${params}`, {
206+
headers: {
207+
"X-Subscription-Token": apiKey,
208+
Accept: "application/json",
209+
"Accept-Encoding": "gzip",
210+
},
211+
signal: ctx.signal,
212+
});
213+
if (!res.ok) {
214+
ctx.logger.error("brave search failed", { status: res.status, query });
215+
return [];
216+
}
217+
const data = (await res.json()) as { web?: { results?: BraveResult[] } };
218+
return data.web?.results ?? [];
148219
}
149220

150221
// ─────────────────────────────────────────────────────────────────────────────

0 commit comments

Comments
 (0)