smart_prompt 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
- require "anthropic"
2
- require "base64"
3
- require "uri"
4
- require "json"
1
+ require "anthropic"
2
+ require "base64"
3
+ require "uri"
4
+ require "json"
5
5
 
6
6
  module SmartPrompt
7
7
  class AnthropicAdapter < LLMAdapter
@@ -78,7 +78,10 @@ module SmartPrompt
78
78
  content
79
79
  end
80
80
 
81
- { role: role, content: [converted_content] }
81
+ # String/scalar content becomes a single-element block array;
82
+ # already-array (multimodal) content must not be double-wrapped.
83
+ final_content = converted_content.is_a?(Array) ? converted_content : [converted_content]
84
+ { role: role, content: final_content }
82
85
  end
83
86
  end
84
87
 
@@ -148,139 +151,163 @@ module SmartPrompt
148
151
  input_schema: parameters,
149
152
  }
150
153
  end.compact # Remove nil values from failed conversions
151
- end
152
-
153
- def convert_response_to_openai_format(response)
154
- begin
155
- # Normalize response to a Hash with symbol keys
156
- raw_response = if response.respond_to?(:to_h)
157
- response.to_h
158
- elsif response.is_a?(Hash)
159
- response
160
- else
161
- JSON.parse(response.to_json)
162
- end
163
-
164
- response_hash = deep_symbolize(raw_response)
165
-
166
- # Handle content blocks (text, tool_use, etc.)
167
- content_blocks = response_hash[:content] || []
168
- text_content = ""
169
- tool_calls = []
170
-
171
- case content_blocks
172
- when String
173
- text_content = content_blocks
174
- when Array
175
- content_blocks.each do |block|
176
- block_hash = block.respond_to?(:to_h) ? block.to_h : block
177
- block_hash = deep_symbolize(block_hash)
178
- next unless block_hash.is_a?(Hash)
179
-
180
- case block_hash[:type]
181
- when "text"
182
- text_content << block_hash[:text].to_s
183
- when "tool_use"
184
- tool_calls << {
185
- "index" => tool_calls.size,
186
- "id" => block_hash[:id] || "tool_call_#{tool_calls.size}",
187
- "type" => "function",
188
- "function" => {
189
- "name" => block_hash[:name],
190
- "arguments" => JSON.generate(block_hash[:input] || {}),
191
- },
192
- }
193
- end
194
- end
195
- else
196
- text_content = content_blocks.to_s
197
- end
198
-
199
- # Map stop reason to OpenAI finish_reason semantics
200
- stop_reason = response_hash[:stop_reason] || response_hash[:finish_reason]
201
- finish_reason = case stop_reason
202
- when "tool_use"
203
- "tool_calls"
204
- when "end_turn", nil
205
- "stop"
206
- else
207
- stop_reason
208
- end
209
-
210
- # Map usage information
211
- usage = response_hash[:usage] || {}
212
- prompt_tokens = usage[:input_tokens]
213
- completion_tokens = usage[:output_tokens]
214
- cache_read_tokens = usage[:cache_read_input_tokens]
215
- cache_creation_tokens = usage[:cache_creation_input_tokens]
216
- total_tokens = if prompt_tokens || completion_tokens
217
- [prompt_tokens, completion_tokens].compact.sum
218
- end
219
- prompt_cache_hit_tokens = cache_read_tokens
220
- prompt_cache_miss_tokens = if prompt_tokens && cache_read_tokens
221
- prompt_tokens - cache_read_tokens
222
- end
223
- prompt_tokens_details = {}
224
- prompt_tokens_details["cached_tokens"] = cache_read_tokens if cache_read_tokens
225
-
226
- usage_hash = {}
227
- usage_hash["prompt_tokens"] = prompt_tokens if prompt_tokens
228
- usage_hash["completion_tokens"] = completion_tokens if completion_tokens
229
- usage_hash["total_tokens"] = total_tokens if total_tokens
230
- usage_hash["prompt_tokens_details"] = prompt_tokens_details unless prompt_tokens_details.empty?
231
- usage_hash["prompt_cache_hit_tokens"] = prompt_cache_hit_tokens if prompt_cache_hit_tokens
232
- usage_hash["prompt_cache_miss_tokens"] = prompt_cache_miss_tokens if prompt_cache_miss_tokens
233
-
234
- created_ts = response_hash[:created_at] || response_hash[:created] || Time.now.to_i
235
-
236
- message_role = response_hash[:role] || "assistant"
237
-
238
- openai_response = {
239
- "id" => response_hash[:id],
240
- "object" => "chat.completion",
241
- "created" => created_ts,
242
- "model" => response_hash[:model],
243
- "choices" => [
244
- {
245
- "index" => 0,
246
- "message" => {
247
- "role" => message_role,
248
- "content" => text_content.empty? ? nil : text_content,
249
- },
250
- "finish_reason" => finish_reason,
251
- },
252
- ],
253
- }
254
-
255
- unless tool_calls.empty?
256
- openai_response["choices"][0]["message"]["tool_calls"] = tool_calls
257
- end
258
-
259
- openai_response["usage"] = usage_hash unless usage_hash.empty?
260
- openai_response["system_fingerprint"] = response_hash[:system_fingerprint] if response_hash[:system_fingerprint]
261
-
262
- @last_response = openai_response
263
- openai_response
264
- rescue => e
265
- SmartPrompt.logger.error "Failed to convert Anthropic response: #{e.message}"
266
- raise LLMAPIError, "Failed to convert Anthropic response: #{e.message}"
267
- end
268
- end
269
-
270
- # Deeply symbolize hash keys for consistent access
271
- def deep_symbolize(obj)
272
- case obj
273
- when Hash
274
- obj.each_with_object({}) do |(k, v), memo|
275
- key = k.is_a?(String) || k.is_a?(Symbol) ? k.to_sym : k
276
- memo[key] = deep_symbolize(v)
277
- end
278
- when Array
279
- obj.map { |item| deep_symbolize(item) }
280
- else
281
- obj
282
- end
283
- end
154
+ end
155
+
156
+ # Extract plain text from an Anthropic response's `content` field.
157
+ # Handles a String, an Array of content blocks, nil, or an empty array.
158
+ # @param response [Hash] Anthropic response (or its `content` value)
159
+ # @return [String] Concatenated text, with multiple text blocks joined by newlines
160
+ def extract_content_from_response(response)
161
+ content = if response.is_a?(Hash)
162
+ response["content"] || response[:content]
163
+ else
164
+ response
165
+ end
166
+
167
+ case content
168
+ when String
169
+ content
170
+ when Array
171
+ content.map do |block|
172
+ next block unless block.is_a?(Hash)
173
+ block["text"] || block[:text]
174
+ end.compact.reject(&:empty?).join("\n")
175
+ else
176
+ content.to_s
177
+ end
178
+ end
179
+
180
+ def convert_response_to_openai_format(response)
181
+ begin
182
+ # Normalize response to a Hash with symbol keys
183
+ raw_response = if response.respond_to?(:to_h)
184
+ response.to_h
185
+ elsif response.is_a?(Hash)
186
+ response
187
+ else
188
+ JSON.parse(response.to_json)
189
+ end
190
+
191
+ response_hash = deep_symbolize(raw_response)
192
+
193
+ # Handle content blocks (text, tool_use, etc.)
194
+ content_blocks = response_hash[:content] || []
195
+ text_content = ""
196
+ tool_calls = []
197
+
198
+ case content_blocks
199
+ when String
200
+ text_content = content_blocks
201
+ when Array
202
+ content_blocks.each do |block|
203
+ block_hash = block.respond_to?(:to_h) ? block.to_h : block
204
+ block_hash = deep_symbolize(block_hash)
205
+ next unless block_hash.is_a?(Hash)
206
+
207
+ case block_hash[:type]
208
+ when "text"
209
+ text_content << block_hash[:text].to_s
210
+ when "tool_use"
211
+ tool_calls << {
212
+ "index" => tool_calls.size,
213
+ "id" => block_hash[:id] || "tool_call_#{tool_calls.size}",
214
+ "type" => "function",
215
+ "function" => {
216
+ "name" => block_hash[:name],
217
+ "arguments" => JSON.generate(block_hash[:input] || {}),
218
+ },
219
+ }
220
+ end
221
+ end
222
+ else
223
+ text_content = content_blocks.to_s
224
+ end
225
+
226
+ # Map stop reason to OpenAI finish_reason semantics
227
+ stop_reason = response_hash[:stop_reason] || response_hash[:finish_reason]
228
+ finish_reason = case stop_reason
229
+ when "tool_use"
230
+ "tool_calls"
231
+ when "end_turn", nil
232
+ "stop"
233
+ else
234
+ stop_reason
235
+ end
236
+
237
+ # Map usage information
238
+ usage = response_hash[:usage] || {}
239
+ prompt_tokens = usage[:input_tokens]
240
+ completion_tokens = usage[:output_tokens]
241
+ cache_read_tokens = usage[:cache_read_input_tokens]
242
+ cache_creation_tokens = usage[:cache_creation_input_tokens]
243
+ total_tokens = if prompt_tokens || completion_tokens
244
+ [prompt_tokens, completion_tokens].compact.sum
245
+ end
246
+ prompt_cache_hit_tokens = cache_read_tokens
247
+ prompt_cache_miss_tokens = if prompt_tokens && cache_read_tokens
248
+ prompt_tokens - cache_read_tokens
249
+ end
250
+ prompt_tokens_details = {}
251
+ prompt_tokens_details["cached_tokens"] = cache_read_tokens if cache_read_tokens
252
+
253
+ usage_hash = {}
254
+ usage_hash["prompt_tokens"] = prompt_tokens if prompt_tokens
255
+ usage_hash["completion_tokens"] = completion_tokens if completion_tokens
256
+ usage_hash["total_tokens"] = total_tokens if total_tokens
257
+ usage_hash["prompt_tokens_details"] = prompt_tokens_details unless prompt_tokens_details.empty?
258
+ usage_hash["prompt_cache_hit_tokens"] = prompt_cache_hit_tokens if prompt_cache_hit_tokens
259
+ usage_hash["prompt_cache_miss_tokens"] = prompt_cache_miss_tokens if prompt_cache_miss_tokens
260
+
261
+ created_ts = response_hash[:created_at] || response_hash[:created] || Time.now.to_i
262
+
263
+ message_role = response_hash[:role] || "assistant"
264
+
265
+ openai_response = {
266
+ "id" => response_hash[:id],
267
+ "object" => "chat.completion",
268
+ "created" => created_ts,
269
+ "model" => response_hash[:model],
270
+ "choices" => [
271
+ {
272
+ "index" => 0,
273
+ "message" => {
274
+ "role" => message_role,
275
+ "content" => text_content.empty? ? nil : text_content,
276
+ },
277
+ "finish_reason" => finish_reason,
278
+ },
279
+ ],
280
+ }
281
+
282
+ unless tool_calls.empty?
283
+ openai_response["choices"][0]["message"]["tool_calls"] = tool_calls
284
+ end
285
+
286
+ openai_response["usage"] = usage_hash unless usage_hash.empty?
287
+ openai_response["system_fingerprint"] = response_hash[:system_fingerprint] if response_hash[:system_fingerprint]
288
+
289
+ @last_response = openai_response
290
+ openai_response
291
+ rescue => e
292
+ SmartPrompt.logger.error "Failed to convert Anthropic response: #{e.message}"
293
+ raise LLMAPIError, "Failed to convert Anthropic response: #{e.message}"
294
+ end
295
+ end
296
+
297
+ # Deeply symbolize hash keys for consistent access
298
+ def deep_symbolize(obj)
299
+ case obj
300
+ when Hash
301
+ obj.each_with_object({}) do |(k, v), memo|
302
+ key = k.is_a?(String) || k.is_a?(Symbol) ? k.to_sym : k
303
+ memo[key] = deep_symbolize(v)
304
+ end
305
+ when Array
306
+ obj.map { |item| deep_symbolize(item) }
307
+ else
308
+ obj
309
+ end
310
+ end
284
311
 
