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
@@ -49,16 +49,14 @@ module DhanHQ
49
49
  puts " 📋 Rejected candidates:"
50
50
  rejected.each do |r|
51
51
  puts " ❌ #{r[:symbol]}: Score #{r[:score]}/100 - #{r[:reason]}"
52
- if r[:details]
53
- puts " Breakdown: Trend=#{r[:details][:trend]}, RSI=#{r[:details][:rsi]}, MACD=#{r[:details][:macd]}, Structure=#{r[:details][:structure]}, Patterns=#{r[:details][:patterns]}"
54
- end
52
+ puts " Breakdown: #{format_score_breakdown(r[:details])}" if r[:details]
55
53
  end
56
54
  end
57
55
 
58
56
  candidates.sort_by { |c| -c[:score] }
59
57
  end
60
58
 
61
- def scan_by_criteria(criteria = {})
59
+ def scan_by_criteria(_criteria = {})
62
60
  # This would typically fetch a list of symbols from an API
63
61
  # For now, return empty - would need symbol list source
64
62
  []
@@ -99,45 +97,15 @@ module DhanHQ
99
97
  end
100
98
 
101
99
  def calculate_swing_score_details(analysis)
102
- return { trend: 0, rsi: 0, macd: 0, structure: 0, patterns: 0 } if analysis.nil? || analysis.empty?
100
+ return empty_score_details if analysis.nil? || analysis.empty?
103
101
 
104
- details = { trend: 0, rsi: 0, macd: 0, structure: 0, patterns: 0 }
105
-
106
- # Trend strength (0-30 points)
107
- trend = analysis[:trend]
108
- details[:trend] = (trend[:strength] || 0).clamp(0, 30) if trend && trend[:trend] == :uptrend
109
-
110
- # RSI (0-20 points) - prefer 40-60 for swing entries
111
- rsi = analysis[:indicators]&.dig(:rsi)
112
- if rsi
113
- if rsi.between?(40, 60)
114
- details[:rsi] = 20
115
- elsif rsi.between?(30, 70)
116
- details[:rsi] = 10
117
- end
118
- end
119
-
120
- # MACD (0-20 points) - bullish crossover
121
- macd = analysis[:indicators]&.dig(:macd)
122
- signal = analysis[:indicators]&.dig(:macd_signal)
123
- if macd && signal && macd > signal
124
- details[:macd] = 20
125
- elsif macd && signal && macd > signal * 0.9
126
- details[:macd] = 10
127
- end
128
-
129
- # Structure break (0-15 points)
130
- structure_break = analysis[:structure_break]
131
- if structure_break && structure_break[:broken] && structure_break[:direction] == :bullish_break
132
- details[:structure] = 15
133
- end
134
-
135
- # Patterns (0-15 points)
136
- patterns = analysis[:patterns]&.dig(:candlestick) || []
137
- bullish_patterns = patterns.count { |p| p.is_a?(Hash) && p[:type].to_s.include?("bullish") }
138
- details[:patterns] = [bullish_patterns * 5, 15].min
139
-
140
- details
102
+ {
103
+ trend: trend_points(analysis),
104
+ rsi: rsi_points(analysis),
105
+ macd: macd_points(analysis),
106
+ structure: structure_points(analysis),
107
+ patterns: pattern_points(analysis)
108
+ }
141
109
  end
142
110
 
143
111
  def interpret_for_swing(analysis)
@@ -149,7 +117,7 @@ module DhanHQ
149
117
 
150
118
  if trend == :uptrend && rsi && rsi < 70 && structure_break && structure_break[:broken]
151
119
  "Strong swing candidate - uptrend with bullish structure break"
152
- elsif trend == :uptrend && rsi && rsi.between?(40, 60)
120
+ elsif trend == :uptrend && rsi&.between?(40, 60)
153
121
  "Good swing candidate - healthy uptrend, RSI in good zone"
154
122
  else
155
123
  "Moderate candidate - review individual factors"
@@ -159,54 +127,120 @@ module DhanHQ
159
127
  def convert_to_ohlc(historical_data)
160
128
  return [] unless historical_data.is_a?(Hash)
161
129
 
