fix: transmettre le moteur Référentiel IA

This commit is contained in:
Manus
2026-10-09 07:30:44 +00:00
parent 3b0be885e2
commit e402233120
2 changed files with 68 additions and 22 deletions

View File

@@ -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 receivedUrl = "";
let receivedInit: RequestInit | undefined; let receivedInit: RequestInit | undefined;
let callCount = 0;
const result = await invokeReferentialIa( const result = await invokeReferentialIa(
{ {
messages: [{ role: "user", content: "Extrais la facture" }], messages: [{ role: "user", content: "Extrais la facture" }],
@@ -58,8 +59,22 @@ describe("Référentiel IA", () => {
}, },
connection, connection,
async (url, init) => { async (url, init) => {
callCount += 1;
receivedUrl = String(url); receivedUrl = String(url);
receivedInit = init; 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({ return jsonResponse({
id: "ref-1", id: "ref-1",
created: 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(receivedUrl).toBe("https://referentiel-ia.recette.santinova-soft.org/api/integration/v1/invoke");
expect(receivedInit?.headers).toMatchObject({ authorization: "Bearer test-token" }); expect(receivedInit?.headers).toMatchObject({ authorization: "Bearer test-token" });
expect(JSON.parse(String(receivedInit?.body))).toMatchObject({ expect(JSON.parse(String(receivedInit?.body))).toMatchObject({
engineSlug: "santino-expert",
useCase: "texte",
payload: { payload: {
messages: [{ role: "user", content: "Extrais la facture" }], messages: [{ role: "user", content: "Extrais la facture" }],
max_tokens: 1200, max_tokens: 1200,
stream: false, stream: false,
}, },
}); });
expect(callCount).toBe(2);
expect(result.choices[0]?.message.content).toBe("{}"); expect(result.choices[0]?.message.content).toBe("{}");
}); });

View File

@@ -6,6 +6,16 @@ export type ReferentielIaConnection = {
token: string; 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<Response>; type Fetcher = (input: string | URL, init?: RequestInit) => Promise<Response>;
const RECOMMENDATIONS_PATH = "/api/integration/v1/recommendations"; 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}).`); 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<ReferentielIaRecommendation> {
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. */ /** Exécute l'appel sur le moteur décidé et tracé par le Référentiel IA. */
export async function invokeReferentialIa( export async function invokeReferentialIa(
params: InvokeParams, params: InvokeParams,
@@ -75,13 +116,19 @@ export async function invokeReferentialIa(
fetcher: Fetcher = fetch, fetcher: Fetcher = fetch,
): Promise<InvokeResult> { ): Promise<InvokeResult> {
const config = requireConnection(connection); const config = requireConnection(connection);
const recommendation = await getReferentialIaRecommendation(config, fetcher);
const engineSlug = selectedEngineSlug(recommendation);
const response = await fetcher(`${config.baseUrl}${INVOCATION_PATH}`, { const response = await fetcher(`${config.baseUrl}${INVOCATION_PATH}`, {
method: "POST", method: "POST",
headers: { headers: {
"content-type": "application/json", "content-type": "application/json",
authorization: `Bearer ${config.token}`, authorization: `Bearer ${config.token}`,
}, },
body: JSON.stringify({ payload: toGatewayPayload(params) }), body: JSON.stringify({
engineSlug,
useCase: "texte",
payload: toGatewayPayload(params),
}),
signal: AbortSignal.timeout(120_000), signal: AbortSignal.timeout(120_000),
}); });
if (!response.ok) throw gatewayError(response.status); if (!response.ok) throw gatewayError(response.status);
@@ -100,26 +147,7 @@ export async function testReferentialIaConnection(
fetcher: Fetcher = fetch, fetcher: Fetcher = fetch,
): Promise<ReferentielIaConnectionTest> { ): Promise<ReferentielIaConnectionTest> {
const config = requireConnection(connection); const config = requireConnection(connection);
const response = await fetcher(`${config.baseUrl}${RECOMMENDATIONS_PATH}`, { const data = await getReferentialIaRecommendation(config, fetcher);
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;
};
return { return {
applicationName: typeof data.application?.name === "string" ? data.application.name : null, applicationName: typeof data.application?.name === "string" ? data.application.name : null,
engineName: typeof data.decision?.selected?.engine?.name === "string" ? data.decision.selected.engine.name : null, engineName: typeof data.decision?.selected?.engine?.name === "string" ? data.decision.selected.engine.name : null,