Tahanot
Un mini-ERP dont l’assistant ne passe que par la porte d’entrée
Tahanot est un mini-ERP auto-hébergé, inspiré des idées d’Odoo et reconstruit de zéro : ventes, achats, stocks, facturation et un dashboard pour une petite entreprise de négoce multi-sociétés. Son assistant IA répond aux questions sur l’activité en appelant les mêmes services, avec les mêmes contrôles de permissions, que le reste de l’application. Il n’écrit jamais de SQL.
- Type
- Projet personnel, 2026, conçu et réalisé de bout en bout
- Modules
- Ventes, achats, stocks avec reliquats, facturation et paiements, dashboard
- Assistant
- Boucle d’outils écrite à la main, 10 outils typés, jusqu’à 8 tours
- Données
- PostgreSQL 16, SQLAlchemy 2, migrations Alembic
- Tests
- 220 fonctions de test backend et 26 frontend
Un prototype fonctionnel, en version 0.1. Les achats, les avoirs et l’interface en français vivent sur une branche de fonctionnalité pas encore fusionnée.
Une question à l’assistant
L’assistant est un modèle dans une boucle, avec des outils typés. Tout ce qu’il peut voir passe par la même couche de services et les mêmes contrôles d’accès que l’API REST : il ne peut donc pas en savoir plus que la personne qui pose la question.
- Entrée ou sortie
- Appel LLM
- Code
- Stockage
- Contrôle
- Utilisateur « Combien d’unités de SKU-102 reste-t-il dans l’entrepôt 1 ? » Depuis la page d’un enregistrement, le panneau envoie aussi l’enregistrement consulté.
- Auth et contexte Le JWT et l’en-tête de société déterminent l’utilisateur, les sociétés qu’il peut voir, ses groupes et ses droits par modèle de données. Une société qu’il ne peut pas voir donne un 403.
- Limite de débit 10 messages par utilisateur et par minute. La conversation est chargée ou créée, et le message enregistré.
- Modèle Le prompt système est reconstruit à chaque requête : date, utilisateur, entrepôts, devise, règles d’ancrage dans les données et un catalogue de champs généré depuis le registre des modèles de données en service. Le modèle demande get_stock(sku="SKU-102", warehouse_id=1).
- Outils typés Les arguments sont validés par Pydantic, puis l’outil appelle le service de stock avec les droits de l’utilisateur lui-même. Les champs de coût reviennent marqués « restricted » pour les utilisateurs sans accès.
- Modèle Le résultat revient encadré comme une donnée non fiable, jamais comme une instruction. Plus aucun appel d’outil : il répond.
- Blocs de résultat Les données renvoyées par les outils deviennent de vrais composants d’interface : ici une tuile de stock ; ailleurs des tableaux qui renvoient aux enregistrements, des graphiques en barres et des tuiles KPI que l’on peut épingler au dashboard.
- Réponse Le chiffre issu du résultat de l’outil, affiché à côté du texte qui l’explique.
Cette requête ne passe pas par ici. Les autres chemins sont listés plus bas.
Voilà toute la requête. Envie de voir tourner quelque chose de ce genre sur vos données ?
Sélectionnez une case du schéma, ou suivez la requête étape par étape.
Les autres chemins du graphe
- Jusqu’à 8 tours d’appels d’outils ; au-delà, la réponse est marquée comme potentiellement partielle.
- Une erreur d’outil revient au modèle comme un résultat, pas comme une exception : il peut ainsi l’expliquer et s’adapter.
- Les outils d’écriture (créer un devis, confirmer une commande, ajouter une règle de réapprovisionnement) sont désactivés par défaut. Une fois activés, le premier appel renvoie une proposition et un jeton HMAC valable 600 s ; seul un second appel avec le même payload l’exécute.
- Erreur du fournisseur : le flux se termine par un événement d’erreur. Il n’y a pas de nouvelle tentative silencieuse.
L’ERP en dessous
Les idées d’Odoo, pas son code : une couche de modèles générique, des documents aux états explicites, et un stock tenu en quantités par emplacement.
-
Un registre de modèles générique
Un seul routeur CRUD avec filtres de domaine sert tous les modèles ; les actions métier, comme confirmer une commande ou valider une livraison, ont leurs propres routes.
-
Des documents en machines à états
Devis, commandes et livraisons passent par des états explicites.
-
Un stock par quants
Le stock est tenu en quantités par produit et par emplacement, avec des variantes de produit, et des reliquats quand une livraison n’est faite qu’en partie.
-
Factures et paiements
Paiements de factures et avoirs, avec des tests de concurrence et de verrouillage exécutés sur PostgreSQL.
-
Une seule primitive d’agrégation
read_group alimente aussi bien le dashboard que les vues pivot et l’assistant.
-
Deux sociétés, deux langues
Multi-sociétés dès le modèle de données, avec une interface en anglais et une en français en cours.
Choix d’ingénierie
-
Le modèle n’écrit jamais de SQL
Il appelle des outils typés et validés, branchés sur les mêmes services et les mêmes contrôles de permissions que l’API REST.
-
Indépendant du fournisseur
Les messages utilisent un format neutre et l’endpoint par défaut est compatible OpenAI : passer à Claude ou à un serveur local n’est qu’un changement de configuration.
-
Modèles sur liste d’autorisation, champs masqués
21 modèles de données sont visibles par l’assistant. Les champs de coût sont réservés à quatre groupes, et les champs secrets ne sont jamais sérialisés.
-
Non fiable par défaut
Les résultats d’outils sont encapsulés comme des données non fiables, et le prompt système précise que ces données ne sont jamais des instructions. Un test vérifie qu’un nom de produit piégé reste une donnée.
-
Les écritures exigent une seconde étape
Une écriture renvoie d’abord une proposition et un jeton signé sur son payload ; un payload modifié est rejeté.
-
Des réponses sous forme de composants
Les résultats s’affichent en tableaux, graphiques et tuiles construits à partir des données des outils, pas seulement en texte.
Technologies
- Backend
- Python 3.12, FastAPI, SQLAlchemy 2, Alembic, Pydantic, PyJWT, Server-Sent Events
- Données
- PostgreSQL 16
- IA
- boucle d’outils écrite à la main, SDK Anthropic et OpenAI
- Frontend
- React 18, TypeScript, TanStack Query et Table, Recharts, Tailwind CSS 4, Vite
- Infra
- Docker Compose, nginx
- Qualité
- pytest, Vitest, Testing Library
Vous construisez quelque chose de similaire ?
Je détaille volontiers la conception, les compromis, ou ce que je changerais ensuite.