test_intent_decorators.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583
  1. # -*- coding: utf-8 -*-
  2. """
  3. Tests fuer die Intent-Dekoratoren (@intent, @pattern, @example).
  4. Testet Stackbarkeit, Slot-Erkennung aus Funktionssignatur,
  5. Zusammenspiel mit @intent und discover_intent_handlers.
  6. """
  7. import asyncio
  8. import inspect
  9. import pytest
  10. from typing import Any
  11. from trixy_core.nlp.decorators import (
  12. intent,
  13. pattern,
  14. example,
  15. get_intent_metadata,
  16. is_intent_handler,
  17. discover_intent_handlers,
  18. _extract_slots_from_signature,
  19. INTENT_METADATA_ATTR,
  20. _PATTERN_LIST_ATTR,
  21. _EXAMPLE_LIST_ATTR,
  22. )
  23. from trixy_core.nlp.intent_registry import IntentSlot
  24. # ============================================================================
  25. # @pattern Dekorator
  26. # ============================================================================
  27. class TestPatternDecorator:
  28. """Tests fuer den @pattern Dekorator."""
  29. def test_einzelnes_pattern(self):
  30. """Ein einzelnes Pattern sollte gespeichert werden."""
  31. @pattern("test muster")
  32. async def handler(data):
  33. pass
  34. patterns = getattr(handler, _PATTERN_LIST_ATTR, [])
  35. assert len(patterns) == 1
  36. assert patterns[0] == "test muster"
  37. def test_mehrere_patterns_stackbar(self):
  38. """Mehrere @pattern Dekoratoren sollten gestackt werden."""
  39. @pattern("(wie ist|wie wird) wetter")
  40. @pattern("wetter [in] {city}")
  41. @pattern("temperatur [draussen]")
  42. async def handler(data):
  43. pass
  44. patterns = getattr(handler, _PATTERN_LIST_ATTR, [])
  45. assert len(patterns) == 3
  46. def test_pattern_behaelt_funktion(self):
  47. """@pattern sollte die Funktion nicht veraendern."""
  48. @pattern("test")
  49. async def handler(data):
  50. return "result"
  51. assert asyncio.iscoroutinefunction(handler)
  52. def test_pattern_kein_intent_handler(self):
  53. """@pattern allein macht keine Funktion zum Intent-Handler."""
  54. @pattern("test")
  55. async def handler(data):
  56. pass
  57. assert not is_intent_handler(handler)
  58. # ============================================================================
  59. # @example Dekorator
  60. # ============================================================================
  61. class TestExampleDecorator:
  62. """Tests fuer den @example Dekorator."""
  63. def test_einzelnes_example(self):
  64. """Ein einzelnes Example sollte gespeichert werden."""
  65. @example("Wie ist das Wetter?")
  66. async def handler(data):
  67. pass
  68. examples = getattr(handler, _EXAMPLE_LIST_ATTR, [])
  69. assert len(examples) == 1
  70. assert examples[0] == "Wie ist das Wetter?"
  71. def test_mehrere_examples_in_einem_aufruf(self):
  72. """Mehrere Examples in einem @example sollten gespeichert werden."""
  73. @example("Hallo", "Hi", "Guten Tag")
  74. async def handler(data):
  75. pass
  76. examples = getattr(handler, _EXAMPLE_LIST_ATTR, [])
  77. assert len(examples) == 3
  78. def test_mehrere_example_dekoratoren_stackbar(self):
  79. """Mehrere @example Dekoratoren sollten gestackt werden."""
  80. @example("Wie ist das Wetter?")
  81. @example("Wie warm ist es?")
  82. async def handler(data):
  83. pass
  84. examples = getattr(handler, _EXAMPLE_LIST_ATTR, [])
  85. assert len(examples) == 2
  86. def test_example_behaelt_funktion(self):
  87. """@example sollte die Funktion nicht veraendern."""
  88. @example("Test")
  89. async def handler(data):
  90. return "result"
  91. assert asyncio.iscoroutinefunction(handler)
  92. def test_example_kein_intent_handler(self):
  93. """@example allein macht keine Funktion zum Intent-Handler."""
  94. @example("Test")
  95. async def handler(data):
  96. pass
  97. assert not is_intent_handler(handler)
  98. # ============================================================================
  99. # @intent Dekorator
  100. # ============================================================================
  101. class TestIntentDecorator:
  102. """Tests fuer den @intent Dekorator."""
  103. def test_grundlegend(self):
  104. """@intent sollte Metadaten hinzufuegen."""
  105. @intent("test_intent")
  106. async def handler(data):
  107. pass
  108. assert is_intent_handler(handler)
  109. metadata = get_intent_metadata(handler)
  110. assert metadata is not None
  111. assert metadata["name"] == "test_intent"
  112. def test_mit_description(self):
  113. """@intent sollte Description speichern."""
  114. @intent("test", description="Testbeschreibung")
  115. async def handler(data):
  116. pass
  117. metadata = get_intent_metadata(handler)
  118. assert metadata["description"] == "Testbeschreibung"
  119. def test_mit_priority(self):
  120. """@intent sollte Priority speichern."""
  121. @intent("test", priority=5)
  122. async def handler(data):
  123. pass
  124. metadata = get_intent_metadata(handler)
  125. assert metadata["priority"] == 5
  126. def test_mit_expliziten_examples(self):
  127. """@intent sollte explizite Examples speichern."""
  128. @intent("test", examples=["Hallo", "Hi"])
  129. async def handler(data):
  130. pass
  131. metadata = get_intent_metadata(handler)
  132. assert "Hallo" in metadata["examples"]
  133. assert "Hi" in metadata["examples"]
  134. def test_mit_expliziten_patterns(self):
  135. """@intent sollte explizite Patterns speichern."""
  136. @intent("test", patterns=["muster eins", "muster zwei"])
  137. async def handler(data):
  138. pass
  139. metadata = get_intent_metadata(handler)
  140. assert "muster eins" in metadata["patterns"]
  141. assert "muster zwei" in metadata["patterns"]
  142. def test_mit_expliziten_slots_type(self):
  143. """@intent sollte Typ-basierte Slots normalisieren."""
  144. @intent("test", slots={"device": str, "count": int})
  145. async def handler(data):
  146. pass
  147. metadata = get_intent_metadata(handler)
  148. assert "device" in metadata["slots"]
  149. assert isinstance(metadata["slots"]["device"], IntentSlot)
  150. assert metadata["slots"]["device"].slot_type is str
  151. assert metadata["slots"]["count"].slot_type is int
  152. def test_mit_expliziten_slots_intentslot(self):
  153. """@intent sollte IntentSlot-Objekte direkt akzeptieren."""
  154. @intent("test", slots={
  155. "temp": IntentSlot(name="temp", slot_type=float, required=True),
  156. })
  157. async def handler(data):
  158. pass
  159. metadata = get_intent_metadata(handler)
  160. assert metadata["slots"]["temp"].slot_type is float
  161. assert metadata["slots"]["temp"].required is True
  162. def test_wrapper_ist_async(self):
  163. """Der Wrapper sollte async sein."""
  164. @intent("test")
  165. async def handler(data):
  166. return "result"
  167. assert asyncio.iscoroutinefunction(handler)
  168. # ============================================================================
  169. # Zusammenspiel @intent + @pattern + @example
  170. # ============================================================================
  171. class TestDekoratorZusammenspiel:
  172. """Tests fuer das Zusammenspiel der Dekoratoren."""
  173. def test_intent_sammelt_pattern(self):
  174. """@intent sollte @pattern-Dekoratoren sammeln."""
  175. @intent("weather")
  176. @pattern("(wie ist|wie wird) wetter")
  177. @pattern("wetter [in] {city}")
  178. async def handler(data):
  179. pass
  180. metadata = get_intent_metadata(handler)
  181. assert len(metadata["patterns"]) == 2
  182. assert "(wie ist|wie wird) wetter" in metadata["patterns"]
  183. assert "wetter [in] {city}" in metadata["patterns"]
  184. def test_intent_sammelt_example(self):
  185. """@intent sollte @example-Dekoratoren sammeln."""
  186. @intent("weather")
  187. @example("Wie ist das Wetter?")
  188. @example("Wetter in Berlin", "Wie warm ist es?")
  189. async def handler(data):
  190. pass
  191. metadata = get_intent_metadata(handler)
  192. assert len(metadata["examples"]) == 3
  193. def test_intent_merged_explizite_und_dekorator_patterns(self):
  194. """Explizite Patterns und @pattern-Dekoratoren sollten gemergt werden."""
  195. @intent("test", patterns=["explizit"])
  196. @pattern("dekorator")
  197. async def handler(data):
  198. pass
  199. metadata = get_intent_metadata(handler)
  200. assert "explizit" in metadata["patterns"]
  201. assert "dekorator" in metadata["patterns"]
  202. def test_intent_merged_explizite_und_dekorator_examples(self):
  203. """Explizite Examples und @example-Dekoratoren sollten gemergt werden."""
  204. @intent("test", examples=["Explizit"])
  205. @example("Dekorator")
  206. async def handler(data):
  207. pass
  208. metadata = get_intent_metadata(handler)
  209. assert "Explizit" in metadata["examples"]
  210. assert "Dekorator" in metadata["examples"]
  211. def test_vollstaendiges_setup(self):
  212. """Komplettes Setup mit allen Dekoratoren."""
  213. @intent("current_weather", description="Wetter abfragen")
  214. @pattern("(wie ist|wie wird) [denn] wetter")
  215. @pattern("wetter [in] {city}")
  216. @example("Wie ist das Wetter?", "Wie warm ist es?")
  217. @example("Wetter in Berlin")
  218. async def handle_weather(self, data, city: str = ""):
  219. pass
  220. metadata = get_intent_metadata(handle_weather)
  221. assert metadata["name"] == "current_weather"
  222. assert metadata["description"] == "Wetter abfragen"
  223. assert len(metadata["patterns"]) == 2
  224. assert len(metadata["examples"]) == 3
  225. assert "city" in metadata["slots"]
  226. assert metadata["slots"]["city"].required is False
  227. # ============================================================================
  228. # Slot-Erkennung aus Signatur
  229. # ============================================================================
  230. class TestSlotExtractionFromSignature:
  231. """Tests fuer _extract_slots_from_signature()."""
  232. def test_optionaler_slot(self):
  233. """Parameter mit Default-Wert sollte optionaler Slot sein."""
  234. async def handler(self, data, city: str = ""):
  235. pass
  236. slots = _extract_slots_from_signature(handler)
  237. assert "city" in slots
  238. assert slots["city"].required is False
  239. assert slots["city"].slot_type is str
  240. def test_erforderlicher_slot(self):
  241. """Parameter ohne Default-Wert sollte erforderlicher Slot sein."""
  242. async def handler(self, data, device: str):
  243. pass
  244. slots = _extract_slots_from_signature(handler)
  245. assert "device" in slots
  246. assert slots["device"].required is True
  247. assert slots["device"].slot_type is str
  248. def test_verschiedene_typen(self):
  249. """Verschiedene Typ-Annotationen sollten erkannt werden."""
  250. async def handler(self, data, count: int = 0, temp: float = 0.0, name: str = ""):
  251. pass
  252. slots = _extract_slots_from_signature(handler)
  253. assert slots["count"].slot_type is int
  254. assert slots["temp"].slot_type is float
  255. assert slots["name"].slot_type is str
  256. def test_skip_self_und_data(self):
  257. """'self' und 'data' sollten uebersprungen werden."""
  258. async def handler(self, data, city: str = ""):
  259. pass
  260. slots = _extract_slots_from_signature(handler)
  261. assert "self" not in slots
  262. assert "data" not in slots
  263. assert "city" in slots
  264. def test_skip_args_kwargs(self):
  265. """*args und **kwargs sollten uebersprungen werden."""
  266. async def handler(self, data, *args, city: str = "", **kwargs):
  267. pass
  268. slots = _extract_slots_from_signature(handler)
  269. assert "args" not in slots
  270. assert "kwargs" not in slots
  271. assert "city" in slots
  272. def test_ohne_annotation(self):
  273. """Parameter ohne Annotation sollte als str behandelt werden."""
  274. async def handler(self, data, city=""):
  275. pass
  276. slots = _extract_slots_from_signature(handler)
  277. assert "city" in slots
  278. assert slots["city"].slot_type is str
  279. def test_keine_slots(self):
  280. """Funktion ohne Slot-Parameter sollte leeres Dict ergeben."""
  281. async def handler(self, data):
  282. pass
  283. slots = _extract_slots_from_signature(handler)
  284. assert len(slots) == 0
  285. def test_intent_erkennt_signatur_slots(self):
  286. """@intent sollte Slots automatisch aus der Signatur erkennen."""
  287. @intent("test")
  288. async def handler(self, data, city: str = "", count: int = 0):
  289. pass
  290. metadata = get_intent_metadata(handler)
  291. assert "city" in metadata["slots"]
  292. assert "count" in metadata["slots"]
  293. assert metadata["slots"]["city"].required is False
  294. assert metadata["slots"]["count"].slot_type is int
  295. def test_explizite_slots_ueberschreiben_signatur(self):
  296. """Explizite Slots sollten Signatur-Slots ueberschreiben."""
  297. @intent("test", slots={
  298. "city": IntentSlot(name="city", slot_type=str, required=True),
  299. })
  300. async def handler(self, data, city: str = ""):
  301. pass
  302. metadata = get_intent_metadata(handler)
  303. # Explizit als required gesetzt, obwohl Signatur Default hat
  304. assert metadata["slots"]["city"].required is True
  305. # ============================================================================
  306. # discover_intent_handlers
  307. # ============================================================================
  308. class TestDiscoverIntentHandlers:
  309. """Tests fuer discover_intent_handlers()."""
  310. def test_findet_handler(self):
  311. """Sollte alle @intent-dekorierten Methoden finden."""
  312. class TestPlugin:
  313. @intent("intent1")
  314. async def handler1(self, data):
  315. pass
  316. @intent("intent2")
  317. async def handler2(self, data):
  318. pass
  319. def normal_method(self):
  320. pass
  321. plugin = TestPlugin()
  322. handlers = discover_intent_handlers(plugin)
  323. assert len(handlers) == 2
  324. names = [h[0] for h in handlers]
  325. assert "intent1" in names
  326. assert "intent2" in names
  327. def test_ueberspringe_private_methoden(self):
  328. """Private Methoden (mit _) sollten uebersprungen werden."""
  329. class TestPlugin:
  330. @intent("public")
  331. async def public_handler(self, data):
  332. pass
  333. @intent("private")
  334. async def _private_handler(self, data):
  335. pass
  336. plugin = TestPlugin()
  337. handlers = discover_intent_handlers(plugin)
  338. names = [h[0] for h in handlers]
  339. assert "public" in names
  340. assert "private" not in names
  341. def test_handler_tupel_struktur(self):
  342. """Jeder Handler sollte (name, func, metadata) Tupel sein."""
  343. class TestPlugin:
  344. @intent("test", description="Testbeschreibung")
  345. @pattern("test muster")
  346. @example("Test Beispiel")
  347. async def handler(self, data, city: str = ""):
  348. pass
  349. plugin = TestPlugin()
  350. handlers = discover_intent_handlers(plugin)
  351. assert len(handlers) == 1
  352. name, func, metadata = handlers[0]
  353. assert name == "test"
  354. assert callable(func)
  355. assert metadata["description"] == "Testbeschreibung"
  356. assert len(metadata["patterns"]) == 1
  357. assert len(metadata["examples"]) == 1
  358. assert "city" in metadata["slots"]
  359. def test_leeres_objekt(self):
  360. """Objekt ohne Handler sollte leere Liste ergeben."""
  361. class EmptyPlugin:
  362. def normal_method(self):
  363. pass
  364. plugin = EmptyPlugin()
  365. handlers = discover_intent_handlers(plugin)
  366. assert handlers == []
  367. def test_mit_pattern_und_example_dekoratoren(self):
  368. """Handler mit @pattern und @example sollten korrekt gefunden werden."""
  369. class TestPlugin:
  370. @intent("weather")
  371. @pattern("(wie ist|wie wird) wetter")
  372. @pattern("wetter [in] {city}")
  373. @example("Wie ist das Wetter?")
  374. async def handle_weather(self, data, city: str = ""):
  375. pass
  376. plugin = TestPlugin()
  377. handlers = discover_intent_handlers(plugin)
  378. assert len(handlers) == 1
  379. _, _, metadata = handlers[0]
  380. assert len(metadata["patterns"]) == 2
  381. assert len(metadata["examples"]) == 1
  382. assert "city" in metadata["slots"]
  383. assert metadata["slots"]["city"].required is False
  384. # ============================================================================
  385. # Hilfsfunktionen
  386. # ============================================================================
  387. class TestHilfsfunktionen:
  388. """Tests fuer get_intent_metadata und is_intent_handler."""
  389. def test_get_intent_metadata_handler(self):
  390. """Sollte Metadaten fuer Intent-Handler zurueckgeben."""
  391. @intent("test")
  392. async def handler(data):
  393. pass
  394. metadata = get_intent_metadata(handler)
  395. assert metadata is not None
  396. assert metadata["name"] == "test"
  397. def test_get_intent_metadata_kein_handler(self):
  398. """Sollte None fuer Nicht-Handler zurueckgeben."""
  399. async def handler(data):
  400. pass
  401. metadata = get_intent_metadata(handler)
  402. assert metadata is None
  403. def test_is_intent_handler_true(self):
  404. """Sollte True fuer Intent-Handler zurueckgeben."""
  405. @intent("test")
  406. async def handler(data):
  407. pass
  408. assert is_intent_handler(handler) is True
  409. def test_is_intent_handler_false(self):
  410. """Sollte False fuer Nicht-Handler zurueckgeben."""
  411. async def handler(data):
  412. pass
  413. assert is_intent_handler(handler) is False
  414. def test_is_intent_handler_mit_pattern_only(self):
  415. """@pattern allein sollte kein Intent-Handler sein."""
  416. @pattern("test")
  417. async def handler(data):
  418. pass
  419. assert is_intent_handler(handler) is False
  420. def test_is_intent_handler_mit_example_only(self):
  421. """@example allein sollte kein Intent-Handler sein."""
  422. @example("Test")
  423. async def handler(data):
  424. pass
  425. assert is_intent_handler(handler) is False