gemini-ai 2.0.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 790a1c353ecc9e3122fc096f674d50fbd0ef36b4b0f9ab56ef66d180b0a5d4de
4
- data.tar.gz: 2eac84cb99804362eea89063ea6ed66467af1ffb3a7d4ffd1ab186f7a2842157
3
+ metadata.gz: c7c5bd6e6cd1d2195b7a437fb0664cd553dc0acbf0d293042d93c3d701e6b6e0
4
+ data.tar.gz: 665458cc152b00efae9f8e2b730fe000ef2caac63f835944c1e76a83b9a0e627
5
5
  SHA512:
6
- metadata.gz: d82d466e8272ab1919c153d3f68ffb3ab4ebd5a750a3822daef4394a814b36c08c0f114e691cb52a5c830a74a0e9d208cd3d4407227592812f0534655dc5afc1
7
- data.tar.gz: 7141a1bff79ea92aac09105b84fd9c2d5b68efe5184bf1ca0588dc420c0180ea65cd2fcca000ab0ea2cd00c1be188877f3f97ec6c7410f61bea4c8acf52b475c
6
+ metadata.gz: 0ca1f3f87c61276902259d937f4d57f324a756d1e63c1e5781680ba970313f3c3c29a2da49c4eb1ec0bf351f984bee378e86b22d1656b2423638e1a70bd5dddf
7
+ data.tar.gz: 50988d0881d37f561e0c75ac1beea4fc9b34c757d0f16fb2dcb2f63d3e63f867194e0f826800c8429ce9f33500dd74c3fa4c5d747a0975378c81cf3e0ad4a61b
data/.gitignore CHANGED
@@ -1 +1,2 @@
1
1
  *.gem
2
+ .devcontainer
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- gemini-ai (2.0.0)
4
+ gemini-ai (2.2.0)
5
5
  event_stream_parser (~> 1.0)
6
6
  faraday (~> 2.7, >= 2.7.12)
7
7
  googleauth (~> 1.9, >= 1.9.1)
@@ -36,7 +36,7 @@ GEM
36
36
  method_source (1.0.0)
37
37
  multi_json (1.15.0)
38
38
  os (1.1.4)
39
- parallel (1.23.0)
39
+ parallel (1.24.0)
40
40
  parser (3.2.2.4)
41
41
  ast (~> 2.4.1)
42
42
  racc
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Gemini AI
2
2
 