285
312
  public
286
313
 
@@ -290,8 +317,8 @@ module SmartPrompt
290
317
  # @param temperature [Float, nil] Temperature value (optional, uses config or 0.7 if nil)
291
318
  # @param tools [Array, nil] Array of tool definitions (optional)
292
319
  # @param proc [Proc, nil] Callback for streaming responses (optional)
293
- # @return [Hash, nil] OpenAI-formatted response (nil for streaming mode)
294
- def send_request(messages, model = nil, temperature = nil, tools = nil, proc = nil)
320
+ # @return [Hash, nil] OpenAI-formatted response (nil for streaming mode)
321
+ def send_request(messages, model = nil, temperature = nil, tools = nil, proc = nil)
295
322
  begin
296
323
  # Determine model name (parameter > config)
297
324
  model_name = model || @config["model"]
@@ -1,10 +1,23 @@
1
1
  require "yaml"
2
2
  require "retriable"
3
3
  require "numo/narray"
4
+ require "base64"
4
5
 
5
6
  module SmartPrompt
6
7
  class Conversation
7
8
  include APIHandler
9
+ MODEL_REQUEST_OPTION_KEYS = %w[
10
+ max_tokens
11
+ max_completion_tokens
12
+ top_p
13
+ top_k
14
+ response_format
15
+ tool_choice
16
+ parallel_tool_calls
17
+ seed
18
+ stop
19
+ ].freeze
20
+
8
21
  attr_reader :messages, :last_response, :config_file
