158 lines
4.7 KiB
JavaScript
158 lines
4.7 KiB
JavaScript
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',
|
|
}
|