alexein 0.3.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.
- alexein/__init__.py +111 -0
- alexein/_async_user.py +259 -0
- alexein/_calls.py +200 -0
- alexein/_client.py +165 -0
- alexein/_errors.py +123 -0
- alexein/_http.py +172 -0
- alexein/_integration.py +115 -0
- alexein/_memories.py +206 -0
- alexein/_results.py +305 -0
- alexein/_types.py +45 -0
- alexein/_user.py +258 -0
- alexein/_version.py +1 -0
- alexein/_writes.py +154 -0
- alexein/crewai.py +334 -0
- alexein/langgraph.py +334 -0
- alexein/openai_agents.py +303 -0
- alexein/py.typed +0 -0
- alexein-0.3.0.dist-info/METADATA +285 -0
- alexein-0.3.0.dist-info/RECORD +21 -0
- alexein-0.3.0.dist-info/WHEEL +4 -0
- alexein-0.3.0.dist-info/licenses/LICENSE +21 -0
alexein/__init__.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Alexein for Python: long-term memory for your app users, behind one API key.
|
|
2
|
+
|
|
3
|
+
```python
|
|
4
|
+
from alexein import Memory
|
|
5
|
+
|
|
6
|
+
dana = Memory().user("dana")
|
|
7
|
+
dana.add("I moved to Lisbon for the job at Acme.")
|
|
8
|
+
print(dana.search("Where does Dana live?").context())
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The framework integrations are modules of their own, imported only when you import them:
|
|
12
|
+
`alexein.langgraph`, `alexein.openai_agents` and `alexein.crewai`.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from alexein._async_user import AsyncUser
|
|
16
|
+
from alexein._client import AsyncMemory, Memory
|
|
17
|
+
from alexein._errors import (
|
|
18
|
+
AlexeinError,
|
|
19
|
+
APIConnectionError,
|
|
20
|
+
APITimeoutError,
|
|
21
|
+
AuthenticationError,
|
|
22
|
+
FieldError,
|
|
23
|
+
InvalidRequestError,
|
|
24
|
+
NotFoundError,
|
|
25
|
+
PermissionDeniedError,
|
|
26
|
+
RateLimitError,
|
|
27
|
+
ServiceUnavailableError,
|
|
28
|
+
)
|
|
29
|
+
from alexein._memories import AnyMemory, Excerpt, Fact, Known, Memories, Opened, SearchResult
|
|
30
|
+
from alexein._results import (
|
|
31
|
+
AppUser,
|
|
32
|
+
ExportPage,
|
|
33
|
+
FactsPage,
|
|
34
|
+
FollowUp,
|
|
35
|
+
History,
|
|
36
|
+
ItemsPage,
|
|
37
|
+
Me,
|
|
38
|
+
OpenItem,
|
|
39
|
+
OpenItems,
|
|
40
|
+
Source,
|
|
41
|
+
Sources,
|
|
42
|
+
StoredItem,
|
|
43
|
+
Summary,
|
|
44
|
+
UsersPage,
|
|
45
|
+
Value,
|
|
46
|
+
)
|
|
47
|
+
from alexein._types import Author, Item
|
|
48
|
+
from alexein._user import User
|
|
49
|
+
from alexein._version import __version__
|
|
50
|
+
from alexein._writes import (
|
|
51
|
+
AddedItem,
|
|
52
|
+
AddResult,
|
|
53
|
+
Correction,
|
|
54
|
+
Deleted,
|
|
55
|
+
Erased,
|
|
56
|
+
Forgotten,
|
|
57
|
+
ItemStatus,
|
|
58
|
+
Statuses,
|
|
59
|
+
Versions,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
__all__ = [
|
|
63
|
+
"APIConnectionError",
|
|
64
|
+
"APITimeoutError",
|
|
65
|
+
"AddResult",
|
|
66
|
+
"AddedItem",
|
|
67
|
+
"AlexeinError",
|
|
68
|
+
"AnyMemory",
|
|
69
|
+
"AppUser",
|
|
70
|
+
"AsyncMemory",
|
|
71
|
+
"AsyncUser",
|
|
72
|
+
"AuthenticationError",
|
|
73
|
+
"Author",
|
|
74
|
+
"Correction",
|
|
75
|
+
"Deleted",
|
|
76
|
+
"Erased",
|
|
77
|
+
"Excerpt",
|
|
78
|
+
"ExportPage",
|
|
79
|
+
"Fact",
|
|
80
|
+
"FactsPage",
|
|
81
|
+
"FieldError",
|
|
82
|
+
"FollowUp",
|
|
83
|
+
"Forgotten",
|
|
84
|
+
"History",
|
|
85
|
+
"InvalidRequestError",
|
|
86
|
+
"Item",
|
|
87
|
+
"ItemStatus",
|
|
88
|
+
"ItemsPage",
|
|
89
|
+
"Known",
|
|
90
|
+
"Me",
|
|
91
|
+
"Memories",
|
|
92
|
+
"Memory",
|
|
93
|
+
"NotFoundError",
|
|
94
|
+
"OpenItem",
|
|
95
|
+
"OpenItems",
|
|
96
|
+
"Opened",
|
|
97
|
+
"PermissionDeniedError",
|
|
98
|
+
"RateLimitError",
|
|
99
|
+
"SearchResult",
|
|
100
|
+
"ServiceUnavailableError",
|
|
101
|
+
"Source",
|
|
102
|
+
"Sources",
|
|
103
|
+
"Statuses",
|
|
104
|
+
"StoredItem",
|
|
105
|
+
"Summary",
|
|
106
|
+
"User",
|
|
107
|
+
"UsersPage",
|
|
108
|
+
"Value",
|
|
109
|
+
"Versions",
|
|
110
|
+
"__version__",
|
|
111
|
+
]
|
alexein/_async_user.py
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""`AsyncUser`: `User` for asyncio, the same methods awaited. Kept beside `_user.py` line for
|
|
2
|
+
line, so the two read alike."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from collections.abc import AsyncIterator, Sequence
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from alexein import _calls
|
|
10
|
+
from alexein._errors import AlexeinError
|
|
11
|
+
from alexein._http import AsyncTransport, ride_out
|
|
12
|
+
from alexein._memories import Fact, Memories, Opened, SearchResult
|
|
13
|
+
from alexein._results import (
|
|
14
|
+
ExportPage,
|
|
15
|
+
FactsPage,
|
|
16
|
+
History,
|
|
17
|
+
ItemsPage,
|
|
18
|
+
OpenItems,
|
|
19
|
+
Sources,
|
|
20
|
+
StoredItem,
|
|
21
|
+
Summary,
|
|
22
|
+
)
|
|
23
|
+
from alexein._types import Author, Item, Json, Kinds
|
|
24
|
+
from alexein._writes import (
|
|
25
|
+
AddResult,
|
|
26
|
+
Correction,
|
|
27
|
+
Deleted,
|
|
28
|
+
Forgotten,
|
|
29
|
+
Statuses,
|
|
30
|
+
Versions,
|
|
31
|
+
merged,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AsyncUser:
|
|
36
|
+
"""`User`, for asyncio: the same methods, awaited; `iter_*` are async generators."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, transport: AsyncTransport, user_id: str, *, fail_open: bool = False) -> None:
|
|
39
|
+
self._transport = transport
|
|
40
|
+
self.id = user_id
|
|
41
|
+
self.fail_open = fail_open
|
|
42
|
+
|
|
43
|
+
def __repr__(self) -> str:
|
|
44
|
+
return f"AsyncUser({self.id!r})"
|
|
45
|
+
|
|
46
|
+
async def _send(self, call: _calls.Call[Any]) -> Any:
|
|
47
|
+
return await self._transport.send(call, self.id)
|
|
48
|
+
|
|
49
|
+
async def add(
|
|
50
|
+
self,
|
|
51
|
+
items: str | Item | Sequence[str | Item],
|
|
52
|
+
*,
|
|
53
|
+
wait: bool = False,
|
|
54
|
+
redact: bool = False,
|
|
55
|
+
kind: str | None = None,
|
|
56
|
+
session: str | None = None,
|
|
57
|
+
role: str | None = None,
|
|
58
|
+
author: str | Author | None = None,
|
|
59
|
+
title: str | None = None,
|
|
60
|
+
about: Sequence[str] | None = None,
|
|
61
|
+
expires: str | None = None,
|
|
62
|
+
at: str | None = None,
|
|
63
|
+
metadata: Json | None = None,
|
|
64
|
+
) -> AddResult:
|
|
65
|
+
"""Remember text: a string, an item dict, or a list of either. The keyword fields fill
|
|
66
|
+
every item that does not set its own. With `wait`, returns once they are searchable;
|
|
67
|
+
with `redact`, phone numbers, cards, keys and the like are replaced before memory keeps
|
|
68
|
+
any copy. More than 500 items go in batches; a retry never saves an item twice."""
|
|
69
|
+
defaults: Json = {
|
|
70
|
+
"kind": kind, "session": session, "role": role, "author": author, "title": title,
|
|
71
|
+
"about": list(about) if about is not None else None, "expires": expires, "at": at,
|
|
72
|
+
"metadata": metadata,
|
|
73
|
+
} # fmt: skip
|
|
74
|
+
calls = _calls.add(items, wait=wait, redact=redact, defaults=defaults)
|
|
75
|
+
if not calls:
|
|
76
|
+
return AddResult(items=[])
|
|
77
|
+
try:
|
|
78
|
+
return merged([await self._send(c) for c in calls])
|
|
79
|
+
except AlexeinError as error:
|
|
80
|
+
ride_out(error, self.fail_open, "add", self.id)
|
|
81
|
+
return AddResult(items=[], waiting=0, failed=True)
|
|
82
|
+
|
|
83
|
+
async def search(
|
|
84
|
+
self,
|
|
85
|
+
query: str,
|
|
86
|
+
*,
|
|
87
|
+
before: str | None = None,
|
|
88
|
+
after: str | None = None,
|
|
89
|
+
filters: Json | None = None,
|
|
90
|
+
provider: str | None = None,
|
|
91
|
+
source: str | None = None,
|
|
92
|
+
kind: Kinds | None = None,
|
|
93
|
+
fast: bool = False,
|
|
94
|
+
) -> SearchResult:
|
|
95
|
+
"""What memory holds for a question. `fast` answers sooner, a little less precisely, and
|
|
96
|
+
free. `before` and `after` are ISO dates; `filters`, `provider`, `source` and `kind`
|
|
97
|
+
narrow it to the facts and excerpts that match."""
|
|
98
|
+
call = _calls.search(
|
|
99
|
+
query, before=before, after=after, filters=filters, provider=provider,
|
|
100
|
+
source=source, kind=kind, fast=fast or None,
|
|
101
|
+
) # fmt: skip
|
|
102
|
+
try:
|
|
103
|
+
return await self._send(call)
|
|
104
|
+
except AlexeinError as error:
|
|
105
|
+
ride_out(error, self.fail_open, "search", self.id)
|
|
106
|
+
return SearchResult.unavailable(query)
|
|
107
|
+
|
|
108
|
+
async def grep(
|
|
109
|
+
self,
|
|
110
|
+
pattern: str,
|
|
111
|
+
*,
|
|
112
|
+
limit: int | None = None,
|
|
113
|
+
provider: str | None = None,
|
|
114
|
+
source: str | None = None,
|
|
115
|
+
kind: Kinds | None = None,
|
|
116
|
+
) -> Memories:
|
|
117
|
+
"""Excerpts holding an exact phrase, ignoring case."""
|
|
118
|
+
call = _calls.grep(pattern, limit=limit, provider=provider, source=source, kind=kind)
|
|
119
|
+
return await self._send(call)
|
|
120
|
+
|
|
121
|
+
async def latest(
|
|
122
|
+
self,
|
|
123
|
+
*,
|
|
124
|
+
k: int | None = None,
|
|
125
|
+
place: str | None = None,
|
|
126
|
+
after: str | None = None,
|
|
127
|
+
before: str | None = None,
|
|
128
|
+
filters: Json | None = None,
|
|
129
|
+
provider: str | None = None,
|
|
130
|
+
source: str | None = None,
|
|
131
|
+
kind: Kinds | None = None,
|
|
132
|
+
) -> Memories:
|
|
133
|
+
"""The most recent excerpts, newest first (10 by default): everywhere or in one `place`,
|
|
134
|
+
or everything between `after` and `before`."""
|
|
135
|
+
call = _calls.latest(
|
|
136
|
+
k=k, place=place, after=after, before=before, filters=filters, provider=provider,
|
|
137
|
+
source=source, kind=kind,
|
|
138
|
+
) # fmt: skip
|
|
139
|
+
return await self._send(call)
|
|
140
|
+
|
|
141
|
+
async def around(self, ref: str, *, width: int | None = None) -> Memories:
|
|
142
|
+
"""The excerpts just before and after one, from the same conversation, in order."""
|
|
143
|
+
return await self._send(_calls.around(ref, width=width))
|
|
144
|
+
|
|
145
|
+
async def open(self, ref: str) -> Opened:
|
|
146
|
+
"""One memory by its ref, whole: an excerpt in full, or a fact with the excerpts it was
|
|
147
|
+
learned from."""
|
|
148
|
+
return await self._send(_calls.open(ref))
|
|
149
|
+
|
|
150
|
+
async def history(self, subject: str, *, attribute: str | None = None) -> History:
|
|
151
|
+
"""Every value a subject was stated to have, and since when."""
|
|
152
|
+
return await self._send(_calls.history(subject, attribute=attribute))
|
|
153
|
+
|
|
154
|
+
async def summary(self) -> Summary:
|
|
155
|
+
"""What this memory holds, in brief."""
|
|
156
|
+
return await self._send(_calls.summary())
|
|
157
|
+
|
|
158
|
+
async def facts(self, *, limit: int | None = None, cursor: str | None = None) -> FactsPage:
|
|
159
|
+
"""A page of the facts memory learned, newest first (20 by default)."""
|
|
160
|
+
return await self._send(_calls.facts(limit=limit, cursor=cursor))
|
|
161
|
+
|
|
162
|
+
async def iter_facts(self, *, limit: int | None = None) -> AsyncIterator[Fact]:
|
|
163
|
+
"""Every fact memory learned, newest first, page after page."""
|
|
164
|
+
cursor: str | None = None
|
|
165
|
+
while True:
|
|
166
|
+
page = await self.facts(limit=limit, cursor=cursor)
|
|
167
|
+
for each in page.facts:
|
|
168
|
+
yield each
|
|
169
|
+
if not page.next:
|
|
170
|
+
return
|
|
171
|
+
cursor = page.next
|
|
172
|
+
|
|
173
|
+
async def open_items(
|
|
174
|
+
self,
|
|
175
|
+
*,
|
|
176
|
+
kind: Kinds | None = None,
|
|
177
|
+
provider: str | None = None,
|
|
178
|
+
place: str | None = None,
|
|
179
|
+
mine: bool | None = None,
|
|
180
|
+
within_days: int | None = None,
|
|
181
|
+
limit: int | None = None,
|
|
182
|
+
) -> OpenItems:
|
|
183
|
+
"""Issues and pull requests still open, newest first - by default the person's own -
|
|
184
|
+
and follow-ups nobody answered."""
|
|
185
|
+
call = _calls.open_items(
|
|
186
|
+
kind=kind, provider=provider, place=place, mine=mine, within_days=within_days,
|
|
187
|
+
limit=limit,
|
|
188
|
+
) # fmt: skip
|
|
189
|
+
return await self._send(call)
|
|
190
|
+
|
|
191
|
+
async def sources(self) -> Sources:
|
|
192
|
+
"""What each connected app delivered to this memory."""
|
|
193
|
+
return await self._send(_calls.sources())
|
|
194
|
+
|
|
195
|
+
async def items(
|
|
196
|
+
self, *, limit: int | None = None, cursor: str | None = None, filters: Json | None = None
|
|
197
|
+
) -> ItemsPage:
|
|
198
|
+
"""A page of the items added to this memory, newest first, as they were given."""
|
|
199
|
+
return await self._send(_calls.items(limit=limit, cursor=cursor, filters=filters))
|
|
200
|
+
|
|
201
|
+
async def iter_items(
|
|
202
|
+
self, *, limit: int | None = None, filters: Json | None = None
|
|
203
|
+
) -> AsyncIterator[StoredItem]:
|
|
204
|
+
"""Every item added to this memory, newest first, page after page."""
|
|
205
|
+
cursor: str | None = None
|
|
206
|
+
while True:
|
|
207
|
+
page = await self.items(limit=limit, cursor=cursor, filters=filters)
|
|
208
|
+
for each in page.items:
|
|
209
|
+
yield each
|
|
210
|
+
if not page.next:
|
|
211
|
+
return
|
|
212
|
+
cursor = page.next
|
|
213
|
+
|
|
214
|
+
async def item(self, item_id: str) -> StoredItem | None:
|
|
215
|
+
"""One added item by its id, whole; None for an id memory does not hold."""
|
|
216
|
+
return await self._send(_calls.item(item_id))
|
|
217
|
+
|
|
218
|
+
async def status(self, ids: Sequence[str]) -> Statuses:
|
|
219
|
+
"""How far each item has got: queued, stored, processed or unknown."""
|
|
220
|
+
return await self._send(_calls.status(ids))
|
|
221
|
+
|
|
222
|
+
async def forget(
|
|
223
|
+
self,
|
|
224
|
+
*,
|
|
225
|
+
items: Sequence[str] | None = None,
|
|
226
|
+
sessions: Sequence[str] | None = None,
|
|
227
|
+
refs: Sequence[str] | None = None,
|
|
228
|
+
) -> Forgotten:
|
|
229
|
+
"""Erase what is named - items by id, whole sessions, memories by ref - with every copy
|
|
230
|
+
and everything learned from it, for good. Needs a key that writes."""
|
|
231
|
+
return await self._send(_calls.forget(items=items, sessions=sessions, refs=refs))
|
|
232
|
+
|
|
233
|
+
async def correct(self, ref: str, replacement: str | None = None) -> Correction:
|
|
234
|
+
"""Retract one fact by its ref, and optionally say what is true instead."""
|
|
235
|
+
return await self._send(_calls.correct(ref, replacement))
|
|
236
|
+
|
|
237
|
+
async def versions(self, item_id: str) -> Versions:
|
|
238
|
+
"""Every version of one added item, and when and why it went. Needs a key that writes."""
|
|
239
|
+
return await self._send(_calls.versions(item_id))
|
|
240
|
+
|
|
241
|
+
async def export(self, *, limit: int | None = None, cursor: str | None = None) -> ExportPage:
|
|
242
|
+
"""A page of everything memory holds for this app user, for a person who asks for theirs."""
|
|
243
|
+
return await self._send(_calls.export(limit=limit, cursor=cursor))
|
|
244
|
+
|
|
245
|
+
async def iter_export(self, *, limit: int | None = None) -> AsyncIterator[Json]:
|
|
246
|
+
"""Every record of the export, page after page."""
|
|
247
|
+
cursor: str | None = None
|
|
248
|
+
while True:
|
|
249
|
+
page = await self.export(limit=limit, cursor=cursor)
|
|
250
|
+
for each in page.records:
|
|
251
|
+
yield each
|
|
252
|
+
if not page.next:
|
|
253
|
+
return
|
|
254
|
+
cursor = page.next
|
|
255
|
+
|
|
256
|
+
async def delete(self) -> Deleted:
|
|
257
|
+
"""Erase this app user whole: every item, fact and version. Not undone; their next add
|
|
258
|
+
starts a new memory. Needs a key that writes."""
|
|
259
|
+
return await self._send(_calls.delete())
|
alexein/_calls.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Each operation as the request it is: path, body and how to read the answer. Written once here
|
|
2
|
+
and sent by both clients, so the sync and async methods cannot drift apart on the wire."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
import uuid
|
|
8
|
+
from collections.abc import Callable, Iterable, Sequence
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Any, Generic, TypeVar, cast
|
|
11
|
+
|
|
12
|
+
from alexein._memories import Memories, Opened, SearchResult
|
|
13
|
+
from alexein._results import (
|
|
14
|
+
ExportPage,
|
|
15
|
+
FactsPage,
|
|
16
|
+
History,
|
|
17
|
+
ItemsPage,
|
|
18
|
+
Me,
|
|
19
|
+
OpenItems,
|
|
20
|
+
Sources,
|
|
21
|
+
StoredItem,
|
|
22
|
+
Summary,
|
|
23
|
+
UsersPage,
|
|
24
|
+
)
|
|
25
|
+
from alexein._types import Item, Json
|
|
26
|
+
from alexein._writes import (
|
|
27
|
+
AddResult,
|
|
28
|
+
Correction,
|
|
29
|
+
Deleted,
|
|
30
|
+
Forgotten,
|
|
31
|
+
Statuses,
|
|
32
|
+
Versions,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
T = TypeVar("T")
|
|
36
|
+
|
|
37
|
+
# The API takes at most this many items in one add; a longer list is sent in batches.
|
|
38
|
+
BATCH = 500
|
|
39
|
+
# What the API accepts as an app user id, checked here so a typo fails where it was made.
|
|
40
|
+
_USER_ID = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,34}")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class Call(Generic[T]):
|
|
45
|
+
"""One request: `user` is whether it names an app user in `X-Alexein-User`."""
|
|
46
|
+
|
|
47
|
+
method: str
|
|
48
|
+
path: str
|
|
49
|
+
parse: Callable[[Json], T]
|
|
50
|
+
body: Json | None = None
|
|
51
|
+
headers: dict[str, str] = field(default_factory=dict[str, str])
|
|
52
|
+
user: bool = True
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def checked_user(user_id: str) -> str:
|
|
56
|
+
if not isinstance(user_id, str) or not _USER_ID.fullmatch(user_id): # pyright: ignore[reportUnnecessaryIsInstance]
|
|
57
|
+
raise ValueError(
|
|
58
|
+
f"not a usable app user id: {user_id!r} (1 to 35 letters, digits, '.', '_' and "
|
|
59
|
+
"'-', starting with a letter or digit)"
|
|
60
|
+
)
|
|
61
|
+
return user_id
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _set(**fields: Any) -> Json:
|
|
65
|
+
"""The fields the caller set: None is not sent, so the API's defaults apply."""
|
|
66
|
+
return {k: _sent(v) for k, v in fields.items() if v is not None}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _sent(value: Any) -> Any:
|
|
70
|
+
"""A sequence as the JSON list it is sent as; anything else as it is."""
|
|
71
|
+
if isinstance(value, Sequence) and not isinstance(value, str):
|
|
72
|
+
return list(cast("Sequence[Any]", value))
|
|
73
|
+
return value
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _post(route: str, parse: Callable[[Json], T], body: Json | None = None) -> Call[T]:
|
|
77
|
+
return Call("POST", f"/v2/memory/{route}", parse, body or {})
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def me() -> Call[Me]:
|
|
81
|
+
return Call("GET", "/v2/me", Me.parse, user=False)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def users(*, limit: int | None, cursor: str | None) -> Call[UsersPage]:
|
|
85
|
+
call = _post("users", UsersPage.parse, _set(limit=limit, cursor=cursor))
|
|
86
|
+
return Call(call.method, call.path, call.parse, call.body, user=False)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def add(
|
|
90
|
+
items: str | Item | Sequence[str | Item],
|
|
91
|
+
*,
|
|
92
|
+
wait: bool,
|
|
93
|
+
redact: bool,
|
|
94
|
+
defaults: Json,
|
|
95
|
+
) -> list[Call[AddResult]]:
|
|
96
|
+
"""One call per batch of `BATCH` items, each with an `Idempotency-Key` of one uuid4 for the
|
|
97
|
+
whole add and the batch's index, so a retried batch is the same items again."""
|
|
98
|
+
given: Iterable[str | Item] = [items] if isinstance(items, (str, dict)) else items
|
|
99
|
+
shaped = [_item(i, _set(**defaults)) for i in given]
|
|
100
|
+
key = str(uuid.uuid4())
|
|
101
|
+
options = _set(wait=wait or None, redact=redact or None)
|
|
102
|
+
return [
|
|
103
|
+
Call(
|
|
104
|
+
"POST",
|
|
105
|
+
"/v2/memory/add",
|
|
106
|
+
AddResult.parse,
|
|
107
|
+
{"items": shaped[at : at + BATCH], **options},
|
|
108
|
+
{"Idempotency-Key": f"{key}-{at // BATCH}"},
|
|
109
|
+
)
|
|
110
|
+
for at in range(0, len(shaped), BATCH)
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _item(given: str | Item, defaults: Json) -> Json:
|
|
115
|
+
item: Json = {"text": given} if isinstance(given, str) else dict(given)
|
|
116
|
+
if not isinstance(item.get("text"), str) or not item["text"]:
|
|
117
|
+
raise ValueError(f"an item needs text: {given!r}")
|
|
118
|
+
return defaults | item
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def search(query: str, **narrowed: Any) -> Call[SearchResult]:
|
|
122
|
+
return _post("search", SearchResult.parse, _set(query=query, **narrowed))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def grep(pattern: str, **narrowed: Any) -> Call[Memories]:
|
|
126
|
+
return _post("grep", Memories.parse, _set(pattern=pattern, **narrowed))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def latest(**narrowed: Any) -> Call[Memories]:
|
|
130
|
+
return _post("latest", Memories.parse, _set(**narrowed))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def around(ref: str, *, width: int | None) -> Call[Memories]:
|
|
134
|
+
return _post("around", Memories.parse, _set(ref=ref, width=width))
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def open(ref: str) -> Call[Opened]:
|
|
138
|
+
return _post("open", Opened.parse, {"ref": ref})
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def history(subject: str, *, attribute: str | None) -> Call[History]:
|
|
142
|
+
return _post("history", History.parse, _set(subject=subject, attribute=attribute))
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def summary() -> Call[Summary]:
|
|
146
|
+
return _post("summary", Summary.parse)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def facts(*, limit: int | None, cursor: str | None) -> Call[FactsPage]:
|
|
150
|
+
return _post("facts", FactsPage.parse, _set(limit=limit, cursor=cursor))
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def open_items(**narrowed: Any) -> Call[OpenItems]:
|
|
154
|
+
return _post("open_items", OpenItems.parse, _set(**narrowed))
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def sources() -> Call[Sources]:
|
|
158
|
+
return _post("sources", Sources.parse)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def items(*, limit: int | None, cursor: str | None, filters: Json | None) -> Call[ItemsPage]:
|
|
162
|
+
return _post("items", ItemsPage.parse, _set(limit=limit, cursor=cursor, filters=filters))
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _found(raw: Json) -> StoredItem | None:
|
|
166
|
+
return StoredItem.parse(raw["item"]) if raw.get("found") and raw.get("item") else None
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def item(item_id: str) -> Call[StoredItem | None]:
|
|
170
|
+
return _post("item", _found, {"id": item_id})
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def status(ids: Sequence[str]) -> Call[Statuses]:
|
|
174
|
+
return _post("status", Statuses.parse, {"items": list(ids)})
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def forget(
|
|
178
|
+
*, items: Sequence[str] | None, sessions: Sequence[str] | None, refs: Sequence[str] | None
|
|
179
|
+
) -> Call[Forgotten]:
|
|
180
|
+
named = {"items": items, "sessions": sessions, "refs": refs}
|
|
181
|
+
body: Json = {k: list(v) for k, v in named.items() if v}
|
|
182
|
+
if not body:
|
|
183
|
+
raise ValueError("forget needs at least one of items, sessions or refs")
|
|
184
|
+
return _post("forget", Forgotten.parse, body)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def correct(ref: str, replacement: str | None) -> Call[Correction]:
|
|
188
|
+
return _post("correct", Correction.parse, _set(ref=ref, replacement=replacement))
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def versions(item_id: str) -> Call[Versions]:
|
|
192
|
+
return _post("versions", Versions.parse, {"id": item_id})
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def export(*, limit: int | None, cursor: str | None) -> Call[ExportPage]:
|
|
196
|
+
return _post("export", ExportPage.parse, _set(limit=limit, cursor=cursor))
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def delete() -> Call[Deleted]:
|
|
200
|
+
return _post("users/delete", Deleted.parse)
|