omniai 1.4.2 → 1.5.1

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: df4f1096ec0d74c8ab26e5cfc8db4902009d1bb398ed9913e801b31203d1ae66
4
- data.tar.gz: 9cff5f3ecdc6a540bbbd3ca6501dbd07585a1f9dfe8127a09ee6421a51f79c2f
3
+ metadata.gz: a989cab03afca85385b23f4684d377b8d1a09533a73899fa79eebcce4f4a3260
4
+ data.tar.gz: bb39575e251b0dd6cf6e0e3b24ba86765fd550feeb3a222ce5f785ca654e8741
5
5
  SHA512:
6
- metadata.gz: db82f7cf3e8b43f55080c1ae1af2f6f9767dec7870bf23e2424eee8a272671b565f279829d9768fbb447eec8821fcd7da22058da8a4016b4de271a389a115c1c
7
- data.tar.gz: 84362d233dbafa2dd6403b3d65056c7b836eb0b2ce420f4395c86188887f12ba7cb59caead7cfbc44d80bfa03ad2ef342b62f8235ace58afd0284e8e25b5c243
6
+ metadata.gz: e20f0ee5ab32ceb95e33e38888c81e28f936294cfcd9a3dad80a8d89a9fd3241477455e706d70fb949502b01cdf968547104e55931138512b4e617dd9688fe02
7
+ data.tar.gz: 58aa9a8a43b2e8a7580beac909b7ddd34fcaaa3d9c60c9105d3dba0cd18923f7361473fdb863890fbfa17013a1339b71008381e105bd961de44dec67db8eac8b
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:
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ class Chat
5
+ module Response
6
+ # For use with MessageChoice or DeltaChoice.
7
+ class Choice < Resource
8
+ # @return [Integer]
9
+ def index
10
+ @data['index']
11
+ end
12
+
13
+ # @return [OmniAI::Chat::Response::Part]
14
+ def part
15
+ raise NotImplementedError, "#{self.class.name}#part undefined"
16
+ end
17
+
18
+ # @return [OmniAI::Chat::Response::ToolCallList]
19
+ def tool_call_list
20
+ part.tool_call_list
21
+ end
22
+
23
+ # @return [Boolean]
24
+ def tool_call_list?
25
+ tool_call_list.any?
26
+ end
27
+
28
+ # @return [String, nil]
29
+ def content
30
+ part.content
31
+ end
32
+
33
+ # @return [Boolean]
34
+ def content?
35
+ !content.nil?
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ class Chat
5
+ module Response
6
+ # A chunk returned by the API.
7
+ class Chunk < Payload
8
+ # @return [Array<OmniAI::Chat::Response::DeltaChoice>]
9
+ def choices
10
+ @choices ||= @data['choices'].map { |data| DeltaChoice.new(data:) }
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ class Chat
5
+ module Response
6
+ # A completion returned by the API.
7
+ class Completion < Payload
8
+ # @return [Array<OmniAI::Chat:Response:::MessageChoice>]
9
+ def choices
10
+ @choices ||= @data['choices'].map { |data| MessageChoice.new(data:) }
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ class Chat
5
+ module Response
6
+ # A delta returned by the API.
7
+ class Delta < Part
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ class Chat
5
+ module Response
6
+ # A delta choice returned by the API.
7
+ class DeltaChoice < Choice
8
+ # @return [String]
9
+ def inspect
10
+ "#<#{self.class.name} index=#{index} delta=#{delta.inspect}>"
11
+ end
12
+
13
+ # @return [OmniAI::Chat::Response::Delta]
14
+ def delta
15
+ @delta ||= Delta.new(data: @data['delta'])
16
+ end
17
+
18
+ # @return [OmniAI::Chat::Response::Delta]
19
+ def part
20
+ delta
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ class Chat
5
+ module Response
6
+ # A function returned by the API.
7
+ class Function < Resource
8
+ # @return [String]
9
+ def inspect
10
+ "#<#{self.class.name} name=#{name.inspect} arguments=#{arguments.inspect}>"
11
+ end
12
+
13
+ # @return [String]
14
+ def name
15
+ @data['name']
16
+ end
17
+
18
+ # @return [Hash, nil]
19
+ def arguments
20
+ JSON.parse(@data['arguments']) if @data['arguments']
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ class Chat
5
+ module Response
6
+ # A message returned by the API.
7
+ class Message < Part
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ class Chat
5
+ module Response
6
+ # A choice returned by the API.
7
+ class MessageChoice < Choice
8
+ # @return [String]
9
+ def inspect
10
+ "#<#{self.class.name} index=#{index} message=#{message.inspect}>"
11
+ end
12
+
13
+ # @return [OmniAI::Chat::Response::Message]
14
+ def message
15
+ @message ||= Message.new(data: @data['message'])
16
+ end
17
+
18
+ # @return [OmniAI::Chat::Response::Message]
19
+ def part
20
+ message
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ class Chat
5
+ module Response
6
+ # Either a delta or message.
7
+ class Part < Resource
8
+ # @return [String]
9
+ def inspect
10
+ "#<#{self.class.name} role=#{role.inspect} content=#{content.inspect}>"
11
+ end
12
+
13
+ # @return [String]
14
+ def role
15
+ @data['role'] || Role::USER
16
+ end
17
+
18
+ # @return [String, nil]
19
+ def content
20
+ @data['content']
21
+ end
22
+
23
+ # @return [Array<OmniAI::Chat::Response::ToolCall>]
24
+ def tool_call_list
25
+ return [] unless @data['tool_calls']
26
+
27
+ @tool_call_list ||= @data['tool_calls'].map { |tool_call_data| ToolCall.new(data: tool_call_data) }
28
+ end
29
+
30
+ # @param index [Integer]
31
+ # @return [OmniAI::Chat::Response::ToolCall, nil]
32
+ def tool_call(index: 0)
33
+ tool_call_list[index]
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ class Chat
5
+ module Response
6
+ # A chunk or completion.
7
+ class Payload < Resource
8
+ # @return [String]
9
+ def inspect
10
+ "#<#{self.class.name} id=#{id.inspect} choices=#{choices.inspect}>"
11
+ end
12
+
13
+ # @return [String]
14
+ def id
15
+ @data['id']
16
+ end
17
+
18
+ # @return [Time]
19
+ def created
20
+ Time.at(@data['created']) if @data['created']
21
+ end
22
+
23
+ # @return [Time]
24
+ def updated
25
+ Time.at(@data['updated']) if @data['updated']
26
+ end
27
+
28
+ # @return [String]
29
+ def model
30
+ @data['model']
31
+ end
32
+
33
+ # @return [Array<OmniAI::Chat::Response::Choice>]
34
+ def choices
35
+ raise NotImplementedError, "#{self.class.name}#choices undefined"
36
+ end
37
+
38
+ # @param index [Integer]
39
+ # @return [OmniAI::Chat::Response::DeltaChoice]
40
+ def choice(index: 0)
41
+ choices[index]
42
+ end
43
+
44
+ # @param index [Integer]
45
+ # @return [OmniAI::Chat::Response::Part]
46
+ def part(index: 0)
47
+ choice(index:).part
48
+ end
49
+
50
+ # @return [OmniAI::Chat::Response::Usage]
51
+ def usage
52
+ @usage ||= Usage.new(data: @data['usage']) if @data['usage']
53
+ end
54
+
55
+ # @return [String, nil]
56
+ def content
57
+ choice.content
58
+ end
59
+
60
+ # @return [Boolean]
61
+ def content?
62
+ choice.content?
63
+ end
64
+
65
+ # @return [Array<OmniAI::Chat::Response:ToolCall>]
66
+ def tool_call_list
67
+ choice.tool_call_list
68
+ end
69
+
70
+ # @return [Boolean]
71
+ def tool_call_list?
72
+ choice.tool_call_list?
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ class Chat
5
+ module Response
6
+ # A generic data to handle response.
7
+ class Resource
8
+ attr_accessor :data
9
+
10
+ # @param data [Hash]
11
+ def initialize(data:)
12
+ @data = data
13
+ end
14
+
15
+ # @return [String]
16
+ def inspect
17
+ "#<#{self.class.name} data=#{@data.inspect}>"
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ class Chat
5
+ module Response
6
+ # A stream given when streaming.
7
+ class Stream
8
+ # @param response [HTTP::Response]
9
+ def initialize(response:)
10
+ @response = response
11
+ @parser = EventStreamParser::Parser.new
12
+ end
13
+
14
+ # @yield [OmniAI::Chat::Chunk]
15
+ def stream!
16
+ @response.body.each do |chunk|
17
+ @parser.feed(chunk) do |_, data|
18
+ next if data.eql?('[DONE]')
19
+
20
+ yield(Chunk.new(data: JSON.parse(data)))
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ class Chat
5
+ module Response
6
+ # A tool-call returned by the API.
7
+ class ToolCall < Resource
8
+ # @return [String]
9
+ def inspect
10
+ "#<#{self.class.name} id=#{id.inspect} type=#{type.inspect}>"
11
+ end
12
+
13
+ # @return [String]
14
+ def id
15
+ @data['id']
16
+ end
17
+
18
+ # @return [String]
19
+ def type
20
+ @data['type']
21
+ end
22
+
23
+ # @return [OmniAI::Chat::Response::Function]
24
+ def function
25
+ @function ||= Function.new(data: @data['function']) if @data['function']
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ class Chat
5
+ module Response
6
+ # A usage returned by the API.
7
+ class Usage < Resource
8
+ # @return [String]
9
+ def inspect
10
+ properties = [
11
+ "input_tokens=#{input_tokens}",
12
+ "output_tokens=#{output_tokens}",
13
+ "total_tokens=#{total_tokens}",
14
+ ]
15
+ "#<#{self.class.name} #{properties.join(' ')}>"
16
+ end
17
+
18
+ # @return [Integer]
19
+ def input_tokens
20
+ @data['input_tokens'] || @data['prompt_tokens']
21
+ end
22
+
23
+ # @return [Integer]
24
+ def output_tokens
25
+ @data['output_tokens'] || @data['completion_tokens']
26
+ end
27
+
28
+ # @return [Integer]
29
+ def total_tokens
30
+ @data['total_tokens'] || (input_tokens + output_tokens)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
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
 
@@ -87,9 +98,21 @@ module OmniAI
87
98
  end
88
99
  end
89
100
 
90
- # @param response [OmniAI::Chat::Completion]
101
+ # @param response [HTTP::Response]
102
+ # @return [OmniAI::Chat::Response::Completion]
91
103
  def complete!(response:)
92
- self.class::Completion.new(data: response.parse)
104
+ completion = self.class::Response::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,21 +120,23 @@ module OmniAI
97
120
  def stream!(response:)
98
121
  raise Error, "#{self.class.name}#stream! unstreamable" unless @stream
99
122
 
100
- self.class::Stream.new(response:).stream! do |chunk|
123
+ self.class::Response::Stream.new(response:).stream! do |chunk|
101
124
  case @stream
102
- when IO
103
- @stream << chunk.choice.delta.content
104
- @stream.flush
125
+ when IO, StringIO
126
+ if chunk.content?
127
+ @stream << chunk.content
128
+ @stream.flush
129
+ end
105
130
  else @stream.call(chunk)
106
131
  end
107
132
  end
108
133
 
109
- @stream.puts if @stream.is_a?(IO)
134
+ @stream.puts if @stream.is_a?(IO) || @stream.is_a?(StringIO)
110
135
  end
111
136
 
112
137
  # @return [Array<Hash>]
