Class: RubyLLM::Contract::Parser

Inherits:
Object
  • Object
show all
Extended by:
Concerns::DeepSymbolize
Defined in:
lib/ruby_llm/contract/contract/parser.rb

Constant Summary collapse

UTF8_BOM =

Strip UTF-8 BOM (Byte Order Mark) that some LLMs/APIs prepend to output

"\xEF\xBB\xBF"
CODE_FENCE_PATTERN =

Strip markdown code fences that LLMs commonly wrap around JSON output Handles “‘json … “`, “` … “`, with optional trailing whitespace

/\A\s*```(?:json|JSON)?\s*\n(.*?)\n\s*```\s*\z/m
JSON_START_PATTERN =

Extract the first JSON object or array from text that may contain prose. Uses bracket-matching to find the outermost balanced { } or [ ] block.

/[{\[]/

Class Method Summary collapse

Methods included from Concerns::DeepSymbolize

deep_symbolize

Class Method Details

.extract_json(text) ⇒ Object



79
80
81
82
83
84
85
86
# File 'lib/ruby_llm/contract/contract/parser.rb', line 79

def self.extract_json(text)
  return nil unless text.is_a?(String)

  start_idx = text.index(JSON_START_PATTERN)
  return nil unless start_idx

  scan_for_balanced_json(text, start_idx)
end

.parse(raw_output, strategy:) ⇒ Object



14
15
16
17
18
19
20
# File 'lib/ruby_llm/contract/contract/parser.rb', line 14

def self.parse(raw_output, strategy:)
  case strategy
  when :json then parse_json(raw_output)
  when :text then raw_output
  else raise ArgumentError, "Unknown parse strategy: #{strategy}"
  end
end

.parse_json(raw_output) ⇒ Object



22
23
24
25
26
27
28
29
30
31
32
# File 'lib/ruby_llm/contract/contract/parser.rb', line 22

def self.parse_json(raw_output)
  return deep_symbolize(raw_output) if raw_output.is_a?(Hash) || raw_output.is_a?(Array)

  # Coerce non-String scalars (boolean, numeric) to their JSON representation
  # to prevent TypeError from JSON.parse on non-string input.
  coerced = raw_output.is_a?(String) ? raw_output : raw_output&.to_s
  text = strip_code_fences(strip_bom(coerced))
  raise RubyLLM::Contract::ParseError.new("Failed to parse JSON: nil content", details: raw_output) if text.nil?

  parse_json_text(text, raw_output)
end

.strip_bom(text) ⇒ Object



58
59
60
61
62
# File 'lib/ruby_llm/contract/contract/parser.rb', line 58

def self.strip_bom(text)
  return text unless text.is_a?(String)

  text.delete_prefix(UTF8_BOM)
end

.strip_code_fences(text) ⇒ Object



68
69
70
71
72
73
# File 'lib/ruby_llm/contract/contract/parser.rb', line 68

def self.strip_code_fences(text)
  return text unless text.is_a?(String)

  match = text.match(CODE_FENCE_PATTERN)
  match ? match[1] : text
end

.symbolize_keys(obj) ⇒ Object



10
11
12
# File 'lib/ruby_llm/contract/contract/parser.rb', line 10

def self.symbolize_keys(obj)
  deep_symbolize(obj)
end