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

Update app/app.py

Browse files
Files changed (1) hide show
  1. app/app.py +98 -148
app/app.py CHANGED
@@ -1,172 +1,122 @@
1
- """
2
- CryptoSentinel AI β€” High-performance FastAPI application.
 
 
 
3
 
4
- This is the main entry point that orchestrates the entire application.
5
- - Integrates the asynchronous PriceFetcher for live market data.
6
- - Integrates the asynchronous SentimentAnalyzer for real-time analysis.
7
- - Serves the interactive frontend and provides all necessary API endpoints.
8
- """
9
- import asyncio
10
- import json
11
- from contextlib import asynccontextmanager
12
-
13
- import httpx
14
- from fastapi import FastAPI, Request, BackgroundTasks
15
- from fastapi.responses import HTMLResponse, StreamingResponse
16
- from fastapi.templating import Jinja2Templates
17
- from pydantic import BaseModel, constr
18
-
19
- # Import our modular, asynchronous service classes using relative paths
20
- from .price_fetcher import PriceFetcher
21
- from .sentiment import SentimentAnalyzer
22
-
23
-
24
- # --- Pydantic Model for API Input Validation ---
25
-
26
- class SentimentRequest(BaseModel):
27
- """Ensures the text for sentiment analysis is a non-empty string."""
28
- text: constr(strip_whitespace=True, min_length=1)
29
-
30
- # --- Application Lifespan for Resource Management ---
31
 
32
  @asynccontextmanager
33
  async def lifespan(app: FastAPI):
34
- """
35
- Manages application startup and shutdown events using the modern
36
- lifespan context manager.
37
- """
38
- # On startup:
39
  async with httpx.AsyncClient() as client:
40
- # Instantiate and store our services in the application state.
41
- # This makes them accessible in any request handler via `request.app.state`.
42
  app.state.price_fetcher = PriceFetcher(client=client, coins=["bitcoin", "ethereum", "dogecoin"])
43
- app.state.sentiment_analyzer = SentimentAnalyzer(client=client)
 
 
 
 
 
44
  app.state.request_counter = 0
45
 
46
- # Create a cancellable background task for continuous price updates.
47
- price_update_task = asyncio.create_task(
48
- run_periodic_updates(app.state.price_fetcher, interval_seconds=10)
49
- )
50
-
51
- print("πŸš€ CryptoSentinel AI started successfully.")
52
- yield # The application is now running and ready to accept requests.
53
 
54
- # On shutdown:
55
  print("⏳ Shutting down background tasks...")
56
- price_update_task.cancel()
57
- try:
58
- await price_update_task
59
- except asyncio.CancelledError:
60
- print("Price update task cancelled successfully.")
61
- print("βœ… Shutdown complete.")
62
 
63
- async def run_periodic_updates(fetcher: PriceFetcher, interval_seconds: int):
64
- """A robust asyncio background task that periodically updates prices."""
 
65
  while True:
66
- await fetcher.update_prices_async()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  await asyncio.sleep(interval_seconds)
68
 
69
- # --- FastAPI App Initialization ---
70
-
71
- app = FastAPI(title="CryptoSentinel AI", lifespan=lifespan)
72
-
73
- # ====================================================================
74
- # FIX APPLIED HERE
75
- # ====================================================================
76
- # The 'templates' directory is at the root of the project, not inside 'app'.
77
- # This path is relative to the directory where the run command is executed.
78
- templates = Jinja2Templates(directory="templates")
79
- # ====================================================================
80
-
81
-
82
- # --- API Endpoints ---
83
-
84
- @app.get("/", response_class=HTMLResponse)
85
- async def serve_dashboard(request: Request):
86
- """Serves the main interactive dashboard from `index.html`."""
87
- return templates.TemplateResponse("index.html", {"request": request})
88
-
89
- @app.get("/api/prices", response_class=HTMLResponse)
90
- async def get_prices_fragment(request: Request):
91
- """Returns an HTML fragment with the latest cached crypto prices for HTMX."""
92
- price_fetcher: PriceFetcher = request.app.state.price_fetcher
93
- prices = price_fetcher.get_current_prices()
94
-
95
- html_fragment = ""
96
- for coin, price in prices.items():
97
- # Format the price nicely, handling the initial '--' state
98
- price_str = f"${price:,.2f}" if isinstance(price, (int, float)) else price
99
- html_fragment += f"<div><strong>{coin.capitalize()}:</strong> {price_str}</div>"
100
-
101
- return HTMLResponse(content=html_fragment)
102
 
 
103
  @app.post("/api/sentiment")
