mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
fix: resolve listed messaging targets consistently
This commit is contained in:
parent
1d2e34c7eb
commit
afccbf253c
5 changed files with 105 additions and 22 deletions
|
|
@ -98,24 +98,26 @@ def _resolve_delivery_target(job: dict) -> Optional[dict]:
|
|||
|
||||
if ":" in deliver:
|
||||
platform_name, rest = deliver.split(":", 1)
|
||||
# Check for thread_id suffix (e.g. "telegram:-1003724596514:17")
|
||||
if ":" in rest:
|
||||
chat_id, thread_id = rest.split(":", 1)
|
||||
platform_key = platform_name.lower()
|
||||
|
||||
from tools.send_message_tool import _parse_target_ref
|
||||
|
||||
parsed_chat_id, parsed_thread_id, is_explicit = _parse_target_ref(platform_key, rest)
|
||||
if is_explicit:
|
||||
chat_id, thread_id = parsed_chat_id, parsed_thread_id
|
||||
else:
|
||||
chat_id, thread_id = rest, None
|
||||
|
||||
# Resolve human-friendly labels like "Alice (dm)" to real IDs.
|
||||
# send_message(action="list") shows labels with display suffixes
|
||||
# that aren't valid platform IDs (e.g. WhatsApp JIDs).
|
||||
try:
|
||||
from gateway.channel_directory import resolve_channel_name
|
||||
target = chat_id
|
||||
# Strip display suffix like " (dm)" or " (group)"
|
||||
if target.endswith(")") and " (" in target:
|
||||
target = target.rsplit(" (", 1)[0].strip()
|
||||
resolved = resolve_channel_name(platform_name.lower(), target)
|
||||
resolved = resolve_channel_name(platform_key, chat_id)
|
||||
if resolved:
|
||||
chat_id = resolved
|
||||
parsed_chat_id, parsed_thread_id, resolved_is_explicit = _parse_target_ref(platform_key, resolved)
|
||||
if resolved_is_explicit:
|
||||
chat_id, thread_id = parsed_chat_id, parsed_thread_id
|
||||
else:
|
||||
chat_id = resolved
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,20 @@ logger = logging.getLogger(__name__)
|
|||
DIRECTORY_PATH = get_hermes_home() / "channel_directory.json"
|
||||
|
||||
|
||||
def _normalize_channel_query(value: str) -> str:
|
||||
return value.lstrip("#").strip().lower()
|
||||
|
||||
|
||||
def _channel_target_name(platform_name: str, channel: Dict[str, Any]) -> str:
|
||||
"""Return the human-facing target label shown to users for a channel entry."""
|
||||
name = channel["name"]
|
||||
if platform_name == "discord" and channel.get("guild"):
|
||||
return f"#{name}"
|
||||
if platform_name != "discord" and channel.get("type"):
|
||||
return f"{name} ({channel['type']})"
|
||||
return name
|
||||
|
||||
|
||||
def _session_entry_id(origin: Dict[str, Any]) -> Optional[str]:
|
||||
chat_id = origin.get("chat_id")
|
||||
if not chat_id:
|
||||
|
|
@ -188,23 +202,25 @@ def resolve_channel_name(platform_name: str, name: str) -> Optional[str]:
|
|||
if not channels:
|
||||
return None
|
||||
|
||||
query = name.lstrip("#").lower()
|
||||
query = _normalize_channel_query(name)
|
||||
|
||||
# 1. Exact name match
|
||||
# 1. Exact name match, including the display labels shown by send_message(action="list")
|
||||
for ch in channels:
|
||||
if ch["name"].lower() == query:
|
||||
if _normalize_channel_query(ch["name"]) == query:
|
||||
return ch["id"]
|
||||
if _normalize_channel_query(_channel_target_name(platform_name, ch)) == query:
|
||||
return ch["id"]
|
||||
|
||||
# 2. Guild-qualified match for Discord ("GuildName/channel")
|
||||
if "/" in query:
|
||||
guild_part, ch_part = query.rsplit("/", 1)
|
||||
for ch in channels:
|
||||
guild = ch.get("guild", "").lower()
|
||||
if guild == guild_part and ch["name"].lower() == ch_part:
|
||||
guild = ch.get("guild", "").strip().lower()
|
||||
if guild == guild_part and _normalize_channel_query(ch["name"]) == ch_part:
|
||||
return ch["id"]
|
||||
|
||||
# 3. Partial prefix match (only if unambiguous)
|
||||
matches = [ch for ch in channels if ch["name"].lower().startswith(query)]
|
||||
matches = [ch for ch in channels if _normalize_channel_query(ch["name"]).startswith(query)]
|
||||
if len(matches) == 1:
|
||||
return matches[0]["id"]
|
||||
|
||||
|
|
@ -239,17 +255,16 @@ def format_directory_for_display() -> str:
|
|||
for guild_name, guild_channels in sorted(guilds.items()):
|
||||
lines.append(f"Discord ({guild_name}):")
|
||||
for ch in sorted(guild_channels, key=lambda c: c["name"]):
|
||||
lines.append(f" discord:#{ch['name']}")
|
||||
lines.append(f" discord:{_channel_target_name(plat_name, ch)}")
|
||||
if dms:
|
||||
lines.append("Discord (DMs):")
|
||||
for ch in dms:
|
||||
lines.append(f" discord:{ch['name']}")
|
||||
lines.append(f" discord:{_channel_target_name(plat_name, ch)}")
|
||||
lines.append("")
|
||||
else:
|
||||
lines.append(f"{plat_name.title()}:")
|
||||
for ch in channels:
|
||||
type_label = f" ({ch['type']})" if ch.get("type") else ""
|
||||
lines.append(f" {plat_name}:{ch['name']}{type_label}")
|
||||
lines.append(f" {plat_name}:{_channel_target_name(plat_name, ch)}")
|
||||
lines.append("")
|
||||
|
||||
lines.append('Use these as the "target" parameter when sending.')
|
||||
|
|
|
|||
|
|
@ -90,8 +90,9 @@ class TestResolveDeliveryTarget:
|
|||
with patch(
|
||||
"gateway.channel_directory.resolve_channel_name",
|
||||
return_value="12345678901234@lid",
|
||||
):
|
||||
) as resolve_mock:
|
||||
result = _resolve_delivery_target(job)
|
||||
resolve_mock.assert_called_once_with("whatsapp", "Alice (dm)")
|
||||
assert result == {
|
||||
"platform": "whatsapp",
|
||||
"chat_id": "12345678901234@lid",
|
||||
|
|
@ -112,6 +113,20 @@ class TestResolveDeliveryTarget:
|
|||
"thread_id": None,
|
||||
}
|
||||
|
||||
def test_human_friendly_topic_label_preserves_thread_id(self):
|
||||
"""Resolved Telegram topic labels should split chat_id and thread_id."""
|
||||
job = {"deliver": "telegram:Coaching Chat / topic 17585 (group)"}
|
||||
with patch(
|
||||
"gateway.channel_directory.resolve_channel_name",
|
||||
return_value="-1009999:17585",
|
||||
):
|
||||
result = _resolve_delivery_target(job)
|
||||
assert result == {
|
||||
"platform": "telegram",
|
||||
"chat_id": "-1009999",
|
||||
"thread_id": "17585",
|
||||
}
|
||||
|
||||
def test_raw_id_not_mangled_when_directory_returns_none(self):
|
||||
"""deliver: 'whatsapp:12345@lid' passes through when directory has no match."""
|
||||
job = {"deliver": "whatsapp:12345@lid"}
|
||||
|
|
|
|||
|
|
@ -119,6 +119,19 @@ class TestResolveChannelName:
|
|||
with self._setup(tmp_path, platforms):
|
||||
assert resolve_channel_name("telegram", "Coaching Chat / topic 17585") == "-1001:17585"
|
||||
|
||||
def test_display_label_with_type_suffix_resolves(self, tmp_path):
|
||||
platforms = {
|
||||
"telegram": [
|
||||
{"id": "123", "name": "Alice", "type": "dm"},
|
||||
{"id": "456", "name": "Dev Group", "type": "group"},
|
||||
{"id": "-1001:17585", "name": "Coaching Chat / topic 17585", "type": "group"},
|
||||
]
|
||||
}
|
||||
with self._setup(tmp_path, platforms):
|
||||
assert resolve_channel_name("telegram", "Alice (dm)") == "123"
|
||||
assert resolve_channel_name("telegram", "Dev Group (group)") == "456"
|
||||
assert resolve_channel_name("telegram", "Coaching Chat / topic 17585 (group)") == "-1001:17585"
|
||||
|
||||
|
||||
class TestBuildFromSessions:
|
||||
def _write_sessions(self, tmp_path, sessions_data):
|
||||
|
|
|
|||
|
|
@ -203,6 +203,44 @@ class TestSendMessageTool:
|
|||
media_files=[],
|
||||
)
|
||||
|
||||
def test_display_label_target_resolves_via_channel_directory(self, tmp_path):
|
||||
config, telegram_cfg = _make_config()
|
||||
cache_file = tmp_path / "channel_directory.json"
|
||||
cache_file.write_text(json.dumps({
|
||||
"updated_at": "2026-01-01T00:00:00",
|
||||
"platforms": {
|
||||
"telegram": [
|
||||
{"id": "-1001:17585", "name": "Coaching Chat / topic 17585", "type": "group"}
|
||||
]
|
||||
},
|
||||
}))
|
||||
|
||||
with patch("gateway.channel_directory.DIRECTORY_PATH", cache_file), \
|
||||
patch("gateway.config.load_gateway_config", return_value=config), \
|
||||
patch("tools.interrupt.is_interrupted", return_value=False), \
|
||||
patch("model_tools._run_async", side_effect=_run_async_immediately), \
|
||||
patch("tools.send_message_tool._send_to_platform", new=AsyncMock(return_value={"success": True})) as send_mock, \
|
||||
patch("gateway.mirror.mirror_to_session", return_value=True):
|
||||
result = json.loads(
|
||||
send_message_tool(
|
||||
{
|
||||
"action": "send",
|
||||
"target": "telegram:Coaching Chat / topic 17585 (group)",
|
||||
"message": "hello",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
assert result["success"] is True
|
||||
send_mock.assert_awaited_once_with(
|
||||
Platform.TELEGRAM,
|
||||
telegram_cfg,
|
||||
"-1001",
|
||||
"hello",
|
||||
thread_id="17585",
|
||||
media_files=[],
|
||||
)
|
||||
|
||||
def test_media_only_message_uses_placeholder_for_mirroring(self):
|
||||
config, telegram_cfg = _make_config()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue