a29c9dbdf0
Construire un workflow n8n (flux SDK) dépasse 8 étapes. maxAgentSteps via CHLOVA_MAX_AGENT_STEPS, passé config -> ChatService -> runAgentTurn. 83 tests verts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016w5jRe87MGdd6AMvXQcHNi
86 lines
2.8 KiB
TypeScript
86 lines
2.8 KiB
TypeScript
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<ChatResult> {
|
|
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<string> {
|
|
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;
|
|
}
|
|
}
|