smart_prompt 0.4.4 → 0.5.0

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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -10
  3. data/README.cn.md +307 -64
  4. data/README.md +311 -64
  5. data/Rakefile +10 -1
  6. data/config/anthropic_config.yml +151 -0
  7. data/config/image_generation_config.yml +22 -0
  8. data/config/multimodal_config.yml +85 -0
  9. data/config/sensenova_config.yml +63 -0
  10. data/config/zhipu_config.yml +73 -0
  11. data/examples/anthropic_basic_chat.rb +143 -0
  12. data/examples/anthropic_example.rb +232 -0
  13. data/examples/anthropic_multimodal.rb +212 -0
  14. data/examples/anthropic_streaming.rb +312 -0
  15. data/examples/anthropic_tool_calling.rb +393 -0
  16. data/examples/automatic_cleanup_example.rb +109 -0
  17. data/examples/history_management_examples.rb +522 -0
  18. data/examples/image_generation_example.rb +130 -0
  19. data/examples/monitoring_example.rb +121 -0
  20. data/examples/multimodal_example.rb +63 -0
  21. data/examples/relevance_based_strategy_example.rb +87 -0
  22. data/examples/sensenova_example.rb +129 -0
  23. data/examples/stt_example.rb +287 -0
  24. data/examples/tts_example.rb +244 -0
  25. data/examples/video_generation_example.rb +189 -0
  26. data/examples/zhipu_example.rb +151 -0
  27. data/lib/smart_prompt/anthropic_adapter.rb +363 -281
  28. data/lib/smart_prompt/compression_engine.rb +201 -0
  29. data/lib/smart_prompt/context_strategy.rb +22 -0
  30. data/lib/smart_prompt/conversation.rb +81 -191
  31. data/lib/smart_prompt/engine.rb +36 -19
  32. data/lib/smart_prompt/history_manager.rb +596 -0
  33. data/lib/smart_prompt/hybrid_strategy.rb +222 -0
  34. data/lib/smart_prompt/image_generation_adapter.rb +297 -0
  35. data/lib/smart_prompt/lru_cache.rb +133 -0
  36. data/lib/smart_prompt/message.rb +57 -0
  37. data/lib/smart_prompt/multimodal_adapter.rb +277 -0
  38. data/lib/smart_prompt/openai_adapter.rb +1 -25
  39. data/lib/smart_prompt/persistence_layer.rb +197 -0
  40. data/lib/smart_prompt/relevance_based_strategy.rb +221 -0
  41. data/lib/smart_prompt/sensenova_adapter.rb +410 -0
  42. data/lib/smart_prompt/session.rb +140 -0
  43. data/lib/smart_prompt/sliding_window_strategy.rb +100 -0
  44. data/lib/smart_prompt/stt_adapter.rb +381 -0
  45. data/lib/smart_prompt/summary_based_strategy.rb +152 -0
  46. data/lib/smart_prompt/token_counter.rb +74 -0
  47. data/lib/smart_prompt/tts_adapter.rb +403 -0
  48. data/lib/smart_prompt/version.rb +1 -1
  49. data/lib/smart_prompt/video_generation_adapter.rb +330 -0
  50. data/lib/smart_prompt/worker.rb +25 -3
  51. data/lib/smart_prompt/zhipu_adapter.rb +616 -0
  52. data/lib/smart_prompt.rb +22 -2
  53. data/workers/history_management_examples.rb +407 -0
  54. data/workers/image_generation_workers.rb +119 -0
  55. data/workers/multimodal_workers.rb +110 -0
  56. data/workers/sensenova_workers.rb +62 -0
  57. data/workers/stt_workers.rb +195 -0
  58. data/workers/tts_workers.rb +388 -0
  59. data/workers/video_generation_workers.rb +264 -0
  60. data/workers/zhipu_workers.rb +113 -0
  61. metadata +84 -8
