Initial commit: CI Agent competitive-intelligence monitoring app
FastAPI + Celery + Next.js + Postgres/Redis app with company monitoring, source collection, LLM-based change analysis, enrichment, and account security (Turnstile, escalating lockout, email verification).
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
"""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 ""
|
||||
Reference in New Issue
Block a user