system.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. # -*- coding: utf-8 -*-
  2. """
  3. System-Prompt Builder für LLM NLP.
  4. Separate Prompts für Intent-Erkennung und Antwort-Generierung.
  5. """
  6. import json
  7. from typing import Any
  8. def build_intent_recognition_prompt(
  9. available_intents: list[dict[str, Any]],
  10. room_id: str = "",
  11. language: str = "de",
  12. max_tokens: int = 3000,
  13. ) -> str:
  14. """
  15. Erstellt den System-Prompt für Intent-Erkennung.
  16. WICHTIG: Dieser Prompt generiert KEINE Antwort, nur Intent + Slots.
  17. Die Antwort wird erst nach Handler-Ausführung generiert.
  18. Args:
  19. available_intents: Liste verfügbarer Intents mit Metadaten
  20. room_id: Aktuelle Raum-ID für Kontext
  21. language: Sprache für Slot-Extraktion
  22. max_tokens: Maximale Token-Anzahl fuer den Intent-Katalog
  23. Returns:
  24. System-Prompt als String
  25. """
  26. # Intents kompakt formatieren und auf Token-Budget trimmen
  27. compact_intents = _compact_intents_for_llm(available_intents)
  28. compact_intents = _trim_intents_to_budget(compact_intents, max_tokens)
  29. intents_json = json.dumps(compact_intents, ensure_ascii=False)
  30. prompt = f"""Du bist ein Intent-Erkennungssystem für einen Sprachassistenten.
  31. Deine EINZIGE Aufgabe ist es, den Intent und die Slots aus der Benutzeranfrage zu extrahieren.
  32. Du generierst KEINE Antwort - das macht ein anderes System nachdem die Aktion ausgeführt wurde.
  33. ## Verfügbare Intents
  34. ```json
  35. {intents_json}
  36. ```
  37. ## Aktuelle Umgebung
  38. - Raum: {room_id or "unbekannt"}
  39. - Sprache: {language}
  40. ## Antwortformat
  41. Antworte IMMER und NUR mit diesem JSON-Format:
  42. ```json
  43. {{
  44. "intent": "<intent_name>",
  45. "confidence": <0.0-1.0>,
  46. "slots": {{
  47. "<slot_name>": "<extrahierter_wert>",
  48. ...
  49. }}
  50. }}
  51. ```
  52. ## Regeln
  53. 1. **Intent-Erkennung**: Wähle den Intent der am besten zur Anfrage passt
  54. 2. **Confidence**:
  55. - 0.9+ für eindeutige Matches
  56. - 0.6-0.8 für wahrscheinliche Matches
  57. - 0.3-0.5 für unsichere Matches
  58. 3. **Slots**: Extrahiere ALLE relevanten Werte aus der Anfrage
  59. 4. **KEINE Antwort**: Generiere NIEMALS einen Antworttext
  60. ## Unbekannte Anfragen
  61. Wenn kein Intent passt:
  62. ```json
  63. {{
  64. "intent": "unknown",
  65. "confidence": 0.2,
  66. "slots": {{}}
  67. }}
  68. ```
  69. ## Beispiele
  70. Benutzer: "Schalte das Licht im Wohnzimmer ein"
  71. ```json
  72. {{
  73. "intent": "turn_on_device",
  74. "confidence": 0.95,
  75. "slots": {{
  76. "device": "Licht",
  77. "room": "Wohnzimmer"
  78. }}
  79. }}
  80. ```
  81. Benutzer: "Wer war Ludwig van Beethoven?"
  82. ```json
  83. {{
  84. "intent": "wikipedia_search",
  85. "confidence": 0.9,
  86. "slots": {{
  87. "query": "Ludwig van Beethoven"
  88. }}
  89. }}
  90. ```
  91. Benutzer: "Wie geht es dir?"
  92. ```json
  93. {{
  94. "intent": "health_check",
  95. "confidence": 0.85,
  96. "slots": {{}}
  97. }}
  98. ```
  99. Antworte NUR mit dem JSON-Objekt, ohne zusätzlichen Text oder Erklärungen."""
  100. return prompt
  101. def build_response_generation_prompt(
  102. intent: str,
  103. slots: dict[str, Any],
  104. handler_result: dict[str, Any],
  105. original_text: str,
  106. room_id: str = "",
  107. language: str = "de",
  108. ) -> str:
  109. """
  110. Erstellt den System-Prompt für Antwort-Generierung.
  111. Wird NACH der Handler-Ausführung aufgerufen, um eine
  112. natürliche Antwort aus den Handler-Daten zu generieren.
  113. Args:
  114. intent: Erkannter Intent
  115. slots: Extrahierte Slots
  116. handler_result: Ergebnis des Intent-Handlers
  117. original_text: Ursprüngliche Benutzeranfrage
  118. room_id: Raum-ID
  119. language: Antwortsprache
  120. Returns:
  121. System-Prompt als String
  122. """
  123. language_name = _get_language_name(language)
  124. # Handler-Ergebnis formatieren
  125. result_json = json.dumps(handler_result, indent=2, ensure_ascii=False)
  126. slots_json = json.dumps(slots, indent=2, ensure_ascii=False)
  127. prompt = f"""Du bist Trixy, ein freundlicher Sprachassistent.
  128. Generiere eine natürliche, kurze Antwort basierend auf dem Ergebnis einer Aktion.
  129. ## Kontext
  130. - **Benutzeranfrage**: "{original_text}"
  131. - **Erkannter Intent**: {intent}
  132. - **Extrahierte Parameter**:
  133. ```json
  134. {slots_json}
  135. ```
  136. - **Aktionsergebnis**:
  137. ```json
  138. {result_json}
  139. ```
  140. - **Raum**: {room_id or "unbekannt"}
  141. ## Regeln
  142. 1. Antworte auf {language_name}
  143. 2. Halte die Antwort KURZ (1-2 Sätze)
  144. 3. Sei freundlich aber nicht übertrieben
  145. 4. Verwende die Daten aus dem Aktionsergebnis
  146. 5. Bei Fehlern: Erkläre kurz was nicht funktioniert hat
  147. ## Antwortformat
  148. Antworte NUR mit dem Antworttext, ohne JSON oder Formatierung.
  149. Die Antwort wird direkt an Text-to-Speech weitergegeben.
  150. ## Beispiele
  151. Für Intent "turn_on_device" mit Ergebnis {{"success": true, "device": "Licht"}}:
  152. "Das Licht wurde eingeschaltet."
  153. Für Intent "wikipedia_search" mit Ergebnis {{"title": "Ludwig van Beethoven", "summary": "Deutscher Komponist, geboren 1770..."}}:
  154. "Ludwig van Beethoven war ein deutscher Komponist, geboren 1770 in Bonn."
  155. Für Intent "health_check" mit Ergebnis {{"status": "ok", "uptime": "3 Tage"}}:
  156. "Mir geht es gut! Ich laufe seit 3 Tagen ohne Probleme."
  157. Generiere jetzt die Antwort:"""
  158. return prompt
  159. def build_fallback_response_prompt(
  160. original_text: str,
  161. language: str = "de",
  162. ) -> str:
  163. """
  164. Erstellt einen Prompt für Fallback-Antworten bei unbekannten Intents.
  165. Args:
  166. original_text: Ursprüngliche Benutzeranfrage
  167. language: Antwortsprache
  168. Returns:
  169. System-Prompt als String
  170. """
  171. language_name = _get_language_name(language)
  172. prompt = f"""Du bist Trixy, ein freundlicher Sprachassistent.
  173. Der Benutzer hat etwas gesagt, das ich nicht als bekannten Befehl erkannt habe.
  174. **Benutzeranfrage**: "{original_text}"
  175. Generiere eine kurze, freundliche Antwort auf {language_name} die:
  176. 1. Höflich erklärt, dass du die Anfrage nicht verstanden hast
  177. 2. Optional einen Hinweis gibt, was du kannst (Smart Home, Informationen, etc.)
  178. 3. KURZ bleibt (1-2 Sätze)
  179. Antworte NUR mit dem Antworttext:"""
  180. return prompt
  181. def _trim_intents_to_budget(
  182. intents: list[dict[str, Any]], max_tokens: int = 3000,
  183. ) -> list[dict[str, Any]]:
  184. """
  185. Kuerzt den Intent-Katalog auf ein Token-Budget.
  186. Strategie (in dieser Reihenfolge):
  187. 1. Beispiele entfernen (spart am meisten)
  188. 2. Beschreibungen entfernen
  189. 3. Intents weglassen (letzte Option)
  190. Grobe Token-Schaetzung: 1 Token ≈ 4 Zeichen
  191. """
  192. def estimate_tokens(data: list) -> int:
  193. return len(json.dumps(data, ensure_ascii=False)) // 4
  194. # Bereits klein genug?
  195. if estimate_tokens(intents) <= max_tokens:
  196. return intents
  197. # Stufe 1: Beispiele auf 1 reduzieren
  198. for intent in intents:
  199. if "examples" in intent:
  200. intent["examples"] = intent["examples"][:1]
  201. if estimate_tokens(intents) <= max_tokens:
  202. return intents
  203. # Stufe 2: Alle Beispiele entfernen
  204. for intent in intents:
  205. intent.pop("examples", None)
  206. if estimate_tokens(intents) <= max_tokens:
  207. return intents
  208. # Stufe 3: Beschreibungen entfernen
  209. for intent in intents:
  210. intent.pop("description", None)
  211. if estimate_tokens(intents) <= max_tokens:
  212. return intents
  213. # Stufe 4: Intents kuerzen (die letzten weglassen)
  214. while estimate_tokens(intents) > max_tokens and len(intents) > 10:
  215. intents.pop()
  216. return intents
  217. def _compact_intents_for_llm(intents: list[dict[str, Any]]) -> list[dict[str, Any]]:
  218. """
  219. Reduziert Intent-Daten auf das fuer das LLM Relevante.
  220. Entfernt patterns (nur fuer Keyword-Matcher), kuerzt examples
  221. und entfernt leere Felder. Spart ca. 50-70% Tokens.
  222. """
  223. compact = []
  224. for intent in intents:
  225. entry: dict[str, Any] = {"name": intent.get("name", "")}
  226. desc = intent.get("description", "")
  227. if desc:
  228. entry["description"] = desc
  229. # Slots: nur Name und Typ (ohne Beispiele/Beschreibung)
  230. slots = intent.get("slots", {})
  231. if slots:
  232. compact_slots = {}
  233. for slot_name, slot_data in slots.items():
  234. if isinstance(slot_data, dict):
  235. compact_slots[slot_name] = slot_data.get("type", "str")
  236. else:
  237. compact_slots[slot_name] = str(slot_data)
  238. if compact_slots:
  239. entry["slots"] = compact_slots
  240. # Maximal 2 Examples (genug fuer LLM-Kontext)
  241. examples = intent.get("examples", [])
  242. if examples:
  243. entry["examples"] = examples[:2]
  244. compact.append(entry)
  245. return compact
  246. def _get_language_name(language: str) -> str:
  247. """Gibt den Sprachnamen für den Prompt zurück."""
  248. language_map = {
  249. "de": "Deutsch",
  250. "en": "English",
  251. "fr": "Français",
  252. "es": "Español",
  253. "it": "Italiano",
  254. "nl": "Nederlands",
  255. "pl": "Polski",
  256. "pt": "Português",
  257. }
  258. return language_map.get(language, language)
  259. # Backward compatibility alias
  260. def build_system_prompt(
  261. available_intents: list[dict[str, Any]],
  262. room_id: str = "",
  263. language: str = "de",
  264. ) -> str:
  265. """
  266. Alias für build_intent_recognition_prompt.
  267. Für Abwärtskompatibilität beibehalten.
  268. """
  269. return build_intent_recognition_prompt(available_intents, room_id, language)