Files
veille-reglementaire/server/aiClassifier.ts

210 lines
9.0 KiB
TypeScript

/**
* Classification IA des articles RSS en une seule étape :
*
* Le LLM détermine simultanément :
* 1. Si l'article est pertinent pour le secteur médico-social
* 2. Le type de veille (réglementaire, concurrentielle, technologique, informationnelle)
*
* En cas d'erreur LLM (timeout, quota, JSON malformé), le module retourne
* classifiedBy = "rules" pour signaler que le fallback doit être utilisé.
*/
import { invokeLLM } from "./_core/llm";
// ─── Types ────────────────────────────────────────────────────────────────────
export type AiTypeVeille =
| "reglementaire"
| "concurrentielle"
| "technologique"
| "informationnelle";
export interface AiClassificationResult {
/** L'article est-il pertinent pour le secteur médico-social ? */
relevant: boolean;
/** Type de veille déduit par l'IA (null si non pertinent) */
typeVeille: AiTypeVeille | null;
/** Explication courte de la décision */
reason: string;
/** Indique si la classification a été faite par l'IA ou par les règles (fallback) */
classifiedBy: "ia" | "rules";
}
// ─── Prompt unique : pertinence + type de veille ─────────────────────────────
const PROMPT_CLASSIFICATION = `Tu es un expert des établissements et services sociaux et médico-sociaux (ESMS) en France, spécialisé dans les secteurs : handicap, personnes âgées, protection de l'enfance, précarité/insertion, sanitaire.
Ta mission est d'analyser un article et de déterminer :
1. S'il est DIRECTEMENT pertinent pour une association gestionnaire d'ESMS
2. Si oui, à quel type de veille il appartient
Un article est pertinent UNIQUEMENT s'il traite explicitement de l'un des sujets suivants :
- Réglementation, lois, décrets, circulaires concernant les ESMS, le secteur social ou médico-social
- Financement, tarification, dotations des structures sociales et médico-sociales (CPOM, SERAFIN-PH, etc.)
- Politiques publiques ciblant les personnes handicapées, âgées, enfants en danger, personnes précaires
- Fonctionnement, organisation, ressources humaines des ESMS
- Droits et accompagnement des usagers des ESMS
- Institutions directement liées : ARS, MDPH, ASE, conseils départementaux (action sociale), CAF, CNSA
- Santé publique avec impact direct sur les ESMS ou leurs usagers
Un article est NON PERTINENT si :
- Il traite de politique générale, économie, international, environnement, sport, culture, agriculture, pêche, tourisme, immobilier, technologie grand public, rénovation énergétique, etc.
- Il mentionne des personnes vulnérables de façon anecdotique sans lien avec les ESMS
- Il concerne la santé uniquement sous l'angle hospitalier ou médical sans lien avec le médico-social
- Le lien avec le secteur médico-social est vague, indirect ou nécessite plusieurs degrés de déduction
Sois STRICT : en cas de doute, réponds pertinent: false.
Si l'article est pertinent, classe-le dans l'un des types de veille suivants :
- "reglementaire" : textes de loi, décrets, circulaires, obligations légales, réformes institutionnelles, instructions ministérielles
- "concurrentielle" : actualités d'autres associations ou structures, appels d'offres, marchés, positionnement sectoriel, benchmarks
- "technologique" : innovations numériques, outils, systèmes d'information, nouvelles pratiques professionnelles, recherche appliquée
- "informationnelle" : informations générales pertinentes pour le secteur sans caractère réglementaire, concurrentiel ou technologique fort (rapports, études, événements sectoriels, nominations, etc.)
Réponds UNIQUEMENT avec un objet JSON valide, sans texte autour, sans balises markdown :
{"pertinent": true/false, "typeVeille": "reglementaire"|"concurrentielle"|"technologique"|"informationnelle"|null, "raison": "explication courte en une phrase"}
Si pertinent est false, typeVeille doit être null.`;
// ─── Prompt résumé IA ─────────────────────────────────────────────────────────
const PROMPT_RESUME = `Tu es un expert des établissements et services sociaux et médico-sociaux (ESMS).
Tu dois rédiger un résumé clair et professionnel de l'article fourni, destiné aux professionnels du secteur médico-social.
Consignes :
- Rédige 3 à 5 phrases en français, en langage clair et accessible
- Mets en avant les points clés : qui est concerné, quelle mesure ou information, quel impact pour les structures
- Reste factuel et objectif, sans jugement de valeur
- N'utilise pas de listes à puces, écris en prose
- Ne commence pas par "Cet article" ou "Le texte"
- Adapte le vocabulaire au secteur médico-social (ESMS, ARS, MDPH, etc.)
Réponds UNIQUEMENT avec le texte du résumé, sans introduction ni conclusion.`;
// ─── Utilitaire : parse JSON robuste ─────────────────────────────────────────
function parseJsonSafe<T>(text: string): T | null {
try {
const cleaned = text
.replace(/^```json\s*/i, "")
.replace(/^```\s*/i, "")
.replace(/```\s*$/i, "")
.trim();
return JSON.parse(cleaned) as T;
} catch {
return null;
}
}
// ─── Classification principale ────────────────────────────────────────────────
/**
* Classifie un article en une seule étape via l'IA.
* Détermine simultanément la pertinence et le type de veille.
* Retourne toujours un résultat (fallback sur rules en cas d'erreur).
*
* @param titre Titre de l'article
* @param resume Résumé / description de l'article
* @param fallbackFn Fonction de fallback appelée si l'IA échoue
*/
export async function classifyArticle(
titre: string,
resume: string,
fallbackFn: () => { typeVeille: AiTypeVeille }
): Promise<AiClassificationResult> {
const userContent = `Titre : ${titre}\n\nRésumé : ${resume}`;
try {
const response = await invokeLLM({
messages: [
{ role: "system", content: PROMPT_CLASSIFICATION },
{ role: "user", content: userContent },
],
response_format: {
type: "json_schema",
json_schema: {
name: "classification_result",
strict: true,
schema: {
type: "object",
properties: {
pertinent: { type: "boolean" },
typeVeille: {
anyOf: [
{
type: "string",
enum: ["reglementaire", "concurrentielle", "technologique", "informationnelle"],
},
{ type: "null" },
],
},
raison: { type: "string" },
},
required: ["pertinent", "typeVeille", "raison"],
additionalProperties: false,
},
},
},
});
const rawContent = response?.choices?.[0]?.message?.content;
if (!rawContent) throw new Error("Réponse LLM vide");
const content = typeof rawContent === "string" ? rawContent : JSON.stringify(rawContent);
const parsed = parseJsonSafe<{
pertinent: boolean;
typeVeille: AiTypeVeille | null;
raison: string;
}>(content);
if (!parsed) throw new Error("JSON LLM invalide");
return {
relevant: parsed.pertinent,
typeVeille: parsed.pertinent ? (parsed.typeVeille ?? "informationnelle") : null,
reason: parsed.raison,
classifiedBy: "ia",
};
} catch (e) {
console.error("[AI Classifier] Erreur classification:", (e as Error).message);
// Fallback sur les règles
const fb = fallbackFn();
return {
relevant: true,
typeVeille: fb.typeVeille,
reason: "Classification par règles (erreur LLM)",
classifiedBy: "rules",
};
}
}
// ─── Génération de résumé IA ──────────────────────────────────────────────────
/**
* Génère un résumé IA de 3-5 phrases pour un article pertinent.
* Retourne null en cas d'échec (le résumé brut RSS sera utilisé à la place).
*/
export async function generateSummary(
titre: string,
resume: string
): Promise<string | null> {
const userContent = `Titre : ${titre}\n\nContenu : ${resume}`;
try {
const response = await invokeLLM({
messages: [
{ role: "system", content: PROMPT_RESUME },
{ role: "user", content: userContent },
],
});
const rawContent = response?.choices?.[0]?.message?.content;
if (!rawContent) return null;
const text = typeof rawContent === "string" ? rawContent.trim() : null;
return text && text.length > 10 ? text : null;
} catch (e) {
console.error("[AI Classifier] Erreur génération résumé:", (e as Error).message);
return null;
}
}