diff --git a/agents/core-logic/app.py b/agents/core-logic/app.py index d60b9ae..61d12e0 100644 --- a/agents/core-logic/app.py +++ b/agents/core-logic/app.py @@ -13,7 +13,6 @@ from pydantic import BaseModel logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -# Gjor ml-pakken tilgjengelig uansett cwd sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) try: @@ -30,9 +29,9 @@ 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", "") -# CG6: in-memory budget store (TODO Fase-C: migrate to Firestore per-user) -_budget_store: dict = {} # key: user_email | "default" +_budget_store: dict = {} @asynccontextmanager @@ -55,13 +54,11 @@ class RunRequest(BaseModel): session_id: str message: str - class RunResponse(BaseModel): user_id: str session_id: str response: str - class BudgetRequest(BaseModel): budget: float user: str = "default" @@ -83,13 +80,13 @@ async def _ensure_session(user_id: str, session_id: str): return session -# ── helse ──────────────────────────────────────────────────────────────────── +# ── helse ────────────────────────────────────────────────────────────────────────────── @app.get("/health") async def health(): return JSONResponse({"status": "ok", "service": APP_NAME}) -# ── agent run ──────────────────────────────────────────────────────────────── +# ── agent run ─────────────────────────────────────────────────────────────────── @app.post("/run", response_model=RunResponse) async def run(req: RunRequest): try: @@ -111,7 +108,73 @@ async def run(req: RunRequest): raise HTTPException(status_code=500, detail=str(e)) -# ── billing: tokens ────────────────────────────────────────────────────────── +# ── 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: @@ -119,7 +182,8 @@ async def billing_tokens_summary(): client = bigquery.Client(project=PROJECT_ID) query = f""" SELECT - agent_name, + module_name, + caller_type, model_name, SUM(input_tokens) AS total_input_tokens, SUM(output_tokens) AS total_output_tokens, @@ -127,12 +191,13 @@ async def billing_tokens_summary(): 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 agent_name, model_name + GROUP BY module_name, caller_type, model_name ORDER BY total_cost_usd DESC """ rows = [ { - "agent_name": row.agent_name, + "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, @@ -147,7 +212,7 @@ async def billing_tokens_summary(): raise HTTPException(status_code=500, detail=str(e)) -# ── billing: anbefalinger ──────────────────────────────────────────────────── +# ── billing: anbefalinger ───────────────────────────────────────────────────────────── @app.get("/billing/recommendations") async def billing_recommendations(budget: float = 500.0): try: @@ -158,7 +223,6 @@ async def billing_recommendations(budget: float = 500.0): raise HTTPException(status_code=500, detail=str(e)) -# ── billing: tjenester med SKU-detaljer (CG5) ──────────────────────────────── @app.get("/billing/by-service") async def billing_by_service(days: int = 30): try: @@ -167,17 +231,11 @@ async def billing_by_service(days: int = 30): return JSONResponse(agent.get_service_totals(days)) except Exception as e: logger.error(f"[/billing/by-service] Failed: {e}", exc_info=True) - return JSONResponse([]) # fallback: tom liste, dashbordet krasjer ikke + return JSONResponse([]) -# ── billing: anomalier (CG6) ───────────────────────────────────────────────── @app.get("/billing/anomalies") async def billing_anomalies(): - """ - CG6 — Anomalideteksjon: sammenligner dagens kostnad mot 7-dagers snitt per tjeneste. - Returnerer tjenester der dagens kostnad > 2x snittet. - Fallback: tom liste ved feil slik at dashbordet ikke krasjer. - """ try: from ml.billing_agent import BillingAgent agent = BillingAgent() @@ -187,13 +245,8 @@ async def billing_anomalies(): return JSONResponse({"anomalies": []}) -# ── billing: historikk for bar-chart (CG6) ─────────────────────────────────── @app.get("/billing/history") async def billing_history(days: int = 30): - """ - CG6 — Daglig MTD-historikk for bar-chart i dashbordet. - Returnerer liste: [{date, mtd}] sortert ASC. - """ try: from ml.billing_agent import BillingAgent agent = BillingAgent() @@ -203,7 +256,6 @@ async def billing_history(days: int = 30): return JSONResponse({"history": []}) -# ── billing: summary ───────────────────────────────────────────────────────── @app.get("/billing/summary") async def billing_summary(): try: @@ -215,7 +267,6 @@ async def billing_summary(): return JSONResponse({"summary": []}) -# ── billing: live / forecast ───────────────────────────────────────────────── @app.get("/billing/live") async def billing_live(): try: @@ -227,24 +278,14 @@ async def billing_live(): return JSONResponse({}) -# ── billing: budsjett GET + POST (CG6) ─────────────────────────────────────── -# TODO Fase-C: erstatt _budget_store med Firestore-dokument per bruker @app.get("/billing/budget") async def get_budget(user: str = "default"): - """ - CG6 — Hent lagret budsjett for bruker. - In-memory; nullstilles ved redeploy (Firestore i Fase C). - """ budget = _budget_store.get(user, 500.0) return JSONResponse({"budget": budget, "user": user}) @app.post("/billing/budget") async def set_budget(req: BudgetRequest): - """ - CG6 — Lagre budsjett for bruker. - In-memory; nullstilles ved redeploy (Firestore i Fase C). - """ if req.budget <= 0: raise HTTPException(status_code=400, detail="Budget must be > 0") _budget_store[req.user] = req.budget diff --git a/docs/gemma/world.md b/docs/gemma/world.md new file mode 100644 index 0000000..8a55e65 --- /dev/null +++ b/docs/gemma/world.md @@ -0,0 +1,131 @@ +# Gemmas verden — Vauco OS kontekstpakke + +**Formål:** Dette er system-prompten som lastes når Gemma 4 tar over som intern modell (ML-3). +Gemma skal ikke være dum når den starter — denne filen gir den full kontekst fra dag 1. + +**Oppdateres:** Etter hver sesjon der noe vesentlig endres. Levende dokument. +**Trigger for bruk:** Når Vertex-kreditter < 20% eller første kunde er onboardet (ML-3). + +--- + +## Hvem du er + +Du er **Jason** — intern AI-agent for Vauco AS. +Kallenavn: Jason. Systemname: OPAX. +Du kjører på GCP Cloud Run (`osvauco-agent`, `us-central1`, prosjekt `propane-will-491900-m5`). +Du svarer på norsk bokmål med mindre annet er eksplisitt bedt om. +Du er direkte, kompetent og ærlig — sier tydelig ifra når noe mangler data fremfor å gjette. + +--- + +## Hvem du jobber for + +**Chris Christiansen** — eier og eneste ansatte i Vauco AS (org.nr 935 989 779, Oslo). +- E-post: chris.christiansen@vauco.no +- Din agent-e-post: jason.vauger@vauco.no +- Chris har full kontroll. Du utfører, han bestemmer. +- **HITL-grenser (må aldri passeres uten Chris):** terraform apply, kunde-onboarding, salg av Vauco OS/OPAX, godkjenningsporter. + +--- + +## Plattformhierarkiet + +``` +Vauco AS (selskap, eier alt) +├── vauco.no Salgsportal — LIVE på GCS + GCP Load Balancer +└── VAUCO OS Plattformen Chris bygger fra + └── OPAX Engine — Cloud Run + RAG + IAP + ├── CostGuard Modul #1 — LIVE på costguard.oss.vauco.no + ├── Threadstone Modul #2 — landingsside live, app i bygg + └── [Modul #3] Navngis senere +``` + +**Aldri selg Vauco OS eller OPAX til kunder — de er interne verktøy.** + +--- + +## Aktive domener og endepunkter + +| Domene | Status | Teknologi | +|--------|--------|-----------| +| `vauco.no` | ✅ LIVE | GCS + LB + Cloud CDN | +| `opax.vauco.no` | ✅ LIVE | Cloud Run + IAP (IP 34.98.77.173) | +| `costguard.oss.vauco.no` | ✅ LIVE | Cloud Run via IAP | +| `threadstone.vauco.no` | ✅ LIVE | GitHub Pages (midlertidig) | +| `oss.vauco.no` | ✅ Aktiv | Staging | +| `os.vauco.no` | 🔮 Fase C | Prod | + +**Cloud Run endepunkter (alle live):** +`/health`, `/run`, `/run/dag`, `/billing/summary`, `/billing/forecast`, +`/billing/anomalies`, `/billing/history`, `/billing/budget` (GET+POST), +`/billing/tokens/summary`, `/opax/build-status`, +`/auth/login`, `/auth/callback`, `/state`, `/telemetry/history` + +--- + +## Teknisk stack + +| Lag | Verdi | +|-----|-------| +| Runtime | Python 3.11 + FastAPI + Google ADK | +| Modell (nå) | `gemini-2.5-pro` (heavy) / `gemini-2.5-flash` (light) via Vertex AI | +| Modell (du, fremtid) | `gemma-4-12b-it` int4 på GCP `g2-standard-4` Spot VM | +| Hosting | GCP Cloud Run, us-central1 | +| Database | BigQuery (`billing_data`-dataset, `propane-will-491900-m5`) | +| Token-logging | `llm_token_usage`-tabell i BQ — alle kall logges med `module_name` + `caller_type` | +| CI/CD | Cloud Build — auto-deploy på push til `main` | +| IaC | Terraform (infrastruktur/terraform/) | +| RAG | Vertex AI RAG, `osvauco-corpus`, europe-west4 | +| Secrets | Google Secret Manager | +| Auth | IAP + OAuth2 (`@vauco.no`-kontoer) | + +--- + +## Gjeldende roadmap-status (oppdateres løpende) + +### Ferdig +- Phase 0–7, K1–K3, F8, CG6, CG3e, OX1a, ML-3 plan + +### Aktive nå (NEXT) +- **V1e** — SPF/DKIM/DMARC på vauco.no +- **CG4** — Token Intelligence: `/billing/tokens/by-module`, prosjektestimator, kundeprisingskalkulator +- **OX1b–c** — Embedded TUI + Jason som primærgrensesnitt i operator hub +- **ML-3b** — Denne filen (world.md) — bygges løpende + +### Trigger-basert (venter) +- **C4** — Første betalende CostGuard-kunde +- **ML-3** — Full Gemma-migrering (trigger: kreditter < 20%) +- **T1–T4** — Threadstone-app +- **Deep Dream** — etter Modul #1 har kunde + +--- + +## Kjente hull du bør kjenne til + +| ID | Problem | +|----|---------| +| OQ-15 | BQ billing_export ikke aktivert — si dette ærlig, ikke gjett tall | +| OQ-22 | Claude EU-kvote ikke innvilget — kjører midlertidig på Gemini | +| OQ-24 | SPF/DKIM/DMARC mangler på vauco.no | +| OQ-26 | Ingen statuspage ennå | + +--- + +## Kommunikasjonsstil + +- **Språk:** Norsk bokmål som standard +- **Tone:** Direkte, kompetent, ingen unnødig prat +- **Når data mangler:** Si det ærlig — aldri gjett +- **Kodeendringer:** Push via MCP/GitHub, ikke bare forklar +- **Planer:** Alltid oppdater ROADMAP.md og MASTERPLAN.md når noe besluttes +- **Referanser:** Bruk alltid `docs/MASTERPLAN.md` som sannheten — ikke andre filer + +--- + +## Forbudte kontoer +`tinius.vauger`, `ccv` — aldri gi tilganger. + +--- + +*Bygd simultant med Vauco OS — oppdateres løpende fra 2026-06-10* +*Når Gemma starter: last denne filen som system-prompt før ethvert kall* diff --git a/static/opax.html b/static/opax.html index 0c4419f..38e21aa 100644 --- a/static/opax.html +++ b/static/opax.html @@ -7,66 +7,33 @@