ollama-client 0.2.2 → 0.2.4

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/README.md +7 -1
  4. data/docs/CLOUD.md +29 -0
  5. data/docs/CONSOLE_IMPROVEMENTS.md +256 -0
  6. data/docs/GEM_RELEASE_GUIDE.md +794 -0
  7. data/docs/GET_RUBYGEMS_SECRET.md +151 -0
  8. data/docs/QUICK_OTP_SETUP.md +80 -0
  9. data/docs/QUICK_RELEASE.md +106 -0
  10. data/docs/README.md +43 -0
  11. data/docs/RUBYGEMS_OTP_SETUP.md +199 -0
  12. data/docs/SCHEMA_FIXES.md +147 -0
  13. data/docs/TEST_UPDATES.md +107 -0
  14. data/examples/README.md +92 -0
  15. data/examples/advanced_complex_schemas.rb +6 -3
  16. data/examples/advanced_multi_step_agent.rb +2 -1
  17. data/examples/chat_console.rb +12 -3
  18. data/examples/complete_workflow.rb +14 -4
  19. data/examples/dhan_console.rb +103 -8
  20. data/examples/dhanhq/agents/technical_analysis_agent.rb +6 -1
  21. data/examples/dhanhq/schemas/agent_schemas.rb +2 -2
  22. data/examples/dhanhq_agent.rb +23 -13
  23. data/examples/dhanhq_tools.rb +311 -246
  24. data/examples/multi_step_agent_with_external_data.rb +368 -0
  25. data/{test_dhanhq_tool_calling.rb → examples/test_dhanhq_tool_calling.rb} +99 -6
  26. data/lib/ollama/agent/executor.rb +30 -30
  27. data/lib/ollama/client.rb +73 -80
  28. data/lib/ollama/dto.rb +7 -7
  29. data/lib/ollama/options.rb +17 -9
  30. data/lib/ollama/response.rb +4 -6
  31. data/lib/ollama/tool/function/parameters.rb +1 -0
  32. data/lib/ollama/version.rb +1 -1
  33. metadata +24 -9
  34. /data/{FEATURES_ADDED.md → docs/FEATURES_ADDED.md} +0 -0
  35. /data/{HANDLERS_ANALYSIS.md → docs/HANDLERS_ANALYSIS.md} +0 -0
  36. /data/{PRODUCTION_FIXES.md → docs/PRODUCTION_FIXES.md} +0 -0
  37. /data/{TESTING.md → docs/TESTING.md} +0 -0
  38. /data/{test_tool_calling.rb → examples/test_tool_calling.rb} +0 -0
