Explorer
/opt/struktur/lead-engine/llm/logger.py
← Zurück ↓ Download
"""
Audit-Logging für LLM-Enrichment.
Trackt welches Modell wann verwendet wurde, Erfolg/Fehler, Kosten.
"""

import logging
from datetime import datetime
from typing import Optional


class EnrichmentLogger:
    """Speichert Enrichment-Attempts in strukturierter Form."""

    def __init__(self, log_name: str = 'enrichment'):
        self.logger = logging.getLogger(log_name)
        self.stats = {}

    def log_attempt(self,
                   contact_id: int,
                   model_used: str,
                   level: int,
                   success: bool,
                   validation_result: str,
                   tokens_used: Optional[int] = None,
                   error_msg: Optional[str] = None,
                   estimated_fields: Optional[list] = None):
        """
        Loggt einen Enrichment-Versuch.

        Args:
            contact_id: Kontakt-ID
            model_used: z.B. "gpt-5-nano"
            level: 1 (billig), 2 (mittel), 3 (stark)
            success: True/False
            validation_result: z.B. "OK" oder "MISSING_CRITICAL: pain_point"
            tokens_used: Input+Output Tokens (falls verfügbar)
            error_msg: Optionale Error-Message
        """
        timestamp = datetime.now().isoformat()

        estimated_fields = estimated_fields or []
        log_entry = {
            'timestamp': timestamp,
            'contact_id': contact_id,
            'model': model_used,
            'level': level,
            'success': success,
            'validation': validation_result,
            'tokens': tokens_used or 0,
            'error': error_msg or '',
            'estimated_fields_count': len(estimated_fields),
            'estimated_fields_list': estimated_fields,
        }

        # Logging
        status = "✓ OK" if success else f"✗ FAIL({validation_result})"
        est_info = f" est={len(estimated_fields)}" if estimated_fields else ""
        self.logger.info(
            f"[{contact_id}] {model_used} (L{level}): {status} "
            f"tokens={tokens_used or '?'}{est_info}"
        )

        # Stats sammeln
        model_key = model_used
        if model_key not in self.stats:
            self.stats[model_key] = {
                'attempts': 0,
                'successes': 0,
                'failures': 0,
                'tokens_total': 0
            }

        self.stats[model_key]['attempts'] += 1
        if success:
            self.stats[model_key]['successes'] += 1
        else:
            self.stats[model_key]['failures'] += 1

        if tokens_used:
            self.stats[model_key]['tokens_total'] += tokens_used

        return log_entry

    def log_contact_budget(self,
                          contact_id: int,
                          budget_used: int,
                          max_budget: int,
                          tokens_per_level: dict,
                          exceeded: bool):
        """
        Loggt Token-Budget-Zusammenfassung pro Kontakt.

        Args:
            contact_id:      Kontakt-ID
            budget_used:     Gesamte verbrauchte Tokens
            max_budget:      Maximales Budget
            tokens_per_level: Dict {"L1": 1200, "L2": 800, ...}
            exceeded:        True wenn Budget überschritten wurde
        """
        pct = (budget_used / max_budget * 100) if max_budget > 0 else 0
        status = "EXCEEDED" if exceeded else "OK"
        level_str = ', '.join(f"{k}={v}" for k, v in tokens_per_level.items())
        self.logger.info(
            f"[{contact_id}] Budget {status}: {budget_used}/{max_budget} "
            f"({pct:.1f}%) | per Level: {level_str or 'keine'}"
        )

        # Globale Budget-Statistik
        if 'budget' not in self.stats:
            self.stats['budget'] = {
                'total_tokens': 0,
                'total_contacts': 0,
                'exceeded_count': 0,
            }
        self.stats['budget']['total_tokens'] += budget_used
        self.stats['budget']['total_contacts'] += 1
        if exceeded:
            self.stats['budget']['exceeded_count'] += 1

    def get_success_rate(self, model: str) -> Optional[float]:
        """Berechnet Success-Rate für ein Modell (z.B. nach 50 Requests)."""
        if model not in self.stats:
            return None
        if self.stats[model]['attempts'] == 0:
            return None
        return self.stats[model]['successes'] / self.stats[model]['attempts']

    def print_stats(self):
        """Gibt Statistiken aus."""
        print("\n=== Enrichment Statistics ===")
        for model, stats in self.stats.items():
            if stats['attempts'] > 0:
                rate = (stats['successes'] / stats['attempts']) * 100
                print(f"  {model}: {stats['successes']}/{stats['attempts']} "
                      f"({rate:.1f}%) | {stats['tokens_total']} tokens")


# Global Logger
enrichment_logger = EnrichmentLogger('enrichment')