gemini-ai 2.2.0 → 3.1.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: c7c5bd6e6cd1d2195b7a437fb0664cd553dc0acbf0d293042d93c3d701e6b6e0
4
- data.tar.gz: 665458cc152b00efae9f8e2b730fe000ef2caac63f835944c1e76a83b9a0e627
3
+ metadata.gz: 36be77448ab7ba00008ac7548c25fdca45b92980762303631de5d6bdbcd2d01a
4
+ data.tar.gz: e0772790afa6019424282e8ac665fb0049da13a76ecc43909b147655f6f95a3a
5
5
  SHA512:
6
- metadata.gz: 0ca1f3f87c61276902259d937f4d57f324a756d1e63c1e5781680ba970313f3c3c29a2da49c4eb1ec0bf351f984bee378e86b22d1656b2423638e1a70bd5dddf
7
- data.tar.gz: 50988d0881d37f561e0c75ac1beea4fc9b34c757d0f16fb2dcb2f63d3e63f867194e0f826800c8429ce9f33500dd74c3fa4c5d747a0975378c81cf3e0ad4a61b
6
+ metadata.gz: e4d6c7391dff2ce75a46e3ceb5c7ef9c07c6cdcfc55477df68be1df2e06f77ff77d795ee6e70abc9ad73001e8dd583d39949a394140b129d08c2db94c2b8f7c4
7
+ data.tar.gz: 2acedebdfe562b7b68d19046a2f2b06bec6fbe75768cc7066b7c1e31cb72bc4caa551a8d656ffc6537c714f0d11965f3cc9f74542e7bc9315f0998f35390d5f4
data/Gemfile.lock CHANGED
@@ -1,9 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- gemini-ai (2.2.0)
4
+ gemini-ai (3.1.0)
5
5
  event_stream_parser (~> 1.0)
6
- faraday (~> 2.7, >= 2.7.12)
6
+ faraday (~> 2.8, >= 2.8.1)
7
7
  googleauth (~> 1.9, >= 1.9.1)
8
8
 
9
9
  GEM
@@ -16,7 +16,7 @@ GEM
16
16
  byebug (11.1.3)
17
17
  coderay (1.1.3)
18
18
  event_stream_parser (1.0.0)
19
- faraday (2.7.12)
19
+ faraday (2.8.1)
20
20
  base64
21
21
  faraday-net_http (>= 2.0, < 3.1)
22
22
  ruby2_keywords (>= 0.0.4)
data/README.md CHANGED
@@ -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.2.0'
12
+ gem 'gemini-ai', '~> 3.1.0'
13
13
  ```
14
14
 
15
15
  ```ruby
@@ -21,7 +21,7 @@ client = Gemini.new(
21
21
  service: 'generative-language-api',
22
22
  api_key: ENV['GOOGLE_API_KEY']
23
23
  },
24
- options: { model: 'gemini-pro', stream: false }
24
+ options: { model: 'gemini-pro', server_sent_events: true }
25
25
  )
26
26
 
27
27
  # With a Service Account Credentials File
@@ -31,7 +31,7 @@ client = Gemini.new(
31
31
  file_path: 'google-credentials.json',
32
32
  region: 'us-east4'
33
33
  },
34
- options: { model: 'gemini-pro', stream: false }
34
+ options: { model: 'gemini-pro', server_sent_events: true }
35
35
  )
36
36
 
37
37
  # With Application Default Credentials
@@ -40,7 +40,7 @@ client = Gemini.new(
40
40
  service: 'vertex-ai-api',
41
41
  region: 'us-east4'
42
42
  },
43
- options: { model: 'gemini-pro', stream: false }
43
+ options: { model: 'gemini-pro', server_sent_events: true }
44
44
  )
45
45
 
