адаптация для установки пакетом

This commit is contained in:
Егор Кандрушин 2026-04-01 23:30:00 +03:00
parent a5ef5abac7
commit b34cbaf677
9 changed files with 260 additions and 258 deletions

243
README.md
View file

@ -1,7 +1,238 @@
## Contributing
# Lambda Agent API
- В качестве таск-трекера - раздел "Задачи" (Issues).
- В названии коммита обязательно указывать ID задачи. Пример: "#1 Описание коммита"
- При закрытии задачи в комментарии делать ссылку на PR.
- Одна задача, одна ветка, один PR.
- Формат названия ветки: #<ID задачи>-<короткое описание>. Пример: "#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

View file

@ -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

View file

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 154 KiB

Before After
Before After

View file

@ -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",

View file

@ -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__)

View file

@ -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"]

View file

@ -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"