From 5ca41d55fb1b2aea9a16a359c9335c562435a9b6 Mon Sep 17 00:00:00 2001
From: Etienne <45695613+etiennejouan@users.noreply.github.com>
Date: Wed, 24 Jun 2026 13:41:09 +0200
Subject: [PATCH] feat(ai): humanize tool-call (#21976)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
# Humanize tool-call labels
cc: https://github.com/twentyhq/twenty/pull/21462
## Preview
## Why
In the AI chat, tool steps were displayed using raw tool identifiers
(`find_many_companies`, `create_one_task`, `send_email`...) and labels
were partially reconstructed/humanized on the frontend. This was hard to
localize and inconsistent across tool categories.
This PR makes the **backend the single source of truth for
human-readable, localized tool labels**, exposes them through
`getToolIndex`, and reduces the frontend to a thin resolver that picks
the right label for the current status (in-progress / completed).
## What changed
### Backend
- `ToolIndexEntry` (and the `getToolIndex` GraphQL DTO) now carry
`label`, `inProgressLabel?`, `completedLabel?`.
- New `getCrudToolLabels(operation, objectLabel, i18nService, locale)`
builds CRUD labels from a verb table (Search / Find / Group / Create /
Update / Upsert / Delete × imperative / in-progress / completed) + the
(translated, lowercased) object label.
- New `translate-tool-label.util.ts` translates a source label via
`I18nService` (`generateMessageId` → fallback to source when no
translation exists).
- Action tools: labels extracted to the `ACTION_TOOL_LABELS` constant
(`msg` + `i18nLabel`) and translated in
`ActionToolProvider.buildDescriptor`.
- Logic-function tools use the function name as label;
`toolSetToDescriptors` (workflow / view / metadata / dashboard) accepts
an optional `labels` map and falls back to a humanized tool name.
- Labels are localized server-side using the request locale
(`@RequestLocale` → `buildToolIndex` → `context.locale`, threaded
through `ToolContext` / `ToolProviderContext`).
- `code_interpreter` schema now asks the model for `loadingMessage`
(present tense) and `completedMessage` (past tense), so its status text
is model-generated.
- Removed the old generic `loadingMessage` injection mechanism
(`wrap-tool-for-execution.util.ts` deleted; `wrapJsonSchemaForExecution`
/ `stripLoadingMessage` no longer wrap every tool).
### Frontend
- New `useToolLabelMap()` hook builds a `Map` from `getToolIndex`.
- `getToolDisplayMessage` → `resolveToolDisplayMessage({ input,
toolName, isFinished, labelMap, output })`: a small resolver registry
keyed by tool name (`execute_tool`, `web_search`, `learn_tools`,
`load_skills`, `code_interpreter`, default).
- Default resolver prefers backend `completedLabel` / `inProgressLabel`,
falling back to `Ran X` / `Running X`.
- `learn_tools` / `load_skills` resolve their inner tool/skill names to
labels (label map → tool output labels via `getToolOutputLabelEntries` →
raw name).
- `code_interpreter` step is now expandable to show the code even while
running.
## How tool labelling flows (BE → FE)
```text
BACKEND
┌───────────────────────────────────────────────────────────────────────────┐
│ Tool providers (per category) → ToolIndexEntry │
│ │
│ DatabaseToolProvider │
│ getCrudToolLabels(operation, object.labelPlural/Singular, i18n, locale) │
│ verb table (Search/Create/Update/Delete…) + translateToolLabel(object) │
│ → { label, inProgressLabel, completedLabel } │
│ │
│ ActionToolProvider │
│ ACTION_TOOL_LABELS[toolId] (msg) → translateToolLabel(…, locale) │
│ → { label, inProgressLabel?, completedLabel? } │
│ │
│ LogicFunctionToolProvider → label = logicFunction.name │
│ toolSetToDescriptors → label = labels[name] ?? humanize(name) │
│ (workflow / view / metadata / dashboard) │
└───────────────────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────────────────┐
│ GraphQL Query getToolIndex : [ToolIndexEntry] │
│ { name, label, inProgressLabel, completedLabel, description, │
│ category, objectName, icon } │
└───────────────────────────────────────────────────────────────────────────┘
│
▼
FRONTEND ─ resolve the right label for the current status
┌───────────────────────────────────────────────────────────────────────────┐
│ useGetToolIndex() → useToolLabelMap() │
│ Map │
└───────────────────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────────────────┐
│ resolveToolDisplayMessage({ input, toolName, isFinished, labelMap, output })│
│ │
│ TOOL_LABEL_RESOLVERS[toolName] ?? defaultResolver │
│ ├─ execute_tool → unwrap { toolName, arguments } then re-resolve │
│ ├─ web_search → "Searching/Searched the web for " │
│ ├─ learn_tools → "Learning/Learned " │
│ ├─ load_skills → "Loading/Loaded " │
│ │ inner names resolved via: labelMap → output labels → raw name │
│ ├─ code_interpreter → model's loadingMessage / completedMessage │
│ └─ default → isFinished │
│ ? completedLabel ?? "Ran