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.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -0
  3. data/README.md +336 -91
  4. data/RELEASE_NOTES_v0.2.6.md +41 -0
  5. data/docs/AREAS_FOR_CONSIDERATION.md +325 -0
  6. data/docs/EXAMPLE_REORGANIZATION.md +412 -0
  7. data/docs/FEATURES_ADDED.md +12 -1
  8. data/docs/GETTING_STARTED.md +361 -0
  9. data/docs/INTEGRATION_TESTING.md +170 -0
  10. data/docs/NEXT_STEPS_SUMMARY.md +114 -0
  11. data/docs/PERSONAS.md +383 -0
  12. data/docs/QUICK_START.md +195 -0
  13. data/docs/TESTING.md +392 -170
  14. data/docs/TEST_CHECKLIST.md +450 -0
  15. data/examples/README.md +62 -63
  16. data/examples/basic_chat.rb +33 -0
  17. data/examples/basic_generate.rb +29 -0
  18. data/examples/mcp_executor.rb +39 -0
  19. data/examples/mcp_http_executor.rb +45 -0
  20. data/examples/tool_calling_parsing.rb +59 -0
  21. data/examples/tool_dto_example.rb +0 -0
  22. data/exe/ollama-client +128 -1
  23. data/lib/ollama/agent/planner.rb +7 -2
  24. data/lib/ollama/chat_session.rb +101 -0
  25. data/lib/ollama/client.rb +41 -35
  26. data/lib/ollama/config.rb +9 -4
  27. data/lib/ollama/document_loader.rb +1 -1
  28. data/lib/ollama/embeddings.rb +61 -28
  29. data/lib/ollama/errors.rb +1 -0
  30. data/lib/ollama/mcp/http_client.rb +149 -0
  31. data/lib/ollama/mcp/stdio_client.rb +146 -0
  32. data/lib/ollama/mcp/tools_bridge.rb +72 -0
  33. data/lib/ollama/mcp.rb +31 -0
  34. data/lib/ollama/options.rb +3 -1
  35. data/lib/ollama/personas.rb +287 -0
  36. data/lib/ollama/version.rb +1 -1
  37. data/lib/ollama_client.rb +17 -5
  38. metadata +22 -48
  39. data/examples/advanced_complex_schemas.rb +0 -366
  40. data/examples/advanced_edge_cases.rb +0 -241
  41. data/examples/advanced_error_handling.rb +0 -200
  42. data/examples/advanced_multi_step_agent.rb +0 -341
  43. data/examples/advanced_performance_testing.rb +0 -186
  44. data/examples/chat_console.rb +0 -143
  45. data/examples/complete_workflow.rb +0 -245
  46. data/examples/dhan_console.rb +0 -843
  47. data/examples/dhanhq/README.md +0 -236
  48. data/examples/dhanhq/agents/base_agent.rb +0 -74
  49. data/examples/dhanhq/agents/data_agent.rb +0 -66
  50. data/examples/dhanhq/agents/orchestrator_agent.rb +0 -120
  51. data/examples/dhanhq/agents/technical_analysis_agent.rb +0 -252
  52. data/examples/dhanhq/agents/trading_agent.rb +0 -81
  53. data/examples/dhanhq/analysis/market_structure.rb +0 -138
  54. data/examples/dhanhq/analysis/pattern_recognizer.rb +0 -192
  55. data/examples/dhanhq/analysis/trend_analyzer.rb +0 -88
  56. data/examples/dhanhq/builders/market_context_builder.rb +0 -67
  57. data/examples/dhanhq/dhanhq_agent.rb +0 -829
  58. data/examples/dhanhq/indicators/technical_indicators.rb +0 -158
  59. data/examples/dhanhq/scanners/intraday_options_scanner.rb +0 -492
  60. data/examples/dhanhq/scanners/swing_scanner.rb +0 -247
  61. data/examples/dhanhq/schemas/agent_schemas.rb +0 -61
  62. data/examples/dhanhq/services/base_service.rb +0 -46
  63. data/examples/dhanhq/services/data_service.rb +0 -118
  64. data/examples/dhanhq/services/trading_service.rb +0 -59
  65. data/examples/dhanhq/technical_analysis_agentic_runner.rb +0 -411
  66. data/examples/dhanhq/technical_analysis_runner.rb +0 -420
  67. data/examples/dhanhq/test_tool_calling.rb +0 -538
  68. data/examples/dhanhq/test_tool_calling_verbose.rb +0 -251
  69. data/examples/dhanhq/utils/instrument_helper.rb +0 -32
  70. data/examples/dhanhq/utils/parameter_cleaner.rb +0 -28
  71. data/examples/dhanhq/utils/parameter_normalizer.rb +0 -45
  72. data/examples/dhanhq/utils/rate_limiter.rb +0 -23
  73. data/examples/dhanhq/utils/trading_parameter_normalizer.rb +0 -72
  74. data/examples/dhanhq_agent.rb +0 -964
  75. data/examples/dhanhq_tools.rb +0 -1663
  76. data/examples/multi_step_agent_with_external_data.rb +0 -368
  77. data/examples/structured_outputs_chat.rb +0 -72
  78. data/examples/structured_tools.rb +0 -89
  79. data/examples/test_dhanhq_tool_calling.rb +0 -375
  80. data/examples/test_tool_calling.rb +0 -160
  81. data/examples/tool_calling_direct.rb +0 -124
  82. data/examples/tool_calling_pattern.rb +0 -269
  83. 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
-
@@ -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)