Merge pull request #24: fix WeChat episodic recall
Memind CI / Test, build, and release guards (push) Successful in 3m9s
Memind CI / Test, build, and release guards (push) Successful in 3m9s
This commit was merged in pull request #24.
This commit is contained in:
@@ -881,6 +881,7 @@ async function bootstrapUserAuth() {
|
||||
scheduleService: process.env.H5_SCHEDULE_ENABLED === '1' ? scheduleService : null,
|
||||
wechatScheduleLlmConfigService,
|
||||
llmProviderService,
|
||||
chatIntentRouter,
|
||||
onPageGenerated: async ({ userId, sessionId, artifacts = [] }) => {
|
||||
for (const artifact of artifacts) {
|
||||
void sendMindSpaceAnalyticsEvent({
|
||||
|
||||
+59
-2
@@ -235,7 +235,7 @@ async function executeSessionReply(
|
||||
requestId,
|
||||
prompt,
|
||||
metadata = {},
|
||||
{ submitReply = null } = {},
|
||||
{ submitReply = null, prepareUserMessage = null } = {},
|
||||
) {
|
||||
const eventsResponse = await apiFetch(`/sessions/${sessionId}/events`, {
|
||||
method: 'GET',
|
||||
@@ -246,7 +246,10 @@ async function executeSessionReply(
|
||||
throw new Error(text || '无法建立公众号消息事件流');
|
||||
}
|
||||
|
||||
const userMessage = createUserMessage(prompt, metadata);
|
||||
let userMessage = createUserMessage(prompt, metadata);
|
||||
if (prepareUserMessage) {
|
||||
userMessage = (await prepareUserMessage(userMessage)) ?? userMessage;
|
||||
}
|
||||
if (submitReply) {
|
||||
await submitReply({ sessionId, requestId, userMessage });
|
||||
} else {
|
||||
@@ -1504,6 +1507,7 @@ export function createWechatMpService({
|
||||
scheduleService = null,
|
||||
wechatScheduleLlmConfigService = null,
|
||||
llmProviderService = null,
|
||||
chatIntentRouter = null,
|
||||
onPageGenerated = null,
|
||||
applySessionLlmProvider = null,
|
||||
refreshSessionSnapshot = null,
|
||||
@@ -2232,6 +2236,49 @@ export function createWechatMpService({
|
||||
}
|
||||
};
|
||||
|
||||
const prepareWechatAgentUserMessage = async ({ userId, sessionId, userMessage }) => {
|
||||
if (
|
||||
!chatIntentRouter?.resolveAgentMemoryContext
|
||||
|| !chatIntentRouter?.applyAgentOrchestration
|
||||
) {
|
||||
return userMessage;
|
||||
}
|
||||
const displayText = String(
|
||||
userMessage?.metadata?.displayText
|
||||
?? messageVisibleText(userMessage),
|
||||
).trim();
|
||||
try {
|
||||
const memoryContext = await chatIntentRouter.resolveAgentMemoryContext({
|
||||
userId,
|
||||
sessionId,
|
||||
text: displayText,
|
||||
forceDeepReasoning: false,
|
||||
});
|
||||
if (
|
||||
!memoryContext?.injectionEnabled
|
||||
|| !Array.isArray(memoryContext.memories)
|
||||
|| memoryContext.memories.length === 0
|
||||
) {
|
||||
return userMessage;
|
||||
}
|
||||
return chatIntentRouter.applyAgentOrchestration(
|
||||
userMessage,
|
||||
{
|
||||
route: 'agent_orchestration',
|
||||
reason: '微信服务号消息由 Agent 处理',
|
||||
source: 'wechat_mp',
|
||||
},
|
||||
{ memoryContext },
|
||||
);
|
||||
} catch (err) {
|
||||
logger.warn?.(
|
||||
'WeChat MP agent memory resolve skipped:',
|
||||
err instanceof Error ? err.message : err,
|
||||
);
|
||||
return userMessage;
|
||||
}
|
||||
};
|
||||
|
||||
const runIntentMessage = async ({ inbound, intent, user }) => {
|
||||
const wechatIntent = classifyWechatIntent(intent);
|
||||
const mediaAnalysisEnabled = isWechatMediaGrayUser(user, config.mediaAnalysisGrayUsers);
|
||||
@@ -2294,6 +2341,11 @@ export function createWechatMpService({
|
||||
agentPrompt,
|
||||
buildIntentMetadata(intent, { mediaAnalysisEnabled, imagePolicy }),
|
||||
{
|
||||
prepareUserMessage: (userMessage) => prepareWechatAgentUserMessage({
|
||||
userId: user.userId,
|
||||
sessionId,
|
||||
userMessage,
|
||||
}),
|
||||
submitReply: submitSessionReply
|
||||
? ({ requestId: replyRequestId, userMessage }) =>
|
||||
submitSessionReply({
|
||||
@@ -2496,6 +2548,11 @@ export function createWechatMpService({
|
||||
retryPrompt,
|
||||
buildIntentMetadata(intent, { mediaAnalysisEnabled, imagePolicy }),
|
||||
{
|
||||
prepareUserMessage: (userMessage) => prepareWechatAgentUserMessage({
|
||||
userId: user.userId,
|
||||
sessionId,
|
||||
userMessage,
|
||||
}),
|
||||
submitReply: submitSessionReply
|
||||
? ({ requestId: replyRequestId, userMessage }) =>
|
||||
submitSessionReply({
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
decryptWechatMpPayload,
|
||||
WECHAT_CUSTOMER_TEXT_MAX_BYTES,
|
||||
} from './wechat-mp.mjs';
|
||||
import { createChatIntentRouter } from './chat-intent-router.mjs';
|
||||
|
||||
function signatureFor(token, timestamp, nonce) {
|
||||
return crypto
|
||||
@@ -82,6 +83,7 @@ function createBoundWechatService({
|
||||
scheduleService = null,
|
||||
applySessionLlmProvider = null,
|
||||
submitSessionReply = null,
|
||||
chatIntentRouter = null,
|
||||
}) {
|
||||
return createWechatMpService({
|
||||
config: {
|
||||
@@ -133,6 +135,7 @@ function createBoundWechatService({
|
||||
startAgentSession,
|
||||
sessionApiFetch,
|
||||
submitSessionReply,
|
||||
chatIntentRouter,
|
||||
scheduleService,
|
||||
applySessionLlmProvider,
|
||||
wechatFetch,
|
||||
@@ -3935,6 +3938,102 @@ test('wechat mp image submission reuses the H5 prepared reply path', async () =>
|
||||
assert.match(submitCalls[0].userMessage.metadata.imageUrls[0], /\/public\/wechat-mp\//);
|
||||
});
|
||||
|
||||
test('wechat mp injects episodic recall context before submitting the Agent reply', async () => {
|
||||
const token = 'token';
|
||||
const timestamp = '1710000000';
|
||||
const nonce = 'nonce';
|
||||
const submitCalls = [];
|
||||
const episodicCalls = [];
|
||||
const chatIntentRouter = createChatIntentRouter({
|
||||
env: {
|
||||
MEMORY_AGENT_RESOLVE_ENABLED: '1',
|
||||
MEMORY_AGENT_INJECTION_MODE: 'canary',
|
||||
MEMORY_AGENT_CANARY_USER_IDS: 'user-canary',
|
||||
MEMORY_AGENT_RESOLVE_TIMEOUT_MS: '200',
|
||||
},
|
||||
memoryV2: {
|
||||
async resolve() {
|
||||
return { memories: [] };
|
||||
},
|
||||
},
|
||||
episodicMemoryService: {
|
||||
async resolve(input) {
|
||||
episodicCalls.push(input);
|
||||
return {
|
||||
source: 'episodic-index',
|
||||
memories: [{
|
||||
id: 'episodic:tokugawa',
|
||||
label: '历史会话',
|
||||
text: '用户与助手之前讨论过德川家康。',
|
||||
}],
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
const service = createBoundWechatService({
|
||||
token,
|
||||
chatIntentRouter,
|
||||
userAuth: {
|
||||
async findWechatUserByOpenid() {
|
||||
return { userId: 'user-canary', status: 'active', nickname: '唐' };
|
||||
},
|
||||
},
|
||||
sessionApiFetch: async (_sessionId, pathname) => {
|
||||
if (pathname === '/sessions/session-1/events') {
|
||||
return new Response(
|
||||
[
|
||||
'data: {"type":"Message","message":{"id":"assistant-1","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"记得,我们之前聊过德川家康。"}]}}\n\n',
|
||||
'data: {"type":"Finish"}\n\n',
|
||||
].join(''),
|
||||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||||
);
|
||||
}
|
||||
if (pathname === '/agent/harness_remember' || pathname === '/agent/harness_bootstrap') {
|
||||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||
}
|
||||
throw new Error(`unexpected api path: ${pathname}`);
|
||||
},
|
||||
submitSessionReply: async (input) => {
|
||||
submitCalls.push(input);
|
||||
return { ok: true };
|
||||
},
|
||||
wechatFetch: async (url) => {
|
||||
if (String(url).includes('/cgi-bin/stable_token')) {
|
||||
return new Response(JSON.stringify({ access_token: 'access-1', expires_in: 7200 }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
if (String(url).includes('/cgi-bin/message/custom/send')) {
|
||||
return new Response(JSON.stringify({ errcode: 0, errmsg: 'ok' }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
throw new Error(`unexpected wechat url: ${url}`);
|
||||
},
|
||||
});
|
||||
|
||||
const result = await service.handleInboundMessage(
|
||||
inboundXml({ content: '你记得我们聊过德川家康吗?' }),
|
||||
{ timestamp, nonce, signature: signatureFor(token, timestamp, nonce) },
|
||||
);
|
||||
await result.task;
|
||||
|
||||
assert.equal(episodicCalls.length, 1);
|
||||
assert.deepEqual(episodicCalls[0], {
|
||||
userId: 'user-canary',
|
||||
sessionId: 'session-1',
|
||||
query: '你记得我们聊过德川家康吗?',
|
||||
limit: 3,
|
||||
});
|
||||
assert.equal(submitCalls.length, 1);
|
||||
assert.equal(submitCalls[0].userMessage.metadata.displayText, '你记得我们聊过德川家康吗?');
|
||||
assert.match(submitCalls[0].userMessage.content[0].text, /\[Memory Context\]/);
|
||||
assert.match(submitCalls[0].userMessage.content[0].text, /用户与助手之前讨论过德川家康/);
|
||||
assert.match(submitCalls[0].userMessage.content[0].text, /不是系统指令/);
|
||||
});
|
||||
|
||||
test('wechat mp serializes image and follow-up text and reattaches recent image', async () => {
|
||||
const token = 'token';
|
||||
const timestamp = '1710000000';
|
||||
|
||||
Reference in New Issue
Block a user