docs: documenter l assistant et son systeme de RAG
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018BAUeCFpDkRD6tU5wGsc1C
This commit is contained in:
parent
f0eb45131b
commit
8e393b611a
142
docs/features/trade-assistant.md
Normal file
142
docs/features/trade-assistant.md
Normal file
@ -0,0 +1,142 @@
|
||||
# Assistant commerce international
|
||||
|
||||
Branche : `bot_ai_help`, créée depuis `preparation_prod`.
|
||||
|
||||
## Fonctionnement
|
||||
|
||||
Page : `/fr/dashboard/assistant` ou `/en/dashboard/assistant`, accessible depuis les ressources du dashboard pour tout compte authentifié actif, y compris Bronze. L’API est protégée par le garde JWT global. L’utilisateur et l’organisation viennent exclusivement de la session validée, jamais du corps de requête.
|
||||
|
||||
| Offre de l’organisation | Questions par utilisateur et par jour |
|
||||
| --- | ---: |
|
||||
| Bronze | 3 |
|
||||
| Silver | 10 |
|
||||
| Gold | 15 |
|
||||
| Platinium | illimité |
|
||||
|
||||
Platinium est une offre sur devis et n’est pas plafonnée : la limite vaut `-1`, la convention déjà utilisée dans le domaine pour `maxLicenses` et `maxShipmentsPerYear`. La consommation reste comptée — c’est la base du suivi de coût — mais elle ne bloque jamais. Le décompte renvoyé vaut alors `remaining: -1` et non `0`, qui se lirait partout comme un quota épuisé.
|
||||
|
||||
**Un compte `ADMIN` dispose de l’offre Platinium, donc d’un quota illimité**, quel que soit l’abonnement de son organisation. Ce n’est pas une règle propre à l’assistant : `SubscriptionService.getSubscriptionOverview()` renvoie déjà `PLATINIUM` à tout compte ADMIN, ce que lit toute l’interface (badge d’offre, licences illimitées, absence d’échéance). L’assistant lisait auparavant l’abonnement brut : un administrateur voyait « Platinium » partout et n’obtenait que les trois questions Bronze de son organisation. La règle vit désormais dans `domain/services/subscription-access.ts`, appelée par les deux services, pour qu’elles ne puissent plus diverger.
|
||||
|
||||
Portée : `ADMIN` est le rôle d’administration de la plateforme, pas celui d’un client — les comptes créés par inscription sont `MANAGER`. L’exemption ne s’étend donc pas aux organisations clientes. À reconsidérer si `ADMIN` devait un jour être attribué côté client.
|
||||
|
||||
Sans abonnement actif, le quota Bronze s’applique. Une offre stockée inconnue (ancienne valeur, ligne écrite à la main) retombe sur Bronze, l’offre la plus restrictive, plutôt que de produire un `NaN`. Les limites sont dans `domain/services/trade-assistant-policy.ts`.
|
||||
|
||||
L’offre appliquée est affichée à côté du décompte, dans le composer : c’est ce qui rend un quota inattendu compréhensible sans ouvrir la page d’abonnement.
|
||||
|
||||
Les quotas sont individuels, persistés dans PostgreSQL, renouvelés à minuit Europe/Paris (changements d’heure compris). Une réservation atomique avant chaque appel empêche les dépassements entre onglets ou instances serveur. Un changement d’offre ne remet pas à zéro les questions déjà consommées dans la journée. Une erreur OpenAI rend la place réservée ; un arrêt brutal du serveur peut conserver une place consommée jusqu’au lendemain. Une requête reçue à la frontière de minuit peut inviter à réessayer après rafraîchissement du quota.
|
||||
|
||||
Après épuisement, l’envoi IA est bloqué côté serveur. L’aide guidée gratuite et `support@xpeditis.com` n’apparaissent qu’à ce moment-là, insérés dans le fil de discussion : tant que du quota reste, ils n’occupent pas l’écran. Le support est disponible même sans clé OpenAI. Le lien ouvre un email : aucun email automatique n’est envoyé.
|
||||
|
||||
Le décompte est affiché en permanence en bas à droite du champ de saisie, et rafraîchi après chaque envoi, au retour dans l’onglet et toutes les 30 secondes. La pastille porte une barre **proportionnelle**, de largeur fixe : une jauge d’une tranche par question ne tenait à aucune échelle — trois filets de 4 px sur Bronze, un bloc rayé de quinze barres sur Gold, et rien de représentable en illimité. Sur Platinium, la barre disparaît au profit de « Questions illimitées ».
|
||||
|
||||
## Conversations
|
||||
|
||||
Les échanges sont organisés en conversations persistées (`trade_conversations`, `trade_messages`), listées par ancienneté dans un rail latéral, renommables et supprimables. Une conversation n’est créée qu’à la première question : ouvrir « Nouvelle conversation » n’écrit rien. Si le fournisseur échoue sur cette première question, la conversation est supprimée et le quota rendu — aucune conversation vide ne subsiste.
|
||||
|
||||
Chaque question consomme une unité de quota, quelle que soit la conversation. Les **huit derniers tours** de la conversation sont retransmis au modèle ; au-delà, l’historique coûte plus qu’il n’apporte. Toutes les requêtes du dépôt portent l’identifiant de l’utilisateur : une conversation appartenant à quelqu’un d’autre répond « introuvable », sans se distinguer d’une conversation inexistante, et la propriété est vérifiée **avant** toute réservation de quota.
|
||||
|
||||
**Les questions et les réponses sont désormais enregistrées en base**, contrairement à la première version où rien n’était persisté. La suppression d’une conversation, ou d’un utilisateur, supprime ses messages par cascade. Ce point est à refléter dans la politique de confidentialité et le registre des traitements.
|
||||
|
||||
## Recherche documentaire (RAG)
|
||||
|
||||
Le modèle reçoit, avec la question, les extraits du wiki Xpeditis les plus proches. Le corpus n’est pas une base séparée à maintenir : il est **extrait du wiki du site lui-même** (`dashboard.wikiPages` dans `apps/frontend/messages/{fr,en}.json`) par `npm run knowledge:build`, qui écrit `apps/backend/src/infrastructure/ai/knowledge/wiki-corpus.json` — 89 fragments par langue, versionnés pour que l’image backend reste autonome. Une réponse et la page wiki citée ne peuvent donc pas diverger.
|
||||
|
||||
L’interface affiche sous chaque réponse les pages d’où proviennent les extraits, en liens cliquables vers le wiki.
|
||||
|
||||
Deux caches évitent de refaire le même calcul :
|
||||
|
||||
1. **L’index** est vectorisé une seule fois. La clé Redis contient l’empreinte SHA-256 du corpus : tant que le wiki ne change pas, aucun appel d’embedding n’est refait, même après un redémarrage ou un déploiement. L’index est aussi gardé en mémoire du processus.
|
||||
2. **Les questions** sont vectorisées une fois par formulation — insensible à la casse, aux accents et à la ponctuation — et pour tous les utilisateurs.
|
||||
|
||||
Sans clé OpenAI, ou si l’API d’embeddings échoue, la recherche bascule sur un score lexical ; si la recherche échoue entièrement, la réponse est produite sans extrait. Aucun de ces cas n’empêche de répondre. PostgreSQL 15 est utilisé sans `pgvector` : l’index tient en mémoire (89 vecteurs de 512 dimensions par langue) et transite en base64 de `Float32Array`.
|
||||
|
||||
Les extraits sont introduits comme de la documentation, pas comme des consignes, et l’instruction système rappelle que toute directive contenue dans la question ou la documentation reste une demande utilisateur. Le modèle ne reçoit jamais les dossiers clients ni les identifiants. Les réponses sont affichées comme du texte, sans exécution HTML. L’assistant explique son absence de vérification en temps réel et oriente les dossiers spécifiques et questions incertaines vers le support.
|
||||
|
||||
## Configuration et lancement
|
||||
|
||||
1. Appliquer les migrations habituelles depuis `apps/backend` : `npm run migration:run`. Deux migrations concernent l’assistant : `trade_assistant_usage` (compteurs) et `trade_conversations` / `trade_messages` (conversations).
|
||||
2. Configurer `OPENAI_API_KEY` dans l’environnement **backend uniquement**. Ne pas utiliser une variable `NEXT_PUBLIC_*`, ni committer une clé.
|
||||
3. `OPENAI_MODEL=gpt-4.1-mini` et `OPENAI_EMBEDDING_MODEL=text-embedding-3-small` par défaut. Toute modification de modèle demande de revoir le prix, les paramètres compatibles et la qualité des réponses. Changer le modèle d’embedding invalide l’index (la clé de cache le contient) : il est reconstruit au premier appel.
|
||||
4. Redémarrer le backend et le frontend.
|
||||
|
||||
Après toute modification du wiki dans `messages/{fr,en}.json`, relancer `npm run knowledge:build` depuis `apps/backend` et committer le corpus régénéré. L’empreinte du corpus changeant, l’index est revectorisé automatiquement au premier appel suivant.
|
||||
|
||||
Sans clé, l’application démarre normalement et propose l’aide guidée. Aucune clé réelle n’est nécessaire aux tests automatisés. L’intégration utilise l’API Responses, `store: false`, un délai maximal de 30 secondes et aucune relance automatique. Limites : 2 000 caractères par question et 800 tokens de sortie. `store: false` ne constitue pas une promesse de rétention nulle chez le fournisseur ; consulter les conditions OpenAI applicables au projet. Les questions et réponses sont, elles, conservées dans la base Xpeditis (voir « Conversations »).
|
||||
|
||||
API, toutes protégées par le garde JWT global :
|
||||
|
||||
| Méthode | Route | Effet |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/api/v1/trade-assistant/quota` | Quota du jour, offre, disponibilité |
|
||||
| `GET` | `/api/v1/trade-assistant/conversations` | Conversations de l’utilisateur, plus récente d’abord |
|
||||
| `GET` | `/api/v1/trade-assistant/conversations/:id` | Messages d’une conversation |
|
||||
| `PATCH` | `/api/v1/trade-assistant/conversations/:id` | Renommer (`{ "title": "..." }`, 60 caractères) |
|
||||
| `DELETE` | `/api/v1/trade-assistant/conversations/:id` | Supprimer la conversation et ses messages |
|
||||
| `POST` | `/api/v1/trade-assistant/questions` | `{ "question": "...", "language": "fr", "conversationId"?: "uuid" }` |
|
||||
|
||||
Sans `conversationId`, la question ouvre une conversation titrée d’après ses 60 premiers caractères, coupés sur un mot entier. Le POST renvoie `mode: ai | guided | unavailable`, le quota à jour et, en mode `ai`, `conversationId`, les deux messages créés, `answer` et `sources`.
|
||||
|
||||
Les compteurs de tokens permettent un suivi du coût observé (la facturation OpenAI fait foi). Une coupure réseau peut être facturée par OpenAI sans qu’une réponse parvienne au serveur ; une nouvelle tentative est alors un nouvel appel. Les coûts correspondants ne sont pas inclus dans le compteur local. La suppression d’un utilisateur supprime ses compteurs par cascade.
|
||||
|
||||
## Chiffrage au 5 septembre 2026
|
||||
|
||||
Source officielle : [GPT-4.1 mini](https://developers.openai.com/api/docs/models/gpt-4.1-mini). Prix standard par million de tokens : **0,40 USD en entrée**, **1,60 USD en sortie**, hors taxes. Le calcul ignore les remises de cache. Intégration : [génération de texte / Responses](https://developers.openai.com/api/docs/guides/text).
|
||||
|
||||
Formule : `(tokens entrée × 0,40 + tokens sortie × 1,60) / 1 000 000` par appel.
|
||||
|
||||
**Les conversations et le RAG augmentent l’entrée.** Une question ne coûte plus seulement l’instruction : s’y ajoutent les extraits du wiki (4 fragments, environ 450 tokens) et jusqu’à huit tours d’historique. Les chiffres ci-dessous remplacent ceux de la première version.
|
||||
|
||||
Hypothèse centrale, à mesurer sur de vraies questions : 1 500 tokens d’entrée (instruction ~300, cadre documentaire ~110, extraits ~450, historique court, question) et 500 tokens de sortie, soit **0,0014 USD par question**.
|
||||
|
||||
| Offre | Appels / 30 jours à quota plein | Coût IA / utilisateur / mois | 1 000 utilisateurs / mois |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| Bronze | 90 | 0,126 USD | 126 USD |
|
||||
| Silver | 300 | 0,420 USD | 420 USD |
|
||||
| Gold | 450 | 0,630 USD | 630 USD |
|
||||
| Platinium | non plafonné | — | — |
|
||||
|
||||
**Platinium n’a plus de plafond, donc plus de coût maximal calculable.** À titre de repère, 50 questions par jour et par utilisateur représentent 1 500 appels sur 30 jours, soit **2,10 USD par utilisateur et par mois** dans le scénario central. Ce poste est à chiffrer au devis, à partir d’un usage estimé avec le client, et à surveiller sur la consommation réelle — les compteurs `trade_assistant_usage` restent alimentés pour cela. Les contrôles de dépense du projet OpenAI sont le seul plafond effectif.
|
||||
|
||||
Un mois de 31 jours coûte 3,33 % de plus. À 25 % d’utilisation des quotas : 0,032 / 0,105 / 0,158 USD par utilisateur et par mois.
|
||||
|
||||
Scénario prudent — conversation longue, historique plein et sortie au plafond : 3 000 tokens d’entrée et 800 de sortie, soit 0,00248 USD/question : **0,2232 USD Bronze**, **0,744 USD Silver**, **1,116 USD Gold** par utilisateur sur 30 jours. La longueur en caractères ne correspond pas exactement aux tokens : ce scénario n’est pas un plafond mathématique. Une provision très conservatrice à 9 000 tokens d’entrée et 800 en sortie représente 0,00488 USD/question, soit 0,4392 / 1,464 / 2,196 USD par mois.
|
||||
|
||||
**Les embeddings sont négligeables, et c’est l’effet recherché.** `text-embedding-3-small` coûte 0,02 USD par million de tokens. L’index complet (178 fragments, environ 21 000 tokens) représente **0,0004 USD une seule fois** — pas par requête, pas par démarrage : la clé de cache est l’empreinte du corpus. Une question vectorisée coûte moins d’un millionième de dollar, et rien du tout si elle a déjà été posée. Sans ces deux caches, revectoriser l’index à chaque redémarrage aurait suffi à rendre ce poste visible en facturation.
|
||||
|
||||
### Viabilité avec les offres du dépôt
|
||||
|
||||
Les montants ci-dessous reprennent les offres présentes dans le code, pas une vérification de vos tarifs commerciaux effectivement publiés : Bronze gratuit (1 utilisateur), Silver 299 EUR/mois (5 utilisateurs), Gold 799 EUR/mois (20 utilisateurs).
|
||||
|
||||
- Silver, 5 utilisateurs consommant tout leur quota : **2,10 USD/mois** d’IA dans le scénario central ; **3,72 USD** dans le scénario prudent.
|
||||
- Gold, 20 utilisateurs consommant tout leur quota : **12,60 USD/mois** d’IA dans le scénario central ; **22,32 USD** dans le scénario prudent.
|
||||
- Bronze gratuit : coût d’acquisition/service, sans revenu d’abonnement. 10 000 utilisateurs actifs à quota plein représentent **1 260 USD/mois** dans le scénario central.
|
||||
- Platinium : quota illimité **et** licences illimitées — le coût est proportionnel à l’usage réel, sans borne technique. C’est le seul poste du chiffrage qui doit être négocié au devis plutôt que déduit du code.
|
||||
|
||||
**Le coût de génération paraît compatible avec Silver et Gold, mais ne suffit pas à démontrer la rentabilité globale.** Les prix d’abonnement sont en EUR et les coûts OpenAI en USD : appliquer le taux réellement facturé. Ne sont inclus ni taxes, hébergement, développement, maintenance, ni temps humain du support. Exemple purement illustratif : si 10 % des utilisateurs sollicitent 10 minutes de support par mois à 30 EUR/heure de coût interne, cela ajoute **0,50 EUR par utilisateur par mois en moyenne**.
|
||||
|
||||
Suivre séparément l’adoption, les tokens, les erreurs, le taux de recours au support et son temps de traitement. Les quotas par compte ne constituent pas un plafond global de dépense : les inscriptions multiples ou la croissance Bronze augmentent le total. Configurer les contrôles de dépense disponibles dans le projet OpenAI avant la mise en service et surveiller la facturation.
|
||||
|
||||
## Vérification
|
||||
|
||||
Backend : `npm test -- --runInBand trade-assistant.service.spec.ts openai-trade.adapter.spec.ts wiki-retriever.spec.ts` depuis `apps/backend`.
|
||||
|
||||
Frontend : `npm test -- trade-assistant assistant-answer-text` depuis `apps/frontend`.
|
||||
|
||||
Test PostgreSQL réel, uniquement sur une instance jetable :
|
||||
|
||||
```sh
|
||||
docker run --detach --rm --name xpeditis-trade-quota-test --tmpfs /var/lib/postgresql/data:rw,size=256m -e POSTGRES_PASSWORD=trade_test_only -p 127.0.0.1:55439:5432 postgres:15-alpine
|
||||
# Depuis apps/backend, une fois PostgreSQL prêt :
|
||||
TRADE_TEST_DATABASE_URL=postgres://postgres:trade_test_only@127.0.0.1:55439/postgres npm test -- --runInBand typeorm-trade-quota.repository.spec.ts
|
||||
docker stop xpeditis-trade-quota-test
|
||||
```
|
||||
|
||||
Le test crée puis supprime un schéma dédié, vérifie 20 réservations concurrentes pour 3 places, l’isolation utilisateur, le jour précédent, la restitution, la comptabilisation de tokens et la suppression par cascade. Il est ignoré sans variable explicite. Validation avant activation réelle : appliquer la migration sur un environnement de test, configurer une clé, vérifier quelques questions représentatives et la pertinence des réponses avec l’équipe métier. Aucun appel OpenAI réel n’a été nécessaire à l’implémentation.
|
||||
|
||||
Validation effectuée : **44 tests backend** (service, validation, adaptateur OpenAI et recherche documentaire), 4 tests sur PostgreSQL 15 réel et **17 tests d’interface** réussis ; compilation backend et contrôles TypeScript frontend réussis. Suites complètes vertes : 281 tests backend, 161 tests frontend.
|
||||
|
||||
Les tests backend couvrent notamment : la propriété d’une conversation vérifiée avant toute dépense de quota, la suppression d’une conversation créée pour une question qui a échoué, le rejeu de l’historique, les sources dédoublonnées par page, la réponse rendue même quand la recherche échoue, la vectorisation du corpus une seule fois par processus, sa réutilisation après redémarrage sans nouvel appel, et l’absence de revectorisation d’une question déjà posée.
|
||||
|
||||
Les tests d’interface couvrent l’envoi (clic et `Ctrl+Entrée`), le quota affiché en bas à droite, la conservation de la question en cas d’erreur réseau, l’affichage du fil avec ses sources cliquables, la liste des conversations groupée par ancienneté, le renommage en place, la confirmation avant suppression, et le fait que l’aide guidée et le support **n’apparaissent qu’une fois le quota atteint** ou le fournisseur indisponible.
|
||||
|
||||
Les réponses OpenAI et les embeddings sont simulés : aucun appel réel n’a été nécessaire. Vérification visuelle effectuée en pilotant Chrome sur le serveur de développement, en 1440, 1024 et 390 px, avec API simulée.
|
||||
Loading…
Reference in New Issue
Block a user