fetch_personalized_news
Retrieve strategic news updates tailored to a founder's profile by analyzing relevance, summarizing content, and managing cached data for timely intelligence.
Instructions
Fetch personalized, strategically-relevant news for a founder. Checks cache freshness first — returns stored articles if <24h old. Otherwise scrapes Google News via Apify, ranks by cosine similarity, summarizes with Groq, and stores results.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| profile_id | Yes | UUID of the profile to fetch news for (must have run analyze_profile first) |
Implementation Reference
- The core logic for fetching, scraping, ranking, and summarizing personalized news based on a founder's profile.
export async function fetchPersonalizedNews({ profile_id }) { if (!profile_id) { throw new Error("profile_id is required."); } // 1. Check staleness const { stale, lastNewsFetch } = await isNewsStale(profile_id); if (!stale) { const cached = await getCachedArticles(profile_id); return { source: "cache", last_fetched: lastNewsFetch?.toISOString(), article_count: cached.length, articles: cached, }; } // 2. Fetch profile + analysis const profile = await getProfile(profile_id); if (!profile) { throw new Error("Profile not found."); } let analysisData = await getProfileAnalysis(profile_id); if (!analysisData) { await analyzeProfile({ profile_id }); analysisData = await getProfileAnalysis(profile_id); } if (!analysisData) { throw new Error("Profile analysis could not be generated."); } // 3. Build search queries const queries = buildSearchQueries(analysisData, profile.business_idea); // 4. Scrape Google News via Apify const rawArticles = await scrapeGoogleNews(queries, 30); if (rawArticles.length === 0) { const cached = await getCachedArticles(profile_id); if (cached.length > 0) { return { source: "cache_fallback", queries_used: queries, article_count: cached.length, articles: cached, message: "Live news fetch returned no items; returning cached articles.", }; } return { source: "live", queries_used: queries, article_count: 0, articles: [], message: "No news articles found for the generated search queries. Check APIFY connectivity/actor config.", }; } // 5. Deduplicate against existing stored URLs const existingUrls = new Set(await getArticleUrls(profile_id)); const newArticles = rawArticles.filter((a) => !existingUrls.has(a.url)); if (newArticles.length === 0) { await touchNewsFetch(profile_id); const cached = await getCachedArticles(profile_id); return { source: "cache_refreshed", article_count: cached.length, articles: cached, message: "All scraped articles already exist in database.", }; } // 6. Generate embeddings for new articles const articleTexts = newArticles.map( (a) => `${a.title}\n${a.description}\n${a.content}`.trim() ); const articleEmbeddings = await generateEmbeddings(articleTexts); // 7. Parse profile embedding let profileEmbedding = profile.embedding; if (typeof profileEmbedding === "string") { try { profileEmbedding = JSON.parse(profileEmbedding); } catch { profileEmbedding = null; } } if (!Array.isArray(profileEmbedding) || profileEmbedding.length === 0) { const seedText = profile.combined_text || profile.business_idea || ""; if (!seedText.trim()) { throw new Error("Profile has no embedding and no text to regenerate embedding."); } profileEmbedding = await generateEmbedding(seedText); } // 8. Compute cosine similarity and rank const scored = newArticles.map((article, idx) => ({ ...article, embedding: articleEmbeddings[idx], similarity_score: cosineSimilarity(profileEmbedding, articleEmbeddings[idx]), })); const relevant = scored .filter((a) => a.similarity_score >= SIMILARITY_THRESHOLD) .sort((a, b) => b.similarity_score - a.similarity_score) .slice(0, TOP_ARTICLES_COUNT); if (relevant.length === 0) { await touchNewsFetch(profile_id); return { source: "live", article_count: 0, articles: [], message: `No articles exceeded similarity threshold (${SIMILARITY_THRESHOLD}).`, }; } // 9. Summarize top articles with Groq const founderContext = `Founder's business: ${profile.business_idea || "N/A"}\nIndustry: ${(analysisData.industry_tags || []).join(", ")}\nInterests: ${(analysisData.interests || []).join(", ")}`; const summarizedArticles = []; for (const article of relevant) { try { const userPrompt = `FOUNDER CONTEXT:\n${founderContext}\n\nARTICLE:\nTitle: ${article.title}\nDescription: ${article.description}\nContent: ${article.content}`; const summary = await callGroq(NEWS_INTELLIGENCE_PROMPT, userPrompt); summarizedArticles.push({ ...article, summarized_output: summary, }); } catch (err) { console.error(`Failed to summarize article "${article.title}":`, err.message); summarizedArticles.push({ ...article, summarized_output: { headline_summary: article.description || article.title, strategic_relevance: "Summary unavailable due to processing error.", category: "MARKET_SIGNAL", action_items: [], relevance_confidence: article.similarity_score, risk_signals: [], opportunity_signals: [], }, }); } } // 10. Store articles const insertRows = summarizedArticles.map((a) => ({ profile_id, title: a.title, description: a.description, content: a.content, url: a.url, embedding: JSON.stringify(a.embedding), similarity_score: a.similarity_score, summarized_output: a.summarized_output, })); await upsertArticles(insertRows); // 11. Update fetch_history await touchNewsFetch(profile_id); // 12. Return intelligence feed const feed = summarizedArticles.map((a) => ({ title: a.title, url: a.url, similarity_score: Math.round(a.similarity_score * 1000) / 1000, summarized_output: a.summarized_output, })); return { source: "live", article_count: feed.length, queries_used: queries, articles: feed, }; } - src/index.js:115-128 (registration)Registration of the 'fetch_personalized_news' tool in the main MCP server setup.
// Tool 3: fetch_personalized_news // ───────────────────────────────────────────────────────────── server.tool( "fetch_personalized_news", "Fetch personalized, strategically-relevant news for a founder. Checks cache freshness first — returns stored articles if <24h old. Otherwise scrapes Google News via Apify, ranks by cosine similarity, summarizes with Groq, and stores results.", { profile_id: z .string() .uuid() .describe("UUID of the profile to fetch news for (must have run analyze_profile first)"), }, async (args) => { try { const result = await fetchPersonalizedNews(args);