omniai 2.5.0 → 2.6.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 89f3eb0b1c62e35bc437833ad64c2c48504ead7bab049b1fc13cbdde30f5c6fe
4
- data.tar.gz: bd8f3e7692996e622e245bd82f4fccc86df703836062d9542d59ec8119e09fe7
3
+ metadata.gz: 6b8a243fb8313c7c1fe3a483cdac33892f1b764cac19c38465a880e3f8a790f0
4
+ data.tar.gz: b24f59696a3befee77cde24b8fd25b88331b3664a327e8e207d9f57a08abb524
5
5
  SHA512:
6
- metadata.gz: eb4748a2dc375e1e36dc096605adacd9822e9f7217ffb5a5c0516bbd21f18228dfdcc329346308ccc9b7a114bc24ee2a5abf04e3c09e06ee50cceac72f8f6d09
7
- data.tar.gz: 6c814845c8fed1e20a042abe94c9ba74227f671a00b9551c79b1b12faedac2a7a998e9a325c942e8b07bdc1ebe55f62ef13563eb982b07ef27b9046695b00295
6
+ metadata.gz: 985aafb6c64874122c77c172ba7dccc1a371a982855b33add51c3371d872db7008391a28382aa7442d640a2f76df1e3ca95426428fb7cd3b12a177d69ed3605e
7
+ data.tar.gz: 42edecdd416fdc53562ea82caf09b94886e761ea2858c28db76dc4e909dd4da87835fe152bbebf3cc87a64a72eded21706c60ba13beba2293fa5470f11e822bd
data/README.md CHANGED
@@ -88,34 +88,62 @@ require 'omniai/google'
88
88
 
89
89
  client = OmniAI::Google::Client.new
90
90
 
91
- class Weather < OmniAI::Tool
92
- description "Lookup the weather for a location"
91
+ class WeatherTool < OmniAI::Tool
92
+ description "Lookup the weather for a lat / lng."
93
93
 
94
- parameter :location, :string, description: "A location (e.g. 'Toronto, Canada')."
94
+ parameter :lat, :number, description: "The latitude of the location."
95
+ parameter :lng, :number, description: "The longitude of the location."
95
96
  parameter :unit, :string, enum: %w[Celsius Fahrenheit], description: "The unit of measurement."
97
+ required %i[lat lng]
98
+
99
+ # @param lat [Float]
100
+ # @param lng [Float]
101
+ # @param unit [String] "Celsius" or "Fahrenheit"
102
+ #
103
+ # @return [String] e.g. "20° Celsius at lat=43.7 lng=-79.4"
104
+ def execute(lat:, lng:, unit: "Celsius")
105
+ puts "[weather] lat=#{lat} lng=#{lng} unit=#{unit}"
106
+ "#{rand(20..50)}° #{unit} at lat=#{lat} lng=#{lng}"
107
+ end
108
+ end
109
+
110
+ class GeocodeTool < OmniAI::Tool
111
+ description "Lookup the latitude and longitude of a location."
112
+
113
+ parameter :location, :string, description: "The location to geocode."
96
114
  required %i[location]
97
115
 
98
- # @param location [String] required
99
- # @param unit [String] optional - "Celsius" or "Fahrenheit"
100
- # @return [String]
101
- def execute(location:, unit: "Celsius")
102
- puts "[weather] location=#{location} unit=#{unit}"
103
- "#{rand(20..50)}° #{unit} at #{location}"
116
+ # @param location [String] "Toronto, Canada"
117
+ #
118
+ # @return [Hash] { lat: Float, lng: Float, location: String }
119
+ def execute(location:)
120
+ puts "[geocode] location=#{location}"
121
+
122
+ {
123
+ lat: rand(-90.0..+90.0),
124
+ lng: rand(-180.0..+180.0),
125
+ location:,
126
+ }
104
127
  end
105
128
  end
106
129
 
107
- client.chat(stream: $stdout, tools: [Weather.new]) do |prompt|
130
+ tools = [
131
+ WeatherTool.new,
132
+ GeocodeTool.new,
133
+ ]
134
+
135
+ client.chat(stream: $stdout, tools:) do |prompt|
108
136
  prompt.system "You are an expert in weather."
109
137
  prompt.user 'What is the weather in "London" in Celsius and "Madrid" in Fahrenheit?'
110
138
  end
111
139
  ```
112
140
 
113
141
  ```
114
- [weather] location=London unit=Celsius
115
- [weather] location=Madrid unit=Fahrenheit
116
- ```
142
+ [geocode] location=London
143
+ [weather] lat=... lng=... unit=Celsius
144
+ [geocode] location=Madrid
145
+ [weather] lat=... lng=... unit=Fahrenheit
117
146
 
118
- ```
119
147
  The weather is 24° Celsius in London and 42° Fahrenheit in Madrid.
120
148
  ```
121
149
 
@@ -146,19 +174,57 @@ loop do
146
174
  end
147
175
  ```
148
176
 
