Merge pull request '#11 Разделение истории сообщений через chat_id' (#12) from #11-chat-id into master
Reviewed-on: #12
This commit is contained in:
commit
aac37db672
3 changed files with 50 additions and 43 deletions
45
README.md
45
README.md
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
except AgentBusyException:
|
||||||
|
print(f"Чат {chat_id} занят другим клиентом")
|
||||||
|
return
|
||||||
|
|
||||||
await api.connect()
|
|
||||||
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, ">>> ")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue