Agentes

API de las bases de conocimiento

Buscar en una base, pedir una respuesta redactada y devolver su resultado desde un módulo de Odoo o cualquier otro programa.

11 min de lecturaActualizado el 13 de noviembre de 2026

La API da a un programa de terceros lo que hace la página Bases de conocimiento: buscar pasajes, pedir una respuesta redactada y después decir qué ha sido de ella. No depende de tu conexión con Odoo: cualquier programa capaz de hacer una llamada HTTPS puede usarla.

Autenticación

Crea una clave en la pestaña Claves API de una base. Empieza por aid_kb_ y solo se muestra una vez.

  • Por defecto, una clave está limitada a la base en la que se creó y solo puede buscar.
  • Marca Permitir la redacción de respuestas para darle el derecho a pedir respuestas.
  • Marca Dar acceso a todas las bases del espacio solo si el programa lo necesita.

Envía la clave en la cabecera Authorization de cada llamada:

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

Una clave se queda en el servidor

No pongas nunca una clave aid_kb_ en una página web ni en una aplicación móvil. En Odoo, guárdala en un parámetro del sistema que solo puedan leer los administradores.

Listar las bases

GET /v1/knowledge/bases devuelve las bases que la clave puede leer, con sus etiquetas.

{
  "bases": [
    {
      "id": "6734c0f2a1b2c3d4e5f60718",
      "name": "Soporte",
      "description": "",
      "use_case": "support",
      "labels": [
        { "key": "product", "name": "Producto", "values": ["Contabilidad", "Nóminas"], "required": false }
      ],
      "entry_count": 412
    }
  ]
}

Buscar

POST /v1/knowledge/search

CampoTipoFunción
basetexto, obligatorioidentificador de la base
querytexto, obligatoriola pregunta (2 000 caracteres como máximo)
labelsobjetofiltro, por ejemplo {"product": ["Nóminas"]}
limitenteronúmero de pasajes (20 como máximo), el de la base por defecto
curl https://api.aidoo.ai/v1/knowledge/search \
  -H "Authorization: Bearer aid_kb_tu_clave" \
  -H "Content-Type: application/json" \
  -d '{"base": "6734c0f2a1b2c3d4e5f60718", "query": "error ERR-1045 en el cierre"}'
{
  "results": [
    {
      "entry_id": "6734c1aa...",
      "title": "Cierre del ejercicio",
      "url": "https://aide.exemple.com/cloture",
      "section": "Cierre del ejercicio > Errores frecuentes",
      "excerpt": "El mensaje ERR-1045 aparece cuando un diario todavía contiene asientos en borrador.",
      "score": 0.71,
      "exact_match": true,
      "labels": { "product": ["Contabilidad"] },
      "stale": false
    }
  ]
}

score es la proximidad de significado (de 0 a 1). exact_match vale true cuando el pasaje contiene las palabras exactas de la pregunta. stale señala una entrada cuya fecha de revalidación ha pasado. Una lista vacía significa que la base no tiene nada lo bastante cercano.

La búsqueda es gratuita.

Pedir una respuesta

POST /v1/knowledge/answers (clave con el derecho de redacción)

CampoTipoFunción
basetexto, obligatorioidentificador de la base
bodytexto, obligatoriola solicitud del cliente (12 000 caracteres como máximo)
subjecttextoel asunto del ticket o del mensaje
labelsobjetofiltro de búsqueda
external_reftextotu referencia, por ejemplo ticket-482:message-9051
clarifybooleanofalse para prohibir las preguntas de aclaración en esta llamada (verdadero por defecto)
curl https://api.aidoo.ai/v1/knowledge/answers \
  -H "Authorization: Bearer aid_kb_tu_clave" \
  -H "Content-Type: application/json" \
  -d '{
    "base": "6734c0f2a1b2c3d4e5f60718",
    "subject": "Cierre imposible",
    "body": "Hola, el cierre falla con el mensaje ERR-1045. ¿Qué hago?",
    "external_ref": "ticket-482:message-9051"
  }'
{
  "answer": {
    "id": "6734d2bb...",
    "base": "6734c0f2a1b2c3d4e5f60718",
    "action": "propose",
    "reason": "review_required",
    "body": "El mensaje ERR-1045 indica que un diario todavía contiene asientos en borrador. Valida esos asientos y vuelve a lanzar el cierre.",
    "body_html": "<p>El mensaje ERR-1045 indica que un diario todavía contiene asientos en borrador. Valida esos asientos y vuelve a lanzar el cierre.</p>",
    "confidence": 1,
    "checks": { "citations": true, "quotes": true, "links": true, "numbers": true, "grounded": 1 },
    "missing_info": [],
    "questions": [],
    "sources": [
      { "index": 1, "entry_id": "6734c1aa...", "title": "Cierre del ejercicio", "url": "https://aide.exemple.com/cloture", "section": "Errores frecuentes", "stale": false }
    ],
    "credits": 3,
    "outcome": null,
    "created_at": "2026-11-13T10:12:44.000Z"
  }
}