@@ -0,0 +1,201 @@
1
+ module SmartPrompt
2
+ # CompressionEngine handles automatic compression of conversation history
3
+ # through summarization using an LLM adapter
4
+ #
5
+ # This engine:
6
+ # - Generates summaries of older messages to reduce token usage
7
+ # - Preserves key facts, decisions, and context in summaries
8
+ # - Falls back to truncation strategies when summarization fails
9
+ # - Tracks compression metrics for monitoring
10
+ class CompressionEngine
11
+ attr_reader :config
12
+
13
+ # Initialize the compression engine
14
+ # @param config [Hash] Configuration options
15
+ # @option config [LLMAdapter] :llm_adapter LLM adapter for generating summaries
16
+ # @option config [String] :prompt Custom summarization prompt template
17
+ # @option config [Float] :compression_ratio (0.5) Target compression ratio
18
+ # @option config [Integer] :min_messages_to_compress (5) Minimum messages needed for compression
19
+ def initialize(config = {})
20
+ @config = config
21
+ @llm_adapter = config[:llm_adapter]
22
+ @summarization_prompt = config[:prompt] || default_prompt
23
+ @compression_ratio = config[:compression_ratio] || 0.5
24
+ @min_messages_to_compress = config[:min_messages_to_compress] || 5
25
+ @token_counter = TokenCounter.new
26
+ end
27
+
28
+ # Summarize a collection of messages into a single summary message
29
+ # @param messages [Array<Message>] Messages to summarize
30
+ # @return [Message, nil] Summary message or nil if summarization fails
31
+ def summarize(messages)
32
+ return nil if messages.nil? || messages.empty?
33
+ return nil if messages.length < @min_messages_to_compress
34
+
35
+ # Build the content to summarize
36
+ content = messages.map { |msg| "#{msg.role}: #{msg.content}" }.join("\n")
37
+
38
+ # Create the summarization prompt
39
+ prompt = @summarization_prompt.gsub("{content}", content)
40
+
41
+ begin
42
+ # Call LLM to generate summary
43
+ summary_text = if @llm_adapter
44
+ @llm_adapter.send_request([
45
+ { role: "user", content: prompt }
46
+ ])
47
+ else
48
+ # If no LLM adapter, create a simple summary
49
+ create_fallback_summary(messages)
50
+ end
51
+
52
+ # Calculate original token count
53
+ original_tokens = messages.sum { |msg| msg.token_count || @token_counter.count(msg.content) }
54
+
55
+ # Create summary message
56
+ summary_message = Message.new(
57
+ role: "system",
58
+ content: "[Summary of previous conversation]\n#{summary_text}",
59
+ is_summary: true,
60
+ metadata: {
61
+ original_count: messages.count,
62
+ original_tokens: original_tokens,
63
+ compressed_at: Time.now.iso8601
64
+ }
65
+ )
66
+
67
+ # Calculate tokens for the summary
68
+ summary_message.calculate_tokens(@token_counter)
69
+
70
+ SmartPrompt.logger.info "Compressed #{messages.count} messages (#{original_tokens} tokens) " \
71
+ "into summary (#{summary_message.token_count} tokens)"
72
+
73
+ summary_message
74
+ rescue => e
75
+ SmartPrompt.logger.error "Summarization failed: #{e.message}\n#{e.backtrace.join("\n")}"
76
+ nil
77
+ end
78
+ end
79
+
80
+ # Compress a session by identifying and summarizing compressible segments
81
+ # @param session [Session] The session to compress
82
+ # @return [Boolean] true if compression was successful
83
+ def compress(session)
84
+ return false if session.nil? || session.messages.empty?
85
+
86
+ begin
87
+ # Identify compressible message segments
88
+ compressible_segments = identify_compressible_segments(session.messages)
89
+
90
+ return false if compressible_segments.empty?
91
+
92
+ # Generate summaries for each segment
93
+ summaries = compressible_segments.map { |segment| summarize(segment) }.compact
94
+
95
+ return false if summaries.empty?
96
+
97
+ # Replace original messages with summaries
98
+ replace_with_summaries(session, compressible_segments, summaries)
99
+
100
+ SmartPrompt.logger.info "Session #{session.id} compressed: #{compressible_segments.flatten.count} " \
101
+ "messages replaced with #{summaries.count} summaries"
102
+ true
103
+ rescue => e
104
+ SmartPrompt.logger.error "Compression failed for session #{session.id}: #{e.message}"
105
+
106
+ # Fall back to truncation strategy
107
+ fallback_truncate(session)
108
+ false
109
+ end
110
+ end
111
+
112
+ # Check if a session should be compressed based on configuration
113
+ # @param session [Session] The session to evaluate
114
+ # @return [Boolean] true if compression is recommended
115
+ def should_compress?(session)
116
+ return false if session.nil?
117
+
118
+ # Check if session has enough messages to warrant compression
119
+ session.message_count > (@min_messages_to_compress * 2)
120
+ end
121
+
122
+ private
123
+
124
+ # Default summarization prompt template
125
+ def default_prompt
126
+ "Please provide a concise summary of the following conversation, " \
127
+ "preserving key facts, decisions, and context. Focus on the most important " \
128
+ "information that would be needed to continue the conversation:\n\n{content}"
129
+ end
130
+
131
+ # Create a simple fallback summary when LLM is not available
132
+ # @param messages [Array<Message>] Messages to summarize
133
+ # @return [String] Simple summary text
134
+ def create_fallback_summary(messages)
135
+ "Previous conversation contained #{messages.count} messages covering various topics."
136
+ end
137
+
138
+ # Identify segments of messages that can be compressed
139
+ # Strategy: Keep recent messages, compress older ones
140
+ # @param messages [Array<Message>] All messages in the session
141
+ # @return [Array<Array<Message>>] Array of message segments to compress
142
+ def identify_compressible_segments(messages)
143
+ return [] if messages.length <= @min_messages_to_compress
144
+
145
+ # Keep the most recent 5 messages uncompressed
146
+ keep_recent = 5
147
+
148
+ # Separate system messages (never compress) from others
149
+ system_messages = messages.select(&:system_message?)
150
+ non_system_messages = messages.reject(&:system_message?)
151
+
152
+ # If we don't have enough non-system messages, don't compress
153
+ return [] if non_system_messages.length <= keep_recent
154
+
155
+ # Identify the older messages that can be compressed
156
+ compressible = non_system_messages[0...-keep_recent]
157
+
158
+ # Group into segments (for now, treat all compressible messages as one segment)
159
+ compressible.empty? ? [] : [compressible]
160
+ end
161
+
162
+ # Replace original messages with summary messages in the session
163
+ # @param session [Session] The session to modify
164
+ # @param segments [Array<Array<Message>>] Original message segments
165
+ # @param summaries [Array<Message>] Summary messages
166
+ def replace_with_summaries(session, segments, summaries)
167
+ # Get all messages to compress (flatten segments)
168
+ messages_to_remove = segments.flatten
169
+
170
+ # Remove the original messages
171
+ session.messages.reject! { |msg| messages_to_remove.include?(msg) }
172
+
173
+ # Insert summaries at the beginning (after system messages)
174
+ system_messages = session.messages.select(&:system_message?)
175
+ other_messages = session.messages.reject(&:system_message?)
176
+
177
+ # Rebuild messages array: system messages + summaries + remaining messages
178
+ session.instance_variable_set(:@messages, system_messages + summaries + other_messages)
179
+ session.instance_variable_set(:@updated_at, Time.now)
180
+ end
181
+
182
+ # Fallback truncation strategy when summarization fails
183
+ # Simply removes oldest non-system messages to reduce size
184
+ # @param session [Session] The session to truncate
185
+ def fallback_truncate(session)
186
+ SmartPrompt.logger.warn "Falling back to truncation for session #{session.id}"
187
+
188
+ # Keep system messages and recent messages
189
+ system_messages = session.messages.select(&:system_message?)
190
+ non_system_messages = session.messages.reject(&:system_message?)
191
+
192
+ # Keep only the most recent half of non-system messages
193
+ keep_count = (non_system_messages.length * 0.5).ceil
194
+ kept_messages = non_system_messages.last(keep_count)
195
+
196
+ # Update session messages
197
+ session.instance_variable_set(:@messages, system_messages + kept_messages)
198
+ session.instance_variable_set(:@updated_at, Time.now)
199
+ end
200
+ end
201
+ end
@@ -0,0 +1,22 @@
1
+ module SmartPrompt
2
+ # ContextStrategy defines the interface for context selection strategies
3
+ # Different strategies implement different algorithms for selecting which
4
+ # messages to include in the context window based on various criteria
5
+ module ContextStrategy
6
+ # Select messages from the session to include in context
7
+ # @param messages [Array<Message>] All messages in the session
8
+ # @param max_tokens [Integer, nil] Maximum token limit for selected messages
9
+ # @param current_message [Message, nil] The current message being processed (for relevance)
10
+ # @return [Array<Message>] Selected messages that fit within constraints
11
+ def select_messages(messages, max_tokens, current_message = nil)
12
+ raise NotImplementedError, "#{self.class} must implement #select_messages"
13
+ end
14
+
15
+ # Determine if the session should be compressed
16
+ # @param session [Session] The session to evaluate
17
+ # @return [Boolean] true if compression is recommended
18
+ def should_compress?(session)
19
+ raise NotImplementedError, "#{self.class} must implement #should_compress?"
20
+ end
21
+ end
22
+ end
@@ -1,97 +1,72 @@
1
1
  require "yaml"
