feat: extract tool ids from text

This commit is contained in:
Shivam Mishra
2025-07-09 17:21:10 +05:30
parent cd8f245483
commit 5d2afc065f
4 changed files with 127 additions and 41 deletions
+1 -1
View File
@@ -16,7 +16,7 @@
#
class Captain::Assistant < ApplicationRecord
include Avatarable
include AgentToolsResolvable
include CaptainToolsHelpers
self.table_name = 'captain_assistants'
+50 -4
View File
@@ -25,7 +25,7 @@
# fk_rails_... (assistant_id => captain_assistants.id)
#
class Captain::Scenario < ApplicationRecord
include AgentToolsResolvable
include CaptainToolsHelpers
self.table_name = 'captain_scenarios'
@@ -37,14 +37,60 @@ class Captain::Scenario < ApplicationRecord
validates :instruction, presence: true
validates :assistant_id, presence: true
validates :account_id, presence: true
validate :validate_instruction_tools
scope :enabled, -> { where(enabled: true) }
before_save :populate_tools
before_save :resolve_tool_references
private
def populate_tools
# TODO: Implement tools population logic
# Validates that all tool references in the instruction are valid.
# Parses the instruction for tool references and checks if they exist
# in the available tools configuration.
#
# @return [void]
# @api private
# @example Valid instruction
# scenario.instruction = "Use (tool://add_contact_note) to document"
# scenario.valid? # => true
#
# @example Invalid instruction
# scenario.instruction = "Use (tool://invalid_tool) to process"
# scenario.valid? # => false
# scenario.errors[:instruction] # => ["contains invalid tools: invalid_tool"]
def validate_instruction_tools
return if instruction.blank?
tool_ids = extract_tool_ids_from_text(instruction)
return if tool_ids.empty?
available_tool_ids = self.class.available_tool_ids
invalid_tools = tool_ids - available_tool_ids
return unless invalid_tools.any?
errors.add(:instruction, "contains invalid tools: #{invalid_tools.join(', ')}")
end
# Resolves tool references from the instruction text into the tools field.
# Parses the instruction for tool references and materializes them as
# tool IDs stored in the tools JSONB field.
#
# @return [void]
# @api private
# @example
# scenario.instruction = "First (tool://add_private_note) then (tool://update_priority)"
# scenario.save!
# scenario.tools # => ["add_private_note", "update_priority"]
#
# scenario.instruction = "No tools mentioned here"
# scenario.save!
# scenario.tools # => nil
def resolve_tool_references
return if instruction.blank?
tool_ids = extract_tool_ids_from_text(instruction)
self.tools = tool_ids.presence
end
end
@@ -1,36 +0,0 @@
module AgentToolsResolvable
extend ActiveSupport::Concern
class_methods do
def available_agent_tools
@available_agent_tools ||= load_agent_tools
end
def resolve_tool_class(tool_id)
class_name = "Captain::Tools::#{tool_id.classify}Tool"
class_name.safe_constantize
end
private
def load_agent_tools
tools_config = YAML.load_file(Rails.root.join('config/agents/tools.yml'))
tools_config.filter_map do |tool_config|
tool_class = resolve_tool_class(tool_config['id'])
if tool_class
{
id: tool_config['id'],
title: tool_config['title'],
description: tool_config['description'],
icon: tool_config['icon']
}
else
Rails.logger.warn "Tool class not found for ID: #{tool_config['id']}"
nil
end
end
end
end
end
@@ -0,0 +1,76 @@
# Provides helper methods for working with Captain agent tools including
# tool resolution, text parsing, and metadata retrieval.
module CaptainToolsHelpers
extend ActiveSupport::Concern
# Regular expression pattern for matching tool references in text.
# Matches patterns like (tool://tool_id) following Chatwoot's mention syntax.
TOOL_REFERENCE_REGEX = %r{\(tool://([^/)]+)\)}
class_methods do
# Returns all available agent tools with their metadata.
# Only includes tools that have corresponding class files and can be resolved.
#
# @return [Array<Hash>] Array of tool hashes with :id, :title, :description, :icon
def available_agent_tools
@available_agent_tools ||= load_agent_tools
end
# Resolves a tool class from a tool ID.
# Converts snake_case tool IDs to PascalCase class names and constantizes them.
#
# @param tool_id [String] The snake_case tool identifier
# @return [Class, nil] The tool class if found, nil if not resolvable
def resolve_tool_class(tool_id)
class_name = "Captain::Tools::#{tool_id.classify}Tool"
class_name.safe_constantize
end
# Returns an array of all available tool IDs.
# Convenience method that extracts just the IDs from available_agent_tools.
#
# @return [Array<String>] Array of available tool IDs
def available_tool_ids
@available_tool_ids ||= available_agent_tools.map { |tool| tool[:id] }
end
private
# Loads agent tools from the YAML configuration file.
# Filters out tools that cannot be resolved to actual classes.
#
# @return [Array<Hash>] Array of resolvable tools with metadata
# @api private
def load_agent_tools
tools_config = YAML.load_file(Rails.root.join('config/agents/tools.yml'))
tools_config.filter_map do |tool_config|
tool_class = resolve_tool_class(tool_config['id'])
if tool_class
{
id: tool_config['id'],
title: tool_config['title'],
description: tool_config['description'],
icon: tool_config['icon']
}
else
Rails.logger.warn "Tool class not found for ID: #{tool_config['id']}"
nil
end
end
end
end
# Extracts tool IDs from text containing tool references.
# Parses text for (tool://tool_id) patterns and returns unique tool IDs.
#
# @param text [String] Text to parse for tool references
# @return [Array<String>] Array of unique tool IDs found in the text
def extract_tool_ids_from_text(text)
return [] if text.blank?
tool_matches = text.scan(TOOL_REFERENCE_REGEX)
tool_matches.flatten.uniq
end
end