104
- async def analyze_sentiment(
105
- payload: SentimentRequest,
106
- request: Request,
107
- background_tasks: BackgroundTasks
108
- ):
109
- """
110
- Validates and queues a text for sentiment analysis. The heavy lifting is
111
- done in the background to ensure the API responds instantly.
112
- """
113
- analyzer: SentimentAnalyzer = request.app.state.sentiment_analyzer
114
- request.app.state.request_counter += 1
115
- request_id = request.app.state.request_counter
116
-
117
- # The actual API call to Hugging Face will run after this response is sent.
118
- background_tasks.add_task(analyzer.compute_and_publish, payload.text, request_id)
119
 
120
- return HTMLResponse(content="<small>Queued for analysis...</small>")
 
 
 
 
 
121
 
 
122
  @app.get("/api/sentiment/stream")
123
  async def sentiment_stream(request: Request):
124
- """
125
- Establishes a Server-Sent Events (SSE) connection. It efficiently pushes
126
- new sentiment results as HTML fragments to the client as they become available.
127
- """
128
- analyzer: SentimentAnalyzer = request.app.state.sentiment_analyzer
129
-
130
  async def event_generator():
131
- # Clear the initial "waiting..." message on the client.
132
- # hx-swap-oob="innerHTML" swaps this div out-of-band without affecting the target.
133
- yield f"event: sentiment_update\ndata: <div id='sentiment-results' hx-swap-oob='innerHTML'></div>\n\n"
134
-
135
- # Listen for new results from the analyzer's internal queue.
136
- async for result_payload in analyzer.stream_results():
137
- try:
138
- result = result_payload['result']
139
- label = str(result.get('label', 'NEUTRAL')).lower()
140
- score = result.get('score', 0.0) * 100
141
- text = result_payload['text']
142
-
143
- # Dynamically build the HTML fragment to be sent to the client.
144
- html_fragment = f"""
145
- <div>
146
- <blockquote>{text}</blockquote>
147
- <p>
148
- <strong>Result:</strong>
149
- <span class="sentiment-{label}">{label.upper()}</span>
150
- (Confidence: {score:.1f}%)
151
- </p>
152
- </div>
153
- """
154
- # First, process the string to remove newlines.
155
- data_payload = html_fragment.replace('\n', '')
156
-
157
- # Then, use the clean variable in the f-string to build the message.
158
- sse_message = f"event: sentiment_update\ndata: {data_payload}\n\n"
159
-
160
- yield sse_message
161
-
162
- except (KeyError, TypeError):
163
- continue # Ignore malformed payloads
164
 
 
 
 
 
 
 
 
 
 
 
165
  return StreamingResponse(event_generator(), media_type="text/event-stream")
166
 
167
- # This block is now mostly for IDEs, the primary run method is the uvicorn command.
168
- if __name__ == "__main__":
169
- # Note: Running this file directly (`python app/main.py`) will fail due to relative imports.
170
- # Use the command: `uvicorn app.main:app --reload` from the project root.
171
- print("To run this application, use the command from the root directory:")
172
- print("uvicorn app.main:app --host 0.0.0.0 --port 7860 --reload")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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."""
81
+ queue: asyncio.Queue = request.app.state.sentiment_queue
 
 
 
 
82
  async def event_generator():
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."""
93
+ queue: asyncio.Queue = request.app.state.news_queue
94
+ async def event_generator():
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
+ """