From a9873b56342b0bbecc54d457c0c9fc5bb4c44e46 Mon Sep 17 00:00:00 2001 From: Alex Curley Date: Mon, 11 May 2026 23:21:45 -0400 Subject: [PATCH] CSV handling --- netlify/functions/agent-proxy.js | 81 ++++++++++++++++++++++++++++++-- 1 file changed, 76 insertions(+), 5 deletions(-) diff --git a/netlify/functions/agent-proxy.js b/netlify/functions/agent-proxy.js index 43a0a4a..9c8eff5 100644 --- a/netlify/functions/agent-proxy.js +++ b/netlify/functions/agent-proxy.js @@ -38,6 +38,49 @@ specific dollar amounts.` const MODEL = process.env.ANTHROPIC_MODEL || 'claude-sonnet-4-5' +// File uploaded to the Anthropic Console / Files API that the agent should +// consult on every call. +const KNOWLEDGE_FILE_ID = + process.env.ANTHROPIC_KNOWLEDGE_FILE_ID || 'file_011Cawv1SPNaknuMmyDtM1S7' + +// --------------------------------------------------------------------------- +// Module-scoped singletons (reused across warm Lambda invocations) +// --------------------------------------------------------------------------- +// Netlify keeps the function process alive between invocations as long as it +// stays warm, so anything declared at module scope persists. We lazily build +// the Anthropic client and download the knowledge file exactly once per +// cold start, then reuse them for the lifetime of the container. + +let _client +function getClient() { + if (_client) return _client + if (!process.env.ANTHROPIC_API_KEY) { + throw new Error('Server is missing ANTHROPIC_API_KEY') + } + _client = new Anthropic({ + apiKey: process.env.ANTHROPIC_API_KEY, + // Required while the Files API is in beta. + defaultHeaders: { 'anthropic-beta': 'files-api-2025-04-14' }, + }) + return _client +} + +// Cache the in-flight promise so concurrent requests during a cold start +// share a single download instead of fetching the CSV N times. +let _knowledgeFilePromise +function loadKnowledgeFile() { + if (_knowledgeFilePromise) return _knowledgeFilePromise + _knowledgeFilePromise = (async () => { + const resp = await getClient().beta.files.download(KNOWLEDGE_FILE_ID) + return await resp.text() + })().catch((err) => { + // Reset on failure so the next request can retry. + _knowledgeFilePromise = undefined + throw err + }) + return _knowledgeFilePromise +} + // --------------------------------------------------------------------------- // Handler // --------------------------------------------------------------------------- @@ -68,12 +111,20 @@ export default async (req /*, context */) => { return new Response('Invalid station website URL', { status: 400 }) } - if (!process.env.ANTHROPIC_API_KEY) { - return new Response('Server is missing ANTHROPIC_API_KEY', { status: 500 }) + let client + let knowledgeFileText + try { + client = getClient() + // First call after a cold start awaits the download; subsequent calls + // resolve immediately from the cached promise. + knowledgeFileText = await loadKnowledgeFile() + } catch (err) { + return new Response( + `Failed to initialize agent: ${err.message || err}`, + { status: 500 }, + ) } - const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }) - const userMessage = `Here is a new public media station intake: - Submitter Name: ${userName} @@ -93,7 +144,27 @@ Please draft the funding outlook brief described in your instructions.` model: MODEL, max_tokens: 10000, system: SYSTEM_PROMPT, - messages: [{ role: 'user', content: userMessage }], + messages: [ + { + role: 'user', + content: [ + { + type: 'document', + source: { + type: 'text', + media_type: 'text/plain', + data: knowledgeFileText, + }, + title: 'Funder knowledge base (CSV)', + citations: { enabled: true }, + // Cache the tokenized CSV on Anthropic's side for ~5 minutes + // so repeat requests pay ~10% of input-token cost for it. + cache_control: { type: 'ephemeral' }, + }, + { type: 'text', text: userMessage }, + ], + }, + ], } if (AGENT_ID && AGENT_ID !== 'YOUR_AGENT_ID_HERE') { streamArgs.agent_id = AGENT_ID