9
22
  attr_reader :last_call_id
10
23
  attr_reader :session_id
@@ -15,34 +28,66 @@ module SmartPrompt
15
28
  @engine = engine
16
29
  @adapters = engine.adapters
17
30
  @llms = engine.llms
31
+ @models = engine.models
18
32
  @current_llm_name = nil
19
33
  @templates = engine.templates
20
34
  @temperature = 0.7
21
35
  @current_adapter = engine.current_adapter
22
36
  @last_response = nil
23
37
  @tools = tools
38
+ @request_options = {}
39
+ @pending_content_parts = []
40
+ @thinking_enabled = nil
24
41
  @session_id = session_id
25
42
  @use_history_manager = false
26
43
  end
27
44
 
28
45
  def use(llm_name)
29
- raise "LLM #{llm_name} not configured" unless @llms.key?(llm_name)
46
+ llm_name = llm_name.to_s
47
+ raise ConfigurationError, "LLM #{llm_name} not configured" unless @llms.key?(llm_name)
30
48
  @current_llm = @llms[llm_name]
31
49
  @current_llm_name = llm_name
32
50
  self
33
51
  end
34
52
 
53
+ def use_model(model_name)
54
+ model_name = model_name.to_s
55
+ model_config = @models[model_name] || @models[model_name.to_sym]
56
+ raise ConfigurationError, "Model #{model_name} not configured" unless model_config
57
+
58
+ llm_name = model_config["use"] || model_config[:use]
59
+ configured_model_name = model_config["model"] || model_config[:model]
60
+ raise ConfigurationError, "Model #{model_name} must define use" if llm_name.nil? || llm_name.empty?
61
+ raise ConfigurationError, "Model #{model_name} must define model" if configured_model_name.nil? || configured_model_name.empty?
62
+
63
+ use(llm_name)
64
+ model(configured_model_name)
65
+ merge_model_request_options(model_config)
66
+ self
67
+ end
68
+
35
69
  def model(model_name)
