mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-26 17:38:36 +00:00
fix(slack): resolve channel IDs to human-readable names
Previously, Slack sessions and the channel directory stored raw channel IDs (e.g. C0ATFHY907L) as chat_name, making it impossible for operators to identify channels in send_message listings or session data. Changes: - Add _channel_name_cache and _resolve_channel_name() to SlackAdapter that calls conversations.info (channels) or users.info (DMs) with in-memory caching to avoid repeated API calls - Use resolved names in _handle_slack_message() build_source() calls - Use cached names in _seed_assistant_thread_session() - Enrich session-sourced entries in channel_directory._build_slack() by cross-referencing API results and falling back to conversations.info + users.info for DMs and private channels not in the bot's scope Fixes: channel directory and session origins now show readable names like 'general' or 'John Doe' instead of 'C0xxx' / 'D0xxx'.
This commit is contained in:
parent
da131aef3a
commit
8685fea0ce
2 changed files with 105 additions and 5 deletions
|
|
@ -321,10 +321,49 @@ async def _build_slack(adapter) -> List[Dict[str, Any]]:
|
|||
continue
|
||||
|
||||
# Merge in DM/group entries discovered from session history.
|
||||
# Build a lookup from API-discovered channels so we can enrich session entries.
|
||||
api_name_lookup = {ch["id"]: ch["name"] for ch in channels}
|
||||
|
||||
for entry in await asyncio.to_thread(_build_from_sessions, "slack"):
|
||||
if entry.get("id") not in seen_ids:
|
||||
eid = entry.get("id")
|
||||
if eid not in seen_ids:
|
||||
# If the entry name is still a raw Slack ID (e.g. C0xxx / D0xxx),
|
||||
# try to resolve it from the API lookup first.
|
||||
if entry.get("name", "").startswith(("C0", "D0", "G0")):
|
||||
if eid in api_name_lookup:
|
||||
entry["name"] = api_name_lookup[eid]
|
||||
channels.append(entry)
|
||||
seen_ids.add(entry.get("id"))
|
||||
seen_ids.add(eid)
|
||||
|
||||
# Resolve remaining raw-ID entries (DMs, private channels not in bot scope)
|
||||
# by calling conversations.info + users.info for each.
|
||||
unresolved = [ch for ch in channels if ch.get("name", "").startswith(("C0", "D0", "G0"))]
|
||||
if unresolved and team_clients:
|
||||
client = next(iter(team_clients.values()))
|
||||
for entry in unresolved:
|
||||
try:
|
||||
resp = await client.conversations_info(channel=entry["id"])
|
||||
if not resp.get("ok"):
|
||||
continue
|
||||
ch_info = resp.get("channel", {})
|
||||
if ch_info.get("is_im"):
|
||||
peer_user = ch_info.get("user", "")
|
||||
if peer_user:
|
||||
user_resp = await client.users_info(user=peer_user)
|
||||
if user_resp.get("ok"):
|
||||
u = user_resp["user"]
|
||||
entry["name"] = (
|
||||
u.get("profile", {}).get("display_name")
|
||||
or u.get("real_name")
|
||||
or u.get("name")
|
||||
or entry["id"]
|
||||
)
|
||||
entry["type"] = "dm"
|
||||
else:
|
||||
entry["name"] = ch_info.get("name") or ch_info.get("name_normalized") or entry["id"]
|
||||
except Exception as e:
|
||||
logger.debug("Channel directory: failed to resolve %s: %s", entry["id"], e)
|
||||
continue
|
||||
|
||||
return channels
|
||||
|
||||
|
|
|
|||
|
|
@ -902,6 +902,12 @@ class SlackAdapter(BasePlatformAdapter):
|
|||
# tenant's display name.
|
||||
self._user_name_cache: Dict[Tuple[str, str], str] = {}
|
||||
self._USER_NAME_CACHE_MAX = 5000
|
||||
# (team_id, channel_id) → resolved channel/DM display name. Channel
|
||||
# IDs are workspace-local like user IDs, so scope by workspace too.
|
||||
# Bounded like the sibling caches (grows per DM — DM channel IDs are
|
||||
# per-user).
|
||||
self._channel_name_cache: Dict[Tuple[str, str], str] = {}
|
||||
self._CHANNEL_NAME_CACHE_MAX = 5000
|
||||
# (team_id, user_id) → Slack bot identity, same workspace scoping as
|
||||
# the name cache. Used to catch peer-agent posts that arrive as plain
|
||||
# user messages without bot_id/subtype=bot_message markers.
|
||||
|
|
@ -3825,6 +3831,53 @@ class SlackAdapter(BasePlatformAdapter):
|
|||
del self._user_name_cache[old_key]
|
||||
return name
|
||||
|
||||
async def _resolve_channel_name(
|
||||
self, channel_id: str, team_id: str = ""
|
||||
) -> str:
|
||||
"""Resolve a Slack channel ID to a human-readable name (cached).
|
||||
|
||||
For public/private channels returns the channel name. For DMs (im)
|
||||
returns the peer user's display name. Falls back to the raw
|
||||
channel_id on any error, so logs and agent context degrade to the
|
||||
current behavior rather than breaking message handling.
|
||||
"""
|
||||
if not channel_id:
|
||||
return channel_id
|
||||
team_id = str(team_id or self._channel_team.get(channel_id, ""))
|
||||
cache_key = (team_id, str(channel_id))
|
||||
cached = self._channel_name_cache.get(cache_key)
|
||||
if cached is not None:
|
||||
return cached
|
||||
if not self._app:
|
||||
return channel_id
|
||||
try:
|
||||
resp = await self._get_client(
|
||||
channel_id, team_id=team_id or None
|
||||
).conversations_info(channel=channel_id)
|
||||
if not isinstance(resp, dict) or not resp.get("ok"):
|
||||
name = channel_id
|
||||
else:
|
||||
ch = resp.get("channel") or {}
|
||||
if ch.get("is_im"):
|
||||
peer_user = ch.get("user", "")
|
||||
name = (
|
||||
await self._resolve_user_name(
|
||||
peer_user, chat_id=channel_id, team_id=team_id
|
||||
)
|
||||
if peer_user
|
||||
else channel_id
|
||||
)
|
||||
else:
|
||||
name = ch.get("name") or ch.get("name_normalized") or channel_id
|
||||
except Exception as e:
|
||||
logger.debug("[Slack] conversations.info failed for %s: %s", channel_id, e)
|
||||
name = channel_id
|
||||
self._channel_name_cache[cache_key] = name
|
||||
self._trim_oldest_dict_entries(
|
||||
self._channel_name_cache, self._CHANNEL_NAME_CACHE_MAX
|
||||
)
|
||||
return name
|
||||
|
||||
async def _humanize_user_mentions(
|
||||
self, text: str, chat_id: str = "", team_id: str = ""
|
||||
) -> str:
|
||||
|
|
@ -4562,7 +4615,9 @@ class SlackAdapter(BasePlatformAdapter):
|
|||
|
||||
source = self.build_source(
|
||||
chat_id=channel_id,
|
||||
chat_name=channel_id,
|
||||
chat_name=self._channel_name_cache.get(
|
||||
(str(metadata.get("team_id") or ""), channel_id), channel_id
|
||||
),
|
||||
chat_type="dm",
|
||||
user_id=user_id,
|
||||
thread_id=thread_ts,
|
||||
|
|
@ -4599,7 +4654,9 @@ class SlackAdapter(BasePlatformAdapter):
|
|||
|
||||
source = self.build_source(
|
||||
chat_id=channel_id,
|
||||
chat_name=channel_id,
|
||||
chat_name=self._channel_name_cache.get(
|
||||
(str(metadata.get("team_id") or ""), channel_id), channel_id
|
||||
),
|
||||
chat_type="dm",
|
||||
user_id=user_id,
|
||||
chat_topic=metadata.get("context_channel_id") or None,
|
||||
|
|
@ -6125,6 +6182,10 @@ class SlackAdapter(BasePlatformAdapter):
|
|||
user_id, chat_id=channel_id, team_id=team_id
|
||||
)
|
||||
|
||||
# Resolve channel display name (cached after first lookup) so logs
|
||||
# and agent context show #channel / peer names instead of raw IDs.
|
||||
channel_name = await self._resolve_channel_name(channel_id, team_id=team_id)
|
||||
|
||||
# Slack's AI Agent Messages tab shows visible app threads; title the
|
||||
# first DM thread turn from the user's prompt when Slack AI APIs are
|
||||
# available. This is best-effort and configurable via config.yaml.
|
||||
|
|
@ -6139,7 +6200,7 @@ class SlackAdapter(BasePlatformAdapter):
|
|||
# Build source
|
||||
source = self.build_source(
|
||||
chat_id=channel_id,
|
||||
chat_name=channel_id, # Will be resolved later if needed
|
||||
chat_name=channel_name,
|
||||
chat_type="dm" if is_dm else "group",
|
||||
user_id=user_id,
|
||||
user_name=user_name,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue