From 98326b4b3586a842e49d4a1ca87b3523981a34a9 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Mon, 20 Apr 2026 09:17:16 +0200 Subject: [PATCH 1/9] [ADD] ai_tool --- ai_tool/README.rst | 145 ++++++++ ai_tool/__init__.py | 2 + ai_tool/__manifest__.py | 22 ++ ai_tool/data/ai_tools.xml | 22 ++ ai_tool/models/__init__.py | 1 + ai_tool/models/ai_tool.py | 94 +++++ ai_tool/readme/CONTRIBUTORS.md | 2 + ai_tool/readme/DESCRIPTION.md | 3 + ai_tool/readme/USAGE.md | 52 +++ ai_tool/security/ir.model.access.csv | 2 + ai_tool/static/description/icon.png | Bin 0 -> 9455 bytes ai_tool/static/description/index.html | 489 ++++++++++++++++++++++++++ ai_tool/tests/__init__.py | 1 + ai_tool/tests/test_ai_tool.py | 49 +++ ai_tool/tools.py | 18 + ai_tool/views/ai_tool.xml | 55 +++ ai_tool/views/menu.xml | 6 + 17 files changed, 963 insertions(+) create mode 100644 ai_tool/README.rst create mode 100644 ai_tool/__init__.py create mode 100644 ai_tool/__manifest__.py create mode 100644 ai_tool/data/ai_tools.xml create mode 100644 ai_tool/models/__init__.py create mode 100644 ai_tool/models/ai_tool.py create mode 100644 ai_tool/readme/CONTRIBUTORS.md create mode 100644 ai_tool/readme/DESCRIPTION.md create mode 100644 ai_tool/readme/USAGE.md create mode 100644 ai_tool/security/ir.model.access.csv create mode 100644 ai_tool/static/description/icon.png create mode 100644 ai_tool/static/description/index.html create mode 100644 ai_tool/tests/__init__.py create mode 100644 ai_tool/tests/test_ai_tool.py create mode 100644 ai_tool/tools.py create mode 100644 ai_tool/views/ai_tool.xml create mode 100644 ai_tool/views/menu.xml diff --git a/ai_tool/README.rst b/ai_tool/README.rst new file mode 100644 index 00000000..5e06e332 --- /dev/null +++ b/ai_tool/README.rst @@ -0,0 +1,145 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +======= +Ai Tool +======= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:24aba4610958ac085492da4b1fd0fe517144c8824ae25d8d3f6c44fe45c1388e + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/license-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/16.0/ai_tool + :alt: OCA/ai +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/ai-16-0/ai-16-0-ai_tool + :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=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This technical module provides the base infrastructure for defining AI +tools in Odoo. It allows other modules to register callable functions as +AI tools, which can then be used by MCP servers, automation flows, or +any AI-native integration. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +This module is technical, however, it adds some specific functions that +might be used in glue modules. + +For example, if we want to add on sales a functionality to find the +sales on a period, we should do: + +.. code:: xml + + + + total_sale_order + Calculate the total amount of sale orders within a date range and optionally for a specific customer. + + _mcp_total_sale_order + + + +.. code:: python + + from odoo.addons.ai_tool import aitool + + class SaleOrder(models.Model): + _inherit = "sale.order" + + @aitool( + input_schema={ + "start_date": {"type": "date"}, + "end_date": {"type": "date"}, + "customer_id": {"type": "integer"}, + }, + required_inputs=["start_date", "end_date"], + output_schema={ + "amount_total": {"type": "number"}, + }, + ) + def _mcp_total_sale_order(self, start_date, end_date, customer_id=None): + domain = [("date_order", ">=", start_date), ("date_order", "<=", end_date)] + if customer_id: + domain.append(("partner_id", "=", customer_id)) + orders = self.read_group(domain, ["amount_total"], []) + return { + "amount_total": (orders[0]["amount_total"] or 0) if orders else 0, + } + +Be aware that this kind of elements must allways return a dict. All the +elements will be defined in output_schema. + +Also, for the signature of the functions, all fields must be in the +inputs with the exception of record. This argument is protected and is +used to define integrations with automation. This argument is required +in ``generic_model`` and ``record`` tools. + +On ``generic_model``\ s we are expecting this value because we want to +do a specific action with the model. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub 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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Dixmit + +Contributors +------------ + +- `Dixmit `__ + + - Enric Tobella + +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. + +This module is part of the `OCA/ai `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/ai_tool/__init__.py b/ai_tool/__init__.py new file mode 100644 index 00000000..738a2eec --- /dev/null +++ b/ai_tool/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import tools diff --git a/ai_tool/__manifest__.py b/ai_tool/__manifest__.py new file mode 100644 index 00000000..e69e5037 --- /dev/null +++ b/ai_tool/__manifest__.py @@ -0,0 +1,22 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Ai Tool", + "summary": """We want to generate some specific AI Tools + that might be used in other places, like MCP or native.""", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "author": "Dixmit,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/ai", + "depends": [ + "mail", + ], + "data": [ + "security/ir.model.access.csv", + "views/menu.xml", + "views/ai_tool.xml", + "data/ai_tools.xml", + ], + "demo": [], +} diff --git a/ai_tool/data/ai_tools.xml b/ai_tool/data/ai_tools.xml new file mode 100644 index 00000000..d21afe69 --- /dev/null +++ b/ai_tool/data/ai_tools.xml @@ -0,0 +1,22 @@ + + + + + + get_date + Get the current date. + + _ai_get_date + generic + + + + post_message + Post a message to a record. + + _ai_post_message + generic_model + + + diff --git a/ai_tool/models/__init__.py b/ai_tool/models/__init__.py new file mode 100644 index 00000000..b6c10aa8 --- /dev/null +++ b/ai_tool/models/__init__.py @@ -0,0 +1 @@ +from . import ai_tool diff --git a/ai_tool/models/ai_tool.py b/ai_tool/models/ai_tool.py new file mode 100644 index 00000000..c01eee29 --- /dev/null +++ b/ai_tool/models/ai_tool.py @@ -0,0 +1,94 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from datetime import date + +from odoo import fields, models +from odoo.tools.mail import html_sanitize, plaintext2html + +from ..tools import aitool + +try: + import markdown +except ImportError: + markdown = None + + +class AiTool(models.Model): + + _name = "ai.tool" + _description = "AI Tool" + + name = fields.Char(required=True) + description = fields.Text() + model_id = fields.Many2one( + "ir.model", readonly=True, required=True, ondelete="cascade" + ) + function_name = fields.Char(readonly=True, required=True) + kind = fields.Selection( + [ + ("generic", "Generic"), + ("generic_model", "Generic but requires a record to work"), + ("record", "Record"), + ], + readonly=True, + required=True, + default="record", + ) + + def _get_tool_definition(self): + func = getattr(self.env[self.model_id.model], self.function_name) + return { + "name": self.name, + "description": self.description, + "inputSchema": func._ai_tool["input_schema"], + "outputSchema": func._ai_tool["output_schema"], + } + + @aitool( + input_schema={}, + output_schema={ + "date": {"type": "date"}, + }, + ) + def _ai_get_date(self): + return {"date": date.today().isoformat()} + + @aitool( + input_schema={ + "message": {"type": "string"}, + }, + required_inputs=["message"], + output_schema={}, + ) + def _ai_post_message(self, message=None, record=None, **kwargs): + if not record or not record.exists(): + raise ValueError("Record must be provided and exist to post a message") + record.message_post(body=self._ai_post_message_parse_body(message)) + return {} + + def _ai_post_message_parse_body(self, message): + """ + Using markdown library if available to convert markdown to html, + otherwise using plaintext2html as fallback + """ + if markdown: + return html_sanitize(markdown.markdown(message)) + return plaintext2html(message) + + def _execute_tool(self, *args, record=None, **kwargs): + if self.kind == "generic": + return getattr(self.env[self.model_id.model], self.function_name)( + *args, **kwargs + ) + if not record: + raise ValueError("Record must be provided for non-generic tools") + if self.kind == "generic_model": + return getattr(self.env[self.model_id.model], self.function_name)( + *args, record=record, **kwargs + ) + elif record._name != self.model_id.model: + raise ValueError( + f"Record model {record._name} does not match tool model {self.model_id.model}" + ) + return getattr(record, self.function_name)(*args, **kwargs) or {} diff --git a/ai_tool/readme/CONTRIBUTORS.md b/ai_tool/readme/CONTRIBUTORS.md new file mode 100644 index 00000000..2c066ba7 --- /dev/null +++ b/ai_tool/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- [Dixmit](https://www.dixmit.com) + - Enric Tobella diff --git a/ai_tool/readme/DESCRIPTION.md b/ai_tool/readme/DESCRIPTION.md new file mode 100644 index 00000000..71747f87 --- /dev/null +++ b/ai_tool/readme/DESCRIPTION.md @@ -0,0 +1,3 @@ +This technical module provides the base infrastructure for defining AI tools in Odoo. +It allows other modules to register callable functions as AI tools, which can then be +used by MCP servers, automation flows, or any AI-native integration. diff --git a/ai_tool/readme/USAGE.md b/ai_tool/readme/USAGE.md new file mode 100644 index 00000000..c18e4ae5 --- /dev/null +++ b/ai_tool/readme/USAGE.md @@ -0,0 +1,52 @@ +This module is technical, however, it adds some specific functions that might be used in glue modules. + +For example, if we want to add on sales a functionality to find the sales on a period, we should do: + +```xml + + + total_sale_order + Calculate the total amount of sale orders within a date range and optionally for a specific customer. + + _mcp_total_sale_order + + +``` + +```python +from odoo.addons.ai_tool import aitool + +class SaleOrder(models.Model): + _inherit = "sale.order" + + @aitool( + input_schema={ + "start_date": {"type": "date"}, + "end_date": {"type": "date"}, + "customer_id": {"type": "integer"}, + }, + required_inputs=["start_date", "end_date"], + output_schema={ + "amount_total": {"type": "number"}, + }, + ) + def _mcp_total_sale_order(self, start_date, end_date, customer_id=None): + domain = [("date_order", ">=", start_date), ("date_order", "<=", end_date)] + if customer_id: + domain.append(("partner_id", "=", customer_id)) + orders = self.read_group(domain, ["amount_total"], []) + return { + "amount_total": (orders[0]["amount_total"] or 0) if orders else 0, + } + +``` + +Be aware that this kind of elements must allways return a dict. All the elements will be defined in output_schema. + +Also, for the signature of the functions, all fields must be in the inputs with the exception of record. +This argument is protected and is used to define integrations with automation. +This argument is required in `generic_model` and `record` tools. + +On `generic_model`s we are expecting this value because we want to do a specific action with the model. diff --git a/ai_tool/security/ir.model.access.csv b/ai_tool/security/ir.model.access.csv new file mode 100644 index 00000000..4f6beed7 --- /dev/null +++ b/ai_tool/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_ai_tool,access_ai_tool,model_ai_tool,base.group_user,1,0,0,0 diff --git a/ai_tool/static/description/icon.png b/ai_tool/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 diff --git a/ai_tool/static/description/index.html b/ai_tool/static/description/index.html new file mode 100644 index 00000000..def5e728 --- /dev/null +++ b/ai_tool/static/description/index.html @@ -0,0 +1,489 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Ai Tool

+ +

Beta License: AGPL-3 OCA/ai Translate me on Weblate Try me on Runboat

+

This technical module provides the base infrastructure for defining AI +tools in Odoo. It allows other modules to register callable functions as +AI tools, which can then be used by MCP servers, automation flows, or +any AI-native integration.

+

Table of contents

+ +
+

Usage

+

This module is technical, however, it adds some specific functions that +might be used in glue modules.

+

For example, if we want to add on sales a functionality to find the +sales on a period, we should do:

+
+<odoo>
+    <record model="ai.tool" id="total_sale_order_tool">
+        <field name="name">total_sale_order</field>
+        <field
+            name="description"
+        >Calculate the total amount of sale orders within a date range and optionally for a specific customer.</field>
+        <field name="model_id" ref="model_sale_order" />
+        <field name="function_name">_mcp_total_sale_order</field>
+    </record>
+</odoo>
+
+
+from odoo.addons.ai_tool import aitool
+
+class SaleOrder(models.Model):
+    _inherit = "sale.order"
+
+    @aitool(
+        input_schema={
+            "start_date": {"type": "date"},
+            "end_date": {"type": "date"},
+            "customer_id": {"type": "integer"},
+        },
+        required_inputs=["start_date", "end_date"],
+        output_schema={
+            "amount_total": {"type": "number"},
+        },
+    )
+    def _mcp_total_sale_order(self, start_date, end_date, customer_id=None):
+        domain = [("date_order", ">=", start_date), ("date_order", "<=", end_date)]
+        if customer_id:
+            domain.append(("partner_id", "=", customer_id))
+        orders = self.read_group(domain, ["amount_total"], [])
+        return {
+            "amount_total": (orders[0]["amount_total"] or 0) if orders else 0,
+        }
+
+

Be aware that this kind of elements must allways return a dict. All the +elements will be defined in output_schema.

+

Also, for the signature of the functions, all fields must be in the +inputs with the exception of record. This argument is protected and is +used to define integrations with automation. This argument is required +in generic_model and record tools.

+

On generic_models we are expecting this value because we want to +do a specific action with the model.

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub 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.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Dixmit
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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.

+

This module is part of the OCA/ai project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+ + diff --git a/ai_tool/tests/__init__.py b/ai_tool/tests/__init__.py new file mode 100644 index 00000000..30e42873 --- /dev/null +++ b/ai_tool/tests/__init__.py @@ -0,0 +1 @@ +from . import test_ai_tool diff --git a/ai_tool/tests/test_ai_tool.py b/ai_tool/tests/test_ai_tool.py new file mode 100644 index 00000000..c9090aaf --- /dev/null +++ b/ai_tool/tests/test_ai_tool.py @@ -0,0 +1,49 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from freezegun import freeze_time + +from odoo.tests.common import TransactionCase + + +class TestAiTool(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.partner = cls.env["res.partner"].create({"name": "Test Partner"}) + + def test_tool(self): + definition = self.env.ref("ai_tool.current_date")._get_tool_definition() + self.assertEqual(definition["inputSchema"]["type"], "object") + self.assertEqual(definition["outputSchema"]["type"], "object") + + def test_get_date_tool(self): + with freeze_time("2024-01-01"): + tool = self.env.ref("ai_tool.current_date") + result = tool._execute_tool(record=tool) + self.assertEqual(result["date"], "2024-01-01") + + def test_post_message_tool(self): + partner = self.partner + messages = partner.message_ids + tool = self.env.ref("ai_tool.post_message") + tool._execute_tool(message="Hello World", record=partner) + self.assertEqual(len(partner.message_ids), len(messages) + 1) + self.assertRegex((partner.message_ids - messages).body, "Hello World") + + def test_tool_no_record(self): + tool = self.env.ref("ai_tool.post_message") + with self.assertRaises(ValueError): + tool._execute_tool(message="Hello World") + + def test_post_message_tool_no_record(self): + tool = self.env.ref("ai_tool.post_message") + tool.kind = "generic" + with self.assertRaises(ValueError): + tool._execute_tool(message="Hello World", record=self.partner) + + def test_post_message_tool_record_different_model(self): + tool = self.env.ref("ai_tool.post_message") + tool.kind = "record" + with self.assertRaises(ValueError): + tool._execute_tool(message="Hello World", record=self.partner) diff --git a/ai_tool/tools.py b/ai_tool/tools.py new file mode 100644 index 00000000..443a1b56 --- /dev/null +++ b/ai_tool/tools.py @@ -0,0 +1,18 @@ +from odoo.api import attrsetter + + +def aitool(input_schema: dict, output_schema: dict, required_inputs: list = None): + return attrsetter( + "_ai_tool", + { + "input_schema": { + "type": "object", + "properties": input_schema, + "required": required_inputs or [], + }, + "output_schema": { + "type": "object", + "properties": output_schema, + }, + }, + ) diff --git a/ai_tool/views/ai_tool.xml b/ai_tool/views/ai_tool.xml new file mode 100644 index 00000000..7ce1a38b --- /dev/null +++ b/ai_tool/views/ai_tool.xml @@ -0,0 +1,55 @@ + + + + + + ai.tool + +
+
+ + + + + + + + + + + + + ai.tool + + + + + + + + + ai.tool + + + + + + + + + AI Tool + ai.tool + tree,form + [] + {} + + + + AI Tool + + + + + + diff --git a/ai_tool/views/menu.xml b/ai_tool/views/menu.xml new file mode 100644 index 00000000..f897888c --- /dev/null +++ b/ai_tool/views/menu.xml @@ -0,0 +1,6 @@ + + + + + From fb844b86db0dfa921395534574c037992076b8e5 Mon Sep 17 00:00:00 2001 From: oca-ci Date: Thu, 14 May 2026 08:46:27 +0000 Subject: [PATCH 2/9] [UPD] Update ai_tool.pot --- ai_tool/i18n/ai_tool.pot | 101 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 ai_tool/i18n/ai_tool.pot diff --git a/ai_tool/i18n/ai_tool.pot b/ai_tool/i18n/ai_tool.pot new file mode 100644 index 00000000..b2ece4bf --- /dev/null +++ b/ai_tool/i18n/ai_tool.pot @@ -0,0 +1,101 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * ai_tool +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: ai_tool +#: model:ir.ui.menu,name:ai_tool.ai_menu_root +msgid "AI" +msgstr "" + +#. module: ai_tool +#: model:ir.actions.act_window,name:ai_tool.ai_tool_act_window +#: model:ir.model,name:ai_tool.model_ai_tool +#: model:ir.ui.menu,name:ai_tool.ai_tool_menu +msgid "AI Tool" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__create_uid +msgid "Created by" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__create_date +msgid "Created on" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__description +msgid "Description" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__display_name +msgid "Display Name" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__function_name +msgid "Function Name" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields.selection,name:ai_tool.selection__ai_tool__kind__generic +msgid "Generic" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields.selection,name:ai_tool.selection__ai_tool__kind__generic_model +msgid "Generic but requires a record to work" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__id +msgid "ID" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__kind +msgid "Kind" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool____last_update +msgid "Last Modified on" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__write_date +msgid "Last Updated on" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__model_id +msgid "Model" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__name +msgid "Name" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields.selection,name:ai_tool.selection__ai_tool__kind__record +msgid "Record" +msgstr "" From 783f44bf2cd87925007cc1682028c8228d493ae6 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Thu, 14 May 2026 08:49:14 +0000 Subject: [PATCH 3/9] [BOT] post-merge updates --- ai_tool/README.rst | 2 +- ai_tool/static/description/index.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ai_tool/README.rst b/ai_tool/README.rst index 5e06e332..1652182b 100644 --- a/ai_tool/README.rst +++ b/ai_tool/README.rst @@ -11,7 +11,7 @@ Ai Tool !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:24aba4610958ac085492da4b1fd0fe517144c8824ae25d8d3f6c44fe45c1388e + !! source digest: sha256:b0dec31c213cecc194ff875e340eb11844acb3ee0e5147864575a90b02f288bf !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/ai_tool/static/description/index.html b/ai_tool/static/description/index.html index def5e728..5201087a 100644 --- a/ai_tool/static/description/index.html +++ b/ai_tool/static/description/index.html @@ -372,7 +372,7 @@

Ai Tool

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:24aba4610958ac085492da4b1fd0fe517144c8824ae25d8d3f6c44fe45c1388e +!! source digest: sha256:b0dec31c213cecc194ff875e340eb11844acb3ee0e5147864575a90b02f288bf !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Beta License: AGPL-3 OCA/ai Translate me on Weblate Try me on Runboat

This technical module provides the base infrastructure for defining AI From ef8111dca94101baa81161a065f2a8b5f1538702 Mon Sep 17 00:00:00 2001 From: mymage Date: Thu, 14 May 2026 13:15:55 +0000 Subject: [PATCH 4/9] Added translation using Weblate (Italian) --- ai_tool/i18n/it.po | 102 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 ai_tool/i18n/it.po diff --git a/ai_tool/i18n/it.po b/ai_tool/i18n/it.po new file mode 100644 index 00000000..ffa9906d --- /dev/null +++ b/ai_tool/i18n/it.po @@ -0,0 +1,102 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * ai_tool +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#. module: ai_tool +#: model:ir.ui.menu,name:ai_tool.ai_menu_root +msgid "AI" +msgstr "" + +#. module: ai_tool +#: model:ir.actions.act_window,name:ai_tool.ai_tool_act_window +#: model:ir.model,name:ai_tool.model_ai_tool +#: model:ir.ui.menu,name:ai_tool.ai_tool_menu +msgid "AI Tool" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__create_uid +msgid "Created by" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__create_date +msgid "Created on" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__description +msgid "Description" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__display_name +msgid "Display Name" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__function_name +msgid "Function Name" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields.selection,name:ai_tool.selection__ai_tool__kind__generic +msgid "Generic" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields.selection,name:ai_tool.selection__ai_tool__kind__generic_model +msgid "Generic but requires a record to work" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__id +msgid "ID" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__kind +msgid "Kind" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool____last_update +msgid "Last Modified on" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__write_date +msgid "Last Updated on" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__model_id +msgid "Model" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__name +msgid "Name" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields.selection,name:ai_tool.selection__ai_tool__kind__record +msgid "Record" +msgstr "" From e73f0f2db5a9ae7266758f34b3083600aad36179 Mon Sep 17 00:00:00 2001 From: mymage Date: Fri, 29 May 2026 11:04:45 +0000 Subject: [PATCH 5/9] Translated using Weblate (Italian) Currently translated at 100.0% (17 of 17 strings) Translation: ai-16.0/ai-16.0-ai_tool Translate-URL: https://translation.odoo-community.org/projects/ai-16-0/ai-16-0-ai_tool/it/ --- ai_tool/i18n/it.po | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/ai_tool/i18n/it.po b/ai_tool/i18n/it.po index ffa9906d..50c1389b 100644 --- a/ai_tool/i18n/it.po +++ b/ai_tool/i18n/it.po @@ -6,97 +6,99 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" -"Last-Translator: Automatically generated\n" +"PO-Revision-Date: 2026-05-29 13:46+0000\n" +"Last-Translator: mymage \n" "Language-Team: none\n" "Language: it\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.15.2\n" #. module: ai_tool #: model:ir.ui.menu,name:ai_tool.ai_menu_root msgid "AI" -msgstr "" +msgstr "IA" #. module: ai_tool #: model:ir.actions.act_window,name:ai_tool.ai_tool_act_window #: model:ir.model,name:ai_tool.model_ai_tool #: model:ir.ui.menu,name:ai_tool.ai_tool_menu msgid "AI Tool" -msgstr "" +msgstr "Strumento IA" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__create_uid msgid "Created by" -msgstr "" +msgstr "Creato da" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__create_date msgid "Created on" -msgstr "" +msgstr "Creato il" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__description msgid "Description" -msgstr "" +msgstr "Descrizione" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__display_name msgid "Display Name" -msgstr "" +msgstr "Nome visualizzato" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__function_name msgid "Function Name" -msgstr "" +msgstr "Nome funzione" #. module: ai_tool #: model:ir.model.fields.selection,name:ai_tool.selection__ai_tool__kind__generic msgid "Generic" -msgstr "" +msgstr "Generico" #. module: ai_tool #: model:ir.model.fields.selection,name:ai_tool.selection__ai_tool__kind__generic_model msgid "Generic but requires a record to work" -msgstr "" +msgstr "Generico ma richiede un record per lavorare" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__id msgid "ID" -msgstr "" +msgstr "ID" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__kind msgid "Kind" -msgstr "" +msgstr "Genere" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool____last_update msgid "Last Modified on" -msgstr "" +msgstr "Ultima modifica il" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__write_uid msgid "Last Updated by" -msgstr "" +msgstr "Ultimo aggiornamento di" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__write_date msgid "Last Updated on" -msgstr "" +msgstr "Ultimo aggiornamento il" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__model_id msgid "Model" -msgstr "" +msgstr "Modello" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__name msgid "Name" -msgstr "" +msgstr "Nome" #. module: ai_tool #: model:ir.model.fields.selection,name:ai_tool.selection__ai_tool__kind__record msgid "Record" -msgstr "" +msgstr "Record" From a16061a9deff37af28200a928e5f8d8685ede812 Mon Sep 17 00:00:00 2001 From: felipemotter Date: Mon, 15 Jun 2026 11:09:59 +0000 Subject: [PATCH 6/9] Added translation using Weblate (Portuguese (Brazil)) --- ai_tool/i18n/pt_BR.po | 102 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 ai_tool/i18n/pt_BR.po diff --git a/ai_tool/i18n/pt_BR.po b/ai_tool/i18n/pt_BR.po new file mode 100644 index 00000000..bb86eb81 --- /dev/null +++ b/ai_tool/i18n/pt_BR.po @@ -0,0 +1,102 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * ai_tool +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" + +#. module: ai_tool +#: model:ir.ui.menu,name:ai_tool.ai_menu_root +msgid "AI" +msgstr "" + +#. module: ai_tool +#: model:ir.actions.act_window,name:ai_tool.ai_tool_act_window +#: model:ir.model,name:ai_tool.model_ai_tool +#: model:ir.ui.menu,name:ai_tool.ai_tool_menu +msgid "AI Tool" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__create_uid +msgid "Created by" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__create_date +msgid "Created on" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__description +msgid "Description" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__display_name +msgid "Display Name" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__function_name +msgid "Function Name" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields.selection,name:ai_tool.selection__ai_tool__kind__generic +msgid "Generic" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields.selection,name:ai_tool.selection__ai_tool__kind__generic_model +msgid "Generic but requires a record to work" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__id +msgid "ID" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__kind +msgid "Kind" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool____last_update +msgid "Last Modified on" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__write_date +msgid "Last Updated on" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__model_id +msgid "Model" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields,field_description:ai_tool.field_ai_tool__name +msgid "Name" +msgstr "" + +#. module: ai_tool +#: model:ir.model.fields.selection,name:ai_tool.selection__ai_tool__kind__record +msgid "Record" +msgstr "" From cce0ccef71a418105cd67130180a7eff859befb2 Mon Sep 17 00:00:00 2001 From: felipemotter Date: Mon, 15 Jun 2026 11:10:08 +0000 Subject: [PATCH 7/9] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (17 of 17 strings) Translation: ai-16.0/ai-16.0-ai_tool Translate-URL: https://translation.odoo-community.org/projects/ai-16-0/ai-16-0-ai_tool/pt_BR/ --- ai_tool/i18n/pt_BR.po | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/ai_tool/i18n/pt_BR.po b/ai_tool/i18n/pt_BR.po index bb86eb81..4ce57c2c 100644 --- a/ai_tool/i18n/pt_BR.po +++ b/ai_tool/i18n/pt_BR.po @@ -6,97 +6,99 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" -"Last-Translator: Automatically generated\n" +"PO-Revision-Date: 2026-06-15 11:11+0000\n" +"Last-Translator: felipemotter \n" "Language-Team: none\n" "Language: pt_BR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Weblate 5.15.2\n" #. module: ai_tool #: model:ir.ui.menu,name:ai_tool.ai_menu_root msgid "AI" -msgstr "" +msgstr "IA" #. module: ai_tool #: model:ir.actions.act_window,name:ai_tool.ai_tool_act_window #: model:ir.model,name:ai_tool.model_ai_tool #: model:ir.ui.menu,name:ai_tool.ai_tool_menu msgid "AI Tool" -msgstr "" +msgstr "Ferramenta de IA" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__create_uid msgid "Created by" -msgstr "" +msgstr "Criado por" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__create_date msgid "Created on" -msgstr "" +msgstr "Criado em" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__description msgid "Description" -msgstr "" +msgstr "Descrição" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__display_name msgid "Display Name" -msgstr "" +msgstr "Nome exibido" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__function_name msgid "Function Name" -msgstr "" +msgstr "Nome da Função" #. module: ai_tool #: model:ir.model.fields.selection,name:ai_tool.selection__ai_tool__kind__generic msgid "Generic" -msgstr "" +msgstr "Genérico" #. module: ai_tool #: model:ir.model.fields.selection,name:ai_tool.selection__ai_tool__kind__generic_model msgid "Generic but requires a record to work" -msgstr "" +msgstr "Genérico, mas requer um registro para funcionar" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__id msgid "ID" -msgstr "" +msgstr "ID" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__kind msgid "Kind" -msgstr "" +msgstr "Espécie" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool____last_update msgid "Last Modified on" -msgstr "" +msgstr "Última modificação em" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__write_uid msgid "Last Updated by" -msgstr "" +msgstr "Última atualização por" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__write_date msgid "Last Updated on" -msgstr "" +msgstr "Última atualização em" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__model_id msgid "Model" -msgstr "" +msgstr "Modelo" #. module: ai_tool #: model:ir.model.fields,field_description:ai_tool.field_ai_tool__name msgid "Name" -msgstr "" +msgstr "Nome" #. module: ai_tool #: model:ir.model.fields.selection,name:ai_tool.selection__ai_tool__kind__record msgid "Record" -msgstr "" +msgstr "Registro" From 5cb2d7e14f430617ba18cd53785473047a310b91 Mon Sep 17 00:00:00 2001 From: Pierre Verkest Date: Mon, 29 Jun 2026 15:12:15 +0200 Subject: [PATCH 8/9] [IMP] ai_tool: pre-commit auto fixes --- ai_tool/models/ai_tool.py | 1 - ai_tool/pyproject.toml | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 ai_tool/pyproject.toml diff --git a/ai_tool/models/ai_tool.py b/ai_tool/models/ai_tool.py index c01eee29..45c8628c 100644 --- a/ai_tool/models/ai_tool.py +++ b/ai_tool/models/ai_tool.py @@ -15,7 +15,6 @@ class AiTool(models.Model): - _name = "ai.tool" _description = "AI Tool" diff --git a/ai_tool/pyproject.toml b/ai_tool/pyproject.toml new file mode 100644 index 00000000..4231d0cc --- /dev/null +++ b/ai_tool/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" From 9d05972925004657f963827fd94675b3b1341a19 Mon Sep 17 00:00:00 2001 From: Pierre Verkest Date: Mon, 29 Jun 2026 15:52:57 +0200 Subject: [PATCH 9/9] [MIG] ai_tool: Migration to 17.0 --- ai_tool/README.rst | 18 ++++++--------- ai_tool/__manifest__.py | 2 +- ai_tool/models/ai_tool.py | 3 ++- ai_tool/readme/USAGE.md | 2 +- ai_tool/static/description/index.html | 32 +++++++++++---------------- 5 files changed, 24 insertions(+), 33 deletions(-) diff --git a/ai_tool/README.rst b/ai_tool/README.rst index 1652182b..4dd67b4c 100644 --- a/ai_tool/README.rst +++ b/ai_tool/README.rst @@ -1,7 +1,3 @@ -.. image:: https://odoo-community.org/readme-banner-image - :target: https://odoo-community.org/get-involved?utm_source=readme - :alt: Odoo Community Association - ======= Ai Tool ======= @@ -17,17 +13,17 @@ Ai Tool .. |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/license-AGPL--3-blue.png +.. |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/16.0/ai_tool + :target: https://github.com/OCA/ai/tree/17.0/ai_tool :alt: OCA/ai .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/ai-16-0/ai-16-0-ai_tool + :target: https://translation.odoo-community.org/projects/ai-17-0/ai-17-0-ai_tool :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=16.0 + :target: https://runboat.odoo-community.org/builds?repo=OCA/ai&target_branch=17.0 :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| @@ -66,7 +62,7 @@ sales on a period, we should do: .. code:: python - from odoo.addons.ai_tool import aitool + from odoo.addons.ai_tool.tools import aitool class SaleOrder(models.Model): _inherit = "sale.order" @@ -108,7 +104,7 @@ Bug Tracker Bugs are tracked on `GitHub 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 `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -140,6 +136,6 @@ 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. -This module is part of the `OCA/ai `_ project on GitHub. +This module is part of the `OCA/ai `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/ai_tool/__manifest__.py b/ai_tool/__manifest__.py index e69e5037..26a4137e 100644 --- a/ai_tool/__manifest__.py +++ b/ai_tool/__manifest__.py @@ -5,7 +5,7 @@ "name": "Ai Tool", "summary": """We want to generate some specific AI Tools that might be used in other places, like MCP or native.""", - "version": "16.0.1.0.0", + "version": "17.0.1.0.0", "license": "AGPL-3", "author": "Dixmit,Odoo Community Association (OCA)", "website": "https://github.com/OCA/ai", diff --git a/ai_tool/models/ai_tool.py b/ai_tool/models/ai_tool.py index 45c8628c..9d7fe70b 100644 --- a/ai_tool/models/ai_tool.py +++ b/ai_tool/models/ai_tool.py @@ -88,6 +88,7 @@ def _execute_tool(self, *args, record=None, **kwargs): ) elif record._name != self.model_id.model: raise ValueError( - f"Record model {record._name} does not match tool model {self.model_id.model}" + f"Record model {record._name} does not match " + f"tool model {self.model_id.model}" ) return getattr(record, self.function_name)(*args, **kwargs) or {} diff --git a/ai_tool/readme/USAGE.md b/ai_tool/readme/USAGE.md index c18e4ae5..c7f118c5 100644 --- a/ai_tool/readme/USAGE.md +++ b/ai_tool/readme/USAGE.md @@ -16,7 +16,7 @@ For example, if we want to add on sales a functionality to find the sales on a p ``` ```python -from odoo.addons.ai_tool import aitool +from odoo.addons.ai_tool.tools import aitool class SaleOrder(models.Model): _inherit = "sale.order" diff --git a/ai_tool/static/description/index.html b/ai_tool/static/description/index.html index 5201087a..733b4d43 100644 --- a/ai_tool/static/description/index.html +++ b/ai_tool/static/description/index.html @@ -3,7 +3,7 @@ -README.rst +Ai Tool -

+
+

Ai Tool

- - -Odoo Community Association - -
-

Ai Tool

-

Beta License: AGPL-3 OCA/ai Translate me on Weblate Try me on Runboat

+

Beta License: AGPL-3 OCA/ai Translate me on Weblate Try me on Runboat

This technical module provides the base infrastructure for defining AI tools in Odoo. It allows other modules to register callable functions as AI tools, which can then be used by MCP servers, automation flows, or @@ -393,7 +388,7 @@

Ai Tool

-

Usage

+

Usage

This module is technical, however, it adds some specific functions that might be used in glue modules.

For example, if we want to add on sales a functionality to find the @@ -411,7 +406,7 @@

Usage

</odoo>
-from odoo.addons.ai_tool import aitool
+from odoo.addons.ai_tool.tools import aitool
 
 class SaleOrder(models.Model):
     _inherit = "sale.order"
@@ -446,23 +441,23 @@ 

Usage

do a specific action with the model.

-

Bug Tracker

+

Bug Tracker

Bugs are tracked on GitHub 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.

+feedback.

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

-

Credits

+

Credits

-

Authors

+

Authors

  • Dixmit
-

Contributors

+

Contributors

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -479,11 +474,10 @@

Maintainers

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.

-

This module is part of the OCA/ai project on GitHub.

+

This module is part of the OCA/ai project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

-