210 lines
9.0 KiB
TypeScript
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;
|
|
}
|
|
}
|