smart_prompt 0.4.4 → 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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -0
  3. data/README.cn.md +305 -11
  4. data/README.md +309 -11
  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/docs/ANTHROPIC_EXAMPLES.md +559 -0
  12. data/docs/CONVERSATION_INTEGRATION_SUMMARY.md +155 -0
  13. data/docs/HISTORY_EXAMPLES_README.md +533 -0
  14. data/docs/HISTORY_MANAGEMENT_GUIDE.md +797 -0
  15. data/docs/MONITORING_GUIDE.md +278 -0
  16. data/docs/MULTIMODAL_README.md +265 -0
  17. data/docs/RELEVANCE_BASED_STRATEGY_IMPLEMENTATION.md +124 -0
  18. data/docs/STT_README.md +302 -0
  19. data/docs/TTS_README.md +303 -0
  20. data/docs/VIDEO_GENERATION_README.md +246 -0
  21. data/docs/delete_files_list.md +124 -0
  22. data/examples/anthropic_basic_chat.rb +143 -0
  23. data/examples/anthropic_example.rb +232 -0
  24. data/examples/anthropic_multimodal.rb +212 -0
  25. data/examples/anthropic_streaming.rb +312 -0
  26. data/examples/anthropic_tool_calling.rb +393 -0
  27. data/examples/automatic_cleanup_example.rb +109 -0
  28. data/examples/history_management_examples.rb +522 -0
  29. data/examples/image_generation_example.rb +130 -0
  30. data/examples/monitoring_example.rb +121 -0
  31. data/examples/multimodal_example.rb +63 -0
  32. data/examples/relevance_based_strategy_example.rb +87 -0
  33. data/examples/sensenova_example.rb +129 -0
  34. data/examples/stt_example.rb +287 -0
  35. data/examples/tts_example.rb +244 -0
  36. data/examples/video_generation_example.rb +189 -0
  37. data/examples/zhipu_example.rb +151 -0
  38. data/lib/smart_prompt/anthropic_adapter.rb +407 -298
  39. data/lib/smart_prompt/compression_engine.rb +201 -0
  40. data/lib/smart_prompt/context_strategy.rb +22 -0
  41. data/lib/smart_prompt/conversation.rb +47 -4
  42. data/lib/smart_prompt/engine.rb +29 -2
  43. data/lib/smart_prompt/history_manager.rb +596 -0
  44. data/lib/smart_prompt/hybrid_strategy.rb +222 -0
  45. data/lib/smart_prompt/image_generation_adapter.rb +297 -0
  46. data/lib/smart_prompt/lru_cache.rb +133 -0
  47. data/lib/smart_prompt/message.rb +57 -0
  48. data/lib/smart_prompt/multimodal_adapter.rb +277 -0
  49. data/lib/smart_prompt/persistence_layer.rb +197 -0
  50. data/lib/smart_prompt/relevance_based_strategy.rb +221 -0
  51. data/lib/smart_prompt/sensenova_adapter.rb +410 -0
  52. data/lib/smart_prompt/session.rb +140 -0
  53. data/lib/smart_prompt/sliding_window_strategy.rb +100 -0
  54. data/lib/smart_prompt/stt_adapter.rb +381 -0
  55. data/lib/smart_prompt/summary_based_strategy.rb +152 -0
  56. data/lib/smart_prompt/token_counter.rb +74 -0
  57. data/lib/smart_prompt/tts_adapter.rb +403 -0
  58. data/lib/smart_prompt/version.rb +1 -1
  59. data/lib/smart_prompt/video_generation_adapter.rb +330 -0
  60. data/lib/smart_prompt/worker.rb +28 -3
  61. data/lib/smart_prompt/zhipu_adapter.rb +616 -0
  62. data/lib/smart_prompt.rb +21 -0
  63. data/workers/history_management_examples.rb +407 -0
  64. data/workers/image_generation_workers.rb +119 -0
  65. data/workers/multimodal_workers.rb +110 -0
  66. data/workers/sensenova_workers.rb +62 -0
  67. data/workers/stt_workers.rb +195 -0
  68. data/workers/tts_workers.rb +388 -0
  69. data/workers/video_generation_workers.rb +264 -0
  70. data/workers/zhipu_workers.rb +113 -0
  71. metadata +88 -1
@@ -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
@@ -20,8 +20,9 @@ module SmartPrompt
20
20
 
21
21
  attr_reader :messages, :last_response, :config_file
22
22
  attr_reader :last_call_id
23
+ attr_reader :session_id
23
24
 
24
- def initialize(engine, tools = nil)
25
+ def initialize(engine, tools = nil, session_id = nil)
25
26
  SmartPrompt.logger.info "Create Conversation"
26
27
  @messages = []
27
28
  @engine = engine
@@ -37,6 +38,8 @@ module SmartPrompt
37
38
  @request_options = {}
38
39
  @pending_content_parts = []
39
40
  @thinking_enabled = nil
41
+ @session_id = session_id
42
+ @use_history_manager = false
40
43
  end
41
44
 
42
45
  def use(llm_name)
@@ -86,12 +89,29 @@ module SmartPrompt
86
89
  end
87
90
 
88
91
  def history_messages
89
- @engine.history_messages
92
+ # If using HistoryManager, get messages from session
93
+ if @use_history_manager && @engine.history_manager
94
+ session_messages = @engine.history_manager.get_context(@session_id)
95
+ # Convert Message objects to hash format for backward compatibility
96
+ session_messages.map(&:to_h)
97
+ else
98
+ # Fall back to old implementation
99
+ @engine.history_messages
100
+ end
90
101
  end
91
102
 
92
103
  def add_message(msg, with_history = false)
