omniai 1.4.2 → 1.5.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: df4f1096ec0d74c8ab26e5cfc8db4902009d1bb398ed9913e801b31203d1ae66
4
- data.tar.gz: 9cff5f3ecdc6a540bbbd3ca6501dbd07585a1f9dfe8127a09ee6421a51f79c2f
3
+ metadata.gz: c05ed0777b6e88c023738b43e5188d722ca9bf372a7cabda61a800ae5a7a13a7
4
+ data.tar.gz: e2f0220221030ce99bc81dfd456a7505bce557594ba03336d5445ff4a7188b7d
5
5
  SHA512:
6
- metadata.gz: db82f7cf3e8b43f55080c1ae1af2f6f9767dec7870bf23e2424eee8a272671b565f279829d9768fbb447eec8821fcd7da22058da8a4016b4de271a389a115c1c
7
- data.tar.gz: 84362d233dbafa2dd6403b3d65056c7b836eb0b2ce420f4395c86188887f12ba7cb59caead7cfbc44d80bfa03ad2ef342b62f8235ace58afd0284e8e25b5c243
6
+ metadata.gz: 9ec41c146e0052129894ad17fdcbd8768b96d76114f85604098153c901a842b4d59cc78121ad5e77a7a7501c17c3caf060ed2bcf4c05fea3523fc8c081f8479b
7
+ data.tar.gz: 11c641f64597f09bff8d67dd00b5599701868c7b31bbb12675191cd554e94b051c97e6f7251e355dcf849b9693f34dc0f29c8b8b0ac92a77fa0ff00ca6c2d05e
data/Gemfile CHANGED
@@ -13,3 +13,4 @@ gem 'rubocop-rake'
13
13
  gem 'rubocop-rspec'
14
14
  gem 'simplecov'
15
15
  gem 'webmock'
