smart_prompt 0.4.3 → 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 -149
  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 +95 -5
@@ -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
@@ -5,92 +5,68 @@ require "numo/narray"
5
5
  module SmartPrompt
6
6
  class Conversation
7
7
  include APIHandler
8
- MODEL_REQUEST_OPTION_KEYS = %w[
9
- max_tokens
10
- max_completion_tokens
11
- top_p
12
- top_k
13
- response_format
14
- tool_choice
15
- parallel_tool_calls
16
- seed
17
- stop
18
- ].freeze
19
-
20
8
  attr_reader :messages, :last_response, :config_file
21
9
  attr_reader :last_call_id
10
+ attr_reader :session_id
22
11
 
23
- def initialize(engine, tools = nil)
12
+ def initialize(engine, tools = nil, session_id = nil)
24
13
  SmartPrompt.logger.info "Create Conversation"
25
14
  @messages = []
26
15
  @engine = engine
27
16
  @adapters = engine.adapters
28
17
  @llms = engine.llms
29
- @models = engine.models
30
18
  @current_llm_name = nil
31
19
  @templates = engine.templates
32
20
  @temperature = 0.7
33
21
  @current_adapter = engine.current_adapter
34
22
  @last_response = nil
35
23
  @tools = tools
36
- @request_options = {}
37
- @pending_content_parts = []
38
- @thinking_enabled = nil
24
+ @session_id = session_id
25
+ @use_history_manager = false
39
26
  end
40
27
 
41
28
  def use(llm_name)
42
- llm_name = llm_name.to_s
43
- raise ConfigurationError, "LLM #{llm_name} not configured" unless @llms.key?(llm_name)
29
+ raise "LLM #{llm_name} not configured" unless @llms.key?(llm_name)
44
30
  @current_llm = @llms[llm_name]
45
31
  @current_llm_name = llm_name
46
32
  self
47
33
  end
48
34
 
49
- def use_model(model_name)
50
- model_name = model_name.to_s
51
- model_config = @models[model_name] || @models[model_name.to_sym]
52
- raise ConfigurationError, "Model #{model_name} not configured" unless model_config
53
-
54
- llm_name = model_config["use"] || model_config[:use]
55
- configured_model_name = model_config["model"] || model_config[:model]
56
- raise ConfigurationError, "Model #{model_name} must define use" if llm_name.nil? || llm_name.empty?
57
- raise ConfigurationError, "Model #{model_name} must define model" if configured_model_name.nil? || configured_model_name.empty?
58
-
59
- use(llm_name)
60
- model(configured_model_name)
61
- merge_model_request_options(model_config)
62
- self
63
- end
64
-
65
35
  def model(model_name)
66
36
  @model_name = model_name
37
+ if @engine.config["better_prompt_db"]
38
+ BetterPrompt.add_model(@current_llm_name, @model_name)
39
+ end
67
40
  end
68
41
 
69
42
  def temperature(temperature)
70
43
  @temperature = temperature
71
44
  end
72
45
 
73
- def request_options(options = {})
74
- @request_options.merge!(options || {})
75
- self
76
- end
77
-
78
- def thinking(enabled = true)
79
- @thinking_enabled = enabled
80
- if @sys_msg
81
- @sys_msg = thinking_system_message(@sys_msg)
82
- refresh_system_message(@sys_msg)
83
- end
84
- self
85
- end
86
-
87
46
  def history_messages
88
- @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
89
56
  end
90
57
 
91
58
  def add_message(msg, with_history = false)
92
59
  if with_history
93
- 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
94
70
  end
95
71
  @messages << msg
96
72
  end
@@ -101,59 +77,67 @@ module SmartPrompt
101
77
  SmartPrompt.logger.info "Use template #{template_name}"
102
78
  raise "Template #{template_name} not found" unless @templates.key?(template_name)
103
79
  content = @templates[template_name].render(params)
104
- 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
105
84
  self
106
85
  else
107
- 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
108
90
  self
109
91
  end
110
92
  end
111
93
 
112
- def sys_msg(message, params)
113
- @sys_msg = thinking_system_message(message)
114
- add_message({ role: "system", content: @sys_msg }, params[:with_history])
115
- self
116
- end
117
-
118
- def multimodal_prompt(parts, with_history: false)
119
- add_message({ role: "user", content: normalize_content_parts(parts) }, with_history)
120
- self
121
- end
122
-
123
- def image(source, token_budget: nil, **metadata)
124
- @pending_content_parts << media_part("image", source, token_budget: token_budget, **metadata)
125
- self
126
- end
127
-
128
- def audio(source, **metadata)
129
- @pending_content_parts << media_part("audio", source, **metadata)
130
- self
131
- end
132
-
133
- def video(source, fps: nil, max_seconds: nil, **metadata)
134
- @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
135
100
  self
136
101
  end
137
102
 
138
103
  def send_msg_once
139
104
  raise "No LLM selected" if @current_llm.nil?
140
- @last_response = send_llm_request(@messages, nil)
105
+ @last_response = @current_llm.send_request(@messages, @model_name, @temperature)
141
106
  @messages = []
