omniai 1.4.1 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2e94a411496cb1e8f10e775a2a6487de99def946de6a4cc1e6c1c873ce160760
4
- data.tar.gz: 5ec7d8ba1b1bbaf67fd3fe633af99b2e4c3d95edc0114313446b8a666bb1f0d6
3
+ metadata.gz: c05ed0777b6e88c023738b43e5188d722ca9bf372a7cabda61a800ae5a7a13a7
4
+ data.tar.gz: e2f0220221030ce99bc81dfd456a7505bce557594ba03336d5445ff4a7188b7d
5
5
  SHA512:
6
- metadata.gz: 4265b9413acdf6bfe5dde9c4081c9e83b212c7ac51b5a77f3bc4c1dd0d2d6c030f48f3830e431ee2556a1bc58cbef980c98757f714c97192fb88a948743ce1e5
7
- data.tar.gz: 2d8ba0d77348b88ccdc064014528ae56c9be5fc14e716ed961c74f8d462b4dac822eacb1f493b28aa65a4178391a19c3f98e1974ca93393153cf9a8e87c09703
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
- 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]
@@ -97,23 +120,23 @@ module OmniAI
97
120
  def stream!(response:)
98
121
  raise Error, "#{self.class.name}#stream! unstreamable" unless @stream
99
122
 
100
- Stream.new(response:).stream! do |chunk|
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
- when String then { role: OmniAI::Chat::Role::USER, content: }
139
+ when String then { role: Role::USER, content: }
117
140
  when Hash then content
118
141
  else raise Error, "Unsupported content=#{content.inspect}"
119
142
  end
@@ -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.1'
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.1
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