Files
enterprise-addons/ai/models/discuss_channel.py
T

124 lines
5.4 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
import random
from odoo import _, fields, models, api
from odoo.exceptions import AccessError
from odoo.fields import Domain
from odoo.addons.mail.tools.discuss import Store
def is_ai_chat_channel(channel):
"""Predicate to filter channels for which the channel type is 'ai_chat'.
:returns: Whether the channel is an ai_chat channel.
:rtype: bool
"""
return channel.channel_type == "ai_chat"
class DiscussChannel(models.Model):
"""Chat Session
Representing a conversation between users.
It extends the base method for usage with AI assistant.
"""
_name = "discuss.channel"
_inherit = ["discuss.channel"]
channel_type = fields.Selection(
selection_add=[("ai_chat", "AI chat")],
ondelete={"ai_chat": "cascade"},
)
ai_env_context = fields.Json("Context for AI agent")
# Having ai_agent_id written by users can compromise the security of channels. For example, adding ai_agent_id
# to a channel will make it garbage collected, a channel member can unlink an ai agent from the channel, etc.
# Thus, the field has group fields.NO_ACCESS so that the field can only be written in controlled flows.
ai_agent_id = fields.Many2one("ai.agent", index="btree_not_null", groups=fields.NO_ACCESS)
_ai_channel_type_check = models.Constraint(
"CHECK(ai_agent_id IS NULL or channel_type = 'ai_chat' or channel_type = 'livechat')",
'AI Agent can only be set for ai_chat or livechat channels.',
)
@api.model
def create_ai_draft_channel(
self,
caller_component,
channel_title=None,
record_model=None,
record_id=None,
front_end_info=None,
text_selection=None,
):
ai_composer = None
if record_model: # if we call the AI within a specific model, we search for composer configs that might include that model and we take the last one
ai_composer = self.env['ai.composer'].sudo().search([
('interface_key', '=', caller_component),
('focused_models', 'in', record_model),
], limit=1, order="create_date DESC")
if not ai_composer: # if we don't find any composer configs or we call the ai from a place with no specific model, fallback to the basic composers
ai_composer = self.env['ai.composer'].sudo().search([
('interface_key', '=', caller_component),
('focused_models', '=', False),
], limit=1, order="create_date DESC")
ai_agent = ai_composer.ai_agent
if not ai_agent:
raise AccessError(_("AI not reachable, AI Agent not found."))
channel_name = self.env._("AI: %(name)s", name=channel_title) if channel_title else ai_agent.name
# create a new AI chat
channel = ai_agent._create_ai_chat_channel(channel_name=channel_name)
# Create the initial context for the AI - the default prompt from the composer
model_context = []
if composer_prompt := ai_composer.default_prompt:
model_context.append(composer_prompt)
model_has_thread = False
if record_model:
original_record = self.env[record_model].search([('id', '=', record_id)])
# Add extra info that are relevant to the where we call the AI from (record info, chatter info, pre-prompts, etc.)
model_context += original_record._ai_initialise_context(
caller_component, text_selection, front_end_info
)
if isinstance(original_record, self.pool['mail.thread']):
model_has_thread = True
# Finally pass the complete "save" the context to the channel
channel.ai_env_context = model_context
prompts = ai_composer.available_prompts
# Don't show prompts related to chatter if the model does not inherit from mail.thread
# (note: name is misleading, 'chatter_ai_button' means 'from the systray in a form view')
if caller_component == "chatter_ai_button" and not model_has_thread:
chatter_prompts = {
self.env.ref('ai.ai_prompt_summarize_chatter', raise_if_not_found=False),
self.env.ref('ai.ai_prompt_write_followup_chatter', raise_if_not_found=False),
}
prompts = [p for p in prompts if p not in chatter_prompts]
random_prompts = random.sample(prompts, min(3, len(prompts)))
return {
"ai_channel_id": channel.id,
"data": Store().add(channel).get_result(),
"prompts": [prompt.name for prompt in random_prompts],
"model_has_thread": model_has_thread,
}
@api.autovacuum
def _remove_ai_chat_channels(self):
# sudo() => ai_agent_id has group fields.NO_ACCESS and the method is only called from cron jobs.
self.sudo().search(
Domain("ai_agent_id", "!=", False)
& Domain('channel_type', '=', 'ai_chat')
& Domain('last_interest_dt', '<', '-1d')
).unlink()
def _to_store_defaults(self, target):
return super()._to_store_defaults(target) + [Store.One("ai_agent_id", predicate=is_ai_chat_channel, sudo=True)]
def _sync_field_names(self):
field_names = super()._sync_field_names()
field_names[None].append(Store.One("ai_agent_id", predicate=is_ai_chat_channel, sudo=True))
return field_names