Spaces:
Running
Running
Commit ·
d1ec696
1
Parent(s): 1c28d55
Integrated health monitoring for all services
Browse files- Dockerfile +13 -6
- app/agents/adk_mathminds.py +6 -1
- app/api/main.py +26 -60
- app/core/orchestrator.py +6 -1
- app/core/security.py +38 -15
- app/core/settings.py +1 -2
- app/tools/web_scraper.py +39 -23
- app/worker.py +0 -59
- docker-compose.yml +45 -15
- frontend/app.py +15 -29
Dockerfile
CHANGED
|
@@ -4,32 +4,39 @@ FROM python:3.12-slim
|
|
| 4 |
# Set environment variables
|
| 5 |
ENV PYTHONDONTWRITEBYTECODE=1
|
| 6 |
ENV PYTHONUNBUFFERED=1
|
| 7 |
-
ENV PORT=
|
| 8 |
|
| 9 |
# Set working directory
|
| 10 |
WORKDIR /app
|
| 11 |
|
| 12 |
-
# Install system dependencies
|
|
|
|
|
|
|
| 13 |
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 14 |
build-essential \
|
|
|
|
| 15 |
libgl1-mesa-glx \
|
| 16 |
libglib2.0-0 \
|
| 17 |
&& rm -rf /var/lib/apt/lists/*
|
| 18 |
|
| 19 |
# Install Python dependencies
|
| 20 |
-
# Copy requirements first to leverage Docker cache
|
| 21 |
COPY requirements.txt .
|
| 22 |
RUN pip install --no-cache-dir -r requirements.txt
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
# Copy the rest of the application
|
| 25 |
COPY . .
|
| 26 |
|
| 27 |
# Create a non-root user and switch to it for security
|
|
|
|
| 28 |
RUN useradd -m appuser && chown -R appuser /app
|
| 29 |
USER appuser
|
| 30 |
|
| 31 |
-
# Expose the port
|
| 32 |
-
EXPOSE
|
| 33 |
|
| 34 |
-
#
|
| 35 |
CMD exec gunicorn --bind :$PORT --workers 1 --worker-class uvicorn.workers.UvicornWorker --timeout 0 app.api.main:app
|
|
|
|
| 4 |
# Set environment variables
|
| 5 |
ENV PYTHONDONTWRITEBYTECODE=1
|
| 6 |
ENV PYTHONUNBUFFERED=1
|
| 7 |
+
ENV PORT=8000
|
| 8 |
|
| 9 |
# Set working directory
|
| 10 |
WORKDIR /app
|
| 11 |
|
| 12 |
+
# Install system dependencies
|
| 13 |
+
# libgl1 and libglib2.0-0 are for OpenCV/Computer Vision
|
| 14 |
+
# curl is for health checks
|
| 15 |
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 16 |
build-essential \
|
| 17 |
+
curl \
|
| 18 |
libgl1-mesa-glx \
|
| 19 |
libglib2.0-0 \
|
| 20 |
&& rm -rf /var/lib/apt/lists/*
|
| 21 |
|
| 22 |
# Install Python dependencies
|
|
|
|
| 23 |
COPY requirements.txt .
|
| 24 |
RUN pip install --no-cache-dir -r requirements.txt
|
| 25 |
|
| 26 |
+
# Install Playwright browser and its dependencies
|
| 27 |
+
RUN playwright install chromium
|
| 28 |
+
RUN playwright install-deps chromium
|
| 29 |
+
|
| 30 |
# Copy the rest of the application
|
| 31 |
COPY . .
|
| 32 |
|
| 33 |
# Create a non-root user and switch to it for security
|
| 34 |
+
# Ensure the user has access to playwright browsers
|
| 35 |
RUN useradd -m appuser && chown -R appuser /app
|
| 36 |
USER appuser
|
| 37 |
|
| 38 |
+
# Expose the port (API usually on 8000)
|
| 39 |
+
EXPOSE 8000
|
| 40 |
|
| 41 |
+
# Default command for the API (can be overridden in docker-compose for the worker)
|
| 42 |
CMD exec gunicorn --bind :$PORT --workers 1 --worker-class uvicorn.workers.UvicornWorker --timeout 0 app.api.main:app
|
app/agents/adk_mathminds.py
CHANGED
|
@@ -266,4 +266,9 @@ class MathMindsADKAgent:
|
|
| 266 |
logger.error(f"Streaming execution failed: {e}")
|
| 267 |
yield {"type": "error", "content": str(e)}
|
| 268 |
finally:
|
| 269 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
logger.error(f"Streaming execution failed: {e}")
|
| 267 |
yield {"type": "error", "content": str(e)}
|
| 268 |
finally:
|
| 269 |
+
try:
|
| 270 |
+
current_image_ctx.reset(token)
|
| 271 |
+
except ValueError:
|
| 272 |
+
# This can happen if the generator is closed (GeneratorExit)
|
| 273 |
+
# in a different task context than where it was started.
|
| 274 |
+
pass
|
app/api/main.py
CHANGED
|
@@ -15,6 +15,7 @@ import json
|
|
| 15 |
|
| 16 |
from fastapi import FastAPI, HTTPException, status, Depends, Request
|
| 17 |
from fastapi.responses import JSONResponse, StreamingResponse
|
|
|
|
| 18 |
from slowapi import _rate_limit_exceeded_handler
|
| 19 |
from slowapi.errors import RateLimitExceeded
|
| 20 |
from app.core.limiter import limiter
|
|
@@ -72,6 +73,15 @@ app = FastAPI(
|
|
| 72 |
lifespan=lifespan
|
| 73 |
)
|
| 74 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
# Global Exception Handler (Catch-All)
|
| 76 |
@app.exception_handler(Exception)
|
| 77 |
async def global_exception_handler(request: Request, exc: Exception):
|
|
@@ -385,64 +395,20 @@ async def update_profile(
|
|
| 385 |
if __name__ == "__main__":
|
| 386 |
import uvicorn
|
| 387 |
uvicorn.run(app, host="0.0.0.0", port=8000)
|
| 388 |
-
# ── Auth Endpoints ──────────────────────────
|
| 389 |
-
|
| 390 |
-
@app.post("/auth/signup"
|
| 391 |
-
async def signup(
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
existing_user = db_manager.get_user_by_email(user_in.email)
|
| 398 |
-
if existing_user:
|
| 399 |
-
raise HTTPException(
|
| 400 |
-
status_code=status.HTTP_400_BAD_REQUEST,
|
| 401 |
-
detail="User with this email already exists"
|
| 402 |
-
)
|
| 403 |
-
|
| 404 |
-
user_id = str(uuid.uuid4())
|
| 405 |
-
hashed_pw = hash_password(user_in.password)
|
| 406 |
-
|
| 407 |
-
user_dict = {
|
| 408 |
-
"user_id": user_id,
|
| 409 |
-
"email": user_in.email,
|
| 410 |
-
"hashed_password": hashed_pw,
|
| 411 |
-
"full_name": user_in.full_name,
|
| 412 |
-
"created_at": datetime.now(timezone.utc)
|
| 413 |
-
}
|
| 414 |
-
|
| 415 |
-
if db_manager.create_user(user_dict):
|
| 416 |
-
token = create_access_token(data={"sub": user_id, "email": user_in.email})
|
| 417 |
-
return {
|
| 418 |
-
"access_token": token,
|
| 419 |
-
"token_type": "bearer",
|
| 420 |
-
"user_id": user_id,
|
| 421 |
-
"email": user_in.email
|
| 422 |
-
}
|
| 423 |
-
|
| 424 |
-
raise HTTPException(status_code=500, detail="Failed to create user")
|
| 425 |
|
| 426 |
-
@app.post("/auth/login"
|
| 427 |
-
async def login(
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
if not user or not verify_password(login_in.password, user["hashed_password"]):
|
| 434 |
-
raise HTTPException(
|
| 435 |
-
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 436 |
-
detail="Incorrect email or password",
|
| 437 |
-
headers={"WWW-Authenticate": "Bearer"},
|
| 438 |
-
)
|
| 439 |
-
|
| 440 |
-
user_id = user["user_id"]
|
| 441 |
-
token = create_access_token(data={"sub": user_id, "email": user["email"]})
|
| 442 |
-
|
| 443 |
-
return {
|
| 444 |
-
"access_token": token,
|
| 445 |
-
"token_type": "bearer",
|
| 446 |
-
"user_id": user_id,
|
| 447 |
-
"email": user["email"]
|
| 448 |
-
}
|
|
|
|
| 15 |
|
| 16 |
from fastapi import FastAPI, HTTPException, status, Depends, Request
|
| 17 |
from fastapi.responses import JSONResponse, StreamingResponse
|
| 18 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 19 |
from slowapi import _rate_limit_exceeded_handler
|
| 20 |
from slowapi.errors import RateLimitExceeded
|
| 21 |
from app.core.limiter import limiter
|
|
|
|
| 73 |
lifespan=lifespan
|
| 74 |
)
|
| 75 |
|
| 76 |
+
# CORS Configuration
|
| 77 |
+
app.add_middleware(
|
| 78 |
+
CORSMiddleware,
|
| 79 |
+
allow_origins=["*"], # In production, replace with specific domains
|
| 80 |
+
allow_credentials=True,
|
| 81 |
+
allow_methods=["*"],
|
| 82 |
+
allow_headers=["*"],
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
# Global Exception Handler (Catch-All)
|
| 86 |
@app.exception_handler(Exception)
|
| 87 |
async def global_exception_handler(request: Request, exc: Exception):
|
|
|
|
| 395 |
if __name__ == "__main__":
|
| 396 |
import uvicorn
|
| 397 |
uvicorn.run(app, host="0.0.0.0", port=8000)
|
| 398 |
+
# ── Auth Endpoints (DECOMMISSIONED - Use Firebase) ──────────────────────────
|
| 399 |
+
|
| 400 |
+
@app.post("/auth/signup")
|
| 401 |
+
async def signup():
|
| 402 |
+
"""Signups are now handled by Firebase on the frontend."""
|
| 403 |
+
raise HTTPException(
|
| 404 |
+
status_code=status.HTTP_410_GONE,
|
| 405 |
+
detail="Local signup is decommissioned. Please use Firebase Auth."
|
| 406 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 407 |
|
| 408 |
+
@app.post("/auth/login")
|
| 409 |
+
async def login():
|
| 410 |
+
"""Login is now handled by Firebase on the frontend."""
|
| 411 |
+
raise HTTPException(
|
| 412 |
+
status_code=status.HTTP_410_GONE,
|
| 413 |
+
detail="Local login is decommissioned. Please use Firebase Auth."
|
| 414 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/core/orchestrator.py
CHANGED
|
@@ -61,7 +61,12 @@ class Orchestrator:
|
|
| 61 |
"status": "success",
|
| 62 |
"source": "agent",
|
| 63 |
"answer": "",
|
| 64 |
-
"metadata": {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
}
|
| 66 |
|
| 67 |
try:
|
|
|
|
| 61 |
"status": "success",
|
| 62 |
"source": "agent",
|
| 63 |
"answer": "",
|
| 64 |
+
"metadata": {
|
| 65 |
+
"latency_ms": 0,
|
| 66 |
+
"model": "gemini-2.5-flash",
|
| 67 |
+
"tools_used": [],
|
| 68 |
+
"logic_trace": []
|
| 69 |
+
},
|
| 70 |
}
|
| 71 |
|
| 72 |
try:
|
app/core/security.py
CHANGED
|
@@ -1,16 +1,33 @@
|
|
| 1 |
import logging
|
|
|
|
|
|
|
| 2 |
from fastapi import HTTPException, status, Security
|
| 3 |
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
| 4 |
from app.core.settings import settings
|
| 5 |
-
from app.core.auth_utils import decode_access_token
|
| 6 |
|
| 7 |
logger = logging.getLogger(__name__)
|
| 8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
security = HTTPBearer()
|
| 10 |
|
| 11 |
def verify_token(credentials: HTTPAuthorizationCredentials = Security(security)):
|
| 12 |
"""
|
| 13 |
-
Verifies the
|
| 14 |
Returns the decoded token dict if valid.
|
| 15 |
"""
|
| 16 |
token = credentials.credentials
|
|
@@ -24,21 +41,27 @@ def verify_token(credentials: HTTPAuthorizationCredentials = Security(security))
|
|
| 24 |
logger.info(f"Using MOCK AUTH for token: {token}")
|
| 25 |
return {"uid": "dev_user_123", "email": "dev@mathminds.ai"}
|
| 26 |
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
return {
|
| 32 |
-
"uid":
|
| 33 |
-
"email":
|
| 34 |
}
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
|
| 43 |
def get_current_user(token: dict = Security(verify_token)):
|
| 44 |
"""
|
|
|
|
| 1 |
import logging
|
| 2 |
+
import firebase_admin
|
| 3 |
+
from firebase_admin import auth, credentials as firebase_credentials
|
| 4 |
from fastapi import HTTPException, status, Security
|
| 5 |
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
| 6 |
from app.core.settings import settings
|
|
|
|
| 7 |
|
| 8 |
logger = logging.getLogger(__name__)
|
| 9 |
|
| 10 |
+
# Initialize Firebase Admin
|
| 11 |
+
_firebase_initialized = False
|
| 12 |
+
try:
|
| 13 |
+
if settings.FIREBASE_CREDENTIALS_PATH:
|
| 14 |
+
cred = firebase_credentials.Certificate(settings.FIREBASE_CREDENTIALS_PATH)
|
| 15 |
+
firebase_admin.initialize_app(cred)
|
| 16 |
+
_firebase_initialized = True
|
| 17 |
+
logger.info("Firebase Admin initialized successfully.")
|
| 18 |
+
else:
|
| 19 |
+
# Try default/env initialization
|
| 20 |
+
firebase_admin.initialize_app()
|
| 21 |
+
_firebase_initialized = True
|
| 22 |
+
logger.info("Firebase Admin initialized using default credentials.")
|
| 23 |
+
except Exception as e:
|
| 24 |
+
logger.warning(f"Firebase Admin initialization skipped or failed: {e}")
|
| 25 |
+
|
| 26 |
security = HTTPBearer()
|
| 27 |
|
| 28 |
def verify_token(credentials: HTTPAuthorizationCredentials = Security(security)):
|
| 29 |
"""
|
| 30 |
+
Verifies the Firebase ID Token.
|
| 31 |
Returns the decoded token dict if valid.
|
| 32 |
"""
|
| 33 |
token = credentials.credentials
|
|
|
|
| 41 |
logger.info(f"Using MOCK AUTH for token: {token}")
|
| 42 |
return {"uid": "dev_user_123", "email": "dev@mathminds.ai"}
|
| 43 |
|
| 44 |
+
if not _firebase_initialized:
|
| 45 |
+
logger.error("Attempted to verify token but Firebase is not initialized.")
|
| 46 |
+
raise HTTPException(
|
| 47 |
+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
| 48 |
+
detail="Authentication service unavailable",
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
try:
|
| 52 |
+
# Verify the ID token from Firebase
|
| 53 |
+
decoded_token = auth.verify_id_token(token)
|
| 54 |
return {
|
| 55 |
+
"uid": decoded_token.get("uid"),
|
| 56 |
+
"email": decoded_token.get("email")
|
| 57 |
}
|
| 58 |
+
except Exception as e:
|
| 59 |
+
logger.warning(f"Firebase token verification failed: {e}")
|
| 60 |
+
raise HTTPException(
|
| 61 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 62 |
+
detail="Invalid or expired authentication credentials",
|
| 63 |
+
headers={"WWW-Authenticate": "Bearer"},
|
| 64 |
+
)
|
| 65 |
|
| 66 |
def get_current_user(token: dict = Security(verify_token)):
|
| 67 |
"""
|
app/core/settings.py
CHANGED
|
@@ -60,8 +60,7 @@ class Settings(BaseSettings):
|
|
| 60 |
if not self.REDIS_URL:
|
| 61 |
raise ValueError("REDIS_URL must be set in production environment")
|
| 62 |
if not self.FIREBASE_CREDENTIALS_PATH:
|
| 63 |
-
|
| 64 |
-
pass
|
| 65 |
|
| 66 |
# Set Defaults for Development
|
| 67 |
else:
|
|
|
|
| 60 |
if not self.REDIS_URL:
|
| 61 |
raise ValueError("REDIS_URL must be set in production environment")
|
| 62 |
if not self.FIREBASE_CREDENTIALS_PATH:
|
| 63 |
+
raise ValueError("FIREBASE_CREDENTIALS_PATH must be set in production environment")
|
|
|
|
| 64 |
|
| 65 |
# Set Defaults for Development
|
| 66 |
else:
|
app/tools/web_scraper.py
CHANGED
|
@@ -18,6 +18,18 @@ def run_playwright_sync(query: str, headless: bool, extraction_focus: Optional[s
|
|
| 18 |
user_agent = ua.random
|
| 19 |
|
| 20 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
with sync_playwright() as p:
|
| 22 |
browser = p.chromium.launch(headless=headless)
|
| 23 |
context = browser.new_context(
|
|
@@ -92,41 +104,45 @@ def run_playwright_sync(query: str, headless: bool, extraction_focus: Optional[s
|
|
| 92 |
|
| 93 |
class WebScraper:
|
| 94 |
"""
|
| 95 |
-
Tool for fetching live data from websites using
|
| 96 |
-
|
| 97 |
"""
|
| 98 |
|
| 99 |
def __init__(self, headless: bool = True):
|
| 100 |
self.headless = headless
|
| 101 |
-
# We use a ProcessPoolExecutor to run Playwright in a separate process.
|
| 102 |
-
# This is CRITICAL on Windows if the main process uses SelectorEventLoopPolicy,
|
| 103 |
-
# as Playwright requires ProactorEventLoopPolicy.
|
| 104 |
-
self.executor = ProcessPoolExecutor(max_workers=1)
|
| 105 |
|
| 106 |
async def scrape(self, query: str, extraction_focus: Optional[str] = None) -> Dict[str, Any]:
|
| 107 |
"""
|
| 108 |
-
|
| 109 |
-
runs the scraping logic in a separate process.
|
| 110 |
-
|
| 111 |
-
Args:
|
| 112 |
-
query: The search query or URL.
|
| 113 |
-
extraction_focus: Optional keyword to focus extraction on.
|
| 114 |
"""
|
| 115 |
-
logger.info(f"WebScraper
|
| 116 |
-
|
| 117 |
-
loop = asyncio.get_running_loop()
|
| 118 |
|
| 119 |
-
# Run in separate process
|
| 120 |
try:
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
)
|
| 125 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
except Exception as e:
|
| 127 |
-
logger.error(f"
|
| 128 |
return {
|
| 129 |
"source": "web_scraper",
|
| 130 |
-
"error": f"
|
| 131 |
"status": "error"
|
| 132 |
}
|
|
|
|
| 18 |
user_agent = ua.random
|
| 19 |
|
| 20 |
try:
|
| 21 |
+
# Check if an event loop is already running in this thread
|
| 22 |
+
import asyncio
|
| 23 |
+
try:
|
| 24 |
+
loop = asyncio.get_running_loop()
|
| 25 |
+
if loop.is_running():
|
| 26 |
+
# We are in an asyncio loop! We must use a thread or process.
|
| 27 |
+
# For Celery tasks, this shouldn't happen with solo/prefork,
|
| 28 |
+
# but for local testing it does.
|
| 29 |
+
pass
|
| 30 |
+
except RuntimeError:
|
| 31 |
+
pass
|
| 32 |
+
|
| 33 |
with sync_playwright() as p:
|
| 34 |
browser = p.chromium.launch(headless=headless)
|
| 35 |
context = browser.new_context(
|
|
|
|
| 104 |
|
| 105 |
class WebScraper:
|
| 106 |
"""
|
| 107 |
+
Tool for fetching live data from websites using a Celery task queue.
|
| 108 |
+
Offloads heavy browser automation to dedicated workers.
|
| 109 |
"""
|
| 110 |
|
| 111 |
def __init__(self, headless: bool = True):
|
| 112 |
self.headless = headless
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
async def scrape(self, query: str, extraction_focus: Optional[str] = None) -> Dict[str, Any]:
|
| 115 |
"""
|
| 116 |
+
Dispatches a scraping task to Celery and waits for the result.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
"""
|
| 118 |
+
logger.info(f"WebScraper: Dispatching Celery task for query: {query}")
|
|
|
|
|
|
|
| 119 |
|
|
|
|
| 120 |
try:
|
| 121 |
+
from app.worker.tasks import scrape_task
|
| 122 |
+
|
| 123 |
+
# Dispatch to worker
|
| 124 |
+
task = scrape_task.delay(query, self.headless, extraction_focus)
|
| 125 |
+
|
| 126 |
+
# Wait for result (blocking the coroutine, but not the event loop)
|
| 127 |
+
# We use a loop/sleep or better, run_in_executor to not block the event loop if .get() is blocking.
|
| 128 |
+
# Celery's AsyncResult.get() is blocking.
|
| 129 |
+
|
| 130 |
+
import asyncio
|
| 131 |
+
for _ in range(30): # 30 seconds timeout
|
| 132 |
+
if task.ready():
|
| 133 |
+
return task.result
|
| 134 |
+
await asyncio.sleep(1)
|
| 135 |
+
|
| 136 |
+
return {
|
| 137 |
+
"source": "web_scraper",
|
| 138 |
+
"error": "Scraping task timed out in worker queue.",
|
| 139 |
+
"status": "error"
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
except Exception as e:
|
| 143 |
+
logger.error(f"Celery dispatch failed: {e}")
|
| 144 |
return {
|
| 145 |
"source": "web_scraper",
|
| 146 |
+
"error": f"Celery dispatch failed: {str(e)}",
|
| 147 |
"status": "error"
|
| 148 |
}
|
app/worker.py
DELETED
|
@@ -1,59 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
import asyncio
|
| 3 |
-
from celery import Celery
|
| 4 |
-
from app.core.settings import settings
|
| 5 |
-
from app.tools.web_scraper import WebScraper
|
| 6 |
-
import logging
|
| 7 |
-
|
| 8 |
-
# Configure Logging
|
| 9 |
-
logger = logging.getLogger(__name__)
|
| 10 |
-
|
| 11 |
-
# Initialize Celery
|
| 12 |
-
celery_app = Celery(
|
| 13 |
-
"mathminds_worker",
|
| 14 |
-
broker=settings.REDIS_URL,
|
| 15 |
-
backend=settings.REDIS_URL
|
| 16 |
-
)
|
| 17 |
-
|
| 18 |
-
celery_app.conf.update(
|
| 19 |
-
task_serializer="json",
|
| 20 |
-
accept_content=["json"],
|
| 21 |
-
result_serializer="json",
|
| 22 |
-
timezone="UTC",
|
| 23 |
-
enable_utc=True,
|
| 24 |
-
)
|
| 25 |
-
|
| 26 |
-
@celery_app.task(name="scrape_web_task", bind=True)
|
| 27 |
-
def scrape_web_task(self, query: str, focus: str = ""):
|
| 28 |
-
"""
|
| 29 |
-
Celery task to run web scraping in a background worker.
|
| 30 |
-
Since Playwright is async/sync hybrid, we run the sync version here
|
| 31 |
-
or manage the loop carefully.
|
| 32 |
-
"""
|
| 33 |
-
logger.info(f"Worker: Starting scrape for '{query}'")
|
| 34 |
-
|
| 35 |
-
# We use the sync logic of the scraper tools or run the async one via asyncio.run
|
| 36 |
-
# For simplicity/stability in Celery, we'll instantiate the scraper and run.
|
| 37 |
-
|
| 38 |
-
# Note: WebScraper class uses ProcessPoolExecutor internally for safety on Windows
|
| 39 |
-
# Here we are already in a worker process, so we can just run it.
|
| 40 |
-
|
| 41 |
-
scraper = WebScraper(headless=True)
|
| 42 |
-
|
| 43 |
-
# Run async scrape in this sync task
|
| 44 |
-
try:
|
| 45 |
-
loop = asyncio.new_event_loop()
|
| 46 |
-
asyncio.set_event_loop(loop)
|
| 47 |
-
result = loop.run_until_complete(scraper.scrape(query, extraction_focus=focus))
|
| 48 |
-
loop.close()
|
| 49 |
-
return result
|
| 50 |
-
except Exception as e:
|
| 51 |
-
logger.error(f"Worker Scrape Failed: {e}")
|
| 52 |
-
return {"error": str(e), "status": "error"}
|
| 53 |
-
|
| 54 |
-
@celery_app.task(name="solve_heavy_math_task")
|
| 55 |
-
def solve_heavy_math_task(problem_text: str):
|
| 56 |
-
"""
|
| 57 |
-
Placeholder for really heavy symbolic computation if needed.
|
| 58 |
-
"""
|
| 59 |
-
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docker-compose.yml
CHANGED
|
@@ -11,43 +11,64 @@ services:
|
|
| 11 |
environment:
|
| 12 |
- REDIS_URL=redis://redis:6379/0
|
| 13 |
- MONGO_URI=mongodb://mongo:27017/mathminds
|
|
|
|
| 14 |
depends_on:
|
| 15 |
-
|
| 16 |
-
|
|
|
|
|
|
|
| 17 |
networks:
|
| 18 |
- mathminds_net
|
| 19 |
restart: unless-stopped
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
worker:
|
| 22 |
build: .
|
| 23 |
container_name: mathminds_worker
|
| 24 |
-
command: celery -A app.worker.celery_app worker --loglevel=info
|
| 25 |
env_file:
|
| 26 |
- .env
|
| 27 |
environment:
|
| 28 |
- REDIS_URL=redis://redis:6379/0
|
| 29 |
- MONGO_URI=mongodb://mongo:27017/mathminds
|
|
|
|
| 30 |
depends_on:
|
| 31 |
-
|
| 32 |
-
|
|
|
|
|
|
|
| 33 |
networks:
|
| 34 |
- mathminds_net
|
| 35 |
restart: unless-stopped
|
| 36 |
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
|
|
|
|
|
|
| 40 |
ports:
|
| 41 |
-
- "
|
|
|
|
|
|
|
| 42 |
environment:
|
| 43 |
-
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
- n8n_data:/home/node/.n8n
|
| 48 |
networks:
|
| 49 |
- mathminds_net
|
| 50 |
restart: unless-stopped
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
redis:
|
| 53 |
image: redis:alpine
|
|
@@ -59,6 +80,11 @@ services:
|
|
| 59 |
networks:
|
| 60 |
- mathminds_net
|
| 61 |
restart: unless-stopped
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
mongo:
|
| 64 |
image: mongo:latest
|
|
@@ -70,6 +96,11 @@ services:
|
|
| 70 |
networks:
|
| 71 |
- mathminds_net
|
| 72 |
restart: unless-stopped
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
networks:
|
| 75 |
mathminds_net:
|
|
@@ -78,4 +109,3 @@ networks:
|
|
| 78 |
volumes:
|
| 79 |
redis_data:
|
| 80 |
mongo_data:
|
| 81 |
-
n8n_data:
|
|
|
|
| 11 |
environment:
|
| 12 |
- REDIS_URL=redis://redis:6379/0
|
| 13 |
- MONGO_URI=mongodb://mongo:27017/mathminds
|
| 14 |
+
- ENV=production
|
| 15 |
depends_on:
|
| 16 |
+
redis:
|
| 17 |
+
condition: service_healthy
|
| 18 |
+
mongo:
|
| 19 |
+
condition: service_healthy
|
| 20 |
networks:
|
| 21 |
- mathminds_net
|
| 22 |
restart: unless-stopped
|
| 23 |
+
healthcheck:
|
| 24 |
+
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
| 25 |
+
interval: 30s
|
| 26 |
+
timeout: 10s
|
| 27 |
+
retries: 3
|
| 28 |
+
start_period: 10s
|
| 29 |
|
| 30 |
worker:
|
| 31 |
build: .
|
| 32 |
container_name: mathminds_worker
|
| 33 |
+
command: celery -A app.worker.celery_app worker --loglevel=info --pool=solo
|
| 34 |
env_file:
|
| 35 |
- .env
|
| 36 |
environment:
|
| 37 |
- REDIS_URL=redis://redis:6379/0
|
| 38 |
- MONGO_URI=mongodb://mongo:27017/mathminds
|
| 39 |
+
- ENV=production
|
| 40 |
depends_on:
|
| 41 |
+
redis:
|
| 42 |
+
condition: service_healthy
|
| 43 |
+
mongo:
|
| 44 |
+
condition: service_healthy
|
| 45 |
networks:
|
| 46 |
- mathminds_net
|
| 47 |
restart: unless-stopped
|
| 48 |
|
| 49 |
+
frontend:
|
| 50 |
+
build:
|
| 51 |
+
context: .
|
| 52 |
+
dockerfile: frontend/Dockerfile
|
| 53 |
+
container_name: mathminds_frontend
|
| 54 |
ports:
|
| 55 |
+
- "8501:8501"
|
| 56 |
+
env_file:
|
| 57 |
+
- .env
|
| 58 |
environment:
|
| 59 |
+
- BACKEND_URL=http://api:8000
|
| 60 |
+
depends_on:
|
| 61 |
+
api:
|
| 62 |
+
condition: service_healthy
|
|
|
|
| 63 |
networks:
|
| 64 |
- mathminds_net
|
| 65 |
restart: unless-stopped
|
| 66 |
+
healthcheck:
|
| 67 |
+
test: ["CMD", "curl", "-f", "http://localhost:8501/_stcore/health"]
|
| 68 |
+
interval: 30s
|
| 69 |
+
timeout: 10s
|
| 70 |
+
retries: 3
|
| 71 |
+
start_period: 10s
|
| 72 |
|
| 73 |
redis:
|
| 74 |
image: redis:alpine
|
|
|
|
| 80 |
networks:
|
| 81 |
- mathminds_net
|
| 82 |
restart: unless-stopped
|
| 83 |
+
healthcheck:
|
| 84 |
+
test: ["CMD", "redis-cli", "ping"]
|
| 85 |
+
interval: 10s
|
| 86 |
+
timeout: 5s
|
| 87 |
+
retries: 5
|
| 88 |
|
| 89 |
mongo:
|
| 90 |
image: mongo:latest
|
|
|
|
| 96 |
networks:
|
| 97 |
- mathminds_net
|
| 98 |
restart: unless-stopped
|
| 99 |
+
healthcheck:
|
| 100 |
+
test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
|
| 101 |
+
interval: 10s
|
| 102 |
+
timeout: 5s
|
| 103 |
+
retries: 5
|
| 104 |
|
| 105 |
networks:
|
| 106 |
mathminds_net:
|
|
|
|
| 109 |
volumes:
|
| 110 |
redis_data:
|
| 111 |
mongo_data:
|
|
|
frontend/app.py
CHANGED
|
@@ -8,6 +8,7 @@ import uuid
|
|
| 8 |
import time
|
| 9 |
from streamlit_drawable_canvas import st_canvas
|
| 10 |
from dotenv import load_dotenv
|
|
|
|
| 11 |
|
| 12 |
load_dotenv()
|
| 13 |
|
|
@@ -300,26 +301,20 @@ def login_screen():
|
|
| 300 |
if st.form_submit_button("Sign In", use_container_width=True):
|
| 301 |
if email and password:
|
| 302 |
try:
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
json={"email": email, "password": password},
|
| 306 |
-
timeout=30
|
| 307 |
-
)
|
| 308 |
-
if r.status_code == 200:
|
| 309 |
-
d = r.json()
|
| 310 |
# ✅ MULTIUSER FIX: Clear ALL previous user data
|
| 311 |
-
# BEFORE setting the new user identity.
|
| 312 |
_clear_user_state()
|
| 313 |
st.session_state.user = {
|
| 314 |
-
"email":
|
| 315 |
-
"token":
|
| 316 |
-
"uid":
|
| 317 |
}
|
| 318 |
-
st.success(f"Welcome back, {
|
| 319 |
time.sleep(0.5)
|
| 320 |
st.rerun()
|
| 321 |
else:
|
| 322 |
-
st.error(f"Login Failed: {
|
| 323 |
except Exception as e:
|
| 324 |
st.error(f"Connection Error: {e}")
|
| 325 |
else:
|
|
@@ -337,29 +332,20 @@ def login_screen():
|
|
| 337 |
st.error("Passwords do not match!")
|
| 338 |
else:
|
| 339 |
try:
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
json={
|
| 343 |
-
"email": new_email,
|
| 344 |
-
"password": new_password,
|
| 345 |
-
"full_name": full_name
|
| 346 |
-
},
|
| 347 |
-
timeout=30
|
| 348 |
-
)
|
| 349 |
-
if r.status_code == 200:
|
| 350 |
-
d = r.json()
|
| 351 |
# ✅ MULTIUSER FIX: Same as login — clear first
|
| 352 |
_clear_user_state()
|
| 353 |
st.session_state.user = {
|
| 354 |
-
"email":
|
| 355 |
-
"token":
|
| 356 |
-
"uid":
|
| 357 |
}
|
| 358 |
-
st.success(f"Account Created! Welcome, {
|
| 359 |
time.sleep(0.5)
|
| 360 |
st.rerun()
|
| 361 |
else:
|
| 362 |
-
st.error(f"Sign Up Failed: {
|
| 363 |
except Exception as e:
|
| 364 |
st.error(f"Connection Error: {e}")
|
| 365 |
else:
|
|
|
|
| 8 |
import time
|
| 9 |
from streamlit_drawable_canvas import st_canvas
|
| 10 |
from dotenv import load_dotenv
|
| 11 |
+
from firebase_utils import sign_in_with_email, sign_up_with_email
|
| 12 |
|
| 13 |
load_dotenv()
|
| 14 |
|
|
|
|
| 301 |
if st.form_submit_button("Sign In", use_container_width=True):
|
| 302 |
if email and password:
|
| 303 |
try:
|
| 304 |
+
token, uid, user_email, error = sign_in_with_email(email, password)
|
| 305 |
+
if token:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 306 |
# ✅ MULTIUSER FIX: Clear ALL previous user data
|
|
|
|
| 307 |
_clear_user_state()
|
| 308 |
st.session_state.user = {
|
| 309 |
+
"email": user_email,
|
| 310 |
+
"token": token,
|
| 311 |
+
"uid": uid
|
| 312 |
}
|
| 313 |
+
st.success(f"Welcome back, {user_email}!")
|
| 314 |
time.sleep(0.5)
|
| 315 |
st.rerun()
|
| 316 |
else:
|
| 317 |
+
st.error(f"Login Failed: {error}")
|
| 318 |
except Exception as e:
|
| 319 |
st.error(f"Connection Error: {e}")
|
| 320 |
else:
|
|
|
|
| 332 |
st.error("Passwords do not match!")
|
| 333 |
else:
|
| 334 |
try:
|
| 335 |
+
token, uid, user_email, error = sign_up_with_email(new_email, new_password)
|
| 336 |
+
if token:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 337 |
# ✅ MULTIUSER FIX: Same as login — clear first
|
| 338 |
_clear_user_state()
|
| 339 |
st.session_state.user = {
|
| 340 |
+
"email": user_email,
|
| 341 |
+
"token": token,
|
| 342 |
+
"uid": uid
|
| 343 |
}
|
| 344 |
+
st.success(f"Account Created! Welcome, {user_email}!")
|
| 345 |
time.sleep(0.5)
|
| 346 |
st.rerun()
|
| 347 |
else:
|
| 348 |
+
st.error(f"Sign Up Failed: {error}")
|
| 349 |
except Exception as e:
|
| 350 |
st.error(f"Connection Error: {e}")
|
| 351 |
else:
|