ollama-client 0.2.1 → 0.2.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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/README.md +220 -12
  4. data/docs/CLOUD.md +29 -0
  5. data/docs/CONSOLE_IMPROVEMENTS.md +256 -0
  6. data/docs/FEATURES_ADDED.md +145 -0
  7. data/docs/HANDLERS_ANALYSIS.md +190 -0
  8. data/docs/README.md +37 -0
  9. data/docs/SCHEMA_FIXES.md +147 -0
  10. data/docs/TEST_UPDATES.md +107 -0
  11. data/examples/README.md +92 -0
  12. data/examples/advanced_complex_schemas.rb +6 -3
  13. data/examples/advanced_multi_step_agent.rb +13 -7
  14. data/examples/chat_console.rb +143 -0
  15. data/examples/complete_workflow.rb +14 -4
  16. data/examples/dhan_console.rb +843 -0
  17. data/examples/dhanhq/agents/base_agent.rb +0 -2
  18. data/examples/dhanhq/agents/orchestrator_agent.rb +1 -2
  19. data/examples/dhanhq/agents/technical_analysis_agent.rb +67 -49
  20. data/examples/dhanhq/analysis/market_structure.rb +44 -28
  21. data/examples/dhanhq/analysis/pattern_recognizer.rb +64 -47
  22. data/examples/dhanhq/analysis/trend_analyzer.rb +6 -8
  23. data/examples/dhanhq/dhanhq_agent.rb +296 -99
  24. data/examples/dhanhq/indicators/technical_indicators.rb +3 -5
  25. data/examples/dhanhq/scanners/intraday_options_scanner.rb +360 -255
  26. data/examples/dhanhq/scanners/swing_scanner.rb +118 -84
  27. data/examples/dhanhq/schemas/agent_schemas.rb +2 -2
  28. data/examples/dhanhq/services/data_service.rb +5 -7
  29. data/examples/dhanhq/services/trading_service.rb +0 -3
  30. data/examples/dhanhq/technical_analysis_agentic_runner.rb +217 -84
  31. data/examples/dhanhq/technical_analysis_runner.rb +216 -162
  32. data/examples/dhanhq/test_tool_calling.rb +538 -0
  33. data/examples/dhanhq/test_tool_calling_verbose.rb +251 -0
  34. data/examples/dhanhq/utils/trading_parameter_normalizer.rb +12 -17
  35. data/examples/dhanhq_agent.rb +159 -116
  36. data/examples/dhanhq_tools.rb +1158 -251
  37. data/examples/multi_step_agent_with_external_data.rb +368 -0
  38. data/examples/structured_tools.rb +89 -0
  39. data/examples/test_dhanhq_tool_calling.rb +375 -0
  40. data/examples/test_tool_calling.rb +160 -0
  41. data/examples/tool_calling_direct.rb +124 -0
  42. data/examples/tool_dto_example.rb +94 -0
  43. data/exe/dhan_console +4 -0
  44. data/exe/ollama-client +1 -1
  45. data/lib/ollama/agent/executor.rb +116 -15
  46. data/lib/ollama/client.rb +118 -55
  47. data/lib/ollama/config.rb +36 -0
  48. data/lib/ollama/dto.rb +187 -0
  49. data/lib/ollama/embeddings.rb +77 -0
  50. data/lib/ollama/options.rb +104 -0
  51. data/lib/ollama/response.rb +121 -0
  52. data/lib/ollama/tool/function/parameters/property.rb +72 -0
  53. data/lib/ollama/tool/function/parameters.rb +101 -0
  54. data/lib/ollama/tool/function.rb +78 -0
  55. data/lib/ollama/tool.rb +60 -0
  56. data/lib/ollama/version.rb +1 -1
  57. data/lib/ollama_client.rb +3 -0
  58. metadata +31 -3
  59. /data/{PRODUCTION_FIXES.md → docs/PRODUCTION_FIXES.md} +0 -0
  60. /data/{TESTING.md → docs/TESTING.md} +0 -0
