Restructure omni services and add Chatwoot research snapshot

This commit is contained in:
Ruslan Bakiev
2026-02-21 11:11:27 +07:00
parent edea7a0034
commit b73babbbf6
7732 changed files with 978203 additions and 32 deletions

View File

@@ -0,0 +1,101 @@
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 'defines article_id parameter' do
expect(service.parameters.keys).to contain_exactly(: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(article_id: nil)).to eq('Article not found')
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,99 @@
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 'defines contact_id parameter' do
expect(service.parameters.keys).to contain_exactly(: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 not found message' do
expect(service.execute(contact_id: nil)).to eq('Contact not found')
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,148 @@
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 'defines conversation_id parameter' do
expect(service.parameters.keys).to contain_exactly(: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 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
it 'includes private messages in the llm text format' do
# Create a regular message
create(:message,
conversation: conversation,
message_type: 'outgoing',
content: 'Regular message',
private: false)
# Create a private message
create(:message,
conversation: conversation,
message_type: 'outgoing',
content: 'Private note content',
private: true)
result = service.execute(conversation_id: conversation.display_id)
# Verify that the result includes both regular and private messages
expect(result).to include('Regular message')
expect(result).to include('Private note content')
expect(result).to include('[Private Note]')
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,115 @@
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 '#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 no articles are found' do
it 'returns no articles found message' do
expect(service.execute(query: 'test', category_id: nil, status: nil)).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') }
it 'returns formatted articles with count' do
result = service.execute(query: 'Test', category_id: nil, status: nil)
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') }
it 'returns only articles from the specified category' do
result = service.execute(query: 'Test', category_id: category.id, status: nil)
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) { create(:article, account: account, portal: portal, author: user, title: 'Test Article 3', status: 'published') }
let!(:article4) { create(:article, account: account, portal: portal, author: user, title: 'Test Article 4', status: 'draft') }
it 'returns only articles with the specified status' do
result = service.execute(query: 'Test', category_id: nil, 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,94 @@
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 'defines email, phone_number, and name parameters' do
expect(service.parameters.keys).to contain_exactly(:email, :phone_number, :name)
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

@@ -0,0 +1,160 @@
require 'rails_helper'
RSpec.describe Captain::Tools::Copilot::SearchConversationsService do
let(:account) { create(:account) }
let(:user) { create(:user, role: 'administrator', 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_conversation')
end
end
describe '#description' do
it 'returns the service description' do
expect(service.description).to eq('Search conversations based on parameters')
end
end
describe '#parameters' do
it 'defines the expected parameters' do
expect(service.parameters.keys).to contain_exactly(:status, :contact_id, :priority, :labels)
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') }
let!(:resolved_conversation) { create(:conversation, account: account, status: 'resolved', priority: 'low') }
it 'returns all conversations when no filters are applied' do
result = service.execute
expect(result).to include('Total number of conversations: 2')
expect(result).to include(open_conversation.to_llm_text(include_contact_details: true))
expect(result).to include(resolved_conversation.to_llm_text(include_contact_details: true))
end
it 'filters conversations by status' do
result = service.execute(status: 'open')
expect(result).to include('Total number of conversations: 1')
expect(result).to include(open_conversation.to_llm_text(include_contact_details: true))
expect(result).not_to include(resolved_conversation.to_llm_text(include_contact_details: true))
end
it 'filters conversations by contact_id' do
result = service.execute(contact_id: contact.id)
expect(result).to include('Total number of conversations: 1')
expect(result).to include(open_conversation.to_llm_text(include_contact_details: true))
expect(result).not_to include(resolved_conversation.to_llm_text(include_contact_details: true))
end
it 'filters conversations by priority' do
result = service.execute(priority: 'high')
expect(result).to include('Total number of conversations: 1')
expect(result).to include(open_conversation.to_llm_text(include_contact_details: true))
expect(result).not_to include(resolved_conversation.to_llm_text(include_contact_details: true))
end
it 'returns appropriate message when no conversations are found' do
result = service.execute(status: 'snoozed')
expect(result).to eq('No conversations found')
end
context 'when invalid status is provided' do
it 'ignores invalid status and returns all conversations' do
result = service.execute(status: 'all')
expect(result).to include('Total number of conversations: 2')
expect(result).to include(open_conversation.to_llm_text(include_contact_details: true))
expect(result).to include(resolved_conversation.to_llm_text(include_contact_details: true))
end
it 'ignores random invalid status values' do
result = service.execute(status: 'invalid_status')
expect(result).to include('Total number of conversations: 2')
end
end
context 'when invalid priority is provided' do
it 'ignores invalid priority and returns all conversations' do
result = service.execute(priority: 'all')
expect(result).to include('Total number of conversations: 2')
expect(result).to include(open_conversation.to_llm_text(include_contact_details: true))
expect(result).to include(resolved_conversation.to_llm_text(include_contact_details: true))
end
it 'ignores random invalid priority values' do
result = service.execute(priority: 'invalid_priority')
expect(result).to include('Total number of conversations: 2')
end
end
context 'when combining valid and invalid parameters' do
it 'applies valid filters and ignores invalid ones' do
result = service.execute(status: 'all', contact_id: contact.id)
expect(result).to include('Total number of conversations: 1')
expect(result).to include(open_conversation.to_llm_text(include_contact_details: true))
expect(result).not_to include(resolved_conversation.to_llm_text(include_contact_details: true))
end
end
end
end

View File

@@ -0,0 +1,139 @@
require 'rails_helper'
RSpec.describe Captain::Tools::Copilot::SearchLinearIssuesService do
let(:account) { create(:account) }
let(:assistant) { create(:captain_assistant, account: account) }
let(:user) { create(:user, 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_linear_issues')
end
end
describe '#description' do
it 'returns the service description' do
expect(service.description).to eq('Search Linear issues based on a search term')
end
end
describe '#parameters' do
it 'defines term parameter' do
expect(service.parameters.keys).to contain_exactly(:term)
end
end
describe '#active?' do
context 'when Linear integration is enabled' do
before do
create(:integrations_hook, :linear, account: account)
end
context 'when user is present' do
it 'returns true' do
expect(service.active?).to be true
end
end
context 'when user is not present' do
let(:service) { described_class.new(assistant) }
it 'returns false' do
expect(service.active?).to be false
end
end
end
context 'when Linear integration is not enabled' do
context 'when user is present' do
it 'returns false' do
expect(service.active?).to be false
end
end
context 'when user is not present' do
let(:service) { described_class.new(assistant) }
it 'returns false' do
expect(service.active?).to be false
end
end
end
end
describe '#execute' do
context 'when Linear integration is not enabled' do
it 'returns error message' do
expect(service.execute(term: 'test')).to eq('Linear integration is not enabled')
end
end
context 'when Linear integration is enabled' do
let(:linear_service) { instance_double(Integrations::Linear::ProcessorService) }
before do
create(:integrations_hook, :linear, account: account)
allow(Integrations::Linear::ProcessorService).to receive(:new).and_return(linear_service)
end
context 'when term is blank' do
before do
allow(linear_service).to receive(:search_issue).with('').and_return({ data: [] })
end
it 'returns no issues found message' do
expect(service.execute(term: '')).to eq('No issues found, I should try another similar search term')
end
end
context 'when search returns error' do
before do
allow(linear_service).to receive(:search_issue).and_return({ error: 'API Error' })
end
it 'returns the error message' do
expect(service.execute(term: 'test')).to eq('API Error')
end
end
context 'when search returns no issues' do
before do
allow(linear_service).to receive(:search_issue).and_return({ data: [] })
end
it 'returns no issues found message' do
expect(service.execute(term: 'test')).to eq('No issues found, I should try another similar search term')
end
end
context 'when search returns issues' do
let(:issues) do
[{
'title' => 'Test Issue',
'id' => 'TEST-123',
'state' => { 'name' => 'In Progress' },
'priority' => 4,
'assignee' => { 'name' => 'John Doe' },
'description' => 'Test description'
}]
end
before do
allow(linear_service).to receive(:search_issue).and_return({ data: issues })
end
it 'returns formatted issues' do
result = service.execute(term: 'test')
expect(result).to include('Total number of issues: 1')
expect(result).to include('Title: Test Issue')
expect(result).to include('ID: TEST-123')
expect(result).to include('State: In Progress')
expect(result).to include('Priority: Low')
expect(result).to include('Assignee: John Doe')
expect(result).to include('Description: Test description')
end
end
end
end
end