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
- .completion_url ⇒ Object
- .finish_reasons ⇒ Object
- .format_interaction_input(messages) ⇒ Object
- .interaction_finish_reason(status, calls) ⇒ Object
- .interaction_state?(content) ⇒ Boolean
- .parse_completion_body(data, raw:, model: data['model'] || @model&.id, cost: nil) ⇒ Object
- .parse_interaction_thinking(steps) ⇒ Object
- .parse_interaction_usage(usage) ⇒ Object
- .render_interaction_history(steps) ⇒ Object
- .render_interaction_message(message) ⇒ Object
- .render_interaction_result(message, calls) ⇒ Object
- .render_interaction_summaries(display) ⇒ Object
- .render_interaction_thinking(thinking) ⇒ Object
- .render_interaction_turn(message, calls) ⇒ Object
- .render_payload(messages, tools:, temperature:, model:, stream: false, max_output_tokens: nil, schema: nil, thinking: nil, tool_prefs: nil) ⇒ Object
Class Method Details
.completion_url ⇒ Object
13 14 15 |
# File 'lib/ruby_llm/protocols/interactions/chat.rb', line 13 def completion_url 'interactions' end |
.finish_reasons ⇒ Object
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() calls = .flat_map { || .tool_calls.to_h.values }.to_h { |call| [call.id, call] } .reject { || .role == :system }.flat_map do || render_interaction_turn(, 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
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']) = Array(data['errors']).filter_map { |error| error['message'] }.join('; ') raise Error.new(.empty? ? "Gemini interaction ended with status #{data['status']}" : , 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 () steps = [] if .content || ..any? steps << { type: .role == :assistant ? 'model_output' : 'user_input', content: render_interaction_content(.content, .) } end .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(, calls) { type: 'function_result', call_id: .tool_call_id, name: calls[.tool_call_id]&.name, result: render_interaction_content(.content, .) }.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
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(, calls) if interaction_state?(.raw_content) render_interaction_history(.raw_content.dig('response', 'steps') || []) elsif .tool_result? [render_interaction_result(, calls)] else () 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(, 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(), stream: stream, store: false, system_instruction: .select { || .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 |