mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
fix(gateway): always inject reply-to pointer, not just when quoted text is absent (#13676)
The [Replying to: "..."] prefix is disambiguation, not deduplication. When a user explicitly replies to a prior message, the agent needs a pointer to which specific message they're referencing — even when the quoted text already exists somewhere in history. History can contain the same or similar text multiple times; without an explicit pointer the agent has to guess (or answer for both subjects), and the reply signal is silently dropped. Example: in a conversation comparing Japan and Italy, replying to the "Japan is great for culture..." message and asking "What's the best time to go?" — previously the found_in_history check suppressed the prefix because the quoted text was already in history, leaving the agent to guess which destination the user meant. Now the pointer is always present. Drops the found_in_history guard added in #1594. Token overhead is minimal (snippet capped at 500 chars on the new user turn; cached prefix unaffected). Behavior becomes deterministic: reply sent ⇒ pointer present. Thanks to smartyi for flagging this.
This commit is contained in:
parent
7ff7155cbd
commit
e889332c99
2 changed files with 166 additions and 7 deletions
159
tests/gateway/test_reply_to_injection.py
Normal file
159
tests/gateway/test_reply_to_injection.py
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
"""Tests for reply-to pointer injection in _prepare_inbound_message_text.
|
||||
|
||||
The `[Replying to: "..."]` prefix is a *disambiguation pointer*, not
|
||||
deduplication. It must always be injected when the user explicitly replies
|
||||
to a prior message — even when the quoted text already exists somewhere
|
||||
in the conversation history. History can contain the same or similar text
|
||||
multiple times, and without an explicit pointer the agent has to guess
|
||||
which prior message the user is referencing.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
from gateway.config import GatewayConfig, Platform, PlatformConfig
|
||||
from gateway.platforms.base import MessageEvent
|
||||
from gateway.run import GatewayRunner
|
||||
from gateway.session import SessionSource
|
||||
|
||||
|
||||
def _make_runner() -> GatewayRunner:
|
||||
runner = object.__new__(GatewayRunner)
|
||||
runner.config = GatewayConfig(
|
||||
platforms={Platform.TELEGRAM: PlatformConfig(enabled=True, token="fake")},
|
||||
)
|
||||
runner.adapters = {}
|
||||
runner._model = "openai/gpt-4.1-mini"
|
||||
runner._base_url = None
|
||||
return runner
|
||||
|
||||
|
||||
def _source() -> SessionSource:
|
||||
return SessionSource(
|
||||
platform=Platform.TELEGRAM,
|
||||
chat_id="123",
|
||||
chat_name="DM",
|
||||
chat_type="private",
|
||||
user_name="Alice",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reply_prefix_injected_when_text_absent_from_history():
|
||||
runner = _make_runner()
|
||||
source = _source()
|
||||
event = MessageEvent(
|
||||
text="What's the best time to go?",
|
||||
source=source,
|
||||
reply_to_message_id="42",
|
||||
reply_to_text="Japan is great for culture, food, and efficiency.",
|
||||
)
|
||||
|
||||
result = await runner._prepare_inbound_message_text(
|
||||
event=event,
|
||||
source=source,
|
||||
history=[{"role": "user", "content": "unrelated"}],
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
assert result.startswith(
|
||||
'[Replying to: "Japan is great for culture, food, and efficiency."]'
|
||||
)
|
||||
assert result.endswith("What's the best time to go?")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reply_prefix_still_injected_when_text_in_history():
|
||||
"""Regression test: the pointer must survive even when the quoted text
|
||||
already appears in history. Previously a `found_in_history` guard
|
||||
silently dropped the prefix, leaving the agent to guess which prior
|
||||
message the user was referencing."""
|
||||
runner = _make_runner()
|
||||
source = _source()
|
||||
quoted = "Japan is great for culture, food, and efficiency."
|
||||
event = MessageEvent(
|
||||
text="What's the best time to go?",
|
||||
source=source,
|
||||
reply_to_message_id="42",
|
||||
reply_to_text=quoted,
|
||||
)
|
||||
|
||||
history = [
|
||||
{"role": "user", "content": "I'm thinking of going to Japan or Italy."},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": (
|
||||
f"{quoted} Italy is better if you prefer a relaxed pace."
|
||||
),
|
||||
},
|
||||
{"role": "user", "content": "How long should I stay?"},
|
||||
{"role": "assistant", "content": "For Japan, 10-14 days is ideal."},
|
||||
]
|
||||
|
||||
result = await runner._prepare_inbound_message_text(
|
||||
event=event,
|
||||
source=source,
|
||||
history=history,
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
assert result.startswith(f'[Replying to: "{quoted}"]')
|
||||
assert result.endswith("What's the best time to go?")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_no_prefix_without_reply_context():
|
||||
runner = _make_runner()
|
||||
source = _source()
|
||||
event = MessageEvent(text="hello", source=source)
|
||||
|
||||
result = await runner._prepare_inbound_message_text(
|
||||
event=event,
|
||||
source=source,
|
||||
history=[],
|
||||
)
|
||||
|
||||
assert result == "hello"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_no_prefix_when_reply_to_text_is_empty():
|
||||
"""reply_to_message_id alone without text (e.g. a reply to a media-only
|
||||
message) should not produce an empty `[Replying to: ""]` prefix."""
|
||||
runner = _make_runner()
|
||||
source = _source()
|
||||
event = MessageEvent(
|
||||
text="hi",
|
||||
source=source,
|
||||
reply_to_message_id="42",
|
||||
reply_to_text=None,
|
||||
)
|
||||
|
||||
result = await runner._prepare_inbound_message_text(
|
||||
event=event,
|
||||
source=source,
|
||||
history=[],
|
||||
)
|
||||
|
||||
assert result == "hi"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reply_snippet_truncated_to_500_chars():
|
||||
runner = _make_runner()
|
||||
source = _source()
|
||||
long_text = "x" * 800
|
||||
event = MessageEvent(
|
||||
text="follow-up",
|
||||
source=source,
|
||||
reply_to_message_id="42",
|
||||
reply_to_text=long_text,
|
||||
)
|
||||
|
||||
result = await runner._prepare_inbound_message_text(
|
||||
event=event,
|
||||
source=source,
|
||||
history=[],
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
assert result.startswith('[Replying to: "' + "x" * 500 + '"]')
|
||||
assert "x" * 501 not in result
|
||||
Loading…
Add table
Add a link
Reference in a new issue