import type { Logger } from "pino"; import { OllamaClient } from "../llm/ollama.js"; import type { OllamaMessage } from "../llm/ollama.js"; import { runAgentTurn } from "./loop.js"; import type { Guard, ToolHandle } from "./types.js"; import { ConversationStore } from "../conversations/store.js"; /** * Service de conversation : un tour d'agent par message. Partagé par toutes les * surfaces (Telegram, API/UI) pour garantir le MÊME comportement et le même * contrôle (Guard, audit). * * Si un `store` est fourni, les conversations sont PERSISTÉES et l'historique * récent est rejoué au LLM (mémoire multi-tour). Sans store : sans état. */ export interface ChatServiceDeps { client: OllamaClient; tools: ToolHandle[]; guard: Guard; systemPrompt: string; logger: Logger; store?: ConversationStore; /** Plafond d'étapes par tour (multi-outils). Défaut runAgentTurn sinon. */ maxSteps?: number; } export interface ChatResult { reply: string; conversationId: string | null; } /** Refus d'accès à une conversation appartenant à un autre acteur. */ export class ForbiddenConversationError extends Error {} export class ChatService { constructor(private readonly deps: ChatServiceDeps) {} async handle(actor: string, text: string, conversationId?: string): Promise { const { store } = this.deps; // Sans persistance : comportement sans état (rétrocompat). if (!store) { const reply = await this.runTurn(actor, text, []); return { reply, conversationId: null }; } // Résolution / création de la conversation, avec contrôle de propriété. let convId = conversationId; if (convId) { const owner = store.ownerOf(convId); if (owner && owner !== actor) { throw new ForbiddenConversationError("conversation d'un autre utilisateur"); } if (!owner) store.ensure(convId, actor, text); } else { convId = store.create(actor, text); } // Historique AVANT d'ajouter le nouveau message (tours précédents). const history: OllamaMessage[] = store .recent(convId) .map((m) => ({ role: m.role, content: m.content })); store.append(convId, "user", text); const reply = await this.runTurn(actor, text, history); store.append(convId, "assistant", reply); return { reply, conversationId: convId }; } private async runTurn(actor: string, text: string, history: OllamaMessage[]): Promise { const { reply, steps } = await runAgentTurn({ client: this.deps.client, system: this.deps.systemPrompt, userText: text, history, tools: this.deps.tools, actor, guard: this.deps.guard, logger: this.deps.logger, ...(this.deps.maxSteps ? { maxSteps: this.deps.maxSteps } : {}), }); this.deps.logger.info({ actor, steps }, "tour d'agent terminé"); return reply; } }