149
- ```
150
- Type 'exit' or 'quit' to leave.
177
+ ### Example #6 [Chat w/ Schema](https://github.com/ksylvest/omniai/blob/main/examples/chat_with_schema)
151
178
 
152
- > What is the capital of France?
153
- The capital of France is Paris.
154
- La capitale de la France est Paris.
179
+ ```ruby
180
+ format = OmniAI::Schema.format(name: "Contact", schema: OmniAI::Schema.object(
181
+ description: "A contact with a name, relationship, and addresses.",
182
+ properties: {
183
+ name: OmniAI::Schema.string,
184
+ relationship: OmniAI::Schema.string(enum: %w[friend family]),
185
+ addresses: OmniAI::Schema.array(
186
+ items: OmniAI::Schema.object(
187
+ title: "Address",
188
+ description: "An address with street, city, state, and zip code.",
189
+ properties: {
190
+ street: OmniAI::Schema.string,
191
+ city: OmniAI::Schema.string,
192
+ state: OmniAI::Schema.string,
193
+ zip: OmniAI::Schema.string,
194
+ },
195
+ required: %i[street city state zip]
196
+ )
197
+ ),
198
+ },
199
+ required: %i[name]
200
+ ))
201
+
202
+ response = client.chat(format:) do |prompt|
203
+ prompt.user <<~TEXT
204
+ Parse the following contact:
205
+
206
+ NAME: George Harrison
207
+ RELATIONSHIP: friend
208
+ HOME: 123 Main St, Springfield, IL, 12345
209
+ WORK: 456 Elm St, Springfield, IL, 12345
210
+ TEXT
211
+ end
212
+
213
+ puts format.parse(response.text)
214
+ ```
155
215
 
156
- > How many people live there?
157
- The population of Paris is approximately 2.1 million.
158
- La population de Paris est d’environ 2,1 million.
216
+ ```
217
+ {
218
+ name: "George Harrison",
219
+ relationship: "friend",
220
+ addresses: [
221
+ { street: "123 Main St", city: "Springfield", state: "IL", zip: "12345" },
222
+ { street: "456 Elm St", city: "Springfield", state: "IL", zip: "12345" },
223
+ ]
224
+ }
159
225
  ```
160
226
 
161
- ### Example #6: [Chat w/ CLI](https://github.com/ksylvest/omniai/blob/main/examples/chat_with_cli)
227
+ ### Example #7: [Chat w/ CLI](https://github.com/ksylvest/omniai/blob/main/examples/chat_with_cli)
162
228
 
163
229
  The `OmniAI` gem also ships with a CLI to simplify quick tests.
164
230
 
@@ -178,7 +244,7 @@ omniai chat --provider="google" --model="gemini-2.0-flash" "Who are you?"
178
244
  I am a large language model, trained by Google.
179
245
  ```
180
246
 
181
- ### Example #7: [Text-to-Speech](https://github.com/ksylvest/omniai/blob/main/examples/text_to_speech)
247
+ ### Example #8: [Text-to-Speech](https://github.com/ksylvest/omniai/blob/main/examples/text_to_speech)
182
248
 
183
249
  This example demonstrates using `OmniAI` with **OpenAI** to convert text to speech and save it to a file.
184
250
 
@@ -194,7 +260,7 @@ File.open(File.join(__dir__, 'audio.wav'), 'wb') do |file|
194
260
  end
195
261
  ```
196
262
 
197
- ### Example #8: [Speech-to-Text](https://github.com/ksylvest/omniai/blob/main/examples/speech_to_text)
263
+ ### Example #9: [Speech-to-Text](https://github.com/ksylvest/omniai/blob/main/examples/speech_to_text)
198
264
 
199
265
  This example demonstrates using `OmniAI` with **OpenAI** to convert speech to text.
200
266
 
@@ -209,7 +275,7 @@ File.open(File.join(__dir__, 'audio.wav'), 'rb') do |file|
209
275
  end
210
276
  ```
211
277
 
212
- ### Example #9: [Embeddings](https://github.com/ksylvest/omniai/blob/main/examples/embeddings)
278
+ ### Example #10: [Embeddings](https://github.com/ksylvest/omniai/blob/main/examples/embeddings)
213
279
 
214
280
  This example demonstrates using `OmniAI` with **Mistral** to generate embeddings for a dataset. It defines a set of entries (e.g. "George is a teacher." or "Ringo is a doctor.") and then compares the embeddings generated from a query (e.g. "What does George do?" or "Who is a doctor?") to rank the entries by relevance.
215
281
 
@@ -426,19 +492,28 @@ client.chat('Tell me a story', stream: $stdout)
426
492
  A chat can also be initialized with tools:
427
493
 
428
494
  ```ruby
429
- tool = OmniAI::Tool.new(
430
- proc { |location:, unit: 'Celsius'| "#{rand(20..50)}° #{unit} in #{location}" },
431
- name: 'Weather',
432
- description: 'Lookup the weather in a location',
433
- parameters: OmniAI::Tool::Parameters.new(
434
- properties: {
435
- location: OmniAI::Tool::Property.string(description: 'e.g. Toronto'),
436
- unit: OmniAI::Tool::Property.string(enum: %w[Celsius Fahrenheit]),
437
- },
438
- required: %i[location]
439
- )
440
- )
441
- client.chat('What is the weather in "London" in Celsius and "Paris" in Fahrenheit?', tools: [tool])
495
+ class WeatherTool
496
+ description "Lookup the weather at a location in either Celsius for Fahrenheit."
497
+
498
+ parameter :location, :string, description: "The location to find the weather."
499
+ parameter :unit, :string, enum: %w[Celsius Fahrenheit], description: "The unit of measurement."
500
+ required %i[location]
501
+
502
+ # @param location [String]
503
+ # @param unit [String] "Celsius" or "Fahrenheit"
504
+ #
505
+ # @return [Hash]
506
+ def execute(location:, unit: "Celsius")
507
+ puts "[weather] location=#{locaiton} unit=#{unit}"
508
+
509
+ {
510
+ temperature: "#{rand(20..50)}°",
511
+ humidty: rand(0..100),
512
+ }
513
+ end
514
+ end
515
+
516
+ client.chat('What is the weather in "London" in Celsius and "Paris" in Fahrenheit?', tools: [WeatherTool.new])
442
517
  ```
443
518
 
444
519
  ### Transcribe
