kaibo 0.1.0__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.
- kaibo/__init__.py +22 -0
- kaibo/_core.py +142 -0
- kaibo/async_client.py +453 -0
- kaibo/client.py +578 -0
- kaibo/errors.py +37 -0
- kaibo/models.py +428 -0
- kaibo/py.typed +0 -0
- kaibo/render.py +47 -0
- kaibo/webhooks.py +46 -0
- kaibo-0.1.0.dist-info/METADATA +39 -0
- kaibo-0.1.0.dist-info/RECORD +12 -0
- kaibo-0.1.0.dist-info/WHEEL +4 -0
kaibo/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""kaibo: memory for AI agents. Four operations: remember, recall, explain, forget."""
|
|
2
|
+
|
|
3
|
+
from ._core import SDK_VERSION
|
|
4
|
+
from .async_client import AsyncKaibo
|
|
5
|
+
from .client import Kaibo
|
|
6
|
+
from .errors import ApiError, KaiboError, NetworkError, RateLimitError, TimeoutError, WebhookSignatureError
|
|
7
|
+
from .render import render_context
|
|
8
|
+
from .webhooks import verify_webhook
|
|
9
|
+
|
|
10
|
+
__version__ = SDK_VERSION
|
|
11
|
+
__all__ = [
|
|
12
|
+
"ApiError",
|
|
13
|
+
"AsyncKaibo",
|
|
14
|
+
"Kaibo",
|
|
15
|
+
"KaiboError",
|
|
16
|
+
"NetworkError",
|
|
17
|
+
"RateLimitError",
|
|
18
|
+
"TimeoutError",
|
|
19
|
+
"WebhookSignatureError",
|
|
20
|
+
"render_context",
|
|
21
|
+
"verify_webhook",
|
|
22
|
+
]
|
kaibo/_core.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import random
|
|
5
|
+
import uuid
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any, Iterable, Mapping, Optional
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from .errors import ApiError, KaiboError, RateLimitError
|
|
12
|
+
|
|
13
|
+
SDK_VERSION = "0.1.0"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def env_default(name: str) -> Optional[str]:
|
|
17
|
+
"""An environment variable, or None when unset or blank."""
|
|
18
|
+
import os
|
|
19
|
+
|
|
20
|
+
v = os.environ.get(name, "").strip()
|
|
21
|
+
return v or None
|
|
22
|
+
RETRYABLE_STATUS = frozenset({408, 425, 429, 500, 502, 503, 504})
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def backoff_seconds(attempt: int, rand: float, base: float = 0.25, cap: float = 8.0) -> float:
|
|
26
|
+
"""Exponential backoff with full jitter: uniform in [0, min(cap, base * 2**attempt)]."""
|
|
27
|
+
return float(rand * min(cap, base * (2**attempt)))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def new_idempotency_key() -> str:
|
|
31
|
+
return f"sdk-{uuid.uuid4()}"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class Prepared:
|
|
36
|
+
method: str
|
|
37
|
+
url: str
|
|
38
|
+
headers: dict[str, str]
|
|
39
|
+
json: Optional[Any]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Config:
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
api_key: str,
|
|
46
|
+
base_url: str,
|
|
47
|
+
project: Optional[str],
|
|
48
|
+
retries: int,
|
|
49
|
+
timeout: float,
|
|
50
|
+
) -> None:
|
|
51
|
+
if not api_key:
|
|
52
|
+
raise KaiboError(
|
|
53
|
+
"no api key. Pass api_key=... or set KAIBO_KEY "
|
|
54
|
+
"(keys look like kaibo_<id>.<secret>; create one in the dashboard, under api keys)."
|
|
55
|
+
)
|
|
56
|
+
if not base_url:
|
|
57
|
+
raise KaiboError(
|
|
58
|
+
"no address. Pass base_url=... or set KAIBO_URL to the address you open "
|
|
59
|
+
"the kaibo dashboard at (for example http://localhost:3000)."
|
|
60
|
+
)
|
|
61
|
+
self.api_key = api_key
|
|
62
|
+
self.base_url = base_url.rstrip("/")
|
|
63
|
+
self.project = project
|
|
64
|
+
self.retries = retries
|
|
65
|
+
self.timeout = timeout
|
|
66
|
+
|
|
67
|
+
def prepare(
|
|
68
|
+
self,
|
|
69
|
+
method: str,
|
|
70
|
+
path: str,
|
|
71
|
+
*,
|
|
72
|
+
query: Optional[Mapping[str, Any]] = None,
|
|
73
|
+
body: Optional[Any] = None,
|
|
74
|
+
accept: str = "application/json",
|
|
75
|
+
) -> Prepared:
|
|
76
|
+
q = {k: str(v) for k, v in (query or {}).items() if v is not None}
|
|
77
|
+
url = f"{self.base_url}{path}"
|
|
78
|
+
if q:
|
|
79
|
+
url += "?" + str(httpx.QueryParams(q))
|
|
80
|
+
headers = {
|
|
81
|
+
"accept": accept,
|
|
82
|
+
"authorization": f"Bearer {self.api_key}",
|
|
83
|
+
"x-sdk": f"kaibo-sdk-py/{SDK_VERSION}",
|
|
84
|
+
}
|
|
85
|
+
if self.project:
|
|
86
|
+
headers["x-project-id"] = self.project
|
|
87
|
+
if body is not None:
|
|
88
|
+
headers["content-type"] = "application/json"
|
|
89
|
+
return Prepared(method, url, headers, body)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def api_error(res: httpx.Response) -> ApiError:
|
|
93
|
+
code, message = "unknown", f"request failed ({res.status_code})"
|
|
94
|
+
request_id = res.headers.get("x-request-id")
|
|
95
|
+
try:
|
|
96
|
+
err = res.json().get("error")
|
|
97
|
+
if isinstance(err, dict):
|
|
98
|
+
code = str(err.get("code", code))
|
|
99
|
+
message = str(err.get("message", message))
|
|
100
|
+
request_id = err.get("requestId", request_id)
|
|
101
|
+
except Exception: # non-JSON error body
|
|
102
|
+
pass
|
|
103
|
+
if res.status_code == 429:
|
|
104
|
+
try:
|
|
105
|
+
ra = float(res.headers.get("retry-after", ""))
|
|
106
|
+
except ValueError:
|
|
107
|
+
ra = 0.0
|
|
108
|
+
return RateLimitError(message, request_id=request_id, retry_after=ra if ra > 0 else None)
|
|
109
|
+
return ApiError(message, code=code, status=res.status_code, request_id=request_id)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def retry_wait(err: ApiError, attempt: int, rand: float) -> float:
|
|
113
|
+
if isinstance(err, RateLimitError) and err.retry_after is not None:
|
|
114
|
+
return err.retry_after
|
|
115
|
+
return backoff_seconds(attempt, rand)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def jitter() -> float:
|
|
119
|
+
return random.random()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def parse_sse(lines: Iterable[str]) -> Iterable[tuple[str, str]]:
|
|
123
|
+
"""Yields (event, data) for each server-sent event in a stream of text lines."""
|
|
124
|
+
event: str = "message"
|
|
125
|
+
data: list[str] = []
|
|
126
|
+
for line in lines:
|
|
127
|
+
if line == "":
|
|
128
|
+
if data:
|
|
129
|
+
yield event, "\n".join(data)
|
|
130
|
+
event, data = "message", []
|
|
131
|
+
elif line.startswith(":"):
|
|
132
|
+
continue
|
|
133
|
+
elif line.startswith("event:"):
|
|
134
|
+
event = line[6:].strip()
|
|
135
|
+
elif line.startswith("data:"):
|
|
136
|
+
data.append(line[5:].lstrip(" "))
|
|
137
|
+
if data:
|
|
138
|
+
yield event, "\n".join(data)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def loads(text: str) -> Any:
|
|
142
|
+
return json.loads(text)
|
kaibo/async_client.py
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any, AsyncIterator, Awaitable, Callable, List, Optional, Sequence, Union
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from ._core import (
|
|
10
|
+
RETRYABLE_STATUS,
|
|
11
|
+
env_default,
|
|
12
|
+
Config,
|
|
13
|
+
Prepared,
|
|
14
|
+
api_error,
|
|
15
|
+
jitter,
|
|
16
|
+
loads,
|
|
17
|
+
parse_sse,
|
|
18
|
+
retry_wait,
|
|
19
|
+
)
|
|
20
|
+
from .client import (
|
|
21
|
+
_RECALL_EVENTS,
|
|
22
|
+
_export_body,
|
|
23
|
+
_export_record,
|
|
24
|
+
_forget_body,
|
|
25
|
+
_import_body,
|
|
26
|
+
_key_body,
|
|
27
|
+
_q,
|
|
28
|
+
_recall_body,
|
|
29
|
+
_remember_body,
|
|
30
|
+
)
|
|
31
|
+
from .render import render_context
|
|
32
|
+
from .errors import ApiError, KaiboError, NetworkError, TimeoutError
|
|
33
|
+
from .models import (
|
|
34
|
+
ApiKey,
|
|
35
|
+
MemoryRef,
|
|
36
|
+
Project,
|
|
37
|
+
ContextPack,
|
|
38
|
+
CreatedApiKey,
|
|
39
|
+
CreatedWebhook,
|
|
40
|
+
DeletionReceipt,
|
|
41
|
+
Explanation,
|
|
42
|
+
ExportRecord,
|
|
43
|
+
ForgetDryRun,
|
|
44
|
+
ImportResult,
|
|
45
|
+
ImportSource,
|
|
46
|
+
JobStatus,
|
|
47
|
+
KeyScope,
|
|
48
|
+
Memory,
|
|
49
|
+
MemoryStatus,
|
|
50
|
+
MemoryType,
|
|
51
|
+
RecallEvent,
|
|
52
|
+
ReceiptVerification,
|
|
53
|
+
RememberResult,
|
|
54
|
+
StoredEvent,
|
|
55
|
+
Usage,
|
|
56
|
+
Webhook,
|
|
57
|
+
WebhookDelivery,
|
|
58
|
+
WebhookEventType,
|
|
59
|
+
WhoAmI,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
ASleep = Callable[[float], Awaitable[None]]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class AsyncKaibo:
|
|
66
|
+
"""asyncio client with the same surface as `Kaibo`.
|
|
67
|
+
|
|
68
|
+
>>> async with AsyncKaibo(api_key=os.environ["KAIBO_KEY"]) as kaibo:
|
|
69
|
+
... await kaibo.remember(text="We ship on July 17.", wait=True)
|
|
70
|
+
... pack = await kaibo.recall("ship date")
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
api_key: Optional[str] = None,
|
|
76
|
+
*,
|
|
77
|
+
base_url: Optional[str] = None,
|
|
78
|
+
project: Optional[str] = None,
|
|
79
|
+
retries: int = 2,
|
|
80
|
+
timeout: float = 30.0,
|
|
81
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
|
82
|
+
sleep: ASleep = asyncio.sleep,
|
|
83
|
+
) -> None:
|
|
84
|
+
self._cfg = Config(
|
|
85
|
+
api_key or env_default("KAIBO_KEY") or "",
|
|
86
|
+
base_url or env_default("KAIBO_URL") or "",
|
|
87
|
+
project or env_default("KAIBO_PROJECT"),
|
|
88
|
+
retries,
|
|
89
|
+
timeout,
|
|
90
|
+
)
|
|
91
|
+
self._http = http_client or httpx.AsyncClient(timeout=timeout)
|
|
92
|
+
self._owns_http = http_client is None
|
|
93
|
+
self._sleep = sleep
|
|
94
|
+
self.events = _AEvents(self)
|
|
95
|
+
self.memories = _AMemories(self)
|
|
96
|
+
self.jobs = _AJobs(self)
|
|
97
|
+
self.webhooks = _AWebhooks(self)
|
|
98
|
+
self.imports = _AImports(self)
|
|
99
|
+
self.exports = _AExports(self)
|
|
100
|
+
self.keys = _AKeys(self)
|
|
101
|
+
self.projects = _AProjects(self)
|
|
102
|
+
|
|
103
|
+
async def aclose(self) -> None:
|
|
104
|
+
if self._owns_http:
|
|
105
|
+
await self._http.aclose()
|
|
106
|
+
|
|
107
|
+
async def __aenter__(self) -> "AsyncKaibo":
|
|
108
|
+
return self
|
|
109
|
+
|
|
110
|
+
async def __aexit__(self, *_: object) -> None:
|
|
111
|
+
await self.aclose()
|
|
112
|
+
|
|
113
|
+
async def _send(self, p: Prepared, *, retry: bool, stream: bool = False) -> httpx.Response:
|
|
114
|
+
attempts = (self._cfg.retries if retry else 0) + 1
|
|
115
|
+
last: Optional[Exception] = None
|
|
116
|
+
for attempt in range(attempts):
|
|
117
|
+
try:
|
|
118
|
+
req = self._http.build_request(
|
|
119
|
+
p.method, p.url, headers=p.headers, json=p.json, timeout=self._cfg.timeout
|
|
120
|
+
)
|
|
121
|
+
res = await self._http.send(req, stream=stream)
|
|
122
|
+
if res.is_success:
|
|
123
|
+
return res
|
|
124
|
+
if stream:
|
|
125
|
+
await res.aread()
|
|
126
|
+
err = api_error(res)
|
|
127
|
+
await res.aclose()
|
|
128
|
+
if res.status_code in RETRYABLE_STATUS and attempt < attempts - 1:
|
|
129
|
+
last = err
|
|
130
|
+
await self._sleep(retry_wait(err, attempt, jitter()))
|
|
131
|
+
continue
|
|
132
|
+
raise err
|
|
133
|
+
except ApiError:
|
|
134
|
+
raise
|
|
135
|
+
except httpx.TimeoutException as e:
|
|
136
|
+
last = TimeoutError(f"request timed out after {self._cfg.timeout}s")
|
|
137
|
+
last.__cause__ = e
|
|
138
|
+
except httpx.HTTPError as e:
|
|
139
|
+
last = NetworkError("network request failed")
|
|
140
|
+
last.__cause__ = e
|
|
141
|
+
if attempt >= attempts - 1:
|
|
142
|
+
raise last
|
|
143
|
+
await self._sleep(retry_wait(ApiError("", code="x", status=0, request_id=None), attempt, jitter()))
|
|
144
|
+
raise last or KaiboError("request failed")
|
|
145
|
+
|
|
146
|
+
async def _request(self, method: str, path: str, *, retry: bool, query: Any = None, body: Any = None) -> Any:
|
|
147
|
+
res = await self._send(self._cfg.prepare(method, path, query=query, body=body), retry=retry)
|
|
148
|
+
return None if res.status_code == 204 else res.json()
|
|
149
|
+
|
|
150
|
+
async def remember(
|
|
151
|
+
self,
|
|
152
|
+
text: Optional[str] = None,
|
|
153
|
+
*,
|
|
154
|
+
events: Optional[list[dict[str, Any]]] = None,
|
|
155
|
+
stream: Optional[str] = None,
|
|
156
|
+
idempotency_key: Optional[str] = None,
|
|
157
|
+
wait: Union[bool, float] = False,
|
|
158
|
+
) -> RememberResult:
|
|
159
|
+
body = _remember_body(text, events, stream, idempotency_key)
|
|
160
|
+
result = RememberResult.model_validate(await self._request("POST", "/v1/remember", body=body, retry=True))
|
|
161
|
+
if wait:
|
|
162
|
+
await self.jobs.wait(result.job_id, timeout=10.0 if wait is True else float(wait))
|
|
163
|
+
return result
|
|
164
|
+
|
|
165
|
+
async def recall(
|
|
166
|
+
self,
|
|
167
|
+
query: str,
|
|
168
|
+
*,
|
|
169
|
+
as_of: Optional[str] = None,
|
|
170
|
+
budget_tokens: Optional[int] = None,
|
|
171
|
+
types: Optional[Sequence[MemoryType]] = None,
|
|
172
|
+
scope: Optional[str] = None,
|
|
173
|
+
mode: Optional[str] = None,
|
|
174
|
+
min_trust: Optional[str] = None,
|
|
175
|
+
) -> ContextPack:
|
|
176
|
+
body = _recall_body(query, as_of, budget_tokens, types, scope, mode, min_trust)
|
|
177
|
+
return ContextPack.model_validate(await self._request("POST", "/v1/recall", body=body, retry=True))
|
|
178
|
+
|
|
179
|
+
async def context(self, query: str, **options: Any) -> str:
|
|
180
|
+
"""Recall, rendered as text for a prompt (see `Kaibo.context`)."""
|
|
181
|
+
return render_context(await self.recall(query, **options))
|
|
182
|
+
|
|
183
|
+
async def verify_pack(self, pack: ContextPack) -> dict[str, Any]:
|
|
184
|
+
body = {"pack": pack.model_dump(by_alias=True, mode="json", exclude_none=True)}
|
|
185
|
+
return await self._request("POST", "/v1/attest/verify", body=body, retry=True)
|
|
186
|
+
|
|
187
|
+
async def diff(self, from_: str, to: str) -> dict[str, Any]:
|
|
188
|
+
return await self._request("POST", "/v1/diff", body={"from": from_, "to": to}, retry=True)
|
|
189
|
+
|
|
190
|
+
async def verify_memory(self, memory_id: MemoryRef) -> dict[str, Any]:
|
|
191
|
+
return await self._request("POST", f"/v1/memories/{_q(memory_id)}/verify", retry=False)
|
|
192
|
+
|
|
193
|
+
async def recall_stream(
|
|
194
|
+
self,
|
|
195
|
+
query: str,
|
|
196
|
+
*,
|
|
197
|
+
as_of: Optional[str] = None,
|
|
198
|
+
budget_tokens: Optional[int] = None,
|
|
199
|
+
types: Optional[Sequence[MemoryType]] = None,
|
|
200
|
+
scope: Optional[str] = None,
|
|
201
|
+
) -> AsyncIterator[RecallEvent]:
|
|
202
|
+
body = _recall_body(query, as_of, budget_tokens, types, scope)
|
|
203
|
+
p = self._cfg.prepare("POST", "/v1/recall", body=body, accept="text/event-stream")
|
|
204
|
+
res = await self._send(p, retry=False, stream=True)
|
|
205
|
+
try:
|
|
206
|
+
lines: list[str] = []
|
|
207
|
+
async for line in res.aiter_lines():
|
|
208
|
+
lines.append(line)
|
|
209
|
+
if line == "":
|
|
210
|
+
for name, data in parse_sse(lines):
|
|
211
|
+
model = _RECALL_EVENTS.get(name)
|
|
212
|
+
if model:
|
|
213
|
+
yield model.model_validate(loads(data))
|
|
214
|
+
lines = []
|
|
215
|
+
finally:
|
|
216
|
+
await res.aclose()
|
|
217
|
+
|
|
218
|
+
async def explain(self, memory_id: MemoryRef, *, depth: Optional[int] = None) -> Explanation:
|
|
219
|
+
return Explanation.model_validate(
|
|
220
|
+
await self._request("GET", f"/v1/explain/{_q(memory_id)}", query={"depth": depth}, retry=True)
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
async def forget(
|
|
224
|
+
self, *, memory_id: Optional[str] = None, query: Optional[str] = None, reason: str
|
|
225
|
+
) -> DeletionReceipt:
|
|
226
|
+
body = _forget_body(memory_id, query, reason, False)
|
|
227
|
+
out = await self._request("POST", "/v1/forget", body=body, retry=False)
|
|
228
|
+
return DeletionReceipt.model_validate(out["receipt"])
|
|
229
|
+
|
|
230
|
+
async def forget_dry_run(
|
|
231
|
+
self, *, memory_id: Optional[str] = None, query: Optional[str] = None, reason: str
|
|
232
|
+
) -> ForgetDryRun:
|
|
233
|
+
body = _forget_body(memory_id, query, reason, True)
|
|
234
|
+
return ForgetDryRun.model_validate(await self._request("POST", "/v1/forget", body=body, retry=False))
|
|
235
|
+
|
|
236
|
+
async def verify_receipt(self, receipt_id: str) -> ReceiptVerification:
|
|
237
|
+
return ReceiptVerification.model_validate(
|
|
238
|
+
await self._request("GET", f"/v1/forget/receipts/{_q(receipt_id)}/verify", retry=True)
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
async def get_receipt(self, receipt_id: str) -> DeletionReceipt:
|
|
242
|
+
return DeletionReceipt.model_validate(
|
|
243
|
+
await self._request("GET", f"/v1/forget/receipts/{_q(receipt_id)}", retry=True)
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
async def whoami(self) -> WhoAmI:
|
|
247
|
+
return WhoAmI.model_validate(await self._request("GET", "/v1/whoami", retry=True))
|
|
248
|
+
|
|
249
|
+
async def usage(self) -> Usage:
|
|
250
|
+
return Usage.model_validate(await self._request("GET", "/v1/usage", retry=True))
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class _AEvents:
|
|
254
|
+
def __init__(self, c: AsyncKaibo) -> None:
|
|
255
|
+
self._c = c
|
|
256
|
+
|
|
257
|
+
async def get(self, event_id: str) -> StoredEvent:
|
|
258
|
+
return StoredEvent.model_validate(await self._c._request("GET", f"/v1/events/{_q(event_id)}", retry=True))
|
|
259
|
+
|
|
260
|
+
async def page(
|
|
261
|
+
self, *, stream: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[str] = None
|
|
262
|
+
) -> tuple[list[StoredEvent], Optional[str]]:
|
|
263
|
+
out = await self._c._request(
|
|
264
|
+
"GET", "/v1/events", query={"stream": stream, "limit": limit, "cursor": cursor}, retry=True
|
|
265
|
+
)
|
|
266
|
+
return [StoredEvent.model_validate(e) for e in out["items"]], out["nextCursor"]
|
|
267
|
+
|
|
268
|
+
async def iter(self, *, stream: str = "default", page_size: int = 100) -> AsyncIterator[StoredEvent]:
|
|
269
|
+
cursor: Optional[str] = None
|
|
270
|
+
while True:
|
|
271
|
+
items, cursor = await self.page(stream=stream, limit=page_size, cursor=cursor)
|
|
272
|
+
for e in items:
|
|
273
|
+
yield e
|
|
274
|
+
if not cursor:
|
|
275
|
+
return
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class _AMemories:
|
|
279
|
+
def __init__(self, c: AsyncKaibo) -> None:
|
|
280
|
+
self._c = c
|
|
281
|
+
|
|
282
|
+
async def get(self, memory_id: MemoryRef) -> Memory:
|
|
283
|
+
return Memory.model_validate(await self._c._request("GET", f"/v1/memories/{_q(memory_id)}", retry=True))
|
|
284
|
+
|
|
285
|
+
async def edit(self, memory_id: MemoryRef, content: str) -> Memory:
|
|
286
|
+
"""Replace what a current memory says. The old text stays as a superseded version. Needs `write`. Not retried."""
|
|
287
|
+
return Memory.model_validate(
|
|
288
|
+
await self._c._request("PATCH", f"/v1/memories/{_q(memory_id)}", body={"content": content}, retry=False)
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
async def page(
|
|
292
|
+
self, *, status: Optional[MemoryStatus] = None, limit: Optional[int] = None, cursor: Optional[str] = None
|
|
293
|
+
) -> tuple[list[Memory], Optional[str]]:
|
|
294
|
+
out = await self._c._request(
|
|
295
|
+
"GET", "/v1/memories", query={"status": status, "limit": limit, "cursor": cursor}, retry=True
|
|
296
|
+
)
|
|
297
|
+
return [Memory.model_validate(m) for m in out["items"]], out["nextCursor"]
|
|
298
|
+
|
|
299
|
+
async def iter(self, *, status: Optional[MemoryStatus] = None, page_size: int = 100) -> AsyncIterator[Memory]:
|
|
300
|
+
cursor: Optional[str] = None
|
|
301
|
+
while True:
|
|
302
|
+
items, cursor = await self.page(status=status, limit=page_size, cursor=cursor)
|
|
303
|
+
for m in items:
|
|
304
|
+
yield m
|
|
305
|
+
if not cursor:
|
|
306
|
+
return
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class _AJobs:
|
|
310
|
+
def __init__(self, c: AsyncKaibo) -> None:
|
|
311
|
+
self._c = c
|
|
312
|
+
|
|
313
|
+
async def get(self, job_id: str) -> JobStatus:
|
|
314
|
+
return JobStatus.model_validate(await self._c._request("GET", f"/v1/jobs/{_q(job_id)}", retry=True))
|
|
315
|
+
|
|
316
|
+
async def wait(self, job_id: str, *, timeout: float = 10.0, interval: float = 0.1) -> JobStatus:
|
|
317
|
+
deadline = time.monotonic() + timeout
|
|
318
|
+
delay = interval
|
|
319
|
+
while True:
|
|
320
|
+
s = await self.get(job_id)
|
|
321
|
+
if s.done:
|
|
322
|
+
return s
|
|
323
|
+
if time.monotonic() + delay > deadline:
|
|
324
|
+
raise TimeoutError(f"job {job_id} did not finish in time")
|
|
325
|
+
await self._c._sleep(delay)
|
|
326
|
+
delay = min(1.0, delay * 2)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class _AWebhooks:
|
|
330
|
+
def __init__(self, c: AsyncKaibo) -> None:
|
|
331
|
+
self._c = c
|
|
332
|
+
|
|
333
|
+
async def create(self, url: str, *, events: Optional[Sequence[WebhookEventType]] = None) -> CreatedWebhook:
|
|
334
|
+
body: dict[str, Any] = {"url": url}
|
|
335
|
+
if events is not None:
|
|
336
|
+
body["events"] = list(events)
|
|
337
|
+
return CreatedWebhook.model_validate(await self._c._request("POST", "/v1/webhooks", body=body, retry=False))
|
|
338
|
+
|
|
339
|
+
async def list(self) -> List[Webhook]:
|
|
340
|
+
return [Webhook.model_validate(w) for w in (await self._c._request("GET", "/v1/webhooks", retry=True))["items"]]
|
|
341
|
+
|
|
342
|
+
async def deliveries(self, webhook_id: str, *, limit: Optional[int] = None) -> List[WebhookDelivery]:
|
|
343
|
+
out = await self._c._request(
|
|
344
|
+
"GET", f"/v1/webhooks/{_q(webhook_id)}/deliveries", query={"limit": limit}, retry=True
|
|
345
|
+
)
|
|
346
|
+
return [WebhookDelivery.model_validate(d) for d in out["items"]]
|
|
347
|
+
|
|
348
|
+
async def test(self, webhook_id: str) -> None:
|
|
349
|
+
await self._c._request("POST", f"/v1/webhooks/{_q(webhook_id)}/test", retry=False)
|
|
350
|
+
|
|
351
|
+
async def delete(self, webhook_id: str) -> None:
|
|
352
|
+
await self._c._request("DELETE", f"/v1/webhooks/{_q(webhook_id)}", retry=False)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
class _AImports:
|
|
356
|
+
def __init__(self, c: AsyncKaibo) -> None:
|
|
357
|
+
self._c = c
|
|
358
|
+
|
|
359
|
+
async def create(
|
|
360
|
+
self,
|
|
361
|
+
source: ImportSource,
|
|
362
|
+
content: str,
|
|
363
|
+
*,
|
|
364
|
+
stream: Optional[str] = None,
|
|
365
|
+
name: Optional[str] = None,
|
|
366
|
+
include_invalid: bool = False,
|
|
367
|
+
dry_run: bool = False,
|
|
368
|
+
) -> ImportResult:
|
|
369
|
+
out = await self._c._request(
|
|
370
|
+
"POST",
|
|
371
|
+
"/v1/imports",
|
|
372
|
+
query={"dryRun": "true"} if dry_run else None,
|
|
373
|
+
body=_import_body(source, content, stream, name, include_invalid),
|
|
374
|
+
retry=True,
|
|
375
|
+
)
|
|
376
|
+
return ImportResult.model_validate(out)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
class _AExports:
|
|
380
|
+
def __init__(self, c: AsyncKaibo) -> None:
|
|
381
|
+
self._c = c
|
|
382
|
+
|
|
383
|
+
async def stream(
|
|
384
|
+
self,
|
|
385
|
+
*,
|
|
386
|
+
stream: Optional[str] = None,
|
|
387
|
+
include: Optional[Sequence[str]] = None,
|
|
388
|
+
memory_status: Optional[Sequence[str]] = None,
|
|
389
|
+
) -> AsyncIterator[ExportRecord]:
|
|
390
|
+
p = self._c._cfg.prepare("POST", "/v1/exports", body=_export_body(stream, include, memory_status))
|
|
391
|
+
res = await self._c._send(p, retry=False, stream=True)
|
|
392
|
+
ended = False
|
|
393
|
+
try:
|
|
394
|
+
async for line in res.aiter_lines():
|
|
395
|
+
if not line.strip():
|
|
396
|
+
continue
|
|
397
|
+
rec = _export_record(line)
|
|
398
|
+
ended = ended or rec.record == "end"
|
|
399
|
+
yield rec
|
|
400
|
+
finally:
|
|
401
|
+
await res.aclose()
|
|
402
|
+
if not ended:
|
|
403
|
+
raise KaiboError("the export was cut off before its end record")
|
|
404
|
+
|
|
405
|
+
async def create(
|
|
406
|
+
self,
|
|
407
|
+
*,
|
|
408
|
+
stream: Optional[str] = None,
|
|
409
|
+
include: Optional[Sequence[str]] = None,
|
|
410
|
+
memory_status: Optional[Sequence[str]] = None,
|
|
411
|
+
) -> List[ExportRecord]:
|
|
412
|
+
return [r async for r in self.stream(stream=stream, include=include, memory_status=memory_status)]
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
class _AProjects:
|
|
416
|
+
def __init__(self, c: AsyncKaibo) -> None:
|
|
417
|
+
self._c = c
|
|
418
|
+
|
|
419
|
+
async def list(self) -> List[Project]:
|
|
420
|
+
return [Project.model_validate(p) for p in (await self._c._request("GET", "/v1/projects", retry=True))["items"]]
|
|
421
|
+
|
|
422
|
+
async def create(self, name: str) -> Project:
|
|
423
|
+
return Project.model_validate(await self._c._request("POST", "/v1/projects", body={"name": name}, retry=False))
|
|
424
|
+
|
|
425
|
+
async def delete(self, project_id: str) -> None:
|
|
426
|
+
await self._c._request("DELETE", f"/v1/projects/{_q(project_id)}", retry=False)
|
|
427
|
+
|
|
428
|
+
async def rename(self, project_id: str, name: str) -> Project:
|
|
429
|
+
return Project.model_validate(
|
|
430
|
+
await self._c._request("PATCH", f"/v1/projects/{_q(project_id)}", body={"name": name}, retry=True)
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
class _AKeys:
|
|
435
|
+
def __init__(self, c: AsyncKaibo) -> None:
|
|
436
|
+
self._c = c
|
|
437
|
+
|
|
438
|
+
async def list(self) -> List[ApiKey]:
|
|
439
|
+
return [ApiKey.model_validate(k) for k in (await self._c._request("GET", "/v1/keys", retry=True))["items"]]
|
|
440
|
+
|
|
441
|
+
async def create(
|
|
442
|
+
self,
|
|
443
|
+
label: str,
|
|
444
|
+
scopes: Sequence[KeyScope],
|
|
445
|
+
*,
|
|
446
|
+
project_id: Optional[str] = None,
|
|
447
|
+
expires_at: Optional[str] = None,
|
|
448
|
+
) -> CreatedApiKey:
|
|
449
|
+
body = _key_body(label, scopes, project_id, expires_at)
|
|
450
|
+
return CreatedApiKey.model_validate(await self._c._request("POST", "/v1/keys", body=body, retry=False))
|
|
451
|
+
|
|
452
|
+
async def revoke(self, key_id: str) -> ApiKey:
|
|
453
|
+
return ApiKey.model_validate(await self._c._request("DELETE", f"/v1/keys/{_q(key_id)}", retry=True))
|