Module: Optimize::Demo::Claude

Defined in:
lib/optimize/demo/claude.rb,
lib/optimize/demo/claude/prompt.rb,
lib/optimize/demo/claude/invoker.rb,
lib/optimize/demo/claude/validator.rb,
lib/optimize/demo/claude/serializer.rb,
lib/optimize/demo/claude/transcript.rb

Defined Under Namespace

Modules: Invoker, Prompt, Serializer, Validator Classes: Outcome, Transcript

Class Method Summary collapse

Class Method Details

.find_function(node, name) ⇒ Object

Recursive walk of the iseq tree to find a function by name.



119
120
121
122
123
124
125
126
# File 'lib/optimize/demo/claude.rb', line 119

def find_function(node, name)
  return node if node.name == name
  (node.children || []).each do |child|
    found = find_function(child, name)
    return found if found
  end
  nil
end

.run(fixture_path:, entry:, cases:, invoker: Invoker, max_iterations: 3) ⇒ Outcome

Parameters:

  • entry (Symbol)

    method name to locate in the iseq tree

  • cases (Array<Array(String, Object)>)

    list of

    entry_source, expected

    pairs. ‘entry_source` is evaluated via

    TOPLEVEL_BINDING after the iseq is loaded; result compared with ==. Validation must pass for EVERY case; multiple cases catch Claude table-looking-up a single expected answer instead of rewriting.

Returns:

Raises:

  • (ArgumentError)


29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/optimize/demo/claude.rb', line 29

def run(fixture_path:, entry:, cases:, invoker: Invoker, max_iterations: 3)
  raise ArgumentError, "cases must not be empty" if cases.empty?

  source = File.read(fixture_path)
  iseq = RubyVM::InstructionSequence.compile_file(fixture_path)
  envelope = Optimize::Codec.decode(iseq.to_binary)
  object_table = envelope.misc[:object_table]

  target_fn = find_function(envelope, entry.to_s) or
    raise ArgumentError, "entry method #{entry} not found"

  iseq_json = Serializer.serialize(target_fn, object_table: object_table)

  transcript = Transcript.new(
    fixture: File.basename(fixture_path, ".rb"),
    source: source,
    cases: cases,
  )

  initial_prompt = Prompt.initial(iseq_json: iseq_json)

  accumulated_prompt = initial_prompt
  last_errors = nil

  max_iterations.times do |i|
    prompt_for_this_call =
      if i.zero?
        initial_prompt
      else
        accumulated_prompt + "\n\n" + Prompt.retry_message(errors: last_errors)
      end
    accumulated_prompt = prompt_for_this_call

    parsed = nil
    raw_display = nil
    errors = nil

    begin
      parsed = invoker.call(prompt: prompt_for_this_call)
      raw_display = JSON.generate(parsed)
    rescue Invoker::ParseError => e
      errors = ["could not parse assistant JSON: #{e.message}"]
      parsed = nil
      raw_display = "(parse failed)"
    end

    if errors.nil?
      # Try deserialize.
      attempt = nil
      begin
        attempt = Serializer.deserialize(
          parsed,
          template: target_fn,
          object_table: object_table,
          strict: false,
        )
      rescue Serializer::DeserializeError => e
        errors = ["deserialize failed: #{e.message}"]
      end

      if errors.nil?
        errors = Validator.structural(attempt)
        if errors.empty?
          modified = substitute_function(envelope, target_fn, attempt)
          errors = Validator.semantic(modified, cases: cases)
        end
      end
    end

    transcript.record(
      iteration: i + 1,
      prompt: prompt_for_this_call,
      raw: raw_display,
      parsed: parsed,
      errors: errors,
    )

    if errors.empty?
      transcript.finish(outcome: :success)
      return Outcome.new(outcome: :success, transcript: transcript)
    end

    last_errors = errors
  end

  transcript.finish(outcome: :gave_up)
  Outcome.new(outcome: :gave_up, transcript: transcript)
end

.substitute_function(envelope, original_fn, replacement) ⇒ Object

Returns a shallow-copied envelope where ‘original_fn` (matched by object identity) is replaced by `replacement`. The envelope’s iseq_list — which is what Codec.encode actually reads — is also cloned with an updated functions array, so the replacement makes it into the encoded binary.



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/optimize/demo/claude.rb', line 133

def substitute_function(envelope, original_fn, replacement)
  new_functions = envelope.children.map do |fn|
    fn.equal?(original_fn) ? replacement : fn
  end
  return envelope if new_functions.each_with_index.all? { |fn, i| fn.equal?(envelope.children[i]) }

  original_iseq_list = envelope.misc[:iseq_list]
  new_iseq_list = original_iseq_list.dup
  new_iseq_list.instance_variable_set(:@functions, new_functions)
  if original_iseq_list.root.equal?(original_fn)
    new_iseq_list.instance_variable_set(:@root, replacement)
  end

  cloned = envelope.dup
  cloned.children = new_functions
  cloned.misc = envelope.misc.merge(iseq_list: new_iseq_list)
  cloned
end