From 244780eaa82a3d65b278d4a9396a72efec52f41e Mon Sep 17 00:00:00 2001 From: Pranav Date: Sun, 11 May 2025 10:15:12 -0700 Subject: [PATCH] Add support for multiple tools in Chatwoot --- app/models/concerns/llm_formattable.rb | 6 +- .../llm_formatter/contact_llm_formatter.rb | 2 +- .../conversation_llm_formatter.rb | 3 +- .../llm_formatter/default_llm_formatter.rb | 2 +- .../llm_text_formatter_service.rb | 4 +- .../services/captain/copilot/chat_service.rb | 5 +- .../captain/llm/system_prompts_service.rb | 3 +- .../tools/copilot/get_contact_service.rb | 40 ++++++++++++ .../tools/copilot/search_contact_service.rb | 63 ++++++++++++++++++ .../copilot/search_conversation_service.rb | 65 +++++++++++++++++++ 10 files changed, 184 insertions(+), 9 deletions(-) create mode 100644 enterprise/app/services/captain/tools/copilot/get_contact_service.rb create mode 100644 enterprise/app/services/captain/tools/copilot/search_contact_service.rb create mode 100644 enterprise/app/services/captain/tools/copilot/search_conversation_service.rb diff --git a/app/models/concerns/llm_formattable.rb b/app/models/concerns/llm_formattable.rb index 086ccc46a..dee085477 100644 --- a/app/models/concerns/llm_formattable.rb +++ b/app/models/concerns/llm_formattable.rb @@ -1,7 +1,9 @@ module LlmFormattable extend ActiveSupport::Concern - def to_llm_text - LlmFormatter::LlmTextFormatterService.new(self).format + def to_llm_text(include_contact_details: false) + LlmFormatter::LlmTextFormatterService.new(self).format( + include_contact_details: include_contact_details + ) end end diff --git a/app/services/llm_formatter/contact_llm_formatter.rb b/app/services/llm_formatter/contact_llm_formatter.rb index 9dcfcd299..e71b821d6 100644 --- a/app/services/llm_formatter/contact_llm_formatter.rb +++ b/app/services/llm_formatter/contact_llm_formatter.rb @@ -1,5 +1,5 @@ class LlmFormatter::ContactLlmFormatter < LlmFormatter::DefaultLlmFormatter - def format + def format(_config) sections = [] sections << "Contact ID: ##{@record.id}" sections << 'Contact Attributes:' diff --git a/app/services/llm_formatter/conversation_llm_formatter.rb b/app/services/llm_formatter/conversation_llm_formatter.rb index 07afc2cac..af0efa200 100644 --- a/app/services/llm_formatter/conversation_llm_formatter.rb +++ b/app/services/llm_formatter/conversation_llm_formatter.rb @@ -1,5 +1,5 @@ class LlmFormatter::ConversationLlmFormatter < LlmFormatter::DefaultLlmFormatter - def format + def format(include_contact_details: false) sections = [] sections << "Conversation ID: ##{@record.display_id}" sections << "Channel: #{@record.inbox.channel.name}" @@ -10,6 +10,7 @@ class LlmFormatter::ConversationLlmFormatter < LlmFormatter::DefaultLlmFormatter 'No messages in this conversation' end + sections << "Contact Details: #{@record.contact.to_llm_text}" if include_contact_details sections.join("\n") end diff --git a/app/services/llm_formatter/default_llm_formatter.rb b/app/services/llm_formatter/default_llm_formatter.rb index b82a039cf..0773b210e 100644 --- a/app/services/llm_formatter/default_llm_formatter.rb +++ b/app/services/llm_formatter/default_llm_formatter.rb @@ -3,7 +3,7 @@ class LlmFormatter::DefaultLlmFormatter @record = record end - def format + def format(config) # override this end end diff --git a/app/services/llm_formatter/llm_text_formatter_service.rb b/app/services/llm_formatter/llm_text_formatter_service.rb index 0f1f37cf8..df904842f 100644 --- a/app/services/llm_formatter/llm_text_formatter_service.rb +++ b/app/services/llm_formatter/llm_text_formatter_service.rb @@ -3,9 +3,9 @@ class LlmFormatter::LlmTextFormatterService @record = record end - def format + def format(include_contact_details: false) formatter_class = find_formatter - formatter_class.new(@record).format + formatter_class.new(@record).format(include_contact_details: include_contact_details) end private diff --git a/enterprise/app/services/captain/copilot/chat_service.rb b/enterprise/app/services/captain/copilot/chat_service.rb index 37de6f0fa..3d8363a32 100644 --- a/enterprise/app/services/captain/copilot/chat_service.rb +++ b/enterprise/app/services/captain/copilot/chat_service.rb @@ -22,6 +22,9 @@ class Captain::Copilot::ChatService < Llm::BaseOpenAiService def register_tools @tool_registry.register_tool(Captain::Tools::Copilot::GetConversationService) + @tool_registry.register_tool(Captain::Tools::Copilot::SearchConversationService) + @tool_registry.register_tool(Captain::Tools::Copilot::GetContactService) + @tool_registry.register_tool(Captain::Tools::Copilot::SearchContactService) end def build_initial_messages(config) @@ -52,7 +55,7 @@ class Captain::Copilot::ChatService < Llm::BaseOpenAiService def system_message { role: 'system', - content: Captain::Llm::SystemPromptsService.copilot_response_generator(@assistant.config['product_name'], @language) + content: Captain::Llm::SystemPromptsService.copilot_response_generator(@assistant.config['product_name'], @language, @assistant.account_id) } end diff --git a/enterprise/app/services/captain/llm/system_prompts_service.rb b/enterprise/app/services/captain/llm/system_prompts_service.rb index 71bb87414..325e58454 100644 --- a/enterprise/app/services/captain/llm/system_prompts_service.rb +++ b/enterprise/app/services/captain/llm/system_prompts_service.rb @@ -56,11 +56,12 @@ class Captain::Llm::SystemPromptsService SYSTEM_PROMPT_MESSAGE end - def copilot_response_generator(product_name, language) + def copilot_response_generator(product_name, language, account_id) <<~SYSTEM_PROMPT_MESSAGE [Identity] You are Captain, a helpful and friendly copilot assistant for support agents using the product #{product_name}. Your primary role is to assist support agents by retrieving information, compiling accurate responses, and guiding them through customer interactions. You should only provide information related to #{product_name} and must not address queries about other products or external events. + You are an assistant for the account with ID: #{account_id} [Context] Identify unresolved queries, and ensure responses are relevant and consistent with previous interactions. Always maintain a coherent and professional tone throughout the conversation. diff --git a/enterprise/app/services/captain/tools/copilot/get_contact_service.rb b/enterprise/app/services/captain/tools/copilot/get_contact_service.rb new file mode 100644 index 000000000..bacea1c0a --- /dev/null +++ b/enterprise/app/services/captain/tools/copilot/get_contact_service.rb @@ -0,0 +1,40 @@ +class Captain::Tools::Copilot::GetContactService < Captain::Tools::BaseService + def name + 'get_contact' + end + + def description + 'Get details of a contact including their profile information' + end + + def parameters + { + type: 'object', + properties: { + contact_id: { + type: 'string', + description: 'The ID of the contact to retrieve' + }, + account_id: { + type: 'string', + description: 'The ID of the account the contact belongs to' + } + }, + required: %w[contact_id account_id] + } + end + + def execute(arguments) + contact_id = arguments['contact_id'] + account_id = arguments['account_id'] + + Rails.logger.info { "[CAPTAIN][GetContact] #{contact_id}, #{account_id}" } + + return 'Missing required parameters' if contact_id.blank? || account_id.blank? + + contact = Contact.find_by(id: contact_id, account_id: account_id) + return 'Contact not found' if contact.nil? + + contact.to_llm_text + end +end diff --git a/enterprise/app/services/captain/tools/copilot/search_contact_service.rb b/enterprise/app/services/captain/tools/copilot/search_contact_service.rb new file mode 100644 index 000000000..bf57926d0 --- /dev/null +++ b/enterprise/app/services/captain/tools/copilot/search_contact_service.rb @@ -0,0 +1,63 @@ +class Captain::Tools::Copilot::SearchContactService < Captain::Tools::BaseService + def name + 'search_contact' + end + + def description + 'Search for contacts based on query parameters' + end + + def parameters + { + type: 'object', + properties: { + account_id: { + type: 'string', + description: 'The ID of the account to search contacts in' + }, + email: { + type: 'string', + description: 'Filter contacts by email' + }, + phone_number: { + type: 'string', + description: 'Filter contacts by phone number' + }, + name: { + type: 'string', + description: 'Filter contacts by name (partial match)' + } + }, + required: %w[account_id] + } + end + + def execute(arguments) + account_id = arguments['account_id'] + email = arguments['email'] + phone_number = arguments['phone_number'] + name = arguments['name'] + + Rails.logger.info do + "[CAPTAIN][SearchContact] account_id: #{account_id}, email: #{email}, phone_number: #{phone_number}, name: #{name}" + end + + return 'Missing required parameters' if account_id.blank? + + contacts = Contact.where(account_id: account_id) + contacts = contacts.where(email: email) if email.present? + contacts = contacts.where(phone_number: phone_number) if phone_number.present? + contacts = contacts.where('name ILIKE ?', "%#{name}%") if name.present? + + return 'No contacts found' unless contacts.exists? + + <<~RESPONSE + Total number of contacts: #{contacts.count} + #{ + contacts.map do |contact| + contact.to_llm_text + end.join("\n---\n") + } + RESPONSE + end +end diff --git a/enterprise/app/services/captain/tools/copilot/search_conversation_service.rb b/enterprise/app/services/captain/tools/copilot/search_conversation_service.rb new file mode 100644 index 000000000..1fb223678 --- /dev/null +++ b/enterprise/app/services/captain/tools/copilot/search_conversation_service.rb @@ -0,0 +1,65 @@ +class Captain::Tools::Copilot::SearchConversationService < Captain::Tools::BaseService + def name + 'search_conversation' + end + + def description + 'Search for conversations based on query parameters' + end + + def parameters + { + type: 'object', + properties: { + account_id: { + type: 'string', + description: 'The ID of the account to search conversations in' + }, + contact_id: { + type: 'string', + description: 'Filter conversations by contact ID' + }, + status: { + type: 'string', + enum: %w[open resolved pending snoozed], + description: 'Filter conversations by status' + }, + priority: { + type: 'string', + enum: %w[low medium high urgent], + description: 'Filter conversations by priority' + } + }, + required: %w[account_id] + } + end + + def execute(arguments) + account_id = arguments['account_id'] + status = arguments['status'] + contact_id = arguments['contact_id'] + priority = arguments['priority'] + + Rails.logger.info do + "[CAPTAIN][SearchConversation] account_id: #{account_id}, status: #{status}, contact_id: #{contact_id}, priority: #{priority}" + end + + return 'Missing required parameters' if account_id.blank? + + conversations = Conversation.where(account_id: account_id) + conversations = conversations.where(contact_id: contact_id) if contact_id.present? + conversations = conversations.where(status: status) if status.present? + conversations = conversations.where(priority: priority) if priority.present? + + return 'No conversations found' unless conversations.exists? + + <<~RESPONSE + Total number of conversations: #{conversations.count} + #{ + conversations.map do |conversation| + conversation.to_llm_text(include_contact_details: true) + end.join("\n---\n") + } + RESPONSE + end +end