roseflow-openai 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 022b3ba6d3486eb3a7ed6abce48815272b2e4d5a52502dc814ebcc26c8c44712
4
+ data.tar.gz: eac633fb4aab864322916a3c5aa4e71cf15fe937d4c34aec84cbf8b444b44778
5
+ SHA512:
6
+ metadata.gz: 4fb415158051794661293c7ec227c9384ac8edd3882e209a70cb559f95710d99248f01bba3c91633d6398594132cb1fcb9c5780477074a1aec1f86a95ba43555
7
+ data.tar.gz: c8d660546cbc37cb46e577f32e51ddfc71c5454f0dfc62fa8fbbbdfa90ba3ae0d40018fd71de856755dab4ed4350ddb30592d82185e0f29d6710b2105441f29d
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.standard.yml ADDED
@@ -0,0 +1,3 @@
1
+ # For available configuration options, see:
2
+ # https://github.com/testdouble/standard
3
+ ruby_version: 2.6
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2023-04-26
4
+
5
+ - Initial release
@@ -0,0 +1,7 @@
1
+ # Contributor Code of Conduct
2
+
3
+ The Roseflow team is committed to fostering a welcoming community.
4
+
5
+ **Our Code of Conduct can be found here**:
6
+
7
+ https://roseflow.ai/conduct
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in roseflow-openai.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+ gem "rspec", "~> 3.5"
10
+ gem "standard", "~> 1.3"
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 Lauri Jutila
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # Roseflow OpenAI integration
2
+
3
+ This gem adds OpenAI support and integration for Roseflow, a framework for interacting with AI in Ruby.
4
+
5
+ ## Prerequisites
6
+
7
+ To use this gem effectively, you need the [core Roseflow gem](https://github.com/roseflow-ai/roseflow).
8
+
9
+ ## Installation
10
+
11
+ Install the gem and add to the application's Gemfile by executing:
12
+
13
+ $ bundle add roseflow-openai
14
+
15
+ If bundler is not being used to manage dependencies, install the gem by executing:
16
+
17
+ $ gem install roseflow-openai
18
+
19
+ ## Usage
20
+
21
+ See full documentation how to configure and use Roseflow with OpenAI at [docs.roseflow.ai](https://docs.roseflow.ai/openai).
22
+
23
+ ## Contributing
24
+
25
+ Bug reports and pull requests are welcome on GitHub at https://github.com/roseflow-ai/roseflow-openai.
26
+
27
+ ## Community
28
+
29
+ ### Discord
30
+
31
+ Join us in our [Discord](https://discord.gg/roseflow).
32
+
33
+ ### Twitter
34
+
35
+ Connect with the core team on Twitter.
36
+
37
+ <a href="https://twitter.com/ljuti" target="_blank">
38
+ <img alt="Twitter Follow" src="https://img.shields.io/twitter/follow/ljuti?logo=twitter&style=social">
39
+ </a>
40
+
41
+ ## License
42
+
43
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
44
+
45
+ ## Code of Conduct
46
+
47
+ Everyone interacting in the Roseflow OpenAI project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/roseflow-ai/roseflow-openai/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "standard/rake"
9
+
10
+ task default: %i[spec standard]
data/config/openai.yml ADDED
@@ -0,0 +1,14 @@
1
+ default: &default
2
+ organization_id: <YOUR OPENAI ORGANIZATION ID>
3
+
4
+ development:
5
+ <<: *default
6
+ api_key: <YOUR OPENAI API KEY FOR DEVELOPMENT>
7
+
8
+ test:
9
+ <<: *default
10
+ api_key: <YOUR OPENAI API KEY FOR TEST>
11
+
12
+ production:
13
+ <<: *default
14
+ api_key: <YOUR OPENAI API KEY FOR PRODUCTION>
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "faraday/retry"
5
+ require "roseflow/openai/config"
6
+ require "roseflow/openai/model"
7
+ require "roseflow/openai/response"
8
+
9
+ FARADAY_RETRY_OPTIONS = {
10
+ max: 3,
11
+ interval: 0.05,
12
+ interval_randomness: 0.5,
13
+ backoff_factor: 2,
14
+ }
15
+
16
+ module Roseflow
17
+ module OpenAI
18
+ class Client
19
+ def initialize(config = Config.new, provider = nil)
20
+ @config = config
21
+ @provider = provider
22
+ end
23
+
24
+ # Returns the available models from the API.
25
+ #
26
+ # @return [Array<OpenAI::Model>] the available models
27
+ def models
28
+ response = connection.get("/v1/models")
29
+ body = JSON.parse(response.body)
30
+ body.fetch("data", []).map do |model|
31
+ OpenAI::Model.new(model, self)
32
+ end
33
+ end
34
+
35
+ # Creates a chat completion.
36
+ #
37
+ # @param model [Roseflow::OpenAI::Model] the model to use
38
+ # @param messages [Array<String>] the messages to use
39
+ # @param options [Hash] the options to use
40
+ # @return [OpenAI::TextApiResponse] the API response object
41
+ def create_chat_completion(model:, messages:, **options)
42
+ response = connection.post("/v1/chat/completions") do |request|
43
+ request.body = options.merge({
44
+ model: model.name,
45
+ messages: messages
46
+ })
47
+ end
48
+ ChatResponse.new(response)
49
+ end
50
+
51
+ # Creates a chat completion and streams the response.
52
+ #
53
+ # @param model [Roseflow::OpenAI::Model] the model to use
54
+ # @param messages [Array<String>] the messages to use
55
+ # @param options [Hash] the options to use
56
+ # @yield [String] the streamed API response
57
+ # @return [Array<String>] the streamed API response if no block is given
58
+ def streaming_chat_completion(model:, messages:, **options, &block)
59
+ streamed = []
60
+ connection.post("/v1/chat/completions") do |request|
61
+ request.body = options.merge({
62
+ model: model.name,
63
+ messages: messages,
64
+ stream: true
65
+ })
66
+ request.options.on_data = Proc.new do |chunk|
67
+ yield streaming_chunk(chunk) if block_given?
68
+ streamed << chunk unless block_given?
69
+ end
70
+ end
71
+ streamed unless block_given?
72
+ end
73
+
74
+ # Creates a text completion for the provided prompt and parameters.
75
+ #
76
+ # @param model [Roseflow::OpenAI::Model] the model to use
77
+ # @param prompt [String] the prompt to use
78
+ # @param options [Hash] the options to use
79
+ # @return [OpenAI::TextApiResponse] the API response object
80
+ def create_completion(model:, prompt:, **options)
81
+ response = connection.post("/v1/completions") do |request|
82
+ request.body = options.merge({
83
+ model: model.name,
84
+ prompt: prompt
85
+ })
86
+ end
87
+ CompletionResponse.new(response)
88
+ end
89
+
90
+ # Creates a text completion for the provided prompt and parameters and streams the response.
91
+ #
92
+ # @param model [Roseflow::OpenAI::Model] the model to use
93
+ # @param prompt [String] the prompt to use
94
+ # @param options [Hash] the options to use
95
+ # @yield [String] the streamed API response
96
+ # @return [Array<String>] the streamed API response if no block is given
97
+ def streaming_completion(model:, prompt:, **options, &block)
98
+ streamed = []
99
+ connection.post("/v1/completions") do |request|
100
+ request.body = options.merge({
101
+ model: model.name,
102
+ prompt: prompt,
103
+ stream: true
104
+ })
105
+ request.options.on_data = Proc.new do |chunk|
106
+ yield streaming_chunk(chunk) if block_given?
107
+ streamed << chunk unless block_given?
108
+ end
109
+ end
110
+ streamed unless block_given?
111
+ end
112
+
113
+ # Given a prompt and an instruction, the model will return an edited version of the prompt.
114
+ #
115
+ # @param model [String] the model to use
116
+ # @param instruction [String] the instruction to use
117
+ # @param options [Hash] the options to use
118
+ # @return [OpenAI::TextApiResponse] the API response object
119
+ def create_edit(model:, instruction:, **options)
120
+ response = connection.post("/v1/edits") do |request|
121
+ request.body = options.merge({
122
+ model: model.name,
123
+ instruction: instruction
124
+ })
125
+ end
126
+ EditResponse.new(response)
127
+ end
128
+
129
+ def create_image(prompt:, **options)
130
+ ImageApiResponse.new(
131
+ connection.post("/v1/images/generations") do |request|
132
+ request.body = options.merge(prompt: prompt)
133
+ end
134
+ )
135
+ end
136
+
137
+ # Creates an embedding vector representing the input text.
138
+ #
139
+ # @param model [Roseflow::OpenAI::Model] the model to use
140
+ # @param input [String] the input text to use
141
+ # @return [OpenAI::EmbeddingApiResponse] the API response object
142
+ def create_embedding(model:, input:)
143
+ EmbeddingApiResponse.new(
144
+ connection.post("/v1/embeddings") do |request|
145
+ request.body = {
146
+ model: model.name,
147
+ input: input
148
+ }
149
+ end
150
+ )
151
+ end
152
+
153
+ private
154
+
155
+ attr_reader :config, :provider
156
+
157
+ # The connection object used to make requests to the API.
158
+ def connection
159
+ @connection ||= Faraday.new(
160
+ url: Config::OPENAI_API_URL,
161
+ headers: {
162
+ # "Content-Type" => "application/json",
163
+ "OpenAI-Organization" => config.organization_id
164
+ }
165
+ ) do |faraday|
166
+ faraday.request :authorization, "Bearer", -> { config.api_key }
167
+ faraday.request :json
168
+ faraday.request :retry, FARADAY_RETRY_OPTIONS
169
+ faraday.adapter Faraday.default_adapter
170
+ end
171
+ end
172
+
173
+ # Parses streaming chunks from the API response.
174
+ #
175
+ # @param chunk [String] the chunk to parse
176
+ # @return [String] the parsed chunk
177
+ def streaming_chunk(chunk)
178
+ return chunk unless chunk.match(/{.*}/)
179
+ chunk.scan(/{.*}/).map do |json|
180
+ JSON.parse(json).dig("choices", 0, "delta", "content")
181
+ end.join("")
182
+ end
183
+ end # Client
184
+ end # OpenAI
185
+ end # Roseflow
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "anyway_config"
4
+
5
+ module Roseflow
6
+ module OpenAI
7
+ # Configuration class for the OpenAI provider.
8
+ class Config < Anyway::Config
9
+ config_name :openai
10
+
11
+ attr_config :api_key, :organization_id
12
+
13
+ required :api_key
14
+ required :organization_id
15
+
16
+ OPENAI_API_URL = "https://api.openai.com"
17
+ CHAT_MODELS = %w(gpt-4 gpt-4-0314 gpt-4-32k gpt-4-32k-0314 gpt-3.5-turbo gpt-3.5-turbo-0301).freeze
18
+ COMPLETION_MODELS = %w(text-davinci-003 text-davinci-002 text-curie-001 text-babbage-001 text-ada-001 davinci curie babbage ada).freeze
19
+ EDIT_MODELS = %w(text-davinci-edit-001 code-davinci-edit-001).freeze
20
+ TRANSCRIPTION_MODELS = %w(whisper-1).freeze
21
+ TRANSLATION_MODELS = %w(whisper-1).freeze
22
+ FINE_TUNE_MODELS = %w(davinci curie babbage ada).freeze
23
+ EMBEDDING_MODELS = %w(text-embedding-ada-002 text-search-ada-doc-001).freeze
24
+ MODERATION_MODELS = %w(text-moderation-stable text-moderation-latest).freeze
25
+ MAX_TOKENS = {
26
+ "gpt-4": 8192,
27
+ "gpt-4-0314": 8192,
28
+ "gpt-4-32k": 32_768,
29
+ "gpt-4-32k-0314": 32_768,
30
+ "gpt-3.5-turbo": 4096,
31
+ "gpt-3.5-turbo-0301": 4096,
32
+ "text-davinci-003": 4097,
33
+ "text-davinci-002": 4097,
34
+ "code-davinci-002": 8001
35
+ }
36
+ end # Config
37
+ end # OpenAI
38
+ end # Roseflow
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "roseflow/types"
4
+ require "roseflow/embeddings/embedding"
5
+
6
+ module Roseflow
7
+ module OpenAI
8
+ class Embedding < Dry::Struct
9
+ transform_keys(&:to_sym)
10
+
11
+ attribute :embedding, Types::Array.of(Types::Float)
12
+
13
+ def to_embedding
14
+ Roseflow::Embeddings::Embedding.new(vector: embedding, length: embedding.length)
15
+ end
16
+ end # Embedding
17
+ end # OpenAI
18
+ end # Roseflow
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-struct"
4
+ require "roseflow/tokenizer"
5
+ require "active_support/core_ext/module/delegation"
6
+
7
+ module Types
8
+ include Dry.Types()
9
+ end
10
+
11
+ module Roseflow
12
+ module OpenAI
13
+ class Model
14
+ attr_reader :name
15
+
16
+ # Initializes a new model instance.
17
+ #
18
+ # @param model [Hash] Model data from the API
19
+ # @param provider [Roseflow::OpenAI::Provider] Provider instance
20
+ def initialize(model, provider)
21
+ @model_ = model
22
+ @provider_ = provider
23
+ assign_attributes
24
+ end
25
+
26
+ # Tokenizer instance for the model.
27
+ def tokenizer
28
+ @tokenizer_ ||= Tokenizer.new(model: name)
29
+ end
30
+
31
+ # Handles the model call.
32
+ # FIXME: Operations should be rewritten to match the client API.
33
+ #
34
+ # @param operation [Symbol] Operation to perform
35
+ # @param input [String] Input to use
36
+ def call(operation, input, **options)
37
+ token_count = tokenizer.count_tokens(transform_chat_messages(input))
38
+ if token_count < max_tokens
39
+ case operation
40
+ when :chat
41
+ @provider_.create_chat_completion(model: name, messages: transform_chat_messages(input), **options)
42
+ when :completion
43
+ @provider_.create_completion(input)
44
+ when :image
45
+ @provider_.create_image_completion(input)
46
+ when :embed
47
+ @provider_.create_embedding(input)
48
+ else
49
+ raise ArgumentError, "Invalid operation: #{operation}"
50
+ end
51
+ else
52
+ raise TokenLimitExceededError, "Token limit for model #{name} exceeded: #{token_count} is more than #{max_tokens}"
53
+ end
54
+ end
55
+
56
+ # Indicates if the model is chattable.
57
+ def chattable?
58
+ OpenAI::Config::CHAT_MODELS.include?(name)
59
+ end
60
+
61
+ # Indicates if the model can do completions.
62
+ def completionable?
63
+ OpenAI::Config::COMPLETION_MODELS.include?(name)
64
+ end
65
+
66
+ # Indicates if the model can do image completions.
67
+ def imageable?
68
+ OpenAI::Config::IMAGE_MODELS.include?(name)
69
+ end
70
+
71
+ # Indicates if the model can do embeddings.
72
+ def embeddable?
73
+ OpenAI::Config::EMBEDDING_MODELS.include?(name)
74
+ end
75
+
76
+ # Indicates if the model is fine-tunable.
77
+ def finetuneable?
78
+ @permissions_.fetch("allow_fine_tuning")
79
+ end
80
+
81
+ # Indicates if the model has searchable indices.
82
+ def searchable_indices?
83
+ @permissions_.fetch("allow_search_indices")
84
+ end
85
+
86
+ # Indicates if the model can be sampled.
87
+ def sampleable?
88
+ @permissions_.fetch("allow_sampling")
89
+ end
90
+
91
+ def blocking?
92
+ @permissions_.fetch("is_blocking")
93
+ end
94
+
95
+ # Returns the maximum number of tokens for the model.
96
+ def max_tokens
97
+ OpenAI::Config::MAX_TOKENS.fetch(name, 2049)
98
+ end
99
+
100
+ private
101
+
102
+ def assign_attributes
103
+ @name = @model_.fetch("id")
104
+ @created_at = Time.at(@model_.fetch("created"))
105
+ @permissions_ = @model_.fetch("permission").first
106
+ end
107
+
108
+ def transform_chat_messages(input)
109
+ input.map(&:to_h)
110
+ end
111
+ end # Model
112
+
113
+ # Represents a model permission.
114
+ class ModelPermission < Dry::Struct
115
+ transform_keys(&:to_sym)
116
+
117
+ attribute :id, Types::String
118
+ attribute :object, Types::String
119
+ attribute :created, Types::Integer
120
+ attribute :allow_create_engine, Types::Bool
121
+ attribute :allow_sampling, Types::Bool
122
+ attribute :allow_logprobs, Types::Bool
123
+ attribute :allow_search_indices, Types::Bool
124
+ attribute :allow_view, Types::Bool
125
+ attribute :allow_fine_tuning, Types::Bool
126
+ attribute :organization, Types::String
127
+ attribute :is_blocking, Types::Bool
128
+
129
+ alias_method :finetuneable?, :allow_fine_tuning
130
+ alias_method :is_blocking?, :is_blocking
131
+ end # ModelPermission
132
+
133
+ # Represents a model configuration.
134
+ class ModelConfiguration < Dry::Struct
135
+ transform_keys(&:to_sym)
136
+
137
+ attribute :id, Types::String
138
+ attribute :created, Types::Integer
139
+ attribute :permission, Types::Array.of(ModelPermission)
140
+ attribute :root, Types::String
141
+ attribute :parent, Types::String | Types::Nil
142
+
143
+ alias_method :name, :id
144
+
145
+ def permissions
146
+ permission.first
147
+ end
148
+
149
+ delegate :finetuneable?, :is_blocking?, to: :permissions
150
+ end # ModelConfiguration
151
+ end # OpenAI
152
+ end # Roseflow
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/module/delegation"
4
+
5
+ module Roseflow
6
+ module OpenAI
7
+ class ModelRepository
8
+ attr_reader :models
9
+
10
+ delegate :each, :all, to: :models
11
+
12
+ def initialize(provider)
13
+ @provider = provider
14
+ @models = provider.client.models
15
+ end
16
+
17
+ # Finds a model by name.
18
+ #
19
+ # @param name [String] Name of the model
20
+ def find(name)
21
+ @models.select{ |model| model.name == name }.first
22
+ end
23
+
24
+ # Returns all models that are chattable.
25
+ def chattable
26
+ @models.select(&:chattable?)
27
+ end
28
+
29
+ # Returns all models that are completionable.
30
+ def completionable
31
+ @models.select(&:completionable?)
32
+ end
33
+
34
+ # Returns all models that are support edits.
35
+ def editable
36
+ @models.select(&:editable?)
37
+ end
38
+
39
+ # Returns all models that are support embeddings.
40
+ def embeddable
41
+ @models.select(&:embeddable?)
42
+ end
43
+ end # ModelRepository
44
+ end # OpenAI
45
+ end # Roseflow
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "roseflow/openai/client"
4
+ require "roseflow/openai/model_repository"
5
+
6
+ module Roseflow
7
+ module OpenAI
8
+ class Provider
9
+ def initialize(config = Roseflow::OpenAI::Config.new)
10
+ @config = config
11
+ end
12
+
13
+ # Returns the client for the provider
14
+ def client
15
+ @client ||= Client.new(config, self)
16
+ end
17
+
18
+ # Returns the model repository for the provider
19
+ def models
20
+ @models ||= ModelRepository.new(self)
21
+ end
22
+
23
+ # Chat with a model
24
+ #
25
+ # @param model [Roseflow::OpenAI::Model] The model object to use
26
+ # @param messages [Array<String>] The messages to send to the model
27
+ # @param options [Hash] Additional options to pass to the API
28
+ # @option options [Integer] :max_tokens The maximum number of tokens to generate in the completion.
29
+ # @option options [Float] :temperature Sampling temperature to use, between 0 and 2
30
+ # @option options [Float] :top_p The cumulative probability of tokens to use.
31
+ # @option options [Integer] :n The number of completions to generate.
32
+ # @option options [Integer] :logprobs Include the log probabilities on the logprobs most likely tokens.
33
+ # @option options [Boolean] :echo Whether to echo the question as part of the completion.
34
+ # @option options [String | Array] :stop Up to 4 sequences where the API will stop generating further tokens. The returned text will not contain the stop sequence.
35
+ # @option options [Float] :presence_penalty Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics.
36
+ # @option options [Float] :frequency_penalty Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.
37
+ # @option options [Integer] :best_of Generates `best_of` completions server-side and returns the "best" (the one with the lowest log probability per token)
38
+ # @option options [Integer] :streaming Whether to stream back partial progress
39
+ # @option options [String] :user A unique identifier representing your end-user
40
+ # @return [Roseflow::OpenAI::ChatResponse] The response object from the API.
41
+ def chat(model:, messages:, **options)
42
+ streaming = options.fetch(:streaming, false)
43
+
44
+ if streaming
45
+ client.streaming_chat_completion(model: model, messages: messages.map(&:to_h), **options)
46
+ else
47
+ client.create_chat_completion(model: model, messages: messages.map(&:to_h), **options)
48
+ end
49
+ end
50
+
51
+ # Create a completion.
52
+ #
53
+ # @param model [Roseflow::OpenAI::Model] The model object to use
54
+ # @param prompt [String] The prompt to use for completion
55
+ # @param options [Hash] Additional options to pass to the API
56
+ # @option options [Integer] :max_tokens The maximum number of tokens to generate in the completion.
57
+ # @option options [Float] :temperature Sampling temperature to use, between 0 and 2
58
+ # @option options [Float] :top_p The cumulative probability of tokens to use.
59
+ # @option options [Integer] :n The number of completions to generate.
60
+ # @option options [Integer] :logprobs Include the log probabilities on the logprobs most likely tokens.
61
+ # @option options [Boolean] :echo Whether to echo the question as part of the completion.
62
+ # @option options [String | Array] :stop Up to 4 sequences where the API will stop generating further tokens.
63
+ # @option options [Float] :presence_penalty Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics.
64
+ # @option options [Float] :frequency_penalty Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.
65
+ # @option options [Integer] :best_of Generates `best_of` completions server-side and returns the "best" (the one with the lowest log probability per token)
66
+ # @option options [Integer] :streaming Whether to stream back partial progress
67
+ # @option options [String] :user A unique identifier representing your end-user
68
+ # @return [Roseflow::OpenAI::CompletionResponse] The response object from the API.
69
+ def completion(model:, prompt:, **options)
70
+ streaming = options.fetch(:streaming, false)
71
+
72
+ if streaming
73
+ client.streaming_completion(model: model, prompt: prompt, **options)
74
+ else
75
+ client.create_completion(model: model, prompt: prompt, **options)
76
+ end
77
+ end
78
+
79
+ # Creates a new edit for the provided input, instruction, and parameters.
80
+ #
81
+ # @param model [Roseflow::OpenAI::Model] The model object to use
82
+ # @param instruction [String] The instruction to use for editing
83
+ # @param options [Hash] Additional options to pass to the API
84
+ # @option options [String] :input The input text to use as a starting point for the edit.
85
+ # @option options [Integer] :n The number of edits to generate.
86
+ # @option options [Float] :temperature Sampling temperature to use, between 0 and 2
87
+ # @option options [Float] :top_p The cumulative probability of tokens to use.
88
+ # @return [Roseflow::OpenAI::EditResponse] The response object from the API.
89
+ def edit(model:, instruction:, **options)
90
+ client.create_edit(model: model, instruction: instruction, **options)
91
+ end
92
+
93
+ # Creates an embedding vector representing the input text.
94
+ #
95
+ # @param model [Roseflow::OpenAI::Model] The model object to use
96
+ # @param input [String] The input text to use for embedding
97
+ # @param options [Hash] Additional options to pass to the API
98
+ # @option options [String] :user A unique identifier representing your end-user
99
+ def embedding(model:, input:, **options)
100
+ client.create_embedding(model: model, input: input, **options).embedding.to_embedding
101
+ end
102
+
103
+ def image(prompt:, **options)
104
+ client.create_image(prompt: prompt, **options)
105
+ end
106
+
107
+ attr_reader :config
108
+ end # Provider
109
+ end # OpenAI
110
+ end # Roseflow
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-struct"
4
+ require "roseflow/types"
5
+ require "roseflow/openai/embedding"
6
+
7
+ module Types
8
+ include Dry.Types()
9
+ Number = Types::Integer | Types::Float
10
+ end
11
+
12
+ module Roseflow
13
+ module OpenAI
14
+ FailedToCreateEmbeddingError = Class.new(StandardError)
15
+
16
+ class ApiResponse
17
+ def initialize(response)
18
+ @response = response
19
+ end
20
+
21
+ def success?
22
+ @response.success?
23
+ end
24
+
25
+ def status
26
+ @response.status
27
+ end
28
+
29
+ def body
30
+ raise NotImplementedError, "Subclasses must implement this method."
31
+ end
32
+ end # ApiResponse
33
+
34
+ class TextApiResponse < ApiResponse
35
+ def body
36
+ @body ||= ApiResponseBody.new(JSON.parse(@response.body))
37
+ end
38
+
39
+ def choices
40
+ body.choices.map { |choice| Choice.new(choice) }
41
+ end
42
+ end # TextApiResponse
43
+
44
+ class ChatResponse < TextApiResponse
45
+ def response
46
+ choices.first
47
+ end
48
+ end
49
+
50
+ class CompletionResponse < TextApiResponse
51
+ def response
52
+ choices.first
53
+ end
54
+
55
+ def responses
56
+ choices
57
+ end
58
+ end
59
+
60
+ class EditResponse < TextApiResponse
61
+ def response
62
+ choices.first
63
+ end
64
+
65
+ def responses
66
+ choices
67
+ end
68
+ end
69
+
70
+ class ImageApiResponse < ApiResponse
71
+ def body
72
+ @body ||= ImageApiResponseBody.new(JSON.parse(@response.body))
73
+ end
74
+
75
+ def images
76
+ body.data.map { |image| Image.new(image) }
77
+ end
78
+ end # ImageApiResponse
79
+
80
+ class EmbeddingApiResponse < ApiResponse
81
+ def body
82
+ @body ||= begin
83
+ case @response.status
84
+ when 200
85
+ EmbeddingApiResponseBody.new(JSON.parse(@response.body))
86
+ else
87
+ EmbeddingApiResponseErrorBody.new(JSON.parse(@response.body))
88
+ end
89
+ end
90
+ end
91
+
92
+ def embedding
93
+ case @response.status
94
+ when 200
95
+ body.data.map { |embedding| Embedding.new(embedding) }.first
96
+ else
97
+ raise FailedToCreateEmbeddingError, body.error.message
98
+ end
99
+ end
100
+ end # EmbeddingApiResponse
101
+
102
+ class Image < Dry::Struct
103
+ transform_keys(&:to_sym)
104
+
105
+ attribute :url, Types::String
106
+ end # Image
107
+
108
+ class Choice < Dry::Struct
109
+ transform_keys(&:to_sym)
110
+
111
+ attribute? :text, Types::String
112
+ attribute? :message do
113
+ attribute :role, Types::String
114
+ attribute :content, Types::String
115
+ end
116
+
117
+ attribute? :finish_reason, Types::String
118
+ attribute :index, Types::Integer
119
+
120
+ def to_s
121
+ return message.content if message
122
+ return text if text
123
+ end
124
+ end # Choice
125
+
126
+ class ApiUsage < Dry::Struct
127
+ transform_keys(&:to_sym)
128
+
129
+ attribute :prompt_tokens, Types::Integer
130
+ attribute? :completion_tokens, Types::Integer
131
+ attribute :total_tokens, Types::Integer
132
+ end # ApiUsage
133
+
134
+ class ImageApiResponseBody < Dry::Struct
135
+ transform_keys(&:to_sym)
136
+
137
+ attribute :created, Types::Integer
138
+ attribute :data, Types::Array(Types::Hash)
139
+ end # ImageApiResponseBody
140
+
141
+ class OpenAIEmbedding < Dry::Struct
142
+ transform_keys(&:to_sym)
143
+
144
+ attribute :object, Types::String.default("embedding")
145
+ attribute :embedding, Types::Array(Types::Number)
146
+ attribute :index, Types::Integer
147
+ end # OpenAIEmbedding
148
+
149
+ class EmbeddingApiResponseBody < Dry::Struct
150
+ transform_keys(&:to_sym)
151
+
152
+ attribute :object, Types::String
153
+ attribute :data, Types::Array(OpenAIEmbedding)
154
+ attribute :model, Types::String
155
+ attribute :usage, ApiUsage
156
+ end # EmbeddingApiResponseBody
157
+
158
+ class EmbeddingApiResponseErrorBody < Dry::Struct
159
+ transform_keys(&:to_sym)
160
+
161
+ attribute :error do
162
+ attribute :message, Types::String
163
+ end
164
+ end # EmbeddingApiResponseErrorBody
165
+
166
+ class ApiResponseBody < Dry::Struct
167
+ transform_keys(&:to_sym)
168
+
169
+ attribute? :id, Types::String
170
+ attribute :object, Types::String
171
+ attribute :created, Types::Integer
172
+ attribute? :model, Types::String
173
+ attribute :usage, ApiUsage
174
+ attribute :choices, Types::Array
175
+
176
+ def success?
177
+ true
178
+ end
179
+ end # ApiResponseBody
180
+ end # OpenAI
181
+ end # Roseflow
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-struct"
4
+
5
+ module Roseflow
6
+ module OpenAI
7
+ # A model instruction struct. Used to pass instructions to the model.
8
+ # @param instruction [String] The instruction that tells the model how to edit the prompt.
9
+ # @param input [String] The input text to use as a starting point for the edit.
10
+ # @param n [Integer] Number of results to be returned by the model.
11
+ # @param temperature [Float] Sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.
12
+ # @param top_p [Float] An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass.
13
+ class EditModelInstruction < Dry::Struct
14
+ attribute :instruction, Types::String
15
+ attribute :input, Types::String.default("")
16
+ attribute :n, Types::Integer.default(1)
17
+ attribute :temperature, Types::Float.default(1)
18
+ attribute :top_p, Types::Float.default(1)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roseflow
4
+ module OpenAI
5
+ def self.gem_version
6
+ Gem::Version.new VERSION::STRING
7
+ end
8
+
9
+ module VERSION
10
+ MAJOR = 0
11
+ MINOR = 1
12
+ PATCH = 0
13
+ PRE = nil
14
+
15
+ STRING = [MAJOR, MINOR, PATCH, PRE].compact.join(".")
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "openai/version"
4
+
5
+ module Roseflow
6
+ module OpenAI
7
+ class Error < StandardError; end
8
+ end
9
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/roseflow/openai/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "roseflow-openai"
7
+ spec.version = Roseflow::OpenAI.gem_version
8
+ spec.authors = ["Lauri Jutila"]
9
+ spec.email = ["git@laurijutila.com"]
10
+
11
+ spec.summary = "Roseflow meets OpenAI"
12
+ spec.description = "OpenAI integration and models for Roseflow."
13
+ spec.homepage = "https://github.com/roseflow-ai/roseflow-openai"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 3.2.0"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = "https://github.com/roseflow-ai/roseflow-openai"
19
+ spec.metadata["changelog_uri"] = "https://github.com/roseflow-ai/roseflow-openai/blob/master/CHANGELOG.md"
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(__dir__) do
24
+ `git ls-files -z`.split("\x0").reject do |f|
25
+ (File.expand_path(f) == __FILE__) || f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor])
26
+ end
27
+ end
28
+ spec.bindir = "exe"
29
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
30
+ spec.require_paths = ["lib"]
31
+
32
+ spec.add_dependency "activesupport"
33
+ spec.add_dependency "anyway_config", "~> 2.0"
34
+ spec.add_dependency "dry-struct"
35
+ spec.add_dependency "faraday"
36
+ spec.add_dependency "faraday-retry"
37
+
38
+ spec.add_development_dependency "awesome_print"
39
+ spec.add_development_dependency "pry"
40
+ spec.add_development_dependency "roseflow"
41
+ spec.add_development_dependency "webmock"
42
+ spec.add_development_dependency "vcr"
43
+ # For more information and examples about making a new gem, check out our
44
+ # guide at: https://bundler.io/guides/creating_gem.html
45
+ end
@@ -0,0 +1,6 @@
1
+ module Roseflow
2
+ module Openai
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end
metadata ADDED
@@ -0,0 +1,207 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: roseflow-openai
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Lauri Jutila
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-05-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: anyway_config
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: dry-struct
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: faraday
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: faraday-retry
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: awesome_print
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: pry
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: roseflow
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: webmock
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: vcr
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ description: OpenAI integration and models for Roseflow.
154
+ email:
155
+ - git@laurijutila.com
156
+ executables: []
157
+ extensions: []
158
+ extra_rdoc_files: []
159
+ files:
160
+ - ".rspec"
161
+ - ".standard.yml"
162
+ - CHANGELOG.md
163
+ - CODE_OF_CONDUCT.md
164
+ - Gemfile
165
+ - LICENSE.txt
166
+ - README.md
167
+ - Rakefile
168
+ - config/openai.yml
169
+ - lib/roseflow/openai.rb
170
+ - lib/roseflow/openai/client.rb
171
+ - lib/roseflow/openai/config.rb
172
+ - lib/roseflow/openai/embedding.rb
173
+ - lib/roseflow/openai/model.rb
174
+ - lib/roseflow/openai/model_repository.rb
175
+ - lib/roseflow/openai/provider.rb
176
+ - lib/roseflow/openai/response.rb
177
+ - lib/roseflow/openai/structs.rb
178
+ - lib/roseflow/openai/version.rb
179
+ - roseflow-openai.gemspec
180
+ - sig/roseflow/openai.rbs
181
+ homepage: https://github.com/roseflow-ai/roseflow-openai
182
+ licenses:
183
+ - MIT
184
+ metadata:
185
+ homepage_uri: https://github.com/roseflow-ai/roseflow-openai
186
+ source_code_uri: https://github.com/roseflow-ai/roseflow-openai
187
+ changelog_uri: https://github.com/roseflow-ai/roseflow-openai/blob/master/CHANGELOG.md
188
+ post_install_message:
189
+ rdoc_options: []
190
+ require_paths:
191
+ - lib
192
+ required_ruby_version: !ruby/object:Gem::Requirement
193
+ requirements:
194
+ - - ">="
195
+ - !ruby/object:Gem::Version
196
+ version: 3.2.0
197
+ required_rubygems_version: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: '0'
202
+ requirements: []
203
+ rubygems_version: 3.4.1
204
+ signing_key:
205
+ specification_version: 4
206
+ summary: Roseflow meets OpenAI
207
+ test_files: []