fix(canvas): 在线 Agent - 系统模型接入工具任务路由
This commit is contained in:
@@ -8,6 +8,7 @@ import { modelDisplayName, modelIcon, normalizeModelOptionValue, resolveModelCha
|
||||
import { canvasThemes } from "@/lib/canvas-theme";
|
||||
import { nanoid } from "nanoid";
|
||||
import { requestToolResponse, type ResponseFunctionTool, type ResponseInputMessage, type ResponseToolCall } from "@/services/api/image";
|
||||
import { logicalModelIDForConfig, runBackendToolGenerationTask } from "@/services/api/generation-task";
|
||||
import { imageToDataUrl } from "@/services/image-storage";
|
||||
import { isCanvasGenerationDurableAckError, persistCanvasCinematicSessionContinuationEffect } from "@/services/canvas-generation-consumer";
|
||||
import { consumeGenerationTaskAgent } from "@/services/project-asset-sync";
|
||||
@@ -376,6 +377,7 @@ export function CanvasAssistantPanel({
|
||||
const hasMessages = messages.length > 0;
|
||||
const agentBusy = isRunning || safeSessions.some((session) => session.pendingBackendSession?.status === "pending");
|
||||
const activeModel = effectiveConfig.textModel || effectiveConfig.model;
|
||||
const activeModelName = activeModel ? modelDisplayName(effectiveConfig, activeModel) : "";
|
||||
const selectedNodeKey = useMemo(() => Array.from(selectedNodeIds).sort().join(","), [selectedNodeIds]);
|
||||
const allSelectedReferences = useMemo(() => buildAssistantReferences(nodes, selectedNodeIds), [nodes, selectedNodeIds]);
|
||||
const selectedReferences = useMemo(() => allSelectedReferences.filter((item) => !removedReferenceIds.has(item.id)), [allSelectedReferences, removedReferenceIds]);
|
||||
@@ -606,17 +608,10 @@ export function CanvasAssistantPanel({
|
||||
const messages = await buildToolAgentMessages(snapshotRef.current, history, userMessage);
|
||||
addOnlineLog(`Agent Tool Loop ${loop.step} 开始`, { toolChoice: "required" });
|
||||
let streamed = "";
|
||||
const result = await requestToolResponse(
|
||||
{ ...requestConfig, systemPrompt: "" },
|
||||
messages,
|
||||
ONLINE_AGENT_TOOLS,
|
||||
"required",
|
||||
(text) => {
|
||||
streamed = text;
|
||||
if (text.trim()) upsertMessage(sessionId, { id: assistantId, role: "assistant", text });
|
||||
},
|
||||
{ promptCacheKey: canvasAgentPromptCacheKey(sessionId) },
|
||||
);
|
||||
const result = await requestOnlineAgentModel({ ...requestConfig, systemPrompt: "" }, messages, "required", userMessage.text, (text) => {
|
||||
streamed = text;
|
||||
if (text.trim()) upsertMessage(sessionId, { id: assistantId, role: "assistant", text });
|
||||
}, canvasAgentPromptCacheKey(sessionId));
|
||||
addOnlineLog("模型工具回复", result);
|
||||
if (result.toolCalls.length) {
|
||||
const writableCalls = result.toolCalls.filter(isWritableToolCall);
|
||||
@@ -672,17 +667,10 @@ export function CanvasAssistantPanel({
|
||||
}
|
||||
const requestConfig = { ...effectiveConfig, model: effectiveConfig.textModel || effectiveConfig.model };
|
||||
let streamed = "";
|
||||
const next = await requestToolResponse(
|
||||
{ ...requestConfig, systemPrompt: "" },
|
||||
nextMessages,
|
||||
ONLINE_AGENT_TOOLS,
|
||||
"auto",
|
||||
(text) => {
|
||||
streamed = text;
|
||||
if (text.trim()) upsertMessage(sessionId, { id: assistantId, role: "assistant", text });
|
||||
},
|
||||
{ promptCacheKey: canvasAgentPromptCacheKey(sessionId) },
|
||||
);
|
||||
const next = await requestOnlineAgentModel({ ...requestConfig, systemPrompt: "" }, nextMessages, "auto", "继续处理画布工具结果", (text) => {
|
||||
streamed = text;
|
||||
if (text.trim()) upsertMessage(sessionId, { id: assistantId, role: "assistant", text });
|
||||
}, canvasAgentPromptCacheKey(sessionId));
|
||||
addOnlineLog(`Agent Tool Loop ${step + 1} 回复`, next);
|
||||
if (next.toolCalls.length) {
|
||||
const writableCalls = next.toolCalls.filter(isWritableToolCall);
|
||||
@@ -1019,7 +1007,7 @@ export function CanvasAssistantPanel({
|
||||
/>
|
||||
|
||||
{view === "setup" ? (
|
||||
<OnlineAgentSetupView theme={theme} activeModel={activeModel} onOpenConfig={() => navigateToSettings({ continueCreation: true })} />
|
||||
<OnlineAgentSetupView theme={theme} activeModel={activeModelName} onOpenConfig={() => navigateToSettings({ continueCreation: true })} />
|
||||
) : (
|
||||
<div ref={chatListRef} className="thin-scrollbar min-h-0 flex-1 space-y-4 overflow-y-auto px-4 py-4">
|
||||
{view === "history" ? (
|
||||
@@ -1036,7 +1024,7 @@ export function CanvasAssistantPanel({
|
||||
<OnlineAgentLogView
|
||||
logs={onlineLogs}
|
||||
theme={theme}
|
||||
context={{ model: activeModel, running: agentBusy, confirmTools, messages: messages.length, nodes: snapshot.nodes.length, connections: snapshot.connections.length }}
|
||||
context={{ model: activeModelName, running: agentBusy, confirmTools, messages: messages.length, nodes: snapshot.nodes.length, connections: snapshot.connections.length }}
|
||||
onClear={() => setOnlineLogs([])}
|
||||
/>
|
||||
) : messages.length ? (
|
||||
@@ -1590,6 +1578,15 @@ function toolCallToResponseInput(call: ResponseToolCall): ResponseInputMessage {
|
||||
return { type: "function_call", call_id: call.id, name: call.function.name, arguments: call.function.arguments, ...(call.thoughtSignature ? { thoughtSignature: call.thoughtSignature } : {}) };
|
||||
}
|
||||
|
||||
async function requestOnlineAgentModel(config: AiConfig, messages: ResponseInputMessage[], toolChoice: "auto" | "required", prompt: string, onDelta: (text: string) => void, promptCacheKey: string) {
|
||||
if (logicalModelIDForConfig(config)) {
|
||||
const result = await runBackendToolGenerationTask({ prompt, config, messages, tools: ONLINE_AGENT_TOOLS, toolChoice });
|
||||
if (result.content.trim()) onDelta(result.content);
|
||||
return result;
|
||||
}
|
||||
return requestToolResponse(config, messages, ONLINE_AGENT_TOOLS, toolChoice, onDelta, { promptCacheKey });
|
||||
}
|
||||
|
||||
function summarizeToolCalls(calls: ResponseToolCall[]) {
|
||||
return calls.map((call) => toolCallLabel(call.function.name)).join(",") || "工具调用";
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { modelOptionName, resolveModelChannel, resolveModelRequestConfig, type A
|
||||
import { useLocalDreaminaModelStore } from "@/stores/use-local-dreamina-model-store";
|
||||
import type { ReferenceImage } from "@/types/image";
|
||||
import type { ReferenceAudio, ReferenceVideo } from "@/types/media";
|
||||
import { buildBackendToolRequests, type ResponseFunctionTool, type ResponseInputMessage, type ToolChoice, type ToolResponseResult } from "@/services/api/image";
|
||||
|
||||
export type BackendGenerationMode = "text" | "image" | "video" | "audio";
|
||||
|
||||
@@ -19,6 +20,8 @@ export type BackendGenerationResult = {
|
||||
video?: { dataUrl: string; storageKey?: string; width?: number; height?: number; durationMs?: number; bytes?: number; mimeType?: string };
|
||||
audio?: { dataUrl: string; storageKey?: string; durationMs?: number; bytes?: number; mimeType?: string; format?: string };
|
||||
text?: string;
|
||||
toolCalls?: Array<{ id: string; type: "function"; function: { name: string; arguments: string }; thoughtSignature?: string }>;
|
||||
reasoning?: string;
|
||||
};
|
||||
|
||||
type BackendGenerationTaskOptions = {
|
||||
@@ -105,6 +108,40 @@ export async function runBackendGenerationTask(
|
||||
return createAndWaitGenerationTask({ projectId, mode, prompt, config, referenceImages, referenceVideos, referenceAudios, textHistory, signal, metadata, onTaskUpdate }, prepared, dependencies);
|
||||
}
|
||||
|
||||
export async function runBackendToolGenerationTask(options: {
|
||||
prompt: string;
|
||||
config: AiConfig;
|
||||
messages: ResponseInputMessage[];
|
||||
tools: ResponseFunctionTool[];
|
||||
toolChoice: ToolChoice;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<ToolResponseResult> {
|
||||
throwIfAborted(options.signal);
|
||||
const logicalModelId = logicalModelIDForConfig(options.config);
|
||||
if (!logicalModelId) throw new Error("当前模型不是平台系统模型");
|
||||
const task = await createGenerationTask({
|
||||
type: "canvas_text",
|
||||
operation: "text",
|
||||
prompt: options.prompt,
|
||||
model: options.config.model,
|
||||
logicalModelId,
|
||||
input: {
|
||||
mode: "text",
|
||||
prompt: options.prompt,
|
||||
config: backendProviderConfig(options.config),
|
||||
agentRequests: buildBackendToolRequests(options.messages, options.tools, options.toolChoice),
|
||||
metadata: { source: "canvas-online-agent" },
|
||||
},
|
||||
});
|
||||
const completed = await waitForGenerationTask(task.id, { signal: options.signal, initialTask: task });
|
||||
const result = parseBackendGenerationResult(completed);
|
||||
return {
|
||||
content: result.text || "",
|
||||
toolCalls: result.toolCalls || [],
|
||||
...(result.reasoning ? { reasoning: result.reasoning } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export async function runBackendGenerationTaskBatch(options: BackendGenerationTaskOptions & { count: number }, dependencies: GenerationTaskDependencies = defaultDependencies) {
|
||||
const count = Math.max(1, Math.min(15, Math.floor(Number(options.count)) || 1));
|
||||
throwIfAborted(options.signal);
|
||||
|
||||
@@ -46,7 +46,11 @@ export type ToolResponseResult = {
|
||||
reasoning?: string;
|
||||
};
|
||||
|
||||
type ToolChoice = "auto" | "required" | { type: "function"; name: string };
|
||||
export type ToolChoice = "auto" | "required" | { type: "function"; name: string };
|
||||
export type BackendToolRequests = {
|
||||
responses: Record<string, unknown>;
|
||||
chatCompletion: Record<string, unknown>;
|
||||
};
|
||||
type ResponseMessageContent = AiTextMessage["content"] | string;
|
||||
type ResponseInputContent = { type: "input_text"; text: string } | { type: "input_image"; image_url: string } | { type: "input_file"; filename: string; file_data?: string; file_url?: string };
|
||||
type ResponseInputItem = { role: "system" | "user" | "assistant"; content: string | ResponseInputContent[] } | { type: "function_call"; call_id: string; name: string; arguments: string } | { type: "function_call_output"; call_id: string; output: string };
|
||||
@@ -407,6 +411,23 @@ function toChatCompletionToolChoice(toolChoice: ToolChoice) {
|
||||
return typeof toolChoice === "object" ? { type: "function", function: { name: toolChoice.name } } : toolChoice;
|
||||
}
|
||||
|
||||
export function buildBackendToolRequests(messages: ResponseInputMessage[], tools: ResponseFunctionTool[], toolChoice: ToolChoice): BackendToolRequests {
|
||||
return {
|
||||
responses: {
|
||||
input: toResponseInput(messages),
|
||||
tools: tools.map(toResponseTool),
|
||||
tool_choice: toolChoice,
|
||||
parallel_tool_calls: false,
|
||||
},
|
||||
chatCompletion: {
|
||||
messages: toChatCompletionMessages(messages),
|
||||
tools,
|
||||
tool_choice: toChatCompletionToolChoice(toolChoice),
|
||||
parallel_tool_calls: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function isToolChoiceCompatibilityError(error: unknown) {
|
||||
const message = error instanceof Error ? error.message : "";
|
||||
return /tool[_\s-]?choice|thinking\s+mode/i.test(message);
|
||||
|
||||
Reference in New Issue
Block a user