agentum-cloud-sdk 0.1.62__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentum_cloud/__init__.py +51 -0
- agentum_cloud/client.py +373 -0
- agentum_cloud/errors.py +21 -0
- agentum_cloud/models.py +301 -0
- agentum_cloud/py.typed +0 -0
- agentum_cloud_sdk-0.1.62.dist-info/METADATA +104 -0
- agentum_cloud_sdk-0.1.62.dist-info/RECORD +9 -0
- agentum_cloud_sdk-0.1.62.dist-info/WHEEL +4 -0
- agentum_cloud_sdk-0.1.62.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""agentum-cloud SDK — типизированный async-клиент над REST API ``/v1``.
|
|
2
|
+
|
|
3
|
+
Один контракт для всех клиентов (бот, MCP, сторонние проекты). Пример::
|
|
4
|
+
|
|
5
|
+
async with AgentumClient(base_url="http://host:8083", api_key="...") as cloud:
|
|
6
|
+
obj = await cloud.upload("doc.pdf", data, owner_id="tg:42")
|
|
7
|
+
ready = await cloud.wait_until_ready(obj.id, owner_id="tg:42")
|
|
8
|
+
answer = await cloud.ask("когда отпуск?", owner_id="tg:42")
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from agentum_cloud.client import AgentumClient
|
|
14
|
+
from agentum_cloud.errors import AgentumAPIError, AgentumError, AgentumTimeoutError
|
|
15
|
+
from agentum_cloud.models import (
|
|
16
|
+
AskAnswer,
|
|
17
|
+
Citation,
|
|
18
|
+
Collection,
|
|
19
|
+
ContentUrl,
|
|
20
|
+
Enrichment,
|
|
21
|
+
MeterCost,
|
|
22
|
+
ObjectDetail,
|
|
23
|
+
ObjectSummary,
|
|
24
|
+
OwnerUsage,
|
|
25
|
+
SearchHit,
|
|
26
|
+
SummaryOut,
|
|
27
|
+
UploadResult,
|
|
28
|
+
UsageBreakdown,
|
|
29
|
+
UsageSummary,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"AgentumClient",
|
|
34
|
+
"AgentumError",
|
|
35
|
+
"AgentumAPIError",
|
|
36
|
+
"AgentumTimeoutError",
|
|
37
|
+
"AskAnswer",
|
|
38
|
+
"Citation",
|
|
39
|
+
"Collection",
|
|
40
|
+
"ContentUrl",
|
|
41
|
+
"Enrichment",
|
|
42
|
+
"MeterCost",
|
|
43
|
+
"ObjectDetail",
|
|
44
|
+
"ObjectSummary",
|
|
45
|
+
"OwnerUsage",
|
|
46
|
+
"SearchHit",
|
|
47
|
+
"SummaryOut",
|
|
48
|
+
"UploadResult",
|
|
49
|
+
"UsageBreakdown",
|
|
50
|
+
"UsageSummary",
|
|
51
|
+
]
|
agentum_cloud/client.py
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"""Async-клиент над agentum-cloud ``/v1``.
|
|
2
|
+
|
|
3
|
+
Тонкая обёртка httpx: Bearer-ключ, опциональный ``owner_id`` (X-Owner-Id, бот мапит
|
|
4
|
+
tg-юзера), ретраи на 429/5xx с экспоненциальным backoff, типизированные ответы.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import re
|
|
11
|
+
from types import TracebackType
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
|
|
16
|
+
from agentum_cloud.errors import AgentumAPIError, AgentumTimeoutError
|
|
17
|
+
from agentum_cloud.models import (
|
|
18
|
+
AskAnswer,
|
|
19
|
+
Collection,
|
|
20
|
+
ContentUrl,
|
|
21
|
+
ObjectDetail,
|
|
22
|
+
ObjectSummary,
|
|
23
|
+
SearchHit,
|
|
24
|
+
SummaryOut,
|
|
25
|
+
UploadResult,
|
|
26
|
+
UsageBreakdown,
|
|
27
|
+
UsageSummary,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# 504 НЕ ретраим: это дедлайн сервера /ask (ask_deadline_seconds) — повтор лишь сожжёт
|
|
31
|
+
# токены и время на том же запросе; пользователю показываем сообщение «не успел».
|
|
32
|
+
_RETRY_STATUSES = frozenset({429, 500, 502, 503})
|
|
33
|
+
_BACKOFF_BASE = 0.5
|
|
34
|
+
_BACKOFF_CAP = 8.0
|
|
35
|
+
_TERMINAL_STATUSES = frozenset({"ready", "failed"})
|
|
36
|
+
# read=75 заведомо больше серверного дедлайна /ask (settings.ask_deadline_seconds=60):
|
|
37
|
+
# клиент не отваливается раньше, чем сервер вернёт корректный ответ/типизированную 504.
|
|
38
|
+
_DEFAULT_TIMEOUT = httpx.Timeout(connect=5.0, read=75.0, write=10.0, pool=5.0)
|
|
39
|
+
# Транспортные ошибки «соединение не установлено» — запрос гарантированно не дошёл
|
|
40
|
+
# до сервера, поэтому ретраить безопасно даже неидемпотентный POST.
|
|
41
|
+
_CONNECT_ERRORS = (httpx.ConnectError, httpx.ConnectTimeout, httpx.PoolTimeout)
|
|
42
|
+
# Тот же контракт, что и сервер (deps._OWNER_RE) — ловим мусор до отправки заголовка.
|
|
43
|
+
_OWNER_RE = re.compile(r"\A[A-Za-z0-9_:.-]{1,64}\Z")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AgentumClient:
|
|
47
|
+
"""Клиент REST API. Используй как async-контекст-менеджер."""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
*,
|
|
52
|
+
base_url: str,
|
|
53
|
+
api_key: str,
|
|
54
|
+
owner_id: str | None = None,
|
|
55
|
+
timeout: float | httpx.Timeout | None = None,
|
|
56
|
+
max_attempts: int = 4,
|
|
57
|
+
client: httpx.AsyncClient | None = None,
|
|
58
|
+
) -> None:
|
|
59
|
+
self._base_url = base_url.rstrip("/")
|
|
60
|
+
self._api_key = api_key
|
|
61
|
+
self._owner_id = owner_id
|
|
62
|
+
self._max_attempts = max(1, max_attempts)
|
|
63
|
+
# Внешний client (для тестов/переиспользования) не закрываем сами.
|
|
64
|
+
# trust_env=False — детерминированный SDK: не подхватываем прокси из окружения
|
|
65
|
+
# (HTTP_PROXY/ALL_PROXY); нужен прокси — передай готовый httpx.AsyncClient.
|
|
66
|
+
self._owns_client = client is None
|
|
67
|
+
self._client = client or httpx.AsyncClient(
|
|
68
|
+
base_url=self._base_url,
|
|
69
|
+
timeout=timeout if timeout is not None else _DEFAULT_TIMEOUT,
|
|
70
|
+
trust_env=False,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
async def __aenter__(self) -> AgentumClient:
|
|
74
|
+
return self
|
|
75
|
+
|
|
76
|
+
async def __aexit__(
|
|
77
|
+
self,
|
|
78
|
+
exc_type: type[BaseException] | None,
|
|
79
|
+
exc: BaseException | None,
|
|
80
|
+
tb: TracebackType | None,
|
|
81
|
+
) -> None:
|
|
82
|
+
await self.aclose()
|
|
83
|
+
|
|
84
|
+
async def aclose(self) -> None:
|
|
85
|
+
if self._owns_client:
|
|
86
|
+
await self._client.aclose()
|
|
87
|
+
|
|
88
|
+
def _headers(self, owner_id: str | None) -> dict[str, str]:
|
|
89
|
+
headers = {"Authorization": f"Bearer {self._api_key}"}
|
|
90
|
+
effective = owner_id or self._owner_id
|
|
91
|
+
if effective:
|
|
92
|
+
if not _OWNER_RE.fullmatch(effective):
|
|
93
|
+
raise ValueError(f"Некорректный owner_id: {effective!r}")
|
|
94
|
+
headers["X-Owner-Id"] = effective
|
|
95
|
+
return headers
|
|
96
|
+
|
|
97
|
+
async def _request(
|
|
98
|
+
self,
|
|
99
|
+
method: str,
|
|
100
|
+
path: str,
|
|
101
|
+
*,
|
|
102
|
+
owner_id: str | None = None,
|
|
103
|
+
idempotent: bool = False,
|
|
104
|
+
**kwargs: Any,
|
|
105
|
+
) -> httpx.Response:
|
|
106
|
+
last_exc: Exception | None = None
|
|
107
|
+
for attempt in range(1, self._max_attempts + 1):
|
|
108
|
+
try:
|
|
109
|
+
resp = await self._client.request(method, path, headers=self._headers(owner_id), **kwargs)
|
|
110
|
+
except httpx.TransportError as exc:
|
|
111
|
+
last_exc = exc
|
|
112
|
+
# Соединение не установлено → запрос не дошёл, ретраим любой метод. Иначе
|
|
113
|
+
# (read/write — байты уже могли уйти) ретраим только идемпотентные: дорогой
|
|
114
|
+
# неидемпотентный POST (/ask, /objects) по таймауту ответа НЕ перезапускаем.
|
|
115
|
+
connect_failed = isinstance(exc, _CONNECT_ERRORS)
|
|
116
|
+
if (not connect_failed and not idempotent) or attempt == self._max_attempts:
|
|
117
|
+
raise AgentumAPIError(0, f"Сеть недоступна: {exc}") from exc
|
|
118
|
+
await asyncio.sleep(_backoff(attempt))
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
if resp.status_code in _RETRY_STATUSES and attempt < self._max_attempts:
|
|
122
|
+
await asyncio.sleep(_backoff(attempt, resp.headers.get("Retry-After")))
|
|
123
|
+
continue
|
|
124
|
+
if resp.status_code >= 400:
|
|
125
|
+
raise AgentumAPIError(resp.status_code, _error_message(resp), body=resp.text[:500])
|
|
126
|
+
return resp
|
|
127
|
+
|
|
128
|
+
# Недостижимо при max_attempts>=1, но успокаиваем тайпчекер.
|
|
129
|
+
raise AgentumAPIError(0, f"Запрос не удался: {last_exc}")
|
|
130
|
+
|
|
131
|
+
async def upload(
|
|
132
|
+
self,
|
|
133
|
+
filename: str,
|
|
134
|
+
data: bytes,
|
|
135
|
+
*,
|
|
136
|
+
mime: str | None = None,
|
|
137
|
+
owner_id: str | None = None,
|
|
138
|
+
) -> UploadResult:
|
|
139
|
+
files = {"file": (filename, data, mime or "application/octet-stream")}
|
|
140
|
+
# idempotent=False (по умолчанию): не плодим дубли загрузки по read-timeout.
|
|
141
|
+
resp = await self._request("POST", "/v1/objects", owner_id=owner_id, files=files)
|
|
142
|
+
return UploadResult.from_dict(resp.json())
|
|
143
|
+
|
|
144
|
+
async def get_object(self, object_id: str, *, owner_id: str | None = None) -> ObjectDetail:
|
|
145
|
+
resp = await self._request("GET", f"/v1/objects/{object_id}", owner_id=owner_id, idempotent=True)
|
|
146
|
+
return ObjectDetail.from_dict(resp.json())
|
|
147
|
+
|
|
148
|
+
async def list_objects(
|
|
149
|
+
self,
|
|
150
|
+
*,
|
|
151
|
+
limit: int | None = None,
|
|
152
|
+
offset: int | None = None,
|
|
153
|
+
doc_type: str | None = None,
|
|
154
|
+
owner_id: str | None = None,
|
|
155
|
+
) -> list[ObjectSummary]:
|
|
156
|
+
"""Список объектов владельца (новые сверху)."""
|
|
157
|
+
params: dict[str, Any] = {}
|
|
158
|
+
if limit:
|
|
159
|
+
params["limit"] = limit
|
|
160
|
+
if offset:
|
|
161
|
+
params["offset"] = offset
|
|
162
|
+
if doc_type:
|
|
163
|
+
params["doc_type"] = doc_type
|
|
164
|
+
resp = await self._request("GET", "/v1/objects", owner_id=owner_id, params=params or None)
|
|
165
|
+
return [ObjectSummary.from_dict(it) for it in resp.json().get("items", [])]
|
|
166
|
+
|
|
167
|
+
async def get_content(self, object_id: str, *, owner_id: str | None = None) -> ContentUrl:
|
|
168
|
+
resp = await self._request(
|
|
169
|
+
"GET", f"/v1/objects/{object_id}/content", owner_id=owner_id, idempotent=True
|
|
170
|
+
)
|
|
171
|
+
return ContentUrl.from_dict(resp.json())
|
|
172
|
+
|
|
173
|
+
async def download_url(self, url: str) -> bytes:
|
|
174
|
+
"""Качает байты по (presigned) URL. Auth-заголовок НЕ шлём — ссылка самодостаточна."""
|
|
175
|
+
try:
|
|
176
|
+
resp = await self._client.get(url, follow_redirects=True)
|
|
177
|
+
except httpx.TransportError as exc:
|
|
178
|
+
raise AgentumAPIError(0, f"Сеть недоступна: {exc}") from exc
|
|
179
|
+
if resp.status_code >= 400:
|
|
180
|
+
raise AgentumAPIError(resp.status_code, "Не удалось скачать файл", body=resp.text[:300])
|
|
181
|
+
return resp.content
|
|
182
|
+
|
|
183
|
+
async def get_usage(self, *, days: int | None = None, owner_id: str | None = None) -> UsageSummary:
|
|
184
|
+
"""Расход текущего владельца (или ``owner_id``): стоимость ₽ + разбивка по статьям."""
|
|
185
|
+
params = {"days": days} if days else None
|
|
186
|
+
resp = await self._request("GET", "/v1/usage", owner_id=owner_id, params=params)
|
|
187
|
+
return UsageSummary.from_dict(resp.json())
|
|
188
|
+
|
|
189
|
+
async def get_usage_breakdown(
|
|
190
|
+
self, *, days: int | None = None, owner_id: str | None = None
|
|
191
|
+
) -> UsageBreakdown:
|
|
192
|
+
"""Расход по всем юзерам тенанта (только админ; иначе AgentumAPIError 403)."""
|
|
193
|
+
params = {"days": days} if days else None
|
|
194
|
+
resp = await self._request("GET", "/v1/usage/breakdown", owner_id=owner_id, params=params)
|
|
195
|
+
return UsageBreakdown.from_dict(resp.json())
|
|
196
|
+
|
|
197
|
+
async def classify_intent(self, query: str, *, owner_id: str | None = None) -> tuple[str, str, str]:
|
|
198
|
+
"""Намерение свободной фразы: ``(action, target, lang)``.
|
|
199
|
+
|
|
200
|
+
``action`` ∈ {send_file, answer, delete, translate}; ``lang`` — целевой язык
|
|
201
|
+
для translate (для остальных — "").
|
|
202
|
+
"""
|
|
203
|
+
resp = await self._request("POST", "/v1/intent", owner_id=owner_id, json={"query": query})
|
|
204
|
+
data = resp.json()
|
|
205
|
+
return (
|
|
206
|
+
str(data.get("action") or "answer"),
|
|
207
|
+
str(data.get("target") or ""),
|
|
208
|
+
str(data.get("lang") or ""),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
async def transcribe(
|
|
212
|
+
self,
|
|
213
|
+
filename: str,
|
|
214
|
+
data: bytes,
|
|
215
|
+
*,
|
|
216
|
+
mime: str | None = None,
|
|
217
|
+
owner_id: str | None = None,
|
|
218
|
+
) -> str:
|
|
219
|
+
"""Распознаёт речь из аудио в текст (без сохранения в облаке). Возвращает текст ("" — нет речи)."""
|
|
220
|
+
files = {"file": (filename, data, mime or "audio/ogg")}
|
|
221
|
+
resp = await self._request("POST", "/v1/transcribe", owner_id=owner_id, files=files)
|
|
222
|
+
return str(resp.json().get("text", ""))
|
|
223
|
+
|
|
224
|
+
async def search(
|
|
225
|
+
self,
|
|
226
|
+
query: str,
|
|
227
|
+
*,
|
|
228
|
+
doc_type: str | None = None,
|
|
229
|
+
top_k: int | None = None,
|
|
230
|
+
owner_id: str | None = None,
|
|
231
|
+
) -> list[SearchHit]:
|
|
232
|
+
params: dict[str, Any] = {"q": query}
|
|
233
|
+
if doc_type:
|
|
234
|
+
params["doc_type"] = doc_type
|
|
235
|
+
if top_k:
|
|
236
|
+
params["top_k"] = top_k
|
|
237
|
+
resp = await self._request("GET", "/v1/search", owner_id=owner_id, idempotent=True, params=params)
|
|
238
|
+
return [SearchHit.from_dict(h) for h in resp.json().get("hits", [])]
|
|
239
|
+
|
|
240
|
+
async def ask(
|
|
241
|
+
self,
|
|
242
|
+
query: str,
|
|
243
|
+
*,
|
|
244
|
+
doc_type: str | None = None,
|
|
245
|
+
top_k: int | None = None,
|
|
246
|
+
owner_id: str | None = None,
|
|
247
|
+
) -> AskAnswer:
|
|
248
|
+
body: dict[str, Any] = {"query": query}
|
|
249
|
+
if doc_type:
|
|
250
|
+
body["doc_type"] = doc_type
|
|
251
|
+
if top_k:
|
|
252
|
+
body["top_k"] = top_k
|
|
253
|
+
# idempotent=False (по умолчанию): дорогой LLM-вызов не перезапускаем по read-timeout;
|
|
254
|
+
# сервер сам ограничен дедлайном (ask_deadline_seconds) и вернёт типизированную ошибку.
|
|
255
|
+
resp = await self._request("POST", "/v1/ask", owner_id=owner_id, json=body)
|
|
256
|
+
return AskAnswer.from_dict(resp.json())
|
|
257
|
+
|
|
258
|
+
async def wait_until_ready(
|
|
259
|
+
self,
|
|
260
|
+
object_id: str,
|
|
261
|
+
*,
|
|
262
|
+
owner_id: str | None = None,
|
|
263
|
+
timeout: float = 120.0, # noqa: ASYNC109 — это бюджет поллинга, не single-shot asyncio.timeout
|
|
264
|
+
interval: float = 2.0,
|
|
265
|
+
) -> ObjectDetail:
|
|
266
|
+
"""Поллит статус объекта до ``ready``/``failed``. Бросает AgentumTimeoutError по таймауту."""
|
|
267
|
+
loop = asyncio.get_running_loop()
|
|
268
|
+
deadline = loop.time() + timeout
|
|
269
|
+
while True:
|
|
270
|
+
detail = await self.get_object(object_id, owner_id=owner_id)
|
|
271
|
+
if detail.status in _TERMINAL_STATUSES:
|
|
272
|
+
return detail
|
|
273
|
+
remaining = deadline - loop.time()
|
|
274
|
+
if remaining <= 0:
|
|
275
|
+
raise AgentumTimeoutError(f"Объект {object_id} не готов за {timeout:.0f}с")
|
|
276
|
+
await asyncio.sleep(min(interval, remaining))
|
|
277
|
+
|
|
278
|
+
# --- Папки/коллекции и удаление (Ф8, Dev A) ----------------------------
|
|
279
|
+
|
|
280
|
+
async def delete_object(self, object_id: str, *, owner_id: str | None = None) -> None:
|
|
281
|
+
"""Удаляет объект (файл) безвозвратно. 204 — успех."""
|
|
282
|
+
await self._request("DELETE", f"/v1/objects/{object_id}", owner_id=owner_id)
|
|
283
|
+
|
|
284
|
+
async def create_collection(
|
|
285
|
+
self, name: str, *, parent_id: str | None = None, owner_id: str | None = None
|
|
286
|
+
) -> Collection:
|
|
287
|
+
"""Создаёт папку. ``parent_id`` — родитель (None для корня)."""
|
|
288
|
+
body: dict[str, Any] = {"name": name}
|
|
289
|
+
if parent_id is not None:
|
|
290
|
+
body["parent_id"] = parent_id
|
|
291
|
+
resp = await self._request("POST", "/v1/collections", owner_id=owner_id, json=body)
|
|
292
|
+
return Collection.from_dict(resp.json())
|
|
293
|
+
|
|
294
|
+
async def list_collections(self, *, owner_id: str | None = None) -> list[Collection]:
|
|
295
|
+
"""Плоский список папок владельца (дерево строится по ``parent_id``)."""
|
|
296
|
+
resp = await self._request("GET", "/v1/collections", owner_id=owner_id)
|
|
297
|
+
return [Collection.from_dict(it) for it in resp.json().get("items", [])]
|
|
298
|
+
|
|
299
|
+
async def rename_collection(
|
|
300
|
+
self, collection_id: str, name: str, *, owner_id: str | None = None
|
|
301
|
+
) -> Collection:
|
|
302
|
+
"""Переименовывает папку."""
|
|
303
|
+
resp = await self._request(
|
|
304
|
+
"PATCH", f"/v1/collections/{collection_id}", owner_id=owner_id, json={"name": name}
|
|
305
|
+
)
|
|
306
|
+
return Collection.from_dict(resp.json())
|
|
307
|
+
|
|
308
|
+
async def move_collection(
|
|
309
|
+
self, collection_id: str, parent_id: str | None, *, owner_id: str | None = None
|
|
310
|
+
) -> Collection:
|
|
311
|
+
"""Переносит папку под нового родителя (``parent_id=None`` — в корень)."""
|
|
312
|
+
resp = await self._request(
|
|
313
|
+
"PATCH",
|
|
314
|
+
f"/v1/collections/{collection_id}",
|
|
315
|
+
owner_id=owner_id,
|
|
316
|
+
json={"parent_id": parent_id},
|
|
317
|
+
)
|
|
318
|
+
return Collection.from_dict(resp.json())
|
|
319
|
+
|
|
320
|
+
async def delete_collection(self, collection_id: str, *, owner_id: str | None = None) -> None:
|
|
321
|
+
"""Удаляет папку (под-папки всплывают в корень, файлы — «без папки»)."""
|
|
322
|
+
await self._request("DELETE", f"/v1/collections/{collection_id}", owner_id=owner_id)
|
|
323
|
+
|
|
324
|
+
async def move_object(self, collection_id: str, object_id: str, *, owner_id: str | None = None) -> None:
|
|
325
|
+
"""Перемещает файл в папку. 204 — успех."""
|
|
326
|
+
await self._request("POST", f"/v1/collections/{collection_id}/objects/{object_id}", owner_id=owner_id)
|
|
327
|
+
|
|
328
|
+
# --- Ф9 (Dev B): контент-функции ---------------------------------------
|
|
329
|
+
|
|
330
|
+
async def get_summary(self, object_id: str, *, owner_id: str | None = None) -> SummaryOut:
|
|
331
|
+
"""Содержание объекта (Ф9): метаданные из обогащения. ``pending`` — статус без полей."""
|
|
332
|
+
resp = await self._request(
|
|
333
|
+
"GET", f"/v1/objects/{object_id}/summary", owner_id=owner_id, idempotent=True
|
|
334
|
+
)
|
|
335
|
+
return SummaryOut.from_dict(resp.json())
|
|
336
|
+
|
|
337
|
+
async def translate(
|
|
338
|
+
self, object_id: str, target_lang: str, *, owner_id: str | None = None
|
|
339
|
+
) -> tuple[str, str]:
|
|
340
|
+
"""Перевод объекта на ``target_lang`` → новый объект. Возвращает ``(object_id, status)``.
|
|
341
|
+
|
|
342
|
+
``idempotent=False``: дорогой LLM-вызов не перезапускаем по read-timeout (дедуп
|
|
343
|
+
нового объекта по ``content_hash`` на сервере всё равно защищает от дублей).
|
|
344
|
+
"""
|
|
345
|
+
resp = await self._request(
|
|
346
|
+
"POST",
|
|
347
|
+
f"/v1/objects/{object_id}/translate",
|
|
348
|
+
owner_id=owner_id,
|
|
349
|
+
json={"target_lang": target_lang},
|
|
350
|
+
)
|
|
351
|
+
data = resp.json()
|
|
352
|
+
return str(data.get("object_id") or ""), str(data.get("status") or "")
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _backoff(attempt: int, retry_after: str | None = None) -> float:
|
|
356
|
+
if retry_after:
|
|
357
|
+
try:
|
|
358
|
+
return min(float(retry_after), _BACKOFF_CAP)
|
|
359
|
+
except ValueError:
|
|
360
|
+
pass
|
|
361
|
+
return min(_BACKOFF_BASE * (2 ** (attempt - 1)), _BACKOFF_CAP)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _error_message(resp: httpx.Response) -> str:
|
|
365
|
+
try:
|
|
366
|
+
payload = resp.json()
|
|
367
|
+
except ValueError:
|
|
368
|
+
return resp.reason_phrase or "ошибка"
|
|
369
|
+
if isinstance(payload, dict):
|
|
370
|
+
detail = payload.get("detail") or payload.get("error") or payload.get("message")
|
|
371
|
+
if isinstance(detail, str):
|
|
372
|
+
return detail
|
|
373
|
+
return resp.reason_phrase or "ошибка"
|
agentum_cloud/errors.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Ошибки SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AgentumError(Exception):
|
|
7
|
+
"""Базовая ошибка SDK."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AgentumAPIError(AgentumError):
|
|
11
|
+
"""Не-2xx ответ от API. Несёт статус и тело для диагностики у клиента."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, status_code: int, message: str, *, body: str = "") -> None:
|
|
14
|
+
super().__init__(f"API {status_code}: {message}")
|
|
15
|
+
self.status_code = status_code
|
|
16
|
+
self.message = message
|
|
17
|
+
self.body = body
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AgentumTimeoutError(AgentumError):
|
|
21
|
+
"""Объект не дошёл до терминального статуса за отведённое время (wait_until_ready)."""
|
agentum_cloud/models.py
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""Типизированные DTO ответов API (зеркало backend/app/schemas).
|
|
2
|
+
|
|
3
|
+
Лёгкие ``frozen``-датаклассы без pydantic — SDK не тянет лишних зависимостей.
|
|
4
|
+
Каждый класс умеет собираться из JSON-словаря через ``from_dict``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class UploadResult:
|
|
15
|
+
id: str
|
|
16
|
+
status: str
|
|
17
|
+
duplicate: bool = False
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def from_dict(cls, d: dict[str, Any]) -> UploadResult:
|
|
21
|
+
return cls(id=str(d["id"]), status=str(d["status"]), duplicate=bool(d.get("duplicate", False)))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class Enrichment:
|
|
26
|
+
title: str = ""
|
|
27
|
+
summary: str = ""
|
|
28
|
+
description: str = ""
|
|
29
|
+
keywords: list[str] = field(default_factory=list)
|
|
30
|
+
doc_type: str = "other"
|
|
31
|
+
language: str = ""
|
|
32
|
+
entities: dict[str, Any] = field(default_factory=dict)
|
|
33
|
+
model: str = ""
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def from_dict(cls, d: dict[str, Any]) -> Enrichment:
|
|
37
|
+
return cls(
|
|
38
|
+
title=str(d.get("title", "")),
|
|
39
|
+
summary=str(d.get("summary", "")),
|
|
40
|
+
description=str(d.get("description", "")),
|
|
41
|
+
keywords=list(d.get("keywords") or []),
|
|
42
|
+
doc_type=str(d.get("doc_type", "other")),
|
|
43
|
+
language=str(d.get("language", "")),
|
|
44
|
+
entities=dict(d.get("entities") or {}),
|
|
45
|
+
model=str(d.get("model", "")),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class SummaryOut:
|
|
51
|
+
"""Карточка содержания объекта (Ф9): метаданные + статус. Зеркало backend ``SummaryOut``."""
|
|
52
|
+
|
|
53
|
+
object_id: str
|
|
54
|
+
status: str
|
|
55
|
+
filename: str = ""
|
|
56
|
+
title: str = ""
|
|
57
|
+
summary: str = ""
|
|
58
|
+
description: str = ""
|
|
59
|
+
doc_type: str = "other"
|
|
60
|
+
keywords: list[str] = field(default_factory=list)
|
|
61
|
+
language: str = ""
|
|
62
|
+
entities: dict[str, Any] = field(default_factory=dict)
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def is_ready(self) -> bool:
|
|
66
|
+
return self.status == "ready"
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def from_dict(cls, d: dict[str, Any]) -> SummaryOut:
|
|
70
|
+
return cls(
|
|
71
|
+
object_id=str(d["object_id"]),
|
|
72
|
+
status=str(d.get("status", "")),
|
|
73
|
+
filename=str(d.get("filename", "")),
|
|
74
|
+
title=str(d.get("title", "")),
|
|
75
|
+
summary=str(d.get("summary", "")),
|
|
76
|
+
description=str(d.get("description", "")),
|
|
77
|
+
doc_type=str(d.get("doc_type", "other")),
|
|
78
|
+
keywords=list(d.get("keywords") or []),
|
|
79
|
+
language=str(d.get("language", "")),
|
|
80
|
+
entities=dict(d.get("entities") or {}),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True)
|
|
85
|
+
class ObjectDetail:
|
|
86
|
+
id: str
|
|
87
|
+
filename: str
|
|
88
|
+
mime: str
|
|
89
|
+
size: int
|
|
90
|
+
kind: str
|
|
91
|
+
status: str
|
|
92
|
+
error: str | None = None
|
|
93
|
+
content_hash: str = ""
|
|
94
|
+
enrichment: Enrichment | None = None
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def is_ready(self) -> bool:
|
|
98
|
+
return self.status == "ready"
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def is_failed(self) -> bool:
|
|
102
|
+
return self.status == "failed"
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
def from_dict(cls, d: dict[str, Any]) -> ObjectDetail:
|
|
106
|
+
enr = d.get("enrichment")
|
|
107
|
+
return cls(
|
|
108
|
+
id=str(d["id"]),
|
|
109
|
+
filename=str(d.get("filename", "")),
|
|
110
|
+
mime=str(d.get("mime", "")),
|
|
111
|
+
size=int(d.get("size", 0)),
|
|
112
|
+
kind=str(d.get("kind", "")),
|
|
113
|
+
status=str(d.get("status", "")),
|
|
114
|
+
error=d.get("error"),
|
|
115
|
+
content_hash=str(d.get("content_hash", "")),
|
|
116
|
+
enrichment=Enrichment.from_dict(enr) if isinstance(enr, dict) else None,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass(frozen=True)
|
|
121
|
+
class SearchHit:
|
|
122
|
+
object_id: str
|
|
123
|
+
filename: str = ""
|
|
124
|
+
doc_type: str = "other"
|
|
125
|
+
title: str = ""
|
|
126
|
+
summary: str = ""
|
|
127
|
+
score: float = 0.0
|
|
128
|
+
snippet: str = ""
|
|
129
|
+
|
|
130
|
+
@classmethod
|
|
131
|
+
def from_dict(cls, d: dict[str, Any]) -> SearchHit:
|
|
132
|
+
return cls(
|
|
133
|
+
object_id=str(d["object_id"]),
|
|
134
|
+
filename=str(d.get("filename", "")),
|
|
135
|
+
doc_type=str(d.get("doc_type", "other")),
|
|
136
|
+
title=str(d.get("title", "")),
|
|
137
|
+
summary=str(d.get("summary", "")),
|
|
138
|
+
score=float(d.get("score", 0.0)),
|
|
139
|
+
snippet=str(d.get("snippet", "")),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@dataclass(frozen=True)
|
|
144
|
+
class Citation:
|
|
145
|
+
object_id: str
|
|
146
|
+
ord: int = 0
|
|
147
|
+
index: int = 0
|
|
148
|
+
filename: str = ""
|
|
149
|
+
title: str = ""
|
|
150
|
+
snippet: str = ""
|
|
151
|
+
|
|
152
|
+
@classmethod
|
|
153
|
+
def from_dict(cls, d: dict[str, Any]) -> Citation:
|
|
154
|
+
return cls(
|
|
155
|
+
object_id=str(d["object_id"]),
|
|
156
|
+
ord=int(d.get("ord", 0)),
|
|
157
|
+
index=int(d.get("index", 0)),
|
|
158
|
+
filename=str(d.get("filename", "")),
|
|
159
|
+
title=str(d.get("title", "")),
|
|
160
|
+
snippet=str(d.get("snippet", "")),
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@dataclass(frozen=True)
|
|
165
|
+
class AskAnswer:
|
|
166
|
+
answer: str
|
|
167
|
+
used_context: bool = False
|
|
168
|
+
citations: list[Citation] = field(default_factory=list)
|
|
169
|
+
|
|
170
|
+
@classmethod
|
|
171
|
+
def from_dict(cls, d: dict[str, Any]) -> AskAnswer:
|
|
172
|
+
cites = d.get("citations") or []
|
|
173
|
+
return cls(
|
|
174
|
+
answer=str(d.get("answer", "")),
|
|
175
|
+
used_context=bool(d.get("used_context", False)),
|
|
176
|
+
citations=[Citation.from_dict(c) for c in cites if isinstance(c, dict)],
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@dataclass(frozen=True)
|
|
181
|
+
class ContentUrl:
|
|
182
|
+
url: str
|
|
183
|
+
expires_in: int = 0
|
|
184
|
+
|
|
185
|
+
@classmethod
|
|
186
|
+
def from_dict(cls, d: dict[str, Any]) -> ContentUrl:
|
|
187
|
+
return cls(url=str(d["url"]), expires_in=int(d.get("expires_in", 0)))
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@dataclass(frozen=True)
|
|
191
|
+
class ObjectSummary:
|
|
192
|
+
id: str
|
|
193
|
+
filename: str = ""
|
|
194
|
+
kind: str = ""
|
|
195
|
+
status: str = ""
|
|
196
|
+
doc_type: str = "other"
|
|
197
|
+
title: str = ""
|
|
198
|
+
created_at: str = ""
|
|
199
|
+
|
|
200
|
+
@classmethod
|
|
201
|
+
def from_dict(cls, d: dict[str, Any]) -> ObjectSummary:
|
|
202
|
+
return cls(
|
|
203
|
+
id=str(d["id"]),
|
|
204
|
+
filename=str(d.get("filename", "")),
|
|
205
|
+
kind=str(d.get("kind", "")),
|
|
206
|
+
status=str(d.get("status", "")),
|
|
207
|
+
doc_type=str(d.get("doc_type", "other")),
|
|
208
|
+
title=str(d.get("title", "")),
|
|
209
|
+
created_at=str(d.get("created_at", "")),
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@dataclass(frozen=True)
|
|
214
|
+
class Collection:
|
|
215
|
+
"""Папка/коллекция (Ф8). ``parent_id`` — родитель в дереве (None для корня)."""
|
|
216
|
+
|
|
217
|
+
id: str
|
|
218
|
+
name: str = ""
|
|
219
|
+
parent_id: str | None = None
|
|
220
|
+
created_at: str = ""
|
|
221
|
+
|
|
222
|
+
@classmethod
|
|
223
|
+
def from_dict(cls, d: dict[str, Any]) -> Collection:
|
|
224
|
+
parent = d.get("parent_id")
|
|
225
|
+
return cls(
|
|
226
|
+
id=str(d["id"]),
|
|
227
|
+
name=str(d.get("name", "")),
|
|
228
|
+
parent_id=str(parent) if parent else None,
|
|
229
|
+
created_at=str(d.get("created_at", "")),
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@dataclass(frozen=True)
|
|
234
|
+
class MeterCost:
|
|
235
|
+
meter: str
|
|
236
|
+
cost_rub: float = 0.0
|
|
237
|
+
prompt_tokens: int = 0
|
|
238
|
+
total_tokens: int = 0
|
|
239
|
+
audio_seconds: int = 0
|
|
240
|
+
calls: int = 0
|
|
241
|
+
|
|
242
|
+
@classmethod
|
|
243
|
+
def from_dict(cls, d: dict[str, Any]) -> MeterCost:
|
|
244
|
+
return cls(
|
|
245
|
+
meter=str(d.get("meter", "")),
|
|
246
|
+
cost_rub=float(d.get("cost_rub", 0.0)),
|
|
247
|
+
prompt_tokens=int(d.get("prompt_tokens", 0)),
|
|
248
|
+
total_tokens=int(d.get("total_tokens", 0)),
|
|
249
|
+
audio_seconds=int(d.get("audio_seconds", 0)),
|
|
250
|
+
calls=int(d.get("calls", 0)),
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@dataclass(frozen=True)
|
|
255
|
+
class UsageSummary:
|
|
256
|
+
owner_id: str | None = None
|
|
257
|
+
days: int | None = None
|
|
258
|
+
total_cost_rub: float = 0.0
|
|
259
|
+
by_meter: list[MeterCost] = field(default_factory=list)
|
|
260
|
+
|
|
261
|
+
@classmethod
|
|
262
|
+
def from_dict(cls, d: dict[str, Any]) -> UsageSummary:
|
|
263
|
+
meters = d.get("by_meter") or []
|
|
264
|
+
return cls(
|
|
265
|
+
owner_id=d.get("owner_id"),
|
|
266
|
+
days=d.get("days"),
|
|
267
|
+
total_cost_rub=float(d.get("total_cost_rub", 0.0)),
|
|
268
|
+
by_meter=[MeterCost.from_dict(m) for m in meters if isinstance(m, dict)],
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@dataclass(frozen=True)
|
|
273
|
+
class OwnerUsage:
|
|
274
|
+
owner_id: str | None = None
|
|
275
|
+
total_cost_rub: float = 0.0
|
|
276
|
+
by_meter: list[MeterCost] = field(default_factory=list)
|
|
277
|
+
|
|
278
|
+
@classmethod
|
|
279
|
+
def from_dict(cls, d: dict[str, Any]) -> OwnerUsage:
|
|
280
|
+
meters = d.get("by_meter") or []
|
|
281
|
+
return cls(
|
|
282
|
+
owner_id=d.get("owner_id"),
|
|
283
|
+
total_cost_rub=float(d.get("total_cost_rub", 0.0)),
|
|
284
|
+
by_meter=[MeterCost.from_dict(m) for m in meters if isinstance(m, dict)],
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@dataclass(frozen=True)
|
|
289
|
+
class UsageBreakdown:
|
|
290
|
+
days: int | None = None
|
|
291
|
+
total_cost_rub: float = 0.0
|
|
292
|
+
owners: list[OwnerUsage] = field(default_factory=list)
|
|
293
|
+
|
|
294
|
+
@classmethod
|
|
295
|
+
def from_dict(cls, d: dict[str, Any]) -> UsageBreakdown:
|
|
296
|
+
owners = d.get("owners") or []
|
|
297
|
+
return cls(
|
|
298
|
+
days=d.get("days"),
|
|
299
|
+
total_cost_rub=float(d.get("total_cost_rub", 0.0)),
|
|
300
|
+
owners=[OwnerUsage.from_dict(o) for o in owners if isinstance(o, dict)],
|
|
301
|
+
)
|
agentum_cloud/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentum-cloud-sdk
|
|
3
|
+
Version: 0.1.62
|
|
4
|
+
Summary: Типизированный async-клиент Agentum Cloud (/v1): загрузка документов, поиск и RAG-вопросы по вашим файлам.
|
|
5
|
+
Project-URL: Homepage, https://cloud.agentums.ru
|
|
6
|
+
Project-URL: Repository, https://gitlab.basis-pro.tech/agentum-systems/agentum-cloud
|
|
7
|
+
Author: Agentum Systems
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: agentum,ai,async,documents,httpx,llm,rag,search
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.13
|
|
20
|
+
Requires-Dist: httpx>=0.27
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# agentum-cloud-sdk
|
|
24
|
+
|
|
25
|
+
Типизированный **async**-клиент для [Agentum Cloud](https://cloud.agentums.ru) — облака
|
|
26
|
+
ваших документов с поиском и ответами на естественном языке (RAG). Загружаете файлы
|
|
27
|
+
(PDF, DOCX, изображения, аудио), а затем ищете по ним и задаёте вопросы — ИИ отвечает
|
|
28
|
+
с ссылками на источники.
|
|
29
|
+
|
|
30
|
+
Тонкая обёртка над `httpx` поверх REST `/v1`. Полные type hints, `py.typed`.
|
|
31
|
+
|
|
32
|
+
## Установка
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install agentum-cloud-sdk
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Обновление до свежей версии: `pip install -U agentum-cloud-sdk`.
|
|
39
|
+
|
|
40
|
+
Требуется Python 3.13+.
|
|
41
|
+
|
|
42
|
+
## Токен
|
|
43
|
+
|
|
44
|
+
1. Войдите в приложение [cloud.agentums.ru](https://cloud.agentums.ru) (через Яндекс).
|
|
45
|
+
2. **Настройки → API-токены → Создать** — скопируйте токен `ak_…` (показывается один раз).
|
|
46
|
+
|
|
47
|
+
Токен привязан к вашему аккаунту и подписке. Держите его в секрете (как пароль);
|
|
48
|
+
если скомпрометирован — отзовите в настройках и создайте новый.
|
|
49
|
+
|
|
50
|
+
## Быстрый старт
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import asyncio
|
|
54
|
+
from agentum_cloud import AgentumClient
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def main() -> None:
|
|
58
|
+
async with AgentumClient(
|
|
59
|
+
base_url="https://api.cloud.agentums.ru",
|
|
60
|
+
api_key="ak_ваш_токен",
|
|
61
|
+
) as cloud:
|
|
62
|
+
# Загрузить документ
|
|
63
|
+
with open("contract.pdf", "rb") as f:
|
|
64
|
+
obj = await cloud.upload("contract.pdf", f.read())
|
|
65
|
+
|
|
66
|
+
# Дождаться, пока документ проиндексируется
|
|
67
|
+
await cloud.wait_until_ready(obj.id)
|
|
68
|
+
|
|
69
|
+
# Спросить — ответ с цитатами из ваших файлов
|
|
70
|
+
answer = await cloud.ask("Какой срок действия договора?")
|
|
71
|
+
print(answer.answer)
|
|
72
|
+
for c in answer.citations:
|
|
73
|
+
print(f" источник: {c.filename}")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
asyncio.run(main())
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Что умеет
|
|
80
|
+
|
|
81
|
+
| Метод | Назначение |
|
|
82
|
+
|-------|-----------|
|
|
83
|
+
| `upload(filename, data)` | загрузить документ/изображение/аудио |
|
|
84
|
+
| `wait_until_ready(object_id)` | дождаться окончания индексации |
|
|
85
|
+
| `list_objects(...)` / `get_object(id)` | список / детали файла (заголовок, summary, теги) |
|
|
86
|
+
| `get_content(id)` | временная ссылка на оригинал (presigned URL) |
|
|
87
|
+
| `search(q)` | гибридный поиск (вектор + полнотекст) по вашим файлам |
|
|
88
|
+
| `ask(query)` | ответ ИИ по вашим документам + цитаты-источники |
|
|
89
|
+
| `transcribe(filename, data)` | расшифровка аудио в текст |
|
|
90
|
+
| `get_usage(days=...)` | расход (₽) и разбивка по статьям |
|
|
91
|
+
| `delete_object(id)` | удалить файл |
|
|
92
|
+
|
|
93
|
+
Все методы — корутины; клиент — async-context-manager (`async with`).
|
|
94
|
+
|
|
95
|
+
## Заметки
|
|
96
|
+
|
|
97
|
+
- Данные изолированы по аккаунту: токен видит только файлы вашего аккаунта. Файлы,
|
|
98
|
+
загруженные через [Telegram-бота](https://cloud.agentums.ru), доступны здесь же после
|
|
99
|
+
привязки бота к аккаунту.
|
|
100
|
+
- Базовый URL прода — `https://api.cloud.agentums.ru`.
|
|
101
|
+
|
|
102
|
+
## Лицензия
|
|
103
|
+
|
|
104
|
+
MIT — см. [LICENSE](LICENSE).
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
agentum_cloud/__init__.py,sha256=nM_ZAPUdpGEVGivIz0ufVwMstLXUi42F6Aez5wD3sKk,1332
|
|
2
|
+
agentum_cloud/client.py,sha256=a6UOFDRrlk6VHALfE6D7VrzKQtfl_ywGCftO7kVBPGA,17378
|
|
3
|
+
agentum_cloud/errors.py,sha256=diRKsl8Rmr-MAK3xfi321Yvr1SyvwtkYDTGJmbntUOY,723
|
|
4
|
+
agentum_cloud/models.py,sha256=d97ZCtszlem6qBF4rzC5TdGqGL0VmdDae3Q1fRxf2GA,8872
|
|
5
|
+
agentum_cloud/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
agentum_cloud_sdk-0.1.62.dist-info/METADATA,sha256=D0EthZMLtyzNGnkjH7x2nr85eK7yiMudVnMpQJTyNA8,4703
|
|
7
|
+
agentum_cloud_sdk-0.1.62.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
8
|
+
agentum_cloud_sdk-0.1.62.dist-info/licenses/LICENSE,sha256=eQ7tb37mTyh-nyBz8b2-gtI2QnI-AYfmr8WUp2C6HCM,1072
|
|
9
|
+
agentum_cloud_sdk-0.1.62.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Agentum Systems
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|