113
138
  def messages
114
- arrayify(@messages).map do |content|
139
+ @messages.map do |content|
115
140
  case content
116
141
  when String then { role: Role::USER, content: }
117
142
  when Hash then content
@@ -133,5 +158,26 @@ module OmniAI
133
158
  .accept(:json)
134
159
  .post(path, json: payload)
135
160
  end
161
+
162
+ # @param tool_call [OmniAI::Chat::ToolCall]
163
+ def execute_tool_call(tool_call)
164
+ function = tool_call.function
165
+
166
+ tool = @tools.find { |entry| function.name == entry.name } || raise(ToolCallLookupError, tool_call)
167
+ result = tool.call(function.arguments)
168
+
169
+ prepare_tool_call_message(tool_call:, content: result)
170
+ end
171
+
172
+ # @param tool_call [OmniAI::Chat::ToolCall]
173
+ # @param content [String]
174
+ def prepare_tool_call_message(tool_call:, content:)
175
+ {
176
+ role: Role::TOOL,
177
+ name: tool_call.function.name,
178
+ tool_call_id: tool_call.id,
179
+ content:,
180
+ }
181
+ end
136
182
  end
137
183
  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
data/lib/omniai/config.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OmniAI
4
- # A configuration for each agent w/ `api_key` / `host` / `logger`. Usage:
4
+ # A configuration for each agent w/ `api_key` / `host` / `logger`.
5
+ #
6
+ # Usage:
5
7
  #