2
2
  require "retriable"
3
3
  require "numo/narray"
4
- require "base64"
5
4
 
6
5
  module SmartPrompt
7
6
  class Conversation
8
7
  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
-
21
8
  attr_reader :messages, :last_response, :config_file
22
9
  attr_reader :last_call_id
10
+ attr_reader :session_id
23
11
 
24
- def initialize(engine, tools = nil)
12
+ def initialize(engine, tools = nil, session_id = nil)
25
13
  SmartPrompt.logger.info "Create Conversation"
26
14
  @messages = []
27
15
  @engine = engine
28
16
  @adapters = engine.adapters
29
17
  @llms = engine.llms
30
- @models = engine.models
31
18
  @current_llm_name = nil
32
19
  @templates = engine.templates
33
20
  @temperature = 0.7
34
21
  @current_adapter = engine.current_adapter
35
22
  @last_response = nil
36
23
  @tools = tools
37
- @request_options = {}
38
- @pending_content_parts = []
39
- @thinking_enabled = nil
24
+ @session_id = session_id
25
+ @use_history_manager = false
40
26
  end
41
27
 
42
28
  def use(llm_name)
43
- llm_name = llm_name.to_s
44
- raise ConfigurationError, "LLM #{llm_name} not configured" unless @llms.key?(llm_name)
29
+ raise "LLM #{llm_name} not configured" unless @llms.key?(llm_name)
45
30
  @current_llm = @llms[llm_name]
