mgbam commited on
Commit
f29497b
Β·
verified Β·
1 Parent(s): 95e25f2

Update app/app.py

Browse files
Files changed (1) hide show
  1. app/app.py +51 -10
app/app.py CHANGED
@@ -16,7 +16,7 @@ from fastapi.responses import HTMLResponse, StreamingResponse
16
  from fastapi.templating import Jinja2Templates
17
  from pydantic import BaseModel, constr
18
 
19
- # These relative imports will now work correctly.
20
  from .price_fetcher import PriceFetcher
21
  from .sentiment import SentimentAnalyzer
22
 
@@ -38,6 +38,7 @@ async def lifespan(app: FastAPI):
38
  # On startup:
39
  async with httpx.AsyncClient() as client:
40
  # Instantiate and store our services in the application state.
 
41
  app.state.price_fetcher = PriceFetcher(client=client, coins=["bitcoin", "ethereum", "dogecoin"])
42
  app.state.sentiment_analyzer = SentimentAnalyzer(client=client)
43
  app.state.request_counter = 0
@@ -48,7 +49,7 @@ async def lifespan(app: FastAPI):
48
  )
49
 
50
  print("πŸš€ CryptoSentinel AI started successfully.")
51
- yield
52
 
53
  # On shutdown:
54
  print("⏳ Shutting down background tasks...")
@@ -68,44 +69,78 @@ async def run_periodic_updates(fetcher: PriceFetcher, interval_seconds: int):
68
  # --- FastAPI App Initialization ---
69
 
70
  app = FastAPI(title="CryptoSentinel AI", lifespan=lifespan)
71
- # This path assumes the app is run from the root directory.
72
- templates = Jinja2Templates(directory="app/templates")
 
 
 
 
 
 
 
73
 
74
  # --- API Endpoints ---
75
- # ... (The rest of the file is unchanged) ...
76
  @app.get("/", response_class=HTMLResponse)
77
  async def serve_dashboard(request: Request):
 
78
  return templates.TemplateResponse("index.html", {"request": request})
79
 
80
  @app.get("/api/prices", response_class=HTMLResponse)
81
  async def get_prices_fragment(request: Request):
 
82
  price_fetcher: PriceFetcher = request.app.state.price_fetcher
83
  prices = price_fetcher.get_current_prices()
 
84
  html_fragment = ""
85
  for coin, price in prices.items():
 
86
  price_str = f"${price:,.2f}" if isinstance(price, (int, float)) else price
87
  html_fragment += f"<div><strong>{coin.capitalize()}:</strong> {price_str}</div>"
 
88
  return HTMLResponse(content=html_fragment)
89
 
90
  @app.post("/api/sentiment")
91
- async def analyze_sentiment(payload: SentimentRequest, request: Request, background_tasks: BackgroundTasks):
 
 
 
 
 
 
 
 
92
  analyzer: SentimentAnalyzer = request.app.state.sentiment_analyzer
93
  request.app.state.request_counter += 1
94
  request_id = request.app.state.request_counter
 
 
95
  background_tasks.add_task(analyzer.compute_and_publish, payload.text, request_id)
 
96
  return HTMLResponse(content="<small>Queued for analysis...</small>")
97
 
98
  @app.get("/api/sentiment/stream")
99
  async def sentiment_stream(request: Request):
 
 
 
 
100
  analyzer: SentimentAnalyzer = request.app.state.sentiment_analyzer
 
101
  async def event_generator():
 
 
102
  yield f"event: sentiment_update\ndata: <div id='sentiment-results' hx-swap-oob='innerHTML'></div>\n\n"
 
 
103
  async for result_payload in analyzer.stream_results():
104
  try:
105
  result = result_payload['result']
106
  label = str(result.get('label', 'NEUTRAL')).lower()
107
  score = result.get('score', 0.0) * 100
108
  text = result_payload['text']
 
 
109
  html_fragment = f"""
110
  <div>
111
  <blockquote>{text}</blockquote>
@@ -116,16 +151,22 @@ async def sentiment_stream(request: Request):
116
  </p>
117
  </div>
118
  """
 
119
  data_payload = html_fragment.replace('\n', '')
 
 
120
  sse_message = f"event: sentiment_update\ndata: {data_payload}\n\n"
 
121
  yield sse_message
 
122
  except (KeyError, TypeError):
123
- continue
 
124
  return StreamingResponse(event_generator(), media_type="text/event-stream")
125
 
126
  # This block is now mostly for IDEs, the primary run method is the uvicorn command.
127
  if __name__ == "__main__":
128
- # Note: Running this file directly (`python app/app.py`) will fail due to relative imports.
129
- # Use the command: `uvicorn app.app:app --reload` from the project root.
130
  print("To run this application, use the command from the root directory:")
131
- print("uvicorn app.app:app --host 0.0.0.0 --port 7860 --reload")
 
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
 
 
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
 
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...")
 
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>
 
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")