diff --git a/enterprise/app/services/captain/tool_registry_service.rb b/enterprise/app/services/captain/tool_registry_service.rb index 088d89483..f2c234060 100644 --- a/enterprise/app/services/captain/tool_registry_service.rb +++ b/enterprise/app/services/captain/tool_registry_service.rb @@ -9,6 +9,8 @@ class Captain::ToolRegistryService def register_tool(tool_class) tool = tool_class.new(@assistant) + return unless tool.active? + @tools[tool.name] = tool @registered_tools << tool.to_registry_format end diff --git a/enterprise/app/services/captain/tools/base_service.rb b/enterprise/app/services/captain/tools/base_service.rb index 10e44d2f4..7fadc806b 100644 --- a/enterprise/app/services/captain/tools/base_service.rb +++ b/enterprise/app/services/captain/tools/base_service.rb @@ -31,4 +31,8 @@ class Captain::Tools::BaseService } } end + + def active? + true + end end diff --git a/enterprise/app/services/captain/tools/copilot/search_linear_issues_service.rb b/enterprise/app/services/captain/tools/copilot/search_linear_issues_service.rb new file mode 100644 index 000000000..599fc7de9 --- /dev/null +++ b/enterprise/app/services/captain/tools/copilot/search_linear_issues_service.rb @@ -0,0 +1,77 @@ +class Captain::Tools::Copilot::SearchLinearIssuesService < Captain::Tools::BaseService + def name + 'search_linear_issues' + end + + def description + 'Search Linear issues based on a search term' + end + + def parameters + { + type: 'object', + properties: { + term: { + type: 'string', + description: 'The search term to find Linear issues' + } + }, + required: %w[term] + } + end + + def execute(arguments) + return 'Linear integration is not enabled' unless active? + + term = arguments['term'] + + Rails.logger.info "#{self.class.name}: Service called with the search term #{term}" + + return 'Missing required parameters' if term.blank? + + linear_service = Integrations::Linear::ProcessorService.new(account: @assistant.account) + result = linear_service.search_issue(term) + + return result[:error] if result[:error] + + issues = result[:data] + return 'No issues found, I should try another similar search term' if issues.blank? + + total_count = issues.length + + <<~RESPONSE + Total number of issues: #{total_count} + #{issues.map { |issue| format_issue(issue) }.join("\n---\n")} + RESPONSE + end + + def active? + @assistant.account.hooks.find_by(app_id: 'linear').present? + end + + private + + def format_issue(issue) + <<~ISSUE + Title: #{issue['title']} + ID: #{issue['id']} + State: #{issue['state']['name']} + Priority: #{format_priority(issue['priority'])} + #{issue['assignee'] ? "Assignee: #{issue['assignee']['name']}" : 'Assignee: Unassigned'} + #{issue['description'].present? ? "\nDescription: #{issue['description']}" : ''} + ISSUE + end + + def format_priority(priority) + return 'No priority' if priority.nil? + + case priority + when 0 then 'No priority' + when 1 then 'Urgent' + when 2 then 'High' + when 3 then 'Medium' + when 4 then 'Low' + else 'Unknown' + end + end +end diff --git a/spec/enterprise/services/captain/tool_registry_service_spec.rb b/spec/enterprise/services/captain/tool_registry_service_spec.rb index e355dea0a..beb4a6a63 100644 --- a/spec/enterprise/services/captain/tool_registry_service_spec.rb +++ b/spec/enterprise/services/captain/tool_registry_service_spec.rb @@ -2,6 +2,13 @@ require 'rails_helper' # Test tool implementation class TestTool < Captain::Tools::BaseService + attr_accessor :tool_active + + def initialize(*args) + super + @tool_active = true + end + def name 'test_tool' end @@ -24,6 +31,10 @@ class TestTool < Captain::Tools::BaseService def execute(*args) args end + + def active? + @tool_active + end end RSpec.describe Captain::ToolRegistryService do @@ -40,27 +51,41 @@ RSpec.describe Captain::ToolRegistryService do describe '#register_tool' do let(:tool_class) { TestTool } - it 'registers a new tool' do - service.register_tool(tool_class) - - expect(service.tools['test_tool']).to be_a(TestTool) - expect(service.registered_tools).to include( - { - type: 'function', - function: { - name: 'test_tool', - description: 'A test tool for specs', - parameters: { - type: 'object', - properties: { - test_param: { - type: 'string' + context 'when tool is active' do + it 'registers a new tool' do + service.register_tool(tool_class) + expect(service.tools['test_tool']).to be_a(TestTool) + expect(service.registered_tools).to include( + { + type: 'function', + function: { + name: 'test_tool', + description: 'A test tool for specs', + parameters: { + type: 'object', + properties: { + test_param: { + type: 'string' + } } } } } - } - ) + ) + end + end + + context 'when tool is inactive' do + it 'does not register the tool' do + tool = tool_class.new(assistant) + tool.tool_active = false + allow(tool_class).to receive(:new).and_return(tool) + + service.register_tool(tool_class) + + expect(service.tools['test_tool']).to be_nil + expect(service.registered_tools).to be_empty + end end end diff --git a/spec/enterprise/services/captain/tools/copilot/search_linear_issues_service_spec.rb b/spec/enterprise/services/captain/tools/copilot/search_linear_issues_service_spec.rb new file mode 100644 index 000000000..f4a5225b1 --- /dev/null +++ b/spec/enterprise/services/captain/tools/copilot/search_linear_issues_service_spec.rb @@ -0,0 +1,125 @@ +require 'rails_helper' + +RSpec.describe Captain::Tools::Copilot::SearchLinearIssuesService do + let(:account) { create(:account) } + let(:assistant) { create(:captain_assistant, account: account) } + let(:service) { described_class.new(assistant) } + + 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 'returns the expected parameter schema' do + expect(service.parameters).to eq( + { + type: 'object', + properties: { + term: { + type: 'string', + description: 'The search term to find Linear issues' + } + }, + required: %w[term] + } + ) + end + end + + describe '#active?' do + context 'when Linear integration is enabled' do + before do + create(:integrations_hook, :linear, account: account) + end + + it 'returns true' do + expect(service.active?).to be true + end + end + + context 'when Linear integration is not enabled' do + it 'returns false' do + expect(service.active?).to be false + 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 + it 'returns error message' do + expect(service.execute({ 'term' => '' })).to eq('Missing required parameters') + 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