Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions ai_tool_server_action/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
=====================
AI Tool Server Action
=====================

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:3607e5a7335eecf5d7886965f487a459b2517a5970d330fca4a9f6e24e294290
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fai-lightgray.png?logo=github
:target: https://github.com/OCA/ai/tree/18.0/ai_tool_server_action
:alt: OCA/ai
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/ai-18-0/ai-18-0-ai_tool_server_action
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/ai&target_branch=18.0
:alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|

This module allows creating and configuring AI tools dynamically via
Odoo Server Actions. Instead of hardcoding tool definitions and logic in
Python, power users and administrators can define LLM-accessible tools
directly from the user interface.

**Table of contents**

.. contents::
:local:

Configuration
=============

To configure an AI Tool backed by a Server Action:

1. Go to *Settings > Technical > Actions > Server Actions*.
2. Create a new Server Action or select an existing one.
3. Open the *AI Tool Configuration* tab.
4. Click on the **Create AI Tool** (or **Configure AI Tool** if it
already exists) button to generate or modify the AI tool for this
action.
5. Provide the *Tool Name*, *Tool Description*, *Input Schema (JSON)*
and *Output Schema (JSON)* in the wizard to strongly type the inputs
and outputs of your tool for the LLM.

Usage
=====

1. For Server Actions containing **Python code**, you have access to the
arguments sent by the AI in ``env.context.get('tool_args')``. To
return a result, simply assign a dictionary to the local ``action``
variable:

.. code:: python

args = env.context.get('tool_args', {})
search_term = args.get('query')
records = env['res.partner'].search([('name', 'ilike', search_term)], limit=5)
action = {'results': records.mapped('display_name')}

2. **Standard Server Actions** (e.g. Update Record, Create Activity,
Send Email) work out-of-the-box. When they successfully execute, the
AI will automatically receive a ``{"status": "success"}`` response.
Any User Interface actions (like opening a wizard or window) are
silently intercepted to prevent confusing the AI.

3. **External / MCP Usage:** When the AI invokes these tools externally
(via the MCP protocol or general chat) it won't have an active Odoo
record context.

- For Standard Actions to work in this scenario, simply declare an
``active_id`` parameter in your **Input Schema** and the module
will intelligently inject it into the execution context for you.
- The wizard pre-loads these intelligent default schemas
automatically for you based on the action type.

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/OCA/ai/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/ai/issues/new?body=module:%20ai_tool_server_action%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

Do not contact contributors directly about support or help with technical issues.

Credits
=======

Authors
-------

* SDi

Maintainers
-----------

This module is maintained by the OCA.

.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org

OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.

.. |maintainer-amoya@sdi.es| image:: https://github.com/amoya@sdi.es.png?size=40px
:target: https://github.com/amoya@sdi.es
:alt: amoya@sdi.es

Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:

|maintainer-amoya@sdi.es|

