omniai 1.3.0 → 1.4.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: 341ce12cf0950b167a43c298f3ccb1df821bbdc469bc6824b14219bef4e38ab5
4
- data.tar.gz: 9badfe7e48d20fe84054d9206e8572a333f382407c3236a3715e1d85cf6b6a84
3
+ metadata.gz: 8b3d66712c9a2fd460abf00c29b056b87ff8cdd08c8b38d69ece0755d8c76578
4
+ data.tar.gz: f0fdf7b20e3f3cc3c4e2bee6d21b68bd917d9267c9f1297dec91f2f8bccc71e9
5
5
  SHA512:
6
- metadata.gz: f81164e402f5e1e4b2ccc2e627675d42631cfda985bf91469eb32735a789eecbe69c7f10bd9d2d5b4959f2d0f2484ae1bf7ced58f607a7cae822846c831ce96c
7
- data.tar.gz: '0182828dad6fb6f6d98550608c3bd57ee980ff4acbcc6dfb3ee7d61a16200566f1c8965424537583d0b73225298ca5eba6ddafd3c51791ca847f53264a7654fd'
6
+ metadata.gz: 98c6c3380181aaf7ad858968b998adab49f8cd200d94755ff6cf79423378dd401c4c538147aeb416b023c47fe7f1806a8742a9486429e90bcc487eb8944cba52
7
+ data.tar.gz: f251bb7436e9536275a285508f63964693a067378f55a72b3cf01fb9cb801f55ef96fd8a64c9b4e5790c6c400ee6adcee64f6dc5dc413e92b42d5f869afce45f
data/README.md CHANGED
@@ -87,40 +87,11 @@ client = OmniAI::Example::Client.new(logger:)
87
87
  ```
88
88
 
89
89
  ```
90
- I, [...] INFO -- : > POST https://...
91
- D, [...] DEBUG -- : Authorization: Bearer ...
90
+ [INFO]: POST https://...
91
+ [INFO]: 200 OK
92
92
  ...
93
- {"messages":[{"role":"user","content":"Tell me a joke!"}],"model":"..."}
94
- I, [...] INFO -- : < 200 OK
95
- D, [...] DEBUG -- : Date: ...
96
- ...
97
- {
98
- "id": "...",
99
- "object": "...",
100
- ...
101
- }
102
93
  ```
103
94
 
104
- The level of the logger can be configured to either `INFO` and `DEBUG`:
105
-
106
- **INFO**:
107
-
108
- ```ruby
109
- logger.level = Logger::INFO
110
- ```
111
-
112
- - Request: verb / URI
113
- - Response: status
114
-
115
- **DEBUG**:
116
-
117
- ```ruby
118
- logger.level = Logger::DEBUG
119
- ```
120
-
121
- - Request: verb / URI / headers / body
122
- - Response: status / headers / body
123
-
124
95
  #### Timeouts
125
96
 
126
97
  Timeouts are configurable by passing a `timeout` an integer duration for the request / response of any APIs using:
@@ -222,3 +193,38 @@ tempfile = client.speak('The quick brown fox jumps over a lazy dog.', voice: 'HA
222
193
  tempfile.close
223
194
  tempfile.unlink
224
195
  ```
196
+
197
+ ## CLI
198
+
199
+ OmniAI packages a basic command line interface (CLI) to allow for exploration of various APIs. A detailed CLI documentation can be found via help:
200
+
201
+ ```bash
202
+ omniai --help
203
+ ```
204
+
205
+ ### Chat
206
+
207
+ #### w/ a Prompt
208
+
209
+ ```bash
210
+ omniai chat "What is the coldest place on earth?"
211
+ ```
212
+
213
+ ```
214
+ The coldest place on earth is Antarctica.
215
+ ```
216
+
217
+ #### w/o a Prompt
218
+
219
+ ```bash
220
+ omniai chat --provider="openai" --model="gpt-4" --temperature="0.5"
221
+ ```
222
+
223
+ ```
224
+ Type 'exit' or 'quit' to abort.
225
+ # What is the warmet place on earth?
226
+ ```
227
+
228
+ ```
229
+ The warmest place on earth is Africa.
230
+ ```
data/exe/omniai ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'omniai'
5
+
6
+ cli = OmniAI::CLI.new
7
+ cli.parse
data/lib/omniai/chat.rb CHANGED
@@ -45,7 +45,7 @@ module OmniAI
45
45
  # @param client [OmniAI::Client] the client
46
46
  # @param model [String] required
47
47
  # @param temperature [Float, nil] optional
48
- # @param stream [Proc, nil] optional
48
+ # @param stream [Proc, IO, nil] optional
49
49
  # @param format [Symbol, nil] optional - :json
