Spaces:
Runtime error
Runtime error
Commit History
23 days ago
-
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
-
feat(decoupage): les grands tableaux coupes entre leurs lignes, le cachet mieux retire
Tableaux. Ils n'etaient jamais coupes : ceux des lois de finances faisaient des chunks jusqu'a 79 000 caracteres, dont l'embedding ne lisait que les 10 000 premiers (110 chunks du corpus). Un tableau markdown se coupe desormais entre deux lignes, jamais au milieu d'une ; chaque morceau repete l'en-tete du tableau et le debut de l'article. Une premiere ligne de donnees prise pour un en-tete n'est pas repetee. Listes nominatives. Le texte de nomination se cherche hors des tableaux : « PROMOTION DE LA FEMME », intitule de programme d'une loi de reglement, faisait d'un tableau budgetaire une liste de 35 personnes. Et des que les tableaux reconnus portent la majorite des lignes, tous les tableaux de l'article sont de la liste : ceux que la detection manquait restaient dans le texte vectorise (155 000 caracteres pour 4 229 gardiens de la paix). Audit sur les 2238 documents : chunks de plus de 10 000 caracteres de 110 a 1 ; plus gros texte vectorise de 136 601 a 15 931 caracteres ; un seul …911f84f -
fix(decoupage): l'article 1er illisible ne sort plus de l'index vectoriel
Le cachet et le sceau recouvrent souvent, dans la marge, le marqueur de l'article 1er. Illisible, l'article partait avec les visas (base legale), ou se collait a la formule d'execution quand c'est l'article 2 qui etait illisible — dans les deux cas hors de l'index vectoriel. Audit du decoupage sur les 2238 documents extraits : 53 n'avaient AUCUN vecteur. Apres correction : 1 (une page que l'OCR n'a pas su lire), et 83 articles reconnus en plus, aucun en moins. - Ce qui suit « DECRETE : », « ARRETE : » ou la formule de promulgation devient un chunk DISPOSITIF, traite comme un article ; a defaut de ces mots, ce qui suit le dernier visa, s'il fait une vraie phrase. - Un chunk n'est une formule d'execution que s'il ne reste presque aucun mot hors des paragraphes qui la portent. - Marqueurs ecorches releves sur tout le corpus : « ARTICIE », « ARTCLE », « RTICLE », « ARTTICLE », « Artice », « ARTI CLE1er », « Article ler », et prefixes parasites (« ■ ARTiCLE », « ww ARTICLE », « 1…f8209f9
-
feat(decoupage): les sorties Docling decoupees sans perte
Mesure sur les sorties Docling du corpus (banc OCR, puis pilote de 19 documents), chaque regle payee par un defaut constate. Nettoyage (markdown_cleanup) : - cachet tolerant a l'OCR (mots colles, lettres confondues), en capitales seulement : « une copie certifiee conforme » a fournir reste du texte ; - filigrane rendu en tableau d'une cellule, lambeaux du cachet ; - nettoyer_markdown met a plat titres et marqueurs decores (« ## ARTICLE », « - ARTICLE 12.- », « ARTIiCLE 81 ») : le sommaire de la page de lecture en voyait 155 sur 191, il en voit 189. Decoupe (text_chunker) : - marqueurs « ARTICLE_1er », « 2. ARTICLE 41.- », mot ecorche par l'OCR, ordinaux jusqu'a la centaine, en-tetes colles (« CHAPITREII ») ; - un renvoi en liste (« - article 12 de la loi ») ne coupe plus l'article. Raffinage (chunk_refiner) : - une liste nominative garde ses noms dans le contenu : elle n'etait enregistree nulle part ailleurs. Seul le vecteur prend la forme resumee, et une longue liste es…14eb55e
-
feat(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 -
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
-
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 -
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 -
fix(chunker,personas): perte de contenu sur faux en-tete, engagement en 500
text_chunker : le motif de section etait compile avec re.IGNORECASE. Un article dont le titre commence par "Titre 1" ou "Chapitre 2" etait donc pris pour un en-tete de section — et le decoupeur coupe tout ce qui suit un en-tete trouve dans un article. L'article perdait la totalite de son texte, sans erreur, juste un "trop court (0 chars), ignore" en journal. Detection desormais sensible a la casse : les en-tetes du corpus sont en capitales. Le compromis est asymetrique, rater un en-tete en minuscules coute une metadonnee, le confondre avec un titre coute l'article. persona_service.get_engagement_metrics : la sortie anticipee "aucune conversation" omettait conversations_per_active_day, champ obligatoire du schema. GET /personas/{p}/engagement renvoyait donc 500 pour tout persona sans conversation sur la periode — soit tous, sur une base neuve. Tests remis en phase avec le code : - extraction : la regle "minimum 3 articles" a ete retiree du chunker et ne doit pas revenir, la majorite…ef3c54d -
fix(validation): trois utilitaires de securite qui ne validaient rien
is_file_size_valid comparait des megaoctets arrondis a deux decimales. Tout fichier de moins de ~5 Ko donnait 0.0 et etait rejete comme "File is empty (0 bytes)" — un decret d'une page pese souvent moins. A l'autre bout, 50,004 Mo s'arrondissait a 50.0 et passait sous la limite. Les deux comparaisons portent desormais sur les octets. sanitize_sql_input interpolait les mots-cles bruts dans rf"\b{kw}\b". La liste contient "--", "/*", "*/", "xp_" et "sp_" : "*/" donnait "\b*/\b", motif invalide (re.error: nothing to repeat). La fonction levait pour TOUTE entree non vide — elle n'a jamais assaini quoi que ce soit. Trois formes de motif selon la nature du mot-cle, et re.escape. sanitize_filename ne retirait que les points en tete : "../../etc/passwd" ressortait en "_.._etc_passwd", qui contient encore la remontee que la fonction existe pour neutraliser. Toute suite de deux points est reduite a un point. sanitize_html retirait la balise <script> mais gardait son corps : "alert(1)" se retr…4806809 -
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 8 : trois bugs du chunker, dont une perte silencieuse de contenu
1. PERTE DE CONTENU (le plus grave) SECTION_PATTERNS utilisait l'alternative de chiffres romains 'I{1,3}V?|IV|V?I{0,3}' — or V?I{0,3} peut matcher la CHAINE VIDE. 'TITRE\s+' suffisait donc a declencher une detection de section : un article intitule "Titre niveau 1" etait pris pour un en-tete, son contenu coupe a zero caractere, puis ecarte comme "trop court". Tout article dont le titre commence par Titre ou Chapitre etait PERDU, sans erreur ni trace. -> [IVXLC]{1,7}, qui ne peut pas etre vide. 2. NUMEROTATION HIERARCHIQUE FUSIONNEE Le groupe capturant s'arretait au premier nombre : '(\d+)(?:er|ère|ème)?(?:\.\d+)*' capturait '1' pour "Article 1.1". Les sous-articles fusionnaient donc avec leur parent — une erreur de citation sur une base juridique. -> '(\d+(?:\.\d+)*)(?:er|ère|ème)?'. Verifie : 1, 1.1 (parent 1), 1.1.1 (parent 1.1), 2. 3. TITRES JAMAIS RENSEIGNES _extract_title etait definie mais appelee NULLE PART : title etait passe en dur a…a00940a -
feat(ocr): extraction LlamaParse et raffinage des chunks pour le RAG
La couche texte des PDF scrapes depuis prc.cm est inexploitable : mesuree a ~20% de rappel sur 6 pages representatives (filigrane www.prc.cm insere au milieu des phrases, cachets officiels lus comme du charabia, et 21% des documents sans aucun texte extractible). Trois moteurs compares sur les memes pages, avec verite terrain relevee visuellement : moteur prose (46 phrases) tableau budgetaire couche texte existante 9/46 0/9 tesseract 5.5 local 34/46 0/9 mistral OCR 46/46 8/9 llamaparse cost_effective 46/46 9/9 Mistral OCR lisait "5 016 910 000" au lieu de "5 816 910 000" de facon reproductible sur 3 tirages — une corruption silencieuse inacceptable sur une base fiscale. LlamaParse retenu. - app/services/llama_parse_service.py : API v2, cache sha256 (une page n'est jamais payee deux fois), retry exponentiel, decoupe des fichi…29a6f4a
-
chore: initial standalone backend for JuriX API (Render)
9cf9a2a