162
- # Navigate to the actual data: result -> result -> data
130
+ data = extract_data_payload(historical_data)
131
+ return [] unless data
132
+
133
+ return ohlc_from_hash(data) if data.is_a?(Hash)
134
+ return ohlc_from_array(data) if data.is_a?(Array)
135
+
136
+ []
137
+ end
138
+
139
+ def format_score_breakdown(details)
140
+ "Trend=#{details[:trend]}, RSI=#{details[:rsi]}, MACD=#{details[:macd]}, " \
141
+ "Structure=#{details[:structure]}, Patterns=#{details[:patterns]}"
142
+ end
143
+
144
+ def empty_score_details
145
+ { trend: 0, rsi: 0, macd: 0, structure: 0, patterns: 0 }
146
+ end
147
+
148
+ def trend_points(analysis)
149
+ trend = analysis[:trend]
150
+ return 0 unless trend && trend[:trend] == :uptrend
151
+
152
+ (trend[:strength] || 0).clamp(0, 30)
153
+ end
154
+
155
+ def rsi_points(analysis)
156
+ rsi = analysis[:indicators]&.dig(:rsi)
157
+ return 0 unless rsi
158
+ return 20 if rsi.between?(40, 60)
159
+ return 10 if rsi.between?(30, 70)
160
+
161
+ 0
162
+ end
163
+
164
+ def macd_points(analysis)
165
+ macd = analysis[:indicators]&.dig(:macd)
166
+ signal = analysis[:indicators]&.dig(:macd_signal)
167
+ return 0 unless macd && signal
168
+
169
+ return 20 if macd > signal
170
+ return 10 if macd > signal * 0.9
171
+
172
+ 0
173
+ end
174
+
175
+ def structure_points(analysis)
176
+ structure_break = analysis[:structure_break]
177
+ return 0 unless structure_break && structure_break[:broken]
178
+ return 0 unless structure_break[:direction] == :bullish_break
179
+
180
+ 15
181
+ end
182
+
183
+ def pattern_points(analysis)
184
+ patterns = analysis[:patterns]&.dig(:candlestick) || []
185
+ bullish_patterns = patterns.count { |pattern| pattern.is_a?(Hash) && pattern[:type].to_s.include?("bullish") }
186
+ [bullish_patterns * 5, 15].min
187
+ end
188
+
189
+ def extract_data_payload(historical_data)
163
190
  outer_result = historical_data[:result] || historical_data["result"]
164
- return [] unless outer_result.is_a?(Hash)
191
+ return nil unless outer_result.is_a?(Hash)
165
192
 
166
- data = outer_result[:data] || outer_result["data"]
167
- return [] unless data
193
+ outer_result[:data] || outer_result["data"]
194
+ end
168
195
 
169
- # Handle DhanHQ format: {open: [...], high: [...], low: [...], close: [...], volume: [...]}
170
- if data.is_a?(Hash)
171
- opens = data[:open] || data["open"] || []
172
- highs = data[:high] || data["high"] || []
173
- lows = data[:low] || data["low"] || []
174
- closes = data[:close] || data["close"] || []
175
- volumes = data[:volume] || data["volume"] || []
176
-
177
- return [] if closes.empty?
178
-
179
- # Convert parallel arrays to array of hashes
180
- max_length = [opens.length, highs.length, lows.length, closes.length].max
181
- ohlc_data = []
182
-
183
- (0...max_length).each do |i|
184
- ohlc_data << {
185
- open: opens[i],
186
- high: highs[i],
187
- low: lows[i],
188
- close: closes[i],
189
- volume: volumes[i] || 0
190
- }
191
- end
196
+ def ohlc_from_hash(data)
197
+ series = extract_series(data)
198
+ return [] if series[:closes].empty?
192
199
 
193
- return ohlc_data
194
- end
200
+ max_length = series_lengths(series).max
201
+ build_ohlc_rows(series, max_length)
202
+ end
195
203
 
196
- # Handle array format: [{open, high, low, close, volume}, ...]
197
- if data.is_a?(Array)
198
- return data.map do |bar|
199
- {
200
- open: bar["open"] || bar[:open],
201
- high: bar["high"] || bar[:high],
202
- low: bar["low"] || bar[:low],
203
- close: bar["close"] || bar[:close],
204
- volume: bar["volume"] || bar[:volume]
205
- }
206
- end.compact
204
+ def extract_series(data)
205
+ {
206
+ opens: data[:open] || data["open"] || [],
207
+ highs: data[:high] || data["high"] || [],
208
+ lows: data[:low] || data["low"] || [],
209
+ closes: data[:close] || data["close"] || [],
210
+ volumes: data[:volume] || data["volume"] || []
211
+ }
212
+ end
213
+
214
+ def series_lengths(series)
215
+ [series[:opens].length, series[:highs].length, series[:lows].length, series[:closes].length]
216
+ end
217
+
218
+ def build_ohlc_rows(series, max_length)
219
+ (0...max_length).map do |index|
220
+ {
221
+ open: series[:opens][index],
222
+ high: series[:highs][index],
223
+ low: series[:lows][index],
224
+ close: series[:closes][index],
225
+ volume: series[:volumes][index] || 0
226
+ }
207
227
  end
