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:
parent
dc7fc0aa79
commit
7686fc7623
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user