ruby_llm-red_candle 0.1.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.
@@ -0,0 +1,320 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Smoke test for RubyLLM::RedCandle
5
+ # Run with: bundle exec ruby examples/smoke_test.rb
6
+ #
7
+ # This script demonstrates all the features documented in the README.
8
+ # It uses TinyLlama by default as it's the fastest model to download and run.
9
+
10
+ require "bundler/setup"
11
+ require "ruby_llm"
12
+ require "ruby_llm-red_candle"
13
+
14
+ # Configuration
15
+ MODEL = ENV.fetch("SMOKE_TEST_MODEL", "TheBloke/TinyLlama-1.1B-Chat-v1.0-GGUF")
16
+ SKIP_SLOW = ENV.fetch("SKIP_SLOW", nil) # Set to skip multi-turn and streaming tests
17
+
18
+ # Helper for printing section headers
19
+ def section(title)
20
+ puts
21
+ puts "=" * 60
22
+ puts " #{title}"
23
+ puts "=" * 60
24
+ puts
25
+ end
26
+
27
+ # Helper for printing test results
28
+ def result(label, value)
29
+ puts " #{label}: #{value}"
30
+ end
31
+
32
+ def success(message)
33
+ puts " [OK] #{message}"
34
+ end
35
+
36
+ def info(message)
37
+ puts " [INFO] #{message}"
38
+ end
39
+
40
+ begin
41
+ section "RubyLLM::RedCandle Smoke Test"
42
+ info "Model: #{MODEL}"
43
+ info "Ruby: #{RUBY_VERSION}"
44
+ info "RubyLLM: #{RubyLLM::VERSION}"
45
+ info "RedCandle: #{RubyLLM::RedCandle::VERSION}"
46
+
47
+ # ---------------------------------------------------------------------------
48
+ section "1. Listing Available Models"
49
+ # ---------------------------------------------------------------------------
50
+
51
+ models = RubyLLM.models.all.select { |m| m.provider == "red_candle" }
52
+ puts " Available Red Candle models:"
53
+ models.each do |m|
54
+ puts " - #{m.id} (#{m.context_window} tokens)"
55
+ end
56
+ success "Found #{models.size} models"
57
+
58
+ # ---------------------------------------------------------------------------
59
+ section "2. Basic Chat (Non-Streaming)"
60
+ # ---------------------------------------------------------------------------
61
+
62
+ info "Creating chat instance..."
63
+ chat = RubyLLM.chat(provider: :red_candle, model: MODEL)
64
+ success "Chat created"
65
+
66
+ info "Asking: 'What is 2 + 2?'"
67
+ response = chat.ask("What is 2 + 2? Answer with just the number.")
68
+
69
+ result "Response", response.content.strip
70
+ result "Input tokens (estimated)", response.input_tokens
71
+ result "Output tokens (estimated)", response.output_tokens
72
+ success "Basic chat completed"
73
+
74
+ # ---------------------------------------------------------------------------
75
+ section "3. Multi-Turn Conversation"
76
+ # ---------------------------------------------------------------------------
77
+
78
+ unless SKIP_SLOW
79
+ info "Creating new chat for multi-turn..."
80
+ chat = RubyLLM.chat(provider: :red_candle, model: MODEL)
81
+
82
+ info "Turn 1: 'My name is Alice.'"
83
+ chat.ask("My name is Alice.")
84
+
85
+ info "Turn 2: 'I love programming in Ruby.'"
86
+ chat.ask("I love programming in Ruby.")
87
+
88
+ info "Turn 3: 'What is my name and what do I love?'"
89
+ response = chat.ask("What is my name and what do I love? Be brief.")
90
+
91
+ result "Response", response.content.strip[0..200]
92
+ success "Multi-turn conversation completed"
93
+ else
94
+ info "Skipped (SKIP_SLOW is set)"
95
+ end
96
+
97
+ # ---------------------------------------------------------------------------
98
+ section "4. Streaming Output"
99
+ # ---------------------------------------------------------------------------
100
+
101
+ unless SKIP_SLOW
102
+ info "Creating chat for streaming..."
103
+ chat = RubyLLM.chat(provider: :red_candle, model: MODEL)
104
+
105
+ info "Streaming response for: 'Count from 1 to 5'"
106
+ print " Response: "
107
+
108
+ chunks = []
109
+ chat.ask("Count from 1 to 5, just list the numbers.") do |chunk|
110
+ print chunk.content
111
+ $stdout.flush
112
+ chunks << chunk
113
+ end
114
+ puts
115
+
116
+ result "Chunks received", chunks.size
117
+ success "Streaming completed"
118
+ else
119
+ info "Skipped (SKIP_SLOW is set)"
120
+ end
121
+
122
+ # ---------------------------------------------------------------------------
123
+ section "5. Temperature Control"
124
+ # ---------------------------------------------------------------------------
125
+
126
+ info "Testing low temperature (0.1) - more deterministic..."
127
+ chat = RubyLLM.chat(provider: :red_candle, model: MODEL)
128
+ response = chat.with_temperature(0.1).ask("What is the capital of France? One word answer.")
129
+ result "Low temp response", response.content.strip
130
+
131
+ info "Testing high temperature (1.5) - more creative..."
132
+ response = chat.with_temperature(1.5).ask("Say something creative about the moon in 10 words or less.")
133
+ result "High temp response", response.content.strip
134
+
135
+ success "Temperature control completed"
136
+
137
+ # ---------------------------------------------------------------------------
138
+ section "6. System Prompts (Instructions)"
139
+ # ---------------------------------------------------------------------------
140
+
141
+ info "Setting system prompt for a pirate assistant..."
142
+ chat = RubyLLM.chat(provider: :red_candle, model: MODEL)
143
+ chat.with_instructions("You are a pirate. Always respond like a pirate would, using pirate slang.")
144
+
145
+ response = chat.ask("Hello, how are you today?")
146
+ result "Pirate response", response.content.strip[0..150]
147
+ success "System prompts completed"
148
+
149
+ # ---------------------------------------------------------------------------
150
+ section "7. Structured Output (JSON Schema)"
151
+ # ---------------------------------------------------------------------------
152
+
153
+ info "Defining schema for person profile..."
154
+ schema = {
155
+ type: "object",
156
+ properties: {
157
+ name: { type: "string" },
158
+ age: { type: "integer" },
159
+ occupation: { type: "string" }
160
+ },
161
+ required: %w[name age occupation]
162
+ }
163
+
164
+ info "Generating structured output..."
165
+ chat = RubyLLM.chat(provider: :red_candle, model: MODEL)
166
+ response = chat.with_schema(schema).ask("Generate a profile for a 28-year-old teacher named Bob")
167
+
168
+ result "Response type", response.content.class
169
+ result "Response", response.content.inspect
170
+
171
+ if response.content.is_a?(Hash)
172
+ result "Name", response.content["name"]
173
+ result "Age", response.content["age"]
174
+ result "Occupation", response.content["occupation"]
175
+ success "Structured output returned a Hash"
176
+ else
177
+ info "Response was a String (may indicate parse issue): #{response.content}"
178
+ end
179
+
180
+ # ---------------------------------------------------------------------------
181
+ section "8. Structured Output with Enums"
182
+ # ---------------------------------------------------------------------------
183
+
184
+ info "Defining schema with enum constraint..."
185
+ sentiment_schema = {
186
+ type: "object",
187
+ properties: {
188
+ sentiment: {
189
+ type: "string",
190
+ enum: %w[positive negative neutral]
191
+ },
192
+ confidence: { type: "number" }
193
+ },
194
+ required: %w[sentiment confidence]
195
+ }
196
+
197
+ info "Analyzing sentiment..."
198
+ chat = RubyLLM.chat(provider: :red_candle, model: MODEL)
199
+ response = chat.with_schema(sentiment_schema).ask("Analyze: 'I absolutely love this product!'")
200
+
201
+ result "Response", response.content.inspect
202
+
203
+ if response.content.is_a?(Hash)
204
+ result "Sentiment", response.content["sentiment"]
205
+ result "Confidence", response.content["confidence"]
206
+ success "Enum-constrained output completed"
207
+ end
208
+
209
+ # ---------------------------------------------------------------------------
210
+ section "9. Structured Output with String Keys (Normalization)"
211
+ # ---------------------------------------------------------------------------
212
+
213
+ info "Testing schema with string keys (should be normalized to symbols)..."
214
+ string_key_schema = {
215
+ "type" => "object",
216
+ "properties" => {
217
+ "answer" => { "type" => "string" }
218
+ },
219
+ "required" => ["answer"]
220
+ }
221
+
222
+ chat = RubyLLM.chat(provider: :red_candle, model: MODEL)
223
+ response = chat.with_schema(string_key_schema).ask("What is 3 + 3?")
224
+
225
+ result "Response", response.content.inspect
226
+ success "String key normalization completed"
227
+
228
+ # ---------------------------------------------------------------------------
229
+ section "10. Schema Validation"
230
+ # ---------------------------------------------------------------------------
231
+
232
+ info "Testing schema validation with invalid schema..."
233
+ begin
234
+ invalid_schema = { type: "object" } # Missing 'properties'
235
+ chat = RubyLLM.chat(provider: :red_candle, model: MODEL)
236
+ chat.with_schema(invalid_schema).ask("This should fail")
237
+ puts " [FAIL] Should have raised a validation error"
238
+ rescue RubyLLM::Error => e
239
+ result "Validation error caught", e.message.lines.first.strip
240
+ success "Schema validation works correctly"
241
+ end
242
+
243
+ info "Testing with non-object schema type..."
244
+ begin
245
+ array_schema = { type: "array", items: { type: "string" } }
246
+ chat = RubyLLM.chat(provider: :red_candle, model: MODEL)
247
+ chat.with_schema(array_schema).ask("This should fail")
248
+ puts " [FAIL] Should have raised a validation error"
249
+ rescue RubyLLM::Error => e
250
+ result "Type error caught", e.message.lines.first.strip
251
+ success "Non-object schema rejection works correctly"
252
+ end
253
+
254
+ # ---------------------------------------------------------------------------
255
+ section "11. Custom JSON Instruction Template"
256
+ # ---------------------------------------------------------------------------
257
+
258
+ info "Current default template:"
259
+ result "Default", RubyLLM::RedCandle::Configuration.json_instruction_template[0..60] + "..."
260
+
261
+ info "Setting custom template..."
262
+ RubyLLM::RedCandle::Configuration.json_instruction_template = <<~TEMPLATE
263
+
264
+ OUTPUT FORMAT: You must respond with valid JSON containing: {schema_description}
265
+ No other text allowed, only the JSON object.
266
+ TEMPLATE
267
+
268
+ result "Custom template set", RubyLLM::RedCandle::Configuration.json_instruction_template.lines.first.strip
269
+
270
+ info "Testing structured output with custom template..."
271
+ chat = RubyLLM.chat(provider: :red_candle, model: MODEL)
272
+ custom_schema = {
273
+ type: "object",
274
+ properties: {
275
+ greeting: { type: "string" }
276
+ },
277
+ required: ["greeting"]
278
+ }
279
+ response = chat.with_schema(custom_schema).ask("Say hello")
280
+
281
+ result "Response", response.content.inspect
282
+ success "Custom template structured output completed"
283
+
284
+ info "Resetting to default template..."
285
+ RubyLLM::RedCandle::Configuration.reset!
286
+ result "Template reset", RubyLLM::RedCandle::Configuration.json_instruction_template[0..60] + "..."
287
+ success "Configuration reset works correctly"
288
+
289
+ # ---------------------------------------------------------------------------
290
+ section "12. Error Handling"
291
+ # ---------------------------------------------------------------------------
292
+
293
+ info "Testing error handling with invalid model..."
294
+ begin
295
+ RubyLLM.chat(provider: :red_candle, model: "invalid/nonexistent-model")
296
+ puts " [FAIL] Should have raised an error"
297
+ rescue RubyLLM::Error => e
298
+ result "Error caught", e.message[0..60]
299
+ success "Error handling works correctly"
300
+ end
301
+
302
+ # ---------------------------------------------------------------------------
303
+ section "Summary"
304
+ # ---------------------------------------------------------------------------
305
+
306
+ puts " All smoke tests completed successfully!"
307
+ puts
308
+ puts " To run with a different model:"
309
+ puts " SMOKE_TEST_MODEL='Qwen/Qwen2.5-1.5B-Instruct-GGUF' bundle exec ruby examples/smoke_test.rb"
310
+ puts
311
+ puts " To skip slow tests:"
312
+ puts " SKIP_SLOW=1 bundle exec ruby examples/smoke_test.rb"
313
+ puts
314
+
315
+ rescue StandardError => e
316
+ puts
317
+ puts " [ERROR] #{e.class}: #{e.message}"
318
+ puts e.backtrace.first(5).map { |line| " #{line}" }.join("\n")
319
+ exit 1
320
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module RedCandle
5
+ # Determines capabilities for RedCandle models
6
+ module Capabilities
7
+ module_function
8
+
9
+ def supports_vision?
10
+ false
11
+ end
12
+
13
+ def supports_functions?(_model_id = nil)
14
+ false
15
+ end
16
+
17
+ def supports_streaming?
18
+ true
19
+ end
20
+
21
+ def supports_structured_output?
22
+ true
23
+ end
24
+
25
+ def supports_regex_constraints?
26
+ true
27
+ end
28
+
29
+ def supports_embeddings?
30
+ false # Future enhancement - Red Candle does support embedding models
31
+ end
32
+
33
+ def supports_audio?
34
+ false
35
+ end
36
+
37
+ def supports_pdf?
38
+ false
39
+ end
40
+
41
+ def normalize_temperature(temperature, _model_id)
42
+ # Red Candle uses standard 0-2 range
43
+ return 0.7 if temperature.nil?
44
+
45
+ temperature = temperature.to_f
46
+ temperature.clamp(0.0, 2.0)
47
+ end
48
+
49
+ def model_context_window(model_id)
50
+ case model_id
51
+ when /gemma-3-4b/i
52
+ 8192
53
+ when /qwen2\.5-1\.5b/i, /mistral-7b/i
54
+ 32_768
55
+ when /tinyllama/i
56
+ 2048
57
+ else
58
+ 4096 # Conservative default
59
+ end
60
+ end
61
+
62
+ def default_max_tokens
63
+ 512
64
+ end
65
+
66
+ def max_temperature
67
+ 2.0
68
+ end
69
+
70
+ def min_temperature
71
+ 0.0
72
+ end
73
+
74
+ def supports_temperature?
75
+ true
76
+ end
77
+
78
+ def supports_top_p?
79
+ true
80
+ end
81
+
82
+ def supports_top_k?
83
+ true
84
+ end
85
+
86
+ def supports_repetition_penalty?
87
+ true
88
+ end
89
+
90
+ def supports_seed?
91
+ true
92
+ end
93
+
94
+ def supports_stop_sequences?
95
+ true
96
+ end
97
+
98
+ def model_families
99
+ %w[gemma llama qwen2 mistral phi]
100
+ end
101
+
102
+ def available_on_platform?
103
+ # Check if Candle can be loaded
104
+
105
+ require "candle"
106
+ true
107
+ rescue LoadError
108
+ false
109
+ end
110
+ end
111
+ end
112
+ end