mgbam commited on
Commit
b159555
Β·
verified Β·
1 Parent(s): c6f94f2

Update app/app.py

Browse files
Files changed (1) hide show
  1. app/app.py +130 -62
app/app.py CHANGED
@@ -1,80 +1,170 @@
1
- # In app/main.py (showing key changes and additions)
 
 
 
 
 
 
 
 
 
 
2
  import os
3
- from typing import Optional, Union # Make sure these are imported
4
- from app.gemini_analyzer import GeminiAnalyzer
5
- from newsapi import NewsApiClient # New import
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
- # ... (keep existing imports and Pydantic model) ...
 
 
 
 
8
 
9
  @asynccontextmanager
10
  async def lifespan(app: FastAPI):
11
- """Manages application startup and shutdown events."""
 
 
 
12
  async with httpx.AsyncClient() as client:
13
- # --- MODIFIED ---
14
  app.state.price_fetcher = PriceFetcher(client=client, coins=["bitcoin", "ethereum", "dogecoin"])
15
- # Use the new Gemini Analyzer
16
- app.state.gemini_analyzer = GeminiAnalyzer(client=client)
17
- # For the news feed
18
  app.state.news_api = NewsApiClient(api_key=os.getenv("NEWS_API_KEY"))
19
- app.state.news_queue: asyncio.Queue = asyncio.Queue()
20
- app.state.sentiment_queue: asyncio.Queue = asyncio.Queue() # New queue for manual analysis
21
- app.state.request_counter = 0
22
 
23
- # --- KEPT THE SAME ---
24
- price_task = asyncio.create_task(run_periodic_updates(app.state.price_fetcher, 30))
25
- # --- NEW BACKGROUND TASK ---
26
- news_task = asyncio.create_task(run_periodic_news_analysis(app, interval_seconds=900)) # every 15 mins
27
 
 
 
 
 
 
 
 
 
28
  print("πŸš€ CryptoSentinel Pro started successfully.")
29
  yield
30
 
31
  print("⏳ Shutting down background tasks...")
32
  price_task.cancel()
33
  news_task.cancel()
34
- # ... (rest of shutdown) ...
 
 
 
 
 
 
 
 
 
 
35
 
36
- # --- NEW BACKGROUND FUNCTION ---
37
  async def run_periodic_news_analysis(app: FastAPI, interval_seconds: int):
38
  """Fetches, analyzes, and queues top crypto news periodically."""
39
  while True:
40
- print("πŸ“° Fetching latest crypto news...")
41
  try:
42
  top_headlines = app.state.news_api.get_everything(
43
- q='bitcoin OR ethereum OR crypto',
44
  language='en',
45
  sort_by='publishedAt',
46
- page_size=5
47
  )
48
  analyzer: GeminiAnalyzer = app.state.gemini_analyzer
49
  for article in top_headlines.get('articles', []):
50
  title = article.get('title')
51
  if title and "[Removed]" not in title:
52
- # Run full analysis on each headline
53
  analysis = await analyzer.analyze_text(title)
54
- # Add article URL to the payload
55
  analysis['url'] = article.get('url')
56
  await app.state.news_queue.put(analysis)
57
  except Exception as e:
58
- print(f"❌ Error fetching or analyzing news: {e}")
59
 
60
  await asyncio.sleep(interval_seconds)
61
 
62
- # ... (keep / and /api/prices endpoints) ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
 
64
- # --- MODIFIED SENTIMENT ENDPOINT ---
65
  @app.post("/api/sentiment")
66
  async def analyze_sentiment(payload: SentimentRequest, request: Request, background_tasks: BackgroundTasks):
67
- """Queues text for a full Gemini-powered analysis."""
68
  analyzer: GeminiAnalyzer = request.app.state.gemini_analyzer
