From eb0cb23fb19ed8738445eb8a4cab91e7b0ec4c64 Mon Sep 17 00:00:00 2001 From: aakashb95 Date: Thu, 29 Jan 2026 16:22:20 +0530 Subject: [PATCH] fix(captain): handle GPT-4.1-mini structured output inconsistency GPT-4.1-mini sometimes returns JSON as a string instead of structured output when tools are present in the request. This adds a fallback parser that extracts the first valid JSON object from the string. - No impact on working cases (Hash returns immediately) - Handles concatenated JSON responses like `{...}{...}` - Safe fallback for future "Choose Your Own LLM" feature Co-Authored-By: Claude Opus 4.5 --- .../captain/assistant/agent_runner_service.rb | 49 +++++++++---------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/enterprise/app/services/captain/assistant/agent_runner_service.rb b/enterprise/app/services/captain/assistant/agent_runner_service.rb index f202034a2..9a0d0ea42 100644 --- a/enterprise/app/services/captain/assistant/agent_runner_service.rb +++ b/enterprise/app/services/captain/assistant/agent_runner_service.rb @@ -73,40 +73,39 @@ class Captain::Assistant::AgentRunnerService # Response formatting methods def process_agent_result(result) - # DEBUG LOGGING - remove after investigation - Rails.logger.info "[Captain V2 DEBUG] result.output class: #{result.output.class.name}" - Rails.logger.info "[Captain V2 DEBUG] result.output value: #{result.output.inspect}" - - if result.output.is_a?(String) - Rails.logger.warn '[Captain V2 DEBUG] Output is STRING not Hash - schema may not be applied!' - Rails.logger.warn "[Captain V2 DEBUG] String content: #{result.output}" - end - # END DEBUG LOGGING - response = format_response(result.output) - - # Extract agent name from context response['agent_name'] = result.context&.dig(:current_agent) - response end def format_response(output) - if output.is_a?(Hash) - Rails.logger.info "[Captain V2 DEBUG] format_response: Hash with keys #{output.keys}" - return output.with_indifferent_access + return output.with_indifferent_access if output.is_a?(Hash) + + # GPT-4.1-mini sometimes returns JSON as a string instead of structured output + # when tools are present in the request. Parse the first valid JSON object. + if output.is_a?(String) && output.strip.start_with?('{') + parsed = parse_first_json_object(output) + return parsed.with_indifferent_access if parsed end - # DEBUG LOGGING - remove after investigation - Rails.logger.warn "[Captain V2 DEBUG] format_response: Non-hash output, type=#{output.class.name}" - Rails.logger.warn "[Captain V2 DEBUG] format_response: content=#{output}" - # END DEBUG LOGGING - # Fallback for backwards compatibility - { - 'response' => output.to_s, - 'reasoning' => 'Processed by agent' - } + { 'response' => output.to_s, 'reasoning' => 'Processed by agent' } + end + + def parse_first_json_object(str) + depth = 0 + start_idx = str.index('{') + return nil unless start_idx + + (start_idx...str.length).each do |i| + depth += 1 if str[i] == '{' + depth -= 1 if str[i] == '}' + return JSON.parse(str[start_idx..i]) if depth.zero? + end + + nil + rescue JSON::ParserError + nil end def error_response(error_message)