ruby-gemini-api 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/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +764 -0
- data/lib/gemini/audio.rb +127 -0
- data/lib/gemini/cached_content.rb +151 -0
- data/lib/gemini/client.rb +540 -0
- data/lib/gemini/documents.rb +141 -0
- data/lib/gemini/embeddings.rb +27 -0
- data/lib/gemini/files.rb +149 -0
- data/lib/gemini/http.rb +226 -0
- data/lib/gemini/http_headers.rb +24 -0
- data/lib/gemini/images.rb +122 -0
- data/lib/gemini/messages.rb +131 -0
- data/lib/gemini/models.rb +20 -0
- data/lib/gemini/response.rb +402 -0
- data/lib/gemini/runs.rb +158 -0
- data/lib/gemini/threads.rb +74 -0
- data/lib/gemini/version.rb +5 -0
- data/lib/gemini.rb +81 -0
- data/lib/ruby/gemini.rb +1 -0
- metadata +193 -0
@@ -0,0 +1,540 @@
|
|
1
|
+
module Gemini
|
2
|
+
class Client
|
3
|
+
include Gemini::HTTP
|
4
|
+
|
5
|
+
SENSITIVE_ATTRIBUTES = %i[@api_key @extra_headers].freeze
|
6
|
+
CONFIG_KEYS = %i[api_key uri_base extra_headers log_errors request_timeout].freeze
|
7
|
+
|
8
|
+
attr_reader(*CONFIG_KEYS, :faraday_middleware)
|
9
|
+
attr_writer :api_key
|
10
|
+
|
11
|
+
def initialize(api_key = nil, config = {}, &faraday_middleware)
|
12
|
+
# Handle API key passed directly as argument
|
13
|
+
config[:api_key] = api_key if api_key
|
14
|
+
|
15
|
+
CONFIG_KEYS.each do |key|
|
16
|
+
# Set instance variables. Use global config if no setting provided
|
17
|
+
instance_variable_set(
|
18
|
+
"@#{key}",
|
19
|
+
config[key].nil? ? Gemini.configuration.send(key) : config[key]
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
@api_key ||= ENV["GEMINI_API_KEY"]
|
24
|
+
@faraday_middleware = faraday_middleware
|
25
|
+
|
26
|
+
raise ConfigurationError, "API key is not set" unless @api_key
|
27
|
+
end
|
28
|
+
|
29
|
+
# Thread management accessor
|
30
|
+
def threads
|
31
|
+
@threads ||= Gemini::Threads.new(client: self)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Message management accessor
|
35
|
+
def messages
|
36
|
+
@messages ||= Gemini::Messages.new(client: self)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Run management accessor
|
40
|
+
def runs
|
41
|
+
@runs ||= Gemini::Runs.new(client: self)
|
42
|
+
end
|
43
|
+
|
44
|
+
def audio
|
45
|
+
@audio ||= Gemini::Audio.new(client: self)
|
46
|
+
end
|
47
|
+
|
48
|
+
def files
|
49
|
+
@files ||= Gemini::Files.new(client: self)
|
50
|
+
end
|
51
|
+
|
52
|
+
# 画像生成アクセサ
|
53
|
+
def images
|
54
|
+
@images ||= Gemini::Images.new(client: self)
|
55
|
+
end
|
56
|
+
|
57
|
+
# ドキュメント処理アクセサ
|
58
|
+
def documents
|
59
|
+
@documents ||= Gemini::Documents.new(client: self)
|
60
|
+
end
|
61
|
+
|
62
|
+
# キャッシュ管理アクセサ
|
63
|
+
def cached_content
|
64
|
+
@cached_content ||= Gemini::CachedContent.new(client: self)
|
65
|
+
end
|
66
|
+
|
67
|
+
def reset_headers
|
68
|
+
@extra_headers = {}
|
69
|
+
end
|
70
|
+
|
71
|
+
# Access to conn (Faraday connection) for Audio features
|
72
|
+
# Wrapper to allow using private methods from HTTP module externally
|
73
|
+
def conn(multipart: false)
|
74
|
+
super(multipart: multipart)
|
75
|
+
end
|
76
|
+
|
77
|
+
# OpenAI chat-like text generation method for Gemini API
|
78
|
+
# Extended to support streaming callbacks
|
79
|
+
def chat(parameters: {}, &stream_callback)
|
80
|
+
model = parameters.delete(:model) || "gemini-2.0-flash-lite"
|
81
|
+
|
82
|
+
# If streaming callback is provided
|
83
|
+
if block_given?
|
84
|
+
path = "models/#{model}:streamGenerateContent"
|
85
|
+
# Set up stream callback
|
86
|
+
stream_params = parameters.dup
|
87
|
+
stream_params[:stream] = proc { |chunk| process_stream_chunk(chunk, &stream_callback) }
|
88
|
+
response = json_post(path: path, parameters: stream_params)
|
89
|
+
return Gemini::Response.new(response)
|
90
|
+
else
|
91
|
+
# Normal batch response mode
|
92
|
+
path = "models/#{model}:generateContent"
|
93
|
+
response = json_post(path: path, parameters: parameters)
|
94
|
+
return Gemini::Response.new(response)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Method corresponding to OpenAI's embeddings
|
99
|
+
def embeddings(parameters: {})
|
100
|
+
model = parameters.delete(:model) || "text-embedding-model"
|
101
|
+
path = "models/#{model}:embedContent"
|
102
|
+
response = json_post(path: path, parameters: parameters)
|
103
|
+
Gemini::Response.new(response)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Method corresponding to OpenAI's completions
|
107
|
+
# Uses same endpoint as chat in Gemini API
|
108
|
+
def completions(parameters: {}, &stream_callback)
|
109
|
+
chat(parameters: parameters, &stream_callback)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Accessor for sub-clients
|
113
|
+
def models
|
114
|
+
@models ||= Gemini::Models.new(client: self)
|
115
|
+
end
|
116
|
+
|
117
|
+
# Helper methods for convenience
|
118
|
+
|
119
|
+
# Method with usage similar to OpenAI's chat
|
120
|
+
def generate_content(prompt, model: "gemini-2.0-flash-lite", system_instruction: nil,
|
121
|
+
response_mime_type: nil, response_schema: nil, **parameters, &stream_callback)
|
122
|
+
# For image/text combinations, the prompt is passed as an array
|
123
|
+
# example: [{type: "text", text: "What is this?"}, {type: "image_url", image_url: {url: "https://example.com/image.jpg"}}]
|
124
|
+
content = format_content(prompt)
|
125
|
+
params = {
|
126
|
+
contents: [content],
|
127
|
+
model: model
|
128
|
+
}
|
129
|
+
|
130
|
+
if system_instruction
|
131
|
+
params[:system_instruction] = format_content(system_instruction)
|
132
|
+
end
|
133
|
+
|
134
|
+
if response_mime_type || response_schema
|
135
|
+
params[:generation_config] ||= {}
|
136
|
+
|
137
|
+
if response_mime_type
|
138
|
+
params[:generation_config]["response_mime_type"] = response_mime_type
|
139
|
+
end
|
140
|
+
|
141
|
+
if response_schema
|
142
|
+
params[:generation_config]["response_schema"] = response_schema
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Merge other parameters
|
147
|
+
params.merge!(parameters)
|
148
|
+
|
149
|
+
if block_given?
|
150
|
+
chat(parameters: params, &stream_callback)
|
151
|
+
else
|
152
|
+
chat(parameters: params)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# Streaming text generation
|
157
|
+
def generate_content_stream(prompt, model: "gemini-2.0-flash-lite", system_instruction: nil,
|
158
|
+
response_mime_type: nil, response_schema: nil, **parameters, &block)
|
159
|
+
raise ArgumentError, "Block is required for streaming" unless block_given?
|
160
|
+
|
161
|
+
content = format_content(prompt)
|
162
|
+
params = {
|
163
|
+
contents: [content],
|
164
|
+
model: model
|
165
|
+
}
|
166
|
+
|
167
|
+
if system_instruction
|
168
|
+
params[:system_instruction] = format_content(system_instruction)
|
169
|
+
end
|
170
|
+
|
171
|
+
if response_mime_type || response_schema
|
172
|
+
params[:generation_config] ||= {}
|
173
|
+
|
174
|
+
if response_mime_type
|
175
|
+
params[:generation_config][:response_mime_type] = response_mime_type
|
176
|
+
end
|
177
|
+
|
178
|
+
if response_schema
|
179
|
+
params[:generation_config][:response_schema] = response_schema
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# Merge other parameters
|
184
|
+
params.merge!(parameters)
|
185
|
+
|
186
|
+
chat(parameters: params, &block)
|
187
|
+
end
|
188
|
+
|
189
|
+
# ファイルを使った会話(複数ファイル対応)
|
190
|
+
def chat_with_multimodal(file_paths, prompt, model: "gemini-1.5-flash", **parameters)
|
191
|
+
# スレッドを作成
|
192
|
+
thread = threads.create(parameters: { model: model })
|
193
|
+
thread_id = thread["id"]
|
194
|
+
|
195
|
+
# 複数のファイルをアップロードして追加
|
196
|
+
file_infos = []
|
197
|
+
|
198
|
+
begin
|
199
|
+
# ファイルをアップロードしてメッセージとして追加
|
200
|
+
file_paths.each do |file_path|
|
201
|
+
file = File.open(file_path, "rb")
|
202
|
+
begin
|
203
|
+
upload_result = files.upload(file: file)
|
204
|
+
file_uri = upload_result["file"]["uri"]
|
205
|
+
file_name = upload_result["file"]["name"]
|
206
|
+
mime_type = determine_mime_type(file_path)
|
207
|
+
|
208
|
+
# ファイル情報を保存
|
209
|
+
file_infos << {
|
210
|
+
uri: file_uri,
|
211
|
+
name: file_name,
|
212
|
+
mime_type: mime_type
|
213
|
+
}
|
214
|
+
|
215
|
+
# ファイルをメッセージとして追加
|
216
|
+
messages.create(
|
217
|
+
thread_id: thread_id,
|
218
|
+
parameters: {
|
219
|
+
role: "user",
|
220
|
+
content: [
|
221
|
+
{ file_data: { mime_type: mime_type, file_uri: file_uri } }
|
222
|
+
]
|
223
|
+
}
|
224
|
+
)
|
225
|
+
ensure
|
226
|
+
file.close
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
# プロンプトメッセージを追加
|
231
|
+
messages.create(
|
232
|
+
thread_id: thread_id,
|
233
|
+
parameters: {
|
234
|
+
role: "user",
|
235
|
+
content: prompt
|
236
|
+
}
|
237
|
+
)
|
238
|
+
|
239
|
+
# 実行
|
240
|
+
run = runs.create(thread_id: thread_id, parameters: parameters)
|
241
|
+
|
242
|
+
# メッセージを取得
|
243
|
+
messages_list = messages.list(thread_id: thread_id)
|
244
|
+
|
245
|
+
# 結果とファイル情報を返す
|
246
|
+
{
|
247
|
+
messages: messages_list,
|
248
|
+
run: run,
|
249
|
+
file_infos: file_infos,
|
250
|
+
thread_id: thread_id
|
251
|
+
}
|
252
|
+
rescue => e
|
253
|
+
# エラー処理
|
254
|
+
{ error: e.message, file_infos: file_infos }
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
def generate_content_with_cache(prompt, cached_content:, model: "gemini-1.5-flash", **parameters)
|
259
|
+
# モデル名にmodels/プレフィックスを追加
|
260
|
+
model_name = model.start_with?("models/") ? model : "models/#{model}"
|
261
|
+
|
262
|
+
# リクエストパラメータを構築
|
263
|
+
params = {
|
264
|
+
contents: [
|
265
|
+
{
|
266
|
+
parts: [{ text: prompt }],
|
267
|
+
role: "user"
|
268
|
+
}
|
269
|
+
],
|
270
|
+
cachedContent: cached_content
|
271
|
+
}
|
272
|
+
|
273
|
+
# その他のパラメータをマージ
|
274
|
+
params.merge!(parameters)
|
275
|
+
|
276
|
+
# 直接エンドポイントURLを構築
|
277
|
+
endpoint = "#{model_name}:generateContent"
|
278
|
+
|
279
|
+
# APIリクエスト
|
280
|
+
response = json_post(
|
281
|
+
path: endpoint,
|
282
|
+
parameters: params
|
283
|
+
)
|
284
|
+
|
285
|
+
Gemini::Response.new(response)
|
286
|
+
end
|
287
|
+
|
288
|
+
# 単一ファイルのヘルパー
|
289
|
+
def chat_with_file(file_path, prompt, model: "gemini-1.5-flash", **parameters)
|
290
|
+
chat_with_multimodal([file_path], prompt, model: model, **parameters)
|
291
|
+
end
|
292
|
+
|
293
|
+
# ファイルをアップロードして質問するシンプルなヘルパー
|
294
|
+
def upload_and_process_file(file_path, prompt, content_type: nil, model: "gemini-1.5-flash", **parameters)
|
295
|
+
# MIMEタイプを自動判定
|
296
|
+
mime_type = content_type || determine_mime_type(file_path)
|
297
|
+
|
298
|
+
# ファイルをアップロード
|
299
|
+
file = File.open(file_path, "rb")
|
300
|
+
begin
|
301
|
+
upload_result = files.upload(file: file)
|
302
|
+
file_uri = upload_result["file"]["uri"]
|
303
|
+
file_name = upload_result["file"]["name"]
|
304
|
+
|
305
|
+
# コンテンツを生成
|
306
|
+
response = generate_content(
|
307
|
+
[
|
308
|
+
{ text: prompt },
|
309
|
+
{ file_data: { mime_type: mime_type, file_uri: file_uri } }
|
310
|
+
],
|
311
|
+
model: model,
|
312
|
+
**parameters
|
313
|
+
)
|
314
|
+
|
315
|
+
# レスポンスと一緒にファイル情報も返す
|
316
|
+
{
|
317
|
+
response: response,
|
318
|
+
file_uri: file_uri,
|
319
|
+
file_name: file_name
|
320
|
+
}
|
321
|
+
ensure
|
322
|
+
file.close
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
# Debug inspect method
|
327
|
+
def inspect
|
328
|
+
vars = instance_variables.map do |var|
|
329
|
+
value = instance_variable_get(var)
|
330
|
+
SENSITIVE_ATTRIBUTES.include?(var) ? "#{var}=[REDACTED]" : "#{var}=#{value.inspect}"
|
331
|
+
end
|
332
|
+
"#<#{self.class}:#{object_id} #{vars.join(', ')}>"
|
333
|
+
end
|
334
|
+
|
335
|
+
# MIMEタイプを判定するメソッド(パブリックに変更)
|
336
|
+
def determine_mime_type(path_or_url)
|
337
|
+
extension = File.extname(path_or_url).downcase
|
338
|
+
|
339
|
+
# ドキュメント形式
|
340
|
+
document_types = {
|
341
|
+
".pdf" => "application/pdf",
|
342
|
+
".js" => "application/x-javascript",
|
343
|
+
".py" => "application/x-python",
|
344
|
+
".txt" => "text/plain",
|
345
|
+
".html" => "text/html",
|
346
|
+
".htm" => "text/html",
|
347
|
+
".css" => "text/css",
|
348
|
+
".md" => "text/md",
|
349
|
+
".csv" => "text/csv",
|
350
|
+
".xml" => "text/xml",
|
351
|
+
".rtf" => "text/rtf"
|
352
|
+
}
|
353
|
+
|
354
|
+
# 画像形式
|
355
|
+
image_types = {
|
356
|
+
".jpg" => "image/jpeg",
|
357
|
+
".jpeg" => "image/jpeg",
|
358
|
+
".png" => "image/png",
|
359
|
+
".gif" => "image/gif",
|
360
|
+
".webp" => "image/webp",
|
361
|
+
".heic" => "image/heic",
|
362
|
+
".heif" => "image/heif"
|
363
|
+
}
|
364
|
+
|
365
|
+
# 音声形式
|
366
|
+
audio_types = {
|
367
|
+
".wav" => "audio/wav",
|
368
|
+
".mp3" => "audio/mp3",
|
369
|
+
".aiff" => "audio/aiff",
|
370
|
+
".aac" => "audio/aac",
|
371
|
+
".ogg" => "audio/ogg",
|
372
|
+
".flac" => "audio/flac"
|
373
|
+
}
|
374
|
+
|
375
|
+
# 拡張子からMIMEタイプを判定
|
376
|
+
mime_type = document_types[extension] || image_types[extension] || audio_types[extension]
|
377
|
+
return mime_type if mime_type
|
378
|
+
|
379
|
+
# ファイルの内容から判定を試みる
|
380
|
+
if File.exist?(path_or_url)
|
381
|
+
# ファイルの最初の数バイトを読み込んで判定
|
382
|
+
first_bytes = File.binread(path_or_url, 8).bytes
|
383
|
+
case
|
384
|
+
when first_bytes[0..1] == [0xFF, 0xD8]
|
385
|
+
return "image/jpeg" # JPEG
|
386
|
+
when first_bytes[0..7] == [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]
|
387
|
+
return "image/png" # PNG
|
388
|
+
when first_bytes[0..2] == [0x47, 0x49, 0x46]
|
389
|
+
return "image/gif" # GIF
|
390
|
+
when first_bytes[0..3] == [0x52, 0x49, 0x46, 0x46] && first_bytes[8..11] == [0x57, 0x45, 0x42, 0x50]
|
391
|
+
return "image/webp" # WEBP
|
392
|
+
when first_bytes[0..3] == [0x25, 0x50, 0x44, 0x46]
|
393
|
+
return "application/pdf" # PDF
|
394
|
+
when first_bytes[0..1] == [0x49, 0x44]
|
395
|
+
return "audio/mp3" # MP3
|
396
|
+
when first_bytes[0..3] == [0x52, 0x49, 0x46, 0x46]
|
397
|
+
return "audio/wav" # WAV
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
# URLまたは判定できない場合
|
402
|
+
if path_or_url.start_with?("http://", "https://")
|
403
|
+
"application/octet-stream"
|
404
|
+
else
|
405
|
+
"application/octet-stream"
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
private
|
410
|
+
|
411
|
+
# Process stream chunk and pass to callback
|
412
|
+
def process_stream_chunk(chunk, &callback)
|
413
|
+
if chunk.respond_to?(:dig) && chunk.dig("candidates", 0, "content", "parts", 0, "text")
|
414
|
+
chunk_text = chunk.dig("candidates", 0, "content", "parts", 0, "text")
|
415
|
+
callback.call(chunk_text, chunk)
|
416
|
+
elsif chunk.respond_to?(:dig) && chunk.dig("candidates", 0, "content", "parts")
|
417
|
+
# Pass empty part to callback if no text
|
418
|
+
callback.call("", chunk)
|
419
|
+
else
|
420
|
+
# Treat other chunk types (metadata, etc.) as empty string
|
421
|
+
callback.call("", chunk)
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
# Convert input to Gemini API format with support for image inputs and file data
|
426
|
+
def format_content(input)
|
427
|
+
case input
|
428
|
+
when String
|
429
|
+
{ parts: [{ text: input }] }
|
430
|
+
when Array
|
431
|
+
# For arrays, convert each element to part form
|
432
|
+
processed_parts = input.map do |part|
|
433
|
+
if part.is_a?(Hash)
|
434
|
+
if part[:type]
|
435
|
+
case part[:type]
|
436
|
+
when "text"
|
437
|
+
{ text: part[:text] }
|
438
|
+
when "image_url"
|
439
|
+
# Convert to Gemini API format
|
440
|
+
{
|
441
|
+
inline_data: {
|
442
|
+
mime_type: determine_mime_type(part[:image_url][:url]),
|
443
|
+
data: encode_image_from_url(part[:image_url][:url])
|
444
|
+
}
|
445
|
+
}
|
446
|
+
when "image_file"
|
447
|
+
{
|
448
|
+
inline_data: {
|
449
|
+
mime_type: determine_mime_type(part[:image_file][:file_path]),
|
450
|
+
data: encode_image_from_file(part[:image_file][:file_path])
|
451
|
+
}
|
452
|
+
}
|
453
|
+
when "image_base64"
|
454
|
+
{
|
455
|
+
inline_data: {
|
456
|
+
mime_type: part[:image_base64][:mime_type],
|
457
|
+
data: part[:image_base64][:data]
|
458
|
+
}
|
459
|
+
}
|
460
|
+
when "file_data"
|
461
|
+
# Support for uploaded files via file_data
|
462
|
+
{
|
463
|
+
file_data: part[:file_data]
|
464
|
+
}
|
465
|
+
# 新しいタイプを追加
|
466
|
+
when "document"
|
467
|
+
{
|
468
|
+
file_data: {
|
469
|
+
mime_type: part[:document][:mime_type] || determine_mime_type(part[:document][:file_path]),
|
470
|
+
file_uri: part[:document][:file_uri]
|
471
|
+
}
|
472
|
+
}
|
473
|
+
when "audio"
|
474
|
+
{
|
475
|
+
file_data: {
|
476
|
+
mime_type: part[:audio][:mime_type] || determine_mime_type(part[:audio][:file_path]),
|
477
|
+
file_uri: part[:audio][:file_uri]
|
478
|
+
}
|
479
|
+
}
|
480
|
+
else
|
481
|
+
# Other types return as is
|
482
|
+
part
|
483
|
+
end
|
484
|
+
elsif part[:file_data]
|
485
|
+
# Direct file_data reference without type (for compatibility)
|
486
|
+
{
|
487
|
+
file_data: part[:file_data]
|
488
|
+
}
|
489
|
+
elsif part[:inline_data]
|
490
|
+
# Direct inline_data reference without type
|
491
|
+
{
|
492
|
+
inline_data: part[:inline_data]
|
493
|
+
}
|
494
|
+
elsif part[:text]
|
495
|
+
# Direct text reference without type
|
496
|
+
{ text: part[:text] }
|
497
|
+
else
|
498
|
+
# Return hash as is if no recognized keys
|
499
|
+
part
|
500
|
+
end
|
501
|
+
elsif part.respond_to?(:to_s)
|
502
|
+
{ text: part.to_s }
|
503
|
+
else
|
504
|
+
part
|
505
|
+
end
|
506
|
+
end
|
507
|
+
{ parts: processed_parts }
|
508
|
+
when Hash
|
509
|
+
if input.key?(:parts)
|
510
|
+
input # If already in proper format, return as is
|
511
|
+
else
|
512
|
+
{ parts: [input] } # Wrapping the hash in parts
|
513
|
+
end
|
514
|
+
else
|
515
|
+
{ parts: [{ text: input.to_s }] }
|
516
|
+
end
|
517
|
+
end
|
518
|
+
|
519
|
+
def encode_image_from_url(url)
|
520
|
+
require 'open-uri'
|
521
|
+
require 'base64'
|
522
|
+
begin
|
523
|
+
# Explicitly read in binary mode
|
524
|
+
data = URI.open(url, 'rb').read
|
525
|
+
Base64.strict_encode64(data)
|
526
|
+
rescue => e
|
527
|
+
raise Error.new("Failed to load image from URL: #{e.message}")
|
528
|
+
end
|
529
|
+
end
|
530
|
+
|
531
|
+
def encode_image_from_file(file_path)
|
532
|
+
require 'base64'
|
533
|
+
begin
|
534
|
+
Base64.strict_encode64(File.binread(file_path))
|
535
|
+
rescue => e
|
536
|
+
raise Error.new("Failed to load image from file: #{e.message}")
|
537
|
+
end
|
538
|
+
end
|
539
|
+
end
|
540
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
module Gemini
|
2
|
+
class Documents
|
3
|
+
def initialize(client:)
|
4
|
+
@client = client
|
5
|
+
end
|
6
|
+
|
7
|
+
# ドキュメントをアップロードして質問する基本メソッド
|
8
|
+
def process(file: nil, file_path: nil, prompt:, model: "gemini-1.5-flash", **parameters)
|
9
|
+
# ファイルパスが指定されている場合はファイルを開く
|
10
|
+
if file_path && !file
|
11
|
+
file = File.open(file_path, "rb")
|
12
|
+
close_file = true
|
13
|
+
else
|
14
|
+
close_file = false
|
15
|
+
end
|
16
|
+
|
17
|
+
begin
|
18
|
+
# ファイルが指定されていない場合はエラー
|
19
|
+
raise ArgumentError, "file or file_path parameter is required" unless file
|
20
|
+
|
21
|
+
# MIMEタイプを判定
|
22
|
+
mime_type = parameters[:mime_type] || determine_document_mime_type(file)
|
23
|
+
|
24
|
+
# ファイルをアップロード
|
25
|
+
upload_result = @client.files.upload(file: file)
|
26
|
+
file_uri = upload_result["file"]["uri"]
|
27
|
+
file_name = upload_result["file"]["name"]
|
28
|
+
|
29
|
+
# コンテンツを生成
|
30
|
+
response = @client.generate_content(
|
31
|
+
[
|
32
|
+
{ text: prompt },
|
33
|
+
{ file_data: { mime_type: mime_type, file_uri: file_uri } }
|
34
|
+
],
|
35
|
+
model: model,
|
36
|
+
**parameters.reject { |k, _| [:mime_type].include?(k) }
|
37
|
+
)
|
38
|
+
|
39
|
+
# レスポンスと一緒にファイル情報も返す
|
40
|
+
{
|
41
|
+
response: response,
|
42
|
+
file_uri: file_uri,
|
43
|
+
file_name: file_name
|
44
|
+
}
|
45
|
+
ensure
|
46
|
+
file.close if file && close_file
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# ドキュメントをキャッシュに保存するメソッド
|
51
|
+
def cache(file: nil, file_path: nil, system_instruction: nil, ttl: "86400s", **parameters)
|
52
|
+
# ファイルパスが指定されている場合はファイルを開く
|
53
|
+
if file_path && !file
|
54
|
+
file = File.open(file_path, "rb")
|
55
|
+
close_file = true
|
56
|
+
else
|
57
|
+
close_file = false
|
58
|
+
end
|
59
|
+
|
60
|
+
begin
|
61
|
+
# ファイルが指定されていない場合はエラー
|
62
|
+
raise ArgumentError, "file or file_path parameter is required" unless file
|
63
|
+
|
64
|
+
# MIMEタイプを判定
|
65
|
+
mime_type = parameters[:mime_type] || determine_document_mime_type(file)
|
66
|
+
|
67
|
+
# ファイルをアップロード
|
68
|
+
upload_result = @client.files.upload(file: file)
|
69
|
+
file_uri = upload_result["file"]["uri"]
|
70
|
+
file_name = upload_result["file"]["name"]
|
71
|
+
|
72
|
+
# モデル名の取得と調整
|
73
|
+
model = parameters[:model] || "gemini-1.5-flash"
|
74
|
+
model = "models/#{model}" unless model.start_with?("models/")
|
75
|
+
|
76
|
+
# キャッシュに保存(パラメータの名前に注意)
|
77
|
+
cache_result = @client.cached_content.create(
|
78
|
+
file_uri: file_uri,
|
79
|
+
mime_type: mime_type,
|
80
|
+
system_instruction: system_instruction,
|
81
|
+
model: model,
|
82
|
+
ttl: ttl,
|
83
|
+
**parameters.reject { |k, _| [:mime_type, :model].include?(k) }
|
84
|
+
)
|
85
|
+
|
86
|
+
# 結果とファイル情報を返す
|
87
|
+
{
|
88
|
+
cache: cache_result,
|
89
|
+
file_uri: file_uri,
|
90
|
+
file_name: file_name
|
91
|
+
}
|
92
|
+
ensure
|
93
|
+
file.close if file && close_file
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
# ドキュメントのMIMEタイプを判定するヘルパーメソッド
|
100
|
+
def determine_document_mime_type(file)
|
101
|
+
return "application/octet-stream" unless file.respond_to?(:path)
|
102
|
+
|
103
|
+
ext = File.extname(file.path).downcase
|
104
|
+
case ext
|
105
|
+
when ".pdf"
|
106
|
+
"application/pdf"
|
107
|
+
when ".js"
|
108
|
+
"application/x-javascript"
|
109
|
+
when ".py"
|
110
|
+
"application/x-python"
|
111
|
+
when ".txt"
|
112
|
+
"text/plain"
|
113
|
+
when ".html", ".htm"
|
114
|
+
"text/html"
|
115
|
+
when ".css"
|
116
|
+
"text/css"
|
117
|
+
when ".md"
|
118
|
+
"text/md"
|
119
|
+
when ".csv"
|
120
|
+
"text/csv"
|
121
|
+
when ".xml"
|
122
|
+
"text/xml"
|
123
|
+
when ".rtf"
|
124
|
+
"text/rtf"
|
125
|
+
else
|
126
|
+
# PDFのマジックナンバーを確認
|
127
|
+
file.rewind
|
128
|
+
header = file.read(4)
|
129
|
+
file.rewind
|
130
|
+
|
131
|
+
# PDFのシグネチャ: %PDF
|
132
|
+
if header && header.bytes.to_a[0..3] == [37, 80, 68, 70]
|
133
|
+
return "application/pdf"
|
134
|
+
end
|
135
|
+
|
136
|
+
# デフォルト
|
137
|
+
"application/octet-stream"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Gemini
|
2
|
+
class Embeddings
|
3
|
+
def initialize(client:)
|
4
|
+
@client = client
|
5
|
+
end
|
6
|
+
|
7
|
+
def create(input:, model: "text-embedding-model", **parameters)
|
8
|
+
content = case input
|
9
|
+
when String
|
10
|
+
{ parts: [{ text: input }] }
|
11
|
+
when Array
|
12
|
+
{ parts: input.map { |text| { text: text.to_s } } }
|
13
|
+
else
|
14
|
+
{ parts: [{ text: input.to_s }] }
|
15
|
+
end
|
16
|
+
|
17
|
+
payload = {
|
18
|
+
content: content
|
19
|
+
}.merge(parameters)
|
20
|
+
|
21
|
+
@client.json_post(
|
22
|
+
path: "models/#{model}:embedContent",
|
23
|
+
parameters: payload
|
24
|
+
)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|