feat: prompt IA dédié AAP avec classification par secteur (Handicap/PA/Enfance/Précarité/Sanitaire)
This commit is contained in:
@@ -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",
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user