# PNS AI Engine (pns_ai_mcp)

**IA dentro de tu Odoo, para consultores.** El Motor de IA corre en el Odoo de tu cliente. Proveedores, agentes, conocimiento y aprobaciones son registros de Odoo, y los agentes viven en módulos normales de Odoo. Código abierto, nada que licenciar. Odoo **14.0** y **19.0**.

## El dolor

**IA para tus clientes como un módulo más. Código abierto, sin licencias.**
No un servicio al lado que conectas cliente a cliente. Una IA que se instala,
se actualiza y se desinstala como cualquier otro módulo, en el Odoo que el
cliente ya tiene. Aquí los agentes viven en módulos normales de Odoo.

| | |
| :--- | :--- |
| **01 — ¿Atado a un proveedor?** | Pon varios, cada uno para lo suyo: conversación, audio, imagen, vídeo. Si uno cae, responde el siguiente. |
| **02 — ¿Y si la IA escribe en producción?** | Su código corre en una caja de arena de solo lectura. Para cambiar algo propone un plan, y lo aprueba una persona. |
| **03 — ¿Un prompt cerrado que no conoce a tu cliente?** | Los contextos se editan y se amplían en el propio Odoo: su vocabulario, sus reglas, sus costumbres. |

## Qué te llevas

**Agentes que son Odoo**
- Un agente es un registro que trae un módulo. Instalas el módulo y el agente está; lo desinstalas y se va.
- Cada agente tiene su propia lista de proveedores, en el orden que tú pongas.
- Todo está en la base de datos del cliente: no hay una segunda copia en otro sitio.

**Proveedores para cada tarea**
- OpenAI y cualquier servicio compatible con OpenAI, Anthropic, OpenRouter. También un modelo local (Ollama, LM Studio) en el propio servidor del cliente.
- Roles por proveedor: conversación, audio, imagen, vídeo, maquetar una página, pasar una frase a los campos de un skill, decisiones de sí o no.
- Si un proveedor falla, responde el siguiente de la lista.
- La clave se queda en la base de datos del cliente, nunca en el módulo.

**Una caja de arena y una persona**
- El código que escribe la IA lee en una transacción de solo lectura, con los permisos de quien preguntó.
- Un cambio en Odoo es un plan que espera a que una persona lo apruebe en Odoo.
- Cada llamada queda registrada: cada usuario ve su actividad, el administrador la de todos.
- Direcciones de fuera, solo las de una lista blanca que tú mantienes.

**Conocimiento tuyo**
- Contextos: las palabras, reglas y costumbres del cliente, editadas en Odoo.
- Skills: procedimientos que se escriben una vez y se lanzan por su nombre. Sus tablas y gráficos salen del código, no de la IA.
- Exportas toda la configuración de IA de un cliente a un fichero y la importas en el siguiente.

**Un servidor MCP en el Odoo del cliente**
- Cursor y otros clientes MCP se conectan a `/mcp` con una clave personal.
- Ninguna contraseña de administrador sale de Odoo. Cada clave lee con los permisos de su usuario.
- Piensa el modelo del propio cliente MCP; Odoo responde a sus llamadas. Para eso no hace falta proveedor.

**Nada que licenciar**
- Apache 2.0. Sin cuota por usuario, por base de datos ni por año.
- Un módulo de Odoo normal: sin servidor externo que mantener.
- El mismo módulo en todos los clientes de una versión soportada.

## Agentes, roles y cadenas

**Qué es un agente.** Cualquier cosa de Odoo que use IA: un chat, un asistente de voz, un punto MCP, un trabajo que corre por detrás. En el motor es un registro (*Motor de IA → Agentes*) con un código, sus contextos, sus skills y su lista de proveedores.

Lo natural es que un agente venga con el módulo de Odoo que lo necesita: instalas el módulo y el agente está; lo desinstalas y se va. Un módulo puede traer más de uno. Este motor trae dos:

- **MCP Server** (`pns_ai_mcp`) da contextos y herramientas a un cliente de fuera, como Cursor. El modelo corre en ese cliente; Odoo contesta a sus llamadas.
- **AI Admin** (`pns_ai_admin`) hace los trabajos del propio motor: escribir un skill, leer la documentación de una API.

