Add support for multiple tools in Chatwoot

This commit is contained in:
Pranav
2025-05-11 10:15:12 -07:00
parent 6609f7637a
commit 244780eaa8
10 changed files with 184 additions and 9 deletions
+4 -2
View File
@@ -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
@@ -1,5 +1,5 @@
class LlmFormatter::ContactLlmFormatter < LlmFormatter::DefaultLlmFormatter
def format
def format(_config)
sections = []
sections << "Contact ID: ##{@record.id}"
sections << 'Contact Attributes:'
@@ -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
@@ -3,7 +3,7 @@ class LlmFormatter::DefaultLlmFormatter
@record = record
end
def format
def format(config)
# override this
end
end
@@ -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
@@ -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
@@ -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.
@@ -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
@@ -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
@@ -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