ai-chatbot/lib/ai/runtime/intent-arbiter.ts
Your Name dd4426ded7
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
fix: improve agent image intent handling
2026-05-11 20:31:17 +08:00

395 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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";
export type IntentArbiterMode =
| "respond"
| "clarify"
| "analyze_image"
| "generate_image"
| "edit_image"
| "generate_video";
export type IntentArbiterPreferredCapability =
| "respond"
| "analyze"
| "generic_edit"
| "remove_background"
| "enhance"
| "extend"
| "resize"
| "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 {
const normalizedText = String(text || "").toLowerCase();
return /抠图|去背景|透明背景|去背|扣图|移除背景|换背景|改背景|背景换成|背景改成|白底|纯色背景|替换.*背景|变清晰|高清|锐化|降噪|提升质量|修复|清晰化|扩展|拓展|延伸|扩图|outpainting|拓宽|扩大|换成.*(?:比例|竖版|横版|竖图|横图|9:16|16:9|1:1)|改成.*(?:比例|竖版|横版|竖图|横图|9:16|16:9|1:1)|比例|画幅|竖版|横版|去除|消除|擦掉|删除|移除|p掉|重绘|风格化|卡通化|动漫风|油画风|转.*风格|变.*风格|改.*风格|调色|调整颜色|亮度|对比度|饱和度|色调|曝光|滤镜|改成.*色|换成.*色|变成.*色|颜色改成|颜色换成|颜色变成|缩放|放大|缩小|调整尺寸|裁剪|resize|crop|修图|p图|改图|调图|编辑.*图|优化.*图|重做|再调整|继续改|继续调/.test(
normalizedText,
);
}
function inferDefaultImageEditSubIntent(text: string): string {
const lowerText = String(text || "").toLowerCase();
if (/抠图|去背景|透明背景|去背|扣图|移除背景/.test(lowerText)) return "remove_background";
if (/换背景|改背景|背景换成|背景改成|白底|透明底/.test(lowerText)) return "replace_background";
if (/扩展|拓展|延伸|扩图|outpainting|拓宽|扩大|比例|画幅|竖版|横版|竖图|横图|9:16|16:9|1:1/.test(lowerText)) return "extend";
if (/风格|卡通|动漫|油画|像素风|写实|插画/.test(lowerText)) return "style_transfer";
if (/颜色|调色|亮度|对比度|饱和度|改成.*色|换成.*色/.test(lowerText)) return "color_adjust";
if (/去除|消除|擦掉|删除|移除|p掉|重绘/.test(lowerText)) return "inpaint";
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 "extend":
return "extend";
case "resize":
return "resize";
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);
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: 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",
};
}
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":
if (hasUsableImage && likelyEdit && !explicitImageGen) {
return {
intent: "image_edit",
subIntent: params.analysis.subIntent || inferDefaultImageEditSubIntent(params.text),
reason: "task_boundary_execution_image_edit",
};
}
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",
};
}
if (hasUsableImage && likelyEdit && params.analysis.intent === "chat") {
return {
intent: "image_edit",
subIntent: params.analysis.subIntent || inferDefaultImageEditSubIntent(params.text),
reason: "image_edit_policy_override",
};
}
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,
};
}