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
data/lib/ollama/client.rb CHANGED
@@ -46,11 +46,7 @@ module Ollama
46
46
  # rubocop:disable Metrics/ParameterLists
47
47
  def chat(messages:, model: nil, format: nil, tools: nil, options: {}, strict: false, allow_chat: false,
48
48
  return_meta: false)
49
- unless allow_chat || strict
50
- raise Error,
51
- "chat() is intentionally gated because it is easy to misuse inside agents. " \
52
- "Prefer generate(). If you really want chat(), pass allow_chat: true (or strict: true)."
53
- end
49
+ ensure_chat_allowed!(allow_chat: allow_chat, strict: strict, method_name: "chat")
54
50
 
55
51
  attempts = 0
56
52
  @current_schema = format # Store for validation
@@ -63,73 +59,22 @@ module Ollama
63
59
  raw = call_chat_api(model: model, messages: messages, format: format, tools: normalized_tools, options: options)
64
60
  attempt_latency_ms = elapsed_ms(attempt_started_at)
65
61
 
66
- emit_response_hook(
67
- raw,
68
- {
69
- endpoint: "/api/chat",
70
- model: model || @config.model,
71
- attempt: attempts,
72
- attempt_latency_ms: attempt_latency_ms
73
- }
74
- )
75
-
76
- # When tools are used, response might have only tool_calls and no content
77
- # In that case, return empty string (caller should use chat_raw() for tool_calls)
78
- if raw.nil? || raw.empty?
79
- return "" unless return_meta
80
-
81
- return {
82
- "data" => "",
83
- "meta" => {
84
- "endpoint" => "/api/chat",
85
- "model" => model || @config.model,
86
- "attempts" => attempts,
87
- "latency_ms" => elapsed_ms(started_at),
88
- "note" => "Empty content (likely tool_calls only - use chat_raw() to access tool_calls)"
89
- }
90
- }
91
- end
62
+ emit_response_hook(raw, chat_response_meta(model: model, attempt: attempts,
63
+ attempt_latency_ms: attempt_latency_ms))
92
64
 
93
- # When tools are used, response might have only tool_calls and no content
94
- # In that case, return empty string (caller should use chat_raw() for tool_calls)
95
- if raw.nil? || raw.empty?
96
- return "" unless return_meta
97
-
98
- return {
99
- "data" => "",
100
- "meta" => {
101
- "endpoint" => "/api/chat",
102
- "model" => model || @config.model,
103
- "attempts" => attempts,
104
- "latency_ms" => elapsed_ms(started_at),
105
- "note" => "Empty content (likely tool_calls only - use chat_raw() to access tool_calls)"
106
- }
107
- }
108
- end
65
+ empty_response = empty_chat_response(raw: raw,
66
+ return_meta: return_meta,
67
+ model: model,
68
+ attempts: attempts,
69
+ started_at: started_at)
70
+ return empty_response unless empty_response.nil?
109
71
 
110
72
  parsed = parse_json_response(raw)
111
-
112
- # CRITICAL: If format is provided, free-text output is forbidden
113
- if format
114
- if parsed.nil? || parsed.empty?
115
- raise SchemaViolationError,
116
- "Empty or nil response when format schema is required"
117
- end
118
-
119
- SchemaValidator.validate!(parsed, format)
120
- end
73
+ validate_chat_format!(parsed: parsed, format: format)
121
74
 
122
75
  return parsed unless return_meta
123
76
 
124
- {
125
- "data" => parsed,
126
- "meta" => {
127
- "endpoint" => "/api/chat",
128
- "model" => model || @config.model,
129
- "attempts" => attempts,
130
- "latency_ms" => elapsed_ms(started_at)
131
- }
132
- }
77
+ chat_response_with_meta(data: parsed, model: model, attempts: attempts, started_at: started_at)
133
78
  rescue NotFoundError => e
134
79
  enhanced_error = enhance_not_found_error(e)
135
80
  raise enhanced_error
@@ -165,11 +110,7 @@ module Ollama
165
110
  # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/ParameterLists
166
111
  def chat_raw(messages:, model: nil, format: nil, tools: nil, options: {}, strict: false, allow_chat: false,
167
112
  return_meta: false, stream: false, &on_chunk)
168
- unless allow_chat || strict
169
- raise Error,
170
- "chat_raw() is intentionally gated because it is easy to misuse inside agents. " \
171
- "Prefer generate(). If you really want chat_raw(), pass allow_chat: true (or strict: true)."
172
- end
113
+ ensure_chat_allowed!(allow_chat: allow_chat, strict: strict, method_name: "chat_raw")
173
114
 