@@ -103,7 +103,13 @@ module OmniAI
103
103
  serializer = context&.serializer(:message)
104
104
  return serializer.call(self, context:) if serializer
105
105
 
106
- content = @content.is_a?(Array) ? @content.map { |content| content.serialize(context:) } : @content
106
+ content =
107
+ case @content
108
+ when Array then @content.map { |content| content.serialize(context:) }
109
+ when Content then @content.serialize(context:)
110
+ else @content
111
+ end
112
+
107
113
  tool_calls = @tool_call_list&.serialize(context:)
108
114
 
109
115
  { role: @role, content:, tool_calls: }.compact
@@ -29,6 +29,8 @@ module OmniAI
29
29
 
30
30
  # @param data [Hash]
31
31
  # @param context [OmniAI::Context] optional
32
+ #
33
+ # @return [OmniAI::Chat::Response]
32
34
  def self.deserialize(data, context: nil)
33
35
  deserialize = context&.deserializer(:response)
34
36
  return deserialize.call(data, context:) if deserialize
data/lib/omniai/chat.rb CHANGED
@@ -65,7 +65,7 @@ module OmniAI
65
65
  # @param temperature [Float, nil] optional
66
66
  # @param stream [Proc, IO, nil] optional
67
67
  # @param tools [Array<OmniAI::Tool>] optional
68
- # @param format [Symbol, nil] optional - :json
68
+ # @param format [:json, :text, OmniAI::Schema::Object, nil] optional
69
69
  #
70
70
  # @yield [prompt] optional
