diff --git a/enterprise/app/services/captain/tool_registry_service.rb b/enterprise/app/services/captain/tool_registry_service.rb index 4841a29e4..8d6632e57 100644 --- a/enterprise/app/services/captain/tool_registry_service.rb +++ b/enterprise/app/services/captain/tool_registry_service.rb @@ -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 diff --git a/enterprise/app/services/captain/tools/base_service.rb b/enterprise/app/services/captain/tools/base_service.rb index 9143726b7..72fde2df8 100644 --- a/enterprise/app/services/captain/tools/base_service.rb +++ b/enterprise/app/services/captain/tools/base_service.rb @@ -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 diff --git a/enterprise/app/services/captain/tools/copilot/get_article_service.rb b/enterprise/app/services/captain/tools/copilot/get_article_service.rb new file mode 100644 index 000000000..9c2ee02da --- /dev/null +++ b/enterprise/app/services/captain/tools/copilot/get_article_service.rb @@ -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 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..290433301 --- /dev/null +++ b/enterprise/app/services/captain/tools/copilot/get_contact_service.rb @@ -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 diff --git a/enterprise/app/services/captain/tools/copilot/get_conversation_service.rb b/enterprise/app/services/captain/tools/copilot/get_conversation_service.rb new file mode 100644 index 000000000..64b52d012 --- /dev/null +++ b/enterprise/app/services/captain/tools/copilot/get_conversation_service.rb @@ -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 diff --git a/enterprise/app/services/captain/tools/copilot/search_articles_service.rb b/enterprise/app/services/captain/tools/copilot/search_articles_service.rb new file mode 100644 index 000000000..5061968ad --- /dev/null +++ b/enterprise/app/services/captain/tools/copilot/search_articles_service.rb @@ -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 diff --git a/enterprise/app/services/captain/tools/copilot/search_contacts_service.rb b/enterprise/app/services/captain/tools/copilot/search_contacts_service.rb new file mode 100644 index 000000000..557fc731a --- /dev/null +++ b/enterprise/app/services/captain/tools/copilot/search_contacts_service.rb @@ -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 diff --git a/enterprise/app/services/captain/tools/copilot/search_conversations_service.rb b/enterprise/app/services/captain/tools/copilot/search_conversations_service.rb index 6b6b063ea..f97604793 100644 --- a/enterprise/app/services/captain/tools/copilot/search_conversations_service.rb +++ b/enterprise/app/services/captain/tools/copilot/search_conversations_service.rb @@ -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) diff --git a/spec/enterprise/services/captain/tool_registry_service_spec.rb b/spec/enterprise/services/captain/tool_registry_service_spec.rb index beb4a6a63..c8d97fe3a 100644 --- a/spec/enterprise/services/captain/tool_registry_service_spec.rb +++ b/spec/enterprise/services/captain/tool_registry_service_spec.rb @@ -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 diff --git a/spec/enterprise/services/captain/tools/copilot/get_article_service_spec.rb b/spec/enterprise/services/captain/tools/copilot/get_article_service_spec.rb new file mode 100644 index 000000000..72f4e1cb4 --- /dev/null +++ b/spec/enterprise/services/captain/tools/copilot/get_article_service_spec.rb @@ -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 diff --git a/spec/enterprise/services/captain/tools/copilot/get_contact_service_spec.rb b/spec/enterprise/services/captain/tools/copilot/get_contact_service_spec.rb new file mode 100644 index 000000000..de319bfa1 --- /dev/null +++ b/spec/enterprise/services/captain/tools/copilot/get_contact_service_spec.rb @@ -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 diff --git a/spec/enterprise/services/captain/tools/copilot/get_conversation_service_spec.rb b/spec/enterprise/services/captain/tools/copilot/get_conversation_service_spec.rb new file mode 100644 index 000000000..4d7f1adc7 --- /dev/null +++ b/spec/enterprise/services/captain/tools/copilot/get_conversation_service_spec.rb @@ -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 diff --git a/spec/enterprise/services/captain/tools/copilot/search_articles_service_spec.rb b/spec/enterprise/services/captain/tools/copilot/search_articles_service_spec.rb new file mode 100644 index 000000000..e4504d7bd --- /dev/null +++ b/spec/enterprise/services/captain/tools/copilot/search_articles_service_spec.rb @@ -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 diff --git a/spec/enterprise/services/captain/tools/copilot/search_contacts_service_spec.rb b/spec/enterprise/services/captain/tools/copilot/search_contacts_service_spec.rb new file mode 100644 index 000000000..f54b2eddf --- /dev/null +++ b/spec/enterprise/services/captain/tools/copilot/search_contacts_service_spec.rb @@ -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 diff --git a/spec/enterprise/services/captain/tools/copilot/search_conversations_service_spec.rb b/spec/enterprise/services/captain/tools/copilot/search_conversations_service_spec.rb index e60ad6f28..ab0865bc2 100644 --- a/spec/enterprise/services/captain/tools/copilot/search_conversations_service_spec.rb +++ b/spec/enterprise/services/captain/tools/copilot/search_conversations_service_spec.rb @@ -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') }