Checkpoint: reclassifyWithAI traite désormais NULL + rules, supprime les non pertinents et conserve les tombstones

This commit is contained in:
Manus
2026-07-06 08:31:02 -04:00
parent 55f3d5a5d4
commit 7261a7d33e
3 changed files with 54 additions and 30 deletions

View File

@@ -1,4 +1,4 @@
{ {
"version": "2bfe4358", "version": "aa6b358c",
"timestamp": 1783252262810 "timestamp": 1783341062192
} }

View File

@@ -37,7 +37,7 @@ import { scheduleDailyImport } from "./_core/index";
import { loginLocalUser, hashPassword, ensureAdminExists } from "./localAuth"; import { loginLocalUser, hashPassword, ensureAdminExists } from "./localAuth";
import { classifyArticle } from "./aiClassifier"; import { classifyArticle } from "./aiClassifier";
import { getDb } from "./db"; import { getDb } from "./db";
import { veilleItems, aapItems, articleReads } from "../drizzle/schema"; import { veilleItems, aapItems, articleReads, processedDedupKeys } from "../drizzle/schema";
import { isNull, or, eq as eqDrizzle, and, inArray, count } from "drizzle-orm"; import { isNull, or, eq as eqDrizzle, and, inArray, count } from "drizzle-orm";
// ─── Middleware admin ───────────────────────────────────────────────────────── // ─── Middleware admin ─────────────────────────────────────────────────────────
@@ -116,14 +116,15 @@ export const appRouter = router({
const db = await getDb(); const db = await getDb();
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "DB indisponible" }); if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "DB indisponible" });
// Récupérer les articles non encore classés par l'IA // Récupérer les articles non encore classés par l'IA OU classés par le fallback rules
const rows = await db const rows = await db
.select() .select()
.from(veilleItems) .from(veilleItems)
.where(or(isNull(veilleItems.iaClassifiedBy), isNull(veilleItems.iaRelevant))) .where(or(isNull(veilleItems.iaClassifiedBy), isNull(veilleItems.iaRelevant), eqDrizzle(veilleItems.iaClassifiedBy, "rules")))
.limit(input.limit); .limit(input.limit);
let processed = 0; let processed = 0;
let deleted = 0;
let errors = 0; let errors = 0;
for (const row of rows) { for (const row of rows) {
@@ -133,22 +134,34 @@ export const appRouter = router({
row.resume || "", row.resume || "",
() => ({ typeVeille: (row.typeVeille || "informationnelle") as "reglementaire" | "concurrentielle" | "technologique" | "informationnelle" }) () => ({ typeVeille: (row.typeVeille || "informationnelle") as "reglementaire" | "concurrentielle" | "technologique" | "informationnelle" })
); );
await db if (!aiResult.relevant) {
.update(veilleItems) // Supprimer l'article non pertinent
.set({ await db.delete(veilleItems).where(eqDrizzle(veilleItems.id, row.id));
typeVeille: (aiResult.typeVeille ?? row.typeVeille ?? "informationnelle") as any, // Conserver le tombstone pour éviter la réinsertion
iaRelevant: aiResult.relevant, if (row.dedupKey) {
iaClassifiedBy: aiResult.classifiedBy, await db.insert(processedDedupKeys)
iaReason: aiResult.reason, .values({ dedupKey: row.dedupKey, feedType: "veille" })
}) .onDuplicateKeyUpdate({ set: { dedupKey: row.dedupKey } });
.where(eqDrizzle(veilleItems.id, row.id)); }
processed++; deleted++;
} else {
await db
.update(veilleItems)
.set({
typeVeille: (aiResult.typeVeille ?? row.typeVeille ?? "informationnelle") as any,
iaRelevant: aiResult.relevant,
iaClassifiedBy: aiResult.classifiedBy,
iaReason: aiResult.reason,
})
.where(eqDrizzle(veilleItems.id, row.id));
processed++;
}
} catch { } catch {
errors++; errors++;
} }
} }
return { processed, errors, total: rows.length }; return { processed, deleted, errors, total: rows.length };
}), }),
// ─── Marquage lu/non lu ────────────────────────────────────────────────────────────────────── // ─── Marquage lu/non lu ──────────────────────────────────────────────────────────────────────
@@ -240,34 +253,45 @@ export const appRouter = router({
const rows = await db const rows = await db
.select() .select()
.from(aapItems) .from(aapItems)
.where(or(isNull(aapItems.iaClassifiedBy), isNull(aapItems.iaRelevant))) .where(or(isNull(aapItems.iaClassifiedBy), isNull(aapItems.iaRelevant), eqDrizzle(aapItems.iaClassifiedBy, "rules")))
.limit(input.limit); .limit(input.limit);
let processed = 0; let processed = 0;
let deleted = 0;
let errors = 0; let errors = 0;
for (const row of rows) { for (const row of rows) {
try { try {
const aiResult = await classifyArticle( const aiResult = await classifyArticle(
row.titre || "", row.titre || "",
"", row.resume || "",
() => ({ typeVeille: "informationnelle" as const }) () => ({ typeVeille: "informationnelle" as const })
); );
await db if (!aiResult.relevant) {
.update(aapItems) await db.delete(aapItems).where(eqDrizzle(aapItems.id, row.id));
.set({ if (row.dedupKey) {
iaRelevant: aiResult.relevant, await db.insert(processedDedupKeys)
iaClassifiedBy: aiResult.classifiedBy, .values({ dedupKey: row.dedupKey, feedType: "aap" })
iaReason: aiResult.reason, .onDuplicateKeyUpdate({ set: { dedupKey: row.dedupKey } });
}) }
.where(eqDrizzle(aapItems.id, row.id)); deleted++;
processed++; } else {
await db
.update(aapItems)
.set({
iaRelevant: aiResult.relevant,
iaClassifiedBy: aiResult.classifiedBy,
iaReason: aiResult.reason,
})
.where(eqDrizzle(aapItems.id, row.id));
processed++;
}
} catch { } catch {
errors++; errors++;
} }
} }
return { processed, errors, total: rows.length }; return { processed, deleted, errors, total: rows.length };
}), }),
// ─── Marquage lu/non lu AAP ────────────────────────────────────────────────────────────────────── // ─── Marquage lu/non lu AAP ──────────────────────────────────────────────────────────────────────

View File

@@ -187,5 +187,5 @@
- [x] Afficher la prochaine date d'exécution dynamique dans les Paramètres (selon mode/heure/jour choisi) - [x] Afficher la prochaine date d'exécution dynamique dans les Paramètres (selon mode/heure/jour choisi)
- [x] Ajouter la règle de rétention configurable dans les Paramètres (supprimer articles > N mois) - [x] Ajouter la règle de rétention configurable dans les Paramètres (supprimer articles > N mois)
- [x] Implémenter la purge automatique au démarrage du serveur selon la règle de rétention - [x] Implémenter la purge automatique au démarrage du serveur selon la règle de rétention
- [ ] Déployer en recette - [x] Déployer en recette
- [ ] Répliquer BDD recette vers production et déployer le code en production - [x] Répliquer BDD recette vers production et déployer le code en production