ai-chatbot/lib/ai/conversation-memory-composer.ts
Your Name 9ecd91376d
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: strengthen agent runtime boundaries
2026-03-16 11:45:00 +08:00

215 lines
7.5 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 { AgentThreadState } from "@/lib/ai/runtime/agent-thread-state";
import type { ImageWorkflowMemory } from "@/lib/ai/image-workflow-memory";
import type { PromptMemoryEntry } from "@/lib/ai/prompt-memory";
import {
applyContextLineLimit,
applyMemoryHintLimit,
type MemoryRelevancePolicy,
} from "@/lib/ai/runtime/memory-relevance";
import type { StoryboardPreferenceMemory } from "@/lib/video-agent/storyboard-preference-memory";
import type {
ResolvedChatMediaInput,
ResolvedTargetAssets,
} from "@/lib/ai/context-memory/types";
export interface ComposedConversationMemory {
contextSummary: string;
memoryHints: string[];
metadata: Record<string, unknown>;
}
function normalizeText(value: unknown, max = 400): string | undefined {
const normalized = String(value || "").trim();
if (!normalized) return undefined;
return normalized.slice(0, max);
}
function uniqueStrings(values: Array<string | undefined>) {
return Array.from(
new Set(
values
.map((item) => normalizeText(item, 240))
.filter((item): item is string => Boolean(item))
)
);
}
function summarizeTargetResolution(target?: ResolvedTargetAssets | null) {
if (!target) return { lines: [] as string[], metadata: {} as Record<string, unknown> };
const lines: string[] = [];
lines.push(
`当前目标解析:mode=${target.resolutionMode}; confidence=${target.confidence}; fromHistory=${
target.fromHistory ? "yes" : "no"
}`
);
if (target.targetImageUrls.length > 0) {
lines.push(`当前命中的目标图片数:${target.targetImageUrls.length}`);
}
if (target.reason) {
lines.push(`目标解析原因:${target.reason}`);
}
return {
lines,
metadata: {
targetResolutionMode: target.resolutionMode,
targetResolutionConfidence: target.confidence,
targetImageCount: target.targetImageUrls.length,
targetReason: target.reason,
},
};
}
function summarizeResolvedMedia(media?: ResolvedChatMediaInput | null) {
if (!media) return { lines: [] as string[], metadata: {} as Record<string, unknown> };
return {
lines: [
`当前媒体输入:source=${media.source}; hasImage=${media.hasImage ? "yes" : "no"}`,
media.currentMessageImageUrls.length > 0
? `本轮上传图片数:${media.currentMessageImageUrls.length}`
: "",
media.selectedImageUrls.length > 0
? `当前选中图片数:${media.selectedImageUrls.length}`
: "",
media.historyAssistantImageUrls.length + media.historyUserImageUrls.length > 0
? `历史可参考图片数:${
media.historyAssistantImageUrls.length + media.historyUserImageUrls.length
}`
: "",
].filter(Boolean),
metadata: {
mediaSource: media.source,
hasImage: media.hasImage,
currentImageCount: media.currentMessageImageUrls.length,
selectedImageCount: media.selectedImageUrls.length,
historyImageCount:
media.historyAssistantImageUrls.length + media.historyUserImageUrls.length,
},
};
}
function summarizePromptMemory(promptMemory?: PromptMemoryEntry | null) {
if (!promptMemory?.optimizedPrompt) {
return { lines: [] as string[], hints: [] as string[], metadata: {} as Record<string, unknown> };
}
const preview = promptMemory.optimizedPrompt.replace(/\s+/g, " ").trim().slice(0, 220);
return {
lines: [`上一轮可复用提示词:${preview}`],
hints: [
"如果用户明确要求沿用上一轮提示词继续做图,优先把这段提示词作为本轮生成基础。",
],
metadata: {
promptMemory,
},
};
}
function summarizeImageWorkflow(memory?: ImageWorkflowMemory | null) {
if (!memory) {
return { lines: [] as string[], hints: [] as string[], metadata: {} as Record<string, unknown> };
}
const lines = [
memory.lastOptimizedPrompt
? `上一轮优化提示词摘要:${memory.lastOptimizedPrompt.replace(/\s+/g, " ").slice(0, 180)}`
: "",
memory.lastRequestedImageCount
? `上一轮目标出图数:${memory.lastRequestedImageCount} 张`
: "",
memory.lastAspectRatio ? `上一轮画幅:${memory.lastAspectRatio}` : "",
memory.lastImageSize ? `上一轮清晰度:${memory.lastImageSize}` : "",
memory.lastSourceImageUrls?.length
? `上一轮参考/目标图数量:${memory.lastSourceImageUrls.length}`
: "",
memory.lastResultImageUrls?.length
? `上一轮生成结果数量:${memory.lastResultImageUrls.length}`
: "",
].filter(Boolean);
const hints = memory.lastOptimizedPrompt
? ["如果用户要求沿用上一轮提示词继续做图,应优先复用这段提示词。"]
: [];
return {
lines,
hints,
metadata: {
imageWorkflowMemory: memory,
},
};
}
function summarizeStoryboardPreference(memory?: StoryboardPreferenceMemory | null) {
if (!memory) {
return { lines: [] as string[], hints: [] as string[], metadata: {} as Record<string, unknown> };
}
const lines = [
memory.preferredGridType ? `用户偏好宫格:${memory.preferredGridType}` : "",
memory.preferredShotCount ? `用户偏好镜头数:${memory.preferredShotCount}` : "",
memory.prefersConfirmation === true ? "用户偏好先确认再执行" : "",
memory.lastUsedGridType && memory.lastUsedShotCount
? `最近一次分镜任务:${memory.lastUsedGridType} / ${memory.lastUsedShotCount} 段`
: "",
].filter(Boolean);
const hints = memory.prefersConfirmation
? ["如果当前任务涉及分镜或高成本执行,优先先确认再推进。"]
: [];
return {
lines,
hints,
metadata: {
storyboardPreferenceMemory: memory,
},
};
}
export function composeConversationMemory(params: {
autoContextText?: string;
resolvedMediaInput?: ResolvedChatMediaInput | null;
targetResolution?: ResolvedTargetAssets | null;
promptMemory?: PromptMemoryEntry | null;
imageWorkflowMemory?: ImageWorkflowMemory | null;
storyboardPreferenceMemory?: StoryboardPreferenceMemory | null;
threadState?: AgentThreadState | null;
memoryRelevancePolicy?: MemoryRelevancePolicy;
}) {
const targetSummary = summarizeTargetResolution(params.targetResolution);
const mediaSummary = summarizeResolvedMedia(params.resolvedMediaInput);
const promptSummary = summarizePromptMemory(params.promptMemory);
const imageWorkflowSummary = summarizeImageWorkflow(params.imageWorkflowMemory);
const storyboardSummary = summarizeStoryboardPreference(params.storyboardPreferenceMemory);
const contextSummary = uniqueStrings([
params.autoContextText,
...mediaSummary.lines,
...targetSummary.lines,
...promptSummary.lines,
...imageWorkflowSummary.lines,
...storyboardSummary.lines,
]).join("\n");
const memoryHints = uniqueStrings([
...(params.threadState?.memoryHints || []),
...promptSummary.hints,
...imageWorkflowSummary.hints,
...storyboardSummary.hints,
]);
const metadata: Record<string, unknown> = {
...(params.threadState?.metadata || {}),
...mediaSummary.metadata,
...targetSummary.metadata,
...promptSummary.metadata,
...imageWorkflowSummary.metadata,
...storyboardSummary.metadata,
};
return {
contextSummary: params.memoryRelevancePolicy
? applyContextLineLimit(contextSummary, params.memoryRelevancePolicy.maxContextLines)
: contextSummary,
memoryHints: params.memoryRelevancePolicy
? applyMemoryHintLimit(memoryHints, params.memoryRelevancePolicy.maxMemoryHints)
: memoryHints,
metadata,
} satisfies ComposedConversationMemory;
}