feat: Introduce the concept of tool registry within Captain (#11516)
This PR introduces the concept of a tool registry. The implementation is straightforward: you can define a tool by creating a class with a function name. The function name gets registered in the registry and can be referenced during LLM calls. When the LLM invokes a tool using the registered name, the registry locates and executes the appropriate tool. If the LLM calls an unregistered tool, the registry returns an error indicating that the tool is not defined.
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
require 'rails_helper'
|
||||
|
||||
# Test tool implementation
|
||||
class TestTool < Captain::Tools::BaseService
|
||||
def name
|
||||
'test_tool'
|
||||
end
|
||||
|
||||
def description
|
||||
'A test tool for specs'
|
||||
end
|
||||
|
||||
def parameters
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
test_param: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def execute(*args)
|
||||
args
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.describe Captain::ToolRegistryService do
|
||||
let(:assistant) { create(:captain_assistant) }
|
||||
let(:service) { described_class.new(assistant) }
|
||||
|
||||
describe '#initialize' do
|
||||
it 'initializes with empty tools and registered_tools' do
|
||||
expect(service.tools).to be_empty
|
||||
expect(service.registered_tools).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'method_missing' do
|
||||
let(:tool_class) { TestTool }
|
||||
|
||||
before do
|
||||
service.register_tool(tool_class)
|
||||
end
|
||||
|
||||
context 'when method corresponds to a registered tool' do
|
||||
it 'executes the tool with given arguments' do
|
||||
result = service.test_tool(test_param: 'arg1')
|
||||
expect(result).to eq([{ test_param: 'arg1' }])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when method does not correspond to a registered tool' do
|
||||
it 'raises NoMethodError' do
|
||||
expect { service.unknown_tool }.to raise_error(NoMethodError)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,77 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Captain::Tools::SearchDocumentationService do
|
||||
let(:assistant) { create(:captain_assistant) }
|
||||
let(:service) { described_class.new(assistant) }
|
||||
let(:question) { 'How to create a new account?' }
|
||||
let(:answer) { 'You can create a new account by clicking on the Sign Up button.' }
|
||||
let(:external_link) { 'https://example.com/docs/create-account' }
|
||||
|
||||
describe '#name' do
|
||||
it 'returns the correct service name' do
|
||||
expect(service.name).to eq('search_documentation')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#description' do
|
||||
it 'returns the service description' do
|
||||
expect(service.description).to eq('Search and retrieve documentation from knowledge base')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#parameters' do
|
||||
it 'returns the required parameters schema' do
|
||||
expected_schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
search_query: {
|
||||
type: 'string',
|
||||
description: 'The search query to look up in the documentation.'
|
||||
}
|
||||
},
|
||||
required: ['search_query']
|
||||
}
|
||||
|
||||
expect(service.parameters).to eq(expected_schema)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
let!(:response) do
|
||||
create(
|
||||
:captain_assistant_response,
|
||||
assistant: assistant,
|
||||
question: question,
|
||||
answer: answer,
|
||||
status: 'approved'
|
||||
)
|
||||
end
|
||||
|
||||
let(:documentable) { create(:captain_document, external_link: external_link) }
|
||||
|
||||
context 'when matching responses exist' do
|
||||
before do
|
||||
response.update(documentable: documentable)
|
||||
allow(Captain::AssistantResponse).to receive(:search).with(question).and_return([response])
|
||||
end
|
||||
|
||||
it 'returns formatted responses for the search query' do
|
||||
result = service.execute({ 'search_query' => question })
|
||||
|
||||
expect(result).to include(question)
|
||||
expect(result).to include(answer)
|
||||
expect(result).to include(external_link)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when no matching responses exist' do
|
||||
before do
|
||||
allow(Captain::AssistantResponse).to receive(:search).with(question).and_return([])
|
||||
end
|
||||
|
||||
it 'returns an empty string' do
|
||||
expect(service.execute({ 'search_query' => question })).to eq('No FAQs found for the given query')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user