feat: Add support for search_conversations in copilot (#11520)

Earlier, we were manually checking if a user was an agent and filtering
their conversations based on inboxes. This logic should have been part
of the conversation permissions service.

This PR moves the check to the right place and updates the logic
accordingly.

Other updates:
- Add support for search_conversations service for copilot.
- Use PermissionFilterService in contacts/conversations, conversations,
copilot search_conversations.

---------

Co-authored-by: Sojan <sojan@pepalo.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
Pranav
2025-05-20 19:22:17 -07:00
committed by GitHub
parent af650af489
commit a07f2a7c1b
9 changed files with 193 additions and 37 deletions

View File

@@ -1,8 +1,9 @@
class Captain::Tools::BaseService
attr_accessor :assistant
def initialize(assistant)
def initialize(assistant, user: nil)
@assistant = assistant
@user = user
end
def name

View File

@@ -0,0 +1,72 @@
class Captain::Tools::Copilot::SearchConversationsService < Captain::Tools::BaseService
def name
'search_conversations'
end
def description
'Search conversations based on parameters'
end
def parameters
{
type: 'object',
properties: properties,
required: []
}
end
def execute(arguments)
status = arguments['status']
contact_id = arguments['contact_id']
priority = arguments['priority']
conversations = get_conversations(status, contact_id, priority)
return 'No conversations found' unless conversations.exists?
total_count = conversations.count
conversations = conversations.limit(100)
<<~RESPONSE
#{total_count > 100 ? "Found #{total_count} conversations (showing first 100)" : "Total number of conversations: #{total_count}"}
#{conversations.map { |conversation| conversation.to_llm_text(include_contact_details: true) }.join("\n---\n")}
RESPONSE
end
private
def get_conversations(status, contact_id, priority)
conversations = permissible_conversations
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?
conversations
end
def permissible_conversations
Conversations::PermissionFilterService.new(
@assistant.account.conversations,
@user,
@assistant.account
).perform
end
def properties
{
contact_id: {
type: 'number',
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'
}
}
end
end

View File

@@ -1,36 +1,37 @@
module Enterprise::Conversations::PermissionFilterService
def perform
account_user = AccountUser.find_by(account_id: account.id, user_id: user.id)
permissions = account_user&.permissions || []
user_role = account_user&.role
return filter_by_permissions(permissions) if user_has_custom_role?
# Skip filtering for administrators
return conversations if user_role == 'administrator'
# Skip filtering for regular agents (without custom roles/permissions)
return conversations if user_role == 'agent' && account_user&.custom_role_id.nil?
filter_by_permissions(permissions)
super
end
private
def user_has_custom_role?
user_role == 'agent' && account_user&.custom_role_id.present?
end
def permissions
account_user&.permissions || []
end
def filter_by_permissions(permissions)
# Permission-based filtering with hierarchy
# conversation_manage > conversation_unassigned_manage > conversation_participating_manage
if permissions.include?('conversation_manage')
conversations
accessible_conversations
elsif permissions.include?('conversation_unassigned_manage')
filter_unassigned_and_mine
elsif permissions.include?('conversation_participating_manage')
conversations.assigned_to(user)
accessible_conversations.assigned_to(user)
else
Conversation.none
end
end
def filter_unassigned_and_mine
mine = conversations.assigned_to(user)
unassigned = conversations.unassigned
mine = accessible_conversations.assigned_to(user)
unassigned = accessible_conversations.unassigned
Conversation.from("(#{mine.to_sql} UNION #{unassigned.to_sql}) as conversations")
.where(account_id: account.id)