El campo action

Es el que tu programa debe seguir.

actionLo que hace tu programa
proposemuestra body a una persona, que revisa, corrige y envía
sendpuede enviar body sin revisión
clarifybody hace al cliente las preguntas listadas en questions: la solicitud es demasiado vaga para responderla
acknowledgebody es un mensaje de acuse de recibo: la base no contiene la respuesta
nonenada que enviar; sources puede listar las entradas más cercanas

send solo se devuelve si el envío automático está activado en los ajustes de la base y se superan todos los controles. Mientras no lo hayas activado, nunca recibirás send.

El campo reason dice por qué: review_required, auto_send, checks_failed, missing_info, stale_source, below_threshold, no_passage, abstained, not_grounded, unclear_request.

body está en Markdown sencillo (párrafos, listas, negrita); body_html es su versión HTML, ya escapada. Los enlaces a los artículos no están en el texto: están en sources, que muestras como quieras.

Las preguntas de aclaración

Cuando la solicitud no dice con suficiente claridad qué se espera («no funciona», sin software, pantalla ni mensaje de error), la base no adivina: action vale clarify, questions contiene de una a tres preguntas concretas y body las presenta en un mensaje listo para revisar, en el idioma del cliente. No se devuelve ninguna fuente ni se afirma ningún hecho.

Una solicitud clara que la base no sabe responder nunca da clarify: sigue el ajuste «Cuando la base no contiene la respuesta».

Cuando el cliente responde, vuelve a llamar a la API con:

  • en body, la solicitud original seguida de su respuesta;
  • una nueva external_ref (la del nuevo mensaje);
  • "clarify": false, para que la base responda o ceda el paso en lugar de hacer más preguntas.

Una respuesta redactada también puede terminar con una pregunta, cuando un detalle cambiaría el procedimiento: action vale entonces propose y questions contiene esa pregunta. Las preguntas se desactivan para toda la base en sus ajustes («Cuando la solicitud no es lo bastante clara»).

Una misma referencia, una sola respuesta

Con external_ref, una segunda llamada para la misma referencia devuelve la misma respuesta, sin recalcular ni volver a facturar nada. Da una referencia que cambie con cada nuevo mensaje del cliente (identificador del ticket y del mensaje), no solo el identificador del ticket.

Duración y créditos

La llamada es síncrona y dura de unos segundos a un minuto. Hazla desde una tarea en segundo plano, nunca mientras se guarda un registro. Consume créditos del espacio (campo credits), salvo cuando no se encuentra nada. Cuando los créditos se han agotado, la respuesta es 402.

Volver a leer una respuesta

GET /v1/knowledge/answers/{id} devuelve la misma forma que más arriba.

Devolver el resultado

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

Dile a Aidoo qué ha sido de la respuesta. Es lo que alimenta los indicadores de la base, y lo que permitirá ajustar el umbral de envío automático a partir de hechos.

CampoTipoFunción
outcometexto, obligatoriosent_unchanged, sent_edited, rejected, auto_sent o reopened
final_bodytextoel texto realmente enviado
reasontextomotivo de un rechazo (200 caracteres como máximo)
curl https://api.aidoo.ai/v1/knowledge/answers/6734d2bb.../outcome \
  -H "Authorization: Bearer aid_kb_tu_clave" \
  -H "Content-Type: application/json" \
  -d '{"outcome": "sent_edited", "final_body": "Hola, valida tus asientos en borrador y vuelve a lanzar el cierre."}'

Si envías final_body, Aidoo compara los dos textos y corrige por sí mismo entre sent_unchanged y sent_edited. El texto enviado sirve para esta medida y después se descarta: ni se conserva ni se añade a la base.

