rubygpt 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 80d7d5153895b17a6dc4bd74f3a55ad90a80e8b1a991256d270dbce2813c46af
4
+ data.tar.gz: 0aee0ce7e85e9a93f05bd26c114bd5cd536cd0bdf70c07e587e5bd751735d82a
5
+ SHA512:
6
+ metadata.gz: ec76d8aa68dffa1584357eac6a8ebf8c75fef4238a9413f190abd688cf095a8d4cc9e1cf5588f6c11fb9f333e88c8325f2b26f698311d63232ea9830cb9e58ed
7
+ data.tar.gz: 8b128b1f7901e72369d3323d521b9c49b00260fa92a2ac1ccca11b36bce72f0f7e78c0f3876bdb2c01d2cc8e491032599cefc2cd7e022749c25040a11b6948d0
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,21 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.2.0
3
+
4
+ Style/StringLiterals:
5
+ Enabled: true
6
+ EnforcedStyle: double_quotes
7
+
8
+ Style/StringLiteralsInInterpolation:
9
+ Enabled: true
10
+ EnforcedStyle: double_quotes
11
+
12
+ Layout/LineLength:
13
+ Max: 120
14
+
15
+ Metrics/BlockLength:
16
+ Exclude:
17
+ - 'spec/**/*'
18
+ - 'config/**/*'
19
+ - 'db/**/*'
20
+ - 'lib/tasks/**/*'
21
+ - 'rubygpt.gemspec'
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-3.2.0
data/CHANGELOG.md ADDED
@@ -0,0 +1,8 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2024-03-08
4
+
5
+ - Created the core of the project
6
+ - Enhanced development environment
7
+ - Chat API integration
8
+ - JSON mode support
@@ -0,0 +1,84 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
6
+
7
+ We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
8
+
9
+ ## Our Standards
10
+
11
+ Examples of behavior that contributes to a positive environment for our community include:
12
+
13
+ * Demonstrating empathy and kindness toward other people
14
+ * Being respectful of differing opinions, viewpoints, and experiences
15
+ * Giving and gracefully accepting constructive feedback
16
+ * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
17
+ * Focusing on what is best not just for us as individuals, but for the overall community
18
+
19
+ Examples of unacceptable behavior include:
20
+
21
+ * The use of sexualized language or imagery, and sexual attention or
22
+ advances of any kind
23
+ * Trolling, insulting or derogatory comments, and personal or political attacks
24
+ * Public or private harassment
25
+ * Publishing others' private information, such as a physical or email
26
+ address, without their explicit permission
27
+ * Other conduct which could reasonably be considered inappropriate in a
28
+ professional setting
29
+
30
+ ## Enforcement Responsibilities
31
+
32
+ Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
33
+
34
+ Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
35
+
36
+ ## Scope
37
+
38
+ This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
39
+
40
+ ## Enforcement
41
+
42
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at feapaydin@gmail.com. All complaints will be reviewed and investigated promptly and fairly.
43
+
44
+ All community leaders are obligated to respect the privacy and security of the reporter of any incident.
45
+
46
+ ## Enforcement Guidelines
47
+
48
+ Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
49
+
50
+ ### 1. Correction
51
+
52
+ **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
53
+
54
+ **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
55
+
56
+ ### 2. Warning
57
+
58
+ **Community Impact**: A violation through a single incident or series of actions.
59
+
60
+ **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
61
+
62
+ ### 3. Temporary Ban
63
+
64
+ **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
65
+
66
+ **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
67
+
68
+ ### 4. Permanent Ban
69
+
70
+ **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
71
+
72
+ **Consequence**: A permanent ban from any sort of public interaction within the community.
73
+
74
+ ## Attribution
75
+
76
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
77
+ available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
78
+
79
+ Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
80
+
81
+ [homepage]: https://www.contributor-covenant.org
82
+
83
+ For answers to common questions about this code of conduct, see the FAQ at
84
+ https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2024 - Furkan Enes Apaydın
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,236 @@
1
+ [![Gem Version](https://badge.fury.io/rb/rubygpt.svg)](https://badge.fury.io/rb/rubygpt)
2
+ [![Ruby Style Guide](https://img.shields.io/badge/code_style-rubocop-brightgreen.svg)](https://github.com/rubocop/rubocop)
3
+ [![Spec](https://github.com/feapaydin/rubygpt/actions/workflows/spec.yml/badge.svg?branch=main)](https://github.com/feapaydin/rubygpt/actions/workflows/spec.yml)
4
+ [![Maintainability](https://api.codeclimate.com/v1/badges/55d19b1c7fbe8c48e9ca/maintainability)](https://codeclimate.com/github/feapaydin/rubygpt/maintainability)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ # RubyGPT
8
+
9
+ This gem aims to provide an easy-to-use Ruby wrapper for all modules of OpenAI's ChatGPT API. It is designed to be simple and easy to use, while also providing a high level of customization. It is also aiming to work efficiently in Ruby on Rails applications.
10
+
11
+ ### Capabilities
12
+
13
+ - [Chat Completions API](#chat-completions-api)
14
+ - [JSON Mode Support](#json-mode-messages)
15
+
16
+ The gem is in the early stages. It's designed to be easily extendable for other modules of the OpenAI APIs and products in the future. See [Contributing](#contributing) section for more details.
17
+
18
+ ## Installation
19
+
20
+ Install the gem directly via bundler:
21
+
22
+ ```sh
23
+ $ bundle install rubygpt
24
+ ```
25
+
26
+ Or add it to your `Gemfile` for your project:
27
+
28
+ ```ruby
29
+ gem "rubygpt"
30
+ ```
31
+
32
+ And then execute:
33
+
34
+ ```sh
35
+ $ bundle install
36
+ ```
37
+
38
+ ## Configuration
39
+
40
+ In order to access the OpenAI APIs, you must configure the Rubygpt client with your API key and the preferred ChatGPT model to use. This can be done globally for the entire application, or on a per-request basis.
41
+
42
+ ```ruby
43
+ Rubygpt.configure(api_key: 'YOUR_API_KEY', model: 'gpt-3.5-turbo')
44
+ ```
45
+
46
+ Alternatively, you can provide a block to set the configuration options:
47
+
48
+ ```ruby
49
+ Rubygpt.configure do |config|
50
+ config.api_key = 'YOUR_API_KEY'
51
+ config.model = 'gpt-3.5-turbo'
52
+ end
53
+ ```
54
+
55
+ The above examples will create a singleton client that works across the entire application.
56
+
57
+ If you'd like to use different configurations for different parts of your application, you can manually create client instances and configure them separately:
58
+
59
+ ```ruby
60
+ # Setup different client objects with different configurations
61
+ client_gpt3 = Rubygpt::Client.new(api_key: 'YOUR_API_KEY', model: 'gpt-3.5-turbo')
62
+ client_gpt4 = Rubygpt::Client.new do |config|
63
+ config.api_key = 'YOUR_SECOND_API_KEY'
64
+ config.model = 'gpt-4'
65
+ config.organization_id = 'OPENAI_ORG_ID'
66
+ end
67
+
68
+ # Use the client objects to create different requesters
69
+ chat_requester_gpt3 = Rubygpt::Requester::ChatRequester.new(client_gpt3)
70
+ chat_requester_gpt4 = Rubygpt::Requester::ChatRequester.new(client_gpt4)
71
+ ```
72
+
73
+ The following attributes can be configured when initializing the client:
74
+
75
+ - `api_key` (required): Your OpenAI API key
76
+ - `model` (required): The model to use for the API requests.
77
+ - `api_url`: The base URL for the API requests. The default is `https://api.openai.com/v1`
78
+ - `organization_id`: The organization ID to use for the API requests.
79
+ - `connection_adapter`: The HTTP connection adapter to use for the API requests. The default is `:faraday`
80
+
81
+ #### Connection Adapters
82
+
83
+ The Rubygpt client uses [Faraday](https://github.com/lostisland/faraday) to manage HTTP connections. This allows the entire power of Faraday to be used for the API requests, including diverse HTTP adapters and features like streaming.
84
+
85
+ ## Chat Completions API
86
+
87
+ Chat Completions is a Text Completion feature provided by OpenAI's ChatGPT. It can be used to generate human-like responses to a given prompt. It is one of the core features of the ChatGPT.
88
+
89
+ See the [OpenAI Chat Completions API documentation](https://platform.openai.com/docs/guides/text-generation/chat-completions-api) and related [Chat API reference](https://platform.openai.com/docs/api-reference/chat/create) for more information.
90
+
91
+ ### Sending Messages
92
+
93
+ After configuring the Rubygpt client, you can perform requests to the Chat Completions API directly.
94
+
95
+ ```ruby
96
+ # Send a message to GPT
97
+ Rubygpt.chat.create("A system of cells interlinked.") # any message you'd like to send
98
+
99
+ # Send multiple messages
100
+ Rubygpt.chat.create(["Within cells interlinked", "Within cells interlinked", "Within one stem"])
101
+ ```
102
+
103
+ To use the received responses, refer to the [Using Chat Completion Responses](#using-chat-completion-responses) section.
104
+
105
+ ### Customizing the Messages
106
+
107
+ By default each message is sent with `system` role and the provided contents in the call.
108
+
109
+ ```ruby
110
+ Rubygpt.chat.create("Test message.") # { role: "system", content: "Test message." }
111
+ ```
112
+
113
+ You can customize the request by providing additional parameters to the `create` method.
114
+
115
+ A `Message` object consist of following attributes:
116
+ - `role`: The role of the messages author.
117
+ - `content`: The content of the message.
118
+ - `name`: The name of the messages author or function name. (has multiple use cases, see OpenAI docs for details)
119
+ - `tool_calls`: The tool calls generated by the model, such as function calls. (role: assistant only)
120
+ - `tool_call_id`: Tool call that this message is responding to. (role: tool only)
121
+
122
+ For extended details on the attributes, visit the [OpenAI Chat API reference](https://platform.openai.com/docs/api-reference/chat/create).
123
+
124
+ ```ruby
125
+ Rubygpt.chat.create(role: 'user', content: "What is Ruby?")
126
+ Rubygpt.chat.create(role: 'assistant', name: 'furkan', content: "Ruby is a...")
127
+ ```
128
+ It is also possible to send multiple message objects with a single request.
129
+
130
+ ```ruby
131
+ messages = [
132
+ { role: 'user', content: "foo" },
133
+ { role: 'assistant', name: 'johndoe', content: "bar" }
134
+ ]
135
+ Rubygpt.chat.create(messages) # send the array/hash directly
136
+ Rubygpt.chat.create(messages:) # also works as a keyword argument
137
+ ```
138
+
139
+ ### Customizing The Requests
140
+
141
+ You can send any available request body parameter supported by OpenAI Chat Completion API to the `Rubygpt.chat.create` method. Just send the messages in the `messages:` keyword argument. Any additional parameters you'll provide will be passed-through to the request body directly.
142
+
143
+ ```ruby
144
+ Rubygpt.chat.create(
145
+ n: 5,
146
+ messages: ["What time is it?"],
147
+ model: 'gpt-4-turbo-preview', # overrides your client config when provided explicity here
148
+ max_tokens: 100,
149
+ frequency_penalty: 1.0,
150
+ tempature: 1,
151
+ user: 'feapaydin',
152
+ json: true # DO NOT provide response_format for JSON mode, use this flag
153
+ )
154
+ ```
155
+
156
+ ### JSON Mode Messages
157
+
158
+ The JSON Mode is a feature of the Chat Completions API that forces the model to generate a JSON response.
159
+
160
+ > A common way to use Chat Completions is to instruct the model to always return a JSON object that makes sense for your use case, by specifying this in the system message. While this does work in some cases, occasionally the models may generate output that does not parse to valid JSON objects.
161
+
162
+ See [JSON Mode official docs](https://platform.openai.com/docs/guides/text-generation/json-mode) for more details.
163
+
164
+ To send a message in JSON mode, you can simply send `json: true` option along with your message.
165
+
166
+ ```ruby
167
+ # Single message with JSON mode
168
+ Rubygpt.chat.create(content: "List all programming languages by their creation date.", json: true)
169
+
170
+ # Multiple messages with JSON mode
171
+ messages = [
172
+ { role: 'user', content: "List all programming languages by their creation date." },
173
+ { role: 'user', content: "Also add their creator's name to the objects." }
174
+ ]
175
+ Rubygpt.chat.create(messages:, json: true)
176
+ ```
177
+
178
+ ### Stream Mode
179
+
180
+ Streaming mode is a feature of the Chat Completions API that allows the model to generate a continuous stream of messages. This is useful for chat applications where the model is expected to generate multiple or longer responses to a single prompt.
181
+
182
+ Stream mode is not supported at the moment, but it's planned to be implemented in the future versions.
183
+
184
+ ### Using Chat Completion Responses
185
+
186
+ Regardless of the amount of messages sent, the response will be an instance of `Response::ChatCompletion` object.
187
+ The object wraps a set of methods to easily access the response data provided by the [OpenAI Chat API: Create](https://platform.openai.com/docs/api-reference/chat/create).
188
+
189
+ ```ruby
190
+ response = Rubygpt.chat.create("What time is it?", "Also tell me the date.")
191
+ response.messages # ["It's 12:00 PM.", "Today is 2032-01-01."]
192
+ response.read # => "It's 12:00 PM. Today is 2032-01-01."
193
+ response.failed? # => true if any message (choice) have finish_reason other than "stop"
194
+ response.cost # => Total cost of the request (usage.total_tokens)
195
+ response.to_h # => Hash representation of the response
196
+
197
+ # Full attributes:
198
+ # :id, :object, :created, :model, :system_fingerprint, :usage, :choices
199
+ ```
200
+
201
+ Each Choice in the response is an instance of `Response::ChatCompletion::Choice` object.
202
+
203
+ ```ruby
204
+ response = Rubygpt.chat.create("What time is it?", "Also tell me the date.")
205
+ response.choices # => [Response::ChatCompletion::Choice, Response::ChatCompletion::Choice]
206
+ response.choices.first.index # => 0
207
+ response.choices.first.message # => <#Common::Message>
208
+ response.choices.first.content # => "It's 12:00 PM." - delegated from message
209
+ response.choices.first.role # => "system" - delegated from message
210
+ response.choices.first.finish_reason # => "stop"
211
+ response.choices.first.failed? # => false, unless finish_reason is not "stop"
212
+ response.choices.first.to_h # => Hash representation of the choice
213
+ response.choices.first.logprobs # => Log probabilities of the tokens, hash or null
214
+ ```
215
+
216
+ ## Development
217
+
218
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
219
+
220
+ The console contains a pre-configured Rubygpt client with a test API key. See [bin/console](bin/console) for more details.
221
+
222
+ To set the API key for the tests, set the environment variable `OPENAI_API_KEY` to access the APIs.
223
+
224
+ ```sh
225
+ export OPENAI_API_KEY=your_api_key
226
+ ```
227
+
228
+ ## Contributing
229
+
230
+ Bug reports and pull requests are welcome on GitHub at https://github.com/feapaydin/rubygpt. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/feapaydin/rubygpt/blob/main/CODE_OF_CONDUCT.md).
231
+
232
+ Participations are much welcome in this project as it's still in the early stages of development. You can contribute by addressing the issues flagged as `good first issue` or `help wanted` in the issues section. You can also contribute by opening new issues, suggesting new features, or reporting bugs.
233
+
234
+ ## Code of Conduct
235
+
236
+ Everyone interacting in the Rubygpt project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/feapaydin/rubygpt/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
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 "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
data/SECURITY.md ADDED
@@ -0,0 +1,7 @@
1
+ # Security Policy
2
+
3
+ ## Reporting a Vulnerability
4
+
5
+ We're running this project solely on GitHub. Please report any vulnerability issues that you encounter by creating a new issue on GitHub. Please be careful not to cause any possible abuses when sharing details.
6
+
7
+ For more delicate vulnerabilities, please contact me directly at feapaydin@gmail.com
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubygpt
4
+ class Client
5
+ # Handles the configuration options when initializing a new Rubygpt::Client object
6
+ class Configuration
7
+ # Initializes a new Rubygpt::Client::Configuration object from a hash or another Configuration object
8
+ def self.from(configuration_input)
9
+ case configuration_input
10
+ when Configuration then configuration_input
11
+ when Hash, nil then Configuration.new(configuration_input || {})
12
+ else raise InvalidConfigurationError, "Invalid configuration provided for client."
13
+ end
14
+ end
15
+
16
+ class InvalidConfigurationError < StandardError; end
17
+
18
+ # The connection adapter to use for the client
19
+ # Defaults to :faraday
20
+ attr_accessor :connection_adapter
21
+
22
+ # The API URL to use for the client
23
+ attr_accessor :api_url
24
+
25
+ # The API key to use for the client
26
+ attr_accessor :api_key
27
+
28
+ # The OpenAI OrganizationID that will be sent when making requests (optional)
29
+ # https://platform.openai.com/docs/api-reference/organization-optional
30
+ attr_accessor :organization_id
31
+
32
+ # The GPT model to use for the client
33
+ # Sample values: gpt-4, gpt-4-turbo-preview, gpt-3.5-turbo, gpt-3.5-turbo-instruct
34
+ # Refer to https://platform.openai.com/docs/models
35
+ attr_accessor :model
36
+
37
+ # The base URL for the OpenAI API
38
+ DEFAULT_API_URL = "https://api.openai.com/v1"
39
+ DEFAULT_CONNECTION_ADAPTER = :faraday
40
+
41
+ # Initializes new Rubygpt::Client::Configuration object
42
+ #
43
+ # @param [Hash] options
44
+ # @option options [String] :api_url required
45
+ # @option options [String] :api_key
46
+ # @option options [String] :organization_id
47
+ # @option options [String] :model required
48
+ # @option options [String] :connection_adapter
49
+ def initialize(options = {})
50
+ @connection_adapter = options[:connection_adapter] || DEFAULT_CONNECTION_ADAPTER
51
+ @api_key = options[:api_key]
52
+ @api_url = options[:api_url] || DEFAULT_API_URL
53
+ @organization_id = options[:organization_id]
54
+ @model = options[:model]
55
+ end
56
+
57
+ def validate!
58
+ raise(InvalidConfigurationError, "model is required") unless model
59
+ raise(InvalidConfigurationError, "api_key is required") unless api_key
60
+ end
61
+
62
+ def to_headers
63
+ {
64
+ "Authorization" => "Bearer #{api_key}",
65
+ "OpenAI-Organization" => organization_id
66
+ }.compact
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubygpt
4
+ # Main class that issues the connection to OpenAI APIs
5
+ class Client
6
+ # The configuration object for the client
7
+ attr_reader :configuration
8
+
9
+ # Initializes new Rubygpt::Client object
10
+ #
11
+ # @param [Hash] configuration
12
+ # @param [Configuration] configuration
13
+ def initialize(configuration = nil)
14
+ @configuration = Configuration.from(configuration)
15
+ yield @configuration if block_given?
16
+ @configuration.validate!
17
+ end
18
+
19
+ def post(*args)
20
+ connection.post(*args)
21
+ end
22
+
23
+ private
24
+
25
+ def connection
26
+ @connection ||= Connection.new(configuration)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Common
6
+ # Represents a Message object that is used in OpenAI Chat API requests
7
+ # This object is referenced by both ChatRequester and ChatCompletion objects
8
+ class Message
9
+ # The role of the author of this message.
10
+ # One of: user, assistant, system
11
+ # Default: system
12
+ attr_reader :role
13
+
14
+ # The contents of the message.
15
+ attr_reader :content
16
+
17
+ # Provides the model information to differentiate between participants of the same role.
18
+ attr_reader :name
19
+
20
+ # The tool calls generated by the model, such as function calls. (assistant msg only)
21
+ attr_reader :tool_calls
22
+
23
+ # Tool call that this message is responding to. (tool msg only)
24
+ attr_reader :tool_call_id
25
+
26
+ # Initializes the Message object
27
+ #
28
+ # @param [String, Hash] options The message content
29
+ def initialize(options = {})
30
+ if options.is_a?(String)
31
+ @role = "system"
32
+ @content = options
33
+ else
34
+ attributes_from_options(options)
35
+ end
36
+ json_content_parse
37
+ end
38
+
39
+ def json?
40
+ !!@json_content
41
+ end
42
+
43
+ def empty?
44
+ content.nil? || content.empty?
45
+ end
46
+
47
+ def to_h
48
+ { role:, content:, name:, tool_calls:, tool_call_id: }.compact
49
+ end
50
+
51
+ private
52
+
53
+ def attributes_from_options(options)
54
+ @role = options[:role] || "system"
55
+ @content = options[:content]
56
+ @name = options[:name]
57
+ @tool_calls = options[:tool_calls]
58
+ @tool_call_id = options[:tool_call_id]
59
+ end
60
+
61
+ # Parses the content if it is a JSON string
62
+ # This is used when the request is made with json: true flag
63
+ # https://platform.openai.com/docs/guides/text-generation/json-mode
64
+ def json_content_parse
65
+ return unless @content.is_a?(String) && @content.start_with?("{", "[")
66
+
67
+ @content = JSON.parse(@content, symbolize_names: true)
68
+ @json_content = true
69
+ rescue JSON::ParserError
70
+ nil
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+
5
+ module Rubygpt
6
+ module Connection
7
+ # The HTTP connection adapter derived from Faraday::Connection
8
+ class Faraday < Faraday::Connection
9
+ # Initializes a new connection object
10
+ #
11
+ # @param [Configuration] configuration
12
+ # @param [Hash] faraday_options
13
+ def initialize(configuration, faraday_options = {})
14
+ options = { url: configuration.api_url, headers: configuration.to_headers }.merge(faraday_options)
15
+ super(options) do |faraday|
16
+ faraday.request :json
17
+ faraday.response :json
18
+ faraday.response :raise_error
19
+ end
20
+ end
21
+
22
+ def post(*args)
23
+ faraday_response = super(*args)
24
+ Rubygpt::Response::StandardApiResponse.new(
25
+ adapter_response: faraday_response,
26
+ **faraday_response.slice(:status, :body, :headers)
27
+ )
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubygpt
4
+ # Moderates available connection adapters
5
+ module Connection
6
+ class << self
7
+ # Find and initialize the connection adapter
8
+ #
9
+ # @param [Configuration] configuration
10
+ # @param [Hash] options
11
+ def new(configuration, options = {})
12
+ const_get(configuration.connection_adapter.to_s.capitalize).new(configuration, options)
13
+ rescue NameError
14
+ raise Client::Configuration::InvalidConfigurationError, "Invalid adapter provided for connection."
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubygpt
4
+ module Requester
5
+ # Performs CRUD operations for OpenAI Chat Completion Objects
6
+ # https://platform.openai.com/docs/api-reference/chat
7
+ class ChatRequester < BaseRequester
8
+ # Initializes the ChatRequester
9
+ #
10
+ # @param [Client] client The client object
11
+ def initialize(client)
12
+ @api_endpoint = "chat/completions"
13
+ super(client)
14
+ end
15
+
16
+ # Performs a POST request to the API endpoint
17
+ # https://platform.openai.com/docs/api-reference/chat/create
18
+ #
19
+ # @param [String] args The single message to send. Message will be sent with system role
20
+ # @param [Array] args The array of messages to send. Each message can be a string or a hash
21
+ # @param [Hash] args The arguments for the request body, including messages
22
+ # @option args [Array] :messages The messages to send
23
+ #
24
+ # @return [Response::ChatCompletion]
25
+ def create(args = {})
26
+ # TODO: handle args[:stream] for streaming completions
27
+ Response::ChatCompletion.new client.post(api_endpoint, create_request_body(args))
28
+ end
29
+
30
+ private
31
+
32
+ # Builds the request body for the POST request
33
+ def create_request_body(args)
34
+ request_body = { model: client.configuration.model }
35
+ if args.is_a?(Hash)
36
+ # Allowing JSON mode for the request
37
+ # https://platform.openai.com/docs/guides/text-generation/json-mode
38
+ request_body[:response_format] = { type: "json_object" } if args[:json]
39
+ request_body[:messages] = messages_from_hash(args)
40
+ request_body.merge! args.except(:messages, :json)
41
+ else
42
+ request_body[:messages] = messages_from_args(args)
43
+ end
44
+ request_body.compact
45
+ end
46
+
47
+ # Handles the message data provided as arguments
48
+ def messages_from_args(args)
49
+ messages =
50
+ case args
51
+ when String then [Common::Message.new(args)]
52
+ when Array then args.map { |message| Common::Message.new(message) }
53
+ else raise ArgumentError, "Invalid message data provided"
54
+ end
55
+ map_messages(messages)
56
+ end
57
+
58
+ # Handles the message data provided as a hash
59
+ def messages_from_hash(hash)
60
+ messages =
61
+ if hash.key?(:messages)
62
+ # If it is a hash with a :messages key, then it's a request config with messages provided explicitly
63
+ # { messages: [{ content: 'foo', role: 'user' }, { content: 'bar', role: 'system' }] }
64
+ hash[:messages].map { |message| Common::Message.new(message) }
65
+ else
66
+ # If it is a hash without a :messages key, then it's a message config itself
67
+ # { content: 'foo', role: 'user' }
68
+ [Common::Message.new(hash)]
69
+ end
70
+ map_messages(messages)
71
+ end
72
+
73
+ # Maps the messages to a hash
74
+ # Validates the messages and raises an error if no messages are provided
75
+ def map_messages(messages)
76
+ messages.map do |message|
77
+ raise ArgumentError, "Empty message contents found." if message.empty?
78
+
79
+ message.to_h
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubygpt
4
+ module Requester
5
+ # Base module for all requester modules
6
+ class BaseRequester
7
+ # The client object that will be used to make the request
8
+ attr_reader :client
9
+
10
+ # The API endpoint for the request
11
+ attr_reader :api_endpoint
12
+
13
+ # Initializes new Rubygpt::Requester object
14
+ #
15
+ # @param [Client] client
16
+ def initialize(client)
17
+ @client = client
18
+ end
19
+
20
+ # Performs a POST request to the API endpoint
21
+ def create
22
+ raise NotImplementedError
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubygpt
4
+ module Response
5
+ # Represents the ChatCompletion response from the Chat API
6
+ # https://platform.openai.com/docs/api-reference/chat/object
7
+ class ChatCompletion < BaseResponse
8
+ # Represents a Choice object in the ChatCompletion response
9
+ class Choice
10
+ # The index of the choice in the list of choices.
11
+ attr_reader :index
12
+
13
+ # A chat completion message generated by the model.
14
+ # A Message object
15
+ attr_reader :message
16
+
17
+ # Log probability information for the choice.
18
+ attr_reader :logprobs
19
+
20
+ # The reason the model stopped generating tokens.
21
+ # One of: stop, length, content_filter, tool_calls
22
+ attr_reader :finish_reason
23
+
24
+ # Initializes the Choice object
25
+ def initialize(index:, message:, logprobs:, finish_reason:)
26
+ @index = index
27
+ @message = Common::Message.new(**message.transform_keys(&:to_sym))
28
+ @logprobs = logprobs
29
+ @finish_reason = finish_reason
30
+ end
31
+
32
+ # Delegations to the message object
33
+ def content
34
+ message.content
35
+ end
36
+
37
+ def role
38
+ message.role
39
+ end
40
+
41
+ # Returns true if the choice failed to generate a complete message
42
+ def failed?
43
+ finish_reason != "stop"
44
+ end
45
+
46
+ def to_h
47
+ { index:, message: message.to_h, logprobs:, finish_reason: }.compact
48
+ end
49
+ end
50
+
51
+ # Readers for the standard attributes of the ChatCompletion object
52
+ attr_reader :id, :object, :created, :model, :system_fingerprint, :usage, :choices
53
+
54
+ # Initializes the ChatCompletion object
55
+ #
56
+ # @param [StandardApiResponse] api_response The response from the API, standardized by the connection object
57
+ def initialize(api_response)
58
+ super
59
+ @choices = @choices.map { |choice| Choice.new(**choice.transform_keys(&:to_sym)) }.sort_by(&:index) if @choices
60
+ end
61
+
62
+ # The list of message strings generated by the model.
63
+ #
64
+ # return [Array<String>]
65
+ def messages
66
+ @messages ||= choices.map { |choice| choice.message.content }
67
+ end
68
+
69
+ # Messages combined into a single string, separated by newlines
70
+ def read(message_separator: "\n")
71
+ @read ||= messages.join(message_separator)
72
+ end
73
+
74
+ def failed?
75
+ choices.any?(&:failed?)
76
+ end
77
+
78
+ def cost
79
+ usage[:total_tokens]
80
+ end
81
+
82
+ def to_h
83
+ { id:, object:, created:, model:, system_fingerprint:, usage:, choices: choices.map(&:to_h) }.compact
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubygpt
4
+ module Response
5
+ class ConnectionReturnNotStandardizedError < StandardError; end
6
+
7
+ # Unifies responses from all connection/adapter objects.
8
+ # This is required since a response from Faraday will not be the same as a response from another adapter
9
+ # that might be introduced in the future. To solve this,
10
+ # we're overriding the post, get, put, delete methods in connection objects to return a StandardApiResponse
11
+ class StandardApiResponse
12
+ # The HTTP package contents of the response
13
+ attr_reader :status, :body, :headers
14
+
15
+ # The original, non-standardized response object received from the adapter
16
+ # This is useful for debugging and for accessing adapter-specific features
17
+ attr_reader :adapter_response
18
+
19
+ # @param [Integer] status The HTTP status code
20
+ # @param [Hash] body The response body
21
+ # @param [Hash] headers The response headers
22
+ # @param [Object] adapter_response The original, non-standardized response object received from the adapter
23
+ def initialize(status:, body:, headers:, adapter_response: nil)
24
+ @status = status
25
+ @body = body
26
+ @headers = headers
27
+ @adapter_response = adapter_response
28
+ end
29
+ end
30
+
31
+ # Base class for all API response objects
32
+ class BaseResponse
33
+ # The response from the API, standardized by the connection object
34
+ attr_reader :api_response
35
+
36
+ # @param [StandardApiResponse] api_response The response from the API, standardized by the connection object
37
+ def initialize(api_response)
38
+ raise ConnectionReturnNotStandardizedError unless api_response.is_a?(StandardApiResponse)
39
+
40
+ @api_response = api_response
41
+ build_attributes_from_body
42
+ end
43
+
44
+ private
45
+
46
+ # Builds instance variables dynamically from the response body
47
+ def build_attributes_from_body
48
+ @api_response.body&.each do |key, value|
49
+ variable_name = key.to_s.downcase
50
+ instance_variable_set("@#{variable_name}", value)
51
+ self.class.send(:attr_reader, variable_name)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubygpt
4
+ VERSION = "0.1.0"
5
+ end
data/lib/rubygpt.rb ADDED
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rubygpt/version"
4
+ require_relative "rubygpt/connection/faraday"
5
+ require_relative "rubygpt/connection"
6
+ require_relative "rubygpt/client/configuration"
7
+ require_relative "rubygpt/client"
8
+ require_relative "rubygpt/common/message"
9
+ require_relative "rubygpt/response"
10
+ require_relative "rubygpt/response/chat_completion"
11
+ require_relative "rubygpt/requester"
12
+ require_relative "rubygpt/requester/chat_requester"
13
+
14
+ # The main module for Rubygpt
15
+ # Contains static methods for configuration
16
+ module Rubygpt
17
+ class << self
18
+ # The default singleton client for the module
19
+ # Allows a global configuration to be set across the module
20
+ #
21
+ # @return [Client]
22
+ def configure(configuration = nil, &block)
23
+ reset_requesters
24
+ @default_client = Client.new(configuration, &block)
25
+ end
26
+
27
+ # A ChatRequester object, configured by default client
28
+ # Allows the Rubygpt.chat calls
29
+ #
30
+ # @return [Requester::ChatRequester]
31
+ def chat
32
+ @chat ||= Requester::ChatRequester.new(@default_client)
33
+ end
34
+
35
+ # Remove memoized requester objects to allow reallocation
36
+ def reset_requesters
37
+ @chat = nil
38
+ end
39
+ end
40
+ end
data/sig/rubygpt.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Rubygpt
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rubygpt
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Furkan Enes Apaydin
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-03-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.9'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.9'
27
+ - !ruby/object:Gem::Dependency
28
+ name: json
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 2.7.1
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 2.7.1
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 3.13.0
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 3.13.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: webmock
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 3.23.0
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 3.23.0
69
+ description: ''
70
+ email:
71
+ - feapaydin@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".rspec"
77
+ - ".rubocop.yml"
78
+ - ".ruby-version"
79
+ - CHANGELOG.md
80
+ - CODE_OF_CONDUCT.md
81
+ - MIT-LICENSE
82
+ - README.md
83
+ - Rakefile
84
+ - SECURITY.md
85
+ - lib/rubygpt.rb
86
+ - lib/rubygpt/client.rb
87
+ - lib/rubygpt/client/configuration.rb
88
+ - lib/rubygpt/common/message.rb
89
+ - lib/rubygpt/connection.rb
90
+ - lib/rubygpt/connection/faraday.rb
91
+ - lib/rubygpt/requester.rb
92
+ - lib/rubygpt/requester/chat_requester.rb
93
+ - lib/rubygpt/response.rb
94
+ - lib/rubygpt/response/chat_completion.rb
95
+ - lib/rubygpt/version.rb
96
+ - sig/rubygpt.rbs
97
+ homepage: https://github.com/feapaydin/rubygpt
98
+ licenses: []
99
+ metadata:
100
+ homepage_uri: https://github.com/feapaydin/rubygpt
101
+ source_code_uri: https://github.com/feapaydin/rubygpt
102
+ changelog_uri: https://github.com/feapaydin/rubygpt/blob/main/CHANGELOG.md
103
+ post_install_message:
104
+ rdoc_options: []
105
+ require_paths:
106
+ - lib
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: 3.2.0
112
+ required_rubygems_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ requirements: []
118
+ rubygems_version: 3.4.1
119
+ signing_key:
120
+ specification_version: 4
121
+ summary: Ruby wrapper for OpenAI's ChatGPT APIs.
122
+ test_files: []