142
107
  @messages << { role: "system", content: @sys_msg }
143
108
  @last_response
144
109
  end
145
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
+
146
120
  def send_msg(params = {})
147
121
  Retriable.retriable(RETRY_OPTIONS) do
148
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
149
130
  if params[:with_history]
150
- @last_response = send_llm_request(history_messages, nil)
131
+ @last_response = @current_llm.send_request(history_messages, @model_name, @temperature, @tools, nil)
151
132
  else
152
- @last_response = send_llm_request(@messages, nil)
133
+ @last_response = @current_llm.send_request(@messages, @model_name, @temperature, @tools, nil)
153
134
  end
154
135
  if @last_response == ""
155
136
  @last_response = @current_llm.last_response
156
137
  end
138
+ if @engine.config["better_prompt_db"]
139
+ BetterPrompt.add_response(@last_call_id, @last_response, false)
140
+ end
157
141
  @messages = []
158
142
  @messages << { role: "system", content: @sys_msg }
159
143
  @last_response
@@ -165,10 +149,20 @@ module SmartPrompt
165
149
  def send_msg_by_stream(params = {}, &proc)
166
150
  Retriable.retriable(RETRY_OPTIONS) do
167
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
168
159
  if params[:with_history]
169
- send_llm_request(history_messages, proc)
160
+ @current_llm.send_request(history_messages, @model_name, @temperature, @tools, proc)
170
161
  else
171
- 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)
172
166
  end
173
167
  @messages = []
174
168
  @messages << { role: "system", content: @sys_msg }
@@ -203,78 +197,16 @@ module SmartPrompt
203
197
  end
204
198
  end
205
199
 
206
- private
207
-
208
- def send_llm_request(messages, proc)
209
- parameters = @current_llm.method(:send_request).parameters
210
- if parameters.length >= 6
211
- @current_llm.send_request(messages, @model_name, @temperature, @tools, proc, @request_options)
212
- else
213
- @current_llm.send_request(messages, @model_name, @temperature, @tools, proc)
214
- end
200
+ def generate_image(prompt, params = {})
201
+ @current_llm.generate_image(prompt, params)
215
202
  end
216
203
 
217
- def merge_model_request_options(model_config)
218
- explicit_options = model_config["request_options"] || model_config[:request_options] || {}
219
- @request_options.merge!(explicit_options)
220
- MODEL_REQUEST_OPTION_KEYS.each do |key|
221
- value = model_config[key] || model_config[key.to_sym]
222
- @request_options[key.to_sym] = value unless value.nil?
223
- end
224
- end
225
-
226
- def add_user_content(content, with_history)
227
- if @pending_content_parts.empty?
228
- add_message({ role: "user", content: content }, with_history)
229
- else
230
- add_message({ role: "user", content: multimodal_content(content) }, with_history)
231
- @pending_content_parts = []
232
- end
233
- end
234
-
235
- def multimodal_content(text)
236
- parts = @pending_content_parts
237
- images_and_videos = parts.select { |part| ["image", "video"].include?(part[:type] || part["type"]) }
238
- audio_parts = parts.select { |part| (part[:type] || part["type"]) == "audio" }
239
- other_parts = parts - images_and_videos - audio_parts
240
- normalize_content_parts(images_and_videos + other_parts + [{ type: "text", text: text.to_s }] + audio_parts)
241
- end
242
-
243
- def normalize_content_parts(parts)
244
- parts.map do |part|
245
- normalized = part.transform_keys(&:to_s)
246
- normalized["text"] = normalized.delete("content") if normalized["type"] == "text" && normalized.key?("content")
247
- normalized
248
- end
249
- end
250
-
251
- def media_part(type, source, **metadata)
252
- part = { type: type }
253
- case type
254
- when "image"
255
- part[:url] = source
256
- when "audio"
257
- part[:audio] = source
258
- when "video"
259
- part[:video] = source
260
- end
261
- metadata.each do |key, value|
262
- part[key] = value unless value.nil?
263
- end
264
- part
265
- end
266
-
267
- def thinking_system_message(message)
268
- message = message.to_s.sub(/\A<\|think\|>\n?/, "")
269
- return message if @thinking_enabled == false
270
- return message unless @thinking_enabled == true
271
-
272
- "<|think|>\n#{message}"
204
+ def edit_image(prompt, params = {})
205
+ @current_llm.edit_image(prompt, params)
273
206
  end
274
207
 
275
- def refresh_system_message(message)
276
- system_message = @messages.find { |item| (item[:role] || item["role"]) == "system" }
277
- 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)
278
210
  end
279
211
  end
280
212
  end
@@ -1,16 +1,16 @@
1
1
  module SmartPrompt
2
2
  class Engine
3
- attr_reader :config_file, :config, :adapters, :current_adapter, :llms, :models, :templates
4
- attr_reader :stream_response
3
+ attr_reader :config_file, :config, :adapters, :current_adapter, :llms, :templates
4
+ attr_reader :stream_response, :history_manager
5
5
 
