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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +220 -12
- data/docs/CLOUD.md +29 -0
- data/docs/CONSOLE_IMPROVEMENTS.md +256 -0
- data/docs/FEATURES_ADDED.md +145 -0
- data/docs/HANDLERS_ANALYSIS.md +190 -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 +13 -7
- data/examples/chat_console.rb +143 -0
- data/examples/complete_workflow.rb +14 -4
- data/examples/dhan_console.rb +843 -0
- data/examples/dhanhq/agents/base_agent.rb +0 -2
- data/examples/dhanhq/agents/orchestrator_agent.rb +1 -2
- data/examples/dhanhq/agents/technical_analysis_agent.rb +67 -49
- data/examples/dhanhq/analysis/market_structure.rb +44 -28
- data/examples/dhanhq/analysis/pattern_recognizer.rb +64 -47
- data/examples/dhanhq/analysis/trend_analyzer.rb +6 -8
- data/examples/dhanhq/dhanhq_agent.rb +296 -99
- data/examples/dhanhq/indicators/technical_indicators.rb +3 -5
- data/examples/dhanhq/scanners/intraday_options_scanner.rb +360 -255
- data/examples/dhanhq/scanners/swing_scanner.rb +118 -84
- data/examples/dhanhq/schemas/agent_schemas.rb +2 -2
- data/examples/dhanhq/services/data_service.rb +5 -7
- data/examples/dhanhq/services/trading_service.rb +0 -3
- data/examples/dhanhq/technical_analysis_agentic_runner.rb +217 -84
- data/examples/dhanhq/technical_analysis_runner.rb +216 -162
- data/examples/dhanhq/test_tool_calling.rb +538 -0
- data/examples/dhanhq/test_tool_calling_verbose.rb +251 -0
- data/examples/dhanhq/utils/trading_parameter_normalizer.rb +12 -17
- data/examples/dhanhq_agent.rb +159 -116
- data/examples/dhanhq_tools.rb +1158 -251
- data/examples/multi_step_agent_with_external_data.rb +368 -0
- data/examples/structured_tools.rb +89 -0
- data/examples/test_dhanhq_tool_calling.rb +375 -0
- data/examples/test_tool_calling.rb +160 -0
- data/examples/tool_calling_direct.rb +124 -0
- data/examples/tool_dto_example.rb +94 -0
- data/exe/dhan_console +4 -0
- data/exe/ollama-client +1 -1
- data/lib/ollama/agent/executor.rb +116 -15
- data/lib/ollama/client.rb +118 -55
- data/lib/ollama/config.rb +36 -0
- data/lib/ollama/dto.rb +187 -0
- data/lib/ollama/embeddings.rb +77 -0
- data/lib/ollama/options.rb +104 -0
- data/lib/ollama/response.rb +121 -0
- data/lib/ollama/tool/function/parameters/property.rb +72 -0
- data/lib/ollama/tool/function/parameters.rb +101 -0
- data/lib/ollama/tool/function.rb +78 -0
- data/lib/ollama/tool.rb +60 -0
- data/lib/ollama/version.rb +1 -1
- data/lib/ollama_client.rb +3 -0
- metadata +31 -3
- /data/{PRODUCTION_FIXES.md → docs/PRODUCTION_FIXES.md} +0 -0
- /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
|