diff --git a/enterprise/app/models/captain/assistant.rb b/enterprise/app/models/captain/assistant.rb index abb7bb9e1..c05c3f35f 100644 --- a/enterprise/app/models/captain/assistant.rb +++ b/enterprise/app/models/captain/assistant.rb @@ -16,7 +16,7 @@ # class Captain::Assistant < ApplicationRecord include Avatarable - include AgentToolsResolvable + include CaptainToolsHelpers self.table_name = 'captain_assistants' diff --git a/enterprise/app/models/captain/scenario.rb b/enterprise/app/models/captain/scenario.rb index 6f607bde1..377bb9f63 100644 --- a/enterprise/app/models/captain/scenario.rb +++ b/enterprise/app/models/captain/scenario.rb @@ -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 diff --git a/enterprise/app/models/concerns/agent_tools_resolvable.rb b/enterprise/app/models/concerns/agent_tools_resolvable.rb deleted file mode 100644 index 2f367ed39..000000000 --- a/enterprise/app/models/concerns/agent_tools_resolvable.rb +++ /dev/null @@ -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 \ No newline at end of file diff --git a/enterprise/app/models/concerns/captain_tools_helpers.rb b/enterprise/app/models/concerns/captain_tools_helpers.rb new file mode 100644 index 000000000..e7bef04ca --- /dev/null +++ b/enterprise/app/models/concerns/captain_tools_helpers.rb @@ -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] 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] 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] 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] 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 \ No newline at end of file