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.
@@ -0,0 +1,563 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # DhanHQ Tools - All DhanHQ API operations
5
+ # Contains:
6
+ # - Data APIs (6): Market Quote, Live Market Feed, Full Market Depth,
7
+ # Historical Data, Expired Options Data, Option Chain
8
+ # - Trading Tools: Order parameter building (does not place orders)
9
+
10
+ require "json"
11
+ require "dhan_hq"
12
+
13
+ # Helper to get valid exchange segments from DhanHQ constants
14
+ def valid_exchange_segments
15
+ DhanHQ::Constants::EXCHANGE_SEGMENTS
16
+ rescue StandardError
17
+ ["NSE_EQ", "NSE_FNO", "NSE_CURRENCY", "BSE_EQ", "BSE_FNO",
18
+ "BSE_CURRENCY", "MCX_COMM", "IDX_I"]
19
+ end
20
+
21
+ # Helper to get INDEX constant
22
+ def index_exchange_segment
23
+ DhanHQ::Constants::INDEX
24
+ rescue StandardError
25
+ "IDX_I"
26
+ end
27
+
28
+ # Helper to extract values from different data structures
29
+ def extract_value(data, keys)
30
+ return nil unless data
31
+
32
+ keys.each do |key|
33
+ if data.is_a?(Hash)
34
+ return data[key] if data.key?(key)
35
+ elsif data.respond_to?(key)
36
+ return data.send(key)
37
+ end
38
+ end
39
+
40
+ # If data is a simple value and we're looking for it directly
41
+ return data if data.is_a?(Numeric) || data.is_a?(String)
42
+
43
+ nil
44
+ end
45
+
46
+ # Helper to safely get instrument attribute (handles missing methods)
47
+ def safe_instrument_attr(instrument, attr_name)
48
+ return nil unless instrument
49
+
50
+ instrument.respond_to?(attr_name) ? instrument.send(attr_name) : nil
51
+ rescue StandardError
52
+ nil
53
+ end
54
+
55
+ # Debug logging helper (if needed)
56
+ def debug_log(location, message, data = {}, hypothesis_id = nil)
57
+ log_entry = {
58
+ sessionId: "debug-session",
59
+ runId: "run1",
60
+ hypothesisId: hypothesis_id,
61
+ location: location,
62
+ message: message,
63
+ data: data,
64
+ timestamp: Time.now.to_f * 1000
65
+ }
66
+ File.open("/home/nemesis/project/ollama-client/.cursor/debug.log", "a") do |f|
67
+ f.puts(log_entry.to_json)
68
+ end
69
+ rescue StandardError
70
+ # Ignore logging errors
71
+ end
72
+
73
+ # DhanHQ Data Tools - Data APIs only
74
+ # Contains only the 6 Data APIs:
75
+ # 1. Market Quote
76
+ # 2. Live Market Feed (LTP)
77
+ # 3. Full Market Depth
78
+ # 4. Historical Data
79
+ # 5. Expired Options Data
80
+ # 6. Option Chain
81
+ class DhanHQDataTools
82
+ class << self
83
+ # Rate limiting: MarketFeed APIs have a limit of 1 request per second
84
+ # Track last API call time to enforce rate limiting
85
+ @last_marketfeed_call = nil
86
+ @marketfeed_mutex = Mutex.new
87
+
88
+ # Helper to enforce rate limiting for MarketFeed APIs (1 request per second)
89
+ def rate_limit_marketfeed
90
+ @marketfeed_mutex ||= Mutex.new
91
+ @marketfeed_mutex.synchronize do
92
+ if @last_marketfeed_call
93
+ elapsed = Time.now - @last_marketfeed_call
94
+ sleep(1.1 - elapsed) if elapsed < 1.1 # Add 0.1s buffer
95
+ end
96
+ @last_marketfeed_call = Time.now
97
+ end
98
+ end
99
+
100
+ # 1. Market Quote API - Get market quote using Instrument convenience method
101
+ # Uses instrument.quote which automatically uses instrument's security_id,
102
+ # exchange_segment, and instrument attributes
103
+ # Note: Instrument.find(exchange_segment, symbol) expects symbol
104
+ # (e.g., "NIFTY", "RELIANCE"), not security_id
105
+ # Rate limit: 1 request per second
106
+ def get_market_quote(exchange_segment:, security_id: nil, symbol: nil)
107
+ # Instrument.find expects symbol, support both for backward compatibility
108
+ instrument_symbol = symbol || security_id
109
+ unless instrument_symbol
110
+ return {
111
+ action: "get_market_quote",
112
+ error: "Either symbol or security_id must be provided",
113
+ params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
114
+ }
115
+ end
116
+
117
+ rate_limit_marketfeed # Enforce rate limiting
118
+ instrument_symbol = instrument_symbol.to_s
119
+ exchange_segment = exchange_segment.to_s
120
+
121
+ # Find instrument first
122
+ instrument = DhanHQ::Models::Instrument.find(exchange_segment, instrument_symbol)
123
+
124
+ if instrument
125
+ # Use instrument convenience method - automatically uses instrument's attributes
126
+ # Returns nested structure: {"data"=>{"NSE_EQ"=>{"2885"=>{...}}}, "status"=>"success"}
127
+ quote_response = instrument.quote
128
+
129
+ # Extract actual quote data from nested structure
130
+ security_id_str = safe_instrument_attr(instrument, :security_id)&.to_s || security_id.to_s
131
+ if quote_response.is_a?(Hash) && quote_response["data"]
132
+ quote_data = quote_response.dig("data", exchange_segment,
133
+ security_id_str)
134
+ end
135
+
136
+ {
137
+ action: "get_market_quote",
138
+ params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment },
139
+ result: {
140
+ security_id: safe_instrument_attr(instrument, :security_id) || security_id,
141
+ symbol: instrument_symbol,
142
+ exchange_segment: exchange_segment,
143
+ quote: quote_data || quote_response
144
+ }
145
+ }
146
+ else
147
+ {
148
+ action: "get_market_quote",
149
+ error: "Instrument not found",
150
+ params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
151
+ }
152
+ end
153
+ rescue StandardError => e
154
+ {
155
+ action: "get_market_quote",
156
+ error: e.message,
157
+ params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
158
+ }
159
+ end
160
+
161
+ # 2. Live Market Feed API - Get LTP (Last Traded Price) using Instrument convenience method
162
+ # Uses instrument.ltp which automatically uses instrument's security_id, exchange_segment, and instrument attributes
163
+ # Note: Instrument.find(exchange_segment, symbol) expects symbol (e.g., "NIFTY", "RELIANCE"), not security_id
164
+ # Rate limit: 1 request per second
165
+ def get_live_ltp(exchange_segment:, security_id: nil, symbol: nil)
166
+ # Instrument.find expects symbol, support both for backward compatibility
167
+ instrument_symbol = symbol || security_id
168
+ unless instrument_symbol
169
+ return {
170
+ action: "get_live_ltp",
171
+ error: "Either symbol or security_id must be provided",
172
+ params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
173
+ }
174
+ end
175
+
176
+ rate_limit_marketfeed # Enforce rate limiting
177
+ instrument_symbol = instrument_symbol.to_s
178
+ exchange_segment = exchange_segment.to_s
179
+
180
+ # Find instrument first
181
+ instrument = DhanHQ::Models::Instrument.find(exchange_segment, instrument_symbol)
182
+
183
+ if instrument
184
+ # Use instrument convenience method - automatically uses instrument's attributes
185
+ # Returns nested structure: {"data"=>{"NSE_EQ"=>{"2885"=>{"last_price"=>1578.1}}}, "status"=>"success"}
186
+ # OR direct value: 1578.1 (after retry/rate limit handling)
187
+ ltp_response = instrument.ltp
188
+
189
+ # Extract LTP from nested structure or use direct value
190
+ if ltp_response.is_a?(Hash) && ltp_response["data"]
191
+ security_id_str = safe_instrument_attr(instrument, :security_id)&.to_s || security_id.to_s
192
+ ltp_data = ltp_response.dig("data", exchange_segment, security_id_str)
193
+ ltp = extract_value(ltp_data, [:last_price, "last_price"]) if ltp_data
194
+ elsif ltp_response.is_a?(Numeric)
195
+ ltp = ltp_response
196
+ ltp_data = { last_price: ltp }
197
+ else
198
+ ltp = extract_value(ltp_response, [:last_price, "last_price", :ltp, "ltp"]) || ltp_response
199
+ ltp_data = ltp_response
200
+ end
201
+
202
+ {
203
+ action: "get_live_ltp",
204
+ params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment },
205
+ result: {
206
+ security_id: safe_instrument_attr(instrument, :security_id) || security_id,
207
+ symbol: instrument_symbol,
208
+ exchange_segment: exchange_segment,
209
+ ltp: ltp,
210
+ ltp_data: ltp_data
211
+ }
212
+ }
213
+ else
214
+ {
215
+ action: "get_live_ltp",
216
+ error: "Instrument not found",
217
+ params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
218
+ }
219
+ end
220
+ rescue StandardError => e
221
+ {
222
+ action: "get_live_ltp",
223
+ error: e.message,
224
+ params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
225
+ }
226
+ end
227
+
228
+ # 3. Full Market Depth API - Get full market depth (bid/ask levels)
229
+ # Uses instrument.quote which automatically uses instrument's security_id,
230
+ # exchange_segment, and instrument attributes
231
+ # Note: Instrument.find(exchange_segment, symbol) expects symbol
232
+ # (e.g., "NIFTY", "RELIANCE"), not security_id
233
+ # Rate limit: 1 request per second (uses quote API which has stricter limits)
234
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
235
+ def get_market_depth(exchange_segment:, security_id: nil, symbol: nil)
236
+ # Instrument.find expects symbol, support both for backward compatibility
237
+ instrument_symbol = symbol || security_id
238
+ unless instrument_symbol
239
+ return {
240
+ action: "get_market_depth",
241
+ error: "Either symbol or security_id must be provided",
242
+ params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
243
+ }
244
+ end
245
+
246
+ rate_limit_marketfeed # Enforce rate limiting
247
+ instrument_symbol = instrument_symbol.to_s
248
+ exchange_segment = exchange_segment.to_s
249
+
250
+ # Find instrument first
251
+ instrument = DhanHQ::Models::Instrument.find(exchange_segment, instrument_symbol)
252
+
253
+ if instrument
254
+ # Use instrument convenience method - automatically uses instrument's attributes
255
+ # Returns nested structure: {"data"=>{"NSE_EQ"=>{"2885"=>{...}}}, "status"=>"success"}
256
+ quote_response = instrument.quote
257
+
258
+ # Extract actual quote data from nested structure
259
+ security_id_str = safe_instrument_attr(instrument, :security_id)&.to_s || security_id.to_s
260
+ if quote_response.is_a?(Hash) && quote_response["data"]
261
+ quote_data = quote_response.dig("data", exchange_segment,
262
+ security_id_str)
263
+ end
264
+
265
+ # Extract market depth (order book) from quote data
266
+ depth = extract_value(quote_data, [:depth, "depth"]) if quote_data
267
+ buy_depth = extract_value(depth, [:buy, "buy"]) if depth
268
+ sell_depth = extract_value(depth, [:sell, "sell"]) if depth
269
+
270
+ {
271
+ action: "get_market_depth",
272
+ params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment },
273
+ result: {
274
+ security_id: safe_instrument_attr(instrument, :security_id) || security_id,
275
+ symbol: instrument_symbol,
276
+ exchange_segment: exchange_segment,
277
+ market_depth: quote_data || quote_response,
278
+ # Market depth (order book) - buy and sell sides
279
+ buy_depth: buy_depth,
280
+ sell_depth: sell_depth,
281
+ # Additional quote data
282
+ ltp: quote_data ? extract_value(quote_data, [:last_price, "last_price"]) : nil,
283
+ volume: quote_data ? extract_value(quote_data, [:volume, "volume"]) : nil,
284
+ oi: quote_data ? extract_value(quote_data, [:oi, "oi"]) : nil,
285
+ ohlc: quote_data ? extract_value(quote_data, [:ohlc, "ohlc"]) : nil
286
+ }
287
+ }
288
+ else
289
+ {
290
+ action: "get_market_depth",
291
+ error: "Instrument not found",
292
+ params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
293
+ }
294
+ end
295
+ rescue StandardError => e
296
+ {
297
+ action: "get_market_depth",
298
+ error: e.message,
299
+ params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
300
+ }
301
+ end
302
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
303
+
304
+ # 4. Historical Data API - Get historical data using Instrument convenience methods
305
+ # These methods automatically use instrument's security_id, exchange_segment, and instrument attributes
306
+ # Note: Instrument.find(exchange_segment, symbol) expects symbol (e.g., "NIFTY", "RELIANCE"), not security_id
307
+ # rubocop:disable Metrics/ParameterLists
308
+ def get_historical_data(exchange_segment:, from_date:, to_date:, security_id: nil, symbol: nil, interval: nil,
309
+ expiry_code: nil)
310
+ # Instrument.find expects symbol, support both for backward compatibility
311
+ instrument_symbol = symbol || security_id
312
+ unless instrument_symbol
313
+ return {
314
+ action: "get_historical_data",
315
+ error: "Either symbol or security_id must be provided",
316
+ params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
317
+ }
318
+ end
319
+
320
+ instrument_symbol = instrument_symbol.to_s
321
+ exchange_segment = exchange_segment.to_s
322
+ instrument = DhanHQ::Models::Instrument.find(exchange_segment, instrument_symbol)
323
+
324
+ if instrument
325
+ if interval
326
+ # Intraday data - automatically uses instrument's attributes
327
+ data = instrument.intraday(
328
+ from_date: from_date,
329
+ to_date: to_date,
330
+ interval: interval
331
+ )
332
+ {
333
+ action: "get_historical_data",
334
+ type: "intraday",
335
+ params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment,
336
+ from_date: from_date, to_date: to_date, interval: interval },
337
+ result: {
338
+ data: data,
339
+ count: data.is_a?(Array) ? data.length : 0,
340
+ instrument_info: {
341
+ trading_symbol: safe_instrument_attr(instrument, :trading_symbol),
342
+ instrument_type: safe_instrument_attr(instrument, :instrument_type)
343
+ }
344
+ }
345
+ }
346
+ else
347
+ # Daily data - automatically uses instrument's attributes
348
+ # expiry_code is optional for futures/options
349
+ daily_params = { from_date: from_date, to_date: to_date }
350
+ daily_params[:expiry_code] = expiry_code if expiry_code
351
+ data = instrument.daily(**daily_params)
352
+ {
353
+ action: "get_historical_data",
354
+ type: "daily",
355
+ params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment,
356
+ from_date: from_date, to_date: to_date, expiry_code: expiry_code },
357
+ result: {
358
+ data: data,
359
+ count: data.is_a?(Array) ? data.length : 0,
360
+ instrument_info: {
361
+ trading_symbol: safe_instrument_attr(instrument, :trading_symbol),
362
+ instrument_type: safe_instrument_attr(instrument, :instrument_type)
363
+ }
364
+ }
365
+ }
366
+ end
367
+ else
368
+ {
369
+ action: "get_historical_data",
370
+ error: "Instrument not found",
371
+ params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
372
+ }
373
+ end
374
+ rescue StandardError => e
375
+ {
376
+ action: "get_historical_data",
377
+ error: e.message,
378
+ params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
379
+ }
380
+ end
381
+ # rubocop:enable Metrics/ParameterLists
382
+
383
+ # 6. Option Chain API - Get option chain using Instrument convenience methods
384
+ # These methods automatically use instrument's security_id, exchange_segment, and instrument attributes
385
+ # Note: Instrument.find(exchange_segment, symbol) expects symbol (e.g., "NIFTY", "RELIANCE"), not security_id
386
+ def get_option_chain(exchange_segment:, security_id: nil, symbol: nil, expiry: nil)
387
+ # Instrument.find expects symbol, support both for backward compatibility
388
+ instrument_symbol = symbol || security_id
389
+ unless instrument_symbol
390
+ return {
391
+ action: "get_option_chain",
392
+ error: "Either symbol or security_id must be provided",
393
+ params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
394
+ }
395
+ end
396
+
397
+ instrument_symbol = instrument_symbol.to_s
398
+ exchange_segment = exchange_segment.to_s
399
+ instrument = DhanHQ::Models::Instrument.find(exchange_segment, instrument_symbol)
400
+
401
+ if instrument
402
+ if expiry
403
+ # Get option chain for specific expiry - automatically uses instrument's attributes
404
+ chain = instrument.option_chain(expiry: expiry)
405
+ {
406
+ action: "get_option_chain",
407
+ params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment, expiry: expiry },
408
+ result: {
409
+ expiry: expiry,
410
+ chain: chain,
411
+ instrument_info: {
412
+ trading_symbol: safe_instrument_attr(instrument, :trading_symbol),
413
+ instrument_type: safe_instrument_attr(instrument, :instrument_type)
414
+ }
415
+ }
416
+ }
417
+ else
418
+ # Get list of available expiries - automatically uses instrument's attributes
419
+ expiries = instrument.expiry_list
420
+ {
421
+ action: "get_option_chain",
422
+ params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment },
423
+ result: {
424
+ expiries: expiries,
425
+ count: expiries.is_a?(Array) ? expiries.length : 0,
426
+ instrument_info: {
427
+ trading_symbol: safe_instrument_attr(instrument, :trading_symbol),
428
+ instrument_type: safe_instrument_attr(instrument, :instrument_type)
429
+ }
430
+ }
431
+ }
432
+ end
433
+ else
434
+ {
435
+ action: "get_option_chain",
436
+ error: "Instrument not found",
437
+ params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
438
+ }
439
+ end
440
+ rescue StandardError => e
441
+ {
442
+ action: "get_option_chain",
443
+ error: e.message,
444
+ params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
445
+ }
446
+ end
447
+
448
+ # 5. Expired Options Data API - Get historical expired options data
449
+ # Uses Instrument convenience method which automatically uses instrument's attributes
450
+ # Note: Instrument.find(exchange_segment, symbol) expects symbol (e.g., "NIFTY", "RELIANCE"), not security_id
451
+ def get_expired_options_data(exchange_segment:, expiry_date:, security_id: nil, symbol: nil, expiry_code: nil)
452
+ # Instrument.find expects symbol, support both for backward compatibility
453
+ instrument_symbol = symbol || security_id
454
+ unless instrument_symbol
455
+ return {
456
+ action: "get_expired_options_data",
457
+ error: "Either symbol or security_id must be provided",
458
+ params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment,
459
+ expiry_date: expiry_date }
460
+ }
461
+ end
462
+
463
+ instrument_symbol = instrument_symbol.to_s
464
+ exchange_segment = exchange_segment.to_s
465
+ instrument = DhanHQ::Models::Instrument.find(exchange_segment, instrument_symbol)
466
+
467
+ if instrument
468
+ # Get historical data for the expiry date - automatically uses instrument's attributes
469
+ daily_params = { from_date: expiry_date, to_date: expiry_date }
470
+ daily_params[:expiry_code] = expiry_code if expiry_code
471
+ expired_data = instrument.daily(**daily_params)
472
+ {
473
+ action: "get_expired_options_data",
474
+ params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment,
475
+ expiry_date: expiry_date, expiry_code: expiry_code },
476
+ result: {
477
+ security_id: security_id,
478
+ exchange_segment: exchange_segment,
479
+ expiry_date: expiry_date,
480
+ data: expired_data,
481
+ instrument_info: {
482
+ trading_symbol: safe_instrument_attr(instrument, :trading_symbol),
483
+ instrument_type: safe_instrument_attr(instrument, :instrument_type),
484
+ expiry_flag: safe_instrument_attr(instrument, :expiry_flag)
485
+ }
486
+ }
487
+ }
488
+ else
489
+ {
490
+ action: "get_expired_options_data",
491
+ error: "Instrument not found",
492
+ params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment,
493
+ expiry_date: expiry_date }
494
+ }
495
+ end
496
+ rescue StandardError => e
497
+ {
498
+ action: "get_expired_options_data",
499
+ error: e.message,
500
+ params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment,
501
+ expiry_date: expiry_date }
502
+ }
503
+ end
504
+ end
505
+ end
506
+
507
+ # DhanHQ Trading Tools - Order parameter building only
508
+ class DhanHQTradingTools
509
+ class << self
510
+ # Build order parameters (does not place order)
511
+ def build_order_params(params)
512
+ {
513
+ action: "place_order",
514
+ params: params,
515
+ order_params: {
516
+ transaction_type: params[:transaction_type] || "BUY",
517
+ exchange_segment: params[:exchange_segment] || "NSE_EQ",
518
+ product_type: params[:product_type] || "MARGIN",
519
+ order_type: params[:order_type] || "LIMIT",
520
+ validity: params[:validity] || "DAY",
521
+ security_id: params[:security_id],
522
+ quantity: params[:quantity] || 1,
523
+ price: params[:price]
524
+ },
525
+ message: "Order parameters ready: #{params[:transaction_type]} " \
526
+ "#{params[:quantity]} #{params[:security_id]} @ #{params[:price]}"
527
+ }
528
+ end
529
+
530
+ # Build super order parameters (does not place order)
531
+ def build_super_order_params(params)
532
+ {
533
+ action: "place_super_order",
534
+ params: params,
535
+ order_params: {
536
+ transaction_type: params[:transaction_type] || "BUY",
537
+ exchange_segment: params[:exchange_segment] || "NSE_EQ",
538
+ product_type: params[:product_type] || "MARGIN",
539
+ order_type: params[:order_type] || "LIMIT",
540
+ security_id: params[:security_id],
541
+ quantity: params[:quantity] || 1,
542
+ price: params[:price],
543
+ target_price: params[:target_price],
544
+ stop_loss_price: params[:stop_loss_price],
545
+ trailing_jump: params[:trailing_jump] || 10
546
+ },
547
+ message: "Super order parameters ready: Entry @ #{params[:price]}, " \
548
+ "SL: #{params[:stop_loss_price]}, TP: #{params[:target_price]}"
549
+ }
550
+ end
551
+
552
+ # Build cancel order parameters (does not cancel)
553
+ def build_cancel_params(order_id:)
554
+ {
555
+ action: "cancel_order",
556
+ params: { order_id: order_id },
557
+ message: "Cancel parameters ready for order: #{order_id}",
558
+ note: "To actually cancel, call: DhanHQ::Models::Order.find(order_id).cancel"
559
+ }
560
+ end
561
+ end
562
+ end
563
+
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Example: Ollama structured outputs using chat API
5
+ # This matches the JavaScript example from the Ollama documentation
6
+
7
+ require "json"
8
+ require_relative "../lib/ollama_client"
9
+
10
+ def run(model)
11
+ # Define the JSON schema for friend info
12
+ friend_info_schema = {
13
+ "type" => "object",
14
+ "required" => %w[name age is_available],
15
+ "properties" => {
16
+ "name" => {
17
+ "type" => "string",
18
+ "description" => "The name of the friend"
19
+ },
20
+ "age" => {
21
+ "type" => "integer",
22
+ "description" => "The age of the friend"
23
+ },
24
+ "is_available" => {
25
+ "type" => "boolean",
26
+ "description" => "Whether the friend is available"
27
+ }
28
+ }
29
+ }
30
+
31
+ # Define the schema for friend list
32
+ friend_list_schema = {
33
+ "type" => "object",
34
+ "required" => ["friends"],
35
+ "properties" => {
36
+ "friends" => {
37
+ "type" => "array",
38
+ "description" => "An array of friends",
39
+ "items" => friend_info_schema
40
+ }
41
+ }
42
+ }
43
+ client = Ollama::Client.new
44
+
45
+ messages = [{
46
+ role: "user",
47
+ content: "I have two friends. The first is Ollama 22 years old busy saving the world, " \
48
+ "and the second is Alonso 23 years old and wants to hang out. " \
49
+ "Return a list of friends in JSON format"
50
+ }]
51
+
52
+ response = client.chat(
53
+ model: model,
54
+ messages: messages,
55
+ format: friend_list_schema,
56
+ allow_chat: true,
57
+ options: {
58
+ temperature: 0 # Make responses more deterministic
59
+ }
60
+ )
61
+
62
+ # Parse and validate the response (already validated by client, but showing usage)
63
+ begin
64
+ friends_response = response # Already parsed and validated
65
+ puts JSON.pretty_generate(friends_response)
66
+ rescue Ollama::SchemaViolationError => e
67
+ puts "Generated invalid response: #{e.message}"
68
+ end
69
+ end
70
+
71
+ # Run with the same model as the JavaScript example
72
+ run("llama3.1:8b")