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,596 @@
|
|
|
1
|
+
require 'thread'
|
|
2
|
+
|
|
3
|
+
module SmartPrompt
|
|
4
|
+
# HistoryManager manages multiple conversation sessions with isolation and configuration
|
|
5
|
+
class HistoryManager
|
|
6
|
+
attr_reader :config
|
|
7
|
+
|
|
8
|
+
def initialize(config = {})
|
|
9
|
+
@config = default_config.merge(config)
|
|
10
|
+
@session_cache = LRUCache.new(@config[:cache_size])
|
|
11
|
+
@persistence = PersistenceLayer.new(@config[:persistence] || {})
|
|
12
|
+
@cleanup_thread = nil
|
|
13
|
+
@cleanup_mutex = Mutex.new
|
|
14
|
+
@session_mutex = Mutex.new # Add mutex for session creation
|
|
15
|
+
@shutdown_requested = false
|
|
16
|
+
|
|
17
|
+
# Initialize metrics tracking
|
|
18
|
+
@metrics = {
|
|
19
|
+
sessions_created: 0,
|
|
20
|
+
sessions_deleted: 0,
|
|
21
|
+
messages_added: 0,
|
|
22
|
+
context_retrievals: 0,
|
|
23
|
+
cache_hits: 0,
|
|
24
|
+
cache_misses: 0,
|
|
25
|
+
persistence_errors: 0,
|
|
26
|
+
compression_operations: 0,
|
|
27
|
+
tokens_saved_by_compression: 0
|
|
28
|
+
}
|
|
29
|
+
@metrics_mutex = Mutex.new
|
|
30
|
+
|
|
31
|
+
# Log initialization
|
|
32
|
+
log_info "HistoryManager initialized with cache_size=#{@config[:cache_size]}"
|
|
33
|
+
|
|
34
|
+
# Start cleanup thread if auto_cleanup is enabled
|
|
35
|
+
start_cleanup_thread if @config[:cleanup][:auto_cleanup]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Get or create a session
|
|
39
|
+
def get_session(session_id, options = {})
|
|
40
|
+
# Check if session is in cache
|
|
41
|
+
session = @session_cache.get(session_id)
|
|
42
|
+
|
|
43
|
+
if session
|
|
44
|
+
# Cache hit
|
|
45
|
+
increment_metric(:cache_hits)
|
|
46
|
+
log_debug "Session #{session_id} retrieved from cache"
|
|
47
|
+
return session
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Cache miss - use mutex to prevent race conditions
|
|
51
|
+
@session_mutex.synchronize do
|
|
52
|
+
# Double-check after acquiring lock
|
|
53
|
+
session = @session_cache.get(session_id)
|
|
54
|
+
return session if session
|
|
55
|
+
|
|
56
|
+
# Cache miss
|
|
57
|
+
increment_metric(:cache_misses)
|
|
58
|
+
log_debug "Session #{session_id} not in cache, loading or creating"
|
|
59
|
+
|
|
60
|
+
# Try to load from persistence first
|
|
61
|
+
session_data = @persistence.load(session_id)
|
|
62
|
+
|
|
63
|
+
if session_data
|
|
64
|
+
# Restore session from persisted data
|
|
65
|
+
session = restore_session(session_data, options)
|
|
66
|
+
log_info "Session #{session_id} restored from persistence (#{session.message_count} messages, #{session.total_tokens} tokens)"
|
|
67
|
+
else
|
|
68
|
+
# Create new session
|
|
69
|
+
session_config = @config[:session_defaults].merge(options)
|
|
70
|
+
session = Session.new(session_id, session_config)
|
|
71
|
+
increment_metric(:sessions_created)
|
|
72
|
+
log_info "Session #{session_id} created with config: max_messages=#{session_config[:max_messages]}, max_tokens=#{session_config[:max_tokens]}, strategy=#{session_config[:context_strategy]}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Add to cache (will handle eviction if needed)
|
|
76
|
+
@session_cache.put(session_id, session)
|
|
77
|
+
|
|
78
|
+
session
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Add a message to a session
|
|
83
|
+
def add_message(session_id, message, options = {})
|
|
84
|
+
begin
|
|
85
|
+
session = get_session(session_id, options)
|
|
86
|
+
msg = session.add_message(message)
|
|
87
|
+
|
|
88
|
+
increment_metric(:messages_added)
|
|
89
|
+
log_debug "Message added to session #{session_id}: role=#{msg.role}, tokens=#{msg.token_count}"
|
|
90
|
+
|
|
91
|
+
# Persist the session asynchronously
|
|
92
|
+
begin
|
|
93
|
+
@persistence.save_async(session)
|
|
94
|
+
rescue => e
|
|
95
|
+
increment_metric(:persistence_errors)
|
|
96
|
+
log_error "Persistence failed for session #{session_id}", e
|
|
97
|
+
# Continue without persistence
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
session
|
|
101
|
+
rescue => e
|
|
102
|
+
log_error "Failed to add message to session #{session_id}", e
|
|
103
|
+
raise HistoryManagerError, "Failed to add message: #{e.message}"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Get context (messages) from a session
|
|
108
|
+
def get_context(session_id, max_tokens = nil, strategy = nil)
|
|
109
|
+
begin
|
|
110
|
+
session = get_session(session_id)
|
|
111
|
+
messages = session.get_messages
|
|
112
|
+
|
|
113
|
+
increment_metric(:context_retrievals)
|
|
114
|
+
|
|
115
|
+
# If no token limit specified, return all messages
|
|
116
|
+
if max_tokens.nil?
|
|
117
|
+
log_debug "Context retrieved for session #{session_id}: all #{messages.count} messages (#{session.total_tokens} tokens)"
|
|
118
|
+
return messages
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Simple token limiting for now (will be enhanced with strategies later)
|
|
122
|
+
selected_messages = []
|
|
123
|
+
current_tokens = 0
|
|
124
|
+
|
|
125
|
+
# Always include system messages first
|
|
126
|
+
system_messages = messages.select(&:system_message?)
|
|
127
|
+
system_messages.each do |msg|
|
|
128
|
+
selected_messages << msg
|
|
129
|
+
current_tokens += msg.token_count || 0
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Add non-system messages from most recent, respecting token limit
|
|
133
|
+
non_system_messages = messages.reject(&:system_message?)
|
|
134
|
+
non_system_messages.reverse_each do |msg|
|
|
135
|
+
msg_tokens = msg.token_count || 0
|
|
136
|
+
if current_tokens + msg_tokens <= max_tokens
|
|
137
|
+
selected_messages << msg
|
|
138
|
+
current_tokens += msg_tokens
|
|
139
|
+
else
|
|
140
|
+
break
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Return in chronological order
|
|
145
|
+
result = selected_messages.sort_by(&:timestamp)
|
|
146
|
+
|
|
147
|
+
log_debug "Context selected for session #{session_id}: #{result.count}/#{messages.count} messages, #{current_tokens}/#{max_tokens} tokens"
|
|
148
|
+
|
|
149
|
+
result
|
|
150
|
+
rescue => e
|
|
151
|
+
log_error "Failed to get context for session #{session_id}", e
|
|
152
|
+
raise HistoryManagerError, "Failed to get context: #{e.message}"
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Clear a session's history
|
|
157
|
+
def clear_session(session_id, keep_system_messages: true)
|
|
158
|
+
begin
|
|
159
|
+
session = get_session(session_id)
|
|
160
|
+
messages_before = session.message_count
|
|
161
|
+
session.clear(preserve_system: keep_system_messages)
|
|
162
|
+
messages_after = session.message_count
|
|
163
|
+
|
|
164
|
+
log_info "Session #{session_id} cleared: #{messages_before} -> #{messages_after} messages (keep_system=#{keep_system_messages})"
|
|
165
|
+
rescue => e
|
|
166
|
+
log_error "Failed to clear session #{session_id}", e
|
|
167
|
+
raise HistoryManagerError, "Failed to clear session: #{e.message}"
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Delete a session completely
|
|
172
|
+
def delete_session(session_id)
|
|
173
|
+
begin
|
|
174
|
+
@session_cache.delete(session_id)
|
|
175
|
+
|
|
176
|
+
# Delete from persistence
|
|
177
|
+
@persistence.delete(session_id)
|
|
178
|
+
|
|
179
|
+
increment_metric(:sessions_deleted)
|
|
180
|
+
log_info "Session #{session_id} deleted"
|
|
181
|
+
rescue => e
|
|
182
|
+
log_error "Failed to delete session #{session_id}", e
|
|
183
|
+
raise HistoryManagerError, "Failed to delete session: #{e.message}"
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Export a session's data
|
|
188
|
+
def export_session(session_id, format: :json)
|
|
189
|
+
begin
|
|
190
|
+
session = get_session(session_id)
|
|
191
|
+
result = case format
|
|
192
|
+
when :json
|
|
193
|
+
require 'json'
|
|
194
|
+
JSON.pretty_generate(session.to_h)
|
|
195
|
+
when :hash
|
|
196
|
+
session.to_h
|
|
197
|
+
else
|
|
198
|
+
raise ArgumentError, "Unsupported format: #{format}"
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
log_info "Session #{session_id} exported in #{format} format"
|
|
202
|
+
result
|
|
203
|
+
rescue => e
|
|
204
|
+
log_error "Failed to export session #{session_id}", e
|
|
205
|
+
raise HistoryManagerError, "Failed to export session: #{e.message}"
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Search messages in a session
|
|
210
|
+
def search_messages(session_id, query, options = {})
|
|
211
|
+
begin
|
|
212
|
+
session = get_session(session_id)
|
|
213
|
+
messages = session.get_messages
|
|
214
|
+
|
|
215
|
+
results = messages.select do |msg|
|
|
216
|
+
msg.content.to_s.include?(query)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
log_debug "Search in session #{session_id} for '#{query}': #{results.count}/#{messages.count} matches"
|
|
220
|
+
results
|
|
221
|
+
rescue => e
|
|
222
|
+
log_error "Failed to search messages in session #{session_id}", e
|
|
223
|
+
raise HistoryManagerError, "Failed to search messages: #{e.message}"
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Get statistics for a session or all sessions
|
|
228
|
+
def get_stats(session_id = nil)
|
|
229
|
+
begin
|
|
230
|
+
if session_id
|
|
231
|
+
# Session-specific statistics
|
|
232
|
+
session = get_session(session_id)
|
|
233
|
+
{
|
|
234
|
+
session_id: session_id,
|
|
235
|
+
message_count: session.message_count,
|
|
236
|
+
total_tokens: session.total_tokens,
|
|
237
|
+
created_at: session.created_at,
|
|
238
|
+
updated_at: session.updated_at,
|
|
239
|
+
config: session.config
|
|
240
|
+
}
|
|
241
|
+
else
|
|
242
|
+
# System-wide statistics
|
|
243
|
+
@metrics_mutex.synchronize do
|
|
244
|
+
cache_total = @metrics[:cache_hits] + @metrics[:cache_misses]
|
|
245
|
+
cache_hit_rate = cache_total > 0 ? @metrics[:cache_hits].to_f / cache_total : 0.0
|
|
246
|
+
|
|
247
|
+
{
|
|
248
|
+
# Session metrics
|
|
249
|
+
active_sessions: @session_cache.size,
|
|
250
|
+
sessions_created: @metrics[:sessions_created],
|
|
251
|
+
sessions_deleted: @metrics[:sessions_deleted],
|
|
252
|
+
|
|
253
|
+
# Message metrics
|
|
254
|
+
total_messages: @session_cache.values.sum(&:message_count),
|
|
255
|
+
messages_added: @metrics[:messages_added],
|
|
256
|
+
messages_per_session_avg: @session_cache.size > 0 ?
|
|
257
|
+
@session_cache.values.sum(&:message_count).to_f / @session_cache.size : 0.0,
|
|
258
|
+
|
|
259
|
+
# Token metrics
|
|
260
|
+
total_tokens: @session_cache.values.sum(&:total_tokens),
|
|
261
|
+
tokens_per_session_avg: @session_cache.size > 0 ?
|
|
262
|
+
@session_cache.values.sum(&:total_tokens).to_f / @session_cache.size : 0.0,
|
|
263
|
+
tokens_per_message_avg: @session_cache.values.sum(&:message_count) > 0 ?
|
|
264
|
+
@session_cache.values.sum(&:total_tokens).to_f / @session_cache.values.sum(&:message_count) : 0.0,
|
|
265
|
+
|
|
266
|
+
# Cache metrics
|
|
267
|
+
cache_size: @config[:cache_size],
|
|
268
|
+
cache_hits: @metrics[:cache_hits],
|
|
269
|
+
cache_misses: @metrics[:cache_misses],
|
|
270
|
+
cache_hit_rate: cache_hit_rate,
|
|
271
|
+
|
|
272
|
+
# Operation metrics
|
|
273
|
+
context_retrievals: @metrics[:context_retrievals],
|
|
274
|
+
|
|
275
|
+
# Compression metrics
|
|
276
|
+
compression_operations: @metrics[:compression_operations],
|
|
277
|
+
tokens_saved_by_compression: @metrics[:tokens_saved_by_compression],
|
|
278
|
+
|
|
279
|
+
# Error metrics
|
|
280
|
+
persistence_errors: @metrics[:persistence_errors]
|
|
281
|
+
}
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
rescue => e
|
|
285
|
+
log_error "Failed to get statistics#{session_id ? " for session #{session_id}" : ""}", e
|
|
286
|
+
raise HistoryManagerError, "Failed to get statistics: #{e.message}"
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Check if a session exists
|
|
291
|
+
def session_exists?(session_id)
|
|
292
|
+
@session_cache.key?(session_id)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Get list of all session IDs
|
|
296
|
+
def session_ids
|
|
297
|
+
@session_cache.keys
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Get the least recently used session ID
|
|
301
|
+
def lru_session_id
|
|
302
|
+
@session_cache.lru_key
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Export metrics in a standard format (Prometheus-style)
|
|
306
|
+
def export_metrics(format: :prometheus)
|
|
307
|
+
stats = get_stats
|
|
308
|
+
|
|
309
|
+
case format
|
|
310
|
+
when :prometheus
|
|
311
|
+
export_prometheus_metrics(stats)
|
|
312
|
+
when :json
|
|
313
|
+
require 'json'
|
|
314
|
+
JSON.pretty_generate(stats)
|
|
315
|
+
when :hash
|
|
316
|
+
stats
|
|
317
|
+
else
|
|
318
|
+
raise ArgumentError, "Unsupported format: #{format}"
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Shutdown the history manager gracefully
|
|
323
|
+
def shutdown
|
|
324
|
+
@shutdown_requested = true
|
|
325
|
+
|
|
326
|
+
# Stop cleanup thread
|
|
327
|
+
if @cleanup_thread
|
|
328
|
+
@cleanup_thread.join(5) # Wait up to 5 seconds for thread to finish
|
|
329
|
+
@cleanup_thread = nil
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
@persistence.shutdown if @persistence
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Manually trigger cleanup of expired sessions
|
|
336
|
+
def cleanup_expired_sessions
|
|
337
|
+
return unless @config[:cleanup]
|
|
338
|
+
|
|
339
|
+
session_ttl = @config[:cleanup][:session_ttl]
|
|
340
|
+
cleanup_callback = @config[:cleanup][:cleanup_callback]
|
|
341
|
+
|
|
342
|
+
expired_session_ids = []
|
|
343
|
+
|
|
344
|
+
@cleanup_mutex.synchronize do
|
|
345
|
+
@session_cache.keys.each do |session_id|
|
|
346
|
+
session = @session_cache.get(session_id)
|
|
347
|
+
next unless session
|
|
348
|
+
|
|
349
|
+
# Check if session has expired based on TTL
|
|
350
|
+
age = Time.now - session.updated_at
|
|
351
|
+
should_cleanup = age > session_ttl
|
|
352
|
+
|
|
353
|
+
# If custom callback is provided, use it to determine cleanup
|
|
354
|
+
if cleanup_callback && cleanup_callback.respond_to?(:call)
|
|
355
|
+
should_cleanup = cleanup_callback.call(session, age)
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
if should_cleanup
|
|
359
|
+
expired_session_ids << session_id
|
|
360
|
+
log_debug "Session #{session_id} marked for cleanup (age: #{age.to_i}s, ttl: #{session_ttl}s)"
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# Remove expired sessions
|
|
365
|
+
expired_session_ids.each do |session_id|
|
|
366
|
+
delete_session(session_id)
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
if expired_session_ids.any?
|
|
371
|
+
log_info "Cleanup completed: #{expired_session_ids.count} expired sessions removed"
|
|
372
|
+
else
|
|
373
|
+
log_debug "Cleanup completed: no expired sessions found"
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
expired_session_ids
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
private
|
|
380
|
+
|
|
381
|
+
# Restore a session from persisted data
|
|
382
|
+
def restore_session(session_data, options = {})
|
|
383
|
+
session_config = @config[:session_defaults].merge(options)
|
|
384
|
+
session = Session.new(session_data[:id], session_config)
|
|
385
|
+
|
|
386
|
+
# Restore metadata
|
|
387
|
+
session.instance_variable_set(:@metadata, session_data[:metadata] || {})
|
|
388
|
+
|
|
389
|
+
# Restore timestamps
|
|
390
|
+
session.instance_variable_set(:@created_at, Time.parse(session_data[:created_at])) if session_data[:created_at]
|
|
391
|
+
session.instance_variable_set(:@updated_at, Time.parse(session_data[:updated_at])) if session_data[:updated_at]
|
|
392
|
+
|
|
393
|
+
# Restore messages
|
|
394
|
+
if session_data[:messages]
|
|
395
|
+
session_data[:messages].each do |msg_data|
|
|
396
|
+
session.add_message(msg_data)
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
session
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# Start the cleanup thread
|
|
404
|
+
def start_cleanup_thread
|
|
405
|
+
return if @cleanup_thread && @cleanup_thread.alive?
|
|
406
|
+
|
|
407
|
+
cleanup_interval = @config[:cleanup][:cleanup_interval]
|
|
408
|
+
|
|
409
|
+
log_info "Starting cleanup thread with interval=#{cleanup_interval}s, ttl=#{@config[:cleanup][:session_ttl]}s"
|
|
410
|
+
|
|
411
|
+
@cleanup_thread = Thread.new do
|
|
412
|
+
loop do
|
|
413
|
+
break if @shutdown_requested
|
|
414
|
+
|
|
415
|
+
begin
|
|
416
|
+
sleep(cleanup_interval)
|
|
417
|
+
break if @shutdown_requested
|
|
418
|
+
|
|
419
|
+
# Perform cleanup
|
|
420
|
+
cleanup_expired_sessions
|
|
421
|
+
rescue => e
|
|
422
|
+
# Log error but keep thread running
|
|
423
|
+
log_error "Cleanup thread error", e
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
log_info "Cleanup thread stopped"
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
@cleanup_thread
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
# Default configuration
|
|
434
|
+
def default_config
|
|
435
|
+
{
|
|
436
|
+
cache_size: 100,
|
|
437
|
+
session_defaults: {
|
|
438
|
+
max_messages: 100,
|
|
439
|
+
max_tokens: 4000,
|
|
440
|
+
context_strategy: :sliding_window,
|
|
441
|
+
preserve_system_messages: true
|
|
442
|
+
},
|
|
443
|
+
persistence: {
|
|
444
|
+
enabled: true,
|
|
445
|
+
backend: :filesystem,
|
|
446
|
+
storage_path: "./history_data",
|
|
447
|
+
async: true
|
|
448
|
+
},
|
|
449
|
+
cleanup: {
|
|
450
|
+
auto_cleanup: false,
|
|
451
|
+
cleanup_interval: 3600, # 1 hour in seconds
|
|
452
|
+
session_ttl: 86400, # 24 hours in seconds
|
|
453
|
+
cleanup_callback: nil
|
|
454
|
+
},
|
|
455
|
+
monitoring: {
|
|
456
|
+
enabled: true,
|
|
457
|
+
log_level: :info
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
# Increment a metric counter
|
|
463
|
+
def increment_metric(metric_name, amount = 1)
|
|
464
|
+
@metrics_mutex.synchronize do
|
|
465
|
+
@metrics[metric_name] ||= 0
|
|
466
|
+
@metrics[metric_name] += amount
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
# Logging helper methods
|
|
471
|
+
def log_info(message)
|
|
472
|
+
return unless monitoring_enabled?
|
|
473
|
+
return unless log_level_enabled?(:info)
|
|
474
|
+
SmartPrompt.logger.info "[HistoryManager] #{message}"
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
def log_debug(message)
|
|
478
|
+
return unless monitoring_enabled?
|
|
479
|
+
return unless log_level_enabled?(:debug)
|
|
480
|
+
SmartPrompt.logger.debug "[HistoryManager] #{message}"
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
def log_warn(message)
|
|
484
|
+
return unless monitoring_enabled?
|
|
485
|
+
SmartPrompt.logger.warn "[HistoryManager] #{message}"
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
def log_error(message, exception = nil)
|
|
489
|
+
return unless monitoring_enabled?
|
|
490
|
+
|
|
491
|
+
error_msg = "[HistoryManager] #{message}"
|
|
492
|
+
if exception
|
|
493
|
+
error_msg += ": #{exception.class.name} - #{exception.message}"
|
|
494
|
+
error_msg += "\n#{exception.backtrace.first(5).join("\n")}" if exception.backtrace
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
SmartPrompt.logger.error error_msg
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
def monitoring_enabled?
|
|
501
|
+
@config[:monitoring] && @config[:monitoring][:enabled] != false
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
def log_level_enabled?(level)
|
|
505
|
+
return true unless @config[:monitoring]
|
|
506
|
+
|
|
507
|
+
configured_level = @config[:monitoring][:log_level] || :info
|
|
508
|
+
# Convert to symbol if it's a string
|
|
509
|
+
configured_level = configured_level.to_sym if configured_level.is_a?(String)
|
|
510
|
+
level_priority = { debug: 0, info: 1, warn: 2, error: 3 }
|
|
511
|
+
|
|
512
|
+
# Return true if either level is not in the priority hash (to avoid nil comparison)
|
|
513
|
+
return true unless level_priority.key?(level) && level_priority.key?(configured_level)
|
|
514
|
+
|
|
515
|
+
level_priority[level] >= level_priority[configured_level]
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
# Export metrics in Prometheus format
|
|
519
|
+
def export_prometheus_metrics(stats)
|
|
520
|
+
lines = []
|
|
521
|
+
|
|
522
|
+
# Session metrics
|
|
523
|
+
lines << "# HELP smart_prompt_active_sessions Number of active sessions in cache"
|
|
524
|
+
lines << "# TYPE smart_prompt_active_sessions gauge"
|
|
525
|
+
lines << "smart_prompt_active_sessions #{stats[:active_sessions]}"
|
|
526
|
+
|
|
527
|
+
lines << "# HELP smart_prompt_sessions_created_total Total number of sessions created"
|
|
528
|
+
lines << "# TYPE smart_prompt_sessions_created_total counter"
|
|
529
|
+
lines << "smart_prompt_sessions_created_total #{stats[:sessions_created]}"
|
|
530
|
+
|
|
531
|
+
lines << "# HELP smart_prompt_sessions_deleted_total Total number of sessions deleted"
|
|
532
|
+
lines << "# TYPE smart_prompt_sessions_deleted_total counter"
|
|
533
|
+
lines << "smart_prompt_sessions_deleted_total #{stats[:sessions_deleted]}"
|
|
534
|
+
|
|
535
|
+
# Message metrics
|
|
536
|
+
lines << "# HELP smart_prompt_total_messages Total number of messages across all sessions"
|
|
537
|
+
lines << "# TYPE smart_prompt_total_messages gauge"
|
|
538
|
+
lines << "smart_prompt_total_messages #{stats[:total_messages]}"
|
|
539
|
+
|
|
540
|
+
lines << "# HELP smart_prompt_messages_added_total Total number of messages added"
|
|
541
|
+
lines << "# TYPE smart_prompt_messages_added_total counter"
|
|
542
|
+
lines << "smart_prompt_messages_added_total #{stats[:messages_added]}"
|
|
543
|
+
|
|
544
|
+
lines << "# HELP smart_prompt_messages_per_session_avg Average messages per session"
|
|
545
|
+
lines << "# TYPE smart_prompt_messages_per_session_avg gauge"
|
|
546
|
+
lines << "smart_prompt_messages_per_session_avg #{stats[:messages_per_session_avg]}"
|
|
547
|
+
|
|
548
|
+
# Token metrics
|
|
549
|
+
lines << "# HELP smart_prompt_total_tokens Total number of tokens across all sessions"
|
|
550
|
+
lines << "# TYPE smart_prompt_total_tokens gauge"
|
|
551
|
+
lines << "smart_prompt_total_tokens #{stats[:total_tokens]}"
|
|
552
|
+
|
|
553
|
+
lines << "# HELP smart_prompt_tokens_per_session_avg Average tokens per session"
|
|
554
|
+
lines << "# TYPE smart_prompt_tokens_per_session_avg gauge"
|
|
555
|
+
lines << "smart_prompt_tokens_per_session_avg #{stats[:tokens_per_session_avg]}"
|
|
556
|
+
|
|
557
|
+
lines << "# HELP smart_prompt_tokens_per_message_avg Average tokens per message"
|
|
558
|
+
lines << "# TYPE smart_prompt_tokens_per_message_avg gauge"
|
|
559
|
+
lines << "smart_prompt_tokens_per_message_avg #{stats[:tokens_per_message_avg]}"
|
|
560
|
+
|
|
561
|
+
# Cache metrics
|
|
562
|
+
lines << "# HELP smart_prompt_cache_hits_total Total number of cache hits"
|
|
563
|
+
lines << "# TYPE smart_prompt_cache_hits_total counter"
|
|
564
|
+
lines << "smart_prompt_cache_hits_total #{stats[:cache_hits]}"
|
|
565
|
+
|
|
566
|
+
lines << "# HELP smart_prompt_cache_misses_total Total number of cache misses"
|
|
567
|
+
lines << "# TYPE smart_prompt_cache_misses_total counter"
|
|
568
|
+
lines << "smart_prompt_cache_misses_total #{stats[:cache_misses]}"
|
|
569
|
+
|
|
570
|
+
lines << "# HELP smart_prompt_cache_hit_rate Cache hit rate (0.0-1.0)"
|
|
571
|
+
lines << "# TYPE smart_prompt_cache_hit_rate gauge"
|
|
572
|
+
lines << "smart_prompt_cache_hit_rate #{stats[:cache_hit_rate]}"
|
|
573
|
+
|
|
574
|
+
# Operation metrics
|
|
575
|
+
lines << "# HELP smart_prompt_context_retrievals_total Total number of context retrievals"
|
|
576
|
+
lines << "# TYPE smart_prompt_context_retrievals_total counter"
|
|
577
|
+
lines << "smart_prompt_context_retrievals_total #{stats[:context_retrievals]}"
|
|
578
|
+
|
|
579
|
+
# Compression metrics
|
|
580
|
+
lines << "# HELP smart_prompt_compression_operations_total Total number of compression operations"
|
|
581
|
+
lines << "# TYPE smart_prompt_compression_operations_total counter"
|
|
582
|
+
lines << "smart_prompt_compression_operations_total #{stats[:compression_operations]}"
|
|
583
|
+
|
|
584
|
+
lines << "# HELP smart_prompt_tokens_saved_by_compression_total Total tokens saved by compression"
|
|
585
|
+
lines << "# TYPE smart_prompt_tokens_saved_by_compression_total counter"
|
|
586
|
+
lines << "smart_prompt_tokens_saved_by_compression_total #{stats[:tokens_saved_by_compression]}"
|
|
587
|
+
|
|
588
|
+
# Error metrics
|
|
589
|
+
lines << "# HELP smart_prompt_persistence_errors_total Total number of persistence errors"
|
|
590
|
+
lines << "# TYPE smart_prompt_persistence_errors_total counter"
|
|
591
|
+
lines << "smart_prompt_persistence_errors_total #{stats[:persistence_errors]}"
|
|
592
|
+
|
|
593
|
+
lines.join("\n")
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
end
|