"""Human-readable labels for tool calls (Notion-style “steps” section). The assistant's conversation window shows a collapsible « N steps » block above each answer. Raw tool names (``search_fulltext``) are developer-centric; this module maps every registered tool to an i18n message key plus the argument worth surfacing (path, query, …), so the UI can render sentences like « Recherche dans le vault : pizza » or « Fichier lu : notes/x.md ». The label KEY is emitted with each ``tool`` SSE event; the client resolves it against its locale dictionaries (``ai.step.``). Unknown tools fall back to a generic key with the raw name prettified, so a new tool never breaks the UI. """ from __future__ import annotations from typing import Any # tool name -> (message-key, primary argument name or None) # NOTE: argument names MUST match the pydantic input models in # backend/tools/schemas.py (verified by tests/test_tool_labels.py). _STEP_LABELS: dict[str, tuple[str, str | None]] = { "list_vaults": ("vaults", None), "list_directory": ("directory", "path"), "list_all_files": ("files", "dir"), "read_file": ("file_read", "path"), "read_file_raw": ("file_read", "path"), "get_backlinks": ("backlinks", "path"), "list_backups": ("backups", "path"), "diff_backup": ("backup_diff", "path"), "get_graph": ("graph", None), "search_fulltext": ("search", "q"), "search_advanced": ("search", "q"), "search_paths": ("search_paths", "q"), "list_tags": ("tags", None), "suggest_tags": ("tags_suggest", "q"), "list_recent": ("recent", None), "create_file": ("file_create", "path"), "create_directory": ("dir_create", "path"), "edit_file": ("file_edit", "path"), "append_to_file": ("file_append", "path"), "rename_file": ("file_rename", "path"), "rename_directory": ("dir_rename", "path"), "move_path": ("move", "source_path"), "replace_in_files": ("replace", "find"), "delete_file": ("file_delete", "path"), "delete_directory": ("dir_delete", "path"), "restore_backup": ("backup_restore", "path"), "web_search": ("web_search", "query"), "fetch_url": ("fetch_url", "url"), "crawl_site": ("crawl", "url"), "git_list_repos": ("git_repos", "provider"), "git_search_issues": ("git_issues", "query"), "git_get_file": ("git_file", "path"), "create_xlsx": ("xlsx_create", "path"), "list_xlsx_sheets": ("xlsx_sheets", "path"), "xlsx_to_markdown": ("xlsx_read", "path"), "update_xlsx_cells": ("xlsx_update", "path"), "append_xlsx_rows": ("xlsx_append", "path"), "create_docx": ("docx_create", "path"), "create_csv": ("csv_create", "path"), "create_pdf": ("pdf_create", "path"), } GENERIC_KEY = "generic" def _prettify(name: str) -> str: return name.replace("_", " ").strip() def _primary_value(arguments: dict[str, Any], arg: str | None) -> str | None: if not arg: return None value = arguments.get(arg) if isinstance(value, str) and value.strip(): return value.strip() return None def tool_step_label(name: str, arguments: dict[str, Any] | None = None) -> dict[str, Any]: """Return ``{key, params}`` describing one tool call in human terms. ``key`` resolves client-side to ``ai.step.``; ``params`` carries the optional ``{value}`` placeholder (path, query, …). For an unknown tool the generic key is used with the prettified name as the value. """ arguments = arguments or {} key, arg = _STEP_LABELS.get(name, (GENERIC_KEY, None)) if key == GENERIC_KEY: return {"key": GENERIC_KEY, "params": {"tool": _prettify(name)}} value = _primary_value(arguments, arg) params: dict[str, str] = {} if value is not None: params["value"] = value return {"key": key, "params": params} def thought_step_label(text: str) -> dict[str, Any]: """Step descriptor for one intermediate reasoning note of the model. The note is shown expanded under a “Thought” sub-section in the UI, so it is kept long enough to be readable (truncation is the safety net). """ cleaned = " ".join((text or "").split()) return {"key": "thought", "params": {"value": cleaned[:1200]}}