feat: scenario tools [CW-4597] (#11908)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Sojan Jose <sojan@pepalo.com>
This commit is contained in:
@@ -33,15 +33,119 @@ RSpec.describe Captain::Scenario, type: :model do
|
||||
let(:account) { create(:account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
|
||||
describe 'before_save :populate_tools' do
|
||||
it 'calls populate_tools before saving' do
|
||||
describe 'before_save :resolve_tool_references' do
|
||||
it 'calls resolve_tool_references before saving' do
|
||||
scenario = build(:captain_scenario, assistant: assistant, account: account)
|
||||
expect(scenario).to receive(:populate_tools)
|
||||
expect(scenario).to receive(:resolve_tool_references)
|
||||
scenario.save
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'tool validation and population' do
|
||||
let(:account) { create(:account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
|
||||
before do
|
||||
# Mock available tools
|
||||
allow(described_class).to receive(:available_tool_ids).and_return(%w[
|
||||
add_contact_note add_private_note update_priority
|
||||
])
|
||||
end
|
||||
|
||||
describe 'validate_instruction_tools' do
|
||||
it 'is valid with valid tool references' do
|
||||
scenario = build(:captain_scenario,
|
||||
assistant: assistant,
|
||||
account: account,
|
||||
instruction: 'Use [@Add Contact Note](tool://add_contact_note) to document')
|
||||
|
||||
expect(scenario).to be_valid
|
||||
end
|
||||
|
||||
it 'is invalid with invalid tool references' do
|
||||
scenario = build(:captain_scenario,
|
||||
assistant: assistant,
|
||||
account: account,
|
||||
instruction: 'Use [@Invalid Tool](tool://invalid_tool) to process')
|
||||
|
||||
expect(scenario).not_to be_valid
|
||||
expect(scenario.errors[:instruction]).to include('contains invalid tools: invalid_tool')
|
||||
end
|
||||
|
||||
it 'is invalid with multiple invalid tools' do
|
||||
scenario = build(:captain_scenario,
|
||||
assistant: assistant,
|
||||
account: account,
|
||||
instruction: 'Use [@Invalid Tool](tool://invalid_tool) and [@Another Invalid](tool://another_invalid)')
|
||||
|
||||
expect(scenario).not_to be_valid
|
||||
expect(scenario.errors[:instruction]).to include('contains invalid tools: invalid_tool, another_invalid')
|
||||
end
|
||||
|
||||
it 'is valid with no tool references' do
|
||||
scenario = build(:captain_scenario,
|
||||
assistant: assistant,
|
||||
account: account,
|
||||
instruction: 'Just respond politely to the customer')
|
||||
|
||||
expect(scenario).to be_valid
|
||||
end
|
||||
|
||||
it 'is valid with blank instruction' do
|
||||
scenario = build(:captain_scenario,
|
||||
assistant: assistant,
|
||||
account: account,
|
||||
instruction: '')
|
||||
|
||||
# Will be invalid due to presence validation, not tool validation
|
||||
expect(scenario).not_to be_valid
|
||||
expect(scenario.errors[:instruction]).not_to include(/contains invalid tools/)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'resolve_tool_references' do
|
||||
it 'populates tools array with referenced tool IDs' do
|
||||
scenario = create(:captain_scenario,
|
||||
assistant: assistant,
|
||||
account: account,
|
||||
instruction: 'First [@Add Contact Note](tool://add_contact_note) then [@Update Priority](tool://update_priority)')
|
||||
|
||||
expect(scenario.tools).to eq(%w[add_contact_note update_priority])
|
||||
end
|
||||
|
||||
it 'sets tools to nil when no tools are referenced' do
|
||||
scenario = create(:captain_scenario,
|
||||
assistant: assistant,
|
||||
account: account,
|
||||
instruction: 'Just respond politely to the customer')
|
||||
|
||||
expect(scenario.tools).to be_nil
|
||||
end
|
||||
|
||||
it 'handles duplicate tool references' do
|
||||
scenario = create(:captain_scenario,
|
||||
assistant: assistant,
|
||||
account: account,
|
||||
instruction: 'Use [@Add Contact Note](tool://add_contact_note) and [@Add Contact Note](tool://add_contact_note) again')
|
||||
|
||||
expect(scenario.tools).to eq(['add_contact_note'])
|
||||
end
|
||||
|
||||
it 'updates tools when instruction changes' do
|
||||
scenario = create(:captain_scenario,
|
||||
assistant: assistant,
|
||||
account: account,
|
||||
instruction: 'Use [@Add Contact Note](tool://add_contact_note)')
|
||||
|
||||
expect(scenario.tools).to eq(['add_contact_note'])
|
||||
|
||||
scenario.update!(instruction: 'Use [@Update Priority](tool://update_priority) instead')
|
||||
expect(scenario.tools).to eq(['update_priority'])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'factory' do
|
||||
it 'creates a valid scenario with associations' do
|
||||
account = create(:account)
|
||||
|
||||
180
spec/enterprise/models/concerns/captain_tools_helpers_spec.rb
Normal file
180
spec/enterprise/models/concerns/captain_tools_helpers_spec.rb
Normal file
@@ -0,0 +1,180 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Concerns::CaptainToolsHelpers, type: :concern do
|
||||
# Create a test class that includes the concern
|
||||
let(:test_class) do
|
||||
Class.new do
|
||||
include Concerns::CaptainToolsHelpers
|
||||
|
||||
def self.name
|
||||
'TestClass'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
let(:test_instance) { test_class.new }
|
||||
|
||||
describe 'TOOL_REFERENCE_REGEX' do
|
||||
it 'matches tool references in text' do
|
||||
text = 'Use [@Add Contact Note](tool://add_contact_note) and [Update Priority](tool://update_priority)'
|
||||
matches = text.scan(Concerns::CaptainToolsHelpers::TOOL_REFERENCE_REGEX)
|
||||
|
||||
expect(matches.flatten).to eq(%w[add_contact_note update_priority])
|
||||
end
|
||||
|
||||
it 'does not match invalid formats' do
|
||||
invalid_formats = [
|
||||
'<tool://invalid>',
|
||||
'tool://invalid',
|
||||
'(tool:invalid)',
|
||||
'(tool://)',
|
||||
'(tool://with/slash)',
|
||||
'(tool://add_contact_note)',
|
||||
'[@Tool](tool://)',
|
||||
'[Tool](tool://with/slash)',
|
||||
'[](tool://valid)'
|
||||
]
|
||||
|
||||
invalid_formats.each do |format|
|
||||
matches = format.scan(Concerns::CaptainToolsHelpers::TOOL_REFERENCE_REGEX)
|
||||
expect(matches).to be_empty, "Should not match: #{format}"
|
||||
end
|
||||
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
|
||||
stub_const('Captain::Tools::AddContactNoteTool', Class.new)
|
||||
|
||||
result = test_class.resolve_tool_class('add_contact_note')
|
||||
expect(result).to eq(Captain::Tools::AddContactNoteTool)
|
||||
end
|
||||
|
||||
it 'returns nil for invalid tool classes' do
|
||||
result = test_class.resolve_tool_class('invalid_tool')
|
||||
expect(result).to be_nil
|
||||
end
|
||||
|
||||
it 'converts snake_case to PascalCase' do
|
||||
stub_const('Captain::Tools::AddPrivateNoteTool', Class.new)
|
||||
|
||||
result = test_class.resolve_tool_class('add_private_note')
|
||||
expect(result).to eq(Captain::Tools::AddPrivateNoteTool)
|
||||
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)'
|
||||
result = test_instance.extract_tool_ids_from_text(text)
|
||||
|
||||
expect(result).to eq(%w[add_contact_note update_priority])
|
||||
end
|
||||
|
||||
it 'returns unique tool IDs' do
|
||||
text = 'Use [@Add Contact Note](tool://add_contact_note) and [@Contact Note](tool://add_contact_note) again'
|
||||
result = test_instance.extract_tool_ids_from_text(text)
|
||||
|
||||
expect(result).to eq(['add_contact_note'])
|
||||
end
|
||||
|
||||
it 'returns empty array for blank text' do
|
||||
expect(test_instance.extract_tool_ids_from_text('')).to eq([])
|
||||
expect(test_instance.extract_tool_ids_from_text(nil)).to eq([])
|
||||
expect(test_instance.extract_tool_ids_from_text(' ')).to eq([])
|
||||
end
|
||||
|
||||
it 'returns empty array when no tools found' do
|
||||
text = 'This text has no tool references'
|
||||
result = test_instance.extract_tool_ids_from_text(text)
|
||||
|
||||
expect(result).to eq([])
|
||||
end
|
||||
|
||||
it 'handles complex text with multiple tools' do
|
||||
text = <<~TEXT
|
||||
Start with [@Add Contact Note](tool://add_contact_note) to document.
|
||||
Then use [@Update Priority](tool://update_priority) if needed.
|
||||
Finally [@Add Private Note](tool://add_private_note) for internal notes.
|
||||
TEXT
|
||||
|
||||
result = test_instance.extract_tool_ids_from_text(text)
|
||||
expect(result).to eq(%w[add_contact_note update_priority add_private_note])
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user