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,330 @@
1
+ require "openai"
2
+ require "base64"
3
+ require "net/http"
4
+ require "uri"
5
+
6
+ module SmartPrompt
7
+ class VideoGenerationAdapter < LLMAdapter
8
+ SUPPORTED_IMAGE_FORMATS = %w[jpg jpeg png gif bmp webp]
9
+ SUPPORTED_VIDEO_FORMATS = %w[mp4 mov avi mkv webm]
10
+
11
+ def initialize(config)
12
+ super
13
+ api_key = @config["api_key"]
14
+ if api_key.is_a?(String) && api_key.start_with?("ENV[") && api_key.end_with?("]")
15
+ api_key = eval(api_key)
16
+ end
17
+ begin
18
+ @client = OpenAI::Client.new(
19
+ access_token: api_key,
20
+ uri_base: @config["url"],
21
+ request_timeout: 600, # Longer timeout for video generation
22
+ )
23
+ rescue OpenAI::ConfigurationError => e
24
+ SmartPrompt.logger.error "Failed to initialize VideoGeneration client: #{e.message}"
25
+ raise LLMAPIError, "Invalid VideoGeneration configuration: #{e.message}"
26
+ rescue OpenAI::Error => e
27
+ SmartPrompt.logger.error "Failed to initialize VideoGeneration client: #{e.message}"
28
+ raise LLMAPIError, "VideoGeneration authentication failed: #{e.message}"
29
+ rescue SocketError => e
30
+ SmartPrompt.logger.error "Failed to initialize VideoGeneration client: #{e.message}"
31
+ raise LLMAPIError, "Network error: Unable to connect to VideoGeneration API"
32
+ rescue => e
33
+ SmartPrompt.logger.error "Failed to initialize VideoGeneration client: #{e.message}"
34
+ raise Error, "Unexpected error initializing VideoGeneration client: #{e.message}"
35
+ ensure
36
+ SmartPrompt.logger.info "Successfully created a VideoGeneration client."
37
+ end
38
+ end
39
+
40
+ # Text-to-video generation
41
+ def generate_video(prompt, model: nil, duration: 4, resolution: "720p", fps: 24, seed: nil)
42
+ SmartPrompt.logger.info "VideoGenerationAdapter: Generating video from text"
43
+
44
+ model_name = model || @config["model"]
45
+
46
+ begin
47
+ # SiliconFlow uses OpenAI-compatible API format for video generation
48
+ # Note: This might require custom implementation as OpenAI gem doesn't have video endpoints
49
+ parameters = {
50
+ model: model_name,
51
+ prompt: prompt,
52
+ duration: duration,
53
+ resolution: resolution,
54
+ fps: fps
55
+ }
56
+
57
+ parameters[:seed] = seed if seed
58
+
59
+ SmartPrompt.logger.info "Video generation parameters: #{parameters}"
60
+
61
+ # Custom implementation for video generation
62
+ # Since OpenAI gem doesn't support video endpoints, we'll use direct HTTP calls
63
+ response = submit_video_generation_request(parameters)
64
+
65
+ @last_response = response
66
+
67
+ # Process response
68
+ if response["data"] && response["data"]["video_url"]
69
+ video_data = {
70
+ video_url: response["data"]["video_url"],
71
+ status: response["data"]["status"],
72
+ job_id: response["data"]["id"],
73
+ created_at: response["data"]["created_at"]
74
+ }
75
+
76
+ SmartPrompt.logger.info "Video generation job submitted successfully"
77
+ return video_data
78
+ else
79
+ SmartPrompt.logger.error "No video data in response"
80
+ raise LLMAPIError, "No video data in response"
81
+ end
82
+
83
+ rescue OpenAI::Error => e
84
+ SmartPrompt.logger.error "Video generation API error: #{e.message}"
85
+ raise LLMAPIError, "Video generation API error: #{e.message}"
86
+ rescue JSON::ParserError => e
87
+ SmartPrompt.logger.error "Failed to parse video generation response"
88
+ raise LLMAPIError, "Failed to parse video generation response"
89
+ rescue => e
90
+ SmartPrompt.logger.error "Unexpected error during video generation: #{e.message}"
91
+ raise Error, "Unexpected error during video generation: #{e.message}"
92
+ end
93
+ end
94
+
95
+ # Image-to-video generation
96
+ def create_video_from_image(image_file, prompt, model: nil, duration: 4, resolution: "720p", fps: 24, seed: nil)
97
+ SmartPrompt.logger.info "VideoGenerationAdapter: Creating video from image"
98
+
99
+ model_name = model || @config["model"]
100
+
101
+ begin
102
+ # Prepare image file
103
+ unless File.exist?(image_file)
104
+ raise Error, "Image file not found: #{image_file}"
105
+ end
106
+
107
+ file_ext = File.extname(image_file).downcase.delete(".")
108
+ unless SUPPORTED_IMAGE_FORMATS.include?(file_ext)
109
+ raise Error, "Unsupported image format: #{file_ext}"
110
+ end
111
+
112
+ # Convert image to base64 for API submission
113
+ image_data = File.binread(image_file)
114
+ base64_image = Base64.strict_encode64(image_data)
115
+
116
+ parameters = {
117
+ model: model_name,
118
+ image: base64_image,
119
+ prompt: prompt,
120
+ duration: duration,
121
+ resolution: resolution,
122
+ fps: fps
123
+ }
124
+
125
+ parameters[:seed] = seed if seed
126
+
127
+ SmartPrompt.logger.info "Image-to-video parameters: #{parameters}"
128
+
129
+ # Custom implementation for image-to-video generation
130
+ response = submit_image_to_video_request(parameters)
131
+
132
+ @last_response = response
133
+
134
+ if response["data"] && response["data"]["video_url"]
135
+ video_data = {
136
+ video_url: response["data"]["video_url"],
137
+ status: response["data"]["status"],
138
+ job_id: response["data"]["id"],
139
+ created_at: response["data"]["created_at"]
140
+ }
141
+
142
+ SmartPrompt.logger.info "Image-to-video job submitted successfully"
143
+ return video_data
144
+ else
145
+ SmartPrompt.logger.error "No video data in image-to-video response"
146
+ raise LLMAPIError, "No video data in image-to-video response"
147
+ end
148
+
149
+ rescue => e
150
+ SmartPrompt.logger.error "Unexpected error during image-to-video generation: #{e.message}"
151
+ raise Error, "Unexpected error during image-to-video generation: #{e.message}"
152
+ end
153
+ end
154
+
155
+ # Check video generation status
156
+ def check_video_status(job_id)
157
+ SmartPrompt.logger.info "VideoGenerationAdapter: Checking video generation status"
158
+
159
+ begin
160
+ response = check_video_generation_status(job_id)
161
+
162
+ @last_response = response
163
+
164
+ if response["data"]
165
+ status_data = {
166
+ job_id: response["data"]["id"],
167
+ status: response["data"]["status"],
168
+ video_url: response["data"]["video_url"],
169
+ progress: response["data"]["progress"],
170
+ created_at: response["data"]["created_at"],
171
+ updated_at: response["data"]["updated_at"]
172
+ }
173
+
174
+ SmartPrompt.logger.info "Video status: #{status_data[:status]}, Progress: #{status_data[:progress]}"
175
+ return status_data
176
+ else
177
+ SmartPrompt.logger.error "No status data in response"
178
+ raise LLMAPIError, "No status data in response"
179
+ end
180
+
181
+ rescue => e
182
+ SmartPrompt.logger.error "Error checking video status: #{e.message}"
183
+ raise Error, "Error checking video status: #{e.message}"
184
+ end
185
+ end
186
+
187
+ # Download video to file
188
+ def download_video(video_url, output_path)
189
+ SmartPrompt.logger.info "VideoGenerationAdapter: Downloading video"
190
+
191
+ begin
192
+ uri = URI.parse(video_url)
193
+ http = Net::HTTP.new(uri.host, uri.port)
194
+ http.use_ssl = (uri.scheme == 'https')
195
+
196
+ request = Net::HTTP::Get.new(uri.request_uri)
197
+ response = http.request(request)
198
+
199
+ if response.is_a?(Net::HTTPSuccess)
200
+ # Create directory if it doesn't exist
201
+ FileUtils.mkdir_p(File.dirname(output_path))
202
+
203
+ File.binwrite(output_path, response.body)
204
+ SmartPrompt.logger.info "Video downloaded successfully to: #{output_path}"
205
+ return output_path
206
+ else
207
+ SmartPrompt.logger.error "Failed to download video: #{response.code}"
208
+ raise Error, "Failed to download video: #{response.code}"
209
+ end
210
+
211
+ rescue => e
212
+ SmartPrompt.logger.error "Error downloading video: #{e.message}"
213
+ raise Error, "Error downloading video: #{e.message}"
214
+ end
215
+ end
216
+
217
+ # Wait for video generation to complete
218
+ def wait_for_video_completion(job_id, check_interval: 10, timeout: 600)
219
+ SmartPrompt.logger.info "VideoGenerationAdapter: Waiting for video generation to complete"
220
+
221
+ start_time = Time.now
222
+
223
+ loop do
224
+ status = check_video_status(job_id)
225
+
226
+ case status[:status]
227
+ when "completed"
228
+ SmartPrompt.logger.info "Video generation completed successfully"
229
+ return status
230
+ when "failed"
231
+ SmartPrompt.logger.error "Video generation failed"
232
+ raise LLMAPIError, "Video generation failed"
233
+ when "cancelled"
234
+ SmartPrompt.logger.error "Video generation was cancelled"
235
+ raise LLMAPIError, "Video generation was cancelled"
236
+ else
237
+ # Still processing
238
+ elapsed_time = Time.now - start_time
239
+ if elapsed_time > timeout
240
+ SmartPrompt.logger.error "Video generation timeout after #{timeout} seconds"
241
+ raise LLMAPIError, "Video generation timeout"
242
+ end
243
+
244
+ SmartPrompt.logger.info "Video generation in progress: #{status[:progress]}%"
245
+ sleep(check_interval)
246
+ end
247
+ end
248
+ end
249
+
250
+ private
251
+
252
+ # Custom implementation for video generation API call
253
+ def submit_video_generation_request(parameters)
254
+ # Since OpenAI gem doesn't support video endpoints, we implement custom HTTP call
255
+ uri = URI.parse("#{@config['url']}/videos/generations")
256
+
257
+ http = Net::HTTP.new(uri.host, uri.port)
258
+ http.use_ssl = (uri.scheme == 'https')
259
+ http.read_timeout = 600
260
+
261
+ request = Net::HTTP::Post.new(uri.request_uri)
262
+ request['Content-Type'] = 'application/json'
263
+ request['Authorization'] = "Bearer #{@config['api_key']}"
264
+
265
+ request.body = parameters.to_json
266
+
267
+ response = http.request(request)
268
+
269
+ if response.is_a?(Net::HTTPSuccess)
270
+ JSON.parse(response.body)
271
+ else
272
+ raise LLMAPIError, "Video generation API error: #{response.code} - #{response.body}"
273
+ end
274
+ end
275
+
276
+ # Custom implementation for image-to-video API call
277
+ def submit_image_to_video_request(parameters)
278
+ uri = URI.parse("#{@config['url']}/videos/generations")
279
+
280
+ http = Net::HTTP.new(uri.host, uri.port)
281
+ http.use_ssl = (uri.scheme == 'https')
282
+ http.read_timeout = 600
283
+
284
+ request = Net::HTTP::Post.new(uri.request_uri)
285
+ request['Content-Type'] = 'application/json'
286
+ request['Authorization'] = "Bearer #{@config['api_key']}"
287
+
288
+ request.body = parameters.to_json
289
+
290
+ response = http.request(request)
291
+
292
+ if response.is_a?(Net::HTTPSuccess)
293
+ JSON.parse(response.body)
294
+ else
295
+ raise LLMAPIError, "Image-to-video API error: #{response.code} - #{response.body}"
296
+ end
297
+ end
298
+
299
+ # Custom implementation for checking video generation status
300
+ def check_video_generation_status(job_id)
301
+ uri = URI.parse("#{@config['url']}/videos/#{job_id}")
302
+
303
+ http = Net::HTTP.new(uri.host, uri.port)
304
+ http.use_ssl = (uri.scheme == 'https')
305
+
306
+ request = Net::HTTP::Get.new(uri.request_uri)
307
+ request['Authorization'] = "Bearer #{@config['api_key']}"
308
+
309
+ response = http.request(request)
310
+
311
+ if response.is_a?(Net::HTTPSuccess)
312
+ JSON.parse(response.body)
313
+ else
314
+ raise LLMAPIError, "Status check API error: #{response.code} - #{response.body}"
315
+ end
316
+ end
317
+
318
+ # Override send_request to provide a meaningful error for chat operations
319
+ def send_request(messages, model = nil, temperature = 0.7, tools = nil, proc = nil)
320
+ SmartPrompt.logger.error "VideoGenerationAdapter does not support chat operations. Use generate_video, create_video_from_image, or check_video_status methods instead."
321
+ raise NotImplementedError, "VideoGenerationAdapter does not support chat operations"
322
+ end
323
+
324
+ # Override embeddings method
325
+ def embeddings(text, model)
326
+ SmartPrompt.logger.error "VideoGenerationAdapter does not support embeddings operations."
327
+ raise NotImplementedError, "VideoGenerationAdapter does not support embeddings operations"
328
+ end
329
+ end
330
+ end
@@ -11,13 +11,28 @@ module SmartPrompt
11
11
  end