@@ -0,0 +1,368 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Use local code instead of installed gem
5
+ $LOAD_PATH.unshift(File.expand_path("lib", __dir__))
6
+ require "ollama_client"
7
+ require "json"
8
+ require "fileutils"
9
+
10
+ puts "\n=== MULTI-STEP AGENT WITH EXTERNAL DATA TEST ===\n"
11
+
12
+ # Configuration via environment variables (defaults for local testing)
13
+ BASE_URL = ENV.fetch("OLLAMA_BASE_URL", "http://localhost:11434")
14
+
15
+ def client_for(model:, temperature:, timeout:)
16
+ config = Ollama::Config.new
17
+ config.base_url = BASE_URL
18
+ config.model = model
19
+ config.temperature = temperature
20
+ config.timeout = timeout
21
+ config.retries = 2
22
+ Ollama::Client.new(config: config)
23
+ end
24
+
25
+ # ------------------------------------------------------------
26
+ # PLANNER SETUP (STATELESS)
27
+ # ------------------------------------------------------------
28
+
29
+ planner_client = client_for(
30
+ model: ENV.fetch("OLLAMA_MODEL", "llama3.1:8b"),
31
+ temperature: 0,
32
+ timeout: 40
33
+ )
34
+
35
+ planner = Ollama::Agent::Planner.new(planner_client)
36
+
37
+ decision_schema = {
38
+ "type" => "object",
39
+ "required" => %w[action reason],
40
+ "properties" => {
41
+ "action" => {
42
+ "type" => "string",
43
+ "enum" => %w[read_file read_reference analyze_with_reference finish]
44
+ },
45
+ "reason" => { "type" => "string" }
46
+ }
47
+ }
48
+
49
+ # ------------------------------------------------------------
50
+ # EXECUTOR SETUP (STATEFUL)
51
+ # ------------------------------------------------------------
52
+
53
+ test_dir = File.expand_path("test_files", __dir__)
54
+
55
+ tools = {
56
+ "read_file" => lambda do |path:|
57
+ full_path = File.expand_path(path, test_dir)
58
+ return { error: "Path must be within test_files directory" } unless full_path.start_with?(test_dir)
59
+
60
+ return { error: "File not found: #{path}" } unless File.exist?(full_path)
61
+
62
+ {
63
+ path: path,
64
+ content: File.read(full_path),
65
+ size: File.size(full_path)
66
+ }
67
+ end,
68
+
69
+ "read_reference" => lambda do |reference_name:|
70
+ # Read external reference files
71
+ reference_files = {
72
+ "style_guide" => "ruby_style_guide.txt",
73
+ "checklist" => "code_review_checklist.txt"
74
+ }
75
+
76
+ filename = reference_files[reference_name.to_s]
77
+ unless filename
78
+ return { error: "Unknown reference: #{reference_name}. Available: #{reference_files.keys.join(", ")}" }
79
+ end
80
+
81
+ full_path = File.expand_path(filename, test_dir)
82
+ return { error: "Reference file not found: #{filename}" } unless File.exist?(full_path)
83
+
84
+ {
85
+ reference_name: reference_name,
86
+ content: File.read(full_path),
87
+ size: File.size(full_path)
88
+ }
89
+ end
90
+ }
91
+
92
+ executor_client = client_for(
93
+ model: ENV.fetch("OLLAMA_MODEL", "llama3.1:8b"),
94
+ temperature: 0.2,
95
+ timeout: 60
96
+ )
97
+
98
+ executor = Ollama::Agent::Executor.new(
99
+ executor_client,
100
+ tools: tools
101
+ )
102
+
103
+ def step_limit_reached?(step, max_steps)
104
+ return false if step <= max_steps
105
+
106
+ puts "\n⚠️ Maximum steps reached (#{max_steps})"
107
+ true
108
+ end
109
+
110
+ def build_status(context, step)
111
+ {
112
+ file_read: context[:file_read] ? "YES" : "NO",
113
+ references_read: context[:references_read].empty? ? "NONE" : context[:references_read].join(", "),
114
+ has_analysis: context[:analysis] ? "YES" : "NO",
115
+ step_number: step
116
+ }
117
+ end
118
+
119
+ def missing_references(references_read)
120
+ %w[style_guide checklist] - references_read
121
+ end
122
+
123
+ def early_plan_for(context)
124
+ return { "action" => "finish", "reason" => "Analysis complete" } if context[:analysis]
125
+
126
+ if context[:file_read] && context[:references_read].length >= 2
127
+ return { "action" => "analyze_with_reference",
128
+ "reason" => "File and all references read, ready to analyze" }
129
+ end
130
+
131
+ return nil unless context[:file_read]
132
+
133
+ missing_refs = missing_references(context[:references_read])
134
+ return nil if missing_refs.empty?
135
+
136
+ { "action" => "read_reference",
137
+ "reason" => "File read, need to read missing references: #{missing_refs.join(", ")}" }
138
+ end
139
+
140
+ def planner_prompt(status)
141
+ <<~PROMPT
142
+ You are a planning agent for code analysis with external reference data.
143
+
144
+ Available actions:
145
+ - read_file: Read the target code file (ONLY if file_read is NO)
146
+ - read_reference: Read external reference data (style_guide or checklist) - ONLY if not already read
147
+ - analyze_with_reference: Analyze code using the reference data (ONLY if file_read is YES and BOTH references are read)
148
+ - finish: Complete task (ONLY if analysis exists)
149
+
150
+ Current Status:
151
+ #{status.to_json}
152
+
153
+ CRITICAL RULES (follow strictly in order):
154
+ 1. If file_read is NO → use read_file (DO NOT read file if already read)
155
+ 2. If file_read is YES and references_read is missing style_guide or checklist → use read_reference for the missing one
156
+ 3. If file_read is YES and BOTH references are read and analysis is NO → use analyze_with_reference
157
+ 4. If analysis exists → use finish
158
+
159
+ DO NOT:
160
+ - Read file if file_read is YES
161
+ - Read a reference if it's already in references_read
162
+ - Analyze if analysis already exists
163
+ - Finish if analysis does not exist
164
+
165
+ Decide the next action. Return ONLY valid JSON.
166
+ PROMPT
167
+ end
168
+
169
+ def plan_next_action(planner, decision_schema, status, context)
170
+ early_plan = early_plan_for(context)
171
+ return early_plan if early_plan
172
+
173
+ planner.run(prompt: planner_prompt(status), schema: decision_schema)
174
+ end
175
+
176
+ def handle_plan(plan, context, executor, test_dir)
177
+ case plan["action"]
178
+ when "read_file"
179
+ handle_read_file(context, test_dir)
180
+ when "read_reference"
181
+ handle_read_reference(context, test_dir)
182
+ when "analyze_with_reference"
183
+ handle_analyze_with_reference(context, executor, test_dir)
184
+ when "finish"
185
+ handle_finish
186
+ else
187
+ raise "Unknown action: #{plan["action"]}"
188
+ end
189
+ end
190
+
191
+ def handle_read_file(context, test_dir)
192
+ if context[:file_read]
193
+ puts "\n⚠️ File already read, skipping..."
194
+ return :skip
195
+ end
196
+
197
+ puts "\n→ Executor: reading target file"
198
+
199
+ full_path = File.expand_path(context[:target_file], test_dir)
200
+ if File.exist?(full_path)
201
+ context[:file_content] = File.read(full_path)
202
+ context[:file_read] = true
203
+ puts "\n✅ File content read (#{context[:file_content].length} bytes)"
204
+ else
205
+ puts "\n❌ File not found: #{context[:target_file]}"
206
+ context[:file_read] = false
207
+ end
208
+
209
+ :continue
210
+ end
211
+
212
+ def reference_file_name(reference_name)
213
+ reference_name == "style_guide" ? "ruby_style_guide.txt" : "code_review_checklist.txt"
214
+ end
215
+
216
+ def handle_read_reference(context, test_dir)
217
+ puts "\n→ Executor: reading reference data"
218
+
219
+ references_to_read = missing_references(context[:references_read])
220
+ if references_to_read.empty?
221
+ puts "\n⚠️ All references already read"
222
+ return :skip
223
+ end
224
+
225
+ reference_name = references_to_read.first
226
+ puts "\nReading reference: #{reference_name}"
227
+
228
+ ref_file = reference_file_name(reference_name)
229
+ ref_path = File.expand_path(ref_file, test_dir)
230
+
231
+ if File.exist?(ref_path)
232
+ context[:reference_data][reference_name] = File.read(ref_path)
233
+ context[:references_read] << reference_name
234
+ puts "\n✅ Reference '#{reference_name}' loaded (#{context[:reference_data][reference_name].length} bytes)"
235
+ else
236
+ puts "\n❌ Reference file not found: #{ref_file}"
237
+ end
238
+
239
+ :continue
240
+ end
241
+
242
+ def analysis_system_prompt
243
+ <<~PROMPT
244
+ You are a Ruby code analysis agent. Analyze code using the provided reference guidelines and checklists.
245
+ Compare the code against the reference standards and provide specific recommendations.
246
+ PROMPT
247
+ end
248
+
249
+ def analysis_user_prompt(code, reference_context)
250
+ <<~PROMPT
251
+ Analyze this Ruby code using the reference data:
252
+
253
+ CODE TO ANALYZE:
254
+ #{code}
255
+
256
+ REFERENCE DATA:
257
+ #{reference_context}
258
+
259
+ Provide a detailed analysis that:
260
+ 1. Compares the code against the style guide
261
+ 2. Checks items from the code review checklist
262
+ 3. Provides specific, actionable recommendations
263
+ 4. References specific guidelines from the reference data
264
+
265
+ Keep your analysis focused and reference the external data when making recommendations.
266
+ PROMPT
267
+ end
268
+
269
+ def handle_analyze_with_reference(context, executor, _test_dir)
270
+ if context[:analysis]
271
+ puts "\n⚠️ Analysis already complete, skipping..."
272
+ return :skip
273
+ end
274
+
275
+ unless context[:file_read]
276
+ puts "\n❌ Cannot analyze: file not read yet"
277
+ return :skip
278
+ end
279
+
280
+ if context[:references_read].length < 2
281
+ puts "\n❌ Cannot analyze: need both references (have: #{context[:references_read].join(", ")})"
282
+ return :skip
283
+ end
284
+
285
+ puts "\n→ Executor: analyzing code using reference data"
286
+
287
+ reference_context = context[:reference_data].map do |name, content|
288
+ "#{name.upcase}:\n#{content}\n"
289
+ end.join("\n---\n\n")
290
+
291
+ result = executor.run(
292
+ system: analysis_system_prompt,
293
+ user: analysis_user_prompt(context[:file_content], reference_context)
294
+ )
295
+
296
+ puts "\nExecutor result:"
297
+ puts result
298
+ puts "\n#{"=" * 60}"
299
+
300
+ context[:analysis] = result
301
+ :continue
302
+ end
303
+
304
+ def handle_finish
305
+ puts "\n→ Agent finished"
306
+ :finish
307
+ end
308
+
309
+ # ------------------------------------------------------------
310
+ # AGENT LOOP
311
+ # ------------------------------------------------------------
312
+
313
+ context = {
314
+ target_file: "user_creator.rb",
315
+ file_read: false,
316
+ file_content: nil,
317
+ references_read: [],
318
+ reference_data: {},
319
+ analysis: nil
320
+ }
321
+
322
+ step = 0
323
+ max_steps = 15
324
+
325
+ loop do
326
+ step += 1
327
+ puts "\n→ Planner step #{step}"
328
+
329
+ break if step_limit_reached?(step, max_steps)
330
+
331
+ status = build_status(context, step)
332
+ plan = plan_next_action(planner, decision_schema, status, context)
333
+ pp plan
334
+
335
+ result = handle_plan(plan, context, executor, test_dir)
336
+ break if result == :finish
337
+ next if result == :skip
338
+ end
339
+
340
+ # ------------------------------------------------------------
341
+ # FINAL ASSERTIONS
342
+ # ------------------------------------------------------------
343
+
344
+ puts "\n→ Assertions"
345
+
346
+ success = true
347
+
348
+ unless context[:file_read]
349
+ puts "❌ No file read"
350
+ success = false
351
+ end
352
+
353
+ unless context[:references_read].any?
354
+ puts "❌ No references read"
355
+ success = false
356
+ end
357
+
358
+ unless context[:analysis]
359
+ puts "❌ No analysis performed"
360
+ success = false
361
+ end
362
+
363
+ raise "❌ MULTI-STEP AGENT WITH EXTERNAL DATA FAILED" unless success
364
+
365
+ puts "\n✅ MULTI-STEP AGENT WITH EXTERNAL DATA PASSED"
366
+ puts "\nReferences used: #{context[:references_read].join(", ")}"
367
+ puts "\nAnalysis length: #{context[:analysis].length} characters"
368
+ puts "\n=== END ===\n"
@@ -6,6 +6,7 @@
6
6
 
