intent_registry.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. # -*- coding: utf-8 -*-
  2. """
  3. Intent Registry.
  4. Globale Registry für alle verfügbaren Intents im System.
  5. Wird vom NLP-Provider verwendet, um verfügbare Intents zu ermitteln.
  6. """
  7. from dataclasses import dataclass, field
  8. from typing import Any, Callable, TYPE_CHECKING
  9. import threading
  10. if TYPE_CHECKING:
  11. from trixy_core.plugins.trixy_plugin import TrixyPlugin
  12. @dataclass
  13. class IntentSlot:
  14. """
  15. Definition eines Intent-Slots.
  16. Attributes:
  17. name: Slot-Name
  18. slot_type: Datentyp des Slots
  19. required: Ist der Slot erforderlich?
  20. description: Beschreibung des Slots
  21. examples: Beispielwerte
  22. """
  23. name: str
  24. slot_type: type = str
  25. required: bool = False
  26. description: str = ""
  27. examples: list[str] = field(default_factory=list)
  28. def to_dict(self) -> dict[str, Any]:
  29. """Konvertiert den Slot in ein Dictionary."""
  30. return {
  31. "name": self.name,
  32. "type": self.slot_type.__name__,
  33. "required": self.required,
  34. "description": self.description,
  35. "examples": self.examples,
  36. }
  37. @dataclass
  38. class IntentDefinition:
  39. """
  40. Definition eines Intents.
  41. Attributes:
  42. name: Eindeutiger Intent-Name
  43. handler: Handler-Funktion
  44. plugin_name: Name des registrierenden Plugins
  45. slots: Slot-Definitionen
  46. examples: Beispiel-Phrasen für den Intent
  47. description: Beschreibung des Intents
  48. priority: Priorität (höher = wird bevorzugt bei Konflikten)
  49. enabled: Ist der Intent aktiv?
  50. """
  51. name: str
  52. handler: Callable[..., Any]
  53. plugin_name: str = ""
  54. slots: dict[str, IntentSlot] = field(default_factory=dict)
  55. examples: list[str] = field(default_factory=list)
  56. description: str = ""
  57. priority: int = 0
  58. enabled: bool = True
  59. patterns: list[str] = field(default_factory=list)
  60. def to_dict(self) -> dict[str, Any]:
  61. """
  62. Konvertiert den Intent in ein Dictionary für NLP-Provider.
  63. Returns:
  64. Dictionary mit Intent-Informationen
  65. """
  66. result = {
  67. "name": self.name,
  68. "description": self.description,
  69. "slots": {name: slot.to_dict() for name, slot in self.slots.items()},
  70. "examples": self.examples,
  71. "plugin": self.plugin_name,
  72. }
  73. if self.patterns:
  74. result["patterns"] = self.patterns
  75. return result
  76. class IntentRegistry:
  77. """
  78. Singleton Registry für alle Intents im System.
  79. Plugins registrieren ihre Intents hier, und NLP-Provider
  80. fragen die Registry ab, um verfügbare Intents zu ermitteln.
  81. Verwendung:
  82. # Intent registrieren
  83. registry = IntentRegistry.get_instance()
  84. registry.register(IntentDefinition(
  85. name="turn_on_light",
  86. handler=my_handler,
  87. plugin_name="smarthome",
  88. slots={"device": IntentSlot("device", str, required=True)},
  89. examples=["Licht an", "Lampe einschalten"]
  90. ))
  91. # Intents für NLP abfragen
  92. intents = registry.get_all_as_dict()
  93. # Handler aufrufen
  94. handler = registry.get_handler("turn_on_light")
  95. if handler:
  96. await handler(intent_data)
  97. """
  98. _instance: "IntentRegistry | None" = None
  99. _lock = threading.Lock()
  100. def __new__(cls) -> "IntentRegistry":
  101. """Singleton-Pattern."""
  102. if cls._instance is None:
  103. with cls._lock:
  104. if cls._instance is None:
  105. cls._instance = super().__new__(cls)
  106. cls._instance._initialized = False
  107. return cls._instance
  108. def __init__(self) -> None:
  109. """Initialisiert die Registry."""
  110. if self._initialized:
  111. return
  112. self._intents: dict[str, IntentDefinition] = {}
  113. self._plugin_intents: dict[str, list[str]] = {} # Plugin -> Intent-Namen
  114. self._initialized = True
  115. @classmethod
  116. def get_instance(cls) -> "IntentRegistry":
  117. """Gibt die Singleton-Instanz zurück."""
  118. return cls()
  119. def register(self, intent: IntentDefinition) -> bool:
  120. """
  121. Registriert einen Intent.
  122. Args:
  123. intent: Intent-Definition
  124. Returns:
  125. True bei Erfolg, False wenn Intent bereits existiert
  126. """
  127. if intent.name in self._intents:
  128. existing = self._intents[intent.name]
  129. # Bei gleichem Plugin ersetzen
  130. if existing.plugin_name == intent.plugin_name:
  131. self._intents[intent.name] = intent
  132. return True
  133. # Bei höherer Priorität ersetzen
  134. if intent.priority > existing.priority:
  135. self._intents[intent.name] = intent
  136. self._update_plugin_intents(existing.plugin_name, intent.plugin_name, intent.name)
  137. return True
  138. return False
  139. self._intents[intent.name] = intent
  140. # Plugin-Zuordnung aktualisieren
  141. if intent.plugin_name:
  142. if intent.plugin_name not in self._plugin_intents:
  143. self._plugin_intents[intent.plugin_name] = []
  144. self._plugin_intents[intent.plugin_name].append(intent.name)
  145. return True
  146. def _update_plugin_intents(
  147. self, old_plugin: str, new_plugin: str, intent_name: str
  148. ) -> None:
  149. """Aktualisiert die Plugin-Intent-Zuordnung."""
  150. if old_plugin and old_plugin in self._plugin_intents:
  151. if intent_name in self._plugin_intents[old_plugin]:
  152. self._plugin_intents[old_plugin].remove(intent_name)
  153. if new_plugin:
  154. if new_plugin not in self._plugin_intents:
  155. self._plugin_intents[new_plugin] = []
  156. if intent_name not in self._plugin_intents[new_plugin]:
  157. self._plugin_intents[new_plugin].append(intent_name)
  158. def unregister(self, intent_name: str) -> bool:
  159. """
  160. Entfernt einen Intent aus der Registry.
  161. Args:
  162. intent_name: Name des Intents
  163. Returns:
  164. True bei Erfolg
  165. """
  166. if intent_name not in self._intents:
  167. return False
  168. intent = self._intents.pop(intent_name)
  169. # Plugin-Zuordnung entfernen
  170. if intent.plugin_name and intent.plugin_name in self._plugin_intents:
  171. if intent_name in self._plugin_intents[intent.plugin_name]:
  172. self._plugin_intents[intent.plugin_name].remove(intent_name)
  173. return True
  174. def unregister_plugin(self, plugin_name: str) -> int:
  175. """
  176. Entfernt alle Intents eines Plugins.
  177. Args:
  178. plugin_name: Plugin-Name
  179. Returns:
  180. Anzahl entfernter Intents
  181. """
  182. if plugin_name not in self._plugin_intents:
  183. return 0
  184. intent_names = self._plugin_intents.pop(plugin_name, []).copy()
  185. count = 0
  186. for name in intent_names:
  187. if name in self._intents:
  188. del self._intents[name]
  189. count += 1
  190. return count
  191. def get(self, intent_name: str) -> IntentDefinition | None:
  192. """
  193. Gibt eine Intent-Definition zurück.
  194. Args:
  195. intent_name: Name des Intents
  196. Returns:
  197. IntentDefinition oder None
  198. """
  199. return self._intents.get(intent_name)
  200. def resolve_intent(self, intent_name: str) -> IntentDefinition | None:
  201. """
  202. Loest einen Intent-Namen auf (exakt oder Suffix-Match).
  203. Sucht zuerst nach exaktem Match. Falls nicht gefunden,
  204. wird ein Suffix-Match versucht (z.B. "weather" findet
  205. "current_weather"). Nur bei genau einem Treffer wird
  206. der Suffix-Match akzeptiert.
  207. Args:
  208. intent_name: Name des Intents (ggf. verkuerzt vom LLM)
  209. Returns:
  210. IntentDefinition oder None
  211. """
  212. # Exakter Match
  213. intent = self._intents.get(intent_name)
  214. if intent and intent.enabled:
  215. return intent
  216. # Suffix-Match: LLM gibt manchmal verkuerzte Namen zurueck
  217. candidates = [
  218. i for i in self._intents.values()
  219. if i.enabled and i.name.endswith(f"_{intent_name}")
  220. ]
  221. if len(candidates) == 1:
  222. return candidates[0]
  223. return None
  224. def get_handler(self, intent_name: str) -> Callable[..., Any] | None:
  225. """
  226. Gibt den Handler für einen Intent zurück.
  227. Nutzt resolve_intent() fuer exakten und Suffix-Match.
  228. Args:
  229. intent_name: Name des Intents
  230. Returns:
  231. Handler-Funktion oder None
  232. """
  233. intent = self.resolve_intent(intent_name)
  234. if intent:
  235. return intent.handler
  236. return None
  237. def get_all(self) -> list[IntentDefinition]:
  238. """
  239. Gibt alle registrierten Intents zurück.
  240. Returns:
  241. Liste aller IntentDefinitions
  242. """
  243. return list(self._intents.values())
  244. def get_enabled(self) -> list[IntentDefinition]:
  245. """
  246. Gibt alle aktivierten Intents zurück.
  247. Returns:
  248. Liste aktivierter IntentDefinitions
  249. """
  250. return [i for i in self._intents.values() if i.enabled]
  251. def get_all_as_dict(self) -> list[dict[str, Any]]:
  252. """
  253. Gibt alle Intents als Dictionary-Liste zurück.
  254. Wird vom NLP-Provider verwendet, um verfügbare Intents
  255. in den System-Prompt aufzunehmen.
  256. Returns:
  257. Liste von Intent-Dictionaries
  258. """
  259. return [i.to_dict() for i in self._intents.values() if i.enabled]
  260. def get_plugin_intents(self, plugin_name: str) -> list[IntentDefinition]:
  261. """
  262. Gibt alle Intents eines Plugins zurück.
  263. Args:
  264. plugin_name: Plugin-Name
  265. Returns:
  266. Liste der Plugin-Intents
  267. """
  268. intent_names = self._plugin_intents.get(plugin_name, [])
  269. return [self._intents[name] for name in intent_names if name in self._intents]
  270. def has_intent(self, intent_name: str) -> bool:
  271. """Prüft ob ein Intent registriert ist."""
  272. return intent_name in self._intents
  273. def enable(self, intent_name: str) -> bool:
  274. """
  275. Aktiviert einen Intent.
  276. Args:
  277. intent_name: Name des Intents
  278. Returns:
  279. True bei Erfolg
  280. """
  281. if intent_name in self._intents:
  282. self._intents[intent_name].enabled = True
  283. return True
  284. return False
  285. def disable(self, intent_name: str) -> bool:
  286. """
  287. Deaktiviert einen Intent.
  288. Args:
  289. intent_name: Name des Intents
  290. Returns:
  291. True bei Erfolg
  292. """
  293. if intent_name in self._intents:
  294. self._intents[intent_name].enabled = False
  295. return True
  296. return False
  297. def clear(self) -> None:
  298. """Löscht alle registrierten Intents."""
  299. self._intents.clear()
  300. self._plugin_intents.clear()
  301. def count(self) -> int:
  302. """Anzahl registrierter Intents."""
  303. return len(self._intents)
  304. def __len__(self) -> int:
  305. """Anzahl registrierter Intents."""
  306. return len(self._intents)
  307. def __contains__(self, intent_name: str) -> bool:
  308. """Prüft ob ein Intent registriert ist."""
  309. return intent_name in self._intents
  310. def __iter__(self):
  311. """Iterator über alle Intent-Definitionen."""
  312. return iter(self._intents.values())