69
-
70
- async def task_wrapper():
71
- analysis = await analyzer.analyze_text(payload.text)
72
- await request.app.state.sentiment_queue.put(analysis)
73
-
74
- background_tasks.add_task(task_wrapper)
75
- return HTMLResponse(content="<small>Queued for deep analysis...</small>")
76
-
77
- # --- NEW SENTIMENT STREAM ---
78
  @app.get("/api/sentiment/stream")
79
  async def sentiment_stream(request: Request):
80
  """SSE stream for results from manual sentiment analysis requests."""
@@ -83,10 +173,10 @@ async def sentiment_stream(request: Request):
83
  while True:
84
  payload = await queue.get()
85
  html = render_analysis_card(payload)
 
86
  yield f"event: sentiment_update\ndata: {html.replace('\n', '')}\n\n"
87
  return StreamingResponse(event_generator(), media_type="text/event-stream")
88
 
89
- # --- NEW NEWS STREAM ---
90
  @app.get("/api/news/stream")
91
  async def news_stream(request: Request):
92
  """SSE stream for the automated Market Intelligence Feed."""
@@ -95,28 +185,6 @@ async def news_stream(request: Request):
95
  while True:
96
  payload = await queue.get()
97
  html = render_analysis_card(payload, is_news=True)
 
98
  yield f"event: news_update\ndata: {html.replace('\n', '')}\n\n"
99
- return StreamingResponse(event_generator(), media_type="text/event-stream")
100
-
101
- # --- NEW HELPER FUNCTION ---
102
- def render_analysis_card(payload: dict, is_news: bool = False) -> str:
103
- """Renders a dictionary of analysis into an HTML card."""
104
- s = payload
105
- text_to_show = s.get('summary', 'N/A')
106
- if is_news:
107
- url = s.get('url', '#')
108
- text_to_show = f'<a href="{url}" target="_blank">{s.get("summary", "N/A")}</a>'
109
-
110
- return f"""
111
- <div class="card impact-{s.get('impact', 'low').lower()}">
112
- <blockquote>{text_to_show}</blockquote>
113
- <div class="grid">
114
- <div><strong>Sentiment:</strong> <span class="sentiment-{s.get('sentiment', 'neutral').lower()}">{s.get('sentiment')} ({s.get('sentiment_score', 0):.2f})</span></div>
115
- <div><strong>Impact:</strong> {s.get('impact')}</div>
116
- </div>
117
- <div class="grid">
118
- <div><strong>Topic:</strong> {s.get('topic')}</div>
119
- <div><strong>Entities:</strong> {', '.join(s.get('entities', []))}</div>
120
- </div>
121
- </div>
122
- """
 
1
+ """
2
+ CryptoSentinel Pro β€” High-performance FastAPI application.
3
+
4
+ This is the main entry point that orchestrates the entire application.
5
+ - Integrates an asynchronous PriceFetcher for live market data.
6
+ - Integrates a sophisticated GeminiAnalyzer for deep text analysis.
7
+ - Implements an automated pipeline to fetch, analyze, and stream top crypto news.
8
+ - Serves the interactive frontend and provides all necessary API endpoints.
9
+ """
10
+ import asyncio
11
+ import json
12
  import os
13
+ from contextlib import asynccontextmanager
14
+ from typing import Optional, Union
15
+
16
+ import httpx
17
+ from fastapi import FastAPI, Request, BackgroundTasks
18
+ from fastapi.responses import HTMLResponse, StreamingResponse
19
+ from fastapi.templating import Jinja2Templates
20
+ from pydantic import BaseModel, constr
21
+
22
+ # Import our modular, asynchronous service classes
23
+ from .price_fetcher import PriceFetcher
24
+ from .gemini_analyzer import GeminiAnalyzer
25
+ from newsapi import NewsApiClient
26
+
27
+ # --- Pydantic Model for API Input Validation ---
28
 
