Agents

API des bases de connaissance

Chercher dans une base, demander une réponse rédigée et renvoyer son issue depuis un module Odoo ou tout autre logiciel.

11 min de lectureMis à jour le 13 novembre 2026

L'API donne à un logiciel tiers ce que fait la page Bases de connaissance : chercher des passages, demander une réponse rédigée, puis dire ce qu'il en est advenu. Elle ne dépend pas de votre connexion Odoo : n'importe quel logiciel capable d'un appel HTTPS peut s'en servir.

Authentification

Créez une clé dans l'onglet Clés d'API d'une base. Elle commence par aid_kb_ et n'est affichée qu'une fois.

  • Par défaut, une clé est limitée à la base où elle a été créée et ne peut que chercher.
  • Cochez Autoriser la rédaction de réponses pour lui donner le droit de demander des réponses.
  • Cochez Donner accès à toutes les bases de l'espace seulement si le logiciel en a besoin.

Envoyez la clé dans l'en-tête Authorization de chaque appel :

curl https://api.aidoo.ai/v1/knowledge/bases \
  -H "Authorization: Bearer aid_kb_votre_cle"

Une clé reste côté serveur

Ne placez jamais une clé aid_kb_ dans une page web ou une application mobile. Dans Odoo, rangez-la dans un paramètre système lisible par les seuls administrateurs.

Lister les bases

GET /v1/knowledge/bases rend les bases que la clé peut lire, avec leurs étiquettes.

{
  "bases": [
    {
      "id": "6734c0f2a1b2c3d4e5f60718",
      "name": "Support",
      "description": "",
      "use_case": "support",
      "labels": [
        { "key": "product", "name": "Produit", "values": ["Compta", "Paie"], "required": false }
      ],
      "entry_count": 412
    }
  ]
}

Chercher

POST /v1/knowledge/search

ChampTypeRôle
basetexte, obligatoireidentifiant de la base
querytexte, obligatoirela question (2 000 caractères au plus)
labelsobjetfiltre, par exemple {"product": ["Paie"]}
limitentiernombre de passages (20 au plus), celui de la base par défaut
curl https://api.aidoo.ai/v1/knowledge/search \
  -H "Authorization: Bearer aid_kb_votre_cle" \
  -H "Content-Type: application/json" \
  -d '{"base": "6734c0f2a1b2c3d4e5f60718", "query": "erreur ERR-1045 à la clôture"}'
{
  "results": [
    {
      "entry_id": "6734c1aa...",
      "title": "Clôture de l'exercice",
      "url": "https://aide.exemple.com/cloture",
      "section": "Clôture de l'exercice > Erreurs fréquentes",
      "excerpt": "Le message ERR-1045 apparaît quand un journal contient encore des écritures en brouillard.",
      "score": 0.71,
      "exact_match": true,
      "labels": { "product": ["Compta"] },
      "stale": false
    }
  ]
}

score est la proximité de sens (0 à 1). exact_match vaut true quand le passage contient les mots exacts de la question. stale signale une entrée dont la date de revalidation est dépassée. Une liste vide signifie que la base n'a rien d'assez proche.

La recherche est gratuite.

Demander une réponse

POST /v1/knowledge/answers (clé avec le droit de rédaction)