50
50
  def initialize(messages, client:, model:, temperature: nil, stream: nil, format: nil)
51
51
  @messages = messages
@@ -59,6 +59,7 @@ module OmniAI
59
59
  # @raise [HTTPError]
60
60
  def process!
61
61
  response = request!
62
+
62
63
  raise HTTPError, response.flush unless response.status.ok?
63
64
 
64
65
  parse!(response:)
@@ -96,7 +97,14 @@ module OmniAI
96
97
  def stream!(response:)
97
98
  raise Error, "#{self.class.name}#stream! unstreamable" unless @stream
98
99
 
99
- Stream.new(response:).stream! { |chunk| @stream.call(chunk) }
100
+ Stream.new(response:).stream! do |chunk|
101
+ case @stream
102
+ when IO then @stream << chunk
103
+ else @stream.call(chunk)
104
+ end
105
+ end
106
+
107
+ @stream.flush if @stream.is_a?(IO)
100
108
  end
101
109
 
102
110
  # @return [Array<Hash>]
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ class CLI
5
+ # A generic handler for CLI commands (e.g. 'omnia chat').
6
+ class BaseHandler
7
+ # @param stdin [IO] an optional stream for stdin
8
+ # @param stdout [IO] an optional stream for stdout
9
+ # @param provider [String] an optional provider (defaults to 'openai')
10
+ # @param argv [Array<String>]
11
+ def self.handle!(argv:, stdin: $stdin, stdout: $stdout, provider: 'openai')
12
+ new(stdin:, stdout:, provider:).handle!(argv:)
13
+ end
14
+
15
+ # @param stdin [IO] an optional stream for stdin
16
+ # @param stdout [IO] an optional stream for stdout
17
+ # @param provider [String] an optional provider (defaults to 'openai')
18
+ def initialize(stdin: $stdin, stdout: $stdout, provider: 'openai')
19
+ @stdin = stdin
20
+ @stdout = stdout
21
+ @provider = provider
22
+ @args = {}
23
+ end
24
+
25
+ # @param argv [Array<String>]
26
+ def handle!(argv:)
27
+ raise NotImplementedError, "#{self.class}#handle! undefined"
28
+ end
29
+
30
+ private
31
+
32
+ # @return [OmniAI::Client]
33
+ def client
34
+ @client ||= OmniAI::Client.find(provider: @provider)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ class CLI
5
+ # Used for CLI usage of 'omnia chat'.
6
+ class ChatHandler < BaseHandler
7
+ def handle!(argv:)
8
+ parser.parse!(argv)
9
+
10
+ if argv.empty?
11
+ listen!
12
+ else
13
+ chat(prompt: argv.join(' '))
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def listen!
20
+ @stdout.puts('Type "exit" or "quit" to leave.')
21
+
22
+ loop do
23
+ @stdout.print('# ')
24
+ prompt = @stdin.gets&.chomp
25
+
26
+ break if prompt.nil? || prompt.match?(/\A(exit|quit)\z/i)
27
+
28
+ chat(prompt:)
29
+ rescue Interrupt
30
+ break
31
+ end
32
+ end
33
+
34
+ # @param prompt [String]
35
+ def chat(prompt:)
36
+ client.chat(prompt, **@args, stream: @stdout)
37
+ end
38
+
39
+ # @return [OptionParser]
40
+ def parser
41
+ OptionParser.new do |options|
42
+ options.banner = 'usage: omniai chat [options] "<prompt>"'
43
+
44
+ options.on('-h', '--help', 'help') do
45
+ @stdout.puts(options)
46
+ exit
47
+ end
48
+
49
+ options.on('-p', '--provider=PROVIDER', 'provider') { |provider| @provider = provider }
50
+ options.on('-m', '--model=MODEL', 'model') { |model| @args[:model] = model }
51
+ options.on('-t', '--temperature=TEMPERATURE', Float, 'temperature') do |temperature|
52
+ @args[:temperature] = temperature
53
+ end
54
+ options.on('-f', '--format=FORMAT', 'format') { |format| @args[:format] = format.intern }
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
data/lib/omniai/cli.rb ADDED
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+
5
+ module OmniAI
6
+ # Used when interacting with the suite from the command line interface (CLI).
7
+ #
8
+ # Usage:
9
+ #
10
+ # cli = OmniAI::CLI.new
11
+ # cli.parse
12
+ class CLI
13
+ ChatArgs = Struct.new(:provider, :model, :temperature)
14
+
15
+ # @param in [IO] a stream
16
+ # @param out [IO] a stream
17
+ # @param provider [String] a provider
18
+ def initialize(stdin: $stdin, stdout: $stdout, provider: 'openai')
19
+ @stdin = stdin
20
+ @stdout = stdout
21
+ @provider = provider
22
+ @args = {}
23
+ end
24
+
25
+ def parse(argv = ARGV)
26
+ parser.order!(argv)
27
+ command = argv.shift
28
+ return if command.nil?
29
+
30
+ case command
31
+ when 'chat' then ChatHandler.handle!(stdin: @stdin, stdout: @stdout, provider: @provider, argv:)
32
+ else raise Error, "unsupported command=#{command.inspect}"
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ # @return [OptionParser]
39
+ def parser
40
+ OptionParser.new do |options|
41
+ options.banner = 'usage: omniai [options] <command> [<args>]'
42
+
43
+ options.on('-h', '--help', 'help') do
44
+ @stdout.puts(options)
45
+ exit
46
+ end
47
+
48
+ options.on('-v', '--version', 'version') do
49
+ @stdout.puts(VERSION)
50
+ exit
51
+ end
52
+
53
+ options.on('-p', '--provider=PROVIDER', 'provider (default="openai")') do |provider|
54
+ @provider = provider
55
+ end
56
+
57
+ options.separator <<~COMMANDS
58
+ commands:
59
+ chat
60
+ COMMANDS
61
+ end
62
+ end
63
+ end
64
+ end
data/lib/omniai/client.rb CHANGED
@@ -16,8 +16,6 @@ module OmniAI
16
16
  # end
