""" CryptoSentinel Pro — High-performance FastAPI application. This is the main entry point that orchestrates the entire application. - Integrates an asynchronous PriceFetcher for live market data. - Integrates a sophisticated GeminiAnalyzer for deep text analysis. - Implements an automated pipeline to fetch, analyze, and stream top crypto news. - Serves the interactive frontend and provides all necessary API endpoints. """ import asyncio import json import os from contextlib import asynccontextmanager from typing import Optional, Union import httpx # =================== FIX APPLIED HERE (1 of 2) =================== from fastapi import FastAPI, Request, BackgroundTasks, Form # ================================================================= from fastapi.responses import HTMLResponse, StreamingResponse from fastapi.templating import Jinja2Templates from pydantic import BaseModel, constr # Import our modular, asynchronous service classes from .price_fetcher import PriceFetcher from .gemini_analyzer import GeminiAnalyzer from newsapi import NewsApiClient # --- Application Lifespan for Resource Management --- @asynccontextmanager async def lifespan(app: FastAPI): """ Manages application startup and shutdown events using the modern lifespan context manager. """ async with httpx.AsyncClient() as client: # Instantiate and store all services in the application state. app.state.price_fetcher = PriceFetcher(client=client, coins=["bitcoin", "ethereum", "dogecoin"]) app.state.gemini_analyzer = GeminiAnalyzer(client=client) app.state.news_api = NewsApiClient(api_key=os.getenv("NEWS_API_KEY")) # Create separate queues for the two real-time feeds app.state.sentiment_queue: asyncio.Queue = asyncio.Queue() app.state.news_queue: asyncio.Queue = asyncio.Queue() # Create cancellable background tasks for periodic updates. price_task = asyncio.create_task( run_periodic_updates(app.state.price_fetcher, interval_seconds=30) ) news_task = asyncio.create_task( run_periodic_news_analysis(app, interval_seconds=900) # Run every 15 minutes ) print("🚀 CryptoSentinel Pro started successfully.") yield print("⏳ Shutting down background tasks...") price_task.cancel() news_task.cancel() try: await asyncio.gather(price_task, news_task) except asyncio.CancelledError: print("Background tasks cancelled successfully.") print("✅ Shutdown complete.") async def run_periodic_updates(fetcher: PriceFetcher, interval_seconds: int): """A robust asyncio background task that periodically updates prices.""" while True: await fetcher.update_prices_async() await asyncio.sleep(interval_seconds) async def run_periodic_news_analysis(app: FastAPI, interval_seconds: int): """Fetches, analyzes, and queues top crypto news periodically.""" while True: print("📰 Fetching latest crypto news for automated analysis...") try: top_headlines = app.state.news_api.get_everything( q='bitcoin OR ethereum OR crypto OR blockchain', language='en', sort_by='publishedAt', page_size=5 ) analyzer: GeminiAnalyzer = app.state.gemini_analyzer for article in top_headlines.get('articles', []): title = article.get('title') if title and "[Removed]" not in title: analysis = await analyzer.analyze_text(title) analysis['url'] = article.get('url') await app.state.news_queue.put(analysis) except Exception as e: print(f"❌ Error during news fetching or analysis: {e}") await asyncio.sleep(interval_seconds) # --- FastAPI App Initialization --- app = FastAPI(title="CryptoSentinel Pro", lifespan=lifespan) templates = Jinja2Templates(directory="templates") # --- HTML Rendering Helper --- def render_analysis_card(payload: dict, is_news: bool = False) -> str: s = payload text_to_show = s.get('summary', 'Analysis failed or not available.') if is_news: url = s.get('url', '#') text_to_show = f'{s.get("summary", "N/A")}' impact_class = f"impact-{s.get('impact', 'low').lower()}" sentiment_class = f"sentiment-{s.get('sentiment', 'neutral').lower()}" return f"""
{text_to_show}
Sentiment: {s.get('sentiment')} ({s.get('sentiment_score', 0):.2f})
Impact: {s.get('impact')}
Topic: {s.get('topic')}
Entities: {', '.join(s.get('entities', []))}
""" # --- API Endpoints --- @app.get("/", response_class=HTMLResponse) async def serve_dashboard(request: Request): return templates.TemplateResponse("index.html", {"request": request}) @app.get("/api/prices", response_class=HTMLResponse) async def get_prices_fragment(request: Request): price_fetcher: PriceFetcher = request.app.state.price_fetcher prices = price_fetcher.get_current_prices() html_fragment = "".join( f"
{coin.capitalize()}: ${price:,.2f}
" if isinstance(price, (int, float)) else f"
{coin.capitalize()}: {price}
" for coin, price in prices.items() ) return HTMLResponse(content=html_fragment) # =================== FIX APPLIED HERE (2 of 2) =================== @app.post("/api/sentiment") async def analyze_sentiment( request: Request, background_tasks: BackgroundTasks, text: str = Form(...) # Tell FastAPI to expect a form field named 'text' ): # ================================================================= """Queues user-submitted text for a full Gemini-powered analysis.""" analyzer: GeminiAnalyzer = request.app.state.gemini_analyzer async def analysis_task_wrapper(): analysis_result = await analyzer.analyze_text(text) # Use the 'text' variable directly await request.app.state.sentiment_queue.put(analysis_result) background_tasks.add_task(analysis_task_wrapper) return HTMLResponse(content="✅ Queued for deep analysis...") @app.get("/api/sentiment/stream") async def sentiment_stream(request: Request): queue: asyncio.Queue = request.app.state.sentiment_queue async def event_generator(): while True: payload = await queue.get() html = render_analysis_card(payload) data_payload = html.replace('\n', '') sse_message = f"event: sentiment_update\ndata: {data_payload}\n\n" yield sse_message return StreamingResponse(event_generator(), media_type="text/event-stream") @app.get("/api/news/stream") async def news_stream(request: Request): queue: asyncio.Queue = request.app.state.news_queue async def event_generator(): while True: payload = await queue.get() html = render_analysis_card(payload, is_news=True) data_payload = html.replace('\n', '') sse_message = f"event: news_update\ndata: {data_payload}\n\n" yield sse_message return StreamingResponse(event_generator(), media_type="text/event-stream")