Files
veille-reglementaire/server/ai-reference.ts
manus-admin ba34233189
All checks were successful
Deploy Production / deploy (push) Has been skipped
Deploy Recette / deploy (push) Successful in 1m4s
feat: finaliser l’intégration au Référentiel IA
2026-10-06 16:58:11 +02:00

55 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { ENV } from "./_core/env";
export type AiEngineStatus = {
state: "available" | "unconfigured" | "authentication_error" | "unreachable";
engine?: { name: string; provider: string; slug: string; model?: string; configured: boolean; origin: "proposal" | "manual" | "local"; operationalStatus: "ready" | "not_tested" | "not_configured" | "not_functional" | "unavailable"; rationale?: string };
checkedAt: string;
message: string;
};
type ReferenceConfiguration = { url: string; token: string; applicationSlug: string };
type DecisionEngine = { origin?: "proposal" | "manual" | "local"; rationale?: string; engine?: { name?: string; provider?: string; slug?: string }; connection?: { model?: string; configured?: boolean; testStatus?: "not_tested" | "success" | "failed" } };
type RecommendationResponse = { application?: { slug?: string }; decision?: { selected?: DecisionEngine | null } | null };
const now = () => new Date().toISOString();
const configuration = (): ReferenceConfiguration => ({ url: ENV.aiReferentielUrl, token: ENV.aiReferentielToken, applicationSlug: ENV.aiReferentielAppSlug });
function operationalStatus(selected: DecisionEngine): NonNullable<AiEngineStatus["engine"]>["operationalStatus"] {
if (selected.origin === "local") return selected.connection?.configured ? "ready" : "unavailable";
if (!selected.connection?.configured) return "not_configured";
if (selected.connection.testStatus === "success") return "ready";
if (selected.connection.testStatus === "failed") return "not_functional";
return "not_tested";
}
/** Test serveur à serveur : décision seulement, sans lancer d’inférence fournisseur. */
export async function getAiEngineStatus(input: ReferenceConfiguration = configuration(), fetcher: typeof fetch = fetch): Promise<AiEngineStatus> {
const checkedAt = now();
if (!input.url || !input.token || !input.applicationSlug) return { state: "unconfigured", checkedAt, message: "Le Référentiel IA n’est pas configuré pour cette application." };
let baseUrl: URL;
try { baseUrl = new URL(input.url); } catch { return { state: "unconfigured", checkedAt, message: "L’URL du Référentiel IA est invalide." }; }
if (baseUrl.protocol !== "https:" || !["referentiel-ia.recette.santinova-soft.org", "referentiel-ia.santinova-soft.org"].includes(baseUrl.hostname)) return { state: "unconfigured", checkedAt, message: "L’URL du Référentiel IA n’est pas autorisée." };
try {
const response = await fetcher(`${baseUrl.toString().replace(/\/$/, "")}/api/integration/v1/recommendations`, {
method: "POST", headers: { Authorization: `Bearer ${input.token}`, "Content-Type": "application/json" },
body: JSON.stringify({ useCase: "texte", optimizeCost: false, needsFreeTier: false, includeConnection: true }), signal: AbortSignal.timeout(15_000),
});
if (response.status === 401) return { state: "authentication_error", checkedAt, message: "Erreur d’authentification auprès du Référentiel IA." };
if (!response.ok) return { state: "unreachable", checkedAt, message: `Le Référentiel IA est indisponible (HTTP ${response.status}).` };
const payload = await response.json() as RecommendationResponse;
if (payload.application?.slug !== input.applicationSlug) return { state: "authentication_error", checkedAt, message: "La décision Référentiel ne correspond pas à cette application." };
const selected = payload.decision?.selected;
if (!selected?.engine?.name || !selected.engine.provider || !selected.engine.slug) return { state: "unconfigured", checkedAt, message: "Le Référentiel IA n’a retourné aucun moteur sélectionné." };
const origin = selected.origin === "manual" || selected.origin === "local" ? selected.origin : "proposal";
const status = operationalStatus(selected);
return { state: "available", checkedAt, message: status === "ready" ? "Connexion établie et moteur prêt." : "Connexion établie ; le moteur sélectionné nécessite une action dans le Référentiel.", engine: { name: selected.engine.name, provider: selected.engine.provider, slug: selected.engine.slug, model: selected.connection?.model, configured: Boolean(selected.connection?.configured), origin, operationalStatus: status, rationale: selected.rationale } };
} catch { return { state: "unreachable", checkedAt, message: "La vérification auprès du Référentiel IA a échoué." }; }
}
/** Conserve seulement les métadonnées affichables ; aucun secret ne quitte le backend. */
export async function getActiveAiConfiguration() {
const status = await getAiEngineStatus();
if (status.state !== "available" || !status.engine?.configured || !status.engine.model) return { active: false, engines: [] };
return { active: true, engines: [{ provider: status.engine.provider, model: status.engine.model, role: "Moteur sélectionné par le Référentiel IA", billing: status.engine.origin === "local" ? "free" : "paid", source: "referential" }] };
}