mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 12:41:54 +00:00
- 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>
95 lines
2.7 KiB
TypeScript
95 lines
2.7 KiB
TypeScript
import {
|
|
BadRequestException,
|
|
Body,
|
|
Controller,
|
|
Delete,
|
|
Get,
|
|
HttpCode,
|
|
Param,
|
|
ParseUUIDPipe,
|
|
Patch,
|
|
Post,
|
|
Query,
|
|
StreamableFile,
|
|
UploadedFile,
|
|
UseInterceptors,
|
|
} from '@nestjs/common';
|
|
import { FileInterceptor } from '@nestjs/platform-express';
|
|
import {
|
|
DOCUMENT_MAX_BYTES,
|
|
DocumentCorpusUpdateSchema,
|
|
type DocumentCorpusUpdate,
|
|
} from '@siop/shared';
|
|
import {
|
|
AuthenticatedUser,
|
|
CurrentUser,
|
|
} from '../auth/current-user.decorator';
|
|
import { ZodValidationPipe } from '../common/zod-validation.pipe';
|
|
import { RequirePermission } from '../permissions/require-permission.decorator';
|
|
import { DocumentsService } from './documents.service';
|
|
|
|
@Controller('documents')
|
|
export class DocumentsController {
|
|
constructor(private readonly documents: DocumentsService) {}
|
|
|
|
/** Bibliothèque interne — authentification seule (les cartes « Documents »
|
|
* des fiches la consomment). */
|
|
@Get()
|
|
list(
|
|
@Query('assetId') assetId?: string,
|
|
@Query('workOrderId') workOrderId?: string,
|
|
@Query('kind') kind?: string,
|
|
) {
|
|
return this.documents.list({ assetId, workOrderId, kind });
|
|
}
|
|
|
|
@Post()
|
|
@UseInterceptors(
|
|
FileInterceptor('file', { limits: { fileSize: DOCUMENT_MAX_BYTES } }),
|
|
)
|
|
upload(
|
|
@UploadedFile() file: Express.Multer.File | undefined,
|
|
@Body() body: { kind?: string; assetId?: string; workOrderId?: string },
|
|
@CurrentUser() user: AuthenticatedUser,
|
|
) {
|
|
if (!file) throw new BadRequestException('Aucun fichier reçu (champ « file »)');
|
|
return this.documents.upload(
|
|
{
|
|
buffer: file.buffer,
|
|
originalName: file.originalname,
|
|
contentType: file.mimetype,
|
|
size: file.size,
|
|
kind: body.kind ?? 'OTHER',
|
|
assetId: body.assetId || undefined,
|
|
workOrderId: body.workOrderId || undefined,
|
|
},
|
|
user,
|
|
);
|
|
}
|
|
|
|
@Get(':id/download')
|
|
async download(@Param('id', ParseUUIDPipe) id: string): Promise<StreamableFile> {
|
|
const { stream, fileName, contentType } = await this.documents.download(id);
|
|
return new StreamableFile(stream as never, {
|
|
type: contentType,
|
|
disposition: `attachment; filename*=UTF-8''${encodeURIComponent(fileName)}`,
|
|
});
|
|
}
|
|
|
|
/** Corpus IA (D3) : réservé aux gestionnaires du référentiel. */
|
|
@Patch(':id/corpus')
|
|
@RequirePermission('ASSETS', 'edit')
|
|
setCorpus(
|
|
@Param('id', ParseUUIDPipe) id: string,
|
|
@Body(new ZodValidationPipe(DocumentCorpusUpdateSchema)) body: DocumentCorpusUpdate,
|
|
) {
|
|
return this.documents.setCorpus(id, body.inCorpus);
|
|
}
|
|
|
|
@Delete(':id')
|
|
@HttpCode(204)
|
|
remove(@Param('id', ParseUUIDPipe) id: string, @CurrentUser() user: AuthenticatedUser) {
|
|
return this.documents.remove(id, user);
|
|
}
|
|
}
|