feat: 完成 E 群組 — 關係子系統
- RelationshipService:E-1 未知對象自動建檔(禮貌+防備=intimacy/trust=0)、 E-3 親密度五階分層+跨階事件(@nestjs/event-emitter,StageChangeLogService 獨立訂閱)、 E-4 久未互動時親密度半衰期衰減並標記「久未見」 - SentimentLedgerService:E-2 關係帳本,負向事件套用放大係數,驗證同等級正負事件淨值為負 - IntentInterpreterService:E-6 依親密度將同一句模稜兩可的話解讀為玩笑或冒犯 - KeywordMemoryRetriever(C-6)擴充 E-5:新增 relatedUserId 加權,retrieve() 簽名改為接受 options 物件 - schema 新增 EpisodicMemory.relatedUserId,並補上所有子表對父表的 onDelete: Cascade/SetNull (E-2 測試時發現先前 B 群組遺漏這些規則會導致刪除關係時外鍵違反) - apps/api 新增 /relationships/* 端點作為引擎驗證介面 - scripts/smoke/E.mjs:涵蓋 E-1~E-6 全部驗收情境 npm run restart && npm run smoke -- E 皆通過(E-V),A/B/C/D 群組冒煙測試無回歸。 本次另排除一個本機環境限定的 Prisma CLI 網路 checkpoint 卡住問題,並從一次部分套用的 migration 中手動恢復資料庫一致性(過程記錄於 todo.md 實作記錄,供後續群組參考)。
This commit is contained in:
@@ -2,14 +2,21 @@ import { Injectable } from "@nestjs/common";
|
||||
import type { EpisodicMemory } from "@kokorone/shared";
|
||||
import { PrismaService } from "../prisma/prisma.service.js";
|
||||
|
||||
export interface RetrieveOptions {
|
||||
limit?: number;
|
||||
// E-5 關係加權檢索接點:與此使用者相關的記憶會被加權排到前面。
|
||||
relatedUserId?: string;
|
||||
}
|
||||
|
||||
export interface MemoryRetriever {
|
||||
retrieve(characterId: string, query: string, limit?: number): Promise<EpisodicMemory[]>;
|
||||
retrieve(characterId: string, query: string, options?: RetrieveOptions): Promise<EpisodicMemory[]>;
|
||||
}
|
||||
|
||||
const WEIGHTS = {
|
||||
keyword: 0.5,
|
||||
recency: 0.3,
|
||||
emotion: 0.2,
|
||||
keyword: 0.4,
|
||||
recency: 0.25,
|
||||
emotion: 0.15,
|
||||
relationship: 0.2,
|
||||
};
|
||||
|
||||
const DEFAULT_LIMIT = 5;
|
||||
@@ -28,7 +35,8 @@ function keywordRelevance(content: string, keywords: string[]): number {
|
||||
export class KeywordMemoryRetriever implements MemoryRetriever {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async retrieve(characterId: string, query: string, limit = DEFAULT_LIMIT): Promise<EpisodicMemory[]> {
|
||||
async retrieve(characterId: string, query: string, options: RetrieveOptions = {}): Promise<EpisodicMemory[]> {
|
||||
const limit = options.limit ?? DEFAULT_LIMIT;
|
||||
const rows = await this.prisma.client.episodicMemory.findMany({ where: { characterId } });
|
||||
const now = Date.now();
|
||||
const keywords = query.split(/\s+/).filter(Boolean);
|
||||
@@ -38,8 +46,12 @@ export class KeywordMemoryRetriever implements MemoryRetriever {
|
||||
const daysSince = (now - row.occurredAt.getTime()) / (1000 * 60 * 60 * 24);
|
||||
const recencyScore = 1 / (1 + Math.max(0, daysSince));
|
||||
const emotionScore = row.emotionIntensity;
|
||||
const relationshipScore = options.relatedUserId && row.relatedUserId === options.relatedUserId ? 1 : 0;
|
||||
const score =
|
||||
keywordScore * WEIGHTS.keyword + recencyScore * WEIGHTS.recency + emotionScore * WEIGHTS.emotion;
|
||||
keywordScore * WEIGHTS.keyword +
|
||||
recencyScore * WEIGHTS.recency +
|
||||
emotionScore * WEIGHTS.emotion +
|
||||
relationshipScore * WEIGHTS.relationship;
|
||||
return { row, score };
|
||||
});
|
||||
|
||||
|
||||
@@ -54,8 +54,12 @@ export class MemoryController {
|
||||
@Param("characterId") characterId: string,
|
||||
@Query("query") query: string,
|
||||
@Query("limit") limit?: string,
|
||||
@Query("relatedUserId") relatedUserId?: string,
|
||||
) {
|
||||
const results = await this.retriever.retrieve(characterId, query ?? "", limit ? Number(limit) : undefined);
|
||||
const results = await this.retriever.retrieve(characterId, query ?? "", {
|
||||
limit: limit ? Number(limit) : undefined,
|
||||
relatedUserId,
|
||||
});
|
||||
return { results };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user