smart_prompt 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -2
- data/README.cn.md +55 -4
- data/README.md +55 -4
- data/docs/ANTHROPIC_EXAMPLES.md +559 -0
- data/docs/CONVERSATION_INTEGRATION_SUMMARY.md +155 -0
- data/docs/HISTORY_EXAMPLES_README.md +533 -0
- data/docs/HISTORY_MANAGEMENT_GUIDE.md +797 -0
- data/docs/MONITORING_GUIDE.md +278 -0
- data/docs/MULTIMODAL_README.md +265 -0
- data/docs/RELEVANCE_BASED_STRATEGY_IMPLEMENTATION.md +124 -0
- data/docs/STT_README.md +302 -0
- data/docs/TTS_README.md +303 -0
- data/docs/VIDEO_GENERATION_README.md +246 -0
- data/docs/delete_files_list.md +124 -0
- data/lib/smart_prompt/anthropic_adapter.rb +167 -140
- data/lib/smart_prompt/conversation.rb +195 -42
- data/lib/smart_prompt/engine.rb +20 -10
- data/lib/smart_prompt/openai_adapter.rb +25 -1
- data/lib/smart_prompt/version.rb +1 -1
- data/lib/smart_prompt/worker.rb +5 -2
- data/lib/smart_prompt.rb +2 -1
- metadata +33 -22
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
require "anthropic"
|
|
2
|
-
require "base64"
|
|
3
|
-
require "uri"
|
|
4
|
-
require "json"
|
|
1
|
+
require "anthropic"
|
|
2
|
+
require "base64"
|
|
3
|
+
require "uri"
|
|
4
|
+
require "json"
|
|
5
5
|
|
|
6
6
|
module SmartPrompt
|
|
7
7
|
class AnthropicAdapter < LLMAdapter
|
|
@@ -78,7 +78,10 @@ module SmartPrompt
|
|
|
78
78
|
content
|
|
79
79
|
end
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
# String/scalar content becomes a single-element block array;
|
|
82
|
+
# already-array (multimodal) content must not be double-wrapped.
|
|
83
|
+
final_content = converted_content.is_a?(Array) ? converted_content : [converted_content]
|
|
84
|
+
{ role: role, content: final_content }
|
|
82
85
|
end
|
|
83
86
|
end
|
|
84
87
|
|
|
@@ -148,139 +151,163 @@ module SmartPrompt
|
|
|
148
151
|
input_schema: parameters,
|
|
149
152
|
}
|
|
150
153
|
end.compact # Remove nil values from failed conversions
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Extract plain text from an Anthropic response's `content` field.
|
|
157
|
+
# Handles a String, an Array of content blocks, nil, or an empty array.
|
|
158
|
+
# @param response [Hash] Anthropic response (or its `content` value)
|
|
159
|
+
# @return [String] Concatenated text, with multiple text blocks joined by newlines
|
|
160
|
+
def extract_content_from_response(response)
|
|
161
|
+
content = if response.is_a?(Hash)
|
|
162
|
+
response["content"] || response[:content]
|
|
163
|
+
else
|
|
164
|
+
response
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
case content
|
|
168
|
+
when String
|
|
169
|
+
content
|
|
170
|
+
when Array
|
|
171
|
+
content.map do |block|
|
|
172
|
+
next block unless block.is_a?(Hash)
|
|
173
|
+
block["text"] || block[:text]
|
|
174
|
+
end.compact.reject(&:empty?).join("\n")
|
|
175
|
+
else
|
|
176
|
+
content.to_s
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def convert_response_to_openai_format(response)
|
|
181
|
+
begin
|
|
182
|
+
# Normalize response to a Hash with symbol keys
|
|
183
|
+
raw_response = if response.respond_to?(:to_h)
|
|
184
|
+
response.to_h
|
|
185
|
+
elsif response.is_a?(Hash)
|
|
186
|
+
response
|
|
187
|
+
else
|
|
188
|
+
JSON.parse(response.to_json)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
response_hash = deep_symbolize(raw_response)
|
|
192
|
+
|
|
193
|
+
# Handle content blocks (text, tool_use, etc.)
|
|
194
|
+
content_blocks = response_hash[:content] || []
|
|
195
|
+
text_content = ""
|
|
196
|
+
tool_calls = []
|
|
197
|
+
|
|
198
|
+
case content_blocks
|
|
199
|
+
when String
|
|
200
|
+
text_content = content_blocks
|
|
201
|
+
when Array
|
|
202
|
+
content_blocks.each do |block|
|
|
203
|
+
block_hash = block.respond_to?(:to_h) ? block.to_h : block
|
|
204
|
+
block_hash = deep_symbolize(block_hash)
|
|
205
|
+
next unless block_hash.is_a?(Hash)
|
|
206
|
+
|
|
207
|
+
case block_hash[:type]
|
|
208
|
+
when "text"
|
|
209
|
+
text_content << block_hash[:text].to_s
|
|
210
|
+
when "tool_use"
|
|
211
|
+
tool_calls << {
|
|
212
|
+
"index" => tool_calls.size,
|
|
213
|
+
"id" => block_hash[:id] || "tool_call_#{tool_calls.size}",
|
|
214
|
+
"type" => "function",
|
|
215
|
+
"function" => {
|
|
216
|
+
"name" => block_hash[:name],
|
|
217
|
+
"arguments" => JSON.generate(block_hash[:input] || {}),
|
|
218
|
+
},
|
|
219
|
+
}
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
else
|
|
223
|
+
text_content = content_blocks.to_s
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Map stop reason to OpenAI finish_reason semantics
|
|
227
|
+
stop_reason = response_hash[:stop_reason] || response_hash[:finish_reason]
|
|
228
|
+
finish_reason = case stop_reason
|
|
229
|
+
when "tool_use"
|
|
230
|
+
"tool_calls"
|
|
231
|
+
when "end_turn", nil
|
|
232
|
+
"stop"
|
|
233
|
+
else
|
|
234
|
+
stop_reason
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Map usage information
|
|
238
|
+
usage = response_hash[:usage] || {}
|
|
239
|
+
prompt_tokens = usage[:input_tokens]
|
|
240
|
+
completion_tokens = usage[:output_tokens]
|
|
241
|
+
cache_read_tokens = usage[:cache_read_input_tokens]
|
|
242
|
+
cache_creation_tokens = usage[:cache_creation_input_tokens]
|
|
243
|
+
total_tokens = if prompt_tokens || completion_tokens
|
|
244
|
+
[prompt_tokens, completion_tokens].compact.sum
|
|
245
|
+
end
|
|
246
|
+
prompt_cache_hit_tokens = cache_read_tokens
|
|
247
|
+
prompt_cache_miss_tokens = if prompt_tokens && cache_read_tokens
|
|
248
|
+
prompt_tokens - cache_read_tokens
|
|
249
|
+
end
|
|
250
|
+
prompt_tokens_details = {}
|
|
251
|
+
prompt_tokens_details["cached_tokens"] = cache_read_tokens if cache_read_tokens
|
|
252
|
+
|
|
253
|
+
usage_hash = {}
|
|
254
|
+
usage_hash["prompt_tokens"] = prompt_tokens if prompt_tokens
|
|
255
|
+
usage_hash["completion_tokens"] = completion_tokens if completion_tokens
|
|
256
|
+
usage_hash["total_tokens"] = total_tokens if total_tokens
|
|
257
|
+
usage_hash["prompt_tokens_details"] = prompt_tokens_details unless prompt_tokens_details.empty?
|
|
258
|
+
usage_hash["prompt_cache_hit_tokens"] = prompt_cache_hit_tokens if prompt_cache_hit_tokens
|
|
259
|
+
usage_hash["prompt_cache_miss_tokens"] = prompt_cache_miss_tokens if prompt_cache_miss_tokens
|
|
260
|
+
|
|
261
|
+
created_ts = response_hash[:created_at] || response_hash[:created] || Time.now.to_i
|
|
262
|
+
|
|
263
|
+
message_role = response_hash[:role] || "assistant"
|
|
264
|
+
|
|
265
|
+
openai_response = {
|
|
266
|
+
"id" => response_hash[:id],
|
|
267
|
+
"object" => "chat.completion",
|
|
268
|
+
"created" => created_ts,
|
|
269
|
+
"model" => response_hash[:model],
|
|
270
|
+
"choices" => [
|
|
271
|
+
{
|
|
272
|
+
"index" => 0,
|
|
273
|
+
"message" => {
|
|
274
|
+
"role" => message_role,
|
|
275
|
+
"content" => text_content.empty? ? nil : text_content,
|
|
276
|
+
},
|
|
277
|
+
"finish_reason" => finish_reason,
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
unless tool_calls.empty?
|
|
283
|
+
openai_response["choices"][0]["message"]["tool_calls"] = tool_calls
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
openai_response["usage"] = usage_hash unless usage_hash.empty?
|
|
287
|
+
openai_response["system_fingerprint"] = response_hash[:system_fingerprint] if response_hash[:system_fingerprint]
|
|
288
|
+
|
|
289
|
+
@last_response = openai_response
|
|
290
|
+
openai_response
|
|
291
|
+
rescue => e
|
|
292
|
+
SmartPrompt.logger.error "Failed to convert Anthropic response: #{e.message}"
|
|
293
|
+
raise LLMAPIError, "Failed to convert Anthropic response: #{e.message}"
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Deeply symbolize hash keys for consistent access
|
|
298
|
+
def deep_symbolize(obj)
|
|
299
|
+
case obj
|
|
300
|
+
when Hash
|
|
301
|
+
obj.each_with_object({}) do |(k, v), memo|
|
|
302
|
+
key = k.is_a?(String) || k.is_a?(Symbol) ? k.to_sym : k
|
|
303
|
+
memo[key] = deep_symbolize(v)
|
|
304
|
+
end
|
|
305
|
+
when Array
|
|
306
|
+
obj.map { |item| deep_symbolize(item) }
|
|
307
|
+
else
|
|
308
|
+
obj
|
|
309
|
+
end
|
|
310
|
+
end
|
|
284
311
|
|
|
285
312
|
public
|
|
286
313
|
|
|
@@ -290,8 +317,8 @@ module SmartPrompt
|
|
|
290
317
|
# @param temperature [Float, nil] Temperature value (optional, uses config or 0.7 if nil)
|
|
291
318
|
# @param tools [Array, nil] Array of tool definitions (optional)
|
|
292
319
|
# @param proc [Proc, nil] Callback for streaming responses (optional)
|
|
293
|
-
# @return [Hash, nil] OpenAI-formatted response (nil for streaming mode)
|
|
294
|
-
def send_request(messages, model = nil, temperature = nil, tools = nil, proc = nil)
|
|
320
|
+
# @return [Hash, nil] OpenAI-formatted response (nil for streaming mode)
|
|
321
|
+
def send_request(messages, model = nil, temperature = nil, tools = nil, proc = nil)
|
|
295
322
|
begin
|
|
296
323
|
# Determine model name (parameter > config)
|
|
297
324
|
model_name = model || @config["model"]
|
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
require "yaml"
|
|
2
2
|
require "retriable"
|
|
3
3
|
require "numo/narray"
|
|
4
|
+
require "base64"
|
|
4
5
|
|
|
5
6
|
module SmartPrompt
|
|
6
7
|
class Conversation
|
|
7
8
|
include APIHandler
|
|
9
|
+
MODEL_REQUEST_OPTION_KEYS = %w[
|
|
10
|
+
max_tokens
|
|
11
|
+
max_completion_tokens
|
|
12
|
+
top_p
|
|
13
|
+
top_k
|
|
14
|
+
response_format
|
|
15
|
+
tool_choice
|
|
16
|
+
parallel_tool_calls
|
|
17
|
+
seed
|
|
18
|
+
stop
|
|
19
|
+
].freeze
|
|
20
|
+
|
|
8
21
|
attr_reader :messages, :last_response, :config_file
|
|
9
22
|
attr_reader :last_call_id
|
|
10
23
|
attr_reader :session_id
|
|
@@ -15,34 +28,66 @@ module SmartPrompt
|
|
|
15
28
|
@engine = engine
|
|
16
29
|
@adapters = engine.adapters
|
|
17
30
|
@llms = engine.llms
|
|
31
|
+
@models = engine.models
|
|
18
32
|
@current_llm_name = nil
|
|
19
33
|
@templates = engine.templates
|
|
20
34
|
@temperature = 0.7
|
|
21
35
|
@current_adapter = engine.current_adapter
|
|
22
36
|
@last_response = nil
|
|
23
37
|
@tools = tools
|
|
38
|
+
@request_options = {}
|
|
39
|
+
@pending_content_parts = []
|
|
40
|
+
@thinking_enabled = nil
|
|
24
41
|
@session_id = session_id
|
|
25
42
|
@use_history_manager = false
|
|
26
43
|
end
|
|
27
44
|
|
|
28
45
|
def use(llm_name)
|
|
29
|
-
|
|
46
|
+
llm_name = llm_name.to_s
|
|
47
|
+
raise ConfigurationError, "LLM #{llm_name} not configured" unless @llms.key?(llm_name)
|
|
30
48
|
@current_llm = @llms[llm_name]
|
|
31
49
|
@current_llm_name = llm_name
|
|
32
50
|
self
|
|
33
51
|
end
|
|
34
52
|
|
|
53
|
+
def use_model(model_name)
|
|
54
|
+
model_name = model_name.to_s
|
|
55
|
+
model_config = @models[model_name] || @models[model_name.to_sym]
|
|
56
|
+
raise ConfigurationError, "Model #{model_name} not configured" unless model_config
|
|
57
|
+
|
|
58
|
+
llm_name = model_config["use"] || model_config[:use]
|
|
59
|
+
configured_model_name = model_config["model"] || model_config[:model]
|
|
60
|
+
raise ConfigurationError, "Model #{model_name} must define use" if llm_name.nil? || llm_name.empty?
|
|
61
|
+
raise ConfigurationError, "Model #{model_name} must define model" if configured_model_name.nil? || configured_model_name.empty?
|
|
62
|
+
|
|
63
|
+
use(llm_name)
|
|
64
|
+
model(configured_model_name)
|
|
65
|
+
merge_model_request_options(model_config)
|
|
66
|
+
self
|
|
67
|
+
end
|
|
68
|
+
|
|
35
69
|
def model(model_name)
|
|
36
70
|
@model_name = model_name
|
|
37
|
-
if @engine.config["better_prompt_db"]
|
|
38
|
-
BetterPrompt.add_model(@current_llm_name, @model_name)
|
|
39
|
-
end
|
|
40
71
|
end
|
|
41
72
|
|
|
42
73
|
def temperature(temperature)
|
|
43
74
|
@temperature = temperature
|
|
44
75
|
end
|
|
45
76
|
|
|
77
|
+
def request_options(options = {})
|
|
78
|
+
@request_options.merge!(options || {})
|
|
79
|
+
self
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def thinking(enabled = true)
|
|
83
|
+
@thinking_enabled = enabled
|
|
84
|
+
if @sys_msg
|
|
85
|
+
@sys_msg = thinking_system_message(@sys_msg)
|
|
86
|
+
refresh_system_message(@sys_msg)
|
|
87
|
+
end
|
|
88
|
+
self
|
|
89
|
+
end
|
|
90
|
+
|
|
46
91
|
def history_messages
|
|
47
92
|
# If using HistoryManager, get messages from session
|
|
48
93
|
if @use_history_manager && @engine.history_manager
|
|
@@ -77,32 +122,43 @@ module SmartPrompt
|
|
|
77
122
|
SmartPrompt.logger.info "Use template #{template_name}"
|
|
78
123
|
raise "Template #{template_name} not found" unless @templates.key?(template_name)
|
|
79
124
|
content = @templates[template_name].render(params)
|
|
80
|
-
|
|
81
|
-
if @engine.config["better_prompt_db"]
|
|
82
|
-
BetterPrompt.add_prompt(template_name, "user", content)
|
|
83
|
-
end
|
|
125
|
+
add_user_content(content, with_history)
|
|
84
126
|
self
|
|
85
127
|
else
|
|
86
|
-
|
|
87
|
-
if @engine.config["better_prompt_db"]
|
|
88
|
-
BetterPrompt.add_prompt("NULL", "user", template_name)
|
|
89
|
-
end
|
|
128
|
+
add_user_content(template_name, with_history)
|
|
90
129
|
self
|
|
91
130
|
end
|
|
92
131
|
end
|
|
93
132
|
|
|
94
133
|
def sys_msg(message, params = {})
|
|
95
|
-
@sys_msg = message
|
|
96
|
-
add_message({ role: "system", content:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
134
|
+
@sys_msg = thinking_system_message(message)
|
|
135
|
+
add_message({ role: "system", content: @sys_msg }, params[:with_history])
|
|
136
|
+
self
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def multimodal_prompt(parts, with_history: false)
|
|
140
|
+
add_message({ role: "user", content: normalize_content_parts(parts) }, with_history)
|
|
141
|
+
self
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def image(source, token_budget: nil, **metadata)
|
|
145
|
+
@pending_content_parts << media_part("image", source, token_budget: token_budget, **metadata)
|
|
146
|
+
self
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def audio(source, **metadata)
|
|
150
|
+
@pending_content_parts << media_part("audio", source, **metadata)
|
|
151
|
+
self
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def video(source, fps: nil, max_seconds: nil, **metadata)
|
|
155
|
+
@pending_content_parts << media_part("video", source, fps: fps, max_seconds: max_seconds, **metadata)
|
|
100
156
|
self
|
|
101
157
|
end
|
|
102
158
|
|
|
103
159
|
def send_msg_once
|
|
104
160
|
raise "No LLM selected" if @current_llm.nil?
|
|
105
|
-
@last_response =
|
|
161
|
+
@last_response = send_llm_request(@messages, nil)
|
|
106
162
|
@messages = []
|
|
107
163
|
@messages << { role: "system", content: @sys_msg }
|
|
108
164
|
@last_response
|
|
@@ -120,24 +176,14 @@ module SmartPrompt
|
|
|
120
176
|
def send_msg(params = {})
|
|
121
177
|
Retriable.retriable(RETRY_OPTIONS) do
|
|
122
178
|
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
|
|
130
179
|
if params[:with_history]
|
|
131
|
-
@last_response =
|
|
180
|
+
@last_response = send_llm_request(history_messages, nil)
|
|
132
181
|
else
|
|
133
|
-
@last_response =
|
|
182
|
+
@last_response = send_llm_request(@messages, nil)
|
|
134
183
|
end
|
|
135
184
|
if @last_response == ""
|
|
136
185
|
@last_response = @current_llm.last_response
|
|
137
186
|
end
|
|
138
|
-
if @engine.config["better_prompt_db"]
|
|
139
|
-
BetterPrompt.add_response(@last_call_id, @last_response, false)
|
|
140
|
-
end
|
|
141
187
|
@messages = []
|
|
142
188
|
@messages << { role: "system", content: @sys_msg }
|
|
143
189
|
@last_response
|
|
@@ -149,20 +195,10 @@ module SmartPrompt
|
|
|
149
195
|
def send_msg_by_stream(params = {}, &proc)
|
|
150
196
|
Retriable.retriable(RETRY_OPTIONS) do
|
|
151
197
|
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
|
|
159
198
|
if params[:with_history]
|
|
160
|
-
|
|
199
|
+
send_llm_request(history_messages, proc)
|
|
161
200
|
else
|
|
162
|
-
|
|
163
|
-
end
|
|
164
|
-
if @engine.config["better_prompt_db"]
|
|
165
|
-
BetterPrompt.add_response(@last_call_id, @engine.stream_response, true)
|
|
201
|
+
send_llm_request(@messages, proc)
|
|
166
202
|
end
|
|
167
203
|
@messages = []
|
|
168
204
|
@messages << { role: "system", content: @sys_msg }
|
|
@@ -197,6 +233,123 @@ module SmartPrompt
|
|
|
197
233
|
end
|
|
198
234
|
end
|
|
199
235
|
|
|
236
|
+
private
|
|
237
|
+
|
|
238
|
+
def send_llm_request(messages, proc)
|
|
239
|
+
parameters = @current_llm.method(:send_request).parameters
|
|
240
|
+
if parameters.length >= 6
|
|
241
|
+
@current_llm.send_request(messages, @model_name, @temperature, @tools, proc, @request_options)
|
|
242
|
+
else
|
|
243
|
+
@current_llm.send_request(messages, @model_name, @temperature, @tools, proc)
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def merge_model_request_options(model_config)
|
|
248
|
+
explicit_options = model_config["request_options"] || model_config[:request_options] || {}
|
|
249
|
+
@request_options.merge!(explicit_options)
|
|
250
|
+
MODEL_REQUEST_OPTION_KEYS.each do |key|
|
|
251
|
+
value = model_config[key] || model_config[key.to_sym]
|
|
252
|
+
@request_options[key.to_sym] = value unless value.nil?
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def add_user_content(content, with_history)
|
|
257
|
+
if @pending_content_parts.empty?
|
|
258
|
+
add_message({ role: "user", content: content }, with_history)
|
|
259
|
+
else
|
|
260
|
+
add_message({ role: "user", content: multimodal_content(content) }, with_history)
|
|
261
|
+
@pending_content_parts = []
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def multimodal_content(text)
|
|
266
|
+
parts = @pending_content_parts
|
|
267
|
+
images_and_videos = parts.select { |part| ["image_url", "image", "video_url", "video"].include?(part[:type] || part["type"]) }
|
|
268
|
+
audio_parts = parts.select { |part| ["input_audio", "audio"].include?(part[:type] || part["type"]) }
|
|
269
|
+
other_parts = parts - images_and_videos - audio_parts
|
|
270
|
+
normalize_content_parts(images_and_videos + other_parts + [{ type: "text", text: text.to_s }] + audio_parts)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def normalize_content_parts(parts)
|
|
274
|
+
parts.map do |part|
|
|
275
|
+
normalized = part.transform_keys(&:to_s)
|
|
276
|
+
normalized["text"] = normalized.delete("content") if normalized["type"] == "text" && normalized.key?("content")
|
|
277
|
+
normalized
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def media_part(type, source, **metadata)
|
|
282
|
+
case type
|
|
283
|
+
when "image"
|
|
284
|
+
mime_type = detect_image_mime(source)
|
|
285
|
+
data = File.binread(source)
|
|
286
|
+
base64_data = Base64.strict_encode64(data)
|
|
287
|
+
url = "data:#{mime_type};base64,#{base64_data}"
|
|
288
|
+
part = { type: "image_url", image_url: { url: url } }
|
|
289
|
+
when "audio"
|
|
290
|
+
format = detect_audio_format(source)
|
|
291
|
+
data = File.binread(source)
|
|
292
|
+
base64_data = Base64.strict_encode64(data)
|
|
293
|
+
part = { type: "input_audio", input_audio: { data: base64_data, format: format } }
|
|
294
|
+
when "video"
|
|
295
|
+
mime_type = detect_video_mime(source)
|
|
296
|
+
data = File.binread(source)
|
|
297
|
+
base64_data = Base64.strict_encode64(data)
|
|
298
|
+
url = "data:#{mime_type};base64,#{base64_data}"
|
|
299
|
+
part = { type: "video_url", video_url: { url: url } }
|
|
300
|
+
else
|
|
301
|
+
part = { type: type }
|
|
302
|
+
end
|
|
303
|
+
metadata.each do |key, value|
|
|
304
|
+
part[key] = value unless value.nil?
|
|
305
|
+
end
|
|
306
|
+
part
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def detect_image_mime(path)
|
|
310
|
+
ext = File.extname(path).downcase
|
|
311
|
+
case ext
|
|
312
|
+
when ".png" then "image/png"
|
|
313
|
+
when ".jpg", ".jpeg" then "image/jpeg"
|
|
314
|
+
when ".gif" then "image/gif"
|
|
315
|
+
when ".webp" then "image/webp"
|
|
316
|
+
when ".bmp" then "image/bmp"
|
|
317
|
+
when ".svg" then "image/svg+xml"
|
|
318
|
+
else "application/octet-stream"
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def detect_audio_format(path)
|
|
323
|
+
ext = File.extname(path).downcase.delete_prefix(".")
|
|
324
|
+
%w[wav mp3 ogg flac aac m4a].include?(ext) ? ext : "wav"
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def detect_video_mime(path)
|
|
328
|
+
ext = File.extname(path).downcase
|
|
329
|
+
case ext
|
|
330
|
+
when ".mp4" then "video/mp4"
|
|
331
|
+
when ".webm" then "video/webm"
|
|
332
|
+
when ".mov" then "video/quicktime"
|
|
333
|
+
when ".avi" then "video/x-msvideo"
|
|
334
|
+
else "application/octet-stream"
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def thinking_system_message(message)
|
|
339
|
+
message = message.to_s.sub(/\A<\|think\|>\n?/, "")
|
|
340
|
+
return message if @thinking_enabled == false
|
|
341
|
+
return message unless @thinking_enabled == true
|
|
342
|
+
|
|
343
|
+
"<|think|>\n#{message}"
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def refresh_system_message(message)
|
|
347
|
+
system_message = @messages.find { |item| (item[:role] || item["role"]) == "system" }
|
|
348
|
+
system_message[:content] = message if system_message
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
public
|
|
352
|
+
|
|
200
353
|
def generate_image(prompt, params = {})
|
|
201
354
|
@current_llm.generate_image(prompt, params)
|
|
202
355
|
end
|