Module: RubyLLM::Protocols::Interactions::Chat

Defined in:
lib/ruby_llm/protocols/interactions/chat.rb

Overview

:nodoc:

Constant Summary collapse

FINISH_REASONS =
{ 'completed' => :stop, 'requires_action' => :tool_calls, 'incomplete' => :max_tokens }.freeze

Class Method Summary collapse

Class Method Details

.completion_urlObject



13
14
15
# File 'lib/ruby_llm/protocols/interactions/chat.rb', line 13

def completion_url
  'interactions'
end

.finish_reasonsObject



11
# File 'lib/ruby_llm/protocols/interactions/chat.rb', line 11

def finish_reasons = FINISH_REASONS

.format_interaction_input(messages) ⇒ Object



53
54
55
56
57
58
# File 'lib/ruby_llm/protocols/interactions/chat.rb', line 53

def format_interaction_input(messages)
  calls = messages.flat_map { |message| message.tool_calls.to_h.values }.to_h { |call| [call.id, call] }
  messages.reject { |message| message.role == :system }.flat_map do |message|
    render_interaction_turn(message, calls)
  end
end

.interaction_finish_reason(status, calls) ⇒ Object



120
121
122
123
124
# File 'lib/ruby_llm/protocols/interactions/chat.rb', line 120

def interaction_finish_reason(status, calls)
  return :tool_calls unless calls.empty?

  finish_reasons.fetch(status)
end

.interaction_state?(content) ⇒ Boolean

Returns:

  • (Boolean)


93
94
95
# File 'lib/ruby_llm/protocols/interactions/chat.rb', line 93

def interaction_state?(content)
  content.is_a?(Hash) && content.dig('response', 'object') == 'interaction'
end

.parse_completion_body(data, raw:, model: data['model'] || @model&.id, cost: nil) ⇒ Object



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/ruby_llm/protocols/interactions/chat.rb', line 97

def parse_completion_body(data, raw:, model: data['model'] || @model&.id, cost: nil)
  unless %w[completed requires_action incomplete].include?(data['status'])
    message = Array(data['errors']).filter_map { |error| error['message'] }.join('; ')
    raise Error.new(message.empty? ? "Gemini interaction ended with status #{data['status']}" : message,
                    response: raw)
  end

  steps = data.fetch('steps', [])
  content = parse_interaction_content(steps)
  calls = parse_interaction_calls(steps)
  if data['status'] == 'requires_action' && calls.empty?
    raise Error.new('Gemini interaction requires an unsupported action', response: raw)
  end

  Message.new(role: :assistant, content: content[:text], attachments: content[:attachments],
              citations: content[:citations], thinking: parse_interaction_thinking(steps),
              tool_calls: calls, server_tool_calls: parse_interaction_server_calls(steps),
              raw_content: { 'response' => data },
              model: model, raw: raw, cost: cost,
              finish_reason: interaction_finish_reason(data['status'], calls),
              **parse_interaction_usage(data['usage'] || {}))
end

.parse_interaction_thinking(steps) ⇒ Object



126
127
128
129
130
# File 'lib/ruby_llm/protocols/interactions/chat.rb', line 126

def parse_interaction_thinking(steps)
  thoughts = steps.select { |step| step['type'] == 'thought' }
  text = thoughts.flat_map { |step| Array(step['summary']) }.filter_map { |part| part['text'] }.join
  Thinking.build(text: text.empty? ? nil : text, signature: thoughts.last&.dig('signature'))
end

.parse_interaction_usage(usage) ⇒ Object



132
133
134
135
136
137
138
139
140
141
# File 'lib/ruby_llm/protocols/interactions/chat.rb', line 132

def parse_interaction_usage(usage)
  prompt = usage['total_input_tokens']
  cached = usage['total_cached_tokens'].to_i
  thoughts = usage['total_thought_tokens'].to_i
  {
    input_tokens: prompt && [prompt + usage['total_tool_use_tokens'].to_i - cached, 0].max,
    output_tokens: usage['total_output_tokens'] && (usage['total_output_tokens'] + thoughts),
    cache_read_tokens: usage['total_cached_tokens'], thinking_tokens: usage['total_thought_tokens']
  }
