Class: Pikuri::Testing::Script

Inherits:
Object
  • Object
show all
Defined in:
lib/pikuri/testing.rb

Overview

Collects the scripted conversation: each verb appends one OpenAI-wire response body to #responses, in call order. You don't build these directly — fake_transport yields one to your block.

fake_transport do |llm|
llm.reply('hi')                                  # plain answer
llm.tool_call('calculator', expression: '2+2')   # a tool turn
llm.stream('The ', 'answer ', 'is ', '4.')       # a streamed answer
end

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeScript

Returns a new instance of Script.



145
146
147
148
# File 'lib/pikuri/testing.rb', line 145

def initialize
  @responses = []
  @tool_call_seq = 0
end

Instance Attribute Details

#responsesArray<Hash> (readonly)

Returns queued response entries in call order.

Returns:

  • (Array<Hash>)

    queued response entries in call order



143
144
145
# File 'lib/pikuri/testing.rb', line 143

def responses
  @responses
end

Instance Method Details

#reply(text) ⇒ self

Queue a plain assistant-text turn (the model's final answer for that step).

Parameters:

  • text (String)

    the assistant message content

Returns:

  • (self)


155
156
157
158
# File 'lib/pikuri/testing.rb', line 155

def reply(text)
  @responses << { json: assistant_body(content: text) }
  self
end

#stream(*deltas) ⇒ self

Queue a streamed assistant-text turn: each delta becomes one SSE chunk, so a streaming: true agent emits a matching sequence of Agent::Event::AssistantDeltas and a final assembled Agent::Event::Assistant. Text only — tool-call streaming is not modelled.

Examples:

llm.stream('The ', 'answer ', 'is ', '4.')

Parameters:

  • deltas (Array<String>)

    content fragments, streamed in order

Returns:

  • (self)


209
210
211
212
213
214
215
216
217
218
# File 'lib/pikuri/testing.rb', line 209

def stream(*deltas)
  chunks = deltas.map do |d|
    "data: #{JSON.generate('model' => 'fake', 'choices' => [{ 'delta' => { 'content' => d } }])}\n\n"
  end
  chunks << "data: #{JSON.generate('model' => 'fake', 'choices' => [{ 'delta' => {} }],
                                    'usage' => usage)}\n\n"
  chunks << "data: [DONE]\n\n"
  @responses << { sse: chunks }
  self
end

#tool_batch(*calls) ⇒ self

Queue an assistant turn that calls several tools at once — one tool_calls array, the shape a provider sends when the model batches. ruby_llm runs them in order, each answered by its own :tool message.

llm.tool_batch(['calculator', { expression: '1 + 1' }],
             ['calculator', { expression: '2 + 2' }])

What a spec needs to exercise anything treating a batch as a unit — the interloper's drain boundary, or which call a mid-batch unwind blames.

Parameters:

  • calls (Array<Array(String, Hash)>)

    [name, arguments] pairs, in the order the model asked for them

Returns:

  • (self)


188
189
190
191
192
193
194
195
196
# File 'lib/pikuri/testing.rb', line 188

def tool_batch(*calls)
  wire = calls.map do |name, arguments|
    @tool_call_seq += 1
    { 'id' => "call_#{@tool_call_seq}", 'type' => 'function',
      'function' => { 'name' => name, 'arguments' => JSON.generate(arguments || {}) } }
  end
  @responses << { json: assistant_body(content: nil, tool_calls: wire) }
  self
end

#tool_call(name, **arguments) ⇒ self

Queue an assistant turn that calls one tool. The real ruby_llm loop parses it, runs the real registered tool, appends the observation, and re-requests — so the next queued entry is the model's follow-up.

Examples:

llm.tool_call('calculator', expression: '2 + 2')

Parameters:

  • name (String)

    registered tool name (e.g. "calculator")

  • arguments (Hash{Symbol => Object})

    tool arguments, JSON-encoded into the wire call

Returns:

  • (self)


171
172
173
# File 'lib/pikuri/testing.rb', line 171

def tool_call(name, **arguments)
  tool_batch([name, arguments])
end