71
71
  # @yieldparam prompt [OmniAI::Chat::Prompt]
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ module Schema
5
+ # @example
6
+ # array = OmniAI::Tool::Array.deserialize({
7
+ # description: "A list of people.",
8
+ # items: {
9
+ # properties: {
10
+ # name: { type: "string" },
11
+ # },
12
+ # required: ["name"],
13
+ # },
14
+ # min_items: 1,
15
+ # max_items: 5,
16
+ # })
17
+ # array.serialize # => { type: "array", items: { ... }, minItems: 1, maxItems: 5 }
18
+ # array.parse([{ "name" => "Ringo Starr" }]) # => [{ name: "Ringo Star" }]
19
+ class Array
20
+ TYPE = "array"
21
+
22
+ # @!attribute [rw] items
23
+ # @return [OmniAI::Schema::Object, OmniAI::Schema::Array, OmniAI::Schema::Scalar]
24
+ attr_accessor :items
25
+
26
+ # @!attribute [rw] max_items
27
+ # @return [Integer, nil]
28
+ attr_accessor :max_items
29
+
30
+ # @!attribute [rw] min_items
31
+ # @return [Integer, nil]
32
+ attr_accessor :min_items
33
+
34
+ # @!attribute [rw] description
35
+ # @return [String, nil]
36
+ attr_accessor :description
37
+
38
+ # @example
39
+ # array = OmniAI::Schema::Array.deserialize({
40
+ # type: "array",
41
+ # items: { type: "string" },
42
+ # minItems: 1,
43
+ # maxItems: 5,
44
+ # description: "A list of strings."
45
+ # }) # => OmniAI::Schema::Array
46
+ #
47
+ # @param data [Hash]
48
+ #
49
+ # @return [OmniAI::Schema::Array]
50
+ def self.deserialize(data)
51
+ new(
52
+ items: OmniAI::Schema.deserialize(data["items"] || data[:items]),
53
+ max_items: data[:maxItems] || data["maxItems"],
54
+ min_items: data[:minItems] || data["minItems"],
55
+ description: data[:description] || data["description"]
56
+ )
57
+ end
58
+
59
+ # @param items [OmniAI::Schema::Object, OmniAI::Schema::Array, OmniAI::Schema::Scalar] required
60
+ # @param min_items [Integer] optional
61
+ # @param max_items [Integer] optional
62
+ # @param description [String] optional
63
+ def initialize(items:, min_items: nil, max_items: nil, description: nil)
64
+ super()
65
+ @items = items
66
+ @min_items = min_items
67
+ @max_items = max_items
68
+ @description = description
69
+ end
70
+
71
+ # @example
72
+ # array.serialize # => { type: "array", items: { type: "string" } }
73
+ #
74
+ # @return [Hash]
75
+ def serialize
76
+ {
77
+ type: TYPE,
78
+ description: @description,
79
+ items: @items.serialize,
80
+ maxItems: @max_items,
81
+ minItems: @min_items,
82
+ }.compact
83
+ end
84
+
85
+ # @example
86
+ # array.parse(["1", "2", "3"]) # => [1, 2, 3]
87
+ #
88
+ # @param data [Array]
89
+ #
90
+ # @return [Array]
91
+ def parse(data)
92
+ data.map { |arg| @items.parse(arg) }
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ module Schema
5
+ # @example
6
+ # format = OmniAI::Schema::Format.deserialize({
7
+ # name: "example",
8
+ # schema: {
9
+ # type: "object",
10
+ # properties: {
11
+ # name: { type: "string" },
12
+ # },
13
+ # required: ["name"],
14
+ # }
15
+ # })
16
+ # format.serialize # => { name: "example", schema: { ... } }
17
+ class Format
18
+ # @!attribute [rw] name
19
+ # @return [String]
20
+ attr_accessor :name
21
+
22
+ # @!attribute [rw] schema
23
+ # @return [OmniAI::Schema::Object]
24
+ attr_accessor :schema
25
+
26
+ # @example
27
+ # array = OmniAI::Schema::Format.deserialize({
28
+ # name: "Contact",
29
+ # schema: { ... },
30
+ # }) # => OmniAI::Schema::Format
31
+ #
32
+ # @param data [Hash]
33
+ #
34
+ # @return [OmniAI::Schema::Format]
35
+ def self.deserialize(data)
36
+ name = data["name"] || data[:name]
37
+ schema = OmniAI::Schema.deserialize(data["schema"] || data[:schema])
38
+
39
+ new(name:, schema:)
40
+ end
41
+
42
+ # @param name [String]
43
+ # @param schema [OmniAI::Schema::Object]
44
+ def initialize(name:, schema:)
45
+ @name = name
46
+ @schema = schema
47
+ end
48
+
49
+ # @example
50
+ # format.serialize # => { name: "...", schema: { ... } }
51
+ #
52
+ # @return [Hash]
53
+ def serialize
54
+ {
55
+ name:,
56
+ schema: schema.serialize,
57
+ }
58
+ end
59
+
60
+ # @example
61
+ # format.parse("{ "name": "Ringo Starr" }"") # => { name: "Ringo Starr" }
62
+ #
63
+ # @param text [String]
64
+ #
65
+ # @return [Hash]
66
+ def parse(text)
67
+ schema.parse(JSON.parse(text))
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ module Schema
5
+ # @example
6
+ # schema = OmniAI::Schema::Object.deserialize({
7
+ # type: "object",
8
+ # properties: {
9
+ # name: { type: "string" }
10
+ # },
11
+ # required: ["name"],
12
+ # })
13
+ # schema.serialize #=> { type: "object", properties: { ... }, required: %i[name] }
14
+ # schema.parse({ "name" => "John Doe" }) #=> { name: "John Doe" }
15
+ class Object
16
+ TYPE = "object"
17
+
18
+ # @!attribute [rw] properties
19
+ # @return [Hash]
20
+ attr_accessor :properties
21
+
22
+ # @!attribute [rw] required
23
+ # @return [Array<String>]
24
+ attr_accessor :required
25
+
26
+ # @!attribute [rw] title
27
+ # @return [String, nil]
28
+ attr_accessor :title
29
+
30
+ # @!attribute [rw] description
31
+ # @return [String, nil]
32
+ attr_accessor :description
33
+
34
+ # @example
35
+ # OmniAI::Schema::Object.deserialize({
36
+ # type: "object",
37
+ # properties: {
38
+ # name: { type: "string" }
39
+ # },
40
+ # required: ["name"],
41
+ # }) # => OmniAI::Schema::Object
42
+ #
43
+ # @param data [Hash]
44
+ #
45
+ # @return [OmniAI::Schema::Object]
46
+ def self.deserialize(data)
47
+ title = data["title"] || data[:title]
48
+ description = data["description"] || data[:description]
49
+ properties = (data["properties"] || data[:properties]).transform_values { |i| OmniAI::Schema.deserialize(i) }
50
+ required = data["required"] || data[:required] || []
51
+
52
+ new(title:, description:, properties:, required:)
53
+ end
54
+
55
+ # @param title [String] optional
56
+ # @param description [String] optional
57
+ # @param properties [Hash] optional
58
+ # @param required [Array<String>] optional
59
+ def initialize(title: nil, description: nil, properties: {}, required: [])
60
+ super()
61
+ @title = title
62
+ @description = description
63
+ @properties = properties
64
+ @required = required
65
+ end
66
+
67
+ # @return [Hash]
68
+ def serialize
69
+ {
70
+ type: TYPE,
71
+ title: @title,
72
+ description: @description,
73
+ properties: @properties.transform_values(&:serialize),
74
+ required: @required,
75
+ }.compact
76
+ end
77
+
78
+ # @param name [Symbol]
79
+ def property(name, ...)
80
+ @properties[name] = OmniAI::Schema::Scalar.build(...)
81
+ end
82
+
83
+ # @param data [Hash]
84
+ #
85
+ # @return [Hash]
86
+ def parse(data)
87
+ result = {}
88
+ @properties.each do |name, property|
89
+ value = data[String(name)]
90
+ result[name.intern] = property.parse(value) unless value.nil?
91
+ end
92
+ result
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ module Schema
5
+ # @example
6
+ # scalar = OmniAI::Schema::Scalar.deserialize({ type: "string" })
7
+ # scalar.serialize #=> { type: "string" }
8
+ # scalar.parse("Hello World") #=> "Hello World"
9
+ #
10
+ # @example
11
+ # scalar = OmniAI::Schema::Scalar.deserialize({ type: "integer" })
12
+ # scalar.serialize #=> { type: "integer" }
13
+ # scalar.parse("123") #=> 123
14
+ #
15
+ # @example
16
+ # scalar = OmniAI::Schema::Scalar.deserialize({ type: "number" })
17
+ # scalar.serialize #=> { type: "number" }
18
+ # scalar.parse("123.45") #=> 123.45
19
+ #
20
+ # @example
21
+ # scalar = OmniAI::Schema::Scalar.deserialize({ type: "boolean" })
22
+ # scalar.serialize #=> { type: "boolean" }
23
+ # scalar.parse(true) #=> true
24
+ # scalar.parse(false) #=> false
25
+ class Scalar
26
+ module Type
27
+ BOOLEAN = "boolean"
28
+ INTEGER = "integer"
29
+ STRING = "string"
30
+ NUMBER = "number"
31
+ end
32
+
33
+ # @!attribute [rw] type
34
+ # @return [String]
35
+ attr_accessor :type
36
+
37
+ # @!attribute [rw] description
38
+ # @return [String, nil]
39
+ attr_accessor :description
40
+
41
+ # @!attribute [rw] enum
42
+ # @return [Array<String>, nil]
43
+ attr_accessor :enum
44
+
45
+ # @example
46
+ # property = OmniAI::Schema::Scalar.deserialize({
47
+ # type: "string",
48
+ # description: "A predefined color.",
49
+ # enum: %w[red organge yellow green blue indigo violet],
50
+ # })
51
+ #
52
+ # @example
53
+ # property = OmniAI::Schema::Scalar.deserialize({
54
+ # type: "integer",
55
+ # })
56
+ #
57
+ # @example
58
+ # property = OmniAI::Schema::Scalar.deserialize({
59
+ # type: "number",
60
+ # })
61
+ #
62
+ # @example
63
+ # property = OmniAI::Schema::Scalar.deserialize({
64
+ # type: "boolean",
65
+ # })
66
+ #
67
+ # @param data [Hash]
68
+ #
69
+ # @return [OmniAI::Schema::Scalar]
70
+ def self.deserialize(data)
71
+ type = data["type"] || data[:type] || Type::STRING
72
+ description = data["description"] || data[:description]
73
+ enum = data["enum"] || data[:enum]
74
+
75
+ new(type:, description:, enum:)
76
+ end
77
+
78
+ # @param type [String] required - the type of the property
79
+ # @param description [String] optional - a description of the property
80
+ # @param enum [Array] optional - the possible values of the property
81
+ #
82
+ # @return [OmniAI::Schema::Scalar]
83
+ def initialize(type:, description: nil, enum: nil)
84
+ super()
85
+ @type = type
86
+ @description = description
87
+ @enum = enum
88
+ end
89
+
90
+ # @example
91
+ # property.serialize #=> { type: "string" }
92
+ #
93
+ # @return [Hash]
94
+ def serialize
95
+ {
96
+ type: @type,
97
+ description: @description,
98
+ enum: @enum,
99
+ }.compact
100
+ end
101
+
102
+ # @example
103
+ # property.parse("123") #=> 123
104
+ #
105
+ # @param value [String, Integer, Float, Boolean, Object]
106
+ #
107
+ # @return [String, Integer, Float, Boolean, Object]
108
+ def parse(value)
109
+ case @type
110
+ when Type::INTEGER then Integer(value)
111
+ when Type::STRING then String(value)
112
+ when Type::NUMBER then Float(value)
113
+ else value
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ # @example
5
+ # format = OmniAI::Schema.format(name: "Contact", schema: OmniAI::Schema.object(
6
+ # description: "A contact with a name, relationship, and addresses.",
7
+ # properties: {
8
+ # name: OmniAI::Schema.string,
9
+ # relationship: OmniAI::Schema.string(enum: %w[friend family]),
10
+ # addresses: OmniAI::Schema.array(
11
+ # items: OmniAI::Schema.object(
12
+ # title: "Address",
13
+ # description: "An address with street, city, state, and zip code.",
14
+ # properties: {
15
+ # street: OmniAI::Schema.string,
16
+ # city: OmniAI::Schema.string,
17
+ # state: OmniAI::Schema.string,
18
+ # zip: OmniAI::Schema.string,
19
+ # },
20
+ # required: %i[street city state zip]
21
+ # )
22
+ # ),
23
+ # },
24
+ # required: %i[name]
25
+ # ))
26
+ module Schema
27
+ # @example
28
+ # OmniAI::Schema.deserialize({
29
+ # type: 'object',
30
+ # title: 'Person',
31
+ # properties: { name: { type: 'string' } },
32
+ # required: %i[name],
33
+ # }) # => OmniAI::Schema::Object
34
+ #
35
+ # @example
36
+ # OmniAI::Schema.deserialize({
37
+ # type: 'array',
38
+ # items: { type: 'string' },
39
+ # }) # => OmniAI::Schema::Array
40
+ #
41
+ # @example
42
+ # OmniAI::Schema.deserialize({
43
+ # type: 'string',
44
+ # description: '...',
45
+ # enum: ['...', ],
46
+ # }) # => OmniAI::Schema::Scalar
47
+ #
48
+ # @param data [OmniAI::Schema::Object, OmniAI::Schema::Array, OmniAI::Schema::Scalar]
49
+ def self.deserialize(data)
50
+ case data["type"] || data[:type]
51
+ when OmniAI::Schema::Array::TYPE then OmniAI::Schema::Array.deserialize(data)
52
+ when OmniAI::Schema::Object::TYPE then OmniAI::Schema::Object.deserialize(data)
53
+ else OmniAI::Schema::Scalar.deserialize(data)
54
+ end
55
+ end
56
+
57
+ # @param kind [Symbol]
58
+ #
59
+ # @return [OmniAI::Schema::Object, OmniAI::Schema::Array, OmniAI::Schema::Scalar]
60
+ def self.build(kind, **args)
61
+ case kind
62
+ when :array then array(**args)
63
+ when :object then object(**args)
64
+ when :boolean then boolean(**args)
65
+ when :integer then integer(**args)
66
+ when :number then number(**args)
67
+ when :string then string(**args)
68
+ end
69
+ end
70
+
71
+ # @example
72
+ # property = OmniAI::Schema.array(
73
+ # items: OmniAI::Schema.string(description: 'The name of the person.'),
74
+ # description: 'A list of names.'
75
+ # min_items: 1,
76
+ # max_items: 5,
77
+ # )
78
+ #
79
+ # @param items [OmniAI::Schema::Scalar] required - the items in the array
80
+ # @param min_items [Integer] optional - the minimum number of items
81
+ # @param max_items [Integer] optional - the maximum number of items
82
+ # @param description [String] optional - a description of the array
83
+ #
84
+ # @return [OmniAI::Schema::Array]
85
+ def self.array(items:, min_items: nil, max_items: nil, description: nil)
86
+ OmniAI::Schema::Array.new(items:, description:, min_items:, max_items:)
87
+ end
88
+
89
+ # @example
90
+ # property = OmniAI::Schema.object(
91
+ # properties: {
92
+ # name: OmniAI::Schema.string(description: 'The name of the person.'),
93
+ # age: OmniAI::Schema.integer(description: 'The age of the person.'),
94
+ # employed: OmniAI::Schema.boolean(description: 'Is the person employed?'),
95
+ # },
96
+ # description: 'A person.'
97
+ # required: %i[name]
98
+ # )
99
+ #
100
+ # @param title [String] optional - the title of the object
101
+ # @param properties [Hash<String, OmniAI::Schema::Scalar>] required - the properties of the object
102
+ # @param required [Array<Symbol>] optional - the required properties
103
+ # @param description [String] optional - a description of the object
104
+ #
105
+ # @return [OmniAI::Schema::Array]
106
+ def self.object(title: nil, properties: {}, required: [], description: nil)
107
+ OmniAI::Schema::Object.new(title:, properties:, required:, description:)
108
+ end
109
+
110
+ # @example
111
+ # OmniAI::Schema.boolean(description: "Is the person employed?") #=> OmniAI::Schema::Scalar
112
+ #
113
+ # @param description [String] optional - a description of the property
114
+ # @param enum [Array<Boolean>] optional - the possible values of the property
115
+ #
116
+ # @return [OmniAI::Schema::Scalar]
117
+ def self.boolean(description: nil, enum: nil)
118
+ OmniAI::Schema::Scalar.new(type: OmniAI::Schema::Scalar::Type::BOOLEAN, description:, enum:)
119
+ end
120
+
121
+ # @example
122
+ # OmniAI::Schema.integer(description: "The age of the person") #=> OmniAI::Schema::Scalar
123
+ #
124
+ # @param description [String] optional - a description of the property
125
+ # @param enum [Array<Integer>] optinoal - the possible values of the property
126
+ #
127
+ # @return [OmniAI::Schema::Scalar]
128
+ def self.integer(description: nil, enum: nil)
129
+ OmniAI::Schema::Scalar.new(type: OmniAI::Schema::Scalar::Type::INTEGER, description:, enum:)
130
+ end
131
+
132
+ # @example
133
+ # OmniAI::Schema.string(description: "The name of the person.") #=> OmniAI::Schema::Scalar
134
+ #
135
+ # @param description [String] optional - a description of the property
136
+ # @param enum [Array<String>] optional - the possible values of the property
137
+ #
138
+ # @return [OmniAI::Schema::Scalar]
139
+ def self.string(description: nil, enum: nil)
140
+ OmniAI::Schema::Scalar.new(type: OmniAI::Schema::Scalar::Type::STRING, description:, enum:)
141
+ end
142
+
143
+ # @example
144
+ # OmniAI::Schema.number(description: "The current temperature.") #=> OmniAI::Schema::Scalar
145
+ #
146
+ # @param description [String] optional - a description of the property
147
+ # @param enum [Array<Number>] optional - the possible values of the property
148
+ #
149
+ # @return [OmniAI::Schema::Scalar]
150
+ def self.number(description: nil, enum: nil)
151
+ OmniAI::Schema::Scalar.new(type: OmniAI::Schema::Scalar::Type::NUMBER, description:, enum:)
152
+ end
153
+
154
+ # @example
155
+ # OmniAI::Schema.format(name: "Contact", schema: OmniAI::Schema.object(...)) #=> OmniAI::Schema::Format
156
+ #
157
+ # @param name [String] required
158
+ # @param schema [OmniAI::Schema::Object] required
159
+ #
160
+ # @return [OmniAI::Schema::Format]
161
+ def self.format(name:, schema:)
162
+ OmniAI::Schema::Format.new(name:, schema:)
163
+ end
164
+ end
165
+ end
data/lib/omniai/tool.rb CHANGED
@@ -25,15 +25,15 @@ module OmniAI
25
25
  @description = description
