Spaces:
Runtime error
Runtime error
Commit History
19 days ago
-
fix(hf): connecter la fonction @spaces.GPU au bouton Gradio pour valider ZeroGPU
3744d40 -
fix: ne pas appeler la fonction @spaces.GPU a l'import et nettoyer PGSSLMODE
a145b74 -
fix(hf): ajouter fonction decoree @spaces.GPU pour valider ZeroGPU
426356e -
docs: ajouter les metadonnees Hugging Face Space au README
717d7c3 -
feat(deploy): configurer le deploiement sur Hugging Face Spaces (Gradio/FastAPI)
8272a44
21 days ago
-
feat(deploiement): le front tourne en serveur Node (rendu serveur), derriere Caddy
Le front passe d'un site statique, servi par Caddy depuis front/, a un serveur Node (SvelteKit, adapter-node) qui rend les pages. Servie comme avant, la sortie d'adapter-node aurait donne / et /_app/* en 404, et try_files aurait rendu le code serveur telechargeable. - docker-compose.yml : service front (node:24.21.0-alpine, build/ et package.json COPIES dans l'image depuis front-node/, utilisateur node, sans env_file). ORIGIN=https://${JURIX_DOMAINE}, API_INTERNE= http://api:8000 (le rendu appelle l'API par le reseau interne), KEEP_ALIVE_TIMEOUT 125 s (au-dela des 2 min de Caddy : pas de 502 sur une connexion fermee au meme instant), SHUTDOWN_TIMEOUT 8 s, tas de V8 plafonne a 256 Mo sous mem_limit 384m, sonde sur /favicon.svg. Rotation des journaux (3 x 10 Mo) sur les quatre services ; db arretee avec 1 min de grace. - Caddyfile : /api et /health inchanges ; tout le reste vers front:3000, avec 10 s de nouvelles tentatives pendant un redemarrage. Front ou API injoig…1342bbd -
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 -
fix(classement): la marge de reflexion de gpt-oss passe de 400 a 1000 jetons
Le classement Groq accorde a chaque requete 400 jetons de marge, plus 45 par verdict. gpt-oss ne permet pas de couper tout a fait sa reflexion, qui se paie sur ce meme plafond. Le 2026-10-14, un lot de 40 titres est revenu coupe (« max_completion_tokens atteint »), puis ses deux moities de 20 aussi : couper le lot reduit le plafond avec lui, alors que c'est la reflexion qui deborde. Chaque essai brulait le quota du jour sans un verdict, et le reclassement du corpus est reste bloque pres d'une heure. Le meme defaut explique l'ordonnance 74/1 restee sans categorie a l'ingestion (« JSON invalide »). Avec 1000 jetons de marge, le lot bloque est passe au premier essai. Le plafond ne coute que ce qui est consomme ; seule la reservation locale du quota attend un peu plus avant de partir. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>bb1f445 -
fix(decoupage): un long chapeau d'article n'est plus tronque a 200 caracteres
Un article trop long se coupe a ses alineas (1), (2), (3). Le texte qui precede le premier alinea (le chapeau) n'etait garde qu'en prefixe de 200 caracteres devant chaque morceau : au-dela, il ne figurait nulle part en base. Ni la recherche ni le chat ne pouvaient le lire. Cas releve : loi 2010/012 sur la cybercriminalite. Les articles 69 a 74 al. 2, colles sur une ligne par l'OCR, formaient le chapeau d'un « article 69 » coupe aux alineas de l'article 74 : il en restait « …celui qui accede san ». Mesure sur jurix_dev : 58 paragraphes (environ 33 000 caracteres) perdus dans une vingtaine de lois, dont le Code penal, le Code de procedure penale, le Code du travail et les lois 2010/012 et 2010/013. Un chapeau long ouvre desormais le premier morceau EN ENTIER (aux paragraphes s'il depasse lui-meme la taille cible), et seul son debut est repete devant les morceaux suivants. Simulation sur les 2275 lois : 15 lois changent, la perte disparait dans 13 d'entre elles ; les autres decoupages s…a42c933
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(confidentialite): le journal d'une conversation reprise ne porte plus les profils
La ligne voisine relie la conversation au compte : les deux ensemble associaient un compte a sa profession. Releve par la revue adversariale. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>9626b84 -
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 -
fix(analytique): le profil general a ses statistiques, les alias du chat sont admis
Le chat repond desormais en profil general quand aucun profil n'est choisi. Seule la repartition d'usage le comptait : l'agregation quotidienne, /stats/all, /compare, les avis par profil et toutes les routes /personas/{persona}/... l'ignoraient ou repondaient 400. Les conversations sans profil disparaissaient des statistiques, et le total des avis ne valait plus la somme par profil. - validate_persona rend la forme canonique, avec les alias du chat (« etudiant » donnait 400 ici alors que le chat l'accepte) ; general=True admet le profil general la ou il a des donnees. La fiche descriptive (/personas/{persona}) reste reservee aux 4 profils. - Agregation, statistiques, comparaison et avis parcourent les 5 profils ; le profil general s'affiche « Profil general ». Releve par la revue adversariale du lot personas. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>0b43949 -
fix(prompts): l'etudiant s'en tient aux documents, des tests qui protegent vraiment
- Etudiant : « Tu peux rappeler une notion generale du droit » et « sa place dans la hierarchie des normes » invitaient a puiser hors du corpus, dans le meme appel que le message utilisateur qui l'interdit (« Base ta reponse UNIQUEMENT sur ces documents »). La notion se definit a partir des documents ; s'ils ne la definissent pas, le modele le dit au lieu de la definir de memoire. Le test « aucun profil ne pousse a inventer » couvre desormais ces formules. - Le test de la phrase-exemple passait des qu'une citation quelconque du prompt etait lue, contre-exemple compris. Il exige la phrase « l'article 5 du Code Minier et l'article 6 du Code Minier » et ses DEUX citations. - Le message sans resultat du citoyen etait compare a lui-meme (NO_RESULTS_MESSAGE en est construit) : le texte d'avant les profils est fige dans le test. Releve par la revue adversariale du lot personas. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>4472b60 -
fix(citations): deux citations dans une phrase, alinea 1er, la loi citee departage
Releves par la revue adversariale du lot personas ; les deux premiers defauts existaient avant lui, mais les nouveaux prompts y poussent. - Le texte cite s'arrete avant la citation suivante (« et l'article », « and Article »), a une parenthese ou un point-virgule. « l'article 5 du Code Minier et l'article 6 du Code Minier », la forme que les prompts demandent pour plusieurs articles, ne donnait qu'une source : le groupe du texte avalait la seconde citation. - « alinéa 1er », « alinéa premier », « paragraphe 2 » et « Article 33(2) » rendaient toute la citation illisible. - Le premier chunk portant le numero l'emportait quelle que soit sa loi : « l'article 5 du Code Minier » devenait l'article 5 d'un decret place avant lui. La loi citee departage desormais ; le numero seul ne sert que si la loi citee n'est reconnue nulle part dans le contexte, jamais pour l'article d'une autre loi presente. Regex lineaire : 300 000 caracteres adverses en moins de 200 ms. Tests : 9 nou…fba6673 -
fix(compte): l'enregistrement du profil est prouve, la garde de 015 vise users
- Le test du PATCH relit le profil par une AUTRE connexion. Le client partage sa session avec la route : il voyait une modification jamais validee, et un commit remplace par flush passait inapercu (verifie par mutation : le test echoue desormais). - 015 : la contrainte n'est cherchee que sur la table users. Une contrainte du meme nom ailleurs (copie de table) faisait croire a sa presence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>9c5085a -
fix(tests): le filet des routes reservees ne verifiait plus aucune route
Depuis FastAPI 0.141, app.routes contient des _IncludedRouter sans attribut `routes` : le parcours ne descendait plus dans les routeurs inclus, ne voyait que / et /health, et le test passait sans avoir verifie une seule route d'administration. - Le parcours descend par effective_candidates(), qui porte le prefixe et les dependances du routeur. - La garde exigee est celle d'un administrateur (get_current_admin_user ou get_current_superadmin_user), cherchee dans tout l'arbre des dependances. L'ancien critere, « current » dans le NOM du parametre, aurait accepte get_current_user_optional et refuse `_`. - Les deux routes personas reservees aux administrateurs sont ajoutees ; /ocr/health rejoint /admin/health parmi les sondes publiques. - Le test echoue s'il ne voit plus les routes reservees : il ne peut plus redevenir vide en silence. Verifie par mutation : une garde remplacee par get_current_user_optional sur /personas/{persona}/questions fait echouer le test. Co-Authored-B…3f06b9e -
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 -
feat(chat): le profil choisi pilote la reponse, une conversation garde le sien
- persona absent, null ou vide vaut desormais « general » (l'utilisateur n'a pas repondu) et non plus « citoyen » : les deux etaient indiscernables, dans la reponse comme dans les statistiques. La casse et les accents sont toleres (normaliser_profil). - Une conversation garde un seul profil. Un session_id existant envoye avec un autre profil ouvre une NOUVELLE conversation (nouveau session_id, historique vide) ; l'ancienne n'est ni modifiee ni adoptee. Le controle d'appartenance passe avant : la conversation d'autrui reste refusee quel que soit le profil. Ni 409 (un profil change entre deux visites ferait echouer la premiere question), ni silence (on repondrait avec un profil que l'utilisateur vient de quitter). - Le dernier evenement du flux porte `persona`, le profil effectif. - Les reponses conversationnelles et le message « aucun resultat » suivent le profil. - _handle_no_results recevait sa conversation en la rechargeant : sans session_id, il en creait une de…2c463a5 -
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 -
fix(confidentialite): questions et avis des utilisateurs reserves aux administrateurs
GET /personas/{p}/questions rendait a n'importe qui le texte exact des questions posees au chat par tous les utilisateurs sur 90 jours (21 questions citoyen dans jurix_dev), et GET /personas/feedback/{id} les commentaires libres des avis. Les deux exigent desormais un compte administrateur : 401 sans jeton, 403 pour un compte ordinaire. Les agregats sans texte restent publics. POST /personas/feedback reste public : hors de ce lot, signale. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>4a7363d -
fix(analytique): compteurs exacts des conversations reprises, le profil general compte
- update_interaction_metrics compte en SQL. Il lisait Conversation.messages par selectinload, mais la collection, deja chargee par _load_or_create_conversation avant l'ajout des messages du tour, etait relue perimee (expire_on_commit=False) : chaque conversation reprise comptait un echange de retard (conversation 1 de jurix_dev : 3 questions reelles, 2 comptees). Le contenu des messages n'est plus charge. - personas_used est une liste neuve : modifiee en place, une colonne JSON simple ne voyait pas le changement, et l'ajout n'etait jamais ecrit. - La repartition d'usage compte « general » : la part des visiteurs qui n'ont pas dit qui ils sont, sans laquelle les pourcentages ne totalisaient pas 100. Les trois tests ajoutes echouent sur l'ancien code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>08a963c -
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 -
feat(personas): une seule liste des profils, alias sans accent, profil general
La liste des profils vivait en trois copies : le validateur du chat (schemas/rag.py), PersonaService.VALID_PERSONAS et les cles des prompts. Une valeur inconnue y recevait trois traitements : 422, 400, ou repli silencieux sur le prompt citoyen (« etudiant » sans accent). app/core/personas.py, sans import applicatif, porte desormais : - PERSONAS, les 4 profils proposes, « étudiant » en NFC ; - PROFIL_GENERAL (« general »), la valeur d'un utilisateur qui n'a pas repondu ; - normaliser_profil : casse, accents et forme NFD ignores, vide = general ; - normaliser_persona : les 4 seulement, pour un compte, qui ne garde jamais un refus. Aucun changement de comportement a cette etape. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>0a62da7 -
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(journal): les journaux de l'application sortent, sans le jeton du WebSocket
- Aucun gestionnaire n'etait pose sur les loggers de l'application : uvicorn ne configure que les siens, et Python ne garde que WARNING. Le journal de chaque appel Mistral ou Groq (fin, jetons, duree) n'apparaissait nulle part, ni en developpement ni dans docker compose logs. - uvicorn journalise chaque WebSocket avec son URL entiere, par son journal d'erreurs que --no-access-log ne coupe pas : le jeton d'administration (?token=...) y passait en clair. Il est masque. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>b229001 -
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(deploiement): corrections de la revue avant le premier deploiement
Issus d'une revue adversariale des fichiers de deploiement : - deployer.sh passait la fin du deploiement a `ssh ... bash -s` sur l'entree standard : le premier `docker compose exec` (qui garde l'entree ouverte) avalait la suite du script. Rien apres l'attente de la base ne s'executait — ni restauration, ni build, ni demarrage — et le script finissait sans erreur. La fin vit dans demarrer.sh, un fichier lance sur la VM ; chaque exec sans entree recoit </dev/null. - La restauration initiale ne regardait que l'existence de la table laws : un seul demarrage de l'API (qui cree le schema) la faisait sauter pour toujours, sans un mot. Elle se fait si la base n'a AUCUNE loi, dans une base recreee, avec --exit-on-error. - Le dump embarquait les comptes de developpement (et leurs mots de passe d'essai), conversations, jetons et caches : joignables depuis internet. Il n'en garde que le schema ; demarrer.sh cree l'administrateur de production (ADMIN_EMAIL, ADMIN_PASSWORD). -…2afa5a5 -
feat(deploiement): JuriX sur une VM Oracle Cloud gratuite, front compris
Une seule VM ARM du palier « Always Free » (2 coeurs, 12 Go, 200 Go) porte tout JuriX, avec Docker Compose (deploiement/oracle) : - db : PostgreSQL 16 + pgvector, aucun port publie, memoire partagee agrandie pour la reconstruction de l'index HNSW ; - api : l'image du Dockerfile, le modele EmbeddingGemma monte en lecture seule, les PDF en volume ; - caddy : seul expose (80/443), certificat HTTPS automatique, front statique avec repli sur index.html, et l'API sous /api sur le meme domaine (pas de CORS), sans tampon pour le flux du chat. installer.sh prepare la VM (Docker, pare-feu des images Ubuntu d'Oracle, swap, sauvegarde nocturne) ; deployer.sh envoie l'etat commite du code (git archive : jamais un .env local), le front construit pour le domaine et, au premier passage, le modele, les PDF et un dump de jurix_dev, restaure seulement si la base de la VM est vide. README : le compte, la region (Marseille), la VM, le DNS, la mise a jour, les sauvegardes, le depannage. .dockerign…5b3126f -
feat(mistral): ministral-14b-2512 epingle, ministral-8b-2512 en secours
Mesure du 07/10/2026 (scripts/eval/comparer_mistral.py) : 20 questions du jeu d'evaluation relu et 4 comparaisons, le meme contexte pour les deux modeles, 48 appels, aucune erreur ni reponse coupee. 14b 8b citations justes 96,6 % 96,4 % premier morceau 0,98 s 0,82 s (medianes) duree 2,74 s 2,21 s comparaisons en JSON 4/4 4/4 ajouts non sources 13,0 14,5 (relecture a l'aveugle, repond a la question 39/40 37/40 2 relecteurs, 20 questions) Regle fixee avant la mesure : garder le 14b, sauf si le 8b egale la justesse des citations sans ajouter plus de contenu non source. Il egale les citations mais ajoute un peu plus : le 14b reste, le 8b sert de secours. Les versions sont epinglees, pas les alias -latest : un changement de modele chez Mistral ne doit pas changer les reponses sans qu'on le sache. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>40829a2 -
test(eval): jeu d'evaluation de la recherche, 102 questions relues
Genere par ministral-14b a partir de 70 articles tires au sort (60 % d'articles normatifs, 20 % de gros textes, 20 % au hasard), deux questions par article : une requete courte, une question en langage courant. 140 candidates. Relues contre l'article complet, puis chaque verdict verifie par un relecteur contradictoire : 13 gardees telles quelles, 89 corrigees (la plupart reprenaient trop de mots de l'article, ou etaient trop vagues pour designer ce texte), 38 retirees (listes de noms, fragments de tableau, preambules sans contenu). Chaque item garde sa question initiale et la raison du verdict ; le recoupement lexical est recalcule (aucun au-dessus de 0,35). 15 items etaient reserves au lot de test avant la relecture : 11 y restent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>9bbc7a0 -
fix(limiteur): attente bornee, 429 retenus, mentions dans la langue de la reponse
Issus de la revue de la branche : - L'attente d'une place de generation (4 par modele) n'avait pas de limite : quand toutes etaient prises par des comparaisons longues, un message du chat attendait des minutes, sans secours ni 503. Elle est bornee par l'attente acceptee ; au-dela, le modele de secours. - Mistral : un Retry-After plus long que le budget, ou recu au dernier essai, n'etait pas inscrit au limiteur ; l'appel suivant retournait frapper le modele sanctionne. Il est inscrit avant de decider d'abandonner. - Un `repousser` (429) ne retenait pas les appels deja endormis sur un depart reserve : ils partaient en pleine penalite. Ils attendent l'echeance. - Le recalage sur les en-tetes de Groq s'inscrivait en queue de fenetre, apres des departs reserves pour plus tard, et le calcul du prochain depart faisait attendre trop (88 s au lieu de 59). Il s'inscrit a sa place. Groq corrige l'usage reel AVANT de recaler, sinon l'estimation masquait la consommation des…ecf0802 -
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 -
fix(migrations): commandes alembic qui marchent, 014 pour les bases deja en 013
Issus de la revue de la branche : - Les commandes du README et du message d'erreur de alembic/env.py passaient DATABASE_URL=postgresql://... : env.py importe l'application, dont le moteur asynchrone refuse ce pilote (InvalidRequestError), et rien ne migrait. Elles utilisent la forme du .env (+asyncpg), que env.py convertit lui-meme ; un test lance `alembic current` sous cette forme. - 013 a ete corrigee sur place, mais jurix_dev est deja a sa revision : `upgrade head` n'y rejouait rien, et 5 lois gardent les identifiants 10 et 11 dans leurs suggestions. La migration 014 nettoie les suggestions orphelines sur toute base, idempotente. - La descente de 013 ne fait plus rien sur une base qui ne porte aucun nom propre a 013 : elle ajoutait les deux procedures et reecrivait des descriptions dans l'etat ancien que fabrique le harnais, avant le retour arriere de 007. Sa docstring dit desormais vrai. - Test ajoute pour la cible de fusion absente : celui que la docstring d…d82ecf1 -
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 -
fix(comparaison): grille au format JSON impose, second essai si elle est coupee
Mistral recoit le schema de la grille en mode strict (voir le commit precedent) ; le prompt nomme en plus chaque cle attendue et le mot JSON. Sans cela, la grille revenait sous d'autres cles et chaque case s'affichait « Non trouvé ». Une grille coupee (tronquee) ou illisible est redemandee une fois, a 16 384 jetons au lieu de 8 192 : le JSON coupe ne se recolle pas, et l'utilisateur recevait une erreur apres une minute d'attente. Un critere sans reponse reste affiche comme absent, et il est journalise. Le re-ranking par le modele recoit un systeme neutre : sans lui, `generate` retombait sur l'instruction du chat, qui exige de finir par « Sources: », au lieu des notes JSON demandees. Test reel sur demande (mistral_live, un appel) : JSON valide, une ligne par critere. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>f8caf8a -
fix(mistral): les reponses ne sont plus coupees, les 429 sont rejoues
Les fins de reponse de Mistral sont traduites dans le vocabulaire du reste du code : « length » et « model_length » deviennent « MAX_TOKENS » (rag_service n'attendait que ce nom, et une reponse coupee en flux s'arretait au milieu d'une phrase, sans mention), « stop » « STOP », « error » « ERROR ». Une reponse coupee — par le budget de jetons, ou par un flux rompu apres les premiers mots — est completee : une suite de 2 048 jetons au plus est demandee, le texte deja produit en prefixe de l'assistant (`"prefix": true`), l'echo eventuel du prefixe retire au fil du flux. Pas de suite pour un texte qui tourne en boucle, ni pour une reponse JSON. La mention reste seulement si la suite echoue ; un flux rompu a sa propre mention (« la connexion avec le modèle a été coupée »). Le chat, les reponses conversationnelles et l'explication d'article la portent. Debit : un limiteur par modele (0,5 requete par seconde pour ministral-14b, 3 pour ministral-8b, 4 generations simultanees). Un 429 de debi…cf54f10 -
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 -
test: fixtures aux 14 domaines canoniques
Les fixtures designaient des categories par leurs anciens noms (« Droit Civil », « Droit Pénal », « Droit des Affaires et OHADA ») : 62 tests tombaient en erreur au setup depuis la migration 013. Elles passent par les constantes CIVIL, PENAL et AFFAIRES de legal_domain_classifier, et les comptes par len(CANONICAL_DOMAINS) : le prochain renommage ne cassera plus rien. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>5ae5d91 -
feat(limiteur): limiteur de debit partage pour les API externes
Les paliers gratuits sont etroits (Groq : 30 requetes et 8 000 jetons par minute, 1 000 requetes et 200 000 jetons par jour, par modele ; Mistral ministral-14b : 30 requetes par minute) et chaque depassement coutait une requete perdue plus une attente imposee. app/core/limiteur.py fait attendre AVANT d'envoyer : - debit par seau a jetons (GCRA), avec rafale ; - fenetres glissantes : jetons par minute, jetons d'entree par minute, requetes et jetons par jour ; - repousser() apres un 429, recaler() sur les en-tetes x-ratelimit-*, qui voient aussi les appels des autres processus ; - attente_max : au-dela, AttenteTropLongue sans rien consommer ; - un semaphore de concurrence par boucle d'evenements. Horloge et sommeil injectables : les 33 tests tournent sans aucun vrai sleep. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>9a31d98 -
fix(migrations): 013 convergente et reversible, Alembic exige DATABASE_URL
La premiere version de 013 avait trois defauts : - un retour arriere vide (`pass`) : `alembic downgrade` annoncait un succes et laissait les 14 nouveaux noms en place ; - les categories fusionnees supprimees sans faire suivre laws.suggested_categories (un tableau d'identifiants, sans cle etrangere) : 5 lois de jurix_dev gardent des identifiants disparus ; - les categories cibles inserees apres le re-pointage des lois. 013 suit desormais le modele de 007 : renommage sur place, insertion des cibles, re-pointage des lois (y compris quand un renommage est saute parce que la cible existe deja), suggestions suivies par array_replace, suppression de ce qui est hors des 14, nettoyage des suggestions (identifiants inconnus et doublons retires, ordre garde), puis controles finaux. Les noms sont mis en minuscules par PostgreSQL des deux cotes : sous une locale C, un .lower() Python ne s'accorde pas avec lower() sur « Éducation ». Le retour arriere defait 013 et rien d'autre : lois de Sant…e53a603