first commit
This commit is contained in:
90
app/providers/gemini_sentiment.py
Normal file
90
app/providers/gemini_sentiment.py
Normal file
@@ -0,0 +1,90 @@
|
||||
"""Gemini sentiment provider using google-genai with search grounding."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
|
||||
from app.exceptions import ProviderError, RateLimitError
|
||||
from app.providers.protocol import SentimentData
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_SENTIMENT_PROMPT = """\
|
||||
Analyze the current market sentiment for the stock ticker {ticker}.
|
||||
Search the web for recent news articles, social media mentions, and analyst opinions.
|
||||
|
||||
Respond ONLY with a JSON object in this exact format (no markdown, no extra text):
|
||||
{{"classification": "<bullish|bearish|neutral>", "confidence": <0-100>, "reasoning": "<brief explanation>"}}
|
||||
|
||||
Rules:
|
||||
- classification must be exactly one of: bullish, bearish, neutral
|
||||
- confidence must be an integer from 0 to 100
|
||||
- reasoning should be a brief one-sentence explanation
|
||||
"""
|
||||
|
||||
VALID_CLASSIFICATIONS = {"bullish", "bearish", "neutral"}
|
||||
|
||||
|
||||
class GeminiSentimentProvider:
|
||||
"""Fetches sentiment analysis from Gemini with search grounding."""
|
||||
|
||||
def __init__(self, api_key: str, model: str = "gemini-2.0-flash") -> None:
|
||||
if not api_key:
|
||||
raise ProviderError("Gemini API key is required")
|
||||
self._client = genai.Client(api_key=api_key)
|
||||
self._model = model
|
||||
|
||||
async def fetch_sentiment(self, ticker: str) -> SentimentData:
|
||||
"""Send a structured prompt to Gemini and parse the JSON response."""
|
||||
try:
|
||||
response = await self._client.aio.models.generate_content(
|
||||
model=self._model,
|
||||
contents=_SENTIMENT_PROMPT.format(ticker=ticker),
|
||||
config=types.GenerateContentConfig(
|
||||
tools=[types.Tool(google_search=types.GoogleSearch())],
|
||||
response_mime_type="application/json",
|
||||
),
|
||||
)
|
||||
|
||||
raw_text = response.text.strip()
|
||||
logger.debug("Gemini raw response for %s: %s", ticker, raw_text)
|
||||
parsed = json.loads(raw_text)
|
||||
|
||||
classification = parsed.get("classification", "").lower()
|
||||
if classification not in VALID_CLASSIFICATIONS:
|
||||
raise ProviderError(
|
||||
f"Invalid classification '{classification}' from Gemini for {ticker}"
|
||||
)
|
||||
|
||||
confidence = int(parsed.get("confidence", 50))
|
||||
confidence = max(0, min(100, confidence))
|
||||
|
||||
reasoning = parsed.get("reasoning", "")
|
||||
if reasoning:
|
||||
logger.info("Gemini sentiment for %s: %s (confidence=%d) — %s",
|
||||
ticker, classification, confidence, reasoning)
|
||||
|
||||
return SentimentData(
|
||||
ticker=ticker,
|
||||
classification=classification,
|
||||
confidence=confidence,
|
||||
source="gemini",
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
except json.JSONDecodeError as exc:
|
||||
logger.error("Failed to parse Gemini JSON for %s: %s", ticker, exc)
|
||||
raise ProviderError(f"Invalid JSON from Gemini for {ticker}") from exc
|
||||
except ProviderError:
|
||||
raise
|
||||
except Exception as exc:
|
||||
msg = str(exc).lower()
|
||||
if "rate" in msg or "quota" in msg or "429" in msg:
|
||||
raise RateLimitError(f"Gemini rate limit hit for {ticker}") from exc
|
||||
logger.error("Gemini provider error for %s: %s", ticker, exc)
|
||||
raise ProviderError(f"Gemini provider error for {ticker}: {exc}") from exc
|
||||
Reference in New Issue
Block a user