end

.render_interaction_history(steps) ⇒ Object



75
76
77
78
79
# File 'lib/ruby_llm/protocols/interactions/chat.rb', line 75

def render_interaction_history(steps)
  steps.map do |step|
    step['type'].to_s.start_with?('mcp_server_') ? step.except('signature') : step
  end
end

.render_interaction_message(message) ⇒ Object



81
82
83
84
85
86
87
88
89
90
91
# File 'lib/ruby_llm/protocols/interactions/chat.rb', line 81

def render_interaction_message(message)
  steps = []
  if message.content || message.attachments.any?
    steps << { type: message.role == :assistant ? 'model_output' : 'user_input',
               content: render_interaction_content(message.content, message.attachments) }
  end
  message.tool_calls&.each_value do |call|
    steps << { type: 'function_call', id: call.id, name: call.name, arguments: call.arguments }
  end
  steps
end

.render_interaction_result(message, calls) ⇒ Object



70
71
72
73
# File 'lib/ruby_llm/protocols/interactions/chat.rb', line 70

def render_interaction_result(message, calls)
  { type: 'function_result', call_id: message.tool_call_id, name: calls[message.tool_call_id]&.name,
    result: render_interaction_content(message.content, message.attachments) }.compact
end

.render_interaction_summaries(display) ⇒ Object



45
46
47
48
49
50
51
# File 'lib/ruby_llm/protocols/interactions/chat.rb', line 45

def render_interaction_summaries(display)
  unless [nil, :summarized, :omitted].include?(display)
    raise ArgumentError, 'Gemini Interactions thinking display must be summarized or omitted'
  end

  display == :omitted ? 'none' : 'auto'
end

.render_interaction_thinking(thinking) ⇒ Object

Raises:

  • (ArgumentError)


32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/ruby_llm/protocols/interactions/chat.rb', line 32

def render_interaction_thinking(thinking)
  if thinking.disabled? || thinking.budget&.zero?
    raise ArgumentError, 'Gemini Interactions does not expose a thinking-off control'
  end
  raise ArgumentError, 'Gemini Interactions accepts thinking effort, not a token budget' if thinking.budget
  if thinking.effort && !%i[minimal low medium high].include?(thinking.effort)
    raise ArgumentError, 'Gemini Interactions thinking effort must be minimal, low, medium, or high'
  end

  { thinking_level: thinking.effort&.to_s,
    thinking_summaries: render_interaction_summaries(thinking.display) }.compact
end

.render_interaction_turn(message, calls) ⇒ Object



60
61
62
63
64
65
66
67
68
# File 'lib/ruby_llm/protocols/interactions/chat.rb', line 60

def render_interaction_turn(message, calls)
  if interaction_state?(message.raw_content)
    render_interaction_history(message.raw_content.dig('response', 'steps') || [])
  elsif message.tool_result?
    [render_interaction_result(message, calls)]
  else
    render_interaction_message(message)
  end
end

.render_payload(messages, tools:, temperature:, model:, stream: false, max_output_tokens: nil, schema: nil, thinking: nil, tool_prefs: nil) ⇒ Object



17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/ruby_llm/protocols/interactions/chat.rb', line 17

def render_payload(messages, tools:, temperature:, model:, stream: false, max_output_tokens: nil,
                   schema: nil, thinking: nil, tool_prefs: nil, **)
  config = { temperature: temperature, max_output_tokens: max_output_tokens }.compact
  config.merge!(render_interaction_thinking(thinking)) if thinking
  choice = tool_prefs&.dig(:choice)
  config[:tool_choice] = render_interaction_choice(choice) if choice
  payload = {
    model: model.id, input: format_interaction_input(messages), stream: stream, store: false,
    system_instruction: messages.select { |message| message.role == :system }.map(&:content).join("\n\n"),
    generation_config: config, tools: render_interaction_tools(tools)
  }
  payload[:response_format] = { type: 'text', mime_type: 'application/json', schema: schema[:schema] } if schema
  payload
end