[Chatboo](https://www.patanegra.com/es/chatboo.html) trae el agente del chat. También se puede crear un agente a mano en Odoo.

**Dos tipos.** *Inferencia*: Odoo llama al modelo, con los proveedores del agente (un chat, un asistente de voz). *Endpoint*: el modelo está fuera y Odoo solo le sirve (MCP).

**Roles.** Un rol dice para qué sirve un proveedor. Un proveedor puede tener varios.

| Rol | Para qué |
| :--- | :--- |
| Conversación | El turno de texto normal. Un proveedor sin rol hace lo mismo. |
| Audio | Un audio de voz a texto. |
| Visión | Una foto o una página escaneada. |
| Vídeo | Un vídeo. |
| Decisión | Un sí o un no cerrado, con su confianza. Si nadie lo tiene, es no. |
| Diseño | Maqueta una página con los datos de un turno. Si nadie lo tiene, contesta Conversación. |
| Parser | Una frase corta a campos, como los argumentos de un skill. Basta un modelo pequeño. Si nadie lo tiene, contesta Conversación. |

**La cadena.** Cada agente tiene sus proveedores, en el orden que pongas. Cuando un turno necesita un rol, el motor toma los proveedores de ese agente que lo tienen, en ese orden. Si uno falla o se queda colgado, contesta el siguiente.

- Cada enlace tiene sus tiempos de espera: segundos sin datos y segundos para una ronda entera.
- Un proveedor que solo tiene ese rol va antes que los que hacen de todo: se eligió para eso.
- Si la cadena no tiene a nadie para un rol que no sea Conversación, el motor busca entre todos los proveedores activos.
- **Sin failover externo** corta la cadena en ese proveedor. Sirve para un modelo local cuyos datos no deben salir.
- Una clave que llegó a su tope de gasto del mes se salta.
- Un agente sin cadena usa todos los proveedores activos.

Por ejemplo: un modelo grande para la conversación, uno pequeño solo con Parser, y un modelo local al final de la cadena como respaldo.

## Qué trae el módulo

Leído de los propios ficheros del módulo, versión 3.1.931. Las descripciones van en inglés, como en el módulo.

### Contextos

| Código | Tipo | Descripción |
| :--- | :--- | :--- |
| `system_prompt` | core | Odoo Autonomous Agent - Strict Tool Execution Protocol |
| `account` | domain | Odoo Accounting module knowledge - invoices, refunds, payments |
| `business_documents` | domain | Read, create and modify business documents (quotations, orders, invoices, pickings) |
| `cost_accounting` | domain | Analytic accounting: correct ORM fields and group-by patterns |
| `dates` | domain | ORM date/period composition: calendar Date/Datetime vs identifier text |
| `email` | domain | Email in Odoo — mail.mail (mailbox) vs mail.notification (delivery) |
| `fetch_url_recipe` | domain | How to fetch external data: use the fetch_url safe operation, never import urllib/requests in relaxaicode |
| `filters` | domain | ir.filters knowledge - creating and managing filters |
| `geography` | domain | Geographic knowledge - country/province/city model, administrative levels (region is NOT a state), resolution funnel, and HONEST handling of missing geo data (report, never invent) |
| `hr_contracts` | domain | [RECOMMENDED] Employment contracts: employee master first, hr.contract only if installed, never contract.contract |
| `hr_payroll` | domain | [RECOMMENDED] Payroll: accounting first, payslip fallback, honest empty state |
| `hr_payroll_accounting_present` | domain | Payroll accounting: partner totals → hr.employee match + structured rows (all locales) |
| `hr_payroll_de_DE` | domain | Payroll Germany - account 4100 SKR first, payslip fallback |
| `hr_payroll_en_US` | domain | Payroll US - accounts 5100-5300 first, payslip fallback |
| `hr_payroll_es_AR` | domain | Payroll Argentina - account 411 first, payslip fallback |
| `hr_payroll_es_ES` | domain | Payroll Spain - accounting 640/4751/476 first, payslip fallback [RECOMMENDED] |
| `hr_payroll_es_MX` | domain | Payroll Mexico - account 5100 first, payslip fallback |
| `hr_payroll_fr_FR` | domain | Payroll France - account 421 first, payslip fallback |
| `hr_payroll_it_IT` | domain | Payroll Italy - account 2120 first, payslip fallback |
| `hr_payroll_pt_BR` | domain | Payroll Brazil - accounts 311/3.1.1 first, payslip fallback |
| `invoice_concepts` | domain | Invoice field mappings, calculations and coding conventions |
| `partners` | domain | res.partner knowledge - customers, suppliers, location search |
| `sale_order_duplicate_workflow` | domain | How to duplicate a sale order and change its lines in two separate steps. Includes how to get the URL of the new order. |
| `self_mcp` | domain | MCP endpoint identity — knowledge layer for an external LLM |
| `trial_balance` | domain | Trial balance - accounts with debits, credits and balance by date range (generic fallback) |
| `trial_balance_es_ES` | domain | Trial balance - accounts with debits, credits and balance by date range (Spain) |
| `corporate_terms` | locale | English corporate terminology mapping - Odoo field and model translations (factory fallback) |
| `corporate_terms_es_ES` | locale | Spanish corporate terminology mapping - Odoo field and model translations |

### Proveedores de ejemplo

En `static/examples/providers/`, sin clave.

| Nombre | Protocolo | Modelo | Roles |
| :--- | :--- | :--- | :--- |
| Anthropic | `anthropic` | `claude-opus-4-8` |  |
| Lemonade | `openai` | `gpt-oss-20b-NPU` |  |
| OpenAI | `openai` | `gpt-4-turbo` |  |
| OpenRouter - Conversation | `openrouter` | `anthropic/claude-opus-5.5` | `conversation`, `vision`, `design` |
| OpenRouter - Vision | `openrouter` | `~anthropic/claude-sonnet-latest` | `vision` |
| OpenRouter - Design | `openrouter` | `~anthropic/claude-opus-latest` | `design` |
| OpenRouter - Audio | `openrouter` | `google/gemini-3.5-transcribe` | `audio` |
| OpenRouter - Parser | `openrouter` | `anthropic/claude-haiku-5.5` | `parser` |
| OpenRouter - Decision | `systemone` | `typesafe/jev-1.13` | `decision` |
| OVH - Backup | `openai` | `Qwen3.8-27B` | `conversation`, `vision` |


## Qué no hace

- **Sin pantalla de chat.** El motor no trae ventana de conversación para los usuarios. Eso es [Chatboo](https://www.patanegra.com/es/chatboo.html).
- **No escribe por su cuenta.** La IA nunca cambia Odoo sola. Cada cambio es un plan que aprueba una persona en Odoo.
- **Cuatro protocolos de proveedor.** Compatible con OpenAI, OpenRouter, Anthropic y SystemOne (decisiones de sí o no). Una API que no hable ninguno no se conecta.
- **Dos versiones probadas.** 14.0 y 19.0. De la 15.0 a la 18.0 construyes tú el módulo y lo pruebas antes; la 13.0 y anteriores no funcionan (ver *Versiones de Odoo*).

## Preguntas

**¿Es un servidor MCP?** Lo incluye, en el mismo módulo. `POST /mcp` habla Streamable HTTP. `/mcp/sse` con `/mcp/message` es para clientes que solo hablan SSE. `/mcp/<agente>` ata la llamada a un agente. Tres herramientas: `relaxaicode` (Python de solo lectura contra Odoo), `safe_plan_request` (proponer un cambio) y `safe_plan_status` (ver si se aprobó).

**¿Adónde van los datos del cliente?** A los proveedores que configures, y a las direcciones de fuera y servidores de API que permitas. Cuando se conecta Cursor u otro cliente MCP, su propio modelo ve lo que devuelven las herramientas.

**¿Necesito Chatboo?** No. El motor solo ya sirve a clientes MCP. Chatboo añade el chat dentro de Odoo y trae el motor con él.

**¿Qué es `pns_base`?** La capa común de los módulos PNS: las mismas pantallas cargan en Odoo 14 y 19, y trae utilidades pequeñas (avisos, importar y exportar, PDF). Se instala como dependencia.

## Puesta en marcha

1. **Instala.** ¿Quieres también el chat? Instala [Chatboo](https://www.patanegra.com/es/chatboo.html#puesta-en-marcha); trae este módulo. Solo el motor: clona la rama `14.0` o `19.0` de `pns_base` y de `pns_ai_mcp`, añade cada clon al `addons_path` e instala **PNS AI Engine** desde Aplicaciones. En Odoo 14, antes, `pip install "openpyxl>=3.1.2"`.
2. **Hazte administrador de IA.** *Ajustes → Usuarios*: date **IA Administrador**. Sal y vuelve a entrar.
3. **Saca tu clave API.** *Motor de IA → Seguridad → Usuarios*: tu fila, marca **IA Conversacional** y pulsa **Generar Clave API**.
4. **Pon un proveedor.** *Motor de IA → Conexiones → Proveedores*: protocolo, dirección, clave API y modelo, con el rol **Conversación**.
   O parte de uno hecho: en `static/examples/providers/` están OpenRouter, OVH, OpenAI, Anthropic y Lemonade, sin clave. *Ajustes → Motor de IA → Importar configuración*, eliges el fichero y escribes tu clave en cada proveedor.

**Listo.** Apunta Cursor (o cualquier cliente MCP) a `https://<tu-odoo>/mcp` con la clave en la cabecera `X-MCP-API-Key`. Ejemplo más abajo, en *Conectar un cliente MCP*.

## Para el equipo de IT

### Conectar un cliente MCP

En Cursor, añade el servidor a `~/.cursor/mcp.json` (todos los proyectos) o a
`.cursor/mcp.json` (un proyecto). Los demás clientes MCP piden lo mismo: la
dirección y la cabecera.

```json
{
  "mcpServers": {
    "odoo-de-mi-cliente": {
      "url": "https://odoo.example.com/mcp",
      "headers": {
        "X-MCP-API-Key": "<tu-clave-api>"
      }
    }
  }
}
```

La clave es la del paso 3 y lee con los permisos de ese usuario. Una clave
por persona; no subas a git el fichero con la clave dentro.

### Estructura del repositorio

`main` es un solo árbol para todas las versiones. Una rama plana (`14.0`, `19.0`) es ese árbol construido para una versión, listo para el addons path.

| Ruta | Contenido |
| :--- | :--- |
| `common/` | Código común a todas las versiones, skills, contextos, datos, ficha de Apps |
| `stacks/owl1/pns_ai_mcp/` | Odoo 14 |
| `stacks/owl2/pns_ai_mcp/` | Odoo 15 y siguientes |

El instalador copia `common/` y encima la pila; si un fichero está en los dos,
gana el de la pila. Lo común se edita solo en `common/`. El módulo plano de
una versión se construye con `./install_flat.sh 19.0 /ruta/a/addons`.
Utilidades de interfaz del dominio: `utils/mcp_ui.py`.

Python: no añadas `requests` ni `reportlab` (ya vienen con Odoo). `pns_base`
no necesita nada de pip. En la 14, usa un `openpyxl` reciente (los antiguos
fallan con NumPy moderno).

### Versiones de Odoo

| Versión | Estado |
| :--- | :--- |
| 14.0, 19.0 | Probadas. Rama plana publicada. |
| 15.0 – 18.0 | Sin probar, ni por personas ni por el CI. Sin rama publicada. |
| 13.0 y anteriores | No soportadas. |

**De la 15.0 a la 18.0.** No hay rama que clonar. Construye tú el módulo
plano desde `main`, que tiene el código de todas las versiones (`owl2` para
la 15.0 y siguientes). Lo mismo con `pns_base`:

```bash
git clone https://github.com/patanegra-soft/pns_base.git
git clone https://github.com/patanegra-soft/pns_ai_mcp.git
cd pns_base   && ./install_flat.sh 17.0 /ruta/a/addons && cd ..
cd pns_ai_mcp && ./install_flat.sh 17.0 /ruta/a/addons
```

El script avisa de que esa versión no está probada. Nadie ha usado el motor
en estas versiones: pruébalo antes en una base de pruebas y pasa ahí los
tests de Odoo (ver *Tests*). Si funciona, o si no, cuéntanoslo
([CONTRIBUTING.md](https://github.com/patanegra-soft/pns_ai_mcp/blob/main/CONTRIBUTING.md)).

Odoo 13 no tiene OWL, sobre el que está hecho el chat. De Odoo 8 a 12 usan un
Python anterior al 3.7, que es el que necesita el motor. La suite mantiene dos
pilas (`owl1` para la 14.0 y `owl2` para la 15.0 y siguientes).

### Tests

| Dónde estás | Qué se ejecuta |
| :--- | :--- |
| `main` | `python -m pytest unit_tests`, sin Odoo. Después los tests de Odoo, tras `./install_flat.sh 19.0 <dir>`. |
| Una rama plana (`14.0`, `19.0`) | Los tests de Odoo en `tests/`. Los unitarios viven en `main`. |

```bash
odoo-bin -d <db> -i pns_ai_mcp --test-enable --stop-after-init --test-tags /pns_ai_mcp
```

## Licencia, marcas, entrenamiento

Apache-2.0: ver [LICENSE](https://github.com/patanegra-soft/pns_ai_mcp/blob/main/LICENSE) y [NOTICE](https://github.com/patanegra-soft/pns_ai_mcp/blob/main/NOTICE).
PATANEGRA Soft™ y PAAP™ (Patanegra Application Agent Protocol, que implementa
este motor) son marcas de PATANEGRA Soft SL; la licencia no las concede. Se
permite la minería de textos y el entrenamiento de IA (TDMRep `tdm-reservation = 0`).

Seguridad: [SECURITY.md](https://github.com/patanegra-soft/pns_ai_mcp/blob/main/SECURITY.md). Errores, ideas y preguntas: [CONTRIBUTING.md](https://github.com/patanegra-soft/pns_ai_mcp/blob/main/CONTRIBUTING.md).
