124 lines
5.4 KiB
Python
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
|