ollama-client 0.2.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 +7 -0
- data/CHANGELOG.md +12 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/CONTRIBUTING.md +36 -0
- data/LICENSE.txt +21 -0
- data/PRODUCTION_FIXES.md +172 -0
- data/README.md +690 -0
- data/Rakefile +12 -0
- data/TESTING.md +286 -0
- data/examples/advanced_complex_schemas.rb +363 -0
- data/examples/advanced_edge_cases.rb +241 -0
- data/examples/advanced_error_handling.rb +200 -0
- data/examples/advanced_multi_step_agent.rb +258 -0
- data/examples/advanced_performance_testing.rb +186 -0
- data/examples/complete_workflow.rb +235 -0
- data/examples/dhanhq_agent.rb +752 -0
- data/examples/dhanhq_tools.rb +563 -0
- data/examples/structured_outputs_chat.rb +72 -0
- data/examples/tool_calling_pattern.rb +266 -0
- data/exe/ollama-client +4 -0
- data/lib/ollama/agent/executor.rb +157 -0
- data/lib/ollama/agent/messages.rb +31 -0
- data/lib/ollama/agent/planner.rb +47 -0
- data/lib/ollama/client.rb +775 -0
- data/lib/ollama/config.rb +29 -0
- data/lib/ollama/errors.rb +54 -0
- data/lib/ollama/schema_validator.rb +79 -0
- data/lib/ollama/schemas/base.json +5 -0
- data/lib/ollama/streaming_observer.rb +22 -0
- data/lib/ollama/version.rb +5 -0
- data/lib/ollama_client.rb +46 -0
- data/sig/ollama/client.rbs +6 -0
- metadata +108 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Advanced Example: Performance Testing and Observability
|
|
5
|
+
# Demonstrates: Latency measurement, throughput testing, error rate tracking, concurrent requests
|
|
6
|
+
|
|
7
|
+
require "json"
|
|
8
|
+
require "benchmark"
|
|
9
|
+
require "time"
|
|
10
|
+
require_relative "../lib/ollama_client"
|
|
11
|
+
|
|
12
|
+
class PerformanceMonitor
|
|
13
|
+
def initialize(client:)
|
|
14
|
+
@client = client
|
|
15
|
+
@metrics = {
|
|
16
|
+
calls: [],
|
|
17
|
+
errors: [],
|
|
18
|
+
latencies: []
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def measure_call(prompt:, schema:)
|
|
23
|
+
start_time = Time.now
|
|
24
|
+
|
|
25
|
+
begin
|
|
26
|
+
result = @client.generate(prompt: prompt, schema: schema)
|
|
27
|
+
latency = (Time.now - start_time) * 1000 # Convert to milliseconds
|
|
28
|
+
|
|
29
|
+
@metrics[:calls] << {
|
|
30
|
+
success: true,
|
|
31
|
+
latency_ms: latency,
|
|
32
|
+
timestamp: Time.now.iso8601
|
|
33
|
+
}
|
|
34
|
+
@metrics[:latencies] << latency
|
|
35
|
+
|
|
36
|
+
{ success: true, result: result, latency_ms: latency }
|
|
37
|
+
rescue Ollama::Error => e
|
|
38
|
+
latency = (Time.now - start_time) * 1000
|
|
39
|
+
@metrics[:calls] << {
|
|
40
|
+
success: false,
|
|
41
|
+
latency_ms: latency,
|
|
42
|
+
error: e.class.name,
|
|
43
|
+
timestamp: Time.now.iso8601
|
|
44
|
+
}
|
|
45
|
+
@metrics[:errors] << { error: e.class.name, message: e.message, latency_ms: latency }
|
|
46
|
+
|
|
47
|
+
{ success: false, error: e, latency_ms: latency }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def run_throughput_test(prompt:, schema:, iterations: 10)
|
|
52
|
+
puts "🚀 Running throughput test (#{iterations} iterations)..."
|
|
53
|
+
results = []
|
|
54
|
+
|
|
55
|
+
total_time = Benchmark.realtime do
|
|
56
|
+
iterations.times do |i|
|
|
57
|
+
print " #{i + 1}/#{iterations}... "
|
|
58
|
+
result = measure_call(prompt: prompt, schema: schema)
|
|
59
|
+
results << result
|
|
60
|
+
puts result[:success] ? "✓" : "✗"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
{
|
|
65
|
+
total_time: total_time,
|
|
66
|
+
iterations: iterations,
|
|
67
|
+
throughput: iterations / total_time,
|
|
68
|
+
results: results
|
|
69
|
+
}
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def run_latency_test(prompt:, schema:, iterations: 10)
|
|
73
|
+
puts "⏱️ Running latency test (#{iterations} iterations)..."
|
|
74
|
+
latencies = []
|
|
75
|
+
|
|
76
|
+
iterations.times do |i|
|
|
77
|
+
print " #{i + 1}/#{iterations}... "
|
|
78
|
+
result = measure_call(prompt: prompt, schema: schema)
|
|
79
|
+
if result[:success]
|
|
80
|
+
latencies << result[:latency_ms]
|
|
81
|
+
puts "#{result[:latency_ms].round(2)}ms"
|
|
82
|
+
else
|
|
83
|
+
puts "ERROR"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
{
|
|
88
|
+
latencies: latencies,
|
|
89
|
+
min: latencies.min,
|
|
90
|
+
max: latencies.max,
|
|
91
|
+
avg: latencies.sum / latencies.length,
|
|
92
|
+
median: latencies.sort[latencies.length / 2],
|
|
93
|
+
p95: latencies.sort[(latencies.length * 0.95).to_i],
|
|
94
|
+
p99: latencies.sort[(latencies.length * 0.99).to_i]
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def display_metrics
|
|
99
|
+
puts "\n" + "=" * 60
|
|
100
|
+
puts "Performance Metrics"
|
|
101
|
+
puts "=" * 60
|
|
102
|
+
|
|
103
|
+
total_calls = @metrics[:calls].length
|
|
104
|
+
successful = @metrics[:calls].count { |c| c[:success] }
|
|
105
|
+
failed = total_calls - successful
|
|
106
|
+
|
|
107
|
+
puts "Total calls: #{total_calls}"
|
|
108
|
+
puts "Successful: #{successful} (#{(successful.to_f / total_calls * 100).round(2)}%)"
|
|
109
|
+
puts "Failed: #{failed} (#{(failed.to_f / total_calls * 100).round(2)}%)"
|
|
110
|
+
|
|
111
|
+
if @metrics[:latencies].any?
|
|
112
|
+
latencies = @metrics[:latencies]
|
|
113
|
+
puts "\nLatency Statistics (ms):"
|
|
114
|
+
puts " Min: #{latencies.min.round(2)}"
|
|
115
|
+
puts " Max: #{latencies.max.round(2)}"
|
|
116
|
+
puts " Avg: #{(latencies.sum / latencies.length).round(2)}"
|
|
117
|
+
puts " Median: #{latencies.sort[latencies.length / 2].round(2)}"
|
|
118
|
+
puts " P95: #{latencies.sort[(latencies.length * 0.95).to_i].round(2)}"
|
|
119
|
+
puts " P99: #{latencies.sort[(latencies.length * 0.99).to_i].round(2)}"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
return unless @metrics[:errors].any?
|
|
123
|
+
|
|
124
|
+
puts "\nErrors by type:"
|
|
125
|
+
error_counts = @metrics[:errors].group_by { |e| e[:error] }
|
|
126
|
+
error_counts.each do |error_type, errors|
|
|
127
|
+
puts " #{error_type}: #{errors.length}"
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def export_metrics(filename: "metrics.json")
|
|
132
|
+
File.write(filename, JSON.pretty_generate(@metrics))
|
|
133
|
+
puts "\n📊 Metrics exported to #{filename}"
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Run performance tests
|
|
138
|
+
if __FILE__ == $PROGRAM_NAME
|
|
139
|
+
# Use longer timeout for performance testing
|
|
140
|
+
config = Ollama::Config.new
|
|
141
|
+
config.timeout = 60 # 60 seconds for complex operations
|
|
142
|
+
client = Ollama::Client.new(config: config)
|
|
143
|
+
monitor = PerformanceMonitor.new(client: client)
|
|
144
|
+
|
|
145
|
+
schema = {
|
|
146
|
+
"type" => "object",
|
|
147
|
+
"required" => ["response"],
|
|
148
|
+
"properties" => {
|
|
149
|
+
"response" => { "type" => "string" }
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
puts "=" * 60
|
|
154
|
+
puts "Performance Testing Suite"
|
|
155
|
+
puts "=" * 60
|
|
156
|
+
|
|
157
|
+
# Test 1: Latency
|
|
158
|
+
latency_results = monitor.run_latency_test(
|
|
159
|
+
prompt: "Respond with a simple acknowledgment",
|
|
160
|
+
schema: schema,
|
|
161
|
+
iterations: 5
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
puts "\nLatency Results:"
|
|
165
|
+
puts " Average: #{latency_results[:avg].round(2)}ms"
|
|
166
|
+
puts " P95: #{latency_results[:p95].round(2)}ms"
|
|
167
|
+
puts " P99: #{latency_results[:p99].round(2)}ms"
|
|
168
|
+
|
|
169
|
+
# Test 2: Throughput
|
|
170
|
+
throughput_results = monitor.run_throughput_test(
|
|
171
|
+
prompt: "Count to 5",
|
|
172
|
+
schema: schema,
|
|
173
|
+
iterations: 5
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
puts "\nThroughput Results:"
|
|
177
|
+
puts " Total time: #{throughput_results[:total_time].round(2)}s"
|
|
178
|
+
puts " Throughput: #{throughput_results[:throughput].round(2)} calls/sec"
|
|
179
|
+
|
|
180
|
+
# Display all metrics
|
|
181
|
+
monitor.display_metrics
|
|
182
|
+
|
|
183
|
+
# Export metrics
|
|
184
|
+
monitor.export_metrics
|
|
185
|
+
end
|
|
186
|
+
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Complete example showing how to use structured outputs in a real workflow
|
|
5
|
+
# This demonstrates the full cycle: schema definition -> LLM call -> using the result
|
|
6
|
+
|
|
7
|
+
require "json"
|
|
8
|
+
require_relative "../lib/ollama_client"
|
|
9
|
+
|
|
10
|
+
# Example: Task Planning Agent
|
|
11
|
+
# The LLM decides what action to take, and we execute it
|
|
12
|
+
|
|
13
|
+
class TaskPlanner
|
|
14
|
+
def initialize(client:)
|
|
15
|
+
@client = client
|
|
16
|
+
@task_schema = {
|
|
17
|
+
"type" => "object",
|
|
18
|
+
"required" => ["action", "reasoning", "confidence", "next_step"],
|
|
19
|
+
"properties" => {
|
|
20
|
+
"action" => {
|
|
21
|
+
"type" => "string",
|
|
22
|
+
"description" => "The action to take",
|
|
23
|
+
"enum" => ["search", "calculate", "store", "retrieve", "finish"]
|
|
24
|
+
},
|
|
25
|
+
"reasoning" => {
|
|
26
|
+
"type" => "string",
|
|
27
|
+
"description" => "Why this action was chosen"
|
|
28
|
+
},
|
|
29
|
+
"confidence" => {
|
|
30
|
+
"type" => "number",
|
|
31
|
+
"minimum" => 0,
|
|
32
|
+
"maximum" => 1,
|
|
33
|
+
"description" => "Confidence in this decision"
|
|
34
|
+
},
|
|
35
|
+
"next_step" => {
|
|
36
|
+
"type" => "string",
|
|
37
|
+
"description" => "What to do next"
|
|
38
|
+
},
|
|
39
|
+
"parameters" => {
|
|
40
|
+
"type" => "object",
|
|
41
|
+
"description" => "Parameters needed for the action"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def plan(context:)
|
|
48
|
+
puts "🤔 Planning next action..."
|
|
49
|
+
puts "Context: #{context}\n\n"
|
|
50
|
+
|
|
51
|
+
begin
|
|
52
|
+
result = @client.generate(
|
|
53
|
+
prompt: "Given this context: #{context}\n\nDecide the next action to take.",
|
|
54
|
+
schema: @task_schema
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# The result is guaranteed to match our schema
|
|
58
|
+
display_decision(result)
|
|
59
|
+
execute_action(result)
|
|
60
|
+
|
|
61
|
+
result
|
|
62
|
+
rescue Ollama::SchemaViolationError => e
|
|
63
|
+
puts "❌ Invalid response structure: #{e.message}"
|
|
64
|
+
puts " This shouldn't happen with format parameter, but we handle it gracefully"
|
|
65
|
+
nil
|
|
66
|
+
rescue Ollama::Error => e
|
|
67
|
+
puts "❌ Error: #{e.message}"
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def display_decision(result)
|
|
75
|
+
puts "📋 Decision:"
|
|
76
|
+
puts " Action: #{result['action']}"
|
|
77
|
+
puts " Reasoning: #{result['reasoning']}"
|
|
78
|
+
puts " Confidence: #{(result['confidence'] * 100).round}%"
|
|
79
|
+
puts " Next Step: #{result['next_step']}"
|
|
80
|
+
puts " Parameters: #{JSON.pretty_generate(result['parameters'] || {})}\n"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def execute_action(result)
|
|
84
|
+
case result["action"]
|
|
85
|
+
when "search"
|
|
86
|
+
query = result.dig("parameters", "query") || "default"
|
|
87
|
+
puts "🔍 Executing search: #{query}"
|
|
88
|
+
# In real code, you'd call your search function here
|
|
89
|
+
puts " → Search results would appear here\n"
|
|
90
|
+
|
|
91
|
+
when "calculate"
|
|
92
|
+
operation = result.dig("parameters", "operation") || "unknown"
|
|
93
|
+
puts "🧮 Executing calculation: #{operation}"
|
|
94
|
+
# In real code, you'd call your calculator here
|
|
95
|
+
puts " → Calculation result would appear here\n"
|
|
96
|
+
|
|
97
|
+
when "store"
|
|
98
|
+
key = result.dig("parameters", "key") || "unknown"
|
|
99
|
+
puts "💾 Storing data with key: #{key}"
|
|
100
|
+
# In real code, you'd save to your storage
|
|
101
|
+
puts " → Data stored successfully\n"
|
|
102
|
+
|
|
103
|
+
when "retrieve"
|
|
104
|
+
key = result.dig("parameters", "key") || "unknown"
|
|
105
|
+
puts "📂 Retrieving data with key: #{key}"
|
|
106
|
+
# In real code, you'd fetch from your storage
|
|
107
|
+
puts " → Data retrieved successfully\n"
|
|
108
|
+
|
|
109
|
+
when "finish"
|
|
110
|
+
puts "✅ Task complete!\n"
|
|
111
|
+
|
|
112
|
+
else
|
|
113
|
+
puts "⚠️ Unknown action: #{result['action']}\n"
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Example: Data Analyzer
|
|
119
|
+
# The LLM analyzes data and returns structured insights
|
|
120
|
+
|
|
121
|
+
class DataAnalyzer
|
|
122
|
+
def initialize(client:)
|
|
123
|
+
@client = client
|
|
124
|
+
@analysis_schema = {
|
|
125
|
+
"type" => "object",
|
|
126
|
+
"required" => ["summary", "confidence", "key_points"],
|
|
127
|
+
"properties" => {
|
|
128
|
+
"summary" => {
|
|
129
|
+
"type" => "string",
|
|
130
|
+
"description" => "Brief summary of the analysis"
|
|
131
|
+
},
|
|
132
|
+
"confidence" => {
|
|
133
|
+
"type" => "number",
|
|
134
|
+
"minimum" => 0,
|
|
135
|
+
"maximum" => 1
|
|
136
|
+
},
|
|
137
|
+
"key_points" => {
|
|
138
|
+
"type" => "array",
|
|
139
|
+
"items" => { "type" => "string" },
|
|
140
|
+
"minItems" => 1,
|
|
141
|
+
"maxItems" => 5
|
|
142
|
+
},
|
|
143
|
+
"sentiment" => {
|
|
144
|
+
"type" => "string",
|
|
145
|
+
"enum" => ["positive", "neutral", "negative"]
|
|
146
|
+
},
|
|
147
|
+
"recommendations" => {
|
|
148
|
+
"type" => "array",
|
|
149
|
+
"items" => { "type" => "string" }
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def analyze(data:)
|
|
156
|
+
puts "📊 Analyzing data..."
|
|
157
|
+
puts "Data: #{data}\n\n"
|
|
158
|
+
|
|
159
|
+
begin
|
|
160
|
+
result = @client.generate(
|
|
161
|
+
prompt: "Analyze this data and provide insights: #{data}",
|
|
162
|
+
schema: @analysis_schema
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
display_analysis(result)
|
|
166
|
+
make_recommendations(result)
|
|
167
|
+
|
|
168
|
+
result
|
|
169
|
+
rescue Ollama::Error => e
|
|
170
|
+
puts "❌ Error: #{e.message}"
|
|
171
|
+
nil
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
private
|
|
176
|
+
|
|
177
|
+
def display_analysis(result)
|
|
178
|
+
puts "📈 Analysis Results:"
|
|
179
|
+
puts " Summary: #{result['summary']}"
|
|
180
|
+
puts " Confidence: #{(result['confidence'] * 100).round}%"
|
|
181
|
+
puts " Sentiment: #{result['sentiment']}"
|
|
182
|
+
puts "\n Key Points:"
|
|
183
|
+
result["key_points"].each_with_index do |point, i|
|
|
184
|
+
puts " #{i + 1}. #{point}"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
if result["recommendations"] && !result["recommendations"].empty?
|
|
188
|
+
puts "\n Recommendations:"
|
|
189
|
+
result["recommendations"].each_with_index do |rec, i|
|
|
190
|
+
puts " #{i + 1}. #{rec}"
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
puts
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def make_recommendations(result)
|
|
197
|
+
if result["confidence"] > 0.8 && result["sentiment"] == "positive"
|
|
198
|
+
puts "✅ High confidence positive analysis - safe to proceed"
|
|
199
|
+
elsif result["confidence"] < 0.5
|
|
200
|
+
puts "⚠️ Low confidence - manual review recommended"
|
|
201
|
+
elsif result["sentiment"] == "negative"
|
|
202
|
+
puts "⚠️ Negative sentiment detected - investigate further"
|
|
203
|
+
end
|
|
204
|
+
puts
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Main execution
|
|
209
|
+
if __FILE__ == $PROGRAM_NAME
|
|
210
|
+
client = Ollama::Client.new
|
|
211
|
+
|
|
212
|
+
puts "=" * 60
|
|
213
|
+
puts "Example 1: Task Planning Agent"
|
|
214
|
+
puts "=" * 60
|
|
215
|
+
puts
|
|
216
|
+
|
|
217
|
+
planner = TaskPlanner.new(client: client)
|
|
218
|
+
planner.plan(context: "User wants to know the weather in Paris")
|
|
219
|
+
|
|
220
|
+
puts "\n" + "=" * 60
|
|
221
|
+
puts "Example 2: Data Analysis"
|
|
222
|
+
puts "=" * 60
|
|
223
|
+
puts
|
|
224
|
+
|
|
225
|
+
analyzer = DataAnalyzer.new(client: client)
|
|
226
|
+
analyzer.analyze(
|
|
227
|
+
data: "Sales increased 25% this quarter. Customer satisfaction is at 4.8/5. " \
|
|
228
|
+
"Revenue: $1.2M. New customers: 150."
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
puts "=" * 60
|
|
232
|
+
puts "Examples complete!"
|
|
233
|
+
puts "=" * 60
|
|
234
|
+
end
|
|
235
|
+
|