""" Anhang A · Die vier Bausteine, die Kapitel 2.10 aufruft ======================================================= sandbox_ausfuehren – ruft den Sandbox-Dienst auf schaetze_kosten – rechnet eine Antwort in Dollar um betrag_extrahieren – zieht den Gesamtbetrag aus einem Lieferschein datenbank_summe – liest Anzahl und Summe aus dem Zielsystem Alle Preise nach der offiziellen Modellseite, Stand 5. September 2026. Prüfen Sie sie nach, bevor Sie damit rechnen. """ from __future__ import annotations import decimal import os import re import sqlite3 import requests # -------------------------------------------------------------------------- # 1) Sandbox aufrufen # -------------------------------------------------------------------------- SANDBOX_URL = os.environ.get("SANDBOX_URL", "http://127.0.0.1:8000/exec") SANDBOX_TOKEN = os.environ.get("SANDBOX_TOKEN", "") def sandbox_ausfuehren(sitzung_id: str, sprache: str, code: str) -> list[dict]: """Führt Code in der Sandbox aus und gibt die Elemente für `function_call_output` zurück.""" headers = {"Content-Type": "application/json"} if SANDBOX_TOKEN: headers["Authorization"] = f"Bearer {SANDBOX_TOKEN}" antwort = requests.post( SANDBOX_URL, json={"session_id": sitzung_id, "language": sprache, "code": code}, headers=headers, timeout=120, ) antwort.raise_for_status() return antwort.json()["items"] # -------------------------------------------------------------------------- # 2) Kosten schätzen # -------------------------------------------------------------------------- # Preise je 1 Mio. Token, gpt-6-astra PREIS_INPUT = decimal.Decimal("10.00") PREIS_INPUT_CACHED = decimal.Decimal("1.00") PREIS_CACHE_WRITE = decimal.Decimal("12.50") PREIS_OUTPUT = decimal.Decimal("50.00") # Ab dieser Grenze: 2x Input/Cache, 1,5x Output — für die GESAMTE Anfrage. LANGKONTEXT_GRENZE = 272_000 FAKTOR_INPUT_LANG = decimal.Decimal("2.0") FAKTOR_OUTPUT_LANG = decimal.Decimal("1.5") MIO = decimal.Decimal(1_000_000) def schaetze_kosten(usage, fast_mode: bool = False) -> decimal.Decimal: """Kosten einer einzelnen Antwort in USD. `usage` ist das usage-Objekt der Responses-API oder ein gleich aufgebautes dict. Nicht abgerechnet werden hier: Gebühren je Tool-Aufruf. Die Modellseite erwähnt sie, die Preisseite beziffert sie nicht — messen Sie sie an echten Läufen nach (Kapitel 2.9). """ def feld(name: str, default: int = 0) -> int: if isinstance(usage, dict): wert = usage.get(name, default) else: wert = getattr(usage, name, default) if isinstance(wert, dict): # z.B. input_tokens_details return wert return int(wert or 0) input_tokens = feld("input_tokens") output_tokens = feld("output_tokens") details = feld("input_tokens_details", {}) if not isinstance(details, dict): details = {} cached = int(details.get("cached_tokens", 0) or 0) cache_write = int(details.get("cache_creation_tokens", 0) or 0) frisch = max(input_tokens - cached - cache_write, 0) f_in = FAKTOR_INPUT_LANG if input_tokens > LANGKONTEXT_GRENZE else decimal.Decimal(1) f_out = FAKTOR_OUTPUT_LANG if input_tokens > LANGKONTEXT_GRENZE else decimal.Decimal(1) kosten = ( decimal.Decimal(frisch) / MIO * PREIS_INPUT * f_in + decimal.Decimal(cached) / MIO * PREIS_INPUT_CACHED * f_in + decimal.Decimal(cache_write) / MIO * PREIS_CACHE_WRITE * f_in + decimal.Decimal(output_tokens) / MIO * PREIS_OUTPUT * f_out ) if fast_mode: kosten *= 2 return kosten.quantize(decimal.Decimal("0.000001")) # -------------------------------------------------------------------------- # 3) Betrag aus einem Lieferschein ziehen # -------------------------------------------------------------------------- # Deutsche Schreibweise: Punkt als Tausender-, Komma als Dezimaltrenner. _BETRAG = re.compile( r"(?:gesamt|gesamtbetrag|summe|rechnungsbetrag|endbetrag|total)" r"[^\d\-]{0,40}?" r"(\d{1,3}(?:\.\d{3})*|\d+)(?:,(\d{2}))?", re.IGNORECASE, ) def betrag_extrahieren(text: str | None) -> decimal.Decimal | None: """Gesamtbetrag oder None. None heißt: überspringen und melden — nicht raten. Bei mehreren Treffern gewinnt der GRÖSSTE. Auf Lieferscheinen stehen Zwischensummen und Nettobeträge oft mit denselben Stichwörtern daneben; der Gesamtbetrag ist der höchste davon. """ if not text: return None treffer = _BETRAG.findall(text) if not treffer: return None werte = [] for ganz, nachkomma in treffer: roh = ganz.replace(".", "") + "." + (nachkomma or "00") try: werte.append(decimal.Decimal(roh)) except decimal.InvalidOperation: continue return max(werte) if werte else None # -------------------------------------------------------------------------- # 4) Abnahme: direkt aus dem Zielsystem lesen # -------------------------------------------------------------------------- DB_PFAD = os.environ.get("ZIEL_DB", "erfassung.sqlite") def datenbank_summe() -> tuple[int, decimal.Decimal]: """Anzahl und Summe der erfassten Datensätze — aus der Datenbank, nicht aus der Antwort des Agenten (Kapitel 2.7).""" with sqlite3.connect(DB_PFAD) as con: zeile = con.execute( "SELECT COUNT(*), COALESCE(SUM(betrag_cent), 0) FROM lieferscheine" ).fetchone() anzahl = int(zeile[0]) summe = (decimal.Decimal(int(zeile[1])) / 100).quantize(decimal.Decimal("0.01")) return anzahl, summe