29
+ class SentimentRequest(BaseModel):
30
+ """Ensures the text for sentiment analysis is a non-empty string."""
31
+ text: constr(strip_whitespace=True, min_length=1)
32
+
33
+ # --- Application Lifespan for Resource Management ---
34
 
35
  @asynccontextmanager
36
  async def lifespan(app: FastAPI):
37
+ """
38
+ Manages application startup and shutdown events using the modern
39
+ lifespan context manager.
40
+ """
41
  async with httpx.AsyncClient() as client:
42
+ # Instantiate and store all services in the application state.
43
  app.state.price_fetcher = PriceFetcher(client=client, coins=["bitcoin", "ethereum", "dogecoin"])
44
+ app.state.gemini_analyzer = GeminiAnalyzer(client=client)
 
 
45
  app.state.news_api = NewsApiClient(api_key=os.getenv("NEWS_API_KEY"))
 
 
 
46
 
47
+ # Create separate queues for the two real-time feeds
48
+ app.state.sentiment_queue: asyncio.Queue = asyncio.Queue()
49
+ app.state.news_queue: asyncio.Queue = asyncio.Queue()
 
50
 
51
+ # Create cancellable background tasks for periodic updates.
52
+ price_task = asyncio.create_task(
53
+ run_periodic_updates(app.state.price_fetcher, interval_seconds=30)
54
+ )
55
+ news_task = asyncio.create_task(
56
+ run_periodic_news_analysis(app, interval_seconds=900) # Run every 15 minutes
57
+ )
58
+
59
  print("πŸš€ CryptoSentinel Pro started successfully.")
60
  yield
61
 
62
  print("⏳ Shutting down background tasks...")
63
  price_task.cancel()
64
  news_task.cancel()
65
+ try:
66
+ await asyncio.gather(price_task, news_task)
67
+ except asyncio.CancelledError:
68
+ print("Background tasks cancelled successfully.")
69
+ print("βœ… Shutdown complete.")
70
+
71
+ async def run_periodic_updates(fetcher: PriceFetcher, interval_seconds: int):
72
+ """A robust asyncio background task that periodically updates prices."""
73
+ while True:
74
+ await fetcher.update_prices_async()
75
+ await asyncio.sleep(interval_seconds)
76
 
 
77
  async def run_periodic_news_analysis(app: FastAPI, interval_seconds: int):
78
  """Fetches, analyzes, and queues top crypto news periodically."""
79
  while True:
80
+ print("πŸ“° Fetching latest crypto news for automated analysis...")
81
  try:
82
  top_headlines = app.state.news_api.get_everything(
83
+ q='bitcoin OR ethereum OR crypto OR blockchain',
84
  language='en',
85
  sort_by='publishedAt',
86
+ page_size=5 # Fetch the 5 most recent articles
87
  )
88
  analyzer: GeminiAnalyzer = app.state.gemini_analyzer
89
  for article in top_headlines.get('articles', []):
90
  title = article.get('title')
91
  if title and "[Removed]" not in title:
92
+ # Run the full Gemini analysis on each headline
93
  analysis = await analyzer.analyze_text(title)
94
+ # Add the article URL to the payload for the frontend
95
  analysis['url'] = article.get('url')
96
  await app.state.news_queue.put(analysis)
97
  except Exception as e:
98
+ print(f"❌ Error during news fetching or analysis: {e}")
99
 
100
  await asyncio.sleep(interval_seconds)
101
 