26
26
  end
27
27
 
28
- # @return [OmniAI::Tool::Parameters]
28
+ # @return [OmniAI::Schema::Object]
29
29
  def parameters
30
- @parameters ||= Parameters.new
30
+ @parameters ||= OmniAI::Schema::Object.new
31
31
  end
32
32
 
33
33
  # @param name [Symbol]
34
34
  # @param kind [Symbol]
35
35
  def parameter(name, kind, **)
36
- parameters.properties[name] = Property.build(kind, **)
36
+ parameters.properties[name] = OmniAI::Schema.build(kind, **)
37
37
  end
38
38
 
39
39
  # @param names [Array<Symbol>]
@@ -118,7 +118,7 @@ module OmniAI
118
118
  function: {
119
119
  name: @name,
120
120
  description: @description,
121
- parameters: @parameters.is_a?(Tool::Parameters) ? @parameters.serialize : @parameters,
121
+ parameters: @parameters.is_a?(Schema::Object) ? @parameters.serialize : @parameters,
122
122
  }.compact,
123
123
  }
124
124
  end
@@ -134,7 +134,7 @@ module OmniAI
134
134
  # @param args [Hash]
135
135
  # @return [String]
136
136
  def call(args = {})
137
- @function.call(**(@parameters.is_a?(Tool::Parameters) ? @parameters.parse(args) : args))
137
+ @function.call(**(@parameters.is_a?(Schema::Object) ? @parameters.parse(args) : args))
138
138
  end