3
- A Ruby Gem for interacting with [Gemini](https://deepmind.google/technologies/gemini/) through [Vertex AI](https://cloud.google.com/vertex-ai) or [AI Studio](https://makersuite.google.com), Google's generative AI services.
3
+ A Ruby Gem for interacting with [Gemini](https://deepmind.google/technologies/gemini/) through [Vertex AI](https://cloud.google.com/vertex-ai), [Generative Language API](https://ai.google.dev/api/rest), or [AI Studio](https://makersuite.google.com), Google's generative AI services.
4
4
 
5
5
  ![The logo shows a gemstone split into red and blue halves, symbolizing Ruby programming and Gemini AI. It's surrounded by a circuit-like design on a dark blue backdrop.](https://raw.githubusercontent.com/gbaptista/assets/main/gemini-ai/ruby-gemini-ai.png)
6
6
 
@@ -9,7 +9,7 @@ A Ruby Gem for interacting with [Gemini](https://deepmind.google/technologies/ge
9
9
  ## TL;DR and Quick Start
10
10
 
11
11
  ```ruby
12
- gem 'gemini-ai', '~> 2.0'
12
+ gem 'gemini-ai', '~> 2.2.0'
13
13
  ```
14
14
 
15
15
  ```ruby
@@ -24,12 +24,20 @@ client = Gemini.new(
24
24
  options: { model: 'gemini-pro', stream: false }
25
25
  )
26
26
 
27
- # With a Service Account
27
+ # With a Service Account Credentials File
28
28
  client = Gemini.new(
29
29
  credentials: {
30
30
  service: 'vertex-ai-api',
31
31
  file_path: 'google-credentials.json',
32
- project_id: 'PROJECT_ID',
32
+ region: 'us-east4'
33
+ },
34
+ options: { model: 'gemini-pro', stream: false }
35
+ )
36
+
37
+ # With Application Default Credentials
38
+ client = Gemini.new(
39
+ credentials: {
40
+ service: 'vertex-ai-api',
33
41
  region: 'us-east4'
34
42
  },
35
43
  options: { model: 'gemini-pro', stream: false }
@@ -67,18 +75,27 @@ Result:
67
75
  - [Setup](#setup)
68
76
  - [Installing](#installing)
69
77
  - [Credentials](#credentials)
70
- - [Option 1: API Key](#option-1-api-key)
71
- - [Option 2: Service Account](#option-2-service-account)
78
+ - [Option 1: API Key (Generative Language API)](#option-1-api-key-generative-language-api)
79
+ - [Option 2: Service Account Credentials File (Vertex AI API)](#option-2-service-account-credentials-file-vertex-ai-api)
80
+ - [Option 3: Application Default Credentials (Vertex AI API)](#option-3-application-default-credentials-vertex-ai-api)
72
81
  - [Required Data](#required-data)
73
82
  - [Usage](#usage)
74
83
  - [Client](#client)
75
84
  - [Generate Content](#generate-content)
85
+ - [Modes](#modes)
86
+ - [Text](#text)
87
+ - [Image](#image)
88
+ - [Video](#video)
76
89
  - [Synchronous](#synchronous)
77
90
  - [Streaming](#streaming)
78
91
  - [Streaming Hang](#streaming-hang)
79
- - [Back-and-Forth Conversations](#back-and-forth-conversations)
80
- - [Tools (Functions) Calling](#tools-functions-calling)
92
+ - [Back-and-Forth Conversations](#back-and-forth-conversations)
93
+ - [Tools (Functions) Calling](#tools-functions-calling)
81
94
  - [New Functionalities and APIs](#new-functionalities-and-apis)
95
+ - [Error Handling](#error-handling)
96
+ - [Rescuing](#rescuing)
97
+ - [For Short](#for-short)
98
+ - [Errors](#errors)
82
99
  - [Development](#development)
83
100
  - [Purpose](#purpose)
84
101
  - [Publish to RubyGems](#publish-to-rubygems)
@@ -91,18 +108,23 @@ Result:
91
108
  ### Installing
92
109
 
93
110
  ```sh
94
- gem install gemini-ai -v 2.0.0
111
+ gem install gemini-ai -v 2.2.0
95
112
  ```
96
113
 
97
114
  ```sh
98
- gem 'gemini-ai', '~> 2.0'
115
+ gem 'gemini-ai', '~> 2.2.0'
99
116
  ```
100
117
 
101
118
  ### Credentials
102
119
 
120
+ - [Option 1: API Key (Generative Language API)](#option-1-api-key-generative-language-api)
121
+ - [Option 2: Service Account Credentials File (Vertex AI API)](#option-2-service-account-credentials-file-vertex-ai-api)
122
+ - [Option 3: Application Default Credentials (Vertex AI API)](#option-3-application-default-credentials-vertex-ai-api)
123
+ - [Required Data](#required-data)
124
+
103
125
  > ⚠️ DISCLAIMER: Be careful with what you are doing, and never trust others' code related to this. These commands and instructions alter the level of access to your Google Cloud Account, and running them naively can lead to security risks as well as financial risks. People with access to your account can use it to steal data or incur charges. Run these commands at your own responsibility and due diligence; expect no warranties from the contributors of this project.
104
126
 
105
- #### Option 1: API Key
127
+ #### Option 1: API Key (Generative Language API)
106
128
 
107
129
  You need a [Google Cloud](https://console.cloud.google.com) [_Project_](https://cloud.google.com/resource-manager/docs/creating-managing-projects), and then you can generate an API Key through the Google Cloud Console [here](https://console.cloud.google.com/apis/credentials).
108
130
 
@@ -111,7 +133,7 @@ You also need to enable the _Generative Language API_ service in your Google Clo
111
133
 
112
134
  Alternatively, you can generate an API Key through _Google AI Studio_ [here](https://makersuite.google.com/app/apikey). However, this approach will automatically create a project for you in your Google Cloud Account.
113
135
 
114
- #### Option 2: Service Account
136
+ #### Option 2: Service Account Credentials File (Vertex AI API)
115
137
 
116
138
  You need a [Google Cloud](https://console.cloud.google.com) [_Project_](https://cloud.google.com/resource-manager/docs/creating-managing-projects) and a [_Service Account_](https://cloud.google.com/iam/docs/service-account-overview) to use [Vertex AI](https://cloud.google.com/vertex-ai) API.
117
139
 
@@ -165,11 +187,24 @@ gcloud projects add-iam-policy-binding PROJECT_ID \
165
187
  --role='roles/ml.admin'
166
188
  ```
167
189
 
190
+ #### Option 3: Application Default Credentials (Vertex AI API)
191
+
192
+ Similar to [Option 2](#option-2-service-account-credentials-file-vertex-ai-api), but you don't need to download a `google-credentials.json`. [_Application Default Credentials_](https://cloud.google.com/docs/authentication/application-default-credentials) automatically find credentials based on the application environment.
193
+
194
+ For local development, you can generate your default credentials using the [gcloud CLI](https://cloud.google.com/sdk/gcloud) as follows:
195
+
196
+ ```sh
197
+ gcloud auth application-default login
198
+ ```
199
+
200
+ For more details about alternative methods and different environments, check the official documentation:
201
+ [Set up Application Default Credentials](https://cloud.google.com/docs/authentication/provide-credentials-adc)
202
+
168
203
  #### Required Data
169
204
 
170
205
  After choosing an option, you should have all the necessary data and access to use Gemini.
171
206
 
172
- For API Key:
207
+ **Option 1**, for API Key:
173
208
 
174
209
  ```ruby
175
210
  {
@@ -187,13 +222,21 @@ Remember that hardcoding your API key in code is unsafe; it's preferable to use
187
222
  }
188
223
  ```
189
224
 
190
- Alternativelly, for Service Account, a `google-credentials.json` file, a `PROJECT_ID`, and a `REGION`:
225
+ **Option 2**: For the Service Account, provide a `google-credentials.json` file and a `REGION`:
191
226
 
192
227
  ```ruby
193
228
  {
194
229
  service: 'vertex-ai-api',
195
230
  file_path: 'google-credentials.json',
196
- project_id: 'PROJECT_ID',
231
+ region: 'us-east4'
232
+ }
233
+ ```
234
+
235
+ **Option 3**: For _Application Default Credentials_, omit both the `api_key` and the `file_path`:
236
+
237
+ ```ruby
238
+ {
239
+ service: 'vertex-ai-api',
197
240
  region: 'us-east4'
198
241
  }
199
242
  ```
@@ -212,6 +255,15 @@ Tokyo, Japan (asia-northeast1)
212
255
 
213
256
  You can follow here if new regions are available: [Gemini API](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/gemini)
214
257
 
258
+ You might want to explicitly set a Google Cloud Project ID, which you can do as follows:
259
+
260
+ ```ruby
261
+ {
262
+ service: 'vertex-ai-api',
263
+ project_id: 'PROJECT_ID'
264
+ }
265
+ ```
266
+
215
267
  ## Usage
216
268
 
217
269
  ### Client
@@ -230,12 +282,20 @@ client = Gemini.new(
230
282
  options: { model: 'gemini-pro', stream: false }
231
283
  )
232
284
 
233
- # With a Service Account
285
+ # With a Service Account Credentials File
234
286
  client = Gemini.new(
235
287
  credentials: {
236
288
  service: 'vertex-ai-api',
237
289
  file_path: 'google-credentials.json',
238
- project_id: 'PROJECT_ID',
290
+ region: 'us-east4'
291
+ },
292
+ options: { model: 'gemini-pro', stream: false }
293
+ )
294
+
295
+ # With Application Default Credentials
296
+ client = Gemini.new(
297
+ credentials: {
298
+ service: 'vertex-ai-api',
239
299
  region: 'us-east4'
240
300
  },
241
301
  options: { model: 'gemini-pro', stream: false }
@@ -244,6 +304,153 @@ client = Gemini.new(
244
304
 
245
305
  ### Generate Content
246
306
 
307
+ #### Modes
308
+
309
+ ##### Text
310
+
311
+ ```ruby
312
+ result = client.stream_generate_content({
313
+ contents: { role: 'user', parts: { text: 'hi!' } }
314
+ })
315
+ ```
316
+
317
+ Result:
318
+ ```ruby
319
+ [{ 'candidates' =>
320
+ [{ 'content' => {
321
+ 'role' => 'model',
322
+ 'parts' => [{ 'text' => 'Hello! How may I assist you?' }]
323
+ },
324
+ 'finishReason' => 'STOP',
325
+ 'safetyRatings' =>
326
+ [{ 'category' => 'HARM_CATEGORY_HARASSMENT', 'probability' => 'NEGLIGIBLE' },
327
+ { 'category' => 'HARM_CATEGORY_HATE_SPEECH', 'probability' => 'NEGLIGIBLE' },
328
+ { 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'probability' => 'NEGLIGIBLE' },
329
+ { 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT', 'probability' => 'NEGLIGIBLE' }] }],
330
+ 'usageMetadata' => {
331
+ 'promptTokenCount' => 2,
332
+ 'candidatesTokenCount' => 8,
333
+ 'totalTokenCount' => 10
334
+ } }]
335
+ ```
336
+
337
+ ##### Image
338
+
339
+ ![A black and white image of an old piano. The piano is an upright model, with the keys on the right side of the image. The piano is sitting on a tiled floor. There is a small round object on the top of the piano.](https://raw.githubusercontent.com/gbaptista/assets/main/gemini-ai/piano.jpg)
340
+
341
+ > _Courtesy of [Unsplash](https://unsplash.com/photos/greyscale-photo-of-grand-piano-czPs0z3-Ggg)_
342
+
343
+ Switch to the `gemini-pro-vision` model:
344
+
345
+ ```ruby
346
+ client = Gemini.new(
347
+ credentials: { service: 'vertex-ai-api', region: 'us-east4' },
348
+ options: { model: 'gemini-pro-vision', stream: true }
349
+ )
350
+ ```
351
+
352
+ Then, encode the image as [Base64](https://en.wikipedia.org/wiki/Base64) and add its [MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types):
353
+
354
+ ```ruby
355
+ require 'base64'
356
+
357
+ result = client.stream_generate_content(
358
+ { contents: [
359
+ { role: 'user', parts: [
360
+ { text: 'Please describe this image.' },
361
+ { inline_data: {
362
+ mime_type: 'image/jpeg',
363
+ data: Base64.strict_encode64(File.read('piano.jpg'))
364
+ } }
365
+ ] }
366
+ ] }
367
+ )
368
+ ```
369
+
370
+ The result:
371
+ ```ruby
372
+ [{ 'candidates' =>
373
+ [{ 'content' =>
374
+ { 'role' => 'model',
375
+ 'parts' =>
376
+ [{ 'text' =>
377
+ ' A black and white image of an old piano. The piano is an upright model, with the keys on the right side of the image. The piano is' }] },
378
+ 'safetyRatings' =>
379
+ [{ 'category' => 'HARM_CATEGORY_HARASSMENT', 'probability' => 'NEGLIGIBLE' },
380
+ { 'category' => 'HARM_CATEGORY_HATE_SPEECH', 'probability' => 'NEGLIGIBLE' },
381
+ { 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'probability' => 'NEGLIGIBLE' },
382
+ { 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT', 'probability' => 'NEGLIGIBLE' }] }] },
383
+ { 'candidates' =>
384
+ [{ 'content' => { 'role' => 'model', 'parts' => [{ 'text' => ' sitting on a tiled floor. There is a small round object on the top of the piano.' }] },
385
+ 'finishReason' => 'STOP',
386
+ 'safetyRatings' =>
387
+ [{ 'category' => 'HARM_CATEGORY_HARASSMENT', 'probability' => 'NEGLIGIBLE' },
388
+ { 'category' => 'HARM_CATEGORY_HATE_SPEECH', 'probability' => 'NEGLIGIBLE' },
389
+ { 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'probability' => 'NEGLIGIBLE' },
390
+ { 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT', 'probability' => 'NEGLIGIBLE' }] }],
391
+ 'usageMetadata' => { 'promptTokenCount' => 263, 'candidatesTokenCount' => 50, 'totalTokenCount' => 313 } }]
392
+ ```
393
+
394
+ ##### Video
395
+
396
+ https://gist.github.com/assets/29520/f82bccbf-02d2-4899-9c48-eb8a0a5ef741
397
+
398
+ > ALT: A white and gold cup is being filled with coffee. The coffee is dark and rich. The cup is sitting on a black surface. The background is blurred.
399
+
400
+ > _Courtesy of [Pexels](https://www.pexels.com/video/pouring-of-coffee-855391/)_
401
+
402
+ Switch to the `gemini-pro-vision` model:
403
+
404
+ ```ruby
405
+ client = Gemini.new(
406
+ credentials: { service: 'vertex-ai-api', region: 'us-east4' },
407
+ options: { model: 'gemini-pro-vision', stream: true }
408
+ )
409
+ ```
410
+
411
+ Then, encode the video as [Base64](https://en.wikipedia.org/wiki/Base64) and add its [MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types):
412
+
413
+ ```ruby
414
+ require 'base64'
415
+
416
+ result = client.stream_generate_content(
417
+ { contents: [
418
+ { role: 'user', parts: [
419
+ { text: 'Please describe this video.' },
420
+ { inline_data: {
421
+ mime_type: 'video/mp4',
422
+ data: Base64.strict_encode64(File.read('coffee.mp4'))
423
+ } }
424
+ ] }
425
+ ] }
426
+ )
427
+ ```
428
+
429
+ The result:
430
+ ```ruby
431
+ [{"candidates"=>
432
+ [{"content"=>
433
+ {"role"=>"model",
434
+ "parts"=>
435
+ [{"text"=>
436
+ " A white and gold cup is being filled with coffee. The coffee is dark and rich. The cup is sitting on a black surface. The background is blurred"}]},
437
+ "safetyRatings"=>
438
+ [{"category"=>"HARM_CATEGORY_HARASSMENT", "probability"=>"NEGLIGIBLE"},
439
+ {"category"=>"HARM_CATEGORY_HATE_SPEECH", "probability"=>"NEGLIGIBLE"},
440
+ {"category"=>"HARM_CATEGORY_SEXUALLY_EXPLICIT", "probability"=>"NEGLIGIBLE"},
441
+ {"category"=>"HARM_CATEGORY_DANGEROUS_CONTENT", "probability"=>"NEGLIGIBLE"}]}],
442
+ "usageMetadata"=>{"promptTokenCount"=>1037, "candidatesTokenCount"=>31, "totalTokenCount"=>1068}},
443
+ {"candidates"=>
444
+ [{"content"=>{"role"=>"model", "parts"=>[{"text"=>"."}]},
445
+ "finishReason"=>"STOP",
446
+ "safetyRatings"=>
447
+ [{"category"=>"HARM_CATEGORY_HARASSMENT", "probability"=>"NEGLIGIBLE"},
448
+ {"category"=>"HARM_CATEGORY_HATE_SPEECH", "probability"=>"NEGLIGIBLE"},
449
+ {"category"=>"HARM_CATEGORY_SEXUALLY_EXPLICIT", "probability"=>"NEGLIGIBLE"},
450
+ {"category"=>"HARM_CATEGORY_DANGEROUS_CONTENT", "probability"=>"NEGLIGIBLE"}]}],
451
+ "usageMetadata"=>{"promptTokenCount"=>1037, "candidatesTokenCount"=>32, "totalTokenCount"=>1069}}]
452
+ ```
453
+
247
454
  #### Synchronous
248
455
 
249
456
  ```ruby
@@ -351,7 +558,7 @@ Result:
351
558
  } }]
352
559
  ```
353
560
 
354
- ### Back-and-Forth Conversations
561
+ #### Back-and-Forth Conversations
355
562
 
356
563
  To maintain a back-and-forth conversation, you need to append the received responses and build a history for your requests:
357
564
 
@@ -393,9 +600,9 @@ Result:
393
600
  } }]
394
601
  ```
395
602
 
396
- ### Tools (Functions) Calling
603
+ #### Tools (Functions) Calling
397
604
 
398
- > As of the writing of this README, only the `gemini-pro` model [supports](https://cloud.google.com/vertex-ai/docs/generative-ai/multimodal/function-calling#supported_models) tools (functions) calls.
605
+ > As of the writing of this README, only the `vertex-ai-api` service and the `gemini-pro` model [supports](https://cloud.google.com/vertex-ai/docs/generative-ai/multimodal/function-calling#supported_models) tools (functions) calls.
399
606
 
400
607
  You can provide specifications for [tools (functions)](https://cloud.google.com/vertex-ai/docs/generative-ai/multimodal/function-calling) using [JSON Schema](https://json-schema.org) to generate potential calls to them:
401
608
 
@@ -549,6 +756,58 @@ result = client.request(
549
756
  )
550
757
  ```
551
758
 
759
+ ### Error Handling
760
+
761
+ #### Rescuing
762
+
763
+ ```ruby
764
+ require 'gemini-ai'
765
+
766
+ begin
767
+ client.stream_generate_content({
768
+ contents: { role: 'user', parts: { text: 'hi!' } }
769
+ })
770
+ rescue Gemini::Errors::GeminiError => error
771
+ puts error.class # Gemini::Errors::RequestError
772
+ puts error.message # 'the server responded with status 500'
773
+
774
+ puts error.payload
775
+ # { contents: [{ role: 'user', parts: { text: 'hi!' } }],
776
+ # generationConfig: { candidateCount: 1 },
777
+ # ...
778
+ # }
779
+
780
+ puts error.request
781
+ # #<Faraday::ServerError response={:status=>500, :headers...
782
+ end
783
+ ```
784
+
785
+ #### For Short
786
+
787
+ ```ruby
788
+ require 'gemini-ai/errors'
789
+
790
+ begin
791
+ client.stream_generate_content({
792
+ contents: { role: 'user', parts: { text: 'hi!' } }
793
+ })
794
+ rescue GeminiError => error
795
+ puts error.class # Gemini::Errors::RequestError
796
+ end
797
+ ```
798
+
799
+ #### Errors
800
+
801
+ ```ruby
802
+ GeminiError
803
+
804
+ MissingProjectIdError
805
+ UnsupportedServiceError
806
+ BlockWithoutStreamError
807
+
808
+ RequestError
809
+ ```
810
+
552
811
  ## Development
553
812
 
554
813
  ```bash
@@ -567,7 +826,7 @@ gem build gemini-ai.gemspec
567
826
 
568
827
  gem signin
569
828
 
570
- gem push gemini-ai-2.0.0.gem
829
+ gem push gemini-ai-2.2.0.gem
571
830
  ```
572
831
 
573
832
  ### Updating the README
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gemini
4
+ module Errors
5
+ class GeminiError < StandardError
6
+ def initialize(message = nil)
7
+ super(message)
8
+ end
9
+ end
10
+
11
+ class MissingProjectIdError < GeminiError; end
12
+ class UnsupportedServiceError < GeminiError; end
13
+ class BlockWithoutStreamError < GeminiError; end
14
+
15
+ class RequestError < GeminiError
16
+ attr_reader :request, :payload
17
+
18
+ def initialize(message = nil, request: nil, payload: nil)
19
+ @request = request
20
+ @payload = payload
21
+
22
+ super(message)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -5,6 +5,8 @@ require 'faraday'
5
5
  require 'json'
6
6
  require 'googleauth'
7
7
 
8
+ require_relative '../components/errors'
9
+
8
10
  module Gemini
9
11
  module Controllers
10
12
  class Client
@@ -12,21 +14,34 @@ module Gemini
12
14
  if config[:credentials][:api_key]
13
15
  @authentication = :api_key
14
16
  @api_key = config[:credentials][:api_key]
15
- else
17
+ elsif config[:credentials][:file_path]
16
18
  @authentication = :service_account
17
19
  @authorizer = ::Google::Auth::ServiceAccountCredentials.make_creds(
18
20
  json_key_io: File.open(config[:credentials][:file_path]),
19
21
  scope: 'https://www.googleapis.com/auth/cloud-platform'
20
22
  )
23
+ else
24
+ @authentication = :default_credentials
25
+ @authorizer = ::Google::Auth.get_application_default
26
+ end
27
+
28
+ if @authentication == :service_account || @authentication == :default_credentials
29
+ @project_id = if config[:credentials][:project_id].nil?
30
+ @authorizer.project_id || @authorizer.quota_project_id
31
+ else
32
+ config[:credentials][:project_id]
33
+ end
34
+
35
+ raise MissingProjectIdError, 'Could not determine project_id, which is required.' if @project_id.nil?
21
36
  end
22
37
 
23
38
  @address = case config[:credentials][:service]
24
39
  when 'vertex-ai-api'
25
- "https://#{config[:credentials][:region]}-aiplatform.googleapis.com/v1/projects/#{config[:credentials][:project_id]}/locations/#{config[:credentials][:region]}/publishers/google/models/#{config[:options][:model]}"
40
+ "https://#{config[:credentials][:region]}-aiplatform.googleapis.com/v1/projects/#{@project_id}/locations/#{config[:credentials][:region]}/publishers/google/models/#{config[:options][:model]}"
26
41
  when 'generative-language-api'
27
42
  "https://generativelanguage.googleapis.com/v1/models/#{config[:options][:model]}"
28
43
  else
29
- raise StandardError, "Unsupported service: #{config[:credentials][:service]}"
44
+ raise UnsupportedServiceError, "Unsupported service: #{config[:credentials][:service]}"
30
45
  end
31
46
 
32
47
  @stream = config[:options][:stream]
@@ -47,15 +62,17 @@ module Gemini
47
62
  url += "?#{params.join('&')}" if params.size.positive?
48
63
 
49
64
  if !callback.nil? && !stream_enabled
50
- raise StandardError, 'You are trying to use a block without stream enabled."'
65
+ raise BlockWithoutStreamError, 'You are trying to use a block without stream enabled.'
51
66
  end
52
67
 
53
68
  results = []
54
69
 
55
- response = Faraday.new.post do |request|
70
+ response = Faraday.new do |faraday|
71
+ faraday.response :raise_error
72
+ end.post do |request|
56
73
  request.url url
57
74
  request.headers['Content-Type'] = 'application/json'
58
- if @authentication == :service_account
75
+ if @authentication == :service_account || @authentication == :default_credentials
59
76
  request.headers['Authorization'] = "Bearer #{@authorizer.fetch_access_token!['access_token']}"
60
77
  end
61
78
 
@@ -93,6 +110,8 @@ module Gemini
93
110
  return safe_parse_json(response.body) unless stream_enabled
94
111
 
95
112
  results.map { |result| result[:event] }
113
+ rescue Faraday::ServerError => e
114
+ raise RequestError.new(e.message, request: e, payload:)
96
115
  end
97
116
 
98
117
  def safe_parse_json(raw)
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../components/errors'
4
+
5
+ include Gemini::Errors
data/static/gem.rb CHANGED
@@ -3,10 +3,10 @@
3
3
  module Gemini
4
4
  GEM = {
5
5
  name: 'gemini-ai',
6
- version: '2.0.0',
6
+ version: '2.2.0',
7
7
  author: 'gbaptista',
8
8
  summary: "Interact with Google's Gemini AI.",
9
- description: "A Ruby Gem for interacting with Gemini through Vertex AI or AI Studio, Google's generative AI services.",
9
+ description: "A Ruby Gem for interacting with Gemini through Vertex AI, Generative Language API, or AI Studio, Google's generative AI services.",
10
10
  github: 'https://github.com/gbaptista/gemini-ai',
11
11
  gem_server: 'https://rubygems.org',
12
12
  license: 'MIT',
data/template.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Gemini AI
2
2
 
3
- A Ruby Gem for interacting with [Gemini](https://deepmind.google/technologies/gemini/) through [Vertex AI](https://cloud.google.com/vertex-ai) or [AI Studio](https://makersuite.google.com), Google's generative AI services.
3
+ A Ruby Gem for interacting with [Gemini](https://deepmind.google/technologies/gemini/) through [Vertex AI](https://cloud.google.com/vertex-ai), [Generative Language API](https://ai.google.dev/api/rest), or [AI Studio](https://makersuite.google.com), Google's generative AI services.
4
4
 
5
5
  ![The logo shows a gemstone split into red and blue halves, symbolizing Ruby programming and Gemini AI. It's surrounded by a circuit-like design on a dark blue backdrop.](https://raw.githubusercontent.com/gbaptista/assets/main/gemini-ai/ruby-gemini-ai.png)
6
6
 
@@ -9,7 +9,7 @@ A Ruby Gem for interacting with [Gemini](https://deepmind.google/technologies/ge
9
9
  ## TL;DR and Quick Start
10
10
 
11
11
  ```ruby
12
- gem 'gemini-ai', '~> 2.0'
12
+ gem 'gemini-ai', '~> 2.2.0'
13
13
  ```
14
14
 
15
15
  ```ruby
@@ -24,12 +24,20 @@ client = Gemini.new(
24
24
  options: { model: 'gemini-pro', stream: false }
25
25
  )
26
26
 
27
- # With a Service Account
27
+ # With a Service Account Credentials File
28
28
  client = Gemini.new(
29
29
  credentials: {
30
30
  service: 'vertex-ai-api',
31
31
  file_path: 'google-credentials.json',
32
- project_id: 'PROJECT_ID',
32
+ region: 'us-east4'
33
+ },
34
+ options: { model: 'gemini-pro', stream: false }
35
+ )
36
+
37
+ # With Application Default Credentials
38
+ client = Gemini.new(
39
+ credentials: {
40
+ service: 'vertex-ai-api',
33
41
  region: 'us-east4'
34
42
  },
35
43
  options: { model: 'gemini-pro', stream: false }
@@ -69,18 +77,23 @@ Result:
69
77
  ### Installing
70
78
 
71
79
  ```sh
72
- gem install gemini-ai -v 2.0.0
80
+ gem install gemini-ai -v 2.2.0
73
81
  ```
74
82
 
75
83
  ```sh
76
- gem 'gemini-ai', '~> 2.0'
84
+ gem 'gemini-ai', '~> 2.2.0'
77
85
  ```
78
86
 
79
87
  ### Credentials
80
88
 
89
+ - [Option 1: API Key (Generative Language API)](#option-1-api-key-generative-language-api)
90
+ - [Option 2: Service Account Credentials File (Vertex AI API)](#option-2-service-account-credentials-file-vertex-ai-api)
91
+ - [Option 3: Application Default Credentials (Vertex AI API)](#option-3-application-default-credentials-vertex-ai-api)
92
+ - [Required Data](#required-data)
93
+
81
94
  > ⚠️ DISCLAIMER: Be careful with what you are doing, and never trust others' code related to this. These commands and instructions alter the level of access to your Google Cloud Account, and running them naively can lead to security risks as well as financial risks. People with access to your account can use it to steal data or incur charges. Run these commands at your own responsibility and due diligence; expect no warranties from the contributors of this project.
82
95
 
83
- #### Option 1: API Key
96
+ #### Option 1: API Key (Generative Language API)
84
97
 
85
98
  You need a [Google Cloud](https://console.cloud.google.com) [_Project_](https://cloud.google.com/resource-manager/docs/creating-managing-projects), and then you can generate an API Key through the Google Cloud Console [here](https://console.cloud.google.com/apis/credentials).
86
99
 
@@ -89,7 +102,7 @@ You also need to enable the _Generative Language API_ service in your Google Clo
89
102
 
90
103
  Alternatively, you can generate an API Key through _Google AI Studio_ [here](https://makersuite.google.com/app/apikey). However, this approach will automatically create a project for you in your Google Cloud Account.
91
104
 
92
- #### Option 2: Service Account
105
+ #### Option 2: Service Account Credentials File (Vertex AI API)
93
106
 
94
107
  You need a [Google Cloud](https://console.cloud.google.com) [_Project_](https://cloud.google.com/resource-manager/docs/creating-managing-projects) and a [_Service Account_](https://cloud.google.com/iam/docs/service-account-overview) to use [Vertex AI](https://cloud.google.com/vertex-ai) API.
95
108
 
@@ -143,11 +156,24 @@ gcloud projects add-iam-policy-binding PROJECT_ID \
143
156
  --role='roles/ml.admin'
144
157
  ```
145
158
 
159
+ #### Option 3: Application Default Credentials (Vertex AI API)
160
+
161
+ Similar to [Option 2](#option-2-service-account-credentials-file-vertex-ai-api), but you don't need to download a `google-credentials.json`. [_Application Default Credentials_](https://cloud.google.com/docs/authentication/application-default-credentials) automatically find credentials based on the application environment.
162
+
163
+ For local development, you can generate your default credentials using the [gcloud CLI](https://cloud.google.com/sdk/gcloud) as follows:
164
+
165
+ ```sh
166
+ gcloud auth application-default login
167
+ ```
168
+
169
+ For more details about alternative methods and different environments, check the official documentation:
170
+ [Set up Application Default Credentials](https://cloud.google.com/docs/authentication/provide-credentials-adc)
171
+
146
172
  #### Required Data
147
173
 
148
174
  After choosing an option, you should have all the necessary data and access to use Gemini.
149
175
 
150
- For API Key:
176
+ **Option 1**, for API Key:
151
177
 
152
178
  ```ruby
153
179
  {
@@ -165,13 +191,21 @@ Remember that hardcoding your API key in code is unsafe; it's preferable to use
165
191
  }
166
192
  ```
167
193
 
168
- Alternativelly, for Service Account, a `google-credentials.json` file, a `PROJECT_ID`, and a `REGION`:
194
+ **Option 2**: For the Service Account, provide a `google-credentials.json` file and a `REGION`:
169
195
 
170
196
  ```ruby
171
197
  {
172
198
  service: 'vertex-ai-api',
173
199
  file_path: 'google-credentials.json',
174
- project_id: 'PROJECT_ID',
200
+ region: 'us-east4'
201
+ }
202
+ ```
203
+
204
+ **Option 3**: For _Application Default Credentials_, omit both the `api_key` and the `file_path`:
205
+
206
+ ```ruby
207
+ {
208
+ service: 'vertex-ai-api',
175
209
  region: 'us-east4'
176
210
  }
177
211
  ```
@@ -190,6 +224,15 @@ Tokyo, Japan (asia-northeast1)
190
224
 
191
225
  You can follow here if new regions are available: [Gemini API](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/gemini)
192
226
 
227
+ You might want to explicitly set a Google Cloud Project ID, which you can do as follows:
228
+
229
+ ```ruby
230
+ {
231
+ service: 'vertex-ai-api',
232
+ project_id: 'PROJECT_ID'
233
+ }
234
+ ```
235
+
193
236
  ## Usage
194
237
 
195
238
  ### Client
@@ -208,12 +251,20 @@ client = Gemini.new(
208
251
  options: { model: 'gemini-pro', stream: false }
209
252
  )
210
253
 
211
- # With a Service Account
254
+ # With a Service Account Credentials File
212
255
  client = Gemini.new(
213
256
  credentials: {
214
257
  service: 'vertex-ai-api',
215
258
  file_path: 'google-credentials.json',
216
- project_id: 'PROJECT_ID',
259
+ region: 'us-east4'
260
+ },
261
+ options: { model: 'gemini-pro', stream: false }
262
+ )
263
+
264
+ # With Application Default Credentials
265
+ client = Gemini.new(
266
+ credentials: {
267
+ service: 'vertex-ai-api',
217
268
  region: 'us-east4'
218
269
  },
219
270
  options: { model: 'gemini-pro', stream: false }
@@ -222,6 +273,153 @@ client = Gemini.new(
222
273
 
223
274
  ### Generate Content
224
275
 
276
+ #### Modes
277
+
278
+ ##### Text
279
+
280
+ ```ruby
281
+ result = client.stream_generate_content({
282
+ contents: { role: 'user', parts: { text: 'hi!' } }
283
+ })
284
+ ```
285
+
286
+ Result:
287
+ ```ruby
288
+ [{ 'candidates' =>
289
+ [{ 'content' => {
290
+ 'role' => 'model',
291
+ 'parts' => [{ 'text' => 'Hello! How may I assist you?' }]
292
+ },
293
+ 'finishReason' => 'STOP',
294
+ 'safetyRatings' =>
295
+ [{ 'category' => 'HARM_CATEGORY_HARASSMENT', 'probability' => 'NEGLIGIBLE' },
296
+ { 'category' => 'HARM_CATEGORY_HATE_SPEECH', 'probability' => 'NEGLIGIBLE' },
297
+ { 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'probability' => 'NEGLIGIBLE' },
298
+ { 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT', 'probability' => 'NEGLIGIBLE' }] }],
299
+ 'usageMetadata' => {
300
+ 'promptTokenCount' => 2,
301
+ 'candidatesTokenCount' => 8,
302
+ 'totalTokenCount' => 10
303
+ } }]
304
+ ```
305
+
306
+ ##### Image
307
+
308
+ ![A black and white image of an old piano. The piano is an upright model, with the keys on the right side of the image. The piano is sitting on a tiled floor. There is a small round object on the top of the piano.](https://raw.githubusercontent.com/gbaptista/assets/main/gemini-ai/piano.jpg)
309
+
310
+ > _Courtesy of [Unsplash](https://unsplash.com/photos/greyscale-photo-of-grand-piano-czPs0z3-Ggg)_
311
+
312
+ Switch to the `gemini-pro-vision` model:
313
+
314
+ ```ruby
315
+ client = Gemini.new(
316
+ credentials: { service: 'vertex-ai-api', region: 'us-east4' },
317
+ options: { model: 'gemini-pro-vision', stream: true }
318
+ )
319
+ ```
320
+
321
+ Then, encode the image as [Base64](https://en.wikipedia.org/wiki/Base64) and add its [MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types):
322
+
323
+ ```ruby
324
+ require 'base64'
325
+
326
+ result = client.stream_generate_content(
327
+ { contents: [
328
+ { role: 'user', parts: [
329
+ { text: 'Please describe this image.' },
330
+ { inline_data: {
331
+ mime_type: 'image/jpeg',
332
+ data: Base64.strict_encode64(File.read('piano.jpg'))
333
+ } }
334
+ ] }
335
+ ] }
336
+ )
337
+ ```
338
+
339
+ The result:
340
+ ```ruby
341
+ [{ 'candidates' =>
342
+ [{ 'content' =>
343
+ { 'role' => 'model',
344
+ 'parts' =>
345
+ [{ 'text' =>
346
+ ' A black and white image of an old piano. The piano is an upright model, with the keys on the right side of the image. The piano is' }] },
347
+ 'safetyRatings' =>
348
+ [{ 'category' => 'HARM_CATEGORY_HARASSMENT', 'probability' => 'NEGLIGIBLE' },
349
+ { 'category' => 'HARM_CATEGORY_HATE_SPEECH', 'probability' => 'NEGLIGIBLE' },
350
+ { 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'probability' => 'NEGLIGIBLE' },
351
+ { 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT', 'probability' => 'NEGLIGIBLE' }] }] },
352
+ { 'candidates' =>
353
+ [{ 'content' => { 'role' => 'model', 'parts' => [{ 'text' => ' sitting on a tiled floor. There is a small round object on the top of the piano.' }] },
354
+ 'finishReason' => 'STOP',
355
+ 'safetyRatings' =>
356
+ [{ 'category' => 'HARM_CATEGORY_HARASSMENT', 'probability' => 'NEGLIGIBLE' },
357
+ { 'category' => 'HARM_CATEGORY_HATE_SPEECH', 'probability' => 'NEGLIGIBLE' },
358
+ { 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'probability' => 'NEGLIGIBLE' },
359
+ { 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT', 'probability' => 'NEGLIGIBLE' }] }],
360
+ 'usageMetadata' => { 'promptTokenCount' => 263, 'candidatesTokenCount' => 50, 'totalTokenCount' => 313 } }]
361
+ ```
362
+
363
+ ##### Video
364
+
365
+ https://gist.github.com/assets/29520/f82bccbf-02d2-4899-9c48-eb8a0a5ef741
366
+
367
+ > ALT: A white and gold cup is being filled with coffee. The coffee is dark and rich. The cup is sitting on a black surface. The background is blurred.
368
+
369
+ > _Courtesy of [Pexels](https://www.pexels.com/video/pouring-of-coffee-855391/)_
370
+
371
+ Switch to the `gemini-pro-vision` model:
372
+
373
+ ```ruby
374
+ client = Gemini.new(
375
+ credentials: { service: 'vertex-ai-api', region: 'us-east4' },
376
+ options: { model: 'gemini-pro-vision', stream: true }
377
+ )
378
+ ```
379
+
380
+ Then, encode the video as [Base64](https://en.wikipedia.org/wiki/Base64) and add its [MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types):
381
+
382
+ ```ruby
383
+ require 'base64'
384
+
385
+ result = client.stream_generate_content(
386
+ { contents: [
387
+ { role: 'user', parts: [
388
+ { text: 'Please describe this video.' },
389
+ { inline_data: {
390
+ mime_type: 'video/mp4',
391
+ data: Base64.strict_encode64(File.read('coffee.mp4'))
392
+ } }
393
+ ] }
394
+ ] }
395
+ )
396
+ ```
397
+
398
+ The result:
399
+ ```ruby
400
+ [{"candidates"=>
401
+ [{"content"=>
402
+ {"role"=>"model",
403
+ "parts"=>
404
+ [{"text"=>
405
+ " A white and gold cup is being filled with coffee. The coffee is dark and rich. The cup is sitting on a black surface. The background is blurred"}]},
406
+ "safetyRatings"=>
407
+ [{"category"=>"HARM_CATEGORY_HARASSMENT", "probability"=>"NEGLIGIBLE"},
408
+ {"category"=>"HARM_CATEGORY_HATE_SPEECH", "probability"=>"NEGLIGIBLE"},
409
+ {"category"=>"HARM_CATEGORY_SEXUALLY_EXPLICIT", "probability"=>"NEGLIGIBLE"},
410
+ {"category"=>"HARM_CATEGORY_DANGEROUS_CONTENT", "probability"=>"NEGLIGIBLE"}]}],
411
+ "usageMetadata"=>{"promptTokenCount"=>1037, "candidatesTokenCount"=>31, "totalTokenCount"=>1068}},
412
+ {"candidates"=>
413
+ [{"content"=>{"role"=>"model", "parts"=>[{"text"=>"."}]},
414
+ "finishReason"=>"STOP",
415
+ "safetyRatings"=>
416
+ [{"category"=>"HARM_CATEGORY_HARASSMENT", "probability"=>"NEGLIGIBLE"},
417
+ {"category"=>"HARM_CATEGORY_HATE_SPEECH", "probability"=>"NEGLIGIBLE"},
418
+ {"category"=>"HARM_CATEGORY_SEXUALLY_EXPLICIT", "probability"=>"NEGLIGIBLE"},
419
+ {"category"=>"HARM_CATEGORY_DANGEROUS_CONTENT", "probability"=>"NEGLIGIBLE"}]}],
420
+ "usageMetadata"=>{"promptTokenCount"=>1037, "candidatesTokenCount"=>32, "totalTokenCount"=>1069}}]
421
+ ```
422
+
225
423
  #### Synchronous
226
424
 
227
425
  ```ruby
@@ -329,7 +527,7 @@ Result:
329
527
  } }]
330
528
  ```
331
529
 
332
- ### Back-and-Forth Conversations
530
+ #### Back-and-Forth Conversations
333
531
 
334
532
  To maintain a back-and-forth conversation, you need to append the received responses and build a history for your requests:
335
533
 
@@ -371,9 +569,9 @@ Result:
371
569
  } }]
372
570
  ```
373
571
 
374
- ### Tools (Functions) Calling
572
+ #### Tools (Functions) Calling
375
573
 
376
- > As of the writing of this README, only the `gemini-pro` model [supports](https://cloud.google.com/vertex-ai/docs/generative-ai/multimodal/function-calling#supported_models) tools (functions) calls.
574
+ > As of the writing of this README, only the `vertex-ai-api` service and the `gemini-pro` model [supports](https://cloud.google.com/vertex-ai/docs/generative-ai/multimodal/function-calling#supported_models) tools (functions) calls.
377
575
 
378
576
  You can provide specifications for [tools (functions)](https://cloud.google.com/vertex-ai/docs/generative-ai/multimodal/function-calling) using [JSON Schema](https://json-schema.org) to generate potential calls to them:
379
577
 
@@ -527,6 +725,58 @@ result = client.request(
527
725
  )
528
726
  ```
529
727
 
728
+ ### Error Handling
729
+
730
+ #### Rescuing
731
+
732
+ ```ruby
733
+ require 'gemini-ai'
734
+
735
+ begin
736
+ client.stream_generate_content({
737
+ contents: { role: 'user', parts: { text: 'hi!' } }
738
+ })
739
+ rescue Gemini::Errors::GeminiError => error
740
+ puts error.class # Gemini::Errors::RequestError
741
+ puts error.message # 'the server responded with status 500'
742
+
743
+ puts error.payload
744
+ # { contents: [{ role: 'user', parts: { text: 'hi!' } }],
745
+ # generationConfig: { candidateCount: 1 },
746
+ # ...
747
+ # }
748
+
749
+ puts error.request
750
+ # #<Faraday::ServerError response={:status=>500, :headers...
751
+ end
752
+ ```
753
+
754
+ #### For Short
755
+
756
+ ```ruby
757
+ require 'gemini-ai/errors'
758
+
759
+ begin
760
+ client.stream_generate_content({
761
+ contents: { role: 'user', parts: { text: 'hi!' } }
762
+ })
763
+ rescue GeminiError => error
764
+ puts error.class # Gemini::Errors::RequestError
765
+ end
766
+ ```
767
+
768
+ #### Errors
769
+
770
+ ```ruby
771
+ GeminiError
772
+
773
+ MissingProjectIdError
774
+ UnsupportedServiceError
775
+ BlockWithoutStreamError
776
+
777
+ RequestError
778
+ ```
779
+
530
780
  ## Development
531
781
 
532
782
  ```bash
@@ -545,7 +795,7 @@ gem build gemini-ai.gemspec
545
795
 
546
796
  gem signin
547
797
 
548
- gem push gemini-ai-2.0.0.gem
798
+ gem push gemini-ai-2.2.0.gem
549
799
  ```
550
800
 
551
801
  ### Updating the README
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gemini-ai
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - gbaptista
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-12-15 00:00:00.000000000 Z
11
+ date: 2023-12-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: event_stream_parser
@@ -64,8 +64,8 @@ dependencies:
64
64
  - - ">="
65
65
  - !ruby/object:Gem::Version
66
66
  version: 1.9.1
67
- description: A Ruby Gem for interacting with Gemini through Vertex AI or AI Studio,
68
- Google's generative AI services.
67
+ description: A Ruby Gem for interacting with Gemini through Vertex AI, Generative
68
+ Language API, or AI Studio, Google's generative AI services.
69
69
  email:
70
70
  executables: []
71
71
  extensions: []
@@ -78,9 +78,11 @@ files:
78
78
  - Gemfile.lock
79
79
  - LICENSE
80
80
  - README.md
81
+ - components/errors.rb
81
82
  - controllers/client.rb
82
83
  - gemini-ai.gemspec
83
84
  - ports/dsl/gemini-ai.rb
85
+ - ports/dsl/gemini-ai/errors.rb
84
86
  - static/gem.rb
85
87
  - tasks/generate-readme.clj
86
88
  - template.md