From b34cbaf677ad43d6a59203d05ffa8cdffed9a3e7 Mon Sep 17 00:00:00 2001 From: MrKan Date: Wed, 1 Apr 2026 23:30:00 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B0=D0=B4=D0=B0=D0=BF=D1=82=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=B4=D0=BB=D1=8F=20=D1=83=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=BA=D0=B8=20=D0=BF=D0=B0=D0=BA=D0=B5=D1=82?= =?UTF-8?q?=D0=BE=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/.python-version => .python-version | 0 README.md | 243 ++++++++++++++++++++++++- api/README.md | 233 ------------------------ {api/docs => docs}/schema.png | Bin {api => lambda_agent_api}/__init__.py | 4 +- {api => lambda_agent_api}/agent_api.py | 3 +- {api => lambda_agent_api}/models.py | 0 api/pyproject.toml => pyproject.toml | 5 +- api/uv.lock => uv.lock | 30 +-- 9 files changed, 260 insertions(+), 258 deletions(-) rename api/.python-version => .python-version (100%) delete mode 100644 api/README.md rename {api/docs => docs}/schema.png (100%) rename {api => lambda_agent_api}/__init__.py (77%) rename {api => lambda_agent_api}/agent_api.py (99%) rename {api => lambda_agent_api}/models.py (100%) rename api/pyproject.toml => pyproject.toml (75%) rename api/uv.lock => uv.lock (99%) diff --git a/api/.python-version b/.python-version similarity index 100% rename from api/.python-version rename to .python-version diff --git a/README.md b/README.md index 8032115..72a44cf 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,238 @@ -## Contributing +# Lambda Agent API -- В качестве таск-трекера - раздел "Задачи" (Issues). -- В названии коммита обязательно указывать ID задачи. Пример: "#1 Описание коммита" -- При закрытии задачи в комментарии делать ссылку на PR. -- Одна задача, одна ветка, один PR. -- Формат названия ветки: #-<короткое описание>. Пример: "#1-task-description". +WebSocket API SDK для взаимодействия с AI-агентом. + +## Установка + +```bash +pip install . +``` + +Требуется Python 3.14+. + +## Быстрый старт (с использованием AgentApi) + +```python +import asyncio +from lambda_agent_api.agent_api import AgentApi, OM + + +def my_callback(message): + if isinstance(message, OM.Error): + print(f"\n[Ошибка: {message.code}] {message.details}") + elif isinstance(message, OM.Status): + print("✓ Agent status update") + elif isinstance(message, OM.GracefulDisconnect): + print("✓ Agent gracefully requested disconnect") + + +async def main(): + api = AgentApi("agent-1", "ws://localhost:8000/ws", callback=my_callback) + + await api.connect() + try: + response = await api.send_message("Привет, агент!") + + async for chunk in response: + if isinstance(chunk, OM.EventTextChunk): + print(chunk.text, end="", flush=True) + + # После окончания Generation возможно получить EventEnd в очереди и сохранить tokens + # (в current implementation: `response` - генератор, для токенов смотрите `EventEnd` в callback) + + finally: + await api.close() + + +asyncio.run(main()) +``` + +## AgentApi - Асинхронный Python клиент + +Новая библиотека `AgentApi` предоставляет типизированный асинхронный клиент для WebSocket взаимодействия с агентом. + +### Характеристики + +- ✅ **Асинхронный клиент** на основе `aiohttp` +- ✅ **Явное подключение/закрытие** через `connect()`/`close()` +- ✅ **Защита от параллельных запросов** через `AgentBusyException` +- ✅ **ResponseIterator** для асинхронной итерации по чанкам ответа +- ✅ **Callback** для обработки событий вне генерации ответа (`Status`, `Error`, `GracefulDisconnect`) +- ✅ **Типизированные сообщения** через Pydantic с дискриминированными объединениями +- ✅ **Обработка ошибок** с кастомным исключением `AgentException` +- ✅ **Логирование** на всех уровнях операций +- ✅ **Полная документация** всех методов + +### Использование + +```python +from lambda_agent_api.agent_api import AgentApi, OM + +api = AgentApi("agent-1", "ws://localhost:8000/ws", callback=my_callback) + +await api.connect() +try: + response = await api.send_message("Your question here") + + async for chunk in response: + if isinstance(chunk, OM.EventTextChunk): + print(chunk.text, end="", flush=True) + + print("\nDone!") +finally: + await api.close() +``` + +# Обработка ошибок + +- `AgentBusyException` возникает, если отправить `send_message` пока предыдущий запрос ещё в процессе. +- `AgentException` возникает, если агент возвращает `ERROR` или есть проблемы с подключением. +- `on_disconnect` callback вызывается один раз при закрытии/разрыве соединения. + +Callback функция для обработки событий вне генерации: + +```python +def my_callback(message): + if isinstance(message, OM.Status): + print("Agent status update") + elif isinstance(message, OM.Error): + print(f"Agent error: {message.code} - {message.details}") + elif isinstance(message, OM.GracefulDisconnect): + print("Agent disconnecting gracefully") +``` + +## Классический подход (низкоуровневый) + +```python +import asyncio +import websockets +from lambda_agent_api.models import ServerMessage, OM + + +async def main(): + uri = "ws://localhost:8000/ws" + + async with websockets.connect(uri) as ws: + # 1. Ждём STATUS - подтверждение готовности + status = await ws.recv() + print(f"Connected: {status}") + + # 2. Отправляем сообщение + await ws.send('{"type": "USER_MESSAGE", "text": "Привет!"}') + + # 3. Читаем ответ в виде потока событий + while True: + msg = await ws.recv() + data = ServerMessage.model_validate_json(msg) + + match data: + case OM.AgentEvent(subtype=OM.AgentEventType.TEXT_CHUNK): + print(data.text, end="", flush=True) + case OM.EventEnd(): + print(f"\n[Завершено, использовано токенов: {data.tokens_used}]") + break + case OM.Error(): + print(f"\n[Ошибка: {data.code}] {data.details}") + break + + +asyncio.run(main()) +``` + +## Протокол + +### Клиент → Сервер + +#### USER_MESSAGE + +Полное сообщение от пользователя. + +```json +{ + "type": "USER_MESSAGE", + "text": "Текст сообщения" +} +``` + +| Поле | Тип | Описание | +|------|-------|-------------------| +| type | string | Всегда `USER_MESSAGE` | +| text | string | Текст сообщения | + +### Сервер → Клиент + +#### STATUS + +Отправляется сервером при открытии соединения с клиентом. Будет дополнен информацией о готовности агента принимать сообщения. + +```json +{ + "type": "STATUS" +} +``` + +#### AGENT_EVENT + +Базовый класс для ивентов, которые стримит агент во время генерации ответа. Конкретный класс для ивента определяется по `subtype`. + +##### TEXT_CHUNK + +Чанк текста ответа агента. + +```json +{ + "type": "AGENT_EVENT", + "subtype": "TEXT_CHUNK", + "text": "Фрагмент текста" +} +``` + +##### END + +Агент закончил генерацию ответа. + +```json +{ + "type": "AGENT_EVENT", + "subtype": "END", + "tokens_used": 42 +} +``` + +| Поле | Тип | Описание | +|-------------|--------|-----------------------| +| tokens_used | int | Количество использованных токенов | + +#### ERROR + +Неопределенная ошибка в работе агента. + +```json +{ + "type": "ERROR", + "code": "error_code", + "details": "Описание ошибки" +} +``` + +| Поле | Тип | Описание | +|---------|-------|----------------| +| code | string | Код ошибки | +| details | string | Подробности | + +#### GRACEFUL_DISCONNECT + +Отправляется перед завершением работы контейнера с агентом. Например, при долгом бездействии. Нужно, чтобы отделять обрыв соединения из-за ошибки с необходимостью повторного подключения. Приход этого сообщения означает, что агент осознанно завершает работу с клиентом по какой-то причине. Для дальнейшего взаимодействия нужно снова обратиться к мастеру. + +```json +{ + "type": "GRACEFUL_DISCONNECT" +} +``` + +![Схема взаимодействия](docs/schema.png) + +## Зависимости + +- Python 3.14+ +- pydantic >= 2.12.5 diff --git a/api/README.md b/api/README.md deleted file mode 100644 index 4796b1d..0000000 --- a/api/README.md +++ /dev/null @@ -1,233 +0,0 @@ -# Lambda Agent API - -WebSocket API SDK для взаимодействия с AI-агентом. - -## Установка - -```bash -pip install . -``` - -Требуется Python 3.14+. - -## Быстрый старт (с использованием AgentApi) - -```python -import asyncio -from agent_api import AgentApi, OM - -def my_callback(message): - if isinstance(message, OM.Error): - print(f"\n[Ошибка: {message.code}] {message.details}") - elif isinstance(message, OM.Status): - print("✓ Agent status update") - elif isinstance(message, OM.GracefulDisconnect): - print("✓ Agent gracefully requested disconnect") - -async def main(): - api = AgentApi("agent-1", "ws://localhost:8000/ws", callback=my_callback) - - await api.connect() - try: - response = await api.send_message("Привет, агент!") - - async for chunk in response: - if isinstance(chunk, OM.EventTextChunk): - print(chunk.text, end="", flush=True) - - # После окончания Generation возможно получить EventEnd в очереди и сохранить tokens - # (в current implementation: `response` - генератор, для токенов смотрите `EventEnd` в callback) - - finally: - await api.close() - -asyncio.run(main()) -``` - -## AgentApi - Асинхронный Python клиент - -Новая библиотека `AgentApi` предоставляет типизированный асинхронный клиент для WebSocket взаимодействия с агентом. - -### Характеристики - -- ✅ **Асинхронный клиент** на основе `aiohttp` -- ✅ **Явное подключение/закрытие** через `connect()`/`close()` -- ✅ **Защита от параллельных запросов** через `AgentBusyException` -- ✅ **ResponseIterator** для асинхронной итерации по чанкам ответа -- ✅ **Callback** для обработки событий вне генерации ответа (`Status`, `Error`, `GracefulDisconnect`) -- ✅ **Типизированные сообщения** через Pydantic с дискриминированными объединениями -- ✅ **Обработка ошибок** с кастомным исключением `AgentException` -- ✅ **Логирование** на всех уровнях операций -- ✅ **Полная документация** всех методов - -### Использование - -```python -from agent_api import AgentApi, OM - -api = AgentApi("agent-1", "ws://localhost:8000/ws", callback=my_callback) - -await api.connect() -try: - response = await api.send_message("Your question here") - - async for chunk in response: - if isinstance(chunk, OM.EventTextChunk): - print(chunk.text, end="", flush=True) - - print("\nDone!") -finally: - await api.close() -``` - -# Обработка ошибок - -- `AgentBusyException` возникает, если отправить `send_message` пока предыдущий запрос ещё в процессе. -- `AgentException` возникает, если агент возвращает `ERROR` или есть проблемы с подключением. -- `on_disconnect` callback вызывается один раз при закрытии/разрыве соединения. - -Callback функция для обработки событий вне генерации: - -```python -def my_callback(message): - if isinstance(message, OM.Status): - print("Agent status update") - elif isinstance(message, OM.Error): - print(f"Agent error: {message.code} - {message.details}") - elif isinstance(message, OM.GracefulDisconnect): - print("Agent disconnecting gracefully") -``` - -## Классический подход (низкоуровневый) - -```python -import asyncio -import websockets -from models import ServerMessage, OM - -async def main(): - uri = "ws://localhost:8000/ws" - - async with websockets.connect(uri) as ws: - # 1. Ждём STATUS - подтверждение готовности - status = await ws.recv() - print(f"Connected: {status}") - - # 2. Отправляем сообщение - await ws.send('{"type": "USER_MESSAGE", "text": "Привет!"}') - - # 3. Читаем ответ в виде потока событий - while True: - msg = await ws.recv() - data = ServerMessage.model_validate_json(msg) - - match data: - case OM.AgentEvent(subtype=OM.AgentEventType.TEXT_CHUNK): - print(data.text, end="", flush=True) - case OM.EventEnd(): - print(f"\n[Завершено, использовано токенов: {data.tokens_used}]") - break - case OM.Error(): - print(f"\n[Ошибка: {data.code}] {data.details}") - break - -asyncio.run(main()) -``` - -## Протокол - -### Клиент → Сервер - -#### USER_MESSAGE - -Полное сообщение от пользователя. - -```json -{ - "type": "USER_MESSAGE", - "text": "Текст сообщения" -} -``` - -| Поле | Тип | Описание | -|------|-------|-------------------| -| type | string | Всегда `USER_MESSAGE` | -| text | string | Текст сообщения | - -### Сервер → Клиент - -#### STATUS - -Отправляется сервером при открытии соединения с клиентом. Будет дополнен информацией о готовности агента принимать сообщения. - -```json -{ - "type": "STATUS" -} -``` - -#### AGENT_EVENT - -Базовый класс для ивентов, которые стримит агент во время генерации ответа. Конкретный класс для ивента определяется по `subtype`. - -##### TEXT_CHUNK - -Чанк текста ответа агента. - -```json -{ - "type": "AGENT_EVENT", - "subtype": "TEXT_CHUNK", - "text": "Фрагмент текста" -} -``` - -##### END - -Агент закончил генерацию ответа. - -```json -{ - "type": "AGENT_EVENT", - "subtype": "END", - "tokens_used": 42 -} -``` - -| Поле | Тип | Описание | -|-------------|--------|-----------------------| -| tokens_used | int | Количество использованных токенов | - -#### ERROR - -Неопределенная ошибка в работе агента. - -```json -{ - "type": "ERROR", - "code": "error_code", - "details": "Описание ошибки" -} -``` - -| Поле | Тип | Описание | -|---------|-------|----------------| -| code | string | Код ошибки | -| details | string | Подробности | - -#### GRACEFUL_DISCONNECT - -Отправляется перед завершением работы контейнера с агентом. Например, при долгом бездействии. Нужно, чтобы отделять обрыв соединения из-за ошибки с необходимостью повторного подключения. Приход этого сообщения означает, что агент осознанно завершает работу с клиентом по какой-то причине. Для дальнейшего взаимодействия нужно снова обратиться к мастеру. - -```json -{ - "type": "GRACEFUL_DISCONNECT" -} -``` - -![Схема взаимодействия](docs/schema.png) - -## Зависимости - -- Python 3.14+ -- pydantic >= 2.12.5 diff --git a/api/docs/schema.png b/docs/schema.png similarity index 100% rename from api/docs/schema.png rename to docs/schema.png diff --git a/api/__init__.py b/lambda_agent_api/__init__.py similarity index 77% rename from api/__init__.py rename to lambda_agent_api/__init__.py index a449215..fadda1f 100644 --- a/api/__init__.py +++ b/lambda_agent_api/__init__.py @@ -7,8 +7,8 @@ - IM, OM, IncomingMessage, OutgoingMessage: Pydantic модели контракта """ -from .agent_api import AgentApi, AgentException -from .models import CM, SM, ClientMessage, ServerMessage +from lambda_agent_api.agent_api import AgentApi, AgentException +from lambda_agent_api.models import CM, SM, ClientMessage, ServerMessage __all__ = [ "AgentApi", diff --git a/api/agent_api.py b/lambda_agent_api/agent_api.py similarity index 99% rename from api/agent_api.py rename to lambda_agent_api/agent_api.py index 21a30d2..c38e01e 100644 --- a/api/agent_api.py +++ b/lambda_agent_api/agent_api.py @@ -2,7 +2,8 @@ import logging from typing import Callable, Optional, AsyncIterator import aiohttp import asyncio -from models import CM, SM, ClientMessage, ServerMessage + +from lambda_agent_api.models import CM, SM, ClientMessage, ServerMessage logger = logging.getLogger(__name__) diff --git a/api/models.py b/lambda_agent_api/models.py similarity index 100% rename from api/models.py rename to lambda_agent_api/models.py diff --git a/api/pyproject.toml b/pyproject.toml similarity index 75% rename from api/pyproject.toml rename to pyproject.toml index 3b284b5..e1f135e 100644 --- a/api/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "api" +name = "lambda_agent_api" version = "0.1.0" description = "WebSocket API SDK для взаимодействия с AI-агентом" readme = "README.md" @@ -8,3 +8,6 @@ dependencies = [ "aiohttp>=3.13.4", "pydantic>=2.12.5", ] + +[tool.setuptools] +packages = ["lambda_agent_api"] \ No newline at end of file diff --git a/api/uv.lock b/uv.lock similarity index 99% rename from api/uv.lock rename to uv.lock index 4e51458..baed3b5 100644 --- a/api/uv.lock +++ b/uv.lock @@ -83,21 +83,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] -[[package]] -name = "api" -version = "0.1.0" -source = { virtual = "." } -dependencies = [ - { name = "aiohttp" }, - { name = "pydantic" }, -] - -[package.metadata] -requires-dist = [ - { name = "aiohttp", specifier = ">=3.13.4" }, - { name = "pydantic", specifier = ">=2.12.5" }, -] - [[package]] name = "attrs" version = "26.1.0" @@ -157,6 +142,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] +[[package]] +name = "lambda-agent-api" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "aiohttp" }, + { name = "pydantic" }, +] + +[package.metadata] +requires-dist = [ + { name = "aiohttp", specifier = ">=3.13.4" }, + { name = "pydantic", specifier = ">=2.12.5" }, +] + [[package]] name = "multidict" version = "6.7.1"