ollama-client 0.2.5 → 0.2.6

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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/README.md +138 -76
  4. data/docs/EXAMPLE_REORGANIZATION.md +412 -0
  5. data/docs/GETTING_STARTED.md +361 -0
  6. data/docs/INTEGRATION_TESTING.md +170 -0
  7. data/docs/NEXT_STEPS_SUMMARY.md +114 -0
  8. data/docs/PERSONAS.md +383 -0
  9. data/docs/QUICK_START.md +195 -0
  10. data/docs/TESTING.md +392 -170
  11. data/docs/TEST_CHECKLIST.md +450 -0
  12. data/examples/README.md +51 -66
  13. data/examples/basic_chat.rb +33 -0
  14. data/examples/basic_generate.rb +29 -0
  15. data/examples/tool_calling_parsing.rb +59 -0
  16. data/exe/ollama-client +128 -1
  17. data/lib/ollama/agent/planner.rb +7 -2
  18. data/lib/ollama/chat_session.rb +101 -0
  19. data/lib/ollama/client.rb +41 -35
  20. data/lib/ollama/config.rb +4 -1
  21. data/lib/ollama/document_loader.rb +1 -1
  22. data/lib/ollama/embeddings.rb +41 -26
  23. data/lib/ollama/errors.rb +1 -0
  24. data/lib/ollama/personas.rb +287 -0
  25. data/lib/ollama/version.rb +1 -1
  26. data/lib/ollama_client.rb +7 -0
  27. metadata +14 -48
  28. data/examples/advanced_complex_schemas.rb +0 -366
  29. data/examples/advanced_edge_cases.rb +0 -241
  30. data/examples/advanced_error_handling.rb +0 -200
  31. data/examples/advanced_multi_step_agent.rb +0 -341
  32. data/examples/advanced_performance_testing.rb +0 -186
  33. data/examples/chat_console.rb +0 -143
  34. data/examples/complete_workflow.rb +0 -245
  35. data/examples/dhan_console.rb +0 -843
  36. data/examples/dhanhq/README.md +0 -236
  37. data/examples/dhanhq/agents/base_agent.rb +0 -74
  38. data/examples/dhanhq/agents/data_agent.rb +0 -66
  39. data/examples/dhanhq/agents/orchestrator_agent.rb +0 -120
  40. data/examples/dhanhq/agents/technical_analysis_agent.rb +0 -252
  41. data/examples/dhanhq/agents/trading_agent.rb +0 -81
  42. data/examples/dhanhq/analysis/market_structure.rb +0 -138
  43. data/examples/dhanhq/analysis/pattern_recognizer.rb +0 -192
  44. data/examples/dhanhq/analysis/trend_analyzer.rb +0 -88
  45. data/examples/dhanhq/builders/market_context_builder.rb +0 -67
  46. data/examples/dhanhq/dhanhq_agent.rb +0 -829
  47. data/examples/dhanhq/indicators/technical_indicators.rb +0 -158
  48. data/examples/dhanhq/scanners/intraday_options_scanner.rb +0 -492
  49. data/examples/dhanhq/scanners/swing_scanner.rb +0 -247
  50. data/examples/dhanhq/schemas/agent_schemas.rb +0 -61
  51. data/examples/dhanhq/services/base_service.rb +0 -46
  52. data/examples/dhanhq/services/data_service.rb +0 -118
  53. data/examples/dhanhq/services/trading_service.rb +0 -59
  54. data/examples/dhanhq/technical_analysis_agentic_runner.rb +0 -411
  55. data/examples/dhanhq/technical_analysis_runner.rb +0 -420
  56. data/examples/dhanhq/test_tool_calling.rb +0 -538
  57. data/examples/dhanhq/test_tool_calling_verbose.rb +0 -251
  58. data/examples/dhanhq/utils/instrument_helper.rb +0 -32
  59. data/examples/dhanhq/utils/parameter_cleaner.rb +0 -28
  60. data/examples/dhanhq/utils/parameter_normalizer.rb +0 -45
  61. data/examples/dhanhq/utils/rate_limiter.rb +0 -23
  62. data/examples/dhanhq/utils/trading_parameter_normalizer.rb +0 -72
  63. data/examples/dhanhq_agent.rb +0 -964
  64. data/examples/dhanhq_tools.rb +0 -1663
  65. data/examples/multi_step_agent_with_external_data.rb +0 -368
  66. data/examples/structured_outputs_chat.rb +0 -72
  67. data/examples/structured_tools.rb +0 -89
  68. data/examples/test_dhanhq_tool_calling.rb +0 -375
  69. data/examples/test_tool_calling.rb +0 -160
  70. data/examples/tool_calling_direct.rb +0 -124
  71. data/examples/tool_calling_pattern.rb +0 -269
  72. data/exe/dhan_console +0 -4