93
104
  if with_history
94
- history_messages << msg
105
+ # If HistoryManager is available, use it
106
+ if @engine.history_manager
107
+ @use_history_manager = true
108
+ # Ensure we have a session ID
109
+ @session_id ||= generate_default_session_id
110
+ @engine.history_manager.add_message(@session_id, msg)
111
+ else
112
+ # Fall back to old implementation
113
+ @engine.history_messages << msg
114
+ end
95
115
  end
96
116
  @messages << msg
97
117
  end
@@ -110,7 +130,7 @@ module SmartPrompt
110
130
  end
111
131
  end
112
132
 
113
- def sys_msg(message, params)
133
+ def sys_msg(message, params = {})
114
134
  @sys_msg = thinking_system_message(message)
115
135
  add_message({ role: "system", content: @sys_msg }, params[:with_history])
116
136
  self
@@ -144,6 +164,15 @@ module SmartPrompt
144
164
  @last_response
145
165
  end
146
166
 
167
+ private
168
+
169
+ def generate_default_session_id
170
+ # Generate a default session ID based on worker name or timestamp
171
+ "default_#{Time.now.to_i}_#{rand(1000)}"
172
+ end
173
+
174
+ public
175
+
147
176
  def send_msg(params = {})
148
177
  Retriable.retriable(RETRY_OPTIONS) do
149
178
  raise ConfigurationError, "No LLM selected" if @current_llm.nil?
@@ -318,5 +347,19 @@ module SmartPrompt
318
347
  system_message = @messages.find { |item| (item[:role] || item["role"]) == "system" }
319
348
  system_message[:content] = message if system_message
320
349
  end
350
+
351
+ public
352
+
353
+ def generate_image(prompt, params = {})
354
+ @current_llm.generate_image(prompt, params)
355
+ end
356
+
357
+ def edit_image(prompt, params = {})
358
+ @current_llm.edit_image(prompt, params)
359
+ end
360
+
361
+ def save_image(image_data, output_dir = "./output", filename_prefix = "generated_image")
362
+ @current_llm.save_image(image_data, output_dir, filename_prefix)
363
+ end
321
364
  end
322
365
  end
@@ -1,7 +1,7 @@
1
1
  module SmartPrompt
2
2
  class Engine
3
3
  attr_reader :config_file, :config, :adapters, :current_adapter, :llms, :models, :templates
4
- attr_reader :stream_response
4
+ attr_reader :stream_response, :history_manager
5
5
 
6
6
  def initialize(config_file)
7
7
  @config_file = config_file
@@ -11,6 +11,7 @@ module SmartPrompt
11
11
  @templates = {}
12
12
  @current_workers = {}
13
13
  @history_messages = []
14
+ @history_manager = nil
14
15
  load_config(config_file)
15
16
  SmartPrompt.logger.info "Started create the SmartPrompt engine."
16
17
  @stream_proc = Proc.new do |chunk, _bytesize|
@@ -79,6 +80,14 @@ module SmartPrompt
79
80
  template_name = file.gsub(@config["template_path"] + "/", "").gsub("\.erb", "")
80
81
  @templates[template_name] = PromptTemplate.new(file)
81
82
  end
83
+
84
+ # Initialize HistoryManager if configured
85
+ if @config["history"]
86
+ history_config = symbolize_keys(@config["history"])
87
+ @history_manager = HistoryManager.new(history_config)
88
+ SmartPrompt.logger.info "HistoryManager initialized with configuration"
89
+ end
90
+
82
91
  load_workers
83
92
  rescue Psych::SyntaxError => ex
84
93
  SmartPrompt.logger.error "YAML syntax error in config file: #{ex.message}"
@@ -146,7 +155,8 @@ module SmartPrompt
146
155
  begin
147
156
  @origin_proc = proc
148
157
  @stream_response = {}
149
- worker.execute_by_stream(params, &@stream_proc)
158
+ ret = worker.execute_by_stream(params, &@stream_proc)
159
+ @stream_response = ret if @stream_response.empty?
150
160
  SmartPrompt.logger.info "Worker #{worker_name} executed(stream) successfully"
151
161
  SmartPrompt.logger.info "Worker #{worker_name} stream response is: #{@stream_response}"
152
162
  rescue => e
@@ -166,10 +176,16 @@ module SmartPrompt
166
176
  end
167
177
 
168
178
  def history_messages
179
+ if @history_manager
180
+ SmartPrompt.logger.warn "[DEPRECATED] Engine#history_messages is deprecated. Use history_manager.get_context(session_id) instead."
181
+ end
169
182
  @history_messages
170
183
  end
171
184
 
172
185
  def clear_history_messages
186
+ if @history_manager
187
+ SmartPrompt.logger.warn "[DEPRECATED] Engine#clear_history_messages is deprecated. Use history_manager.clear_session(session_id) instead."
188
+ end
173
189
  @history_messages = []
174
190
  end
175
191
 
@@ -189,5 +205,16 @@ module SmartPrompt
189
205
  def sanitize_history_content(content)
190
206
  content.to_s.gsub(/<\|channel\>thought\n.*?<channel\|>/m, "")
191
207
  end
208
+
209
+ # Recursively convert hash keys from strings to symbols
210
+ def symbolize_keys(hash)
211
+ return hash unless hash.is_a?(Hash)
212
+
213
+ hash.each_with_object({}) do |(key, value), result|
214
+ new_key = key.is_a?(String) ? key.to_sym : key
215
+ new_value = value.is_a?(Hash) ? symbolize_keys(value) : value
216
+ result[new_key] = new_value
217
+ end
218
+ end
192
219
  end
193
220
  end