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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 396c6097973289a34143e86b65f428d55919d0e755916992a5c714e289ebf5a2
4
- data.tar.gz: 5d2c2d81b486e1fb05b53f116047ab1f90be77c9536fd6440a96b573bdad00c3
3
+ metadata.gz: 683f259828b34a687bb598abff6fe3ef547e918954b7422eecf2d357ba237495
4
+ data.tar.gz: 5410ee6c08d46643ac1c6c9ccbccc38f5729973a65063454c8b75c7787a587a6
5
5
  SHA512:
6
- metadata.gz: c6880395149678a195ea6efc46a81623d61c14c56534936041a625616a9e6b716597c078ccee0ac0d47c3b2a8e67d272742ee36940fa64321426799db0b26e4d
7
- data.tar.gz: 63f0b8ae0f6f62363443731cae6fed3eafe746c463d259c9b88eee8563d6ef0cdcd84e684d6daeedd7329c3dcab84748ab62358fa2935b9d0278ec347df45ffb
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
- add_message({ role: "user", content: content }, with_history)
104
+ add_user_content(content, with_history)
75
105
  self
76
106
  else
77
- add_message({ role: "user", content: template_name }, with_history)
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: message }, params[:with_history])
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 = @current_llm.send_request(@messages, @model_name, @temperature)
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 = @current_llm.send_request(history_messages, @model_name, @temperature, @tools, nil)
150
+ @last_response = send_llm_request(history_messages, nil)
101
151
  else
102
- @last_response = @current_llm.send_request(@messages, @model_name, @temperature, @tools, nil)
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
- @current_llm.send_request(history_messages, @model_name, @temperature, @tools, proc)
169
+ send_llm_request(history_messages, proc)
120
170
  else
121
- @current_llm.send_request(@messages, @model_name, @temperature, @tools, proc)
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
@@ -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
- def send_request(messages, model = nil, temperature = 0.7, tools = nil, proc = nil)
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
@@ -1,3 +1,3 @@
1
1
  module SmartPrompt
2
- VERSION = "0.4.2"
2
+ VERSION = "0.4.3"
3
3
  end
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.2
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.10
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: []