diff --git a/config/features.yml b/config/features.yml index 2b5b47924..e1f3b20d9 100644 --- a/config/features.yml +++ b/config/features.yml @@ -223,6 +223,3 @@ - name: quoted_email_reply display_name: Quoted Email Reply enabled: false -- name: captain_custom_tools - display_name: Captain Custom Tools - enabled: false diff --git a/enterprise/app/models/captain/assistant.rb b/enterprise/app/models/captain/assistant.rb index 3ee5f617c..21ecf05c4 100644 --- a/enterprise/app/models/captain/assistant.rb +++ b/enterprise/app/models/captain/assistant.rb @@ -53,10 +53,8 @@ class Captain::Assistant < ApplicationRecord def available_agent_tools tools = self.class.built_in_agent_tools.dup - if account.feature_enabled?(:captain_custom_tools) - custom_tools = Captain::CustomTool.where(account_id: account_id, enabled: true).map(&:to_tool_metadata) - tools.concat(custom_tools) - end + custom_tools = account.captain_custom_tools.enabled.map(&:to_tool_metadata) + tools.concat(custom_tools) tools end diff --git a/enterprise/app/models/enterprise/concerns/account.rb b/enterprise/app/models/enterprise/concerns/account.rb index 091b5697a..82b65c025 100644 --- a/enterprise/app/models/enterprise/concerns/account.rb +++ b/enterprise/app/models/enterprise/concerns/account.rb @@ -11,6 +11,7 @@ module Enterprise::Concerns::Account has_many :captain_assistant_responses, dependent: :destroy_async, class_name: 'Captain::AssistantResponse' has_many :captain_custom_tools, dependent: :destroy_async, class_name: 'Captain::CustomTool' has_many :captain_documents, dependent: :destroy_async, class_name: 'Captain::Document' + has_many :captain_custom_tools, dependent: :destroy_async, class_name: 'Captain::CustomTool' has_many :copilot_threads, dependent: :destroy_async has_many :voice_channels, dependent: :destroy_async, class_name: '::Channel::Voice' diff --git a/spec/enterprise/lib/captain/tools/http_tool_spec.rb b/spec/enterprise/lib/captain/tools/http_tool_spec.rb index 4e53e0ae5..d48af2752 100644 --- a/spec/enterprise/lib/captain/tools/http_tool_spec.rb +++ b/spec/enterprise/lib/captain/tools/http_tool_spec.rb @@ -7,14 +7,6 @@ RSpec.describe Captain::Tools::HttpTool, type: :model do let(:tool) { described_class.new(assistant, custom_tool) } let(:tool_context) { Struct.new(:state).new({}) } - describe '#description' do - it 'returns the custom tool description' do - custom_tool.update!(description: 'Fetches order status from external API') - - expect(tool.description).to eq('Fetches order status from external API') - end - end - describe '#active?' do it 'returns true when custom tool is enabled' do custom_tool.update!(enabled: true) @@ -34,10 +26,10 @@ RSpec.describe Captain::Tools::HttpTool, type: :model do before do custom_tool.update!( http_method: 'GET', - endpoint_url: 'https://api.example.com/orders/123', + endpoint_url: 'https://example.com/orders/123', response_template: nil ) - stub_request(:get, 'https://api.example.com/orders/123') + stub_request(:get, 'https://example.com/orders/123') .to_return(status: 200, body: '{"status": "success"}') end @@ -45,7 +37,7 @@ RSpec.describe Captain::Tools::HttpTool, type: :model do result = tool.perform(tool_context) expect(result).to eq('{"status": "success"}') - expect(WebMock).to have_requested(:get, 'https://api.example.com/orders/123') + expect(WebMock).to have_requested(:get, 'https://example.com/orders/123') end end @@ -53,11 +45,11 @@ RSpec.describe Captain::Tools::HttpTool, type: :model do before do custom_tool.update!( http_method: 'POST', - endpoint_url: 'https://api.example.com/orders', + endpoint_url: 'https://example.com/orders', request_template: '{"order_id": "{{ order_id }}"}', response_template: nil ) - stub_request(:post, 'https://api.example.com/orders') + stub_request(:post, 'https://example.com/orders') .with(body: '{"order_id": "123"}', headers: { 'Content-Type' => 'application/json' }) .to_return(status: 200, body: '{"created": true}') end @@ -66,7 +58,7 @@ RSpec.describe Captain::Tools::HttpTool, type: :model do result = tool.perform(tool_context, order_id: '123') expect(result).to eq('{"created": true}') - expect(WebMock).to have_requested(:post, 'https://api.example.com/orders') + expect(WebMock).to have_requested(:post, 'https://example.com/orders') .with(body: '{"order_id": "123"}') end end @@ -74,10 +66,10 @@ RSpec.describe Captain::Tools::HttpTool, type: :model do context 'with template variables in URL' do before do custom_tool.update!( - endpoint_url: 'https://api.example.com/orders/{{ order_id }}', + endpoint_url: 'https://example.com/orders/{{ order_id }}', response_template: nil ) - stub_request(:get, 'https://api.example.com/orders/456') + stub_request(:get, 'https://example.com/orders/456') .to_return(status: 200, body: '{"order_id": "456"}') end @@ -85,7 +77,7 @@ RSpec.describe Captain::Tools::HttpTool, type: :model do result = tool.perform(tool_context, order_id: '456') expect(result).to eq('{"order_id": "456"}') - expect(WebMock).to have_requested(:get, 'https://api.example.com/orders/456') + expect(WebMock).to have_requested(:get, 'https://example.com/orders/456') end end @@ -94,10 +86,10 @@ RSpec.describe Captain::Tools::HttpTool, type: :model do custom_tool.update!( auth_type: 'bearer', auth_config: { 'token' => 'secret_bearer_token' }, - endpoint_url: 'https://api.example.com/data', + endpoint_url: 'https://example.com/data', response_template: nil ) - stub_request(:get, 'https://api.example.com/data') + stub_request(:get, 'https://example.com/data') .with(headers: { 'Authorization' => 'Bearer secret_bearer_token' }) .to_return(status: 200, body: '{"authenticated": true}') end @@ -106,7 +98,7 @@ RSpec.describe Captain::Tools::HttpTool, type: :model do result = tool.perform(tool_context) expect(result).to eq('{"authenticated": true}') - expect(WebMock).to have_requested(:get, 'https://api.example.com/data') + expect(WebMock).to have_requested(:get, 'https://example.com/data') .with(headers: { 'Authorization' => 'Bearer secret_bearer_token' }) end end @@ -116,10 +108,10 @@ RSpec.describe Captain::Tools::HttpTool, type: :model do custom_tool.update!( auth_type: 'basic', auth_config: { 'username' => 'user123', 'password' => 'pass456' }, - endpoint_url: 'https://api.example.com/data', + endpoint_url: 'https://example.com/data', response_template: nil ) - stub_request(:get, 'https://api.example.com/data') + stub_request(:get, 'https://example.com/data') .with(basic_auth: %w[user123 pass456]) .to_return(status: 200, body: '{"authenticated": true}') end @@ -128,7 +120,7 @@ RSpec.describe Captain::Tools::HttpTool, type: :model do result = tool.perform(tool_context) expect(result).to eq('{"authenticated": true}') - expect(WebMock).to have_requested(:get, 'https://api.example.com/data') + expect(WebMock).to have_requested(:get, 'https://example.com/data') .with(basic_auth: %w[user123 pass456]) end end @@ -138,10 +130,10 @@ RSpec.describe Captain::Tools::HttpTool, type: :model do custom_tool.update!( auth_type: 'api_key', auth_config: { 'key' => 'api_key_123', 'location' => 'header', 'name' => 'X-API-Key' }, - endpoint_url: 'https://api.example.com/data', + endpoint_url: 'https://example.com/data', response_template: nil ) - stub_request(:get, 'https://api.example.com/data') + stub_request(:get, 'https://example.com/data') .with(headers: { 'X-API-Key' => 'api_key_123' }) .to_return(status: 200, body: '{"authenticated": true}') end @@ -150,7 +142,7 @@ RSpec.describe Captain::Tools::HttpTool, type: :model do result = tool.perform(tool_context) expect(result).to eq('{"authenticated": true}') - expect(WebMock).to have_requested(:get, 'https://api.example.com/data') + expect(WebMock).to have_requested(:get, 'https://example.com/data') .with(headers: { 'X-API-Key' => 'api_key_123' }) end end @@ -158,10 +150,10 @@ RSpec.describe Captain::Tools::HttpTool, type: :model do context 'with response template' do before do custom_tool.update!( - endpoint_url: 'https://api.example.com/orders/123', + endpoint_url: 'https://example.com/orders/123', response_template: 'Order status: {{ response.status }}, ID: {{ response.order_id }}' ) - stub_request(:get, 'https://api.example.com/orders/123') + stub_request(:get, 'https://example.com/orders/123') .to_return(status: 200, body: '{"status": "shipped", "order_id": "123"}') end @@ -174,8 +166,8 @@ RSpec.describe Captain::Tools::HttpTool, type: :model do context 'when handling errors' do it 'returns generic error message on network failure' do - custom_tool.update!(endpoint_url: 'https://api.example.com/data') - stub_request(:get, 'https://api.example.com/data').to_raise(SocketError.new('Failed to connect')) + custom_tool.update!(endpoint_url: 'https://example.com/data') + stub_request(:get, 'https://example.com/data').to_raise(SocketError.new('Failed to connect')) result = tool.perform(tool_context) @@ -183,8 +175,8 @@ RSpec.describe Captain::Tools::HttpTool, type: :model do end it 'returns generic error message on timeout' do - custom_tool.update!(endpoint_url: 'https://api.example.com/data') - stub_request(:get, 'https://api.example.com/data').to_timeout + custom_tool.update!(endpoint_url: 'https://example.com/data') + stub_request(:get, 'https://example.com/data').to_timeout result = tool.perform(tool_context) @@ -192,8 +184,8 @@ RSpec.describe Captain::Tools::HttpTool, type: :model do end it 'returns generic error message on HTTP 404' do - custom_tool.update!(endpoint_url: 'https://api.example.com/data') - stub_request(:get, 'https://api.example.com/data').to_return(status: 404, body: 'Not found') + custom_tool.update!(endpoint_url: 'https://example.com/data') + stub_request(:get, 'https://example.com/data').to_return(status: 404, body: 'Not found') result = tool.perform(tool_context) @@ -201,8 +193,8 @@ RSpec.describe Captain::Tools::HttpTool, type: :model do end it 'returns generic error message on HTTP 500' do - custom_tool.update!(endpoint_url: 'https://api.example.com/data') - stub_request(:get, 'https://api.example.com/data').to_return(status: 500, body: 'Server error') + custom_tool.update!(endpoint_url: 'https://example.com/data') + stub_request(:get, 'https://example.com/data').to_return(status: 500, body: 'Server error') result = tool.perform(tool_context) @@ -210,8 +202,8 @@ RSpec.describe Captain::Tools::HttpTool, type: :model do end it 'logs error details' do - custom_tool.update!(endpoint_url: 'https://api.example.com/data') - stub_request(:get, 'https://api.example.com/data').to_raise(StandardError.new('Test error')) + custom_tool.update!(endpoint_url: 'https://example.com/data') + stub_request(:get, 'https://example.com/data').to_raise(StandardError.new('Test error')) expect(Rails.logger).to receive(:error).with(/HttpTool execution error.*Test error/) @@ -223,14 +215,14 @@ RSpec.describe Captain::Tools::HttpTool, type: :model do it 'correctly integrates URL rendering, body rendering, auth, and response formatting' do custom_tool.update!( http_method: 'POST', - endpoint_url: 'https://api.example.com/users/{{ user_id }}/orders', + endpoint_url: 'https://example.com/users/{{ user_id }}/orders', request_template: '{"product": "{{ product }}", "quantity": {{ quantity }}}', auth_type: 'bearer', auth_config: { 'token' => 'integration_token' }, response_template: 'Created order #{{ response.order_number }} for {{ response.product }}' ) - stub_request(:post, 'https://api.example.com/users/42/orders') + stub_request(:post, 'https://example.com/users/42/orders') .with( body: '{"product": "Widget", "quantity": 5}', headers: { diff --git a/spec/enterprise/models/concerns/captain_tools_helpers_spec.rb b/spec/enterprise/models/concerns/captain_tools_helpers_spec.rb index afe482385..7e36e9006 100644 --- a/spec/enterprise/models/concerns/captain_tools_helpers_spec.rb +++ b/spec/enterprise/models/concerns/captain_tools_helpers_spec.rb @@ -42,58 +42,6 @@ RSpec.describe Concerns::CaptainToolsHelpers, type: :concern do end end - describe '.available_agent_tools' do - before do - # Mock the YAML file loading - allow(YAML).to receive(:load_file).and_return([ - { - 'id' => 'add_contact_note', - 'title' => 'Add Contact Note', - 'description' => 'Add a note to a contact', - 'icon' => 'note-add' - }, - { - 'id' => 'invalid_tool', - 'title' => 'Invalid Tool', - 'description' => 'This tool does not exist', - 'icon' => 'invalid' - } - ]) - - # Mock class resolution - only add_contact_note exists - allow(test_class).to receive(:resolve_tool_class) do |tool_id| - case tool_id - when 'add_contact_note' - Captain::Tools::AddContactNoteTool - end - end - end - - it 'returns only resolvable tools' do - tools = test_class.available_agent_tools - - expect(tools.length).to eq(1) - expect(tools.first).to eq({ - id: 'add_contact_note', - title: 'Add Contact Note', - description: 'Add a note to a contact', - icon: 'note-add' - }) - end - - it 'logs warnings for unresolvable tools' do - expect(Rails.logger).to receive(:warn).with('Tool class not found for ID: invalid_tool') - - test_class.available_agent_tools - end - - it 'memoizes the result' do - expect(YAML).to receive(:load_file).once.and_return([]) - - 2.times { test_class.available_agent_tools } - end - end - describe '.resolve_tool_class' do it 'resolves valid tool classes' do # Mock the constantize to return a class @@ -116,28 +64,6 @@ RSpec.describe Concerns::CaptainToolsHelpers, type: :concern do end end - describe '.available_tool_ids' do - before do - allow(test_class).to receive(:available_agent_tools).and_return([ - { id: 'add_contact_note', title: 'Add Contact Note', description: '...', - icon: 'note' }, - { id: 'update_priority', title: 'Update Priority', description: '...', - icon: 'priority' } - ]) - end - - it 'returns array of tool IDs' do - ids = test_class.available_tool_ids - expect(ids).to eq(%w[add_contact_note update_priority]) - end - - it 'memoizes the result' do - expect(test_class).to receive(:available_agent_tools).once.and_return([]) - - 2.times { test_class.available_tool_ids } - end - end - describe '#extract_tool_ids_from_text' do it 'extracts tool IDs from text' do text = 'First [@Add Contact Note](tool://add_contact_note) then [@Update Priority](tool://update_priority)'