gemini-ai 2.2.0 → 3.0.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: 80621f6cd2de526141e994a339d645b53492f6ece960955bc56f2e7430be1d0c
4
+ data.tar.gz: b627386a0dacc899112e0b08806f63b571dd980f2fb749df7681d7c70656d707
5
5
  SHA512:
6
- metadata.gz: 0ca1f3f87c61276902259d937f4d57f324a756d1e63c1e5781680ba970313f3c3c29a2da49c4eb1ec0bf351f984bee378e86b22d1656b2423638e1a70bd5dddf
7
- data.tar.gz: 50988d0881d37f561e0c75ac1beea4fc9b34c757d0f16fb2dcb2f63d3e63f867194e0f826800c8429ce9f33500dd74c3fa4c5d747a0975378c81cf3e0ad4a61b
6
+ metadata.gz: f5425c3239dac6eca23cccdb2483f90bea23500b0830b865e8acc0fe235cf2ad3cb001d110e7a6ec0cbb59ed5c2f65e74f883edef961cb675b76e7d6117d88fc
7
+ data.tar.gz: 041122dc5a6d1d596411ea697503aa8d7846b4d2bc6c493b8b7d9102033c9ef3226957340d8caeb443751f9f4f465624a80ef1749e0f5893833e4c72d43ba907
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- gemini-ai (2.2.0)
4
+ gemini-ai (3.0.0)
5
5
  event_stream_parser (~> 1.0)
6
6
  faraday (~> 2.7, >= 2.7.12)
7
7
  googleauth (~> 1.9, >= 1.9.1)
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.0.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,16 +81,20 @@ 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)
95
99
  - [Error Handling](#error-handling)
96
100
  - [Rescuing](#rescuing)
@@ -108,11 +112,11 @@ Result:
108
112
  ### Installing
109
113
 
110
114
  ```sh
111
- gem install gemini-ai -v 2.2.0
115
+ gem install gemini-ai -v 3.0.0
112
116
  ```
113
117
 
114
118
  ```sh
115
- gem 'gemini-ai', '~> 2.2.0'
119
+ gem 'gemini-ai', '~> 3.0.0'
116
120
  ```
117
121
 
118
122
  ### Credentials
@@ -279,7 +283,7 @@ client = Gemini.new(
279
283
  service: 'generative-language-api',
280
284
  api_key: ENV['GOOGLE_API_KEY']
281
285
  },
282
- options: { model: 'gemini-pro', stream: false }
286
+ options: { model: 'gemini-pro', server_sent_events: true }
283
287
  )
284
288
 
285
289
  # With a Service Account Credentials File
@@ -289,7 +293,7 @@ client = Gemini.new(
289
293
  file_path: 'google-credentials.json',
290
294
  region: 'us-east4'
291
295
  },
292
- options: { model: 'gemini-pro', stream: false }
296
+ options: { model: 'gemini-pro', server_sent_events: true }
293
297
  )
294
298
 
295
299
  # With Application Default Credentials
@@ -298,15 +302,118 @@ client = Gemini.new(
298
302
  service: 'vertex-ai-api',
299
303
  region: 'us-east4'
300
304
  },
301
- options: { model: 'gemini-pro', stream: false }
305
+ options: { model: 'gemini-pro', server_sent_events: true }
302
306
  )
