main.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641
  1. # -*- coding: utf-8 -*-
  2. """
  3. Notes-Plugin fuer Trixy.
  4. Ermoeglicht das Erstellen, Auflisten, Suchen und Loeschen von Notizen
  5. per Sprache. Unterstuetzt Audio-Mitschnitt, laengere Aufnahmen mit
  6. 5s Silence-Detection, Datumsabfragen und Audio-Wiedergabe.
  7. Nutzt den SyncStore fuer persistente Speicherung und
  8. automatische Synchronisation mit dem Server.
  9. """
  10. from datetime import datetime, timedelta
  11. from pathlib import Path
  12. from typing import Any
  13. from trixy_core.plugins import TrixyPlugin
  14. from trixy_core.sync.models import SyncItem, _now_iso
  15. from trixy_core.sync.store import SyncStore
  16. from trixy_core.nlp.intent_registry import IntentRegistry
  17. from trixy_core.nlp.decorators import intent, pattern, example
  18. from trixy_core.nlp.handler import IntentReceivedData, IntentResult
  19. from trixy_core.utils.debug import pinfo, pdebug, perror
  20. DATA_TYPE = "notes.note"
  21. class NotesPlugin(TrixyPlugin):
  22. """
  23. Notizen-Plugin mit Audio-Unterstuetzung.
  24. Bietet:
  25. - Schnell-Notizen per Sprache (Einzeiler)
  26. - Aufnahme-Modus mit laengerer Aufnahme (5s Silence, 5min Max)
  27. - Audio-Mitschnitt speichern und wiedergeben
  28. - Datumsabfragen (heute, gestern, nach Datum)
  29. - Textsuche in Notizen
  30. - Einzelnes und Bulk-Loeschen (mit Rueckfrage)
  31. Nutzt SyncStore fuer Persistenz und Sync.
  32. """
  33. NAME = "notes"
  34. VERSION = "2.0.0"
  35. DESCRIPTION = "Notizen per Sprachbefehl erstellen und verwalten"
  36. AUTHOR = "Trixy"
  37. def __init__(self, application: Any, plugin_path: Any, config: dict[str, Any] | None = None) -> None:
  38. super().__init__(application, plugin_path, config)
  39. self._registry = IntentRegistry.get_instance()
  40. self._store: SyncStore | None = None
  41. # Dialog-State fuer Multi-Turn (record_note, delete_all)
  42. self._active_recordings: dict[str, dict[str, Any]] = {}
  43. self._pending_delete_all: dict[str, bool] = {}
  44. # Audio-Puffer fuer aktive Aufnahme-Sessions
  45. self._recording_audio: dict[str, bytes] = {}
  46. # =========================================================================
  47. # Lifecycle
  48. # =========================================================================
  49. async def on_load(self) -> None:
  50. """Initialisiert das Plugin."""
  51. pinfo(f"NotesPlugin: Lade Plugin v{self.VERSION}")
  52. # SyncStore holen
  53. self._store = self.application.sync_store
  54. if self._store is None:
  55. self._store = SyncStore(base_directory="./sync_data")
  56. pdebug("NotesPlugin: Eigenen SyncStore erstellt (kein globaler verfuegbar)")
  57. # Audio-Verzeichnis sicherstellen
  58. audio_dir = Path(self._store._base_dir) / DATA_TYPE
  59. audio_dir.mkdir(parents=True, exist_ok=True)
  60. # Event-Handler fuer Audio-Mitschnitt bei aktiven Aufnahme-Sessions
  61. self.application.events.register("raw_audio_received", self._on_raw_audio_received)
  62. pinfo("NotesPlugin: Geladen")
  63. async def on_unload(self) -> None:
  64. """Entlaedt das Plugin."""
  65. self._registry.unregister_plugin(self.NAME)
  66. self.application.events.unregister("raw_audio_received", self._on_raw_audio_received)
  67. self._active_recordings.clear()
  68. self._pending_delete_all.clear()
  69. self._recording_audio.clear()
  70. pdebug("NotesPlugin: Entladen")
  71. # =========================================================================
  72. # Audio-Mitschnitt Handler
  73. # =========================================================================
  74. async def _on_raw_audio_received(self, event_name: str, data) -> None:
  75. """Speichert Audio-Daten wenn eine Notiz-Aufnahme aktiv ist."""
  76. session_id = data.get("session_id", "") if isinstance(data, dict) else getattr(data, "session_id", "")
  77. audio_data = data.get("audio_data", b"") if isinstance(data, dict) else getattr(data, "audio_data", b"")
  78. if session_id and session_id in self._active_recordings and audio_data:
  79. if isinstance(audio_data, str):
  80. audio_data = bytes.fromhex(audio_data)
  81. self._recording_audio[session_id] = self._recording_audio.get(session_id, b"") + audio_data
  82. # =========================================================================
  83. # Intent-Handler: Schnell-Notiz
  84. # =========================================================================
  85. @intent("create_note", description="Notiz erstellen")
  86. @pattern("(merke|merk) [dir] {content}")
  87. @pattern("notiz {content}")
  88. @pattern("(schreibe|schreib) [dir] auf {content}")
  89. @pattern("(notiere|notier) [dir] {content}")
  90. @example("Merke dir Milch kaufen", "Notiz Arzttermin am Freitag")
  91. @example("Schreibe auf Schluessel mitnehmen", "Notiere Geburtstag planen")
  92. async def handle_create_note(self, data: IntentReceivedData) -> IntentResult:
  93. """Erstellt eine neue Schnell-Notiz."""
  94. content = data.get_slot("content", "")
  95. if not content:
  96. content = data.original_text
  97. if not content:
  98. return IntentResult.failure(
  99. "Kein Inhalt angegeben",
  100. response_text="Was soll ich mir merken?",
  101. )
  102. # Titel aus erstem Satz oder ersten 50 Zeichen ableiten
  103. title = content[:50].strip()
  104. if len(content) > 50:
  105. title += "..."
  106. # SyncItem erstellen
  107. now = _now_iso()
  108. today = datetime.now().strftime("%Y-%m-%d")
  109. item = SyncItem(
  110. data_type=DATA_TYPE,
  111. data={
  112. "title": title,
  113. "content": content,
  114. "tags": [],
  115. "has_audio": False,
  116. "audio_file": "",
  117. "audio_duration_seconds": 0.0,
  118. "created_date": today,
  119. },
  120. created_at=now,
  121. updated_at=now,
  122. source_id=data.satellite_id or "standalone",
  123. )
  124. self._store.save(item)
  125. pinfo(f"NotesPlugin: Notiz erstellt: {title}")
  126. return IntentResult.success_with_response(f"Notiert: {title}")
  127. # =========================================================================
  128. # Intent-Handler: Aufnahme-Modus
  129. # =========================================================================
  130. @intent("record_note", description="Notizaufnahme starten")
  131. @pattern("(nimm|nehme) [eine] notiz auf")
  132. @pattern("(starte|start) [eine] notizaufnahme")
  133. @pattern("notiz aufnehmen")
  134. @example("Nimm eine Notiz auf", "Starte Notizaufnahme")
  135. async def handle_record_note(self, data: IntentReceivedData) -> IntentResult:
  136. """Startet den Aufnahme-Modus fuer laengere Notizen."""
  137. # Recording-Config temporaer aendern (5s Silence, 5min Max)
  138. silence = self.get_config_value("silence_timeout_seconds", 5.0)
  139. max_rec = self.get_config_value("max_recording_seconds", 300.0)
  140. await self.application.events.emit("recording_config_override", {
  141. "silence_timeout_seconds": silence,
  142. "max_recording_seconds": max_rec,
  143. })
  144. # Dialog-State merken
  145. session_id = data.session_id or "unknown"
  146. self._active_recordings[session_id] = {
  147. "satellite_id": data.satellite_id,
  148. "room_id": data.room_id,
  149. "started_at": datetime.now().isoformat(),
  150. }
  151. return IntentResult(
  152. success=True,
  153. response_text="Was moechtest du notieren?",
  154. follow_up_intent="record_note_content",
  155. )
  156. @intent("record_note_content", description="Notiz-Inhalt nach Aufnahme")
  157. async def handle_record_note_content(self, data: IntentReceivedData) -> IntentResult:
  158. """Empfaengt den gesprochenen Notiz-Inhalt nach der Aufnahme."""
  159. content = data.original_text
  160. if not content:
  161. return IntentResult.failure(
  162. "Kein Inhalt erkannt",
  163. response_text="Ich habe nichts verstanden. Versuche es nochmal.",
  164. )
  165. title = content[:50].strip()
  166. if len(content) > 50:
  167. title += "..."
  168. # Audio speichern (falls vorhanden)
  169. session_id = data.session_id or "unknown"
  170. has_audio = False
  171. audio_file = ""
  172. audio_duration = 0.0
  173. now = _now_iso()
  174. today = datetime.now().strftime("%Y-%m-%d")
  175. item = SyncItem(
  176. data_type=DATA_TYPE,
  177. data={
  178. "title": title,
  179. "content": content,
  180. "tags": [],
  181. "has_audio": False,
  182. "audio_file": "",
  183. "audio_duration_seconds": 0.0,
  184. "created_date": today,
  185. },
  186. created_at=now,
  187. updated_at=now,
  188. source_id=data.satellite_id or "standalone",
  189. )
  190. # Audio-Daten aus Puffer holen und als PCM-Datei speichern
  191. audio_data = self._recording_audio.pop(session_id, b"")
  192. if audio_data and self.get_config_value("audio_storage_enabled", True):
  193. audio_file = f"{DATA_TYPE}/{item.item_id}.pcm"
  194. audio_path = Path(self._store._base_dir) / audio_file
  195. audio_path.parent.mkdir(parents=True, exist_ok=True)
  196. try:
  197. audio_path.write_bytes(audio_data)
  198. has_audio = True
  199. # 16KHz, 16-bit Mono → 32000 Bytes/Sekunde
  200. audio_duration = len(audio_data) / 32000.0
  201. pdebug(f"NotesPlugin: Audio gespeichert: {audio_path} ({audio_duration:.1f}s)")
  202. except Exception as e:
  203. perror(f"NotesPlugin: Audio-Speicherung fehlgeschlagen: {e}")
  204. item.data["has_audio"] = has_audio
  205. item.data["audio_file"] = audio_file
  206. item.data["audio_duration_seconds"] = round(audio_duration, 1)
  207. self._store.save(item)
  208. # Aufnahme-State aufraeumen
  209. self._active_recordings.pop(session_id, None)
  210. duration_text = ""
  211. if audio_duration > 0:
  212. duration_text = f" Audio: {audio_duration:.0f} Sekunden."
  213. pinfo(f"NotesPlugin: Notiz aufgenommen: {title}")
  214. return IntentResult.success_with_response(
  215. f"Notiz gespeichert: {title}.{duration_text}"
  216. )
  217. # =========================================================================
  218. # Intent-Handler: Auflisten
  219. # =========================================================================
  220. @intent("list_notes", description="Alle Notizen auflisten")
  221. @pattern("zeig [mir] [meine|alle] notizen")
  222. @pattern("welche notizen [habe ich]")
  223. @pattern("was sind meine notizen")
  224. @pattern("liste [meine|alle] notizen [auf]")
  225. @pattern("meine notizen")
  226. @example("Zeige meine Notizen", "Welche Notizen habe ich?")
  227. @example("Was sind meine Notizen", "Liste alle Notizen auf")
  228. async def handle_list_notes(self, data: IntentReceivedData) -> IntentResult:
  229. """Listet alle aktiven Notizen auf."""
  230. notes = self._store.get_all(DATA_TYPE)
  231. if not notes:
  232. return IntentResult.success_with_response(
  233. "Du hast keine Notizen."
  234. )
  235. if len(notes) == 1:
  236. title = notes[0].data.get("title", "Unbenannt")
  237. return IntentResult.success_with_response(
  238. f"Du hast eine Notiz: {title}."
  239. )
  240. titles = [n.data.get("title", "Unbenannt") for n in notes]
  241. note_list = ", ".join(titles[:5])
  242. if len(titles) > 5:
  243. response = f"Du hast {len(notes)} Notizen. Die letzten fuenf: {note_list}."
  244. else:
  245. response = f"Du hast {len(notes)} Notizen: {note_list}."
  246. return IntentResult.success_with_response(response)
  247. # =========================================================================
  248. # Intent-Handler: Datumsabfragen
  249. # =========================================================================
  250. @intent("get_note_today", description="Notizen von heute anzeigen")
  251. @pattern("[was] [war|waren] [die] notiz[en] von heute")
  252. @pattern("notizen von heute")
  253. @pattern("heutige notizen")
  254. @pattern("was habe ich heute notiert")
  255. @example("Was war die Notiz von heute?", "Notizen von heute")
  256. @example("Was habe ich heute notiert?", "Heutige Notizen")
  257. async def handle_get_note_today(self, data: IntentReceivedData) -> IntentResult:
  258. """Zeigt Notizen von heute."""
  259. today = datetime.now().strftime("%Y-%m-%d")
  260. return self._get_notes_by_date(today, "heute")
  261. @intent("get_note_yesterday", description="Notizen von gestern anzeigen")
  262. @pattern("[was] [war|waren] [die] notiz[en] von gestern")
  263. @pattern("notizen von gestern")
  264. @pattern("was habe ich gestern notiert")
  265. @example("Notiz von gestern", "Was habe ich gestern notiert?")
  266. async def handle_get_note_yesterday(self, data: IntentReceivedData) -> IntentResult:
  267. """Zeigt Notizen von gestern."""
  268. yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
  269. return self._get_notes_by_date(yesterday, "gestern")
  270. @intent("get_note_by_date", description="Notizen nach Datum filtern")
  271. @pattern("notizen vom {date}")
  272. @pattern("[was] [war|waren] [die] notiz[en] vom {date}")
  273. @pattern("notizen von {date}")
  274. @example("Notizen vom Montag", "Notiz von letzter Woche")
  275. async def handle_get_note_by_date(self, data: IntentReceivedData) -> IntentResult:
  276. """Zeigt Notizen fuer ein bestimmtes Datum."""
  277. date_text = data.get_slot("date", "")
  278. if not date_text:
  279. return IntentResult.failure(
  280. "Kein Datum angegeben",
  281. response_text="Von welchem Tag moechtest du die Notizen sehen?",
  282. )
  283. # Versuche Datum zu parsen
  284. target_date = self._parse_date_text(date_text)
  285. if not target_date:
  286. return IntentResult.failure(
  287. f"Datum '{date_text}' nicht erkannt",
  288. response_text=f"Ich konnte das Datum '{date_text}' nicht verstehen.",
  289. )
  290. label = date_text
  291. return self._get_notes_by_date(target_date, label)
  292. def _get_notes_by_date(self, date_str: str, label: str) -> IntentResult:
  293. """Filtert Notizen nach Datum und erstellt Antwort."""
  294. notes = self._store.get_all(DATA_TYPE)
  295. matches = [n for n in notes if n.data.get("created_date", "") == date_str]
  296. if not matches:
  297. return IntentResult.success_with_response(
  298. f"Keine Notizen von {label}."
  299. )
  300. if len(matches) == 1:
  301. content = matches[0].data.get("content", "")
  302. return IntentResult.success_with_response(
  303. f"Eine Notiz von {label}: {content}."
  304. )
  305. titles = [m.data.get("title", "Unbenannt") for m in matches]
  306. return IntentResult.success_with_response(
  307. f"{len(matches)} Notizen von {label}: {', '.join(titles[:5])}."
  308. )
  309. def _parse_date_text(self, text: str) -> str | None:
  310. """Versucht deutschen Datumstext in YYYY-MM-DD zu parsen."""
  311. text_lower = text.lower().strip()
  312. today = datetime.now()
  313. if text_lower in ("heute",):
  314. return today.strftime("%Y-%m-%d")
  315. if text_lower in ("gestern",):
  316. return (today - timedelta(days=1)).strftime("%Y-%m-%d")
  317. if text_lower in ("vorgestern",):
  318. return (today - timedelta(days=2)).strftime("%Y-%m-%d")
  319. # Wochentage
  320. wochentage = {
  321. "montag": 0, "dienstag": 1, "mittwoch": 2, "donnerstag": 3,
  322. "freitag": 4, "samstag": 5, "sonntag": 6,
  323. }
  324. for tag, weekday in wochentage.items():
  325. if tag in text_lower:
  326. days_ago = (today.weekday() - weekday) % 7
  327. if days_ago == 0:
  328. days_ago = 7 # Letzten gleichen Wochentag
  329. return (today - timedelta(days=days_ago)).strftime("%Y-%m-%d")
  330. # DD.MM.YYYY oder DD.MM.
  331. import re
  332. match = re.match(r"(\d{1,2})\.(\d{1,2})\.?(\d{2,4})?", text_lower)
  333. if match:
  334. day = int(match.group(1))
  335. month = int(match.group(2))
  336. year = int(match.group(3)) if match.group(3) else today.year
  337. if year < 100:
  338. year += 2000
  339. try:
  340. return datetime(year, month, day).strftime("%Y-%m-%d")
  341. except ValueError:
  342. pass
  343. return None
  344. # =========================================================================
  345. # Intent-Handler: Audio-Wiedergabe
  346. # =========================================================================
  347. @intent("play_note_audio", description="Audio einer Notiz abspielen")
  348. @pattern("(spiele|spiel) [die] [letzte] notiz [audio|aufnahme] [ab|wieder]")
  349. @pattern("(gib|geb) [die] audio [der] [letzten] notiz [wieder|ab]")
  350. @pattern("notiz abspielen")
  351. @example("Spiele die letzte Notiz ab", "Gib die Audio der letzten Notiz wieder")
  352. async def handle_play_note_audio(self, data: IntentReceivedData) -> IntentResult:
  353. """Spielt die Audio-Aufnahme der letzten Notiz ab."""
  354. notes = self._store.get_all(DATA_TYPE)
  355. audio_notes = [n for n in notes if n.data.get("has_audio")]
  356. if not audio_notes:
  357. return IntentResult.success_with_response(
  358. "Keine Notizen mit Audio-Aufnahme vorhanden."
  359. )
  360. # Letzte Notiz mit Audio nehmen
  361. note = audio_notes[-1]
  362. audio_file = note.data.get("audio_file", "")
  363. if not audio_file:
  364. return IntentResult.success_with_response(
  365. "Die Audio-Datei wurde nicht gefunden."
  366. )
  367. audio_path = Path(self._store._base_dir) / audio_file
  368. if not audio_path.exists():
  369. return IntentResult.success_with_response(
  370. "Die Audio-Datei existiert nicht mehr."
  371. )
  372. try:
  373. audio_bytes = audio_path.read_bytes()
  374. # Audio als tts_completed Event abspielen (PCM-Daten hex-kodiert)
  375. await self.application.events.emit("tts_completed", {
  376. "satellite_id": data.satellite_id,
  377. "audio_data": audio_bytes.hex(),
  378. "sample_rate": 16000,
  379. "channels": 1,
  380. "sample_width": 2,
  381. "request_id": "",
  382. "success": True,
  383. })
  384. title = note.data.get("title", "Notiz")
  385. duration = note.data.get("audio_duration_seconds", 0)
  386. return IntentResult(
  387. success=True,
  388. response_text="",
  389. suppress_tts=True,
  390. )
  391. except Exception as e:
  392. perror(f"NotesPlugin: Audio-Wiedergabe fehlgeschlagen: {e}")
  393. return IntentResult.failure(
  394. str(e),
  395. response_text="Die Audio-Wiedergabe ist fehlgeschlagen.",
  396. )
  397. # =========================================================================
  398. # Intent-Handler: Suche
  399. # =========================================================================
  400. @intent("search_notes", description="In Notizen suchen")
  401. @pattern("(suche|such) [in] [meinen] notizen [nach] {query}")
  402. @pattern("(finde|find) [in] [meinen] notizen {query}")
  403. @pattern("(finde|find) notiz {query}")
  404. @example("Suche in Notizen nach Milch", "Finde in Notizen Arzttermin")
  405. async def handle_search_notes(self, data: IntentReceivedData) -> IntentResult:
  406. """Sucht in Notizen nach einem Suchbegriff."""
  407. query = data.get_slot("query", "")
  408. if not query:
  409. return IntentResult.failure(
  410. "Kein Suchbegriff",
  411. response_text="Wonach soll ich suchen?",
  412. )
  413. query_lower = query.lower()
  414. notes = self._store.get_all(DATA_TYPE)
  415. matches = [
  416. n for n in notes
  417. if query_lower in n.data.get("content", "").lower()
  418. or query_lower in n.data.get("title", "").lower()
  419. ]
  420. if not matches:
  421. return IntentResult.success_with_response(
  422. f"Keine Notizen mit '{query}' gefunden."
  423. )
  424. if len(matches) == 1:
  425. content = matches[0].data.get("content", "")
  426. return IntentResult.success_with_response(
  427. f"Eine Notiz gefunden: {content}."
  428. )
  429. titles = [m.data.get("title", "Unbenannt") for m in matches]
  430. return IntentResult.success_with_response(
  431. f"{len(matches)} Notizen gefunden: {', '.join(titles[:5])}."
  432. )
  433. # =========================================================================
  434. # Intent-Handler: Loeschen
  435. # =========================================================================
  436. @intent("delete_note", description="Notiz loeschen")
  437. @pattern("(loesche|entferne|loesch|entfern) [die] notiz {query}")
  438. @pattern("notiz {query} (loeschen|entfernen)")
  439. @example("Loesche die Notiz Milch kaufen", "Entferne Notiz Arzttermin")
  440. async def handle_delete_note(self, data: IntentReceivedData) -> IntentResult:
  441. """Loescht eine Notiz (Soft-Delete)."""
  442. query = data.get_slot("query", "")
  443. if not query:
  444. return IntentResult.failure(
  445. "Kein Suchbegriff",
  446. response_text="Welche Notiz soll ich loeschen?",
  447. )
  448. query_lower = query.lower()
  449. notes = self._store.get_all(DATA_TYPE)
  450. matches = [
  451. n for n in notes
  452. if query_lower in n.data.get("content", "").lower()
  453. or query_lower in n.data.get("title", "").lower()
  454. ]
  455. if not matches:
  456. return IntentResult.failure(
  457. f"Notiz '{query}' nicht gefunden",
  458. response_text=f"Ich habe keine Notiz mit '{query}' gefunden.",
  459. )
  460. if len(matches) > 1:
  461. titles = [m.data.get("title", "Unbenannt") for m in matches]
  462. return IntentResult.failure(
  463. "Mehrere Treffer",
  464. response_text=f"Mehrere Notizen gefunden: {', '.join(titles[:3])}. Welche soll ich loeschen?",
  465. )
  466. # Genau ein Treffer: Soft-Delete
  467. note = matches[0]
  468. self._store.delete(DATA_TYPE, note.item_id)
  469. # Audio-Datei loeschen falls vorhanden
  470. audio_file = note.data.get("audio_file", "")
  471. if audio_file:
  472. audio_path = Path(self._store._base_dir) / audio_file
  473. if audio_path.exists():
  474. try:
  475. audio_path.unlink()
  476. except Exception as e:
  477. pdebug(f"NotesPlugin: Audio-Datei Loeschung fehlgeschlagen: {e}")
  478. title = note.data.get("title", "Unbenannt")
  479. pinfo(f"NotesPlugin: Notiz geloescht: {title}")
  480. return IntentResult.success_with_response(f"Notiz '{title}' geloescht.")
  481. @intent("delete_all_notes", description="Alle Notizen loeschen")
  482. @pattern("(loesche|entferne|loesch|entfern) alle notizen")
  483. @example("Loesche alle Notizen")
  484. async def handle_delete_all_notes(self, data: IntentReceivedData) -> IntentResult:
  485. """Startet Bulk-Delete mit Rueckfrage."""
  486. notes = self._store.get_all(DATA_TYPE)
  487. if not notes:
  488. return IntentResult.success_with_response("Du hast keine Notizen.")
  489. # Rueckfrage-State merken
  490. session_id = data.session_id or "unknown"
  491. self._pending_delete_all[session_id] = True
  492. return IntentResult(
  493. success=True,
  494. response_text=f"Du hast {len(notes)} Notizen. Bist du sicher? Sage Ja oder Nein.",
  495. follow_up_intent="confirm_delete_all_notes",
  496. )
  497. @intent("confirm_delete_all_notes", description="Bestaetigung fuer Alle-Loeschen")
  498. async def handle_confirm_delete_all(self, data: IntentReceivedData) -> IntentResult:
  499. """Verarbeitet die Bestaetigung fuer Bulk-Delete."""
  500. session_id = data.session_id or "unknown"
  501. if session_id not in self._pending_delete_all:
  502. return IntentResult.failure(
  503. "Keine ausstehende Loeschung",
  504. response_text="Es gibt keine ausstehende Loeschanfrage.",
  505. )
  506. text = data.original_text.lower().strip()
  507. self._pending_delete_all.pop(session_id, None)
  508. if text in ("ja", "yes", "klar", "sicher", "mach", "ok", "genau"):
  509. notes = self._store.get_all(DATA_TYPE)
  510. count = 0
  511. for note in notes:
  512. self._store.delete(DATA_TYPE, note.item_id)
  513. # Audio-Datei loeschen
  514. audio_file = note.data.get("audio_file", "")
  515. if audio_file:
  516. audio_path = Path(self._store._base_dir) / audio_file
  517. if audio_path.exists():
  518. try:
  519. audio_path.unlink()
  520. except Exception:
  521. pass
  522. count += 1
  523. pinfo(f"NotesPlugin: {count} Notizen geloescht (Bulk)")
  524. return IntentResult.success_with_response(
  525. f"Alle {count} Notizen wurden geloescht."
  526. )
  527. return IntentResult.success_with_response("Abgebrochen. Notizen bleiben erhalten.")
  528. # =========================================================================
  529. # Intent-Handler: Anzahl
  530. # =========================================================================
  531. @intent("note_count", description="Anzahl der Notizen")
  532. @pattern("wie viele notizen [habe ich]")
  533. @pattern("anzahl [meiner|der] notizen")
  534. @example("Wie viele Notizen habe ich?")
  535. async def handle_note_count(self, data: IntentReceivedData) -> IntentResult:
  536. """Gibt die Anzahl der Notizen zurueck."""
  537. notes = self._store.get_all(DATA_TYPE)
  538. count = len(notes)
  539. if count == 0:
  540. return IntentResult.success_with_response("Du hast keine Notizen.")
  541. if count == 1:
  542. return IntentResult.success_with_response("Du hast eine Notiz.")
  543. return IntentResult.success_with_response(f"Du hast {count} Notizen.")