6
8
  # OmniAI::OpenAI.config do |config|
7
9
  # config.api_key = '...'
@@ -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.1'
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.1
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-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: event_stream_parser
@@ -68,18 +68,24 @@ files:
68
68
  - exe/omniai
69
69
  - lib/omniai.rb
70
70
  - lib/omniai/chat.rb
71
- - lib/omniai/chat/chunk.rb
72
- - lib/omniai/chat/completion.rb
73
71
  - lib/omniai/chat/content/file.rb
74
72
  - lib/omniai/chat/content/media.rb
75
73
  - lib/omniai/chat/content/text.rb
76
74
  - lib/omniai/chat/content/url.rb
77
- - lib/omniai/chat/delta.rb
78
- - lib/omniai/chat/delta_choice.rb
79
- - lib/omniai/chat/message.rb
80
- - lib/omniai/chat/message_choice.rb
81
- - lib/omniai/chat/stream.rb
82
- - lib/omniai/chat/usage.rb
75
+ - lib/omniai/chat/response/choice.rb
76
+ - lib/omniai/chat/response/chunk.rb
77
+ - lib/omniai/chat/response/completion.rb
78
+ - lib/omniai/chat/response/delta.rb
79
+ - lib/omniai/chat/response/delta_choice.rb
80
+ - lib/omniai/chat/response/function.rb
81
+ - lib/omniai/chat/response/message.rb
82
+ - lib/omniai/chat/response/message_choice.rb
83
+ - lib/omniai/chat/response/part.rb
84
+ - lib/omniai/chat/response/payload.rb
85
+ - lib/omniai/chat/response/resource.rb
86
+ - lib/omniai/chat/response/stream.rb
87
+ - lib/omniai/chat/response/tool_call.rb
88
+ - lib/omniai/chat/response/usage.rb
83
89
  - lib/omniai/cli.rb
84
90
  - lib/omniai/cli/base_handler.rb
85
91
  - lib/omniai/cli/chat_handler.rb
@@ -87,6 +93,9 @@ files:
87
93
  - lib/omniai/config.rb
88
94
  - lib/omniai/instrumentation.rb
89
95
  - lib/omniai/speak.rb
96
+ - lib/omniai/tool.rb
97
+ - lib/omniai/tool/parameters.rb
98
+ - lib/omniai/tool/property.rb
90
99
  - lib/omniai/transcribe.rb
91
100
  - lib/omniai/transcribe/transcription.rb
92
101
  - lib/omniai/version.rb
@@ -112,7 +121,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
112
121
  - !ruby/object:Gem::Version
113
122
  version: '0'
114
123
  requirements: []
115
- rubygems_version: 3.5.3
124
+ rubygems_version: 3.5.14
116
125
  signing_key:
117
126
  specification_version: 4
118
127
  summary: A generalized framework for interacting with many AI services
