Files
hermes-agent/tools/tool_labels.py
Siddharth Balyan 8c86e112f7 feat(connectors): connector results name the account they ran under
The gateway now reports the alias of the account each call used. Keep it
through the wire model, the result dict, both dispatch paths and the
model-facing entry, and show the chosen alias in the tool label.
2026-10-11 17:21:07 +05:30

164 lines
6.0 KiB
Python

from __future__ import annotations
import logging
from dataclasses import dataclass
from typing import Any, Dict, List, Optional
from tools.connectors.gateway.names import parse_connector_name
from tools.mcp_tool_schema import MCP_TOOL_NAME_PREFIX
logger = logging.getLogger("tools.tool_labels")
CONNECTOR_EMOJI = "🔗"
MCP_EMOJI = "🔌"
SEARCH_EMOJI = "🔎"
DESCRIBE_EMOJI = "📖"
DEFAULT_TOOL_EMOJI = "⚡"
_APP_TITLES = {
"discord": "Discord",
"figma": "Figma",
"github": "GitHub",
"gmail": "Gmail",
"googlecalendar": "Google Calendar",
"googledocs": "Google Docs",
"googledrive": "Google Drive",
"jira": "Jira",
"linear": "Linear",
"notion": "Notion",
"outlook": "Outlook",
"slack": "Slack",
"stripe_mcp": "Stripe",
"todoist": "Todoist",
}
BRIDGE_TOOL_NAMES = frozenset({"tool_call", "tool_search", "tool_describe"})
__all__ = [
"BRIDGE_TOOL_NAMES", "CONNECTOR_EMOJI", "DEFAULT_TOOL_EMOJI", "MCP_EMOJI", "ToolLabel",
"app_title", "label_for_tool_name", "labels_for_call"]
@dataclass(frozen=True)
class ToolLabel:
kind: str
app: str
action: str
emoji: str
name: str
preview: str = ""
@property
def text(self) -> str:
return f"{self.app} · {self.action}" if self.action else self.app
def as_payload(self) -> dict[str, str]:
return {"kind": self.kind, "app": self.app, "action": self.action, "emoji": self.emoji,
"text": self.text, "name": self.name, "preview": self.preview}
def app_title(slug: str) -> str:
key = (slug or "").strip()
known = _APP_TITLES.get(key.lower())
if known:
return known
words = [word for word in key.replace("-", " ").replace("_", " ").split() if word]
return " ".join(word[:1].upper() + word[1:] for word in words) or key
def _action_phrase(tool: str, app_slug: str = "") -> str:
text = (tool or "").strip()
prefix = f"{app_slug.replace('-', '_').upper()}_"
if app_slug and text.upper().startswith(prefix):
text = text[len(prefix):]
words = [word for word in text.replace("-", " ").replace("_", " ").split() if word]
return " ".join(words).lower()
def _registry_emoji(name: str) -> str:
try:
from tools.registry import registry
return registry.get_emoji(name, default="") or DEFAULT_TOOL_EMOJI
except Exception:
return DEFAULT_TOOL_EMOJI
def _arg_preview(name: str, arguments: Any) -> str:
if not isinstance(arguments, dict) or not arguments:
return ""
try:
from agent.display import build_tool_preview
return build_tool_preview(name, arguments) or ""
except Exception:
logger.debug("argument preview failed for %s", name, exc_info=True)
return ""
def _local_tool_label(name: str, arguments: Any) -> ToolLabel:
verb = ""
try:
from agent.display import get_tool_verb
verb = get_tool_verb(name) or ""
except Exception:
logger.debug("friendly verb lookup failed for %s", name, exc_info=True)
return ToolLabel(kind="tool", app=verb or app_title(name), action="",
emoji=_registry_emoji(name), name=name, preview=_arg_preview(name, arguments))
def _unnamed_label(arguments: Any) -> ToolLabel:
return ToolLabel(kind="tool", app="Unnamed call", action="", emoji=DEFAULT_TOOL_EMOJI,
name="", preview=_arg_preview("", arguments))
def label_for_tool_name(name: str, arguments: Any = None) -> Optional[ToolLabel]:
if not isinstance(name, str) or not name.strip():
return None
connector = parse_connector_name(name)
if connector is not None:
action = _action_phrase(connector.tool, connector.connector)
alias = arguments.get("connector_alias") if isinstance(arguments, dict) else None
if isinstance(alias, str) and alias.strip():
action = f"{action} ({alias.strip()})"
return ToolLabel(kind="connector", app=app_title(connector.connector), action=action,
emoji=CONNECTOR_EMOJI, name=name, preview=_arg_preview(name, arguments))
if name.startswith(MCP_TOOL_NAME_PREFIX):
server, _, tool = name[len(MCP_TOOL_NAME_PREFIX):].partition("__")
if tool:
return ToolLabel(kind="mcp", app=app_title(server), action=_action_phrase(tool),
emoji=MCP_EMOJI, name=name, preview=_arg_preview(name, arguments))
return _local_tool_label(name, arguments)
def _bridge_labels(function_name: str, args: dict[str, Any]) -> list[ToolLabel]:
if function_name == "tool_search":
queries = args.get("queries")
queries = [queries] if isinstance(queries, str) else queries
first = next((str(q) for q in queries if str(q or "").strip()), "") if isinstance(queries, list) else ""
return [ToolLabel(kind="tool", app="Searching tools", action=first,
emoji=SEARCH_EMOJI, name=function_name)]
names = args.get("names")
names = [names] if isinstance(names, str) else names
count = len(names) if isinstance(names, list) else 0
return [ToolLabel(kind="tool", app="Reading tool details",
action=f"{count} tool" if count == 1 else f"{count} tools",
emoji=DESCRIBE_EMOJI, name=function_name)]
def labels_for_call(function_name: str, function_args: Any) -> list[ToolLabel]:
args = function_args if isinstance(function_args, dict) else {}
if function_name in ("tool_search", "tool_describe"):
return _bridge_labels(function_name, args)
if function_name != "tool_call":
label = label_for_tool_name(function_name, args)
return [label] if label is not None and label.kind != "tool" else []
entries = args.get("calls")
entries = entries if isinstance(entries, list) else [args]
labels = []
for entry in entries:
entry = entry if isinstance(entry, dict) else {}
arguments = entry.get("arguments")
label = label_for_tool_name(entry.get("name"), arguments)
labels.append(label if label is not None else _unnamed_label(arguments))
return labels