Code Schéma d’API (minimal, stateless)
mapping RAG↔slides
Dans cette page 3 sections
- 1) Schéma d’API (minimal, stateless)
- 2) Swagger / OpenAPI 3.0 minimal couvrant les routes proposées
- 3) version JSON (OpenAPI 3.0), directement importable dans Postman ou Swagger UI.
1) Schéma d’API (minimal, stateless)
Voici un kit prêt-à-l’emploi : schéma d’API minimal (routes + payloads), modèle de table pour le mapping RAG ↔ Odoo slides, et un exemple de message interactif WhatsApp.
Base URL: https://api.votre-domaine.com Auth: Bearer <JWT> (ou clé d’API) + signature HMAC optionnelle Versioning: /v1
1.1 Webhook WhatsApp (réception des messages)
POST /v1/wa/webhook
Headers: X-Hub-Signature-256 (Meta)
Body (extrait) :
{ "object": "whatsapp_business_account", "entry": [{ "changes": [{ "value": { "contacts": [{"wa_id": "261324567890", "profile": {"name": "Jean"}}], "messages": [{ "from": "261324567890", "id": "wamid.HBgM...", "timestamp": "1693839382", "type": "text", "text": {"body": "Je veux apprendre les courbes I-V"} }] } }] }] }
Réponse: 200 OK (toujours)
Traitement: publier un job asynchrone handle_incoming_message.
1.2 Recommandation RAG (interne)
POST /v1/rag/recommend
But: transformer le message en slide (sous-chapitre) recommandé.
Request:
{ "user_id": "wa:261324567890", "text": "Je veux apprendre les courbes I-V", "lang": "fr_FR", "top_k": 5, "min_score": 0.75, "context": { "role": "technicien solaire junior", "level": "débutant", "channel_whitelist": ["PV-BASE", "PV-COMPOSANTS"] } }
Response (succès):
{ "best": { "score": 0.86, "slide_ref": "PV-COURBES-IV-01", "title": "Courbes I–V : principes", "snippet": "Relation courant-tension, MPP, effet température...", "channel": {"id": 42, "slug": "composants-pv"}, "odoo": {"slide_id": 1234, "website_url": "https://learn.ex.com/fr/slides/composants-pv-42?slide=1234"} }, "alts": [ {"score": 0.79, "slide_ref": "PV-COURBES-IV-02", "slide_id": 1235}, {"score": 0.77, "slide_ref": "PV-FICHE-TECHNIQUE-01", "slide_id": 1201} ] }
Response (ambigu) :
{ "need_clarification": true, "question": "Plutôt principes I–V ou effets de température ?" }
1.3 Résolution Odoo (métadonnées du slide)
GET /v1/odoo/slides/{slide_ref}
But: récupérer website_url, slide_id, channel.
Response:
{ "slide_ref": "PV-COURBES-IV-01", "slide_id": 1234, "title": "Courbes I–V : principes", "channel_id": 42, "website_url": "https://learn.ex.com/fr/slides/composants-pv-42?slide=1234", "privacy": "public" // "portal" si privé }
1.4 Envoi WhatsApp (sortant)
POST /v1/wa/send
Request:
{ "to": "261324567890", "message": { "type": "interactive", "template": "reco_formation", "params": { "title": "Recommandation de formation", "subtitle": "Composants PV → Courbes I–V", "url": "https://learn.ex.com/fr/slides/composants-pv-42?slide=1234&utm_source=whatsapp&utm_medium=bot&utm_campaign=elearn", "alts": [ {"label": "Effet température", "ref": "PV-COURBES-IV-02"}, {"label": "Lire une fiche technique", "ref": "PV-FICHE-TECHNIQUE-01"} ] } } }
Response: 202 Accepted
1.5 Journalisation CRM (option)
POST /v1/logs/reco
{ "wa_id": "261324567890", "slide_ref": "PV-COURBES-IV-01", "score": 0.86, "ts": "2025-09-04T17:20:00Z" }
2) Modèle de table (mapping RAG ↔ slides)
2.1 SQL (PostgreSQL)
CREATE TABLE rag_slide_map ( slide_ref VARCHAR(64) PRIMARY KEY, -- clé stable pour RAG/WhatsApp slide_id INT NOT NULL, -- id Odoo slide.slide channel_id INT NOT NULL, -- id Odoo slide.channel title TEXT NOT NULL, lang VARCHAR(10) DEFAULT 'fr_FR', website_url TEXT, -- lien direct si public privacy VARCHAR(10) DEFAULT 'public', -- 'public' | 'portal' tags TEXT[], -- ["pv","composants","iv"] embedding_vector VECTOR(1536), -- si vous stockez embeddings ici (pgvector) updated_at TIMESTAMP DEFAULT now() ); -- Index utiles CREATE INDEX idx_rag_tags ON rag_slide_map USING GIN (tags); CREATE INDEX idx_rag_lang ON rag_slide_map (lang); -- Si pgvector CREATE INDEX idx_rag_vec ON rag_slide_map USING ivfflat (embedding_vector vector_cosine_ops) WITH (lists=100); -- Exemple d'upsert INSERT INTO rag_slide_map (slide_ref, slide_id, channel_id, title, lang, website_url, privacy, tags) VALUES ('PV-COURBES-IV-01', 1234, 42, 'Courbes I–V : principes', 'fr_FR', 'https://learn.ex.com/fr/slides/composants-pv-42?slide=1234', 'public', ARRAY['pv','composants','iv']) ON CONFLICT (slide_ref) DO UPDATE SET slide_id=EXCLUDED.slide_id, channel_id=EXCLUDED.channel_id, title=EXCLUDED.title, lang=EXCLUDED.lang, website_url=EXCLUDED.website_url, privacy=EXCLUDED.privacy, tags=EXCLUDED.tags, updated_at=now();
2.2 Variante Odoo (champ technique)
Dans Odoo, ajoutez sur slide.slide un champ personnalisé x_whatsapp_ref (Char, unique).
Vous pouvez alors synchroniser périodiquement vers rag_slide_map.
3) Exemple d’appel Odoo (JSON-RPC) pour résoudre une ref
import requests, json def odoo_search_by_ref(base_url, db, uid, api_key, slide_ref): payload = { "jsonrpc":"2.0","method":"call","id":1, "params":{ "service":"object","method":"execute_kw", "args":[db, uid, api_key, "slide.slide","search_read", [[["x_whatsapp_ref","=", slide_ref]]], {"fields":["id","name","channel_id","website_url","is_published"]} ] } } r = requests.post(f"{base_url}/jsonrpc", json=payload, timeout=8) s = r.json()["result"][0] return { "slide_ref": slide_ref, "slide_id": s["id"], "title": s["name"], "channel_id": s["channel_id"][0], "website_url": s.get("website_url"), "privacy": "public" if s.get("is_published") else "portal" }
4) Message interactif WhatsApp (Cloud API) – prêt à coller
4.1 Boutons d’appel à l’action (URL + 2 alternatives)
curl -X POST "https://graph.facebook.com/v21.0/<PHONE_NUMBER_ID>/messages" \ -H "Authorization: Bearer <META_ACCESS_TOKEN>" \ -H "Content-Type: application/json" \ -d '{ "messaging_product": "whatsapp", "to": "261324567890", "type": "interactive", "interactive": { "type": "button", "body": { "text": "🎓 Recommandation : Composants PV → Courbes I–V" }, "footer": { "text": "OdoLearn • Langue : FR" }, "action": { "buttons": [ { "type": "url", "url": "https://learn.ex.com/fr/slides/composants-pv-42?slide=1234&utm_source=whatsapp&utm_medium=bot&utm_campaign=elearn", "title": "Ouvrir le sous-chapitre" }, { "type": "reply", "reply": { "id": "ALT:PV-COURBES-IV-02", "title": "Effet température" } }, { "type": "reply", "reply": { "id": "ALT:PV-FICHE-TECHNIQUE-01", "title": "Fiche technique" } } ] } } }'
4.2 Liste déroulante (si beaucoup d’alternatives)
curl -X POST "https://graph.facebook.com/v21.0/<PHONE_NUMBER_ID>/messages" \ -H "Authorization: Bearer <META_ACCESS_TOKEN>" \ -H "Content-Type: application/json" \ -d '{ "messaging_product":"whatsapp", "to":"261324567890", "type":"interactive", "interactive":{ "type":"list", "header":{"type":"text","text":"🎯 Choisissez un module"}, "body":{"text":"J’ai trouvé plusieurs contenus pertinents :"}, "footer":{"text":"OdoLearn"}, "action":{ "button":"Voir les options", "sections":[ {"title":"Composants PV","rows":[ {"id":"GO:PV-COURBES-IV-01","title":"Courbes I–V : principes","description":"~10 min"}, {"id":"GO:PV-COURBES-IV-02","title":"Effet température","description":"~8 min"} ]} ] } } }'
Votre webhook devra traiter les reply.id (ALT:* ou GO:*) pour renvoyer le lien exact.
5) Flux complet (résumé opératoire)
- Webhook reçoit le message WA → publie un job.
- RAG retourne slide_ref, score, alternatives.
- Resolver Odoo sécurise website_url (et gère portal si privé : login-redirect ou proxy signé).
- Sender WA expédie le message interactif avec URL + alternatives.
- Logger écrit l’événement dans Odoo CRM / table rag_slide_map (utile pour analytics & A/B test).
6) Petites bonnes pratiques
- UTM sur chaque URL pour suivre l’usage (Analytics / Odoo).
- Seuil de confiance RAG + clarification si < seuil.
- Langue : préfixe /fr/, /mg/ selon le contact.
- Sécurité : ne divulguez pas d’URL privées → utilisez un proxy (/go/<token>) qui vérifie le droit et redirige.
- Cache de slide_ref → url (TTL 5–15 min) pour éviter de solliciter Odoo à chaque message.
Souhaitez-vous que je vous génère un Swagger (OpenAPI 3.0) minimal à partir de ce schéma pour documentation/développeurs ?
Swagger / OpenAPI 3.0 minimal couvrant les routes proposées
Voici un Swagger / OpenAPI 3.0 minimal couvrant les routes proposées. Vous pouvez le copier tel quel dans Swagger UI / Stoplight / Postman.
openapi: 3.0.3 info: title: API Bot WhatsApp ↔ RAG ↔ Odoo eLearning version: 1.0.0 description: > API minimale pour recommander des sous-chapitres (slides) Odoo eLearning via WhatsApp en s'appuyant sur un moteur RAG. Toutes les réponses sont en JSON. servers: - url: https://api.votre-domaine.com/v1 security: - BearerAuth: [] tags: - name: WhatsApp description: Webhook de réception et envoi de messages - name: RAG description: Recommandations de contenu par similarité sémantique - name: Odoo description: Résolution des métadonnées de slides Odoo - name: Logs description: Journalisation analytique paths: /wa/webhook: post: tags: [WhatsApp] summary: Réception du webhook WhatsApp (Meta Cloud API) description: > Endpoint appelé par Meta. Retourne 200 OK immédiatement puis traite le message en tâche asynchrone (file de jobs). security: - MetaSignature: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/WaWebhookPayload' examples: simple: value: object: whatsapp_business_account entry: - changes: - value: contacts: - wa_id: "261324567890" profile: { name: "Jean" } messages: - from: "261324567890" id: "wamid.HBgM..." timestamp: "1693839382" type: "text" text: { body: "Je veux apprendre les courbes I-V" } responses: "200": description: Accusé de réception /rag/recommend: post: tags: [RAG] summary: Obtenir une recommandation de slide à partir d’un texte utilisateur requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/RagRecommendRequest' examples: demande: value: user_id: "wa:261324567890" text: "Je veux apprendre les courbes I-V" lang: "fr_FR" top_k: 5 min_score: 0.75 context: role: "technicien solaire junior" level: "débutant" channel_whitelist: ["PV-BASE","PV-COMPOSANTS"] responses: "200": description: Recommandation ou demande de clarification content: application/json: schema: oneOf: - $ref: '#/components/schemas/RagRecommendSuccess' - $ref: '#/components/schemas/RagNeedClarification' /odoo/slides/{slide_ref}: get: tags: [Odoo] summary: Résoudre une référence de slide (clé stable) vers ses métadonnées Odoo parameters: - in: path name: slide_ref required: true schema: { type: string, example: "PV-COURBES-IV-01" } responses: "200": description: Métadonnées du slide content: application/json: schema: $ref: '#/components/schemas/OdooSlideMeta' "404": description: Référence inconnue /wa/send: post: tags: [WhatsApp] summary: Envoyer un message interactif WhatsApp (template bouton ou liste) requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/WaSendRequest' examples: bouton: value: to: "261324567890" message: type: "interactive" template: "reco_formation" params: title: "Recommandation de formation" subtitle: "Composants PV → Courbes I–V" url: "https://learn.ex.com/fr/slides/composants-pv-42?slide=1234&utm_source=whatsapp&utm_medium=bot&utm_campaign=elearn" alts: - { label: "Effet température", ref: "PV-COURBES-IV-02" } - { label: "Fiche technique", ref: "PV-FICHE-TECHNIQUE-01" } responses: "202": description: Message accepté pour envoi /logs/reco: post: tags: [Logs] summary: Journaliser une recommandation pour analytics/CRM requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/RecommandationLog' examples: log: value: wa_id: "261324567890" slide_ref: "PV-COURBES-IV-01" score: 0.86 ts: "2025-09-04T17:20:00Z" responses: "201": description: Événement consigné components: securitySchemes: BearerAuth: type: http scheme: bearer bearerFormat: JWT MetaSignature: type: apiKey in: header name: X-Hub-Signature-256 schemas: WaWebhookPayload: type: object additionalProperties: true description: Payload brut envoyé par Meta Cloud API (schéma simplifié). RagRecommendRequest: type: object required: [user_id, text] properties: user_id: { type: string, example: "wa:261324567890" } text: { type: string, example: "Je veux apprendre les courbes I-V" } lang: { type: string, example: "fr_FR" } top_k: { type: integer, default: 5 } min_score: { type: number, format: float, default: 0.75 } context: type: object additionalProperties: true example: role: "technicien solaire junior" level: "débutant" channel_whitelist: ["PV-BASE","PV-COMPOSANTS"] RagHit: type: object properties: score: { type: number, format: float } slide_ref: { type: string } title: { type: string } snippet: { type: string } channel: type: object properties: id: { type: integer } slug: { type: string } odoo: type: object properties: slide_id: { type: integer } website_url: { type: string, format: uri } RagRecommendSuccess: type: object properties: best: { $ref: '#/components/schemas/RagHit' } alts: type: array items: type: object properties: score: { type: number, format: float } slide_ref: { type: string } slide_id: { type: integer } RagNeedClarification: type: object properties: need_clarification: { type: boolean, example: true } question: { type: string, example: "Plutôt principes I–V ou effets de température ?" } OdooSlideMeta: type: object properties: slide_ref: { type: string } slide_id: { type: integer } title: { type: string } channel_id: { type: integer } website_url: { type: string, format: uri } privacy: { type: string, enum: ["public","portal"] } WaSendRequest: type: object required: [to, message] properties: to: { type: string, example: "261324567890" } message: type: object required: [type] properties: type: { type: string, enum: ["interactive","text"], example: "interactive" } template: { type: string, example: "reco_formation" } params: type: object properties: title: { type: string } subtitle: { type: string } url: { type: string, format: uri } alts: type: array items: type: object properties: label: { type: string } ref: { type: string } RecommandationLog: type: object required: [wa_id, slide_ref, score, ts] properties: wa_id: { type: string, example: "261324567890" } slide_ref: { type: string, example: "PV-COURBES-IV-01" } score: { type: number, format: float, example: 0.86 } ts: { type: string, format: date-time }
Version JSON (OpenAPI 3.0), directement importable dans Postman ou Swagger UI.
Voici la version JSON (OpenAPI 3.0), directement importable dans Postman ou Swagger UI.
{ "openapi": "3.0.3", "info": { "title": "API Bot WhatsApp ↔ RAG ↔ Odoo eLearning", "version": "1.0.0", "description": "API minimale pour recommander des sous-chapitres (slides) Odoo eLearning via WhatsApp en s'appuyant sur un moteur RAG." }, "servers": [ { "url": "https://api.votre-domaine.com/v1" } ], "security": [{ "BearerAuth": [] }], "tags": [ { "name": "WhatsApp", "description": "Webhook de réception et envoi de messages" }, { "name": "RAG", "description": "Recommandations de contenu par similarité sémantique" }, { "name": "Odoo", "description": "Résolution des métadonnées de slides Odoo" }, { "name": "Logs", "description": "Journalisation analytique" } ], "paths": { "/wa/webhook": { "post": { "tags": ["WhatsApp"], "summary": "Réception du webhook WhatsApp (Meta Cloud API)", "responses": { "200": { "description": "Accusé de réception" } } } }, "/rag/recommend": { "post": { "tags": ["RAG"], "summary": "Obtenir une recommandation de slide à partir d’un texte utilisateur", "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RagRecommendRequest" } } } }, "responses": { "200": { "description": "Recommandation ou clarification", "content": { "application/json": { "schema": { "oneOf": [ { "$ref": "#/components/schemas/RagRecommendSuccess" }, { "$ref": "#/components/schemas/RagNeedClarification" } ] } } } } } } }, "/odoo/slides/{slide_ref}": { "get": { "tags": ["Odoo"], "summary": "Résoudre une référence de slide vers ses métadonnées Odoo", "parameters": [ { "in": "path", "name": "slide_ref", "schema": { "type": "string" }, "required": true } ], "responses": { "200": { "description": "Métadonnées du slide", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OdooSlideMeta" } } } }, "404": { "description": "Référence inconnue" } } } }, "/wa/send": { "post": { "tags": ["WhatsApp"], "summary": "Envoyer un message interactif WhatsApp", "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WaSendRequest" } } } }, "responses": { "202": { "description": "Message accepté pour envoi" } } } }, "/logs/reco": { "post": { "tags": ["Logs"], "summary": "Journaliser une recommandation", "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RecommandationLog" } } } }, "responses": { "201": { "description": "Événement consigné" } } } } }, "components": { "securitySchemes": { "BearerAuth": { "type": "http", "scheme": "bearer", "bearerFormat": "JWT" } }, "schemas": { "RagRecommendRequest": { "type": "object", "properties": { "user_id": { "type": "string" }, "text": { "type": "string" }, "lang": { "type": "string" }, "top_k": { "type": "integer" }, "min_score": { "type": "number" } } }, "RagHit": { "type": "object", "properties": { "score": { "type": "number" }, "slide_ref": { "type": "string" }, "title": { "type": "string" }, "snippet": { "type": "string" } } }, "RagRecommendSuccess": { "type": "object", "properties": { "best": { "$ref": "#/components/schemas/RagHit" }, "alts": { "type": "array", "items": { "$ref": "#/components/schemas/RagHit" } } } }, "RagNeedClarification": { "type": "object", "properties": { "need_clarification": { "type": "boolean" }, "question": { "type": "string" } } }, "OdooSlideMeta": { "type": "object", "properties": { "slide_ref": { "type": "string" }, "slide_id": { "type": "integer" }, "title": { "type": "string" }, "channel_id": { "type": "integer" }, "website_url": { "type": "string" }, "privacy": { "type": "string" } } }, "WaSendRequest": { "type": "object", "properties": { "to": { "type": "string" }, "message": { "type": "object" } } }, "RecommandationLog": { "type": "object", "properties": { "wa_id": { "type": "string" }, "slide_ref": { "type": "string" }, "score": { "type": "number" }, "ts": { "type": "string", "format": "date-time" } } } } } }
👉 Vous pouvez importer ce JSON dans Postman (Collections > Import > Raw text) ou dans Swagger UI.