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

@@ -4,7 +4,7 @@ require 'rails_helper'
class TestTool < Captain::Tools::BaseService
attr_accessor :tool_active
def initialize(*args)
def initialize(assistant, user: nil)
super
@tool_active = true
end

View File

@@ -0,0 +1,112 @@
require 'rails_helper'
RSpec.describe Captain::Tools::Copilot::GetArticleService do
let(:account) { create(:account) }
let(:user) { create(:user, account: account) }
let(:assistant) { create(:captain_assistant, account: account) }
let(:service) { described_class.new(assistant, user: user) }
describe '#name' do
it 'returns the correct service name' do
expect(service.name).to eq('get_article')
end
end
describe '#description' do
it 'returns the service description' do
expect(service.description).to eq('Get details of an article including its content and metadata')
end
end
describe '#parameters' do
it 'returns the expected parameter schema' do
expect(service.parameters).to eq(
{
type: 'object',
properties: {
article_id: {
type: 'number',
description: 'The ID of the article to retrieve'
}
},
required: %w[article_id]
}
)
end
end
describe '#active?' do
context 'when user is an admin' do
let(:user) { create(:user, :administrator, account: account) }
let(:assistant) { create(:captain_assistant, account: account) }
it 'returns true' do
expect(service.active?).to be true
end
end
context 'when user has custom role with knowledge_base_manage permission' do
let(:user) { create(:user, account: account) }
let(:assistant) { create(:captain_assistant, account: account) }
let(:custom_role) { create(:custom_role, account: account, permissions: ['knowledge_base_manage']) }
before do
account_user = AccountUser.find_by(user: user, account: account)
account_user.update(role: :agent, custom_role: custom_role)
end
it 'returns true' do
expect(service.active?).to be true
end
end
context 'when user has custom role without knowledge_base_manage permission' do
let(:user) { create(:user, account: account) }
let(:assistant) { create(:captain_assistant, account: account) }
let(:custom_role) { create(:custom_role, account: account, permissions: []) }
before do
account_user = AccountUser.find_by(user: user, account: account)
account_user.update(role: :agent, custom_role: custom_role)
end
it 'returns false' do
expect(service.active?).to be false
end
end
end
describe '#execute' do
context 'when article_id is blank' do
it 'returns error message' do
expect(service.execute({})).to eq('Missing required parameters')
end
end
context 'when article is not found' do
it 'returns not found message' do
expect(service.execute({ 'article_id' => 999 })).to eq('Article not found')
end
end
context 'when article exists' do
let(:portal) { create(:portal, account: account) }
let(:article) { create(:article, account: account, portal: portal, author: user, title: 'Test Article', content: 'Content') }
it 'returns the article in llm text format' do
result = service.execute({ 'article_id' => article.id })
expect(result).to eq(article.to_llm_text)
end
context 'when article belongs to different account' do
let(:other_account) { create(:account) }
let(:other_portal) { create(:portal, account: other_account) }
let(:other_article) { create(:article, account: other_account, portal: other_portal, author: user, title: 'Other Article') }
it 'returns not found message' do
expect(service.execute({ 'article_id' => other_article.id })).to eq('Article not found')
end
end
end
end
end

View File

@@ -0,0 +1,110 @@
require 'rails_helper'
RSpec.describe Captain::Tools::Copilot::GetContactService do
let(:account) { create(:account) }
let(:user) { create(:user, account: account) }
let(:assistant) { create(:captain_assistant, account: account) }
let(:service) { described_class.new(assistant, user: user) }
describe '#name' do
it 'returns the correct service name' do
expect(service.name).to eq('get_contact')
end
end
describe '#description' do
it 'returns the service description' do
expect(service.description).to eq('Get details of a contact including their profile information')
end
end
describe '#parameters' do
it 'returns the expected parameter schema' do
expect(service.parameters).to eq(
{
type: 'object',
properties: {
contact_id: {
type: 'number',
description: 'The ID of the contact to retrieve'
}
},
required: %w[contact_id]
}
)
end
end
describe '#active?' do
context 'when user is an admin' do
let(:user) { create(:user, :administrator, account: account) }
let(:assistant) { create(:captain_assistant, account: account) }
it 'returns true' do
expect(service.active?).to be true
end
end
context 'when user has custom role with contact_manage permission' do
let(:user) { create(:user, account: account) }
let(:assistant) { create(:captain_assistant, account: account) }
let(:custom_role) { create(:custom_role, account: account, permissions: ['contact_manage']) }
before do
account_user = AccountUser.find_by(user: user, account: account)
account_user.update(role: :agent, custom_role: custom_role)
end
it 'returns true' do
expect(service.active?).to be true
end
end
context 'when user has custom role without contact_manage permission' do
let(:user) { create(:user, account: account) }
let(:assistant) { create(:captain_assistant, account: account) }
let(:custom_role) { create(:custom_role, account: account, permissions: []) }
before do
account_user = AccountUser.find_by(user: user, account: account)
account_user.update(role: :agent, custom_role: custom_role)
end
it 'returns false' do
expect(service.active?).to be false
end
end
end
describe '#execute' do
context 'when contact_id is blank' do
it 'returns error message' do
expect(service.execute({})).to eq('Missing required parameters')
end
end
context 'when contact is not found' do
it 'returns not found message' do
expect(service.execute({ 'contact_id' => 999 })).to eq('Contact not found')
end
end
context 'when contact exists' do
let(:contact) { create(:contact, account: account) }
it 'returns the contact in llm text format' do
result = service.execute({ 'contact_id' => contact.id })
expect(result).to eq(contact.to_llm_text)
end
context 'when contact belongs to different account' do
let(:other_account) { create(:account) }
let(:other_contact) { create(:contact, account: other_account) }
it 'returns not found message' do
expect(service.execute({ 'contact_id' => other_contact.id })).to eq('Contact not found')
end
end
end
end
end

