Spaces:
Running
Running
Commit
Β·
c8243d5
1
Parent(s):
88e835f
Implement conditional authentication middleware and OAuth routes for Hugging Face Spaces integration. Enhance environment debugging and add environment info endpoint. Update README with OAuth configuration details and improve startup logging.
Browse files- README.md +7 -2
- backend/app.py +24 -0
- backend/middleware/__init__.py +7 -0
- backend/middleware/auth.py +130 -0
- backend/routers/auth.py +208 -0
- backend/routers/observability.py +12 -0
- main.py +2 -0
- utils/environment.py +121 -0
README.md
CHANGED
|
@@ -7,6 +7,11 @@ sdk: docker
|
|
| 7 |
pinned: false
|
| 8 |
license: mit
|
| 9 |
app_port: 7860
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
---
|
| 11 |
|
| 12 |
# πΈοΈ AgentGraph
|
|
@@ -21,7 +26,7 @@ The easiest way to get started is using our setup script:
|
|
| 21 |
|
| 22 |
```bash
|
| 23 |
# 1. Clone the repository
|
| 24 |
-
git clone
|
| 25 |
cd AgentGraph
|
| 26 |
|
| 27 |
# 2. Run the setup script
|
|
@@ -41,7 +46,7 @@ If you prefer manual control:
|
|
| 41 |
|
| 42 |
```bash
|
| 43 |
# 1. Clone and setup environment
|
| 44 |
-
git clone
|
| 45 |
cd AgentGraph
|
| 46 |
cp .env.example .env
|
| 47 |
# Edit .env and add your OpenAI API key
|
|
|
|
| 7 |
pinned: false
|
| 8 |
license: mit
|
| 9 |
app_port: 7860
|
| 10 |
+
hf_oauth: true
|
| 11 |
+
hf_oauth_scopes:
|
| 12 |
+
- openid
|
| 13 |
+
- profile
|
| 14 |
+
hf_oauth_expiration_minutes: 480
|
| 15 |
---
|
| 16 |
|
| 17 |
# πΈοΈ AgentGraph
|
|
|
|
| 26 |
|
| 27 |
```bash
|
| 28 |
# 1. Clone the repository
|
| 29 |
+
git clone https://huggingface.co/spaces/holistic-ai/AgentGraph
|
| 30 |
cd AgentGraph
|
| 31 |
|
| 32 |
# 2. Run the setup script
|
|
|
|
| 46 |
|
| 47 |
```bash
|
| 48 |
# 1. Clone and setup environment
|
| 49 |
+
git clone https://huggingface.co/spaces/holistic-ai/AgentGraph
|
| 50 |
cd AgentGraph
|
| 51 |
cp .env.example .env
|
| 52 |
# Edit .env and add your OpenAI API key
|
backend/app.py
CHANGED
|
@@ -10,7 +10,10 @@ import sys
|
|
| 10 |
from fastapi import FastAPI, Request, status
|
| 11 |
from fastapi.staticfiles import StaticFiles
|
| 12 |
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
| 13 |
from fastapi.responses import RedirectResponse, HTMLResponse
|
|
|
|
|
|
|
| 14 |
|
| 15 |
|
| 16 |
# Add server module to path if not already there
|
|
@@ -30,6 +33,7 @@ from backend.routers import (
|
|
| 30 |
example_traces,
|
| 31 |
methods,
|
| 32 |
observability,
|
|
|
|
| 33 |
)
|
| 34 |
|
| 35 |
# Setup logging
|
|
@@ -38,6 +42,16 @@ logger = logging.getLogger("agent_monitoring_server")
|
|
| 38 |
# Create FastAPI app
|
| 39 |
app = FastAPI(title="Agent Monitoring System", version="1.0.0")
|
| 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
# Add CORS middleware
|
| 42 |
app.add_middleware(
|
| 43 |
CORSMiddleware,
|
|
@@ -51,6 +65,7 @@ app.add_middleware(
|
|
| 51 |
app.mount("/data", StaticFiles(directory="datasets"), name="data")
|
| 52 |
|
| 53 |
# Include routers
|
|
|
|
| 54 |
app.include_router(traces.router)
|
| 55 |
app.include_router(knowledge_graphs.router)
|
| 56 |
app.include_router(agentgraph.router)
|
|
@@ -69,6 +84,9 @@ async def startup_event():
|
|
| 69 |
"""Start background services on app startup"""
|
| 70 |
logger.info("β
Backend server starting...")
|
| 71 |
|
|
|
|
|
|
|
|
|
|
| 72 |
# π§ Create necessary directories
|
| 73 |
ensure_directories()
|
| 74 |
logger.info("π Directory structure created")
|
|
@@ -82,6 +100,12 @@ async def startup_event():
|
|
| 82 |
logger.error(f"β Database initialization failed: {e}")
|
| 83 |
# Don't fail startup - continue with empty database
|
| 84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
logger.info("π Backend API available at: http://0.0.0.0:7860")
|
| 86 |
# scheduler_service.start() # This line is now commented out
|
| 87 |
|
|
|
|
| 10 |
from fastapi import FastAPI, Request, status
|
| 11 |
from fastapi.staticfiles import StaticFiles
|
| 12 |
from fastapi.middleware.cors import CORSMiddleware
|
| 13 |
+
from starlette.middleware.sessions import SessionMiddleware
|
| 14 |
from fastapi.responses import RedirectResponse, HTMLResponse
|
| 15 |
+
from backend.middleware.auth import ConditionalAuthMiddleware
|
| 16 |
+
from utils.environment import should_enable_auth, debug_environment
|
| 17 |
|
| 18 |
|
| 19 |
# Add server module to path if not already there
|
|
|
|
| 33 |
example_traces,
|
| 34 |
methods,
|
| 35 |
observability,
|
| 36 |
+
auth,
|
| 37 |
)
|
| 38 |
|
| 39 |
# Setup logging
|
|
|
|
| 42 |
# Create FastAPI app
|
| 43 |
app = FastAPI(title="Agent Monitoring System", version="1.0.0")
|
| 44 |
|
| 45 |
+
# Add session middleware (required for OAuth)
|
| 46 |
+
app.add_middleware(
|
| 47 |
+
SessionMiddleware,
|
| 48 |
+
secret_key=os.getenv("SESSION_SECRET_KEY", "your-secret-key-change-in-production"),
|
| 49 |
+
max_age=86400, # 24 hours
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
# Add conditional authentication middleware
|
| 53 |
+
app.add_middleware(ConditionalAuthMiddleware)
|
| 54 |
+
|
| 55 |
# Add CORS middleware
|
| 56 |
app.add_middleware(
|
| 57 |
CORSMiddleware,
|
|
|
|
| 65 |
app.mount("/data", StaticFiles(directory="datasets"), name="data")
|
| 66 |
|
| 67 |
# Include routers
|
| 68 |
+
app.include_router(auth.router) # Add auth router first
|
| 69 |
app.include_router(traces.router)
|
| 70 |
app.include_router(knowledge_graphs.router)
|
| 71 |
app.include_router(agentgraph.router)
|
|
|
|
| 84 |
"""Start background services on app startup"""
|
| 85 |
logger.info("β
Backend server starting...")
|
| 86 |
|
| 87 |
+
# π Debug environment information
|
| 88 |
+
debug_environment()
|
| 89 |
+
|
| 90 |
# π§ Create necessary directories
|
| 91 |
ensure_directories()
|
| 92 |
logger.info("π Directory structure created")
|
|
|
|
| 100 |
logger.error(f"β Database initialization failed: {e}")
|
| 101 |
# Don't fail startup - continue with empty database
|
| 102 |
|
| 103 |
+
# π Log authentication status
|
| 104 |
+
if should_enable_auth():
|
| 105 |
+
logger.info("π Authentication ENABLED (HF Spaces environment)")
|
| 106 |
+
else:
|
| 107 |
+
logger.info("π Authentication DISABLED (Local development)")
|
| 108 |
+
|
| 109 |
logger.info("π Backend API available at: http://0.0.0.0:7860")
|
| 110 |
# scheduler_service.start() # This line is now commented out
|
| 111 |
|
backend/middleware/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Middleware package for AgentGraph backend.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from .auth import ConditionalAuthMiddleware
|
| 6 |
+
|
| 7 |
+
__all__ = ["ConditionalAuthMiddleware"]
|
backend/middleware/auth.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Conditional Authentication Middleware
|
| 3 |
+
|
| 4 |
+
This middleware only enables authentication when running in Hugging Face Spaces.
|
| 5 |
+
Local development bypasses authentication entirely.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
import logging
|
| 10 |
+
from typing import Optional, Dict, Any
|
| 11 |
+
from fastapi import Request, Response
|
| 12 |
+
from starlette.middleware.base import BaseHTTPMiddleware
|
| 13 |
+
from starlette.responses import RedirectResponse, JSONResponse
|
| 14 |
+
from utils.environment import should_enable_auth, get_oauth_config, is_huggingface_space
|
| 15 |
+
|
| 16 |
+
logger = logging.getLogger(__name__)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class ConditionalAuthMiddleware(BaseHTTPMiddleware):
|
| 20 |
+
"""
|
| 21 |
+
Middleware that conditionally enables authentication based on deployment environment.
|
| 22 |
+
|
| 23 |
+
- In HF Spaces: Full OAuth authentication required
|
| 24 |
+
- In local development: Authentication bypassed
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
def __init__(self, app, excluded_paths: Optional[list] = None):
|
| 28 |
+
super().__init__(app)
|
| 29 |
+
|
| 30 |
+
# Paths that don't require authentication even in HF Spaces
|
| 31 |
+
self.excluded_paths = excluded_paths or [
|
| 32 |
+
"/",
|
| 33 |
+
"/docs",
|
| 34 |
+
"/redoc",
|
| 35 |
+
"/openapi.json",
|
| 36 |
+
"/api/observability/health-check",
|
| 37 |
+
"/api/observability/environment",
|
| 38 |
+
"/auth/login",
|
| 39 |
+
"/auth/callback",
|
| 40 |
+
"/auth/logout",
|
| 41 |
+
"/assets/",
|
| 42 |
+
"/static/",
|
| 43 |
+
"/agentgraph", # Allow React app to load
|
| 44 |
+
]
|
| 45 |
+
|
| 46 |
+
# Check if auth should be enabled
|
| 47 |
+
self.auth_enabled = should_enable_auth()
|
| 48 |
+
self.oauth_config = get_oauth_config() if self.auth_enabled else None
|
| 49 |
+
|
| 50 |
+
# Log auth status
|
| 51 |
+
if self.auth_enabled:
|
| 52 |
+
logger.info("π Authentication middleware ENABLED (HF Spaces environment)")
|
| 53 |
+
if not self.oauth_config:
|
| 54 |
+
logger.warning("β οΈ OAuth configuration not found in HF Spaces environment")
|
| 55 |
+
else:
|
| 56 |
+
logger.info("π Authentication middleware DISABLED (Local development)")
|
| 57 |
+
|
| 58 |
+
async def dispatch(self, request: Request, call_next):
|
| 59 |
+
"""
|
| 60 |
+
Process request through conditional authentication.
|
| 61 |
+
"""
|
| 62 |
+
# If auth is disabled (local dev), bypass all authentication
|
| 63 |
+
if not self.auth_enabled:
|
| 64 |
+
return await call_next(request)
|
| 65 |
+
|
| 66 |
+
# If auth is enabled but OAuth not properly configured, log warning and continue
|
| 67 |
+
if not self.oauth_config:
|
| 68 |
+
logger.warning("OAuth not configured properly, bypassing auth")
|
| 69 |
+
return await call_next(request)
|
| 70 |
+
|
| 71 |
+
# Check if path is excluded from authentication
|
| 72 |
+
if self._is_excluded_path(request.url.path):
|
| 73 |
+
return await call_next(request)
|
| 74 |
+
|
| 75 |
+
# Check user authentication
|
| 76 |
+
user = await self._get_current_user(request)
|
| 77 |
+
if not user:
|
| 78 |
+
# For API calls, return JSON error
|
| 79 |
+
if request.url.path.startswith("/api/"):
|
| 80 |
+
return JSONResponse(
|
| 81 |
+
status_code=401,
|
| 82 |
+
content={"error": "Authentication required", "login_url": "/auth/login"}
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
# For web requests, redirect to login
|
| 86 |
+
return RedirectResponse(url="/auth/login", status_code=302)
|
| 87 |
+
|
| 88 |
+
# Add user info to request state
|
| 89 |
+
request.state.user = user
|
| 90 |
+
return await call_next(request)
|
| 91 |
+
|
| 92 |
+
def _is_excluded_path(self, path: str) -> bool:
|
| 93 |
+
"""Check if the request path should bypass authentication."""
|
| 94 |
+
return any(
|
| 95 |
+
path.startswith(excluded_path)
|
| 96 |
+
for excluded_path in self.excluded_paths
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
async def _get_current_user(self, request: Request) -> Optional[Dict[str, Any]]:
|
| 100 |
+
"""
|
| 101 |
+
Get current user from session or token.
|
| 102 |
+
|
| 103 |
+
In a full implementation, this would:
|
| 104 |
+
1. Check session cookies
|
| 105 |
+
2. Validate JWT tokens
|
| 106 |
+
3. Call HF API to verify user info
|
| 107 |
+
|
| 108 |
+
For now, we'll implement a basic session check.
|
| 109 |
+
"""
|
| 110 |
+
# Check if user info is in session
|
| 111 |
+
user = request.session.get("user") if hasattr(request, "session") else None
|
| 112 |
+
|
| 113 |
+
# Check Authorization header as fallback
|
| 114 |
+
if not user:
|
| 115 |
+
auth_header = request.headers.get("Authorization")
|
| 116 |
+
if auth_header and auth_header.startswith("Bearer "):
|
| 117 |
+
# In a full implementation, validate this token with HF API
|
| 118 |
+
# For now, we'll assume it's valid if present
|
| 119 |
+
pass
|
| 120 |
+
|
| 121 |
+
return user
|
| 122 |
+
|
| 123 |
+
def get_auth_status(self) -> Dict[str, Any]:
|
| 124 |
+
"""Get current authentication configuration status."""
|
| 125 |
+
return {
|
| 126 |
+
"auth_enabled": self.auth_enabled,
|
| 127 |
+
"environment": "huggingface_spaces" if is_huggingface_space() else "local_development",
|
| 128 |
+
"oauth_configured": bool(self.oauth_config),
|
| 129 |
+
"excluded_paths": self.excluded_paths,
|
| 130 |
+
}
|
backend/routers/auth.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Authentication routes for Hugging Face OAuth integration.
|
| 3 |
+
|
| 4 |
+
These routes are only active when running in HF Spaces environment.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import logging
|
| 9 |
+
import secrets
|
| 10 |
+
from typing import Optional
|
| 11 |
+
from fastapi import APIRouter, Request, Response, HTTPException
|
| 12 |
+
from fastapi.responses import RedirectResponse, HTMLResponse, JSONResponse
|
| 13 |
+
from utils.environment import should_enable_auth, get_oauth_config, is_huggingface_space
|
| 14 |
+
import requests
|
| 15 |
+
|
| 16 |
+
logger = logging.getLogger(__name__)
|
| 17 |
+
|
| 18 |
+
router = APIRouter(prefix="/auth", tags=["authentication"])
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@router.get("/status")
|
| 22 |
+
async def auth_status():
|
| 23 |
+
"""Get authentication status and configuration."""
|
| 24 |
+
config = get_oauth_config()
|
| 25 |
+
return {
|
| 26 |
+
"auth_enabled": should_enable_auth(),
|
| 27 |
+
"environment": "huggingface_spaces" if is_huggingface_space() else "local_development",
|
| 28 |
+
"oauth_available": bool(config),
|
| 29 |
+
"login_required": should_enable_auth() and bool(config),
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
@router.get("/login")
|
| 34 |
+
async def login(request: Request):
|
| 35 |
+
"""
|
| 36 |
+
Initiate OAuth login flow.
|
| 37 |
+
Only available in HF Spaces environment.
|
| 38 |
+
"""
|
| 39 |
+
if not should_enable_auth():
|
| 40 |
+
return JSONResponse(
|
| 41 |
+
content={"message": "Authentication not required in local development"},
|
| 42 |
+
status_code=200
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
oauth_config = get_oauth_config()
|
| 46 |
+
if not oauth_config:
|
| 47 |
+
raise HTTPException(
|
| 48 |
+
status_code=500,
|
| 49 |
+
detail="OAuth not configured in this environment"
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
# Generate state for CSRF protection
|
| 53 |
+
state = secrets.token_urlsafe(32)
|
| 54 |
+
request.session["oauth_state"] = state
|
| 55 |
+
|
| 56 |
+
# Get the current host for redirect URI
|
| 57 |
+
base_url = str(request.base_url).rstrip('/')
|
| 58 |
+
redirect_uri = f"{base_url}/auth/callback"
|
| 59 |
+
|
| 60 |
+
# Build authorization URL
|
| 61 |
+
auth_url = (
|
| 62 |
+
f"{oauth_config['provider_url']}/oauth/authorize"
|
| 63 |
+
f"?client_id={oauth_config['client_id']}"
|
| 64 |
+
f"&redirect_uri={redirect_uri}"
|
| 65 |
+
f"&response_type=code"
|
| 66 |
+
f"&scope={oauth_config['scopes']}"
|
| 67 |
+
f"&state={state}"
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
return RedirectResponse(url=auth_url, status_code=302)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
@router.get("/callback")
|
| 74 |
+
async def oauth_callback(request: Request, code: str, state: str):
|
| 75 |
+
"""
|
| 76 |
+
Handle OAuth callback from Hugging Face.
|
| 77 |
+
"""
|
| 78 |
+
if not should_enable_auth():
|
| 79 |
+
return RedirectResponse(url="/", status_code=302)
|
| 80 |
+
|
| 81 |
+
oauth_config = get_oauth_config()
|
| 82 |
+
if not oauth_config:
|
| 83 |
+
raise HTTPException(status_code=500, detail="OAuth not configured")
|
| 84 |
+
|
| 85 |
+
# Verify state parameter (CSRF protection)
|
| 86 |
+
stored_state = request.session.get("oauth_state")
|
| 87 |
+
if not stored_state or stored_state != state:
|
| 88 |
+
raise HTTPException(status_code=400, detail="Invalid state parameter")
|
| 89 |
+
|
| 90 |
+
# Exchange code for tokens
|
| 91 |
+
base_url = str(request.base_url).rstrip('/')
|
| 92 |
+
redirect_uri = f"{base_url}/auth/callback"
|
| 93 |
+
|
| 94 |
+
try:
|
| 95 |
+
token_response = requests.post(
|
| 96 |
+
f"{oauth_config['provider_url']}/oauth/token",
|
| 97 |
+
data={
|
| 98 |
+
"grant_type": "authorization_code",
|
| 99 |
+
"code": code,
|
| 100 |
+
"redirect_uri": redirect_uri,
|
| 101 |
+
"client_id": oauth_config['client_id'],
|
| 102 |
+
"client_secret": oauth_config['client_secret'],
|
| 103 |
+
},
|
| 104 |
+
timeout=10
|
| 105 |
+
)
|
| 106 |
+
token_response.raise_for_status()
|
| 107 |
+
tokens = token_response.json()
|
| 108 |
+
|
| 109 |
+
except requests.RequestException as e:
|
| 110 |
+
logger.error(f"Token exchange failed: {e}")
|
| 111 |
+
raise HTTPException(status_code=400, detail="Token exchange failed")
|
| 112 |
+
|
| 113 |
+
access_token = tokens.get("access_token")
|
| 114 |
+
if not access_token:
|
| 115 |
+
raise HTTPException(status_code=400, detail="No access token received")
|
| 116 |
+
|
| 117 |
+
# Get user information
|
| 118 |
+
try:
|
| 119 |
+
user_response = requests.get(
|
| 120 |
+
f"{oauth_config['provider_url']}/api/whoami-v2",
|
| 121 |
+
headers={"Authorization": f"Bearer {access_token}"},
|
| 122 |
+
timeout=10
|
| 123 |
+
)
|
| 124 |
+
user_response.raise_for_status()
|
| 125 |
+
user_info = user_response.json()
|
| 126 |
+
|
| 127 |
+
except requests.RequestException as e:
|
| 128 |
+
logger.error(f"User info fetch failed: {e}")
|
| 129 |
+
raise HTTPException(status_code=400, detail="Failed to fetch user information")
|
| 130 |
+
|
| 131 |
+
# Store user in session
|
| 132 |
+
request.session["user"] = {
|
| 133 |
+
"id": user_info.get("id"),
|
| 134 |
+
"name": user_info.get("name"),
|
| 135 |
+
"username": user_info.get("login"), # HF username
|
| 136 |
+
"email": user_info.get("email"),
|
| 137 |
+
"avatar_url": user_info.get("avatarUrl"),
|
| 138 |
+
"access_token": access_token, # Store for future API calls if needed
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
# Clean up state
|
| 142 |
+
request.session.pop("oauth_state", None)
|
| 143 |
+
|
| 144 |
+
logger.info(f"User logged in: {user_info.get('name')} ({user_info.get('login')})")
|
| 145 |
+
|
| 146 |
+
# Redirect to main application
|
| 147 |
+
return RedirectResponse(url="/", status_code=302)
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
@router.get("/logout")
|
| 151 |
+
async def logout(request: Request):
|
| 152 |
+
"""Log out the current user."""
|
| 153 |
+
if hasattr(request, "session"):
|
| 154 |
+
request.session.clear()
|
| 155 |
+
|
| 156 |
+
return RedirectResponse(url="/", status_code=302)
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
@router.get("/user")
|
| 160 |
+
async def get_current_user(request: Request):
|
| 161 |
+
"""Get current user information."""
|
| 162 |
+
if not should_enable_auth():
|
| 163 |
+
return {"message": "Authentication disabled in local development"}
|
| 164 |
+
|
| 165 |
+
user = getattr(request.state, "user", None) or request.session.get("user")
|
| 166 |
+
if not user:
|
| 167 |
+
raise HTTPException(status_code=401, detail="Not authenticated")
|
| 168 |
+
|
| 169 |
+
# Return user info without sensitive data
|
| 170 |
+
return {
|
| 171 |
+
"id": user.get("id"),
|
| 172 |
+
"name": user.get("name"),
|
| 173 |
+
"username": user.get("username"),
|
| 174 |
+
"email": user.get("email"),
|
| 175 |
+
"avatar_url": user.get("avatar_url"),
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
@router.get("/login-page")
|
| 180 |
+
async def login_page(request: Request):
|
| 181 |
+
"""
|
| 182 |
+
Serve a simple login page for environments where auth is required.
|
| 183 |
+
"""
|
| 184 |
+
if not should_enable_auth():
|
| 185 |
+
return RedirectResponse(url="/", status_code=302)
|
| 186 |
+
|
| 187 |
+
html_content = """
|
| 188 |
+
<!DOCTYPE html>
|
| 189 |
+
<html>
|
| 190 |
+
<head>
|
| 191 |
+
<title>AgentGraph - Login Required</title>
|
| 192 |
+
<style>
|
| 193 |
+
body { font-family: Arial, sans-serif; text-align: center; margin-top: 100px; }
|
| 194 |
+
.login-container { max-width: 400px; margin: 0 auto; padding: 20px; border: 1px solid #ddd; border-radius: 8px; }
|
| 195 |
+
.login-btn { background: #ff6b35; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; font-weight: bold; }
|
| 196 |
+
</style>
|
| 197 |
+
</head>
|
| 198 |
+
<body>
|
| 199 |
+
<div class="login-container">
|
| 200 |
+
<h1>πΈοΈ AgentGraph</h1>
|
| 201 |
+
<p>Please log in with your Hugging Face account to access AgentGraph.</p>
|
| 202 |
+
<a href="/auth/login" class="login-btn">Login with Hugging Face</a>
|
| 203 |
+
</div>
|
| 204 |
+
</body>
|
| 205 |
+
</html>
|
| 206 |
+
"""
|
| 207 |
+
|
| 208 |
+
return HTMLResponse(content=html_content)
|
backend/routers/observability.py
CHANGED
|
@@ -18,6 +18,7 @@ from datetime import datetime
|
|
| 18 |
from typing import Dict, List, Optional, cast
|
| 19 |
|
| 20 |
import psutil
|
|
|
|
| 21 |
import requests
|
| 22 |
from fastapi import APIRouter, Depends, HTTPException
|
| 23 |
from fastapi.responses import JSONResponse
|
|
@@ -871,6 +872,17 @@ async def clean_up(session: Session = Depends(get_db)): # noqa: B008
|
|
| 871 |
logger.error(f"Error cleaning up resources: {str(e)}")
|
| 872 |
raise HTTPException(status_code=500, detail=f"Error cleaning up resources: {str(e)}") from e
|
| 873 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 874 |
@router.get("/health-check")
|
| 875 |
async def health_check():
|
| 876 |
"""Comprehensive health check for the system."""
|
|
|
|
| 18 |
from typing import Dict, List, Optional, cast
|
| 19 |
|
| 20 |
import psutil
|
| 21 |
+
from utils.environment import get_environment_info, debug_environment
|
| 22 |
import requests
|
| 23 |
from fastapi import APIRouter, Depends, HTTPException
|
| 24 |
from fastapi.responses import JSONResponse
|
|
|
|
| 872 |
logger.error(f"Error cleaning up resources: {str(e)}")
|
| 873 |
raise HTTPException(status_code=500, detail=f"Error cleaning up resources: {str(e)}") from e
|
| 874 |
|
| 875 |
+
@router.get("/environment")
|
| 876 |
+
async def get_environment():
|
| 877 |
+
"""Get environment information and authentication status."""
|
| 878 |
+
env_info = get_environment_info()
|
| 879 |
+
|
| 880 |
+
return {
|
| 881 |
+
"environment": env_info,
|
| 882 |
+
"timestamp": datetime.now().isoformat()
|
| 883 |
+
}
|
| 884 |
+
|
| 885 |
+
|
| 886 |
@router.get("/health-check")
|
| 887 |
async def health_check():
|
| 888 |
"""Comprehensive health check for the system."""
|
main.py
CHANGED
|
@@ -10,6 +10,7 @@ from utils.fix_litellm_stop_param import * # This applies all the patches
|
|
| 10 |
|
| 11 |
# Import configuration and debug utilities
|
| 12 |
from utils.config import validate_config, debug_config
|
|
|
|
| 13 |
|
| 14 |
# Continue with regular imports
|
| 15 |
import argparse
|
|
@@ -224,6 +225,7 @@ def main():
|
|
| 224 |
# Debug configuration on startup (but only if not just showing help)
|
| 225 |
if len(sys.argv) > 1:
|
| 226 |
debug_config()
|
|
|
|
| 227 |
if not validate_config():
|
| 228 |
logger.error("β Configuration validation failed. Please check your environment variables.")
|
| 229 |
logger.error("π‘ Tip: Copy .env.example to .env and fill in your API keys")
|
|
|
|
| 10 |
|
| 11 |
# Import configuration and debug utilities
|
| 12 |
from utils.config import validate_config, debug_config
|
| 13 |
+
from utils.environment import debug_environment as debug_env_info
|
| 14 |
|
| 15 |
# Continue with regular imports
|
| 16 |
import argparse
|
|
|
|
| 225 |
# Debug configuration on startup (but only if not just showing help)
|
| 226 |
if len(sys.argv) > 1:
|
| 227 |
debug_config()
|
| 228 |
+
debug_env_info() # Also show environment info
|
| 229 |
if not validate_config():
|
| 230 |
logger.error("β Configuration validation failed. Please check your environment variables.")
|
| 231 |
logger.error("π‘ Tip: Copy .env.example to .env and fill in your API keys")
|
utils/environment.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Environment Detection Utilities
|
| 3 |
+
|
| 4 |
+
This module provides utilities to detect the deployment environment
|
| 5 |
+
and configure authentication accordingly.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
from typing import Dict, Any, Optional
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def is_huggingface_space() -> bool:
|
| 13 |
+
"""
|
| 14 |
+
Detect if the application is running in Hugging Face Spaces.
|
| 15 |
+
|
| 16 |
+
Returns:
|
| 17 |
+
bool: True if running in HF Spaces, False otherwise
|
| 18 |
+
"""
|
| 19 |
+
# HF Spaces sets specific environment variables
|
| 20 |
+
hf_indicators = [
|
| 21 |
+
"SPACE_ID", # HF Space identifier
|
| 22 |
+
"SPACE_AUTHOR_NAME", # Space author
|
| 23 |
+
"SPACE_REPO_NAME", # Space repository name
|
| 24 |
+
"OAUTH_CLIENT_ID", # OAuth client ID (when oauth enabled)
|
| 25 |
+
]
|
| 26 |
+
|
| 27 |
+
return any(os.getenv(indicator) for indicator in hf_indicators)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def is_local_development() -> bool:
|
| 31 |
+
"""
|
| 32 |
+
Detect if the application is running in local development mode.
|
| 33 |
+
|
| 34 |
+
Returns:
|
| 35 |
+
bool: True if running locally, False otherwise
|
| 36 |
+
"""
|
| 37 |
+
return not is_huggingface_space()
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def get_environment_info() -> Dict[str, Any]:
|
| 41 |
+
"""
|
| 42 |
+
Get comprehensive environment information.
|
| 43 |
+
|
| 44 |
+
Returns:
|
| 45 |
+
Dict containing environment details
|
| 46 |
+
"""
|
| 47 |
+
env_info = {
|
| 48 |
+
"is_hf_space": is_huggingface_space(),
|
| 49 |
+
"is_local_dev": is_local_development(),
|
| 50 |
+
"space_id": os.getenv("SPACE_ID"),
|
| 51 |
+
"space_author": os.getenv("SPACE_AUTHOR_NAME"),
|
| 52 |
+
"space_repo": os.getenv("SPACE_REPO_NAME"),
|
| 53 |
+
"oauth_enabled": bool(os.getenv("OAUTH_CLIENT_ID")),
|
| 54 |
+
"host": os.getenv("HOST", "localhost"),
|
| 55 |
+
"port": os.getenv("PORT", "7860"),
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
return env_info
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def should_enable_auth() -> bool:
|
| 62 |
+
"""
|
| 63 |
+
Determine if authentication should be enabled based on environment.
|
| 64 |
+
|
| 65 |
+
Returns:
|
| 66 |
+
bool: True if auth should be enabled, False otherwise
|
| 67 |
+
"""
|
| 68 |
+
return is_huggingface_space()
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def get_oauth_config() -> Optional[Dict[str, str]]:
|
| 72 |
+
"""
|
| 73 |
+
Get OAuth configuration if available.
|
| 74 |
+
|
| 75 |
+
Returns:
|
| 76 |
+
Dict with OAuth config or None if not available
|
| 77 |
+
"""
|
| 78 |
+
if not should_enable_auth():
|
| 79 |
+
return None
|
| 80 |
+
|
| 81 |
+
oauth_config = {
|
| 82 |
+
"client_id": os.getenv("OAUTH_CLIENT_ID"),
|
| 83 |
+
"client_secret": os.getenv("OAUTH_CLIENT_SECRET"),
|
| 84 |
+
"scopes": os.getenv("OAUTH_SCOPES", "openid profile"),
|
| 85 |
+
"provider_url": os.getenv("OPENID_PROVIDER_URL", "https://huggingface.co"),
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
# Only return config if client_id is available
|
| 89 |
+
if oauth_config["client_id"]:
|
| 90 |
+
return oauth_config
|
| 91 |
+
|
| 92 |
+
return None
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def debug_environment() -> None:
|
| 96 |
+
"""
|
| 97 |
+
Print debug information about the current environment.
|
| 98 |
+
"""
|
| 99 |
+
env_info = get_environment_info()
|
| 100 |
+
oauth_config = get_oauth_config()
|
| 101 |
+
|
| 102 |
+
print("π Environment Debug Information:")
|
| 103 |
+
print("=" * 50)
|
| 104 |
+
print(f"ποΈ Environment Type: {'HF Spaces' if env_info['is_hf_space'] else 'Local Development'}")
|
| 105 |
+
print(f"π Authentication: {'Enabled' if should_enable_auth() else 'Disabled'}")
|
| 106 |
+
|
| 107 |
+
if env_info['is_hf_space']:
|
| 108 |
+
print(f"π Space ID: {env_info['space_id']}")
|
| 109 |
+
print(f"π€ Author: {env_info['space_author']}")
|
| 110 |
+
print(f"π¦ Repo: {env_info['space_repo']}")
|
| 111 |
+
|
| 112 |
+
if oauth_config:
|
| 113 |
+
print(f"π OAuth Client ID: {oauth_config['client_id'][:8]}..." if oauth_config['client_id'] else "Not set")
|
| 114 |
+
print(f"π OAuth Scopes: {oauth_config['scopes']}")
|
| 115 |
+
else:
|
| 116 |
+
print("β OAuth not configured")
|
| 117 |
+
else:
|
| 118 |
+
print("π Running in local development mode")
|
| 119 |
+
print("π‘ Authentication will be automatically enabled when deployed to HF Spaces")
|
| 120 |
+
|
| 121 |
+
print("=" * 50)
|