module Captain module Llm class JasmineBrain Decision = Struct.new(:strategy, :tool_key, :reasoning, keyword_init: true) def self.decide(assistant:, conversation:, message:, history:) new(assistant, conversation, message, history).decide end def initialize(assistant, conversation, message, history) @assistant = assistant @conversation = conversation @message = message.to_s @history = history @contact = conversation.contact end def decide # 1. Gate: Check if AI is disabled for this contact return Decision.new(strategy: :skip_ai, reasoning: 'Contact has desligar_ia label') if contact_has_disabled_label? # 2. ASK THE BRAIN (LLM) llm_decision = ask_brain_for_classification # 3. Fallback safely if LLM fails return Decision.new(strategy: :direct, reasoning: 'LLM Classification Failed') unless llm_decision # 4. Return structured decision Decision.new( strategy: llm_decision['strategy'].to_sym, tool_key: llm_decision['tool_key'], reasoning: llm_decision['reasoning'] ) rescue StandardError => e Rails.logger.error "[JasmineBrain] Error in decision: #{e.message}" Decision.new(strategy: :direct, reasoning: "Error: #{e.message}") end private def contact_has_disabled_label? @contact.labels.exists?(name: 'desligar_ia') rescue StandardError false end def ask_brain_for_classification system_prompt = build_classification_prompt model = @assistant.try(:llm_model).presence || 'gpt-4o-mini' chat = RubyLLM.chat(model: model) chat = chat.with_params( response_format: { type: 'json_object' }, temperature: 0.1 ) chat.add_message({ role: 'system', content: system_prompt }) if @history.is_a?(Array) @history.each do |msg| chat.add_message({ role: msg[:role], content: msg[:content] }) end end raw_response = chat.ask(@message) parse_json(raw_response) end def build_classification_prompt # Carregamos as ferramentas e cenários dinamicamente do assistente # Incluímos as ferramentas básicas e os "Cenários" (que são ScenarioDelegatorTool) available_tools = @assistant.agent_tools(conversation: @conversation, user: nil) tools_list = available_tools.map do |tool| "- #{tool.name}: #{tool.description}" end.join("\n") <<~PROMPT You are Jasmine, the Brain of the operation. Your goal is to classify the user's intent based on their latest message and decide the action strategy. AVAILABLE INTENTS (TOOLS): #{tools_list} - direct: For general conversation, doubts unrelated to specific tools, or if unsure. IMPORTANT: - If the user says "Oi", "Ola", "Tudo bem?", "Bom dia" -> Use "direct". - If the user's request matches one of the specialized departments (scenarios) above, use that tool. - Do NOT trigger "escalar_humano" for greeting messages or simple questions. - Only use "escalar_humano" if the user is explicitly requesting a human or is angry. - If the list of AVAILABLE INTENTS (TOOLS) above is empty, ALWAYS use "direct". Output MUST be a valid JSON object with: { "strategy": "execute_tool" OR "direct", "tool_key": "THE_INTENT_KEY_IF_EXECUTE_TOOL_ELSE_NULL", "reasoning": "A brief explanation of why you chose this intent." } Example: User: "Tem vaga agora?" JSON: {"strategy": "execute_tool", "tool_key": "status_suites", "reasoning": "User asked about vacancy."} PROMPT end def parse_json(response_obj) content = response_obj.respond_to?(:content) ? response_obj.content : response_obj.to_s # Attempt to clean code blocks if present (common with some LLMs) clean_content = content.gsub(/^```json\s*|```$/, '').strip JSON.parse(clean_content) rescue JSON::ParserError Rails.logger.warn "[JasmineBrain] Failed to parse JSON: #{content}" nil end end end end