11"""
22Google Gemini API provider for fact and episode extraction.
33
4- Direct integration with google-generativeai library.
5- Supports: gemini-2-flash, gemini-2.5-flash, gemini-3-pro , etc.
4+ Direct integration with google-genai library.
5+ Supports: gemini-2.5 -flash, gemini-2.5-flash-lite , gemini-3-flash-preview , etc.
66
7- Features:
8- - Direct API calls (no middleware)
9- - Automatic retry with exponential backoff
10- - Fault tolerance for transient errors
11- - Rate limiting awareness
12-
13- Install: pip install google-generativeai
14-
15- Usage:
16- v = Vektori(extraction_model="gemini:gemini-2.5-flash-lite")
7+ Install: pip install google-genai
178"""
189
1910from __future__ import annotations
2011
2112import asyncio
2213import json
2314import logging
15+ import os
2416import random
2517from typing import Any
2618
3022
3123DEFAULT_MODEL = "gemini-2.5-flash-lite"
3224
33- # Retry configuration
3425DEFAULT_MAX_RETRIES = 3
35- DEFAULT_INITIAL_BACKOFF = 1.0 # seconds
36- DEFAULT_MAX_BACKOFF = 32.0 # seconds
26+ DEFAULT_INITIAL_BACKOFF = 1.0
27+ DEFAULT_MAX_BACKOFF = 32.0
3728DEFAULT_BACKOFF_MULTIPLIER = 2.0
3829
3930
4031class GeminiLLM (LLMProvider ):
4132 """
42- Google Gemini API provider for fact + episode extraction.
43-
44- Direct google-generativeai integration (not via LiteLLM).
45- Supports all Gemini models: gemini-2-flash, gemini-2.5-flash, gemini-3-pro, etc.
46-
47- Features:
48- - Automatic retry with exponential backoff for transient failures
49- - Handles rate limiting, network errors, and API timeouts
50- - Configurable max retries and backoff strategy
51- - Fault-tolerant: continues despite temporary failures
52-
53- Install: pip install google-generativeai
33+ Google Gemini API provider using the google-genai SDK.
5434
55- Usage:
56- provider = GeminiLLM(model="gemini-2-5-flash-lite")
57- response = await provider.generate("Extract facts from: ...", max_tokens=2000)
35+ Supports all Gemini models. Reads GEMINI_API_KEY or GOOGLE_API_KEY from env.
5836 """
5937
6038 def __init__ (
@@ -65,82 +43,47 @@ def __init__(
6543 initial_backoff : float = DEFAULT_INITIAL_BACKOFF ,
6644 max_backoff : float = DEFAULT_MAX_BACKOFF ,
6745 ) -> None :
68- """
69- Initialize Gemini LLM provider with retry configuration.
70-
71- Args:
72- model: Model name (e.g., "gemini-2-5-flash-lite", "gemini-2-flash", "gemini-3-pro")
73- Defaults to "gemini-2-5-flash-lite" if not specified.
74- api_key: Google API key. If not provided, uses GOOGLE_API_KEY environment variable.
75- max_retries: Maximum number of retry attempts (default: 3)
76- initial_backoff: Initial backoff time in seconds (default: 1.0)
77- max_backoff: Maximum backoff time in seconds (default: 32.0)
78- """
7946 self .model = model or DEFAULT_MODEL
8047 self ._api_key = api_key
8148 self ._client = None
82-
83- # Retry configuration
8449 self .max_retries = max_retries
8550 self .initial_backoff = initial_backoff
8651 self .max_backoff = max_backoff
8752
8853 def _get_client (self ):
89- """Lazy-load Gemini client."""
9054 if self ._client is None :
9155 try :
92- import google . generativeai as genai
56+ from google import genai
9357 except ImportError as e :
94- raise ImportError (
95- "google-generativeai required: pip install google-generativeai"
96- ) from e
97-
98- # Configure API key
99- genai . configure ( api_key = self . _api_key )
100- self . _client = genai
101-
58+ raise ImportError ("google-genai required: pip install google-genai" ) from e
59+
60+ api_key = (
61+ self . _api_key
62+ or os . environ . get ( "GEMINI_API_KEY" )
63+ or os . environ . get ( "GOOGLE_API_KEY" )
64+ )
65+ self . _client = genai . Client ( api_key = api_key )
10266 return self ._client
10367
10468 async def _calculate_backoff (self , attempt : int ) -> float :
105- """
106- Calculate backoff time with exponential backoff + jitter.
107-
108- Prevents thundering herd problem when many requests retry simultaneously.
109- """
11069 backoff = min (
11170 self .initial_backoff * (DEFAULT_BACKOFF_MULTIPLIER ** attempt ),
11271 self .max_backoff ,
11372 )
114- # Add jitter: ±25% randomness
115- jitter = backoff * random .uniform (0.75 , 1.25 )
116- return jitter
73+ return backoff * random .uniform (0.75 , 1.25 )
11774
11875 async def generate (self , prompt : str , max_tokens : int | None = None ) -> str :
119- """
120- Generate a completion using Gemini API with automatic retry.
121-
122- Args:
123- prompt: The prompt to send to Gemini
124- max_tokens: Maximum tokens in response. None = use Gemini defaults.
76+ from google .genai import types
12577
126- Returns:
127- Generated text (should be valid JSON for extraction prompts)
78+ client = self ._get_client ()
12879
129- Raises:
130- RuntimeError: If all retry attempts fail
131- """
132- genai = self ._get_client ()
133-
134- # Build request kwargs — force JSON output so the model never returns prose
135- kwargs : dict [str , Any ] = {
136- "temperature" : 0.1 , # Low randomness for extraction tasks
80+ config_kwargs : dict [str , Any ] = {
81+ "temperature" : 0.1 ,
13782 "response_mime_type" : "application/json" ,
13883 }
139-
14084 if max_tokens is not None :
141- kwargs ["max_output_tokens" ] = max_tokens
85+ config_kwargs ["max_output_tokens" ] = max_tokens
14286
143- # Retry loop with exponential backoff
14487 last_exception = None
14588
14689 for attempt in range (self .max_retries + 1 ):
@@ -150,80 +93,42 @@ async def generate(self, prompt: str, max_tokens: int | None = None) -> str:
15093 f"model={ self .model } , tokens={ max_tokens } "
15194 )
15295
153- # Run in executor to avoid blocking event loop
154- loop = asyncio .get_event_loop ()
155-
156- def _generate ():
157- try :
158- model = genai .GenerativeModel (self .model )
159- response = model .generate_content (
160- prompt ,
161- generation_config = genai .types .GenerationConfig (** kwargs ),
162- )
163- return response .text or ""
164- except Exception as e :
165- raise e
166-
167- response_text = await loop .run_in_executor (None , _generate )
96+ response = await client .aio .models .generate_content (
97+ model = self .model ,
98+ contents = prompt ,
99+ config = types .GenerateContentConfig (** config_kwargs ),
100+ )
168101
169- # Success
170102 logger .debug (f"Gemini API call succeeded on attempt { attempt + 1 } " )
171- return response_text
103+ return response . text or ""
172104
173105 except Exception as e :
174106 last_exception = e
175-
176- # Check if this is a retryable error
177107 error_msg = str (e ).lower ()
178108 is_retryable = any (
179109 term in error_msg
180- for term in [
181- "timeout" ,
182- "500" ,
183- "429" , # Rate limit
184- "503" , # Service unavailable
185- "connection" ,
186- "network" ,
187- "temporarily" ,
188- "try again" ,
189- ]
110+ for term in ["timeout" , "500" , "429" , "503" , "connection" , "network" , "temporarily" , "try again" ]
190111 )
191112
192- # If not retryable or last attempt, raise
193113 if not is_retryable or attempt >= self .max_retries :
194114 logger .error (f"Gemini API call failed after { attempt + 1 } attempts: { e } " )
195115 raise RuntimeError (
196116 f"Gemini API call failed after { self .max_retries + 1 } attempts: { e } "
197117 ) from e
198118
199- # Calculate backoff and wait
200119 backoff_time = await self ._calculate_backoff (attempt )
201120 logger .warning (
202121 f"Gemini API call failed (attempt { attempt + 1 } ): { e } . "
203122 f"Retrying in { backoff_time :.1f} s..."
204123 )
205124 await asyncio .sleep (backoff_time )
206125
207- # Should never reach here, but just in case
208126 raise RuntimeError (
209127 f"Gemini API call failed after { self .max_retries + 1 } attempts"
210128 ) from last_exception
211129
212130 async def generate_json (self , prompt : str , max_tokens : int | None = None ) -> dict [str , Any ]:
213- """
214- Generate JSON response from Gemini with retry.
215-
216- Convenience method that parses the response as JSON.
217-
218- Args:
219- prompt: Prompt that asks for JSON output
220- max_tokens: Maximum tokens
221-
222- Returns:
223- Parsed JSON object
224- """
225131 response_text = await self .generate (prompt , max_tokens )
226-
227132 try :
228133 return json .loads (response_text )
229134 except json .JSONDecodeError as e :
0 commit comments