36
70
  @model_name = model_name
37
- if @engine.config["better_prompt_db"]
38
- BetterPrompt.add_model(@current_llm_name, @model_name)
39
- end
40
71
  end
41
72
 
42
73
  def temperature(temperature)
43
74
  @temperature = temperature
44
75
  end
45
76
 
77
+ def request_options(options = {})
78
+ @request_options.merge!(options || {})
79
+ self
80
+ end
81
+
82
+ def thinking(enabled = true)
83
+ @thinking_enabled = enabled
84
+ if @sys_msg
85
+ @sys_msg = thinking_system_message(@sys_msg)
86
+ refresh_system_message(@sys_msg)
87
+ end
88
+ self
89
+ end
90
+
46
91
  def history_messages
47
92
  # If using HistoryManager, get messages from session
48
93
  if @use_history_manager && @engine.history_manager
@@ -77,32 +122,43 @@ module SmartPrompt
77
122
  SmartPrompt.logger.info "Use template #{template_name}"
78
123
  raise "Template #{template_name} not found" unless @templates.key?(template_name)
79
124
  content = @templates[template_name].render(params)
80
- add_message({ role: "user", content: content }, with_history)
81
- if @engine.config["better_prompt_db"]
82
- BetterPrompt.add_prompt(template_name, "user", content)
83
- end
125
+ add_user_content(content, with_history)
84
126
  self
85
127
  else
86
- add_message({ role: "user", content: template_name }, with_history)
87
- if @engine.config["better_prompt_db"]
88
- BetterPrompt.add_prompt("NULL", "user", template_name)
89
- end
128
+ add_user_content(template_name, with_history)
90
129
  self
91
130
  end
92
131
  end
93
132
 
94
133
  def sys_msg(message, params = {})
