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:
@@ -18,6 +18,7 @@
|
||||
#
|
||||
class Captain::Assistant < ApplicationRecord
|
||||
include Avatarable
|
||||
include Concerns::CaptainToolsHelpers
|
||||
|
||||
self.table_name = 'captain_assistants'
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
# index_captain_scenarios_on_enabled (enabled)
|
||||
#
|
||||
class Captain::Scenario < ApplicationRecord
|
||||
include Concerns::CaptainToolsHelpers
|
||||
|
||||
self.table_name = 'captain_scenarios'
|
||||
|
||||
belongs_to :assistant, class_name: 'Captain::Assistant'
|
||||
@@ -31,14 +33,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 [Add Contact Note](tool://add_contact_note) to document"
|
||||
# scenario.valid? # => true
|
||||
#
|
||||
# @example Invalid instruction
|
||||
# scenario.instruction = "Use [Invalid Tool](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 [@Add Private Note](tool://add_private_note) then [@Update Priority](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
|
||||
|
||||
76
enterprise/app/models/concerns/captain_tools_helpers.rb
Normal file
76
enterprise/app/models/concerns/captain_tools_helpers.rb
Normal file
@@ -0,0 +1,76 @@
|
||||
# Provides helper methods for working with Captain agent tools including
|
||||
# tool resolution, text parsing, and metadata retrieval.
|
||||
module Concerns::CaptainToolsHelpers
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
# Regular expression pattern for matching tool references in text.
|
||||
# Matches patterns like [Tool name](tool://tool_id) following markdown link 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
|
||||
Reference in New Issue
Block a user