This module is part of the `OCA/ai <https://github.com/OCA/ai/tree/18.0/ai_tool_server_action>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
5 changes: 5 additions & 0 deletions ai_tool_server_action/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Copyright 2026 SDi
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from . import models
from . import wizard
23 changes: 23 additions & 0 deletions ai_tool_server_action/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Copyright 2026 SDi
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

{
"name": "AI Tool Server Action",
"summary": "Allow configuring AI Tools using Server Actions",
"version": "18.0.1.0.0",
"category": "Hidden",
"author": "SDi, Odoo Community Association (OCA)",
"maintainers": ["amoya@sdi.es"],
"website": "https://github.com/OCA/ai",
"license": "AGPL-3",
"depends": [
"ai_tool",
],
"data": [
"security/ir.model.access.csv",
"wizard/ai_tool_server_action_wizard_views.xml",
"views/ai_tool_views.xml",
"views/ir_actions_server_views.xml",
],
"installable": True,
}
5 changes: 5 additions & 0 deletions ai_tool_server_action/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Copyright 2026 SDi
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from . import ai_tool
from . import ir_actions_server
92 changes: 92 additions & 0 deletions ai_tool_server_action/models/ai_tool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Copyright 2026 SDi
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

import json

from odoo import fields, models


class AiTool(models.Model):
_inherit = "ai.tool"

kind = fields.Selection(
selection_add=[("server_action", "Server Action")],
ondelete={"server_action": "cascade"},
)
model_id = fields.Many2one(required=False)
function_name = fields.Char(required=False)

server_action_id = fields.Many2one(
"ir.actions.server",
string="Server Action",
help="The server action to execute when this tool is called.",
)
input_schema_json = fields.Text(
string="Input Schema (JSON)",
help="Define the expected input schema for the LLM in JSON format.",
)
output_schema_json = fields.Text(
string="Output Schema (JSON)",
help="Define the expected output schema for the LLM in JSON format.",
)

def _get_tool_definition(self):
if self.kind == "server_action":
input_schema = {}
if self.input_schema_json:
try:
input_schema = json.loads(self.input_schema_json)
except ValueError:
input_schema = {}
output_schema = {}
if self.output_schema_json:
try:
output_schema = json.loads(self.output_schema_json)
except ValueError:
output_schema = {}
return {
"name": self.name,
"description": self.description,
"inputSchema": input_schema,
"outputSchema": output_schema,
}
return super()._get_tool_definition()

def _execute_tool(self, *args, record=None, **kwargs):
if self.kind == "server_action":
# Extract active_id and active_model from record, or fallback
# to kwargs (useful for MCP integration)
active_id = record.id if record else kwargs.get("active_id", False)
active_model = (
record._name
if record
else kwargs.get("active_model", self.server_action_id.model_id.model)
)
active_ids = [active_id] if active_id else kwargs.get("active_ids", [])

action = self.server_action_id.with_context(
tool_args=kwargs,
active_id=active_id,
active_ids=active_ids,
active_model=active_model,
)
result = action.run()
if isinstance(result, dict):
# If the action returns a UI action (like opening a window),
# we ignore it and return a success message since
# the AI cannot interact with the UI.
if result.get("type") and str(result.get("type")).startswith(
"ir.actions."
):
return {
"status": "success",
"message": "Action executed successfully (UI action ignored)",
}
return result

# For actions that return False/None (like Update Record, Send Email)
return {
"status": "success",
"message": "Action executed successfully",
}
return super()._execute_tool(*args, record=record, **kwargs)
76 changes: 76 additions & 0 deletions ai_tool_server_action/models/ir_actions_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Copyright 2026 SDi
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from odoo import api, fields, models


class IrActionsServer(models.Model):
_inherit = "ir.actions.server"

ai_tool_ids = fields.One2many(
"ai.tool",
"server_action_id",
string="AI Tools",
)
ai_tool_id = fields.Many2one(
"ai.tool",
string="AI Tool",
compute="_compute_ai_tool_id",
)
ai_tool_description = fields.Text(
compute="_compute_ai_tool_fields",
inverse="_inverse_ai_tool_description",
string="AI Tool Description",
)
input_schema_json = fields.Text(
compute="_compute_ai_tool_fields",
inverse="_inverse_input_schema_json",
string="AI Tool Input Schema",
)
output_schema_json = fields.Text(
compute="_compute_ai_tool_fields",
inverse="_inverse_output_schema_json",
string="AI Tool Output Schema",
)

@api.depends("ai_tool_ids")
def _compute_ai_tool_id(self):
for rec in self:
rec.ai_tool_id = rec.ai_tool_ids[:1]

@api.depends(
"ai_tool_ids.input_schema_json",
"ai_tool_ids.output_schema_json",
"ai_tool_ids.description",
)
def _compute_ai_tool_fields(self):
for rec in self:
rec.ai_tool_description = rec.ai_tool_id.description
rec.input_schema_json = rec.ai_tool_id.input_schema_json
rec.output_schema_json = rec.ai_tool_id.output_schema_json

def _inverse_ai_tool_description(self):
for rec in self:
if rec.ai_tool_id:
rec.ai_tool_id.sudo().description = rec.ai_tool_description

def _inverse_input_schema_json(self):
for rec in self:
if rec.ai_tool_id:
rec.ai_tool_id.sudo().input_schema_json = rec.input_schema_json

def _inverse_output_schema_json(self):
for rec in self:
if rec.ai_tool_id:
rec.ai_tool_id.sudo().output_schema_json = rec.output_schema_json

def action_open_ai_tool_wizard(self):
self.ensure_one()
return {
"name": "Create AI Tool",
"type": "ir.actions.act_window",
"res_model": "ai.tool.server.action.wizard",
"view_mode": "form",
"target": "new",
"context": {"default_server_action_id": self.id},
}
3 changes: 3 additions & 0 deletions ai_tool_server_action/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"
7 changes: 7 additions & 0 deletions ai_tool_server_action/readme/CONFIGURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
To configure an AI Tool backed by a Server Action:

1. Go to *Settings > Technical > Actions > Server Actions*.
2. Create a new Server Action or select an existing one.
3. Open the *AI Tool Configuration* tab.
4. Click on the **Create AI Tool** (or **Configure AI Tool** if it already exists) button to generate or modify the AI tool for this action.
5. Provide the *Tool Name*, *Tool Description*, *Input Schema (JSON)* and *Output Schema (JSON)* in the wizard to strongly type the inputs and outputs of your tool for the LLM.
4 changes: 4 additions & 0 deletions ai_tool_server_action/readme/DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
This module allows creating and configuring AI tools dynamically via
Odoo Server Actions. Instead of hardcoding tool definitions and logic in
Python, power users and administrators can define LLM-accessible tools
directly from the user interface.
14 changes: 14 additions & 0 deletions ai_tool_server_action/readme/USAGE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
1. For Server Actions containing **Python code**, you have access to the arguments sent by the AI in `env.context.get('tool_args')`. To return a result, simply assign a dictionary to the local `action` variable:

```python
args = env.context.get('tool_args', {})
search_term = args.get('query')
records = env['res.partner'].search([('name', 'ilike', search_term)], limit=5)
action = {'results': records.mapped('display_name')}
```

2. **Standard Server Actions** (e.g. Update Record, Create Activity, Send Email) work out-of-the-box. When they successfully execute, the AI will automatically receive a `{"status": "success"}` response. Any User Interface actions (like opening a wizard or window) are silently intercepted to prevent confusing the AI.

3. **External / MCP Usage:** When the AI invokes these tools externally (via the MCP protocol or general chat) it won't have an active Odoo record context.
- For Standard Actions to work in this scenario, simply declare an `active_id` parameter in your **Input Schema** and the module will intelligently inject it into the execution context for you.
- The wizard pre-loads these intelligent default schemas automatically for you based on the action type.
2 changes: 2 additions & 0 deletions ai_tool_server_action/security/ir.model.access.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_ai_tool_server_action_wizard,ai.tool.server.action.wizard,model_ai_tool_server_action_wizard,base.group_user,1,1,1,1
Loading
Loading