From 89770c03725ef9363cc3b7da767ac5d7d30a0d08 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Fri, 29 May 2026 13:01:49 -0500 Subject: [PATCH] fix(gateway): bind session context for auto tts --- gateway/platforms/base.py | 21 +++++++- tests/gateway/test_tts_media_routing.py | 65 +++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/gateway/platforms/base.py b/gateway/platforms/base.py index 0d141d0fcf1da..dddc7534a538d 100644 --- a/gateway/platforms/base.py +++ b/gateway/platforms/base.py @@ -3756,9 +3756,26 @@ async def _stop_typing_task() -> None: speech_text = self.prepare_tts_text(text_content) if not speech_text: raise ValueError("Empty text after markdown cleanup") - tts_result_str = await asyncio.to_thread( - text_to_speech_tool, text=speech_text + from gateway.session_context import clear_session_vars, set_session_vars + + source_platform = getattr(event.source, "platform", None) + source_platform = getattr(source_platform, "value", source_platform) + tts_context_tokens = set_session_vars( + platform=str(source_platform or self.platform.value or ""), + chat_id=str(getattr(event.source, "chat_id", "") or ""), + chat_name=str(getattr(event.source, "chat_name", "") or ""), + thread_id=str(getattr(event.source, "thread_id", "") or ""), + user_id=str(getattr(event.source, "user_id", "") or ""), + user_name=str(getattr(event.source, "user_name", "") or ""), + session_key=session_key, + message_id=str(getattr(event, "message_id", "") or ""), ) + try: + tts_result_str = await asyncio.to_thread( + text_to_speech_tool, text=speech_text + ) + finally: + clear_session_vars(tts_context_tokens) tts_data = _json.loads(tts_result_str) _tts_path = tts_data.get("file_path") except Exception as tts_err: diff --git a/tests/gateway/test_tts_media_routing.py b/tests/gateway/test_tts_media_routing.py index eaf9c59280894..2370bd349c6a0 100644 --- a/tests/gateway/test_tts_media_routing.py +++ b/tests/gateway/test_tts_media_routing.py @@ -121,6 +121,71 @@ async def test_base_adapter_routes_voice_tagged_telegram_ogg_media_tag_to_voice_ adapter.send_document.assert_not_awaited() +@pytest.mark.asyncio +async def test_auto_tts_rebinds_gateway_session_context_for_tts_tool(tmp_path, monkeypatch): + """Auto-TTS runs after the message handler has returned. + + The handler path may clear gateway session contextvars before the base + adapter calls ``text_to_speech_tool``. The auto-TTS call must re-bind the + current event context so Telegram replies generate voice-compatible audio + instead of falling back to contextless MP3 output. + """ + from gateway.session_context import ( + _UNSET, + _VAR_MAP, + clear_session_vars, + get_session_env, + set_session_vars, + ) + + captured_context = {} + adapter = _MediaRoutingAdapter() + adapter._auto_tts_enabled_chats.add("chat-1") + event = _event() + event.message_type = MessageType.VOICE + session_key = build_session_key(event.source) + audio_file = tmp_path / "auto-tts.ogg" + + async def _handler(_event): + tokens = set_session_vars( + platform="telegram", + chat_id=_event.source.chat_id, + user_id=getattr(_event.source, "user_id", "") or "", + session_key=session_key, + message_id=getattr(_event, "message_id", "") or "", + ) + clear_session_vars(tokens) + return "spoken reply" + + def _fake_tts_tool(*, text, **_kwargs): + captured_context["platform"] = get_session_env("HERMES_SESSION_PLATFORM", "") + captured_context["chat_id"] = get_session_env("HERMES_SESSION_CHAT_ID", "") + captured_context["session_key"] = get_session_env("HERMES_SESSION_KEY", "") + captured_context["message_id"] = get_session_env("HERMES_SESSION_MESSAGE_ID", "") + audio_file.write_bytes(b"ogg") + return '{"success": true, "file_path": "' + str(audio_file) + '"}' + + adapter._message_handler = AsyncMock(side_effect=_handler) + adapter.send_voice = AsyncMock(return_value=SendResult(success=True, message_id="voice")) + adapter.send = AsyncMock(return_value=SendResult(success=True, message_id="text")) + monkeypatch.setattr("tools.tts_tool.check_tts_requirements", lambda: True) + monkeypatch.setattr("tools.tts_tool.text_to_speech_tool", _fake_tts_tool) + + try: + await adapter._process_message_background(event, session_key) + finally: + for var in _VAR_MAP.values(): + var.set(_UNSET) + + assert captured_context == { + "platform": "telegram", + "chat_id": "chat-1", + "session_key": session_key, + "message_id": "msg-1", + } + adapter.send_voice.assert_awaited_once() + + def _fake_runner(thread_meta): """Build a fake GatewayRunner-like object with the helper methods needed by _deliver_media_from_response."""