ollama-client 0.2.1 → 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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/README.md +220 -12
  4. data/docs/CLOUD.md +29 -0
  5. data/docs/CONSOLE_IMPROVEMENTS.md +256 -0
  6. data/docs/FEATURES_ADDED.md +145 -0
  7. data/docs/HANDLERS_ANALYSIS.md +190 -0
  8. data/docs/README.md +37 -0
  9. data/docs/SCHEMA_FIXES.md +147 -0
  10. data/docs/TEST_UPDATES.md +107 -0
  11. data/examples/README.md +92 -0
  12. data/examples/advanced_complex_schemas.rb +6 -3
  13. data/examples/advanced_multi_step_agent.rb +13 -7
  14. data/examples/chat_console.rb +143 -0
  15. data/examples/complete_workflow.rb +14 -4
  16. data/examples/dhan_console.rb +843 -0
  17. data/examples/dhanhq/agents/base_agent.rb +0 -2
  18. data/examples/dhanhq/agents/orchestrator_agent.rb +1 -2
  19. data/examples/dhanhq/agents/technical_analysis_agent.rb +67 -49
  20. data/examples/dhanhq/analysis/market_structure.rb +44 -28
  21. data/examples/dhanhq/analysis/pattern_recognizer.rb +64 -47
  22. data/examples/dhanhq/analysis/trend_analyzer.rb +6 -8
  23. data/examples/dhanhq/dhanhq_agent.rb +296 -99
  24. data/examples/dhanhq/indicators/technical_indicators.rb +3 -5
  25. data/examples/dhanhq/scanners/intraday_options_scanner.rb +360 -255
  26. data/examples/dhanhq/scanners/swing_scanner.rb +118 -84
  27. data/examples/dhanhq/schemas/agent_schemas.rb +2 -2
  28. data/examples/dhanhq/services/data_service.rb +5 -7
  29. data/examples/dhanhq/services/trading_service.rb +0 -3
  30. data/examples/dhanhq/technical_analysis_agentic_runner.rb +217 -84
  31. data/examples/dhanhq/technical_analysis_runner.rb +216 -162
  32. data/examples/dhanhq/test_tool_calling.rb +538 -0
  33. data/examples/dhanhq/test_tool_calling_verbose.rb +251 -0
  34. data/examples/dhanhq/utils/trading_parameter_normalizer.rb +12 -17
  35. data/examples/dhanhq_agent.rb +159 -116
  36. data/examples/dhanhq_tools.rb +1158 -251
  37. data/examples/multi_step_agent_with_external_data.rb +368 -0
  38. data/examples/structured_tools.rb +89 -0
  39. data/examples/test_dhanhq_tool_calling.rb +375 -0
  40. data/examples/test_tool_calling.rb +160 -0
  41. data/examples/tool_calling_direct.rb +124 -0
  42. data/examples/tool_dto_example.rb +94 -0
  43. data/exe/dhan_console +4 -0
  44. data/exe/ollama-client +1 -1
  45. data/lib/ollama/agent/executor.rb +116 -15
  46. data/lib/ollama/client.rb +118 -55
  47. data/lib/ollama/config.rb +36 -0
  48. data/lib/ollama/dto.rb +187 -0
  49. data/lib/ollama/embeddings.rb +77 -0
  50. data/lib/ollama/options.rb +104 -0
  51. data/lib/ollama/response.rb +121 -0
  52. data/lib/ollama/tool/function/parameters/property.rb +72 -0
  53. data/lib/ollama/tool/function/parameters.rb +101 -0
  54. data/lib/ollama/tool/function.rb +78 -0
  55. data/lib/ollama/tool.rb +60 -0
  56. data/lib/ollama/version.rb +1 -1
  57. data/lib/ollama_client.rb +3 -0
  58. metadata +31 -3
  59. /data/{PRODUCTION_FIXES.md → docs/PRODUCTION_FIXES.md} +0 -0
  60. /data/{TESTING.md → docs/TESTING.md} +0 -0
