feat: finaliser l’intégration au Référentiel IA
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user