feat: 完成 C 群組 — 記憶子系統
- 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 群組冒煙測試無回歸。
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
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 };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user