View File

@@ -0,0 +1,142 @@
require 'rails_helper'
RSpec.describe Captain::Tools::Copilot::GetConversationService do
let(:account) { create(:account) }
let(:user) { create(:user, account: account) }
let(:assistant) { create(:captain_assistant, account: account) }
let(:service) { described_class.new(assistant, user: user) }
describe '#name' do
it 'returns the correct service name' do
expect(service.name).to eq('get_conversation')
end
end
describe '#description' do
it 'returns the service description' do
expect(service.description).to eq('Get details of a conversation including messages and contact information')
end
end
describe '#parameters' do
it 'returns the expected parameter schema' do
expect(service.parameters).to eq(
{
type: 'object',
properties: {
conversation_id: {
type: 'number',
description: 'The ID of the conversation to retrieve'
}
},
required: %w[conversation_id]
}
)
end
end
describe '#active?' do
context 'when user is an admin' do
let(:user) { create(:user, :administrator, account: account) }
let(:assistant) { create(:captain_assistant, account: account) }
it 'returns true' do
expect(service.active?).to be true
end
end
context 'when user has custom role with conversation_manage permission' do
let(:user) { create(:user, account: account) }
let(:assistant) { create(:captain_assistant, account: account) }
let(:custom_role) { create(:custom_role, account: account, permissions: ['conversation_manage']) }
before do
account_user = AccountUser.find_by(user: user, account: account)
account_user.update(role: :agent, custom_role: custom_role)
end
it 'returns true' do
expect(service.active?).to be true
end
end
context 'when user has custom role with conversation_unassigned_manage permission' do
let(:user) { create(:user, account: account) }
let(:assistant) { create(:captain_assistant, account: account) }
let(:custom_role) { create(:custom_role, account: account, permissions: ['conversation_unassigned_manage']) }
before do
account_user = AccountUser.find_by(user: user, account: account)
account_user.update(role: :agent, custom_role: custom_role)
end
it 'returns true' do
expect(service.active?).to be true
end
end
context 'when user has custom role with conversation_participating_manage permission' do
let(:user) { create(:user, account: account) }
let(:assistant) { create(:captain_assistant, account: account) }
let(:custom_role) { create(:custom_role, account: account, permissions: ['conversation_participating_manage']) }
before do
account_user = AccountUser.find_by(user: user, account: account)
account_user.update(role: :agent, custom_role: custom_role)
end
it 'returns true' do
expect(service.active?).to be true
end
end
context 'when user has custom role without any conversation permissions' do
let(:user) { create(:user, account: account) }
let(:assistant) { create(:captain_assistant, account: account) }
let(:custom_role) { create(:custom_role, account: account, permissions: []) }
before do
account_user = AccountUser.find_by(user: user, account: account)
account_user.update(role: :agent, custom_role: custom_role)
end
it 'returns false' do
expect(service.active?).to be false
end
end
end
describe '#execute' do
context 'when conversation_id is blank' do
it 'returns error message' do
expect(service.execute({})).to eq('Missing required parameters')
end
end
context 'when conversation is not found' do
it 'returns not found message' do
expect(service.execute({ 'conversation_id' => 999 })).to eq('Conversation not found')
end
end
context 'when conversation exists' do
let(:inbox) { create(:inbox, account: account) }
let(:conversation) { create(:conversation, account: account, inbox: inbox) }
it 'returns the conversation in llm text format' do
result = service.execute({ 'conversation_id' => conversation.display_id })
expect(result).to eq(conversation.to_llm_text)
end
context 'when conversation belongs to different account' do
let(:other_account) { create(:account) }
let(:other_inbox) { create(:inbox, account: other_account) }
let(:other_conversation) { create(:conversation, account: other_account, inbox: other_inbox) }
it 'returns not found message' do
expect(service.execute({ 'conversation_id' => other_conversation.display_id })).to eq('Conversation not found')
end
end
end
end
end