@@ -1,251 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- # DhanHQ Tool Calling Test - Verbose Version
5
- # Shows the complete tool calling flow: LLM decides → Executor executes
6
-
7
- require_relative "../../lib/ollama_client"
8
- require_relative "../dhanhq_tools"
9
-
10
- puts "\n=== DHANHQ TOOL CALLING TEST (VERBOSE) ===\n"
11
- puts "This demonstrates REAL tool calling:\n"
12
- puts " 1. LLM receives user query + tool definitions"
13
- puts " 2. LLM DECIDES which tools to call (not your code!)"
14
- puts " 3. LLM returns tool_calls in response"
15
- puts " 4. Executor detects tool_calls and executes callables"
16
- puts " 5. Tool results fed back to LLM"
17
- puts " 6. LLM generates final answer\n"
18
-
19
- # Configure DhanHQ
20
- begin
21
- DhanHQ.configure_with_env
22
- puts "✅ DhanHQ configured\n"
23
- rescue StandardError => e
24
- puts "⚠️ DhanHQ configuration error: #{e.message}\n"
25
- end
26
-
27
- # Create client
28
- config = Ollama::Config.new
29
- config.model = ENV.fetch("OLLAMA_MODEL", "llama3.1:8b")
30
- config.temperature = 0.2
31
- config.timeout = 60
32
- client = Ollama::Client.new(config: config)
33
-
34
- # Define tools
35
- market_quote_tool = Ollama::Tool.new(
36
- type: "function",
37
- function: Ollama::Tool::Function.new(
38
- name: "get_market_quote",
39
- description: "Get market quote for a symbol",
40
- parameters: Ollama::Tool::Function::Parameters.new(
41
- type: "object",
42
- properties: {
43
- symbol: Ollama::Tool::Function::Parameters::Property.new(
44
- type: "string",
45
- description: "Stock symbol"
46
- ),
47
- exchange_segment: Ollama::Tool::Function::Parameters::Property.new(
48
- type: "string",
49
- description: "Exchange segment",
50
- enum: %w[NSE_EQ BSE_EQ IDX_I]
51
- )
52
- },
53
- required: %w[symbol exchange_segment]
54
- )
55
- )
56
- )
57
-
58
- live_ltp_tool = Ollama::Tool.new(
59
- type: "function",
60
- function: Ollama::Tool::Function.new(
61
- name: "get_live_ltp",
62
- description: "Get live last traded price",
63
- parameters: Ollama::Tool::Function::Parameters.new(
64
- type: "object",
65
- properties: {
66
- symbol: Ollama::Tool::Function::Parameters::Property.new(
67
- type: "string",
68
- description: "Stock symbol"
69
- ),
70
- exchange_segment: Ollama::Tool::Function::Parameters::Property.new(
71
- type: "string",
72
- description: "Exchange segment",
73
- enum: %w[NSE_EQ BSE_EQ IDX_I]
74
- )
75
- },
76
- required: %w[symbol exchange_segment]
77
- )
78
- )
79
- )
80
-
81
- option_chain_tool = Ollama::Tool.new(
82
- type: "function",
83
- function: Ollama::Tool::Function.new(
84
- name: "get_option_chain",
85
- description: "Get option chain for an index (NIFTY, SENSEX, BANKNIFTY). " \
86
- "Returns available expiries and option chain data with strikes, Greeks, OI, and IV.",
87
- parameters: Ollama::Tool::Function::Parameters.new(
88
- type: "object",
89
- properties: {
90
- symbol: Ollama::Tool::Function::Parameters::Property.new(
91
- type: "string",
92
- description: "Index symbol (NIFTY, SENSEX, or BANKNIFTY)",
93
- enum: %w[NIFTY SENSEX BANKNIFTY]
94
- ),
95
- exchange_segment: Ollama::Tool::Function::Parameters::Property.new(
96
- type: "string",
97
- description: "Exchange segment (must be IDX_I for indices)",
98
- enum: %w[IDX_I]
99
- ),
100
- expiry: Ollama::Tool::Function::Parameters::Property.new(
101
- type: "string",
102
- description: "Optional expiry date (YYYY-MM-DD format). If not provided, returns available expiries list."
103
- )
104
- },
105
- required: %w[symbol exchange_segment]
106
- )
107
- )
108
- )
109
-
110
- # Define callables (these are just implementations - LLM decides when to call them)
111
- tools = {
112
- "get_option_chain" => {
113
- tool: option_chain_tool,
114
- callable: lambda do |symbol:, exchange_segment:, expiry: nil|
115
- # Normalize empty string to nil (LLM might pass "" when expiry is optional)
116
- expiry = nil if expiry.is_a?(String) && expiry.empty?
117
- puts "\n [TOOL EXECUTION] get_option_chain called by Executor"
118
- puts " Args: symbol=#{symbol}, exchange_segment=#{exchange_segment}, expiry=#{expiry || 'nil'}"
119
- puts " Note: This is called AFTER LLM decided to use this tool!"
120
- result = DhanHQDataTools.get_option_chain(
121
- symbol: symbol.to_s,
122
- exchange_segment: exchange_segment.to_s,
123
- expiry: expiry
124
- )
125
- if result[:error]
126
- { error: result[:error] }
127
- elsif result[:result] && result[:result][:expiries]
128
- # Return expiry list
129
- {
130
- symbol: symbol,
131
- expiries_available: result[:result][:expiries],
132
- count: result[:result][:count]
133
- }
134
- elsif result[:result] && result[:result][:chain]
135
- # Return option chain data
136
- chain = result[:result][:chain]
137
- strikes = chain.is_a?(Hash) ? chain.keys.sort_by(&:to_f) : []
138
- {
139
- symbol: symbol,
140
- expiry: result[:result][:expiry],
141
- underlying_price: result[:result][:underlying_last_price],
142
- strikes_count: strikes.length,
143
- sample_strikes: strikes.first(5)
144
- }
145
- else
146
- { error: "Unexpected response format" }
147
- end
148
- rescue StandardError => e
149
- { error: e.message }
150
- end
151
- },
152
-
153
- "get_market_quote" => {
154
- tool: market_quote_tool,
155
- callable: lambda do |symbol:, exchange_segment:|
156
- puts "\n [TOOL EXECUTION] get_market_quote called by Executor"
157
- puts " Args: symbol=#{symbol}, exchange_segment=#{exchange_segment}"
158
- puts " Note: This is called AFTER LLM decided to use this tool!"
159
- result = DhanHQDataTools.get_market_quote(
160
- symbol: symbol.to_s,
161
- exchange_segment: exchange_segment.to_s
162
- )
163
- if result[:error]
164
- { error: result[:error] }
165
- else
166
- quote = result[:result][:quote]
167
- {
168
- symbol: symbol,
169
- last_price: quote[:last_price],
170
- volume: quote[:volume],
171
- ohlc: quote[:ohlc]
172
- }
173
- end
174
- end
175
- },
176
-
177
- "get_live_ltp" => {
178
- tool: live_ltp_tool,
179
- callable: lambda do |symbol:, exchange_segment:|
180
- puts "\n [TOOL EXECUTION] get_live_ltp called by Executor"
181
- puts " Args: symbol=#{symbol}, exchange_segment=#{exchange_segment}"
182
- puts " Note: This is called AFTER LLM decided to use this tool!"
183
- sleep(1.2) # Rate limiting
184
- result = DhanHQDataTools.get_live_ltp(
185
- symbol: symbol.to_s,
186
- exchange_segment: exchange_segment.to_s
187
- )
188
- if result[:error]
189
- { error: result[:error] }
190
- else
191
- {
192
- symbol: symbol,
193
- ltp: result[:result][:ltp]
194
- }
195
- end
196
- end
197
- }
198
- }
199
-
200
- puts "--- Step 1: Show what tools are available to LLM ---"
201
- puts "Tools defined:"
202
- puts " - get_market_quote: Get market quote"
203
- puts " - get_live_ltp: Get live price"
204
- puts " - get_option_chain: Get option chain for indices (NIFTY, SENSEX, BANKNIFTY)"
205
- puts "\nThese tool DEFINITIONS are sent to LLM (not executed yet)\n"
206
-
207
- puts "--- Step 2: LLM receives query and DECIDES which tools to call ---"
208
- puts "User query: 'Get RELIANCE quote, NIFTY price, and SENSEX option chain'\n"
209
- puts "LLM will analyze this and decide to call:"
210
- puts " 1. get_market_quote(RELIANCE, NSE_EQ)"
211
- puts " 2. get_live_ltp(NIFTY, IDX_I)"
212
- puts " 3. get_option_chain(SENSEX, IDX_I)"
213
- puts "\nThis decision is made by the LLM, not by your code!\n"
214
-
215
- puts "--- Step 3: Executor sends request to LLM with tool definitions ---"
216
- puts "Sending to LLM via chat_raw() with tools parameter...\n"
217
-
218
- # Create executor
219
- executor = Ollama::Agent::Executor.new(
220
- client,
221
- tools: tools,
222
- max_steps: 10
223
- )
224
-
225
- begin
226
- result = executor.run(
227
- system: "You are a market data assistant. Use the available tools to get market data. " \
228
- "For option chains, you can get SENSEX options using get_option_chain with " \
229
- "symbol='SENSEX' and exchange_segment='IDX_I'.",
230
- user: "Get market quote for RELIANCE stock, check NIFTY's current price, and get SENSEX option chain"
231
- )
232
-
233
- puts "\n--- Step 4: Final result from LLM (after tool execution) ---"
234
- puts "=" * 60
235
- puts result
236
- puts "=" * 60
237
- rescue Ollama::Error => e
238
- puts "\n❌ Error: #{e.message}"
239
- rescue StandardError => e
240
- puts "\n❌ Unexpected error: #{e.message}"
241
- end
242
-
243
- puts "\n--- Summary ---"
244
- puts "✅ This IS real tool calling:"
245
- puts " - LLM decides which tools to call (not your code)"
246
- puts " - LLM returns tool_calls in response"
247
- puts " - Executor detects tool_calls and executes callables"
248
- puts " - Tool results fed back to LLM automatically"
249
- puts " - LLM generates final answer based on tool results"
250
- puts "\nThe callables are just implementations - the LLM decides WHEN to call them!"
251
- puts "\n=== DONE ===\n"
@@ -1,32 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module DhanHQ
4
- module Utils
5
- # Helper utilities for working with DhanHQ instruments
6
- class InstrumentHelper
7
- def self.safe_attr(instrument, attr_name)
8
- return nil unless instrument
9
-
10
- instrument.respond_to?(attr_name) ? instrument.send(attr_name) : nil
11
- rescue StandardError
12
- nil
13
- end
14
-
15
- def self.extract_value(data, keys)
16
- return nil unless data
17
-
18
- keys.each do |key|
19
- if data.is_a?(Hash)
20
- return data[key] if data.key?(key)
21
- elsif data.respond_to?(key)
22
- return data.send(key)
23
- end
24
- end
25
-
26
- return data if data.is_a?(Numeric) || data.is_a?(String)
27
-
28
- nil
29
- end
30
- end
31
- end
32
- end
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module DhanHQ
4
- module Utils
5
- # Cleans parameters from LLM responses (removes comments, instructions, etc.)
6
- class ParameterCleaner
7
- MAX_KEY_LENGTH = 50
8
-
9
- def self.clean(params)
10
- return {} unless params.is_a?(Hash)
11
-
12
- params.reject do |key, _value|
13
- key_str = key.to_s
14
- invalid_key?(key_str)
15
- end
16
- end
17
-
18
- def self.invalid_key?(key_str)
19
- key_str.start_with?(">") ||
20
- key_str.start_with?("//") ||
21
- key_str.include?("adjust") ||
22
- key_str.length > MAX_KEY_LENGTH
23
- end
24
-
25
- private_class_method :invalid_key?
26
- end
27
- end
28
- end
@@ -1,45 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
-
5
- module DhanHQ
6
- module Utils
7
- # Normalizes parameters from LLM responses (handles arrays, stringified JSON, etc.)
8
- class ParameterNormalizer
9
- STRING_FIELDS = %w[symbol exchange_segment].freeze
10
-
11
- def self.normalize(params)
12
- return {} unless params.is_a?(Hash)
13
-
14
- params.each_with_object({}) do |(key, value), normalized|
15
- normalized[key] = if STRING_FIELDS.include?(key.to_s)
16
- normalize_string_field(value)
17
- else
18
- value
19
- end
20
- end
21
- end
22
-
23
- def self.normalize_string_field(value)
24
- return value.to_s if value.nil?
25
-
26
- if value.is_a?(Array) && !value.empty?
27
- value.first.to_s
28
- elsif value.is_a?(String) && value.strip.start_with?("[") && value.strip.end_with?("]")
29
- parse_stringified_array(value)
30
- else
31
- value.to_s
32
- end
33
- end
34
-
35
- def self.parse_stringified_array(value)
36
- parsed = JSON.parse(value)
37
- parsed.is_a?(Array) && !parsed.empty? ? parsed.first.to_s : value.to_s
38
- rescue JSON::ParserError
39
- value.to_s
40
- end
41
-
42
- private_class_method :normalize_string_field, :parse_stringified_array
43
- end
44
- end
45
- end
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module DhanHQ
4
- module Utils
5
- # Rate limiter for DhanHQ MarketFeed APIs (1 request per second)
6
- class RateLimiter
7
- MARKETFEED_INTERVAL = 1.1 # 1 second + 0.1s buffer
8
-
9
- @last_marketfeed_call = nil
10
- @marketfeed_mutex = Mutex.new
11
-
12
- def self.marketfeed
13
- @marketfeed_mutex.synchronize do
14
- if @last_marketfeed_call
15
- elapsed = Time.now - @last_marketfeed_call
16
- sleep(MARKETFEED_INTERVAL - elapsed) if elapsed < MARKETFEED_INTERVAL
17
- end
18
- @last_marketfeed_call = Time.now
19
- end
20
- end
21
- end
22
- end
23
- end
@@ -1,72 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module DhanHQ
4
- module Utils
5
- # Normalizes trading parameters from LLM responses
6
- # Handles common issues like comma-separated numbers, string numbers, etc.
7
- class TradingParameterNormalizer
8
- NUMERIC_FIELDS = %w[price target_price stop_loss_price quantity trailing_jump].freeze
9
-
10
- def self.normalize(params)
11
- return {} unless params.is_a?(Hash)
12
-
13
- params.each_with_object({}) do |(key, value), normalized|
14
- key_str = key.to_s
15
- normalized[key] = if NUMERIC_FIELDS.include?(key_str)
16
- normalize_numeric(value)
17
- elsif key_str == "security_id"
18
- normalize_security_id(value)
19
- else
20
- value
21
- end
22
- end
23
- end
24
-
25
- def self.normalize_numeric(value)
26
- return nil if value.nil?
27
-
28
- # If already a number, return as-is (but validate it's reasonable)
29
- return value if value.is_a?(Numeric)
30
-
31
- # If string, clean and convert
32
- return nil unless value.is_a?(String)
33
-
34
- # Remove commas, spaces, currency symbols, and convert
35
- cleaned = value.to_s.gsub(/[,\s$₹]/, "")
36
- return nil if cleaned.empty?
37
-
38
- # Try to convert to float (for prices) or integer (for quantity)
39
- if cleaned.include?(".")
40
- cleaned.to_f
41
- else
42
- cleaned.to_i
43
- end
44
- rescue StandardError
45
- nil
46
- end
47
-
48
- # Validates if a price value seems reasonable (not obviously wrong)
49
- def self.valid_price?(price, context_hint = nil)
50
- return false if price.nil?
51
-
52
- # If price is suspiciously low (< 10), it might be wrong
53
- # But we can't be too strict since some stocks might legitimately be < 10
54
- # Check if context hint suggests a higher price
55
- if price.is_a?(Numeric) && price.positive? && price < 10 &&
56
- context_hint.is_a?(Numeric) && context_hint > price * 100
57
- return false # Likely wrong
58
- end
59
-
60
- true
61
- end
62
-
63
- def self.normalize_security_id(value)
64
- return nil if value.nil?
65
-
66
- value.to_s
67
- end
68
-
69
- private_class_method :normalize_numeric, :normalize_security_id, :valid_price?
70
- end
71
- end
72
- end