139
139
  end
140
140
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OmniAI
4
- VERSION = "2.5.0"
4
+ VERSION = "2.6.0"
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omniai
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.5.0
4
+ version: 2.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin Sylvestre
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-04-24 00:00:00.000000000 Z
10
+ date: 2025-05-08 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: event_stream_parser
@@ -119,13 +119,14 @@ files:
119
119
  - lib/omniai/mcp/server.rb
120
120
  - lib/omniai/mcp/transport/base.rb
121
121
  - lib/omniai/mcp/transport/stdio.rb
122
+ - lib/omniai/schema.rb
123
+ - lib/omniai/schema/array.rb
124
+ - lib/omniai/schema/format.rb
125
+ - lib/omniai/schema/object.rb
126
+ - lib/omniai/schema/scalar.rb
122
127
  - lib/omniai/speak.rb
123
128
  - lib/omniai/ssl_error.rb
124
129
  - lib/omniai/tool.rb
125
- - lib/omniai/tool/array.rb
126
- - lib/omniai/tool/object.rb
127
- - lib/omniai/tool/parameters.rb
128
- - lib/omniai/tool/property.rb
129
130
  - lib/omniai/transcribe.rb
130
131
  - lib/omniai/transcribe/transcription.rb
131
132
  - lib/omniai/version.rb
