bot WhatsApp


version “prompt API” directement utilisable

Voici une version “prompt API” prête à l’emploi pour OpenAI, en Node.js et Python, avec :

  • un system prompt FR/MG,
  • deux “tools” (fonction calling) : classify_subject (router RAG) et retrieve_passages (votre backend renvoie les extraits),
  • un message template pour injecter la question WhatsApp et les extraits,
  • un format de réponse standardisé avec citations.

Node.js (Express/NestJS)

// ---- System prompt (FR + MG) ---- const SYSTEM_PROMPT = ` Vous êtes un assistant pédagogique universitaire pour une licence d’agronomie (3 ans). Vous répondez UNIQUEMENT à partir des extraits RAG fournis par l’outil retrieve_passages. Règles générales : 1) Détectez automatiquement la langue de la question (FR/MG) et répondez dans cette langue. 2) Si l’information n’est pas dans les extraits : - FR : "Je n’ai pas trouvé d’information précise dans les documents fournis. Veuillez consulter votre enseignant ou le support de cours." - MG : "Tsy nahita vaovao mazava aho tao amin'ny tahirin-kevitra nomena. Azafady mifandraisa amin’ny mpampianatra na ny fitaovana fampianarana." 3) Style : concis, pédagogique (niveau licence), structuré en puces ou courts paragraphes, exemples concrets si possible. 4) Citez toujours les sources ainsi : - FR : "Source : [Titre, page/section]" - MG : "Loharano : [Loharano, pejy/fizarana]" 5) Ne pas halluciner, ne pas inventer. Répondez uniquement à partir des extraits fournis. `; // ---- Function specs ---- const tools = [ { type: "function", function: { name: "classify_subject", description: "Classifier la question vers un namespace RAG (UE/matière).", parameters: { type: "object", properties: { question: { type: "string" }, candidates: { type: "array", items: { type: "string" }, description: "Liste des namespaces disponibles (ex: agro:UE1_sols)." } }, required: ["question","candidates"] } } }, { type: "function", function: { name: "retrieve_passages", description: "Récupérer les extraits RAG top-k pour la question dans un namespace.", parameters: { type: "object", properties: { namespace: { type: "string" }, question: { type: "string" }, top_k: { type: "integer", default: 6 } }, required: ["namespace","question"] } } } ]; // ---- Exemple d’appel (OpenAI Responses API style) ---- import OpenAI from "openai"; const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); export async function answerWhatsApp({ question, namespaces }) { // 1) Router const routing = await openai.chat.completions.create({ model: "gpt-4o-mini", // ou modèle de votre choix messages: [ { role: "system", content: "Tu es un routeur de matière pour des cours d’agronomie." }, { role: "user", content: JSON.stringify({ question, namespaces }) } ], tools: [tools[0]], // classify_subject tool_choice: "auto" }); const call = routing.choices[0]?.message?.tool_calls?.[0]; const picked = call ? JSON.parse(call.function.arguments).candidates?.[0] || namespaces[0] : namespaces[0]; // 2) Retrieval (votre backend doit implémenter cette fonction) // Ici on appelle le modèle pour déclencher le tool, mais en pratique // vous pouvez bypass et appeler directement votre base vecteur. const retrieval = await openai.chat.completions.create({ model: "gpt-4o-mini", messages: [ { role: "system", content: "Tu orchestras un appel à l'outil retrieve_passages." }, { role: "user", content: JSON.stringify({ namespace: picked, question }) } ], tools: [tools[1]], tool_choice: { type: "function", function: { name: "retrieve_passages" } } }); // Simulez la réponse tool avec vos vrais extraits : const passages = await myVectorDbSearch({ namespace: picked, query: question, topK: 6 }); const toolMessage = { role: "tool", tool_call_id: retrieval.choices[0].message.tool_calls[0].id, name: "retrieve_passages", content: JSON.stringify(passages) // [{text, title, page, section, url}] }; // 3) Réponse finale const final = await openai.chat.completions.create({ model: "gpt-4o-mini", messages: [ { role: "system", content: SYSTEM_PROMPT }, { role: "user", content: question }, toolMessage ], temperature: 0.2 }); return final.choices[0].message.content; } // Exemple d’objet passages attendu par le prompt final // passages = [ // { text: "La permaculture ...", title: "Cours de permaculture UE5", page: "p.12", section:"2.1", url:"https://..."}, // { text: "Les sols argileux ...", title: "Introduction aux sols UE2", page: "p.7", section:"1.3", url:"https://..."} // ]; async function myVectorDbSearch({ namespace, query, topK }) { // TODO: interroger pgvector/Qdrant/etc. et formater comme ci-dessus return []; }