95
- @sys_msg = message
96
- add_message({ role: "system", content: message }, params[:with_history])
97
- if @engine.config["better_prompt_db"]
98
- BetterPrompt.add_prompt("NULL", "system", message)
99
- end
134
+ @sys_msg = thinking_system_message(message)
135
+ add_message({ role: "system", content: @sys_msg }, params[:with_history])
136
+ self
137
+ end
138
+
139
+ def multimodal_prompt(parts, with_history: false)
140
+ add_message({ role: "user", content: normalize_content_parts(parts) }, with_history)
141
+ self
142
+ end
143
+
144
+ def image(source, token_budget: nil, **metadata)
145
+ @pending_content_parts << media_part("image", source, token_budget: token_budget, **metadata)
146
+ self
147
+ end
148
+
149
+ def audio(source, **metadata)
150
+ @pending_content_parts << media_part("audio", source, **metadata)
151
+ self
152
+ end
153
+
154
+ def video(source, fps: nil, max_seconds: nil, **metadata)
155
+ @pending_content_parts << media_part("video", source, fps: fps, max_seconds: max_seconds, **metadata)
100
156
  self
101
157
  end
102
158
 
103
159
  def send_msg_once
104
160
  raise "No LLM selected" if @current_llm.nil?
105
- @last_response = @current_llm.send_request(@messages, @model_name, @temperature)
161
+ @last_response = send_llm_request(@messages, nil)
106
162
  @messages = []
107
163
  @messages << { role: "system", content: @sys_msg }
108
164
  @last_response
@@ -120,24 +176,14 @@ module SmartPrompt
120
176
  def send_msg(params = {})
121
177
  Retriable.retriable(RETRY_OPTIONS) do
122
178
  raise ConfigurationError, "No LLM selected" if @current_llm.nil?
123
- if @engine.config["better_prompt_db"]
124
- if params[:with_history]
125
- @last_call_id = BetterPrompt.add_model_call(@current_llm_name, @model_name, history_messages, false, @temperature, 0, 0.0, 0, @tools)
126
- else
127
- @last_call_id = BetterPrompt.add_model_call(@current_llm_name, @model_name, @messages, false, @temperature, 0, 0.0, 0, @tools)
128
- end
129
- end
130
179
  if params[:with_history]
131
- @last_response = @current_llm.send_request(history_messages, @model_name, @temperature, @tools, nil)
180
+ @last_response = send_llm_request(history_messages, nil)
132
181
  else
133
- @last_response = @current_llm.send_request(@messages, @model_name, @temperature, @tools, nil)
182
+ @last_response = send_llm_request(@messages, nil)
134
183
  end
135
184
  if @last_response == ""
136
185
  @last_response = @current_llm.last_response
137
186
  end
138
- if @engine.config["better_prompt_db"]
139
- BetterPrompt.add_response(@last_call_id, @last_response, false)
140
- end
141
187
  @messages = []
142
188
  @messages << { role: "system", content: @sys_msg }
143
189
  @last_response
@@ -149,20 +195,10 @@ module SmartPrompt
149
195
  def send_msg_by_stream(params = {}, &proc)
150
196
  Retriable.retriable(RETRY_OPTIONS) do
151
197
  raise ConfigurationError, "No LLM selected" if @current_llm.nil?
152
- if @engine.config["better_prompt_db"]
153
- if params[:with_history]
154
- @last_call_id = BetterPrompt.add_model_call(@current_llm_name, @model_name, history_messages, true, @temperature, 0, 0.0, 0, @tools)
155
- else
156
- @last_call_id = BetterPrompt.add_model_call(@current_llm_name, @model_name, @messages, true, @temperature, 0, 0.0, 0, @tools)
157
- end
158
- end
159
198
  if params[:with_history]
160
- @current_llm.send_request(history_messages, @model_name, @temperature, @tools, proc)
199
+ send_llm_request(history_messages, proc)
161
200
  else
162
- @current_llm.send_request(@messages, @model_name, @temperature, @tools, proc)
163
- end
164
- if @engine.config["better_prompt_db"]
165
- BetterPrompt.add_response(@last_call_id, @engine.stream_response, true)
201
+ send_llm_request(@messages, proc)
166
202
  end
167
203
  @messages = []
168
204
  @messages << { role: "system", content: @sys_msg }
@@ -197,6 +233,123 @@ module SmartPrompt
197
233
  end
198
234
  end
199
235
 
