Add LLM endpoints, web frontend, and rate limiting config
- Added OpenAI-compatible LLM endpoints to API backend - Introduced web frontend with Jinja2 templates and static assets - Implemented API proxy routes in web service - Added sample db.json data for items, users, orders, reviews, categories, llm_requests - Updated ADC and Helm configs for separate AI and standard rate limiting - Upgraded FastAPI, Uvicorn, and added httpx, Jinja2, python-multipart dependencies - Added API configuration modal and client-side JS for web app
This commit is contained in:
236
web/main.py
236
web/main.py
@@ -1,137 +1,133 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi import FastAPI, Request, HTTPException
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
import uvicorn
|
||||
import os
|
||||
import subprocess
|
||||
import httpx
|
||||
|
||||
app = FastAPI(title="Web Demo Application")
|
||||
|
||||
# Build MkDocs documentation on startup
|
||||
def build_docs():
|
||||
docs_dir = os.path.join(os.path.dirname(__file__), "docs")
|
||||
site_dir = os.path.join(os.path.dirname(__file__), "site")
|
||||
# Get the directory where this script is located
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
if os.path.exists(docs_dir):
|
||||
# API Configuration - can be set via environment variable
|
||||
API_BASE_URL = os.getenv("API_BASE_URL", "http://localhost:8001")
|
||||
|
||||
# Mount static files
|
||||
static_dir = os.path.join(BASE_DIR, "static")
|
||||
if os.path.exists(static_dir):
|
||||
app.mount("/static", StaticFiles(directory=static_dir), name="static")
|
||||
|
||||
# Setup templates
|
||||
templates_dir = os.path.join(BASE_DIR, "templates")
|
||||
templates = Jinja2Templates(directory=templates_dir)
|
||||
|
||||
# HTTP client for API calls
|
||||
async def api_request(method: str, endpoint: str, **kwargs):
|
||||
"""Make a request to the API backend"""
|
||||
url = f"{API_BASE_URL}{endpoint}"
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
try:
|
||||
subprocess.run(
|
||||
["mkdocs", "build", "-f", os.path.join(docs_dir, "mkdocs.yml"), "-d", site_dir],
|
||||
check=True,
|
||||
capture_output=True
|
||||
)
|
||||
print(f"✓ Documentation built successfully at {site_dir}")
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"✗ Failed to build documentation: {e.stderr.decode()}")
|
||||
return False
|
||||
except FileNotFoundError:
|
||||
print("✗ MkDocs not installed. Install with: pip install mkdocs mkdocs-material")
|
||||
return False
|
||||
return False
|
||||
|
||||
# Build docs on startup
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
build_docs()
|
||||
|
||||
# Mount static documentation site at /docs
|
||||
site_dir = os.path.join(os.path.dirname(__file__), "site")
|
||||
if os.path.exists(site_dir):
|
||||
app.mount("/docs", StaticFiles(directory=site_dir, html=True), name="docs")
|
||||
|
||||
# Simple HTML template inline
|
||||
HTML_TEMPLATE = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Web Demo</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 50px auto;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
background-color: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
border-bottom: 2px solid #4CAF50;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.info-box {
|
||||
background-color: #e8f5e9;
|
||||
padding: 15px;
|
||||
margin: 20px 0;
|
||||
border-left: 4px solid #4CAF50;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.metric {
|
||||
display: inline-block;
|
||||
margin: 10px 20px 10px 0;
|
||||
padding: 10px 20px;
|
||||
background-color: #2196F3;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.doc-link {
|
||||
display: inline-block;
|
||||
margin: 20px 10px 0 0;
|
||||
padding: 12px 24px;
|
||||
background-color: #673AB7;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
.doc-link:hover {
|
||||
background-color: #512DA8;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Welcome to Web Demo Application</h1>
|
||||
<div class="info-box">
|
||||
<h2>Application Information</h2>
|
||||
<p><strong>Service:</strong> Web Frontend</p>
|
||||
<p><strong>Status:</strong> ✓ Running</p>
|
||||
<p><strong>Version:</strong> 1.0.0</p>
|
||||
</div>
|
||||
<h2>Metrics Dashboard</h2>
|
||||
<div>
|
||||
<span class="metric">Requests: 1,234</span>
|
||||
<span class="metric">Uptime: 99.9%</span>
|
||||
<span class="metric">Users: 567</span>
|
||||
</div>
|
||||
<div class="info-box">
|
||||
<h3>About</h3>
|
||||
<p>This is a demo FastAPI web application serving HTML content.
|
||||
It demonstrates a simple web interface with metrics and information display.</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/docs/" class="doc-link">📚 View Documentation</a>
|
||||
<a href="/health" class="doc-link">🏥 Health Check</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
response = await client.request(method, url, **kwargs)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except httpx.HTTPStatusError as e:
|
||||
raise HTTPException(status_code=e.response.status_code, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"API request failed: {str(e)}")
|
||||
|
||||
# ===== ROUTES - HTML Pages =====
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def root():
|
||||
"""Serve the main webpage"""
|
||||
return HTML_TEMPLATE
|
||||
async def home(request: Request):
|
||||
"""Serve the home page"""
|
||||
return templates.TemplateResponse("index.html", {"request": request})
|
||||
|
||||
@app.get("/items", response_class=HTMLResponse)
|
||||
async def items_page(request: Request):
|
||||
"""Serve the items page"""
|
||||
return templates.TemplateResponse("items.html", {"request": request})
|
||||
|
||||
@app.get("/users", response_class=HTMLResponse)
|
||||
async def users_page(request: Request):
|
||||
"""Serve the users page"""
|
||||
return templates.TemplateResponse("users.html", {"request": request})
|
||||
|
||||
@app.get("/llm", response_class=HTMLResponse)
|
||||
async def llm_page(request: Request):
|
||||
"""Serve the LLM chat page"""
|
||||
return templates.TemplateResponse("llm.html", {"request": request})
|
||||
|
||||
# ===== API PROXY ENDPOINTS =====
|
||||
@app.get("/api/items")
|
||||
async def proxy_get_items():
|
||||
"""Proxy GET /items to API backend"""
|
||||
return await api_request("GET", "/items")
|
||||
|
||||
@app.get("/api/items/{item_id}")
|
||||
async def proxy_get_item(item_id: int):
|
||||
"""Proxy GET /items/{id} to API backend"""
|
||||
return await api_request("GET", f"/items/{item_id}")
|
||||
|
||||
@app.get("/api/users")
|
||||
async def proxy_get_users():
|
||||
"""Proxy GET /users to API backend"""
|
||||
return await api_request("GET", "/users")
|
||||
|
||||
@app.get("/api/users/{user_id}")
|
||||
async def proxy_get_user(user_id: int):
|
||||
"""Proxy GET /users/{id} to API backend"""
|
||||
return await api_request("GET", f"/users/{user_id}")
|
||||
|
||||
@app.post("/api/llm/chat")
|
||||
async def proxy_llm_chat(request: Request):
|
||||
"""Proxy POST /llm/chat to API backend"""
|
||||
body = await request.json()
|
||||
return await api_request("POST", "/llm/chat", json=body)
|
||||
|
||||
@app.get("/api/llm/models")
|
||||
async def proxy_llm_models():
|
||||
"""Proxy GET /llm/models to API backend"""
|
||||
return await api_request("GET", "/llm/models")
|
||||
|
||||
@app.get("/api/llm/health")
|
||||
async def proxy_llm_health():
|
||||
"""Proxy GET /llm/health to API backend"""
|
||||
return await api_request("GET", "/llm/health")
|
||||
|
||||
# ===== WEB HEALTH CHECK =====
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
"""Health check endpoint"""
|
||||
return {"status": "healthy", "service": "web"}
|
||||
# Try to connect to API backend
|
||||
api_status = "unknown"
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5.0) as client:
|
||||
response = await client.get(f"{API_BASE_URL}/health")
|
||||
if response.status_code == 200:
|
||||
api_status = "healthy"
|
||||
else:
|
||||
api_status = "unhealthy"
|
||||
except:
|
||||
api_status = "unreachable"
|
||||
|
||||
return {
|
||||
"status": "healthy",
|
||||
"service": "web",
|
||||
"version": "1.0.0",
|
||||
"api_backend": API_BASE_URL,
|
||||
"api_status": api_status
|
||||
}
|
||||
|
||||
# ===== CONFIG ENDPOINT =====
|
||||
@app.get("/api/config")
|
||||
async def get_config():
|
||||
"""Get current API configuration"""
|
||||
return {
|
||||
"api_base_url": API_BASE_URL
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"Starting Web service")
|
||||
print(f"API Backend: {API_BASE_URL}")
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
|
||||
Reference in New Issue
Block a user