Spaces:
Runtime error
Runtime error
Commit History
21 days ago
-
feat(embeddings): voyage-4-large remplace EmbeddingGemma (API Voyage, halfvec 1024)
Mesure sur tout le corpus (32 303 morceaux) et le jeu relu de 102 questions : en recherche dense, MRR@20 0,758 contre 0,537 pour EmbeddingGemma en francais, 0,680 contre 0,537 en anglais. Par le chemin du chat lui-meme (mots-cles en plein texte, question entiere vectorisee, 8 extraits envoyes au modele), l'article attendu figure parmi les extraits 92 % du temps en francais contre 61 % avec Gemma (MRR@8 0,504 contre 0,336), et 87 % en anglais contre 56 % (0,538 contre 0,282). Vectoriser la question entiere plutot que ses mots-cles gagne a lui seul +0,087 de MRR@8 en francais [IC 95 % +0,035 ; +0,143] et +0,127 en anglais [+0,067 ; +0,191]. - embedding_service.py : VoyageProvider (httpx, connexion gardee : 0,42 s par question contre 1,14 s avec une connexion neuve). Son corps de requete reproduit les vecteurs deja calcules du corpus : un test voyage_live le prouve, cosinus >= 0,999 sur deux questions et deux documents. Lots de 900 textes et 90 000 jetons estimes, refus definit…57c8117
26 days ago
-
feat(compte): le profil choisi suit le compte d'un appareil a l'autre
- Migration 015 (f7a8b9c0d1e2) : users.persona VARCHAR(50) NULL, et la contrainte ck_users_persona_connu, qui n'admet que les quatre profils (« étudiant » en NFC). NULL : aucun choix ; un refus de repondre n'est jamais stocke sur un compte. Rejouable, comme 013 et 014 : le test d'idempotence de 013 recule la version par stamp puis rejoue la suite. Instantanee sous PostgreSQL 16 (colonne nullable). - UserResponse.persona : /auth/me, la connexion, l'inscription, Google et la liste admin le renvoient. Pas dans UserBase : UserCreate en heriterait et la creation admin l'ignorerait. - PATCH /auth/me : le profil, seul champ modifiable en libre-service. MiseAJourDuCompte n'herite pas de UserUpdate (email, mot de passe, role : escalade de privileges) et refuse toute autre cle (422). Journal : l'identifiant du compte seulement. Jamais dans le JWT. - README : la route et les regles de synchronisation du front. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>4808bed
30 days ago
-
docs(config): .env.example aligne sur Settings, seed aux 14 domaines, README a jour
.env.example : - chaque reglage de Mistral et de Groq y figure, avec les plafonds verifies du palier gratuit ; MISTRAL_MODEL suit le defaut de config.py (14b) ; INTENT_CLASSIFIER n'annonce plus « local » ; - plus aucun commentaire en fin de ligne : docker --env-file le garde dans la valeur, et « groq # groq | local » etait refuse au demarrage ; - l'extraction des PDF annonce Docling, son moteur par defaut. tests/test_core/test_env_example.py verifie que chaque cle est un reglage (Settings ignore en silence une cle inconnue), qu'aucune ligne ne porte de commentaire, que les valeurs actives se chargent et que chaque reglage des fournisseurs est documente. scripts/seed_database.py semait cinq categories aux identifiants fixes et aux noms d'avant la migration 013 : sur une base migree, une quinzieme categorie, et des lois rattachees a l'identifiant 1, 2 ou 3, quel qu'il soit. Il cree les 14 domaines manquants par leur nom et rattache ses lois par le nom. Textes perimes : « 12 cat…deb8048
-
feat(decoupage): les sorties Docling decoupees sans perte
Mesure sur les sorties Docling du corpus (banc OCR, puis pilote de 19 documents), chaque regle payee par un defaut constate. Nettoyage (markdown_cleanup) : - cachet tolerant a l'OCR (mots colles, lettres confondues), en capitales seulement : « une copie certifiee conforme » a fournir reste du texte ; - filigrane rendu en tableau d'une cellule, lambeaux du cachet ; - nettoyer_markdown met a plat titres et marqueurs decores (« ## ARTICLE », « - ARTICLE 12.- », « ARTIiCLE 81 ») : le sommaire de la page de lecture en voyait 155 sur 191, il en voit 189. Decoupe (text_chunker) : - marqueurs « ARTICLE_1er », « 2. ARTICLE 41.- », mot ecorche par l'OCR, ordinaux jusqu'a la centaine, en-tetes colles (« CHAPITREII ») ; - un renvoi en liste (« - article 12 de la loi ») ne coupe plus l'article. Raffinage (chunk_refiner) : - une liste nominative garde ses noms dans le contenu : elle n'etait enregistree nulle part ailleurs. Seul le vecteur prend la forme resumee, et une longue liste es…14eb55e -
feat(embeddings): schema en 768 dimensions, index HNSW sur la colonne
La colonne articles.embedding passe de vector(3072) a vector(768) : la sortie native d'EmbeddingGemma, et une dimension que gemini-embedding-001 produit aussi. Un seul schema pour les deux fournisseurs. Le fournisseur par defaut reste gemini : ce commit ne change que le schema et ce qui en depend. LA MIGRATION DETRUIT TOUS LES VECTEURS (c4d5e6f7a8b9). C'est inevitable : un vecteur 3072 n'a pas d'equivalent dans l'espace d'un autre modele. Ici les bases etaient vides. Une base peuplee devra etre regeneree ensuite, et comme le conteneur lance `alembic upgrade head` a chaque demarrage, deployer ce code declenche la purge. La procedure est a ecrire avant tout deploiement. L'index HNSW se pose directement sur la colonne, sans le detour par une expression halfvec. Les anciens index sont supprimes AVANT l'ALTER : laisse en place, l'index halfvec survit a la conversion et ne sert plus a rien. UN PIEGE TROUVE EN CHEMIN, mesure par EXPLAIN. La recherche garde deux etages : l'index fournit des…195c1c3
-
feat: deployment on free tiers, object-storage documents, and email
Enables the project to run on a free hosting tier — ephemeral disk, managed Postgres behind a connection pooler, no outbound SMTP — and adds the password reset flow that was previously a dead end. Two blocking deployment bugs, both reproduced rather than deduced: DATABASE_URL with ?sslmode=require TypeError: connect() got an unexpected keyword argument 'sslmode' SQLAlchemy's asyncpg dialect forwards unknown query parameters straight to asyncpg.connect(), whose signature accepts neither sslmode nor channel_binding and has no **kwargs. The failure surfaces only on the first database access, long after an apparently successful start — and that is exactly the URL the "copy" buttons of managed Postgres providers hand out, and what .env.example recommended. Settings now refuse such a URL at load time, naming PGSSLMODE as the remedy. alembic/env.py, swapping asyncpg for psycopg2 but keeping the query string ProgrammingError: invalid dsn: invalid URI query parameter prepared_s…6429ad3
-
feat(auth): inscription publique, connexion Google, et conversations rattachees a un compte
L'utilisateur peut desormais creer un compte — nom, adresse, mot de passe — ou se connecter par Google, afin de retrouver ses conversations d'une visite a l'autre. PAS DE FIREBASE. Le navigateur obtient un jeton d'identite de Google, ce serveur le verifie avec google-auth, puis emet le JWT maison. Une dependance, zero paquet npm, et surtout UN SEUL fichier d'utilisateurs — Firebase en imposerait deux, a garder synchronises. L'ESCALADE DE PRIVILEGE EST FERMEE PAR CONSTRUCTION. `UserCreate` porte un champ `role` fourni par le client ; le reutiliser pour une route publique aurait laisse n'importe qui poster {"role": "superadmin"}. `SignupRequest` n'herite pas de `UserBase`, donc ce champ n'existe pas, et `extra="forbid"` fait de son envoi un 422 plutot qu'un silence — la tentative laisse une trace. LE TROU DE CONFIDENTIALITE EST FERME, et c'etait la condition pour promettre quoi que ce soit. GET et DELETE /rag/conversations/{sid} n'avaient aucune authentification ni controle d'apparten…d21ea1e
-
fix(qualite): ruff devient une vraie barriere, et trouve un schema mort et casse
Ruff tournait sans jeu de regles declare : 1332 signalements, dont 98 faux positifs sur le motif `Depends()` de FastAPI et 615 modernisations de style. Un controle que personne ne peut passer n'est pas un controle. Le jeu retenu ne garde que ce qui signale un defaut reel (F, E4, E7, E9, I, RUF100), avec des exceptions nommees la ou l'ordre des imports EST la protection : env.py doit poser sys.path avant d'importer les modeles, conftest doit surcharger DATABASE_URL avant d'importer l'application. Ce que le controle a alors trouve : - UN SCHEMA MORT ET CASSE. `LawDetailResponse` existait deja, exposant `List[ArticleResponse]`. Il n'etait branche sur aucune route, et il etait doublement inutilisable : ArticleResponse porte le CONTENU integral de chaque article — pour le Code Minier, 166 Ko servis deux fois — et son `number` etait plafonne a 20 caracteres alors que la colonne en fait 64 depuis qu'elle accueille « QUATRE-VINGT-DIX-SEPTIEME ». Le plafond du schema est corrige, et…7435134 -
refactor: retirer 165 Mo de dependances mortes et dix scripts a usage unique
DEPENDANCES scikit-learn et joblib : ZERO occurrence dans tout le depot depuis la suppression du classifieur ML. Avec scipy qu'ils entrainent, ils pesaient 165 Mo dans l'image pour rien. Le classement de domaine est fait par des regles depuis legal_domain_classifier.py. SCRIPTS Sept scripts a usage unique, sans argparse, avec `law_id = 1` code en dur et aucun garde `__main__` — donc executables au simple import. Quatre d'entre eux etaient DESTRUCTIFS sans confirmation : force_clean_extract, reextract_ constitution, reextract_pdfplumber effacaient les articles de la loi 1. reprocess_law.py portait en plus un file_id code en dur ; il figurait dans un plan de suppression precedent et n'avait pas ete supprime. Trois scripts de maintenance des categories (add_category_icons, update_category_icons, cleanup_categories) sont perimes par la migration 007, qui possede desormais les 14 domaines avec leurs icones et le verifie par assertion. Les executer se battrait contre elle. reset_databas…d41abd1
-
feat(pipeline): chunk_refiner enfin cable, avec kind, embed et embed_text
599 lignes ecrites, testees, appelees par personne. Le raffinage tourne desormais entre le decoupage et la persistance. Trois colonnes sur articles (migration b7c8d9e0f1a2) : - kind : article, legal_basis, preamble, boilerplate, roster, table, fragment, continuation ; - embed : faut-il vectoriser ce chunk ; - embed_text : le texte REELLEMENT envoye au modele. Ce que cela change, mesure sur le corpus reel de 849 articles : 62 % font moins de 120 caracteres, et l'echantillon montre ce qu'ils sont — des lignes de liste nominative ("100. DOURLAI STANISLAS 0769502K"), des fragments de tableau, un separateur "---". Ils partaient tous au vectoriel, consommant du quota et saturant les resultats semantiques de textes qui ne repondent a aucune question. Ils restent en base, affichables et trouvables en plein texte ; ils ne sont plus vectorises. embed_text prefixe le contenu de l'en-tete du document — reference, titre, date, categorie, section, page. Sans lui, "Article 3.- La depense…8da83c7 -
feat(search): 3072 dimensions indexees via halfvec, recherche en deux etages
La migration precedente ramenait les embeddings a 1536 dimensions en partant du principe que 3072 etait inindexable. C'est vrai du type `vector`, plafonne a 2000 dimensions pour HNSW et IVFFlat. C'est FAUX du type `halfvec`, qui monte a 4000 — verifie sur ce serveur : pgvector 0.8.6, opclass hnsw/halfvec_cosine_ops presente, et pgvector.sqlalchemy expose HALFVEC. On garde donc la sortie native du modele, sans troncature : - la colonne repasse en vector(3072), fp32, valeurs exactes ; - l'index HNSW est pose sur l'EXPRESSION embedding::halfvec(3072), donc en fp16, ce qui le rend possible et divise sa taille par deux ; - la requete devient un deux-etages : parcours de l'index pour extraire des candidats sur-echantillonnes (x8, plancher 100, plafond 500), puis reclassement de ces candidats a la distance fp32 EXACTE. Le fp16 ne sert qu'a selectionner, jamais a classer. Quatre details portent le montage, et chacun a une variante fausse qui a l'air correcte : la distan…c6e37b4
-
feat(search): retrieval au niveau chunk, embeddings 1536 indexables
La recherche travaillait au niveau document et ne pouvait pas etre indexee. 1. Niveau chunk. postgres_search_service appliquait un SELECT DISTINCT ON (law_id) ... ORDER BY law_id : les articles etaient ecrases a un par loi, et l'ordre transmis a la fusion RRF n'etait plus celui de la pertinence mais celui des identifiants. Cote semantique, la distance etait calculee par article puis passee a func.max() + GROUP BY Law.* : l'article gagnant etait jete, le resultat sortait avec matched_articles vide, highlights vide et content nul — soit aucun texte pour le prompt. Nouveau schema ChunkResult, une ligne par article, avec son numero, son titre, sa section, sa page et son contenu integral. SearchResult est inchangee champ pour champ et desormais DERIVEE des chunks : le front n'a rien a changer et recoit enfin des matched_articles remplis. SearchResponse.chunks est additif, avec un defaut, donc les reponses deja en cache se deserialisent encore. 2. Filtre…72be68d -
Etape 13 : suppression du code mort de l'ancienne architecture
Fichiers supprimes (~950 lignes, aucun importateur) : - app/tasks/analytics.py — importait celery au niveau module, paquet absent des deux manifestes ; codait en dur un courtier redis://localhost:6379. - app/scripts/reindex_search.py — importait meilisearch ET lisait settings.MEILISEARCH_URL/KEY, champs supprimes de la configuration : double echec garanti. Le repertoire app/scripts/ ne contenait que ce fichier. - app/tasks/clean_text_helper.py — orphelin, doublon plus ancien de process_law._clean_extracted_text. - 6 scripts Meilisearch/Redis inexecutables : check_meili_content, index_articles_meili, reindex_meilisearch, test_meilisearch, flush_cache, debug_search. - scripts/reset_database.py (references Redis) ; reset_database_fixed.py, qui le remplacait deja, reprend son nom. Alias MeilisearchError supprime de search_service (le test qui l'importait pointe desormais sur TextSearchError). Documentation realignee sur la pile reelle dans une dizaine de fichiers : plus auc…51a64ba -
Etape 7 : type de colonne suggested_categories + tests d'embeddings
BUG DE SCHEMA : le modele Law declarait suggested_categories en JSON alors que la migration 001 cree la colonne en integer[]. Toute ecriture echouait en DatatypeMismatchError ("column is of type integer[] but expression is of type json") — donc toute creation de loi avec des categories suggerees. Ironie : un adaptateur get_array_type() existait juste au-dessus pour choisir ARRAY sous PostgreSQL, mais n'a jamais ete appele. Le projet ne ciblant que PostgreSQL, le type est declare directement et l'adaptateur mort supprime. embedding_service : la liste vide levait un AssertionError. Les assertions sautent sous python -O : la validation aurait disparu en production. -> ValueError. Tests : - test_embedding_service appelait la VRAIE API Gemini (echecs 400, et consommation de quota si la cle avait ete valide). Doublure deterministe installee : le vecteur derive du hash du texte, donc stable et discriminant, ce que supposent les tests de similarite. 131 s -> 0,4 s. - Les fixtures servi…bb367b5
-
chore: initial standalone backend for JuriX API (Render)
9cf9a2a