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:
Pranav
2025-05-19 15:26:38 -07:00
committed by GitHub
parent ad41fd90f9
commit d657f35a76
8 changed files with 303 additions and 51 deletions

View File

@@ -1,24 +1,4 @@
module Captain::ChatHelper
def search_documentation_tool
{
type: 'function',
function: {
name: 'search_documentation',
description: "Use this function to get documentation on functionalities you don't know about.",
parameters: {
type: 'object',
properties: {
search_query: {
type: 'string',
description: 'The search query to look up in the documentation.'
}
},
required: ['search_query']
}
}
}
end
def request_chat_completion
Rails.logger.debug { "[CAPTAIN][ChatCompletion] #{@messages}" }
@@ -26,13 +6,12 @@ module Captain::ChatHelper
parameters: {
model: @model,
messages: @messages,
tools: [search_documentation_tool],
tools: @tool_registry&.registered_tools || [],
response_format: { type: 'json_object' }
}
)
handle_response(response)
@response
end
def handle_response(response)
@@ -41,7 +20,7 @@ module Captain::ChatHelper
if message['tool_calls']
process_tool_calls(message['tool_calls'])
else
@response = JSON.parse(message['content'].strip)
JSON.parse(message['content'].strip)
end
end
@@ -54,38 +33,20 @@ module Captain::ChatHelper
end
def process_tool_call(tool_call)
arguments = JSON.parse(tool_call['function']['arguments'])
function_name = tool_call['function']['name']
tool_call_id = tool_call['id']
if tool_call['function']['name'] == 'search_documentation'
query = JSON.parse(tool_call['function']['arguments'])['search_query']
sections = fetch_documentation(query)
append_tool_response(sections, tool_call_id)
if @tool_registry.respond_to?(function_name)
execute_tool(function_name, arguments, tool_call_id)
else
append_tool_response('', tool_call_id)
process_invalid_tool_call(tool_call_id)
end
end
def fetch_documentation(query)
Rails.logger.debug { "[CAPTAIN][DocumentationSearch] #{query}" }
@assistant
.responses
.approved
.search(query)
.map { |response| format_response(response) }.join
end
def format_response(response)
formatted_response = "
Question: #{response.question}
Answer: #{response.answer}
"
if response.documentable.present? && response.documentable.try(:external_link)
formatted_response += "
Source: #{response.documentable.external_link}
"
end
formatted_response
def execute_tool(function_name, arguments, tool_call_id)
result = @tool_registry.send(function_name, arguments)
append_tool_response(result, tool_call_id)
end
def append_tool_calls(tool_calls)
@@ -95,11 +56,15 @@ module Captain::ChatHelper
}
end
def append_tool_response(sections, tool_call_id)
def process_invalid_tool_call(tool_call_id)
append_tool_response('Tool not available', tool_call_id)
end
def append_tool_response(content, tool_call_id)
@messages << {
role: 'tool',
tool_call_id: tool_call_id,
content: "Found the following FAQs in the documentation:\n #{sections}"
content: content
}
end
end