feat: finaliser l’intégration au Référentiel IA
All checks were successful
Deploy Production / deploy (push) Has been skipped
Deploy Recette / deploy (push) Successful in 1m4s

This commit is contained in:
manus-admin
2026-10-06 16:58:11 +02:00
parent fd58573685
commit ba34233189
9 changed files with 218 additions and 183 deletions

View File

@@ -7,34 +7,70 @@ export type ResolvedAiTrace = {
origin: string;
};
type ReferenceDecision = {
application?: { slug?: string };
decision?: { selected?: { engine?: { slug?: string } } | null } | null;
};
function configuration() {
const url = ENV.aiReferentielUrl.trim().replace(/\/$/, "");
const token = ENV.aiReferentielToken.trim();
if (!url || !token) {
const applicationSlug = ENV.aiReferentielAppSlug.trim();
if (!url || !token || !applicationSlug) {
throw new Error("Référentiel IA non configuré pour cette application.");
}
return { url, token };
const parsed = new URL(url);
const allowedHosts = new Set([
"referentiel-ia.recette.santinova-soft.org",
"referentiel-ia.santinova-soft.org",
]);
if (parsed.protocol !== "https:" || !allowedHosts.has(parsed.hostname)) {
throw new Error("URL du Référentiel IA non autorisée.");
}
return { url, token, applicationSlug };
}
/** Résout à chaque appel le moteur choisi par le Référentiel, sans cache durable. */
async function resolveSelectedEngine(
currentConfiguration: { url: string; token: string; applicationSlug: string },
fetcher: typeof fetch,
): Promise<string> {
const response = await fetcher(`${currentConfiguration.url}/api/integration/v1/recommendations`, {
method: "POST",
headers: { "content-type": "application/json", authorization: `Bearer ${currentConfiguration.token}` },
body: JSON.stringify({ useCase: "texte", optimizeCost: false, needsFreeTier: false, includeConnection: false }),
signal: AbortSignal.timeout(15_000),
});
if (!response.ok) {
throw new Error(`Référentiel IA : décision indisponible (HTTP ${response.status}).`);
}
const decision = await response.json() as ReferenceDecision;
if (decision.application?.slug !== currentConfiguration.applicationSlug) {
throw new Error("Référentiel IA : la décision ne correspond pas à l’application attendue.");
}
const engineSlug = decision.decision?.selected?.engine?.slug?.trim();
if (!engineSlug) {
throw new Error("Référentiel IA : aucun moteur n’est sélectionné.");
}
return engineSlug;
}
/**
* Le Référentiel décide seul du moteur et écrase tout modèle éventuellement
* présent dans le payload. La trace détaillée est persistée centralement.
* Le Référentiel décide seul du moteur. La Veille transmet ensuite son slug exact
* à la passerelle centrale et ne connaît jamais de clé fournisseur.
*/
export async function invokeSelectedReference<T>(payload: Record<string, unknown>, fetcher: typeof fetch = fetch): Promise<T> {
const { url, token } = configuration();
const response = await fetcher(`${url}/api/integration/v1/invoke`, {
const currentConfiguration = configuration();
const engineSlug = await resolveSelectedEngine(currentConfiguration, fetcher);
const response = await fetcher(`${currentConfiguration.url}/api/integration/v1/invoke`, {
method: "POST",
headers: {
"content-type": "application/json",
authorization: `Bearer ${token}`,
},
body: JSON.stringify({ payload }),
headers: { "content-type": "application/json", authorization: `Bearer ${currentConfiguration.token}` },
body: JSON.stringify({ engineSlug, payload }),
signal: AbortSignal.timeout(120_000),
});
if (!response.ok) {
throw new Error(`Référentiel IA : invocation refusée (HTTP ${response.status}).`);
}
const trace: ResolvedAiTrace = {
provider: response.headers.get("x-santinova-ia-provider") || "unknown",
resolvedModel: response.headers.get("x-santinova-ia-model") || "unknown",
@@ -44,10 +80,7 @@ export async function invokeSelectedReference<T>(payload: Record<string, unknown
console.info(`[IA_TRACE] provider=${trace.provider} model=${trace.resolvedModel} transport=${trace.transport} origin=${trace.origin}`);
const body = await response.json() as unknown;
if (body && typeof body === "object" && "error" in body) {
const message = typeof (body as { error?: { message?: unknown } }).error?.message === "string"
? (body as { error: { message: string } }).error.message
: "erreur non détaillée";
throw new Error(`Référentiel IA : ${message}`);
throw new Error("Référentiel IA : le moteur a renvoyé une erreur métier.");
}
return body as T;
}