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.

Une question de stock, du panneau latéral à la réponse
Utilisateur via le panneau latéral Auth et contexte JWT, société, droits Limite de débit 10 par minute Modèle toute API compatible Outils typés validés par Pydantic Couche services PostgreSQL, même accès Blocs de résultat tableaux, graphes, KPI Réponse texte + composants Utilisateur via le panneau latéral Auth et contexte JWT, société, droits Limite de débit 10 par minute Modèle toute API compatible Outils typés validés par Pydantic Couche services PostgreSQL, même accès Blocs de résultat tableaux, graphes, KPI Réponse texte + composants
  • Entrée ou sortie
  • Appel LLM
  • Code
  • Stockage
  • Contrôle
  1. 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é.
  2. 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.
  3. Limite de débit 10 messages par utilisateur et par minute. La conversation est chargée ou créée, et le message enregistré.
  4. 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).
  5. 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.
  6. 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.
  7. 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.
  8. Réponse Le chiffre issu du résultat de l’outil, affiché à côté du texte qui l’explique.

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.