# -*- coding: utf-8 -*- """ System-Prompt Builder für LLM NLP. Separate Prompts für Intent-Erkennung und Antwort-Generierung. """ import json from typing import Any def build_intent_recognition_prompt( available_intents: list[dict[str, Any]], room_id: str = "", language: str = "de", max_tokens: int = 3000, ) -> str: """ Erstellt den System-Prompt für Intent-Erkennung. WICHTIG: Dieser Prompt generiert KEINE Antwort, nur Intent + Slots. Die Antwort wird erst nach Handler-Ausführung generiert. Args: available_intents: Liste verfügbarer Intents mit Metadaten room_id: Aktuelle Raum-ID für Kontext language: Sprache für Slot-Extraktion max_tokens: Maximale Token-Anzahl fuer den Intent-Katalog Returns: System-Prompt als String """ # Intents kompakt formatieren und auf Token-Budget trimmen compact_intents = _compact_intents_for_llm(available_intents) compact_intents = _trim_intents_to_budget(compact_intents, max_tokens) intents_json = json.dumps(compact_intents, ensure_ascii=False) prompt = f"""Du bist ein Intent-Erkennungssystem für einen Sprachassistenten. Deine EINZIGE Aufgabe ist es, den Intent und die Slots aus der Benutzeranfrage zu extrahieren. Du generierst KEINE Antwort - das macht ein anderes System nachdem die Aktion ausgeführt wurde. ## Verfügbare Intents ```json {intents_json} ``` ## Aktuelle Umgebung - Raum: {room_id or "unbekannt"} - Sprache: {language} ## Antwortformat Antworte IMMER und NUR mit diesem JSON-Format: ```json {{ "intent": "", "confidence": <0.0-1.0>, "slots": {{ "": "", ... }} }} ``` ## Regeln 1. **Intent-Erkennung**: Wähle den Intent der am besten zur Anfrage passt 2. **Confidence**: - 0.9+ für eindeutige Matches - 0.6-0.8 für wahrscheinliche Matches - 0.3-0.5 für unsichere Matches 3. **Slots**: Extrahiere ALLE relevanten Werte aus der Anfrage 4. **KEINE Antwort**: Generiere NIEMALS einen Antworttext ## Unbekannte Anfragen Wenn kein Intent passt: ```json {{ "intent": "unknown", "confidence": 0.2, "slots": {{}} }} ``` ## Beispiele Benutzer: "Schalte das Licht im Wohnzimmer ein" ```json {{ "intent": "turn_on_device", "confidence": 0.95, "slots": {{ "device": "Licht", "room": "Wohnzimmer" }} }} ``` Benutzer: "Wer war Ludwig van Beethoven?" ```json {{ "intent": "wikipedia_search", "confidence": 0.9, "slots": {{ "query": "Ludwig van Beethoven" }} }} ``` Benutzer: "Wie geht es dir?" ```json {{ "intent": "health_check", "confidence": 0.85, "slots": {{}} }} ``` Antworte NUR mit dem JSON-Objekt, ohne zusätzlichen Text oder Erklärungen.""" return prompt def build_response_generation_prompt( intent: str, slots: dict[str, Any], handler_result: dict[str, Any], original_text: str, room_id: str = "", language: str = "de", ) -> str: """ Erstellt den System-Prompt für Antwort-Generierung. Wird NACH der Handler-Ausführung aufgerufen, um eine natürliche Antwort aus den Handler-Daten zu generieren. Args: intent: Erkannter Intent slots: Extrahierte Slots handler_result: Ergebnis des Intent-Handlers original_text: Ursprüngliche Benutzeranfrage room_id: Raum-ID language: Antwortsprache Returns: System-Prompt als String """ language_name = _get_language_name(language) # Handler-Ergebnis formatieren result_json = json.dumps(handler_result, indent=2, ensure_ascii=False) slots_json = json.dumps(slots, indent=2, ensure_ascii=False) prompt = f"""Du bist Trixy, ein freundlicher Sprachassistent. Generiere eine natürliche, kurze Antwort basierend auf dem Ergebnis einer Aktion. ## Kontext - **Benutzeranfrage**: "{original_text}" - **Erkannter Intent**: {intent} - **Extrahierte Parameter**: ```json {slots_json} ``` - **Aktionsergebnis**: ```json {result_json} ``` - **Raum**: {room_id or "unbekannt"} ## Regeln 1. Antworte auf {language_name} 2. Halte die Antwort KURZ (1-2 Sätze) 3. Sei freundlich aber nicht übertrieben 4. Verwende die Daten aus dem Aktionsergebnis 5. Bei Fehlern: Erkläre kurz was nicht funktioniert hat ## Antwortformat Antworte NUR mit dem Antworttext, ohne JSON oder Formatierung. Die Antwort wird direkt an Text-to-Speech weitergegeben. ## Beispiele Für Intent "turn_on_device" mit Ergebnis {{"success": true, "device": "Licht"}}: "Das Licht wurde eingeschaltet." Für Intent "wikipedia_search" mit Ergebnis {{"title": "Ludwig van Beethoven", "summary": "Deutscher Komponist, geboren 1770..."}}: "Ludwig van Beethoven war ein deutscher Komponist, geboren 1770 in Bonn." Für Intent "health_check" mit Ergebnis {{"status": "ok", "uptime": "3 Tage"}}: "Mir geht es gut! Ich laufe seit 3 Tagen ohne Probleme." Generiere jetzt die Antwort:""" return prompt def build_fallback_response_prompt( original_text: str, language: str = "de", ) -> str: """ Erstellt einen Prompt für Fallback-Antworten bei unbekannten Intents. Args: original_text: Ursprüngliche Benutzeranfrage language: Antwortsprache Returns: System-Prompt als String """ language_name = _get_language_name(language) prompt = f"""Du bist Trixy, ein freundlicher Sprachassistent. Der Benutzer hat etwas gesagt, das ich nicht als bekannten Befehl erkannt habe. **Benutzeranfrage**: "{original_text}" Generiere eine kurze, freundliche Antwort auf {language_name} die: 1. Höflich erklärt, dass du die Anfrage nicht verstanden hast 2. Optional einen Hinweis gibt, was du kannst (Smart Home, Informationen, etc.) 3. KURZ bleibt (1-2 Sätze) Antworte NUR mit dem Antworttext:""" return prompt def _trim_intents_to_budget( intents: list[dict[str, Any]], max_tokens: int = 3000, ) -> list[dict[str, Any]]: """ Kuerzt den Intent-Katalog auf ein Token-Budget. Strategie (in dieser Reihenfolge): 1. Beispiele entfernen (spart am meisten) 2. Beschreibungen entfernen 3. Intents weglassen (letzte Option) Grobe Token-Schaetzung: 1 Token ≈ 4 Zeichen """ def estimate_tokens(data: list) -> int: return len(json.dumps(data, ensure_ascii=False)) // 4 # Bereits klein genug? if estimate_tokens(intents) <= max_tokens: return intents # Stufe 1: Beispiele auf 1 reduzieren for intent in intents: if "examples" in intent: intent["examples"] = intent["examples"][:1] if estimate_tokens(intents) <= max_tokens: return intents # Stufe 2: Alle Beispiele entfernen for intent in intents: intent.pop("examples", None) if estimate_tokens(intents) <= max_tokens: return intents # Stufe 3: Beschreibungen entfernen for intent in intents: intent.pop("description", None) if estimate_tokens(intents) <= max_tokens: return intents # Stufe 4: Intents kuerzen (die letzten weglassen) while estimate_tokens(intents) > max_tokens and len(intents) > 10: intents.pop() return intents def _compact_intents_for_llm(intents: list[dict[str, Any]]) -> list[dict[str, Any]]: """ Reduziert Intent-Daten auf das fuer das LLM Relevante. Entfernt patterns (nur fuer Keyword-Matcher), kuerzt examples und entfernt leere Felder. Spart ca. 50-70% Tokens. """ compact = [] for intent in intents: entry: dict[str, Any] = {"name": intent.get("name", "")} desc = intent.get("description", "") if desc: entry["description"] = desc # Slots: nur Name und Typ (ohne Beispiele/Beschreibung) slots = intent.get("slots", {}) if slots: compact_slots = {} for slot_name, slot_data in slots.items(): if isinstance(slot_data, dict): compact_slots[slot_name] = slot_data.get("type", "str") else: compact_slots[slot_name] = str(slot_data) if compact_slots: entry["slots"] = compact_slots # Maximal 2 Examples (genug fuer LLM-Kontext) examples = intent.get("examples", []) if examples: entry["examples"] = examples[:2] compact.append(entry) return compact def _get_language_name(language: str) -> str: """Gibt den Sprachnamen für den Prompt zurück.""" language_map = { "de": "Deutsch", "en": "English", "fr": "Français", "es": "Español", "it": "Italiano", "nl": "Nederlands", "pl": "Polski", "pt": "Português", } return language_map.get(language, language) # Backward compatibility alias def build_system_prompt( available_intents: list[dict[str, Any]], room_id: str = "", language: str = "de", ) -> str: """ Alias für build_intent_recognition_prompt. Für Abwärtskompatibilität beibehalten. """ return build_intent_recognition_prompt(available_intents, room_id, language)