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:
@@ -35,6 +35,10 @@ import {
|
||||
import { importVeille, importAAP, runFullImport, getImportConfig } from "./importer";
|
||||
import { scheduleRssFetch } from "./_core/index";
|
||||
import { loginLocalUser, hashPassword, ensureAdminExists } from "./localAuth";
|
||||
import { classifyArticle } from "./aiClassifier";
|
||||
import { getDb } from "./db";
|
||||
import { veilleItems, aapItems } from "../drizzle/schema";
|
||||
import { isNull, or, eq as eqDrizzle } from "drizzle-orm";
|
||||
|
||||
// ─── Middleware admin ─────────────────────────────────────────────────────────
|
||||
|
||||
@@ -105,6 +109,49 @@ export const appRouter = router({
|
||||
const count = await purgeVeilleItems();
|
||||
return { success: true, deleted: count };
|
||||
}),
|
||||
|
||||
reclassifyWithAI: adminProcedure
|
||||
.input(z.object({ limit: z.number().int().positive().max(200).default(50) }))
|
||||
.mutation(async ({ input }) => {
|
||||
const db = await getDb();
|
||||
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "DB indisponible" });
|
||||
|
||||
// Récupérer les articles non encore classés par l'IA
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(veilleItems)
|
||||
.where(or(isNull(veilleItems.iaClassifiedBy), isNull(veilleItems.iaRelevant)))
|
||||
.limit(input.limit);
|
||||
|
||||
let processed = 0;
|
||||
let errors = 0;
|
||||
|
||||
for (const row of rows) {
|
||||
try {
|
||||
const aiResult = await classifyArticle(
|
||||
row.titre || "",
|
||||
row.resume || "",
|
||||
() => ({ categorie: (row.categorie as any) || "Autre", typeVeille: row.typeVeille || "generale" })
|
||||
);
|
||||
await db
|
||||
.update(veilleItems)
|
||||
.set({
|
||||
categorie: aiResult.categorie,
|
||||
typeVeille: (aiResult.typeVeille ?? row.typeVeille ?? "generale") as any,
|
||||
iaRelevant: aiResult.relevant,
|
||||
iaCategorie: aiResult.categorie,
|
||||
iaClassifiedBy: aiResult.classifiedBy,
|
||||
iaReason: aiResult.reason,
|
||||
})
|
||||
.where(eqDrizzle(veilleItems.id, row.id));
|
||||
processed++;
|
||||
} catch {
|
||||
errors++;
|
||||
}
|
||||
}
|
||||
|
||||
return { processed, errors, total: rows.length };
|
||||
}),
|
||||
}),
|
||||
// ─── AAPP ────────────────────────────────────────────────────────────────────
|
||||
aap: router({
|
||||
@@ -134,6 +181,47 @@ export const appRouter = router({
|
||||
const count = await purgeAapItems();
|
||||
return { success: true, deleted: count };
|
||||
}),
|
||||
|
||||
reclassifyWithAI: adminProcedure
|
||||
.input(z.object({ limit: z.number().int().positive().max(200).default(50) }))
|
||||
.mutation(async ({ input }) => {
|
||||
const db = await getDb();
|
||||
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "DB indisponible" });
|
||||
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(aapItems)
|
||||
.where(or(isNull(aapItems.iaClassifiedBy), isNull(aapItems.iaRelevant)))
|
||||
.limit(input.limit);
|
||||
|
||||
let processed = 0;
|
||||
let errors = 0;
|
||||
|
||||
for (const row of rows) {
|
||||
try {
|
||||
const aiResult = await classifyArticle(
|
||||
row.titre || "",
|
||||
"",
|
||||
() => ({ categorie: (row.categorie as any) || "Autre", typeVeille: "generale" as const })
|
||||
);
|
||||
await db
|
||||
.update(aapItems)
|
||||
.set({
|
||||
categorie: aiResult.categorie as any,
|
||||
iaRelevant: aiResult.relevant,
|
||||
iaCategorie: aiResult.categorie,
|
||||
iaClassifiedBy: aiResult.classifiedBy,
|
||||
iaReason: aiResult.reason,
|
||||
})
|
||||
.where(eqDrizzle(aapItems.id, row.id));
|
||||
processed++;
|
||||
} catch {
|
||||
errors++;
|
||||
}
|
||||
}
|
||||
|
||||
return { processed, errors, total: rows.length };
|
||||
}),
|
||||
}),
|
||||
// ─── Importt ─────────────────────────────────────────────────────────────────
|
||||
import: router({
|
||||
|
||||
Reference in New Issue
Block a user