| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350 |
- # -*- 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": "<intent_name>",
- "confidence": <0.0-1.0>,
- "slots": {{
- "<slot_name>": "<extrahierter_wert>",
- ...
- }}
- }}
- ```
- ## 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)
|