46
31
  @current_llm_name = llm_name
47
32
  self
48
33
  end
49
34
 
50
- def use_model(model_name)
51
- model_name = model_name.to_s
52
- model_config = @models[model_name] || @models[model_name.to_sym]
53
- raise ConfigurationError, "Model #{model_name} not configured" unless model_config
54
-
55
- llm_name = model_config["use"] || model_config[:use]
56
- configured_model_name = model_config["model"] || model_config[:model]
57
- raise ConfigurationError, "Model #{model_name} must define use" if llm_name.nil? || llm_name.empty?
58
- raise ConfigurationError, "Model #{model_name} must define model" if configured_model_name.nil? || configured_model_name.empty?
59
-
60
- use(llm_name)
61
- model(configured_model_name)
62
- merge_model_request_options(model_config)
63
- self
64
- end
65
-
66
35
  def model(model_name)
67
36
  @model_name = model_name
37
+ if @engine.config["better_prompt_db"]
38
+ BetterPrompt.add_model(@current_llm_name, @model_name)
39
+ end
68
40
  end
69
41
 
70
42
  def temperature(temperature)
71
43
  @temperature = temperature
72
44
  end
73
45
 
74
- def request_options(options = {})
75
- @request_options.merge!(options || {})
76
- self
77
- end
78
-
79
- def thinking(enabled = true)
80
- @thinking_enabled = enabled
81
- if @sys_msg
82
- @sys_msg = thinking_system_message(@sys_msg)
83
- refresh_system_message(@sys_msg)
84
- end
85
- self
86
- end
87
-
88
46
  def history_messages
89
- @engine.history_messages
47
+ # If using HistoryManager, get messages from session
48
+ if @use_history_manager && @engine.history_manager
49
+ session_messages = @engine.history_manager.get_context(@session_id)
50
+ # Convert Message objects to hash format for backward compatibility
51
+ session_messages.map(&:to_h)
52
+ else
53
+ # Fall back to old implementation
54
+ @engine.history_messages
55
+ end
90
56
  end
91
57
 
92
58
  def add_message(msg, with_history = false)
93
59
  if with_history
94
- history_messages << msg
60
+ # If HistoryManager is available, use it
61
+ if @engine.history_manager
62
+ @use_history_manager = true
63
+ # Ensure we have a session ID
64
+ @session_id ||= generate_default_session_id
65
+ @engine.history_manager.add_message(@session_id, msg)
66
+ else
67
+ # Fall back to old implementation
68
+ @engine.history_messages << msg
69
+ end
95
70
  end