17
17
  # end
18
18
  class Client
19
- class Error < StandardError; end
20
-
21
19
  # @return [String, nil]
22
20
  attr_accessor :api_key
23
21
 
@@ -30,6 +28,68 @@ module OmniAI
30
28
  # @return [Integer, nil]
31
29
  attr_accessor :timeout
32
30
 
31
+ # Initialize a client for Anthropic. This method requires the provider if it is undefined.
32
+ #
33
+ # @raise [OmniAI::Error] if the provider is not defined and the gem is not installed
34
+ # @return [Class<OmniAI::Client>]
35
+ def self.anthropic
36
+ require 'omniai/anthropic' unless defined?(OmniAI::Anthropic::Client)
37
+ OmniAI::Anthropic::Client
38
+ rescue LoadError
39
+ raise Error, "requires 'omniai-anthropic': `gem install omniai-anthropic`"
40
+ end
41
+
42
+ # Lookup the `OmniAI::Google::Client``. This method requires the provider if it is undefined.
43
+ #
44
+ # @raise [OmniAI::Error] if the provider is not defined and the gem is not installed
45
+ # @return [Class<OmniAI::Client>]
46
+ def self.google
47
+ require 'omniai/google' unless defined?(OmniAI::Google::Client)
48
+ OmniAI::Google::Client
49
+ rescue LoadError
50
+ raise Error, "requires 'omniai-google': `gem install omniai-google`"
51
+ end
52
+
53
+ # Initialize a client for Mistral. This method requires the provider if it is undefined.
54
+ #
55
+ # @raise [OmniAI::Error] if the provider is not defined and the gem is not installed
56
+ # @return [Class<OmniAI::Client>]
57
+ def self.mistral
58
+ require 'omniai/mistral' unless defined?(OmniAI::Mistral::Client)
59
+ OmniAI::Mistral::Client
60
+ rescue LoadError
61
+ raise Error, "requires 'omniai-mistral': `gem install omniai-mistral`"
62
+ end
63
+
64
+ # Initialize a client for OpenAI. This method requires the provider if it is undefined.
65
+ #
66
+ # @raise [OmniAI::Error] if the provider is not defined and the gem is not installed
67
+ # @return [Class<OmniAI::Client>]
68
+ def self.openai
69
+ require 'omniai/openai' unless defined?(OmniAI::OpenAI::Client)
70
+ OmniAI::OpenAI::Client
71
+ rescue LoadError
72
+ raise Error, "requires 'omniai-openai': `gem install omniai-openai`"
73
+ end
74
+
75
+ # Initialize a client by provider (e.g. 'openai'). This method attempts to require the provider.
76
+ #
77
+ # @raise [OmniAI::Error] if the provider is not defined and the gem is not installed
78
+ # @param provider [String] required (e.g. 'anthropic', 'google', 'mistral', 'openai', etc)
79
+ # @return [OmniAI::Client]
80
+ def self.find(provider:, **)
81
+ klass =
82
+ case provider
83
+ when 'anthropic' then anthropic
84
+ when 'google' then google
85
+ when 'mistral' then mistral
86
+ when 'openai' then openai
87
+ else raise Error, "unknown provider=#{provider.inspect}"
88
+ end
89
+
90
+ klass.new(**)
91
+ end
92
+
33
93
  # @param api_key [String, nil] optional