236
+ private
237
+
238
+ def send_llm_request(messages, proc)
239
+ parameters = @current_llm.method(:send_request).parameters
240
+ if parameters.length >= 6
241
+ @current_llm.send_request(messages, @model_name, @temperature, @tools, proc, @request_options)
242
+ else
243
+ @current_llm.send_request(messages, @model_name, @temperature, @tools, proc)
244
+ end
245
+ end
246
+
247
+ def merge_model_request_options(model_config)
248
+ explicit_options = model_config["request_options"] || model_config[:request_options] || {}
249
+ @request_options.merge!(explicit_options)
250
+ MODEL_REQUEST_OPTION_KEYS.each do |key|
251
+ value = model_config[key] || model_config[key.to_sym]
252
+ @request_options[key.to_sym] = value unless value.nil?
253
+ end
254
+ end
255
+
256
+ def add_user_content(content, with_history)
257
+ if @pending_content_parts.empty?
258
+ add_message({ role: "user", content: content }, with_history)
259
+ else
260
+ add_message({ role: "user", content: multimodal_content(content) }, with_history)
261
+ @pending_content_parts = []
262
+ end
263
+ end
264
+
265
+ def multimodal_content(text)
266
+ parts = @pending_content_parts
267
+ images_and_videos = parts.select { |part| ["image_url", "image", "video_url", "video"].include?(part[:type] || part["type"]) }
268
+ audio_parts = parts.select { |part| ["input_audio", "audio"].include?(part[:type] || part["type"]) }
269
+ other_parts = parts - images_and_videos - audio_parts
270
+ normalize_content_parts(images_and_videos + other_parts + [{ type: "text", text: text.to_s }] + audio_parts)
271
+ end
272
+
273
+ def normalize_content_parts(parts)
274
+ parts.map do |part|
275
+ normalized = part.transform_keys(&:to_s)
276
+ normalized["text"] = normalized.delete("content") if normalized["type"] == "text" && normalized.key?("content")
277
+ normalized
278
+ end
279
+ end
280
+
281
+ def media_part(type, source, **metadata)
282
+ case type
283
+ when "image"
284
+ mime_type = detect_image_mime(source)
285
+ data = File.binread(source)
286
+ base64_data = Base64.strict_encode64(data)
287
+ url = "data:#{mime_type};base64,#{base64_data}"
288
+ part = { type: "image_url", image_url: { url: url } }
289
+ when "audio"
290
+ format = detect_audio_format(source)
291
+ data = File.binread(source)
292
+ base64_data = Base64.strict_encode64(data)
293
+ part = { type: "input_audio", input_audio: { data: base64_data, format: format } }
294
+ when "video"
295
+ mime_type = detect_video_mime(source)
296
+ data = File.binread(source)
297
+ base64_data = Base64.strict_encode64(data)
298
+ url = "data:#{mime_type};base64,#{base64_data}"
299
+ part = { type: "video_url", video_url: { url: url } }
300
+ else
301
+ part = { type: type }
302
+ end
303
+ metadata.each do |key, value|
304
+ part[key] = value unless value.nil?
305
+ end
306
+ part
307
+ end
308
+
309
+ def detect_image_mime(path)
310
+ ext = File.extname(path).downcase
311
+ case ext
312
+ when ".png" then "image/png"
313
+ when ".jpg", ".jpeg" then "image/jpeg"
314
+ when ".gif" then "image/gif"
315
+ when ".webp" then "image/webp"
316
+ when ".bmp" then "image/bmp"
317
+ when ".svg" then "image/svg+xml"
318
+ else "application/octet-stream"
319
+ end
320
+ end
321
+
322
+ def detect_audio_format(path)
323
+ ext = File.extname(path).downcase.delete_prefix(".")
324
+ %w[wav mp3 ogg flac aac m4a].include?(ext) ? ext : "wav"
325
+ end
326
+
327
+ def detect_video_mime(path)
328
+ ext = File.extname(path).downcase
329
+ case ext
330
+ when ".mp4" then "video/mp4"
331
+ when ".webm" then "video/webm"
332
+ when ".mov" then "video/quicktime"
333
+ when ".avi" then "video/x-msvideo"
334
+ else "application/octet-stream"
335
+ end
336
+ end
337
+
338
+ def thinking_system_message(message)
339
+ message = message.to_s.sub(/\A<\|think\|>\n?/, "")
340
+ return message if @thinking_enabled == false
341
+ return message unless @thinking_enabled == true
342
+
343
+ "<|think|>\n#{message}"
344
+ end
345
+
346
+ def refresh_system_message(message)
347
+ system_message = @messages.find { |item| (item[:role] || item["role"]) == "system" }
348
+ system_message[:content] = message if system_message
349
+ end
350
+
351
+ public
352
+
200
353
  def generate_image(prompt, params = {})
201
354
  @current_llm.generate_image(prompt, params)
202
355
  end