smart_prompt 0.4.2 → 0.4.3
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/README.cn.md +32 -0
- data/README.md +32 -0
- data/lib/smart_prompt/conversation.rb +133 -9
- data/lib/smart_prompt/engine.rb +19 -5
- data/lib/smart_prompt/openai_adapter.rb +25 -1
- data/lib/smart_prompt/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 683f259828b34a687bb598abff6fe3ef547e918954b7422eecf2d357ba237495
|
|
4
|
+
data.tar.gz: 5410ee6c08d46643ac1c6c9ccbccc38f5729973a65063454c8b75c7787a587a6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 25755db9db82c9af27d8007753da625767831f835bd5767726db738c2863dfea9a3429d4a600e7dbd5005729cfbd3b679b75e4657eab13dd7b6e446379984ea0
|
|
7
|
+
data.tar.gz: 4fb6b5f037c504b7a0ae440066aa4608fafa5a97c516ac220ffcd0817ffd03e880d0d7473e0e6537b50c6e771ca59f3b08aa9cbb3b24fabb41665a06ebe69ca2
|
data/README.cn.md
CHANGED
|
@@ -75,6 +75,14 @@ llms:
|
|
|
75
75
|
adapter: openai
|
|
76
76
|
url: http://localhost:11434/
|
|
77
77
|
default_model: deepseek-r1
|
|
78
|
+
gemma4_local:
|
|
79
|
+
adapter: openai
|
|
80
|
+
url: http://localhost:8000/v1
|
|
81
|
+
api_key: dummy
|
|
82
|
+
default_model: gemma-4-12B-it
|
|
83
|
+
temperature: 1.0
|
|
84
|
+
top_p: 0.95
|
|
85
|
+
top_k: 64
|
|
78
86
|
deepseek:
|
|
79
87
|
adapter: openai
|
|
80
88
|
url: https://api.deepseek.com
|
|
@@ -89,6 +97,10 @@ models:
|
|
|
89
97
|
deepseekv3.2:
|
|
90
98
|
use: SiliconFlow
|
|
91
99
|
model: Pro/deepseek-ai/DeepSeek-V3.2
|
|
100
|
+
gemma4/12b:
|
|
101
|
+
use: gemma4_local
|
|
102
|
+
model: gemma-4-12B-it
|
|
103
|
+
max_tokens: 1024
|
|
92
104
|
|
|
93
105
|
# 默认设置
|
|
94
106
|
default_llm: SiliconFlow
|
|
@@ -170,6 +182,26 @@ engine.call_worker_by_stream(:streaming_chat, {
|
|
|
170
182
|
end
|
|
171
183
|
```
|
|
172
184
|
|
|
185
|
+
### Gemma 4 12B 多模态
|
|
186
|
+
|
|
187
|
+
Gemma 4 12B 可以通过 LiteRT-LM、LM Studio、Ollama、llama.cpp 等 OpenAI 兼容本地服务接入。SmartPrompt 会把图片放在文本前、音频放在文本后,以匹配 Gemma 4 的多模态最佳实践。
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
SmartPrompt.define_worker :gemma_multimodal_assistant do
|
|
191
|
+
use_model "gemma4/12b"
|
|
192
|
+
thinking params.fetch(:thinking, true)
|
|
193
|
+
sys_msg("你是一个严谨的本地多模态助手。", params)
|
|
194
|
+
|
|
195
|
+
image(params[:image], token_budget: params[:token_budget] || 280) if params[:image]
|
|
196
|
+
video(params[:video], fps: 1, max_seconds: 60) if params[:video]
|
|
197
|
+
audio(params[:audio]) if params[:audio]
|
|
198
|
+
prompt(params[:message])
|
|
199
|
+
|
|
200
|
+
request_options(response_format: { type: "json_object" }) if params[:json]
|
|
201
|
+
send_msg
|
|
202
|
+
end
|
|
203
|
+
```
|
|
204
|
+
|
|
173
205
|
### 工具集成
|
|
174
206
|
|
|
175
207
|
```ruby
|
data/README.md
CHANGED
|
@@ -75,6 +75,14 @@ llms:
|
|
|
75
75
|
adapter: openai
|
|
76
76
|
url: http://localhost:11434/
|
|
77
77
|
default_model: deepseek-r1
|
|
78
|
+
gemma4_local:
|
|
79
|
+
adapter: openai
|
|
80
|
+
url: http://localhost:8000/v1
|
|
81
|
+
api_key: dummy
|
|
82
|
+
default_model: gemma-4-12B-it
|
|
83
|
+
temperature: 1.0
|
|
84
|
+
top_p: 0.95
|
|
85
|
+
top_k: 64
|
|
78
86
|
deepseek:
|
|
79
87
|
adapter: openai
|
|
80
88
|
url: https://api.deepseek.com
|
|
@@ -89,6 +97,10 @@ models:
|
|
|
89
97
|
deepseekv3.2:
|
|
90
98
|
use: SiliconFlow
|
|
91
99
|
model: Pro/deepseek-ai/DeepSeek-V3.2
|
|
100
|
+
gemma4/12b:
|
|
101
|
+
use: gemma4_local
|
|
102
|
+
model: gemma-4-12B-it
|
|
103
|
+
max_tokens: 1024
|
|
92
104
|
|
|
93
105
|
# Default settings
|
|
94
106
|
default_llm: SiliconFlow
|
|
@@ -170,6 +182,26 @@ engine.call_worker_by_stream(:streaming_chat, {
|
|
|
170
182
|
end
|
|
171
183
|
```
|
|
172
184
|
|
|
185
|
+
### Gemma 4 12B Multimodal
|
|
186
|
+
|
|
187
|
+
Gemma 4 12B can be connected through OpenAI-compatible local servers such as LiteRT-LM, LM Studio, Ollama, or llama.cpp. SmartPrompt places images before text and audio after text to match Gemma 4 multimodal best practices.
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
SmartPrompt.define_worker :gemma_multimodal_assistant do
|
|
191
|
+
use_model "gemma4/12b"
|
|
192
|
+
thinking params.fetch(:thinking, true)
|
|
193
|
+
sys_msg("You are a precise local multimodal assistant.", params)
|
|
194
|
+
|
|
195
|
+
image(params[:image], token_budget: params[:token_budget] || 280) if params[:image]
|
|
196
|
+
video(params[:video], fps: 1, max_seconds: 60) if params[:video]
|
|
197
|
+
audio(params[:audio]) if params[:audio]
|
|
198
|
+
prompt(params[:message])
|
|
199
|
+
|
|
200
|
+
request_options(response_format: { type: "json_object" }) if params[:json]
|
|
201
|
+
send_msg
|
|
202
|
+
end
|
|
203
|
+
```
|
|
204
|
+
|
|
173
205
|
### Tool Integration
|
|
174
206
|
|
|
175
207
|
```ruby
|
|
@@ -5,6 +5,18 @@ 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
|
+
|
|
8
20
|
attr_reader :messages, :last_response, :config_file
|
|
9
21
|
attr_reader :last_call_id
|
|
10
22
|
|
|
@@ -21,6 +33,9 @@ module SmartPrompt
|
|
|
21
33
|
@current_adapter = engine.current_adapter
|
|
22
34
|
@last_response = nil
|
|
23
35
|
@tools = tools
|
|
36
|
+
@request_options = {}
|
|
37
|
+
@pending_content_parts = []
|
|
38
|
+
@thinking_enabled = nil
|
|
24
39
|
end
|
|
25
40
|
|
|
26
41
|
def use(llm_name)
|
|
@@ -43,6 +58,7 @@ module SmartPrompt
|
|
|
43
58
|
|
|
44
59
|
use(llm_name)
|
|
45
60
|
model(configured_model_name)
|
|
61
|
+
merge_model_request_options(model_config)
|
|
46
62
|
self
|
|
47
63
|
end
|
|
48
64
|
|
|
@@ -54,6 +70,20 @@ module SmartPrompt
|
|
|
54
70
|
@temperature = temperature
|
|
55
71
|
end
|
|
56
72
|
|
|
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
|
+
|
|
57
87
|
def history_messages
|
|
58
88
|
@engine.history_messages
|
|
59
89
|
end
|
|
@@ -71,23 +101,43 @@ module SmartPrompt
|
|
|
71
101
|
SmartPrompt.logger.info "Use template #{template_name}"
|
|
72
102
|
raise "Template #{template_name} not found" unless @templates.key?(template_name)
|
|
73
103
|
content = @templates[template_name].render(params)
|
|
74
|
-
|
|
104
|
+
add_user_content(content, with_history)
|
|
75
105
|
self
|
|
76
106
|
else
|
|
77
|
-
|
|
107
|
+
add_user_content(template_name, with_history)
|
|
78
108
|
self
|
|
79
109
|
end
|
|
80
110
|
end
|
|
81
111
|
|
|
82
112
|
def sys_msg(message, params)
|
|
83
|
-
@sys_msg = message
|
|
84
|
-
add_message({ role: "system", content:
|
|
113
|
+
@sys_msg = thinking_system_message(message)
|
|
114
|
+
add_message({ role: "system", content: @sys_msg }, params[:with_history])
|
|
115
|
+
self
|
|
116
|
+
end
|
|
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)
|
|
85
135
|
self
|
|
86
136
|
end
|
|
87
137
|
|
|
88
138
|
def send_msg_once
|
|
89
139
|
raise "No LLM selected" if @current_llm.nil?
|
|
90
|
-
@last_response =
|
|
140
|
+
@last_response = send_llm_request(@messages, nil)
|
|
91
141
|
@messages = []
|
|
92
142
|
@messages << { role: "system", content: @sys_msg }
|
|
93
143
|
@last_response
|
|
@@ -97,9 +147,9 @@ module SmartPrompt
|
|
|
97
147
|
Retriable.retriable(RETRY_OPTIONS) do
|
|
98
148
|
raise ConfigurationError, "No LLM selected" if @current_llm.nil?
|
|
99
149
|
if params[:with_history]
|
|
100
|
-
@last_response =
|
|
150
|
+
@last_response = send_llm_request(history_messages, nil)
|
|
101
151
|
else
|
|
102
|
-
@last_response =
|
|
152
|
+
@last_response = send_llm_request(@messages, nil)
|
|
103
153
|
end
|
|
104
154
|
if @last_response == ""
|
|
105
155
|
@last_response = @current_llm.last_response
|
|
@@ -116,9 +166,9 @@ module SmartPrompt
|
|
|
116
166
|
Retriable.retriable(RETRY_OPTIONS) do
|
|
117
167
|
raise ConfigurationError, "No LLM selected" if @current_llm.nil?
|
|
118
168
|
if params[:with_history]
|
|
119
|
-
|
|
169
|
+
send_llm_request(history_messages, proc)
|
|
120
170
|
else
|
|
121
|
-
|
|
171
|
+
send_llm_request(@messages, proc)
|
|
122
172
|
end
|
|
123
173
|
@messages = []
|
|
124
174
|
@messages << { role: "system", content: @sys_msg }
|
|
@@ -152,5 +202,79 @@ module SmartPrompt
|
|
|
152
202
|
normalize(@last_response, length)
|
|
153
203
|
end
|
|
154
204
|
end
|
|
205
|
+
|
|
206
|
+
private
|
|
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
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def merge_model_request_options(model_config)
|
|
218
|
+
explicit_options = model_config["request_options"] || model_config[:request_options] || {}
|
|
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}"
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def refresh_system_message(message)
|
|
276
|
+
system_message = @messages.find { |item| (item[:role] || item["role"]) == "system" }
|
|
277
|
+
system_message[:content] = message if system_message
|
|
278
|
+
end
|
|
155
279
|
end
|
|
156
280
|
end
|
data/lib/smart_prompt/engine.rb
CHANGED
|
@@ -123,15 +123,12 @@ module SmartPrompt
|
|
|
123
123
|
if result.class == String
|
|
124
124
|
recive_message = {
|
|
125
125
|
"role": "assistant",
|
|
126
|
-
"content": result,
|
|
126
|
+
"content": sanitize_history_content(result),
|
|
127
127
|
}
|
|
128
128
|
elsif result.class == Array
|
|
129
129
|
recive_message = nil
|
|
130
130
|
else
|
|
131
|
-
recive_message =
|
|
132
|
-
"role": result.dig("choices", 0, "message", "role"),
|
|
133
|
-
"content": result.dig("choices", 0, "message", "content").to_s + result.dig("choices", 0, "message", "tool_calls").to_s,
|
|
134
|
-
}
|
|
131
|
+
recive_message = assistant_history_message(result)
|
|
135
132
|
end
|
|
136
133
|
worker.conversation.add_message(recive_message) if recive_message
|
|
137
134
|
SmartPrompt.logger.info "Worker result is: #{result}"
|
|
@@ -175,5 +172,22 @@ module SmartPrompt
|
|
|
175
172
|
def clear_history_messages
|
|
176
173
|
@history_messages = []
|
|
177
174
|
end
|
|
175
|
+
|
|
176
|
+
private
|
|
177
|
+
|
|
178
|
+
def assistant_history_message(result)
|
|
179
|
+
message = result.dig("choices", 0, "message") || {}
|
|
180
|
+
history_message = {
|
|
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
|
|
188
|
+
|
|
189
|
+
def sanitize_history_content(content)
|
|
190
|
+
content.to_s.gsub(/<\|channel\>thought\n.*?<channel\|>/m, "")
|
|
191
|
+
end
|
|
178
192
|
end
|
|
179
193
|
end
|
|
@@ -31,7 +31,19 @@ module SmartPrompt
|
|
|
31
31
|
end
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
REQUEST_PARAMETER_KEYS = %w[
|
|
35
|
+
max_tokens
|
|
36
|
+
max_completion_tokens
|
|
37
|
+
top_p
|
|
38
|
+
top_k
|
|
39
|
+
response_format
|
|
40
|
+
tool_choice
|
|
41
|
+
parallel_tool_calls
|
|
42
|
+
seed
|
|
43
|
+
stop
|
|
44
|
+
].freeze
|
|
45
|
+
|
|
46
|
+
def send_request(messages, model = nil, temperature = 0.7, tools = nil, proc = nil, request_options = {})
|
|
35
47
|
SmartPrompt.logger.info "OpenAIAdapter: Sending request to OpenAI"
|
|
36
48
|
temperature = 0.7 if temperature == nil
|
|
37
49
|
if model
|
|
@@ -46,6 +58,8 @@ module SmartPrompt
|
|
|
46
58
|
messages: messages,
|
|
47
59
|
temperature: @config["temperature"] || temperature,
|
|
48
60
|
}
|
|
61
|
+
parameters.merge!(configured_request_parameters)
|
|
62
|
+
parameters.merge!(request_options || {})
|
|
49
63
|
if proc
|
|
50
64
|
parameters[:stream] = proc
|
|
51
65
|
end
|
|
@@ -99,5 +113,15 @@ module SmartPrompt
|
|
|
99
113
|
end
|
|
100
114
|
return response.dig("data", 0, "embedding")
|
|
101
115
|
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
def configured_request_parameters
|
|
120
|
+
REQUEST_PARAMETER_KEYS.each_with_object({}) do |key, parameters|
|
|
121
|
+
next unless @config.key?(key)
|
|
122
|
+
|
|
123
|
+
parameters[key.to_sym] = @config[key]
|
|
124
|
+
end
|
|
125
|
+
end
|
|
102
126
|
end
|
|
103
127
|
end
|
data/lib/smart_prompt/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: smart_prompt
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- zhuang biaowei
|
|
@@ -138,7 +138,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
138
138
|
- !ruby/object:Gem::Version
|
|
139
139
|
version: '0'
|
|
140
140
|
requirements: []
|
|
141
|
-
rubygems_version: 4.0.
|
|
141
|
+
rubygems_version: 4.0.13
|
|
142
142
|
specification_version: 4
|
|
143
143
|
summary: A smart prompt management and LLM interaction gem
|
|
144
144
|
test_files: []
|