Files
chlova/orchestrator/src/agent/chat-service.ts
T
Kantin-Petit a29c9dbdf0 feat(agent): maxSteps configurable, défaut 8->24 (tâches multi-outils) (v0.37.0)
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
2026-06-24 00:11:45 +02:00

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;
}
}