161 lines
5.8 KiB
Python
161 lines
5.8 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:
|
|
return ToolLabel(kind="connector", app=app_title(connector.connector),
|
|
action=_action_phrase(connector.tool, connector.connector),
|
|
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
|