Python (FastAPI)

# ---- System prompt (FR + MG) ---- SYSTEM_PROMPT = """ Vous êtes un assistant pédagogique universitaire pour une licence d’agronomie (3 ans). Vous répondez UNIQUEMENT à partir des extraits RAG fournis par l’outil retrieve_passages. Règles : 1) Détecter la langue (FR/MG) et répondre dans cette langue. 2) Si info manquante : - FR : "Je n’ai pas trouvé d’information précise dans les documents fournis. Veuillez consulter votre enseignant ou le support de cours." - MG : "Tsy nahita vaovao mazava aho tao amin'ny tahirin-kevitra nomena. Azafady mifandraisa amin’ny mpampianatra na ny fitaovana fampianarana." 3) Style concis, pédagogique, structuré, exemples si possible. 4) Citer les sources : - FR : "Source : [Titre, page/section]" - MG : "Loharano : [Loharano, pejy/fizarana]" 5) Ne pas inventer : répondre uniquement depuis les extraits fournis. """ TOOLS = [ { "type": "function", "function": { "name": "classify_subject", "description": "Classifier la question vers un namespace RAG (UE/matière).", "parameters": { "type": "object", "properties": { "question": {"type": "string"}, "candidates": {"type": "array", "items": {"type": "string"}} }, "required": ["question","candidates"] } } }, { "type": "function", "function": { "name": "retrieve_passages", "description": "Récupérer extraits RAG top-k pour la question.", "parameters": { "type": "object", "properties": { "namespace": {"type": "string"}, "question": {"type": "string"}, "top_k": {"type": "integer", "default": 6} }, "required": ["namespace","question"] } } } ] # ---- Exemple (openai Python SDK v1) ---- from openai import OpenAI client = OpenAI() def answer_whatsapp(question: str, namespaces: list[str]) -> str: # 1) Router routing = client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role":"system", "content":"Tu es un routeur de matière pour des cours d’agronomie."}, {"role":"user", "content": str({"question": question, "namespaces": namespaces})} ], tools=[TOOLS[0]], tool_choice="auto" ) tc = routing.choices[0].message.tool_calls picked = namespaces[0] if tc: args = tc[0].function.arguments try: import json picked = json.loads(args).get("candidates", [namespaces[0]])[0] except Exception: pass # 2) Retrieval — faites l’appel à votre base vecteur ici : passages = my_vector_db_search(namespace=picked, query=question, top_k=6) tool_msg = { "role": "tool", "tool_call_id": tc[0].id if tc else "retrieve_passages#local", "name": "retrieve_passages", "content": json.dumps(passages, ensure_ascii=False) } # 3) Réponse finale final = client.chat.completions.create( model="gpt-4o-mini", temperature=0.2, messages=[ {"role":"system", "content": SYSTEM_PROMPT}, {"role":"user", "content": question}, tool_msg ] ) return final.choices[0].message.content def my_vector_db_search(namespace: str, query: str, top_k: int = 6): # TODO: retourner une liste de dicts: # [{"text":..., "title":..., "page":"p.X", "section":"Y.Z", "url":"https://..."}] return []

Format attendu des extraits (à respecter dans votre backend)

[ { "text": "Passage pertinent…", "title": "Titre du document", "page": "p.12", "section": "2.1", "url": "https://lien-optionnel" } ]

Astuces d’intégration WhatsApp

  • Coupez les réponses > 900–1 100 caractères et ajoutez “(suite)” avec un lien interne/S3 vers la version longue.
  • Ajoutez deux Quick Replies : “Plus d’exemples” / “Sources”.
  • Journalisez (uuid étudiant, horodatage, namespace, top-k, 👍/👎).