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
23 days ago
-
feat(chat): chaque extrait dit sa date et si un texte plus recent l'a change
Le corpus garde chaque texte dans sa version publiee : le Code electoral de 2012 tel quel, et a cote la loi de 2026 qui en reecrit des articles. La recherche remontait souvent l'ancien article, et le chat le citait comme la regle en vigueur (« l'article 38 de la Loi N°2004/004 » pour le constat de la vacance de la presidence, reecrit en 2026). Archiver les textes anciens n'etait pas la reponse : la plupart de leurs articles restent applicables. - app/services/liens_entre_textes.py : 67 liens verifies (texte touche, texte recent, nature, articles, portee), chacun avec sa preuve citee : Constitution, Code electoral, Conseil constitutionnel, Cour supreme, traite OHADA, Code penal, Code civil (etat civil, suretes, vente et bail commerciaux, prescription...), liberte d'association, cybersecurite, foncier, incitations a l'investissement, lois de finances. Releves en base, contre-verifies, relus par une revue adversariale ; les liens ecartes le sont avec leur raison. - app/serv…c193c4e
24 days ago
-
fix(ingestion): l'annee du numero est celle de l'acte, pas d'un texte modificatif
« Decret N°94/199 du 07 octobre 1994 portant…, modifie et complete par le decret n°2000/287 du 12 octobre 2000 » etait date du 12 octobre 2000 : le numero cherche dans tout le titre etait celui du texte modificatif, et la vraie date, trop loin de son annee, etait rejetee. Le numero est desormais lu avant l'objet de l'acte. Sur les 2287 titres des deux index, seule cette date change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>34508a7 -
feat(ingestion): textes venus d'ailleurs que prc.cm, Actes uniformes OHADA
Le corpus complementaire (Actes uniformes OHADA, Code CIMA, lois que prc.cm ne sert pas) passe par la meme ingestion que le reste. - L'index peut donner la reference du texte (« OHADA-AUA-2017 », « LOI-1992-007 ») ; sans elle, `PRC-{doc_id}` comme avant. Un Acte uniforme ne pretend plus venir de la Presidence. - « Acte uniforme » devient un type d'acte, en dernier dans la recherche sur tout le titre : « Decret portant ratification de l'Acte uniforme… » reste un decret. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>c148f31
26 days ago
-
fix(eval): relecture vraiment a l'aveugle, budget au pire cas, mesures justes
Releves par la revue adversariale du lot personas. - Relecture a l'aveugle : les etiquettes etaient tirees au hasard, mais les reponses ecrites dans l'ordre des variantes ; citoyen toujours premier, temoin toujours dernier. Elles suivent desormais l'ordre des etiquettes. - Budget : il porte sur le pire cas (une reponse coupee demande une suite, une grille illisible est redemandee), soit deux appels par tache ; 96 par defaut pour les memes 48 taches. Un nombre negatif est refuse (`--comparaisons -1` retranchait un appel et en lancait trois). - Le reclassement par le modele est coupe : il ajoutait un appel par question hors budget, et en faisait dans --dry-run. - distinction ne compare que des profils de meme modele et memes consignes ; similarite ne tient plus « a b » et « c d » pour identiques ; les suffixes (quater, septies) sont ceux du chat ; les renvois vers un avocat reconnaissent « vous adresser a un avocat », « l'avocat de votre choix », « an attorney ». …84f70cd -
feat(prompts): quatre personas vraiment distincts, un profil general, registre adapte hors du droit
Les quatre profils ne differaient que de quatre ou cinq lignes de role, et deux poussaient a inventer (« jurisprudence pertinente si disponible », « contexte historique ») alors que le corpus n'en contient pas. Chaque profil dit desormais qui est l'interlocuteur, ce qu'il attend et comment lui repondre : - citoyen : mots simples, demarche, delai, autorite ; avocat si l'enjeu est serieux ; - avocat : vocabulaire exact, conditions, exceptions, sanctions, articulation des textes ; ni jurisprudence ni renvoi vers un avocat ; - entrepreneur : obligation, etapes, delais, montants ; obligatoire et facultatif distingues ; - etudiant : notion definie, raisonnement, place dans la hierarchie des normes ; - general (n'a pas repondu) : ne presume rien du lecteur. Le bloc partage (longueur, grammaire de citation, interdits) ne change pas. Un profil inconnu recoit le profil general, avec un avertissement, et non plus citoyen en silence. Les reponses conversationnelles portent une ligne d'audi…406ec90 -
feat(eval): comparer_mistral mesure les profils, avec temoin de bruit et budget borne
- --personas : chaque question posee sous chaque profil, avec le MEME contexte ; alias acceptes, profil sans prompt refuse (sinon le repli de get_system_prompt ferait mesurer un autre profil). - --temoin : le premier profil rejoue, plancher de bruit a 0,7. - --budget : au-dela, rien n'est lance, avant meme la cle d'API. - --questions-libres avec --dry-run : la recherche du chat, sans appel au modele, montre si une suggestion a une reponse dans le corpus. - Mesures : forme_relue (citations que le chat sait relire en sources), traits (listes, renvois, definitions), similarite et distinction entre profils. La regle de decision des nouveaux prompts est ecrite dans la docstring, avant la mesure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>df97adf -
fix(deploiement): corrections de la seconde revue
Revue adversariale du deploiement, trouvailles verifiees une a une. - demarrer.sh : un psql en echec dans la condition d'un if passait pour une base sans table, donc a restaurer. Une mise a jour pouvait ainsi faire DROP DATABASE de la production, puis restaurer le corpus du premier jour, sans un compte. La reponse doit etre t ou f, sinon arret. Le dump initial est renomme apres une restauration reussie : plus aucune mise a jour ne peut le rejouer. Un temoin fait reprendre une restauration interrompue. Une sauvegarde precede chaque mise a jour. - ADMIN_PASSWORD : docker compose interprete $, # et les guillemets ; « Jurix2026$admin » arrivait sous la forme « Jurix2026 ». Refuses. - create_admin.py promouvait superadmin un compte ordinaire deja inscrit a ADMIN_EMAIL, avec son propre mot de passe, alors que le site est public avant la creation de l'administrateur. Refus sans --promouvoir, qui remplace alors le mot de passe. Le test qui devait prouver « aucun acces a …7cf23f7 -
fix(recherche): la passe floue compare les titres mot par mot
« peine de mort au cameroun » rendait en tete des decrets de pret, a egalite (0,959), devant les 11 articles du Code penal sur la peine de mort. La passe floue sur les titres, faite pour rattraper les fautes de frappe, comparait la requete ENTIERE : « de », « au » et « cameroun » font plus de la moitie de ses trigrammes, et tout titre en « ... de ... au Cameroun » passait le seuil (0,516 pour 0,5). Etiquetes correspondance de titre, ces decrets passaient devant toute correspondance d'article. - Chaque mot significatif (sans mots vides, ni « cameroun », « loi », « decret »...) doit se retrouver dans le titre, meme mal orthographie. Les fautes de frappe (« nominaton », « code miner ») restent rattrapees. - Le mode entre dans la cle du cache : une recherche texte et une recherche hybride de la meme requete se servaient l'une l'autre. - scripts/eval/evaluer_recherche.py rejoue la requete de la page sur les 102 questions relues et 10 requetes temoins. Mesure (102 questions), mode …baaafb1
29 days ago
-
fix(deploiement): trois pannes trouvees par l'essai de bout en bout
Essai complet de demarrer.sh dans une copie isolee (https://localhost) : base neuve, migrations depuis zero, relance a l'identique, restauration d'un dump sur un schema deja cree, sauvegarde. - demarrer.sh s'arretait sur une base neuve, avant toute restauration : PostgreSQL resout `laws` a l'analyse, meme dans la branche non prise du CASE. Deux requetes a la place. Le premier deploiement aurait echoue. - Les redirections de l'API (barre oblique finale) partaient vers http:// : uvicorn ne croyait pas les en-tetes X-Forwarded-* de Caddy, et le navigateur bloque une redirection vers http depuis une page https. FORWARDED_ALLOW_IPS="*" : sans danger, seul Caddy joint l'API. - Un ADMIN_PASSWORD hors politique (sans majuscule) faisait echouer create_admin.py, qui affichait l'erreur Pydantic entiere, mot de passe en clair compris. Le script n'affiche plus que le champ et la regle, et demarrer.sh verifie la politique des l'etape 1, avant vingt minutes de construction. Aussi …6e91d61 -
fix(image): les versions de l'image sont celles que la suite a testees
L'essai de deploiement de bout en bout a fait planter l'API au demarrage, des la migration : « The SQLAlchemy asyncio module requires that the Python 'greenlet' library is installed ». requirements.txt ne donne que des fourchettes, et l'image prenait les dernieres versions du jour : 21 paquets sur 73 differaient du venv, dont SQLAlchemy 2.1.4 (contre 2.0.52), qui n'installe plus greenlet de lui-meme. - requirements.txt : sqlalchemy[asyncio], qui apporte greenlet. - constraints.txt : les 76 versions exactes que requirements.txt fait installer, prises dans le venv ; le Dockerfile les impose (pip -c). scripts/figer_versions.sh le regenere, par une resolution dans python:3.11-slim. Chaque paquet compile a une roue cp311 manylinux aarch64 : rien a compiler sur la VM ARM. - Dockerfile : utilisateur et modele fastText (126 Mo) AVANT la copie du code, qui passe en COPY --chown. Avant, chaque mise a jour retelechargeait le modele, le chown -R le recopiait dans une couche de plus (i…e74628e
30 days ago
-
feat(eval): comparer_mistral mesure aussi un plus petit modele et un prompt renforce
--modeles et --consignes font varier le modele et le prompt systeme du chat : « actuelles » (la production) ou « renforcees » (cinq regles de fond en tete). Les etiquettes de la relecture a l'aveugle suivent le nombre de variantes, et la synthese compte les citations orphelines des comparaisons : une case remplie ne vaut que si ses citations tiennent. Mesure du 07/10 (20 questions, 4 comparaisons) : - ministral-3b-2512 : 90,9 % de citations justes (22) avec le prompt actuel, 68,8 % (32) avec le renforce ; 1,7 a 1,8 s par reponse ; - ministral-14b-2512 : 96,2 % (26) et 96,4 % (28) ; 3,0 a 3,3 s ; - comparaisons : le 3b remplit les 28 cases, avec 37 articles absents des extraits ; le 14b en remplit 22, avec 20. Regle fixee avant la mesure : le 3b passait a 94,6 % de citations justes au moins. Rate avec les deux prompts : ministral-14b-2512 reste. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>b16c746 -
fix(reclassement): la simulation montre tout ce que --force ecrira
Issus de la revue de la branche : - `appliquer --dry-run` sans --force n'inscrivait pas dans changements.csv les categories deja posees que le verdict contredit : sur le corpus, ou toutes les lois en ont une, on relisait un fichier presque vide avant un --force qui les reecrivait toutes. changements.csv liste desormais toute loi dont la categorie changerait, avec sa suite (appliquee, ou gardee faute de --force), et le README recommande `--dry-run --force`. - Une relance `--incertains` apres un arret sur quota repayait les lois deja retentees avec un extrait : elle les saute. - Une ligne de journal coupee par un arret brutal absorbait la ligne suivante : le journal est termine par un saut de ligne avant d'ecrire. - Le journal est indexe par l'identifiant de la loi : chaque verdict porte desormais sa reference, et celui d'une autre loi (base reconstruite) n'est ni repris ni applique. - L'anomalie « Classement indisponible » posee par le pipeline est effacee une foi…f508b99 -
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 -
fix(intention): budget de sortie suffisant, nominations reconnues comme du droit
Mesure du classement d'intention par Groq (qwen3.8-27b) sur les 122 messages de tests/fixtures/intent_eval.json : - a 40 jetons de sortie, 2 messages sur 60 revenaient VIDES : le modele depense des jetons invisibles avant son verdict (128 factures pour un verdict de 15), et Groq repond alors 400 json_validate_failed. Ces messages partaient en « juridique » par defaut. Le plafond passe a 256 ; le quota compte l'usage reel. GroqService traduit desormais json_validate_failed en GroqReponseInvalideError : un lot de classement des lois se redecoupe au lieu d'arreter le reclassement comme une panne ; - 3 questions de droit sur 72 partaient en hors sujet : un nom de personne, « les universites d'Etat et leurs recteurs », « who was appointed governor of the Littoral region? ». Le corpus est fait aux deux tiers de decrets de nomination ; les consignes le disent maintenant en toutes lettres. Apres correction, sur les 122 : 121 justes (99,2 %), rappel « juridique » 1,000 (ex…c84fadf -
feat(eval): comparaison ministral-14b / 8b, et jeu d'evaluation par le modele du chat
scripts/eval/comparer_mistral.py mesure les deux modeles sur 20 questions du jeu d'evaluation relu et 4 comparaisons de regimes, au plus 48 appels : meme contexte pour les deux (la recherche est faite une fois), aucun appel a Groq, modele de secours coupe. Mesures : premier morceau, duree, jetons, fin, articles cites presents dans les extraits, JSON lisible et lignes remplies. Les reponses sont ecrites anonymisees (X/Y tires au sort) pour une relecture a l'aveugle des ajouts non sources. La regle de decision est ecrite avant la mesure : on garde le 14b, sauf si le 8b egale la justesse des citations a 2 points pres sans ajouter plus de contenu non source. generate_eval_set.py passe par get_llm_service() au lieu de Gemini, avec un systeme neutre : sans lui, l'instruction du chat exigeait « Sources: » au milieu d'un JSON. MistralService.generate_stream accepte un rappel `usage` (jetons du flux, modele qui a repondu), pour les mesures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.…f565440 -
feat(intention): classement par Groq court et en cache, plus de classement local
Le classement local par embeddings est retire (intent_local.py, intent_examples.py, leurs tests et le prechauffage au demarrage). INTENT_CLASSIFIER accepte « groq » (defaut) ou « llm » (le modele du chat) ; l'ancienne valeur « gemini » est ramenee a « llm », et « local » est refuse avec la raison plutot qu'avec l'erreur generique de pydantic. Le mode groq menage le quota du jour (200 000 jetons, 1 000 requetes) : - consignes courtes (~300 jetons au lieu de ~870 : le chat plafonnait vers 215 questions par jour), schema strict, sortie de 40 jetons ; - deux derniers echanges seulement, 200 caracteres chacun ; - cache des verdicts (2 000 entrees, 24 h) : un message identique ne coute plus de requete ; un repli n'y entre jamais. Quand Groq ne peut pas repondre dans les temps — quota epuise (aucune requete n'est alors envoyee), attente du limiteur au-dela de 0,5 s, delai GROQ_INTENTION_TIMEOUT_S (3 s) depasse, panne — la question part aussitot en « juridique », avec une regle `defaut-g…21d412f -
feat(reclassement): reclassement par lots en deux temps, reprenable
scripts/maintenance/reclassify_domains.py est reecrit : - `classer` interroge Groq par lots (40 lois par requete, titres seuls ; puis les incertains avec l'article premier, 15 par requete) et ecrit les verdicts dans data/reclassement/verdicts.jsonl : domaine, secondaires, confiance, modele, version des consignes, jetons. Rien n'est ecrit en base. Une relance saute les lois deja classees ; une ligne tronquee par une coupure est ignoree. Quota epuise : arret propre, code 4, avec l'heure de reprise ; panne : code 3 ; une saturation courte est attendue sur place. - `appliquer` ecrit categorie, confiance et suggestions, par paquets de 200. Un verdict « a revoir » ou sous le seuil (--seuil, 0,6) n'est jamais applique : il part dans a_revoir.csv. Sans --force, une categorie deja posee est gardee. --dry-run n'ecrit rien en base et produit le rapport, changements.csv et a_revoir.csv, a relire avant d'appliquer pour de bon. L'ancienne version rangeait en Droit Administr…43190dd -
feat(groq): classement des lois par lots, sans repli silencieux, et suite isolee du reseau
GroqService (app/services/groq_service.py) : - chaque appel passe par le limiteur de son modele (les quotas Groq sont par modele) ; un 429 est respecte (Retry-After) puis rejoue ; une attente imposee de plus de GROQ_ATTENTE_MAX_429_S, ou un message de quota journalier, ferme le modele jusqu'a l'echeance (GroqQuotaError, sans requete) ; 5xx et pannes reseau : 4 essais ; - erreurs typees au lieu de None : GroqLimiteError, GroqQuotaError, GroqIndisponibleError, GroqReponseInvalideError ; - schema JSON strict, max_completion_tokens, temperature 0, reflexion coupee (qwen : « none ») ou reduite et masquee (gpt-oss) ; - la sonde de sante lit /models/{modele} au lieu de generer. Classement des lois (legal_domain_classifier.py) : - par LOTS de documents numerotes, codes courts en sortie : 2 226 lois une par une demandaient 11 jours de quota ; un lot incomplet est redemande (les manquants, ou le lot coupe en deux), un document qui manque encore est « a revoir » ; - modele prop…1a3d3f5 -
fix(imports): un seul choix du fournisseur, et chaque fichier importe ce qu'il utilise
La comparaison et l'explication d'article lisaient settings.LLM_PROVIDER sans importer settings. Rien n'echouait a l'import : le nom n'etait resolu qu'a la construction du service, donc chaque /compare et chaque /explain-article repondait 500 des qu'aucune doublure n'etait injectee — en production, jamais dans les tests. Le choix du fournisseur, recopie trois fois, vit desormais dans app/services/llm.py : get_llm_service() rend Mistral ou Gemini selon LLM_PROVIDER, et les erreurs sont regroupees par nature (ERREURS_QUOTA, ERREURS_SATURATION, ERREURS_LLM), quel que soit le fournisseur. Les messages « Please set up Gemini API » deviennent neutres. ruff rendait 18 signalements (2 noms inconnus, 14 imports inutiles, 2 blocs mal ordonnes) ; il en rend zero. Les __import__("sqlalchemy") et __import__("datetime") deviennent de vrais imports. Garde permanente, tests/test_qualite/test_imports.py : ruff doit sortir a zero sur app, scripts, tests et alembic, et chaque module de app doit s'impo…e77f0a8 -
feat: Mistral pour le chat, Groq pour le routage et le classement, 14 categories
Travail de l'utilisateur, verse tel quel avant les correctifs qui suivent : - MistralService (generation et flux), LLM_PROVIDER=mistral par defaut ; - GroqService : classement d'intention et classement des lois par Groq ; - 14 categories canoniques (migration 013) et script de reclassement ; - alembic/env.py retombe sur settings.DATABASE_URL. Defauts connus, corriges dans les commits suivants : settings non importe dans comparison_service et explanation_service, tests appelant les vraies API, migration 013 sans retour arriere.26dddaf
-
fix(ingestion): les doublons se reconnaissent sur tout le corpus, pas sur le lot
La reindexation passe par lots de 50 documents (--doc-id). Les doublons (meme PDF sous deux numeros d'acte) etaient determines sur la seule selection : PRC-10016, jumeau du 10018 reste dans un autre lot, a passe pour unique, est parti en traitement sans fichier, et son echec a arrete la reindexation entiere. Le canonique se choisit desormais sur le corpus complet, quelle que soit la selection traitee. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>2d6e405 -
fix(vecteurs): l'index HNSW se reconstruit dans un conteneur Docker
En parallele, pgvector construit le graphe en memoire partagee dynamique, dans /dev/shm, que Docker limite a 64 Mo par defaut. Sur le corpus (20 394 vecteurs), REINDEX echouait : « could not resize shared memory segment ... No space left on device ». La construction se fait desormais en serie, dans la memoire du processus : 8 s pour le corpus. Un REINDEX CONCURRENTLY interrompu laissait une copie invalide de l'index (_ccnew), maintenue a chaque ecriture : elle est supprimee sur echec. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>5a82781
-
feat(ingestion): le corpus en deux passes reprenables
Passe 1, scripts/extraire_corpus.py : Docling seul, sans base. Le travail se fait dans un processus enfant (spawn) recycle tous les 25 documents ou au-dela de 5 Go : la memoire de Docling grossit au fil des pages. Le superviseur compte une tentative au lot pendant lequel l'enfant meurt, tue un enfant bloque, s'arrete sur une panne generale (initialisation, echecs en serie) ou si .env et les paquets ont change depuis le lancement — sans quoi l'enfant ecrirait dans un autre cache et la boucle ne finirait pas. Arret propre par data/ingestion/STOP ; suivi dans progression.json. Passe 2, scripts/ingest_corpus.py : ne relit que le cache, jamais Docling. Reprend les lois pending, processing et refused ; une panne de base est rejouee avec attente croissante au lieu de compter chaque document en echec. Les PDF identiques sous deux numeros d'acte sont indexes une fois, l'autre signale. Type et date de l'acte relus dans le titre, l'annee controlee par le numero d'acte. Pipeline (process_law) : …1be6163 -
feat(extraction): Docling en local, OCR pleine page, cache reprenable
Docling remplace l'API Gemini, payante, comme extracteur par defaut (PDF_EXTRACTION_ENGINE=docling ; « gemini » reste disponible). - Chaque page est rasterisee (pypdfium2, 216 dpi) et passe par l'OCR pleine page : 42 % des pages n'ont pas de couche texte, et docling-parse rend NOIRES les 59 pages de scans masques (MRC) du corpus. - OCR RapidOCR sur torch, donc sur le GPU : mesure sur 97 pages contre Gemini et LlamaParse, il lit les numeros d'article la ou Tesseract en fausse (« ARTICLE 16 » lu « 13 »), a 3,8-4 s par page. - Conversion par lots de 8 pages, chaque lot mis en cache aussitot, brut : une interruption ne coute que le lot en cours, et ameliorer le nettoyage n'oblige jamais a refaire l'OCR. L'empreinte du cache porte les versions, les revisions des modeles et le rendu ; les lots se relisent quelle que soit leur taille. - Un lot en echec est rejoue ; au bout de TENTATIVES_MAX, il est repris page a page, et seule la page fautive est abandonnee et signalee. - F…69b13a2 -
feat(embeddings): EmbeddingGemma par defaut, et une panne qui se voit
EMBEDDING_PROVIDER vaut desormais gemma : les embeddings sont calcules en local, gratuitement, et Gemini ne sert plus qu'au chat. `gemini` reste disponible, a 768 dimensions lui aussi. LA PANNE LA PLUS PROBABLE DU SEMANTIQUE ETAIT LA SEULE INVISIBLE. Modele absent, fichier tronque, vecteurs d'un autre modele : dans chacun de ces cas, la recherche hybride avalait l'erreur et repondait en plein texte seul, pendant que GET /search/health disait « healthy ». Trois filets : - le modele est charge AU DEMARRAGE, et une question est reellement encodee. Un echec n'empeche pas l'API de demarrer — plein texte, chat et comptes s'en passent — mais il est journalise, et le service est ECARTE : garde, il retenterait de charger 300 Mo a chaque question ; - la sante passe en « degraded » si le service manque, si des vecteurs d'un AUTRE fournisseur sont en base (leurs cosinus ne veulent rien dire), ou si aucun vecteur n'est exploitable alors qu'il y a des articles a vectoriser. Ce dernier …7a1d560 -
style: ordre des imports et import inutilise laisses par mes commits du jour
ruff signalait trois ecarts dans des fichiers touches par 153dd7c et ad3ae33 : deux blocs d'imports mal ordonnes et un `Optional` importe sans usage. Aucun changement de comportement. Restent deux signalements anterieurs, dans tests/test_main.py et tests/test_migrations/test_email_tokens_schema.py, laisses en l'etat. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>a9c1b19 -
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 -
chore(eval): banc de comparaison des extracteurs PDF
Mesure trois choses contre la couche texte du PDF, page par page : la pagination (le critere qui a fait abandonner LlamaParse), la densite, et l'ancrage lexical avec detection des numeros d'article inventes. Moteurs : Gemini (chemin de production), Docling, Chandra 2 via Datalab, DeepSeek. Reference mesuree sur le Code Minier, pages 21 a 32 : pagination 12/12, 97,4 % d'ancrage, aucun article invente. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>ad3ae33 -
chore(scripts): verifier la configuration Brevo sans jamais afficher la cle
Lit .env, interroge le compte et la liste des expediteurs, et dit ce qui manque. N'ecrit rien et n'envoie aucun message. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>240a5f5
-
feat(extraction): remplacer LlamaParse par Gemini multimodal
La cle LlamaParse ayant ete retiree, l'ingestion de tout nouveau PDF levait et marquait le document `refused` — le bon echec, mais l'ingestion etait a l'arret. LE MOTIF DU CHANGEMENT EST LA PAGINATION, PAS LE PRIX. LlamaParse rend le document en un bloc sans signaler ses coupures de page ; `_split_markdown_pages` les reconstruisait en coupant sur les lignes de trois tirets, et sa propre docstring admettait la limite : « la pagination ne devient exacte qu'avec un moteur qui renvoie un numero de page explicite ». Mesure en base : les 710 articles portent tous `page_number = 1`, et sur les 27 lois, 271 pages de PDF ne donnent que 27 pages extraites — couverture moyenne 0,60. Un lien `?article=35` ouvre donc le PDF a la premiere page. MESURE AVANT D'ECRIRE, sur le Code Minier dont la sortie LlamaParse est en base : densite 2352 car/page contre 2405 — equivalent, rien n'est perdu marqueurs de page 1 par page contre 1 pour 69 pages lot pages 21 a 32 numerote 21…ca48b04 -
chore: dernieres corrections ruff automatiques
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>c7fa337 -
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(recherche): le titre passe devant le corps du texte, et les fautes de frappe passent
Trois demandes utilisateur, quatre causes. 1. Chercher "nomination" remontait des documents sans ce mot dans leur titre, et le moins pertinent sortait premier. `articles.search_vector` ne contenait ni le titre de la loi ni celui de l'article ; `laws.search_vector` collait titre et contenu sans un seul `setweight` ; et la seule branche qui voyait `laws.title` etait gardee derriere `if not chunks:`, donc jamais executee. Mesure avant : le decret SOCADEL (titre sans le mot) sortait a 0,4000 devant cinq documents dont le titre le porte, a 0,2000. Apres ponderation : 2,10 contre 0,20. 2. "article 35 du code minier" ne pouvait pas aboutir : le decoupeur attendait `Article 35` la ou le markdown ecrit `**ARTICLE 1er.**-` et `**Article1er.-**`. 8 lois sur 27 avaient un numero reconnu, contre 27 sur 27 apres correction ; 127 numeros reels contre 599. Le Code Minier passe de 41 a 193 articles. 3. Aucune tolerance aux fautes : `word_similarity` sur titre deplie, s…893a679
-
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 -
fix(embeddings): un quota journalier ne doit pas etre rejoue
Google renvoie 429 aussi bien pour un quota par minute que par jour, et suggere un retryDelay d'une trentaine de secondes dans les deux cas. Seul le quotaId les distingue : EmbedContentRequestsPerDayPerUserPerProjectPerModel-FreeTier, limite 1000 Pour un quota journalier, ce delai est trompeur : les trois tentatives brulent une minute avant d'echouer de toute facon, puis le lot suivant recommence, et ainsi de suite pour tout le corpus. Constate en ingerant le corpus reel — chaque document perdait une trentaine de secondes en reprises vouees a l'echec. QuotaExhaustedError est levee immediatement, sans reprise. Le pipeline la distingue d'une panne : le document reste correctement decoupe et indexe en plein texte, seuls les vecteurs manquent, et ils se rattrapent plus tard. Le script de backfill s'arrete net en annoncant combien d'articles restent et quand le quota se reinitialise, au lieu d'echouer lot apres lot. Quatre tests : reconnaissance du quota journalier, non-reconnaissance…b9769d8 -
fix: autocompletion vide, categories jamais ecrites, titre de resultat inerte
Trois pannes constatees en utilisant l'application sur un corpus reel. 1. /search/suggest ne renvoyait RIEN, pour a peu pres toute saisie. Deux causes cumulees. similarity() compare la requete au titre ENTIER : sur ce corpus un titre fait quatre-vingt-dix caracteres et le mot cherche une dizaine, la similarite plafonne a 0,13 quand le seuil est a 0,3 — mesure sur la base. Et la branche prefixe ne rattrapait rien, aucun titre ne commencant par le mot cherche. word_similarity mesure la requete contre le MEILLEUR extrait du titre et vaut 1,0 dans ce meme cas ; l'operateur %> est celui que gin_trgm_ops sait accelerer. S'y ajoutait la sensibilite aux accents : "societe" contre un titre portant "societe" accentue donne 0,500, sous le seuil de 0,6 — or personne ne tape les accents. La migration a6b7c8d9e0f1 installe unaccent, un enrobage IMMUTABLE (unaccent est STABLE, donc inutilisable dans un index) et deux index trigrammes sur l'expression depliee, sans…a381e6a -
fix(gemini): aucun delai maximal sur les appels, blocage indefini possible
genai.Client etait construit sans http_options, donc sans le moindre delai. Constate en conditions reelles pendant l'ingestion du corpus : le processus est reste 40 MINUTES endormi sur une lecture de prise reseau au milieu d'un lot d'embeddings — vivant, 16 secondes d'UC en une heure, aucune trace. Comme aucune exception n'est levee, la boucle de reprise du service ne se declenche jamais : le blocage est silencieux et sans fin. settings.GEMINI_TIMEOUT_S (120 s) est applique aux deux clients, celui des embeddings et celui de la generation. Un appel qui depasse leve desormais, ce qui reactive les reprises deja en place. Deux tests verifient que le delai est bien pose sur chaque client. Ajout de scripts/ingest_corpus.py, qui a servi a trouver le probleme : ingestion d'un echantillon graine du corpus prc.cm par la chaine complete (copie, extraction LlamaParse, decoupage, embeddings, tsvector). Echantillonnage stratifie a 70 % de textes normatifs : un tirage uniforme donne 52,6 % de decr…7dc0e8b -
test(eval): harnais de mesure de la recuperation
Sans mesure, le choix de la dimension et les poids de fusion se decident sur les chiffres MTEB d'un fournisseur et sur une constante tiree d'un papier TREC. Ce harnais permet de trancher sur CE corpus. scripts/eval/metrics.py — recall@k, MRR et intervalle de confiance bootstrap. Fonctions pures, definitions ecrites noir sur blanc dans le module : avec un seul article pertinent par question, recall@k est un taux de succes, et MRR est la metrique principale parce que le RAG n'envoie que 8 articles au modele — ce qui compte est que le bon soit HAUT, pas seulement present. Le module dit aussi qu'avec 60 questions l'erreur type est d'environ 6 points, et le rapport affiche l'intervalle a cote de chaque nombre : un ecart de 2 points n'est pas un resultat. scripts/eval/generate_eval_set.py — echantillonnage stratifie (60 % normatif, 20 % gros textes, 20 % aleatoire, formules d'execution exclues), generation de deux questions par article, et surtout les garde-fous qui ouvrent la boucle ferme…4f32045 -
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
-
fix: backfill relancable, boucle d'evenements, reglages morts
scripts/regenerate_embeddings.py reecrit. Il s'executait A L'IMPORT, sans main() ni garde __main__, avec LAW_ID = 3 code en dur, un appel API par article et un message final renvoyant vers un script Meilisearch supprime. Desormais : argparse (--all / --law-id / --reindex / --dry-run / --force), reprise par defaut sur embedding IS NULL avec curseur sur l'id (un OFFSET saute des lignes, celles traitees sortant du filtre au fur et a mesure), API batch, attente exponentielle plafonnee sur quota avec reprise du MEME lot, commit par lot, troncature des contenus trop longs, et ecriture via CAST(:embedding AS vector) sur une chaine — une liste Python passee a travers text() est adaptee en ARRAY, qui ne se caste pas en vector. --reindex reconstruit l'index HNSW en CONCURRENTLY : la migration le cree sur une table vide, la reconstruction en masse apres backfill donne un meilleur graphe. process_law._extract_pdf_text appelait asyncio.run() depuis un thread de l'executor : cela ne fonctionnait qu…202650d -
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 11 : authentification reelle branchee (demande n°2)
app/core/auth.py (188 lignes, JWT + bcrypt + roles) etait complet et importe par PERSONNE. Les 9 endpoints d'administration passaient par deux stubs renvoyant {"id": 1, "role": "admin"} : ils etaient ouverts a tous. BUG BLOQUANT decouvert en chemin : passlib 1.7.4 (derniere version, 2020) est incompatible avec bcrypt >= 4.1 — il lit bcrypt.__about__.__version__, supprime depuis. Avec bcrypt 5.x installe, hash_password echouait avec "password cannot be longer than 72 bytes" MEME pour 13 caracteres : l'authentification aurait ete inutilisable telle que declaree. -> bcrypt utilise directement, passlib retire des manifestes. Pre-hachage SHA-256 + base64 avant bcrypt : entree de longueur fixe, donc support des phrases de passe longues sans la troncature silencieuse a 72 octets de bcrypt (deux mots de passe partageant leurs 72 premiers octets etaient equivalents). Ajouts : - app/api/routes/auth.py : POST /login (formulaire OAuth2, l'URL exacte attendue par oauth2_scheme, donc le…da2ad52
-
chore: initial standalone backend for JuriX API (Render)
9cf9a2a