Checkpoint: Classification IA en deux étapes (filtre pertinence + classification catégorie) intégrée dans le moteur RSS. Inclut : module aiClassifier.ts avec les deux prompts, intégration dans rssEngine.ts avec fallback sur règles, procédures tRPC reclassifyWithAI pour veille et AAP, badge IA violet dans VeilleDashboard, bouton "Classer avec l'IA" (admin), section "Analyse par l'IA" dans la boîte de dialogue détail, 7 nouveaux tests Vitest (20 tests au total). Migration BDD appliquée (4 nouveaux champs IA dans veille_items et aap_items).

This commit is contained in:
Manus
2026-06-03 05:31:58 -04:00
parent d9cded5970
commit 9c26772617
11 changed files with 1618 additions and 17 deletions

View File

@@ -4,6 +4,7 @@
* et insère les nouveaux articles dans veille_items ou aap_items.
*
* Enrichissement automatique :
* - Classification IA en deux étapes (pertinence + catégorie) avec fallback sur mots-clés
* - AAP : région (toujours Auvergne-Rhône-Alpes) + département extrait du titre/description
* - Veille : territoire, catégorie (Handicap/PA/Enfance/Précarité/Sanitaire/Autre), niveau
*
@@ -21,6 +22,7 @@ import {
type RssFeed,
} from "../drizzle/schema";
import { eq, sql } from "drizzle-orm";
import { classifyArticle } from "./aiClassifier";
// ─── Types internes ───────────────────────────────────────────────────────────
@@ -349,13 +351,26 @@ async function processFeed(feed: RssFeed): Promise<FetchResult> {
const dedupKey = dedupHash(normalizedTitle + "|" + (feed.feedType ?? ""));
if (feed.feedType === "veille") {
const matchedRule = applyAutoRules(title, description, rules);
const typeVeille = (matchedRule?.typeVeille ?? feed.defaultTypeVeille ?? "generale") as
"reglementaire" | "concurrentielle" | "technologique" | "generale";
const categorie = detectVeilleCategorie(fullText);
// ── Classification IA (avec fallback sur mots-clés) ──────────────────
const { niveau, territoire } = detectVeilleNiveauTerritoire(fullText);
const aiResult = await classifyArticle(
title,
description,
() => {
const matchedRule = applyAutoRules(title, description, rules);
return {
categorie: detectVeilleCategorie(fullText),
typeVeille: (matchedRule?.typeVeille ?? feed.defaultTypeVeille ?? "generale") as
"reglementaire" | "concurrentielle" | "technologique" | "generale",
};
}
);
const categorie = aiResult.categorie;
const typeVeille = (aiResult.typeVeille ?? feed.defaultTypeVeille ?? "generale") as
"reglementaire" | "concurrentielle" | "technologique" | "generale";
try {
// Essayer d'insérer
await db.insert(veilleItems).values({
@@ -370,6 +385,10 @@ async function processFeed(feed: RssFeed): Promise<FetchResult> {
lien: link || null,
typeVeille,
datePublication: pubDate,
iaRelevant: aiResult.relevant,
iaCategorie: aiResult.categorie,
iaClassifiedBy: aiResult.classifiedBy,
iaReason: aiResult.reason,
});
result.newItems++;
} catch (e: any) {
@@ -396,12 +415,25 @@ async function processFeed(feed: RssFeed): Promise<FetchResult> {
}
}
} else if (feed.feedType === "aap") {
const matchedRule = applyAutoRules(title, description, rules);
const categorie = (matchedRule?.categorieAap ?? feed.defaultCategorieAap ?? "Autre") as
"Handicap" | "PA" | "Enfance" | "Précarité" | "Sanitaire" | "Autre";
// ── Classification IA pour les AAP ───────────────────────────────────
const { region, departement } = detectAapGeo(fullText);
const aiResult = await classifyArticle(
title,
description,
() => {
const matchedRule = applyAutoRules(title, description, rules);
return {
categorie: (matchedRule?.categorieAap ?? feed.defaultCategorieAap ?? "Autre") as
"Handicap" | "PA" | "Enfance" | "Précarité" | "Sanitaire" | "Autre",
typeVeille: "generale" as const,
};
}
);
const categorie = aiResult.categorie as
"Handicap" | "PA" | "Enfance" | "Précarité" | "Sanitaire" | "Autre";
try {
await db.insert(aapItems).values({
dedupKey,
@@ -412,6 +444,10 @@ async function processFeed(feed: RssFeed): Promise<FetchResult> {
departements: JSON.stringify(departement ? [departement] : []),
lien: link || null,
datePublication: pubDate,
iaRelevant: aiResult.relevant,
iaCategorie: aiResult.categorie,
iaClassifiedBy: aiResult.classifiedBy,
iaReason: aiResult.reason,
});
result.newItems++;
} catch (e: any) {