"""LlmBewerter — Port für den LLM-Call in der Antragsbewertung. Trennt die *Rohantwort* des LLMs (JSON-String) vom umgebenden Application-Flow (Retry, Prompt-Composition, Citation-Binding). Die Retry-Logik samt Temperatur-Escalation bleibt Adapter-Detail — ein zweiter Adapter (Claude, OpenAI-kompatible Proxies) kann eine ganz andere Strategie wählen. Ein späterer Tag-Schritt (Kapitel 10.5 der DDD-Bewertung) kapselt zusätzlich die JSON-Parse-Kaskade hinter dem Port; heute bekommt der Caller noch einen JSON-String zurück. """ from __future__ import annotations from dataclasses import dataclass from typing import Protocol, runtime_checkable @dataclass(frozen=True) class LlmRequest: """Alles, was der Adapter zum Generieren der Bewertung braucht — inkl. Retry-Verhalten auf der Adapter-Seite.""" system_prompt: str user_prompt: str model: str = "qwen-plus" max_retries: int = 3 max_tokens: int = 4000 base_temperature: float = 0.3 @runtime_checkable class LlmBewerter(Protocol): """Port: wandelt einen Prompt in einen JSON-String (LLM-Rohantwort). Der Adapter kümmert sich um: - Markdown-Fence-Entfernung, - JSON-Parse-Retry mit steigender Temperatur, - Content-Fingerprint-Logging zur Forensik. Raises: json.JSONDecodeError: wenn alle Retries scheitern. Höhere Schichten behandeln das als Fehlschlag der Analyse. """ async def bewerte(self, request: LlmRequest) -> dict: ...