mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-29 18:46:59 +00:00
fix(gateway): steer busy voice follow-ups after STT
This commit is contained in:
parent
aa40f16d3e
commit
0fd0161dfd
2 changed files with 95 additions and 1 deletions
|
|
@ -6160,6 +6160,62 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
|
||||
self._enqueue_fifo(session_key, event, adapter)
|
||||
|
||||
async def _prepare_busy_steer_text(self, event: MessageEvent) -> str:
|
||||
"""Return steerable text for a busy follow-up, transcribing voice first.
|
||||
|
||||
Fresh and queued voice messages reach the normal inbound STT pipeline,
|
||||
but successful steer messages intentionally bypass that queue. Without
|
||||
preprocessing here, a media-only voice follow-up has an empty text
|
||||
payload and steer mode silently degrades to queue mode.
|
||||
|
||||
Audio file attachments remain files; only voice-message media follows
|
||||
the automatic STT contract used by ``_prepare_inbound_message_text``.
|
||||
If transcription fails, preserve any caption and let the existing
|
||||
steer fallback handle an otherwise empty event without losing it.
|
||||
"""
|
||||
text = (event.text or "").strip()
|
||||
media_urls = getattr(event, "media_urls", None) or []
|
||||
media_types = getattr(event, "media_types", None) or []
|
||||
voice_paths: List[str] = []
|
||||
|
||||
for index, path in enumerate(media_urls):
|
||||
media_type = media_types[index] if index < len(media_types) else ""
|
||||
is_voice = event.message_type == MessageType.VOICE or (
|
||||
media_type.startswith("audio/")
|
||||
and event.message_type not in {MessageType.AUDIO, MessageType.DOCUMENT}
|
||||
)
|
||||
if is_voice:
|
||||
voice_paths.append(path)
|
||||
|
||||
if not voice_paths:
|
||||
return text
|
||||
|
||||
enriched_text, successful_transcripts = await self._enrich_message_with_transcription(
|
||||
text,
|
||||
voice_paths,
|
||||
)
|
||||
if not successful_transcripts:
|
||||
return text
|
||||
|
||||
if self._should_echo_stt_transcripts():
|
||||
adapter = self._adapter_for_source(event.source)
|
||||
if adapter:
|
||||
echo_meta = self._thread_metadata_for_source(
|
||||
event.source,
|
||||
self._reply_anchor_for_event(event),
|
||||
)
|
||||
for transcript in successful_transcripts:
|
||||
try:
|
||||
await adapter.send(
|
||||
event.source.chat_id,
|
||||
f'🎙️ "{transcript}"',
|
||||
metadata=echo_meta,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.debug("Busy-steer transcript echo failed (non-fatal): %s", exc)
|
||||
|
||||
return (enriched_text or text).strip()
|
||||
|
||||
async def _handle_active_session_busy_message(self, event: MessageEvent, session_key: str) -> bool:
|
||||
# --- Authorization gate (#17775) ---
|
||||
# The cold path (_handle_message) checks _is_user_authorized before
|
||||
|
|
@ -6347,7 +6403,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
steered = False
|
||||
redirected = False
|
||||
if effective_mode == "steer":
|
||||
steer_text = (event.text or "").strip()
|
||||
steer_text = await self._prepare_busy_steer_text(event)
|
||||
can_steer = (
|
||||
steer_text
|
||||
and event.message_type == MessageType.TEXT
|
||||
|
|
|
|||
|
|
@ -349,6 +349,44 @@ class TestBusySessionAck:
|
|||
assert "Steered" in content or "steer" in content.lower()
|
||||
assert "Interrupting" not in content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_steer_mode_transcribes_voice_before_injection(self, monkeypatch):
|
||||
"""A busy voice follow-up is transcribed and steered, never queued."""
|
||||
import gateway.run as _gr
|
||||
|
||||
monkeypatch.delenv("HERMES_GATEWAY_BUSY_STEER_ACK_ENABLED", raising=False)
|
||||
monkeypatch.setattr(_gr, "_load_gateway_config", lambda: {})
|
||||
runner, _sentinel = _make_runner()
|
||||
runner._busy_input_mode = "steer"
|
||||
runner._should_echo_stt_transcripts = MagicMock(return_value=False)
|
||||
runner._enrich_message_with_transcription = AsyncMock(
|
||||
return_value=('"yönü teknik mimariye çevir"', ["yönü teknik mimariye çevir"])
|
||||
)
|
||||
adapter = _make_adapter()
|
||||
|
||||
event = _make_event(text="")
|
||||
event.message_type = MessageType.VOICE
|
||||
event.media_urls = ["/tmp/follow-up.ogg"]
|
||||
event.media_types = ["audio/ogg"]
|
||||
sk = build_session_key(event.source)
|
||||
runner.adapters[event.source.platform] = adapter
|
||||
|
||||
agent = MagicMock()
|
||||
agent.steer = MagicMock(return_value=True)
|
||||
runner._running_agents[sk] = agent
|
||||
|
||||
await runner._handle_active_session_busy_message(event, sk)
|
||||
|
||||
runner._enrich_message_with_transcription.assert_awaited_once_with(
|
||||
"", ["/tmp/follow-up.ogg"]
|
||||
)
|
||||
agent.steer.assert_called_once_with('"yönü teknik mimariye çevir"')
|
||||
agent.interrupt.assert_not_called()
|
||||
assert sk not in adapter._pending_messages
|
||||
content = adapter._send_with_retry.call_args.kwargs["content"]
|
||||
assert "Steered" in content
|
||||
assert "Queued" not in content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_steer_mode_can_suppress_visible_ack_without_disabling_steer(self, monkeypatch):
|
||||
"""busy_steer_ack_enabled=false keeps steering but drops the echo bubble."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue