feat: 完成 F 群組 — 對話生成管線與 LLM Provider 抽象
- LLMProvider 介面(generate/stream)+ LLM_PROVIDER 切換(llm.module.ts useFactory, LLM_PROVIDER=mock|claude 環境變數決定注入哪個實作) - MockProvider:依性格原型 × 情緒狀態 × 親密度從模板庫選填回應,seeded PRNG 確保同 seed 可重現, 支援 *動作描寫* 標記抽取(目前只有元氣原型模板,G 群組需補齊其餘五種原型) - ClaudeProvider 空殼:LLM_PROVIDER=claude 時啟動即輸出 ERR log,呼叫 generate/stream 時拋出例外 - OutputFilterService:全域+角色禁則詞彙過濾(攔截並改寫) - FastChannelDetector:固定問候與危險輸入(含求助資源)走快速通道,跳過記憶檢索 - BehaviorReinforcementService:稱讚加權、糾正後修正版繼承舊權重以確保排序領先(實作時抓到並修正一個 「降權後打平」的真實 bug) - ContextAssemblerService:組裝人設/情緒/關係/檢索記憶/對話歷史為統一上下文快照 - DialogueService/Controller:串起以上所有元件,作為 H-1 正式對話端點的基礎 - scripts/smoke/F.mjs:涵蓋可重現性、上下文快照完整性、雙速通道、行為強化、輸出過濾、 以 child process 驗證 Provider 切換 npm run restart && npm run smoke -- F 皆通過(F-V),A/B/C/D/E 群組冒煙測試無回歸。
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { PrismaService } from "../prisma/prisma.service.js";
|
||||
import { EmotionService, dominantState } from "../emotion/emotion.service.js";
|
||||
import { toResponseStyle } from "../emotion/response-style.js";
|
||||
import { RelationshipService } from "../relationship/relationship.service.js";
|
||||
import { KeywordMemoryRetriever } from "../memory/memory-retriever.service.js";
|
||||
import { WorkingMemoryService } from "../memory/working-memory.service.js";
|
||||
import { hashToSeed } from "./seeded-random.js";
|
||||
import type { GenerationContext } from "./types.js";
|
||||
|
||||
export interface AssembleOptions {
|
||||
skipMemoryRetrieval?: boolean;
|
||||
seed?: number;
|
||||
now?: Date;
|
||||
}
|
||||
|
||||
// F-6 上下文組裝器:把人設、當前情緒、檢索到的記憶、關係參數、對話歷史組裝成統一上下文物件。
|
||||
@Injectable()
|
||||
export class ContextAssemblerService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly emotion: EmotionService,
|
||||
private readonly relationship: RelationshipService,
|
||||
private readonly retriever: KeywordMemoryRetriever,
|
||||
private readonly workingMemory: WorkingMemoryService,
|
||||
) {}
|
||||
|
||||
async assemble(
|
||||
characterId: string,
|
||||
userId: string,
|
||||
sessionId: string,
|
||||
userInput: string,
|
||||
options: AssembleOptions = {},
|
||||
): Promise<GenerationContext> {
|
||||
const now = options.now ?? new Date();
|
||||
|
||||
const character = await this.prisma.client.character.findUniqueOrThrow({ where: { id: characterId } });
|
||||
const emotionState = await this.emotion.getState(characterId, now);
|
||||
const dominant = dominantState(emotionState);
|
||||
const style = toResponseStyle(dominant);
|
||||
const { relationship } = await this.relationship.getState(characterId, userId, now);
|
||||
|
||||
const retrievedMemories = options.skipMemoryRetrieval
|
||||
? []
|
||||
: await this.retriever.retrieve(characterId, userInput, { relatedUserId: userId });
|
||||
|
||||
const history = this.workingMemory.getContext(sessionId).map((entry) => ({
|
||||
role: entry.role,
|
||||
content: entry.content,
|
||||
timestamp: entry.timestamp.toISOString(),
|
||||
}));
|
||||
|
||||
const seed = options.seed ?? hashToSeed(`${sessionId}:${history.length}`);
|
||||
|
||||
return {
|
||||
character: {
|
||||
id: character.id,
|
||||
personalityArchetype: character.personalityArchetype,
|
||||
speechStyle: character.speechStyle,
|
||||
likesDislikes: character.likesDislikes,
|
||||
basicInfo: character.basicInfo,
|
||||
},
|
||||
emotion: { dominant, style },
|
||||
relationship: {
|
||||
intimacy: relationship.intimacy,
|
||||
trust: relationship.trust,
|
||||
stage: relationship.stage,
|
||||
},
|
||||
retrievedMemories,
|
||||
history,
|
||||
userInput,
|
||||
injectedMemoryIds: retrievedMemories.map((memory) => memory.id),
|
||||
seed,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user