6
6
  def initialize(config_file)
7
7
  @config_file = config_file
8
8
  @adapters = {}
9
9
  @llms = {}
10
- @models = {}
11
10
  @templates = {}
12
11
  @current_workers = {}
13
12
  @history_messages = []
13
+ @history_manager = nil
14
14
  load_config(config_file)
15
15
  SmartPrompt.logger.info "Started create the SmartPrompt engine."
16
16
  @stream_proc = Proc.new do |chunk, _bytesize|
@@ -65,7 +65,10 @@ module SmartPrompt
65
65
  SmartPrompt.logger = Logger.new(@config["logger_file"])
66
66
  end
67
67
  SmartPrompt.logger.info "Loading configuration from file: #{config_file}"
68
- @models = @config["models"] || {}
68
+ if @config["better_prompt_db"]
69
+ require "better_prompt"
70
+ BetterPrompt.setup(db_path: @config["better_prompt_db"])
71
+ end
69
72
  @config["adapters"].each do |adapter_name, adapter_class|
70
73
  adapter_class = SmartPrompt.const_get(adapter_class)
71
74
  @adapters[adapter_name] = adapter_class
@@ -79,6 +82,14 @@ module SmartPrompt
79
82
  template_name = file.gsub(@config["template_path"] + "/", "").gsub("\.erb", "")
80
83
  @templates[template_name] = PromptTemplate.new(file)
81
84
  end
85
+
86
+ # Initialize HistoryManager if configured
87
+ if @config["history"]
88
+ history_config = symbolize_keys(@config["history"])
89
+ @history_manager = HistoryManager.new(history_config)
90
+ SmartPrompt.logger.info "HistoryManager initialized with configuration"
91
+ end
92
+
82
93
  load_workers
83
94
  rescue Psych::SyntaxError => ex
84
95
  SmartPrompt.logger.error "YAML syntax error in config file: #{ex.message}"
@@ -123,12 +134,15 @@ module SmartPrompt
123
134
  if result.class == String
124
135
  recive_message = {
125
136
  "role": "assistant",
126
- "content": sanitize_history_content(result),
137
+ "content": result,
127
138
  }
128
139
  elsif result.class == Array
129
140
  recive_message = nil
130
141
  else
131
- recive_message = assistant_history_message(result)
142
+ recive_message = {
143
+ "role": result.dig("choices", 0, "message", "role"),
144
+ "content": result.dig("choices", 0, "message", "content").to_s + result.dig("choices", 0, "message", "tool_calls").to_s,
145
+ }
132
146
  end
133
147
  worker.conversation.add_message(recive_message) if recive_message
134
148
  SmartPrompt.logger.info "Worker result is: #{result}"
@@ -146,7 +160,8 @@ module SmartPrompt
146
160
  begin
147
161
  @origin_proc = proc
148
162
  @stream_response = {}
149
- worker.execute_by_stream(params, &@stream_proc)
163
+ ret = worker.execute_by_stream(params, &@stream_proc)
164
+ @stream_response = ret if @stream_response.empty?
150
165
  SmartPrompt.logger.info "Worker #{worker_name} executed(stream) successfully"
151
166
  SmartPrompt.logger.info "Worker #{worker_name} stream response is: #{@stream_response}"
152
167
  rescue => e
@@ -166,28 +181,30 @@ module SmartPrompt
166
181
  end
167
182
 
168
183
  def history_messages
184
+ if @history_manager
185
+ SmartPrompt.logger.warn "[DEPRECATED] Engine#history_messages is deprecated. Use history_manager.get_context(session_id) instead."
186
+ end
169
187
  @history_messages
170
188
  end
171
189
 
172
190
  def clear_history_messages
191
+ if @history_manager
192
+ SmartPrompt.logger.warn "[DEPRECATED] Engine#clear_history_messages is deprecated. Use history_manager.clear_session(session_id) instead."
193
+ end
173
194
  @history_messages = []
174
195
  end
175
196
 
176
197
  private
177
198
 
178
- def assistant_history_message(result)
179
- message = result.dig("choices", 0, "message") || {}
180
- history_message = {
181
- "role": message["role"] || "assistant",
182
- "content": sanitize_history_content(message["content"].to_s),
183
- }
184
- tool_calls = message["tool_calls"]
185
- history_message["tool_calls"] = tool_calls if tool_calls && !tool_calls.empty?
186
- history_message
187
- end
199
+ # Recursively convert hash keys from strings to symbols
200
+ def symbolize_keys(hash)
201
+ return hash unless hash.is_a?(Hash)
188
202
 
189
- def sanitize_history_content(content)
190
- content.to_s.gsub(/<\|channel\>thought\n.*?<channel\|>/m, "")
203
+ hash.each_with_object({}) do |(key, value), result|
204
+ new_key = key.is_a?(String) ? key.to_sym : key
205
+ new_value = value.is_a?(Hash) ? symbolize_keys(value) : value
206
+ result[new_key] = new_value
207
+ end
191
208
  end
192
209
  end
193
210
  end