import Anthropic from '@anthropic-ai/sdk' /** * Netlify Functions v2 handler. * * Proxies a request from the React frontend to the Anthropic Messages API, * configured for a Managed Agent workflow. We pipe `content_block_delta` * events directly to the client through a ReadableStream so we never hit * Netlify's 26-second synchronous execution limit. */ // --------------------------------------------------------------------------- // Managed Agent configuration // --------------------------------------------------------------------------- // The Managed Agent ID is supplied via the ANTHROPIC_AGENT_ID env var. The // agent's system prompt and model are defined in the Claude Console and used // by default. ANTHROPIC_MODEL and ANTHROPIC_SYSTEM_PROMPT may optionally // override them. // --------------------------------------------------------------------------- // Anthropic client (reused across warm Lambda invocations) // --------------------------------------------------------------------------- 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, }) return _client } // --------------------------------------------------------------------------- // Handler // --------------------------------------------------------------------------- export default async (req /*, context */) => { if (req.method !== 'POST') { return new Response('Method Not Allowed', { status: 405 }) } let payload try { payload = await req.json() } catch { return new Response('Invalid JSON body', { status: 400 }) } const { stationName = '', stationLocation = '', stationWebsite = '', } = payload || {} // Server-side URL validation (mirrors the client-side regex). const URL_REGEX = /^(https?:\/\/)?([\w-]+\.)+[\w-]{2,}(\/[\w\-._~:/?#[\]@!$&'()*+,;=%]*)?$/i if (!URL_REGEX.test(stationWebsite)) { return new Response('Invalid station website URL', { status: 400 }) } let client try { client = getClient() } catch (err) { return new Response( `Failed to initialize agent: ${err.message || err}`, { status: 500 }, ) } const userMessage = `Here is a new public media station intake: - Station Name: ${stationName} - Station Location: ${stationLocation} - Station Website: ${stationWebsite} Please follow your instructions to produce the funding outlook brief.` // ----- Build the streaming request ----- // The Managed Agent's system prompt and model (configured in the Claude // Console) are used by default. Optional env vars can override them. const streamArgs = { model: process.env.ANTHROPIC_MODEL || 'claude-opus-4-6', max_tokens: 600000, messages: [ { role: 'user', content: [{ type: 'text', text: userMessage }], }, ], } if (process.env.ANTHROPIC_AGENT_ID) { streamArgs.agent_id = process.env.ANTHROPIC_AGENT_ID } if (process.env.ANTHROPIC_SYSTEM_PROMPT) { streamArgs.system = process.env.ANTHROPIC_SYSTEM_PROMPT } const upstream = client.messages.stream(streamArgs) // ----- Pipe content_block_delta events straight to the client ----- const encoder = new TextEncoder() const stream = new ReadableStream({ async start(controller) { try { for await (const event of upstream) { if ( event.type === 'content_block_delta' && event.delta?.type === 'text_delta' && event.delta.text ) { controller.enqueue(encoder.encode(event.delta.text)) } // Surface an explicit message if the agent decides to call a tool. // (Managed Agents may emit `input_json_delta` or `tool_use` blocks.) if ( event.type === 'content_block_delta' && event.delta?.type === 'input_json_delta' && event.delta.partial_json ) { controller.enqueue( encoder.encode( `\n\n_[agent requested tool input: ${event.delta.partial_json}]_\n\n`, ), ) } } controller.close() } catch (err) { controller.enqueue( encoder.encode(`\n\n[stream error: ${err.message || err}]`), ) controller.close() } }, cancel() { // If the client disconnects, abort the upstream Anthropic request. upstream.controller?.abort?.() }, }) return new Response(stream, { status: 200, headers: { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'no-cache, no-transform', 'X-Accel-Buffering': 'no', }, }) } export const config = { path: '/.netlify/functions/agent-proxy', }