data/lib/ollama/dto.rb ADDED
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "set"
5
+
6
+ module Ollama
7
+ # A module that provides a foundation for data transfer objects (DTOs) within
8
+ # the Ollama library.
9
+ #
10
+ # The DTO module includes common functionality for converting objects to and
11
+ # from JSON, handling attribute management, and providing utility methods for
12
+ # processing arrays and hashes. It serves as a base for various command
13
+ # and data structures used in communicating with the Ollama API.
14
+ #
15
+ # @example Using DTO functionality in a command class
16
+ # class MyCommand
17
+ # include Ollama::DTO
18
+ # attr_reader :name, :value
19
+ # def initialize(name:, value:)
20
+ # @name, @value = name, value
21
+ # end
22
+ # end
23
+ module DTO
24
+ def self.included(base)
25
+ base.extend(ClassMethods)
26
+ base.instance_variable_set(:@attributes, Set.new)
27
+ end
28
+
29
+ # Class-level helpers for DTO attribute tracking.
30
+ module ClassMethods
31
+ # The attributes accessor reads and writes the attributes instance variable.
32
+ #
33
+ # @return [Set] the set of attributes stored in the instance variable
34
+ def attributes
35
+ @attributes ||= Set.new
36
+ end
37
+
38
+ def attributes=(value)
39
+ @attributes = value
40
+ end
41
+
42
+ # The from_hash method creates a new instance of the class by converting a
43
+ # hash into keyword arguments.
44
+ #
45
+ # This method is typically used to instantiate objects from JSON data or
46
+ # other hash-based sources, transforming the hash keys to symbols and
47
+ # passing them as keyword arguments to the constructor.
48
+ #
49
+ # @param hash [Hash] a hash containing the attributes for the new instance
50
+ # @return [self] a new instance of the class initialized with the hash data
51
+ def from_hash(hash)
52
+ new(**hash.transform_keys(&:to_sym))
53
+ end
54
+
55
+ # The attr_reader method extends the functionality of the standard
56
+ # attr_reader by also registering the declared attributes in the class's
57
+ # attributes set.
58
+ #
59
+ # @param names [Array<Symbol>] one or more attribute names to be declared
60
+ # as readable and registered
61
+ def attr_reader(*names)
62
+ super
63
+ attributes.merge(names.map(&:to_sym))
64
+ end
65
+
66
+ # The attr_accessor method extends the functionality of the standard
67
+ # attr_accessor by also registering the declared attributes in the class's
68
+ # attributes set.
69
+ #
70
+ # @param names [Array<Symbol>] one or more attribute names to be declared
71
+ # as readable and registered
72
+ def attr_accessor(*names)
73
+ super
74
+ attributes.merge(names.map(&:to_sym))
75
+ end
76
+ end
77
+
78
+ # The as_array_of_hashes method converts an object into an array of hashes.
79
+ #
80
+ # If the object responds to to_hash, it wraps the result in an array.
81
+ # If the object responds to to_ary, it maps each element to a hash and
82
+ # returns the resulting array.
83
+ #
84
+ # @param obj [Object] the object to be converted
85
+ # @return [Array<Hash>, nil] an array of hashes if the conversion was
86
+ # possible, or nil otherwise
87
+ def as_array_of_hashes(obj)
88
+ if obj.respond_to?(:to_hash)
89
+ [obj.to_hash]
90
+ elsif obj.respond_to?(:to_ary)
91
+ obj.to_ary.map(&:to_hash)
92
+ end
93
+ end
94
+
95
+ # The as_hash method converts an object to a hash representation.
96
+ #
97
+ # If the object responds to to_hash, it returns the result of that method call.
98
+ # If the object does not respond to to_hash, it returns nil.
99
+ #
100
+ # @param obj [Object] the object to be converted to a hash
101
+ # @return [Hash, nil] the hash representation of the object or nil if the
102
+ # object does not respond to to_hash
103
+ def as_hash(obj)
104
+ obj&.to_hash
105
+ end
106
+
107
+ # The as_array method converts an object into an array representation.
108
+ #
109
+ # If the object is nil, it returns nil.
110
+ # If the object responds to to_ary, it calls to_ary and returns the result.
111
+ # Otherwise, it wraps the object in an array and returns it.
112
+ #
113
+ # @param obj [Object] the object to be converted to an array
114
+ # @return [Array, nil] an array containing the object or its elements, or
115
+ # nil if the input is nil
116
+ def as_array(obj)
117
+ if obj.nil?
118
+ obj
119
+ elsif obj.respond_to?(:to_ary)
120
+ obj.to_ary
121
+ else
122
+ [obj]
123
+ end
124
+ end
125
+
126
+ # The as_json method converts the object's attributes into a JSON-compatible
127
+ # hash.
128
+ #
129
+ # This method gathers all defined attributes of the object and constructs a
130
+ # hash representation, excluding any nil values or empty collections.
131
+ #
132
+ # @param _ignored [Array] ignored arguments
133
+ # @return [Hash] a hash containing the object's non-nil and non-empty attributes
134
+ def as_json(*_ignored)
135
+ self.class.attributes.each_with_object({}) do |attr, hash|
136
+ value = send(attr)
137
+ next if value.nil?
138
+
139
+ # Check if it's an empty collection (responds to size and size is 0)
140
+ next if value.respond_to?(:size) && value.empty?
141
+
142
+ hash[attr] = value
143
+ end
144
+ end
145
+
146
+ # The == method compares two objects for equality based on their JSON representation.
147
+ #
148
+ # This method checks if the JSON representation of the current object is
149
+ # equal to the JSON representation of another object.
150
+ #
151
+ # @param other [Object] the object to compare against
152
+ # @return [TrueClass, FalseClass] true if both objects have identical JSON
153
+ # representations, false otherwise
154
+ def ==(other)
155
+ return false unless other.is_a?(self.class)
156
+
157
+ as_json == other.as_json
158
+ end
159
+
160
+ alias to_hash as_json
161
+
162
+ # The empty? method checks whether the object has any attributes defined.
163
+ #
164
+ # This method determines if the object contains no attributes by checking
165
+ # if its hash representation is empty. It is typically used to verify
166
+ # if an object, such as a DTO, has been initialized with any values.
167
+ #
168
+ # @return [TrueClass, FalseClass] true if the object has no attributes,
169
+ # false otherwise
170
+ def empty?
171
+ to_hash.empty?
172
+ end
173
+
174
+ # The to_json method converts the object's JSON representation into a JSON
175
+ # string format.
176
+ #
177
+ # This method utilizes the object's existing as_json representation and
178
+ # applies the standard JSON serialization to produce a formatted JSON string
179
+ # output.
180
+ #
181
+ # @param args [Array] pass-through args
182
+ # @return [String] a JSON string representation of the object
183
+ def to_json(*args)
184
+ as_json.to_json(*args)
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "json"
6
+ require_relative "errors"
7
+
8
+ module Ollama
9
+ # Embeddings API helper for semantic search and RAG in agents
10
+ #
11
+ # This is a helper module used internally by Client.
12
+ # Use client.embeddings.embed() instead of instantiating this directly.
13
+ class Embeddings
14
+ def initialize(config)
15
+ @config = config
16
+ end
17
+
18
+ # Generate embeddings for text input(s)
19
+ #
20
+ # @param model [String] Embedding model name (e.g., "all-minilm")
21
+ # @param input [String, Array<String>] Single text or array of texts
22
+ # @return [Array<Float>, Array<Array<Float>>] Embedding vector(s)
23
+ def embed(model:, input:)
24
+ uri = URI("#{@config.base_url}/api/embeddings")
25
+ req = Net::HTTP::Post.new(uri)
26
+ req["Content-Type"] = "application/json"
27
+
28
+ body = {
29
+ model: model,
30
+ input: input
31
+ }
32
+
33
+ req.body = body.to_json
34
+
35
+ res = Net::HTTP.start(
36
+ uri.hostname,
37
+ uri.port,
38
+ read_timeout: @config.timeout,
39
+ open_timeout: @config.timeout
40
+ ) { |http| http.request(req) }
41
+
42
+ handle_http_error(res, requested_model: model) unless res.is_a?(Net::HTTPSuccess)
43
+
44
+ response_body = JSON.parse(res.body)
45
+ embedding = response_body["embedding"]
46
+
47
+ # Return single array for single input, or array of arrays for multiple inputs
48
+ if input.is_a?(Array)
49
+ # Ollama returns single embedding array even for multiple inputs
50
+ # We need to check the response structure
51
+ if embedding.is_a?(Array) && embedding.first.is_a?(Array)
52
+ embedding
53
+ else
54
+ # Single embedding returned, wrap it
55
+ [embedding]
56
+ end
57
+ else
58
+ embedding
59
+ end
60
+ rescue JSON::ParserError => e
61
+ raise InvalidJSONError, "Failed to parse embeddings response: #{e.message}"
62
+ rescue Net::ReadTimeout, Net::OpenTimeout
63
+ raise TimeoutError, "Request timed out after #{@config.timeout}s"
64
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, SocketError => e
65
+ raise Error, "Connection failed: #{e.message}"
66
+ end
67
+
68
+ private
69
+
70
+ def handle_http_error(res, requested_model: nil)
71
+ status_code = res.code.to_i
72
+ raise NotFoundError.new(res.message, requested_model: requested_model) if status_code == 404
73
+
74
+ raise HTTPError.new("HTTP #{res.code}: #{res.message}", status_code)
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ollama
4
+ # Options class for model parameters with basic type checking
5
+ #
6
+ # Provides type-safe access to Ollama model options.
7
+ # Useful for agents that need to adjust model behavior dynamically.
8
+ #
9
+ # Example:
10
+ # options = Ollama::Options.new(temperature: 0.7, top_p: 0.95)
11
+ # client.generate(prompt: "...", schema: {...}, options: options.to_h)
12
+ class Options
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
24
+ end
25
+
26
+ def temperature=(value)
27
+ validate_numeric_range(value, 0.0, 2.0, "temperature")
28
+ @temperature = value
29
+ end
30
+
31
+ def top_p=(value)
32
+ validate_numeric_range(value, 0.0, 1.0, "top_p")
33
+ @top_p = value
34
+ end
35
+
36
+ def top_k=(value)
37
+ validate_integer_min(value, 1, "top_k")
38
+ @top_k = value
39
+ end
40
+
41
+ def num_ctx=(value)
42
+ validate_integer_min(value, 1, "num_ctx")
43
+ @num_ctx = value
44
+ end
45
+
46
+ def repeat_penalty=(value)
47
+ validate_numeric_range(value, 0.0, 2.0, "repeat_penalty")
48
+ @repeat_penalty = value
49
+ end
50
+
51
+ def seed=(value)
52
+ validate_integer(value, "seed")
53
+ @seed = value
54
+ end
55
+
56
+ # Convert to hash for API calls
57
+ def to_h
58
+ hash = {}
59
+ hash[:temperature] = @temperature if @temperature
60
+ hash[:top_p] = @top_p if @top_p
61
+ hash[:top_k] = @top_k if @top_k
62
+ hash[:num_ctx] = @num_ctx if @num_ctx
63
+ hash[:repeat_penalty] = @repeat_penalty if @repeat_penalty
64
+ hash[:seed] = @seed if @seed
65
+ hash
66
+ end
67
+
68
+ private
69
+
70
+ def assign_option(name, value)
71
+ return if value.nil?
72
+
73
+ public_send("#{name}=", value)
74
+ end
75
+
76
+ def validate_numeric_range(value, min, max, name)
77
+ return if value.nil?
78
+
79
+ raise ArgumentError, "#{name} must be numeric, got #{value.class}" unless value.is_a?(Numeric)
80
+
81
+ return if value.between?(min, max)
82
+
83
+ raise ArgumentError, "#{name} must be between #{min} and #{max}, got #{value}"
84
+ end
85
+
86
+ def validate_integer_min(value, min, name)
87
+ return if value.nil?
88
+
89
+ raise ArgumentError, "#{name} must be an integer, got #{value.class}" unless value.is_a?(Integer)
90
+
91
+ return if value >= min
92
+
93
+ raise ArgumentError, "#{name} must be >= #{min}, got #{value}"
94
+ end
95
+
96
+ def validate_integer(value, name)
97
+ return if value.nil?
98
+
99
+ return if value.is_a?(Integer)
100
+
101
+ raise ArgumentError, "#{name} must be an integer, got #{value.class}"
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Ollama
6
+ # Response wrapper for chat_raw() that provides method access to response data
7
+ #
8
+ # Example:
9
+ # response = client.chat_raw(...)
10
+ # response.message&.tool_calls # Access tool_calls
11
+ # response.message&.content # Access content
12
+ class Response
13
+ def initialize(data)
14
+ @data = data
15
+ end
16
+
17
+ # Access the message object
18
+ def message
19
+ msg = @data["message"] || @data[:message]
20
+ return nil unless msg
21
+
22
+ Message.new(msg)
23
+ end
24
+
25
+ # Access raw data as hash
26
+ def to_h
27
+ @data
28
+ end
29
+
30
+ # Delegate other methods to underlying hash
31
+ def method_missing(method, ...)
32
+ return super unless @data.respond_to?(method)
33
+
34
+ @data.public_send(method, ...)
35
+ end
36
+
37
+ def respond_to_missing?(method, include_private = false)
38
+ @data.respond_to?(method, include_private) || super
39
+ end
40
+
41
+ # Message wrapper for accessing message fields
42
+ class Message
43
+ def initialize(data)
44
+ @data = data
45
+ end
46
+
47
+ def content
48
+ @data["content"] || @data[:content]
49
+ end
50
+
51
+ def tool_calls
52
+ calls = @data["tool_calls"] || @data[:tool_calls]
53
+ return [] unless calls
54
+
55
+ calls.map { |call| ToolCall.new(call) }
56
+ end
57
+
58
+ def role
59
+ @data["role"] || @data[:role]
60
+ end
61
+
62
+ def to_h
63
+ @data
64
+ end
65
+
66
+ # ToolCall wrapper for accessing tool call fields
67
+ class ToolCall
68
+ def initialize(data)
69
+ @data = data
70
+ end
71
+
72
+ def id
73
+ @data["id"] || @data[:id] || @data["tool_call_id"] || @data[:tool_call_id]
74
+ end
75
+
76
+ def function
77
+ func = @data["function"] || @data[:function]
78
+ return nil unless func
79
+
80
+ Function.new(func)
81
+ end
82
+
83
+ def name
84
+ function&.name
85
+ end
86
+
87
+ def arguments
88
+ function&.arguments
89
+ end
90
+
91
+ def to_h
92
+ @data
93
+ end
94
+
95
+ # Function wrapper for accessing function fields
96
+ class Function
97
+ def initialize(data)
98
+ @data = data
99
+ end
100
+
101
+ def name
102
+ @data["name"] || @data[:name]
103
+ end
104
+
105
+ def arguments
106
+ args = @data["arguments"] || @data[:arguments]
107
+ return {} unless args
108
+
109
+ args.is_a?(String) ? JSON.parse(args) : args
110
+ rescue JSON::ParserError
111
+ {}
112
+ end
113
+
114
+ def to_h
115
+ @data
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../../dto"
4
+
5
+ module Ollama
6
+ # Tool, Function, and Parameters classes are defined in previous files
7
+ # This file adds Property class to Parameters
8
+ class Tool
9
+ class Function
10
+ class Parameters
11
+ # A single property within the parameters specification
12
+ #
13
+ # Defines an individual parameter with its type, description, and
14
+ # optional enumeration of valid values.
15
+ #
16
+ # Example:
17
+ # property = Ollama::Tool::Function::Parameters::Property.new(
18
+ # type: 'string',
19
+ # description: 'The location to get weather for'
20
+ # )
21
+ #
22
+ # Example with enum:
23
+ # property = Ollama::Tool::Function::Parameters::Property.new(
24
+ # type: 'string',
25
+ # description: 'Temperature unit',
26
+ # enum: %w[celsius fahrenheit]
27
+ # )
28
+ #
29
+ # # Deserialize from hash
30
+ # property = Ollama::Tool::Function::Parameters::Property.from_hash({ type: 'string', ... })
31
+ class Property
32
+ include DTO
33
+
34
+ attr_reader :type, :description, :enum
35
+
36
+ def initialize(type:, description:, enum: nil)
37
+ @type = type.to_s
38
+ @description = description.to_s
39
+ @enum = enum ? Array(enum).map(&:to_s) : nil
40
+ end
41
+
42
+ # Create instance from hash
43
+ #
44
+ # @param hash [Hash] Hash with type, description, and optional enum keys
45
+ # @return [Property] New Property instance
46
+ def self.from_hash(hash)
47
+ normalized = hash.transform_keys(&:to_sym)
48
+ new(
49
+ type: normalized[:type] || normalized["type"],
50
+ description: normalized[:description] || normalized["description"],
51
+ enum: normalized[:enum] || normalized["enum"]
52
+ )
53
+ end
54
+
55
+ def to_h
56
+ hash = {
57
+ "type" => @type,
58
+ "description" => @description
59
+ }
60
+ hash["enum"] = @enum if @enum && !@enum.empty?
61
+ hash
62
+ end
63
+
64
+ # Override as_json to use explicit attributes instead of tracked ones
65
+ def as_json(*_ignored)
66
+ to_h
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../dto"
4
+
5
+ module Ollama
6
+ # Tool and Function classes are defined in tool.rb and tool/function.rb
7
+ # This file adds Parameters class to Function
8
+ class Tool
9
+ # Function metadata and schema for tool calls.
10
+ class Function
11
+ # Parameters specification for a tool function
12
+ #
13
+ # Defines the structure of parameters that a function tool accepts.
14
+ # This matches JSON Schema format used by Ollama's tool calling API.
15
+ #
16
+ # Example:
17
+ # parameters = Ollama::Tool::Function::Parameters.new(
18
+ # type: 'object',
19
+ # properties: {
20
+ # location: Ollama::Tool::Function::Parameters::Property.new(
21
+ # type: 'string',
22
+ # description: 'The location to get weather for'
23
+ # )
24
+ # },
25
+ # required: %w[location]
26
+ # )
27
+ #
28
+ # # Deserialize from hash
29
+ # parameters = Ollama::Tool::Function::Parameters.from_hash({ type: 'object', ... })
30
+ class Parameters
31
+ include DTO
32
+
33
+ attr_reader :type, :properties, :required
34
+
35
+ def initialize(type:, properties: {}, required: [])
36
+ @type = type.to_s
37
+ @properties = normalize_properties(properties)
38
+ @required = Array(required).map(&:to_s)
39
+ end
40
+
41
+ # Create instance from hash
42
+ #
43
+ # @param hash [Hash] Hash with type, properties, and optional required keys
44
+ # @return [Parameters] New Parameters instance
45
+ def self.from_hash(hash)
46
+ normalized = hash.transform_keys(&:to_sym)
47
+ props_hash = normalized[:properties] || normalized["properties"] || {}
48
+
49
+ properties = props_hash.transform_values do |value|
50
+ case value
51
+ when Hash
52
+ Property.from_hash(value)
53
+ when Property
54
+ value
55
+ else
56
+ raise Error, "Invalid property type: #{value.class}. Use Property or Hash"
57
+ end
58
+ end
59
+
60
+ new(
61
+ type: normalized[:type] || normalized["type"],
62
+ properties: properties,
63
+ required: normalized[:required] || normalized["required"] || []
64
+ )
65
+ end
66
+
67
+ def to_h
68
+ hash = { "type" => @type }
69
+ hash["properties"] = @properties.transform_values(&:to_h) unless @properties.empty?
70
+ hash["required"] = @required unless @required.empty?
71
+ hash
72
+ end
73
+
74
+ # Override as_json to use explicit attributes instead of tracked ones
75
+ def as_json(*_ignored)
76
+ to_h
77
+ end
78
+
79
+ private
80
+
81
+ def normalize_properties(props)
82
+ return {} if props.nil? || props.empty?
83
+
84
+ props.transform_values do |value|
85
+ case value
86
+ when Property
87
+ value
88
+ when Hash
89
+ Property.from_hash(value)
90
+ else
91
+ raise Error, "Invalid property type: #{value.class}. Use Property or Hash"
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ # Load Property class after Parameters is fully defined
98
+ require_relative "parameters/property"
99
+ end
100
+ end
101
+ end