@@ -1,46 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OmniAI
4
- class Chat
5
- # A chunk returned by the API.
6
- class Chunk
7
- attr_accessor :data
8
-
9
- # @param data [Hash]
10
- def initialize(data:)
11
- @data = data
12
- end
13
-
14
- # @return [String]
15
- def id
16
- @data['id']
17
- end
18
-
19
- # @return [Time]
20
- def created
21
- Time.at(@data['created']) if @data['created']
22
- end
23
-
24
- # @return [Time]
25
- def updated
26
- Time.at(@data['updated']) if @data['updated']
27
- end
28
-
29
- # @return [String]
30
- def model
31
- @data['model']
32
- end
33
-
34
- # @return [Array<OmniAI::Chat::DeltaChoice>]
35
- def choices
36
- @choices ||= @data['choices'].map { |data| DeltaChoice.for(data:) }
37
- end
38
-
39
- # @param index [Integer]
40
- # @return [OmniAI::Chat::Delta]
41
- def choice(index: 0)
42
- choices[index]
43
- end
44
- end
45
- end
46
- end
@@ -1,53 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OmniAI
4
- class Chat
5
- # A completion returned by the API.
6
- class Completion
7
- attr_accessor :data
8
-
9
- # @param data [Hash]
10
- def initialize(data:)
11
- @data = data
12
- end
13
-
14
- # @return [String]
15
- def id
16
- @data['id']
17
- end
18
-
19
- # @return [Time]
20
- def created
21
- Time.at(@data['created']) if @data['created']
22
- end
23
-
24
- # @return [Time]
25
- def updated
26
- Time.at(@data['updated']) if @data['updated']
27
- end
28
-
29
- # @return [String]
30
- def model
31
- @data['model']
32
- end
33
-
34
- # @return [OmniAI::Chat::Usage]
35
- def usage
36
- return unless @data['usage']
37
-
38
- @usage ||= Usage.for(data: @data['usage'])
39
- end
40
-
41
- # @return [Array<OmniAI::Chat::MessageChoice>]
42
- def choices
43
- @choices ||= @data['choices'].map { |data| MessageChoice.for(data:) }
44
- end
45
-
46
- # @param index [Integer] optional - default is 0
47
- # @return [OmniAI::Chat::MessageChoice]
48
- def choice(index: 0)
49
- choices[index]
50
- end
51
- end
52
- end
53
- end
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OmniAI
4
- class Chat
5
- # A delta returned by the API.
6
- class Delta
7
- attr_accessor :role, :content
8
-
9
- # @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]
20
- def initialize(content:, role: nil)
21
- @content = content
22
- @role = role
23
- end
24
-
25
- # @return [String]
26
- def inspect
27
- "#<#{self.class.name} role=#{role.inspect} content=#{content.inspect}>"
28
- end
29
- end
30
- end
31
- end
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OmniAI
4
- class Chat
5
- # A delta choice returned by the API.
6
- class DeltaChoice
7
- attr_accessor :index, :delta
8
-
9
- # @param data [Hash]
10
- # @return [OmniAI::Chat::Choice]
11
- def self.for(data:)
12
- index = data['index']
13
- delta = Delta.for(data: data['delta'])
14
-
15
- new(index:, delta:)
16
- end
17
-
18
- # @param index [Integer]
19
- # @param delta [Delta]
20
- def initialize(index:, delta:)
21
- @index = index
22
- @delta = delta
23
- end
24
- end
25
- end
26
- end
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OmniAI
4
- class Chat
5
- # A message returned by the API.
6
- class Message
7
- attr_accessor :role, :content
8
-
9
- # @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
23
- end
24
-
25
- # @return [String]
26
- def inspect
27
- "#<#{self.class.name} role=#{role.inspect} content=#{content.inspect}>"
28
- end
29
- end
30
- end
31
- end
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OmniAI
4
- class Chat
5
- # A choice returned by the API.
6
- class MessageChoice
7
- attr_accessor :index, :message
8
-
9
- # @param data [Hash]
10
- # @return [OmniAI::Chat::Choice]
11
- def self.for(data:)
12
- index = data['index']
13
- message = Message.for(data: data['message'])
14
-
15
- new(index:, message:)
16
- end
17
-
18
- # @param index [Integer]
19
- # @param message [OmniAI::Chat::Message]
20
- def initialize(index:, message:)
21
- @index = index
22
- @message = message
23
- end
24
- end
25
- end
26
- end
@@ -1,25 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OmniAI
4
- class Chat
5
- # A stream given when streaming.
6
- class Stream
7
- # @param response [HTTP::Response]
8
- def initialize(response:)
9
- @response = response
10
- @parser = EventStreamParser::Parser.new
11
- end
12
-
13
- # @yield [OmniAI::Chat::Chunk]
14
- def stream!
15
- @response.body.each do |chunk|
16
- @parser.feed(chunk) do |_, data|
17
- next if data.eql?('[DONE]')
18
-
19
- yield(Chunk.new(data: JSON.parse(data)))
20
- end
21
- end
22
- end
23
- end
24
- end
25
- end
@@ -1,48 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OmniAI
4
- class Chat
5
- # A usage returned by the API.
6
- class Usage
7
- attr_accessor :input_tokens, :output_tokens, :total_tokens
8
-
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
- )
21
- end
22
-
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
30
- end
31
-
32
- # @return [Integer]
33
- def completion_tokens
34
- @output_tokens
35
- end
36
-
37
- # @return [Integer]
38
- def prompt_tokens
39
- @input_tokens
40
- end
41
-
42
- # @return [String]
43
- def inspect
44
- "#<#{self.class.name} input_tokens=#{input_tokens} output_tokens=#{output_tokens} total_tokens=#{total_tokens}>"
45
- end
46
- end
47
- end
48
- end