fix: remove agent progress bubble and add dialogue memory
This commit is contained in:
parent
6a2b928080
commit
c17de9def5
@ -250,6 +250,7 @@ import {
|
|||||||
pruneTaskBoundaryContext,
|
pruneTaskBoundaryContext,
|
||||||
pruneTaskBoundaryHistoryMessages,
|
pruneTaskBoundaryHistoryMessages,
|
||||||
} from "@/lib/ai/runtime/task-boundary-context-pruner";
|
} from "@/lib/ai/runtime/task-boundary-context-pruner";
|
||||||
|
import { buildRecentDialogueMemory } from "@/lib/ai/runtime/recent-dialogue-memory";
|
||||||
import {
|
import {
|
||||||
resolveAgentModelSlotDescriptor,
|
resolveAgentModelSlotDescriptor,
|
||||||
upsertAgentModelSlot,
|
upsertAgentModelSlot,
|
||||||
@ -1085,6 +1086,10 @@ export async function POST(request: Request) {
|
|||||||
let confirmedPromptForConversationState: string | undefined;
|
let confirmedPromptForConversationState: string | undefined;
|
||||||
let smartAutoHistoryMessagesFromDb: DBMessage[] = [];
|
let smartAutoHistoryMessagesFromDb: DBMessage[] = [];
|
||||||
let smartAutoMediaHistoryMessagesFromDb: DBMessage[] = [];
|
let smartAutoMediaHistoryMessagesFromDb: DBMessage[] = [];
|
||||||
|
let recentDialogueMemory = buildRecentDialogueMemory({
|
||||||
|
messages: [],
|
||||||
|
currentText: requestTextContent,
|
||||||
|
});
|
||||||
let preferredAgentModelId: string | undefined;
|
let preferredAgentModelId: string | undefined;
|
||||||
let configuredAgentConversationModelId: string | undefined;
|
let configuredAgentConversationModelId: string | undefined;
|
||||||
let agentModelSlots: AgentModelSlots = {};
|
let agentModelSlots: AgentModelSlots = {};
|
||||||
@ -1489,6 +1494,17 @@ export async function POST(request: Request) {
|
|||||||
taskType: taskBoundaryDecision.taskType,
|
taskType: taskBoundaryDecision.taskType,
|
||||||
taskMode: taskBoundaryDecision.taskMode,
|
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;
|
autoTextForFallback = smartAutoInputText;
|
||||||
if (!requestWorkingState && !shouldIsolateDirectComposerContext) {
|
if (!requestWorkingState && !shouldIsolateDirectComposerContext) {
|
||||||
@ -1563,23 +1579,12 @@ export async function POST(request: Request) {
|
|||||||
resolutionConfidence: requestTargetResolution.confidence,
|
resolutionConfidence: requestTargetResolution.confidence,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 🆕 从历史消息中提取对话文本(用于上下文理解)
|
const historyMessages: { role: "user" | "assistant"; content: string }[] =
|
||||||
const historyMessages: { role: "user" | "assistant"; content: string }[] = [];
|
recentDialogueMemory.entries.map((entry) => ({
|
||||||
for (const msg of smartAutoHistoryMessagesFromDb.slice(-10)) { // 最多取最近10条
|
role: entry.role,
|
||||||
const parts = msg.parts as any[];
|
content: entry.content,
|
||||||
if (parts && parts.length > 0) {
|
}));
|
||||||
// 提取文本内容
|
chatApiLogger.info("[Smart Auto Mode] History context:", historyMessages.length, "memory entries");
|
||||||
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");
|
|
||||||
|
|
||||||
// 2. 使用 LangGraph 驱动的自动模式工作流
|
// 2. 使用 LangGraph 驱动的自动模式工作流
|
||||||
const { runAutoAgentGraph } = await import("@/lib/ai/auto-agent-graph");
|
const { runAutoAgentGraph } = await import("@/lib/ai/auto-agent-graph");
|
||||||
@ -1612,8 +1617,15 @@ export async function POST(request: Request) {
|
|||||||
workflowState: composedConversationState.threadState.workflowState,
|
workflowState: composedConversationState.threadState.workflowState,
|
||||||
memoryHints: composedConversationState.threadState.memoryHints,
|
memoryHints: composedConversationState.threadState.memoryHints,
|
||||||
contextSummary: composedConversationState.threadState.contextSummary,
|
contextSummary: composedConversationState.threadState.contextSummary,
|
||||||
|
recentDialogueMemory: recentDialogueMemory.contextSummary,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const agentGraphContext = [
|
||||||
|
composedConversationState.composedConversationMemory.contextSummary,
|
||||||
|
recentDialogueMemory.contextSummary,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n");
|
||||||
const autoGraphResult = await withOperationTimeout(
|
const autoGraphResult = await withOperationTimeout(
|
||||||
runAutoAgentGraph({
|
runAutoAgentGraph({
|
||||||
text: smartAutoInputText,
|
text: smartAutoInputText,
|
||||||
@ -1641,10 +1653,7 @@ export async function POST(request: Request) {
|
|||||||
requestResolvedMediaInput.historyUserImageUrls.length,
|
requestResolvedMediaInput.historyUserImageUrls.length,
|
||||||
currentUploadImageCount: requestResolvedMediaInput.currentMessageImageUrls.length,
|
currentUploadImageCount: requestResolvedMediaInput.currentMessageImageUrls.length,
|
||||||
historyMessages,
|
historyMessages,
|
||||||
context:
|
context: agentGraphContext || effectiveAutoContextText || undefined,
|
||||||
composedConversationState.composedConversationMemory.contextSummary ||
|
|
||||||
effectiveAutoContextText ||
|
|
||||||
undefined,
|
|
||||||
preferredModelId: preferredAgentModelId,
|
preferredModelId: preferredAgentModelId,
|
||||||
threadState: composedConversationState.threadState,
|
threadState: composedConversationState.threadState,
|
||||||
}),
|
}),
|
||||||
@ -2747,6 +2756,21 @@ export async function POST(request: Request) {
|
|||||||
purpose: "media",
|
purpose: "media",
|
||||||
hasCurrentImageInput: hasImageInputForConversationAction,
|
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) {
|
if (!requestWorkingState && !shouldIsolateDirectComposerContext) {
|
||||||
requestWorkingState = await getConversationWorkingStateByChatId({
|
requestWorkingState = await getConversationWorkingStateByChatId({
|
||||||
@ -8591,16 +8615,33 @@ export async function POST(request: Request) {
|
|||||||
resolvedRuntimeModelLookupId = actualModelId;
|
resolvedRuntimeModelLookupId = actualModelId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const effectiveConversationSystemPrompt = shouldRunAgentConversation
|
const recentDialogueSystemContext = recentDialogueMemory.contextSummary
|
||||||
? `${await buildAgentRoleIdentityInstruction({
|
? [
|
||||||
|
recentDialogueMemory.contextSummary,
|
||||||
|
...recentDialogueMemory.memoryHints,
|
||||||
|
].join("\n")
|
||||||
|
: "";
|
||||||
|
const baseConversationSystemPrompt = systemPrompt({
|
||||||
|
selectedChatModel: finalModelId,
|
||||||
|
requestHints,
|
||||||
|
});
|
||||||
|
const agentIdentityInstruction = shouldRunAgentConversation
|
||||||
|
? await buildAgentRoleIdentityInstruction({
|
||||||
allowVideoGeneration: allowAgentVideoGeneration,
|
allowVideoGeneration: allowAgentVideoGeneration,
|
||||||
hasCanvasContext: Boolean(
|
hasCanvasContext: Boolean(
|
||||||
effectiveContextPayload?.source === "canvas" ||
|
effectiveContextPayload?.source === "canvas" ||
|
||||||
effectiveContextPayload?.selectedNodeIds?.length ||
|
effectiveContextPayload?.selectedNodeIds?.length ||
|
||||||
effectiveContextPayload?.canvasImageNodes?.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(
|
const activeRuntimeModelDescriptor = await resolveAgentModelSlotDescriptor(
|
||||||
resolvedRuntimeModelLookupId,
|
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({
|
function CanvasComposerUploadProgressDialog({
|
||||||
progress,
|
progress,
|
||||||
}: {
|
}: {
|
||||||
@ -13672,10 +13548,6 @@ export function CanvasChatPanel({
|
|||||||
<CanvasAssistantTypingPlaceholder hint={clawdProcessingHint} />
|
<CanvasAssistantTypingPlaceholder hint={clawdProcessingHint} />
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{isLoading && !shouldRenderImageLoading && (
|
|
||||||
<CanvasAgentProgressTimeline progress={activeAgentProgress} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div ref={messagesEndRef} />
|
<div ref={messagesEndRef} />
|
||||||
</div>
|
</div>
|
||||||
</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,
|
resolveCanvasAgentTargetPreference,
|
||||||
} from "@/lib/ai/runtime/canvas-agent-kernel";
|
} from "@/lib/ai/runtime/canvas-agent-kernel";
|
||||||
import { inferAgentImageCountFromText } from "@/lib/ai/runtime/image-count-intent";
|
import { inferAgentImageCountFromText } from "@/lib/ai/runtime/image-count-intent";
|
||||||
|
import {
|
||||||
|
buildRecentDialogueMemory,
|
||||||
|
isRecentDialogueRecallRequest,
|
||||||
|
} from "@/lib/ai/runtime/recent-dialogue-memory";
|
||||||
import {
|
import {
|
||||||
isAgentModeScopedChatId,
|
isAgentModeScopedChatId,
|
||||||
isScopedEmptyHistoryRequest,
|
isScopedEmptyHistoryRequest,
|
||||||
@ -221,6 +225,19 @@ import {
|
|||||||
import type { PickedObjectAttachment } from "@/components/canvas/chat-panel/canvas-chat-session";
|
import type { PickedObjectAttachment } from "@/components/canvas/chat-panel/canvas-chat-session";
|
||||||
import type { CanvasAgentContext } from "@/types/canvas/agent-protocol";
|
import type { CanvasAgentContext } from "@/types/canvas/agent-protocol";
|
||||||
import type { ChatMessage } from "@/lib/types";
|
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.describe("canvas chat pure helpers", () => {
|
||||||
test("keeps optimistic messages without timestamps after persisted history", () => {
|
test("keeps optimistic messages without timestamps after persisted history", () => {
|
||||||
@ -309,6 +326,45 @@ test.describe("canvas chat pure helpers", () => {
|
|||||||
expect(upstream).toContain("| SKU | 销量 |");
|
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", () => {
|
test("infers explicit agent image counts from user text", () => {
|
||||||
expect(inferAgentImageCountFromText("再出三版")).toBe(3);
|
expect(inferAgentImageCountFromText("再出三版")).toBe(3);
|
||||||
expect(inferAgentImageCountFromText("基于这张图再来 3 张")).toBe(3);
|
expect(inferAgentImageCountFromText("基于这张图再来 3 张")).toBe(3);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user