228
+ end
208
229
 
209
- []
230
+ def ohlc_from_array(data)
231
+ data.filter_map { |bar| normalize_bar(bar) }
232
+ end
233
+
234
+ def normalize_bar(bar)
235
+ return nil unless bar.is_a?(Hash)
236
+
237
+ {
238
+ open: bar["open"] || bar[:open],
239
+ high: bar["high"] || bar[:high],
240
+ low: bar["low"] || bar[:low],
241
+ close: bar["close"] || bar[:close],
242
+ volume: bar["volume"] || bar[:volume]
243
+ }
210
244
  end
211
245
  end
212
246
  end
@@ -21,7 +21,7 @@ module DhanHQ
21
21
  "type" => "number",
22
22
  "minimum" => 0,
23
23
  "maximum" => 1,
24
- "description" => "Confidence in this decision"
24
+ "description" => "Confidence in this decision (0.0 to 1.0, where 1.0 is 100% confident)"
25
25
  },
26
26
  "parameters" => {
27
27
  "type" => "object",
@@ -47,7 +47,7 @@ module DhanHQ
47
47
  "type" => "number",
48
48
  "minimum" => 0,
49
49
  "maximum" => 1,
50
- "description" => "Confidence in this decision"
50
+ "description" => "Confidence in this decision (0.0 to 1.0, where 1.0 is 100% confident)"
51
51
  },
