text_input_handler.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. # -*- coding: utf-8 -*-
  2. """
  3. Text-Input-Handler für Server.
  4. Verarbeitet TextInput-Commands vom Client und integriert sie
  5. in das Conversation- und NLP-System.
  6. Flow:
  7. 1. Client sendet TextInput Command
  8. 2. Server empfängt text_input_received Event
  9. 3. Handler erstellt/fortsetzt Conversation
  10. 4. Handler emittiert speech_recognized Event
  11. 5. NLP Pipeline verarbeitet (wie bei echtem STT)
  12. 6. Bei Follow-Up: Server sendet FollowUpRequest zurück
  13. """
  14. import uuid
  15. from typing import TYPE_CHECKING
  16. from trixy_core.events.decorators import TrixyEvent
  17. from trixy_core.events.event_data.basic import SpeechRecognized
  18. from trixy_core.network.cmd import TextInputAck, FollowUpRequest
  19. from trixy_core.utils.debug import pinfo, pdebug, perror
  20. if TYPE_CHECKING:
  21. from trixy_core.application import IApplication
  22. from trixy_core.conversation.manager import ConversationManager
  23. class TextInputHandler:
  24. """
  25. Handler für Text-Input vom Client.
  26. Integriert Keyboard-Eingaben in das Conversation-System
  27. und leitet sie an die NLP-Pipeline weiter.
  28. Usage:
  29. handler = TextInputHandler(app)
  30. handler.register() # Registriert Event-Listener
  31. """
  32. def __init__(self, application: "IApplication") -> None:
  33. """
  34. Initialisiert den Handler.
  35. Args:
  36. application: Die Server-Anwendung
  37. """
  38. self._app = application
  39. self._active_sessions: dict[str, str] = {} # session_id -> satellite_id
  40. def register(self) -> None:
  41. """Registriert die Event-Handler."""
  42. # Event-Handler für text_input_received
  43. self._app.events.register("text_input_received", self._on_text_input)
  44. # Event-Handler für intent_handled (für Follow-Up-Erkennung)
  45. self._app.events.register("intent_handled", self._on_intent_handled)
  46. # Event-Handler für output_text_created (Follow-Up mit expects_response)
  47. self._app.events.register("output_text_created", self._on_output_created)
  48. pdebug("TextInputHandler registriert")
  49. def unregister(self) -> None:
  50. """Entfernt die Event-Handler."""
  51. self._app.events.unregister("text_input_received", self._on_text_input)
  52. self._app.events.unregister("intent_handled", self._on_intent_handled)
  53. self._app.events.unregister("output_text_created", self._on_output_created)
  54. async def _on_text_input(self, event_name: str, data: dict) -> None:
  55. """
  56. Verarbeitet eingehende Text-Eingaben.
  57. Args:
  58. event_name: Event-Name
  59. data: Event-Daten mit TextInput-Feldern
  60. """
  61. # Daten extrahieren
  62. satellite_id = data.get("satellite_id", "")
  63. text = data.get("text", "")
  64. session_id = data.get("session_id", "")
  65. room_id = data.get("satellite_room", data.get("room_id", ""))
  66. language = data.get("language", "de")
  67. is_follow_up = data.get("is_follow_up", False)
  68. if not text:
  69. perror("TextInput ohne Text empfangen")
  70. return
  71. pdebug(f"TextInput von {satellite_id}: '{text[:50]}...' (session={session_id})")
  72. # Session-ID generieren falls keine vorhanden
  73. if not session_id:
  74. session_id = f"txt-{uuid.uuid4().hex[:8]}"
  75. # Session tracken
  76. self._active_sessions[session_id] = satellite_id
  77. # Bestätigung senden
  78. await self._send_ack(satellite_id, session_id)
  79. # SpeechRecognized Event emittieren (wie bei echtem STT)
  80. speech_data = SpeechRecognized(
  81. satellite_id=satellite_id,
  82. room_id=room_id,
  83. text=text,
  84. confidence=1.0,
  85. language=language,
  86. is_final=True,
  87. source="keyboard",
  88. )
  89. # Zusätzliche Felder für Conversation-Tracking
  90. speech_data.session_id = session_id # type: ignore
  91. speech_data.is_follow_up = is_follow_up # type: ignore
  92. await self._app.events.trigger("speech_recognized", speech_data)
  93. async def _send_ack(self, satellite_id: str, session_id: str) -> None:
  94. """
  95. Sendet Bestätigung an den Client.
  96. Args:
  97. satellite_id: ID des Satellites
  98. session_id: Session-ID
  99. """
  100. try:
  101. # Satellite finden
  102. satellites = getattr(self._app, "satellites", None)
  103. if not satellites:
  104. return
  105. satellite = satellites.get(satellite_id)
  106. if not satellite:
  107. # Versuche über Alias
  108. for sat in satellites.all():
  109. if sat.alias == satellite_id:
  110. satellite = sat
  111. break
  112. if satellite:
  113. ack = TextInputAck(
  114. session_id=session_id,
  115. received=True,
  116. )
  117. await satellite.send_command(ack)
  118. pdebug(f"TextInputAck gesendet an {satellite_id}")
  119. except Exception as e:
  120. perror(f"Fehler beim Senden von TextInputAck: {e}")
  121. async def _on_intent_handled(self, event_name: str, data) -> None:
  122. """
  123. Prüft ob ein Follow-Up erwartet wird.
  124. Args:
  125. event_name: Event-Name
  126. data: IntentHandled-Event-Daten
  127. """
  128. # Prüfen ob Follow-Up nötig
  129. needs_followup = getattr(data, "needs_followup", False)
  130. if isinstance(data, dict):
  131. needs_followup = data.get("needs_followup", False)
  132. if not needs_followup:
  133. return
  134. # Session-ID und Satellite ermitteln
  135. session_id = getattr(data, "session_id", "") or ""
  136. satellite_id = getattr(data, "satellite_id", "") or ""
  137. followup_prompt = getattr(data, "followup_prompt", "") or ""
  138. if isinstance(data, dict):
  139. session_id = data.get("session_id", "")
  140. satellite_id = data.get("satellite_id", "")
  141. followup_prompt = data.get("followup_prompt", "")
  142. # Nur für Keyboard-Sessions
  143. if session_id not in self._active_sessions:
  144. return
  145. pdebug(f"Follow-Up für TextInput-Session: {session_id}")
  146. # FollowUpRequest wird nach TTS gesendet (in _on_output_created)
  147. async def _on_output_created(self, event_name: str, data) -> None:
  148. """
  149. Prüft ob nach Output ein Follow-Up-Input erwartet wird.
  150. Args:
  151. event_name: Event-Name
  152. data: OutputTextCreated-Event-Daten
  153. """
  154. expects_response = getattr(data, "expects_response", False)
  155. if isinstance(data, dict):
  156. expects_response = data.get("expects_response", False)
  157. if not expects_response:
  158. return
  159. # Session-ID und Satellite ermitteln
  160. session_id = getattr(data, "session_id", "") or ""
  161. satellite_id = getattr(data, "satellite_id", "") or ""
  162. text = getattr(data, "text", "") or ""
  163. if isinstance(data, dict):
  164. session_id = data.get("session_id", "")
  165. satellite_id = data.get("satellite_id", "")
  166. text = data.get("text", "")
  167. # Nur für Keyboard-Sessions
  168. if session_id not in self._active_sessions:
  169. return
  170. # FollowUpRequest an Client senden
  171. await self._send_follow_up_request(
  172. satellite_id=satellite_id or self._active_sessions.get(session_id, ""),
  173. session_id=session_id,
  174. question=text,
  175. )
  176. async def _send_follow_up_request(
  177. self,
  178. satellite_id: str,
  179. session_id: str,
  180. question: str,
  181. ) -> None:
  182. """
  183. Sendet Follow-Up-Request an den Client.
  184. Args:
  185. satellite_id: ID des Satellites
  186. session_id: Session-ID
  187. question: Die Rückfrage
  188. """
  189. try:
  190. satellites = getattr(self._app, "satellites", None)
  191. if not satellites:
  192. return
  193. satellite = satellites.get(satellite_id)
  194. if not satellite:
  195. for sat in satellites.all():
  196. if sat.alias == satellite_id:
  197. satellite = sat
  198. break
  199. if satellite:
  200. request = FollowUpRequest(
  201. session_id=session_id,
  202. question=question,
  203. )
  204. await satellite.send_command(request)
  205. pdebug(f"FollowUpRequest gesendet an {satellite_id}: {question[:50]}...")
  206. except Exception as e:
  207. perror(f"Fehler beim Senden von FollowUpRequest: {e}")
  208. def end_session(self, session_id: str) -> None:
  209. """
  210. Beendet eine Text-Input-Session.
  211. Args:
  212. session_id: Die Session-ID
  213. """
  214. self._active_sessions.pop(session_id, None)
  215. def setup_text_input_handler(application: "IApplication") -> TextInputHandler:
  216. """
  217. Erstellt und registriert den TextInputHandler.
  218. Args:
  219. application: Die Server-Anwendung
  220. Returns:
  221. Der registrierte Handler
  222. """
  223. handler = TextInputHandler(application)
  224. handler.register()
  225. return handler