7
7
  require_relative "lib/ollama_client"
8
8
  require_relative "examples/dhanhq_tools"
9
+ require "date"
9
10
 
10
11
  puts "\n=== DHANHQ TOOL CALLING TEST ===\n"
11
12
  puts "Using chat_raw() to access tool_calls\n"
@@ -68,7 +69,11 @@ historical_data_tool = Ollama::Tool.new(
68
69
  type: "function",
69
70
  function: Ollama::Tool::Function.new(
70
71
  name: "get_historical_data",
71
- description: "Get historical price data (OHLCV) for a symbol. Supports daily, weekly, monthly intervals.",
72
+ description: "Get historical price data (OHLCV) or technical indicators. " \
73
+ "Use interval for intraday data (1, 5, 15, 25, 60 minutes). " \
74
+ "Omit interval for daily data. " \
75
+ "Set calculate_indicators=true to get technical indicators " \
76
+ "(RSI, MACD, SMA, EMA, Bollinger Bands, ATR) instead of raw data.",
72
77
  parameters: Ollama::Tool::Function::Parameters.new(
73
78
  type: "object",
74
79
  properties: {
@@ -83,8 +88,9 @@ historical_data_tool = Ollama::Tool.new(
83
88
  ),
84
89
  interval: Ollama::Tool::Function::Parameters::Property.new(
85
90
  type: "string",
86
- description: "Data interval",
87
- enum: %w[daily weekly monthly]
91
+ description: "Minute interval for intraday data. Omit for daily data. " \
92
+ "Values: 1 (1-min), 5 (5-min), 15 (15-min), 25 (25-min), 60 (1-hour)",
93
+ enum: %w[1 5 15 25 60]
88
94
  ),
89
95
  from_date: Ollama::Tool::Function::Parameters::Property.new(
90
96
  type: "string",
@@ -92,10 +98,15 @@ historical_data_tool = Ollama::Tool.new(
92
98
  ),
93
99
  to_date: Ollama::Tool::Function::Parameters::Property.new(
94
100
  type: "string",
95
- description: "End date (YYYY-MM-DD format)"
101
+ description: "End date (YYYY-MM-DD format, non-inclusive)"
102
+ ),
103
+ calculate_indicators: Ollama::Tool::Function::Parameters::Property.new(
104
+ type: "boolean",
105
+ description: "If true, returns technical indicators instead of raw data. " \
106
+ "Reduces response size and provides ready-to-use indicator values."
96
107
  )
97
108
  },
98
- required: %w[symbol exchange_segment]
109
+ required: %w[symbol exchange_segment from_date to_date]
99
110
  )
100
111
  )
101
112
  )
@@ -249,9 +260,11 @@ begin
249
260
  puts " ✅ Instrument found and quote retrieved"
250
261
  quote = result[:result][:quote]
251
262
  if quote
263
+ ohlc = quote[:ohlc]
252
264
  puts " 📊 Last Price: #{quote[:last_price]}"
253
265
  puts " 📊 Volume: #{quote[:volume]}"
254
- puts " 📊 OHLC: O=#{quote[:ohlc][:open]}, H=#{quote[:ohlc][:high]}, L=#{quote[:ohlc][:low]}, C=#{quote[:ohlc][:close]}"
266
+ puts " 📊 OHLC: O=#{ohlc[:open]}, H=#{ohlc[:high]}, " \
267
+ "L=#{ohlc[:low]}, C=#{ohlc[:close]}"
255
268
  end
256
269
  end
257
270
  rescue StandardError => e
@@ -271,6 +284,81 @@ rescue Ollama::Error => e
271
284
  puts " Message: #{e.message}"
272
285
  end
273
286
 
287
+ puts "\n--- Test 5: Historical Data with Technical Indicators ---"
288
+ puts "Request: Get NIFTY intraday data with technical indicators\n"
289
+
290
+ begin
291
+ response = client.chat_raw(
292
+ model: "llama3.1:8b",
293
+ messages: [Ollama::Agent::Messages.user(
294
+ "Get intraday historical data for NIFTY with technical indicators for the last 30 days"
295
+ )],
296
+ tools: historical_data_tool,
297
+ allow_chat: true
298
+ )
299
+
300
+ tool_calls = response.message&.tool_calls
301
+ if tool_calls && !tool_calls.empty?
302
+ puts "✅ Tool call received"
303
+ call = tool_calls.first
304
+ puts " Tool: #{call.name}"
305
+ puts " Arguments: #{call.arguments.inspect}\n"
306
+
307
+ args = call.arguments
308
+ puts " Expected parameters:"
309
+ puts " - symbol: #{args['symbol'] || args[:symbol]}"
310
+ puts " - exchange_segment: #{args['exchange_segment'] || args[:exchange_segment]}"
311
+ puts " - interval: #{args['interval'] || args[:interval]} (for intraday)"
312
+ puts " - from_date: #{args['from_date'] || args[:from_date]}"
313
+ puts " - to_date: #{args['to_date'] || args[:to_date]}"
314
+ puts " - calculate_indicators: #{args['calculate_indicators'] || args[:calculate_indicators]}"
315
+ else
316
+ puts "⚠️ No tool calls detected"
317
+ puts "Content: #{response.message&.content}\n"
318
+ end
319
+ rescue Ollama::Error => e
320
+ puts "❌ Error: #{e.class.name}"
321
+ puts " Message: #{e.message}"
322
+ end
323
+
324
+ puts "\n--- Test 6: Intraday Data with 5-minute intervals ---"
325
+ puts "Request: Get NIFTY 5-minute intraday data for today\n"
326
+
327
+ begin
328
+ today = Date.today.strftime("%Y-%m-%d")
329
+ response = client.chat_raw(
330
+ model: "llama3.1:8b",
331
+ messages: [Ollama::Agent::Messages.user(
332
+ "Get NIFTY intraday data with 5-minute intervals for today (#{today})"
333
+ )],
334
+ tools: historical_data_tool,
335
+ allow_chat: true
336
+ )
337
+
338
+ tool_calls = response.message&.tool_calls
339
+ if tool_calls && !tool_calls.empty?
340
+ puts "✅ Tool call received"
341
+ call = tool_calls.first
342
+ args = call.arguments
343
+
344
+ puts " Verifying intraday parameters:"
345
+ interval = args["interval"] || args[:interval]
346
+ if %w[1 5 15 25 60].include?(interval.to_s)
347
+ puts " ✅ Interval: #{interval} (valid intraday value)"
348
+ else
349
+ puts " ❌ Interval: #{interval} (should be one of: 1, 5, 15, 25, 60)"
350
+ end
351
+
352
+ from_date = args["from_date"] || args[:from_date]
353
+ to_date = args["to_date"] || args[:to_date]
354
+ puts " Date range: #{from_date} to #{to_date}"
355
+ else
356
+ puts "⚠️ No tool calls detected"
357
+ end
358
+ rescue Ollama::Error => e
359
+ puts "❌ Error: #{e.message}"
360
+ end
361
+
274
362
  puts "\n--- Summary ---"
275
363
  puts "✅ Use chat_raw() for tool calling - gives access to tool_calls"
276
364
  puts "⚠️ Use chat() only for simple content responses (no tool_calls needed)"
@@ -279,4 +367,9 @@ puts " 1. LLM requests tool via chat_raw() → get tool_calls"
279
367
  puts " 2. Find instrument using exchange_segment + symbol"
280
368
  puts " 3. Execute tool with instrument"
281
369
  puts " 4. Feed result back to LLM (if using Executor)"
370
+ puts "\n📊 Historical Data enhancements:"
371
+ puts " - Use interval (1, 5, 15, 25, 60) for intraday data"
372
+ puts " - Omit interval for daily data"
373
+ puts " - Use calculate_indicators: true for technical indicators (RSI, MACD, etc.)"
374
+ puts " - Reduces response size and provides ready-to-use indicator values"
282
375
  puts "\n=== DONE ===\n"
@@ -180,34 +180,10 @@ module Ollama
180
180
 
181
181
  def invoke_tool(callable, args_hash)
182
182
  sym_args = normalize_parameter_names(args_hash)
183
+ keyword_result = call_with_keywords(callable, sym_args)
184
+ return keyword_result[:value] if keyword_result[:success]
183
185
 
184
- # Try keyword invocation first (common for Ruby tools).
185
- begin
186
- callable.call(**sym_args)
187
- rescue ArgumentError => e
188
- # If the error indicates required keyword arguments, try parameter aliases.
189
- if e.message.include?("required keyword") || e.message.include?("missing keyword")
190
- aliased_args = apply_parameter_aliases(sym_args, callable)
191
- if aliased_args != sym_args
192
- begin
193
- return callable.call(**aliased_args)
194
- rescue ArgumentError
195
- # Aliases didn't help, continue to try positional
196
- end
197
- end
198
- end
199
-
200
- # Try positional hash for callables that accept a hash argument.
201
- # This handles cases where tools are defined as `lambda { |h| ... }` instead of keyword args.
202
- begin
203
- callable.call(args_hash)
204
- rescue ArgumentError => positional_error
205
- # If both keyword and positional fail, re-raise with context.
206
- raise ArgumentError,
207
- "Tool invocation failed: #{positional_error.message}. Arguments provided: #{args_hash.inspect}. " \
208
- "Ensure the tool call includes all required parameters."
209
- end
210
- end
186
+ call_with_positional(callable, args_hash)
211
187
  end
212
188
 
213
189
  def normalize_parameter_names(args_hash)
@@ -234,6 +210,33 @@ module Ollama
234
210
  aliased
235
211
  end
236
212
 
213
+ def call_with_keywords(callable, sym_args)
214
+ { success: true, value: callable.call(**sym_args) }
215
+ rescue ArgumentError => e
216
+ return { success: false } unless missing_keyword_error?(e)
217
+
218
+ aliased_args = apply_parameter_aliases(sym_args, callable)
219
+ return { success: false } if aliased_args == sym_args
220
+
221
+ begin
222
+ { success: true, value: callable.call(**aliased_args) }
223
+ rescue ArgumentError
224
+ { success: false }
225
+ end
226
+ end
227
+
228
+ def call_with_positional(callable, args_hash)
229
+ callable.call(args_hash)
230
+ rescue ArgumentError => e
231
+ raise ArgumentError,
232
+ "Tool invocation failed: #{e.message}. Arguments provided: #{args_hash.inspect}. " \
233
+ "Ensure the tool call includes all required parameters."
234
+ end
235
+
236
+ def missing_keyword_error?(error)
237
+ error.message.include?("required keyword") || error.message.include?("missing keyword")
238
+ end
239
+
237
240
  def encode_tool_result(result)
238
241
  return result if result.is_a?(String)
239
242
 
@@ -248,9 +251,6 @@ module Ollama
248
251
  tool_entry
249
252
  when Hash
250
253
  tool_entry[:callable] || tool_entry["callable"]
251
- else
252
- # Tool objects are schema definitions only, not callables
253
- nil
254
254
  end
255
255
  end
256
256
  end