diff --git a/config/agents/tools.yml b/config/agents/tools.yml new file mode 100644 index 000000000..4d1823b33 --- /dev/null +++ b/config/agents/tools.yml @@ -0,0 +1,21 @@ +###### Captain Agent Tools Configuration ####### +# id: Tool identifier used to resolve the class (Captain::Tools::{PascalCase(id)}Tool) +# title: Human-readable tool name for frontend display +# description: Brief description of what the tool does +# icon: Icon name for frontend display (frontend icon system) +####################################################### + +- id: add_contact_note + title: "Add Contact Note" + description: "Add a note to a contact profile" + icon: "note-add" + +- id: add_private_note + title: "Add Private Note" + description: "Add a private note to a conversation (internal only)" + icon: "eye-off" + +- id: update_priority + title: "Update Priority" + description: "Update conversation priority level" + icon: "exclamation-triangle" \ No newline at end of file diff --git a/enterprise/lib/captain/tools/add_contact_note_tool.rb b/enterprise/lib/captain/tools/add_contact_note_tool.rb new file mode 100644 index 000000000..d39b53c8a --- /dev/null +++ b/enterprise/lib/captain/tools/add_contact_note_tool.rb @@ -0,0 +1,39 @@ +class Captain::Tools::AddContactNoteTool < Captain::Tools::BaseAgentTool + description 'Add a note to a contact profile' + param :contact_id, type: 'string', desc: 'The ID of the contact (optional if conversation_id is provided)' + param :conversation_id, type: 'string', desc: 'The display ID of the conversation to find the contact (optional if contact_id is provided)' + param :note, type: 'string', desc: 'The note content to add to the contact' + + def perform(_tool_context, note:, contact_id: nil, conversation_id: nil) + log_tool_usage('add_contact_note', { contact_id: contact_id, conversation_id: conversation_id, note_length: note.length }) + + return 'Missing required parameters: need either contact_id or conversation_id, and note' if note.blank? + return 'Must provide either contact_id or conversation_id' if contact_id.blank? && conversation_id.blank? + + contact = nil + + if contact_id.present? + contact = account_scoped(::Contact).find_by(id: contact_id) + return 'Contact not found' if contact.nil? + elsif conversation_id.present? + conversation = account_scoped(::Conversation).find_by(display_id: conversation_id) + return 'Conversation not found' if conversation.nil? + + contact = conversation.contact + end + + # Create contact note + contact.notes.create!( + account: @assistant.account, + contact: contact, + content: note, + user: @user + ) + + "Note added successfully to contact #{contact.name} (ID: #{contact.id})" + end + + def active? + user_has_permission('contact_manage') + end +end \ No newline at end of file diff --git a/enterprise/lib/captain/tools/add_private_note_tool.rb b/enterprise/lib/captain/tools/add_private_note_tool.rb new file mode 100644 index 000000000..80b7bdbfe --- /dev/null +++ b/enterprise/lib/captain/tools/add_private_note_tool.rb @@ -0,0 +1,32 @@ +class Captain::Tools::AddPrivateNoteTool < Captain::Tools::BaseAgentTool + description 'Add a private note to a conversation' + param :conversation_id, type: 'string', desc: 'The display ID of the conversation' + param :note, type: 'string', desc: 'The private note content' + + def perform(_tool_context, conversation_id:, note:) + log_tool_usage('add_private_note', { conversation_id: conversation_id, note_length: note.length }) + + return 'Missing required parameters' if conversation_id.blank? || note.blank? + + conversation = account_scoped(::Conversation).find_by(display_id: conversation_id) + return 'Conversation not found' if conversation.nil? + + # Create private note message + conversation.messages.create!( + account: @assistant.account, + inbox: conversation.inbox, + sender: @user, + content: note, + message_type: 'activity', + private: true + ) + + "Private note added successfully to conversation #{conversation_id}" + 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/lib/captain/tools/base_agent_tool.rb b/enterprise/lib/captain/tools/base_agent_tool.rb new file mode 100644 index 000000000..4b12e6a64 --- /dev/null +++ b/enterprise/lib/captain/tools/base_agent_tool.rb @@ -0,0 +1,44 @@ +require 'agents' + +class Captain::Tools::BaseAgentTool < Agents::Tool + def initialize(assistant, user: nil) + @assistant = assistant + @user = user + super() + end + + def active? + user_has_permission(required_permission) + end + + protected + + def required_permission + # Override in subclasses to specify the required permission + 'agent' + 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 + + def account_scoped(model_class) + model_class.where(account_id: @assistant.account_id) + end + + def log_tool_usage(action, details = {}) + Rails.logger.info do + "#{self.class.name}: #{action} by user #{@user&.id} for assistant #{@assistant&.id} - #{details.inspect}" + end + end +end diff --git a/enterprise/lib/captain/tools/update_priority_tool.rb b/enterprise/lib/captain/tools/update_priority_tool.rb new file mode 100644 index 000000000..86f280163 --- /dev/null +++ b/enterprise/lib/captain/tools/update_priority_tool.rb @@ -0,0 +1,32 @@ +class Captain::Tools::UpdatePriorityTool < Captain::Tools::BaseAgentTool + description 'Update the priority of a conversation' + param :conversation_id, type: 'string', desc: 'The display ID of the conversation' + param :priority, type: 'string', desc: 'The priority level: nil, low, medium, high, urgent' + + def perform(_tool_context, conversation_id:, priority:) + log_tool_usage('update_priority', { conversation_id: conversation_id, priority: priority }) + + return 'Missing required parameters' if conversation_id.blank? + + conversation = account_scoped(::Conversation).find_by(display_id: conversation_id) + return 'Conversation not found' if conversation.nil? + + # Validate priority value + valid_priorities = [nil, 'low', 'medium', 'high', 'urgent'] + priority = nil if priority == 'nil' || priority.blank? + + return "Invalid priority. Valid options: #{valid_priorities.join(', ')}" unless valid_priorities.include?(priority) + + # Update the priority + conversation.update!(priority: priority) + + priority_text = priority || 'none' + "Priority updated to '#{priority_text}' for conversation #{conversation_id}" + end + + def active? + user_has_permission('conversation_manage') || + user_has_permission('conversation_unassigned_manage') || + user_has_permission('conversation_participating_manage') + end +end \ No newline at end of file