Module: RubyLLM::Protocols::Mistral::Conversations::Chat

Defined in:
lib/ruby_llm/protocols/mistral/conversations/chat.rb

Overview

:nodoc:

Class Method Summary collapse

Class Method Details

.completion_urlObject



10
11
12
# File 'lib/ruby_llm/protocols/mistral/conversations/chat.rb', line 10

def completion_url
  'conversations'
end

.format_conversation_message(message) ⇒ Object



90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/ruby_llm/protocols/mistral/conversations/chat.rb', line 90

def format_conversation_message(message)
  entries = []
  if message.content || message.attachments.any?
    entries << {
      type: 'message.input', role: message.role.to_s,
      content: format_message_content(message)
    }
  end
  message.tool_calls&.each_value do |call|
    entries << { type: 'function.call', tool_call_id: call.id, name: call.name,
                 arguments: JSON.generate(call.arguments) }
  end
  entries
end

.format_entries(messages) ⇒ Object



50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/ruby_llm/protocols/mistral/conversations/chat.rb', line 50

def format_entries(messages)
  entries = messages.reject { |message| message.role == :system }.flat_map do |message|
    if message.raw_content
      Array(message.raw_content)
    elsif message.tool_result?
      [{ type: 'function.result', tool_call_id: message.tool_call_id, result: message.content.to_s }]
    else
      format_conversation_message(message)
    end
  end
  entries.each_with_index.flat_map { |entry, index| replay_conversation_entry(entry, index) }
end

.normalize_conversation_choice(options) ⇒ Object



41
42
43
44
45
46
47
48
# File 'lib/ruby_llm/protocols/mistral/conversations/chat.rb', line 41

def normalize_conversation_choice(options)
  choice = options[:tool_choice]
  if choice.is_a?(Hash)
    raise ArgumentError, 'Mistral Conversations supports :auto, :none, or :required for tool choice'
  end

  options[:tool_choice] = 'any' if choice == 'required'
end

.parse_completion_body(data, raw:) ⇒ Object



105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/ruby_llm/protocols/mistral/conversations/chat.rb', line 105

def parse_completion_body(data, raw:)
  output = data.fetch('outputs')
  content = parse_conversation_content(output)
  calls = parse_conversation_calls(output, raw:)
  response_model = output.filter_map { |entry| entry['model'] }.last || @model&.id
  Message.new(
    role: :assistant, content: content[:text], attachments: content[:attachments],
    citations: content[:citations], thinking: Thinking.build(text: content[:thinking]),
    tool_calls: calls, server_tool_calls: parse_conversation_steps(output),
    raw_content: output, model: response_model,
    finish_reason: calls.empty? ? :stop : :tool_calls, raw: raw,
    **parse_conversation_usage(data['usage'] || {})
  )
end

.parse_conversation_calls(output, raw:) ⇒ Object



120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/ruby_llm/protocols/mistral/conversations/chat.rb', line 120

def parse_conversation_calls(output, raw:)
  if output.any? { |entry| entry['type'] == 'function.call' && entry['confirmation_status'] == 'pending' }
    raise Error.new('Mistral returned a hosted tool confirmation that requires provider conversation storage',
                    response: raw)
  end

  output.select { |entry| pending_conversation_call?(entry) }.to_h do |entry|
    call = parse_tool_calls([
                              { 'id' => entry['tool_call_id'], 'type' => 'function',
                                'function' => entry.slice('name', 'arguments') }
                            ], response: raw, finish_reason: :tool_calls).values.first
    [call.id, call]
  end
end

.parse_conversation_steps(output) ⇒ Object



139
140
141
142
143
144
145
146
147
# File 'lib/ruby_llm/protocols/mistral/conversations/chat.rb', line 139

def parse_conversation_steps(output)
  output.filter_map do |entry|
    next unless entry['type'] == 'tool.execution' ||
                (entry['type'] == 'function.call' && !pending_conversation_call?(entry))

    ServerToolCall.new(type: entry['type'], name: entry['name'], id: entry['id'],
                       input: entry['arguments'], result: entry['info'], raw: entry)
  end
end

.parse_conversation_usage(usage) ⇒ Object



149
150
151
152
153
154
155
# File 'lib/ruby_llm/protocols/mistral/conversations/chat.rb', line 149

def parse_conversation_usage(usage)
  input = usage['prompt_tokens'] && (usage['prompt_tokens'] + usage.fetch('connector_tokens', 0).to_i)
  {
    input_tokens: input,
    output_tokens: usage['completion_tokens'], server_tool_use: usage['connectors']
  }
end

.pending_conversation_call?(entry) ⇒ Boolean

Returns:

  • (Boolean)


135
136
137
# File 'lib/ruby_llm/protocols/mistral/conversations/chat.rb', line 135

def pending_conversation_call?(entry)
  entry['type'] == 'function.call' && entry['confirmation_status'].nil?
end

.renderObject



14
15
16
17
18
19
20
21
22
# File 'lib/ruby_llm/protocols/mistral/conversations/chat.rb', line 14

def render(...)
  payload = super
  payload[:tools] = Support::Utils.deep_stringify_keys(payload[:tools]).uniq
  if payload[:tools].any? { |tool| Array(tool.dig('tool_configuration', 'requires_confirmation')).any? }
    raise ArgumentError, 'Mistral hosted tool confirmations require provider conversation storage'
  end

  payload
end

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



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/ruby_llm/protocols/mistral/conversations/chat.rb', line 24

def render_payload(messages, tools:, temperature:, model:, stream: false, max_output_tokens: nil,
                   schema: nil, thinking: nil, tool_prefs: nil, **)
  options = super(messages, tools:, temperature:, model:, stream:, max_output_tokens:,
                            schema:, thinking:, citations: false, caching: nil, tool_prefs:)
  normalize_conversation_choice(options)
  {
    model: model.id,
    inputs: format_entries(messages),
    instructions: messages.select { |message| message.role == :system }.map(&:content).join("\n\n"),
    completion_args: options.slice(:temperature, :max_tokens, :response_format, :reasoning_effort,
                                   :tool_choice),
    tools: options.fetch(:tools, []),
    store: false,
    stream: stream
  }
end

.replay_conversation_entry(entry, index) ⇒ Object



63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/ruby_llm/protocols/mistral/conversations/chat.rb', line 63

def replay_conversation_entry(entry, index)
  return replay_conversation_execution(entry, index) if entry['type'] == 'tool.execution'

  result = entry.except('id', 'object', 'created_at', 'completed_at')
  if entry['type'] == 'message.output' && entry['content'].is_a?(Array)
    result['content'] = entry['content'].map do |part|
      next part unless part['type'] == 'tool_reference'

      { 'type' => 'text', 'text' => "[#{part['title']}](#{part['url']})" }
    end
  end
  [result]
end

.replay_conversation_execution(entry, index) ⇒ Object



77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/ruby_llm/protocols/mistral/conversations/chat.rb', line 77

def replay_conversation_execution(entry, index)
  identity = entry['id'] || "#{index}:#{JSON.generate(entry)}"
  id = entry['tool_call_id'] || Digest::SHA256.hexdigest(identity)[0, 9]
  info = entry['info']
  result = info.is_a?(Hash) && info.key?('result') ? info['result'] : info
  [
    { 'type' => 'function.call', 'tool_call_id' => id,
      'name' => entry['function'] || entry['name'], 'arguments' => entry['arguments'] },
    { 'type' => 'function.result', 'tool_call_id' => id,
      'result' => result.is_a?(String) ? result : JSON.generate(result) }
  ]
end