"""Gemini provider: structured output via the SDK's native `response_schema` support (the model is constrained to the schema and the SDK parses the result into an instance of it directly), with the same bounded repair loop on a malformed/unparsed response as `anthropic_provider.py`. Chosen as the production LLM_PROVIDER option alongside Anthropic because Gemini has an actual free rate-limited API tier (gemini-2.0-flash), unlike OpenAI's expiring trial credits. """ from __future__ import annotations from google import genai from google.genai import types from pydantic import BaseModel, ValidationError from app.analysis.llm.base import LLMResponseError from app.core.config import Settings class GeminiLLMProvider: provider_name = "gemini" def __init__(self, settings: Settings) -> None: self._settings = settings self._client = genai.Client(api_key=settings.gemini_api_key) async def generate_structured[T: BaseModel]( self, system_prompt: str, user_prompt: str, response_model: type[T] ) -> T: last_error: Exception | None = None prompt = user_prompt for attempt in range(self._settings.llm_max_retries + 1): if attempt > 0 and last_error is not None: prompt = ( f"{user_prompt}\n\nYour previous response did not match the required " f"schema: {last_error}. Please try again." ) response = await self._client.aio.models.generate_content( model=self._settings.gemini_model, contents=prompt, config=types.GenerateContentConfig( system_instruction=system_prompt, response_mime_type="application/json", response_schema=response_model, max_output_tokens=self._settings.llm_max_tokens_per_request, ), ) if response.parsed is None: last_error = ValueError("Gemini did not return a parsed structured response") continue try: return response_model.model_validate(response.parsed) except ValidationError as exc: last_error = exc continue raise LLMResponseError( f"Gemini provider failed to produce a valid {response_model.__name__} after " f"{self._settings.llm_max_retries + 1} attempt(s): {last_error}" ) async def generate_text(self, system_prompt: str, user_prompt: str) -> str: response = await self._client.aio.models.generate_content( model=self._settings.gemini_model, contents=user_prompt, config=types.GenerateContentConfig( system_instruction=system_prompt, max_output_tokens=self._settings.llm_max_tokens_per_request, ), ) return response.text or ""