96
71
  @messages << msg
97
72
  end
@@ -102,59 +77,67 @@ module SmartPrompt
102
77
  SmartPrompt.logger.info "Use template #{template_name}"
103
78
  raise "Template #{template_name} not found" unless @templates.key?(template_name)
104
79
  content = @templates[template_name].render(params)
105
- add_user_content(content, with_history)
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
106
84
  self
107
85
  else
108
- add_user_content(template_name, with_history)
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
109
90
  self
110
91
  end
111
92
  end
112
93
 
113
- def sys_msg(message, params)
114
- @sys_msg = thinking_system_message(message)
115
- add_message({ role: "system", content: @sys_msg }, params[:with_history])
116
- self
117
- end
118
-
119
- def multimodal_prompt(parts, with_history: false)
120
- add_message({ role: "user", content: normalize_content_parts(parts) }, with_history)
121
- self
122
- end
123
-
124
- def image(source, token_budget: nil, **metadata)
125
- @pending_content_parts << media_part("image", source, token_budget: token_budget, **metadata)
126
- self
127
- end
128
-
129
- def audio(source, **metadata)
130
- @pending_content_parts << media_part("audio", source, **metadata)
131
- self
132
- end
133
-
134
- def video(source, fps: nil, max_seconds: nil, **metadata)
135
- @pending_content_parts << media_part("video", source, fps: fps, max_seconds: max_seconds, **metadata)
94
+ 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
136
100
  self
137
101
  end
138
102
 
139
103
  def send_msg_once
140
104
  raise "No LLM selected" if @current_llm.nil?
141
- @last_response = send_llm_request(@messages, nil)
105
+ @last_response = @current_llm.send_request(@messages, @model_name, @temperature)
142
106
  @messages = []
143
107
  @messages << { role: "system", content: @sys_msg }
144
108
  @last_response
145
109
  end
146
110
 
111
+ private
112
+
113
+ def generate_default_session_id
114
+ # Generate a default session ID based on worker name or timestamp
115
+ "default_#{Time.now.to_i}_#{rand(1000)}"
116
+ end
117
+
118
+ public
119
+
147
120
  def send_msg(params = {})
148
121
  Retriable.retriable(RETRY_OPTIONS) do
149
122
  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
150
130
  if params[:with_history]
151
- @last_response = send_llm_request(history_messages, nil)
131
+ @last_response = @current_llm.send_request(history_messages, @model_name, @temperature, @tools, nil)
152
132
  else
153
- @last_response = send_llm_request(@messages, nil)
133
+ @last_response = @current_llm.send_request(@messages, @model_name, @temperature, @tools, nil)
154
134
  end
155
135
  if @last_response == ""
156
136
  @last_response = @current_llm.last_response
157
137
  end
138
+ if @engine.config["better_prompt_db"]
139
+ BetterPrompt.add_response(@last_call_id, @last_response, false)
140
+ end
158
141
  @messages = []
159
142
  @messages << { role: "system", content: @sys_msg }
160
143
  @last_response
@@ -166,10 +149,20 @@ module SmartPrompt
166
149
  def send_msg_by_stream(params = {}, &proc)
167
150
  Retriable.retriable(RETRY_OPTIONS) do
168
151
  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
169
159
  if params[:with_history]
170
- send_llm_request(history_messages, proc)
160
+ @current_llm.send_request(history_messages, @model_name, @temperature, @tools, proc)
171
161
  else
172
- send_llm_request(@messages, proc)
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)
173
166
  end
174
167
  @messages = []
175
168
  @messages << { role: "system", content: @sys_msg }
@@ -204,119 +197,16 @@ module SmartPrompt
204
197
  end
205
198
  end
206
199
 
