ollama-client 0.2.2 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +7 -1
- data/docs/CLOUD.md +29 -0
- data/docs/CONSOLE_IMPROVEMENTS.md +256 -0
- data/docs/README.md +37 -0
- data/docs/SCHEMA_FIXES.md +147 -0
- data/docs/TEST_UPDATES.md +107 -0
- data/examples/README.md +92 -0
- data/examples/advanced_complex_schemas.rb +6 -3
- data/examples/advanced_multi_step_agent.rb +2 -1
- data/examples/chat_console.rb +12 -3
- data/examples/complete_workflow.rb +14 -4
- data/examples/dhan_console.rb +103 -8
- data/examples/dhanhq/agents/technical_analysis_agent.rb +6 -1
- data/examples/dhanhq/schemas/agent_schemas.rb +2 -2
- data/examples/dhanhq_agent.rb +23 -13
- data/examples/dhanhq_tools.rb +311 -246
- data/examples/multi_step_agent_with_external_data.rb +368 -0
- data/{test_dhanhq_tool_calling.rb → examples/test_dhanhq_tool_calling.rb} +99 -6
- data/lib/ollama/agent/executor.rb +30 -30
- data/lib/ollama/client.rb +73 -80
- data/lib/ollama/dto.rb +7 -7
- data/lib/ollama/options.rb +17 -9
- data/lib/ollama/response.rb +4 -6
- data/lib/ollama/tool/function/parameters.rb +1 -0
- data/lib/ollama/version.rb +1 -1
- metadata +14 -7
- /data/{FEATURES_ADDED.md → docs/FEATURES_ADDED.md} +0 -0
- /data/{HANDLERS_ANALYSIS.md → docs/HANDLERS_ANALYSIS.md} +0 -0
- /data/{PRODUCTION_FIXES.md → docs/PRODUCTION_FIXES.md} +0 -0
- /data/{TESTING.md → docs/TESTING.md} +0 -0
- /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)
|
|
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: "
|
|
87
|
-
|
|
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=#{
|
|
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
|
-
|
|
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
|