- WorkingMemoryService:session 對話上下文緩衝,token 溢位時優先裁切最舊的低情緒段落 - JobQueue 抽象層(now/schedule(at)/every(cron))與 InProcessJobQueue 實作(node-cron), R-6 會換成 BullMQ 但呼叫端介面不變 - MemoryConsolidationService:睡眠固化,只有高情緒或重複提及的內容才寫入 EpisodicMemory - ForgettingSweepService:低情緒且長期未提取的記憶逐次降權、weight 過低後刪除, 已掛上每小時一次的 JobQueue.every 排程 - KeywordMemoryRetriever:關鍵字+時間近因+情緒權重排序,提取命中即更新提取次數/時間 - apps/api 新增 /memory/* 端點作為引擎驗證介面(H 群組會決定併入正式對話管線後的去留) - scripts/smoke/C.mjs:端到端驗證溢位裁切、只寫工作記憶、固化篩選、檢索排序、 提取即改寫、schedule(at) 準時觸發、遺忘衰減與刪除 npm run restart && npm run smoke -- C 皆通過(C-V),A/B 群組冒煙測試無回歸。
83 lines
3.0 KiB
TypeScript
83 lines
3.0 KiB
TypeScript
import { Body, Controller, Get, Param, Post, Query } from "@nestjs/common";
|
|
import type { EmotionTag } from "@kokorone/shared";
|
|
import { WorkingMemoryService } from "./working-memory.service.js";
|
|
import { MemoryConsolidationService } from "./memory-consolidation.service.js";
|
|
import { ForgettingSweepService } from "./forgetting-sweep.service.js";
|
|
import { KeywordMemoryRetriever } from "./memory-retriever.service.js";
|
|
import { InProcessJobQueue } from "./in-process-job-queue.service.js";
|
|
|
|
interface AppendMessageBody {
|
|
role: "user" | "character";
|
|
content: string;
|
|
emotionTag?: EmotionTag;
|
|
emotionIntensity?: number;
|
|
}
|
|
|
|
@Controller("memory")
|
|
export class MemoryController {
|
|
// C-3 排程抽象層驗證用:記錄測試性延遲工作是否已觸發,僅供 smoke test 檢查 schedule(at) 準時性。
|
|
private readonly scheduledJobFired = new Map<string, boolean>();
|
|
|
|
constructor(
|
|
private readonly workingMemory: WorkingMemoryService,
|
|
private readonly consolidation: MemoryConsolidationService,
|
|
private readonly forgettingSweep: ForgettingSweepService,
|
|
private readonly retriever: KeywordMemoryRetriever,
|
|
private readonly jobQueue: InProcessJobQueue,
|
|
) {}
|
|
|
|
@Post(":characterId/sessions/:sessionId/messages")
|
|
appendMessage(@Param("sessionId") sessionId: string, @Body() body: AppendMessageBody) {
|
|
this.workingMemory.append(sessionId, {
|
|
role: body.role,
|
|
content: body.content,
|
|
timestamp: new Date(),
|
|
emotionTag: body.emotionTag,
|
|
emotionIntensity: body.emotionIntensity,
|
|
});
|
|
return { context: this.workingMemory.getContext(sessionId) };
|
|
}
|
|
|
|
@Get(":characterId/sessions/:sessionId/messages")
|
|
getContext(@Param("sessionId") sessionId: string) {
|
|
return { context: this.workingMemory.getContext(sessionId) };
|
|
}
|
|
|
|
@Post(":characterId/sessions/:sessionId/consolidate")
|
|
async consolidate(@Param("characterId") characterId: string, @Param("sessionId") sessionId: string) {
|
|
await this.consolidation.consolidate(characterId, sessionId);
|
|
return { ok: true };
|
|
}
|
|
|
|
@Get(":characterId/retrieve")
|
|
async retrieve(
|
|
@Param("characterId") characterId: string,
|
|
@Query("query") query: string,
|
|
@Query("limit") limit?: string,
|
|
) {
|
|
const results = await this.retriever.retrieve(characterId, query ?? "", limit ? Number(limit) : undefined);
|
|
return { results };
|
|
}
|
|
|
|
@Post("forgetting-sweep")
|
|
async runForgettingSweep(@Body() body: { now?: string }) {
|
|
const now = body?.now ? new Date(body.now) : new Date();
|
|
return this.forgettingSweep.run(now);
|
|
}
|
|
|
|
@Post("test-scheduled-job")
|
|
scheduleTestJob(@Body() body: { id: string; delaySeconds: number }) {
|
|
this.scheduledJobFired.set(body.id, false);
|
|
const at = new Date(Date.now() + body.delaySeconds * 1000);
|
|
this.jobQueue.schedule(at, `test-job-${body.id}`, () => {
|
|
this.scheduledJobFired.set(body.id, true);
|
|
});
|
|
return { scheduled: true };
|
|
}
|
|
|
|
@Get("test-scheduled-job/:id")
|
|
checkTestJob(@Param("id") id: string) {
|
|
return { fired: this.scheduledJobFired.get(id) ?? false };
|
|
}
|
|
}
|