16
+ gem 'yard'
data/README.md CHANGED
@@ -143,7 +143,9 @@ completion = client.chat(messages, model: '...', temperature: 0.7, format: :json
143
143
  completion.choice.message.content # '...'
144
144
  ```
145
145
 
146
- #### Completions using Real-Time Streaming
146
+ #### Completions using Streaming via Proc
147
+
148
+ A real-time stream of messages can be generated by passing in a proc:
147
149
 
148
150
  ```ruby
149
151
  stream = proc do |chunk|
@@ -152,6 +154,35 @@ end
152
154
  client.chat('Tell me a joke.', stream:)
153
155
  ```
154
156
 
157
+ #### Completion using Streaming via IO
158
+
159
+ The above code can also be supplied any IO (e.g. `File`, `$stdout`, `$stdin`, etc):
160
+
161
+ ```ruby
162
+ client.chat('Tell me a story', stream: $stdout)
163
+ ```
164
+
165
+ #### Completion with Tools
166
+
167
+ A chat can also be initialized with tools:
168
+
169
+ ```ruby
170
+ client.chat('What is the weather in "London, England" and "Madrid, Spain"?', tools: [
171
+ OmniAI::Tool.new(
172
+ proc { |location:, unit: 'celsius'| "It is #{rand(20..50)}° #{unit} in #{location}" },
173
+ name: 'Weather',
174
+ description: 'Lookup the weather in a location',
175
+ parameters: OmniAI::Tool::Parameters.new(
176
+ properties: {
177
+ location: OmniAI::Tool::Property.string(description: 'The city and country (e.g. Toronto, Canada).'),
178
+ unit: OmniAI::Tool::Property.string(enum: %w[celcius farenheit]),
179
+ },
180
+ required: %i[location]
181
+ )
182
+ )
183
+ ])
184
+ ```
185
+
155
186
  ### Transcribe
156
187
 
157
188
  Clients that support transcribe (e.g. OpenAI w/ "Whisper") convert recordings to text via the following calls:
@@ -33,7 +33,7 @@ module OmniAI
33
33
 
34
34
  # @return [Array<OmniAI::Chat::DeltaChoice>]
35
35
  def choices
36
- @choices ||= @data['choices'].map { |data| DeltaChoice.for(data:) }
36
+ @choices ||= @data['choices'].map { |data| DeltaChoice.new(data:) }
37
37
  end
38
38
 
39
39
  # @param index [Integer]
@@ -11,6 +11,11 @@ module OmniAI
11
11
  @data = data
12
12
  end
13
13
 
14
+ # @return [String]
15
+ def inspect
16
+ "#<#{self.class.name} id=#{id.inspect} choices=#{choices.inspect}"
17
+ end
18
+
14
19
  # @return [String]
15
20
  def id
16
21
  @data['id']
@@ -33,14 +38,12 @@ module OmniAI
33
38
 
34
39
  # @return [OmniAI::Chat::Usage]
35
40
  def usage
36
- return unless @data['usage']
37
-
38
- @usage ||= Usage.for(data: @data['usage'])
41
+ @usage ||= Usage.new(data: @data['usage']) if @data['usage']
39
42
  end
40
43
 
41
44
  # @return [Array<OmniAI::Chat::MessageChoice>]
42
45
  def choices
43
- @choices ||= @data['choices'].map { |data| MessageChoice.for(data:) }
46
+ @choices ||= @data['choices'].map { |data| MessageChoice.new(data:) }
44
47
  end
45
48
 
46
49
  # @param index [Integer] optional - default is 0
@@ -48,6 +51,20 @@ module OmniAI
48
51
  def choice(index: 0)
49
52
  choices[index]
50
53
  end
54
+
55
+ # @return [Boolean]
56
+ def tool_call_required?
57
+ choices.any? { |choice| choice.message.tool_call_list.any? }
58
+ end
59
+
60
+ # @return [Array<OmniAI::Chat::ToolCall>]
61
+ def tool_call_list
62
+ list = []
63
+ choices.each do |choice|
64
+ list += choice.message.tool_call_list
65
+ end
66
+ list
67
+ end
51
68
  end
52
69
  end
53
70
  end
@@ -4,27 +4,24 @@ module OmniAI
4
4
  class Chat
5
5
  # A delta returned by the API.
6
6
  class Delta
7
- attr_accessor :role, :content
8
-
9
7
  # @param data [Hash]
10
- # @return [OmniAI::Chat::Message]
11
- def self.for(data:)
12
- content = data['content'] || data[:content]
13
- role = data['role'] || data[:role]
8
+ def initialize(data:)
9
+ @data = data
10
+ end
14
11
 
15
- new(content:, role: role || Role::USER)
12
+ # @return [String]
13
+ def inspect
14
+ "#<#{self.class.name} role=#{role.inspect} content=#{content.inspect}>"
16
15
  end
17
16
 
18
- # @param content [String]
19
- # @param role [String]
20
- def initialize(content:, role: nil)
21
- @content = content
22
- @role = role
17
+ # @return [String, nil]
18
+ def content
19
+ @data['content']
23
20
  end
24
21
 
25
22
  # @return [String]
26
- def inspect
27
- "#<#{self.class.name} role=#{role.inspect} content=#{content.inspect}>"
23
+ def role
24
+ @data['role'] || Role::USER
28
25
  end
29
26
  end
30
27
  end
@@ -4,22 +4,24 @@ module OmniAI
4
4
  class Chat
5
5
  # A delta choice returned by the API.
6
6
  class DeltaChoice
7
- attr_accessor :index, :delta
8
-
9
7
  # @param data [Hash]
10
- # @return [OmniAI::Chat::Choice]
11
- def self.for(data:)
12
- index = data['index']
13
- delta = Delta.for(data: data['delta'])
8
+ def initialize(data:)
9
+ @data = data
10
+ end
11
+
12
+ # @return [Integer]
13
+ def index
14
+ @data['index']
15
+ end
14
16
 
15
- new(index:, delta:)
17
+ # @return [OmniAI::Chat::Delta]
18
+ def delta
19
+ @delta ||= Delta.new(data: @data['delta'])
16
20
  end
17
21
 
18
- # @param index [Integer]
19
- # @param delta [Delta]
20
- def initialize(index:, delta:)
21
- @index = index
22
- @delta = delta
22
+ # @return [String]
23
+ def inspect
24
+ "#<#{self.class.name} index=#{index} delta=#{delta.inspect}>"
23
25
  end
24
26
  end
25
27
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ class Chat
5
+ # A function returned by the API.
6
+ class Function
7
+ # @param data [Hash]
8
+ def initialize(data:)
9
+ @data = data
10
+ end
11
+
12
+ # @return [String]
13
+ def name
14
+ @data['name']
15
+ end
16
+
17
+ # @return [Hash, nil]
18
+ def arguments
19
+ JSON.parse(@data['arguments']) if @data['arguments']
20
+ end
21
+
22
+ # @return [String]
23
+ def inspect
24
+ "#<#{self.class.name} name=#{name.inspect} arguments=#{arguments.inspect}>"
25
+ end
26
+ end
27
+ end
28
+ end
@@ -4,28 +4,34 @@ module OmniAI
4
4
  class Chat
5
5
  # A message returned by the API.
6
6
  class Message
7
- attr_accessor :role, :content
7
+ # @return [Hash]
8
+ attr_accessor :data
8
9
 
9
10
  # @param data [Hash]
10
- # @return [OmniAI::Chat::Message]
11
- def self.for(data:)
12
- content = data['content'] || data[:content]
13
- role = data['role'] || data[:role]
14
-
15
- new(content:, role: role || Role::USER)
16
- end
17
-
18
- # @param content [String]
19
- # @param role [String] optional (default to "user") e.g. "assistant" / "user" / "system"
20
- def initialize(content:, role: OmniAI::Chat::Role::USER)
21
- @role = role
22
- @content = content
11
+ def initialize(data:)
12
+ @data = data
23
13
  end
24
14
 
25
15
  # @return [String]
26
16
  def inspect
27
17
  "#<#{self.class.name} role=#{role.inspect} content=#{content.inspect}>"
28
18
  end
19
+
20
+ # @return [String]
21
+ def role
22
+ @data['role'] || Role::USER
23
+ end
24
+
25
+ # @return [String, nil]
26
+ def content
27
+ @data['content']
28
+ end
29
+
30
+ # @return [Array<OmniAI::Chat::ToolCall>]
31
+ def tool_call_list
32
+ @tool_call_list ||=
33
+ @data['tool_calls'] ? @data['tool_calls'].map { |tool_call_data| ToolCall.new(data: tool_call_data) } : []
34
+ end
29
35
  end
30
36
  end
31
37
  end
@@ -4,22 +4,24 @@ module OmniAI
4
4
  class Chat
5
5
  # A choice returned by the API.
6
6
  class MessageChoice
7
- attr_accessor :index, :message
8
-
9
7
  # @param data [Hash]
10
- # @return [OmniAI::Chat::Choice]
11
- def self.for(data:)
12
- index = data['index']
13
- message = Message.for(data: data['message'])
8
+ def initialize(data:)
9
+ @data = data
10
+ end
11
+
12
+ # @return [Integer]
13
+ def index
14
+ @data['index']
15
+ end
14
16
 
15
- new(index:, message:)
17
+ # @return [OmniAI::Chat::Message]
18
+ def message
19
+ @message ||= Message.new(data: @data['message'])
16
20
  end
17
21
 
18
- # @param index [Integer]
19
- # @param message [OmniAI::Chat::Message]
20
- def initialize(index:, message:)
21
- @index = index
22
- @message = message
22
+ # @return [String]
23
+ def inspect
24
+ "#<#{self.class.name} index=#{index} message=#{message.inspect}>"
23
25
  end
24
26
  end
25
27
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ class Chat
5
+ # A tool-call returned by the API.
6
+ class ToolCall
7
+ # @param data [Hash]
8
+ def initialize(data:)
9
+ @data = data
10
+ end
11
+
12
+ # @return [String]
13
+ def id
14
+ @data['id']
15
+ end
16
+
17
+ # @return [String]
18
+ def type
19
+ @data['type']
20
+ end
21
+
22
+ # @return [OmniAI::Chat::Function]
23
+ def function
24
+ @function ||= Function.new(data: @data['function']) if @data['function']
25
+ end
26
+
27
+ # @return [String]
28
+ def inspect
29
+ "#<#{self.class.name} id=#{id.inspect} type=#{type.inspect}>"
30
+ end
31
+ end
32
+ end
33
+ end
@@ -4,39 +4,25 @@ module OmniAI
4
4
  class Chat
5
5
  # A usage returned by the API.
6
6
  class Usage
7
- attr_accessor :input_tokens, :output_tokens, :total_tokens
7
+ attr_accessor :data
8
8
 
9
9
  # @param data [Hash]
10
- # @return [OmniAI::Chat::Usage]
11
- def self.for(data:)
12
- input_tokens = data['input_tokens'] || data['prompt_tokens']
13
- output_tokens = data['output_tokens'] || data['completion_tokens']
14
- total_tokens = data['total_tokens'] || (input_tokens + output_tokens)
15
-
16
- new(
17
- input_tokens:,
18
- output_tokens:,
19
- total_tokens:
20
- )
10
+ def initialize(data:)
11
+ @data = data
21
12
  end
22
13
 
23
- # @param input_tokens [Integer]
24
- # @param output_tokens [Integer]
25
- # @param total_tokens [Integer]
26
- def initialize(input_tokens:, output_tokens:, total_tokens:)
27
- @input_tokens = input_tokens
28
- @output_tokens = output_tokens
29
- @total_tokens = total_tokens
14
+ # @return [Integer]
15
+ def input_tokens
16
+ @data['input_tokens'] || @data['prompt_tokens']
30
17
  end
31
18
 
32
19
  # @return [Integer]
33
- def completion_tokens
34
- @output_tokens
20
+ def output_tokens
21
+ @data['output_tokens'] || @data['completion_tokens']
35
22
  end
36
23
 
37
- # @return [Integer]
38
- def prompt_tokens
39
- @input_tokens
24
+ def total_tokens
25
+ @data['total_tokens'] || (input_tokens + output_tokens)
40
26
  end
41
27
 
42
28
  # @return [String]
data/lib/omniai/chat.rb CHANGED
@@ -27,10 +27,19 @@ module OmniAI
27
27
  class Chat
28
28
  JSON_PROMPT = 'Respond with valid JSON. Do not include any non-JSON in the response.'
29
29
 
30
+ # An error raised when a chat makes a tool-call for a tool that cannot be found.
31
+ class ToolCallLookupError < Error
32
+ def initialize(tool_call)
33
+ super("missing tool for tool_call=#{tool_call.inspect}")
34
+ @tool_call = tool_call
35
+ end
36
+ end
37
+
30
38
  module Role
31
39
  ASSISTANT = 'assistant'
32
40
  USER = 'user'
33
41
  SYSTEM = 'system'
42
+ TOOL = 'tool'
34
43
  end
35
44
 
36
45
  module Format
@@ -46,13 +55,15 @@ module OmniAI
46
55
  # @param model [String] required
47
56
  # @param temperature [Float, nil] optional
48
57
  # @param stream [Proc, IO, nil] optional
58
+ # @param tools [Array<OmniAI::Tool>] optional
49
59
  # @param format [Symbol, nil] optional - :json
50
- def initialize(messages, client:, model:, temperature: nil, stream: nil, format: nil)
51
- @messages = messages
60
+ def initialize(messages, client:, model:, temperature: nil, stream: nil, tools: nil, format: nil)
61
+ @messages = arrayify(messages)
52
62
  @client = client
53
63
  @model = model
54
64
  @temperature = temperature
55
65
  @stream = stream
66
+ @tools = tools
56
67
  @format = format
57
68
  end
58
69
 
@@ -88,8 +99,20 @@ module OmniAI
88
99
  end
89
100
 
90
101
  # @param response [OmniAI::Chat::Completion]
102
+ # @return [OmniAI::Chat::Completion]
91
103
  def complete!(response:)
92
- self.class::Completion.new(data: response.parse)
104
+ completion = self.class::Completion.new(data: response.parse)
105
+
106
+ if @tools && completion.tool_call_required?
107
+ @messages = [
108
+ *@messages,
109
+ *completion.choices.map(&:message).map(&:data),
110
+ *(completion.tool_call_list.map { |tool_call| execute_tool_call(tool_call) }),
111
+ ]
112
+ process!
113
+ else
114
+ completion
115
+ end
93
116
  end
94
117
 
95
118
  # @param response [HTTP::Response]
@@ -99,19 +122,19 @@ module OmniAI
99
122
 
100
123
  self.class::Stream.new(response:).stream! do |chunk|
101
124
  case @stream
102
- when IO
125
+ when IO, StringIO
103
126
  @stream << chunk.choice.delta.content
104
127
  @stream.flush
105
128
  else @stream.call(chunk)
106
129
  end
107
130
  end
108
131
 
109
- @stream.puts if @stream.is_a?(IO)
132
+ @stream.puts if @stream.is_a?(IO) || @stream.is_a?(StringIO)
110
133
  end
111
134
 
112
135
  # @return [Array<Hash>]
113
136
  def messages
114
- arrayify(@messages).map do |content|
137
+ @messages.map do |content|
115
138
  case content
116
139
  when String then { role: Role::USER, content: }
117
140
  when Hash then content
@@ -133,5 +156,26 @@ module OmniAI
133
156
  .accept(:json)
134
157
  .post(path, json: payload)
135
158
  end
159
+
160
+ # @param tool_call [OmniAI::Chat::ToolCall]
161
+ def execute_tool_call(tool_call)
162
+ function = tool_call.function
163
+
164
+ tool = @tools.find { |entry| function.name == entry.name } || raise(ToolCallLookupError, tool_call)
165
+ result = tool.call(function.arguments)
166
+
167
+ prepare_tool_call_message(tool_call:, content: result)
168
+ end
169
+
170
+ # @param tool_call [OmniAI::Chat::ToolCall]
171
+ # @param content [String]
172
+ def prepare_tool_call_message(tool_call:, content:)
173
+ {
174
+ role: Role::TOOL,
175
+ name: tool_call.function.name,
176
+ tool_call_id: tool_call.id,
177
+ content:,
178
+ }
179
+ end
136
180
  end
137
181
  end
@@ -4,6 +4,7 @@ module OmniAI
4
4
  class CLI
5
5
  # Used for CLI usage of 'omnia chat'.
6
6
  class ChatHandler < BaseHandler
7
+ # @param argv [Array<String>]
7
8
  def handle!(argv:)
8
9
  parser.parse!(argv)
9
10
 
data/lib/omniai/cli.rb CHANGED
@@ -12,8 +12,8 @@ module OmniAI
12
12
  class CLI
13
13
  ChatArgs = Struct.new(:provider, :model, :temperature)
14
14
 
15
- # @param in [IO] a stream
16
- # @param out [IO] a stream
15
+ # @param stdin [IO] a stream
16
+ # @param stdout [IO] a stream
17
17
  # @param provider [String] a provider
18
18
  def initialize(stdin: $stdin, stdout: $stdout, provider: 'openai')
19
19
  @stdin = stdin
@@ -22,6 +22,7 @@ module OmniAI
22
22
  @args = {}
23
23
  end
24
24
 
25
+ # @param argv [Array<String>]
25
26
  def parse(argv = ARGV)
26
27
  parser.order!(argv)
27
28
  command = argv.shift
data/lib/omniai/client.rb CHANGED
@@ -129,9 +129,10 @@ module OmniAI
129
129
  # @param format [Symbol] optional :text or :json
130
130
  # @param temperature [Float, nil] optional
131
131
  # @param stream [Proc, nil] optional
132
+ # @param tools [Array<OmniAI::Tool>] optional
132
133
  #
133
134
  # @return [OmniAI::Chat::Completion]
134
- def chat(messages, model:, temperature: nil, format: nil, stream: nil)
135
+ def chat(messages, model:, temperature: nil, format: nil, stream: nil, tools: nil)
135
136
  raise NotImplementedError, "#{self.class.name}#chat undefined"
136
137
  end
137
138
 
@@ -144,7 +145,7 @@ module OmniAI
144
145
  # @param temperature [Float, nil] optional
145
146
  # @param format [Symbol] :text, :srt, :vtt, or :json (default)
146
147
  #
147
- # @return text [OmniAI::Transcribe::Transcription]
148
+ # @return [OmniAI::Transcribe::Transcription]
148
149
  def transcribe(io, model:, language: nil, prompt: nil, temperature: nil, format: nil)
149
150
  raise NotImplementedError, "#{self.class.name}#speak undefined"
150
151
  end
@@ -18,7 +18,6 @@ module OmniAI
18
18
  @logger.error("#{name}: #{error.message}")
19
19
  end
20
20
 
21
- # @param name [String]
22
21
  # @param payload [Hash]
23
22
  # @option payload [HTTP::Request] :request
24
23
  def start(_, payload)
@@ -26,7 +25,6 @@ module OmniAI
26
25
  @logger.info("#{request.verb.upcase} #{request.uri}")
27
26
  end
28
27
 
29
- # @param name [String]
30
28
  # @param payload [Hash]
31
29
  # @option payload [HTTP::Response] :response
32
30
  def finish(_, payload)
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ class Tool
5
+ # Usage:
6
+ #
7
+ # parameters = OmniAI::Tool::Parameters.new(properties: {
8
+ # n: OmniAI::Tool::Parameters.integer(description: 'The nth number to calculate.')
9
+ # required: %i[n]
10
+ # })
11
+ class Parameters
12
+ module Type
13
+ OBJECT = 'object'
14
+ end
15
+
16
+ # @param type [String]
17
+ # @param properties [Hash]
18
+ # @param required [Array<String>]
19
+ # @return [OmniAI::Tool::Parameters]
20
+ def initialize(type: Type::OBJECT, properties: {}, required: [])
21
+ @type = type
22
+ @properties = properties
23
+ @required = required
24
+ end
25
+
26
+ # @return [Hash]
27
+ def prepare
28
+ {
29
+ type: @type,
30
+ properties: @properties.transform_values(&:prepare),
31
+ required: @required,
32
+ }.compact
33
+ end
34
+
35
+ # @param args [Hash]
36
+ # @return [Hash]
37
+ def parse(args)
38
+ result = {}
39
+ @properties.each do |name, property|
40
+ value = args[String(name)]
41
+ result[name.intern] = property.parse(value) if value
42
+ end
43
+ result
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ class Tool
5
+ # Usage:
6
+ #
7
+ # property = OmniAI::Tool::Property.new(type: 'string', description: 'The nth number to calculate.')
8
+ class Property
9
+ module Type
10
+ BOOLEAN = 'boolean'
11
+ INTEGER = 'integer'
12
+ STRING = 'string'
13
+ NUMBER = 'number'
14
+ end
15
+
16
+ # @return [String]
17
+ attr_reader :type
18
+
19
+ # @return [String, nil]
20
+ attr_reader :description
21
+
22
+ # @return [Array<String>, nil]
23
+ attr_reader :enum
24
+
25
+ # @param description [String]
26
+ # @param enum [Array<String>]
27
+ # @return [OmniAI::Tool::Property]
28
+ def self.boolean(description: nil, enum: nil)
29
+ new(type: Type::BOOLEAN, description:, enum:)
30
+ end
31
+
32
+ # @param description [String]
33
+ # @param enum [Array<String>]
34
+ # @return [OmniAI::Tool::Property]
35
+ def self.integer(description: nil, enum: nil)
36
+ new(type: Type::INTEGER, description:, enum:)
37
+ end
38
+
39
+ # @param description [String]
40
+ # @param enum [Array<String>]
41
+ # @return [OmniAI::Tool::Property]
42
+ def self.string(description: nil, enum: nil)
43
+ new(type: Type::STRING, description:, enum:)
44
+ end
45
+
46
+ # @param description [String]
47
+ # @param enum [Array<String>]
48
+ # @return [OmniAI::Tool::Property]
49
+ def self.number(description: nil, enum: nil)
50
+ new(type: Type::NUMBER, description:, enum:)
51
+ end
52
+
53
+ # @param description [String]
54
+ # @param enum [Array<String>]
55
+ # @return [OmniAI::Tool::Property]
56
+ def initialize(type:, description: nil, enum: nil)
57
+ @type = type
58
+ @description = description
59
+ @enum = enum
60
+ end
61
+
62
+ # @example
63
+ # property.prepare
64
+ # # {
65
+ # # type: 'string',
66
+ # # description: 'The unit (e.g. "fahrenheit" or "celsius").'
67
+ # # enum: %w[fahrenheit celsius]
68
+ # # }
69
+ #
70
+ # @return [Hash]
71
+ def prepare
72
+ {
73
+ type: @type,
74
+ description: @description,
75
+ enum: @enum,
76
+ }.compact
77
+ end
78
+
79
+ # @return [String, Integer, Float, Boolean, Object]
80
+ def parse(value)
81
+ case @type
82
+ when Type::INTEGER then Integer(value)
83
+ when Type::STRING then String(value)
84
+ when Type::NUMBER then Float(value)
85
+ else value
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ # Usage:
5
+ #
6
+ # fibonacci = proc do |n:|
7
+ # next(0) if n == 0
8
+ # next(1) if n == 1
9
+ # fibonacci.call(n: n - 1) + fibonacci.call(n: n - 2)
10
+ # end
11
+ #
12
+ # tool = OmniAI::Tool.new(fibonacci,
13
+ # name: 'Fibonacci',
14
+ # description: 'Cacluate the nth Fibonacci',
15
+ # parameters: OmniAI::Tool::Parameters.new(
16
+ # properties: {
17
+ # n: OmniAI::Tool::Property.integer(description: 'The nth Fibonacci number to calculate')
18
+ # },
19
+ # required: %i[n],
20
+ # )
21
+ # )
22
+ class Tool
23
+ # @return [Proc]
24
+ attr_accessor :function
25
+
26
+ # @return [String]
27
+ attr_accessor :name
28
+
29
+ # @return [String, nil]
30
+ attr_accessor :description
31
+
32
+ # @return [Hash, nil]
33
+ attr_accessor :parameters
34
+
35
+ # @param function [Proc]
36
+ # @param name [String]
37
+ # @param description [String]
38
+ # @param parameters [Hash]
39
+ def initialize(function, name:, description: nil, parameters: nil)
40
+ @function = function
41
+ @name = name
42
+ @description = description
43
+ @parameters = parameters
44
+ end
45
+
46
+ # @example
47
+ # tool.prepare
48
+ # # {
49
+ # # type: 'function',
50
+ # # function: {
51
+ # # name: 'Fibonacci',
52
+ # # description: 'Calculate the nth Fibonacci number',
53
+ # # parameters: {
54
+ # # type: 'object',
55
+ # # properties: {
56
+ # # n: OmniAI::Tool::Property.integer(description: 'The nth Fibonacci number to calculate')
57
+ # # },
58
+ # # required: ['n']
59
+ # # }
60
+ # # }
61
+ # # }
62
+ #
63
+ # @return [Hash]
64
+ def prepare
65
+ {
66
+ type: 'function',
67
+ function: {
68
+ name: @name,
69
+ description: @description,
70
+ parameters: @parameters&.prepare,
71
+ }.compact,
72
+ }
73
+ end
74
+
75
+ # @example
76
+ # tool.call({ "n" => 6 })
77
+ # #=> 8
78
+ #
79
+ # @param args [Hash]
80
+ # @return [String]
81
+ def call(args = {})
82
+ @function.call(**(@parameters ? @parameters.parse(args) : args))
83
+ end
84
+ end
85
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OmniAI
4
- VERSION = '1.4.2'
4
+ VERSION = '1.5.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omniai
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.2
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin Sylvestre
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-07-04 00:00:00.000000000 Z
11
+ date: 2024-07-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: event_stream_parser
@@ -76,9 +76,11 @@ files:
76
76
  - lib/omniai/chat/content/url.rb
77
77
  - lib/omniai/chat/delta.rb
78
78
  - lib/omniai/chat/delta_choice.rb
79
+ - lib/omniai/chat/function.rb
79
80
  - lib/omniai/chat/message.rb
80
81
  - lib/omniai/chat/message_choice.rb
81
82
  - lib/omniai/chat/stream.rb
83
+ - lib/omniai/chat/tool_call.rb
82
84
  - lib/omniai/chat/usage.rb
83
85
  - lib/omniai/cli.rb
84
86
  - lib/omniai/cli/base_handler.rb
@@ -87,6 +89,9 @@ files:
87
89
  - lib/omniai/config.rb
88
90
  - lib/omniai/instrumentation.rb
89
91
  - lib/omniai/speak.rb
92
+ - lib/omniai/tool.rb
93
+ - lib/omniai/tool/parameters.rb
94
+ - lib/omniai/tool/property.rb
90
95
  - lib/omniai/transcribe.rb
91
96
  - lib/omniai/transcribe/transcription.rb
92
97
  - lib/omniai/version.rb
@@ -112,7 +117,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
112
117
  - !ruby/object:Gem::Version
113
118
  version: '0'
114
119
  requirements: []
115
- rubygems_version: 3.5.3
120
+ rubygems_version: 3.5.14
116
121
  signing_key:
117
122
  specification_version: 4
118
123
  summary: A generalized framework for interacting with many AI services