Spaces:
Runtime error
Runtime error
Commit History
22 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
-
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(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
-
feat(chat): classement de l'intention en local, sans appel a Gemini
Chaque question coutait deux appels a Gemini : le classement (juridique, bavardage, question sur l'assistant, hors sujet) puis la reponse. Le classement prenait 3,6 s en reflexion minimale et une requete sur deux du quota gratuit. Il se fait desormais par EmbeddingGemma, deja en memoire pour la recherche : le message est rapproche d'une banque d'exemples etiquetes (intent_examples.py, questions de droit surtout en mots de tous les jours, noms de personnes et nominations compris). Chaque categorie recoit la moyenne de ses 3 exemples les plus proches ; une categorie non juridique ne l'emporte qu'avec 0,04 d'avance — dans le doute, c'est une question de droit. Un message court qui n'est pas clairement juridique est reclasse avec la question precedente (« et pour un mineur ? ») ; un verdict non juridique est encore contredit si un article du corpus est a 0,42 de similarite ou plus. Mesure sur 122 messages etiquetes (tests/fixtures/intent_eval.json, distincts de la banque), banque seule :…52efadf
-
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 -
test: une suite qui ne coute rien et qui ne ment pas sur ce qu'elle verifie
TROIS DEFAUTS, TOUS SILENCIEUX. La base jurix_test n'existait pas, et la sonde ne distinguait pas une base absente d'un serveur injoignable : environ 93 tests — schema vectoriel, index, migrations, SQL de recherche — etaient ignores sous « PostgreSQL injoignable » alors que le serveur repondait. La suite affichait vert sans les avoir executes. La sonde distingue desormais les deux cas par le type d'exception (InvalidCatalogNameError), avec un remede propre a chacun. La cle Gemini factice n'etait posee que si la variable etait vide. Une vraie cle vivant dans .env, tout test atteignant le vrai service d'embeddings appelait l'API PAYANTE — et pouvait passer. La cle factice est maintenant imposee ; JURIX_E2E=1 garde la vraie pour un essai delibere. Tout SearchService construit dans un test prenait le singleton reel. Une doublure automatique, deterministe et unitaire, le remplace. Les tests marques `gemma` en sont exemptes : ils existent pour executer le vrai modele local. LIGNE DE BASE…3c99252
-
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 -
fix(analytics): mesurer au lieu d'inventer, et fermer la porte
Le tableau de bord admin affichait des chiffres fabriques comme des mesures. `GET /analytics/search` et `/analytics/usage` renvoyaient des constantes codees en dur — 350 recherches, 150 ms, 1150 appels, 25 utilisateurs actifs, une repartition 100/50/200 entre les modes — accompagnees d'un champ `note: "Mock data"` que le front ne lisait pas. S'y ajoutaient deux litteraux `change="+12%"` et `"+5.4%"` ecrits directement dans le composant, rendus sous « depuis le mois dernier ». Hors `total_laws` et `recent_laws`, tout le tableau de bord etait fictif. Rien en base ne permettait de faire mieux pour la recherche : `query_cache` ne garde qu'un hachage, avec cinq minutes de duree de vie, et une requete servie depuis le cache n'y cree aucune ligne. Ce n'est pas un journal. La migration f1a2b3c4d5e6 en ajoute un — `search_events`, une ligne par recherche, ecrite sans jamais pouvoir faire echouer la recherche elle-meme. Les metriques d'usage, elles, etaient deja mesurables : `conversations` e…9066a18 -
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
-
test: couverture des comportements qui etaient silencieusement casses
69 tests nouveaux, chacun attache a une panne precise plutot qu'a une fonction. test_chunk_retrieval.py (12) — plusieurs articles d'une meme loi remontent ensemble (acte de deces du DISTINCT ON), l'ordre est celui de la pertinence (acte de deces du ORDER BY law_id), numero/section/page arrivent jusqu'au resultat, l'extrait est balise et centre sur le terme cherche, les filtres mordent sur la branche article, une ligne niveau-loi ne porte pas d'article_id, le semantique rend une ligne par article et des scores bornes, et les resultats document derives portent enfin leurs matched_articles et leur contenu. test_embedding_dimensions.py (11) — output_dimensionality est bien demande, les vecteurs sortent unitaires meme avec normalize=False (l'API ne normalise que la sortie pleine), une dimension inattendue ou un lot tronque levent au lieu de passer, et la cle de cache isole la dimension, le type de tache et la casse. Le test de lecture de cache insere une entree 3072 et verifie qu'elle est…cd71f3c -
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 -
Etape 6 : isolation des tests corrigee — 300 tests passent
Trois defauts d'isolation, dont un qui masquait tous les autres. 1. Boucles d'evenements desalignees (cause de ~40 HTTP 500) asyncio_default_fixture_loop_scope = session placait les fixtures dans une boucle differente de celle des tests. Les connexions asyncpg creees par les fixtures y etaient "attached to a different loop" ; les routes attrapaient l'exception et renvoyaient 500, ce qui donnait l'illusion de bugs applicatifs. -> portee de boucle alignee (function), moteur async cree par test, migration Alembic isolee dans une fixture SYNCHRONE de session (psycopg2, donc independante des boucles) pour ne pas la rejouer a chaque test. 2. Sequence PostgreSQL non realignee apres semis Inserer les 12 categories avec des ids explicites ne fait pas avancer categories_id_seq : le prochain id auto-genere repartait a 1 et heurtait la premiere categorie. -> setval apres le semis. 3. Purge seulement en sortie de test Une interruption laissait la base sale …9729782
-
Etape 4 : harnais de tests sur PostgreSQL reel
Le conftest utilisait SQLite en memoire. C'etait intenable : le coeur du produit est du SQL PostgreSQL brut (websearch_to_tsquery, ts_rank_cd, DISTINCT ON, = ANY(), similarity(), extension vector) et les objets dont il depend (search_vector, index GIN, triggers, query_cache, embedding_cache) n'existent QUE dans les migrations, jamais dans Base.metadata. create_all ne pouvait pas produire un schema utilisable — une suite verte sur SQLite aurait certifie du code qui renvoie 500 en production. - conftest reecrit : moteur de session construit par `alembic upgrade head` sur une vraie base, isolation par TRUNCATE ... RESTART IDENTITY CASCADE. L'isolation transactionnelle a ete essayee et documentee comme ecartee : les SAVEPOINT du dialecte asyncpg refusent de s'executer dans une transaction ouverte manuellement, et rollback_only casse les tests qui relisent leurs propres ecritures. - Marquage `integration` automatique selon les fixtures demandees, plutot que declare dans 21 fich…0fdac35
-
chore: initial standalone backend for JuriX API (Render)
9cf9a2a