"""Genere le rapport technique GlobalAdministration-ChatBot en PDF (polices Windows systeme).""" from fpdf import FPDF import os BASE_DIR = os.path.abspath(os.path.dirname(__file__)) OUT_PATH = os.path.join(BASE_DIR, "Rapport_Technique_GlobalAdministration_ChatBot.pdf") WIN_FONTS = r"C:\Windows\Fonts" F_REG = os.path.join(WIN_FONTS, "arial.ttf") F_BOLD = os.path.join(WIN_FONTS, "arialbd.ttf") F_ITA = os.path.join(WIN_FONTS, "ariali.ttf") F_MONO = os.path.join(WIN_FONTS, "cour.ttf") class PDF(FPDF): def setup_fonts(self): self.add_font("Arial", "", F_REG, uni=True) self.add_font("Arial", "B", F_BOLD, uni=True) self.add_font("Arial", "I", F_ITA, uni=True) self.add_font("Mono", "", F_MONO, uni=True) def header(self): self.set_font("Arial", "B", 10) self.set_text_color(80, 80, 80) self.cell(0, 8, "Rapport Technique - GlobalAdministration-ChatBot", align="C") self.ln(4) self.set_draw_color(180, 180, 180) self.line(10, self.get_y(), 200, self.get_y()) self.ln(4) def footer(self): self.set_y(-13) self.set_font("Arial", "I", 8) self.set_text_color(150, 150, 150) self.cell(0, 10, f"Page {self.page_no()}", align="C") def chapter_title(self, text): self.set_font("Arial", "B", 13) self.set_fill_color(30, 80, 160) self.set_text_color(255, 255, 255) self.cell(0, 9, " " + text, ln=True, fill=True) self.set_text_color(0, 0, 0) self.ln(2) def section_title(self, text): self.set_font("Arial", "B", 11) self.set_text_color(30, 80, 160) self.cell(0, 7, text, ln=True) self.set_text_color(0, 0, 0) self.ln(1) def body(self, text): self.set_font("Arial", "", 10) self.multi_cell(0, 6, text) self.ln(1) def bullet(self, text): self.set_font("Arial", "", 10) self.set_x(self.l_margin + 5) self.multi_cell(self.epw - 5, 6, "- " + text) def code_block(self, text): self.set_font("Mono", "", 8.5) self.set_fill_color(240, 242, 245) self.set_draw_color(200, 200, 200) self.multi_cell(0, 5.5, text, fill=True, border=1) self.set_font("Arial", "", 10) self.ln(2) def table_row(self, cols, widths, header=False): if header: self.set_font("Arial", "B", 9) self.set_fill_color(220, 230, 245) else: self.set_font("Arial", "", 9) self.set_fill_color(255, 255, 255) for text, w in zip(cols, widths): self.cell(w, 6, str(text), border=1, fill=True) self.ln() # ─── Init ──────────────────────────────────────────────────────────────────── pdf = PDF() pdf.setup_fonts() pdf.set_auto_page_break(auto=True, margin=15) pdf.add_page() # ─── Page de titre ─────────────────────────────────────────────────────────── pdf.set_font("Arial", "B", 26) pdf.set_text_color(30, 80, 160) pdf.ln(10) pdf.cell(0, 14, "GlobalAdministration-ChatBot", align="C", ln=True) pdf.set_font("Arial", "", 14) pdf.set_text_color(60, 60, 60) pdf.cell(0, 9, "Rapport Technique - Chatbot Bancaire & Services Publics Algeriens", align="C", ln=True) pdf.ln(4) pdf.set_font("Arial", "I", 10) pdf.set_text_color(120, 120, 120) pdf.cell(0, 7, "Projet de Fin d'Etudes (PFE) - Mai 2026", align="C", ln=True) pdf.ln(8) pdf.set_draw_color(30, 80, 160) pdf.set_line_width(0.8) pdf.line(30, pdf.get_y(), 180, pdf.get_y()) pdf.set_line_width(0.2) pdf.ln(12) # ─── 1. Vue d'ensemble ─────────────────────────────────────────────────────── pdf.chapter_title("1. Vue d'ensemble") pdf.body( "GlobalAdministration-ChatBot est une plateforme multi-tenant d'assistance " "conversationnelle intelligente destinee aux services administratifs et bancaires algeriens. " "Elle permet a plusieurs organisations (banques, poste, securite sociale, mairie, universite) " "de partager la meme infrastructure tout en conservant une isolation stricte de leurs donnees.\n\n" "Les citoyens peuvent poser des questions en langage naturel (francais, arabe, darija) et " "obtenir des reponses depuis la base de connaissances de l'organisation, accompagnees d'une " "carte interactive des services. Quand la base ne contient pas de reponse suffisamment " "pertinente, un LLM leger (LLaMA 3.1 via Groq) prend le relai de facon transparente.\n\n" "L'application est deployable sur HuggingFace Spaces via Docker et fonctionne entierement " "en local sans dependance cloud obligatoire." ) # ─── 2. Architecture ───────────────────────────────────────────────────────── pdf.chapter_title("2. Architecture Generale") pdf.body("Architecture monolithique full-stack avec separation claire frontend / backend :") pdf.code_block( " Navigateur (React SPA)\n" " Chatbot UI | Admin | SuperAdmin\n" " |\n" " HTTP / REST API\n" " |\n" " Backend Flask (Python 3.10)\n" " /ask | /api/... | /api/admin/... | /api/superadmin/...\n" " | |\n" " SQLite DB SentenceTransformer\n" " platform.db + Embeddings (.pt par organisation)" ) # ─── 3. Stack ──────────────────────────────────────────────────────────────── pdf.chapter_title("3. Stack Technologique") widths3 = [50, 60, 80] pdf.table_row(["Composant", "Technologie", "Details"], widths3, header=True) for row in [ ("Backend", "Python 3.10 + Flask", "Flask-CORS, Werkzeug ProxyFix"), ("Frontend", "React 18 + TypeScript", "Vite, React Router v6"), ("NLP / Embeddings", "SentenceTransformers", "Solon-embeddings-mini-beta-1.1"), ("LLM Fallback", "Groq / LLaMA 3.1-8b", "Reponse hors-base (si GROQ_API_KEY)"), ("Recherche", "PyTorch", "Cosine similarity vectorisee"), ("Base de donnees", "SQLite", "platform.db - 4 tables"), ("Cartes", "Leaflet.js", "Marqueurs geolocalises"), ("Deploiement", "Docker + HF Spaces", "Port 7860, user non-root"), ]: pdf.table_row(row, widths3) pdf.ln(4) # ─── 4. NLP ────────────────────────────────────────────────────────────────── pdf.chapter_title("4. Module NLP - Moteur de Recherche Semantique") pdf.section_title("4.1 Pipeline de traitement d'une question") for step in [ "Question recue via POST /ask", "Filtre stopwords FR : rejet des requetes sans contenu semantique (< 4 chars ou mots vides uniquement)", "Detection de salutation -> reponse directe immediate (regex multilingue FR/AR/Darija)", "Encodage double : vecteur brut + vecteur normalise -> moyenne normalisee (PyTorch)", "Cosine similarity avec tous les embeddings de la base, org par org", "Score >= 0.52 : reponse directe de la base + score de confiance affiche", "Score < 0.52 + GROQ_API_KEY : LLaMA 3.1-8b-instant via Groq (reponse contextuelle)", "Score < 0.52 sans cle Groq : message d'orientation listant les organisations disponibles", ]: pdf.bullet(step) pdf.ln(3) pdf.section_title("4.2 Seuil de confiance et formule d'affichage") pdf.body( "Le seuil de confiance operationnel est tau = 0.52 (cosinus brut). Toute correspondance\n" "dont le score est inferieur a ce seuil declenche le mecanisme de fallback.\n\n" "Le score brut est ensuite converti en pourcentage lisible par la formule :\n\n" " confiance(%) = ((c - 0.52) / (1.0 - 0.52)) x 50 + 50\n\n" "Resultat : toute reponse affichee indique >= 50% de confiance. " "Les questions sans reponse (score < seuil) affichent 0%." ) pdf.section_title("4.3 Fallback LLM via Groq") pdf.body( "Lorsque le score de similarite est inferieur a tau = 0.52, le systeme tente un repli\n" "sur LLaMA 3.1 8B Instant via l'API Groq. Ce LLM legere est initialise uniquement si\n" "la variable d'environnement GROQ_API_KEY est definie. Il recoit un prompt systeme\n" "contextualise avec les noms des organisations disponibles, ce qui lui permet de repondre\n" "de facon pertinente meme sans entree correspondante dans la base. En l'absence de cle\n" "Groq, le systeme affiche le message d'orientation classique (liste des organisations).\n" ) pdf.section_title("4.4 Cache des embeddings par organisation") pdf.body( "Les embeddings sont calcules une seule fois puis sauvegardes sur disque " "(embeddings/org_N.pt v=4 + org_N_meta.json). Ils sont recharges en memoire au " "demarrage et invalides automatiquement apres toute operation CRUD sur les entrees.\n" "Cela garantit des reponses rapides sans recalcul a chaque requete." ) pdf.section_title("4.5 Recommandations associees") pdf.body( "En plus de la meilleure reponse, le systeme retourne jusqu'a 5 questions semantiquement " "proches issues de la meme organisation, avec diversite d'intents " "(maximum 2 entrees par categorie d'intent). Ces questions sont affichees comme " "suggestions cliquables dans l'interface." ) # ─── 5. Base de données ────────────────────────────────────────────────────── pdf.chapter_title("5. Base de Donnees (SQLite)") pdf.code_block( "organisations users\n" "------------- -----\n" "id (PK) id (PK)\n" "name (UNIQUE) org_id (FK -> organisations)\n" "slug (UNIQUE) username (UNIQUE)\n" "created_at password (SHA-256)\n" " role : 'admin' | 'superadmin'\n\n" "entries services\n" "------- --------\n" "id (PK) id (PK)\n" "org_id (FK) org_id (FK)\n" "processus name\n" "procedure description\n" "intent link\n" "sub_intent latitude / longitude\n" "question <- indexe semantiquement\n" "response\n" "service / service_link\n" "latitude / longitude" ) # ─── 6. API REST ───────────────────────────────────────────────────────────── pdf.chapter_title("6. API REST") w2 = [22, 84, 84] pdf.section_title("Routes publiques") pdf.table_row(["Methode", "URL", "Description"], w2, header=True) for row in [ ("POST", "/ask", "Poser une question au chatbot"), ("GET", "/api/organisations", "Lister toutes les organisations"), ("GET", "/api/services?org_id=N", "Services geolocalises"), ("GET", "/api/organisations/{id}/services", "Services d'une organisation"), ]: pdf.table_row(row, w2) pdf.ln(3) pdf.section_title("Routes d'authentification") pdf.table_row(["Methode", "URL", "Description"], w2, header=True) for row in [ ("POST", "/api/auth/login", "Connexion (session Flask cookie)"), ("POST", "/api/auth/logout", "Deconnexion"), ("GET", "/api/auth/me", "Utilisateur connecte"), ]: pdf.table_row(row, w2) pdf.ln(3) pdf.section_title("Routes Admin (org-admin, session requise)") pdf.table_row(["Methode", "URL", "Description"], w2, header=True) for row in [ ("GET/POST", "/api/admin/entries", "Lister / Creer une entree FAQ"), ("PUT/DELETE", "/api/admin/entries/{id}", "Modifier / Supprimer une entree"), ("POST", "/api/admin/entries/import", "Import CSV en masse"), ("GET/POST", "/api/admin/services", "Lister / Creer un service"), ("PUT/DELETE", "/api/admin/services/{id}", "Modifier / Supprimer un service"), ]: pdf.table_row(row, w2) pdf.ln(3) pdf.body( "Les routes /api/superadmin/... permettent au superadmin la gestion globale : " "organisations, utilisateurs, toutes les entrees et tous les services, " "toutes organisations confondues." ) # ─── 7. Authentification ───────────────────────────────────────────────────── pdf.chapter_title("7. Systeme d'Authentification") for b in [ "Sessions Flask avec cookies HttpOnly, Secure, SameSite=None (requis pour iframe HF Spaces)", "Mots de passe hasches en SHA-256", "Deux roles : 'admin' (scope = son organisation) et 'superadmin' (scope global)", "Decorateurs @login_required et @superadmin_required protegent toutes les routes sensibles", "Aucune donnee sensible exposee dans les reponses API", ]: pdf.bullet(b) pdf.ln(3) # ─── 8. Frontend React ─────────────────────────────────────────────────────── pdf.chapter_title("8. Frontend React (TypeScript + Vite)") pdf.section_title("Pages principales") w3b = [60, 130] pdf.table_row(["Page", "Role"], w3b, header=True) for row in [ ("App.tsx", "Interface chatbot principale"), ("LoginPage.tsx", "Connexion utilisateur"), ("AdminPage.tsx", "Back-office : CRUD FAQ + services + import CSV"), ("SuperAdminPage.tsx", "Back-office global : organisations et utilisateurs"), ("OrganisationsPage.tsx","Vue publique des organisations"), ]: pdf.table_row(row, w3b) pdf.ln(3) pdf.section_title("Composants cles") for b in [ "BotMessage.tsx : reponse, badge organisation, mini-carte Leaflet, lien externe, GPS, recommandations", "MiniMap.tsx / GlobalMapModal.tsx : cartes Leaflet interactives avec marqueurs", "ServicesPanel.tsx : panneau lateral des services de l'organisation active", "AuthContext.tsx : contexte React global pour la gestion de session", "ChatInput.tsx : saisie utilisateur avec envoi POST /ask", ]: pdf.bullet(b) pdf.ln(3) pdf.section_title("Gestion d'etat principal (App.tsx)") for b in [ "selectedOrgId : organisation choisie manuellement dans le selecteur", "lastOrgId : derniere organisation identifiee par le bot", "effectiveOrgId = selectedOrgId ?? lastOrgId ?? user.org_id", "Indicateur de confiance et d'intent affiches en temps reel dans le header", ]: pdf.bullet(b) pdf.ln(3) # ─── 9. Deploiement ────────────────────────────────────────────────────────── pdf.chapter_title("9. Deploiement (Docker + HuggingFace Spaces)") pdf.body("Le Dockerfile construit l'image en 3 etapes distinctes :") for b in [ "1. Image de base python:3.10-slim + installation Node.js 20 via NodeSource", "2. Build du frontend React : npm install + npm run build -> frontend/dist/", "3. Flask sert a la fois l'API (/ask, /api/...) et les fichiers statiques du build React", ]: pdf.bullet(b) pdf.ln(2) pdf.body( "Le conteneur est execute sous un utilisateur non-root (hfuser, uid=1000) conformement " "aux exigences de securite HuggingFace Spaces. Port expose : 7860.\n\n" "Variables d'environnement : SECRET_KEY (cle Flask), CORS_ORIGINS (origines autorisees)." ) # ─── 10. Points Forts ──────────────────────────────────────────────────────── pdf.chapter_title("10. Points Forts du Projet") for b in [ "Recherche semantique multilingue (francais + arabe) sans API cloud externe", "Architecture multi-tenant : chaque organisation a son espace isole", "Cache intelligent des embeddings : reponses rapides apres le premier calcul", "Geolocalisation integree : chaque reponse peut inclure position GPS + lien Google Maps", "Interface admin complete avec import CSV pour alimenter la base rapidement", "Modele de secours automatique si le modele principal echoue au chargement", "SPA React + API Flask dans un seul conteneur Docker (deploiement simple)", ]: pdf.bullet(b) pdf.ln(3) # ─── 11. Limites ───────────────────────────────────────────────────────────── pdf.chapter_title("11. Limites et Ameliorations Possibles") w4 = [85, 105] pdf.table_row(["Limite actuelle", "Amelioration recommandee"], w4, header=True) for row in [ ("SQLite non adapte a la forte concurrence", "Migrer vers PostgreSQL en production"), ("SHA-256 sans sel pour les mots de passe", "Utiliser bcrypt ou argon2"), ("Inference NLP sur CPU (latence variable)", "GPU ou modele distille plus leger"), ("Pas de pagination backend sur les listes", "Ajouter LIMIT/OFFSET et meta-donnees"), ("SECRET_KEY avec valeur par defaut codee", "Forcer la variable en production"), ("Pas de gestion de sessions expirantes", "Ajouter SESSION_PERMANENT + timeout"), ]: pdf.table_row(row, w4) pdf.ln(6) pdf.set_font("Arial", "I", 9) pdf.set_text_color(100, 100, 100) pdf.cell(0, 6, "Rapport genere automatiquement - GlobalAdministration-ChatBot PFE 2026", align="C", ln=True) # ─── Sauvegarde ────────────────────────────────────────────────────────────── pdf.output(OUT_PATH) print(f"PDF genere avec succes : {OUT_PATH}")