Initial
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
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
|
||||
// ---------------------------------------------------------------------------
|
||||
// If you have provisioned a Managed Agent in the Anthropic console, set its
|
||||
// ID here (or via the AGENT_ID env var). When AGENT_ID is present the call
|
||||
// is routed to that agent. Otherwise we fall back to a system-prompt-driven
|
||||
// "virtual agent" that defines the persona inline.
|
||||
const AGENT_ID = process.env.ANTHROPIC_AGENT_ID || 'YOUR_AGENT_ID_HERE'
|
||||
|
||||
const SYSTEM_PROMPT = `You are the "PMC Funder Discovery Agent", an expert
|
||||
fundraising consultant for U.S. public media stations (NPR/PBS member
|
||||
stations, community radio, and independent public broadcasters).
|
||||
|
||||
Your job is to take a brief intake form about a public media station and
|
||||
produce a tailored funding outlook that includes:
|
||||
1. A short profile of the station based on the supplied details.
|
||||
2. 3-5 likely foundation funders that match the station's mission and
|
||||
geography (CPB, Knight, MacArthur, Wyncote, regional community
|
||||
foundations, etc.).
|
||||
3. 2-3 corporate underwriting angles relevant to the station's market.
|
||||
4. Concrete next steps for the development director.
|
||||
|
||||
Write in clear, direct prose. Use Markdown headings and bullet lists. If
|
||||
you need additional information, you may emit a tool_use block requesting
|
||||
it; otherwise produce the brief in one pass. Avoid speculation about
|
||||
specific dollar amounts.`
|
||||
|
||||
const MODEL = process.env.ANTHROPIC_MODEL || 'claude-sonnet-4-5'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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 {
|
||||
userName = '',
|
||||
userEmail = '',
|
||||
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 })
|
||||
}
|
||||
|
||||
if (!process.env.ANTHROPIC_API_KEY) {
|
||||
return new Response('Server is missing ANTHROPIC_API_KEY', { 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}
|
||||
- Submitter Email: ${userEmail}
|
||||
- Station Name: ${stationName}
|
||||
- Station Location: ${stationLocation}
|
||||
- Station Website: ${stationWebsite}
|
||||
|
||||
Please draft the funding outlook brief described in your instructions.`
|
||||
|
||||
// ----- Build the streaming request -----
|
||||
// Note: when calling a Managed Agent, the Anthropic SDK accepts an
|
||||
// `agent_id` field on the messages.stream() call. We include it
|
||||
// conditionally so the same code path also works with a plain system
|
||||
// prompt while you're wiring things up.
|
||||
const streamArgs = {
|
||||
model: MODEL,
|
||||
max_tokens: 10000,
|
||||
system: SYSTEM_PROMPT,
|
||||
messages: [{ role: 'user', content: userMessage }],
|
||||
}
|
||||
if (AGENT_ID && AGENT_ID !== 'YOUR_AGENT_ID_HERE') {
|
||||
streamArgs.agent_id = AGENT_ID
|
||||
}
|
||||
|
||||
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',
|
||||
}
|
||||
Reference in New Issue
Block a user