123 lines
7.6 KiB
Markdown
123 lines
7.6 KiB
Markdown
---
|
||
translation:
|
||
sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 0284b215e85366c4, 8534d8dbb4053a70, 2966fac6fe697007]
|
||
tool: 1
|
||
---
|
||
# Ход выполнения {#progress}
|
||
|
||
Инструмент, который работает тридцать секунд и все тридцать секунд молчит, выглядит сломанным.
|
||
|
||
**Уведомления о ходе выполнения** решают эту проблему. Инструмент сообщает, насколько он продвинулся, а клиент решает, что из этого нарисовать: полосу, спиннер, строку в логе.
|
||
|
||
## Отчёт о ходе из инструмента {#report-it-from-the-tool}
|
||
|
||
Примите параметр **`Context`** и вызовите `report_progress`:
|
||
|
||
```python title="server.py" hl_lines="8 11"
|
||
--8<-- "docs_src/progress/tutorial001.py"
|
||
```
|
||
|
||
Три аргумента, и их смысл определяете вы:
|
||
|
||
* `progress`: насколько вы продвинулись. Спецификация требует, чтобы значение **росло** с каждым отчётом: никогда не повторяйте значение и не уменьшайте его.
|
||
* `total`: сколько всего, если это известно. Необязательный.
|
||
* `message`: одна понятная человеку строка об *этом* шаге. Необязательный.
|
||
|
||
`ctx` внедряется по аннотации типов, и модель его не видит: во входной схеме `import_catalog` единственное свойство — `urls`. Этому объекту целиком посвящена страница **[Объект Context](context.md)**; ход выполнения — одна из возможностей, которые он даёт.
|
||
|
||
## Приём на стороне клиента {#listen-for-it-from-the-client}
|
||
|
||
Клиент подписывается **на каждый вызов отдельно**, передавая `progress_callback=` в `call_tool`:
|
||
|
||
```python title="client.py" hl_lines="7 16"
|
||
import anyio
|
||
from mcp import Client
|
||
|
||
from server import mcp
|
||
|
||
|
||
async def show(progress: float, total: float | None, message: str | None) -> None:
|
||
print(f"{message} ({progress}/{total})")
|
||
|
||
|
||
async def main() -> None:
|
||
async with Client(mcp) as client:
|
||
result = await client.call_tool(
|
||
"import_catalog",
|
||
{"urls": ["https://example.com/a.json", "https://example.com/b.json"]},
|
||
progress_callback=show,
|
||
)
|
||
print(result.structured_content)
|
||
|
||
|
||
anyio.run(main)
|
||
```
|
||
|
||
Колбэк — это `async`-функция, принимающая ровно то, что сообщил сервер: `progress`, `total`, `message`.
|
||
|
||
!!! info
|
||
`Client(mcp)` подключается напрямую к объекту сервера, в памяти, — это тот же клиент, на котором
|
||
построена страница **[Тестирование](../get-started/testing.md)**. Параметр `progress_callback` один
|
||
и тот же, какой бы транспорт ни использовал `Client`; а вот *временны́е характеристики*, которые вы
|
||
сейчас увидите, относятся к подключению в памяти. Оно выполняет колбэк прямо на месте, поэтому
|
||
каждый отчёт приходит до того, как `call_tool` вернёт управление. По настоящему транспорту
|
||
уведомления соревнуются с результатом, и медленный колбэк может всё ещё работать после того, как
|
||
`call_tool` уже вернул управление.
|
||
|
||
### Попробуйте сами {#try-it}
|
||
|
||
Положите `client.py` рядом с `server.py` и запустите:
|
||
|
||
```console
|
||
python client.py
|
||
```
|
||
|
||
```text
|
||
Imported https://example.com/a.json (1/2)
|
||
Imported https://example.com/b.json (2/2)
|
||
{'result': 'Imported 2 records.'}
|
||
```
|
||
|
||
Каждый `await ctx.report_progress(...)` на сервере превратился в один вызов `show` на клиенте, в том же порядке, и обе строки напечатались **до** того, как `call_tool` вернул управление. Ход выполнения не упаковывается в результат: он передаётся потоком, пока инструмент ещё работает.
|
||
|
||
!!! warning
|
||
`progress_callback` относится к **вызову**, а не к `Client`. Аргумента конструктора для него нет,
|
||
потому что разным вызовам нужны разные колбэки: один управляет полосой загрузки, следующий —
|
||
строкой в логе.
|
||
|
||
!!! check
|
||
Теперь удалите `progress_callback=show` и запустите снова:
|
||
|
||
```text
|
||
{'result': 'Imported 2 records.'}
|
||
```
|
||
|
||
Ни ошибки, ни предупреждения, тот же результат. `report_progress` **ничего не делает, если
|
||
вызывающая сторона не запросила ход выполнения**, поэтому сообщайте о нём безусловно и никогда
|
||
не гадайте, слушает ли кто-нибудь.
|
||
|
||
## Когда общий объём неизвестен {#when-you-dont-know-the-total}
|
||
|
||
`total` нужен, когда известен знаменатель. Часто это не так: вы вычитываете ленту, идёте по курсору, скачиваете что-то без заголовка длины.
|
||
|
||
Просто не указывайте его:
|
||
|
||
```python title="server.py" hl_lines="20"
|
||
--8<-- "docs_src/progress/tutorial002.py"
|
||
```
|
||
|
||
Колбэк получает `total=None`. Клиент по-прежнему может показывать *активность* («пока импортировано 3...»), но не процент. Не выдумывайте общий объём ради полосы покрасивее.
|
||
|
||
!!! tip
|
||
`progress` не обязан считать что-то конкретное. Байты, строки, страницы: выберите единицу,
|
||
понятную пользователю, и обещайте `total`, только если сможете это обещание сдержать.
|
||
|
||
## Итоги {#recap}
|
||
|
||
* `await ctx.report_progress(progress, total=None, message=None)` из любого инструмента, принимающего `Context`.
|
||
* Клиент передаёт `progress_callback=` в `call_tool`: на каждый вызов, никогда не в `Client`.
|
||
* Колбэк имеет вид `async (progress, total, message) -> None` и срабатывает, пока инструмент ещё работает.
|
||
* Нет колбэка у вызова — `report_progress` ничего не делает. Сообщайте безусловно.
|
||
* Опускайте `total`, когда он неизвестен; колбэк получит `None`.
|
||
|
||
Ход выполнения — это то, что работающий инструмент показывает *пользователю*. Строки, которые он пишет в лог для *вас*, человека, обслуживающего сервер, — это другой канал: **[Логирование](logging.md)**.
|