12
12
 
13
13
  def execute(params = {})
14
- @conversation = Conversation.new(@engine, params[:tools]) unless @conversation
14
+ # Generate default session ID if using history and no session_id provided
15
+ session_id = params[:session_id] || "default"
16
+ if params[:with_history] && !session_id && @engine.history_manager
17
+ session_id = "worker_#{@name}_#{Time.now.to_i}"
18
+ SmartPrompt.logger.info "Generated default session ID: #{session_id}"
19
+ end
20
+ if @conversation.nil? || @conversation.session_id != session_id
21
+ @conversation = Conversation.new(@engine, params[:tools], session_id)
22
+ end
15
23
  context = WorkerContext.new(@conversation, params, @engine)
16
24
  context.instance_eval(&@code)
17
25
  end
18
26
 
19
27
  def execute_by_stream(params = {}, &proc)
20
- @conversation = Conversation.new(@engine, params[:tools])
28
+ # Generate default session ID if using history and no session_id provided
29
+ session_id = params[:session_id]
30
+ if params[:with_history] && !session_id && @engine.history_manager
31
+ session_id = "worker_#{@name}_#{Time.now.to_i}"
32
+ SmartPrompt.logger.info "Generated default session ID: #{session_id}"
33
+ end
34
+
35
+ @conversation = Conversation.new(@engine, params[:tools], session_id)
21
36
  context = WorkerContext.new(@conversation, params, @engine, proc)
22
37
  context.instance_eval(&@code)
23
38
  end
@@ -50,7 +65,7 @@ module SmartPrompt
50
65
  @conversation.send_msg_by_stream(params, &@proc)
51
66
  end
52
67
  elsif method == :sys_msg
53
- @conversation.sys_msg(*args, params)
68
+ @conversation.sys_msg(*args)
54
69
  elsif method == :prompt
55
70
  @conversation.prompt(*args, with_history: params[:with_history])
56
71
  else
@@ -73,6 +88,13 @@ module SmartPrompt
73
88
  @proc
74
89
  end
75
90
 
91
+ # Expose the engine so workers can reach a configured adapter directly (e.g.
92
+ # `engine.llms["..."]`) for methods Conversation doesn't delegate, such as
93
+ # generate_video / synthesize_to_file / transcribe_audio.
94
+ def engine
95
+ @engine
96
+ end
97
+
76
98
  def call_worker(worker_name, params = {})
77
99
  worker = Worker.new(worker_name, @engine)
78
100
  worker.execute(params)