Merge pull request '#11 Разделение истории сообщений через chat_id' (#12) from #11-chat-id into master

Reviewed-on: #12
This commit is contained in:
Егор Кандрушин 2026-04-19 14:41:48 +00:00
commit aac37db672
3 changed files with 50 additions and 43 deletions

View file

@ -1,9 +1,14 @@
from lambda_agent_api import AgentApi
# Lambda Agent API # Lambda Agent API
WebSocket API SDK для взаимодействия с AI-агентом. 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 клиентом.
## Установка ## Установка
В `master` всегда будет актуальная рабочая версия. В `master` всегда будет актуальная рабочая версия.
```bash ```bash
@ -14,42 +19,10 @@ pip install git+https://git.lambda.coredump.ru/platform/agent_api.git
## Быстрый старт (с использованием AgentApi) ## Быстрый старт (с использованием AgentApi)
```python **Рабочий REPL пример: [tests/manual.py](tests/manual.py).**
import asyncio
from lambda_agent_api.agent_api import AgentApi
from lambda_agent_api.server import MsgEventTextChunk
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 ```python
from lambda_agent_api.agent_api import AgentApi from lambda_agent_api.agent_api import AgentApi

View file

@ -2,6 +2,9 @@ import logging
from typing import Callable, Optional, AsyncIterator from typing import Callable, Optional, AsyncIterator
import aiohttp import aiohttp
import asyncio import asyncio
from urllib.parse import urljoin
from aiohttp import WSServerHandshakeError
from lambda_agent_api.server import * from lambda_agent_api.server import *
from lambda_agent_api.client import * from lambda_agent_api.client import *
@ -25,12 +28,14 @@ class AgentApi:
def __init__( def __init__(
self, self,
agent_id: str, agent_id: str,
url: str, base_url: str,
callback: Optional[Callable[[ServerMessage], None]] = None, 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.id = agent_id # ID агента для словаря
self.url = url self.chat_id = chat_id
self.url = urljoin(base_url, f"/v1/agent_ws/{chat_id}/")
self.callback = callback self.callback = callback
self.on_disconnect = on_disconnect self.on_disconnect = on_disconnect
@ -43,7 +48,11 @@ class AgentApi:
self._listen_task: asyncio.Task | None = None self._listen_task: asyncio.Task | None = None
async def connect(self): async def connect(self):
"""Явное подключение к агенту.""" """Явное подключение к агенту.
:raise AgentBusyException: Чат занят другим клиентом.
:raise AgentException: Непредвиденная ошибка протокола, см. code и details
"""
self._session = aiohttp.ClientSession() self._session = aiohttp.ClientSession()
try: try:
self._ws = await self._session.ws_connect(self.url, heartbeat=30) self._ws = await self._session.ws_connect(self.url, heartbeat=30)
@ -85,6 +94,25 @@ class AgentApi:
await self._session.close() await self._session.close()
raise 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: except Exception as e:
# Обработка всех остальных ошибок (например, aiohttp.ClientConnectionError) # Обработка всех остальных ошибок (например, aiohttp.ClientConnectionError)
if self._ws and not self._ws.closed: if self._ws and not self._ws.closed:

View file

@ -1,15 +1,21 @@
import asyncio import asyncio
import traceback 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 from lambda_agent_api.server import MsgEventTextChunk, MsgEventToolCallChunk, MsgEventToolResult
async def main(): 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/", chat_id=chat_id)
try:
await api.connect() await api.connect()
except AgentBusyException:
print(f"Чат {chat_id} занят другим клиентом")
return
while True: while True:
try: try:
prompt = await asyncio.get_event_loop().run_in_executor(None, input, ">>> ") prompt = await asyncio.get_event_loop().run_in_executor(None, input, ">>> ")