#!/usr/bin/env python3 """ main.py — Cloud Run entrypoint, OSVauco OPAX. Modes: light (gemini-2.5-flash) | heavy (gemini-2.5-pro) ML-1: telemetri, state store, DAG. CG1+CG2: billing endpoints + IAP-beskyttelse. CG3: static-mappe serveres fra /static/*. """ import os import sys import time import pathlib sys.path.insert(0, os.path.join(os.path.dirname(__file__), "agents", "core-logic")) from fastapi import FastAPI, HTTPException, Request from fastapi.responses import FileResponse, JSONResponse from fastapi.staticfiles import StaticFiles from pydantic import BaseModel, Field from typing import List from agent import run, authorize_mode from ml import build_agent_dag, execute_dag, get_store, log_agent_call from ml.telemetry import log_dag_execution from ml.billing_agent import BillingAgent from ml.anomaly_detector import AnomalyDetector app = FastAPI(title="OSVauco OPAX Agent") AGENT_ID = "opax-core" # ── IAP-MIDDLEWARE ──────────────────────────────────────────────────────────── # Cloud Run + IAP: verifiserer X-Goog-IAP-JWT-Assertion på /billing/*. # Sett env-variabelen IAP_AUDIENCE til IAP backend service audience: # /projects/PROJECT_NUMBER/global/backendServices/SERVICE_ID # Sett IAP_ENABLED=true i Cloud Run for å aktivere sjekken. # Lokalt (IAP_ENABLED ikke satt): alle kall tillates. IAP_ENABLED = os.environ.get("IAP_ENABLED", "").lower() == "true" IAP_AUDIENCE = os.environ.get("IAP_AUDIENCE", "") def _verify_iap_jwt(token: str, audience: str) -> dict: """Verifiserer IAP JWT med google-auth. Kaster ValueError ved feil.""" try: from google.auth.transport import requests as google_requests from google.oauth2 import id_token request = google_requests.Request() return id_token.verify_token(token, request, audience=audience, certs_url="https://www.gstatic.com/iap/verify/public_key") except Exception as exc: raise ValueError(f"IAP JWT ugyldig: {exc}") from exc @app.middleware("http") async def iap_guard(request: Request, call_next): """Blokkerer /billing/* uten gyldig IAP-token når IAP_ENABLED=true.""" if IAP_ENABLED and request.url.path.startswith("/billing"): token = request.headers.get("X-Goog-IAP-JWT-Assertion", "") if not token: return JSONResponse(status_code=401, content={"error": "IAP-token mangler (X-Goog-IAP-JWT-Assertion)"}) try: _verify_iap_jwt(token, IAP_AUDIENCE) except ValueError as exc: return JSONResponse(status_code=403, content={"error": str(exc)}) return await call_next(request) # ── STATIC FILES ────────────────────────────────────────────────────────────── # Serverer static/ direkte: GET /static/billing-dashboard.html osv. _static_dir = pathlib.Path(__file__).parent / "static" if _static_dir.is_dir(): app.mount("/static", StaticFiles(directory=str(_static_dir), html=True), name="static") # ── MODELLER ────────────────────────────────────────────────────────────────── class RunRequest(BaseModel): message: str user_id: str = "opax" session_id: str = "default" mode: str = "light" class DagRequest(BaseModel): messages: List[str] = Field(...) user_id: str = "opax" session_id: str = "default" mode: str = "light" scheduler: str = Field("threads") # ── HEALTH ──────────────────────────────────────────────────────────────────── @app.get("/health") def health(): return {"status": "ok"} # ── BILLING ENDPOINTS (CG1 + CG2) ──────────────────────────────────────────── @app.get("/billing/summary") def billing_summary(): try: return BillingAgent().get_summary() except Exception as exc: return JSONResponse(status_code=500, content={"error": str(exc)}) @app.get("/billing/forecast") def billing_forecast(): try: return BillingAgent().get_forecast() except Exception as exc: return JSONResponse(status_code=500, content={"error": str(exc)}) @app.get("/billing/anomalies") def billing_anomalies(): try: return AnomalyDetector().detect_anomalies() except Exception as exc: return JSONResponse(status_code=500, content={"error": str(exc)}) # ── AGENT ENDPOINTS ─────────────────────────────────────────────────────────── @app.post("/run") def run_agent(req: RunRequest): try: authorize_mode(req.user_id, req.mode) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except PermissionError as e: raise HTTPException(status_code=403, detail=str(e)) store = get_store() start = time.monotonic() error_msg = None response = None try: response = run( message=req.message, user_id=req.user_id, session_id=req.session_id, mode=req.mode, ) except Exception as e: error_msg = str(e) raise HTTPException(status_code=500, detail=error_msg) finally: duration = round(time.monotonic() - start, 3) success = error_msg is None model = "gemini-2.5-flash" if req.mode in ("light", "A") else "gemini-2.5-pro" log_agent_call( agent_id=AGENT_ID, input_payload={"message": req.message, "mode": req.mode}, output=response, model_used=model, mode=req.mode, duration_s=duration, success=success, error=error_msg, ) store.push(AGENT_ID, "last_duration_s", duration) store.push(AGENT_ID, "last_mode", req.mode) store.push(AGENT_ID, "last_success", success) if response: store.push(AGENT_ID, "last_result_preview", response[:200]) return {"response": response} @app.post("/run/dag") def run_dag(req: DagRequest): try: authorize_mode(req.user_id, req.mode) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except PermissionError as e: raise HTTPException(status_code=403, detail=str(e)) def make_agent_fn(msg: str, idx: int): def _agent_fn(payload: dict): return run( message=msg, user_id=payload["user_id"], session_id=f"{payload['session_id']}-dag-{idx}", mode=payload["mode"], ) _agent_fn.__name__ = f"opax-dag-{idx}" return _agent_fn payload = {"user_id": req.user_id, "session_id": req.session_id, "mode": req.mode} agent_fns = [make_agent_fn(msg, i) for i, msg in enumerate(req.messages)] agent_ids = [f"opax-dag-{i}" for i in range(len(req.messages))] start = time.monotonic() tasks = build_agent_dag(agent_fns, payload, agent_ids) results = execute_dag(tasks, scheduler=req.scheduler) total_dur = round(time.monotonic() - start, 3) store = get_store() log_dag_execution(dag_id=req.session_id, agent_results=results, total_duration_s=total_dur) store.push(AGENT_ID, "last_dag_total_duration_s", total_dur) store.push(AGENT_ID, "last_dag_agent_count", len(results)) store.push(AGENT_ID, "last_dag_success_count", sum(1 for r in results if r["success"])) return { "total_duration_s": total_dur, "results": [ { "index": i, "message": req.messages[i], "response": r["result"], "success": r["success"], "duration_s": r["duration_s"], "error": r["error"], } for i, r in enumerate(results) ], } @app.get("/state") def get_state(): return get_store().snapshot() @app.get("/state/agents") def list_agents(): return {"agents": get_store().list_agents()} @app.get("/state/aggregate/{key}") def aggregate_key(key: str): return get_store().aggregate(key) @app.get("/telemetry/history") def telemetry_history(limit: int = 50): return {"history": get_store().history(limit=limit)} @app.get("/billing/summary") def get_billing_summary(): try: agent = BillingAgent() return agent.get_summary() except Exception as e: return {"error": str(e)} @app.get("/billing/forecast") def get_billing_forecast(): try: agent = BillingAgent() return agent.get_forecast() except Exception as e: return {"error": str(e)} @app.get("/billing/anomalies") def get_billing_anomalies(): try: detector = AnomalyDetector() return detector.detect_anomalies() except Exception as e: return {"error": str(e)} if __name__ == "__main__": import uvicorn port = int(os.environ.get("PORT", 8080)) uvicorn.run(app, host="0.0.0.0", port=port)