intent_loader.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. # -*- coding: utf-8 -*-
  2. """
  3. YAML-basierter Intent-Loader fuer multilinguale Pattern/Example-Definitionen.
  4. Laedt Patterns und Examples aus YAML-Dateien, benannt nach Sprache:
  5. intents_de.yml, intents_en.yml, ...
  6. Prioritaet beim Mergen:
  7. 1. YAML-Datei fuer aktuelle Sprache (bevorzugt)
  8. 2. YAML-Datei fuer Fallback-Sprache ("de")
  9. 3. @pattern()/@example() Dekoratoren auf der Funktion (Fallback)
  10. YAML-Format:
  11. current_weather:
  12. patterns:
  13. - "(wie ist|wie wird) [denn] {das|} wetter [draussen|gerade]"
  14. - "wetter [in] {city}"
  15. examples:
  16. - "Wie ist das Wetter?"
  17. - "Wetter in Hamburg"
  18. set_timer:
  19. patterns:
  20. - "(setze|stelle) [einen] timer [auf] {duration}"
  21. examples:
  22. - "Setze Timer auf 5 Minuten"
  23. """
  24. from __future__ import annotations
  25. from pathlib import Path
  26. from typing import Any
  27. from trixy_core.utils.debug import pdebug, pwarn
  28. # Fallback-Sprache wenn die gewuenschte nicht vorhanden ist
  29. _FALLBACK_LANGUAGE = "de"
  30. # Cache: Pfad → geparste YAML-Daten
  31. _yaml_cache: dict[str, dict[str, Any]] = {}
  32. def load_intent_yaml(
  33. directory: Path,
  34. language: str = "de",
  35. ) -> dict[str, dict[str, Any]]:
  36. """
  37. Laedt Intent-Definitionen aus einer YAML-Datei.
  38. Sucht nach intents_{language}.yml im angegebenen Verzeichnis.
  39. Bei Nicht-Vorhandensein wird die Fallback-Sprache versucht.
  40. Args:
  41. directory: Verzeichnis mit der YAML-Datei
  42. language: Sprachcode (z.B. "de", "en")
  43. Returns:
  44. Dict von intent_name → {"patterns": [...], "examples": [...]}
  45. Leeres Dict wenn keine Datei gefunden
  46. """
  47. # Sprach-spezifische Datei versuchen
  48. yaml_path = directory / f"intents_{language}.yml"
  49. if not yaml_path.exists():
  50. # Fallback-Sprache
  51. if language != _FALLBACK_LANGUAGE:
  52. yaml_path = directory / f"intents_{_FALLBACK_LANGUAGE}.yml"
  53. if not yaml_path.exists():
  54. return {}
  55. return _load_yaml_file(yaml_path)
  56. def _load_yaml_file(path: Path) -> dict[str, dict[str, Any]]:
  57. """
  58. Laedt und cached eine YAML-Datei.
  59. Args:
  60. path: Pfad zur YAML-Datei
  61. Returns:
  62. Geparste Intent-Daten
  63. """
  64. cache_key = str(path)
  65. # Zeitstempel-basierter Cache
  66. try:
  67. mtime = path.stat().st_mtime
  68. except OSError:
  69. return {}
  70. cached = _yaml_cache.get(cache_key)
  71. if cached is not None and cached.get("_mtime") == mtime:
  72. return cached.get("_data", {})
  73. # YAML laden
  74. try:
  75. import yaml
  76. except ImportError:
  77. # PyYAML nicht installiert — stiller Fallback
  78. return {}
  79. try:
  80. with open(path, "r", encoding="utf-8") as f:
  81. raw = yaml.safe_load(f)
  82. except Exception as e:
  83. pwarn(f"[IntentLoader] YAML-Fehler in {path}: {e}")
  84. return {}
  85. if not isinstance(raw, dict):
  86. pwarn(f"[IntentLoader] Ungueltiges Format in {path}: Root muss dict sein")
  87. return {}
  88. # Validieren und normalisieren
  89. data: dict[str, dict[str, Any]] = {}
  90. for intent_name, intent_data in raw.items():
  91. if not isinstance(intent_data, dict):
  92. continue
  93. entry: dict[str, Any] = {}
  94. patterns = intent_data.get("patterns")
  95. if isinstance(patterns, list):
  96. entry["patterns"] = [str(p) for p in patterns if p]
  97. examples = intent_data.get("examples")
  98. if isinstance(examples, list):
  99. entry["examples"] = [str(e) for e in examples if e]
  100. if entry:
  101. data[intent_name] = entry
  102. # In Cache speichern
  103. _yaml_cache[cache_key] = {"_mtime": mtime, "_data": data}
  104. pdebug(
  105. f"[IntentLoader] {path.name}: {len(data)} Intents geladen"
  106. )
  107. return data
  108. def merge_yaml_into_metadata(
  109. metadata: dict[str, Any],
  110. yaml_data: dict[str, dict[str, Any]],
  111. ) -> dict[str, Any]:
  112. """
  113. Merged YAML-Daten in Intent-Metadaten. YAML hat Vorrang.
  114. Decorator-Patterns/Examples bleiben nur erhalten wenn
  115. in der YAML-Datei KEIN Eintrag fuer diesen Intent existiert.
  116. Args:
  117. metadata: Bestehende Intent-Metadaten (aus Dekorator)
  118. yaml_data: Geladene YAML-Daten
  119. Returns:
  120. Aktualisierte Metadaten (gleiche Referenz, in-place modifiziert)
  121. """
  122. intent_name = metadata.get("name", "")
  123. if not intent_name or intent_name not in yaml_data:
  124. return metadata
  125. yaml_entry = yaml_data[intent_name]
  126. # YAML-Patterns haben Vorrang — ersetzen Dekorator-Patterns komplett
  127. if "patterns" in yaml_entry:
  128. metadata["patterns"] = yaml_entry["patterns"]
  129. # YAML-Examples haben Vorrang — ersetzen Dekorator-Examples komplett
  130. if "examples" in yaml_entry:
  131. metadata["examples"] = yaml_entry["examples"]
  132. return metadata
  133. def load_entity_yaml(
  134. directory: Path,
  135. language: str = "de",
  136. ) -> dict[str, dict[str, Any]]:
  137. """
  138. Laedt Entity-Definitionen aus einer YAML-Datei.
  139. Sucht nach entities_{language}.yml im angegebenen Verzeichnis.
  140. Bei Nicht-Vorhandensein wird die Fallback-Sprache versucht.
  141. Args:
  142. directory: Verzeichnis mit der YAML-Datei
  143. language: Sprachcode (z.B. "de", "en")
  144. Returns:
  145. Dict von entity_name → {"values": [...], "open": bool, "regex": [...]}
  146. Leeres Dict wenn keine Datei gefunden
  147. """
  148. yaml_path = directory / f"entities_{language}.yml"
  149. if not yaml_path.exists():
  150. if language != _FALLBACK_LANGUAGE:
  151. yaml_path = directory / f"entities_{_FALLBACK_LANGUAGE}.yml"
  152. if not yaml_path.exists():
  153. return {}
  154. return _load_entity_file(yaml_path)
  155. def _load_entity_file(path: Path) -> dict[str, dict[str, Any]]:
  156. """
  157. Laedt eine Entity-YAML-Datei.
  158. Args:
  159. path: Pfad zur YAML-Datei
  160. Returns:
  161. Geparste Entity-Daten
  162. """
  163. try:
  164. import yaml
  165. except ImportError:
  166. return {}
  167. try:
  168. with open(path, "r", encoding="utf-8") as f:
  169. raw = yaml.safe_load(f)
  170. except Exception as e:
  171. pwarn(f"[IntentLoader] Entity-YAML-Fehler in {path}: {e}")
  172. return {}
  173. if not isinstance(raw, dict):
  174. pwarn(f"[IntentLoader] Ungueltiges Format in {path}: Root muss dict sein")
  175. return {}
  176. data: dict[str, dict[str, Any]] = {}
  177. for entity_name, entity_data in raw.items():
  178. if not isinstance(entity_data, dict):
  179. continue
  180. data[entity_name] = entity_data
  181. pdebug(f"[IntentLoader] {path.name}: {len(data)} Entity-Typen geladen")
  182. return data
  183. def clear_cache() -> None:
  184. """Leert den YAML-Cache (z.B. bei Sprach-Wechsel)."""
  185. _yaml_cache.clear()