File size: 6,278 Bytes
ddaf3fc
 
 
 
 
 
 
 
1a96a66
ddaf3fc
 
 
 
8e4b913
1a96a66
ddaf3fc
 
 
 
1a96a66
ddaf3fc
 
 
 
 
 
 
 
 
1a96a66
ddaf3fc
 
 
 
 
 
 
 
 
 
 
 
 
 
1a96a66
 
ddaf3fc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1a96a66
ddaf3fc
 
 
 
 
 
 
1a96a66
ddaf3fc
 
 
 
1a96a66
ddaf3fc
 
 
1a96a66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
"""
A sophisticated analyzer using the Google Gemini Pro API.

This module provides structured analysis of financial text, including:
- Nuanced sentiment with reasoning.
- Key entity extraction (e.g., cryptocurrencies).
- Topic classification.
- Potential market impact assessment.
- Synthesis of multiple news items into a daily briefing.
"""
import os
import logging
import httpx
import json
from typing import Optional, TypedDict, List, Union

# Configure logging
logger = logging.getLogger(__name__)

# --- Type Definitions for Structured Data ---
class AnalysisResult(TypedDict):
    sentiment: str
    sentiment_score: float
    reason: str
    entities: List[str]
    topic: str
    impact: str
    summary: str
    error: Optional[str]
    url: Optional[str] # To store the article URL

class GeminiAnalyzer:
    """Manages interaction with the Google Gemini API for deep text analysis."""

    API_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-pro-latest:generateContent"

    def __init__(self, client: httpx.AsyncClient, api_key: Optional[str] = None):
        self.client = client
        self.api_key = api_key or os.getenv("GEMINI_API_KEY")
        if not self.api_key:
            raise ValueError("GEMINI_API_KEY is not set. Please add it as a repository secret.")
        self.params = {"key": self.api_key}
        self.headers = {"Content-Type": "application/json"}

    def _build_analysis_prompt(self, text: str) -> dict:
        """Creates the structured JSON prompt for analyzing a single piece of text."""
        return {
            "contents": [{
                "parts": [{
                    "text": f"""
                    Analyze the following financial text from the cryptocurrency world.
                    Provide your analysis as a single, minified JSON object with NO markdown formatting.

                    The JSON object must have these exact keys: "sentiment", "sentiment_score", "reason", "entities", "topic", "impact", "summary".

                    - "sentiment": MUST be one of "POSITIVE", "NEGATIVE", or "NEUTRAL".
                    - "sentiment_score": A float between -1.0 (very negative) and 1.0 (very positive).
                    - "reason": A brief, one-sentence explanation for the sentiment score.
                    - "entities": A JSON array of strings listing the primary cryptocurrencies or tokens mentioned (e.g., ["Bitcoin", "ETH"]).
                    - "topic": MUST be one of "Regulation", "Partnership", "Technical Update", "Market Hype", "Security", or "General News".
                    - "impact": Assess the potential short-term market impact. MUST be one of "LOW", "MEDIUM", or "HIGH".
                    - "summary": A concise, one-sentence summary of the provided text.

                    Text to analyze: "{text}"
                    """
                }]
            }]
        }

    async def analyze_text(self, text: str) -> AnalysisResult:
        """Sends text to Gemini and returns a structured analysis."""
        prompt = self._build_analysis_prompt(text)
        try:
            response = await self.client.post(self.API_URL, headers=self.headers, params=self.params, json=prompt, timeout=60.0)
            response.raise_for_status()

            full_response = response.json()
            json_text = full_response["candidates"][0]["content"]["parts"][0]["text"]
            
            analysis: AnalysisResult = json.loads(json_text)
            analysis["error"] = None
            return analysis

        except Exception as e:
            logger.error(f"❌ Gemini Analysis Error: {e}")
            return {
                "sentiment": "ERROR", "sentiment_score": 0, "reason": str(e),
                "entities": [], "topic": "Unknown", "impact": "Unknown",
                "summary": "Failed to analyze text due to an API or parsing error.", "error": str(e)
            }

    async def generate_daily_briefing(self, analysis_items: List[dict]) -> str:
        """Generates a high-level market briefing from a list of analyzed news items."""
        if not analysis_items:
            return "### Briefing Unavailable\nNo news items were analyzed in the last period."

        context = "\n".join([f"- {item.get('summary')} (Impact: {item.get('impact')}, Topic: {item.get('topic')})" for item in analysis_items])

        briefing_prompt = {
            "contents": [{
                "parts": [{
                    "text": f"""
                    You are a senior crypto market analyst named 'Sentinel'. Your tone is professional, concise, and insightful.
                    Based on the following list of analyzed news items from the last 24 hours, write a "Daily Market Briefing".

                    The briefing must have three sections using markdown:
                    1. "### Executive Summary": A single, impactful paragraph summarizing the overall market mood and key events.
                    2. "### Top Bullish Signals": 2-3 bullet points on the most positive developments.
                    3. "### Top Bearish Signals": 2-3 bullet points on the most significant risks or negative news.

                    Here is the data to analyze:
                    {context}
                    """
                }]
            }],
            "safetySettings": [
                {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
                {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
                {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE"},
                {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"},
            ]
        }
        
        try:
            response = await self.client.post(self.API_URL, headers=self.headers, params=self.params, json=briefing_prompt, timeout=120.0)
            response.raise_for_status()
            full_response = response.json()
            briefing_text = full_response["candidates"][0]["content"]["parts"][0]["text"]
            return briefing_text
        except Exception as e:
            logger.error(f"❌ Gemini Briefing Error: {e}")
            return "### Briefing Unavailable\nCould not generate the daily market briefing due to a Gemini API error."