Spaces:
Runtime error
Runtime error
Commit History
21 days ago
-
docs: ajouter les metadonnees Hugging Face Space au README
717d7c3
23 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
28 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 -
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(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(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
-
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 -
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
-
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 -
docs(deploiement): procedure de bascule des embeddings sur une base peuplee
Ecrite, PAS executee : on ne sait pas encore s'il existe une base de production. Rien n'a ete fait hors du poste de developpement. LE PIEGE QU'ELLE DESAMORCE. Le conteneur lance `alembic upgrade head` a chaque demarrage, et la migration c4d5e6f7a8b9 met tous les vecteurs a NULL. Deployer suffit donc a declencher la purge, sans autre geste. La procedure commence par lire l'etat de la base visee, puis la sauvegarde : `alembic downgrade` retablit le schema 3072, pas les vecteurs. La regeneration se fait hors du processus de l'API. Le script charge sa propre copie du modele (~1,5 Go) et occupe les coeurs ; sur un petit hebergement, il tourne depuis une machine qui a le modele, contre la base distante. La fin se verifie par la sante : vectors.missing et vectors.foreign a 0. Commandes verifiees sur ce poste, sur les bases locales : - --dry-run et --reindex sur jurix_dev, qui est vide ; - de bout en bout sur jurix_test, avec le vrai modele : trois articles regeneres, provenance ecrite, s…7b3ffb0 -
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
-
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 -
feat(ia): expliquer un article et comparer deux regimes, sur la page, sources a l'appui
Deux usages du modele qui ne passent pas par le chat, parce qu'il n'y a rien a converser. POST /laws/{id}/explain-article explique un article en langage courant a partir de son texte, de ses deux voisins et des metadonnees du document. La page envoie aussi l'extrait affiche : il sert de repli quand la base n'a pas de ligne pour ce numero (193 articles reconnus par la page contre 230 en base), mais seulement apres verification qu'il provient bien du document — sans ce controle, une route publique et non limitee serait un proxy de prompt gratuit sur un quota partage avec le chat. POST /compare compare deux regimes sur des criteres imposes. UNE recherche par sujet, l'une apres l'autre : mesure sur le corpus, la requete unique du chat rendait six articles d'un regime contre trois de l'autre et ratait le bloc 49-54 qui definit le second. Un schema JSON force chaque cellule a citer ses articles ; un numero sans article correspondant part dans unmatched_citations plutot que d'etre avale ; l…88e3dbc
-
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 -
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
-
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 -
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
-
docs: README aligne sur le retrieval par chunk et les embeddings 1536
La table des technologies annoncait des embeddings 3072 dimensions sans mentionner qu'aucun index vectoriel n'etait possible a cette taille. Nouvelle section decrivant ce que renvoie la recherche (des chunks, avec SearchResult derive pour le front), pourquoi la dimension est 1536, et la procedure de backfill apres migration — y compris le fait que la recherche semantique ne rend rien tant qu'il n'est pas termine. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>7c35a54 -
Etape 15 : README realigne sur la pile reelle
Le README (659 lignes) decrivait un autre systeme : Redis 7.2, Meilisearch 1.5, Ollama mistral:7b en prerequis, detection de langue triple-methode avec spaCy, taches Celery, docker-compose.yml inexistant, DATABASE_URL sans le driver asyncpg pourtant obligatoire. Sur une trentaine d'affirmations concretes des sections installation et configuration, environ 24 etaient fausses — plus trompeur qu'absent. Reecrit (165 lignes) sur ce qui tourne reellement : PostgreSQL 16 + pgvector + pg_trgm, Gemini via google-genai, LlamaParse, BackgroundTasks. Documente le chemin d'installation reel (alembic upgrade head -> create_admin.py -> uvicorn), le telechargement du modele fastText non versionne, les routes d'authentification, la politique d'echec de l'ingestion, la mise en place de la base de test, et trois points de deploiement a verifier (migrations non jouees par le conteneur, data/ ephemere, --workers 1 load-bearing). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>641ec0b
-
chore: initial standalone backend for JuriX API (Render)
9cf9a2a