geminai 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +115 -0
- data/lib/geminai/client.rb +93 -0
- data/lib/geminai/grounding_metadata.rb +15 -0
- data/lib/geminai/interaction.rb +97 -0
- data/lib/geminai/step.rb +31 -0
- data/lib/geminai/version.rb +3 -0
- data/lib/geminai.rb +12 -0
- metadata +48 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9d805349d2b5e83401e3d9e299d237210d3bc758985d42e5308b1302f8e436c6
|
|
4
|
+
data.tar.gz: '0887d0adca55f7ec97bc12932be5e523ab0859dce30ecbb0d42b99625ce2f121'
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1b9bc352fc5c5222b4ed7d2236e2a7299de81676344175d8d975384025d62705c10312013ee3874dc401e3204ba1006f20a0716be9fa48331bf74d8f5cace6db
|
|
7
|
+
data.tar.gz: 000f7985bf738b8dfb0f2f13f72260f8e179b78c6bdb12255309c6c80ed310003e3fa8367b0a0658db52eaba6f2541f20c85c9ea26751628f27167f81472f350
|
data/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# geminai
|
|
2
|
+
|
|
3
|
+
A gem for gemini's interactions api
|
|
4
|
+
|
|
5
|
+
* **Built-in Tools**: Native search grounding (`google_search`), mapping citations, and suggestions.
|
|
6
|
+
* **Image Generation**: High-fidelity native image generation (`gemini-3.1-flash-image`) with direct config like `aspect_ratio` and `image_size`.
|
|
7
|
+
* **Stateful Conversations**: Seamless multi-turn conversations managed server-side via `previous_interaction_id`.
|
|
8
|
+
* **Stateless Conversations**: Client-managed conversation paths using `store: false`.
|
|
9
|
+
* **Zero Dependencies**: Pure Ruby standard library (`net/http` and `json`)
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
# Gemfile
|
|
15
|
+
gem 'geminai'
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Setup
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
require 'geminai'
|
|
22
|
+
|
|
23
|
+
ai = Geminai.new(ENV['GEMINI_API_KEY'])
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Usage Examples
|
|
29
|
+
|
|
30
|
+
### 1. Google Search
|
|
31
|
+
|
|
32
|
+
Geminai enables Google Search by passing the `google_search` tool
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
prompt = "what is the latest news on alphabet's stock price?"
|
|
36
|
+
|
|
37
|
+
interaction = client.interact(
|
|
38
|
+
model: 'gemini-3.5-flash',
|
|
39
|
+
input: prompt,
|
|
40
|
+
tools: [{ type: 'google_search' }],
|
|
41
|
+
store: false # Operate statelessly
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# 1. Output the generation
|
|
45
|
+
puts interaction.output_text
|
|
46
|
+
|
|
47
|
+
# 2. Extract grounding metadata
|
|
48
|
+
metadata = interaction.grounding_metadata
|
|
49
|
+
|
|
50
|
+
puts "\nWeb Search Queries Run:"
|
|
51
|
+
p metadata.web_search_queries
|
|
52
|
+
# => ["Bloomberg", ...]
|
|
53
|
+
|
|
54
|
+
puts "\nWeb Citations:"
|
|
55
|
+
metadata.citations.each do |citation|
|
|
56
|
+
puts "- #{citation[:title]} @ #{citation[:url]} (indexes: #{citation[:start_index]}..#{citation[:end_index]})"
|
|
57
|
+
end
|
|
58
|
+
# => - bloomberge.com @ https://vertexaisearch.cloud.google.com/grounding-api-redirect/...
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 2. Image Generation
|
|
62
|
+
|
|
63
|
+
Geminai includes a helper method `generate_image` which calls the Interactions API specifying `type: "image"` under `response_format`.
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
require 'base64'
|
|
67
|
+
|
|
68
|
+
interaction = client.generate_image(
|
|
69
|
+
"An image of a pelican riding a bicycle",
|
|
70
|
+
model: 'gemini-3.1-flash-image',
|
|
71
|
+
aspect_ratio: '1:1',
|
|
72
|
+
image_size: '1K',
|
|
73
|
+
store: false
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Extract and save generated images
|
|
77
|
+
interaction.output_images.each_with_index do |img, index|
|
|
78
|
+
File.binwrite("pelican#{index}.png", Base64.decode64(img[:data]))
|
|
79
|
+
puts "Saved pelican_#{index}.png"
|
|
80
|
+
end
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 3. Multi-turn Stateful Conversations
|
|
84
|
+
|
|
85
|
+
The Interactions API handles conversation history server-side. To continue a thread, pass `store: true` on your turns and feed the `id` of the previous interaction back into your subsequent requests using `previous_interaction_id`.
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
# Turn 1: Save the context on the server
|
|
89
|
+
interaction_1 = client.interact(
|
|
90
|
+
model: 'gemini-3.5-flash',
|
|
91
|
+
input: 'My favorite jihad is a butlerian jihad',
|
|
92
|
+
store: true
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Turn 2: Query context referencing the first interaction ID
|
|
96
|
+
interaction_2 = client.interact(
|
|
97
|
+
model: 'gemini-3.5-flash',
|
|
98
|
+
input: 'What is my favorite jihad?',
|
|
99
|
+
previous_interaction_id: interaction_1.id,
|
|
100
|
+
store: true
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
puts interaction_2.output_text
|
|
104
|
+
# => "Your favorite jihad is a butlerian jihad."
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Running Tests
|
|
110
|
+
|
|
111
|
+
To run the integration tests (which execute live requests to the API), verify your `GEMINI_API_KEY` is present in your environment and run:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
ruby -Ilib test/geminai_test.rb
|
|
115
|
+
```
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
require "net/http"
|
|
2
|
+
require "uri"
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Geminai
|
|
6
|
+
class Client
|
|
7
|
+
attr_reader :api_key, :base_url
|
|
8
|
+
|
|
9
|
+
def initialize(api_key: nil, base_url: nil)
|
|
10
|
+
@api_key = api_key || ENV["GEMINI_API_KEY"]
|
|
11
|
+
@base_url = base_url || "https://generativelanguage.googleapis.com"
|
|
12
|
+
|
|
13
|
+
if @api_key.nil? || @api_key.empty?
|
|
14
|
+
raise ArgumentError, "API Key is required"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def interact(model:, input:, **options)
|
|
19
|
+
uri = URI.parse("#{@base_url}/v1beta/interactions")
|
|
20
|
+
|
|
21
|
+
body = {
|
|
22
|
+
model: model,
|
|
23
|
+
input: input
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# Forward all recognized interactions API request parameters
|
|
27
|
+
[
|
|
28
|
+
:system_instruction,
|
|
29
|
+
:generation_config,
|
|
30
|
+
:response_format,
|
|
31
|
+
:tools,
|
|
32
|
+
:previous_interaction_id,
|
|
33
|
+
:stream,
|
|
34
|
+
:store
|
|
35
|
+
].each do |opt|
|
|
36
|
+
body[opt] = options[opt] if options.key?(opt)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Handle any extra keys passed in options as well to be fully future-proof
|
|
40
|
+
options.each do |k, v|
|
|
41
|
+
next if body.key?(k)
|
|
42
|
+
body[k] = v
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
46
|
+
http.use_ssl = (uri.scheme == "https")
|
|
47
|
+
|
|
48
|
+
# Net::HTTP defaults read timeout is 60s, but search grounding or image generation can take longer,
|
|
49
|
+
# let's set a generous timeout.
|
|
50
|
+
http.read_timeout = 120
|
|
51
|
+
|
|
52
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
|
53
|
+
request["Content-Type"] = "application/json"
|
|
54
|
+
request["x-goog-api-key"] = @api_key
|
|
55
|
+
request.body = JSON.generate(body)
|
|
56
|
+
|
|
57
|
+
response = http.request(request)
|
|
58
|
+
|
|
59
|
+
unless response.code == "200"
|
|
60
|
+
raise(
|
|
61
|
+
ApiError.new("API Request failed with code #{response.code}: #{response.body}", response.code, response.body)
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
data = JSON.parse(response.body, symbolize_names: true)
|
|
66
|
+
Interaction.new(data)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Helper method for image generation using response_format
|
|
70
|
+
def generate_image(prompt, model:, aspect_ratio: "1:1", image_size: "1K", **options)
|
|
71
|
+
interact(
|
|
72
|
+
model: model,
|
|
73
|
+
input: prompt,
|
|
74
|
+
response_format: {
|
|
75
|
+
type: "image",
|
|
76
|
+
aspect_ratio: aspect_ratio,
|
|
77
|
+
image_size: image_size
|
|
78
|
+
},
|
|
79
|
+
**options
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
class ApiError < StandardError
|
|
85
|
+
attr_reader :code, :response_body
|
|
86
|
+
|
|
87
|
+
def initialize(message, code = nil, response_body = nil)
|
|
88
|
+
super(message)
|
|
89
|
+
@code = code
|
|
90
|
+
@response_body = response_body
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Geminai
|
|
2
|
+
class GroundingMetadata
|
|
3
|
+
attr_reader :web_search_queries, :citations, :search_suggestions
|
|
4
|
+
|
|
5
|
+
def initialize(web_search_queries: [], citations: [], search_suggestions: nil)
|
|
6
|
+
@web_search_queries = web_search_queries
|
|
7
|
+
@citations = citations
|
|
8
|
+
@search_suggestions = search_suggestions
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def empty?
|
|
12
|
+
@web_search_queries.empty? && @citations.empty? && @search_suggestions.nil?
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
module Geminai
|
|
2
|
+
class Interaction
|
|
3
|
+
attr_reader :id, :status, :usage, :created, :updated, :model, :steps, :raw_data
|
|
4
|
+
|
|
5
|
+
def initialize(data)
|
|
6
|
+
@raw_data = data
|
|
7
|
+
@id = data[:id]
|
|
8
|
+
@status = data[:status]
|
|
9
|
+
@usage = data[:usage]
|
|
10
|
+
@created = data[:created]
|
|
11
|
+
@updated = data[:updated]
|
|
12
|
+
@model = data[:model]
|
|
13
|
+
|
|
14
|
+
@steps = (data[:steps] || []).map { |step_data| Step.new(step_data) }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def output_text
|
|
18
|
+
text_parts = []
|
|
19
|
+
@steps.each do |step|
|
|
20
|
+
next unless step.model_output?
|
|
21
|
+
(step.content || []).each do |part|
|
|
22
|
+
text_parts << part[:text] if part[:type] == "text" && part[:text]
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
text_parts.join("")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def output_images
|
|
30
|
+
images = []
|
|
31
|
+
@steps.each do |step|
|
|
32
|
+
next unless step.model_output?
|
|
33
|
+
(step.content || []).each do |part|
|
|
34
|
+
if part[:type] == "image"
|
|
35
|
+
images <<
|
|
36
|
+
{
|
|
37
|
+
data: part[:data],
|
|
38
|
+
mime_type: part[:mime_type]
|
|
39
|
+
}
|
|
40
|
+
elsif part[:image]
|
|
41
|
+
# Handle other possible image shapes in responses if applicable
|
|
42
|
+
images <<
|
|
43
|
+
{
|
|
44
|
+
data: part[:image][:data] || part[:image][:image_bytes],
|
|
45
|
+
mime_type: part[:image][:mime_type]
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
images
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def grounding_metadata
|
|
55
|
+
queries = []
|
|
56
|
+
citations = []
|
|
57
|
+
suggestions = nil
|
|
58
|
+
|
|
59
|
+
@steps.each do |step|
|
|
60
|
+
if step.google_search_call?
|
|
61
|
+
if step.arguments && step.arguments[:queries]
|
|
62
|
+
queries.concat(step.arguments[:queries])
|
|
63
|
+
end
|
|
64
|
+
elsif step.google_search_result?
|
|
65
|
+
if step.result
|
|
66
|
+
(step.result || []).each do |res|
|
|
67
|
+
if res[:search_suggestions]
|
|
68
|
+
suggestions = res[:search_suggestions]
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
elsif step.model_output?
|
|
73
|
+
(step.content || []).each do |part|
|
|
74
|
+
if part[:type] == "text" && part[:annotations]
|
|
75
|
+
part[:annotations].each do |ann|
|
|
76
|
+
citations <<
|
|
77
|
+
{
|
|
78
|
+
start_index: ann[:start_index],
|
|
79
|
+
end_index: ann[:end_index],
|
|
80
|
+
url: ann[:url],
|
|
81
|
+
title: ann[:title],
|
|
82
|
+
type: ann[:type]
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
GroundingMetadata.new(
|
|
91
|
+
web_search_queries: queries.uniq,
|
|
92
|
+
citations: citations,
|
|
93
|
+
search_suggestions: suggestions
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
data/lib/geminai/step.rb
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Geminai
|
|
2
|
+
class Step
|
|
3
|
+
attr_reader :id, :type, :signature, :content, :arguments, :result, :raw_step
|
|
4
|
+
|
|
5
|
+
def initialize(data)
|
|
6
|
+
@raw_step = data
|
|
7
|
+
@id = data[:id]
|
|
8
|
+
@type = data[:type]
|
|
9
|
+
@signature = data[:signature]
|
|
10
|
+
@content = data[:content]
|
|
11
|
+
@arguments = data[:arguments]
|
|
12
|
+
@result = data[:result]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def thought?
|
|
16
|
+
@type == "thought"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def model_output?
|
|
20
|
+
@type == "model_output"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def google_search_call?
|
|
24
|
+
@type == "google_search_call"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def google_search_result?
|
|
28
|
+
@type == "google_search_result"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
data/lib/geminai.rb
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
require_relative "geminai/version"
|
|
2
|
+
require_relative "geminai/step"
|
|
3
|
+
require_relative "geminai/grounding_metadata"
|
|
4
|
+
require_relative "geminai/interaction"
|
|
5
|
+
require_relative "geminai/client"
|
|
6
|
+
|
|
7
|
+
module Geminai
|
|
8
|
+
# Helper to construct a client
|
|
9
|
+
def self.new(api_key, base_url: nil)
|
|
10
|
+
Client.new(api_key: api_key, base_url: base_url)
|
|
11
|
+
end
|
|
12
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: geminai
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- swlkr
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: A gem for google's gemini interactions api
|
|
13
|
+
email: []
|
|
14
|
+
executables: []
|
|
15
|
+
extensions: []
|
|
16
|
+
extra_rdoc_files: []
|
|
17
|
+
files:
|
|
18
|
+
- README.md
|
|
19
|
+
- lib/geminai.rb
|
|
20
|
+
- lib/geminai/client.rb
|
|
21
|
+
- lib/geminai/grounding_metadata.rb
|
|
22
|
+
- lib/geminai/interaction.rb
|
|
23
|
+
- lib/geminai/step.rb
|
|
24
|
+
- lib/geminai/version.rb
|
|
25
|
+
homepage: https://github.com/swlkr/geminai
|
|
26
|
+
licenses:
|
|
27
|
+
- 0BSD
|
|
28
|
+
metadata:
|
|
29
|
+
homepage_uri: https://github.com/swlkr/geminai
|
|
30
|
+
source_code_uri: https://github.com/swlkr/geminai
|
|
31
|
+
rdoc_options: []
|
|
32
|
+
require_paths:
|
|
33
|
+
- lib
|
|
34
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: 2.6.0
|
|
39
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
40
|
+
requirements:
|
|
41
|
+
- - ">="
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: '0'
|
|
44
|
+
requirements: []
|
|
45
|
+
rubygems_version: 3.7.2
|
|
46
|
+
specification_version: 4
|
|
47
|
+
summary: Use google's gemini interactions api
|
|
48
|
+
test_files: []
|