mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-29 18:46:59 +00:00
fix(gateway): pass channel_prompt into voice-channel STT events; guard empty transcripts
Two hand-written fixes in the voice input path: - _handle_voice_channel_input now resolves the bound text channel's channel_prompt via the adapter's _resolve_channel_prompt so voice input gets the same per-channel context as typed messages (fixes #50149). - _enrich_message_with_transcription now guards success=True results whose transcript is empty/whitespace-only (silence, cut-off, inaudible audio): instead of emitting empty quotes the agent gets a clear sentinel note. Reimplemented against the current plain-quoted note wording; original concept and tests by @deacon-botdoctor in PR #41603 (fixes #41603).
This commit is contained in:
parent
a4c9994837
commit
f76b2b47aa
3 changed files with 81 additions and 0 deletions
|
|
@ -15755,11 +15755,22 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
# Use SimpleNamespace as raw_message so _get_guild_id() can extract
|
||||
# guild_id and _send_voice_reply() plays audio in the voice channel.
|
||||
from types import SimpleNamespace
|
||||
# Resolve the bound text channel's channel_prompt so voice input gets
|
||||
# the same per-channel context as typed messages (#50149).
|
||||
channel_prompt: Optional[str] = None
|
||||
resolver = getattr(adapter, "_resolve_channel_prompt", None)
|
||||
if callable(resolver):
|
||||
try:
|
||||
resolved = resolver(str(text_ch_id))
|
||||
channel_prompt = resolved if isinstance(resolved, str) else None
|
||||
except Exception:
|
||||
channel_prompt = None
|
||||
event = MessageEvent(
|
||||
source=source,
|
||||
text=transcript,
|
||||
message_type=MessageType.VOICE,
|
||||
raw_message=SimpleNamespace(guild_id=guild_id, guild=None),
|
||||
channel_prompt=channel_prompt,
|
||||
)
|
||||
|
||||
await adapter.handle_message(event)
|
||||
|
|
@ -18012,6 +18023,19 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
result = await asyncio.to_thread(transcribe_audio, path)
|
||||
if result["success"]:
|
||||
transcript = result["transcript"]
|
||||
# Speech-to-text can return success=True with an empty or
|
||||
# whitespace-only transcript on silence, cut-off, or
|
||||
# inaudible audio. Emitting empty quotes ('""') makes the
|
||||
# agent reply to nothing and can loop, so that case gets a
|
||||
# clear sentinel note instead (#41603).
|
||||
if not (transcript or "").strip():
|
||||
enriched_parts.append(
|
||||
"[The user sent a voice message but it came through "
|
||||
"empty or inaudible — speech-to-text returned no "
|
||||
"words. Do not guess at the content; ask the user "
|
||||
"to resend or type it out.]"
|
||||
)
|
||||
continue
|
||||
successful_transcripts.append(transcript)
|
||||
# Pass the transcript through as a plain quoted line. The
|
||||
# earlier wording ("The user sent a voice message~ Here's
|
||||
|
|
|
|||
|
|
@ -181,6 +181,30 @@ async def test_enrich_message_with_transcription_handles_missing_transcription_m
|
|||
assert transcripts == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_enrich_message_with_transcription_guards_empty_transcript():
|
||||
"""success=True with an empty/whitespace transcript must not emit empty
|
||||
quotes — it gets a sentinel note and is excluded from transcripts (#41603)."""
|
||||
from gateway.run import GatewayRunner
|
||||
|
||||
runner = GatewayRunner.__new__(GatewayRunner)
|
||||
runner.config = GatewayConfig(stt_enabled=True)
|
||||
runner._has_setup_skill = lambda: False
|
||||
|
||||
with patch(
|
||||
"tools.transcription_tools.transcribe_audio",
|
||||
return_value={"success": True, "transcript": " \n\t", "provider": "local_command"},
|
||||
):
|
||||
result, transcripts = await runner._enrich_message_with_transcription(
|
||||
"caption",
|
||||
["/tmp/voice.ogg"],
|
||||
)
|
||||
|
||||
assert "empty or inaudible" in result
|
||||
assert '""' not in result
|
||||
assert transcripts == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prepare_inbound_message_text_transcribes_queued_voice_event():
|
||||
from gateway.run import GatewayRunner
|
||||
|
|
|
|||
|
|
@ -1030,6 +1030,39 @@ class TestVoiceChannelCommands:
|
|||
assert event.source.chat_id == "123"
|
||||
assert event.source.chat_type == "channel"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_input_resolves_channel_prompt(self, runner):
|
||||
"""Voice input must carry the bound text channel's channel_prompt (#50149)."""
|
||||
from gateway.config import Platform
|
||||
mock_adapter = AsyncMock()
|
||||
mock_adapter._voice_text_channels = {111: 123}
|
||||
mock_adapter._voice_sources = {}
|
||||
mock_adapter._client = MagicMock()
|
||||
mock_adapter._client.get_channel = MagicMock(return_value=AsyncMock())
|
||||
mock_adapter.handle_message = AsyncMock()
|
||||
mock_adapter._resolve_channel_prompt = MagicMock(return_value="Be terse in #dev.")
|
||||
runner.adapters[Platform.DISCORD] = mock_adapter
|
||||
await runner._handle_voice_channel_input(111, 42, "Hello from VC")
|
||||
mock_adapter._resolve_channel_prompt.assert_called_once_with("123")
|
||||
event = mock_adapter.handle_message.call_args[0][0]
|
||||
assert event.channel_prompt == "Be terse in #dev."
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_input_channel_prompt_resolver_failure_is_non_fatal(self, runner):
|
||||
"""A failing channel_prompt resolver must not block voice input."""
|
||||
from gateway.config import Platform
|
||||
mock_adapter = AsyncMock()
|
||||
mock_adapter._voice_text_channels = {111: 123}
|
||||
mock_adapter._voice_sources = {}
|
||||
mock_adapter._client = MagicMock()
|
||||
mock_adapter._client.get_channel = MagicMock(return_value=AsyncMock())
|
||||
mock_adapter.handle_message = AsyncMock()
|
||||
mock_adapter._resolve_channel_prompt = MagicMock(side_effect=RuntimeError("boom"))
|
||||
runner.adapters[Platform.DISCORD] = mock_adapter
|
||||
await runner._handle_voice_channel_input(111, 42, "Hello from VC")
|
||||
event = mock_adapter.handle_message.call_args[0][0]
|
||||
assert event.channel_prompt is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_input_reuses_bound_source_metadata(self, runner):
|
||||
"""Voice input should share the linked text channel session metadata."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue