From e4022331207b1aa60e28b35f1e59bc11f2d0738b Mon Sep 17 00:00:00 2001 From: Manus Date: Fri, 9 Oct 2026 07:30:44 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20transmettre=20le=20moteur=20R=C3=A9f?= =?UTF-8?q?=C3=A9rentiel=20IA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/referentialIa.test.ts | 20 ++++++++++- server/referentialIa.ts | 70 +++++++++++++++++++++++++----------- 2 files changed, 68 insertions(+), 22 deletions(-) diff --git a/server/referentialIa.test.ts b/server/referentialIa.test.ts index 0e66a35..5ee9eae 100644 --- a/server/referentialIa.test.ts +++ b/server/referentialIa.test.ts @@ -48,9 +48,10 @@ describe("Référentiel IA", () => { } }); - it("transmet une requête OpenAI-compatible à la passerelle sélectionnée", async () => { + it("transmet le slug du moteur décidé avant la requête OpenAI-compatible", async () => { let receivedUrl = ""; let receivedInit: RequestInit | undefined; + let callCount = 0; const result = await invokeReferentialIa( { messages: [{ role: "user", content: "Extrais la facture" }], @@ -58,8 +59,22 @@ describe("Référentiel IA", () => { }, connection, async (url, init) => { + callCount += 1; receivedUrl = String(url); receivedInit = init; + if (callCount === 1) { + expect(receivedUrl).toBe("https://referentiel-ia.recette.santinova-soft.org/api/integration/v1/recommendations"); + expect(JSON.parse(String(receivedInit?.body))).toEqual({ + useCase: "texte", + optimizeCost: true, + needsFreeTier: false, + includeConnection: false, + }); + return jsonResponse({ + application: { name: "Démat. Facturation DSI" }, + decision: { selected: { engine: { name: "SANTINO Expert", slug: "santino-expert" } } }, + }); + } return jsonResponse({ id: "ref-1", created: 1, @@ -72,12 +87,15 @@ describe("Référentiel IA", () => { expect(receivedUrl).toBe("https://referentiel-ia.recette.santinova-soft.org/api/integration/v1/invoke"); expect(receivedInit?.headers).toMatchObject({ authorization: "Bearer test-token" }); expect(JSON.parse(String(receivedInit?.body))).toMatchObject({ + engineSlug: "santino-expert", + useCase: "texte", payload: { messages: [{ role: "user", content: "Extrais la facture" }], max_tokens: 1200, stream: false, }, }); + expect(callCount).toBe(2); expect(result.choices[0]?.message.content).toBe("{}"); }); diff --git a/server/referentialIa.ts b/server/referentialIa.ts index 49b022a..4a60bbb 100644 --- a/server/referentialIa.ts +++ b/server/referentialIa.ts @@ -6,6 +6,16 @@ export type ReferentielIaConnection = { token: string; }; +type ReferentielIaRecommendation = { + application?: { name?: unknown }; + decision?: { + selected?: { + engine?: { name?: unknown; slug?: unknown }; + connection?: { model?: unknown }; + } | null; + } | null; +}; + type Fetcher = (input: string | URL, init?: RequestInit) => Promise; const RECOMMENDATIONS_PATH = "/api/integration/v1/recommendations"; @@ -68,6 +78,37 @@ function gatewayError(status: number): Error { return new Error(`Le Référentiel IA est indisponible (HTTP ${status}).`); } +/** Demande au Référentiel le moteur actuellement décidé pour cette application. */ +async function getReferentialIaRecommendation( + connection: ReferentielIaConnection, + fetcher: Fetcher, +): Promise { + const response = await fetcher(`${connection.baseUrl}${RECOMMENDATIONS_PATH}`, { + method: "POST", + headers: { + "content-type": "application/json", + authorization: `Bearer ${connection.token}`, + }, + body: JSON.stringify({ + useCase: "texte", + optimizeCost: true, + needsFreeTier: false, + includeConnection: false, + }), + signal: AbortSignal.timeout(15_000), + }); + if (!response.ok) throw gatewayError(response.status); + return (await readJson(response)) as ReferentielIaRecommendation; +} + +function selectedEngineSlug(recommendation: ReferentielIaRecommendation): string { + const slug = recommendation.decision?.selected?.engine?.slug; + if (typeof slug !== "string" || !slug.trim()) { + throw new Error("Le Référentiel IA ne fournit aucun moteur sélectionné pour cette application."); + } + return slug.trim(); +} + /** Exécute l'appel sur le moteur décidé et tracé par le Référentiel IA. */ export async function invokeReferentialIa( params: InvokeParams, @@ -75,13 +116,19 @@ export async function invokeReferentialIa( fetcher: Fetcher = fetch, ): Promise { const config = requireConnection(connection); + const recommendation = await getReferentialIaRecommendation(config, fetcher); + const engineSlug = selectedEngineSlug(recommendation); const response = await fetcher(`${config.baseUrl}${INVOCATION_PATH}`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${config.token}`, }, - body: JSON.stringify({ payload: toGatewayPayload(params) }), + body: JSON.stringify({ + engineSlug, + useCase: "texte", + payload: toGatewayPayload(params), + }), signal: AbortSignal.timeout(120_000), }); if (!response.ok) throw gatewayError(response.status); @@ -100,26 +147,7 @@ export async function testReferentialIaConnection( fetcher: Fetcher = fetch, ): Promise { const config = requireConnection(connection); - const response = await fetcher(`${config.baseUrl}${RECOMMENDATIONS_PATH}`, { - method: "POST", - headers: { - "content-type": "application/json", - authorization: `Bearer ${config.token}`, - }, - body: JSON.stringify({ - useCase: "texte", - optimizeCost: true, - needsFreeTier: false, - includeConnection: false, - }), - signal: AbortSignal.timeout(15_000), - }); - if (!response.ok) throw gatewayError(response.status); - - const data = (await readJson(response)) as { - application?: { name?: unknown }; - decision?: { selected?: { engine?: { name?: unknown }; connection?: { model?: unknown } } | null } | null; - }; + const data = await getReferentialIaRecommendation(config, fetcher); return { applicationName: typeof data.application?.name === "string" ? data.application.name : null, engineName: typeof data.decision?.selected?.engine?.name === "string" ? data.decision.selected.engine.name : null,