303
307
  ```
304
308
 
305
- ### Generate Content
309
+ ### Methods
306
310
 
307
- #### Modes
311
+ #### stream_generate_content
308
312
 
309
- ##### Text
313
+ ##### Receiving Stream Events
314
+
315
+ Ensure that you have enabled [Server-Sent Events](#streaming-vs-server-sent-events-sse) before using blocks for streaming:
316
+
317
+ ```ruby
318
+ client.stream_generate_content(
319
+ { contents: { role: 'user', parts: { text: 'hi!' } } }
320
+ ) do |event, parsed, raw|
321
+ puts event
322
+ end
323
+ ```
324
+
325
+ Event:
326
+ ```ruby
327
+ { 'candidates' =>
328
+ [{ 'content' => {
329
+ 'role' => 'model',
330
+ 'parts' => [{ 'text' => 'Hello! How may I assist you?' }]
331
+ },
332
+ 'finishReason' => 'STOP',
333
+ 'safetyRatings' =>
334
+ [{ 'category' => 'HARM_CATEGORY_HARASSMENT', 'probability' => 'NEGLIGIBLE' },
335
+ { 'category' => 'HARM_CATEGORY_HATE_SPEECH', 'probability' => 'NEGLIGIBLE' },
336
+ { 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'probability' => 'NEGLIGIBLE' },
337
+ { 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT', 'probability' => 'NEGLIGIBLE' }] }],
338
+ 'usageMetadata' => {
339
+ 'promptTokenCount' => 2,
340
+ 'candidatesTokenCount' => 8,
341
+ 'totalTokenCount' => 10
342
+ } }
343
+ ```
344
+
345
+ ##### Without Events
346
+
347
+ You can use `stream_generate_content` without events:
348
+
349
+ ```ruby
350
+ result = client.stream_generate_content(
351
+ { contents: { role: 'user', parts: { text: 'hi!' } } }
352
+ )
353
+ ```
354
+
355
+ In this case, the result will be an array with all the received events:
356
+
357
+ ```ruby
358
+ [{ 'candidates' =>
359
+ [{ 'content' => {
360
+ 'role' => 'model',
361
+ 'parts' => [{ 'text' => 'Hello! How may I assist you?' }]
362
+ },
363
+ 'finishReason' => 'STOP',
364
+ 'safetyRatings' =>
365
+ [{ 'category' => 'HARM_CATEGORY_HARASSMENT', 'probability' => 'NEGLIGIBLE' },
366
+ { 'category' => 'HARM_CATEGORY_HATE_SPEECH', 'probability' => 'NEGLIGIBLE' },
367
+ { 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'probability' => 'NEGLIGIBLE' },
368
+ { 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT', 'probability' => 'NEGLIGIBLE' }] }],
369
+ 'usageMetadata' => {
370
+ 'promptTokenCount' => 2,
371
+ 'candidatesTokenCount' => 8,
372
+ 'totalTokenCount' => 10
373
+ } }]
374
+ ```
375
+
376
+ You can mix both as well:
377
+ ```ruby
378
+ result = client.stream_generate_content(
379
+ { contents: { role: 'user', parts: { text: 'hi!' } } }
380
+ ) do |event, parsed, raw|
381
+ puts event
382
+ end
383
+ ```
384
+
385
+ #### generate_content
386
+
387
+ ```ruby
388
+ result = client.generate_content(
389
+ { contents: { role: 'user', parts: { text: 'hi!' } } }
390
+ )
391
+ ```
392
+
393
+ Result:
394
+ ```ruby
395
+ { 'candidates' =>
396
+ [{ 'content' => { 'parts' => [{ 'text' => 'Hello! How can I assist you today?' }], 'role' => 'model' },
397
+ 'finishReason' => 'STOP',
398
+ 'index' => 0,
399
+ 'safetyRatings' =>
400
+ [{ 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'probability' => 'NEGLIGIBLE' },
401
+ { 'category' => 'HARM_CATEGORY_HATE_SPEECH', 'probability' => 'NEGLIGIBLE' },
402
+ { 'category' => 'HARM_CATEGORY_HARASSMENT', 'probability' => 'NEGLIGIBLE' },
403
+ { 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT', 'probability' => 'NEGLIGIBLE' }] }],
404
+ 'promptFeedback' =>
405
+ { 'safetyRatings' =>
406
+ [{ 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'probability' => 'NEGLIGIBLE' },
407
+ { 'category' => 'HARM_CATEGORY_HATE_SPEECH', 'probability' => 'NEGLIGIBLE' },
408
+ { 'category' => 'HARM_CATEGORY_HARASSMENT', 'probability' => 'NEGLIGIBLE' },
409
+ { 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT', 'probability' => 'NEGLIGIBLE' }] } }
410
+ ```
411
+
412
+ As of the writing of this README, only the `generative-language-api` service supports the `generate_content` method; `vertex-ai-api` does not.
413
+
414
+ ### Modes
415
+
416
+ #### Text
310
417
 
311
418
  ```ruby
312
419
  result = client.stream_generate_content({
@@ -334,7 +441,7 @@ Result:
334
441
  } }]
335
442
  ```
336
443
 
337
- ##### Image
444
+ #### Image
338
445
 
339
446
  ![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
447
 
@@ -345,7 +452,7 @@ Switch to the `gemini-pro-vision` model:
345
452
  ```ruby
346
453
  client = Gemini.new(
347
454
  credentials: { service: 'vertex-ai-api', region: 'us-east4' },
348
- options: { model: 'gemini-pro-vision', stream: true }
455
+ options: { model: 'gemini-pro-vision', server_sent_events: true }
349
456
  )
350
457
  ```
351
458
 
@@ -391,7 +498,7 @@ The result:
391
498
  'usageMetadata' => { 'promptTokenCount' => 263, 'candidatesTokenCount' => 50, 'totalTokenCount' => 313 } }]
392
499
  ```
393
500
 
394
- ##### Video
501
+ #### Video
395
502
 
396
503
  https://gist.github.com/assets/29520/f82bccbf-02d2-4899-9c48-eb8a0a5ef741
397
504
 
@@ -404,7 +511,7 @@ Switch to the `gemini-pro-vision` model:
404
511
  ```ruby
405
512
  client = Gemini.new(
406
513
  credentials: { service: 'vertex-ai-api', region: 'us-east4' },
407
- options: { model: 'gemini-pro-vision', stream: true }
514
+ options: { model: 'gemini-pro-vision', server_sent_events: true }
408
515
  )
409
516
  ```
410
517
 
@@ -451,41 +558,15 @@ The result:
451
558
  "usageMetadata"=>{"promptTokenCount"=>1037, "candidatesTokenCount"=>32, "totalTokenCount"=>1069}}]
452
559
  ```
453
560
 
454
- #### Synchronous
561
+ ### Streaming vs. Server-Sent Events (SSE)
455
562
 
456
- ```ruby
457
- result = client.stream_generate_content({
458
- contents: { role: 'user', parts: { text: 'hi!' } }
459
- })
460
- ```
563
+ [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.
461
564
 
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
- ```
481
-
482
- #### Streaming
483
-
484
- You can set up the client to use streaming for all supported endpoints:
565
+ You can set up the client to use Server-Sent Events (SSE) for all supported endpoints:
485
566
  ```ruby
486
567
  client = Gemini.new(
487
568
  credentials: { ... },
488
- options: { model: 'gemini-pro', stream: true }
569
+ options: { model: 'gemini-pro', server_sent_events: true }
489
570
  )
490
571
  ```
491
572
 
@@ -493,11 +574,11 @@ Or, you can decide on a request basis:
493
574
  ```ruby
494
575
  client.stream_generate_content(
495
576
  { contents: { role: 'user', parts: { text: 'hi!' } } },
496
- stream: true
577
+ server_sent_events: true
497
578
  )
498
579
  ```
499
580
 
500
- With streaming enabled, you can use a block to receive the results:
581
+ 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
582
 
502
583
  ```ruby
503
584
  client.stream_generate_content(
@@ -527,14 +608,16 @@ Event:
527
608
  } }
528
609
  ```
529
610
 
530
- #### Streaming Hang
611
+ 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.
531
612
 
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:
613
+ #### Server-Sent Events (SSE) Hang
614
+
615
+ 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
616
 
534
617
  ```ruby
535
618
  result = client.stream_generate_content(
536
619
  { contents: { role: 'user', parts: { text: 'hi!' } } },
537
- stream: true
620
+ server_sent_events: true
538
621
  )
539
622
  ```
540
623
 
@@ -558,7 +641,40 @@ Result:
558
641
  } }]
559
642
  ```
560
643
 
561
- #### Back-and-Forth Conversations
644
+ #### Non-Streaming
645
+
646
+ Depending on the service, you can use the [`generate_content`](#generate_content) method, which does not stream the answer.
647
+
648
+ You can also use methods designed for streaming without necessarily processing partial events; instead, you can wait for the result of all received events:
649
+
650
+ ```ruby
651
+ result = client.stream_generate_content({
652
+ contents: { role: 'user', parts: { text: 'hi!' } },
653
+ server_sent_events: false
654
+ })
655
+ ```
656
+
657
+ Result:
658
+ ```ruby
659
+ [{ 'candidates' =>
660
+ [{ 'content' => {
661
+ 'role' => 'model',
662
+ 'parts' => [{ 'text' => 'Hello! How may I assist you?' }]
663
+ },
664
+ 'finishReason' => 'STOP',
665
+ 'safetyRatings' =>
666
+ [{ 'category' => 'HARM_CATEGORY_HARASSMENT', 'probability' => 'NEGLIGIBLE' },
667
+ { 'category' => 'HARM_CATEGORY_HATE_SPEECH', 'probability' => 'NEGLIGIBLE' },
668
+ { 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'probability' => 'NEGLIGIBLE' },
669
+ { 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT', 'probability' => 'NEGLIGIBLE' }] }],
670
+ 'usageMetadata' => {
671
+ 'promptTokenCount' => 2,
672
+ 'candidatesTokenCount' => 8,
673
+ 'totalTokenCount' => 10
674
+ } }]
675
+ ```
676
+
677
+ ### Back-and-Forth Conversations
562
678
 
563
679
  To maintain a back-and-forth conversation, you need to append the received responses and build a history for your requests:
564
680
 
@@ -600,7 +716,7 @@ Result:
600
716
  } }]
601
717
  ```
602
718
 
603
- #### Tools (Functions) Calling
719
+ ### Tools (Functions) Calling
604
720
 
605
721
  > 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
722
 
@@ -803,7 +919,7 @@ GeminiError
803
919
 
804
920
  MissingProjectIdError
805
921
  UnsupportedServiceError
806
- BlockWithoutStreamError
922
+ BlockWithoutServerSentEventsError
807
923
 
808
924
  RequestError
809
925
  ```
@@ -826,7 +942,7 @@ gem build gemini-ai.gemspec
826
942
 
827
943
  gem signin
828
944
 
829
- gem push gemini-ai-2.2.0.gem
945
+ gem push gemini-ai-3.0.0.gem
830
946
  ```
831
947
 
832
948
  ### 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,7 +5,7 @@ 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
@@ -26,43 +26,50 @@ module Gemini
26
26
  end
27
27
 
28
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
29
+ @project_id = config[:credentials][:project_id] || @authorizer.project_id || @authorizer.quota_project_id
34
30
 
35
31
  raise MissingProjectIdError, 'Could not determine project_id, which is required.' if @project_id.nil?
36
32
  end
37
33
 
38
- @address = case config[:credentials][:service]
34
+ @service = config[:credentials][:service]
35
+
36
+ @address = case @service
39
37
  when 'vertex-ai-api'
40
38
  "https://#{config[:credentials][:region]}-aiplatform.googleapis.com/v1/projects/#{@project_id}/locations/#{config[:credentials][:region]}/publishers/google/models/#{config[:options][:model]}"
41
39
  when 'generative-language-api'
42
40
  "https://generativelanguage.googleapis.com/v1/models/#{config[:options][:model]}"
43
41
  else
44
- raise UnsupportedServiceError, "Unsupported service: #{config[:credentials][:service]}"
42
+ raise UnsupportedServiceError, "Unsupported service: #{@service}"
45
43
  end
46
44
 
47
- @stream = config[:options][:stream]
45
+ @server_sent_events = config[:options][:server_sent_events]
46
+ end
47
+
48
+ def stream_generate_content(payload, server_sent_events: nil, &callback)
49
+ request('streamGenerateContent', payload, server_sent_events:, &callback)
48
50
  end
49
51
 
50
- def stream_generate_content(payload, stream: nil, &callback)
51
- request('streamGenerateContent', payload, stream:, &callback)
52
+ def generate_content(payload, server_sent_events: nil, &callback)
53
+ result = request('generateContent', payload, server_sent_events:, &callback)
54
+
55
+ return result.first if result.is_a?(Array) && result.size == 1
56
+
57
+ result
52
58
  end
53
59
 
54
- def request(path, payload, stream: nil, &callback)
55
- stream_enabled = stream.nil? ? @stream : stream
60
+ def request(path, payload, server_sent_events: nil, &callback)
61
+ server_sent_events_enabled = server_sent_events.nil? ? @server_sent_events : server_sent_events
56
62
  url = "#{@address}:#{path}"
57
63
  params = []
58
64
 
59
- params << 'alt=sse' if stream_enabled
65
+ params << 'alt=sse' if server_sent_events_enabled
60
66
  params << "key=#{@api_key}" if @authentication == :api_key
61
67
 
62
68
  url += "?#{params.join('&')}" if params.size.positive?
63
69
 
64
- if !callback.nil? && !stream_enabled
65
- raise BlockWithoutStreamError, 'You are trying to use a block without stream enabled.'
70
+ if !callback.nil? && !server_sent_events_enabled
71
+ raise BlockWithoutServerSentEventsError,
72
+ 'You are trying to use a block without Server Sent Events (SSE) enabled.'
66
73
  end
67
74
 
68
75
  results = []
@@ -78,7 +85,7 @@ module Gemini
78
85
 
79
86
  request.body = payload.to_json
80
87
 
81
- if stream_enabled
88
+ if server_sent_events_enabled
82
89
  parser = EventStreamParser::Parser.new
83
90
 
84
91
  request.options.on_data = proc do |chunk, bytes, env|
@@ -107,7 +114,7 @@ module Gemini
107
114
  end
108
115
  end
109
116
 
110
- return safe_parse_json(response.body) unless stream_enabled
117
+ return safe_parse_json(response.body) unless server_sent_events_enabled
111
118
 
112
119
  results.map { |result| result[:event] }
113
120
  rescue Faraday::ServerError => e
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.0.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.0.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.0.0
81
81
  ```
82
82
 
83
83
  ```sh
84
- gem 'gemini-ai', '~> 2.2.0'
84
+ gem 'gemini-ai', '~> 3.0.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
526
+ ### Streaming vs. Server-Sent Events (SSE)
424
527
 
425
- ```ruby
426
- result = client.stream_generate_content({
427
- contents: { role: 'user', parts: { text: 'hi!' } }
428
- })
429
- ```
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.
430
529
 
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
- ```
450
-
451
- #### Streaming
452
-
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.
500
577
 
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:
578
+ #### Server-Sent Events (SSE) Hang
579
+
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
 
@@ -772,7 +884,7 @@ GeminiError
772
884
 
773
885
  MissingProjectIdError
774
886
  UnsupportedServiceError
775
- BlockWithoutStreamError
887
+ BlockWithoutServerSentEventsError
776
888
 
777
889
  RequestError
778
890
  ```
@@ -795,7 +907,7 @@ gem build gemini-ai.gemspec
795
907
 
796
908
  gem signin
797
909
 
798
- gem push gemini-ai-2.2.0.gem
910
+ gem push gemini-ai-3.0.0.gem
799
911
  ```
800
912
 
801
913
  ### 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.0.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-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: event_stream_parser
@@ -109,7 +109,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
109
109
  - !ruby/object:Gem::Version
110
110
  version: '0'
111
111
  requirements: []
112
- rubygems_version: 3.3.3
112
+ rubygems_version: 3.4.22
113
113
  signing_key:
114
114
  specification_version: 4
115
115
  summary: Interact with Google's Gemini AI.