View File

@@ -0,0 +1,167 @@
require 'rails_helper'
RSpec.describe Captain::Tools::Copilot::SearchArticlesService do
let(:account) { create(:account) }
let(:user) { create(:user, account: account) }
let(:assistant) { create(:captain_assistant, account: account) }
let(:service) { described_class.new(assistant, user: user) }
describe '#name' do
it 'returns the correct service name' do
expect(service.name).to eq('search_articles')
end
end
describe '#description' do
it 'returns the service description' do
expect(service.description).to eq('Search articles based on parameters')
end
end
describe '#parameters' do
it 'returns the expected parameter schema' do
expect(service.parameters).to eq(
{
type: 'object',
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'
}
},
required: ['query']
}
)
end
end
describe '#active?' do
context 'when user is an admin' do
let(:user) { create(:user, :administrator, account: account) }
let(:assistant) { create(:captain_assistant, account: account) }
it 'returns true' do
expect(service.active?).to be true
end
end
context 'when user is an agent' do
let(:user) { create(:user, account: account) }
let(:assistant) { create(:captain_assistant, account: account) }
it 'returns true' do
expect(service.active?).to be true
end
end
context 'when user has custom role with knowledge_base_manage permission' do
let(:user) { create(:user, account: account) }
let(:assistant) { create(:captain_assistant, account: account) }
let(:custom_role) { create(:custom_role, account: account, permissions: ['knowledge_base_manage']) }
before do
account_user = AccountUser.find_by(user: user, account: account)
account_user.update(role: :agent, custom_role: custom_role)
end
it 'returns true' do
expect(service.active?).to be true
end
end
context 'when user has custom role without knowledge_base_manage permission' do
let(:user) { create(:user, account: account) }
let(:assistant) { create(:captain_assistant, account: account) }
let(:custom_role) { create(:custom_role, account: account, permissions: []) }
before do
account_user = AccountUser.find_by(user: user, account: account)
account_user.update(role: :agent, custom_role: custom_role)
end
it 'returns false' do
expect(service.active?).to be false
end
end
end
describe '#execute' do
context 'when query is blank' do
it 'returns error message' do
expect(service.execute({})).to eq('Missing required parameters')
end
end
context 'when no articles are found' do
before do
allow(Article).to receive(:where).and_return(Article.none)
end
it 'returns no articles found message' do
expect(service.execute({ 'query' => 'test' })).to eq('No articles found')
end
end
context 'when articles are found' do
let(:portal) { create(:portal, account: account) }
let(:article1) { create(:article, account: account, portal: portal, author: user, title: 'Test Article 1', content: 'Content 1') }
let(:article2) { create(:article, account: account, portal: portal, author: user, title: 'Test Article 2', content: 'Content 2') }
before do
article1
article2
end
it 'returns formatted articles with count' do
result = service.execute({ 'query' => 'Test' })
expect(result).to include('Total number of articles: 2')
expect(result).to include(article1.to_llm_text)
expect(result).to include(article2.to_llm_text)
end
context 'when filtered by category' do
let(:category) { create(:category, slug: 'test-category', portal: portal, account: account) }
let(:article3) { create(:article, account: account, portal: portal, author: user, category: category, title: 'Test Article 3') }
before do
article3
end
it 'returns only articles from the specified category' do
result = service.execute({ 'query' => 'Test', 'category_id' => category.id })
expect(result).to include('Total number of articles: 1')
expect(result).to include(article3.to_llm_text)
expect(result).not_to include(article1.to_llm_text)
expect(result).not_to include(article2.to_llm_text)
end
end
context 'when filtered by status' do
let(:article3) do
create(:article, account: account, portal: portal, author: user, title: 'Test Article 3', status: 'published')
end
let(:article4) { create(:article, account: account, portal: portal, author: user, title: 'Test Article 4', status: 'draft') }
before do
article3
article4
end
it 'returns only articles with the specified status' do
result = service.execute({ 'query' => 'Test', 'status' => 'published' })
expect(result).to include(article3.to_llm_text)
expect(result).not_to include(article4.to_llm_text)
end
end
end
end
end

View File

