feat: split editor actions to multiple services
This commit is contained in:
@@ -1,8 +1,24 @@
|
||||
class Api::V1::Accounts::Captain::EditorController < Api::V1::Accounts::BaseController
|
||||
before_action :check_authorization
|
||||
|
||||
EVENT_SERVICE_MAP = {
|
||||
'fix_spelling_grammar' => Captain::RewriteService,
|
||||
'casual' => Captain::RewriteService,
|
||||
'professional' => Captain::RewriteService,
|
||||
'friendly' => Captain::RewriteService,
|
||||
'confident' => Captain::RewriteService,
|
||||
'straightforward' => Captain::RewriteService,
|
||||
'improve' => Captain::RewriteService,
|
||||
'summarize' => Captain::SummaryService,
|
||||
'reply_suggestion' => Captain::ReplySuggestionService,
|
||||
'label_suggestion' => Captain::LabelSuggestionService
|
||||
}.freeze
|
||||
|
||||
def process_event
|
||||
result = Captain::EditorService.new(
|
||||
service_class = EVENT_SERVICE_MAP[params[:event]['name']]
|
||||
return render json: { error: 'Unknown event' }, status: :unprocessable_entity unless service_class
|
||||
|
||||
result = service_class.new(
|
||||
account: Current.account,
|
||||
event: params[:event]
|
||||
).perform
|
||||
|
||||
@@ -7,15 +7,11 @@ class Captain::BaseEditorService
|
||||
# 120000 * 4 = 480,000 characters (rounding off downwards to 400,000 to be safe)
|
||||
TOKEN_LIMIT = 400_000
|
||||
GPT_MODEL = Llm::Config::DEFAULT_MODEL
|
||||
ALLOWED_EVENT_NAMES = %w[fix_spelling_grammar casual professional friendly confident
|
||||
straightforward improve summarize reply_suggestion label_suggestion].freeze
|
||||
CACHEABLE_EVENTS = %w[label_suggestion].freeze
|
||||
CACHEABLE_EVENTS = [].freeze
|
||||
|
||||
pattr_initialize [:account!, :event!]
|
||||
|
||||
def perform
|
||||
return nil unless valid_event_name?
|
||||
|
||||
return value_from_cache if value_from_cache.present?
|
||||
|
||||
response = send("#{event_name}_message")
|
||||
@@ -70,12 +66,6 @@ class Captain::BaseEditorService
|
||||
@conversation ||= account.conversations.find_by(display_id: event['data']['conversation_display_id'])
|
||||
end
|
||||
|
||||
def valid_event_name?
|
||||
# self.class::ALLOWED_EVENT_NAMES is way to access ALLOWED_EVENT_NAMES defined in the class hierarchy of the current object.
|
||||
# This ensures that if ALLOWED_EVENT_NAMES is updated elsewhere in it's ancestors, we access the latest value.
|
||||
self.class::ALLOWED_EVENT_NAMES.include?(event_name)
|
||||
end
|
||||
|
||||
def event_is_cacheable?
|
||||
# self.class::CACHEABLE_EVENTS is way to access CACHEABLE_EVENTS defined in the class hierarchy of the current object.
|
||||
# This ensures that if CACHEABLE_EVENTS is updated elsewhere in it's ancestors, we access the latest value.
|
||||
@@ -168,8 +158,19 @@ class Captain::BaseEditorService
|
||||
{ error: error.message, request_messages: messages }
|
||||
end
|
||||
|
||||
# To be overridden by child class
|
||||
def api_key
|
||||
raise NotImplementedError, 'Subclasses must implement api_key method'
|
||||
@api_key ||= openai_hook&.settings&.dig('api_key') || system_api_key
|
||||
end
|
||||
|
||||
def openai_hook
|
||||
@openai_hook ||= account.hooks.find_by(app_id: 'openai', status: 'enabled')
|
||||
end
|
||||
|
||||
def system_api_key
|
||||
@system_api_key ||= InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_API_KEY')&.value
|
||||
end
|
||||
|
||||
def prompt_from_file(file_name)
|
||||
Rails.root.join('lib/integrations/openai/openai_prompts', "#{file_name}.liquid").read
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,46 +1,5 @@
|
||||
class Captain::EditorService < Captain::BaseEditorService
|
||||
def fix_spelling_grammar_message
|
||||
call_llm_with_prompt(fix_spelling_grammar_prompt)
|
||||
end
|
||||
|
||||
def confident_message
|
||||
call_llm_with_prompt(tone_rewrite_prompt('confident'))
|
||||
end
|
||||
|
||||
def straightforward_message
|
||||
call_llm_with_prompt(tone_rewrite_prompt('straightforward'))
|
||||
end
|
||||
|
||||
def casual_message
|
||||
call_llm_with_prompt(tone_rewrite_prompt('casual'))
|
||||
end
|
||||
|
||||
def friendly_message
|
||||
call_llm_with_prompt(tone_rewrite_prompt('friendly'))
|
||||
end
|
||||
|
||||
def professional_message
|
||||
call_llm_with_prompt(tone_rewrite_prompt('professional'))
|
||||
end
|
||||
|
||||
def improve_message
|
||||
template = prompt_from_file('improve')
|
||||
|
||||
system_prompt = render_liquid_template(template, {
|
||||
'conversation_context' => conversation.to_llm_text(include_contact_details: true),
|
||||
'draft_message' => event['data']['content']
|
||||
})
|
||||
|
||||
call_llm_with_prompt(system_prompt, event['data']['content'])
|
||||
end
|
||||
|
||||
def summarize_message
|
||||
make_api_call(summarize_body)
|
||||
end
|
||||
|
||||
def reply_suggestion_message
|
||||
make_api_call(reply_suggestion_body)
|
||||
end
|
||||
class Captain::LabelSuggestionService < Captain::BaseEditorService
|
||||
CACHEABLE_EVENTS = %w[label_suggestion].freeze
|
||||
|
||||
def label_suggestion_message
|
||||
payload = label_suggestion_body
|
||||
@@ -56,65 +15,6 @@ class Captain::EditorService < Captain::BaseEditorService
|
||||
|
||||
private
|
||||
|
||||
def api_key
|
||||
@api_key ||= openai_hook&.settings&.dig('api_key') || system_api_key
|
||||
end
|
||||
|
||||
def openai_hook
|
||||
@openai_hook ||= account.hooks.find_by(app_id: 'openai', status: 'enabled')
|
||||
end
|
||||
|
||||
def system_api_key
|
||||
@system_api_key ||= InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_API_KEY')&.value
|
||||
end
|
||||
|
||||
def call_llm_with_prompt(system_content, user_content = event['data']['content'])
|
||||
body = {
|
||||
model: GPT_MODEL,
|
||||
messages: [
|
||||
{ role: 'system', content: system_content },
|
||||
{ role: 'user', content: user_content }
|
||||
]
|
||||
}.to_json
|
||||
make_api_call(body)
|
||||
end
|
||||
|
||||
def prompt_from_file(file_name)
|
||||
Rails.root.join('lib/integrations/openai/openai_prompts', "#{file_name}.liquid").read
|
||||
end
|
||||
|
||||
def render_liquid_template(template_content, variables = {})
|
||||
Liquid::Template.parse(template_content).render(variables)
|
||||
end
|
||||
|
||||
def tone_rewrite_prompt(tone)
|
||||
template = prompt_from_file('tone_rewrite')
|
||||
render_liquid_template(template, 'tone' => tone)
|
||||
end
|
||||
|
||||
def fix_spelling_grammar_prompt
|
||||
prompt_from_file('fix_spelling_grammar')
|
||||
end
|
||||
|
||||
def summarize_body
|
||||
{
|
||||
model: GPT_MODEL,
|
||||
messages: [
|
||||
{ role: 'system', content: prompt_from_file('summary') },
|
||||
{ role: 'user', content: conversation_messages }
|
||||
]
|
||||
}.to_json
|
||||
end
|
||||
|
||||
def reply_suggestion_body
|
||||
{
|
||||
model: GPT_MODEL,
|
||||
messages: [
|
||||
{ role: 'system', content: prompt_from_file('reply') }
|
||||
].concat(conversation_messages(in_array_format: true))
|
||||
}.to_json
|
||||
end
|
||||
|
||||
def label_suggestion_body
|
||||
# TODO: Enable based on separate model and settings source
|
||||
# Future: Different model for label suggestion
|
||||
@@ -0,0 +1,71 @@
|
||||
class Captain::ReplySuggestionService < Captain::BaseEditorService
|
||||
def reply_suggestion_message
|
||||
make_api_call(reply_suggestion_body)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def reply_suggestion_body
|
||||
{
|
||||
model: GPT_MODEL,
|
||||
messages: [
|
||||
{ role: 'system', content: prompt_from_file('reply') }
|
||||
].concat(conversation_messages(in_array_format: true))
|
||||
}.to_json
|
||||
end
|
||||
|
||||
def conversation_messages(in_array_format: false)
|
||||
messages = init_messages_body(in_array_format)
|
||||
add_messages_until_token_limit(conversation, messages, in_array_format)
|
||||
end
|
||||
|
||||
def init_messages_body(in_array_format)
|
||||
in_array_format ? [] : ''
|
||||
end
|
||||
|
||||
def add_messages_until_token_limit(conversation, messages, in_array_format, start_from = 0)
|
||||
character_count = start_from
|
||||
conversation.messages
|
||||
.where(message_type: [:incoming, :outgoing])
|
||||
.where(private: false)
|
||||
.reorder('id desc')
|
||||
.each do |message|
|
||||
character_count, message_added = add_message_if_within_limit(character_count, message, messages, in_array_format)
|
||||
break unless message_added
|
||||
end
|
||||
messages
|
||||
end
|
||||
|
||||
def add_message_if_within_limit(character_count, message, messages, in_array_format)
|
||||
content = message.content_for_llm
|
||||
if valid_message?(content, character_count)
|
||||
add_message_to_list(message, messages, in_array_format, content)
|
||||
character_count += content.length
|
||||
[character_count, true]
|
||||
else
|
||||
[character_count, false]
|
||||
end
|
||||
end
|
||||
|
||||
def valid_message?(content, character_count)
|
||||
content.present? && character_count + content.length <= TOKEN_LIMIT
|
||||
end
|
||||
|
||||
def add_message_to_list(message, messages, in_array_format, content)
|
||||
formatted_message = format_message(message, in_array_format, content)
|
||||
messages.prepend(formatted_message)
|
||||
end
|
||||
|
||||
def format_message(message, in_array_format, content)
|
||||
in_array_format ? format_message_in_array(message, content) : format_message_in_string(message, content)
|
||||
end
|
||||
|
||||
def format_message_in_array(message, content)
|
||||
{ role: (message.incoming? ? 'user' : 'assistant'), content: content }
|
||||
end
|
||||
|
||||
def format_message_in_string(message, content)
|
||||
sender_type = message.incoming? ? 'Customer' : 'Agent'
|
||||
"#{sender_type} #{message.sender&.name} : #{content}\n"
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,58 @@
|
||||
class Captain::RewriteService < Captain::BaseEditorService
|
||||
def fix_spelling_grammar_message
|
||||
call_llm_with_prompt(prompt_from_file('fix_spelling_grammar'))
|
||||
end
|
||||
|
||||
def confident_message
|
||||
call_llm_with_prompt(tone_rewrite_prompt('confident'))
|
||||
end
|
||||
|
||||
def straightforward_message
|
||||
call_llm_with_prompt(tone_rewrite_prompt('straightforward'))
|
||||
end
|
||||
|
||||
def casual_message
|
||||
call_llm_with_prompt(tone_rewrite_prompt('casual'))
|
||||
end
|
||||
|
||||
def friendly_message
|
||||
call_llm_with_prompt(tone_rewrite_prompt('friendly'))
|
||||
end
|
||||
|
||||
def professional_message
|
||||
call_llm_with_prompt(tone_rewrite_prompt('professional'))
|
||||
end
|
||||
|
||||
def improve_message
|
||||
template = prompt_from_file('improve')
|
||||
|
||||
system_prompt = render_liquid_template(template, {
|
||||
'conversation_context' => conversation.to_llm_text(include_contact_details: true),
|
||||
'draft_message' => event['data']['content']
|
||||
})
|
||||
|
||||
call_llm_with_prompt(system_prompt, event['data']['content'])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def call_llm_with_prompt(system_content, user_content = event['data']['content'])
|
||||
body = {
|
||||
model: GPT_MODEL,
|
||||
messages: [
|
||||
{ role: 'system', content: system_content },
|
||||
{ role: 'user', content: user_content }
|
||||
]
|
||||
}.to_json
|
||||
make_api_call(body)
|
||||
end
|
||||
|
||||
def render_liquid_template(template_content, variables = {})
|
||||
Liquid::Template.parse(template_content).render(variables)
|
||||
end
|
||||
|
||||
def tone_rewrite_prompt(tone)
|
||||
template = prompt_from_file('tone_rewrite')
|
||||
render_liquid_template(template, 'tone' => tone)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,72 @@
|
||||
class Captain::SummaryService < Captain::BaseEditorService
|
||||
def summarize_message
|
||||
make_api_call(summarize_body)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def summarize_body
|
||||
{
|
||||
model: GPT_MODEL,
|
||||
messages: [
|
||||
{ role: 'system', content: prompt_from_file('summary') },
|
||||
{ role: 'user', content: conversation_messages }
|
||||
]
|
||||
}.to_json
|
||||
end
|
||||
|
||||
def conversation_messages(in_array_format: false)
|
||||
messages = init_messages_body(in_array_format)
|
||||
add_messages_until_token_limit(conversation, messages, in_array_format)
|
||||
end
|
||||
|
||||
def init_messages_body(in_array_format)
|
||||
in_array_format ? [] : ''
|
||||
end
|
||||
|
||||
def add_messages_until_token_limit(conversation, messages, in_array_format, start_from = 0)
|
||||
character_count = start_from
|
||||
conversation.messages
|
||||
.where(message_type: [:incoming, :outgoing])
|
||||
.where(private: false)
|
||||
.reorder('id desc')
|
||||
.each do |message|
|
||||
character_count, message_added = add_message_if_within_limit(character_count, message, messages, in_array_format)
|
||||
break unless message_added
|
||||
end
|
||||
messages
|
||||
end
|
||||
|
||||
def add_message_if_within_limit(character_count, message, messages, in_array_format)
|
||||
content = message.content_for_llm
|
||||
if valid_message?(content, character_count)
|
||||
add_message_to_list(message, messages, in_array_format, content)
|
||||
character_count += content.length
|
||||
[character_count, true]
|
||||
else
|
||||
[character_count, false]
|
||||
end
|
||||
end
|
||||
|
||||
def valid_message?(content, character_count)
|
||||
content.present? && character_count + content.length <= TOKEN_LIMIT
|
||||
end
|
||||
|
||||
def add_message_to_list(message, messages, in_array_format, content)
|
||||
formatted_message = format_message(message, in_array_format, content)
|
||||
messages.prepend(formatted_message)
|
||||
end
|
||||
|
||||
def format_message(message, in_array_format, content)
|
||||
in_array_format ? format_message_in_array(message, content) : format_message_in_string(message, content)
|
||||
end
|
||||
|
||||
def format_message_in_array(message, content)
|
||||
{ role: (message.incoming? ? 'user' : 'assistant'), content: content }
|
||||
end
|
||||
|
||||
def format_message_in_string(message, content)
|
||||
sender_type = message.incoming? ? 'Customer' : 'Agent'
|
||||
"#{sender_type} #{message.sender&.name} : #{content}\n"
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user