207
- private
208
-
209
- def send_llm_request(messages, proc)
210
- parameters = @current_llm.method(:send_request).parameters
211
- if parameters.length >= 6
212
- @current_llm.send_request(messages, @model_name, @temperature, @tools, proc, @request_options)
213
- else
214
- @current_llm.send_request(messages, @model_name, @temperature, @tools, proc)
215
- end
216
- end
217
-
218
- def merge_model_request_options(model_config)
219
- explicit_options = model_config["request_options"] || model_config[:request_options] || {}
220
- @request_options.merge!(explicit_options)
221
- MODEL_REQUEST_OPTION_KEYS.each do |key|
222
- value = model_config[key] || model_config[key.to_sym]
223
- @request_options[key.to_sym] = value unless value.nil?
224
- end
225
- end
226
-
227
- def add_user_content(content, with_history)
228
- if @pending_content_parts.empty?
229
- add_message({ role: "user", content: content }, with_history)
230
- else
231
- add_message({ role: "user", content: multimodal_content(content) }, with_history)
232
- @pending_content_parts = []
233
- end
234
- end
235
-
236
- def multimodal_content(text)
237
- parts = @pending_content_parts
238
- images_and_videos = parts.select { |part| ["image_url", "image", "video_url", "video"].include?(part[:type] || part["type"]) }
239
- audio_parts = parts.select { |part| ["input_audio", "audio"].include?(part[:type] || part["type"]) }
240
- other_parts = parts - images_and_videos - audio_parts
241
- normalize_content_parts(images_and_videos + other_parts + [{ type: "text", text: text.to_s }] + audio_parts)
242
- end
243
-
244
- def normalize_content_parts(parts)
245
- parts.map do |part|
246
- normalized = part.transform_keys(&:to_s)
247
- normalized["text"] = normalized.delete("content") if normalized["type"] == "text" && normalized.key?("content")
248
- normalized
249
- end
250
- end
251
-
252
- def media_part(type, source, **metadata)
253
- case type
254
- when "image"
255
- mime_type = detect_image_mime(source)
256
- data = File.binread(source)
257
- base64_data = Base64.strict_encode64(data)
258
- url = "data:#{mime_type};base64,#{base64_data}"
259
- part = { type: "image_url", image_url: { url: url } }
260
- when "audio"
261
- format = detect_audio_format(source)
262
- data = File.binread(source)
263
- base64_data = Base64.strict_encode64(data)
264
- part = { type: "input_audio", input_audio: { data: base64_data, format: format } }
265
- when "video"
266
- mime_type = detect_video_mime(source)
267
- data = File.binread(source)
268
- base64_data = Base64.strict_encode64(data)
269
- url = "data:#{mime_type};base64,#{base64_data}"
270
- part = { type: "video_url", video_url: { url: url } }
271
- else
272
- part = { type: type }
273
- end
274
- metadata.each do |key, value|
275
- part[key] = value unless value.nil?
276
- end
277
- part
200
+ def generate_image(prompt, params = {})
201
+ @current_llm.generate_image(prompt, params)
278
202
  end
279
203
 
280
- def detect_image_mime(path)
281
- ext = File.extname(path).downcase
282
- case ext
283
- when ".png" then "image/png"
284
- when ".jpg", ".jpeg" then "image/jpeg"
285
- when ".gif" then "image/gif"
286
- when ".webp" then "image/webp"
287
- when ".bmp" then "image/bmp"
288
- when ".svg" then "image/svg+xml"
289
- else "application/octet-stream"
290
- end
291
- end
292
-
293
- def detect_audio_format(path)
294
- ext = File.extname(path).downcase.delete_prefix(".")
295
- %w[wav mp3 ogg flac aac m4a].include?(ext) ? ext : "wav"
296
- end
297
-
298
- def detect_video_mime(path)
299
- ext = File.extname(path).downcase
300
- case ext
301
- when ".mp4" then "video/mp4"
302
- when ".webm" then "video/webm"
303
- when ".mov" then "video/quicktime"
304
- when ".avi" then "video/x-msvideo"
305
- else "application/octet-stream"
306
- end
307
- end
308
-
309
- def thinking_system_message(message)
310
- message = message.to_s.sub(/\A<\|think\|>\n?/, "")
311
- return message if @thinking_enabled == false
312
- return message unless @thinking_enabled == true
313
-
314
- "<|think|>\n#{message}"
204
+ def edit_image(prompt, params = {})
205
+ @current_llm.edit_image(prompt, params)
315
206
  end
316
207
 
317
- def refresh_system_message(message)
318
- system_message = @messages.find { |item| (item[:role] || item["role"]) == "system" }
319
- system_message[:content] = message if system_message
208
+ def save_image(image_data, output_dir = "./output", filename_prefix = "generated_image")
209
+ @current_llm.save_image(image_data, output_dir, filename_prefix)
320
210
  end
321
211
  end
322
212
  end