@@ -0,0 +1,538 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # DhanHQ Tool Calling Test
5
+ # Dedicated test file for tool calling with DhanHQ tools
6
+ # Uses Executor + Structured Tool Classes
7
+
8
+ require_relative "../../lib/ollama_client"
9
+ require_relative "../dhanhq_tools"
10
+
11
+ puts "\n=== DHANHQ TOOL CALLING TEST ===\n"
12
+
13
+ # Configure DhanHQ
14
+ begin
15
+ DhanHQ.configure_with_env
16
+ puts "✅ DhanHQ configured"
17
+ rescue StandardError => e
18
+ puts "⚠️ DhanHQ configuration error: #{e.message}"
19
+ puts " Make sure CLIENT_ID and ACCESS_TOKEN are set in ENV"
20
+ puts " Continuing with test (may fail on actual API calls)..."
21
+ end
22
+
23
+ # Create client
24
+ config = Ollama::Config.new
25
+ config.model = ENV.fetch("OLLAMA_MODEL", "llama3.1:8b")
26
+ config.temperature = 0.2
27
+ config.timeout = 60
28
+ client = Ollama::Client.new(config: config)
29
+
30
+ def find_strike_key(strike_keys, strike_value)
31
+ strike_keys.find { |key| (key.to_s.to_f - strike_value).abs < 0.01 }
32
+ end
33
+
34
+ def log_missing_strike_key(strike_value, strike_keys)
35
+ puts "⚠️ Could not find key for strike ₹#{strike_value}"
36
+ puts " Available keys sample: #{strike_keys.first(3).inspect}"
37
+ end
38
+
39
+ def print_option_leg(heading, data, label)
40
+ puts heading
41
+ if data && !data.empty?
42
+ puts " LTP: ₹#{option_value(data, :ltp)}"
43
+ puts " IV: #{option_value(data, :iv)}%"
44
+ puts " OI: #{option_value(data, :oi)}"
45
+ puts " Volume: #{option_value(data, :volume)}"
46
+ puts " Delta: #{option_value(data, :delta)}"
47
+ puts " Gamma: #{option_value(data, :gamma)}"
48
+ puts " Theta: #{option_value(data, :theta)}"
49
+ puts " Vega: #{option_value(data, :vega)}"
50
+ else
51
+ puts " No #{label} data available"
52
+ end
53
+ end
54
+
55
+ def option_value(data, key)
56
+ return "N/A" unless data
57
+
58
+ data[key] || data[key.to_s] || "N/A"
59
+ end
60
+
61
+ def print_strike_summary(strike_value, strike_keys, chain)
62
+ actual_key = find_strike_key(strike_keys, strike_value)
63
+ unless actual_key
64
+ log_missing_strike_key(strike_value, strike_keys)
65
+ return
66
+ end
67
+
68
+ strike_data = chain[actual_key]
69
+ unless strike_data
70
+ puts "⚠️ No data found for strike ₹#{strike_value} (key: #{actual_key.inspect})"
71
+ puts " Strike data type: #{strike_data.class}" if strike_data
72
+ puts " Strike data: #{strike_data.inspect}" if strike_data
73
+ return
74
+ end
75
+
76
+ puts "=" * 60
77
+ puts "Strike: ₹#{strike_value} (key: #{actual_key.inspect})"
78
+ puts "-" * 60
79
+
80
+ # Extract CALL and PUT data (DhanHQ uses "ce" for CALL and "pe" for PUT)
81
+ call_data = strike_data[:ce] || strike_data["ce"] || {}
82
+ put_data = strike_data[:pe] || strike_data["pe"] || {}
83
+
84
+ print_option_leg("CALL Options (CE):", call_data, "CALL")
85
+ print_option_leg("\nPUT Options (PE):", put_data, "PUT")
86
+ puts
87
+ end
88
+
89
+ # Define DhanHQ tools using structured Tool classes
90
+ puts "\n--- Defining Tools ---"
91
+
92
+ market_quote_tool = Ollama::Tool.new(
93
+ type: "function",
94
+ function: Ollama::Tool::Function.new(
95
+ name: "get_market_quote",
96
+ description: "Get market quote for a symbol. Returns OHLC, depth, volume, and other market data. " \
97
+ "Finds instrument automatically using exchange_segment and symbol.",
98
+ parameters: Ollama::Tool::Function::Parameters.new(
99
+ type: "object",
100
+ properties: {
101
+ symbol: Ollama::Tool::Function::Parameters::Property.new(
102
+ type: "string",
103
+ description: "Stock or index symbol (e.g., RELIANCE, NIFTY)"
104
+ ),
105
+ exchange_segment: Ollama::Tool::Function::Parameters::Property.new(
106
+ type: "string",
107
+ description: "Exchange segment",
108
+ enum: %w[NSE_EQ NSE_FNO BSE_EQ BSE_FNO IDX_I]
109
+ )
110
+ },
111
+ required: %w[symbol exchange_segment]
112
+ )
113
+ )
114
+ )
115
+
116
+ live_ltp_tool = Ollama::Tool.new(
117
+ type: "function",
118
+ function: Ollama::Tool::Function.new(
119
+ name: "get_live_ltp",
120
+ description: "Get live last traded price (LTP) for a symbol. Fast API for current price. " \
121
+ "Finds instrument automatically using exchange_segment and symbol.",
122
+ parameters: Ollama::Tool::Function::Parameters.new(
123
+ type: "object",
124
+ properties: {
125
+ symbol: Ollama::Tool::Function::Parameters::Property.new(
126
+ type: "string",
127
+ description: "Stock or index symbol"
128
+ ),
129
+ exchange_segment: Ollama::Tool::Function::Parameters::Property.new(
130
+ type: "string",
131
+ description: "Exchange segment",
132
+ enum: %w[NSE_EQ NSE_FNO BSE_EQ BSE_FNO IDX_I]
133
+ )
134
+ },
135
+ required: %w[symbol exchange_segment]
136
+ )
137
+ )
138
+ )
139
+
140
+ # Option Chain Tool (for indices: NIFTY, SENSEX, BANKNIFTY)
141
+ option_chain_tool = Ollama::Tool.new(
142
+ type: "function",
143
+ function: Ollama::Tool::Function.new(
144
+ name: "get_option_chain",
145
+ description: "Get option chain for an index (NIFTY, SENSEX, BANKNIFTY). " \
146
+ "Returns available expiries and option chain data with strikes, Greeks, OI, and IV.",
147
+ parameters: Ollama::Tool::Function::Parameters.new(
148
+ type: "object",
149
+ properties: {
150
+ symbol: Ollama::Tool::Function::Parameters::Property.new(
151
+ type: "string",
152
+ description: "Index symbol (NIFTY, SENSEX, or BANKNIFTY)",
153
+ enum: %w[NIFTY SENSEX BANKNIFTY]
154
+ ),
155
+ exchange_segment: Ollama::Tool::Function::Parameters::Property.new(
156
+ type: "string",
157
+ description: "Exchange segment (must be IDX_I for indices)",
158
+ enum: %w[IDX_I]
159
+ ),
160
+ expiry: Ollama::Tool::Function::Parameters::Property.new(
161
+ type: "string",
162
+ description: "Optional expiry date (YYYY-MM-DD format). If not provided, returns available expiries list."
163
+ )
164
+ },
165
+ required: %w[symbol exchange_segment]
166
+ )
167
+ )
168
+ )
169
+
170
+ puts "✅ Tools defined: get_market_quote, get_live_ltp, get_option_chain"
171
+
172
+ # Define tools with structured Tool classes and callables
173
+ tools = {
174
+ "get_market_quote" => {
175
+ tool: market_quote_tool,
176
+ callable: lambda do |symbol:, exchange_segment:|
177
+ puts " 🔧 Executing: get_market_quote(#{symbol}, #{exchange_segment})"
178
+ begin
179
+ result = DhanHQDataTools.get_market_quote(
180
+ symbol: symbol.to_s,
181
+ exchange_segment: exchange_segment.to_s
182
+ )
183
+
184
+ if result[:error]
185
+ puts " ❌ Error: #{result[:error]}"
186
+ { error: result[:error] }
187
+ else
188
+ quote = result[:result][:quote]
189
+ response = {
190
+ symbol: symbol,
191
+ exchange_segment: exchange_segment,
192
+ last_price: quote[:last_price],
193
+ volume: quote[:volume],
194
+ ohlc: quote[:ohlc],
195
+ change_percent: quote[:net_change]
196
+ }
197
+ puts " ✅ Success: LTP=#{quote[:last_price]}, Volume=#{quote[:volume]}"
198
+ response
199
+ end
200
+ rescue StandardError => e
201
+ puts " ❌ Exception: #{e.message}"
202
+ { error: e.message }
203
+ end
204
+ end
205
+ },
206
+
207
+ "get_live_ltp" => {
208
+ tool: live_ltp_tool,
209
+ callable: lambda do |symbol:, exchange_segment:|
210
+ puts " 🔧 Executing: get_live_ltp(#{symbol}, #{exchange_segment})"
211
+ begin
212
+ # Add rate limiting delay for MarketFeed APIs
213
+ sleep(1.2) if defined?(DhanHQDataTools) && DhanHQDataTools.respond_to?(:rate_limit_marketfeed)
214
+ DhanHQDataTools.rate_limit_marketfeed if DhanHQDataTools.respond_to?(:rate_limit_marketfeed)
215
+
216
+ result = DhanHQDataTools.get_live_ltp(
217
+ symbol: symbol.to_s,
218
+ exchange_segment: exchange_segment.to_s
219
+ )
220
+
221
+ if result[:error]
222
+ puts " ❌ Error: #{result[:error]}"
223
+ { error: result[:error] }
224
+ else
225
+ response = {
226
+ symbol: symbol,
227
+ exchange_segment: exchange_segment,
228
+ ltp: result[:result][:ltp],
229
+ timestamp: result[:result][:timestamp]
230
+ }
231
+ puts " ✅ Success: LTP=#{result[:result][:ltp]}"
232
+ response
233
+ end
234
+ rescue StandardError => e
235
+ puts " ❌ Exception: #{e.message}"
236
+ { error: e.message }
237
+ end
238
+ end
239
+ },
240
+
241
+ "get_option_chain" => {
242
+ tool: option_chain_tool,
243
+ callable: lambda do |symbol:, exchange_segment:, expiry: nil|
244
+ # Normalize empty string to nil (LLM might pass "" when expiry is optional)
245
+ expiry = nil if expiry.is_a?(String) && expiry.empty?
246
+ puts " 🔧 Executing: get_option_chain(#{symbol}, #{exchange_segment}, expiry=#{expiry || 'nil'})"
247
+ begin
248
+ result = DhanHQDataTools.get_option_chain(
249
+ symbol: symbol.to_s,
250
+ exchange_segment: exchange_segment.to_s,
251
+ expiry: expiry
252
+ )
253
+
254
+ if result[:error]
255
+ puts " ❌ Error: #{result[:error]}"
256
+ { error: result[:error] }
257
+ elsif result[:result] && result[:result][:expiries]
258
+ puts " ✅ Success: #{result[:result][:count]} expiries available"
259
+ {
260
+ symbol: symbol,
261
+ expiries_available: result[:result][:expiries],
262
+ count: result[:result][:count]
263
+ }
264
+ elsif result[:result] && result[:result][:chain]
265
+ chain = result[:result][:chain]
266
+ strikes = chain.is_a?(Hash) ? chain.keys.sort_by(&:to_f) : []
267
+ puts " ✅ Success: #{strikes.length} strikes for expiry #{result[:result][:expiry]}"
268
+ {
269
+ symbol: symbol,
270
+ expiry: result[:result][:expiry],
271
+ underlying_price: result[:result][:underlying_last_price],
272
+ strikes_count: strikes.length,
273
+ sample_strikes: strikes.first(5)
274
+ }
275
+ else
276
+ { error: "Unexpected response format" }
277
+ end
278
+ rescue StandardError => e
279
+ puts " ❌ Exception: #{e.message}"
280
+ { error: e.message }
281
+ end
282
+ end
283
+ }
284
+ }
285
+
286
+ puts "\n--- Test 1: Single Tool Call ---"
287
+ puts "Request: Get market quote for RELIANCE\n"
288
+
289
+ executor1 = Ollama::Agent::Executor.new(
290
+ client,
291
+ tools: { "get_market_quote" => tools["get_market_quote"] },
292
+ max_steps: 5
293
+ )
294
+
295
+ begin
296
+ result1 = executor1.run(
297
+ system: "You are a market data assistant. Use the get_market_quote tool to get market data.",
298
+ user: "Get the market quote for RELIANCE stock on NSE"
299
+ )
300
+
301
+ puts "\n✅ Result:"
302
+ puts result1
303
+ rescue Ollama::Error => e
304
+ puts "\n❌ Error: #{e.message}"
305
+ rescue StandardError => e
306
+ puts "\n❌ Unexpected error: #{e.message}"
307
+ puts e.backtrace.first(3).join("\n")
308
+ end
309
+
310
+ puts "\n" + ("=" * 60)
311
+ puts "--- Test 2: Multiple Tools (LLM Chooses) ---"
312
+ puts "Request: Get RELIANCE quote, NIFTY price, and SENSEX option chain\n"
313
+
314
+ executor2 = Ollama::Agent::Executor.new(
315
+ client,
316
+ tools: tools,
317
+ max_steps: 10
318
+ )
319
+
320
+ begin
321
+ result2 = executor2.run(
322
+ system: "You are a market data assistant. Use the available tools to get market data. " \
323
+ "You can call multiple tools in sequence. When you have the data, summarize it clearly. " \
324
+ "For option chains, use get_option_chain with symbol='SENSEX' and exchange_segment='IDX_I'.",
325
+ user: "Get market quote for RELIANCE stock, check NIFTY's current price, and get SENSEX option chain"
326
+ )
327
+
328
+ puts "\n✅ Result:"
329
+ puts result2
330
+ rescue Ollama::Error => e
331
+ puts "\n❌ Error: #{e.message}"
332
+ rescue StandardError => e
333
+ puts "\n❌ Unexpected error: #{e.message}"
334
+ puts e.backtrace.first(3).join("\n")
335
+ end
336
+
337
+ puts "\n" + ("=" * 60)
338
+ puts "--- Test 3: Option Chain (SENSEX) - Expiry List ---"
339
+ puts "Request: Get SENSEX option chain expiry list\n"
340
+
341
+ executor3 = Ollama::Agent::Executor.new(
342
+ client,
343
+ tools: { "get_option_chain" => tools["get_option_chain"] },
344
+ max_steps: 5
345
+ )
346
+
347
+ begin
348
+ result3 = executor3.run(
349
+ system: "You are a market data assistant. Use the get_option_chain tool to get option chain data for indices. " \
350
+ "When no expiry is specified, it returns the list of available expiries.",
351
+ user: "Get the option chain for SENSEX index"
352
+ )
353
+
354
+ puts "\n✅ Result:"
355
+ puts result3
356
+ rescue Ollama::Error => e
357
+ puts "\n❌ Error: #{e.message}"
358
+ rescue StandardError => e
359
+ puts "\n❌ Unexpected error: #{e.message}"
360
+ puts e.backtrace.first(3).join("\n")
361
+ end
362
+
363
+ puts "\n" + ("=" * 60)
364
+ puts "--- Test 3b: Option Chain (SENSEX) - Full Chain with Strikes ---"
365
+ puts "Request: Get SENSEX option chain for specific expiry\n"
366
+
367
+ begin
368
+ # First get the expiry list to use a valid expiry
369
+ expiry_result = DhanHQDataTools.get_option_chain(
370
+ symbol: "SENSEX",
371
+ exchange_segment: "IDX_I"
372
+ )
373
+
374
+ if expiry_result[:result] && expiry_result[:result][:expiries] && !expiry_result[:result][:expiries].empty?
375
+ first_expiry = expiry_result[:result][:expiries].first
376
+ puts "Using expiry: #{first_expiry}\n"
377
+
378
+ # Call directly to avoid LLM date confusion
379
+ chain_result = DhanHQDataTools.get_option_chain(
380
+ symbol: "SENSEX",
381
+ exchange_segment: "IDX_I",
382
+ expiry: first_expiry
383
+ )
384
+
385
+ if chain_result[:error]
386
+ puts "❌ Error: #{chain_result[:error]}"
387
+ elsif chain_result[:result] && chain_result[:result][:chain]
388
+ chain = chain_result[:result][:chain]
389
+ underlying_price = chain_result[:result][:underlying_last_price]
390
+ strikes = if chain.is_a?(Hash)
391
+ chain.keys.sort_by { |k| k.to_s.to_f }
392
+ else
393
+ []
394
+ end
395
+
396
+ puts "✅ Option chain retrieved successfully"
397
+ puts " Underlying Price: ₹#{underlying_price}"
398
+ puts " Expiry: #{chain_result[:result][:expiry]}"
399
+ puts " Total Strikes: #{strikes.length}"
400
+ puts " Strike Range: ₹#{strikes.first} to ₹#{strikes.last}" unless strikes.empty?
401
+ puts " Sample strikes: #{strikes.first(5).join(', ')}" unless strikes.empty?
402
+ else
403
+ puts "⚠️ Unexpected response format"
404
+ end
405
+ else
406
+ puts "⚠️ Could not get expiry list to test full chain"
407
+ end
408
+ rescue StandardError => e
409
+ puts "\n❌ Unexpected error: #{e.message}"
410
+ puts e.backtrace.first(3).join("\n")
411
+ end
412
+
413
+ puts "\n" + ("=" * 60)
414
+ puts "--- Test 4: ATM and ATM+1 Strikes (CALL & PUT) ---"
415
+ puts "Request: Get SENSEX option chain and extract ATM, ATM+1 for CALL and PUT\n"
416
+
417
+ begin
418
+ # Get expiry list first
419
+ expiry_result = DhanHQDataTools.get_option_chain(
420
+ symbol: "SENSEX",
421
+ exchange_segment: "IDX_I"
422
+ )
423
+
424
+ expiries = expiry_result.dig(:result, :expiries)
425
+ if expiry_result[:error] || !expiries.is_a?(Array) || expiries.empty?
426
+ error_message = expiry_result[:error] || "No expiries found"
427
+ puts "❌ Error: Could not get expiry list - #{error_message}"
428
+ else
429
+ first_expiry = expiries.first
430
+ puts "Using expiry: #{first_expiry}\n"
431
+
432
+ # Get full option chain for this expiry
433
+ chain_result = DhanHQDataTools.get_option_chain(
434
+ symbol: "SENSEX",
435
+ exchange_segment: "IDX_I",
436
+ expiry: first_expiry
437
+ )
438
+
439
+ if chain_result[:error]
440
+ puts "❌ Error getting option chain: #{chain_result[:error]}"
441
+ elsif chain_result[:result] && chain_result[:result][:chain]
442
+ underlying_price = chain_result[:result][:underlying_last_price].to_f
443
+ chain = chain_result[:result][:chain]
444
+
445
+ puts "Underlying Price (SENSEX): ₹#{underlying_price}\n"
446
+
447
+ # Extract all strikes and sort them
448
+ # Chain keys are typically strings with decimal precision (e.g., "83600.000000")
449
+ strike_keys = if chain.is_a?(Hash)
450
+ chain.keys.sort_by { |k| k.to_s.to_f }
451
+ else
452
+ []
453
+ end
454
+
455
+ strikes = strike_keys.map { |k| k.to_s.to_f }
456
+
457
+ if strikes.empty?
458
+ puts "❌ No strikes found in chain data"
459
+ puts " Chain keys: #{chain.keys.first(5).inspect}" if chain.is_a?(Hash)
460
+ else
461
+ # Find ATM strike (closest to underlying price)
462
+ atm_strike = strikes.min_by { |s| (s - underlying_price).abs }
463
+ atm_index = strikes.index(atm_strike)
464
+ atm_plus_one = strikes[atm_index + 1] if atm_index && (atm_index + 1) < strikes.length
465
+
466
+ puts "ATM Strike: ₹#{atm_strike}"
467
+ puts "ATM+1 Strike: ₹#{atm_plus_one || 'N/A'}\n"
468
+
469
+ # Extract data for ATM and ATM+1 strikes
470
+ # Match strike values to actual keys in chain hash
471
+ [atm_strike, atm_plus_one].compact.each do |strike_value|
472
+ print_strike_summary(strike_value, strike_keys, chain)
473
+ end
474
+
475
+ puts "=" * 60
476
+ puts "✅ Successfully extracted ATM and ATM+1 strikes for CALL and PUT"
477
+ end
478
+ else
479
+ puts "❌ Unexpected response format"
480
+ end
481
+ end
482
+ rescue StandardError => e
483
+ puts "\n❌ Unexpected error: #{e.message}"
484
+ puts e.backtrace.first(5).join("\n")
485
+ end
486
+
487
+ puts "\n" + ("=" * 60)
488
+ puts "--- Test 5: Direct chat_raw() Test ---"
489
+ puts "Testing chat_raw() to access tool_calls directly\n"
490
+
491
+ begin
492
+ response = client.chat_raw(
493
+ model: ENV.fetch("OLLAMA_MODEL", "llama3.1:8b"),
494
+ messages: [Ollama::Agent::Messages.user("Get the option chain for SENSEX index")],
495
+ tools: option_chain_tool,
496
+ allow_chat: true
497
+ )
498
+
499
+ puts "✅ Response received"
500
+ puts "Response class: #{response.class.name}"
501
+
502
+ # Method access (like ollama-ruby)
503
+ tool_calls = response.message&.tool_calls
504
+ if tool_calls && !tool_calls.empty?
505
+ puts "\n✅ Tool calls detected (via method access):"
506
+ tool_calls.each do |call|
507
+ puts " Tool: #{call.name}"
508
+ puts " Arguments: #{call.arguments.inspect}"
509
+ puts " ID: #{call.id || 'N/A'}"
510
+ end
511
+ else
512
+ puts "\n⚠️ No tool calls detected"
513
+ puts "Content: #{response.message&.content}"
514
+ end
515
+
516
+ # Hash access (backward compatible)
517
+ tool_calls_hash = response.to_h.dig("message", "tool_calls")
518
+ if tool_calls_hash && !tool_calls_hash.empty?
519
+ puts "\n✅ Tool calls also accessible via hash:"
520
+ puts " Count: #{tool_calls_hash.length}"
521
+ end
522
+ rescue Ollama::Error => e
523
+ puts "\n❌ Error: #{e.class.name}"
524
+ puts " Message: #{e.message}"
525
+ rescue StandardError => e
526
+ puts "\n❌ Unexpected error: #{e.message}"
527
+ end
528
+
529
+ puts "\n" + ("=" * 60)
530
+ puts "--- Summary ---"
531
+ puts "✅ Tool calling with Executor: Working"
532
+ puts "✅ Structured Tool classes: Working"
533
+ puts "✅ chat_raw() method access: Working"
534
+ puts "✅ Hash access (backward compatible): Working"
535
+ puts "✅ Option chain expiry list: Working"
536
+ puts "✅ Option chain full data with strikes: Working"
537
+ puts "✅ ATM/ATM+1 strike extraction: Working"
538
+ puts "\n=== DONE ===\n"