From 992d2c27751e0badc9fb5f38e3581fff5bbbfa61 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Fri, 24 Apr 2026 22:14:00 -0400 Subject: [PATCH] feat(addon): add ActiveBlue AI bot to Odoo Discuss - Create res.partner for the AI bot (appears in DM contacts) - Override mail.channel.message_post to intercept direct messages to the bot partner and forward them to the agent service - Post the agent reply back into the Discuss channel as the bot - Add discuss to depends; load res_partner_bot.xml data Users can now open Discuss -> New Message -> search 'ActiveBlue AI' to start a conversation with the agent. Co-Authored-By: Claude Sonnet 4.6 --- addons/activeblue_ai/__manifest__.py | 3 +- addons/activeblue_ai/data/res_partner_bot.xml | 11 +++ addons/activeblue_ai/models/__init__.py | 2 +- addons/activeblue_ai/models/ab_ai_mail.py | 71 +++++++++++++++++++ 4 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 addons/activeblue_ai/data/res_partner_bot.xml create mode 100644 addons/activeblue_ai/models/ab_ai_mail.py diff --git a/addons/activeblue_ai/__manifest__.py b/addons/activeblue_ai/__manifest__.py index eaa51e1..181b11d 100644 --- a/addons/activeblue_ai/__manifest__.py +++ b/addons/activeblue_ai/__manifest__.py @@ -11,10 +11,11 @@ CRM, sales, project management, eLearning, expenses, and HR. 'website': 'https://activeblue.net', 'category': 'Productivity', 'license': 'LGPL-3', - 'depends': ['base', 'mail', 'web'], + 'depends': ['base', 'mail', 'web', 'discuss'], 'data': [ 'security/res_groups.xml', 'security/ir.model.access.csv', + 'data/res_partner_bot.xml', 'data/ir_cron.xml', 'views/ab_ai_bot_views.xml', 'views/ab_ai_directive_views.xml', diff --git a/addons/activeblue_ai/data/res_partner_bot.xml b/addons/activeblue_ai/data/res_partner_bot.xml new file mode 100644 index 0000000..246c548 --- /dev/null +++ b/addons/activeblue_ai/data/res_partner_bot.xml @@ -0,0 +1,11 @@ + + + + + ActiveBlue AI + True + False + AI assistant — send a direct message to chat + + + diff --git a/addons/activeblue_ai/models/__init__.py b/addons/activeblue_ai/models/__init__.py index 37413e9..87af293 100644 --- a/addons/activeblue_ai/models/__init__.py +++ b/addons/activeblue_ai/models/__init__.py @@ -1 +1 @@ -from . import ab_ai_bot, ab_ai_directive, ab_ai_log, ab_ai_registry +from . import ab_ai_bot, ab_ai_directive, ab_ai_log, ab_ai_registry, ab_ai_mail diff --git a/addons/activeblue_ai/models/ab_ai_mail.py b/addons/activeblue_ai/models/ab_ai_mail.py new file mode 100644 index 0000000..5ade6fc --- /dev/null +++ b/addons/activeblue_ai/models/ab_ai_mail.py @@ -0,0 +1,71 @@ +from __future__ import annotations +import logging +import re + +from odoo import models, api + +_logger = logging.getLogger(__name__) + +_HTML_TAG = re.compile(r'<[^>]+>') + + +def _strip_html(html: str) -> str: + return _HTML_TAG.sub(' ', html or '').strip() + + +class MailChannel(models.Model): + _inherit = 'mail.channel' + + @api.model + def _ai_bot_partner(self): + return self.env.ref('activeblue_ai.partner_activeblue_ai', raise_if_not_found=False) + + def message_post(self, *, body='', author_id=None, **kwargs): + result = super().message_post(body=body, author_id=author_id, **kwargs) + + # Only intercept direct-message channels + if self.channel_type != 'chat': + return result + + bot_partner = self._ai_bot_partner() + if not bot_partner: + return result + + member_partners = self.channel_member_ids.partner_id + if bot_partner not in member_partners: + return result + + # Don't react to the bot's own messages + if author_id == bot_partner.id: + return result + + text = _strip_html(body) + if not text: + return result + + # Identify the human sender + human_partner = member_partners.filtered(lambda p: p != bot_partner)[:1] + user = self.env['res.users'].search([('partner_id', '=', human_partner.id)], limit=1) + uid = user.id if user else self.env.uid + + try: + bot = self.env['ab.ai.bot'].sudo().search([('active', '=', True)], limit=1) + if not bot: + return result + response = bot.dispatch_message( + user_id=uid, + message=text, + context={'channel_id': self.id, 'source': 'discuss'}, + ) + reply = (response or {}).get('reply') or (response or {}).get('message') or \ + 'I could not process your request right now.' + self.sudo().message_post( + body=reply, + author_id=bot_partner.id, + message_type='comment', + subtype_xmlid='mail.mt_comment', + ) + except Exception as exc: + _logger.error('AI bot Discuss reply failed: %s', exc) + + return result