ChampTypeRôle
basetexte, obligatoireidentifiant de la base
bodytexte, obligatoirela demande du client (12 000 caractères au plus)
subjecttextel'objet du ticket ou du message
labelsobjetfiltre de recherche
external_reftextevotre référence, par exemple ticket-482:message-9051
clarifybooléenfalse pour interdire les questions de précision sur cet appel (vrai par défaut)
curl https://api.aidoo.ai/v1/knowledge/answers \
  -H "Authorization: Bearer aid_kb_votre_cle" \
  -H "Content-Type: application/json" \
  -d '{
    "base": "6734c0f2a1b2c3d4e5f60718",
    "subject": "Clôture impossible",
    "body": "Bonjour, la clôture échoue avec le message ERR-1045. Que faire ?",
    "external_ref": "ticket-482:message-9051"
  }'
{
  "answer": {
    "id": "6734d2bb...",
    "base": "6734c0f2a1b2c3d4e5f60718",
    "action": "propose",
    "reason": "review_required",
    "body": "Le message ERR-1045 signale qu'un journal contient encore des écritures en brouillard. Validez ces écritures, puis relancez la clôture.",
    "body_html": "<p>Le message ERR-1045 signale qu'un journal contient encore des écritures en brouillard. Validez ces écritures, puis relancez la clôture.</p>",
    "confidence": 1,
    "checks": { "citations": true, "quotes": true, "links": true, "numbers": true, "grounded": 1 },
    "missing_info": [],
    "questions": [],
    "sources": [
      { "index": 1, "entry_id": "6734c1aa...", "title": "Clôture de l'exercice", "url": "https://aide.exemple.com/cloture", "section": "Erreurs fréquentes", "stale": false }
    ],
    "credits": 3,
    "outcome": null,
    "created_at": "2026-11-13T10:12:44.000Z"
  }
}

Le champ action

C'est lui que votre logiciel doit suivre.

actionCe que fait votre logiciel
proposeaffiche body à une personne, qui relit, corrige et envoie
sendpeut envoyer body sans relecture
clarifybody pose au client les questions listées dans questions : la demande est trop vague pour y répondre
acknowledgebody est un message de prise en charge : la base ne contient pas la réponse
nonerien à envoyer ; sources peut lister les entrées les plus proches

send n'est rendu que si l'envoi automatique est activé dans les réglages de la base et que tous les contrôles passent. Tant que vous ne l'avez pas activé, vous ne recevrez jamais send.

Le champ reason dit pourquoi : review_required, auto_send, checks_failed, missing_info, stale_source, below_threshold, no_passage, abstained, not_grounded, unclear_request.

body est en Markdown simple (paragraphes, listes, gras) ; body_html en est la version HTML, déjà échappée. Les liens vers les articles ne sont pas dans le texte : ils sont dans sources, que vous affichez comme vous le souhaitez.

Les questions de précision

