ollama-client 0.2.5 → 0.2.7
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 +22 -0
- data/README.md +336 -91
- data/RELEASE_NOTES_v0.2.6.md +41 -0
- data/docs/AREAS_FOR_CONSIDERATION.md +325 -0
- data/docs/EXAMPLE_REORGANIZATION.md +412 -0
- data/docs/FEATURES_ADDED.md +12 -1
- data/docs/GETTING_STARTED.md +361 -0
- data/docs/INTEGRATION_TESTING.md +170 -0
- data/docs/NEXT_STEPS_SUMMARY.md +114 -0
- data/docs/PERSONAS.md +383 -0
- data/docs/QUICK_START.md +195 -0
- data/docs/TESTING.md +392 -170
- data/docs/TEST_CHECKLIST.md +450 -0
- data/examples/README.md +62 -63
- data/examples/basic_chat.rb +33 -0
- data/examples/basic_generate.rb +29 -0
- data/examples/mcp_executor.rb +39 -0
- data/examples/mcp_http_executor.rb +45 -0
- data/examples/tool_calling_parsing.rb +59 -0
- data/examples/tool_dto_example.rb +0 -0
- data/exe/ollama-client +128 -1
- data/lib/ollama/agent/planner.rb +7 -2
- data/lib/ollama/chat_session.rb +101 -0
- data/lib/ollama/client.rb +41 -35
- data/lib/ollama/config.rb +9 -4
- data/lib/ollama/document_loader.rb +1 -1
- data/lib/ollama/embeddings.rb +61 -28
- data/lib/ollama/errors.rb +1 -0
- data/lib/ollama/mcp/http_client.rb +149 -0
- data/lib/ollama/mcp/stdio_client.rb +146 -0
- data/lib/ollama/mcp/tools_bridge.rb +72 -0
- data/lib/ollama/mcp.rb +31 -0
- data/lib/ollama/options.rb +3 -1
- data/lib/ollama/personas.rb +287 -0
- data/lib/ollama/version.rb +1 -1
- data/lib/ollama_client.rb +17 -5
- metadata +22 -48
- data/examples/advanced_complex_schemas.rb +0 -366
- data/examples/advanced_edge_cases.rb +0 -241
- data/examples/advanced_error_handling.rb +0 -200
- data/examples/advanced_multi_step_agent.rb +0 -341
- data/examples/advanced_performance_testing.rb +0 -186
- data/examples/chat_console.rb +0 -143
- data/examples/complete_workflow.rb +0 -245
- data/examples/dhan_console.rb +0 -843
- data/examples/dhanhq/README.md +0 -236
- data/examples/dhanhq/agents/base_agent.rb +0 -74
- data/examples/dhanhq/agents/data_agent.rb +0 -66
- data/examples/dhanhq/agents/orchestrator_agent.rb +0 -120
- data/examples/dhanhq/agents/technical_analysis_agent.rb +0 -252
- data/examples/dhanhq/agents/trading_agent.rb +0 -81
- data/examples/dhanhq/analysis/market_structure.rb +0 -138
- data/examples/dhanhq/analysis/pattern_recognizer.rb +0 -192
- data/examples/dhanhq/analysis/trend_analyzer.rb +0 -88
- data/examples/dhanhq/builders/market_context_builder.rb +0 -67
- data/examples/dhanhq/dhanhq_agent.rb +0 -829
- data/examples/dhanhq/indicators/technical_indicators.rb +0 -158
- data/examples/dhanhq/scanners/intraday_options_scanner.rb +0 -492
- data/examples/dhanhq/scanners/swing_scanner.rb +0 -247
- data/examples/dhanhq/schemas/agent_schemas.rb +0 -61
- data/examples/dhanhq/services/base_service.rb +0 -46
- data/examples/dhanhq/services/data_service.rb +0 -118
- data/examples/dhanhq/services/trading_service.rb +0 -59
- data/examples/dhanhq/technical_analysis_agentic_runner.rb +0 -411
- data/examples/dhanhq/technical_analysis_runner.rb +0 -420
- data/examples/dhanhq/test_tool_calling.rb +0 -538
- data/examples/dhanhq/test_tool_calling_verbose.rb +0 -251
- data/examples/dhanhq/utils/instrument_helper.rb +0 -32
- data/examples/dhanhq/utils/parameter_cleaner.rb +0 -28
- data/examples/dhanhq/utils/parameter_normalizer.rb +0 -45
- data/examples/dhanhq/utils/rate_limiter.rb +0 -23
- data/examples/dhanhq/utils/trading_parameter_normalizer.rb +0 -72
- data/examples/dhanhq_agent.rb +0 -964
- data/examples/dhanhq_tools.rb +0 -1663
- data/examples/multi_step_agent_with_external_data.rb +0 -368
- data/examples/structured_outputs_chat.rb +0 -72
- data/examples/structured_tools.rb +0 -89
- data/examples/test_dhanhq_tool_calling.rb +0 -375
- data/examples/test_tool_calling.rb +0 -160
- data/examples/tool_calling_direct.rb +0 -124
- data/examples/tool_calling_pattern.rb +0 -269
- data/exe/dhan_console +0 -4
|
@@ -1,341 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
# Advanced Example: Multi-Step Agent with Complex Decision Making
|
|
5
|
-
# Demonstrates: Nested schemas, state management, error recovery, confidence thresholds
|
|
6
|
-
|
|
7
|
-
require "json"
|
|
8
|
-
require_relative "../lib/ollama_client"
|
|
9
|
-
|
|
10
|
-
class MultiStepAgent
|
|
11
|
-
def initialize(client:)
|
|
12
|
-
@client = client
|
|
13
|
-
@state = {
|
|
14
|
-
steps_completed: [],
|
|
15
|
-
data_collected: {},
|
|
16
|
-
errors: []
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
# Complex nested schema for decision making
|
|
20
|
-
@decision_schema = {
|
|
21
|
-
"type" => "object",
|
|
22
|
-
"required" => ["step", "action", "reasoning", "confidence", "next_steps"],
|
|
23
|
-
"properties" => {
|
|
24
|
-
"step" => {
|
|
25
|
-
"type" => "integer",
|
|
26
|
-
"description" => "Current step number in the workflow"
|
|
27
|
-
},
|
|
28
|
-
"action" => {
|
|
29
|
-
"type" => "object",
|
|
30
|
-
"required" => ["type", "parameters"],
|
|
31
|
-
"properties" => {
|
|
32
|
-
"type" => {
|
|
33
|
-
"type" => "string",
|
|
34
|
-
"enum" => ["collect", "analyze", "transform", "validate", "complete"]
|
|
35
|
-
},
|
|
36
|
-
"parameters" => {
|
|
37
|
-
"type" => "object",
|
|
38
|
-
"additionalProperties" => true
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
},
|
|
42
|
-
"reasoning" => {
|
|
43
|
-
"type" => "string",
|
|
44
|
-
"description" => "Why this action was chosen"
|
|
45
|
-
},
|
|
46
|
-
"confidence" => {
|
|
47
|
-
"type" => "number",
|
|
48
|
-
"minimum" => 0,
|
|
49
|
-
"maximum" => 1,
|
|
50
|
-
"description" => "Confidence level (0.0 to 1.0, where 1.0 is 100% confident)"
|
|
51
|
-
},
|
|
52
|
-
"next_steps" => {
|
|
53
|
-
"type" => "array",
|
|
54
|
-
"items" => {
|
|
55
|
-
"type" => "string"
|
|
56
|
-
},
|
|
57
|
-
"minItems" => 0,
|
|
58
|
-
"maxItems" => 5
|
|
59
|
-
},
|
|
60
|
-
"risk_assessment" => {
|
|
61
|
-
"type" => "object",
|
|
62
|
-
"properties" => {
|
|
63
|
-
"level" => {
|
|
64
|
-
"type" => "string",
|
|
65
|
-
"enum" => ["low", "medium", "high"]
|
|
66
|
-
},
|
|
67
|
-
"factors" => {
|
|
68
|
-
"type" => "array",
|
|
69
|
-
"items" => { "type" => "string" }
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def execute_workflow(goal:)
|
|
78
|
-
puts "🚀 Starting multi-step workflow"
|
|
79
|
-
puts "Goal: #{goal}\n\n"
|
|
80
|
-
|
|
81
|
-
max_steps = 10
|
|
82
|
-
step_count = 0
|
|
83
|
-
|
|
84
|
-
loop do
|
|
85
|
-
step_count += 1
|
|
86
|
-
break if step_count > max_steps
|
|
87
|
-
|
|
88
|
-
puts "─" * 60
|
|
89
|
-
puts "Step #{step_count}/#{max_steps}"
|
|
90
|
-
puts "─" * 60
|
|
91
|
-
|
|
92
|
-
context = build_context(goal: goal)
|
|
93
|
-
|
|
94
|
-
begin
|
|
95
|
-
decision = @client.generate(
|
|
96
|
-
prompt: build_prompt(goal: goal, context: context),
|
|
97
|
-
schema: @decision_schema
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
# Validate confidence threshold
|
|
101
|
-
if decision["confidence"] < 0.5
|
|
102
|
-
puts "⚠️ Low confidence (#{(decision["confidence"] * 100).round}%) - requesting manual review"
|
|
103
|
-
break
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
display_decision(decision)
|
|
107
|
-
result = execute_action(decision)
|
|
108
|
-
|
|
109
|
-
# Update state
|
|
110
|
-
@state[:steps_completed] << {
|
|
111
|
-
step: decision["step"],
|
|
112
|
-
action: decision["action"]["type"],
|
|
113
|
-
result: result
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
# Check if workflow is complete
|
|
117
|
-
if decision["action"]["type"] == "complete"
|
|
118
|
-
puts "\n✅ Workflow completed successfully!"
|
|
119
|
-
break
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
# Prevent infinite loops - if we've done the same action 3+ times, force progression
|
|
123
|
-
recent_actions = @state[:steps_completed].last(3).map { |s| s[:action] }
|
|
124
|
-
if recent_actions.length == 3 && recent_actions.uniq.length == 1
|
|
125
|
-
puts "⚠️ Detected repetitive actions - forcing workflow progression"
|
|
126
|
-
# Force next phase
|
|
127
|
-
case recent_actions.first
|
|
128
|
-
when "collect"
|
|
129
|
-
puts " → Moving to analysis phase"
|
|
130
|
-
decision["action"]["type"] = "analyze"
|
|
131
|
-
decision["action"]["parameters"] = { "target" => "collected_data" }
|
|
132
|
-
result = execute_action(decision)
|
|
133
|
-
@state[:steps_completed] << {
|
|
134
|
-
step: decision["step"],
|
|
135
|
-
action: "analyze",
|
|
136
|
-
result: result
|
|
137
|
-
}
|
|
138
|
-
when "analyze"
|
|
139
|
-
puts " → Moving to validation phase"
|
|
140
|
-
decision["action"]["type"] = "validate"
|
|
141
|
-
decision["action"]["parameters"] = { "type" => "results" }
|
|
142
|
-
result = execute_action(decision)
|
|
143
|
-
@state[:steps_completed] << {
|
|
144
|
-
step: decision["step"],
|
|
145
|
-
action: "validate",
|
|
146
|
-
result: result
|
|
147
|
-
}
|
|
148
|
-
when "validate"
|
|
149
|
-
puts " → Completing workflow"
|
|
150
|
-
decision["action"]["type"] = "complete"
|
|
151
|
-
result = execute_action(decision)
|
|
152
|
-
@state[:steps_completed] << {
|
|
153
|
-
step: decision["step"],
|
|
154
|
-
action: "complete",
|
|
155
|
-
result: result
|
|
156
|
-
}
|
|
157
|
-
puts "\n✅ Workflow completed successfully!"
|
|
158
|
-
break
|
|
159
|
-
end
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
# Handle risk
|
|
163
|
-
if decision["risk_assessment"] && decision["risk_assessment"]["level"] == "high"
|
|
164
|
-
puts "⚠️ High risk detected - proceeding with caution"
|
|
165
|
-
end
|
|
166
|
-
rescue Ollama::SchemaViolationError => e
|
|
167
|
-
puts "❌ Schema violation: #{e.message}"
|
|
168
|
-
@state[:errors] << { step: step_count, error: "schema_violation", message: e.message }
|
|
169
|
-
break
|
|
170
|
-
rescue Ollama::RetryExhaustedError => e
|
|
171
|
-
puts "❌ Retries exhausted: #{e.message}"
|
|
172
|
-
@state[:errors] << { step: step_count, error: "retry_exhausted", message: e.message }
|
|
173
|
-
break
|
|
174
|
-
rescue Ollama::Error => e
|
|
175
|
-
puts "❌ Error: #{e.message}"
|
|
176
|
-
@state[:errors] << { step: step_count, error: "general", message: e.message }
|
|
177
|
-
# Try to recover or break
|
|
178
|
-
break if step_count > 3 # Don't loop forever
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
puts
|
|
182
|
-
sleep 0.5 # Small delay for readability
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
display_summary
|
|
186
|
-
@state
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
private
|
|
190
|
-
|
|
191
|
-
def build_context(goal:) # rubocop:disable Lint/UnusedMethodArgument
|
|
192
|
-
{
|
|
193
|
-
steps_completed: @state[:steps_completed].map { |s| s[:action] },
|
|
194
|
-
data_collected: @state[:data_collected].keys,
|
|
195
|
-
error_count: @state[:errors].length,
|
|
196
|
-
step_count: @state[:steps_completed].length
|
|
197
|
-
}
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
def build_prompt(goal:, context:)
|
|
201
|
-
step_count = context[:step_count]
|
|
202
|
-
completed_actions = context[:steps_completed]
|
|
203
|
-
collected_data = context[:data_collected]
|
|
204
|
-
|
|
205
|
-
# Determine current phase based on what's been done
|
|
206
|
-
phase = if completed_actions.empty?
|
|
207
|
-
"collection"
|
|
208
|
-
elsif completed_actions.include?("collect") && !completed_actions.include?("analyze")
|
|
209
|
-
"analysis"
|
|
210
|
-
elsif completed_actions.include?("analyze") && !completed_actions.include?("validate")
|
|
211
|
-
"validation"
|
|
212
|
-
else
|
|
213
|
-
"completion"
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
<<~PROMPT
|
|
217
|
-
Goal: #{goal}
|
|
218
|
-
|
|
219
|
-
Current Phase: #{phase}
|
|
220
|
-
Steps completed: #{step_count}
|
|
221
|
-
Actions taken: #{completed_actions.join(", ") || "none"}
|
|
222
|
-
Data collected: #{collected_data.join(", ") || "none"}
|
|
223
|
-
Errors encountered: #{context[:error_count]}
|
|
224
|
-
|
|
225
|
-
Workflow Phases (in order):
|
|
226
|
-
1. COLLECTION: Collect initial data (user data, patterns, etc.)
|
|
227
|
-
2. ANALYSIS: Analyze collected data for patterns and insights
|
|
228
|
-
3. VALIDATION: Validate the analysis results
|
|
229
|
-
4. COMPLETION: Finish the workflow
|
|
230
|
-
|
|
231
|
-
Current State Analysis:
|
|
232
|
-
- You are in the #{phase} phase
|
|
233
|
-
- You have completed #{step_count} steps
|
|
234
|
-
- You have collected: #{collected_data.any? ? collected_data.join(", ") : "nothing yet"}
|
|
235
|
-
|
|
236
|
-
Decision Guidelines:
|
|
237
|
-
- If in COLLECTION phase and no data collected: use action "collect" with specific data_type (e.g., "user_data", "patterns")
|
|
238
|
-
- If data collected but not analyzed: use action "analyze" with target
|
|
239
|
-
- If analyzed but not validated: use action "validate"
|
|
240
|
-
- If all phases done: use action "complete"
|
|
241
|
-
- AVOID repeating the same action multiple times unless necessary
|
|
242
|
-
- Progress through phases: collect → analyze → validate → complete
|
|
243
|
-
|
|
244
|
-
Provide a structured decision with high confidence (>0.7) if possible.
|
|
245
|
-
Set step number to #{step_count + 1}.
|
|
246
|
-
PROMPT
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
def display_decision(decision)
|
|
250
|
-
puts "\n📋 Decision:"
|
|
251
|
-
puts " Step: #{decision['step']}"
|
|
252
|
-
puts " Action: #{decision['action']['type']}"
|
|
253
|
-
puts " Reasoning: #{decision['reasoning']}"
|
|
254
|
-
puts " Confidence: #{(decision['confidence'] * 100).round}%"
|
|
255
|
-
if decision["risk_assessment"]
|
|
256
|
-
puts " Risk Level: #{decision['risk_assessment']['level']}"
|
|
257
|
-
if decision["risk_assessment"]["factors"]
|
|
258
|
-
puts " Risk Factors: #{decision['risk_assessment']['factors'].join(', ')}"
|
|
259
|
-
end
|
|
260
|
-
end
|
|
261
|
-
return unless decision["next_steps"] && !decision["next_steps"].empty?
|
|
262
|
-
|
|
263
|
-
puts " Next Steps: #{decision['next_steps'].join(' → ')}"
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
def execute_action(decision)
|
|
267
|
-
action_type = decision["action"]["type"]
|
|
268
|
-
params = decision["action"]["parameters"] || {}
|
|
269
|
-
|
|
270
|
-
case action_type
|
|
271
|
-
when "collect"
|
|
272
|
-
data_key = params["data_type"] || params["key"] || "user_data"
|
|
273
|
-
# Prevent collecting the same generic data repeatedly
|
|
274
|
-
data_key = "user_data" if should_collect_user_data?(data_key)
|
|
275
|
-
puts " 📥 Collecting: #{data_key}"
|
|
276
|
-
@state[:data_collected][data_key] = "collected_at_#{Time.now.to_i}"
|
|
277
|
-
{ status: "collected", key: data_key }
|
|
278
|
-
|
|
279
|
-
when "analyze"
|
|
280
|
-
target = params["target"] || "collected_data"
|
|
281
|
-
puts " 🔍 Analyzing: #{target}"
|
|
282
|
-
# Mark that analysis has been done
|
|
283
|
-
@state[:data_collected]["analysis_complete"] = true
|
|
284
|
-
{ status: "analyzed", target: target, insights: "Patterns identified in collected data" }
|
|
285
|
-
|
|
286
|
-
when "transform"
|
|
287
|
-
transformation = params["type"] || "default"
|
|
288
|
-
puts " 🔄 Transforming: #{transformation}"
|
|
289
|
-
{ status: "transformed", type: transformation }
|
|
290
|
-
|
|
291
|
-
when "validate"
|
|
292
|
-
validation_type = params["type"] || "results"
|
|
293
|
-
puts " ✓ Validating: #{validation_type}"
|
|
294
|
-
# Mark that validation has been done
|
|
295
|
-
@state[:data_collected]["validation_complete"] = true
|
|
296
|
-
{ status: "validated", type: validation_type, result: "All checks passed" }
|
|
297
|
-
|
|
298
|
-
when "complete"
|
|
299
|
-
puts " ✅ Completing workflow"
|
|
300
|
-
{ status: "complete" }
|
|
301
|
-
|
|
302
|
-
else
|
|
303
|
-
{ status: "unknown_action" }
|
|
304
|
-
end
|
|
305
|
-
end
|
|
306
|
-
|
|
307
|
-
def should_collect_user_data?(data_key)
|
|
308
|
-
@state[:data_collected].key?(data_key) &&
|
|
309
|
-
data_key.match?(/^(missing|unknown|data)$/i) &&
|
|
310
|
-
!@state[:data_collected].key?("user_data")
|
|
311
|
-
end
|
|
312
|
-
|
|
313
|
-
def display_summary
|
|
314
|
-
puts "\n" + "=" * 60
|
|
315
|
-
puts "Workflow Summary"
|
|
316
|
-
puts "=" * 60
|
|
317
|
-
puts "Steps completed: #{@state[:steps_completed].length}"
|
|
318
|
-
puts "Data collected: #{@state[:data_collected].keys.join(', ') || 'none'}"
|
|
319
|
-
puts "Errors: #{@state[:errors].length}"
|
|
320
|
-
return unless @state[:errors].any?
|
|
321
|
-
|
|
322
|
-
puts "\nErrors:"
|
|
323
|
-
@state[:errors].each do |error|
|
|
324
|
-
puts " Step #{error[:step]}: #{error[:error]} - #{error[:message]}"
|
|
325
|
-
end
|
|
326
|
-
end
|
|
327
|
-
end
|
|
328
|
-
|
|
329
|
-
# Run example
|
|
330
|
-
if __FILE__ == $PROGRAM_NAME
|
|
331
|
-
# Use longer timeout for multi-step workflows
|
|
332
|
-
config = Ollama::Config.new
|
|
333
|
-
config.timeout = 60 # 60 seconds for complex operations
|
|
334
|
-
client = Ollama::Client.new(config: config)
|
|
335
|
-
|
|
336
|
-
agent = MultiStepAgent.new(client: client)
|
|
337
|
-
agent.execute_workflow(
|
|
338
|
-
goal: "Collect user data, analyze patterns, validate results, and generate report"
|
|
339
|
-
)
|
|
340
|
-
end
|
|
341
|
-
|
|
@@ -1,186 +0,0 @@
|
|
|
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
|
-
|
data/examples/chat_console.rb
DELETED
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
require_relative "../lib/ollama_client"
|
|
5
|
-
require "tty-reader"
|
|
6
|
-
require "tty-screen"
|
|
7
|
-
require "tty-cursor"
|
|
8
|
-
|
|
9
|
-
def build_config
|
|
10
|
-
config = Ollama::Config.new
|
|
11
|
-
config.base_url = ENV["OLLAMA_BASE_URL"] if ENV["OLLAMA_BASE_URL"]
|
|
12
|
-
config.model = ENV["OLLAMA_MODEL"] if ENV["OLLAMA_MODEL"]
|
|
13
|
-
config.temperature = ENV["OLLAMA_TEMPERATURE"].to_f if ENV["OLLAMA_TEMPERATURE"]
|
|
14
|
-
config
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def exit_command?(text)
|
|
18
|
-
%w[/exit /quit exit quit].include?(text.downcase)
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def add_system_message(messages)
|
|
22
|
-
system_prompt = ENV.fetch("OLLAMA_SYSTEM", nil)
|
|
23
|
-
return unless system_prompt && !system_prompt.strip.empty?
|
|
24
|
-
|
|
25
|
-
messages << { role: "system", content: system_prompt }
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def print_banner(config)
|
|
29
|
-
puts "Ollama chat console"
|
|
30
|
-
puts "Model: #{config.model}"
|
|
31
|
-
puts "Base URL: #{config.base_url}"
|
|
32
|
-
puts "Type /exit to quit."
|
|
33
|
-
puts "Screen: #{TTY::Screen.width}x#{TTY::Screen.height}"
|
|
34
|
-
puts
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
HISTORY_PATH = ".ollama_chat_history"
|
|
38
|
-
MAX_HISTORY = 200
|
|
39
|
-
COLOR_RESET = "\e[0m"
|
|
40
|
-
COLOR_USER = "\e[32m"
|
|
41
|
-
COLOR_LLM = "\e[36m"
|
|
42
|
-
USER_PROMPT = "#{COLOR_USER}you>#{COLOR_RESET} ".freeze
|
|
43
|
-
LLM_PROMPT = "#{COLOR_LLM}llm>#{COLOR_RESET} ".freeze
|
|
44
|
-
|
|
45
|
-
def build_reader
|
|
46
|
-
TTY::Reader.new
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def read_input(reader)
|
|
50
|
-
reader.read_line(USER_PROMPT)
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def load_history(reader, path)
|
|
54
|
-
history = load_history_list(path)
|
|
55
|
-
history.reverse_each { |line| reader.add_to_history(line) }
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def load_history_list(path)
|
|
59
|
-
return [] unless File.exist?(path)
|
|
60
|
-
|
|
61
|
-
unique_history(normalize_history(File.readlines(path, chomp: true)))
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def normalize_history(lines)
|
|
65
|
-
lines.map(&:strip).reject(&:empty?)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def unique_history(lines)
|
|
69
|
-
seen = {}
|
|
70
|
-
lines.each_with_object([]) do |line, unique|
|
|
71
|
-
next if seen[line]
|
|
72
|
-
|
|
73
|
-
unique << line
|
|
74
|
-
seen[line] = true
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def update_history(path, text)
|
|
79
|
-
history = load_history_list(path)
|
|
80
|
-
history.delete(text)
|
|
81
|
-
history.unshift(text)
|
|
82
|
-
history = history.first(MAX_HISTORY)
|
|
83
|
-
|
|
84
|
-
File.write(path, history.join("\n") + (history.empty? ? "" : "\n"))
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def chat_response(client, messages, config)
|
|
88
|
-
content = +""
|
|
89
|
-
prompt_printed = false
|
|
90
|
-
|
|
91
|
-
print "#{COLOR_LLM}...#{COLOR_RESET}"
|
|
92
|
-
$stdout.flush
|
|
93
|
-
|
|
94
|
-
client.chat_raw(
|
|
95
|
-
messages: messages,
|
|
96
|
-
allow_chat: true,
|
|
97
|
-
options: { temperature: config.temperature },
|
|
98
|
-
stream: true
|
|
99
|
-
) do |chunk|
|
|
100
|
-
token = chunk.dig("message", "content").to_s
|
|
101
|
-
next if token.empty?
|
|
102
|
-
|
|
103
|
-
unless prompt_printed
|
|
104
|
-
print "\r#{LLM_PROMPT}"
|
|
105
|
-
prompt_printed = true
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
content << token
|
|
109
|
-
print token
|
|
110
|
-
$stdout.flush
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
puts
|
|
114
|
-
content
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def run_console(client, config)
|
|
118
|
-
messages = []
|
|
119
|
-
add_system_message(messages)
|
|
120
|
-
print_banner(config)
|
|
121
|
-
reader = build_reader
|
|
122
|
-
load_history(reader, HISTORY_PATH)
|
|
123
|
-
|
|
124
|
-
loop do
|
|
125
|
-
input = read_input(reader)
|
|
126
|
-
break unless input
|
|
127
|
-
|
|
128
|
-
text = input.strip
|
|
129
|
-
next if text.empty?
|
|
130
|
-
break if exit_command?(text)
|
|
131
|
-
|
|
132
|
-
update_history(HISTORY_PATH, text)
|
|
133
|
-
messages << { role: "user", content: text }
|
|
134
|
-
content = chat_response(client, messages, config)
|
|
135
|
-
messages << { role: "assistant", content: content }
|
|
136
|
-
end
|
|
137
|
-
rescue Interrupt
|
|
138
|
-
puts "\nExiting..."
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
config = build_config
|
|
142
|
-
client = Ollama::Client.new(config: config)
|
|
143
|
-
run_console(client, config)
|