mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 12:41:54 +00:00
feat(r5.3): écrans IA — assistant web, corpus administrable, suggestions OT/mobile
- Contrat (76 opérations) : Document expose inCorpus/indexedAt/chunkCount,
PATCH /documents/{id}/corpus (ASSETS.edit), POST /assistant/reindex
(bilan chiffré) ; ci-contract vérifie désormais aussi le client mobile.
- Web : page /assistant (chat sourcé — extraits exacts cités, Ouvrir vers
PDF authentifié ou fiche OT, avertissement permanent ; refus honnête
chiffré avec action utile) ; Bibliothèque = corpus (bandeau 09-08,
statut d'indexation par document, interrupteur d'exclusion PDF,
Réindexer tout) ; fiche OT : « Décrire pour suggérer » (Appliquer =
geste humain, liseré « suggéré » retiré au choix manuel).
- Mobile : chips de suggestion dans la clôture (un appui = un champ
pré-rempli, « réseau requis » hors-ligne — la file R4 n'en dépend pas).
- apps/ai : seuils AI_SEUIL_* configurables par env (CI + calibrage).
- CI e2e : service siop2-ai (embeddeur déterministe, seuils calibrés sur
mesures : match 0,66 vs bruit 0,11) + parcours R5 Playwright (PDF généré
xref valide → réindexation → réponse sourcée → refus → suggestion).
- Vérifié : 16/16 Playwright, 78 tests API, 23 pytest, 17 jest-expo ;
chaîne réelle au vrai modèle ONNX (web 7/7, mobile Expo web 6/6).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -42,12 +42,20 @@ const REPONSE_SUGGEST = {
|
||||
],
|
||||
};
|
||||
|
||||
const REPONSE_REINDEX = {
|
||||
documents_indexes: 6,
|
||||
documents_ignores: 2,
|
||||
bilans_indexes: 214,
|
||||
extraits: 180,
|
||||
};
|
||||
|
||||
describe('Assistant (e2e — stub du service IA)', () => {
|
||||
let app: INestApplication;
|
||||
let stub: Server;
|
||||
let ahmed: string; // Technicien : view + edit sur WORK_ORDERS
|
||||
let ahmed: string; // Technicien : view + edit sur WORK_ORDERS, ASSETS en lecture
|
||||
let karim: string; // Demandeur : aucun droit WORK_ORDERS
|
||||
let rachid: string; // Vue seule : view sans edit
|
||||
let nadia: string; // Gestionnaire : ASSETS.edit — administre le corpus
|
||||
const requetesRecues: { url: string; jeton: string | undefined }[] = [];
|
||||
const http = () => request(app.getHttpServer());
|
||||
const auth = (t: string) => ({ Authorization: `Bearer ${t}` });
|
||||
@@ -62,6 +70,7 @@ describe('Assistant (e2e — stub du service IA)', () => {
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
if (req.url === '/internal/ask') res.end(JSON.stringify(REPONSE_ASK));
|
||||
else if (req.url === '/internal/suggest') res.end(JSON.stringify(REPONSE_SUGGEST));
|
||||
else if (req.url === '/internal/reindex') res.end(JSON.stringify(REPONSE_REINDEX));
|
||||
else {
|
||||
res.statusCode = 404;
|
||||
res.end('{}');
|
||||
@@ -87,6 +96,7 @@ describe('Assistant (e2e — stub du service IA)', () => {
|
||||
ahmed = await login('Technicien');
|
||||
karim = await login('Demandeur');
|
||||
rachid = await login('Vue seule');
|
||||
nadia = await login('Gestionnaire');
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -146,6 +156,19 @@ describe('Assistant (e2e — stub du service IA)', () => {
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
it('reindex : traduit le bilan d’indexation, réservé à ASSETS.edit (D3)', async () => {
|
||||
const res = await http().post('/assistant/reindex').set(auth(nadia)).expect(200);
|
||||
expect(res.body).toEqual({
|
||||
documentsIndexed: 6,
|
||||
documentsSkipped: 2,
|
||||
reportsIndexed: 214,
|
||||
chunks: 180,
|
||||
});
|
||||
expect(requetesRecues.at(-1)!.url).toBe('/internal/reindex');
|
||||
// Ahmed (Technicien) lit le parc mais n'administre pas le corpus
|
||||
await http().post('/assistant/reindex').set(auth(ahmed)).expect(403);
|
||||
});
|
||||
|
||||
it('question trop courte : 400 avant tout appel au service IA', async () => {
|
||||
const avant = requetesRecues.length;
|
||||
await http().post('/assistant/ask').set(auth(ahmed)).send({ question: 'ab' }).expect(400);
|
||||
|
||||
Reference in New Issue
Block a user