# 5. Инструменты — основы

🔵 Общая механика всех инструментов: поля YAML, шаблоны MiniJinja, визуальные
блоки, встроенные движки. Сводную таблицу всех движков см. в
[«Справочник инструментов»](05-tools-reference.md).

## 5.1 Общие поля

Каждый инструмент-YAML разделяет общие верхнеуровневые поля:

| Поле | Описание |
|---|---|
| `kind` | дискриминатор движка (обязательно) |
| `name` | имя инструмента, которое вызывает LLM (по умолчанию — имя файла) |
| `description` | текст, показываемый LLM для function-calling |
| `parameters` | JSON Schema аргументов инструмента |
| `ui` | рендер в виджете: `icon`, `call_template`, `result_template`, `failure_template`, плюс `result_blocks` / `chart` / `table` |
| `visual` | UI-only payload (упорядоченный список блоков) поверх сырого `extracted` — показывается пользователю, никогда не попадает в контекст LLM |
| `warnings` | декларативные правила предупреждений, всплывающие в трейсах |

### `ui`

```yaml
ui:
  icon: 🚗
  call_template: "Ищу {{ input.query }}..."      # до выполнения
  result_template: "{{ extracted.total }} найдено"  # после успеха
  failure_template: "⚠️ {{ error }}"                # после ошибки
```