@@ -0,0 +1,113 @@
require 'rails_helper'
RSpec.describe Captain::Tools::Copilot::SearchContactsService do
let(:account) { create(:account) }
let(:user) { create(:user, account: account) }
let(:assistant) { create(:captain_assistant, account: account) }
let(:service) { described_class.new(assistant, user: user) }
describe '#name' do
it 'returns the correct service name' do
expect(service.name).to eq('search_contacts')
end
end
describe '#description' do
it 'returns the service description' do
expect(service.description).to eq('Search contacts based on query parameters')
end
end
describe '#parameters' do
it 'returns the expected parameter schema' do
expect(service.parameters).to eq(
{
type: 'object',
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)'
}
},
required: []
}
)
end
end
describe '#active?' do
context 'when user has contact_manage permission' do
let(:user) { create(:user, account: account) }
let(:assistant) { create(:captain_assistant, account: account) }
let(:custom_role) { create(:custom_role, account: account, permissions: ['contact_manage']) }
before do
account_user = AccountUser.find_by(user: user, account: account)
account_user.update(role: :agent, custom_role: custom_role)
end
it 'returns true' do
expect(service.active?).to be true
end
end
context 'when user does not have contact_manage permission' do
let(:user) { create(:user, account: account) }
let(:assistant) { create(:captain_assistant, account: account) }
let(:custom_role) { create(:custom_role, account: account, permissions: []) }
before do
account_user = AccountUser.find_by(user: user, account: account)
account_user.update(role: :agent, custom_role: custom_role)
end
it 'returns false' do
expect(service.active?).to be false
end
end
end
describe '#execute' do
context 'when contacts are found' do
let(:contact1) { create(:contact, account: account, email: 'test1@example.com', name: 'Test Contact 1', phone_number: '+1234567890') }
let(:contact2) { create(:contact, account: account, email: 'test2@example.com', name: 'Test Contact 2', phone_number: '+1234567891') }
before do
contact1
contact2
end
it 'returns contacts when filtered by email' do
result = service.execute({ 'email' => 'test1@example.com' })
expect(result).to include(contact1.to_llm_text)
expect(result).not_to include(contact2.to_llm_text)
end
it 'returns contacts when filtered by phone number' do
result = service.execute({ 'phone_number' => '+1234567890' })
expect(result).to include(contact1.to_llm_text)
expect(result).not_to include(contact2.to_llm_text)
end
it 'returns contacts when filtered by name' do
result = service.execute({ 'name' => 'Contact 1' })
expect(result).to include(contact1.to_llm_text)
expect(result).not_to include(contact2.to_llm_text)
end
it 'returns all matching contacts when no filters are provided' do
result = service.execute({})
expect(result).to include(contact1.to_llm_text)
expect(result).to include(contact2.to_llm_text)
end
end
end
end

View File

@@ -26,6 +26,64 @@ RSpec.describe Captain::Tools::Copilot::SearchConversationsService do
end
end
describe '#active?' do
context 'when user has conversation_manage permission' do
let(:custom_role) { create(:custom_role, account: account, permissions: ['conversation_manage']) }
let(:user) { create(:user, account: account) }
before do
account_user = AccountUser.find_by(user: user, account: account)
account_user.update(role: :agent, custom_role: custom_role)
end
it 'returns true' do
expect(service.active?).to be true
end
end
context 'when user has conversation_unassigned_manage permission' do
let(:custom_role) { create(:custom_role, account: account, permissions: ['conversation_unassigned_manage']) }
let(:user) { create(:user, account: account) }
before do
account_user = AccountUser.find_by(user: user, account: account)
account_user.update(role: :agent, custom_role: custom_role)
end
it 'returns true' do
expect(service.active?).to be true
end
end
context 'when user has conversation_participating_manage permission' do
let(:custom_role) { create(:custom_role, account: account, permissions: ['conversation_participating_manage']) }
let(:user) { create(:user, account: account) }
before do
account_user = AccountUser.find_by(user: user, account: account)
account_user.update(role: :agent, custom_role: custom_role)
end
it 'returns true' do
expect(service.active?).to be true
end
end
context 'when user has no relevant conversation permissions' do
let(:custom_role) { create(:custom_role, account: account, permissions: []) }
let(:user) { create(:user, account: account) }
before do
account_user = AccountUser.find_by(user: user, account: account)
account_user.update(role: :agent, custom_role: custom_role)
end
it 'returns false' do
expect(service.active?).to be false
end
end
end
describe '#execute' do
let(:contact) { create(:contact, account: account) }
let!(:open_conversation) { create(:conversation, account: account, contact: contact, status: 'open', priority: 'high') }