fix: remove agent progress bubble and add dialogue memory
This commit is contained in:
parent
6a2b928080
commit
c17de9def5
@ -250,6 +250,7 @@ import {
|
||||
pruneTaskBoundaryContext,
|
||||
pruneTaskBoundaryHistoryMessages,
|
||||
} from "@/lib/ai/runtime/task-boundary-context-pruner";
|
||||
import { buildRecentDialogueMemory } from "@/lib/ai/runtime/recent-dialogue-memory";
|
||||
import {
|
||||
resolveAgentModelSlotDescriptor,
|
||||
upsertAgentModelSlot,
|
||||
@ -1085,6 +1086,10 @@ export async function POST(request: Request) {
|
||||
let confirmedPromptForConversationState: string | undefined;
|
||||
let smartAutoHistoryMessagesFromDb: DBMessage[] = [];
|
||||
let smartAutoMediaHistoryMessagesFromDb: DBMessage[] = [];
|
||||
let recentDialogueMemory = buildRecentDialogueMemory({
|
||||
messages: [],
|
||||
currentText: requestTextContent,
|
||||
});
|
||||
let preferredAgentModelId: string | undefined;
|
||||
let configuredAgentConversationModelId: string | undefined;
|
||||
let agentModelSlots: AgentModelSlots = {};
|
||||
@ -1489,6 +1494,17 @@ export async function POST(request: Request) {
|
||||
taskType: taskBoundaryDecision.taskType,
|
||||
taskMode: taskBoundaryDecision.taskMode,
|
||||
});
|
||||
recentDialogueMemory = buildRecentDialogueMemory({
|
||||
messages: loadedHistoryMessages,
|
||||
currentText: smartAutoInputText,
|
||||
maxEntries: taskBoundaryDecision.taskType === "chat" ? 6 : 4,
|
||||
maxTotalChars: taskBoundaryDecision.taskType === "chat" ? 1000 : 700,
|
||||
});
|
||||
chatApiLogger.info("[RecentDialogueMemory] Smart auto recall:", {
|
||||
entries: recentDialogueMemory.entries.length,
|
||||
isRecallRequest: recentDialogueMemory.isRecallRequest,
|
||||
taskType: taskBoundaryDecision.taskType,
|
||||
});
|
||||
|
||||
autoTextForFallback = smartAutoInputText;
|
||||
if (!requestWorkingState && !shouldIsolateDirectComposerContext) {
|
||||
@ -1563,23 +1579,12 @@ export async function POST(request: Request) {
|
||||
resolutionConfidence: requestTargetResolution.confidence,
|
||||
});
|
||||
|
||||
// 🆕 从历史消息中提取对话文本(用于上下文理解)
|
||||
const historyMessages: { role: "user" | "assistant"; content: string }[] = [];
|
||||
for (const msg of smartAutoHistoryMessagesFromDb.slice(-10)) { // 最多取最近10条
|
||||
const parts = msg.parts as any[];
|
||||
if (parts && parts.length > 0) {
|
||||
// 提取文本内容
|
||||
const textParts = parts.filter((p: any) => p.type === 'text');
|
||||
const content = textParts.map((p: any) => p.text || '').join(' ').trim();
|
||||
if (content) {
|
||||
historyMessages.push({
|
||||
role: msg.role as "user" | "assistant",
|
||||
content: content.substring(0, 300), // 限制长度
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
chatApiLogger.info("[Smart Auto Mode] History context:", historyMessages.length, "messages");
|
||||
const historyMessages: { role: "user" | "assistant"; content: string }[] =
|
||||
recentDialogueMemory.entries.map((entry) => ({
|
||||
role: entry.role,
|
||||
content: entry.content,
|
||||
}));
|
||||
chatApiLogger.info("[Smart Auto Mode] History context:", historyMessages.length, "memory entries");
|
||||
|
||||
// 2. 使用 LangGraph 驱动的自动模式工作流
|
||||
const { runAutoAgentGraph } = await import("@/lib/ai/auto-agent-graph");
|
||||
@ -1612,8 +1617,15 @@ export async function POST(request: Request) {
|
||||
workflowState: composedConversationState.threadState.workflowState,
|
||||
memoryHints: composedConversationState.threadState.memoryHints,
|
||||
contextSummary: composedConversationState.threadState.contextSummary,
|
||||
recentDialogueMemory: recentDialogueMemory.contextSummary,
|
||||
},
|
||||
});
|
||||
const agentGraphContext = [
|
||||
composedConversationState.composedConversationMemory.contextSummary,
|
||||
recentDialogueMemory.contextSummary,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
const autoGraphResult = await withOperationTimeout(
|
||||
runAutoAgentGraph({
|
||||
text: smartAutoInputText,
|
||||
@ -1641,10 +1653,7 @@ export async function POST(request: Request) {
|
||||
requestResolvedMediaInput.historyUserImageUrls.length,
|
||||
currentUploadImageCount: requestResolvedMediaInput.currentMessageImageUrls.length,
|
||||
historyMessages,
|
||||
context:
|
||||
composedConversationState.composedConversationMemory.contextSummary ||
|
||||
effectiveAutoContextText ||
|
||||
undefined,
|
||||
context: agentGraphContext || effectiveAutoContextText || undefined,
|
||||
preferredModelId: preferredAgentModelId,
|
||||
threadState: composedConversationState.threadState,
|
||||
}),
|
||||
@ -2747,6 +2756,21 @@ export async function POST(request: Request) {
|
||||
purpose: "media",
|
||||
hasCurrentImageInput: hasImageInputForConversationAction,
|
||||
});
|
||||
recentDialogueMemory = shouldIsolateDirectComposerContext
|
||||
? recentDialogueMemory
|
||||
: buildRecentDialogueMemory({
|
||||
messages: loadedMessagesForBoundary,
|
||||
currentText: requestTextContent,
|
||||
maxEntries: taskBoundaryDecision.taskType === "chat" ? 6 : 4,
|
||||
maxTotalChars: taskBoundaryDecision.taskType === "chat" ? 1000 : 700,
|
||||
});
|
||||
if (recentDialogueMemory.entries.length > 0) {
|
||||
chatApiLogger.info("[RecentDialogueMemory] Request recall:", {
|
||||
entries: recentDialogueMemory.entries.length,
|
||||
isRecallRequest: recentDialogueMemory.isRecallRequest,
|
||||
taskType: taskBoundaryDecision.taskType,
|
||||
});
|
||||
}
|
||||
|
||||
if (!requestWorkingState && !shouldIsolateDirectComposerContext) {
|
||||
requestWorkingState = await getConversationWorkingStateByChatId({
|
||||
@ -8591,16 +8615,33 @@ export async function POST(request: Request) {
|
||||
resolvedRuntimeModelLookupId = actualModelId;
|
||||
}
|
||||
|
||||
const effectiveConversationSystemPrompt = shouldRunAgentConversation
|
||||
? `${await buildAgentRoleIdentityInstruction({
|
||||
const recentDialogueSystemContext = recentDialogueMemory.contextSummary
|
||||
? [
|
||||
recentDialogueMemory.contextSummary,
|
||||
...recentDialogueMemory.memoryHints,
|
||||
].join("\n")
|
||||
: "";
|
||||
const baseConversationSystemPrompt = systemPrompt({
|
||||
selectedChatModel: finalModelId,
|
||||
requestHints,
|
||||
});
|
||||
const agentIdentityInstruction = shouldRunAgentConversation
|
||||
? await buildAgentRoleIdentityInstruction({
|
||||
allowVideoGeneration: allowAgentVideoGeneration,
|
||||
hasCanvasContext: Boolean(
|
||||
effectiveContextPayload?.source === "canvas" ||
|
||||
effectiveContextPayload?.selectedNodeIds?.length ||
|
||||
effectiveContextPayload?.canvasImageNodes?.length,
|
||||
),
|
||||
})}\n\n${systemPrompt({ selectedChatModel: finalModelId, requestHints })}`
|
||||
: systemPrompt({ selectedChatModel: finalModelId, requestHints });
|
||||
})
|
||||
: "";
|
||||
const effectiveConversationSystemPrompt = [
|
||||
agentIdentityInstruction,
|
||||
recentDialogueSystemContext,
|
||||
baseConversationSystemPrompt,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n");
|
||||
|
||||
const activeRuntimeModelDescriptor = await resolveAgentModelSlotDescriptor(
|
||||
resolvedRuntimeModelLookupId,
|
||||
|
||||
@ -1636,130 +1636,6 @@ function CanvasAssistantTypingPlaceholder({ hint }: { hint?: string | null }) {
|
||||
);
|
||||
}
|
||||
|
||||
function CanvasAgentProgressTimeline({
|
||||
progress,
|
||||
}: {
|
||||
progress?: AgentProgressStreamPayload | null;
|
||||
}) {
|
||||
const steps = buildAgentProgressNodeSteps(progress);
|
||||
const title = progress?.title?.trim() || getAgentProgressFallbackTitle(progress);
|
||||
const isFailed = progress?.stage === "failed";
|
||||
const isCompleted = progress?.stage === "completed";
|
||||
const isClarify = progress?.stage === "clarify";
|
||||
|
||||
return (
|
||||
<div className="flex w-full justify-start">
|
||||
<div
|
||||
aria-label={title}
|
||||
className={cn(
|
||||
"w-full max-w-[min(92%,860px)] rounded-[22px] border px-4 py-3 shadow-[0_16px_44px_-32px_rgba(15,23,42,0.65)] backdrop-blur-sm",
|
||||
isFailed
|
||||
? "border-rose-300/40 bg-rose-50/70 dark:border-rose-400/20 dark:bg-rose-950/20"
|
||||
: isClarify
|
||||
? "border-amber-300/40 bg-amber-50/70 dark:border-amber-400/20 dark:bg-amber-950/20"
|
||||
: "border-[#d8deea]/70 bg-white/72 dark:border-white/10 dark:bg-white/6"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-full",
|
||||
isFailed
|
||||
? "bg-rose-500/12 text-rose-600 dark:text-rose-300"
|
||||
: isClarify
|
||||
? "bg-amber-500/12 text-amber-600 dark:text-amber-300"
|
||||
: isCompleted
|
||||
? "bg-emerald-500/12 text-emerald-600 dark:text-emerald-300"
|
||||
: "bg-sky-500/12 text-sky-600 dark:text-sky-300"
|
||||
)}
|
||||
>
|
||||
{isFailed ? (
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
) : isCompleted ? (
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
) : (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
)}
|
||||
</span>
|
||||
<span className="text-[15px] font-medium text-[#5d6675] dark:text-zinc-300">
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 overflow-x-auto pb-1 scrollbar-hide">
|
||||
<div className="flex min-w-max items-center">
|
||||
{steps.map((step, index) => {
|
||||
const isStepCompleted = step.state === "completed";
|
||||
const isStepCurrent = step.state === "current";
|
||||
const isStepFailed = step.state === "failed";
|
||||
const lineIsActive =
|
||||
isStepCompleted || isStepCurrent || isStepFailed;
|
||||
|
||||
return (
|
||||
<div key={step.key} className="flex items-center">
|
||||
<div className="flex min-w-max items-center gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
"relative inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full border-2 transition-colors",
|
||||
isStepFailed
|
||||
? "border-rose-500 bg-rose-500 text-white"
|
||||
: isStepCompleted
|
||||
? "border-emerald-500 bg-emerald-500 text-white"
|
||||
: isStepCurrent
|
||||
? "border-sky-500 bg-white text-sky-500 dark:bg-[#111318]"
|
||||
: "border-[#cfd6e2] bg-white text-transparent dark:border-white/18 dark:bg-white/8"
|
||||
)}
|
||||
>
|
||||
{isStepCompleted ? (
|
||||
<Check className="h-2.5 w-2.5" />
|
||||
) : isStepFailed ? (
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-current" />
|
||||
) : isStepCurrent ? (
|
||||
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-current" />
|
||||
) : (
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-current" />
|
||||
)}
|
||||
{isStepCurrent ? (
|
||||
<span className="absolute inset-[-5px] rounded-full border border-sky-400/35" />
|
||||
) : null}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"whitespace-nowrap text-[14px] font-medium leading-5",
|
||||
isStepFailed
|
||||
? "text-rose-600 dark:text-rose-300"
|
||||
: isStepCompleted
|
||||
? "text-emerald-600 dark:text-emerald-300"
|
||||
: isStepCurrent
|
||||
? "text-sky-600 dark:text-sky-300"
|
||||
: "text-[#9aa3b2] dark:text-zinc-500"
|
||||
)}
|
||||
>
|
||||
{step.label}
|
||||
</span>
|
||||
</div>
|
||||
{index < steps.length - 1 ? (
|
||||
<span
|
||||
className={cn(
|
||||
"mx-3 h-px w-9 shrink-0 transition-colors",
|
||||
lineIsActive
|
||||
? isStepFailed
|
||||
? "bg-rose-300 dark:bg-rose-400/35"
|
||||
: "bg-sky-300 dark:bg-sky-400/35"
|
||||
: "bg-[#d8dee9] dark:bg-white/10"
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CanvasComposerUploadProgressDialog({
|
||||
progress,
|
||||
}: {
|
||||
@ -13672,10 +13548,6 @@ export function CanvasChatPanel({
|
||||
<CanvasAssistantTypingPlaceholder hint={clawdProcessingHint} />
|
||||
) : null}
|
||||
|
||||
{isLoading && !shouldRenderImageLoading && (
|
||||
<CanvasAgentProgressTimeline progress={activeAgentProgress} />
|
||||
)}
|
||||
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
146
lib/ai/runtime/recent-dialogue-memory.ts
Normal file
146
lib/ai/runtime/recent-dialogue-memory.ts
Normal file
@ -0,0 +1,146 @@
|
||||
import type { DBMessage } from "@/lib/db/schema";
|
||||
|
||||
export interface RecentDialogueMemoryEntry {
|
||||
role: "user" | "assistant";
|
||||
content: string;
|
||||
messageId?: string;
|
||||
}
|
||||
|
||||
export interface RecentDialogueMemory {
|
||||
entries: RecentDialogueMemoryEntry[];
|
||||
contextSummary: string;
|
||||
memoryHints: string[];
|
||||
isRecallRequest: boolean;
|
||||
}
|
||||
|
||||
const USER_RECALL_PATTERN =
|
||||
/(?:我|用户)?(?:刚才|刚刚|前面|之前|上面|上一(?:句|条|轮)|最近).{0,12}(?:说|发|问|讲|提|输入|发送).{0,12}(?:什么|啥|哪句|哪些|内容)|(?:我说了什么|我刚才说啥|我前面说啥|我之前说啥|我发了什么|我问了什么|记得我.{0,12}说|没有记忆)/i;
|
||||
|
||||
const ASSISTANT_RECALL_PATTERN =
|
||||
/(?:你|助手|ai|AI).{0,12}(?:刚才|刚刚|前面|之前|上面|上一(?:句|条|轮)|最近).{0,12}(?:说|回复|回答|讲).{0,12}(?:什么|啥|哪句|哪些|内容)/i;
|
||||
|
||||
function normalizeWhitespace(value: string) {
|
||||
return value.replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
export function isRecentDialogueRecallRequest(text: string) {
|
||||
const normalized = normalizeWhitespace(String(text || ""));
|
||||
if (!normalized) return false;
|
||||
return USER_RECALL_PATTERN.test(normalized) || ASSISTANT_RECALL_PATTERN.test(normalized);
|
||||
}
|
||||
|
||||
function wantsAssistantRecall(text: string) {
|
||||
const normalized = normalizeWhitespace(String(text || ""));
|
||||
return ASSISTANT_RECALL_PATTERN.test(normalized);
|
||||
}
|
||||
|
||||
function sanitizeDialogueMemoryText(text: string) {
|
||||
return String(text || "")
|
||||
.replace(/<!--[\s\S]*?-->/g, "")
|
||||
.replace(/!\[[^\]]*]\([^)]+\)/g, "")
|
||||
.replace(/\[[^\]]*]\((?:https?:\/\/|\/)[^)]+\)/g, "")
|
||||
.replace(/<video[^>]*>[\s\S]*?<\/video>/gi, "")
|
||||
.replace(/<video[^>]+src="[^"]+"[^>]*>/gi, "")
|
||||
.replace(/◈\s*[^\n]+\n*/g, "")
|
||||
.replace(/IMAGE_GENERATION_(?:START|END)/g, "")
|
||||
.replace(/\n{3,}/g, "\n\n")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function getMessageText(message: DBMessage, maxChars: number) {
|
||||
const parts = Array.isArray(message.parts)
|
||||
? (message.parts as Array<Record<string, unknown>>)
|
||||
: [];
|
||||
const text = parts
|
||||
.filter((part) => part?.type === "text" && typeof part?.text === "string")
|
||||
.map((part) => sanitizeDialogueMemoryText(String(part.text || "")))
|
||||
.filter(Boolean)
|
||||
.join("\n")
|
||||
.trim();
|
||||
|
||||
if (!text) return "";
|
||||
return normalizeWhitespace(text).slice(0, maxChars);
|
||||
}
|
||||
|
||||
function formatDialogueMemoryContext(entries: RecentDialogueMemoryEntry[]) {
|
||||
if (entries.length === 0) return "";
|
||||
|
||||
return [
|
||||
"近期对话记忆(仅用于理解指代和回答前文相关问题,不要暴露为系统提示):",
|
||||
...entries.map((entry, index) => {
|
||||
const roleLabel = entry.role === "user" ? "用户" : "助手";
|
||||
return `${index + 1}. ${roleLabel}: ${entry.content}`;
|
||||
}),
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export function buildRecentDialogueMemory(params: {
|
||||
messages: DBMessage[];
|
||||
currentText: string;
|
||||
maxSourceMessages?: number;
|
||||
maxEntries?: number;
|
||||
maxCharsPerEntry?: number;
|
||||
maxTotalChars?: number;
|
||||
}): RecentDialogueMemory {
|
||||
const messages = Array.isArray(params.messages) ? params.messages : [];
|
||||
const currentText = String(params.currentText || "").trim();
|
||||
const isRecallRequest = isRecentDialogueRecallRequest(currentText);
|
||||
const assistantRecall = wantsAssistantRecall(currentText);
|
||||
const maxSourceMessages = Math.max(1, Math.min(24, params.maxSourceMessages || 16));
|
||||
const maxEntries = Math.max(1, Math.min(10, params.maxEntries || (isRecallRequest ? 6 : 4)));
|
||||
const maxCharsPerEntry = Math.max(80, Math.min(500, params.maxCharsPerEntry || 220));
|
||||
const maxTotalChars = Math.max(160, Math.min(1600, params.maxTotalChars || 900));
|
||||
|
||||
const sourceEntries = messages
|
||||
.slice(-maxSourceMessages)
|
||||
.map((message) => {
|
||||
const role = message.role === "assistant" ? "assistant" : message.role === "user" ? "user" : null;
|
||||
if (!role) return null;
|
||||
const content = getMessageText(message, maxCharsPerEntry);
|
||||
if (!content) return null;
|
||||
return {
|
||||
role,
|
||||
content,
|
||||
messageId: message.id,
|
||||
} satisfies RecentDialogueMemoryEntry;
|
||||
})
|
||||
.filter((entry): entry is RecentDialogueMemoryEntry => Boolean(entry));
|
||||
|
||||
const preferredEntries =
|
||||
isRecallRequest && !assistantRecall
|
||||
? sourceEntries.filter((entry) => entry.role === "user")
|
||||
: isRecallRequest && assistantRecall
|
||||
? sourceEntries.filter((entry) => entry.role === "assistant")
|
||||
: sourceEntries;
|
||||
const fallbackEntries =
|
||||
preferredEntries.length > 0 ? preferredEntries : sourceEntries;
|
||||
|
||||
const selectedReversed: RecentDialogueMemoryEntry[] = [];
|
||||
let totalChars = 0;
|
||||
for (let index = fallbackEntries.length - 1; index >= 0; index -= 1) {
|
||||
const entry = fallbackEntries[index];
|
||||
if (!entry) continue;
|
||||
if (selectedReversed.length >= maxEntries) break;
|
||||
if (totalChars + entry.content.length > maxTotalChars && selectedReversed.length > 0) {
|
||||
continue;
|
||||
}
|
||||
selectedReversed.push(entry);
|
||||
totalChars += entry.content.length;
|
||||
}
|
||||
|
||||
const entries = selectedReversed.reverse();
|
||||
const memoryHints = entries.length
|
||||
? [
|
||||
isRecallRequest
|
||||
? "用户正在询问前文或记忆,应优先根据近期对话记忆回答,不要声称没有上下文。"
|
||||
: "近期对话记忆只用于理解指代和延续语境,不要逐字复述,除非用户明确询问前文。",
|
||||
]
|
||||
: [];
|
||||
|
||||
return {
|
||||
entries,
|
||||
contextSummary: formatDialogueMemoryContext(entries),
|
||||
memoryHints,
|
||||
isRecallRequest,
|
||||
};
|
||||
}
|
||||
@ -52,6 +52,10 @@ import {
|
||||
resolveCanvasAgentTargetPreference,
|
||||
} from "@/lib/ai/runtime/canvas-agent-kernel";
|
||||
import { inferAgentImageCountFromText } from "@/lib/ai/runtime/image-count-intent";
|
||||
import {
|
||||
buildRecentDialogueMemory,
|
||||
isRecentDialogueRecallRequest,
|
||||
} from "@/lib/ai/runtime/recent-dialogue-memory";
|
||||
import {
|
||||
isAgentModeScopedChatId,
|
||||
isScopedEmptyHistoryRequest,
|
||||
@ -221,6 +225,19 @@ import {
|
||||
import type { PickedObjectAttachment } from "@/components/canvas/chat-panel/canvas-chat-session";
|
||||
import type { CanvasAgentContext } from "@/types/canvas/agent-protocol";
|
||||
import type { ChatMessage } from "@/lib/types";
|
||||
import type { DBMessage } from "@/lib/db/schema";
|
||||
|
||||
function dbTextMessage(
|
||||
id: string,
|
||||
role: "user" | "assistant",
|
||||
text: string,
|
||||
): DBMessage {
|
||||
return {
|
||||
id,
|
||||
role,
|
||||
parts: [{ type: "text", text }],
|
||||
} as unknown as DBMessage;
|
||||
}
|
||||
|
||||
test.describe("canvas chat pure helpers", () => {
|
||||
test("keeps optimistic messages without timestamps after persisted history", () => {
|
||||
@ -309,6 +326,45 @@ test.describe("canvas chat pure helpers", () => {
|
||||
expect(upstream).toContain("| SKU | 销量 |");
|
||||
});
|
||||
|
||||
test("builds bounded recent dialogue memory for user recall requests", () => {
|
||||
const memory = buildRecentDialogueMemory({
|
||||
messages: [
|
||||
dbTextMessage("m1", "user", "你好"),
|
||||
dbTextMessage("m2", "assistant", "你好,我在。"),
|
||||
dbTextMessage("m3", "user", "你是一个小可爱"),
|
||||
dbTextMessage("m4", "assistant", "嘿嘿,谢谢你的夸奖。"),
|
||||
],
|
||||
currentText: "我前面说了什么?",
|
||||
});
|
||||
|
||||
expect(isRecentDialogueRecallRequest("我前面说了什么?")).toBe(true);
|
||||
expect(memory.isRecallRequest).toBe(true);
|
||||
expect(memory.entries.map((entry) => entry.role)).toEqual(["user", "user"]);
|
||||
expect(memory.entries.at(-1)?.content).toBe("你是一个小可爱");
|
||||
expect(memory.contextSummary).toContain("用户: 你是一个小可爱");
|
||||
expect(memory.memoryHints.join(" ")).toContain("不要声称没有上下文");
|
||||
});
|
||||
|
||||
test("sanitizes media markup from recent dialogue memory", () => {
|
||||
const memory = buildRecentDialogueMemory({
|
||||
messages: [
|
||||
dbTextMessage("m1", "user", "生成一只小猫"),
|
||||
dbTextMessage(
|
||||
"m2",
|
||||
"assistant",
|
||||
"<!-- IMAGE_GENERATION_START -->\n\n已完成本次生成"
|
||||
),
|
||||
],
|
||||
currentText: "你刚才回复了什么?",
|
||||
});
|
||||
|
||||
expect(memory.entries).toHaveLength(1);
|
||||
expect(memory.entries[0].role).toBe("assistant");
|
||||
expect(memory.entries[0].content).toBe("已完成本次生成");
|
||||
expect(memory.contextSummary).not.toContain("https://example.com");
|
||||
expect(memory.contextSummary).not.toContain("IMAGE_GENERATION_START");
|
||||
});
|
||||
|
||||
test("infers explicit agent image counts from user text", () => {
|
||||
expect(inferAgentImageCountFromText("再出三版")).toBe(3);
|
||||
expect(inferAgentImageCountFromText("基于这张图再来 3 张")).toBe(3);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user