feat(CG3e+CG4): utvidet token-logging — caller_type, module_name, session_id + CG4-fase i ROADMAP

- 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
This commit is contained in:
chrischristiansen-glitch 2026-06-10 00:36:25 +02:00
parent dc7fc0aa79
commit 7686fc7623
3 changed files with 134 additions and 26 deletions

View File

@ -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))

View File

@ -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}")

View File

@ -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
---