Vue d'ensemble
Le Djezzy Commercial Copilot est un assistant multi-agents qui permet à la direction commerciale de Djezzy de poser des questions en langage naturel sur cinq flux de données commerciales et d'obtenir des réponses narratives avec des graphiques en direct, sans passer par une file de tickets BI. J'ai dirigé l'équipe de cinq personnes qui l'a construit pendant le stage du Data Science Immersion Program de 6 semaines, pris en charge l'architecture commune et réalisé l'essentiel de l'implémentation sur l'ensemble du monorepo.
Problème
Les responsables commerciaux qui voulaient comprendre les chiffres de la veille devaient déposer une demande et attendre dans une file BI déjà surchargée. L'extraction manuelle introduisait des erreurs d'interprétation, la plus dommageable étant de comparer un jour férié à un jour ordinaire un an plus tôt et d'en tirer la mauvaise conclusion. Et 13 mois de données saisonnières riches restaient en grande partie inexploités pour y chercher des corrélations expliquant les variations de la demande.
Contexte
Le système sert la direction commerciale de Djezzy : le personnel des ventes, du marketing et de la planification comme utilisateurs finaux, la direction commerciale comme consommatrice des tableaux de bord, et les équipes BI/données comme groupe dont le Copilot est censé alléger la charge. C'est un monorepo de cinq services déployables indépendamment (api, workers, mcp-proxy, etl et frontend) qui partagent un même paquet de contrat Pydantic/SQLAlchemy, pour que le schéma de sortie ne puisse jamais diverger d'un service à l'autre.
Mon rôle
J'ai dirigé une équipe de cinq personnes (ce rôle de responsable a été convenu au sein de l'équipe, ce n'est pas un titre formel), pris en charge l'architecture commune (l'organisation du monorepo, l'ossature d'exécution Temporal/LangGraph, le pipeline de livraison asynchrone et les contrôles de CI) et réalisé l'essentiel du travail d'implémentation sur les services, d'après l'historique des commits. Le reste de l'équipe a développé ses composants déployables au-dessus de cette ossature commune.
Objectifs
- Permettre à tout utilisateur commercial autorisé de répondre seul à ses questions sur les données, en langage naturel
- Détourner de l'équipe BI les demandes d'extraction ponctuelles et répétitives
- Faire passer le délai d'obtention d'une information de plusieurs heures ou jours à quelques secondes
- Imposer des bases de comparaison tenant compte des jours fériés et des événements pour que les comparaisons de périodes soient mathématiquement équitables
- Conserver les analyses résolues dans une mémoire partagée pour que le département ne recalcule jamais deux fois la même réponse
- Garder l'accès web externe sous approbation humaine et limité à une liste blanche de domaines
Décisions techniques
- Séparer les composants déployables (api, workers, mcp-proxy, etl, frontend) sans les séparer en dépôts : un monorepo, cinq services à l'échelle indépendante, partageant un contrat de sortie Pydantic unique et un jeu de modèles SQLAlchemy via packages/common
- Utiliser Temporal pour le tour de l'agent, afin qu'une requête analytique lourde survive aux plantages de workers et aux reprises au lieu de bloquer l'utilisateur derrière un indicateur de chargement, avec des tests de déterminisme par le Replayer de Temporal plutôt que de simples vérifications du cas nominal
- Utiliser LangGraph pour que chaque persona (Explorer/Researcher/Planner) soit un vrai sous-graphe compilé avec sa propre boucle d'appel d'outils, plutôt qu'un seul prompt censé tout faire
- Valider chaque requête SQL générée par le LLM avec sqlglot avant exécution, puisque l'auteur de la requête (le modèle) n'est pas fiable. La validation s'exécute après la génération et avant tout contact avec l'entrepôt
- Garder le LLM auto-hébergé (Qwen servi via vLLM) plutôt que d'appeler une API tierce, car les données commerciales ne peuvent pas quitter l'infrastructure de Djezzy
- Soumettre toute recherche web externe à une approbation humaine et à une liste blanche de domaines via un service de proxy MCP dédié, pour que l'agent puisse ajouter du contexte sans accès supervisé à internet
- Livrer les résultats par Kafka et WebSocket plutôt que par interrogation périodique, avec un niveau de cache par tâche pour qu'un succès de cache notifie quand même l'utilisateur sans relancer le pipeline
Architecture
Une interface de chat React/Vite parle à un BFF FastAPI qui ne fait lui-même aucun raisonnement d'agent ; il lance un workflow Temporal et renvoie un identifiant de tâche. Ce workflow consulte un cache sémantique partagé, puis exécute un tour LangGraph comme activité durable : le chemin du persona détermine quels outils sont disponibles parmi le text-to-SQL encadré, la recherche en mémoire partagée, la normalisation des dates et la recherche web soumise à un humain. Si l'agent a besoin de contexte web, le workflow se met en pause sur un signal jusqu'à ce qu'un humain l'approuve. La réponse finale respecte toujours un contrat strict texte plus graphiques ; elle est mise en cache et publiée comme événement de fin Kafka, puis livrée au frontend par WebSocket.
Pipeline de requêtes asynchrone
ne bloque jamaistask_id en quelques millisecondes et la connexion est libre. Durable :un workflow Temporal fait le travail lent et la réponse arrive par un socket dès qu'elle est prête. Chaque instruction SQL générée est analysée par sqlglot avant d'atteindre un entrepôt en lecture seule, et toute recherche web s'arrête pour une approbation humaine.Captures d'écran de l'étude de cas
2 vuesFonctionnalités clés
- Trois personas avec une profondeur de raisonnement et un accès aux outils distincts : descriptif, diagnostic et prescriptif
- Text-to-SQL encadré sur un entrepôt analytique compact en lecture seule
- Moteur de normalisation des dates couvrant l'Aïd, le Ramadan, les périodes d'examens et les campagnes commerciales
- Mémoire partagée sur pgvector avec un filtre de disponibilité des KPI et de période avant de servir toute réponse en cache
- Outil de corrélations cachées qui calcule les comparaisons en code plutôt que de se fier à une moyenne naïve issue d'un SQL généré
- Exécution asynchrone et durable avec des reprises bornées par activité et une récupération après plantage de worker
- Authentification par rôle (analyst/manager/admin/viewer) avec un parcours JWT complet d'inscription, connexion, rafraîchissement et réinitialisation
Défis
- Garder honnêtes les garanties de sortie structurée d'un LLM auto-hébergé : le modèle déployé ignorait silencieusement un paramètre de décodage guidé, si bien que les appels d'outils et les réponses JSON échouaient à la validation jusqu'à ce que l'équipe passe au standard de décodage que ce modèle respecte réellement
- Coordonner cinq personnes sur cinq branches parallèles dans un seul dépôt sans conflits entre composants, ce qui est précisément la raison d'être du paquet de contrat partagé
- Intégrer l'équité dans les comparaisons de dates au lieu de la laisser à la discipline de chaque analyste, car une moyenne avant/après naïve tombe droit dans les effets de jour de la semaine
- Prouver les garanties de durabilité au lieu de les supposer : la récupération après plantage de worker demandait l'outil Replayer de Temporal, pas un simple test du cas nominal réussi
- Cadrer une vision BI large dans une fenêtre de livraison de 6 semaines sans perdre le cœur asynchrone, le moteur agentique ni la sortie multimodale
Ce qui a été livré
- Un pipeline agentique fonctionnel, vérifié en direct de bout en bout : prompt en entrée, exécution Temporal durable, livraison Kafka/WebSocket, réponse en cache relue correctement
- Livraison du text-to-SQL encadré, de l'orchestrateur LangGraph avec ses trois personas, du pipeline de livraison asynchrone, de la mémoire sémantique partagée, de la normalisation des dates et de la découverte de corrélations cachées
- Un vrai système d'authentification (accès par rôle, JWT limités à un usage précis) et un frontend de chat fonctionnel avec rendu de graphiques en direct
- Une PR par ticket suivi, avec les contrôles de CI (lint, types, tests, test de fumée du conteneur) au vert à chaque fusion
Ce que j'ai appris
- Servir soi-même un modèle échange la commodité d'une API contre un vrai contrôle de la localisation des données, et fait passer la question « le modèle respecte-t-il exactement ce paramètre de décodage ? » de problème d'un autre à votre problème
- La durabilité est une propriété testable, pas une hypothèse. Les garanties de reprise après plantage demandent un outil comme le Replayer de Temporal, pas une suite de cas nominaux au vert
- Un monorepo de services déployables indépendamment ne reste cohérent que si le contrat partagé est imposé par le code, via un seul paquet importé, et non par une convention dont cinq personnes sont censées se souvenir
- Les systèmes agentiques sont plus lisibles quand chaque outil correspond à une règle métier (valider le SQL, normaliser la date, filtrer l'accès à la mémoire) plutôt qu'à un grand prompt censé tenir toutes les règles à la fois
Améliorations futures
- Terminer les objectifs d'infrastructure restants : déploiement Kubernetes/Helm en production et observabilité renforcée
- Passer le modèle de raisonnement auto-hébergé à un Qwen3 plus grand quand la capacité du VPS le permettra
- Combler l'écart SSR/tableau de bord du matin laissé par le passage du frontend de Next.js à Vite