ai-chatbot/lib/ai/runtime/intent-arbiter.ts
2026-07-07 21:39:56 +08:00

408 lines
14 KiB
TypeScript

import "server-only";
import type { ConversationActionResolution } from "@/lib/ai/conversation-action-resolver";
import type { ResolvedTargetAssets } from "@/lib/ai/context-memory/types";
import type { AnalysisResult } from "@/lib/ai/smart-analyzer";
import type { TaskBoundaryDecision } from "@/lib/ai/runtime/task-boundary-classifier";
import { hasDirectAgentLocalEditIntent } from "@/lib/ai/runtime/local-edit-options";
import {
hasCanvasAgentActionableImageEditIntent,
inferCanvasAgentImageEditCapability,
} from "@/lib/ai/runtime/canvas-agent-kernel";
export type IntentArbiterMode =
| "respond"
| "clarify"
| "analyze_image"
| "generate_image"
| "edit_image"
| "generate_video";
export type IntentArbiterPreferredCapability =
| "respond"
| "analyze"
| "generic_edit"
| "remove_background"
| "remove_watermark"
| "enhance"
| "extend"
| "resize"
| "angle_variant"
| "local_edit"
| "box_cutout"
| "generate_image"
| "generate_video"
| "publish_canvas";
export interface IntentArbiterExecution {
mode: IntentArbiterMode;
routeIntent: AnalysisResult["intent"];
routeSubIntent?: string;
targetSource: ResolvedTargetAssets["resolutionMode"];
preferredCapability: IntentArbiterPreferredCapability;
shouldUseStructuredReply: boolean;
shouldOptimizePrompt: boolean;
shouldApplyImagePipeline: boolean;
shouldApplyVideoPipeline: boolean;
shouldStayConversational: boolean;
shouldPublishToCanvas: boolean;
toolCalls: Array<{ name: string; reason?: string; args?: Record<string, unknown> }>;
availableTools: string[];
}
export interface IntentArbiterResult {
analysis: AnalysisResult;
execution: IntentArbiterExecution;
overridden: boolean;
reason: string;
friendlyMessage?: string;
}
function normalizeCompactText(text: string): string {
return String(text || "").toLowerCase().replace(/\s+/g, "").trim();
}
function isLikelyAdvisoryOrPlanningRequest(text: string): boolean {
const normalizedText = String(text || "").toLowerCase();
return /怎么|如何|为什么|是否|可不可以|能不能|行不行|建议|思路|方案|流程|步骤|教程|推荐|对比|区别|差别|优缺点|评估|分析|判断|解释|讲讲|科普|适合吗|可行吗|值不值得|要不要|帮我看看|帮我分析|帮我判断/.test(
normalizedText,
);
}
function isExplicitVideoGenerationRequest(text: string): boolean {
const normalizedText = normalizeCompactText(text);
if (!normalizedText) return false;
const hasStrongVideoMarkers =
/图生视频|文生视频|首帧|尾帧|分镜|运镜|视频生成/.test(normalizedText);
if (hasStrongVideoMarkers) return true;
if (isLikelyAdvisoryOrPlanningRequest(text)) return false;
return (
/让.+动起来/.test(normalizedText) ||
(/(生成|制作|做|创建|输出|渲染|合成|出|做成|变成|转成)/.test(normalizedText) &&
/(视频|动画|短片|片子|动起来|会动)/.test(normalizedText))
);
}
function isExplicitImageGenerationRequest(text: string): boolean {
const normalizedText = normalizeCompactText(text);
if (!normalizedText || isLikelyAdvisoryOrPlanningRequest(text)) return false;
if (/不要生图|先别生图|不需要生图|不要生成|别生成|只要提示词|仅给提示词|先给提示词/.test(normalizedText)) {
return false;
}
const countToken = "(?:一|两|二|三|四|五|六|七|八|九|十|[1-9]\\d?)";
const visualTarget =
"(?:图|图片|海报|头像|插画|场景|主图|产品图|商品图|电商图|壁纸|封面|logo|图标)";
const createVisualTarget = new RegExp(
`(?:生成|创作|创建|制作|设计|做|出|画(?!布)|绘制)${countToken}?张?.{0,32}${visualTarget}`,
);
const drawSubject = new RegExp(
`(?:画(?!布)|绘制|生成|创作|创建)${countToken}?(?:只|个|位|幅|张)?.{0,24}(?:小猫|猫咪|猫|狗|人物|角色|ip|头像|logo|图标|插画)`,
);
const continueVariants = new RegExp(
`(?:再来|再出|再做|再给我|补|多来|继续生成|继续出)${countToken}?张`,
);
return (
/重新生成|生成新图|新建图片|创作一张|生成一张|画一张|给我画|帮我画|出一张|做一张图|做一张海报|创建一张图片|生成一只|生成一个|生成一幅|生成一版|生成一套|生成一组|生图|出图/.test(
normalizedText,
) ||
createVisualTarget.test(normalizedText) ||
drawSubject.test(normalizedText) ||
continueVariants.test(normalizedText) ||
/参考.*生成|按这个风格生成|以这张图为灵感生成|仿照.*生成|同风格再来|同样风格再来/.test(
normalizedText,
) ||
/帮我生成.*(?:图|图片|海报|头像|插画|场景|小猫|猫咪|人物|产品图)|给我生成.*(?:图|图片|海报|头像|插画|场景|小猫|猫咪|人物|产品图)/.test(
normalizedText,
)
);
}
function isLikelyImageEditRequest(text: string): boolean {
return hasDirectAgentLocalEditIntent(text) || hasCanvasAgentActionableImageEditIntent(text);
}
function inferDefaultImageEditSubIntent(text: string): string {
const inferredCapability = inferCanvasAgentImageEditCapability(text);
if (
inferredCapability &&
inferredCapability !== "generic_edit" &&
inferredCapability !== "generate_image" &&
inferredCapability !== "generate_video" &&
inferredCapability !== "analyze"
) {
return inferredCapability;
}
if (hasDirectAgentLocalEditIntent(text)) return "local_edit";
return "enhance";
}
function mapIntentToMode(
intent: AnalysisResult["intent"],
needsClarification?: boolean,
): IntentArbiterMode {
if (needsClarification) return "clarify";
switch (intent) {
case "image_edit":
return "edit_image";
case "image_gen":
return "generate_image";
case "video_gen":
return "generate_video";
case "vision":
return "analyze_image";
case "chat":
default:
return "respond";
}
}
function mapPreferredCapability(
intent: AnalysisResult["intent"],
subIntent?: string,
): IntentArbiterPreferredCapability {
if (intent === "video_gen") return "generate_video";
if (intent === "image_gen") return "generate_image";
if (intent === "vision") return "analyze";
if (intent === "chat") return "respond";
switch (subIntent) {
case "remove_background":
return "remove_background";
case "remove_watermark":
return "remove_watermark";
case "extend":
return "extend";
case "resize":
return "resize";
case "angle_variant":
case "generate_angle_variant":
return "angle_variant";
case "local_edit":
case "inpaint_region":
return "local_edit";
case "box_cutout":
return "box_cutout";
case "enhance":
return "enhance";
default:
return "generic_edit";
}
}
function buildExecutionFromAnalysis(params: {
analysis: AnalysisResult;
execution?: IntentArbiterExecution | null;
targetSource: ResolvedTargetAssets["resolutionMode"];
}): IntentArbiterExecution {
const mode = mapIntentToMode(params.analysis.intent, params.analysis.needsClarification);
const shouldKeepPlannedToolCalls =
params.execution?.mode === mode &&
params.execution?.routeIntent === params.analysis.intent;
return {
mode,
routeIntent: params.analysis.intent,
routeSubIntent: params.analysis.subIntent,
targetSource: params.targetSource,
preferredCapability: mapPreferredCapability(params.analysis.intent, params.analysis.subIntent),
shouldUseStructuredReply:
params.analysis.intent === "image_edit" ||
params.analysis.intent === "image_gen" ||
params.analysis.intent === "video_gen",
shouldOptimizePrompt:
params.analysis.intent === "image_edit" ||
params.analysis.intent === "image_gen" ||
params.analysis.intent === "video_gen",
shouldApplyImagePipeline:
params.analysis.intent === "image_edit" || params.analysis.intent === "image_gen",
shouldApplyVideoPipeline: params.analysis.intent === "video_gen",
shouldStayConversational:
mode === "respond" || mode === "analyze_image" || mode === "clarify",
shouldPublishToCanvas: params.execution?.shouldPublishToCanvas || false,
toolCalls: shouldKeepPlannedToolCalls ? params.execution?.toolCalls || [] : [],
availableTools: params.execution?.availableTools || [],
};
}
function buildDefaultFriendlyMessage(
intent: AnalysisResult["intent"],
params: { hasImage: boolean; text: string },
) {
switch (intent) {
case "image_gen":
return params.hasImage ? "收到,我会结合你提供的参考图直接执行生图。" : "好的,我来直接帮你生成这张图。";
case "image_edit":
return params.hasImage ? "收到,我会按你的目标直接处理这张图。" : "收到,我来继续执行这轮图片处理。";
case "video_gen":
return params.hasImage ? "收到,我会结合这轮图片先规划并执行视频生成。" : "收到,我先按你的需求规划并执行视频生成。";
case "vision":
return params.hasImage ? "收到,我先分析这张图。" : "收到,我先按你的问题做分析。";
case "chat":
default:
return undefined;
}
}
function resolvePolicyIntent(params: {
text: string;
hasImage: boolean;
hasHistoryImage: boolean;
taskBoundaryDecision: TaskBoundaryDecision;
actionResolution: ConversationActionResolution;
analysis: AnalysisResult;
}): { intent: AnalysisResult["intent"]; subIntent?: string; reason: string } {
const explicitImageGen = isExplicitImageGenerationRequest(params.text);
const explicitVideoGen = isExplicitVideoGenerationRequest(params.text);
const likelyEdit = isLikelyImageEditRequest(params.text);
const hasUsableImage = params.hasImage || params.hasHistoryImage;
if (explicitVideoGen) {
return {
intent: "video_gen",
reason: "explicit_video_generation_request",
};
}
if (hasUsableImage && likelyEdit) {
return {
intent: "image_edit",
subIntent: params.analysis.subIntent || inferDefaultImageEditSubIntent(params.text),
reason: "image_edit_policy_override",
};
}
switch (params.actionResolution.kind) {
case "image_edit_followup":
return {
intent: "image_edit",
subIntent: params.analysis.subIntent || inferDefaultImageEditSubIntent(params.text),
reason: "conversation_action_image_edit_followup",
};
case "image_generation_followup":
case "prompt_carry_forward":
return {
intent: "image_gen",
reason: "conversation_action_image_generation_followup",
};
default:
break;
}
switch (params.taskBoundaryDecision.taskType) {
case "prompt_only":
case "analysis_only":
return {
intent: "vision",
reason: `task_boundary_${params.taskBoundaryDecision.taskType}`,
};
case "execution_video":
return {
intent: "video_gen",
reason: "task_boundary_execution_video",
};
case "execution_image":
return {
intent: "image_gen",
reason: "task_boundary_execution_image_generate",
};
case "workflow_followup":
if (hasUsableImage && likelyEdit) {
return {
intent: "image_edit",
subIntent: params.analysis.subIntent || inferDefaultImageEditSubIntent(params.text),
reason: "task_boundary_workflow_followup_edit",
};
}
if (explicitImageGen) {
return {
intent: "image_gen",
reason: "task_boundary_workflow_followup_generate",
};
}
break;
default:
break;
}
if (explicitImageGen) {
return {
intent: "image_gen",
reason: "explicit_image_generation_request",
};
}
return {
intent: params.analysis.intent,
subIntent:
params.analysis.intent === "image_edit"
? params.analysis.subIntent || inferDefaultImageEditSubIntent(params.text)
: params.analysis.subIntent,
reason: "keep_ai_analysis",
};
}
export function arbitrateAgentIntent(params: {
text: string;
hasImage: boolean;
hasHistoryImage: boolean;
actionResolution: ConversationActionResolution;
taskBoundaryDecision: TaskBoundaryDecision;
analysis: AnalysisResult;
execution?: IntentArbiterExecution | null;
targetSource: ResolvedTargetAssets["resolutionMode"];
}): IntentArbiterResult {
const resolved = resolvePolicyIntent(params);
const nextAnalysis: AnalysisResult = {
...params.analysis,
intent: resolved.intent,
subIntent:
resolved.intent === "image_edit"
? resolved.subIntent || inferDefaultImageEditSubIntent(params.text)
: undefined,
confidence:
resolved.reason === "keep_ai_analysis"
? params.analysis.confidence
: Math.max(params.analysis.confidence, 0.84),
needsClarification:
resolved.reason === "keep_ai_analysis"
? Boolean(params.analysis.needsClarification)
: false,
clarifyingQuestions:
resolved.reason === "keep_ai_analysis" && params.analysis.needsClarification
? params.analysis.clarifyingQuestions || []
: [],
};
const nextExecution = buildExecutionFromAnalysis({
analysis: nextAnalysis,
execution: params.execution,
targetSource: params.targetSource,
});
const overridden =
nextAnalysis.intent !== params.analysis.intent ||
nextAnalysis.subIntent !== params.analysis.subIntent ||
nextExecution.mode !== params.execution?.mode ||
nextExecution.routeIntent !== params.execution?.routeIntent;
const friendlyMessage =
overridden || !params.analysis.conversationalReply?.trim()
? buildDefaultFriendlyMessage(nextAnalysis.intent, {
hasImage: params.hasImage || params.hasHistoryImage,
text: params.text,
})
: undefined;
return {
analysis: friendlyMessage
? {
...nextAnalysis,
conversationalReply: friendlyMessage,
}
: nextAnalysis,
execution: nextExecution,
overridden,
reason: resolved.reason,
friendlyMessage,
};
}