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 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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Geminai
2
+ VERSION = "0.1.0"
3
+ 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: []