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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
14aad67f0a
commit
eb0cb23fb1
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user