ollama-client 0.2.0

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.
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ollama
4
+ # Configuration class with safe defaults for agent-grade usage
5
+ #
6
+ # ⚠️ THREAD SAFETY WARNING:
7
+ # Global configuration via OllamaClient.configure is NOT thread-safe.
8
+ # For concurrent agents or multi-threaded applications, use per-client
9
+ # configuration instead:
10
+ #
11
+ # config = Ollama::Config.new
12
+ # config.model = "llama3.1"
13
+ # client = Ollama::Client.new(config: config)
14
+ #
15
+ class Config
16
+ attr_accessor :base_url, :model, :timeout, :retries, :temperature, :top_p, :num_ctx, :on_response
17
+
18
+ def initialize
19
+ @base_url = "http://localhost:11434"
20
+ @model = "llama3.1:8b"
21
+ @timeout = 20
22
+ @retries = 2
23
+ @temperature = 0.2
24
+ @top_p = 0.9
25
+ @num_ctx = 8192
26
+ @on_response = nil
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ollama
4
+ class Error < StandardError; end
5
+ class TimeoutError < Error; end
6
+ class InvalidJSONError < Error; end
7
+ class SchemaViolationError < Error; end
8
+ class RetryExhaustedError < Error; end
9
+
10
+ # HTTP error with retry logic
11
+ class HTTPError < Error
12
+ attr_reader :status_code
13
+
14
+ def initialize(message, status_code = nil)
15
+ super(message)
16
+ @status_code = status_code
17
+ end
18
+
19
+ def retryable?
20
+ # Explicit retry policy:
21
+ # - Retry: 408 (Request Timeout), 429 (Too Many Requests),
22
+ # 500 (Internal Server Error), 502 (Bad Gateway), 503 (Service Unavailable)
23
+ # - Never retry: 400-407, 409-428, 430-499 (client errors)
24
+ # - Never retry: 501, 504-599 (other server errors - may indicate permanent issues)
25
+ return true if @status_code.nil? # Unknown status - retry for safety
26
+ return true if [408, 429, 500, 502, 503].include?(@status_code)
27
+
28
+ false
29
+ end
30
+ end
31
+
32
+ # Specific error for 404 Not Found responses
33
+ class NotFoundError < HTTPError
34
+ attr_reader :requested_model, :suggestions
35
+
36
+ def initialize(message = "Resource not found", requested_model: nil, suggestions: [])
37
+ super("HTTP 404: #{message}", 404)
38
+ @requested_model = requested_model
39
+ @suggestions = suggestions
40
+ end
41
+
42
+ def retryable?
43
+ false
44
+ end
45
+
46
+ def to_s
47
+ msg = super
48
+ return msg unless @requested_model && !@suggestions.empty?
49
+
50
+ suggestion_text = @suggestions.map { |m| " - #{m}" }.join("\n")
51
+ "#{msg}\n\nModel '#{@requested_model}' not found. Did you mean one of these?\n#{suggestion_text}"
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json-schema"
4
+ require_relative "errors"
5
+
6
+ module Ollama
7
+ # Validates JSON data against JSON Schema
8
+ #
9
+ # For agent-grade usage, enforces strict schemas by default:
10
+ # - additionalProperties: false (unless explicitly set)
11
+ # - Prevents LLMs from adding unexpected fields
12
+ class SchemaValidator
13
+ def self.validate!(data, schema)
14
+ JSON::Validator.validate!(prepare_schema(schema), data)
15
+ rescue JSON::Schema::ValidationError => e
16
+ raise SchemaViolationError, e.message
17
+ end
18
+
19
+ # JSON Schema defaults to allowing additional properties unless
20
+ # `additionalProperties: false` is specified. For agent-grade contracts,
21
+ # we want the stricter default, while still allowing callers to override
22
+ # it explicitly on any object schema.
23
+ def self.prepare_schema(schema)
24
+ enforce_no_additional_properties(schema)
25
+ end
26
+
27
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
28
+ def self.enforce_no_additional_properties(node)
29
+ case node
30
+ when Array
31
+ node.map { |v| enforce_no_additional_properties(v) }
32
+ when Hash
33
+ h = node.dup
34
+
35
+ # Recurse into common schema composition keywords
36
+ %w[anyOf oneOf allOf].each do |k|
37
+ h[k] = h[k].map { |v| enforce_no_additional_properties(v) } if h[k].is_a?(Array)
38
+ end
39
+
40
+ # Recurse into nested schemas
41
+ h["not"] = enforce_no_additional_properties(h["not"]) if h["not"].is_a?(Hash)
42
+
43
+ if h["properties"].is_a?(Hash)
44
+ h["properties"] = h["properties"].transform_values { |v| enforce_no_additional_properties(v) }
45
+ end
46
+
47
+ if h["patternProperties"].is_a?(Hash)
48
+ h["patternProperties"] = h["patternProperties"].transform_values { |v| enforce_no_additional_properties(v) }
49
+ end
50
+
51
+ h["items"] = enforce_no_additional_properties(h["items"]) if h["items"]
52
+
53
+ h["additionalItems"] = enforce_no_additional_properties(h["additionalItems"]) if h["additionalItems"]
54
+
55
+ # JSON Schema draft variants
56
+ if h["definitions"].is_a?(Hash)
57
+ h["definitions"] = h["definitions"].transform_values { |v| enforce_no_additional_properties(v) }
58
+ end
59
+
60
+ h["$defs"] = h["$defs"].transform_values { |v| enforce_no_additional_properties(v) } if h["$defs"].is_a?(Hash)
61
+
62
+ # Enforce strict object shape by default.
63
+ is_objectish =
64
+ h["type"] == "object" ||
65
+ h.key?("properties") ||
66
+ h.key?("patternProperties")
67
+
68
+ h["additionalProperties"] = false if is_objectish && !h.key?("additionalProperties")
69
+
70
+ h
71
+ else
72
+ node
73
+ end
74
+ end
75
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
76
+
77
+ private_class_method :prepare_schema, :enforce_no_additional_properties
78
+ end
79
+ end
@@ -0,0 +1,5 @@
1
+ {
2
+ "type": "object",
3
+ "additionalProperties": true
4
+ }
5
+
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ollama
4
+ # Presentation-only streaming observer for agent loops.
5
+ # This object must never control tool execution or loop termination.
6
+ class StreamingObserver
7
+ Event = Struct.new(:type, :text, :name, :state, :data, keyword_init: true)
8
+
9
+ def initialize(&block)
10
+ @block = block
11
+ end
12
+
13
+ def emit(type, text: nil, name: nil, state: nil, data: nil)
14
+ return unless @block
15
+
16
+ @block.call(Event.new(type: type, text: text, name: name, state: state, data: data))
17
+ rescue StandardError
18
+ # Observers must never break control flow.
19
+ nil
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ollama
4
+ VERSION = "0.2.0"
5
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ollama/config"
4
+ require_relative "ollama/errors"
5
+ require_relative "ollama/schema_validator"
6
+ require_relative "ollama/client"
7
+ require_relative "ollama/streaming_observer"
8
+ require_relative "ollama/agent/messages"
9
+ require_relative "ollama/agent/planner"
10
+ require_relative "ollama/agent/executor"
11
+
12
+ # Main entry point for OllamaClient gem
13
+ #
14
+ # ⚠️ THREAD SAFETY WARNING:
15
+ # Global configuration via OllamaClient.configure is NOT thread-safe.
16
+ # For concurrent agents or multi-threaded applications, use per-client
17
+ # configuration instead:
18
+ #
19
+ # config = Ollama::Config.new
20
+ # config.model = "llama3.1"
21
+ # client = Ollama::Client.new(config: config)
22
+ #
23
+ module OllamaClient
24
+ @config_mutex = Mutex.new
25
+ @warned_thread_config = false
26
+
27
+ def self.config
28
+ @config_mutex.synchronize do
29
+ @config ||= Ollama::Config.new
30
+ end
31
+ end
32
+
33
+ def self.configure
34
+ if Thread.current != Thread.main && !@warned_thread_config
35
+ @warned_thread_config = true
36
+ msg = "[ollama-client] Global OllamaClient.configure is not thread-safe. " \
37
+ "Prefer per-client config (Ollama::Client.new(config: ...))."
38
+ warn(msg)
39
+ end
40
+
41
+ @config_mutex.synchronize do
42
+ @config ||= Ollama::Config.new
43
+ yield(@config)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,6 @@
1
+ module Ollama
2
+ module Client
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end
metadata ADDED
@@ -0,0 +1,108 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ollama-client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Shubham Taywade
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: bigdecimal
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: json-schema
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '4.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '4.0'
40
+ description: A production-ready, agent-first Ruby client for the Ollama API with schema
41
+ validation, bounded retries, and explicit safety defaults. Includes a minimal agent
42
+ layer (Ollama::Agent::Planner for deterministic /api/generate, and Ollama::Agent::Executor
43
+ for stateful /api/chat tool loops with disciplined, observer-only streaming). Not
44
+ a chatbot UI and not a promise of full Ollama endpoint coverage.
45
+ email:
46
+ - shubhamtaywade82@gmail.com
47
+ executables:
48
+ - ollama-client
49
+ extensions: []
50
+ extra_rdoc_files: []
51
+ files:
52
+ - CHANGELOG.md
53
+ - CODE_OF_CONDUCT.md
54
+ - CONTRIBUTING.md
55
+ - LICENSE.txt
56
+ - PRODUCTION_FIXES.md
57
+ - README.md
58
+ - Rakefile
59
+ - TESTING.md
60
+ - examples/advanced_complex_schemas.rb
61
+ - examples/advanced_edge_cases.rb
62
+ - examples/advanced_error_handling.rb
63
+ - examples/advanced_multi_step_agent.rb
64
+ - examples/advanced_performance_testing.rb
65
+ - examples/complete_workflow.rb
66
+ - examples/dhanhq_agent.rb
67
+ - examples/dhanhq_tools.rb
68
+ - examples/structured_outputs_chat.rb
69
+ - examples/tool_calling_pattern.rb
70
+ - exe/ollama-client
71
+ - lib/ollama/agent/executor.rb
72
+ - lib/ollama/agent/messages.rb
73
+ - lib/ollama/agent/planner.rb
74
+ - lib/ollama/client.rb
75
+ - lib/ollama/config.rb
76
+ - lib/ollama/errors.rb
77
+ - lib/ollama/schema_validator.rb
78
+ - lib/ollama/schemas/base.json
79
+ - lib/ollama/streaming_observer.rb
80
+ - lib/ollama/version.rb
81
+ - lib/ollama_client.rb
82
+ - sig/ollama/client.rbs
83
+ homepage: https://github.com/shubhamtaywade82/ollama-client
84
+ licenses:
85
+ - MIT
86
+ metadata:
87
+ homepage_uri: https://github.com/shubhamtaywade82/ollama-client
88
+ source_code_uri: https://github.com/shubhamtaywade82/ollama-client
89
+ changelog_uri: https://github.com/shubhamtaywade82/ollama-client/blob/main/CHANGELOG.md
90
+ rubygems_mfa_required: 'true'
91
+ rdoc_options: []
92
+ require_paths:
93
+ - lib
94
+ required_ruby_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: 3.2.0
99
+ required_rubygems_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ requirements: []
105
+ rubygems_version: 4.0.3
106
+ specification_version: 4
107
+ summary: An agent-first Ruby client for Ollama (planner/executor + safe tool loops)
108
+ test_files: []