102
+ # --- FastAPI App Initialization ---
103
+
104
+ app = FastAPI(title="CryptoSentinel Pro", lifespan=lifespan)
105
+ templates = Jinja2Templates(directory="templates")
106
+
107
+ # --- HTML Rendering Helper ---
108
+
109
+ def render_analysis_card(payload: dict, is_news: bool = False) -> str:
110
+ """Renders a dictionary of analysis into a styled HTML card."""
111
+ s = payload
112
+ text_to_show = s.get('summary', 'Analysis failed or not available.')
113
+
114
+ # Make the summary a clickable link if it's a news item
115
+ if is_news:
116
+ url = s.get('url', '#')
117
+ text_to_show = f'<a href="{url}" target="_blank" rel="noopener noreferrer">{s.get("summary", "N/A")}</a>'
118
+
119
+ # Dynamically set CSS classes based on analysis results
120
+ impact_class = f"impact-{s.get('impact', 'low').lower()}"
121
+ sentiment_class = f"sentiment-{s.get('sentiment', 'neutral').lower()}"
122
+
123
+ return f"""
124
+ <div class="card {impact_class}">
125
+ <blockquote>{text_to_show}</blockquote>
126
+ <div class="grid">
127
+ <div><strong>Sentiment:</strong> <span class="{sentiment_class}">{s.get('sentiment')} ({s.get('sentiment_score', 0):.2f})</span></div>
128
+ <div><strong>Impact:</strong> {s.get('impact')}</div>
129
+ </div>
130
+ <div class="grid">
131
+ <div><strong>Topic:</strong> {s.get('topic')}</div>
132
+ <div><strong>Entities:</strong> {', '.join(s.get('entities', []))}</div>
133
+ </div>
134
+ </div>
135
+ """
136
+
137
+ # --- API Endpoints ---
138
+
139
+ @app.get("/", response_class=HTMLResponse)
140
+ async def serve_dashboard(request: Request):
141
+ """Serves the main interactive dashboard from `index.html`."""
142
+ return templates.TemplateResponse("index.html", {"request": request})
143
+
144
+ @app.get("/api/prices", response_class=HTMLResponse)
145
+ async def get_prices_fragment(request: Request):
146
+ """Returns an HTML fragment with the latest cached crypto prices for HTMX."""
147
+ price_fetcher: PriceFetcher = request.app.state.price_fetcher
148
+ prices = price_fetcher.get_current_prices()
149
+ html_fragment = "".join(
150
+ f"<div><strong>{coin.capitalize()}:</strong> ${price:,.2f}</div>" if isinstance(price, (int, float))
151
+ else f"<div><strong>{coin.capitalize()}:</strong> {price}</div>"
152
+ for coin, price in prices.items()
153
+ )
154
+ return HTMLResponse(content=html_fragment)
155
 
 
156
  @app.post("/api/sentiment")
157
  async def analyze_sentiment(payload: SentimentRequest, request: Request, background_tasks: BackgroundTasks):
158
+ """Queues user-submitted text for a full Gemini-powered analysis."""
159
  analyzer: GeminiAnalyzer = request.app.state.gemini_analyzer
160
+
161
+ async def analysis_task_wrapper():
162
+ analysis_result = await analyzer.analyze_text(payload.text)
163
+ await request.app.state.sentiment_queue.put(analysis_result)
164
+
165
+ background_tasks.add_task(analysis_task_wrapper)
166
+ return HTMLResponse(content="<small>βœ… Queued for deep analysis...</small>")
167
+
 
168
  @app.get("/api/sentiment/stream")
169
  async def sentiment_stream(request: Request):
170
  """SSE stream for results from manual sentiment analysis requests."""
 
173
  while True:
174
  payload = await queue.get()
175
  html = render_analysis_card(payload)
176
+ # Use a custom event name for the HTMX listener
177
  yield f"event: sentiment_update\ndata: {html.replace('\n', '')}\n\n"
178
  return StreamingResponse(event_generator(), media_type="text/event-stream")
179
 
 
180
  @app.get("/api/news/stream")
181
  async def news_stream(request: Request):
182
  """SSE stream for the automated Market Intelligence Feed."""
 
185
  while True:
186
  payload = await queue.get()
187
  html = render_analysis_card(payload, is_news=True)
188
+ # Use a different custom event name
189
  yield f"event: news_update\ndata: {html.replace('\n', '')}\n\n"
190
+ return StreamingResponse(event_generator(), media_type="text/event-stream")