feat: prompt IA dédié AAP avec classification par secteur (Handicap/PA/Enfance/Précarité/Sanitaire)

This commit is contained in:
Manus
2026-07-06 10:24:40 -04:00
parent b3d1e9e6ed
commit 2d0de2a30d
2 changed files with 131 additions and 29 deletions

View File

@@ -19,18 +19,28 @@ export type AiTypeVeille =
| "technologique"
| "informationnelle";
export type AiCategorieAap =
| "Handicap"
| "Précarité"
| "Enfance"
| "PA"
| "Sanitaire"
| "Autre";
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;
/** Catégorie AAP déduite par l'IA (null si non AAP ou non pertinent) */
categorieAap: AiCategorieAap | 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 ─────────────────────────────
// ─── Prompt veille stratégique : pertinence + type de veille ─────────────────
const PROMPT_CLASSIFICATION = `Tu es un expert des établissements et services sociaux et médico-sociaux (ESMS) en France.
Analyse attentivement le texte fourni. Détermine si le contenu est pertinent au regard du périmètre d'intervention de l'association, s'il a un impact professionnel direct ou indirect sur les activités, les services, la gestion, la réglementation, le financement, les professionnels, les usagers ou l'environnement stratégique de l'association.
@@ -67,6 +77,46 @@ Réponds UNIQUEMENT avec un objet JSON valide, sans texte autour, sans balises m
Si pertinent est false, typeVeille doit être null.`;
// ─── Prompt AAP : pertinence + catégorie secteur ─────────────────────────────
const PROMPT_CLASSIFICATION_AAP = `Tu es un expert des établissements et services sociaux et médico-sociaux (ESMS) en France.
Analyse attentivement le texte fourni. Détermine si le contenu est pertinent au regard du périmètre d'intervention de l'association, s'il a un impact professionnel direct ou indirect sur les activités, les services, la gestion, la réglementation, le financement, les professionnels, les usagers ou l'environnement stratégique de l'association.
Un article est pertinent UNIQUEMENT s'il traite explicitement d'un sujet en lien avec l'un des 5 secteurs d'activité de l'association :
1. Handicap (enfant et adulte) : services, dispositifs politiques et financement liés au handicap ; ESAT, IME, ITEP, SESSAD, MAS, FAM, SAVS, SAMSAH, MDPH, RQTH, inclusion scolaire, emploi des personnes handicapées
2. Précarité / Logement : les problématiques et situations de précarité, d'exclusion, de vulnérabilité, d'accès aux droits ou d'insertion ; CHRS, SIAO, hébergement d'urgence, insertion par le logement, RSA, sans-abrisme, expulsions
3. Protection de l'enfance : services de protection de l'enfance ; ASE, MECS, famille d'accueil, pupilles de l'État, prévention spécialisée, mineurs non accompagnés
4. Personnes âgées : l'accompagnement des personnes âgées, les soins associés et la perte d'autonomie ; EHPAD, SSIAD, résidences autonomie, APA, dépendance, plan grand âge
5. Sanitaire : la gestion sanitaire et les questions de santé ; SMR (Soins de Suite et de Réadaptation), HAD, cliniques de réadaptation, parcours de soins post-aigus
Un article est NON PERTINENT s'il porte principalement sur l'un des sujets suivants, même s'il mentionne des termes administratifs ou régionaux :
- Sylviculture, forêts, gestion forestière, bois, chasse, pêche
- Agriculture, élevage, viticulture, pêche maritime, filières agroalimentaires
- Urbanisme, construction, permis de construire, plans locaux d'urbanisme (PLU), logement neuf (hors hébergement social)
- Transports, mobilité, infrastructures routières, ferroviaires ou aéroportuaires
- Environnement, biodiversité, eau, énergie, transition écologique (sauf si lien direct avec les ESMS)
- Économie générale, fiscalité des entreprises, commerce, industrie, artisanat
- Défense nationale, sécurité intérieure, police, justice (sauf protection de l'enfance)
- Sport, culture, tourisme (sauf si lien direct avec des usagers ESMS)
- Enseignement supérieur, recherche (sauf si lien direct avec le secteur médico-social)
- Nominations administratives sans lien avec les secteurs ci-dessus
En cas de doute, réponds pertinent: false — il vaut mieux rejeter un article ambigu que d'en laisser passer un hors périmètre.
Si l'article est pertinent, classe-le dans l'une des 5 catégories suivantes (utilise exactement ces valeurs) :
- "Handicap" : lien avec le secteur handicap (enfant ou adulte)
- "Précarité" : lien avec la précarité, l'exclusion, le logement social ou l'insertion
- "Enfance" : lien avec la protection de l'enfance
- "PA" : lien avec les personnes âgées ou la perte d'autonomie
- "Sanitaire" : lien avec la santé, les soins ou le sanitaire
Si l'article est pertinent mais ne correspond clairement à aucune des 5 catégories, utilise "Autre".
Réponds UNIQUEMENT avec un objet JSON valide, sans texte autour, sans balises markdown :
{"pertinent": true/false, "categorie": "Handicap"|"Précarité"|"Enfance"|"PA"|"Sanitaire"|"Autre"|null, "raison": "explication courte en une phrase"}
Si pertinent est false, categorie doit être null.`;
// ─── Prompt résumé IA ─────────────────────────────────────────────────────────
const PROMPT_RESUME = `Tu es un expert des établissements et services sociaux et médico-sociaux (ESMS).
@@ -100,14 +150,8 @@ function parseJsonSafe<T>(text: string): T | null {
// ─── Classification principale ────────────────────────────────────────────────
/**
* Classifie un article en une seule étape via l'IA.
* Détermine simultanément la pertinence et le type de veille.
* Classifie un article de veille stratégique : pertinence + 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
* @param contenuPage Premiers paragraphes de l'article complet (optionnel, améliore la précision)
*/
export async function classifyArticle(
titre: string,
@@ -115,7 +159,6 @@ export async function classifyArticle(
fallbackFn: () => { typeVeille: AiTypeVeille },
contenuPage?: string | null
): Promise<AiClassificationResult> {
// Construire le contenu utilisateur en enrichissant avec le contenu de la page si disponible
const contenuSection = contenuPage
? `\n\nExtrait de l'article (premiers paragraphes) : ${contenuPage}`
: "";
@@ -138,10 +181,7 @@ export async function classifyArticle(
pertinent: { type: "boolean" },
typeVeille: {
anyOf: [
{
type: "string",
enum: ["reglementaire", "concurrentielle", "technologique", "informationnelle"],
},
{ type: "string", enum: ["reglementaire", "concurrentielle", "technologique", "informationnelle"] },
{ type: "null" },
],
},
@@ -156,29 +196,94 @@ export async function classifyArticle(
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);
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,
categorieAap: null,
reason: parsed.raison,
classifiedBy: "ia",
};
} catch (e) {
console.error("[AI Classifier] Erreur classification:", (e as Error).message);
// Fallback sur les règles
console.error("[AI Classifier] Erreur classification veille:", (e as Error).message);
const fb = fallbackFn();
return {
relevant: true,
typeVeille: fb.typeVeille,
categorieAap: null,
reason: "Classification par règles (erreur LLM)",
classifiedBy: "rules",
};
}
}
/**
* Classifie un appel à projet : pertinence + catégorie secteur (Handicap/PA/Enfance/Précarité/Sanitaire).
* Retourne toujours un résultat (fallback sur rules en cas d'erreur).
*/
export async function classifyAap(
titre: string,
resume: string,
fallbackCategorie: AiCategorieAap,
contenuPage?: string | null
): Promise<AiClassificationResult> {
const contenuSection = contenuPage
? `\n\nExtrait de l'article (premiers paragraphes) : ${contenuPage}`
: "";
const userContent = `Titre : ${titre}\n\nRésumé RSS : ${resume}${contenuSection}`;
try {
const response = await invokeLLM({
messages: [
{ role: "system", content: PROMPT_CLASSIFICATION_AAP },
{ role: "user", content: userContent },
],
response_format: {
type: "json_schema",
json_schema: {
name: "classification_aap_result",
strict: true,
schema: {
type: "object",
properties: {
pertinent: { type: "boolean" },
categorie: {
anyOf: [
{ type: "string", enum: ["Handicap", "Précarité", "Enfance", "PA", "Sanitaire", "Autre"] },
{ type: "null" },
],
},
raison: { type: "string" },
},
required: ["pertinent", "categorie", "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; categorie: AiCategorieAap | null; raison: string }>(content);
if (!parsed) throw new Error("JSON LLM invalide");
return {
relevant: parsed.pertinent,
typeVeille: null,
categorieAap: parsed.pertinent ? (parsed.categorie ?? "Autre") : null,
reason: parsed.raison,
classifiedBy: "ia",
};
} catch (e) {
console.error("[AI Classifier] Erreur classification AAP:", (e as Error).message);
return {
relevant: true,
typeVeille: null,
categorieAap: fallbackCategorie,
reason: "Classification par règles (erreur LLM)",
classifiedBy: "rules",
};