Шаблоны — [MiniJinja](https://github.com/mitsuhiko/minijinja). `call_template`
видит `input`; `result_template` видит `extracted`. При неудаче
(`success: false`) `result_template` пропускается, а `failure_template` (если
задан) рендерится с `error` (сообщение), `status` (HTTP-статус, если есть),
`extracted` (распарсенное тело ошибки, если JSON), `content` (сырьё) и
`success: false`. Без `failure_template` показывается сырой текст ошибки.

### Визуальные блоки (графики и таблицы)

Инструмент может вернуть **визуальный слой** — упорядоченный список типизированных
блоков (`text`, `chart`, `table`, `file`). Блоки рендерятся в веб-виджете
(Chart.js + таблицы + ссылка на скачивание файла), Android (Vico + Compose) и
Telegram (PNG + таблица + документ).

### Два слоя: данные для LLM и презентация для UI

Результат инструмента служит **двум потребителям**, и их нельзя смешивать:

| Слой | Кому | Что несёт |
|---|---|---|
| `content` (строка) | LLM | компактная сводка результата |
| `visual` (опционально) | UI | полные данные / готовые блоки, минуя LLM |
| `result_blocks` | клиенту | упорядоченные блоки, отправляемые по проводу |

`content` по-прежнему уходит в контекст модели; `visual` — только в UI и
никогда в LLM. Клиент вычисляет `result_blocks` либо из `visual`, либо из
`content` + `ui`-шаблонов.

### Способы задать блоки

1. **`ui.result_blocks` / `ui.chart` / `ui.table`** — декларативные шаблоны
   поверх `extracted`:

   ```yaml
   ui:
     result_blocks:
       - type: text
         markdown: "Найдено **{{ extracted.count }}**"
       - type: table
         columns: [{ key: model, title: "Модель" }]
         rows: "{{ extracted.items | tojson }}"
   ```

   `ui.chart:` и `ui.table:` — сокращения для одного блока; `result_template:` —
   один `text`-блок.

2. **`visual`** — верхнеуровневое поле, рендерится поверх **сырого** `extracted`
   (не `content`), уходит напрямую в UI, а `content` остаётся компактной
   сводкой:

   ```yaml
   output:
     count: "{{ extracted.items | length }}"   # компактная сводка → LLM
   visual:
     - type: table
       rows: "{{ extracted.items | tojson }}"  # полные данные → только UI
   ```

3. **Специализированные движки** — [`chart_tool` / `table_tool`](05-tools-state.md#512-chart_tool-и-table_tool) /
   [`visualize_tool`](05-tools-state.md#513-visualize_tool) сами заполняют
   `content` (сводка) и `visual` (полный блок). Скриптовые инструменты задают
   `visual` ключом в `run(args)` (см. [07-scripts](07-scripts.md)).

4. **Fenced-блоки в ответе модели** — ```` ```chart {…} ```` и ```` ```table ````
   в markdown-ответе парсятся клиентом.

### Формат блоков

Блок `chart` несёт `spec`:

```json
{
  "type": "bar",            // bar | line | area | pie | scatter
  "title": "Цены",
  "labels": ["Q1", "Q2", "Q3", "Q4"],
  "series": [ { "name": "Цена", "values": [10, 20, 15, 30] } ],
  "options": { "stacked": false }
}
```

Блок `table` несёт `columns` + `rows` (массив объектов), опционально `title`,
`page_info` и `options` (`sortable`/`searchable`/`paged`):

```json
{
  "type": "table",
  "columns": [
    { "key": "model", "title": "Модель", "align": "left" },
    { "key": "price", "title": "Цена",  "align": "right" }
  ],
  "rows": [ { "model": "A", "price": 100 } ],
  "page_info": { "returned": 50, "total": 500, "has_more": true },
  "options": { "sortable": true, "searchable": true }
}
```

Блок `file` несёт файл для скачивания — `name` (имя файла), `mime` и `data`
(base64-содержимое). Создаётся движком [`file_tool`](05-tools-compute.md#520-file_tool):

```json
{
  "type": "file",
  "name": "report.html",
  "mime": "text/html",
  "data": "PGgxPkhpPC9oMT4="
}
```

### Ленивая пагинация (`continuation`)

Блок `table` может нести `page_info` и `continuation` — клиент запрашивает
следующую страницу через `POST /v1/continuations` с id, сервер повторно вызывает
инструмент с сохранёнными аргументами (без LLM-раунда). Web — infinite scroll,
Telegram — кнопка «Load more». Первая страница рендерится всегда; мёртвый id
просто отключает подгрузку.

### `warnings`

```yaml
warnings:
  - contains: '"total_server": 0'   # подстрока в сериализованном выводе
    message: "Нет результатов"
  - failed: true                    # срабатывает при success=false
    message: "Инструмент упал"
```

Сработавшие предупреждения видны в трейсах (`trace_sessions`, `trace_search`).

---

## 5.2 Шаблоны и переменные

Инструменты используют **MiniJinja** (подмножество Jinja2) в полях-шаблонах:
`url`, заголовки, тело запроса, `call_template` / `result_template` /
`failure_template`, `output`, указатели в `extract`, `summary`, `computed`.

### Контекстные переменные

| Переменная | Где доступна | Значение |
|---|---|---|
| `input` | `fetch` (url/headers/body), `extract` (указатели), `call_template`, `output` | аргументы вызова инструмента |
| `extracted` | `result_template`, `output`, `visual` | извлечённые поля |
| `item` | `json_tool.computed` | текущий элемент результата |
| `count` | `summary` (chart/table/visualize) | число элементов |
| `error` / `status` / `content` / `extracted` | `failure_template` | детали ошибки (сообщение, HTTP-статус, сырое тело, распарсенное тело) |
| `env_var('NAME')` | везде (функция) | значение переменной окружения процесса |

### Встроенные фильтры MiniJinja

| Фильтр | Описание |
|---|---|
| `slugify` | в нижний регистр, неалфавитно-цифровые → `_` |
| `transliterate` | кириллица → латиница (для slug) |
| `urlencode` | percent-encoding для URL |
| `tojson` | сериализация значения в корректный JSON (`true`/`false`/`null`, кавычки) |
| `strip_html` | убрать HTML/XML-теги |
| `fromunixtime` | unix-время → строка даты |
| кастомные из `filters:` | таблица ключ→значение (lookup) с `case_insensitive` и `default` |

Пост-рендер: MiniJinja выводит `null`/булевы как `None`/`True`/`False` — движок
преобразует их обратно в корректный JSON `null`/`true`/`false`.

### Переменные в инструкциях агента

В `instructions:` агента доступны подстановки, заполняемые ядром при создании
сессии (пишутся заглавными в двойных подчёркиваниях):

| Плейсхолдер | Значение |
|---|---|
| `__DATE__` / `__WEEKDAY__` / `__TIME__` | текущая дата / день недели / время (локальное сервера) |
| `__GEOIP__` | город пользователя (резолв по IP; `unknown`, если не определён) |
| `__COUNTRY__` / `__COUNTRY_CODE__` | страна / код страны |
| `__USER_ID__` | id пользователя |
| `__ADMIN_TEMPLATES__` | скоуп видимости админа (`*` или список шаблонов) |

Пример из `drom-agent`: `User's city: __GEOIP__. If user doesn't specify region, use this city.`

---

## 5.3 Встроенные инструменты

Реализация уже в ядре, YAML не нужен — перечисли имя в `tools:` в `agent.yaml`.

| Имя | Назначение | Ключевые параметры |
|---|---|---|
| `http_fetch` | скачать URL | `url`, `max_chars` (по умолчанию 8000, 0=всё), `start_char` |
| `fs_read` | прочитать файл | `path`, `max_chars`, `start_char` |
| `job_done` | сигнал завершения | `reason` (или схема `output_schema`) |
