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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -10
- data/README.cn.md +307 -64
- data/README.md +311 -64
- data/Rakefile +10 -1
- data/config/anthropic_config.yml +151 -0
- data/config/image_generation_config.yml +22 -0
- data/config/multimodal_config.yml +85 -0
- data/config/sensenova_config.yml +63 -0
- data/config/zhipu_config.yml +73 -0
- data/examples/anthropic_basic_chat.rb +143 -0
- data/examples/anthropic_example.rb +232 -0
- data/examples/anthropic_multimodal.rb +212 -0
- data/examples/anthropic_streaming.rb +312 -0
- data/examples/anthropic_tool_calling.rb +393 -0
- data/examples/automatic_cleanup_example.rb +109 -0
- data/examples/history_management_examples.rb +522 -0
- data/examples/image_generation_example.rb +130 -0
- data/examples/monitoring_example.rb +121 -0
- data/examples/multimodal_example.rb +63 -0
- data/examples/relevance_based_strategy_example.rb +87 -0
- data/examples/sensenova_example.rb +129 -0
- data/examples/stt_example.rb +287 -0
- data/examples/tts_example.rb +244 -0
- data/examples/video_generation_example.rb +189 -0
- data/examples/zhipu_example.rb +151 -0
- data/lib/smart_prompt/anthropic_adapter.rb +363 -281
- data/lib/smart_prompt/compression_engine.rb +201 -0
- data/lib/smart_prompt/context_strategy.rb +22 -0
- data/lib/smart_prompt/conversation.rb +81 -191
- data/lib/smart_prompt/engine.rb +36 -19
- data/lib/smart_prompt/history_manager.rb +596 -0
- data/lib/smart_prompt/hybrid_strategy.rb +222 -0
- data/lib/smart_prompt/image_generation_adapter.rb +297 -0
- data/lib/smart_prompt/lru_cache.rb +133 -0
- data/lib/smart_prompt/message.rb +57 -0
- data/lib/smart_prompt/multimodal_adapter.rb +277 -0
- data/lib/smart_prompt/openai_adapter.rb +1 -25
- data/lib/smart_prompt/persistence_layer.rb +197 -0
- data/lib/smart_prompt/relevance_based_strategy.rb +221 -0
- data/lib/smart_prompt/sensenova_adapter.rb +410 -0
- data/lib/smart_prompt/session.rb +140 -0
- data/lib/smart_prompt/sliding_window_strategy.rb +100 -0
- data/lib/smart_prompt/stt_adapter.rb +381 -0
- data/lib/smart_prompt/summary_based_strategy.rb +152 -0
- data/lib/smart_prompt/token_counter.rb +74 -0
- data/lib/smart_prompt/tts_adapter.rb +403 -0
- data/lib/smart_prompt/version.rb +1 -1
- data/lib/smart_prompt/video_generation_adapter.rb +330 -0
- data/lib/smart_prompt/worker.rb +25 -3
- data/lib/smart_prompt/zhipu_adapter.rb +616 -0
- data/lib/smart_prompt.rb +22 -2
- data/workers/history_management_examples.rb +407 -0
- data/workers/image_generation_workers.rb +119 -0
- data/workers/multimodal_workers.rb +110 -0
- data/workers/sensenova_workers.rb +62 -0
- data/workers/stt_workers.rb +195 -0
- data/workers/tts_workers.rb +388 -0
- data/workers/video_generation_workers.rb +264 -0
- data/workers/zhipu_workers.rb +113 -0
- 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
|
data/lib/smart_prompt/worker.rb
CHANGED
|
@@ -11,13 +11,28 @@ module SmartPrompt
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def execute(params = {})
|
|
14
|
-
|
|
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
|
-
|
|
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
|
|
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)
|