@@ -1,74 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OmniAI
4
- class Tool
5
- # Represents a schema object.
6
- #
7
- # @example
8
- # array = OmniAI::Tool::Array.new(
9
- # description: 'A list of people.',
10
- # items: OmniAI::Tool::Object.new(
11
- # properties: {
12
- # name: OmniAI::Tool::Property.string(description: 'The name of the person.'),
13
- # age: OmniAI::Tool::Property.integer(description: 'The age of the person.'),
14
- # },
15
- # required: %i[name]
16
- # ),
17
- # min_items: 1,
18
- # max_items: 5,
19
- # })
20
- class Array
21
- TYPE = "array"
22
-
23
- # @!attribute [rw] items
24
- # @return [OmniAI::Tool::Object, OmniAI::Tool::Array, OmniAI::Tool::Property]
25
- attr_accessor :items
26
-
27
- # @!attribute [rw] max_items
28
- # @return [Integer, nil]
29
- attr_accessor :max_items
30
-
31
- # @!attribute [rw] min_items
32
- # @return [Integer, nil]
33
- attr_accessor :min_items
34
-
35
- # @!attribute [rw] description
36
- # @return [String, nil]
37
- attr_accessor :description
38
-
39
- # @param items [OmniAI::Tool::Object, OmniAI::Tool::Array, OmniAI::Tool::Property] required
40
- # @param max_items [Integer] optional
41
- # @param min_items [Integer] optional
42
- # @param description [String] optional
43
- def initialize(items:, max_items: nil, min_items: nil, description: nil)
44
- @items = items
45
- @description = description
46
- @max_items = max_items
47
- @min_items = min_items
48
- end
49
-
50
- # @example
51
- # array.serialize # => { type: 'array', items: { type: 'string' } }
52
- #
53
- # @return [Hash]
54
- def serialize
55
- {
56
- type: TYPE,
57
- description: @description,
58
- items: @items.serialize,
59
- maxItems: @max_items,
60
- minItems: @min_items,
61
- }.compact
62
- end
63
-
64
- # @example
65
- # array.parse(['1', '2', '3']) # => [1, 2, 3]
66
- # @param args [Array]
67
- #
68
- # @return [Array]
69
- def parse(args)
70
- args.map { |arg| @items.parse(arg) }
71
- end
72
- end
73
- end
74
- end
@@ -1,62 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OmniAI
4
- class Tool
5
- # Represents a schema object.
6
- #
7
- # @example
8
- # object = OmniAI::Tool::Object.new(
9
- # properties: {
10
- # name: OmniAI::Tool::Property.string(description: 'The name of the person.'),
11
- # age: OmniAI::Tool::Property.integer(description: 'The age of the person.'),
12
- # },
13
- # required: %i[name]
14
- # })
15
- class Object
16
- TYPE = "object"
17
-
18
- # @!attribute [rw] properties
19
- # @return [Hash]
20
- attr_accessor :properties
21
-
22
- # @!attribute [rw] required
23
- # @return [Array<String>]
24
- attr_accessor :required
25
-
26
- # @!attribute [rw] description
27
- # @return [String, nil]
28
- attr_accessor :description
29
-
30
- # @param properties [Hash]
31
- # @param required [Array<String>]
32
- # @return [OmniAI::Tool::Parameters]
33
- def initialize(properties: {}, required: [], description: nil)
34
- @properties = properties
35
- @required = required
36
- @description = description
37
- end
38
-
39
- # @return [Hash]
40
- def serialize
41
- {
42
- type: TYPE,
43
- description: @description,
44
- properties: @properties.transform_values(&:serialize),
45
- required: @required,
46
- }.compact
47
- end
48
-
49
- # @param args [Hash]
50
- #
51
- # @return [Hash]
52
- def parse(args)
53
- result = {}
54
- @properties.each do |name, property|
55
- value = args[String(name)]
56
- result[name.intern] = property.parse(value) unless value.nil?
57
- end
58
- result
59
- end
60
- end
61
- end
62
- end
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OmniAI
4
- class Tool
5
- # Parameters are used to define the arguments for a tool.
6
- #
7
- # @example
8
- # parameters = OmniAI::Tool::Parameters.new(properties: {
9
- # people: OmniAI::Tool::Parameters.array(
10
- # items: OmniAI::Tool::Parameters.object(
11
- # properties: {
12
- # name: OmniAI::Tool::Parameters.string(description: 'The name of the person.'),
13
- # age: OmniAI::Tool::Parameters.integer(description: 'The age of the person.'),
14
- # employed: OmniAI::Tool::Parameters.boolean(description: 'Is the person employed?'),
15
- # }
16
- # n: OmniAI::Tool::Parameters.integer(description: 'The nth number to calculate.')
17
- # required: %i[n]
18
- # })
19
- # tool = OmniAI::Tool.new(fibonacci, parameters: parameters)
20
- class Parameters < Object
21
- end
22
- end
23
- end
@@ -1,151 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OmniAI
4
- class Tool
5
- # A property used for a tool parameter.
6
- #
7
- # @example
8
- # OmniAI::Tool::Property.array(description: '...', items: ...)
9
- # OmniAI::Tool::Property.object(description: '...', properties: { ... }, required: %i[...])
10
- # OmniAI::Tool::Property.string(description: '...')
11
- # OmniAI::Tool::Property.integer(description: '...')
12
- # OmniAI::Tool::Property.number(description: '...')
13
- # OmniAI::Tool::Property.boolean(description: '...')
14
- class Property
15
- module Type
16
- BOOLEAN = "boolean"
17
- INTEGER = "integer"
18
- STRING = "string"
19
- NUMBER = "number"
20
- end
21
-
22
- # @return [String]
23
- attr_reader :type
24
-
25
- # @return [String, nil]
26
- attr_reader :description
27
-
28
- # @return [Array<String>, nil]
29
- attr_reader :enum
30
-
31
- # @param kind [Symbol]
32
- # @return [OmniAI::Tool::Property]
33
- def self.build(kind, **args)
34
- case kind
35
- when :array then array(**args)
36
- when :object then object(**args)
37
- when :boolean then boolean(**args)
38
- when :integer then integer(**args)
39
- when :string then string(**args)
40
- when :number then number(**args)
41
- end
42
- end
43
-
44
- # @example
45
- # property = OmniAI::Tool::Property.array(
46
- # items: OmniAI::Tool::Property.string(description: 'The name of the person.'),
47
- # description: 'A list of names.'
48
- # min_items: 1,
49
- # max_items: 5,
50
- # )
51
- #
52
- # @param items [OmniAI::Tool::Property] required - the items in the array
53
- # @param min_items [Integer] optional - the minimum number of items
54
- # @param max_items [Integer] optional - the maximum number of items
55
- # @param description [String] optional - a description of the array
56
- #
57
- # @return [OmniAI::Tool::Array]
58
- def self.array(items:, min_items: nil, max_items: nil, description: nil)
59
- OmniAI::Tool::Array.new(items:, description:, min_items:, max_items:)
60
- end
61
-
62
- # @example
63
- # property = OmniAI::Tool::Property.object(
64
- # properties: {
65
- # name: OmniAI::Tool::Property.string(description: 'The name of the person.'),
66
- # age: OmniAI::Tool::Property.integer(description: 'The age of the person.'),
67
- # employed: OmniAI::Tool::Property.boolean(description: 'Is the person employed?'),
68
- # },
69
- # description: 'A person.'
70
- # required: %i[name]
71
- # )
72
- #
73
- # @param properties [Hash<String, OmniAI::Tool::Property>] required - the properties of the object
74
- # @param required [Array<Symbol>] optional - the required properties
75
- # @param description [String] optional - a description of the object
76
- #
77
- # @return [OmniAI::Tool::Array]
78
- def self.object(properties: {}, required: [], description: nil)
79
- OmniAI::Tool::Object.new(properties:, required:, description:)
80
- end
81
-
82
- # @param description [String] optional - a description of the property
83
- # @param enum [Array<Boolean>] optional - the possible values of the property
84
- #
85
- # @return [OmniAI::Tool::Property]
86
- def self.boolean(description: nil, enum: nil)
87
- new(type: Type::BOOLEAN, description:, enum:)
88
- end
89
-
90
- # @param description [String] optional - a description of the property
91
- # @param enum [Array<Integer>] optinoal - the possible values of the property
92
- #
93
- # @return [OmniAI::Tool::Property]
94
- def self.integer(description: nil, enum: nil)
95
- new(type: Type::INTEGER, description:, enum:)
96
- end
97
-
98
- # @param description [String] optional - a description of the property
99
- # @param enum [Array<String>] optional - the possible values of the property
100
- #
101
- # @return [OmniAI::Tool::Property]
102
- def self.string(description: nil, enum: nil)
103
- new(type: Type::STRING, description:, enum:)
104
- end
105
-
106
- # @param description [String] optional - a description of the property
107
- # @param enum [Array<Number>] optional - the possible values of the property
108
- #
109
- # @return [OmniAI::Tool::Property]
110
- def self.number(description: nil, enum: nil)
111
- new(type: Type::NUMBER, description:, enum:)
112
- end
113
-
114
- # @param type [String] required - the type of the property
115
- # @param description [String] optional - a description of the property
116
- # @param enum [Array] optional - the possible values of the property
117
- #
118
- # @return [OmniAI::Tool::Property]
119
- def initialize(type:, description: nil, enum: nil)
120
- @type = type
121
- @description = description
122
- @enum = enum
123
- end
124
-
125
- # @example
126
- # property.serialize #=> { type: 'string' }
127
- #
128
- # @return [Hash]
129
- def serialize
130
- {
131
- type: @type,
132
- description: @description,
133
- enum: @enum,
134
- }.compact
135
- end
136
-
137
- # @example
138
- # property.parse('123') #=> 123
139
- #
140
- # @return [String, Integer, Float, Boolean, Object]
141
- def parse(value)
142
- case @type
143
- when Type::INTEGER then Integer(value)
144
- when Type::STRING then String(value)
145
- when Type::NUMBER then Float(value)
146
- else value
147
- end
148
- end
149
- end
150
- end
151
- end