reopened se devuelve cuando el cliente reabre el ticket después de una respuesta: es la mejor señal de que una respuesta era errónea.

Leer los indicadores

GET /v1/knowledge/stats?base={id}&days=30 (clave con permiso de redacción)

Devuelve los indicadores de la pestaña Indicadores sobre 7, 30 o 90 días: número de solicitudes, decisiones tomadas (by_action), resultados devueltos (by_outcome), parte enviada sin retoques (unchanged_rate), parte sin respuesta en la base (no_answer_rate), créditos consumidos y las entradas más citadas.

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

unchanged_rate solo cubre las respuestas cuyo resultado has devuelto: sin llamada a outcome, se queda en cero.

Errores y límites

CódigoSignificado
400campo ausente o no válido (KNOWLEDGE_INVALID_INPUT, KNOWLEDGE_INVALID_LABELS)
401clave ausente, desconocida o revocada
402créditos del espacio agotados (KNOWLEDGE_CREDITS_EXHAUSTED)
403la clave no tiene este derecho, o no sobre esta base (KNOWLEDGE_SCOPE_REQUIRED)
404base o respuesta no encontrada
409ya hay una respuesta en curso para esta external_ref: vuelve a intentarlo un poco más tarde (KNOWLEDGE_ANSWER_IN_PROGRESS)
429demasiadas llamadas: espera el plazo de la cabecera Retry-After
503servicio momentáneamente no disponible, vuelve a intentarlo

Límites por clave: 60 llamadas por minuto, de las cuales 20 solicitudes de respuesta.

Conectar un módulo de Odoo

Este es el camino más corto para proponer una respuesta en un ticket, ya sea con el Helpdesk de Odoo Enterprise, el módulo helpdesk_mgmt de la OCA o cualquier modelo que lleve un hilo de conversación.

1. Guardar los ajustes

Dos parámetros del sistema (ir.config_parameter), que solo pueden leer los administradores:

  • aidoo_knowledge.api_key: la clave aid_kb_;
  • aidoo_knowledge.base_id: el identificador de la base.

2. Añadir los campos al 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", "Para revisar"), ("send", "Envío automático"),
         ("clarify", "Preguntas al cliente"), ("acknowledge", "Acuse de recibo"),
         ("none", "Nada que enviar")],
        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 recién creado tiene aidoo_answer_pending a verdadero: será la tarea planificada la que lo trate. Para proponer también una respuesta con cada nuevo mensaje del cliente, vuelve a poner este campo a verdadero en message_post cuando el autor sea el cliente.

3. Llamar a la API desde una tarea planificada

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 próxima pasada lo reintentará
            if response.status_code == 402:
                break  # créditos agotados: no sirve de nada insistir
            if response.status_code in (409, 429, 503):
                continue  # ya en curso, o demasiadas llamadas: la próxima pasada lo reintentará
            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()

Planifica _cron_aidoo_answers cada minuto o cada cinco minutos. La llamada dura varios segundos: nunca debe salir de create ni de write.

Filtrar por etiqueta

Si tu base tiene una etiqueta product, deduce su valor del equipo o del tipo del ticket y pasa "labels": {"product": ["Nóminas"]}. La búsqueda solo mira entonces las entradas de ese producto, lo que mejora claramente las respuestas de un soporte de varios productos.

4. Mostrar la respuesta en la ficha

Muestra aidoo_answer_body en un recuadro del formulario del ticket, con la lista de las sources y dos botones:

  • Enviar: publica el texto (corregido si hace falta) en el hilo de conversación con message_post y después devuelve el resultado;
  • Ignorar: devuelve el resultado 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  # el resultado es un indicador, no debe bloquear el envío

Envía siempre sent_unchanged con final_body: Aidoo lo recalifica como sent_edited si el texto se ha retocado.

5. El envío automático, más adelante

Cuando action vale send, tu módulo puede publicar la respuesta sin esperar y después devolver el resultado auto_sent. Actívalo en los ajustes de la base solo después de haber observado, durante varias semanas, la proporción de respuestas enviadas sin retoques.

El mensaje de acuse de recibo

Cuando action vale acknowledge, body no aporta ninguna respuesta: es un acuse de recibo. Decide en tu módulo si sale solo o si espera a una persona, y no lo presentes nunca como una respuesta al problema.