Files
veille-reglementaire/server/ai-reference.ts
manus-admin 9db76af02c
All checks were successful
Deploy Production / deploy (push) Has been skipped
Deploy Recette / deploy (push) Successful in 1m32s
Validation applicative / TypeScript, tests et build (push) Successful in 2m42s
feat: centraliser le moteur IA via le Référentiel
2026-09-14 08:35:32 +00:00

168 lines
5.5 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" | "unreachable";
engine?: {
name: string;
provider: string;
slug: string;
model?: string;
accessMode?: "gateway" | "recommendation_only";
configured: boolean;
origin: "proposal" | "manual" | "local";
rationale?: string;
};
proposal?: { name: string; provider: string; model?: string; origin: "proposal"; rationale?: string };
usage?: UsageSummary;
checkedAt: string;
message: string;
};
type ReferenceConfiguration = {
url: string;
token: string;
};
type RecommendationResponse = {
decision?: {
proposal?: DecisionEngine;
selected?: DecisionEngine;
};
recommendations?: DecisionEngine[];
usage?: UsageSummary;
};
type UsageMetric = { used: number; limit: number | null; percent: number | null; status: "not_configured" | "normal" | "alert" | "limit_reached" | "unpriced"; shouldBlock?: boolean };
type UsageAlert = { metric: "budget" | "requests" | "tokens"; label: string; status: "alert" | "limit_reached"; percent: number; message: string };
type UsageSummary = { canInvoke: boolean; blockReason?: string | null; budget: UsageMetric & { currency: string; estimated?: boolean }; requests: UsageMetric; tokens: UsageMetric; alerts: UsageAlert[] };
type DecisionEngine = {
origin?: "proposal" | "manual" | "local";
rationale?: string;
engine?: { name?: string; provider?: string; slug?: string };
connection?: { model?: string; accessMode?: "gateway" | "recommendation_only"; configured?: boolean };
};
const now = () => new Date().toISOString();
export async function getAiEngineStatus(
configuration: ReferenceConfiguration = {
url: ENV.aiReferentielUrl,
token: ENV.aiReferentielToken,
},
fetcher: typeof fetch = fetch,
): Promise<AiEngineStatus> {
if (!configuration.url || !configuration.token) {
return {
state: "unconfigured",
checkedAt: now(),
message: "Le Référentiel IA n’est pas encore configuré pour cette application.",
};
}
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 6_000);
try {
const response = await fetcher(`${configuration.url.replace(/\/$/, "")}/api/integration/v1/recommendations`, {
method: "POST",
headers: {
Authorization: `Bearer ${configuration.token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
useCase: "texte",
optimizeCost: true,
needsFreeTier: true,
includeConnection: true,
}),
signal: controller.signal,
});
if (!response.ok) {
return {
state: "unreachable",
checkedAt: now(),
message: `Le Référentiel IA est indisponible (HTTP ${response.status}).`,
};
}
const payload = (await response.json()) as RecommendationResponse;
const selected = payload.decision?.selected ?? payload.recommendations?.[0];
if (!selected?.engine?.name || !selected.engine.provider || !selected.engine.slug) {
return {
state: "unreachable",
checkedAt: now(),
message: "Le Référentiel IA n’a retourné aucun moteur autorisé.",
};
}
const proposal = payload.decision?.proposal;
const origin = selected.origin === "manual" || selected.origin === "local" ? selected.origin : "proposal";
return {
state: "available",
checkedAt: now(),
message: selected.connection?.configured
? origin === "local"
? "Moteur IA local sélectionné par le Référentiel IA."
: origin === "manual"
? "Moteur choisi par un administrateur pour cette application."
: "Proposition du Référentiel IA appliquée à cette application."
: "Moteur décidé par le Référentiel IA, en attente de configuration fournisseur.",
engine: {
name: selected.engine.name,
provider: selected.engine.provider,
slug: selected.engine.slug,
model: selected.connection?.model,
accessMode: selected.connection?.accessMode,
configured: Boolean(selected.connection?.configured),
origin,
rationale: selected.rationale,
},
proposal: proposal?.engine?.name && proposal.engine.provider ? {
name: proposal.engine.name,
provider: proposal.engine.provider,
model: proposal.connection?.model,
origin: "proposal",
rationale: proposal.rationale,
} : undefined,
usage: payload.usage,
};
} catch {
return {
state: "unreachable",
checkedAt: now(),
message: "La vérification auprès du Référentiel IA a échoué.",
};
} finally {
clearTimeout(timeout);
}
}
/**
* Métadonnées strictement descriptives de la décision active. Cette fonction
* ne retourne jamais le jeton applicatif ni une URL d'intégration privée.
*/
export async function getActiveAiConfiguration(
configuration: ReferenceConfiguration = {
url: ENV.aiReferentielUrl,
token: ENV.aiReferentielToken,
},
fetcher: typeof fetch = fetch,
) {
const status = await getAiEngineStatus(configuration, fetcher);
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",
}],
};
}