52
52
  "parameters" => {
53
53
  "type" => "object",
@@ -3,10 +3,6 @@
3
3
  require_relative "base_service"
4
4
  require_relative "../../dhanhq_tools"
5
5
 
6
- # Keep backward compatibility - these are still used by the service
7
- DhanHQDataTools = DhanHQDataTools unless defined?(DhanHQDataTools)
8
- DhanHQTradingTools = DhanHQTradingTools unless defined?(DhanHQTradingTools)
9
-
10
6
  module DhanHQ
11
7
  module Services
12
8
  # Service for executing data retrieval actions
@@ -85,9 +81,11 @@ module DhanHQ
85
81
  end
86
82
 
87
83
  def execute_expired_options_data(params)
88
- return error_response("get_expired_options_data",
89
- "Either symbol or security_id, and expiry_date are required",
90
- params) if missing_expired_options_params?(params)
84
+ if missing_expired_options_params?(params)
85
+ return error_response("get_expired_options_data",
86
+ "Either symbol or security_id, and expiry_date are required",
87
+ params)
88
+ end
91
89
 
92
90
  DhanHQDataTools.get_expired_options_data(
93
91
  symbol: params["symbol"],
@@ -3,9 +3,6 @@
3
3
  require_relative "base_service"
4
4
  require_relative "../../dhanhq_tools"
5
5
 
6
- # Keep backward compatibility
7
- DhanHQTradingTools = DhanHQTradingTools unless defined?(DhanHQTradingTools)
8
-
9
6
  module DhanHQ
10
7
  module Services
11
8
  # Service for executing trading order actions
@@ -78,104 +78,187 @@ puts
78
78
  # Initialize agent
79
79
  agent = DhanHQ::Agent.new
80
80
 
81
- # Define tools as callable functions
82
- tools = {
83
- "swing_scan" => lambda do |args|
84
- symbol = args["symbol"] || args[:symbol]
85
- exchange_segment = args["exchange_segment"] || args[:exchange_segment] || "NSE_EQ"
86
- min_score = args["min_score"] || args[:min_score] || 40
87
- verbose = args["verbose"] || args[:verbose] || false
88
-
89
- unless symbol
90
- return { error: "symbol is required for swing_scan" }
91
- end
92
-
93
- # CRITICAL: Only allow equity stocks, not indices
94
- if %w[NIFTY SENSEX BANKNIFTY].include?(symbol.to_s.upcase)
95
- return { error: "#{symbol} is an index, not a stock. Use options_scan for indices." }
96
- end
97
-
98
- begin
99
- candidates = agent.swing_scanner.scan_symbols(
100
- [symbol.to_s],
101
- exchange_segment: exchange_segment.to_s,
102
- min_score: min_score.to_i,
103
- verbose: verbose
104
- )
105
-
106
- {
107
- symbol: symbol,
108
- exchange_segment: exchange_segment,
109
- candidates_found: candidates.length,
110
- candidates: candidates.map do |c|
111
- {
112
- symbol: c[:symbol],
113
- score: c[:score],
114
- trend: c[:analysis][:trend][:trend],
115
- recommendation: c[:recommendation]
116
- }
117
- end
118
- }
119
- rescue StandardError => e
120
- { error: e.message, backtrace: e.backtrace.first(3) }
121
- end
122
- end,
81
+ # Define tools using structured Tool classes for better type safety and LLM understanding
82
+ # This provides explicit schemas with descriptions, types, and enums
83
+
84
+ # Swing Scan Tool - Structured definition
85
+ swing_scan_tool = Ollama::Tool.new(
86
+ type: "function",
87
+ function: Ollama::Tool::Function.new(
88
+ name: "swing_scan",
89
+ description: "Scans for swing trading opportunities in EQUITY STOCKS only. " \
90
+ "Use for stocks like RELIANCE, TCS, INFY, HDFC. " \
91
+ "Do NOT use for indices (NIFTY, SENSEX, BANKNIFTY).",
92
+ parameters: Ollama::Tool::Function::Parameters.new(
93
+ type: "object",
94
+ properties: {
95
+ symbol: Ollama::Tool::Function::Parameters::Property.new(
96
+ type: "string",
97
+ description: "Stock symbol to scan (e.g., RELIANCE, TCS, INFY). Must be a stock, not an index."
98
+ ),
99
+ exchange_segment: Ollama::Tool::Function::Parameters::Property.new(
100
+ type: "string",
101
+ description: "Exchange segment (default: NSE_EQ)",
102
+ enum: %w[NSE_EQ BSE_EQ]
103
+ ),
104
+ min_score: Ollama::Tool::Function::Parameters::Property.new(
105
+ type: "integer",
106
+ description: "Minimum score threshold (default: 40)"
107
+ ),
108
+ verbose: Ollama::Tool::Function::Parameters::Property.new(
109
+ type: "boolean",
110
+ description: "Verbose output (default: false)"
111
+ )
112
+ },
113
+ required: %w[symbol]
114
+ )
115
+ )
116
+ )
123
117
 
124
- "options_scan" => lambda do |args|
125
- symbol = args["symbol"] || args[:symbol]
126
- exchange_segment = args["exchange_segment"] || args[:exchange_segment] || "IDX_I"
127
- min_score = args["min_score"] || args[:min_score] || 40
128
- verbose = args["verbose"] || args[:verbose] || false
118
+ # Options Scan Tool - Structured definition
119
+ options_scan_tool = Ollama::Tool.new(
120
+ type: "function",
121
+ function: Ollama::Tool::Function.new(
122
+ name: "options_scan",
123
+ description: "Scans for intraday options buying opportunities in INDICES only. " \
124
+ "Use for NIFTY, SENSEX, BANKNIFTY. Do NOT use for stocks.",
125
+ parameters: Ollama::Tool::Function::Parameters.new(
126
+ type: "object",
127
+ properties: {
128
+ symbol: Ollama::Tool::Function::Parameters::Property.new(
129
+ type: "string",
130
+ description: "Index symbol (NIFTY, SENSEX, or BANKNIFTY). Must be an index, not a stock.",
131
+ enum: %w[NIFTY SENSEX BANKNIFTY]
132
+ ),
133
+ exchange_segment: Ollama::Tool::Function::Parameters::Property.new(
134
+ type: "string",
135
+ description: "Exchange segment (default: IDX_I)",
136
+ enum: %w[IDX_I]
137
+ ),
138
+ min_score: Ollama::Tool::Function::Parameters::Property.new(
139
+ type: "integer",
140
+ description: "Minimum score threshold (default: 40)"
141
+ ),
142
+ verbose: Ollama::Tool::Function::Parameters::Property.new(
143
+ type: "boolean",
144
+ description: "Verbose output (default: false)"
145
+ )
146
+ },
147
+ required: %w[symbol]
148
+ )
149
+ )
150
+ )
129
151
 
130
- unless symbol
131
- return { error: "symbol is required for options_scan" }
132
- end
152
+ # Technical Analysis Tool - Structured definition
153
+ technical_analysis_tool = Ollama::Tool.new(
154
+ type: "function",
155
+ function: Ollama::Tool::Function.new(
156
+ name: "technical_analysis",
157
+ description: "Performs full technical analysis including trend, indicators, and patterns. " \
158
+ "Can be used for both stocks and indices.",
159
+ parameters: Ollama::Tool::Function::Parameters.new(
160
+ type: "object",
161
+ properties: {
162
+ symbol: Ollama::Tool::Function::Parameters::Property.new(
163
+ type: "string",
164
+ description: "Symbol to analyze (stock or index)"
165
+ ),
166
+ exchange_segment: Ollama::Tool::Function::Parameters::Property.new(
167
+ type: "string",
168
+ description: "Exchange segment (default: NSE_EQ)",
169
+ enum: %w[NSE_EQ BSE_EQ NSE_FNO BSE_FNO IDX_I]
170
+ )
171
+ },
172
+ required: %w[symbol]
173
+ )
174
+ )
175
+ )
133
176
 
134
- # CRITICAL: Only allow indices, not stocks
135
- unless %w[NIFTY SENSEX BANKNIFTY].include?(symbol.to_s.upcase)
136
- return { error: "#{symbol} is not an index. Use swing_scan for stocks. Options are only available for indices (NIFTY, SENSEX, BANKNIFTY)." }
137
- end
177
+ # Define tools with structured Tool classes and callables
178
+ tools = {
179
+ "swing_scan" => {
180
+ tool: swing_scan_tool,
181
+ callable: lambda do |symbol:, exchange_segment: "NSE_EQ", min_score: 40, verbose: false|
182
+ # CRITICAL: Only allow equity stocks, not indices
183
+ if %w[NIFTY SENSEX BANKNIFTY].include?(symbol.to_s.upcase)
184
+ return { error: "#{symbol} is an index, not a stock. Use options_scan for indices." }
185
+ end
138
186
 
139
- begin
140
- options_setups = agent.options_scanner.scan_for_options_setups(
141
- symbol.to_s,
142
- exchange_segment: exchange_segment.to_s,
143
- min_score: min_score.to_i,
144
- verbose: verbose
145
- )
187
+ begin
188
+ candidates = agent.swing_scanner.scan_symbols(
189
+ [symbol.to_s],
190
+ exchange_segment: exchange_segment.to_s,
191
+ min_score: min_score.to_i,
192
+ verbose: verbose
193
+ )
146
194
 
147
- if options_setups[:error]
148
- { error: options_setups[:error] }
149
- else
150
195
  {
151
196
  symbol: symbol,
152
197
  exchange_segment: exchange_segment,
153
- setups_found: options_setups[:setups]&.length || 0,
154
- setups: options_setups[:setups]&.map do |s|
198
+ candidates_found: candidates.length,
199
+ candidates: candidates.map do |c|
155
200
  {
156
- type: s[:type],
157
- strike: s[:strike],
158
- score: s[:score],
159
- iv: s[:iv],
160
- recommendation: s[:recommendation]
201
+ symbol: c[:symbol],
202
+ score: c[:score],
203
+ trend: c[:analysis][:trend][:trend],
204
+ recommendation: c[:recommendation]
161
205
  }
162
- end || []
206
+ end
163
207
  }
208
+ rescue StandardError => e
209
+ { error: e.message, backtrace: e.backtrace.first(3) }
164
210
  end
165
- rescue StandardError => e
166
- { error: e.message, backtrace: e.backtrace.first(3) }
167
211
  end
168
- end,
169
-
170
- "technical_analysis" => lambda do |args|
171
- symbol = args["symbol"] || args[:symbol]
172
- exchange_segment = args["exchange_segment"] || args[:exchange_segment] || "NSE_EQ"
212
+ },
213
+
214
+ "options_scan" => {
215
+ tool: options_scan_tool,
216
+ callable: lambda do |symbol:, exchange_segment: "IDX_I", min_score: 40, verbose: false|
217
+ # CRITICAL: Only allow indices, not stocks
218
+ unless %w[NIFTY SENSEX BANKNIFTY].include?(symbol.to_s.upcase)
219
+ error_message = "#{symbol} is not an index. Use swing_scan for stocks. " \
220
+ "Options are only available for indices (NIFTY, SENSEX, BANKNIFTY)."
221
+ return { error: error_message }
222
+ end
173
223
 
174
- unless symbol
175
- return { error: "symbol is required for technical_analysis" }
224
+ begin
225
+ options_setups = agent.options_scanner.scan_for_options_setups(
226
+ symbol.to_s,
227
+ exchange_segment: exchange_segment.to_s,
228
+ min_score: min_score.to_i,
229
+ verbose: verbose
230
+ )
231
+
232
+ if options_setups[:error]
233
+ { error: options_setups[:error] }
234
+ else
235
+ underlying_price = options_setups.dig(:underlying_analysis, :current_price)
236
+ {
237
+ symbol: symbol,
238
+ exchange_segment: exchange_segment,
239
+ underlying_price: underlying_price,
240
+ setups_found: options_setups[:setups]&.length || 0,
241
+ setups: options_setups[:setups]&.map do |s|
242
+ {
243
+ type: s[:type],
244
+ strike: s[:strike],
245
+ score: s[:score],
246
+ iv: s[:iv],
247
+ ltp: s[:ltp],
248
+ recommendation: s[:recommendation]
249
+ }
250
+ end || []
251
+ }
252
+ end
253
+ rescue StandardError => e
254
+ { error: e.message, backtrace: e.backtrace.first(3) }
255
+ end
176
256
  end
257
+ },
177
258
 
178
- begin
259
+ "technical_analysis" => {
260
+ tool: technical_analysis_tool,
261
+ callable: lambda do |symbol:, exchange_segment: "NSE_EQ"|
179
262
  analysis_result = agent.analysis_agent.analyze_symbol(
180
263
  symbol: symbol.to_s,
181
264
  exchange_segment: exchange_segment.to_s
@@ -200,7 +283,7 @@ tools = {
200
283
  rescue StandardError => e
201
284
  { error: e.message, backtrace: e.backtrace.first(3) }
202
285
  end
203
- end
286
+ }
204
287
  }
205
288
 
206
289
  # Get user query and market context
@@ -254,6 +337,51 @@ executor = Ollama::Agent::Executor.new(
254
337
  max_steps: 20
255
338
  )
256
339
 
340
+ def tool_messages(messages)
341
+ messages.select { |message| message[:role] == "tool" }
342
+ end
343
+
344
+ def print_tool_results(messages)
345
+ puts "Tool Results:"
346
+ tool_messages(messages).each do |message|
347
+ print_tool_message(message)
348
+ end
349
+ end
350
+
351
+ def print_tool_message(message)
352
+ tool_name = message[:name] || "unknown_tool"
353
+ puts "- #{tool_name}"
354
+ puts format_tool_content(message[:content])
355
+ end
356
+
357
+ def format_tool_content(content)
358
+ parsed = parse_tool_content(content)
359
+ return parsed if parsed.is_a?(String)
360
+
361
+ JSON.pretty_generate(parsed)
362
+ end
363
+
364
+ def parse_tool_content(content)
365
+ return content unless content.is_a?(String)
366
+
367
+ JSON.parse(content)
368
+ rescue JSON::ParserError
369
+ content
370
+ end
371
+
372
+ def print_llm_summary(result)
373
+ return unless ENV["SHOW_LLM_SUMMARY"] == "true"
374
+
375
+ puts
376
+ puts "LLM Summary (unverified):"
377
+ puts result
378
+ end
379
+
380
+ def print_hallucination_warning
381
+ puts "No tool results were produced."
382
+ puts "LLM output suppressed to avoid hallucinated data."
383
+ end
384
+
257
385
  # Run the agentic loop
258
386
  begin
259
387
  puts "🔄 Starting agentic tool-calling loop..."
@@ -268,7 +396,12 @@ begin
268
396
  puts "=" * 60
269
397
  puts "Agentic Analysis Complete"
270
398
  puts "=" * 60
271
- puts result
399
+ if tool_messages(executor.messages).empty?
400
+ print_hallucination_warning
401
+ else
402
+ print_tool_results(executor.messages)
403
+ print_llm_summary(result)
404
+ end
272
405
  rescue Ollama::Error => e
273
406
  puts "❌ Error: #{e.message}"
274
407
  puts e.backtrace.first(5).join("\n") if e.backtrace