feat: Add support for additional tools in Copilot (#11531)

- Added GetConversation, GetContact, GetArticle, SearchContacts,
SearchArticles
- Update SearchConversations to handle the permissions properly.
This commit is contained in:
Pranav
2025-05-21 15:49:35 -07:00
committed by GitHub
parent bc42aec68e
commit dc7f1597e5
15 changed files with 977 additions and 3 deletions

View File

@@ -1,14 +1,15 @@
class Captain::ToolRegistryService
attr_reader :registered_tools, :tools
def initialize(assistant)
def initialize(assistant, user: nil)
@assistant = assistant
@user = user
@registered_tools = []
@tools = {}
end
def register_tool(tool_class)
tool = tool_class.new(@assistant)
tool = tool_class.new(@assistant, user: @user)
return unless tool.active?
@tools[tool.name] = tool

View File

@@ -36,4 +36,18 @@ class Captain::Tools::BaseService
def active?
true
end
private
def user_has_permission(permission)
return false if @user.blank?
account_user = AccountUser.find_by(account_id: @assistant.account_id, user_id: @user.id)
return false if account_user.blank?
return account_user.custom_role.permissions.include?(permission) if account_user.custom_role.present?
# Default permission for agents without custom roles
account_user.administrator? || account_user.agent?
end
end

View File

@@ -0,0 +1,39 @@
class Captain::Tools::Copilot::GetArticleService < Captain::Tools::BaseService
def name
'get_article'
end
def description
'Get details of an article including its content and metadata'
end
def parameters
{
type: 'object',
properties: {
article_id: {
type: 'number',
description: 'The ID of the article to retrieve'
}
},
required: %w[article_id]
}
end
def execute(arguments)
article_id = arguments['article_id']
Rails.logger.info { "#{self.class.name}: Article ID: #{article_id}" }
return 'Missing required parameters' if article_id.blank?
article = Article.find_by(id: article_id, account_id: @assistant.account_id)
return 'Article not found' if article.nil?
article.to_llm_text
end
def active?
user_has_permission('knowledge_base_manage')
end
end

View File

@@ -0,0 +1,39 @@
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: 'number',
description: 'The ID of the contact to retrieve'
}
},
required: %w[contact_id]
}
end
def execute(arguments)
contact_id = arguments['contact_id']
Rails.logger.info "#{self.class.name}: Contact ID: #{contact_id}"
return 'Missing required parameters' if contact_id.blank?
contact = Contact.find_by(id: contact_id, account_id: @assistant.account_id)
return 'Contact not found' if contact.nil?
contact.to_llm_text
end
def active?
user_has_permission('contact_manage')
end
end

View File

@@ -0,0 +1,41 @@
class Captain::Tools::Copilot::GetConversationService < Captain::Tools::BaseService
def name
'get_conversation'
end
def description
'Get details of a conversation including messages and contact information'
end
def parameters
{
type: 'object',
properties: {
conversation_id: {
type: 'number',
description: 'The ID of the conversation to retrieve'
}
},
required: %w[conversation_id]
}
end
def execute(arguments)
conversation_id = arguments['conversation_id']
Rails.logger.info "#{self.class.name}: Conversation ID: #{conversation_id}"
return 'Missing required parameters' if conversation_id.blank?
conversation = Conversation.find_by(display_id: conversation_id, account_id: @assistant.account_id)
return 'Conversation not found' if conversation.blank?
conversation.to_llm_text
end
def active?
user_has_permission('conversation_manage') ||
user_has_permission('conversation_unassigned_manage') ||
user_has_permission('conversation_participating_manage')
end
end

View File

@@ -0,0 +1,71 @@
class Captain::Tools::Copilot::SearchArticlesService < Captain::Tools::BaseService
def name
'search_articles'
end
def description
'Search articles based on parameters'
end
def parameters
{
type: 'object',
properties: properties,
required: ['query']
}
end
def execute(arguments)
query = arguments['query']
category_id = arguments['category_id']
status = arguments['status']
Rails.logger.info "#{self.class.name}: Query: #{query}, Category ID: #{category_id}, Status: #{status}"
return 'Missing required parameters' if query.blank?
articles = fetch_articles(query, category_id, status)
return 'No articles found' unless articles.exists?
total_count = articles.count
articles = articles.limit(100)
<<~RESPONSE
#{total_count > 100 ? "Found #{total_count} articles (showing first 100)" : "Total number of articles: #{total_count}"}
#{articles.map(&:to_llm_text).join("\n---\n")}
RESPONSE
end
def active?
user_has_permission('knowledge_base_manage')
end
private
def fetch_articles(query, category_id, status)
articles = Article.where(account_id: @assistant.account_id)
articles = articles.where('title ILIKE :query OR content ILIKE :query', query: "%#{query}%") if query.present?
articles = articles.where(category_id: category_id) if category_id.present?
articles = articles.where(status: status) if status.present?
articles
end
def properties
{
query: {
type: 'string',
description: 'Search articles by title or content (partial match)'
},
category_id: {
type: 'number',
description: 'Filter articles by category ID'
},
status: {
type: 'string',
enum: %w[draft published archived],
description: 'Filter articles by status'
}
}
end
end

View File

@@ -0,0 +1,61 @@
class Captain::Tools::Copilot::SearchContactsService < Captain::Tools::BaseService
def name
'search_contacts'
end
def description
'Search contacts based on query parameters'
end
def parameters
{
type: 'object',
properties: properties,
required: []
}
end
def execute(arguments)
email = arguments['email']
phone_number = arguments['phone_number']
name = arguments['name']
Rails.logger.info "#{self.class.name} Email: #{email}, Phone Number: #{phone_number}, Name: #{name}"
contacts = Contact.where(account_id: @assistant.account_id)
contacts = contacts.where(email: email) if email.present?
contacts = contacts.where(phone_number: phone_number) if phone_number.present?
contacts = contacts.where('LOWER(name) ILIKE ?', "%#{name.downcase}%") if name.present?
return 'No contacts found' unless contacts.exists?
contacts = contacts.limit(100)
<<~RESPONSE
#{contacts.map(&:to_llm_text).join("\n---\n")}
RESPONSE
end
def active?
user_has_permission('contact_manage')
end
private
def properties
{
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)'
}
}
end
end

View File

@@ -33,6 +33,12 @@ class Captain::Tools::Copilot::SearchConversationsService < Captain::Tools::Base
RESPONSE
end
def active?
user_has_permission('conversation_manage') ||
user_has_permission('conversation_unassigned_manage') ||
user_has_permission('conversation_participating_manage')
end
private
def get_conversations(status, contact_id, priority)