174
115
  attempts = 0
175
116
  @current_schema = format # Store for validation
@@ -265,7 +206,7 @@ module Ollama
265
206
  end
266
207
  # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/ParameterLists
267
208
 
268
- def generate(prompt:, schema:, strict: false, return_meta: false)
209
+ def generate(prompt:, schema:, model: nil, strict: false, return_meta: false)
269
210
  attempts = 0
270
211
  @current_schema = schema # Store for prompt enhancement
271
212
  started_at = monotonic_time
@@ -273,14 +214,14 @@ module Ollama
273
214
  begin
274
215
  attempts += 1
275
216
  attempt_started_at = monotonic_time
276
- raw = call_api(prompt)
217
+ raw = call_api(prompt, model: model)
277
218
  attempt_latency_ms = elapsed_ms(attempt_started_at)
278
219
 
279
220
  emit_response_hook(
280
221
  raw,
281
222
  {
282
223
  endpoint: "/api/generate",
283
- model: @config.model,
224
+ model: model || @config.model,
284
225
  attempt: attempts,
285
226
  attempt_latency_ms: attempt_latency_ms
286
227
  }
@@ -298,7 +239,7 @@ module Ollama
298
239
  "data" => parsed,
299
240
  "meta" => {
300
241
  "endpoint" => "/api/generate",
301
- "model" => @config.model,
242
+ "model" => model || @config.model,
302
243
  "attempts" => attempts,
303
244
  "latency_ms" => elapsed_ms(started_at)
304
245
  }
@@ -326,8 +267,8 @@ module Ollama
326
267
  end
327
268
  # rubocop:enable Metrics/MethodLength
328
269
 
329
- def generate_strict!(prompt:, schema:, return_meta: false)
330
- generate(prompt: prompt, schema: schema, strict: true, return_meta: return_meta)
270
+ def generate_strict!(prompt:, schema:, model: nil, return_meta: false)
271
+ generate(prompt: prompt, schema: schema, model: model, strict: true, return_meta: return_meta)
331
272
  end
332
273
 
333
274
  # Lightweight server health check.
@@ -407,6 +348,14 @@ module Ollama
407
348
 
408
349
  private
409
350
 
351
+ def ensure_chat_allowed!(allow_chat:, strict:, method_name:)
352
+ return if allow_chat || strict
353
+
354
+ raise Error,
355
+ "#{method_name}() is intentionally gated because it is easy to misuse inside agents. " \
356
+ "Prefer generate(). If you really want #{method_name}(), pass allow_chat: true (or strict: true)."
357
+ end
358
+
410
359
  # Normalize tools to array of hashes for API
411
360
  # Supports: Tool object, Array of Tool objects, Array of hashes, or nil
412
361
  def normalize_tools(tools)
@@ -424,6 +373,50 @@ module Ollama
424
373
  tools
425
374
  end
426
375
 
376
+ def chat_response_meta(model:, attempt:, attempt_latency_ms:)
377
+ {
378
+ endpoint: "/api/chat",
379
+ model: model || @config.model,
380
+ attempt: attempt,
381
+ attempt_latency_ms: attempt_latency_ms
382
+ }
383
+ end
384
+
385
+ def empty_chat_response(raw:, return_meta:, model:, attempts:, started_at:)
386
+ return nil unless raw.nil? || raw.empty?
387
+ return "" unless return_meta
388
+
389
+ {
390
+ "data" => "",
391
+ "meta" => {
392
+ "endpoint" => "/api/chat",
393
+ "model" => model || @config.model,
394
+ "attempts" => attempts,
395
+ "latency_ms" => elapsed_ms(started_at),
396
+ "note" => "Empty content (likely tool_calls only - use chat_raw() to access tool_calls)"
397
+ }
398
+ }
399
+ end
400
+
401
+ def validate_chat_format!(parsed:, format:)
402
+ return unless format
403
+ raise SchemaViolationError, "Empty or nil response when format schema is required" if parsed.nil? || parsed.empty?
404
+
405
+ SchemaValidator.validate!(parsed, format)
406
+ end
407
+
408
+ def chat_response_with_meta(data:, model:, attempts:, started_at:)
409
+ {
410
+ "data" => data,
411
+ "meta" => {
412
+ "endpoint" => "/api/chat",
413
+ "model" => model || @config.model,
414
+ "attempts" => attempts,
415
+ "latency_ms" => elapsed_ms(started_at)
416
+ }
417
+ }
418
+ end
419
+
427
420
  def handle_http_error(res, requested_model: nil)
428
421
  status_code = res.code.to_i
429
422
  requested_model ||= @config.model
@@ -646,13 +639,13 @@ module Ollama
646
639
  raise Error, "Connection failed: #{e.message}"
647
640
  end
648
641
 
649
- def call_api(prompt)
642
+ def call_api(prompt, model: nil)
650
643
  req = Net::HTTP::Post.new(@uri)
651
644
  req["Content-Type"] = "application/json"
652
645
 
653
646
  # Build request body
654
647
  body = {
655
- model: @config.model,
648
+ model: model || @config.model,
656
649
  prompt: prompt,
657
650
  stream: false,
658
651
  temperature: @config.temperature,
@@ -676,7 +669,7 @@ module Ollama
676
669
  open_timeout: @config.timeout
677
670
  ) { |http| http.request(req) }
678
671
 
679
- handle_http_error(res) unless res.is_a?(Net::HTTPSuccess)
672
+ handle_http_error(res, requested_model: model || @config.model) unless res.is_a?(Net::HTTPSuccess)
680
673
 
681
674
  body = JSON.parse(res.body)
682
675
  body["response"]
data/lib/ollama/dto.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
+ require "set"
4
5
 
5
6
  module Ollama
6
7
  # A module that provides a foundation for data transfer objects (DTOs) within
@@ -21,22 +22,21 @@ module Ollama
21
22
  # end
22
23
  module DTO
23
24
  def self.included(base)
24
- unless defined?(Set)
25
- end
26
25
  base.extend(ClassMethods)
27
- base.class_variable_set(:@@attributes, Set.new)
26
+ base.instance_variable_set(:@attributes, Set.new)
28
27
  end
29
28
 
29
+ # Class-level helpers for DTO attribute tracking.
30
30
  module ClassMethods
31
31
  # The attributes accessor reads and writes the attributes instance variable.
32
32
  #
33
33
  # @return [Set] the set of attributes stored in the instance variable
34
34
  def attributes
35
- class_variable_get(:@@attributes)
35
+ @attributes ||= Set.new
36
36
  end
37
37
 
38
38
  def attributes=(value)
39
- class_variable_set(:@@attributes, value)
39
+ @attributes = value
40
40
  end
41
41
 
42
42
  # The from_hash method creates a new instance of the class by converting a
@@ -180,8 +180,8 @@ module Ollama
180
180
  #
181
181
  # @param args [Array] pass-through args
182
182
  # @return [String] a JSON string representation of the object
183
- def to_json(*)
184
- as_json.to_json(*)
183
+ def to_json(*args)
184
+ as_json.to_json(*args)
185
185
  end
186
186
  end
187
187
  end
@@ -10,15 +10,17 @@ module Ollama
10
10
  # options = Ollama::Options.new(temperature: 0.7, top_p: 0.95)
11
11
  # client.generate(prompt: "...", schema: {...}, options: options.to_h)
12
12
  class Options
13
- attr_accessor :temperature, :top_p, :top_k, :num_ctx, :repeat_penalty, :seed
14
-
15
- def initialize(temperature: nil, top_p: nil, top_k: nil, num_ctx: nil, repeat_penalty: nil, seed: nil)
16
- self.temperature = temperature if temperature
17
- self.top_p = top_p if top_p
18
- self.top_k = top_k if top_k
19
- self.num_ctx = num_ctx if num_ctx
20
- self.repeat_penalty = repeat_penalty if repeat_penalty
21
- self.seed = seed if seed
13
+ VALID_KEYS = %i[temperature top_p top_k num_ctx repeat_penalty seed].freeze
14
+
15
+ attr_reader :temperature, :top_p, :top_k, :num_ctx, :repeat_penalty, :seed
16
+
17
+ def initialize(**options)
18
+ unknown_keys = options.keys - VALID_KEYS
19
+ raise ArgumentError, "Unknown options: #{unknown_keys.join(", ")}" if unknown_keys.any?
20
+
21
+ VALID_KEYS.each do |key|
22
+ assign_option(key, options[key])
23
+ end
22
24
  end
23
25
 
24
26
  def temperature=(value)
@@ -65,6 +67,12 @@ module Ollama
65
67
 
66
68
  private
67
69
 
70
+ def assign_option(name, value)
71
+ return if value.nil?
72
+
73
+ public_send("#{name}=", value)
74
+ end
75
+
68
76
  def validate_numeric_range(value, min, max, name)
69
77
  return if value.nil?
70
78
 
@@ -28,12 +28,10 @@ module Ollama
28
28
  end
29
29
 
30
30
  # Delegate other methods to underlying hash
31
- def method_missing(method, *, &)
32
- if @data.respond_to?(method)
33
- @data.public_send(method, *, &)
34
- else
35
- super
36
- end
31
+ def method_missing(method, ...)
32
+ return super unless @data.respond_to?(method)
33
+
34
+ @data.public_send(method, ...)
37
35
  end
38
36
 
39
37
  def respond_to_missing?(method, include_private = false)
@@ -6,6 +6,7 @@ module Ollama
6
6
  # Tool and Function classes are defined in tool.rb and tool/function.rb
7
7
  # This file adds Parameters class to Function
8
8
  class Tool
9
+ # Function metadata and schema for tool calls.
9
10
  class Function
10
11
  # Parameters specification for a tool function
11
12
  #
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ollama
4
- VERSION = "0.2.2"
4
+ VERSION = "0.2.4"
5
5
  end
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ollama-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shubham Taywade
8
+ autorequire:
8
9
  bindir: exe
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-01-18 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: bigdecimal
@@ -53,13 +54,24 @@ files:
53
54
  - CHANGELOG.md
54
55
  - CODE_OF_CONDUCT.md
55
56
  - CONTRIBUTING.md
56
- - FEATURES_ADDED.md
57
- - HANDLERS_ANALYSIS.md
58
57
  - LICENSE.txt
59
- - PRODUCTION_FIXES.md
60
58
  - README.md
61
59
  - Rakefile
62
- - TESTING.md
60
+ - docs/CLOUD.md
61
+ - docs/CONSOLE_IMPROVEMENTS.md
62
+ - docs/FEATURES_ADDED.md
63
+ - docs/GEM_RELEASE_GUIDE.md
64
+ - docs/GET_RUBYGEMS_SECRET.md
65
+ - docs/HANDLERS_ANALYSIS.md
66
+ - docs/PRODUCTION_FIXES.md
67
+ - docs/QUICK_OTP_SETUP.md
68
+ - docs/QUICK_RELEASE.md
69
+ - docs/README.md
70
+ - docs/RUBYGEMS_OTP_SETUP.md
71
+ - docs/SCHEMA_FIXES.md
72
+ - docs/TESTING.md
73
+ - docs/TEST_UPDATES.md
74
+ - examples/README.md
63
75
  - examples/advanced_complex_schemas.rb
64
76
  - examples/advanced_edge_cases.rb
65
77
  - examples/advanced_error_handling.rb
@@ -97,8 +109,11 @@ files:
97
109
  - examples/dhanhq/utils/trading_parameter_normalizer.rb
98
110
  - examples/dhanhq_agent.rb
99
111
  - examples/dhanhq_tools.rb
112
+ - examples/multi_step_agent_with_external_data.rb
100
113
  - examples/structured_outputs_chat.rb
101
114
  - examples/structured_tools.rb
115
+ - examples/test_dhanhq_tool_calling.rb
116
+ - examples/test_tool_calling.rb
102
117
  - examples/tool_calling_direct.rb
103
118
  - examples/tool_calling_pattern.rb
104
119
  - examples/tool_dto_example.rb
@@ -124,8 +139,6 @@ files:
124
139
  - lib/ollama/version.rb
125
140
  - lib/ollama_client.rb
126
141
  - sig/ollama/client.rbs
127
- - test_dhanhq_tool_calling.rb
128
- - test_tool_calling.rb
129
142
  homepage: https://github.com/shubhamtaywade82/ollama-client
130
143
  licenses:
131
144
  - MIT
@@ -134,6 +147,7 @@ metadata:
134
147
  source_code_uri: https://github.com/shubhamtaywade82/ollama-client
135
148
  changelog_uri: https://github.com/shubhamtaywade82/ollama-client/blob/main/CHANGELOG.md
136
149
  rubygems_mfa_required: 'true'
150
+ post_install_message:
137
151
  rdoc_options: []
138
152
  require_paths:
139
153
  - lib
@@ -148,7 +162,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
148
162
  - !ruby/object:Gem::Version
149
163
  version: '0'
150
164
  requirements: []
151
- rubygems_version: 4.0.3
165
+ rubygems_version: 3.5.11
166
+ signing_key:
152
167
  specification_version: 4
153
168
  summary: An agent-first Ruby client for Ollama (planner/executor + safe tool loops)
154
169
  test_files: []
File without changes
File without changes
File without changes