Quand la demande ne dit pas assez clairement ce qui est attendu (« ça ne marche pas », sans logiciel, écran ni message d'erreur), la base ne devine pas : action vaut clarify, questions porte une à trois questions précises et body les présente dans un message prêt à relire, dans la langue du client. Aucune source n'est rendue, aucun fait n'est avancé.

Une demande claire à laquelle la base ne sait pas répondre ne donne jamais clarify : elle suit le réglage « Quand la base ne contient pas la réponse ».

Quand le client répond, rappelez l'API avec :

  • dans body, la demande d'origine suivie de sa réponse ;
  • une nouvelle external_ref (celle du nouveau message) ;
  • "clarify": false, pour que la base réponde ou passe la main au lieu de poser de nouvelles questions.

Une réponse rédigée peut aussi se terminer par une question, quand un détail changerait la marche à suivre : action vaut alors propose et questions porte cette question. Les questions se désactivent pour toute la base dans ses réglages (« Quand la demande n'est pas assez claire »).

Une même référence, une seule réponse

Avec external_ref, un second appel pour la même référence rend la même réponse, sans rien recalculer ni refacturer. Donnez une référence qui change à chaque nouveau message du client (identifiant du ticket et du message), pas seulement l'identifiant du ticket.

Durée et crédits

L'appel est synchrone et dure de quelques secondes à une minute. Appelez-le depuis une tâche de fond, jamais pendant l'enregistrement d'une fiche. Il consomme des crédits de l'espace (champ credits), sauf quand rien n'est trouvé. Quand les crédits sont épuisés, la réponse est 402.

Relire une réponse

GET /v1/knowledge/answers/{id} rend la même forme que ci-dessus.

Renvoyer l'issue

POST /v1/knowledge/answers/{id}/outcome

Dites à Aidoo ce qu'il est advenu de la réponse. C'est ce qui alimente les indicateurs de la base, et ce qui permettra de régler le seuil d'envoi automatique sur des faits.

ChampTypeRôle
outcometexte, obligatoiresent_unchanged, sent_edited, rejected, auto_sent ou reopened
final_bodytextele texte réellement envoyé
reasontextemotif d'un rejet (200 caractères au plus)
curl https://api.aidoo.ai/v1/knowledge/answers/6734d2bb.../outcome \
  -H "Authorization: Bearer aid_kb_votre_cle" \
  -H "Content-Type: application/json" \
  -d '{"outcome": "sent_edited", "final_body": "Bonjour, validez vos écritures en brouillard puis relancez la clôture."}'

Si vous envoyez final_body, Aidoo compare les deux textes et corrige lui-même entre sent_unchanged et sent_edited. Le texte envoyé sert à cette mesure puis il est jeté : il n'est ni conservé, ni versé dans la base.

reopened se renvoie quand le client rouvre le ticket après une réponse : c'est le meilleur signal qu'une réponse était fausse.

Lire les indicateurs

GET /v1/knowledge/stats?base={id}&days=30 (clé avec le droit de rédaction)

Rend les indicateurs de l'onglet Indicateurs sur 7, 30 ou 90 jours : nombre de demandes, décisions prises (by_action), issues renvoyées (by_outcome), part envoyée sans retouche (unchanged_rate), part sans réponse dans la base (no_answer_rate), crédits consommés, et les entrées les plus citées.

curl "https://api.aidoo.ai/v1/knowledge/stats?base=6734c0f2a1b2c3d4e5f60718&days=30" \
  -H "Authorization: Bearer aid_kb_votre_cle"

unchanged_rate ne porte que sur les réponses dont vous avez renvoyé l'issue : sans appel à outcome, il reste à zéro.

Erreurs et limites

CodeSens
400champ manquant ou invalide (KNOWLEDGE_INVALID_INPUT, KNOWLEDGE_INVALID_LABELS)
401clé absente, inconnue ou révoquée
402crédits de l'espace épuisés (KNOWLEDGE_CREDITS_EXHAUSTED)
403la clé n'a pas ce droit, ou pas sur cette base (KNOWLEDGE_SCOPE_REQUIRED)
404base ou réponse introuvable
409une réponse est déjà en cours pour cette external_ref : réessayez un peu plus tard (KNOWLEDGE_ANSWER_IN_PROGRESS)
429trop d'appels : attendez le délai de l'en-tête Retry-After
503service momentanément indisponible, réessayez

Limites par clé : 60 appels par minute, dont 20 demandes de réponse.

Brancher un module Odoo

Voici le chemin le plus court pour proposer une réponse sur un ticket, que ce soit avec le Helpdesk d'Odoo Enterprise, le module helpdesk_mgmt de l'OCA ou tout modèle qui porte un fil de discussion.

1. Ranger les réglages

Deux paramètres système (ir.config_parameter), lisibles par les administrateurs seulement :

  • aidoo_knowledge.api_key : la clé aid_kb_ ;
  • aidoo_knowledge.base_id : l'identifiant de la base.

2. Ajouter les champs au ticket

from odoo import fields, models


class HelpdeskTicket(models.Model):
    _inherit = "helpdesk.ticket"

    aidoo_answer_id = fields.Char(copy=False)
    aidoo_answer_action = fields.Selection(
        [("propose", "À relire"), ("send", "Envoi automatique"),
         ("clarify", "Questions au client"), ("acknowledge", "Prise en charge"),
         ("none", "Rien à envoyer")],
        copy=False,
    )
    aidoo_answer_body = fields.Html(copy=False, sanitize=True)
    aidoo_answer_sources = fields.Json(copy=False)
    aidoo_answer_pending = fields.Boolean(default=True, copy=False, index=True)

Un ticket créé a aidoo_answer_pending à vrai : c'est la tâche planifiée qui le traitera. Pour proposer aussi une réponse à chaque nouveau message du client, remettez ce champ à vrai dans message_post quand l'auteur est le client.

3. Appeler l'API depuis une tâche planifiée

import requests

from odoo import api, models

API = "https://api.aidoo.ai/v1/knowledge"


class HelpdeskTicket(models.Model):
    _inherit = "helpdesk.ticket"

    @api.model
    def _cron_aidoo_answers(self, limit=20):
        params = self.env["ir.config_parameter"].sudo()
        key = params.get_param("aidoo_knowledge.api_key")
        base = params.get_param("aidoo_knowledge.base_id")
        if not key or not base:
            return
        for ticket in self.search([("aidoo_answer_pending", "=", True)], limit=limit):
            last = ticket.message_ids.filtered(lambda m: m.message_type == "email")[:1]
            payload = {
                "base": base,
                "subject": ticket.name or "",
                "body": ticket.description or "",
                "external_ref": f"ticket-{ticket.id}:message-{last.id or 0}",
            }
            try:
                response = requests.post(
                    f"{API}/answers",
                    json=payload,
                    headers={"Authorization": f"Bearer {key}"},
                    timeout=90,
                )
            except requests.RequestException:
                continue  # la prochaine passe réessaiera
            if response.status_code == 402:
                break  # crédits épuisés : inutile d'insister
            if response.status_code in (409, 429, 503):
                continue  # déjà en cours, ou trop d'appels : la prochaine passe réessaiera
            if response.status_code != 201:
                ticket.aidoo_answer_pending = False
                continue
            answer = response.json()["answer"]
            ticket.write({
                "aidoo_answer_pending": False,
                "aidoo_answer_id": answer["id"],
                "aidoo_answer_action": answer["action"],
                "aidoo_answer_body": answer["body_html"],
                "aidoo_answer_sources": answer["sources"],
            })
            self.env.cr.commit()

Planifiez _cron_aidoo_answers toutes les minutes ou toutes les cinq minutes. L'appel dure plusieurs secondes : il ne doit jamais partir de create ou de write.

Filtrer par étiquette

Si votre base a une étiquette product, déduisez sa valeur de l'équipe ou du type du ticket et passez "labels": {"product": ["Paie"]}. La recherche ne regarde alors que les entrées de ce produit, ce qui améliore nettement les réponses d'un support multi-produits.

4. Montrer la réponse sur la fiche

Affichez aidoo_answer_body dans un encart du formulaire du ticket, avec la liste des sources et deux boutons :

  • Envoyer : poste le texte (éventuellement corrigé) dans le fil de discussion avec message_post, puis renvoie l'issue ;
  • Ignorer : renvoie l'issue rejected.
    def action_aidoo_send(self):
        self.ensure_one()
        self.message_post(
            body=self.aidoo_answer_body,
            message_type="comment",
            subtype_xmlid="mail.mt_comment",
            partner_ids=self.partner_id.ids,
        )
        self._aidoo_outcome("sent_unchanged", final_body=self.aidoo_answer_body)

    def _aidoo_outcome(self, outcome, final_body=None, reason=None):
        params = self.env["ir.config_parameter"].sudo()
        key = params.get_param("aidoo_knowledge.api_key")
        if not key or not self.aidoo_answer_id:
            return
        payload = {"outcome": outcome}
        if final_body:
            payload["final_body"] = final_body
        if reason:
            payload["reason"] = reason
        try:
            requests.post(
                f"{API}/answers/{self.aidoo_answer_id}/outcome",
                json=payload,
                headers={"Authorization": f"Bearer {key}"},
                timeout=20,
            )
        except requests.RequestException:
            pass  # l'issue est un indicateur, elle ne doit pas bloquer l'envoi

Envoyez toujours sent_unchanged avec final_body : Aidoo le requalifie en sent_edited si le texte a été retouché.

5. L'envoi automatique, plus tard

Quand action vaut send, votre module peut poster la réponse sans attendre, puis renvoyer l'issue auto_sent. Ne l'activez dans les réglages de la base qu'après avoir observé, pendant plusieurs semaines, la part des réponses envoyées sans retouche.

Le message de prise en charge

Quand action vaut acknowledge, body n'apporte aucune réponse : c'est un accusé de réception. Décidez dans votre module s'il part tout seul ou s'il attend une personne, et ne le présentez jamais comme une réponse au problème.