openai.rb 0.0.0 → 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,203 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe OpenAI::API::Response do
4
+ before do
5
+ user_class = Class.new(described_class) do
6
+ field :username, path: %i[handle]
7
+ end
8
+
9
+ stub_const('OpenAISpec::SampleResponse::User', user_class)
10
+
11
+ comment_class = Class.new(described_class) do
12
+ field :body
13
+ field :user, wrapper: OpenAISpec::SampleResponse::User
14
+ end
15
+
16
+ stub_const('OpenAISpec::SampleResponse::Comment', comment_class)
17
+
18
+ post_class = Class.new(described_class) do
19
+ field :created_at, path: %i[meta birth created]
20
+ field :text
21
+ field :comments, wrapper: OpenAISpec::SampleResponse::Comment
22
+ field :author, wrapper: OpenAISpec::SampleResponse::User
23
+ optional_field :co_author, wrapper: OpenAISpec::SampleResponse::User
24
+ optional_field :subtitle
25
+
26
+ # For demonstrating that we can use the instance method without specifying
27
+ # a wrapper class
28
+ define_method(:other_author) do
29
+ optional_field([:co_author])
30
+ end
31
+ end
32
+
33
+ stub_const('OpenAISpec::SampleResponse::Post', post_class)
34
+ end
35
+
36
+ let(:sample_response) do
37
+ OpenAISpec::SampleResponse::Post.new(sample_response_payload)
38
+ end
39
+
40
+ let(:sample_response_payload) do
41
+ {
42
+ meta: {
43
+ birth: {
44
+ created: Time.new(2023).to_i
45
+ }
46
+ },
47
+ text: 'This is a post',
48
+ comments: [
49
+ {
50
+ body: 'This is a comment',
51
+ user: {
52
+ handle: 'alice'
53
+ }
54
+ },
55
+ {
56
+ body: 'This is a spicy comment',
57
+ user: {
58
+ handle: 'bob'
59
+ }
60
+ }
61
+ ],
62
+ author: {
63
+ handle: 'carl'
64
+ }
65
+ }
66
+ end
67
+
68
+ context 'when inspecting the response' do
69
+ # Define a smaller response payload so this is less annoying to test
70
+ let(:sample_response_payload) do
71
+ {
72
+ meta: { birth: { created: 1234 } },
73
+ text: 'This is a post',
74
+ comments: [],
75
+ author: { handle: 'carl' }
76
+ }
77
+ end
78
+
79
+ before do
80
+ # Mark a field as private so we can prove that the #inspect method
81
+ # should use __send__ in case a response class chooses to make a
82
+ # field private.
83
+ OpenAISpec::SampleResponse::Post.class_eval do
84
+ private(:created_at)
85
+ end
86
+ end
87
+
88
+ it 'defines a nice clean inspect method' do
89
+ expect(sample_response.inspect).to eql(
90
+ '#<OpenAISpec::SampleResponse::Post ' \
91
+ 'created_at=1234 ' \
92
+ 'text="This is a post" ' \
93
+ 'comments=[] ' \
94
+ 'author=#<OpenAISpec::SampleResponse::User username="carl"> ' \
95
+ 'co_author=nil ' \
96
+ 'subtitle=nil>'
97
+ )
98
+ end
99
+
100
+ it 'tracks the fields on a class for the sake of the #inspect method' do
101
+ expect(OpenAISpec::SampleResponse::Comment.__send__(:field_registry))
102
+ .to eql(%i[body user])
103
+ end
104
+ end
105
+
106
+ it 'can parse a JSON response' do
107
+ expect(
108
+ OpenAISpec::SampleResponse::Post.from_json(
109
+ JSON.dump(sample_response_payload)
110
+ )
111
+ ).to eql(sample_response)
112
+ end
113
+
114
+ it 'exposes the original payload' do
115
+ expect(sample_response.original_payload).to eql(sample_response_payload)
116
+ end
117
+
118
+ it 'deep freezes the original payload' do
119
+ original = sample_response.original_payload
120
+ expect(original).to be_frozen
121
+ expect(original[:comments]).to be_frozen
122
+ expect(original[:comments].first).to be_frozen
123
+ expect(original[:comments].first[:user]).to be_frozen
124
+ end
125
+
126
+ describe '.field' do
127
+ it 'exposes the field' do
128
+ expect(sample_response.text).to eql('This is a post')
129
+ expect(sample_response.created_at).to eql(1_672_549_200)
130
+ end
131
+
132
+ it 'can expose fields under a different name than the key path' do
133
+ expect(sample_response.author.username).to eql('carl')
134
+ end
135
+
136
+ it 'wraps the field if a wrapper is provided' do
137
+ expect(sample_response.author).to eql(
138
+ OpenAISpec::SampleResponse::User.new(handle: 'carl')
139
+ )
140
+ end
141
+
142
+ it 'wraps each element in a the wrapper if the value is an array' do
143
+ expect(sample_response.comments).to all(
144
+ be_an_instance_of(OpenAISpec::SampleResponse::Comment)
145
+ )
146
+ expect(sample_response.comments[0].user).to eql(
147
+ OpenAISpec::SampleResponse::User.new(handle: 'alice')
148
+ )
149
+ expect(sample_response.comments[1].user).to eql(
150
+ OpenAISpec::SampleResponse::User.new(handle: 'bob')
151
+ )
152
+ end
153
+
154
+ context 'when a required field is not present' do
155
+ let(:sample_response_payload) do
156
+ { meta: { error: 'you did something wrong bro' } }
157
+ end
158
+
159
+ it 'raises an error when the field is accessed' do
160
+ expect { sample_response.text }.to raise_error(
161
+ described_class::MissingFieldError, <<~ERROR
162
+ Missing field :text in response payload!
163
+ Was attempting to access value at path `[:text]`.
164
+ Payload: {
165
+ "meta": {
166
+ "error": "you did something wrong bro"
167
+ }
168
+ }
169
+ ERROR
170
+ )
171
+
172
+ expect { sample_response.created_at }.to raise_error(
173
+ described_class::MissingFieldError, <<~ERROR
174
+ Missing field :birth in response payload!
175
+ Was attempting to access value at path `[:meta, :birth, :created]`.
176
+ Payload: {
177
+ "meta": {
178
+ "error": "you did something wrong bro"
179
+ }
180
+ }
181
+ ERROR
182
+ )
183
+ end
184
+ end
185
+ end
186
+
187
+ describe '.optional_field' do
188
+ it 'does not raise an error when a field is not present' do
189
+ expect(sample_response.co_author).to be_nil
190
+ expect(sample_response.other_author).to be_nil
191
+ end
192
+
193
+ context 'when the optional field is present' do
194
+ let(:sample_response_payload) do
195
+ super().merge(co_author: { handle: 'dave' })
196
+ end
197
+
198
+ it 'exposes the field' do
199
+ expect(sample_response.co_author.username).to eql('dave')
200
+ end
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe OpenAI::Tokenizer do
4
+ let(:tokenizer) { described_class.new }
5
+
6
+ it 'can get an encoder by model name' do
7
+ expect(tokenizer.for_model('gpt-4')).to eql(
8
+ OpenAI::Tokenizer::Encoding.new(:cl100k_base)
9
+ )
10
+ end
11
+
12
+ it 'can get an encoder by name' do
13
+ expect(tokenizer.get(:cl100k_base)).to eql(
14
+ OpenAI::Tokenizer::Encoding.new(:cl100k_base)
15
+ )
16
+ end
17
+
18
+ it 'raises an error if the model name is not valid' do
19
+ expect { tokenizer.for_model('gpt-42') }.to raise_error(
20
+ 'Invalid model name or not recognized by Tiktoken: "gpt-42"'
21
+ )
22
+ end
23
+
24
+ it 'raises an error if the encoding name is not valid' do
25
+ expect { tokenizer.get('aaaaaaaaaaaa') }.to raise_error(
26
+ 'Invalid encoding name or not recognized by Tiktoken: "aaaaaaaaaaaa"'
27
+ )
28
+ end
29
+
30
+ it 'can encode text' do
31
+ expect(tokenizer.for_model('gpt-4').encode('Hello, world!')).to eql(
32
+ [9906, 11, 1917, 0]
33
+ )
34
+ end
35
+
36
+ it 'can decode tokens' do
37
+ expect(tokenizer.for_model('gpt-4').decode([9906, 11, 1917, 0])).to eql(
38
+ 'Hello, world!'
39
+ )
40
+ end
41
+
42
+ it 'can count the number of tokens in text' do
43
+ expect(tokenizer.for_model('gpt-4').num_tokens('Hello, world!')).to eql(4)
44
+ end
45
+ end