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.
- 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 -149
- 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 +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
|
-
@
|
|
37
|
-
@
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
114
|
-
add_message({ role: "system", content:
|
|
115
|
-
|
|
116
|
-
|
|
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 =
|
|
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 =
|
|
131
|
+
@last_response = @current_llm.send_request(history_messages, @model_name, @temperature, @tools, nil)
|
|
151
132
|
else
|
|
152
|
-
@last_response =
|
|
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
|
-
|
|
160
|
+
@current_llm.send_request(history_messages, @model_name, @temperature, @tools, proc)
|
|
170
161
|
else
|
|
171
|
-
|
|
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
|
-
|
|
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
|
|
218
|
-
|
|
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
|
|
276
|
-
|
|
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
|
data/lib/smart_prompt/engine.rb
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
module SmartPrompt
|
|
2
2
|
class Engine
|
|
3
|
-
attr_reader :config_file, :config, :adapters, :current_adapter, :llms, :
|
|
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
|
-
|
|
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":
|
|
137
|
+
"content": result,
|
|
127
138
|
}
|
|
128
139
|
elsif result.class == Array
|
|
129
140
|
recive_message = nil
|
|
130
141
|
else
|
|
131
|
-
recive_message =
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
190
|
-
|
|
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
|