Initial commit of integrated agent system
This commit is contained in:
122
backend/apps/frontend_service.py
Normal file
122
backend/apps/frontend_service.py
Normal file
@@ -0,0 +1,122 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Production frontend service.
|
||||
|
||||
Serves the built frontend static files on port 80 (configurable via
|
||||
FRONTEND_PORT) and proxies API / WebSocket requests to backend services.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
import websockets
|
||||
from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
|
||||
from fastapi.responses import FileResponse, Response
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
FRONTEND_DIST = Path(__file__).resolve().parent.parent.parent / "frontend" / "dist"
|
||||
|
||||
AGENT_SERVICE_URL = os.getenv("AGENT_SERVICE_URL", "http://localhost:8000")
|
||||
RUNTIME_SERVICE_URL = os.getenv("RUNTIME_SERVICE_URL", "http://localhost:8003")
|
||||
GATEWAY_WS_URL = os.getenv("GATEWAY_WS_URL", "")
|
||||
|
||||
app = FastAPI(title="EvoTraders Frontend")
|
||||
|
||||
|
||||
async def _resolve_gateway_ws_url() -> str:
|
||||
"""Resolve the Gateway WebSocket URL dynamically from runtime API."""
|
||||
if GATEWAY_WS_URL:
|
||||
return GATEWAY_WS_URL
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5) as client:
|
||||
resp = await client.get(f"{RUNTIME_SERVICE_URL}/api/runtime/gateway/port")
|
||||
data = resp.json()
|
||||
if data.get("is_running") and data.get("port"):
|
||||
url = f"ws://localhost:{data['port']}"
|
||||
logger.info(f"[Frontend] Resolved gateway URL: {url}")
|
||||
return url
|
||||
except Exception as e:
|
||||
logger.warning(f"[Frontend] Failed to resolve gateway port: {e}")
|
||||
fallback = f"ws://localhost:{os.getenv('GATEWAY_PORT', '8765')}"
|
||||
logger.info(f"[Frontend] Using fallback gateway URL: {fallback}")
|
||||
return fallback
|
||||
|
||||
|
||||
# ── API reverse proxy ────────────────────────────────────────────────
|
||||
@app.api_route(
|
||||
"/api/{path:path}",
|
||||
methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
|
||||
)
|
||||
async def proxy_api(request: Request, path: str):
|
||||
"""Forward /api/* requests to the appropriate backend service."""
|
||||
target = RUNTIME_SERVICE_URL if path.startswith("runtime/") else AGENT_SERVICE_URL
|
||||
body = await request.body()
|
||||
async with httpx.AsyncClient(timeout=30) as client:
|
||||
resp = await client.request(
|
||||
method=request.method,
|
||||
url=f"{target}/api/{path}",
|
||||
content=body,
|
||||
headers={
|
||||
k: v
|
||||
for k, v in request.headers.items()
|
||||
if k.lower() not in ("host", "transfer-encoding")
|
||||
},
|
||||
)
|
||||
return Response(
|
||||
content=resp.content,
|
||||
status_code=resp.status_code,
|
||||
headers=dict(resp.headers),
|
||||
)
|
||||
|
||||
|
||||
# ── WebSocket proxy ──────────────────────────────────────────────────
|
||||
@app.websocket("/ws")
|
||||
async def proxy_ws(ws: WebSocket):
|
||||
"""Proxy WebSocket connections to the Gateway (port resolved dynamically)."""
|
||||
gateway_url = await _resolve_gateway_ws_url()
|
||||
await ws.accept()
|
||||
upstream = None
|
||||
try:
|
||||
upstream = await websockets.asyncio.client.connect(gateway_url)
|
||||
|
||||
async def client_to_upstream():
|
||||
try:
|
||||
while True:
|
||||
data = await ws.receive_text()
|
||||
await upstream.send(data)
|
||||
except WebSocketDisconnect:
|
||||
pass
|
||||
|
||||
async def upstream_to_client():
|
||||
try:
|
||||
async for msg in upstream:
|
||||
if isinstance(msg, str):
|
||||
await ws.send_text(msg)
|
||||
else:
|
||||
await ws.send_bytes(msg)
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
pass
|
||||
|
||||
await asyncio.gather(client_to_upstream(), upstream_to_client())
|
||||
except Exception as e:
|
||||
logger.warning(f"[Frontend] WebSocket proxy error: {e}")
|
||||
finally:
|
||||
if upstream:
|
||||
await upstream.close()
|
||||
|
||||
|
||||
# ── Static files (SPA fallback) ─────────────────────────────────────
|
||||
if FRONTEND_DIST.is_dir():
|
||||
|
||||
@app.get("/{full_path:path}")
|
||||
async def serve_spa(full_path: str):
|
||||
"""Serve static files; fall back to index.html for SPA routing."""
|
||||
file_path = FRONTEND_DIST / full_path
|
||||
if full_path and file_path.is_file():
|
||||
return FileResponse(file_path)
|
||||
return FileResponse(FRONTEND_DIST / "index.html")
|
||||
|
||||
Reference in New Issue
Block a user