import { createOpenAI } from '@ai-sdk/openai' import { convertToModelMessages, stepCountIs, streamText, type UIMessage } from 'ai' import { NextResponse } from 'next/server' import { z } from 'zod' import { validatePascalAgentAccess } from '@/lib/pascal-agent-access' import { getPascalAgentConfig } from '@/lib/pascal-agent-config' import { createPascalAgentMcpSession } from '@/lib/pascal-agent-mcp' import { buildPascalAgentPrompt } from '@/lib/pascal-agent-prompt' import { getPascalHostIdentity } from '@/lib/pascal-host-identity' import { getSceneOperations } from '@/lib/scene-store-server' export const dynamic = 'force-dynamic' export const maxDuration = 600 export const runtime = 'nodejs' const requestSchema = z.object({ messages: z.array(z.unknown()).min(1).max(100), sceneId: z .string() .min(1) .max(64) .regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/), }) const AGENT_ERROR_MESSAGE = 'AI design agent is temporarily unavailable. Please try again.' const SCENE_ERROR_MESSAGE = 'The current scene could not be loaded.' function accessErrorResponse(result: ReturnType) { if (result === 'missing_proxy_secret') { return NextResponse.json({ error: 'agent_proxy_not_configured' }, { status: 503 }) } if (result === 'unauthorized') { return NextResponse.json({ error: 'unauthorized' }, { status: 401 }) } return null } export function GET(request: Request) { const accessError = accessErrorResponse(validatePascalAgentAccess(request.headers)) if (accessError) return accessError const identity = getPascalHostIdentity(request.headers) if (!identity.allowed) { return NextResponse.json({ error: 'host_user_required' }, { status: 401 }) } const config = getPascalAgentConfig() return NextResponse.json({ configured: config.configured, maxSteps: config.maxSteps, model: config.model, }) } export async function POST(request: Request) { const accessError = accessErrorResponse(validatePascalAgentAccess(request.headers)) if (accessError) return accessError const identity = getPascalHostIdentity(request.headers) if (!identity.allowed) { return NextResponse.json({ error: 'host_user_required' }, { status: 401 }) } const config = getPascalAgentConfig() if (!config.configured) { return NextResponse.json( { error: 'agent_not_configured', message: 'Configure PASCAL_AGENT_API_KEY before using the AI design agent.', }, { status: 503 }, ) } let body: unknown try { body = await request.json() } catch { return NextResponse.json({ error: 'invalid_json' }, { status: 400 }) } const parsed = requestSchema.safeParse(body) if (!parsed.success) { return NextResponse.json( { error: 'invalid_request', details: parsed.error.issues }, { status: 400 }, ) } const operations = await getSceneOperations() const storedScene = await operations.loadStoredScene(parsed.data.sceneId) if (!storedScene || (identity.userId && storedScene.ownerId !== identity.userId)) { return NextResponse.json( { error: 'scene_unavailable', message: SCENE_ERROR_MESSAGE }, { status: 404 }, ) } const abortSignal = AbortSignal.any([request.signal, AbortSignal.timeout(config.totalTimeoutMs)]) let session: Awaited> try { session = await createPascalAgentMcpSession({ abortSignal, sceneId: parsed.data.sceneId, stepTimeoutMs: config.stepTimeoutMs, }) } catch (error) { console.error('Pascal agent could not load the requested scene.', error) return NextResponse.json( { error: 'scene_unavailable', message: SCENE_ERROR_MESSAGE }, { status: 404 }, ) } const provider = createOpenAI({ apiKey: config.apiKey, baseURL: config.baseUrl, name: 'pascal-agent', }) let finalized = false const finalize = async (save: boolean) => { if (finalized) return finalized = true try { if (save && !abortSignal.aborted) { await session.saveDraft(abortSignal) } } finally { await session.close() } } try { const result = streamText({ abortSignal, maxOutputTokens: 1600, messages: convertToModelMessages(parsed.data.messages as UIMessage[]), model: provider.chat(config.model), onAbort: async () => finalize(false), onError: async () => finalize(false), onFinish: async () => finalize(true), stopWhen: stepCountIs(config.maxSteps), system: buildPascalAgentPrompt({ sceneId: parsed.data.sceneId, sceneName: session.sceneName, }), temperature: 0.2, tools: session.tools, }) return result.toUIMessageStreamResponse({ onError: (error) => { console.error('Pascal agent stream failed.', error) return AGENT_ERROR_MESSAGE }, }) } catch (error) { await finalize(false) console.error('Pascal agent failed before streaming.', error) return NextResponse.json( { error: 'agent_failed', message: AGENT_ERROR_MESSAGE }, { status: 500 }, ) } }