import { Inject, Injectable } from "@nestjs/common"; import { PrismaService } from "../prisma/prisma.service.js"; import { WorkingMemoryService } from "../memory/working-memory.service.js"; import { EmotionService } from "../emotion/emotion.service.js"; import { RelationshipService } from "../relationship/relationship.service.js"; import { LLM_PROVIDER, type LLMProvider } from "./llm-provider.js"; import { ContextAssemblerService } from "./context-assembler.service.js"; import { OutputFilterService } from "./output-filter.service.js"; import { FastChannelDetector, DEFAULT_FAST_REPLIES } from "./fast-channel.service.js"; import { extractActions } from "./action-markup.js"; import { getForbiddenWords, applySpeechQuirks } from "../personality/language-style.js"; import type { GenerationContext } from "./types.js"; export interface HandleMessageOptions { seed?: number; now?: Date; } export interface HandleMessageResult { text: string; actions: string[]; isFastChannel: boolean; context: GenerationContext; } function fastChannelSituation(reason: string): string { return `fast-channel:${reason}`; } // F 群組整合入口:串起雙速通道判斷、上下文組裝、Provider 生成、輸出過濾與工作記憶寫入。 // H-1 的 POST /chat/:characterId 會以此為基礎擴充(見 todo.md 實作記錄)。 @Injectable() export class DialogueService { constructor( @Inject(LLM_PROVIDER) private readonly llmProvider: LLMProvider, private readonly fastChannel: FastChannelDetector, private readonly contextAssembler: ContextAssemblerService, private readonly outputFilter: OutputFilterService, private readonly workingMemory: WorkingMemoryService, private readonly emotion: EmotionService, private readonly relationship: RelationshipService, private readonly prisma: PrismaService, ) {} async handleMessage( characterId: string, userId: string, sessionId: string, userInput: string, options: HandleMessageOptions = {}, ): Promise { const now = options.now ?? new Date(); this.workingMemory.append(sessionId, { role: "user", content: userInput, timestamp: now }); // H-1:每輪對話先標記情緒、累積日常互動的親密度,讓後續組裝的上下文反映本輪的變化。 await this.emotion.processInput(characterId, userInput, { now }); await this.relationship.recordInteraction(characterId, userId, now); const { isFastChannel, reason } = this.fastChannel.detect(userInput); const context = await this.contextAssembler.assemble(characterId, userId, sessionId, userInput, { skipMemoryRetrieval: isFastChannel, seed: options.seed, now, }); let text: string; let actions: string[]; if (isFastChannel && reason) { const rule = await this.prisma.client.proceduralRule.findFirst({ where: { characterId, situation: fastChannelSituation(reason) }, orderBy: { weight: "desc" }, }); text = rule?.responsePattern ?? DEFAULT_FAST_REPLIES[reason]; actions = extractActions(text); } else { const generated = await this.llmProvider.generate(context); text = generated.text; actions = generated.actions; } const forbiddenWords = getForbiddenWords(context.character.personalityArchetype); const filteredText = applySpeechQuirks( this.outputFilter.filter(text, forbiddenWords), context.character.personalityArchetype, ); this.workingMemory.append(sessionId, { role: "character", content: filteredText, timestamp: now }); return { text: filteredText, actions, isFastChannel, context }; } }