diff --git a/services/pascal-editor/apps/editor/app/api/agent/chat/route.ts b/services/pascal-editor/apps/editor/app/api/agent/chat/route.ts index 61c9843fe..403d821f3 100644 --- a/services/pascal-editor/apps/editor/app/api/agent/chat/route.ts +++ b/services/pascal-editor/apps/editor/app/api/agent/chat/route.ts @@ -1,5 +1,13 @@ import { createOpenAI } from '@ai-sdk/openai' -import { convertToModelMessages, stepCountIs, streamText, type UIMessage } from 'ai' +import { + convertToModelMessages, + createUIMessageStream, + createUIMessageStreamResponse, + type ModelMessage, + stepCountIs, + streamText, + type UIMessage, +} from 'ai' import { NextResponse } from 'next/server' import { z } from 'zod' import { validatePascalAgentAccess } from '@/lib/pascal-agent-access' @@ -24,7 +32,9 @@ const requestSchema = z.object({ }) const AGENT_ERROR_MESSAGE = 'AI design agent is temporarily unavailable. Please try again.' +const AGENT_NO_MUTATION_MESSAGE = '模型未执行任何建模动作,请重试或换用支持工具调用的多模态模型。' const SCENE_ERROR_MESSAGE = 'The current scene could not be loaded.' +const MAX_MUTATION_CONTINUATIONS = 2 function accessErrorResponse(result: ReturnType) { if (result === 'missing_proxy_secret') { @@ -114,6 +124,11 @@ export async function POST(request: Request) { baseURL: config.baseUrl, name: 'pascal-agent', }) + const initialMessages = convertToModelMessages(parsed.data.messages as UIMessage[]) + const systemPrompt = buildPascalAgentPrompt({ + sceneId: parsed.data.sceneId, + sceneName: session.sceneName, + }) let finalized = false const finalize = async (save: boolean) => { @@ -134,36 +149,91 @@ export async function POST(request: Request) { } } - try { - const result = streamText({ + const runAgent = (messages: ModelMessage[], mutationOnly: boolean) => + streamText({ abortSignal, + ...(mutationOnly + ? { + activeTools: session.mutationToolNames, + toolChoice: 'required' as const, + } + : { + prepareStep: ({ stepNumber }) => + pascalAgentStepPolicy({ + hasMutation: session.hasMutation(), + mutationRequired, + mutationToolNames: session.mutationToolNames, + stepNumber, + }), + }), maxOutputTokens: 1600, - messages: convertToModelMessages(parsed.data.messages as UIMessage[]), + messages, model: provider.chat(config.model), - onAbort: async () => finalize(false), - onError: async () => finalize(false), - onFinish: async () => finalize(session.hasMutation()), - prepareStep: ({ stepNumber }) => - pascalAgentStepPolicy({ - hasMutation: session.hasMutation(), - mutationRequired, - mutationToolNames: session.mutationToolNames, - stepNumber, - }), stopWhen: stepCountIs(config.maxSteps), - system: buildPascalAgentPrompt({ - sceneId: parsed.data.sceneId, - sceneName: session.sceneName, - }), + system: systemPrompt, temperature: 0.2, tools: session.tools, }) - return result.toUIMessageStreamResponse({ - onError: (error) => { - console.error('Pascal agent stream failed.', error) - return AGENT_ERROR_MESSAGE - }, + const streamErrorText = (error: unknown) => { + console.error('Pascal agent stream failed.', error) + return AGENT_ERROR_MESSAGE + } + + const stream = createUIMessageStream({ + execute: async ({ writer }) => { + let messages = initialMessages + let sendStart = true + let finishReason: Awaited['finishReason']> | undefined + + try { + for (let attempt = 0; attempt <= MAX_MUTATION_CONTINUATIONS; attempt++) { + const mutationOnly = attempt > 0 + const result = runAgent(messages, mutationOnly) + for await (const chunk of result.toUIMessageStream({ + onError: streamErrorText, + sendFinish: false, + sendStart, + })) { + writer.write(chunk) + } + + finishReason = await result.finishReason + const response = await result.response + if (!mutationRequired || session.hasMutation()) break + + messages = [ + ...messages, + ...response.messages, + { + role: 'user', + content: + 'You only inspected the scene and did not change it. Continue now. Do not explain or finish. Call one of the available scene mutation tools with valid arguments, then verify the persisted result.', + }, + ] + sendStart = false + } + + if (mutationRequired && !session.hasMutation()) { + console.error( + 'Pascal agent exhausted mutation continuations without changing the scene.', + { + sceneId: parsed.data.sceneId, + }, + ) + writer.write({ type: 'error', errorText: AGENT_NO_MUTATION_MESSAGE }) + } + writer.write({ type: 'finish', finishReason }) + } finally { + await finalize(session.hasMutation()) + } + }, + onError: streamErrorText, + }) + + try { + return createUIMessageStreamResponse({ + stream, }) } catch (error) { await finalize(false)