From 649834beaeffbe781ad55e9eb07a832a2eb9dd43 Mon Sep 17 00:00:00 2001 From: MrKan Date: Sun, 19 Apr 2026 11:58:45 +0300 Subject: [PATCH 1/4] =?UTF-8?q?=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80=D0=B6?= =?UTF-8?q?=D0=BA=D0=B0=20chat=5Fid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lambda_agent_api/agent_api.py | 7 +++++-- tests/manual.py | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lambda_agent_api/agent_api.py b/lambda_agent_api/agent_api.py index d130bcf..e2e5346 100644 --- a/lambda_agent_api/agent_api.py +++ b/lambda_agent_api/agent_api.py @@ -2,6 +2,7 @@ import logging from typing import Callable, Optional, AsyncIterator import aiohttp import asyncio +from urllib.parse import urljoin from lambda_agent_api.server import * from lambda_agent_api.client import * @@ -27,10 +28,12 @@ class AgentApi: agent_id: str, url: str, callback: Optional[Callable[[ServerMessage], None]] = None, - on_disconnect: Optional[Callable[['AgentApi'], None]] = None + on_disconnect: Optional[Callable[['AgentApi'], None]] = None, + chat_id: int = 0 # значение по умолчанию для обратной совместимости ): self.id = agent_id # ID агента для словаря - self.url = url + self.chat_id = chat_id + self.url = urljoin(url, f"{chat_id}/") self.callback = callback self.on_disconnect = on_disconnect diff --git a/tests/manual.py b/tests/manual.py index a39802b..b801a2d 100644 --- a/tests/manual.py +++ b/tests/manual.py @@ -7,7 +7,8 @@ from lambda_agent_api.server import MsgEventTextChunk, MsgEventToolCallChunk, Ms async def main(): - api = AgentApi("agent-1", "ws://localhost:8000/agent_ws/") + chat_id = input("Chat id: ") or 0 + api = AgentApi("agent-1", "ws://localhost:8000/agent_ws/", chat_id=chat_id) await api.connect() while True: From ee98eb09d91f4a6659988ef3613ec0ae48ca2747 Mon Sep 17 00:00:00 2001 From: MrKan Date: Sun, 19 Apr 2026 14:41:48 +0300 Subject: [PATCH 2/4] =?UTF-8?q?=D0=BA=D0=BE=D1=80=D1=80=D0=B5=D0=BA=D1=82?= =?UTF-8?q?=D0=BD=D0=B0=D1=8F=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B8,=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=B3=D0=B4=D0=B0=20=D1=87=D0=B0=D1=82=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=BD=D1=8F=D1=82=20=D0=B4=D1=80=D1=83=D0=B3=D0=B8=D0=BC=20?= =?UTF-8?q?=D0=BA=D0=BB=D0=B8=D0=B5=D0=BD=D1=82=D0=BE=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lambda_agent_api/agent_api.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/lambda_agent_api/agent_api.py b/lambda_agent_api/agent_api.py index e2e5346..cc25416 100644 --- a/lambda_agent_api/agent_api.py +++ b/lambda_agent_api/agent_api.py @@ -4,6 +4,8 @@ import aiohttp import asyncio from urllib.parse import urljoin +from aiohttp import WSServerHandshakeError + from lambda_agent_api.server import * from lambda_agent_api.client import * @@ -46,7 +48,11 @@ class AgentApi: self._listen_task: asyncio.Task | None = None async def connect(self): - """Явное подключение к агенту.""" + """Явное подключение к агенту. + + :raise AgentBusyException: Чат занят другим клиентом. + :raise AgentException: Непредвиденная ошибка протокола, см. code и details + """ self._session = aiohttp.ClientSession() try: self._ws = await self._session.ws_connect(self.url, heartbeat=30) @@ -88,6 +94,25 @@ class AgentApi: await self._session.close() raise + except WSServerHandshakeError as e: # если при открытии подключения сервер вернул какую-то ошибку + if self._session and not self._session.closed: + await self._session.close() + + # во-первых, aiohttp зачем-то приводит WS коды ошибок к HTTP. + # во-вторых, делает он это некорректно. Любой неизвестный код WS ошибки становится 403 HTTP + # в-третьих, он не передает оригинальное сообщение об ошибке. + # Т. е. какое бы сообщение сервер не отправлял, тут всегда будет "Invalid response status" + # см. site-packages\aiohttp\client.py, line 1104, in _ws_connect + # итого понять реальную причину ошибки почти невозможно. Нужно менять библиотеку + # сейчас сервер специально кидает только ошибку с WS кодом 1008 Policy Violation, когда внутри ловит ChatBusyError + # поэтому скорее всего, если мы получили WSServerHandshakeError с 403, то это внутренний ChatBusyError + if e.status != 403: + # обрабатываем как обычную ошибку WS по примеру except блока ниже + raise AgentException(code="CONNECTION_ERROR", + details=f"Failed to connect agent {self.id}: {e}") from e + + raise AgentBusyException(f"Chat {self.chat_id} is already in use by other client") + except Exception as e: # Обработка всех остальных ошибок (например, aiohttp.ClientConnectionError) if self._ws and not self._ws.closed: From 234050df9faf6eb427b15527e0c5cd4710366a51 Mon Sep 17 00:00:00 2001 From: MrKan Date: Sun, 19 Apr 2026 15:12:07 +0300 Subject: [PATCH 3/4] =?UTF-8?q?=D0=B0=D0=BA=D1=82=D1=83=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D1=82=D0=B0=D1=86=D0=B8=D0=B8=20=D0=B8=20manual=20?= =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 41 +++++++---------------------------------- tests/manual.py | 9 +++++++-- 2 files changed, 14 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 496c876..470317e 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,11 @@ from lambda_agent_api import AgentApi WebSocket API SDK для взаимодействия с AI-агентом. +## Release Notes +# v1.1 +- Добавлен параметр `chat_id` в конструктор `AgentAPI`. Нужен для разделения истории сообщений по чатам/веткам. +- `AgentAPI.connect()` вызывает `AgentBusyException`, если выбранный чат уже занят другим API клиентом. + ## Установка В `master` всегда будет актуальная рабочая версия. ```bash @@ -14,42 +19,10 @@ pip install git+https://git.lambda.coredump.ru/platform/agent_api.git ## Быстрый старт (с использованием AgentApi) -```python -import asyncio - -from lambda_agent_api.agent_api import AgentApi -from lambda_agent_api.server import MsgEventTextChunk +**Рабочий REPL пример: [tests/manual.py](tests/manual.py).** -async def main(): - api = AgentApi("agent-1", "ws://localhost:8000/ws") - - await api.connect() - try: - response = await api.send_message("Привет, агент!") - - async for chunk in response: - if isinstance(chunk, MsgEventTextChunk): - print(chunk.text, end="", flush=True) - elif isinstance(chunk, MsgEventToolCallChunk): - print(f"Tool call started: {chunk.tool_name}") - elif isinstance(chunk, MsgEventToolResult): - print(f"Tool result: {chunk.result}") - elif isinstance(chunk, MsgEventCustomUpdate): - print(f"Progress update: {chunk.payload}") - elif isinstance(chunk, MsgEventEnd): - print(f"Generation ended, tokens used: {chunk.tokens_used}") - - finally: - await api.close() - - -asyncio.run(main()) -``` - -> `AgentApi.send_message()` возвращает стриминг-итерируемый объект, который может выдавать не только текстовые чанки, но и события инструментов (`MsgEventToolCallChunk`, `MsgEventToolResult`, `MsgEventCustomUpdate`) и финальный `MsgEventEnd`. - -## Предполагаемое использование +## Предполагаемое управление подключениями ```python from lambda_agent_api.agent_api import AgentApi diff --git a/tests/manual.py b/tests/manual.py index b801a2d..0ba6e0a 100644 --- a/tests/manual.py +++ b/tests/manual.py @@ -1,7 +1,7 @@ import asyncio import traceback -from lambda_agent_api.agent_api import AgentApi +from lambda_agent_api.agent_api import AgentApi, AgentBusyException from lambda_agent_api.server import MsgEventTextChunk, MsgEventToolCallChunk, MsgEventToolResult @@ -10,7 +10,12 @@ async def main(): chat_id = input("Chat id: ") or 0 api = AgentApi("agent-1", "ws://localhost:8000/agent_ws/", chat_id=chat_id) - await api.connect() + try: + await api.connect() + except AgentBusyException: + print(f"Чат {chat_id} занят другим клиентом") + return + while True: try: prompt = await asyncio.get_event_loop().run_in_executor(None, input, ">>> ") From 0c9906ecb4a01a67b704676a10fb7d5d65173291 Mon Sep 17 00:00:00 2001 From: MrKan Date: Sun, 19 Apr 2026 15:24:14 +0300 Subject: [PATCH 4/4] =?UTF-8?q?AgentAPI=20=D0=BF=D1=80=D0=B8=D0=BD=D0=B8?= =?UTF-8?q?=D0=BC=D0=B0=D0=B5=D1=82=20base=5Furl=20(=D0=BF=D0=BE=20=D0=B8?= =?UTF-8?q?=D0=B4=D0=B5=D0=B5=20ws://host:port)=20=D0=B8=20=D1=81=D0=B0?= =?UTF-8?q?=D0=BC=20=D0=B4=D0=BE=D0=BF=D0=B8=D1=81=D1=8B=D0=B2=D0=B0=D0=B5?= =?UTF-8?q?=D1=82=20=D0=BD=D1=83=D0=B6=D0=BD=D1=8B=D0=B9=20=D1=8D=D0=BD?= =?UTF-8?q?=D0=B4=D0=BF=D0=BE=D0=B8=D0=BD=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- lambda_agent_api/agent_api.py | 4 ++-- tests/manual.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 470317e..23841e6 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -from lambda_agent_api import AgentApi - # Lambda Agent API WebSocket API SDK для взаимодействия с AI-агентом. ## Release Notes # v1.1 +- **CRITICAL**: `AgentAPI` вместо `url` принимает `base_url` и сам дописывает нужный эндпоинт. +Раньше: `AgentAPI(url="ws://localhost:8000/agent_ws/")`. Сейчас: `AgentAPI(base_url="ws://localhost:8000/")` - Добавлен параметр `chat_id` в конструктор `AgentAPI`. Нужен для разделения истории сообщений по чатам/веткам. - `AgentAPI.connect()` вызывает `AgentBusyException`, если выбранный чат уже занят другим API клиентом. diff --git a/lambda_agent_api/agent_api.py b/lambda_agent_api/agent_api.py index cc25416..e22a0b8 100644 --- a/lambda_agent_api/agent_api.py +++ b/lambda_agent_api/agent_api.py @@ -28,14 +28,14 @@ class AgentApi: def __init__( self, agent_id: str, - url: str, + base_url: str, callback: Optional[Callable[[ServerMessage], None]] = None, on_disconnect: Optional[Callable[['AgentApi'], None]] = None, chat_id: int = 0 # значение по умолчанию для обратной совместимости ): self.id = agent_id # ID агента для словаря self.chat_id = chat_id - self.url = urljoin(url, f"{chat_id}/") + self.url = urljoin(base_url, f"/v1/agent_ws/{chat_id}/") self.callback = callback self.on_disconnect = on_disconnect diff --git a/tests/manual.py b/tests/manual.py index 0ba6e0a..10f3005 100644 --- a/tests/manual.py +++ b/tests/manual.py @@ -8,7 +8,7 @@ from lambda_agent_api.server import MsgEventTextChunk, MsgEventToolCallChunk, Ms async def main(): chat_id = input("Chat id: ") or 0 - api = AgentApi("agent-1", "ws://localhost:8000/agent_ws/", chat_id=chat_id) + api = AgentApi("agent-1", "ws://localhost:8000/", chat_id=chat_id) try: await api.connect()