"""Recherche sémantique dans le corpus (pgvector, distance cosinus). Ne renvoie QUE des extraits sourcés — la brique de « sourcé ou silencieux ». """ from dataclasses import dataclass import asyncpg from .embeddings import Embeddeur from .ingestion import _vecteur_sql @dataclass class ExtraitTrouve: source_type: str document_id: str | None work_order_id: str | None titre: str # nom de fichier ou référence d'OT locator: str content: str score: float # similarité cosinus (0..1) async def chercher( cnx: asyncpg.Connection, embeddeur: Embeddeur, question: str, limite: int = 5, ) -> list[ExtraitTrouve]: vecteur = _vecteur_sql(embeddeur.encoder([question])[0]) lignes = await cnx.fetch( ''' SELECT c."sourceType", c."documentId", c."workOrderId", c.locator, c.content, 1 - (c.embedding <=> $1::vector) AS score, COALESCE(d."fileName", wo.reference, '?') AS titre FROM "RagChunk" c LEFT JOIN "Document" d ON d.id = c."documentId" LEFT JOIN "WorkOrder" wo ON wo.id = c."workOrderId" WHERE c."documentId" IS NULL OR d."inCorpus" -- l'exclusion D3 s'applique aussi à la lecture ORDER BY c.embedding <=> $1::vector LIMIT $2 ''', vecteur, limite, ) return [ ExtraitTrouve( source_type=ligne["sourceType"], document_id=str(ligne["documentId"]) if ligne["documentId"] else None, work_order_id=str(ligne["workOrderId"]) if ligne["workOrderId"] else None, titre=ligne["titre"], locator=ligne["locator"], content=ligne["content"], score=float(ligne["score"]), ) for ligne in lignes ]