46
46
  result = client.stream_generate_content({
@@ -81,17 +81,23 @@ Result:
81
81
  - [Required Data](#required-data)
82
82
  - [Usage](#usage)
83
83
  - [Client](#client)
84
- - [Generate Content](#generate-content)
85
- - [Modes](#modes)
86
- - [Text](#text)
87
- - [Image](#image)
88
- - [Video](#video)
89
- - [Synchronous](#synchronous)
90
- - [Streaming](#streaming)
91
- - [Streaming Hang](#streaming-hang)
92
- - [Back-and-Forth Conversations](#back-and-forth-conversations)
93
- - [Tools (Functions) Calling](#tools-functions-calling)
84
+ - [Methods](#methods)
85
+ - [stream_generate_content](#stream_generate_content)
86
+ - [Receiving Stream Events](#receiving-stream-events)
87
+ - [Without Events](#without-events)
88
+ - [generate_content](#generate_content)
89
+ - [Modes](#modes)
90
+ - [Text](#text)
91
+ - [Image](#image)
92
+ - [Video](#video)
93
+ - [Streaming vs. Server-Sent Events (SSE)](#streaming-vs-server-sent-events-sse)
94
+ - [Server-Sent Events (SSE) Hang](#server-sent-events-sse-hang)
95
+ - [Non-Streaming](#non-streaming)
96
+ - [Back-and-Forth Conversations](#back-and-forth-conversations)
97
+ - [Tools (Functions) Calling](#tools-functions-calling)
94
98
  - [New Functionalities and APIs](#new-functionalities-and-apis)
99
+ - [Request Options](#request-options)
100
+ - [Timeout](#timeout)
95
101
  - [Error Handling](#error-handling)
96
102
  - [Rescuing](#rescuing)
97
103
  - [For Short](#for-short)
@@ -108,11 +114,11 @@ Result:
108
114
  ### Installing
109
115
 
110
116
  ```sh
111
- gem install gemini-ai -v 2.2.0
117
+ gem install gemini-ai -v 3.1.0
112
118
  ```
113
119
 
114
120
  ```sh
115
- gem 'gemini-ai', '~> 2.2.0'
121
+ gem 'gemini-ai', '~> 3.1.0'
116
122
  ```
117
123
 
118
124
  ### Credentials
@@ -279,7 +285,7 @@ client = Gemini.new(
279
285
  service: 'generative-language-api',
280
286
  api_key: ENV['GOOGLE_API_KEY']
281
287
  },
282
- options: { model: 'gemini-pro', stream: false }
288
+ options: { model: 'gemini-pro', server_sent_events: true }
283
289
  )
284
290
 
285
291
  # With a Service Account Credentials File
@@ -289,7 +295,7 @@ client = Gemini.new(
289
295
  file_path: 'google-credentials.json',
290
296
  region: 'us-east4'
291
297
  },
292
- options: { model: 'gemini-pro', stream: false }
298
+ options: { model: 'gemini-pro', server_sent_events: true }
293
299
  )
294
300
 
295
301
  # With Application Default Credentials
@@ -298,15 +304,118 @@ client = Gemini.new(
298
304
  service: 'vertex-ai-api',
299
305
  region: 'us-east4'
300
306
  },
301
- options: { model: 'gemini-pro', stream: false }
307
+ options: { model: 'gemini-pro', server_sent_events: true }
302
308
  )
303
309
  ```
304
310
 
305
- ### Generate Content
311
+ ### Methods
306
312
 
307
- #### Modes
313
+ #### stream_generate_content
308
314
 
309
- ##### Text
315
+ ##### Receiving Stream Events
316
+
317
+ Ensure that you have enabled [Server-Sent Events](#streaming-vs-server-sent-events-sse) before using blocks for streaming:
318
+
319
+ ```ruby
320
+ client.stream_generate_content(
321
+ { contents: { role: 'user', parts: { text: 'hi!' } } }
322
+ ) do |event, parsed, raw|
323
+ puts event
324
+ end
325
+ ```
326
+
327
+ Event:
328
+ ```ruby
329
+ { 'candidates' =>
330
+ [{ 'content' => {
331
+ 'role' => 'model',
332
+ 'parts' => [{ 'text' => 'Hello! How may I assist you?' }]
333
+ },
334
+ 'finishReason' => 'STOP',
335
+ 'safetyRatings' =>
336
+ [{ 'category' => 'HARM_CATEGORY_HARASSMENT', 'probability' => 'NEGLIGIBLE' },
337
+ { 'category' => 'HARM_CATEGORY_HATE_SPEECH', 'probability' => 'NEGLIGIBLE' },
338
+ { 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'probability' => 'NEGLIGIBLE' },
339
+ { 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT', 'probability' => 'NEGLIGIBLE' }] }],
340
+ 'usageMetadata' => {
341
+ 'promptTokenCount' => 2,
342
+ 'candidatesTokenCount' => 8,
343
+ 'totalTokenCount' => 10
344
+ } }
345
+ ```
346
+
347
+ ##### Without Events
348
+
349
+ You can use `stream_generate_content` without events:
350
+
351
+ ```ruby
352
+ result = client.stream_generate_content(
353
+ { contents: { role: 'user', parts: { text: 'hi!' } } }
354
+ )
355
+ ```
356
+
357
+ In this case, the result will be an array with all the received events:
358
+
359
+ ```ruby
360
+ [{ 'candidates' =>
361
+ [{ 'content' => {
362
+ 'role' => 'model',
363
+ 'parts' => [{ 'text' => 'Hello! How may I assist you?' }]
364
+ },
365
+ 'finishReason' => 'STOP',
366
+ 'safetyRatings' =>
367
+ [{ 'category' => 'HARM_CATEGORY_HARASSMENT', 'probability' => 'NEGLIGIBLE' },
368
+ { 'category' => 'HARM_CATEGORY_HATE_SPEECH', 'probability' => 'NEGLIGIBLE' },
369
+ { 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'probability' => 'NEGLIGIBLE' },
370
+ { 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT', 'probability' => 'NEGLIGIBLE' }] }],
371
+ 'usageMetadata' => {
372
+ 'promptTokenCount' => 2,
373
+ 'candidatesTokenCount' => 8,
374
+ 'totalTokenCount' => 10
375
+ } }]
376
+ ```
377
+
378
+ You can mix both as well:
379
+ ```ruby
380
+ result = client.stream_generate_content(
381
+ { contents: { role: 'user', parts: { text: 'hi!' } } }
382
+ ) do |event, parsed, raw|
383
+ puts event
384
+ end
385
+ ```
386
+
387
+ #### generate_content
388
+
389
+ ```ruby
390
+ result = client.generate_content(
391
+ { contents: { role: 'user', parts: { text: 'hi!' } } }
392
+ )
393
+ ```
394
+
395
+ Result:
396
+ ```ruby
397
+ { 'candidates' =>
398
+ [{ 'content' => { 'parts' => [{ 'text' => 'Hello! How can I assist you today?' }], 'role' => 'model' },
399
+ 'finishReason' => 'STOP',
400
+ 'index' => 0,
401
+ 'safetyRatings' =>
402
+ [{ 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'probability' => 'NEGLIGIBLE' },
403
+ { 'category' => 'HARM_CATEGORY_HATE_SPEECH', 'probability' => 'NEGLIGIBLE' },
404
+ { 'category' => 'HARM_CATEGORY_HARASSMENT', 'probability' => 'NEGLIGIBLE' },
405
+ { 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT', 'probability' => 'NEGLIGIBLE' }] }],
406
+ 'promptFeedback' =>
407
+ { 'safetyRatings' =>
408
+ [{ 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'probability' => 'NEGLIGIBLE' },
409
+ { 'category' => 'HARM_CATEGORY_HATE_SPEECH', 'probability' => 'NEGLIGIBLE' },
410
+ { 'category' => 'HARM_CATEGORY_HARASSMENT', 'probability' => 'NEGLIGIBLE' },
411
+ { 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT', 'probability' => 'NEGLIGIBLE' }] } }
412
+ ```
413
+
414
+ As of the writing of this README, only the `generative-language-api` service supports the `generate_content` method; `vertex-ai-api` does not.
415
+
416
+ ### Modes
417
+
418
+ #### Text
310
419
 
311
420
  ```ruby
312
421
  result = client.stream_generate_content({
@@ -334,7 +443,7 @@ Result:
334
443
  } }]
335
444
  ```
336
445
 
337
- ##### Image
446
+ #### Image
338
447
 
339
448
  ![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
449
 
@@ -345,7 +454,7 @@ Switch to the `gemini-pro-vision` model:
345
454
  ```ruby
346
455
  client = Gemini.new(
347
456
  credentials: { service: 'vertex-ai-api', region: 'us-east4' },
348
- options: { model: 'gemini-pro-vision', stream: true }
457
+ options: { model: 'gemini-pro-vision', server_sent_events: true }
349
458
  )
350
459
  ```
351
460
 
@@ -391,7 +500,7 @@ The result:
391
500
  'usageMetadata' => { 'promptTokenCount' => 263, 'candidatesTokenCount' => 50, 'totalTokenCount' => 313 } }]
392
501
  ```
393
502
 
394
- ##### Video
503
+ #### Video
395
504
 
396
505
  https://gist.github.com/assets/29520/f82bccbf-02d2-4899-9c48-eb8a0a5ef741
397
506
 
@@ -404,7 +513,7 @@ Switch to the `gemini-pro-vision` model:
404
513
  ```ruby
405
514
  client = Gemini.new(
406
515
  credentials: { service: 'vertex-ai-api', region: 'us-east4' },
407
- options: { model: 'gemini-pro-vision', stream: true }
516
+ options: { model: 'gemini-pro-vision', server_sent_events: true }
408
517
  )
409
518
  ```
410
519
 
@@ -451,41 +560,15 @@ The result:
451
560
  "usageMetadata"=>{"promptTokenCount"=>1037, "candidatesTokenCount"=>32, "totalTokenCount"=>1069}}]
452
561
  ```
453
562
 
454
- #### Synchronous
455
-
456
- ```ruby
457
- result = client.stream_generate_content({
458
- contents: { role: 'user', parts: { text: 'hi!' } }
459
- })
460
- ```
461
-
462
- Result:
463
- ```ruby
464
- [{ 'candidates' =>
465
- [{ 'content' => {
466
- 'role' => 'model',
467
- 'parts' => [{ 'text' => 'Hello! How may I assist you?' }]
468
- },
469
- 'finishReason' => 'STOP',
470
- 'safetyRatings' =>
471
- [{ 'category' => 'HARM_CATEGORY_HARASSMENT', 'probability' => 'NEGLIGIBLE' },
472
- { 'category' => 'HARM_CATEGORY_HATE_SPEECH', 'probability' => 'NEGLIGIBLE' },
473
- { 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'probability' => 'NEGLIGIBLE' },
474
- { 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT', 'probability' => 'NEGLIGIBLE' }] }],
475
- 'usageMetadata' => {
476
- 'promptTokenCount' => 2,
477
- 'candidatesTokenCount' => 8,
478
- 'totalTokenCount' => 10
479
- } }]
480
- ```
563
+ ### Streaming vs. Server-Sent Events (SSE)
481
564
 
482
- #### Streaming
565
+ [Server-Sent Events (SSE)](https://en.wikipedia.org/wiki/Server-sent_events) is a technology that allows certain endpoints to offer streaming capabilities, such as creating the impression that "the model is typing along with you," rather than delivering the entire answer all at once.
483
566
 
484
- You can set up the client to use streaming for all supported endpoints:
567
+ You can set up the client to use Server-Sent Events (SSE) for all supported endpoints:
485
568
  ```ruby
486
569
  client = Gemini.new(
487
570
  credentials: { ... },
488
- options: { model: 'gemini-pro', stream: true }
571
+ options: { model: 'gemini-pro', server_sent_events: true }
489
572
  )
490
573
  ```
491
574
 
@@ -493,11 +576,11 @@ Or, you can decide on a request basis:
493
576
  ```ruby
494
577
  client.stream_generate_content(
495
578
  { contents: { role: 'user', parts: { text: 'hi!' } } },
496
- stream: true
579
+ server_sent_events: true
497
580
  )
498
581
  ```
499
582
 
500
- With streaming enabled, you can use a block to receive the results:
583
+ With Server-Sent Events (SSE) enabled, you can use a block to receive partial results via events. This feature is particularly useful for methods that offer streaming capabilities, such as `stream_generate_content`:
501
584
 
502
585
  ```ruby
503
586
  client.stream_generate_content(
@@ -527,14 +610,16 @@ Event:
527
610
  } }
528
611
  ```
529
612
 
530
- #### Streaming Hang
613
+ Even though streaming methods utilize Server-Sent Events (SSE), using this feature doesn't necessarily mean streaming data. For example, when `generate_content` is called with SSE enabled, you will receive all the data at once in a single event, rather than through multiple partial events. This occurs because `generate_content` isn't designed for streaming, even though it is capable of utilizing Server-Sent Events.
614
+
615
+ #### Server-Sent Events (SSE) Hang
531
616
 
532
- Method calls will _hang_ until the stream finishes, so even without providing a block, you can get the final results of the stream events:
617
+ Method calls will _hang_ until the server-sent events finish, so even without providing a block, you can obtain the final results of the received events:
533
618
 
534
619
  ```ruby
535
620
  result = client.stream_generate_content(
536
621
  { contents: { role: 'user', parts: { text: 'hi!' } } },
537
- stream: true
622
+ server_sent_events: true
538
623
  )
539
624
  ```
540
625
 
@@ -558,7 +643,40 @@ Result:
558
643
  } }]
559
644
  ```
560
645
 
561
- #### Back-and-Forth Conversations
646
+ #### Non-Streaming
647
+
648
+ Depending on the service, you can use the [`generate_content`](#generate_content) method, which does not stream the answer.
649
+
650
+ You can also use methods designed for streaming without necessarily processing partial events; instead, you can wait for the result of all received events:
651
+
652
+ ```ruby
653
+ result = client.stream_generate_content({
654
+ contents: { role: 'user', parts: { text: 'hi!' } },
655
+ server_sent_events: false
656
+ })
657
+ ```
658
+
659
+ Result:
660
+ ```ruby
661
+ [{ 'candidates' =>
662
+ [{ 'content' => {
663
+ 'role' => 'model',
664
+ 'parts' => [{ 'text' => 'Hello! How may I assist you?' }]
665
+ },
666
+ 'finishReason' => 'STOP',
667
+ 'safetyRatings' =>
668
+ [{ 'category' => 'HARM_CATEGORY_HARASSMENT', 'probability' => 'NEGLIGIBLE' },
669
+ { 'category' => 'HARM_CATEGORY_HATE_SPEECH', 'probability' => 'NEGLIGIBLE' },
670
+ { 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'probability' => 'NEGLIGIBLE' },
671
+ { 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT', 'probability' => 'NEGLIGIBLE' }] }],
672
+ 'usageMetadata' => {
673
+ 'promptTokenCount' => 2,
674
+ 'candidatesTokenCount' => 8,
675
+ 'totalTokenCount' => 10
676
+ } }]
677
+ ```
678
+
679
+ ### Back-and-Forth Conversations
562
680
 
563
681
  To maintain a back-and-forth conversation, you need to append the received responses and build a history for your requests:
564
682
 
@@ -600,7 +718,7 @@ Result:
600
718
  } }]
601
719
  ```
602
720
 
603
- #### Tools (Functions) Calling
721
+ ### Tools (Functions) Calling
604
722
 
605
723
  > 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.
606
724
 
@@ -756,6 +874,42 @@ result = client.request(
756
874
  )
757
875
  ```
758
876
 
877
+ ### Request Options
878
+
879
+ #### Timeout
880
+
881
+ You can set the maximum number of seconds to wait for the request to complete with the `timeout` option:
882
+
883
+ ```ruby
884
+ client = Gemini.new(
885
+ credentials: { service: 'vertex-ai-api', region: 'us-east4' },
886
+ options: {
887
+ model: 'gemini-pro',
888
+ connection: { request: { timeout: 5 } }
889
+ }
890
+ )
891
+ ```
892
+
893
+ You can also have more fine-grained control over [Faraday's Request Options](https://lostisland.github.io/faraday/#/customization/request-options?id=request-options) if you prefer:
894
+
895
+ ```ruby
896
+ client = Gemini.new(
897
+ credentials: { service: 'vertex-ai-api', region: 'us-east4' },
898
+ options: {
899
+ model: 'gemini-pro',
900
+ connection: {
901
+ request: {
902
+ timeout: 5,
903
+ open_timeout: 5,
904
+ read_timeout: 5,
905
+ write_timeout: 5
906
+ }
907
+ }
908
+ }
909
+ )
910
+ ```
911
+
912
+
759
913
  ### Error Handling
760
914
 
761
915
  #### Rescuing
@@ -803,7 +957,7 @@ GeminiError
803
957
 
804
958
  MissingProjectIdError
805
959
  UnsupportedServiceError
806
- BlockWithoutStreamError
960
+ BlockWithoutServerSentEventsError
807
961
 
808
962
  RequestError
809
963
  ```
@@ -826,7 +980,7 @@ gem build gemini-ai.gemspec
826
980
 
827
981
  gem signin
828
982
 
829
- gem push gemini-ai-2.2.0.gem
983
+ gem push gemini-ai-3.1.0.gem
830
984
  ```
831
985
 
832
986
  ### Updating the README
data/components/errors.rb CHANGED
@@ -10,7 +10,7 @@ module Gemini
10
10
 
11
11
  class MissingProjectIdError < GeminiError; end
12
12
  class UnsupportedServiceError < GeminiError; end
13
- class BlockWithoutStreamError < GeminiError; end
13
+ class BlockWithoutServerSentEventsError < GeminiError; end
14
14
 
15
15
  class RequestError < GeminiError
16
16
  attr_reader :request, :payload
@@ -5,11 +5,13 @@ require 'faraday'
5
5
  require 'json'
6
6
  require 'googleauth'
7
7
 
8
- require_relative '../components/errors'
8
+ require_relative '../ports/dsl/gemini-ai/errors'
9
9
 
10
10
  module Gemini
11
11
  module Controllers
12
12
  class Client
13
+ ALLOWED_REQUEST_OPTIONS = %i[timeout open_timeout read_timeout write_timeout].freeze
14
+
13
15
  def initialize(config)
14
16
  if config[:credentials][:api_key]
15
17
  @authentication = :api_key
@@ -26,49 +28,67 @@ module Gemini
26
28
  end
27
29
 
28
30
  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
31
+ @project_id = config[:credentials][:project_id] || @authorizer.project_id || @authorizer.quota_project_id
34
32
 
35
33
  raise MissingProjectIdError, 'Could not determine project_id, which is required.' if @project_id.nil?
36
34
  end
37
35
 
38
- @address = case config[:credentials][:service]
36
+ @service = config[:credentials][:service]
37
+
38
+ @address = case @service
39
39
  when 'vertex-ai-api'
40
40
  "https://#{config[:credentials][:region]}-aiplatform.googleapis.com/v1/projects/#{@project_id}/locations/#{config[:credentials][:region]}/publishers/google/models/#{config[:options][:model]}"
41
41
  when 'generative-language-api'
42
42
  "https://generativelanguage.googleapis.com/v1/models/#{config[:options][:model]}"
43
43
  else
44
- raise UnsupportedServiceError, "Unsupported service: #{config[:credentials][:service]}"
44
+ raise UnsupportedServiceError, "Unsupported service: #{@service}"
45
45
  end
46
46
 
47
- @stream = config[:options][:stream]
47
+ @server_sent_events = config[:options][:server_sent_events]
48
+
49
+ @request_options = config.dig(:options, :connection, :request)
50
+
51
+ @request_options = if @request_options.is_a?(Hash)
52
+ @request_options.select do |key, _|
53
+ ALLOWED_REQUEST_OPTIONS.include?(key)
54
+ end
55
+ else
56
+ {}
57
+ end
48
58
  end
49
59
 
50
- def stream_generate_content(payload, stream: nil, &callback)
51
- request('streamGenerateContent', payload, stream:, &callback)
60
+ def stream_generate_content(payload, server_sent_events: nil, &callback)
61
+ request('streamGenerateContent', payload, server_sent_events:, &callback)
62
+ end
63
+
64
+ def generate_content(payload, server_sent_events: nil, &callback)
65
+ result = request('generateContent', payload, server_sent_events:, &callback)
66
+
67
+ return result.first if result.is_a?(Array) && result.size == 1
68
+
69
+ result
52
70
  end
53
71
 
54
- def request(path, payload, stream: nil, &callback)
55
- stream_enabled = stream.nil? ? @stream : stream
72
+ def request(path, payload, server_sent_events: nil, &callback)
73
+ server_sent_events_enabled = server_sent_events.nil? ? @server_sent_events : server_sent_events
56
74
  url = "#{@address}:#{path}"
57
75
  params = []
58
76
 
59
- params << 'alt=sse' if stream_enabled
77
+ params << 'alt=sse' if server_sent_events_enabled
60
78
  params << "key=#{@api_key}" if @authentication == :api_key
61
79
 
62
80
  url += "?#{params.join('&')}" if params.size.positive?
63
81
 
64
- if !callback.nil? && !stream_enabled
65
- raise BlockWithoutStreamError, 'You are trying to use a block without stream enabled.'
82
+ if !callback.nil? && !server_sent_events_enabled
83
+ raise BlockWithoutServerSentEventsError,
84
+ 'You are trying to use a block without Server Sent Events (SSE) enabled.'
66
85
  end
67
86
 
68
87
  results = []
69
88
 
70
- response = Faraday.new do |faraday|
89
+ response = Faraday.new(request: @request_options) do |faraday|
71
90
  faraday.response :raise_error
91
+ faraday.options.timeout = @timeout if @timeout
72
92
  end.post do |request|
73
93
  request.url url
74
94
  request.headers['Content-Type'] = 'application/json'
@@ -78,7 +98,7 @@ module Gemini
78
98
 
79
99
  request.body = payload.to_json
80
100
 
81
- if stream_enabled
101
+ if server_sent_events_enabled
82
102
  parser = EventStreamParser::Parser.new
83
103
 
84
104
  request.options.on_data = proc do |chunk, bytes, env|
@@ -107,7 +127,7 @@ module Gemini
107
127
  end
108
128
  end
109
129
 
110
- return safe_parse_json(response.body) unless stream_enabled
130
+ return safe_parse_json(response.body) unless server_sent_events_enabled
111
131
 
112
132
  results.map { |result| result[:event] }
113
133
  rescue Faraday::ServerError => e
data/gemini-ai.gemspec CHANGED
@@ -30,7 +30,7 @@ Gem::Specification.new do |spec|
30
30
  spec.require_paths = ['ports/dsl']
31
31
 
32
32
  spec.add_dependency 'event_stream_parser', '~> 1.0'
33
- spec.add_dependency 'faraday', '~> 2.7', '>= 2.7.12'
33
+ spec.add_dependency 'faraday', '~> 2.8', '>= 2.8.1'
34
34
  spec.add_dependency 'googleauth', '~> 1.9', '>= 1.9.1'
35
35
 
36
36
  spec.metadata['rubygems_mfa_required'] = 'true'
data/static/gem.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  module Gemini
4
4
  GEM = {
5
5
  name: 'gemini-ai',
6
- version: '2.2.0',
6
+ version: '3.1.0',
7
7
  author: 'gbaptista',
8
8
  summary: "Interact with Google's Gemini AI.",
9
9
  description: "A Ruby Gem for interacting with Gemini through Vertex AI, Generative Language API, or AI Studio, Google's generative AI services.",
@@ -4,7 +4,7 @@
4
4
  (-> text
5
5
  (clojure.string/lower-case)
6
6
  (clojure.string/replace " " "-")
7
- (clojure.string/replace #"[^a-z0-9\-]" "")))
7
+ (clojure.string/replace #"[^a-z0-9\-_]" "")))
8
8
 
9
9
  (defn remove-code-blocks [content]
10
10
  (let [code-block-regex #"(?s)```.*?```"]
data/template.md CHANGED
@@ -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.2.0'
12
+ gem 'gemini-ai', '~> 3.1.0'
13
13
  ```
14
14
 
15
15
  ```ruby
@@ -21,7 +21,7 @@ client = Gemini.new(
21
21
  service: 'generative-language-api',
22
22
  api_key: ENV['GOOGLE_API_KEY']
23
23
  },
24
- options: { model: 'gemini-pro', stream: false }
24
+ options: { model: 'gemini-pro', server_sent_events: true }
25
25
  )
26
26
 
27
27
  # With a Service Account Credentials File
@@ -31,7 +31,7 @@ client = Gemini.new(
31
31
  file_path: 'google-credentials.json',
32
32
  region: 'us-east4'
33
33
  },
34
- options: { model: 'gemini-pro', stream: false }
34
+ options: { model: 'gemini-pro', server_sent_events: true }
35
35
  )
36
36
 
37
37
  # With Application Default Credentials
@@ -40,7 +40,7 @@ client = Gemini.new(
40
40
  service: 'vertex-ai-api',
41
41
  region: 'us-east4'
42
42
  },
43
- options: { model: 'gemini-pro', stream: false }
43
+ options: { model: 'gemini-pro', server_sent_events: true }
44
44
  )
45
45
 
46
46
  result = client.stream_generate_content({
@@ -77,11 +77,11 @@ Result:
77
77
  ### Installing
78
78
 
79
79
  ```sh
80
- gem install gemini-ai -v 2.2.0
80
+ gem install gemini-ai -v 3.1.0
81
81
  ```
82
82
 
83
83
  ```sh
84
- gem 'gemini-ai', '~> 2.2.0'
84
+ gem 'gemini-ai', '~> 3.1.0'
85
85
  ```
86
86
 
87
87
  ### Credentials
@@ -248,7 +248,7 @@ client = Gemini.new(
248
248
  service: 'generative-language-api',
249
249
  api_key: ENV['GOOGLE_API_KEY']
250
250
  },
251
- options: { model: 'gemini-pro', stream: false }
251
+ options: { model: 'gemini-pro', server_sent_events: true }
252
252
  )
253
253
 
254
254
  # With a Service Account Credentials File
@@ -258,7 +258,7 @@ client = Gemini.new(
258
258
  file_path: 'google-credentials.json',
259
259
  region: 'us-east4'
260
260
  },
261
- options: { model: 'gemini-pro', stream: false }
261
+ options: { model: 'gemini-pro', server_sent_events: true }
262
262
  )
263
263
 
264
264
  # With Application Default Credentials
@@ -267,15 +267,118 @@ client = Gemini.new(
267
267
  service: 'vertex-ai-api',
268
268
  region: 'us-east4'
269
269
  },
270
- options: { model: 'gemini-pro', stream: false }
270
+ options: { model: 'gemini-pro', server_sent_events: true }
271
271
  )
272
272
  ```
273
273
 
274
- ### Generate Content
274
+ ### Methods
275
275
 
276
- #### Modes
276
+ #### stream_generate_content
277
277
 
278
- ##### Text
278
+ ##### Receiving Stream Events
279
+
280
+ Ensure that you have enabled [Server-Sent Events](#streaming-vs-server-sent-events-sse) before using blocks for streaming:
281
+
282
+ ```ruby
283
+ client.stream_generate_content(
284
+ { contents: { role: 'user', parts: { text: 'hi!' } } }
285
+ ) do |event, parsed, raw|
286
+ puts event
287
+ end
288
+ ```
289
+
290
+ Event:
291
+ ```ruby
292
+ { 'candidates' =>
293
+ [{ 'content' => {
294
+ 'role' => 'model',
295
+ 'parts' => [{ 'text' => 'Hello! How may I assist you?' }]
296
+ },
297
+ 'finishReason' => 'STOP',
298
+ 'safetyRatings' =>
299
+ [{ 'category' => 'HARM_CATEGORY_HARASSMENT', 'probability' => 'NEGLIGIBLE' },
300
+ { 'category' => 'HARM_CATEGORY_HATE_SPEECH', 'probability' => 'NEGLIGIBLE' },
301
+ { 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'probability' => 'NEGLIGIBLE' },
302
+ { 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT', 'probability' => 'NEGLIGIBLE' }] }],
303
+ 'usageMetadata' => {
304
+ 'promptTokenCount' => 2,
305
+ 'candidatesTokenCount' => 8,
306
+ 'totalTokenCount' => 10
307
+ } }
308
+ ```
309
+
310
+ ##### Without Events
311
+
312
+ You can use `stream_generate_content` without events:
313
+
314
+ ```ruby
315
+ result = client.stream_generate_content(
316
+ { contents: { role: 'user', parts: { text: 'hi!' } } }
317
+ )
318
+ ```
319
+
320
+ In this case, the result will be an array with all the received events:
321
+
322
+ ```ruby
323
+ [{ 'candidates' =>
324
+ [{ 'content' => {
325
+ 'role' => 'model',
326
+ 'parts' => [{ 'text' => 'Hello! How may I assist you?' }]
327
+ },
328
+ 'finishReason' => 'STOP',
329
+ 'safetyRatings' =>
330
+ [{ 'category' => 'HARM_CATEGORY_HARASSMENT', 'probability' => 'NEGLIGIBLE' },
331
+ { 'category' => 'HARM_CATEGORY_HATE_SPEECH', 'probability' => 'NEGLIGIBLE' },
332
+ { 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'probability' => 'NEGLIGIBLE' },
333
+ { 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT', 'probability' => 'NEGLIGIBLE' }] }],
334
+ 'usageMetadata' => {
335
+ 'promptTokenCount' => 2,
336
+ 'candidatesTokenCount' => 8,
337
+ 'totalTokenCount' => 10
338
+ } }]
339
+ ```
340
+
341
+ You can mix both as well:
342
+ ```ruby
343
+ result = client.stream_generate_content(
344
+ { contents: { role: 'user', parts: { text: 'hi!' } } }
345
+ ) do |event, parsed, raw|
346
+ puts event
347
+ end
348
+ ```
349
+
350
+ #### generate_content
351
+
352
+ ```ruby
353
+ result = client.generate_content(
354
+ { contents: { role: 'user', parts: { text: 'hi!' } } }
355
+ )
356
+ ```
357
+
358
+ Result:
359
+ ```ruby
360
+ { 'candidates' =>
361
+ [{ 'content' => { 'parts' => [{ 'text' => 'Hello! How can I assist you today?' }], 'role' => 'model' },
362
+ 'finishReason' => 'STOP',
363
+ 'index' => 0,
364
+ 'safetyRatings' =>
365
+ [{ 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'probability' => 'NEGLIGIBLE' },
366
+ { 'category' => 'HARM_CATEGORY_HATE_SPEECH', 'probability' => 'NEGLIGIBLE' },
367
+ { 'category' => 'HARM_CATEGORY_HARASSMENT', 'probability' => 'NEGLIGIBLE' },
368
+ { 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT', 'probability' => 'NEGLIGIBLE' }] }],
369
+ 'promptFeedback' =>
370
+ { 'safetyRatings' =>
371
+ [{ 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'probability' => 'NEGLIGIBLE' },
372
+ { 'category' => 'HARM_CATEGORY_HATE_SPEECH', 'probability' => 'NEGLIGIBLE' },
373
+ { 'category' => 'HARM_CATEGORY_HARASSMENT', 'probability' => 'NEGLIGIBLE' },
374
+ { 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT', 'probability' => 'NEGLIGIBLE' }] } }
375
+ ```
376
+
377
+ As of the writing of this README, only the `generative-language-api` service supports the `generate_content` method; `vertex-ai-api` does not.
378
+
379
+ ### Modes
380
+
381
+ #### Text
279
382
 
280
383
  ```ruby
281
384
  result = client.stream_generate_content({
@@ -303,7 +406,7 @@ Result:
303
406
  } }]
304
407
  ```
305
408
 
306
- ##### Image
409
+ #### Image
307
410
 
308
411
  ![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
412
 
@@ -314,7 +417,7 @@ Switch to the `gemini-pro-vision` model:
314
417
  ```ruby
315
418
  client = Gemini.new(
316
419
  credentials: { service: 'vertex-ai-api', region: 'us-east4' },
317
- options: { model: 'gemini-pro-vision', stream: true }
420
+ options: { model: 'gemini-pro-vision', server_sent_events: true }
318
421
  )
319
422
  ```
320
423
 
@@ -360,7 +463,7 @@ The result:
360
463
  'usageMetadata' => { 'promptTokenCount' => 263, 'candidatesTokenCount' => 50, 'totalTokenCount' => 313 } }]
361
464
  ```
362
465
 
363
- ##### Video
466
+ #### Video
364
467
 
365
468
  https://gist.github.com/assets/29520/f82bccbf-02d2-4899-9c48-eb8a0a5ef741
366
469
 
@@ -373,7 +476,7 @@ Switch to the `gemini-pro-vision` model:
373
476
  ```ruby
374
477
  client = Gemini.new(
375
478
  credentials: { service: 'vertex-ai-api', region: 'us-east4' },
376
- options: { model: 'gemini-pro-vision', stream: true }
479
+ options: { model: 'gemini-pro-vision', server_sent_events: true }
377
480
  )
378
481
  ```
379
482
 
@@ -420,41 +523,15 @@ The result:
420
523
  "usageMetadata"=>{"promptTokenCount"=>1037, "candidatesTokenCount"=>32, "totalTokenCount"=>1069}}]
421
524
  ```
422
525
 
423
- #### Synchronous
424
-
425
- ```ruby
426
- result = client.stream_generate_content({
427
- contents: { role: 'user', parts: { text: 'hi!' } }
428
- })
429
- ```
430
-
431
- Result:
432
- ```ruby
433
- [{ 'candidates' =>
434
- [{ 'content' => {
435
- 'role' => 'model',
436
- 'parts' => [{ 'text' => 'Hello! How may I assist you?' }]
437
- },
438
- 'finishReason' => 'STOP',
439
- 'safetyRatings' =>
440
- [{ 'category' => 'HARM_CATEGORY_HARASSMENT', 'probability' => 'NEGLIGIBLE' },
441
- { 'category' => 'HARM_CATEGORY_HATE_SPEECH', 'probability' => 'NEGLIGIBLE' },
442
- { 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'probability' => 'NEGLIGIBLE' },
443
- { 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT', 'probability' => 'NEGLIGIBLE' }] }],
444
- 'usageMetadata' => {
445
- 'promptTokenCount' => 2,
446
- 'candidatesTokenCount' => 8,
447
- 'totalTokenCount' => 10
448
- } }]
449
- ```
526
+ ### Streaming vs. Server-Sent Events (SSE)
450
527
 
451
- #### Streaming
528
+ [Server-Sent Events (SSE)](https://en.wikipedia.org/wiki/Server-sent_events) is a technology that allows certain endpoints to offer streaming capabilities, such as creating the impression that "the model is typing along with you," rather than delivering the entire answer all at once.
452
529
 
453
- You can set up the client to use streaming for all supported endpoints:
530
+ You can set up the client to use Server-Sent Events (SSE) for all supported endpoints:
454
531
  ```ruby
455
532
  client = Gemini.new(
456
533
  credentials: { ... },
457
- options: { model: 'gemini-pro', stream: true }
534
+ options: { model: 'gemini-pro', server_sent_events: true }
458
535
  )
459
536
  ```
460
537
 
@@ -462,11 +539,11 @@ Or, you can decide on a request basis:
462
539
  ```ruby
463
540
  client.stream_generate_content(
464
541
  { contents: { role: 'user', parts: { text: 'hi!' } } },
465
- stream: true
542
+ server_sent_events: true
466
543
  )
467
544
  ```
468
545
 
469
- With streaming enabled, you can use a block to receive the results:
546
+ With Server-Sent Events (SSE) enabled, you can use a block to receive partial results via events. This feature is particularly useful for methods that offer streaming capabilities, such as `stream_generate_content`:
470
547
 
471
548
  ```ruby
472
549
  client.stream_generate_content(
@@ -496,14 +573,16 @@ Event:
496
573
  } }
497
574
  ```
498
575
 
499
- #### Streaming Hang
576
+ Even though streaming methods utilize Server-Sent Events (SSE), using this feature doesn't necessarily mean streaming data. For example, when `generate_content` is called with SSE enabled, you will receive all the data at once in a single event, rather than through multiple partial events. This occurs because `generate_content` isn't designed for streaming, even though it is capable of utilizing Server-Sent Events.
577
+
578
+ #### Server-Sent Events (SSE) Hang
500
579
 
501
- Method calls will _hang_ until the stream finishes, so even without providing a block, you can get the final results of the stream events:
580
+ Method calls will _hang_ until the server-sent events finish, so even without providing a block, you can obtain the final results of the received events:
502
581
 
503
582
  ```ruby
504
583
  result = client.stream_generate_content(
505
584
  { contents: { role: 'user', parts: { text: 'hi!' } } },
506
- stream: true
585
+ server_sent_events: true
507
586
  )
508
587
  ```
509
588
 
@@ -527,7 +606,40 @@ Result:
527
606
  } }]
528
607
  ```
529
608
 
530
- #### Back-and-Forth Conversations
609
+ #### Non-Streaming
610
+
611
+ Depending on the service, you can use the [`generate_content`](#generate_content) method, which does not stream the answer.
612
+
613
+ You can also use methods designed for streaming without necessarily processing partial events; instead, you can wait for the result of all received events:
614
+
615
+ ```ruby
616
+ result = client.stream_generate_content({
617
+ contents: { role: 'user', parts: { text: 'hi!' } },
618
+ server_sent_events: false
619
+ })
620
+ ```
621
+
622
+ Result:
623
+ ```ruby
624
+ [{ 'candidates' =>
625
+ [{ 'content' => {
626
+ 'role' => 'model',
627
+ 'parts' => [{ 'text' => 'Hello! How may I assist you?' }]
628
+ },
629
+ 'finishReason' => 'STOP',
630
+ 'safetyRatings' =>
631
+ [{ 'category' => 'HARM_CATEGORY_HARASSMENT', 'probability' => 'NEGLIGIBLE' },
632
+ { 'category' => 'HARM_CATEGORY_HATE_SPEECH', 'probability' => 'NEGLIGIBLE' },
633
+ { 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'probability' => 'NEGLIGIBLE' },
634
+ { 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT', 'probability' => 'NEGLIGIBLE' }] }],
635
+ 'usageMetadata' => {
636
+ 'promptTokenCount' => 2,
637
+ 'candidatesTokenCount' => 8,
638
+ 'totalTokenCount' => 10
639
+ } }]
640
+ ```
641
+
642
+ ### Back-and-Forth Conversations
531
643
 
532
644
  To maintain a back-and-forth conversation, you need to append the received responses and build a history for your requests:
533
645
 
@@ -569,7 +681,7 @@ Result:
569
681
  } }]
570
682
  ```
571
683
 
572
- #### Tools (Functions) Calling
684
+ ### Tools (Functions) Calling
573
685
 
574
686
  > 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.
575
687
 
@@ -725,6 +837,42 @@ result = client.request(
725
837
  )
726
838
  ```
727
839
 
840
+ ### Request Options
841
+
842
+ #### Timeout
843
+
844
+ You can set the maximum number of seconds to wait for the request to complete with the `timeout` option:
845
+
846
+ ```ruby
847
+ client = Gemini.new(
848
+ credentials: { service: 'vertex-ai-api', region: 'us-east4' },
849
+ options: {
850
+ model: 'gemini-pro',
851
+ connection: { request: { timeout: 5 } }
852
+ }
853
+ )
854
+ ```
855
+
856
+ You can also have more fine-grained control over [Faraday's Request Options](https://lostisland.github.io/faraday/#/customization/request-options?id=request-options) if you prefer:
857
+
858
+ ```ruby
859
+ client = Gemini.new(
860
+ credentials: { service: 'vertex-ai-api', region: 'us-east4' },
861
+ options: {
862
+ model: 'gemini-pro',
863
+ connection: {
864
+ request: {
865
+ timeout: 5,
866
+ open_timeout: 5,
867
+ read_timeout: 5,
868
+ write_timeout: 5
869
+ }
870
+ }
871
+ }
872
+ )
873
+ ```
874
+
875
+
728
876
  ### Error Handling
729
877
 
730
878
  #### Rescuing
@@ -772,7 +920,7 @@ GeminiError
772
920
 
773
921
  MissingProjectIdError
774
922
  UnsupportedServiceError
775
- BlockWithoutStreamError
923
+ BlockWithoutServerSentEventsError
776
924
 
777
925
  RequestError
778
926
  ```
@@ -795,7 +943,7 @@ gem build gemini-ai.gemspec
795
943
 
796
944
  gem signin
797
945
 
798
- gem push gemini-ai-2.2.0.gem
946
+ gem push gemini-ai-3.1.0.gem
799
947
  ```
800
948
 
801
949
  ### 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.2.0
4
+ version: 3.1.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-16 00:00:00.000000000 Z
11
+ date: 2023-12-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: event_stream_parser
@@ -30,20 +30,20 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '2.7'
33
+ version: '2.8'
34
34
  - - ">="
35
35
  - !ruby/object:Gem::Version
36
- version: 2.7.12
36
+ version: 2.8.1
37
37
  type: :runtime
38
38
  prerelease: false
39
39
  version_requirements: !ruby/object:Gem::Requirement
40
40
  requirements:
41
41
  - - "~>"
42
42
  - !ruby/object:Gem::Version
43
- version: '2.7'
43
+ version: '2.8'
44
44
  - - ">="
45
45
  - !ruby/object:Gem::Version
46
- version: 2.7.12
46
+ version: 2.8.1
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: googleauth
49
49
  requirement: !ruby/object:Gem::Requirement