diff --git a/app/controllers/webhooks/bot_runtime_controller.rb b/app/controllers/webhooks/bot_runtime_controller.rb index 3987eb370..7e9c9b094 100644 --- a/app/controllers/webhooks/bot_runtime_controller.rb +++ b/app/controllers/webhooks/bot_runtime_controller.rb @@ -22,7 +22,20 @@ def postback return end - message = AgentBots::MessageCreator.new(agent_bot).create_bot_reply(content, conversation) + content_type = params[:content_type].presence || 'text' + raw_items = params[:items] + content_attributes = nil + + if content_type == 'input_select' && raw_items.present? + items = raw_items.map { |item| { title: item[:title].to_s, value: item[:value].to_s } } + content_attributes = { items: items } + end + + message = AgentBots::MessageCreator.new(agent_bot).create_bot_reply( + content, conversation, + content_type: content_type, + content_attributes: content_attributes + ) if message Rails.logger.info "[BotRuntime::Postback] Message created: #{message.id} conversation=#{conversation.display_id}" diff --git a/app/services/agent_bots/message_creator.rb b/app/services/agent_bots/message_creator.rb index 6f9303cf5..4576438f3 100644 --- a/app/services/agent_bots/message_creator.rb +++ b/app/services/agent_bots/message_creator.rb @@ -3,7 +3,7 @@ def initialize(agent_bot) @agent_bot = agent_bot end - def create_bot_reply(content, conversation, force: false) + def create_bot_reply(content, conversation, force: false, content_type: 'text', content_attributes: nil) return if content.blank? # If force is true, skip eligibility check (e.g., for final response after transfer) @@ -18,7 +18,7 @@ def create_bot_reply(content, conversation, force: false) end Rails.logger.info "[AgentBot HTTP] Creating bot reply in conversation #{conversation.id}" - create_message_with_fallback(content, conversation) + create_message_with_fallback(content, conversation, content_type: content_type, content_attributes: content_attributes) end private @@ -53,23 +53,26 @@ def conversation_eligible_for_bot_reply?(conversation) eligible end - def create_message_with_fallback(content, conversation) - create_direct_message(content, conversation) + def create_message_with_fallback(content, conversation, content_type:, content_attributes:) + create_direct_message(content, conversation, content_type: content_type, content_attributes: content_attributes) rescue StandardError => e log_creation_error(e) - create_message_with_builder(content, conversation) + create_message_with_builder(content, conversation, content_type: content_type, content_attributes: content_attributes) end - def create_direct_message(content, conversation) + def create_direct_message(content, conversation, content_type:, content_attributes:) message_attributes = { inbox: conversation.inbox, conversation: conversation, content: content, message_type: 'outgoing', sender: @agent_bot, - content_type: 'text' + content_type: content_type } + merged_content_attributes = {} + merged_content_attributes.merge!(content_attributes) if content_attributes.present? + # Check if send_as_reply is enabled in bot_config send_as_reply = @agent_bot.bot_config&.dig('send_as_reply') == true @@ -77,9 +80,11 @@ def create_direct_message(content, conversation) # OR if send_as_reply is enabled in bot_config if conversation.post_conversation? || send_as_reply reply_attributes = build_reply_attributes(conversation) - message_attributes[:content_attributes] = reply_attributes if reply_attributes.present? + merged_content_attributes.merge!(reply_attributes) if reply_attributes.present? end + message_attributes[:content_attributes] = merged_content_attributes if merged_content_attributes.present? + message = Message.create!(message_attributes) Rails.logger.info "[AgentBot HTTP] Successfully created message #{message.id}" @@ -94,8 +99,11 @@ def log_creation_error(error) Rails.logger.info '[AgentBot HTTP] Trying with MessageBuilder as fallback' end - def create_message_with_builder(content, conversation) - message_params = { content: content, message_type: 'outgoing' } + def create_message_with_builder(content, conversation, content_type:, content_attributes:) + message_params = { content: content, message_type: 'outgoing', content_type: content_type } + + merged_content_attributes = {} + merged_content_attributes.merge!(content_attributes) if content_attributes.present? # Check if send_as_reply is enabled in bot_config send_as_reply = @agent_bot.bot_config&.dig('send_as_reply') == true @@ -104,9 +112,11 @@ def create_message_with_builder(content, conversation) # OR if send_as_reply is enabled in bot_config if conversation.post_conversation? || send_as_reply reply_attributes = build_reply_attributes(conversation) - message_params[:content_attributes] = reply_attributes if reply_attributes.present? + merged_content_attributes.merge!(reply_attributes) if reply_attributes.present? end + message_params[:content_attributes] = merged_content_attributes if merged_content_attributes.present? + message = Messages::MessageBuilder.new(@agent_bot, conversation, message_params).perform Rails.logger.info "[AgentBot HTTP] MessageBuilder fallback successful: #{message.id}" Rails.logger.info "[AgentBot HTTP] Reply attributes: #{message.content_attributes.slice(:in_reply_to, :in_reply_to_external_id).inspect}" if conversation.post_conversation? || send_as_reply diff --git a/app/services/agent_bots/response_processor.rb b/app/services/agent_bots/response_processor.rb index 9be30de75..e2cb36294 100644 --- a/app/services/agent_bots/response_processor.rb +++ b/app/services/agent_bots/response_processor.rb @@ -43,14 +43,18 @@ def process_bot_response(parsed_response) artifacts = extract_artifacts(parsed_response) return unless artifacts - text_content = extract_text_from_artifacts(artifacts) + extracted = extract_content_from_artifacts(artifacts) + text_content = extracted[:text] return unless text_content conversation = AgentBots::ConversationFinder.new(@agent_bot, @payload).find_conversation return unless conversation + select_part = extracted[:select] + select_items = select_part&.dig('items') + # Check if text segmentation is enabled for this agent bot - if @agent_bot.text_segmentation_enabled && ['evo_ai_provider', 'n8n_provider'].include?(@agent_bot.bot_provider) + if select_items.blank? && @agent_bot.text_segmentation_enabled && ['evo_ai_provider', 'n8n_provider'].include?(@agent_bot.bot_provider) process_segmented_response(text_content, conversation) else # Process as a single message with signature @@ -59,13 +63,15 @@ def process_bot_response(parsed_response) # Try to create message normally first message_creator = AgentBots::MessageCreator.new(@agent_bot) - message = message_creator.create_bot_reply(final_content, conversation) + content_type = select_items.present? ? 'input_select' : 'text' + content_attributes = select_items.present? ? { items: select_items } : nil + message = message_creator.create_bot_reply(final_content, conversation, content_type: content_type, content_attributes: content_attributes) # If message creation failed (conversation not eligible, e.g., after transfer), # try to force create it anyway (for final responses after transfer) unless message Rails.logger.info "[AgentBot HTTP] Message creation failed (conversation not eligible), attempting force create..." - message = message_creator.create_bot_reply(final_content, conversation, force: true) + message = message_creator.create_bot_reply(final_content, conversation, force: true, content_type: content_type, content_attributes: content_attributes) end message @@ -79,12 +85,27 @@ def extract_artifacts(parsed_response) artifacts end - def extract_text_from_artifacts(artifacts) - artifact = artifacts.first - return unless artifact['parts']&.any? + def extract_content_from_artifacts(artifacts) + text = nil + select = nil + + artifacts.each do |artifact| + next unless artifact.is_a?(Hash) && artifact['parts'].is_a?(Array) + + artifact['parts'].each do |part| + next unless part.is_a?(Hash) + + if text.nil? && part['type'] == 'text' && part['text'].present? + text = part['text'] + end + + if select.nil? && part['type'] == 'select' + select = part + end + end + end - text_part = artifact['parts'].find { |p| p['type'] == 'text' } - text_part&.dig('text') + { text: text, select: select } end def process_segmented_response(text_content, conversation) diff --git a/app/services/whatsapp/providers/base_service.rb b/app/services/whatsapp/providers/base_service.rb index 56f734608..c521499e4 100644 --- a/app/services/whatsapp/providers/base_service.rb +++ b/app/services/whatsapp/providers/base_service.rb @@ -114,10 +114,27 @@ def create_payload_based_on_items(message) end end + def interactive_body_text(message) + content = html_to_whatsapp(message.content.to_s) + return content if content.blank? + + lines = content.split("\n") + removed_any = false + + while lines.any? && lines.last.strip.match?(/^\d+\.\s+\S/) + lines.pop + removed_any = true + end + + pruned = lines.join("\n").strip + return content unless removed_any + pruned.presence || content + end + def create_button_payload(message) buttons = create_buttons(message.content_attributes['items']) json_hash = { 'buttons' => buttons } - create_payload('button', message.content, JSON.generate(json_hash)) + create_payload('button', interactive_body_text(message), JSON.generate(json_hash)) end def create_list_payload(message) @@ -125,6 +142,6 @@ def create_list_payload(message) section1 = { 'rows' => rows } sections = [section1] json_hash = { :button => 'Choose an item', 'sections' => sections } - create_payload('list', message.content, JSON.generate(json_hash)) + create_payload('list', interactive_body_text(message), JSON.generate(json_hash)) end end diff --git a/app/services/whatsapp/providers/evolution_go_service.rb b/app/services/whatsapp/providers/evolution_go_service.rb index 2ffb8de2e..43b4e0074 100644 --- a/app/services/whatsapp/providers/evolution_go_service.rb +++ b/app/services/whatsapp/providers/evolution_go_service.rb @@ -233,7 +233,7 @@ def send_button_message(clean_number, message, items) { type: 'reply', displayText: item['title'].to_s.truncate(20), id: item['value'].to_s } end - content = html_to_whatsapp(message.content.to_s) + content = interactive_body_text(message) body = { number: clean_number, @@ -265,7 +265,7 @@ def send_list_message(clean_number, message, items) { rowId: item['value'].to_s, title: item['title'].to_s.truncate(24), description: '' } end - content = html_to_whatsapp(message.content.to_s) + content = interactive_body_text(message) body = { number: clean_number, diff --git a/app/services/whatsapp/providers/evolution_service.rb b/app/services/whatsapp/providers/evolution_service.rb index 606615022..91a05e5f2 100644 --- a/app/services/whatsapp/providers/evolution_service.rb +++ b/app/services/whatsapp/providers/evolution_service.rb @@ -320,7 +320,7 @@ def send_button_message(clean_number, message, items) { type: 'reply', displayText: item['title'].to_s.truncate(20), id: item['value'].to_s } end - content = html_to_whatsapp(message.content.to_s) + content = interactive_body_text(message) body = { number: clean_number, @@ -348,7 +348,7 @@ def send_list_message(clean_number, message, items) { rowId: item['value'].to_s, title: item['title'].to_s.truncate(24), description: '' } end - content = html_to_whatsapp(message.content.to_s) + content = interactive_body_text(message) body = { number: clean_number,