34
94
  # @param host [String, nil] optional - supports for customzing the host of the client (e.g. 'http://localhost:8080')
35
95
  # @param logger [Logger, nil] optional
@@ -57,7 +117,7 @@ module OmniAI
57
117
  # @return [HTTP::Client]
58
118
  def connection
59
119
  http = HTTP.persistent(@host)
60
- http = http.use(logging: { logger: @logger }) if @logger
120
+ http = http.use(instrumentation: { instrumenter: Instrumentation.new(logger: @logger) }) if @logger
61
121
  http = http.timeout(@timeout) if @timeout
62
122
  http
63
123
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ # Used for logging.
5
+ class Instrumentation
6
+ # @param logger [Logger]
7
+ def initialize(logger:)
8
+ @logger = logger
9
+ end
10
+
11
+ # @param name [String]
12
+ # @param payload [Hash]
13
+ # @option payload [Exception] :error
14
+ def instrument(name, payload = {})
15
+ error = payload[:error]
16
+ return unless error
17
+
18
+ @logger.error("#{name}: #{error.message}")
19
+ end
20
+
21
+ # @param name [String]
22
+ # @param payload [Hash]
23
+ # @option payload [HTTP::Request] :request
24
+ def start(_, payload)
25
+ request = payload[:request]
26
+ @logger.info("#{request.verb.upcase} #{request.uri}")
27
+ end
28
+
29
+ # @param name [String]
30
+ # @param payload [Hash]
31
+ # @option payload [HTTP::Response] :response
32
+ def finish(_, payload)
33
+ response = payload[:response]
34
+ @logger.info("#{response.status.code} #{response.status.reason}")
35
+ end
36
+ end
37
+ end
data/lib/omniai/speak.rb CHANGED
@@ -84,6 +84,7 @@ module OmniAI
84
84
  # @return [Tempfile]
85
85
  def process!(&block)
86
86
  response = request!
87
+
87
88
  raise HTTPError, response.flush unless response.status.ok?
88
89
 
89
90
  if block
@@ -117,6 +117,7 @@ module OmniAI
117
117
  # @return [OmniAI::Transcribe::Transcription]
118
118
  def process!
119
119
  response = request!
120
+
120
121
  raise HTTPError, response.flush unless response.status.ok?
121
122
 
122
123
  text = @format.nil? || @format.eql?(Format::JSON) ? response.parse['text'] : String(response.body)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OmniAI
4
- VERSION = '1.3.0'
4
+ VERSION = '1.4.0'
5
5
  end
data/lib/omniai.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'logger'
3
4
  require 'event_stream_parser'
4
5
  require 'http'
5
6
  require 'uri'
@@ -8,6 +9,7 @@ require 'zeitwerk'
8
9
  loader = Zeitwerk::Loader.for_gem
9
10
  loader.inflector.inflect 'omniai' => 'OmniAI'
10
11
  loader.inflector.inflect 'url' => 'URL'
12
+ loader.inflector.inflect 'cli' => 'CLI'
11
13
  loader.setup
12
14
 
13
15
  module OmniAI
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.3.0
4
+ version: 1.4.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-06-21 00:00:00.000000000 Z
11
+ date: 2024-07-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: event_stream_parser
@@ -56,7 +56,8 @@ description: An interface for OpenAI's ChatGPT, Google's Gemini, Anthropic's Cla
56
56
  Mistral's LeChat, etc.
57
57
  email:
58
58
  - kevin@ksylvest.com
59
- executables: []
59
+ executables:
60
+ - omniai
60
61
  extensions: []
61
62
  extra_rdoc_files: []
62
63
  files:
@@ -64,6 +65,7 @@ files:
64
65
  - README.md
65
66
  - bin/console
66
67
  - bin/setup
68
+ - exe/omniai
67
69
  - lib/omniai.rb
68
70
  - lib/omniai/chat.rb
69
71
  - lib/omniai/chat/chunk.rb
@@ -78,8 +80,12 @@ files:
78
80
  - lib/omniai/chat/message_choice.rb
79
81
  - lib/omniai/chat/stream.rb
80
82
  - lib/omniai/chat/usage.rb
83
+ - lib/omniai/cli.rb
84
+ - lib/omniai/cli/base_handler.rb
85
+ - lib/omniai/cli/chat_handler.rb
81
86
  - lib/omniai/client.rb
82
87
  - lib/omniai/config.rb
88
+ - lib/omniai/instrumentation.rb
83
89
  - lib/omniai/speak.rb
84
90
  - lib/omniai/transcribe.rb
85
91
  - lib/omniai/transcribe/transcription.rb