test_entities.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749
  1. # -*- coding: utf-8 -*-
  2. """
  3. Tests fuer das Entity-System.
  4. Testet EntityDefinition, EntityRegistry, EntityResolver,
  5. Datum-/Zeit-/Dauer-Aufloesung und Integration mit dem Matcher.
  6. """
  7. import re
  8. from datetime import date, time, timedelta
  9. import pytest
  10. from trixy_core.nlp.entities import (
  11. EntityDefinition,
  12. EntityRegistry,
  13. EntityResolver,
  14. ResolvedEntity,
  15. _next_weekday,
  16. _parse_absolute_date,
  17. _parse_time_string,
  18. _format_date_tts,
  19. _format_time_tts,
  20. _format_duration_tts,
  21. )
  22. # ============================================================================
  23. # Fixtures
  24. # ============================================================================
  25. @pytest.fixture
  26. def registry():
  27. """EntityRegistry mit Testdaten."""
  28. r = EntityRegistry()
  29. r.load_from_dict({
  30. "datum": {
  31. "open": False,
  32. "values": [
  33. "heute", "morgen", "uebermorgen", "übermorgen",
  34. "montag", "dienstag", "mittwoch", "donnerstag",
  35. "freitag", "samstag", "sonntag",
  36. "nächste woche", "am wochenende",
  37. ],
  38. "regex": [
  39. r"\d{1,2}\.\d{1,2}\.",
  40. r"\d{1,2}\.\d{1,2}\.\d{4}",
  41. ],
  42. },
  43. "zeit": {
  44. "open": False,
  45. "values": ["mittag", "mitternacht", "abend", "morgens", "nachmittag"],
  46. "regex": [
  47. r"\d{1,2} uhr",
  48. r"\d{1,2} uhr \d{1,2}",
  49. r"\d{1,2}:\d{2}",
  50. r"halb \w+",
  51. ],
  52. },
  53. "dauer": {
  54. "open": False,
  55. "values": ["eine minute", "eine stunde", "eine halbe stunde"],
  56. "regex": [
  57. r"\d+ (sekunden|sekunde)",
  58. r"\d+ (minuten|minute)",
  59. r"\d+ (stunden|stunde)",
  60. ],
  61. },
  62. "stadt": {
  63. "open": True,
  64. "values": ["berlin", "hamburg", "münchen", "muenchen"],
  65. },
  66. "person": {
  67. "open": True,
  68. "values": [],
  69. },
  70. })
  71. return r
  72. @pytest.fixture
  73. def resolver(registry):
  74. """EntityResolver mit Registry."""
  75. return EntityResolver(registry)
  76. # ============================================================================
  77. # EntityDefinition
  78. # ============================================================================
  79. class TestEntityDefinition:
  80. """Tests fuer EntityDefinition."""
  81. def test_defaults(self):
  82. """Standard-Werte pruefen."""
  83. ed = EntityDefinition(name="test")
  84. assert ed.name == "test"
  85. assert ed.values == set()
  86. assert ed.multi_word_values == set()
  87. assert ed.open is False
  88. assert ed.regex_patterns == []
  89. def test_mit_werten(self):
  90. """Definition mit Werten erstellen."""
  91. ed = EntityDefinition(
  92. name="stadt",
  93. values={"berlin", "hamburg"},
  94. open=True,
  95. )
  96. assert "berlin" in ed.values
  97. assert ed.open is True
  98. # ============================================================================
  99. # EntityRegistry
  100. # ============================================================================
  101. class TestEntityRegistry:
  102. """Tests fuer EntityRegistry."""
  103. def test_get_existierend(self, registry):
  104. """Existierenden Entity-Typ abrufen."""
  105. entity = registry.get("datum")
  106. assert entity is not None
  107. assert entity.name == "datum"
  108. assert "heute" in entity.values
  109. assert "morgen" in entity.values
  110. def test_get_nicht_existierend(self, registry):
  111. """Nicht existierenden Entity-Typ abrufen."""
  112. assert registry.get("unbekannt") is None
  113. def test_matches_bekannter_wert(self, registry):
  114. """Bekannter Wert sollte matchen."""
  115. assert registry.matches("heute", "datum") is True
  116. assert registry.matches("morgen", "datum") is True
  117. assert registry.matches("montag", "datum") is True
  118. def test_matches_unbekannter_wert_strikt(self, registry):
  119. """Unbekannter Wert bei strikter Entity sollte nicht matchen."""
  120. assert registry.matches("xyz", "datum") is False
  121. assert registry.matches("foobar", "zeit") is False
  122. def test_matches_unbekannter_wert_offen(self, registry):
  123. """Unbekannter Wert bei offener Entity sollte matchen."""
  124. assert registry.matches("tokio", "stadt") is True
  125. assert registry.matches("xyz", "stadt") is True
  126. def test_matches_mehrwort(self, registry):
  127. """Mehrwort-Werte sollten matchen."""
  128. assert registry.matches("nächste woche", "datum") is True
  129. assert registry.matches("am wochenende", "datum") is True
  130. def test_matches_regex(self, registry):
  131. """Regex-Werte sollten matchen."""
  132. assert registry.matches("15.03.", "datum") is True
  133. assert registry.matches("1.1.2026", "datum") is True
  134. assert registry.matches("15 uhr", "zeit") is True
  135. assert registry.matches("9:30", "zeit") is True
  136. assert registry.matches("halb drei", "zeit") is True
  137. assert registry.matches("5 minuten", "dauer") is True
  138. def test_matches_regex_kein_match(self, registry):
  139. """Nicht passende Regex sollte nicht matchen."""
  140. assert registry.matches("xyz", "dauer") is False
  141. def test_matches_unbekannter_typ(self, registry):
  142. """Unbekannter Typ sollte alles akzeptieren."""
  143. assert registry.matches("whatever", "unbekannt") is True
  144. def test_mehrwort_match(self, registry):
  145. """Mehrwort-Match ab Position testen."""
  146. tokens = ["nächste", "woche"]
  147. result = registry.match_multi_word(tokens, 0, "datum")
  148. assert result is not None
  149. assert result[0] == "nächste woche"
  150. assert result[1] == 2
  151. def test_mehrwort_match_kein_treffer(self, registry):
  152. """Kein Mehrwort-Match."""
  153. tokens = ["berlin", "test"]
  154. result = registry.match_multi_word(tokens, 0, "datum")
  155. assert result is None
  156. def test_mehrwort_regex_match(self, registry):
  157. """Mehrwort-Regex-Match (z.B. '5 minuten')."""
  158. tokens = ["5", "minuten"]
  159. result = registry.match_multi_word(tokens, 0, "dauer")
  160. assert result is not None
  161. assert result[0] == "5 minuten"
  162. assert result[1] == 2
  163. def test_entity_types(self, registry):
  164. """entity_types sollte alle registrierten Typen listen."""
  165. types = registry.entity_types
  166. assert "datum" in types
  167. assert "zeit" in types
  168. assert "dauer" in types
  169. assert "stadt" in types
  170. assert "person" in types
  171. def test_load_from_dict_leer(self):
  172. """Leeres Dict sollte keine Fehler verursachen."""
  173. r = EntityRegistry()
  174. r.load_from_dict({})
  175. assert r.entity_types == []
  176. # ============================================================================
  177. # EntityResolver — Datum
  178. # ============================================================================
  179. class TestResolveDatum:
  180. """Tests fuer den Datum-Resolver."""
  181. def test_heute(self, resolver):
  182. """'heute' sollte heutiges Datum ergeben."""
  183. result = resolver.resolve("heute", "datum")
  184. assert isinstance(result, ResolvedEntity)
  185. assert result.entity_type == "datum"
  186. assert result.value == date.today()
  187. assert result.formatted["iso"] == date.today().isoformat()
  188. assert result.formatted["tts"] == "heute"
  189. def test_morgen(self, resolver):
  190. """'morgen' sollte morgiges Datum ergeben."""
  191. result = resolver.resolve("morgen", "datum")
  192. assert result.value == date.today() + timedelta(days=1)
  193. assert result.formatted["tts"] == "morgen"
  194. def test_uebermorgen(self, resolver):
  195. """'uebermorgen' sollte uebermorgen ergeben."""
  196. result = resolver.resolve("uebermorgen", "datum")
  197. assert result.value == date.today() + timedelta(days=2)
  198. assert result.formatted["tts"] == "uebermorgen"
  199. def test_uebermorgen_umlaut(self, resolver):
  200. """'übermorgen' mit Umlaut sollte auch funktionieren."""
  201. result = resolver.resolve("übermorgen", "datum")
  202. assert result.value == date.today() + timedelta(days=2)
  203. def test_wochentag(self, resolver):
  204. """Wochentag sollte naechstes Vorkommen ergeben."""
  205. result = resolver.resolve("montag", "datum")
  206. assert result.value.weekday() == 0 # Montag
  207. assert result.value > date.today()
  208. def test_am_wochenende(self, resolver):
  209. """'am wochenende' sollte naechsten Samstag ergeben."""
  210. result = resolver.resolve("am wochenende", "datum")
  211. assert result.value.weekday() == 5 # Samstag
  212. def test_absolutes_datum(self, resolver):
  213. """'15.03.' sollte korrekt geparst werden."""
  214. result = resolver.resolve("15.03.", "datum")
  215. assert result.value.day == 15
  216. assert result.value.month == 3
  217. def test_absolutes_datum_mit_jahr(self, resolver):
  218. """'15.03.2027' sollte korrekt geparst werden."""
  219. result = resolver.resolve("15.03.2027", "datum")
  220. assert result.value == date(2027, 3, 15)
  221. def test_formatted_eu(self, resolver):
  222. """EU-Format sollte DD.MM.YYYY sein."""
  223. result = resolver.resolve("heute", "datum")
  224. assert result.formatted["eu"] == date.today().strftime("%d.%m.%Y")
  225. def test_formatted_api(self, resolver):
  226. """API-Format sollte ISO sein."""
  227. result = resolver.resolve("morgen", "datum")
  228. expected = (date.today() + timedelta(days=1)).isoformat()
  229. assert result.formatted["api"] == expected
  230. def test_morgen_ist_today_plus_1(self, resolver):
  231. """'morgen' muss exakt date.today() + timedelta(days=1) sein."""
  232. today = date.today()
  233. expected = today + timedelta(days=1)
  234. result = resolver.resolve("morgen", "datum")
  235. assert result.value == expected, (
  236. f"'morgen' sollte {expected.isoformat()} sein, "
  237. f"aber ist {result.value}"
  238. )
  239. assert result.formatted["iso"] == expected.isoformat()
  240. assert result.formatted["api"] == expected.isoformat()
  241. def test_heute_ist_today(self, resolver):
  242. """'heute' muss exakt date.today() sein."""
  243. expected = date.today()
  244. result = resolver.resolve("heute", "datum")
  245. assert result.value == expected
  246. def test_uebermorgen_ist_today_plus_2(self, resolver):
  247. """'uebermorgen' muss exakt date.today() + 2 sein."""
  248. expected = date.today() + timedelta(days=2)
  249. result = resolver.resolve("uebermorgen", "datum")
  250. assert result.value == expected
  251. def test_wochentag_ist_in_zukunft_und_korrekt(self, resolver):
  252. """Wochentag muss in der Zukunft liegen und der richtige Wochentag sein."""
  253. today = date.today()
  254. for name, weekday_num in [
  255. ("montag", 0), ("dienstag", 1), ("mittwoch", 2),
  256. ("donnerstag", 3), ("freitag", 4), ("samstag", 5), ("sonntag", 6),
  257. ]:
  258. result = resolver.resolve(name, "datum")
  259. assert result.value.weekday() == weekday_num, (
  260. f"'{name}' sollte Wochentag {weekday_num} sein, "
  261. f"aber ist {result.value.weekday()}"
  262. )
  263. assert result.value > today, (
  264. f"'{name}' sollte nach heute ({today}) liegen"
  265. )
  266. # Maximal 7 Tage in der Zukunft
  267. assert (result.value - today).days <= 7
  268. def test_morgen_formatted_outputs(self, resolver):
  269. """'morgen' muss alle Formatvarianten korrekt liefern."""
  270. result = resolver.resolve("morgen", "datum")
  271. tomorrow = date.today() + timedelta(days=1)
  272. assert result.formatted["iso"] == tomorrow.isoformat()
  273. assert result.formatted["eu"] == tomorrow.strftime("%d.%m.%Y")
  274. assert result.formatted["tts"] == "morgen"
  275. assert result.formatted["api"] == tomorrow.isoformat()
  276. def test_unbekannter_wert(self, resolver):
  277. """Unbekannter Wert sollte Fallback liefern."""
  278. result = resolver.resolve("xyz", "datum")
  279. assert result.entity_type == "datum"
  280. assert result.value == "xyz" # Generischer Resolver
  281. def test_raw_erhalten(self, resolver):
  282. """Rohwert sollte erhalten bleiben."""
  283. result = resolver.resolve("morgen", "datum")
  284. assert result.raw == "morgen"
  285. # ============================================================================
  286. # EntityResolver — Zeit
  287. # ============================================================================
  288. class TestResolveZeit:
  289. """Tests fuer den Zeit-Resolver."""
  290. def test_mittag(self, resolver):
  291. """'mittag' sollte 12:00 ergeben."""
  292. result = resolver.resolve("mittag", "zeit")
  293. assert result.value == time(12, 0)
  294. assert result.formatted["24h"] == "12:00"
  295. assert result.formatted["tts"] == "12 Uhr"
  296. def test_mitternacht(self, resolver):
  297. """'mitternacht' sollte 00:00 ergeben."""
  298. result = resolver.resolve("mitternacht", "zeit")
  299. assert result.value == time(0, 0)
  300. def test_uhrzeit_format(self, resolver):
  301. """'15 uhr' sollte korrekt geparst werden."""
  302. result = resolver.resolve("15 uhr", "zeit")
  303. assert result.value == time(15, 0)
  304. assert result.formatted["tts"] == "15 Uhr"
  305. def test_uhrzeit_mit_minuten(self, resolver):
  306. """'15:30' sollte korrekt geparst werden."""
  307. result = resolver.resolve("15:30", "zeit")
  308. assert result.value == time(15, 30)
  309. assert result.formatted["24h"] == "15:30"
  310. def test_uhrzeit_uhr_minuten(self, resolver):
  311. """'15 uhr 30' sollte korrekt geparst werden."""
  312. result = resolver.resolve("15 uhr 30", "zeit")
  313. assert result.value == time(15, 30)
  314. def test_halb_drei(self, resolver):
  315. """'halb drei' sollte 2:30 ergeben."""
  316. result = resolver.resolve("halb drei", "zeit")
  317. assert result.value == time(2, 30)
  318. def test_abend(self, resolver):
  319. """'abend' sollte 18:00 ergeben."""
  320. result = resolver.resolve("abend", "zeit")
  321. assert result.value == time(18, 0)
  322. def test_unbekannte_zeit(self, resolver):
  323. """Unbekannte Zeitangabe sollte Fallback liefern."""
  324. result = resolver.resolve("xyz", "zeit")
  325. assert result.entity_type == "zeit"
  326. assert result.value == "xyz"
  327. # ============================================================================
  328. # EntityResolver — Dauer
  329. # ============================================================================
  330. class TestResolveDauer:
  331. """Tests fuer den Dauer-Resolver."""
  332. def test_minuten(self, resolver):
  333. """'5 minuten' sollte 300 Sekunden ergeben."""
  334. result = resolver.resolve("5 minuten", "dauer")
  335. assert result.value == 300
  336. assert result.formatted["seconds"] == "300"
  337. assert result.formatted["minutes"] == "5"
  338. assert result.formatted["tts"] == "5 Minuten"
  339. def test_sekunden(self, resolver):
  340. """'30 sekunden' sollte 30 Sekunden ergeben."""
  341. result = resolver.resolve("30 sekunden", "dauer")
  342. assert result.value == 30
  343. assert result.formatted["seconds"] == "30"
  344. assert result.formatted["tts"] == "30 Sekunden"
  345. def test_stunden(self, resolver):
  346. """'2 stunden' sollte 7200 Sekunden ergeben."""
  347. result = resolver.resolve("2 stunden", "dauer")
  348. assert result.value == 7200
  349. assert result.formatted["hours"] == "2"
  350. assert result.formatted["tts"] == "2 Stunden"
  351. def test_eine_minute(self, resolver):
  352. """'eine minute' sollte 60 Sekunden ergeben."""
  353. result = resolver.resolve("eine minute", "dauer")
  354. assert result.value == 60
  355. def test_eine_halbe_stunde(self, resolver):
  356. """'eine halbe stunde' sollte 1800 Sekunden ergeben."""
  357. result = resolver.resolve("eine halbe stunde", "dauer")
  358. assert result.value == 1800
  359. assert result.formatted["minutes"] == "30"
  360. def test_eine_stunde(self, resolver):
  361. """'eine stunde' sollte 3600 Sekunden ergeben."""
  362. result = resolver.resolve("eine stunde", "dauer")
  363. assert result.value == 3600
  364. def test_1_sekunde(self, resolver):
  365. """'1 sekunde' sollte 1 Sekunde ergeben."""
  366. result = resolver.resolve("1 sekunde", "dauer")
  367. assert result.value == 1
  368. assert result.formatted["tts"] == "1 Sekunde"
  369. def test_unbekannte_dauer(self, resolver):
  370. """Unbekannte Dauer sollte Fallback liefern."""
  371. result = resolver.resolve("xyz", "dauer")
  372. assert result.entity_type == "dauer"
  373. assert result.value == "xyz"
  374. # ============================================================================
  375. # EntityResolver — Generisch
  376. # ============================================================================
  377. class TestResolveGeneric:
  378. """Tests fuer den generischen Resolver."""
  379. def test_unbekannter_entity_typ(self, resolver):
  380. """Unbekannter Entity-Typ sollte generisch aufgeloest werden."""
  381. result = resolver.resolve("test", "unbekannt")
  382. assert result.entity_type == "unbekannt"
  383. assert result.value == "test"
  384. assert result.formatted["raw"] == "test"
  385. def test_stadt(self, resolver):
  386. """Stadt (offene Entity, kein spezieller Resolver) → generisch."""
  387. result = resolver.resolve("berlin", "stadt")
  388. assert result.entity_type == "stadt"
  389. assert result.value == "berlin"
  390. def test_raw_wird_getrimmt(self, resolver):
  391. """Rohwert wird getrimmt."""
  392. result = resolver.resolve(" berlin ", "stadt")
  393. assert result.value == "berlin"
  394. # ============================================================================
  395. # Hilfsfunktionen
  396. # ============================================================================
  397. class TestHilfsfunktionen:
  398. """Tests fuer interne Hilfsfunktionen."""
  399. def test_next_weekday(self):
  400. """Naechster Wochentag berechnen."""
  401. today = date.today()
  402. # Naechster Montag
  403. result = _next_weekday(today, 0)
  404. assert result.weekday() == 0
  405. assert result > today
  406. def test_parse_absolute_date_dd_mm(self):
  407. """Absolutes Datum DD.MM. parsen."""
  408. result = _parse_absolute_date("15.03.")
  409. assert result is not None
  410. assert result.day == 15
  411. assert result.month == 3
  412. def test_parse_absolute_date_dd_mm_yyyy(self):
  413. """Absolutes Datum DD.MM.YYYY parsen."""
  414. result = _parse_absolute_date("15.03.2027")
  415. assert result == date(2027, 3, 15)
  416. def test_datum_mit_monatname(self):
  417. """'5 mai' sollte ein Datum mit Tag 5, Monat 5 ergeben."""
  418. result = _parse_absolute_date("5 mai")
  419. assert result is not None
  420. assert result.day == 5
  421. assert result.month == 5
  422. def test_datum_mit_monatname_vergangenheit(self):
  423. """Vergangenes Datum mit Monatsname sollte naechstes Jahr ergeben."""
  424. today = date.today()
  425. # Waehle ein Datum das sicher in der Vergangenheit liegt
  426. past_month = today.month - 1 if today.month > 1 else 12
  427. past_day = 1
  428. result = _parse_absolute_date(f"{past_day} {_get_month_name(past_month)}")
  429. assert result is not None
  430. if past_month < today.month or (past_month == today.month and past_day < today.day):
  431. assert result.year == today.year + 1
  432. else:
  433. assert result.year == today.year
  434. def test_datum_maerz_varianten(self):
  435. """'maerz' und 'märz' sollten beide als Maerz erkannt werden."""
  436. result1 = _parse_absolute_date("1 maerz")
  437. result2 = _parse_absolute_date("1 märz")
  438. assert result1 is not None
  439. assert result2 is not None
  440. assert result1.month == 3
  441. assert result2.month == 3
  442. assert result1 == result2
  443. def test_parse_absolute_date_ungueltig(self):
  444. """Ungueltiges Datum sollte None ergeben."""
  445. assert _parse_absolute_date("abc") is None
  446. assert _parse_absolute_date("32.13.") is None
  447. def test_parse_time_string_hh_mm(self):
  448. """Zeitformat HH:MM parsen."""
  449. assert _parse_time_string("15:30") == (15, 30)
  450. assert _parse_time_string("9:00") == (9, 0)
  451. def test_parse_time_string_hh_uhr(self):
  452. """Zeitformat 'HH Uhr' parsen."""
  453. assert _parse_time_string("15 uhr") == (15, 0)
  454. def test_parse_time_string_hh_uhr_mm(self):
  455. """Zeitformat 'HH Uhr MM' parsen."""
  456. assert _parse_time_string("15 uhr 30") == (15, 30)
  457. def test_parse_time_string_halb(self):
  458. """'halb drei' → (2, 30)."""
  459. assert _parse_time_string("halb drei") == (2, 30)
  460. def test_parse_time_string_ungueltig(self):
  461. """Ungueltiger Zeitstring sollte None ergeben."""
  462. assert _parse_time_string("abc") is None
  463. def test_format_date_tts(self):
  464. """TTS-Formatierung fuer Datum."""
  465. today = date.today()
  466. assert _format_date_tts(today) == "heute"
  467. assert _format_date_tts(today + timedelta(days=1)) == "morgen"
  468. assert _format_date_tts(today + timedelta(days=2)) == "uebermorgen"
  469. def test_format_time_tts(self):
  470. """TTS-Formatierung fuer Zeit."""
  471. assert _format_time_tts(time(15, 0)) == "15 Uhr"
  472. assert _format_time_tts(time(15, 30)) == "15 Uhr 30"
  473. def test_format_duration_tts(self):
  474. """TTS-Formatierung fuer Dauer."""
  475. assert _format_duration_tts(30) == "30 Sekunden"
  476. assert _format_duration_tts(1) == "1 Sekunde"
  477. assert _format_duration_tts(60) == "1 Minute"
  478. assert _format_duration_tts(300) == "5 Minuten"
  479. assert _format_duration_tts(3600) == "1 Stunde"
  480. assert _format_duration_tts(7200) == "2 Stunden"
  481. assert _format_duration_tts(5400) == "1 Stunde 30 Minuten"
  482. # ============================================================================
  483. # Integration: Matcher + Entities
  484. # ============================================================================
  485. class TestMatcherIntegration:
  486. """Integrationstests: KeywordMatcher mit Entity-System."""
  487. @pytest.fixture
  488. def entity_matcher(self, registry):
  489. """Matcher mit Entity-Registry."""
  490. from trixy_core.nlp.keyword_matcher import KeywordIntentMatcher
  491. m = KeywordIntentMatcher()
  492. m.set_entity_registry(registry)
  493. return m
  494. def test_datum_slot_match(self, entity_matcher):
  495. """Pattern mit {date:datum} sollte 'morgen' matchen."""
  496. entity_matcher.compile_intents([{
  497. "name": "weather_forecast",
  498. "patterns": ["wetter {date:datum}"],
  499. }])
  500. result = entity_matcher.match("Wetter morgen")
  501. assert result is not None
  502. assert result.intent == "weather_forecast"
  503. assert "date" in result.slots
  504. slot = result.slots["date"]
  505. assert isinstance(slot, ResolvedEntity)
  506. assert slot.entity_type == "datum"
  507. assert slot.value == date.today() + timedelta(days=1)
  508. def test_datum_slot_strikt_ablehnung(self, entity_matcher):
  509. """Pattern mit {date:datum} sollte unbekannte Werte ablehnen."""
  510. entity_matcher.compile_intents([{
  511. "name": "weather_forecast",
  512. "patterns": ["wetter {date:datum}"],
  513. }])
  514. result = entity_matcher.match("Wetter xyz")
  515. assert result is None
  516. def test_stadt_slot_offen(self, entity_matcher):
  517. """Pattern mit {city:stadt} sollte beliebige Werte akzeptieren."""
  518. entity_matcher.compile_intents([{
  519. "name": "current_weather",
  520. "patterns": ["wetter [in] {city:stadt}"],
  521. }])
  522. result = entity_matcher.match("Wetter in Tokyo")
  523. assert result is not None
  524. assert "city" in result.slots
  525. slot = result.slots["city"]
  526. assert isinstance(slot, ResolvedEntity)
  527. assert slot.value == "tokyo"
  528. def test_stadt_slot_bekannter_wert(self, entity_matcher):
  529. """Bekannter Stadtwert sollte gematcht werden."""
  530. entity_matcher.compile_intents([{
  531. "name": "current_weather",
  532. "patterns": ["wetter [in] {city:stadt}"],
  533. }])
  534. result = entity_matcher.match("Wetter in Berlin")
  535. assert result is not None
  536. assert "city" in result.slots
  537. def test_dauer_slot_match(self, entity_matcher):
  538. """Pattern mit {duration:dauer} sollte '5 minuten' matchen."""
  539. entity_matcher.compile_intents([{
  540. "name": "set_timer",
  541. "patterns": ["timer {duration:dauer}"],
  542. }])
  543. result = entity_matcher.match("Timer 5 Minuten")
  544. assert result is not None
  545. assert "duration" in result.slots
  546. slot = result.slots["duration"]
  547. assert isinstance(slot, ResolvedEntity)
  548. assert slot.value == 300
  549. def test_slot_ohne_entity_typ(self, entity_matcher):
  550. """Slot ohne Entity-Typ sollte wie bisher als String funktionieren."""
  551. entity_matcher.compile_intents([{
  552. "name": "volume_set",
  553. "patterns": ["lautstaerke [auf] {level}"],
  554. }])
  555. result = entity_matcher.match("Lautstärke auf 50")
  556. assert result is not None
  557. assert "level" in result.slots
  558. assert isinstance(result.slots["level"], str)
  559. def test_pattern_mit_entity_typ_parse(self, entity_matcher):
  560. """Slot mit Entity-Typ sollte korrekt geparst werden."""
  561. from trixy_core.nlp.keyword_matcher import PatternToken
  562. tokens = entity_matcher._parse_pattern("{date:datum}")
  563. assert len(tokens) == 1
  564. assert tokens[0].kind == "slot"
  565. assert tokens[0].slot_name == "date"
  566. assert tokens[0].entity_type == "datum"
  567. def test_dauer_slot_strikt_ablehnung(self, entity_matcher):
  568. """Pattern mit {duration:dauer} sollte 'xyz' ablehnen."""
  569. entity_matcher.compile_intents([{
  570. "name": "set_timer",
  571. "patterns": ["timer {duration:dauer}"],
  572. }])
  573. result = entity_matcher.match("Timer xyz")
  574. assert result is None
  575. def test_wetter_morgen_resolved_date(self, entity_matcher):
  576. """'Wetter morgen' → ResolvedEntity mit date.today() + 1."""
  577. entity_matcher.compile_intents([{
  578. "name": "weather_forecast",
  579. "patterns": ["wetter {date:datum}"],
  580. }])
  581. result = entity_matcher.match("Wetter morgen")
  582. assert result is not None
  583. assert result.intent == "weather_forecast"
  584. slot = result.slots["date"]
  585. assert isinstance(slot, ResolvedEntity)
  586. tomorrow = date.today() + timedelta(days=1)
  587. assert slot.value == tomorrow, (
  588. f"'morgen' sollte {tomorrow.isoformat()} sein, "
  589. f"aber ist {slot.value}"
  590. )
  591. def test_wetter_montag_resolved_date(self, entity_matcher):
  592. """'Wetter Montag' → ResolvedEntity mit naechstem Montag."""
  593. entity_matcher.compile_intents([{
  594. "name": "weather_forecast",
  595. "patterns": ["wetter {date:datum}"],
  596. }])
  597. result = entity_matcher.match("Wetter Montag")
  598. assert result is not None
  599. slot = result.slots["date"]
  600. assert isinstance(slot, ResolvedEntity)
  601. assert slot.value.weekday() == 0 # Montag
  602. assert slot.value > date.today()
  603. def test_wie_wird_am_montag_das_wetter(self, entity_matcher):
  604. """'Wie wird am Montag das Wetter' → weather_forecast mit Montag."""
  605. entity_matcher.compile_intents([{
  606. "name": "weather_forecast",
  607. "patterns": [
  608. "wie wird {das|} [am] {date:datum} [das] wetter",
  609. "wie wird [das] wetter [am] {date:datum}",
  610. ],
  611. }, {
  612. "name": "current_weather",
  613. "patterns": [
  614. "(wie ist|wie wird) [denn] {das|} wetter [draussen|gerade|heute]",
  615. ],
  616. }])
  617. result = entity_matcher.match("Wie wird am Montag das Wetter")
  618. assert result is not None
  619. assert result.intent == "weather_forecast"
  620. slot = result.slots["date"]
  621. assert isinstance(slot, ResolvedEntity)
  622. assert slot.value.weekday() == 0 # Montag
  623. assert slot.value > date.today()
  624. # ============================================================================
  625. # Hilfsfunktionen fuer Tests
  626. # ============================================================================
  627. def _get_month_name(month: int) -> str:
  628. """Gibt den deutschen Monatsnamen (lowercase) fuer Tests zurueck."""
  629. names = [
  630. "", "januar", "februar", "maerz", "april", "mai", "juni",
  631. "juli", "august", "september", "oktober", "november", "dezember",
  632. ]
  633. return names[month]