OSVauco/agents/core-logic/app.py
chrischristiansen-glitch 1c447c4d3a feat(OX1a+ML-3b): Cloud Build live-widget i opax.html + /opax/build-status endepunkt + Gemma world.md
- app.py: ny GET /opax/build-status — poller Cloud Build API, returnerer siste build status/timing
- static/opax.html: ny Deploy-seksjon med live spinner → /, poller hvert 10s under aktiv build
- docs/gemma/world.md: Gemma kontekstpakke opprettet — alt Gemma trenger å forstå Vauco fra dag 1
2026-06-10 01:11:09 +02:00

294 lines
11 KiB
Python

# agents/core-logic/app.py
# OSVauco-NMTMD-GCOS — FastAPI HTTP entrypoint for Cloud Run
import os
import sys
import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse
from pydantic import BaseModel
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
try:
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.genai.types import Content, Part
from agent import root_agent
from agents.recommendations_engine import get_recommendations
except ImportError as e:
logger.error(f"Failed to import ADK dependencies: {e}")
raise
session_service = InMemorySessionService()
APP_NAME = os.environ.get("CLOUD_RUN_SERVICE", "gcp-orchestrator")
PROJECT_ID = os.environ.get("GOOGLE_CLOUD_PROJECT", "propane-will-491900-m5")
BQ_BILLING_DATASET = os.environ.get("BQ_BILLING_DATASET", "billing_data")
CLOUD_BUILD_TRIGGER_ID = os.environ.get("CLOUD_BUILD_TRIGGER_ID", "")
_budget_store: dict = {}
@asynccontextmanager
async def lifespan(app: FastAPI):
logger.info(f"OSVauco agent '{APP_NAME}' starting up")
yield
logger.info(f"OSVauco agent '{APP_NAME}' shutting down")
app = FastAPI(
title="OSVauco GCP Agent",
description="ADK-based multi-agent orchestrator on Cloud Run",
version="1.0.0",
lifespan=lifespan,
)
class RunRequest(BaseModel):
user_id: str
session_id: str
message: str
class RunResponse(BaseModel):
user_id: str
session_id: str
response: str
class BudgetRequest(BaseModel):
budget: float
user: str = "default"
async def _ensure_session(user_id: str, session_id: str):
try:
session = await session_service.get_session(
app_name=APP_NAME, user_id=user_id, session_id=session_id
)
if session is not None:
return session
except Exception:
pass
session = await session_service.create_session(
app_name=APP_NAME, user_id=user_id, session_id=session_id
)
logger.info(f"Created new session: {session_id} for user: {user_id}")
return session
# ── helse ──────────────────────────────────────────────────────────────────────────────
@app.get("/health")
async def health():
return JSONResponse({"status": "ok", "service": APP_NAME})
# ── agent run ───────────────────────────────────────────────────────────────────
@app.post("/run", response_model=RunResponse)
async def run(req: RunRequest):
try:
await _ensure_session(req.user_id, req.session_id)
runner = Runner(agent=root_agent, app_name=APP_NAME, session_service=session_service)
user_content = Content(role="user", parts=[Part(text=req.message)])
final_response = ""
async for event in runner.run_async(
user_id=req.user_id, session_id=req.session_id, new_message=user_content
):
if event.is_final_response() and event.content:
for part in event.content.parts:
if part.text:
final_response += part.text
logger.info(f"[{req.user_id}/{req.session_id}] Response length: {len(final_response)}")
return RunResponse(user_id=req.user_id, session_id=req.session_id, response=final_response)
except Exception as e:
logger.error(f"Agent run failed: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
# ── OX1a: Cloud Build status ────────────────────────────────────────────────────────
@app.get("/opax/build-status")
async def build_status():
"""
OX1a — Hent siste Cloud Build-kjøring for OSVauco-repoet.
Returnerer status, tidspunkt og varighet slik at opax.html kan vise
live build-widget (spinner → ✅/❌).
Tilstandsverdier fra Cloud Build API:
QUEUED, WORKING → bygger nå (vis spinner)
SUCCESS → ok
FAILURE / TIMEOUT / CANCELLED / INTERNAL_ERROR → feil
"""
try:
from google.cloud.devtools import cloudbuild_v1
client = cloudbuild_v1.CloudBuildClient()
# Hent siste 5 builds for prosjektet
request = cloudbuild_v1.ListBuildsRequest(
project_id=PROJECT_ID,
filter='trigger_id!=""', # kun trigger-builds, ikke manuelle
page_size=5,
)
builds = list(client.list_builds(request=request))
if not builds:
return JSONResponse({"status": "unknown", "message": "Ingen builds funnet"})
b = builds[0] # nyeste
status_map = {
1: "queued",
2: "working",
3: "success",
4: "failure",
5: "internal_error",
6: "timeout",
7: "cancelled",
}
status_str = status_map.get(int(b.status), "unknown")
# Beregn varighet
duration_s = None
start = b.start_time
finish = b.finish_time
if start and finish:
duration_s = int((finish.seconds - start.seconds))
elif start:
import time
duration_s = int(time.time() - start.seconds)
return JSONResponse({
"status": status_str, # queued|working|success|failure|timeout|cancelled
"build_id": b.id,
"trigger_id": b.build_trigger_id or "",
"branch": (b.substitutions or {}).get("BRANCH_NAME", "main"),
"commit": (b.substitutions or {}).get("SHORT_SHA", ""),
"duration_s": duration_s,
"start_time": str(b.start_time) if b.start_time else None,
"finish_time": str(b.finish_time) if b.finish_time else None,
"log_url": b.log_url or "",
})
except Exception as e:
logger.error(f"[/opax/build-status] Failed: {e}", exc_info=True)
return JSONResponse({"status": "error", "message": str(e)}, status_code=200)
# ── billing: tokens ──────────────────────────────────────────────────────────────────
@app.get("/billing/tokens/summary")
async def billing_tokens_summary():
try:
from google.cloud import bigquery
client = bigquery.Client(project=PROJECT_ID)
query = f"""
SELECT
module_name,
caller_type,
model_name,
SUM(input_tokens) AS total_input_tokens,
SUM(output_tokens) AS total_output_tokens,
SUM(total_tokens) AS total_tokens,
SUM(estimated_cost_usd) AS total_cost_usd
FROM `{PROJECT_ID}.{BQ_BILLING_DATASET}.llm_token_usage`
WHERE timestamp >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
GROUP BY module_name, caller_type, model_name
ORDER BY total_cost_usd DESC
"""
rows = [
{
"module_name": row.module_name,
"caller_type": row.caller_type,
"model_name": row.model_name,
"total_input_tokens": row.total_input_tokens,
"total_output_tokens": row.total_output_tokens,
"total_tokens": row.total_tokens,
"total_cost_usd": round(float(row.total_cost_usd), 6),
}
for row in client.query(query).result()
]
return JSONResponse({"period_days": 30, "rows": rows})
except Exception as e:
logger.error(f"[/billing/tokens/summary] Failed: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
# ── billing: anbefalinger ─────────────────────────────────────────────────────────────
@app.get("/billing/recommendations")
async def billing_recommendations(budget: float = 500.0):
try:
recommendations = get_recommendations(budget)
return JSONResponse(recommendations)
except Exception as e:
logger.error(f"[/billing/recommendations] Failed: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
@app.get("/billing/by-service")
async def billing_by_service(days: int = 30):
try:
from ml.billing_agent import BillingAgent
agent = BillingAgent()
return JSONResponse(agent.get_service_totals(days))
except Exception as e:
logger.error(f"[/billing/by-service] Failed: {e}", exc_info=True)
return JSONResponse([])
@app.get("/billing/anomalies")
async def billing_anomalies():
try:
from ml.billing_agent import BillingAgent
agent = BillingAgent()
return JSONResponse(agent.get_anomalies())
except Exception as e:
logger.error(f"[/billing/anomalies] Failed: {e}", exc_info=True)
return JSONResponse({"anomalies": []})
@app.get("/billing/history")
async def billing_history(days: int = 30):
try:
from ml.billing_agent import BillingAgent
agent = BillingAgent()
return JSONResponse({"history": agent.get_daily_history(days)})
except Exception as e:
logger.error(f"[/billing/history] Failed: {e}", exc_info=True)
return JSONResponse({"history": []})
@app.get("/billing/summary")
async def billing_summary():
try:
from ml.billing_agent import BillingAgent
agent = BillingAgent()
return JSONResponse(agent.get_summary())
except Exception as e:
logger.error(f"[/billing/summary] Failed: {e}", exc_info=True)
return JSONResponse({"summary": []})
@app.get("/billing/live")
async def billing_live():
try:
from ml.billing_agent import BillingAgent
agent = BillingAgent()
return JSONResponse(agent.get_forecast())
except Exception as e:
logger.error(f"[/billing/live] Failed: {e}", exc_info=True)
return JSONResponse({})
@app.get("/billing/budget")
async def get_budget(user: str = "default"):
budget = _budget_store.get(user, 500.0)
return JSONResponse({"budget": budget, "user": user})
@app.post("/billing/budget")
async def set_budget(req: BudgetRequest):
if req.budget <= 0:
raise HTTPException(status_code=400, detail="Budget must be > 0")
_budget_store[req.user] = req.budget
logger.info(f"Budget updated: {req.user} -> {req.budget}")
return JSONResponse({"budget": req.budget, "user": req.user})