From 7686fc76237e1360d4b7eaec496270a4f34d6ac3 Mon Sep 17 00:00:00 2001 From: chrischristiansen-glitch Date: Wed, 10 Jun 2026 00:36:25 +0200 Subject: [PATCH] =?UTF-8?q?feat(CG3e+CG4):=20utvidet=20token-logging=20?= =?UTF-8?q?=E2=80=94=20caller=5Ftype,=20module=5Fname,=20session=5Fid=20+?= =?UTF-8?q?=20CG4-fase=20i=20ROADMAP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - token_logger.py: nytt schema med caller_type (agent|cli|browser|cron|api), module_name (jason/opax, costguard, threadstone, cli/gemini-tui, billing_agent), session_id - agent.py: agent_name → strukturert label (module_name + mode), caller_type satt til 'agent' - ROADMAP.md: CG4 Token Intelligence-fase lagt til (CG4a–CG4d) under NEXT --- agents/core-logic/agent.py | 51 +++++++++++++++---- agents/core-logic/token_logger.py | 82 ++++++++++++++++++++++++++----- docs/ROADMAP.md | 27 ++++++++-- 3 files changed, 134 insertions(+), 26 deletions(-) diff --git a/agents/core-logic/agent.py b/agents/core-logic/agent.py index c8fa273..ca2584f 100644 --- a/agents/core-logic/agent.py +++ b/agents/core-logic/agent.py @@ -51,10 +51,15 @@ APP_NAME = "opax" # Token logger — feiler stille, stopper aldri agent try: - from token_logger import log_token_usage + from token_logger import log_token_usage, create_bq_table_if_not_exists except ImportError: def log_token_usage(*args, **kwargs): pass + def create_bq_table_if_not_exists(): + pass + +# Opprett BQ-tabell ved oppstart (idempotent, feiler stille) +create_bq_table_if_not_exists() def _normalize_mode(mode: str) -> str: @@ -143,10 +148,18 @@ def build_agent(mode: str = "light") -> Agent: root_agent = build_agent(mode="light") -async def _run_async(message: str, user_id: str, session_id: str, mode: str) -> str: +async def _run_async( + message: str, + user_id: str, + session_id: str, + mode: str, + caller_type: str = "agent", +) -> str: mode = _normalize_mode(mode) agent = build_agent(mode=mode) models = get_models_for_mode(mode) + module_name = f"jason/{mode}" # CG4: 'jason/light' eller 'jason/heavy' + session_service = InMemorySessionService() session = await session_service.create_session( app_name=APP_NAME, user_id=user_id, session_id=session_id, @@ -163,20 +176,22 @@ async def _run_async(message: str, user_id: str, session_id: str, mode: str) -> ): if event.is_final_response() and event.content and event.content.parts: final_text = event.content.parts[0].text or "" - # Hent token-metadata fra event hvis tilgjengelig if hasattr(event, "usage_metadata") and event.usage_metadata: um = event.usage_metadata input_tokens += getattr(um, "prompt_token_count", 0) or 0 output_tokens += getattr(um, "candidates_token_count", 0) or 0 - # Logg token-bruk — feiler stille + # Logg med full CG4-labeling — feiler stille if input_tokens > 0 or output_tokens > 0: log_token_usage( - agent_name=user_id, + agent_name=module_name, # bakoverkompatibelt model_name=models["orchestrator"], input_tokens=input_tokens, output_tokens=output_tokens, request_id=request_id, + module_name=module_name, # CG4: 'jason/light' | 'jason/heavy' + caller_type=caller_type, # CG4: agent|browser|cli|cron|api + session_id=session_id, # CG4: for sesjon-gruppering ) return final_text @@ -187,19 +202,35 @@ def run( user_id: str = "opax", session_id: str = "default", mode: str = "light", + caller_type: str = "agent", ) -> str: + """ + Kjør OPAX-agenten. + + caller_type-verdier: + 'agent' — programmatisk kall fra server/app.py + 'browser' — direkte fra browser-chat (opax.vauco.no) + 'cli' — lokal kjøring fra terminalen + 'cron' — bakgrunnsjobb + 'api' — ekstern REST-kall + """ mode = _normalize_mode(mode) authorize_mode(user_id, mode) return asyncio.run(_run_async( - message=message, user_id=user_id, session_id=session_id, mode=mode, + message=message, + user_id=user_id, + session_id=session_id, + mode=mode, + caller_type=caller_type, )) if __name__ == "__main__": import sys logging.basicConfig(level=logging.WARNING) - query = sys.argv[1] if len(sys.argv) > 1 else "Hva er OPAX?" - mode = sys.argv[2] if len(sys.argv) > 2 else "light" - print(f"Mode: {mode} | Query: {query}") + query = sys.argv[1] if len(sys.argv) > 1 else "Hva er OPAX?" + mode = sys.argv[2] if len(sys.argv) > 2 else "light" + caller_type = sys.argv[3] if len(sys.argv) > 3 else "cli" + print(f"Mode: {mode} | Caller: {caller_type} | Query: {query}") print("-" * 60) - print(run(message=query, user_id="opax", mode=mode)) + print(run(message=query, user_id="opax", mode=mode, caller_type=caller_type)) diff --git a/agents/core-logic/token_logger.py b/agents/core-logic/token_logger.py index 1e2e652..b81e414 100644 --- a/agents/core-logic/token_logger.py +++ b/agents/core-logic/token_logger.py @@ -1,10 +1,10 @@ -# token_logger.py — Logg LLM token-bruk til BigQuery (CG3e) +# token_logger.py — Logg LLM token-bruk til BigQuery (CG3e + CG4) import os import logging import uuid from datetime import datetime, timezone -from typing import Optional +from typing import Optional, Literal logger = logging.getLogger(__name__) @@ -13,9 +13,26 @@ DATASET = os.environ.get("BQ_BILLING_DATASET", "billing_data") TABLE = "llm_token_usage" FULL_TABLE = f"{PROJECT_ID}.{DATASET}.{TABLE}" +# Gyldige caller_type-verdier +# agent = OPAX/Jason kjørt som agent via /run eller /run/dag +# browser = direkte kall fra browser-chat (opax.vauco.no) +# cli = Gemini TUI CLI eller lokal agent.py kjørt manuelt +# cron = bakgrunnsjobb (billing_agent, anomaly_detector, dag-runner) +# api = ekstern REST-kall til /run uten sesjon +CallerType = Literal["agent", "browser", "cli", "cron", "api"] + +# Gyldige module_name-verdier (utvides etter hvert som moduler comes online) +# jason/light = OPAX-agenten, gemini-2.5-flash +# jason/heavy = OPAX-agenten, gemini-2.5-pro +# costguard = CostGuard-analyse og anomali-endepunkter +# billing_agent = ml/billing_agent.py bakgrunns-analyse +# threadstone = Threadstone-modul (fremtidig) +# cli/gemini-tui = lokal Gemini TUI (arkivert, men kan aktiveres) +ModuleName = str # ikke enum — for fremtidssikkerhet + # Prismodell (USD per 1M tokens) — oppdater ved modellbytte MODEL_PRICING = { - "gemini-2.5-pro": {"input": 1.25, "output": 10.00}, + "gemini-2.5-pro": {"input": 1.25, "output": 10.00}, "gemini-2.5-flash": {"input": 0.075, "output": 0.30}, } DEFAULT_PRICING = {"input": 1.25, "output": 10.00} @@ -29,26 +46,39 @@ def _estimate_cost(model_name: str, input_tokens: int, output_tokens: int) -> fl def log_token_usage( - agent_name: str, + agent_name: str, # deprecated alias — bruk module_name model_name: str, input_tokens: int, output_tokens: int, - request_id: Optional[str] = None, + request_id: Optional[str] = None, + module_name: Optional[str] = None, # CG4: f.eks. 'jason/light', 'costguard' + caller_type: CallerType = "agent", # CG4: hvem/hva som kalte + session_id: Optional[str] = None, # CG4: sesjon-ID for gruppering ) -> None: """ Logg ett LLM-kall til BigQuery-tabellen llm_token_usage. Feiler stille slik at applikasjonen aldri krasjer pga logging. + + Labeling-konvensjoner: + module_name — hvilken komponent (jason/light, costguard, billing_agent, …) + caller_type — hvordan kallet ble trigget (agent, browser, cli, cron, api) + agent_name — beholdt for bakoverkompatibilitet; settes til module_name om gitt """ try: + from google.cloud import bigquery client = bigquery.Client(project=PROJECT_ID) - total_tokens = input_tokens + output_tokens - estimated_cost = _estimate_cost(model_name, input_tokens, output_tokens) + effective_module = module_name or agent_name + total_tokens = input_tokens + output_tokens + estimated_cost = _estimate_cost(model_name, input_tokens, output_tokens) row = { "timestamp": datetime.now(timezone.utc).isoformat(), - "agent_name": agent_name, + "agent_name": effective_module, # bakoverkompatibelt felt + "module_name": effective_module, # CG4: nytt felt + "caller_type": caller_type, # CG4: nytt felt + "session_id": session_id or "", # CG4: nytt felt "model_name": model_name, "input_tokens": input_tokens, "output_tokens": output_tokens, @@ -62,17 +92,21 @@ def log_token_usage( logger.warning(f"[token_logger] BQ insert errors: {errors}") else: logger.info( - f"[token_logger] Logged: agent={agent_name} model={model_name} " + f"[token_logger] Logged: module={effective_module} " + f"caller={caller_type} model={model_name} " f"in={input_tokens} out={output_tokens} cost=${estimated_cost:.6f}" ) except Exception as e: logger.warning(f"[token_logger] Failed to log token usage (non-fatal): {e}") -# BQ table schema — brukes som referanse ved manuell oppretting eller Terraform +# BQ table schema — brukes som referanse ved manuell oppretting, Terraform eller create_bq_table() BQ_SCHEMA = [ {"name": "timestamp", "type": "TIMESTAMP", "mode": "REQUIRED"}, - {"name": "agent_name", "type": "STRING", "mode": "REQUIRED"}, + {"name": "agent_name", "type": "STRING", "mode": "REQUIRED"}, # bakoverkompatibelt + {"name": "module_name", "type": "STRING", "mode": "REQUIRED"}, # CG4 + {"name": "caller_type", "type": "STRING", "mode": "REQUIRED"}, # CG4: agent|browser|cli|cron|api + {"name": "session_id", "type": "STRING", "mode": "NULLABLE"}, # CG4 {"name": "model_name", "type": "STRING", "mode": "REQUIRED"}, {"name": "input_tokens", "type": "INTEGER", "mode": "REQUIRED"}, {"name": "output_tokens", "type": "INTEGER", "mode": "REQUIRED"}, @@ -80,3 +114,29 @@ BQ_SCHEMA = [ {"name": "estimated_cost_usd", "type": "FLOAT", "mode": "REQUIRED"}, {"name": "request_id", "type": "STRING", "mode": "NULLABLE"}, ] + + +def create_bq_table_if_not_exists() -> None: + """ + Opprett llm_token_usage-tabellen i BigQuery hvis den ikke finnes. + Trygt å kalle ved app-oppstart (CREATE TABLE IF NOT EXISTS-semantikk). + """ + try: + from google.cloud import bigquery + client = bigquery.Client(project=PROJECT_ID) + dataset_ref = client.dataset(DATASET) + table_ref = dataset_ref.table(TABLE) + + try: + client.get_table(table_ref) + logger.info(f"[token_logger] Tabell {FULL_TABLE} finnes allerede.") + return + except Exception: + pass # tabell finnes ikke — opprett + + schema = [bigquery.SchemaField(f["name"], f["type"], mode=f["mode"]) for f in BQ_SCHEMA] + table = bigquery.Table(table_ref, schema=schema) + client.create_table(table) + logger.info(f"[token_logger] Opprettet tabell {FULL_TABLE}.") + except Exception as e: + logger.warning(f"[token_logger] Kunne ikke opprette BQ-tabell (non-fatal): {e}") diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 88476a2..cb6742a 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -35,10 +35,12 @@ - Alle dashboard-moduler draggable + collapsible + layout persistert i localStorage - Compute Engine SKU-duplikater fikset — grupperes nå per tjeneste - TODO Fase-C: `/billing/budget` migreres til Firestore per bruker -- [ ] **CG3e — Tokenmeter for agenter (LLM-bruk i CostGuard)** - - Logg LLM-bruk (tokens, modell, agent) til egen BigQuery-tabell `llm_token_usage` - - Endpoint `/billing/tokens/summary` eksisterer ✅ — mangler data (tabellen ikke populert ennå) - - Kort på dashbordet vises ✅ — venter på data +- [x] **CG3e — Tokenmeter: token-logging til BigQuery** · 2026-06-10 + - `token_logger.py`: utvidet schema med `module_name`, `caller_type`, `session_id` + - `agent.py`: labeling `jason/light` | `jason/heavy`, `caller_type` propagert fra call-site + - BQ-tabell opprettes automatisk ved oppstart (`create_bq_table_if_not_exists()`) + - `/billing/tokens/summary` endepunkt finnes ✅ — vil nå få data + - Tokenkortet på dashbordet vil vise data ved neste agent-kall --- @@ -57,7 +59,7 @@ - **Dobbelt formål:** Egenbruk (intern kostnadsovervåkning) + Salgsmodus (kundedemoer og pitch) - **Modellstrategi:** `gemini-2.5-pro` for high-touch, Flash-modell (fremtidig) for bakgrunnsoppgaver - **Hierarkisk koststruktur:** Kostnader grupperes per tjeneste (ikke flate SKU-labels) -- **Token-logging:** All AI-bruk logges til `llm_token_usage`-tabell +- **Token-logging:** All AI-bruk logges til `llm_token_usage`-tabell med full labeling ### CostGuard frontend wiring - [x] **CG3a** — Bygg `static/costguard.html` @@ -66,6 +68,20 @@ - [x] **CG3d** — Test OAuth-flow - [ ] **OQ-21** — Bygg `app.threadstone.vauco.no` eller fjern CNAME +### CG4 — Token Intelligence (neste etter CG3e) + +> Bygger på `llm_token_usage`-rådata fra CG3e. Formål: full synlighet på AI-kostnader per modul, +> prosjektestimering FØR oppgave startes, og korrekt prising av oppdrag til kunder. + +| # | Oppgave | Formål | +|---|---------|--------| +| CG4a | `/billing/tokens/by-module` endepunkt — aggreger token-bruk per `module_name` og `caller_type` | Vise hvilken komponent som spiser tokens | +| CG4b | Dashboard-utvidelse: tokenmodul viser breakdown per `jason/light`, `jason/heavy`, `costguard`, `billing_agent`, `cli/gemini-tui` | Visuell oversikt per kaller | +| CG4c | Prosjektestimator: historisk snitt-tokens per oppgavetype × kompleksitetsfaktor → estimert kostnad i USD før oppgave startes | Estimer hva et prosjekt koster FØR det bygges | +| CG4d | Kundeprisingskalkulator: basert på token-historikk per modul → autogenerer prisforslag per oppdrag (basis for CostGuard-faktura til kunder) | Korrekt prising av AI-arbeid | + +**Trigger for CG4:** CG3e live og produserer data (minst 50 loggede kall). + --- ## LATER — Backlog (vent på trigger) @@ -134,6 +150,7 @@ - [x] **K1 — Repo-konsolidering** · 2026-06-09 - [x] **K3 — Fiks nattlig GDrive backup** · 2026-06-09 - [x] **CG6 — Modulært dashboard, anomalier, historikk, budsjett** · 2026-06-10 +- [x] **CG3e — Token-logging til BQ med full CG4-labeling** · 2026-06-10 ---