alexein 0.3.0__tar.gz

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.
@@ -0,0 +1,6 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ dist/
5
+ .pytest_cache/
6
+ .ruff_cache/
@@ -0,0 +1,37 @@
1
+ # Changelog
2
+
3
+ ## 0.3.0 - 2026-11-04
4
+
5
+ The client is `alexein` now (`pip install alexein`, `import alexein`), at the API's own version, and speaks
6
+ its `/v2` API.
7
+
8
+ - Renamed from `twinmemory`. The key is read from `ALEXEIN_API_KEY` and the host from `ALEXEIN_API_URL`; every
9
+ error is an `AlexeinError`; the logger is `alexein`; the `User-Agent` is `alexein-python/0.3.0`.
10
+ - The `/v2` API: an app user is named by `X-Alexein-User`, and every memory has a ref. Search answers
11
+ `memories` in order (`Known`, `Fact`, `Excerpt`), `notes` and `answerable`; `open(ref)` reads one whole,
12
+ where `read`, `ls` and `lineage` were; `summary()` replaces `overview()`; `forget` takes `refs`;
13
+ `correct` takes a ref; `status` answers queued, stored, processed or unknown.
14
+ - New: `facts`, `items`, `item`, `open_items`, `sources`, `export`, `delete`, and `memory.users()`, with
15
+ `iter_facts`, `iter_items`, `iter_export` and `iter_users` to follow every page; `add(redact=True)`.
16
+ - A 422 names each field it refused in `AlexeinError.errors`; every 5xx is a `ServiceUnavailableError`.
17
+ - A base URL given with a trailing `/` or `/v2` names the same host.
18
+ - Integrations, as extras: `alexein.langgraph` (`alexein[langgraph]`), `alexein.openai_agents`
19
+ (`alexein[openai-agents]`) and `alexein.crewai` (`alexein[crewai]`), each offering a model `memory_search`,
20
+ `memory_open` and `memory_add`. `import alexein` imports none of them.
21
+ - Every call sends an `X-Request-Id` of its own (`req_...`), kept across its retries, and every error
22
+ carries it as `request_id` - even a 500 a proxy answered, or no answer at all - so it can be found in
23
+ the console's request logs.
24
+
25
+ ## 0.2.0 - unreleased
26
+
27
+ - `Memory(fail_open=True)`: an outage - memory unreachable, timing out, or answering 429 or 5xx after
28
+ retries - answers `search` empty and skips `add`, with `failed=True` and a warning logged, instead of
29
+ raising. A wrong key or request still raises.
30
+ - `outage` on every error: whether it means memory is unavailable.
31
+ - `Memory(integration=...)`: a word added to the `User-Agent`, counted in the request log.
32
+ - The default base URL is `https://api.alexein.ai`.
33
+
34
+ ## 0.1.0 - unreleased
35
+
36
+ First release: `Memory` and `AsyncMemory`, typed results, one error class per failure, retries on 429 and 503
37
+ honouring `Retry-After`, and an `Idempotency-Key` on every add.
alexein-0.3.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alexein AI
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.
alexein-0.3.0/PKG-INFO ADDED
@@ -0,0 +1,285 @@
1
+ Metadata-Version: 2.5
2
+ Name: alexein
3
+ Version: 0.3.0
4
+ Summary: Python client for Alexein: long-term memory for your app users and agents, with LangGraph, OpenAI Agents SDK and CrewAI integrations.
5
+ Project-URL: Homepage, https://alexein.ai
6
+ Project-URL: Documentation, https://docs.alexein.ai/sdks/python
7
+ Project-URL: Repository, https://github.com/Alexein-AI/sdk
8
+ Project-URL: Changelog, https://github.com/Alexein-AI/sdk/blob/main/python/CHANGELOG.md
9
+ Author: Alexein AI
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: agents,ai,crewai,langgraph,llm,memory,openai-agents,rag
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: httpx<1,>=0.27
23
+ Provides-Extra: crewai
24
+ Requires-Dist: crewai<1.16,>=1.15; extra == 'crewai'
25
+ Provides-Extra: langgraph
26
+ Requires-Dist: langchain>=1; extra == 'langgraph'
27
+ Requires-Dist: langgraph>=1.2; extra == 'langgraph'
28
+ Provides-Extra: openai-agents
29
+ Requires-Dist: openai-agents>=0.24; extra == 'openai-agents'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # Alexein for Python
33
+
34
+ Long-term memory for your app users and agents. Add what they say; search it later in plain language.
35
+
36
+ ```bash
37
+ pip install alexein
38
+ ```
39
+
40
+ Python 3.10 or newer. One dependency: `httpx`. The framework integrations are extras:
41
+ `alexein[langgraph]`, `alexein[openai-agents]`, `alexein[crewai]`.
42
+
43
+ ## Quickstart
44
+
45
+ ```python
46
+ from alexein import Memory
47
+
48
+ memory = Memory() # reads ALEXEIN_API_KEY
49
+ dana = memory.user("dana") # your own id for one of your app users
50
+
51
+ dana.add("I moved to Lisbon for the job at Acme.", role="user", session="onboarding")
52
+ dana.add({"id": "diet", "kind": "note", "text": "Vegetarian, allergic to peanuts."}, wait=True)
53
+
54
+ result = dana.search("Where does Dana live?")
55
+ print(result.context()) # ready to paste into a prompt
56
+ ```
57
+
58
+ Make a key in the console, under **API keys** (https://app.alexein.ai/console/keys). A key belongs to one
59
+ project; each app user you name with `memory.user(...)` has a memory of their own inside it. An app user id is
60
+ 1 to 35 letters, digits, `.`, `_` or `-`, starting with a letter or digit; another raises `ValueError` where
61
+ you named it.
62
+
63
+ ## Settings
64
+
65
+ | Setting | Argument | Environment variable | Default |
66
+ | ----------- | ------------- | -------------------- | ---------------------------- |
67
+ | API key | `api_key` | `ALEXEIN_API_KEY` | required |
68
+ | Base URL | `base_url` | `ALEXEIN_API_URL` | `https://api.alexein.ai` |
69
+ | Timeout | `timeout` | | 60 seconds per attempt |
70
+ | Retries | `max_retries` | | 2 |
71
+ | Fail open | `fail_open` | | `False` |
72
+ | Integration | `integration` | | none |
73
+ | HTTP client | `http_client` | | an `httpx.Client` of its own |
74
+
75
+ The base URL is the host: the client adds `/v2`, and `http://localhost:8210/v2/` means the same host as
76
+ `http://localhost:8210`. `integration="my-app"` adds one lowercase word to the `User-Agent`
77
+ (`alexein-python/0.3.0 my-app`), so the console's request logs show where requests came from.
78
+
79
+ ## Adding
80
+
81
+ `add` takes a string, an item dict, or a list of either. Keyword fields fill every item that does not set its
82
+ own.
83
+
84
+ ```python
85
+ dana.add(
86
+ [
87
+ {"text": "Can you book the same hotel as last time?", "role": "user"},
88
+ {"text": "Booked the Memmo Alfama again for 3-5 May.", "role": "assistant"},
89
+ ],
90
+ session="trip-may",
91
+ )
92
+ dana.add("Moving house on 1 December.", kind="note", expires="2026-12-31")
93
+ ```
94
+
95
+ | Item field | Meaning |
96
+ | ---------- | --------------------------------------------------------- |
97
+ | `text` | required |
98
+ | `kind` | `message` (default), `note`, or `document` |
99
+ | `id` | your id for it; the same id again is an edit of that item |
100
+ | `session` | groups messages into one conversation |
101
+ | `role` | `user`, `assistant`, ... |
102
+ | `author` | a name, or `{"id", "name", "email", "handle", "self"}` |
103
+ | `title` | a document's title |
104
+ | `about` | work it names, as `kind:name` (`project:atlas`) |
105
+ | `at` | when it was said, an ISO date or time |
106
+ | `expires` | when it stops being true; it is erased then |
107
+ | `metadata` | your own JSON |
108
+
109
+ An add is accepted at once and searchable a moment later; `wait=True` returns once it is searchable.
110
+ `redact=True` replaces phone numbers, cards, keys and the like before memory keeps any copy. Lists longer than
111
+ 500 items go in batches, and every request carries an `Idempotency-Key` that a retry repeats, so a retried add
112
+ never saves an item twice.
113
+
114
+ ## Searching
115
+
116
+ ```python
117
+ result = dana.search("What should I avoid cooking for Dana?")
118
+ result.notes # guidance for the model reading the answer
119
+ result.memories # in order: known lines, then facts, then excerpts
120
+ result.answerable # False when memory does not seem to hold the answer; None when not checked
121
+ result.refs # the ref of every fact and excerpt
122
+ result.context(max_chars=4000)
123
+ ```
124
+
125
+ Each memory is a `Known(text)`, a `Fact(ref, text, source)` or an `Excerpt(ref, text, date, added_at, session,
126
+ roles)`; `result.known`, `.facts` and `.excerpts` hold each kind. `dana.open(ref)` reads one whole: an
127
+ excerpt in full, or a fact with the excerpts it was learned from.
128
+
129
+ `context()` is the notes, a blank line, then each memory as `- <text>` (an excerpt after its date), cut at a
130
+ whole memory so it never exceeds `max_chars`; empty when memory found nothing.
131
+
132
+ `fast=True` answers sooner, a little less precisely, and free: use it where a reply cannot wait, like a voice
133
+ agent's turn. `before` and `after` (ISO dates), `filters`, `provider`, `source` and `kind` narrow a search.
134
+
135
+ ## Everything else
136
+
137
+ | Method | What it answers |
138
+ | ------------------------------------------------------------- | ------------------------------------------------- |
139
+ | `memory.me()` | the key: organisation, project, whether it writes |
140
+ | `memory.users(limit=, cursor=)` / `memory.iter_users()` | the project's app users with memory |
141
+ | `user.grep(pattern, limit=)` | excerpts holding an exact phrase |
142
+ | `user.latest(k=10, place=, after=, before=)` | the most recent excerpts |
143
+ | `user.around(ref, width=2)` | the excerpts either side of one |
144
+ | `user.open(ref)` | one memory whole |
145
+ | `user.history(subject, attribute=None)` | every value a subject was stated to have |
146
+ | `user.summary()` | counts, dates, connected apps and the known lines |
147
+ | `user.facts(limit=, cursor=)` / `user.iter_facts()` | the facts memory learned, newest first |
148
+ | `user.open_items(...)` | open issues and pull requests, and follow-ups |
149
+ | `user.sources()` | what each connected app delivered |
150
+ | `user.items(limit=, cursor=, filters=)` / `user.iter_items()` | the items you added, as you added them |
151
+ | `user.item(item_id)` | one item whole, or `None` |
152
+ | `user.status(ids)` | queued, stored, processed or unknown, per item |
153
+ | `user.forget(items=, sessions=, refs=)` | erase every copy, for good; returns a receipt |
154
+ | `user.correct(ref, replacement=None)` | retract one fact, optionally saying what is true |
155
+ | `user.versions(item_id)` | an item's versions, and when and why it went |
156
+ | `user.export(limit=, cursor=)` / `user.iter_export()` | everything memory holds for this app user |
157
+ | `user.delete()` | erase this app user whole |
158
+
159
+ Arguments left out are not sent, so the API's defaults apply. Every result is a frozen dataclass, and `.raw`
160
+ holds the JSON as the API sent it. The `iter_*` methods follow `next` page by page.
161
+
162
+ ## Async
163
+
164
+ ```python
165
+ from alexein import AsyncMemory
166
+
167
+ async with AsyncMemory() as memory:
168
+ dana = memory.user("dana")
169
+ await dana.add("Prefers green tea.")
170
+ result = await dana.search("What does Dana drink?", fast=True)
171
+ async for fact in dana.iter_facts():
172
+ print(fact.text)
173
+ ```
174
+
175
+ ## Errors
176
+
177
+ Every error is an `AlexeinError` with `status` (None when no response came), `detail` (the API's sentence),
178
+ `errors` (on a 422, each field as `FieldError(field, message)`), `request_id`, and `outage`: whether memory is
179
+ unavailable rather than the request or the key being wrong.
180
+
181
+ | Error | When |
182
+ | ------------------------- | ---------------------------------------------------------------------- |
183
+ | `AuthenticationError` | 401: no key, or one the service did not issue |
184
+ | `PermissionDeniedError` | 403: the key reads, and the call writes |
185
+ | `NotFoundError` | 404: nothing added for this app user yet, or a ref memory did not give |
186
+ | `InvalidRequestError` | 400, 413, 422 and any other 4xx |
187
+ | `RateLimitError` | 429, after retries |
188
+ | `ServiceUnavailableError` | 5xx, after retries |
189
+ | `APIConnectionError` | the service could not be reached, after retries |
190
+ | `APITimeoutError` | the service did not answer in time, after retries |
191
+
192
+ 429, 503 and connection failures are retried up to `max_retries` times, waiting as `Retry-After` says (one
193
+ over 60 seconds is raised at once) or backing off from half a second. Nothing else is retried.
194
+
195
+ ### Failing open
196
+
197
+ Memory being down should cost a reply its memory, not the reply. With `Memory(fail_open=True)`, an outage -
198
+ memory unreachable, timing out, or answering 429 or 5xx after its retries - does not raise from `search` or
199
+ `add`: `search` answers an empty result with `failed=True`, `add` saves nothing and answers `failed=True`, and
200
+ each logs a warning on the `alexein` logger. Every other method, and every other error (a wrong key, a wrong
201
+ request), still raises. Pair it with a short `timeout` where a reply cannot wait.
202
+
203
+ ## LangGraph
204
+
205
+ ```bash
206
+ pip install "alexein[langgraph]"
207
+ ```
208
+
209
+ ```python
210
+ from alexein.langgraph import AlexeinMemory, MemoryContext, MemoryState
211
+
212
+ memory = AlexeinMemory() # or AlexeinMemory(client, fast=False, max_chars=4000, strict=False)
213
+
214
+ builder = StateGraph(MemoryState, context_schema=MemoryContext)
215
+ builder.add_node("recall", memory.recall) # what memory holds for the latest message
216
+ builder.add_node("agent", agent) # put memory.prompt(state) in its system prompt
217
+ builder.add_node("remember", memory.remember) # save the turn
218
+ builder.add_edge(START, "recall")
219
+ builder.add_edge("recall", "agent")
220
+ builder.add_edge("agent", "remember")
221
+ graph = builder.compile(checkpointer=InMemorySaver())
222
+
223
+ graph.invoke(inputs, config={"configurable": {"thread_id": "t-1"}}, context={"user_id": "dana"})
224
+ ```
225
+
226
+ Or give a model tools to call when it decides to: `create_agent(model, tools=memory.tools(write=True),
227
+ context_schema=MemoryContext)` offers `memory_search`, `memory_open` and `memory_add`.
228
+
229
+ Whose memory is read is decided by your code, never by the model: the user id comes from the run's `context`
230
+ (a tool also reads `config["configurable"]["user_id"]`), and a run without one raises a `ValueError` saying how to pass
231
+ it. Each message is saved under its LangGraph id, so a turn run again edits rather than duplicates; the thread
232
+ id is the session. Tool calls and tool results are not saved. The nodes and tools run under `invoke` and
233
+ `ainvoke`; with an `AsyncMemory`, add `memory.arecall` and `memory.aremember`. Module-level `recall`,
234
+ `remember`, `memory_prompt` and `memory_tools` use one default memory. See `examples/langgraph_agent.py`.
235
+
236
+ ## OpenAI Agents SDK
237
+
238
+ ```bash
239
+ pip install "alexein[openai-agents]"
240
+ ```
241
+
242
+ ```python
243
+ from alexein.openai_agents import AlexeinMemory, MemoryContext
244
+
245
+ memory = AlexeinMemory()
246
+ agent = Agent(name="Assistant", tools=memory.tools())
247
+ result = await Runner.run(
248
+ agent,
249
+ message,
250
+ context=MemoryContext(user_id="dana", session_id="conv-7"),
251
+ run_config=RunConfig(call_model_input_filter=memory.recall),
252
+ )
253
+ await memory.remember(result)
254
+ ```
255
+
256
+ `recall` searches once a turn and adds what it finds to the instructions of every model call in that turn, so
257
+ it never lands in your session. `remember` saves the user's message and the final answer under the agent's
258
+ name. Any context with a `user_id` will do.
259
+
260
+ ## CrewAI
261
+
262
+ ```bash
263
+ pip install "alexein[crewai]"
264
+ ```
265
+
266
+ ```python
267
+ from alexein.crewai import AlexeinMemory
268
+
269
+ memory = AlexeinMemory()
270
+ crew = Crew(agents=..., tasks=..., memory=memory, before_kickoff_callbacks=[memory.whose])
271
+ crew.kickoff(inputs={"user_id": "dana", "city": "Porto"})
272
+ ```
273
+
274
+ `AlexeinMemory` is CrewAI's own memory, kept in Alexein: CrewAI recalls the user's memory before every task and
275
+ saves what each task found. `whose` takes the user from the kickoff's inputs and checks the key once; a run
276
+ that names no user stops before any task. For tools with the user fixed by your code,
277
+ `Agent(..., tools=memory.tools("dana"))`, and `memory.save_result("dana", output)` after a run.
278
+
279
+ In every integration, a memory outage answers the model "Memory is unavailable right now; carry on without it."
280
+ and lets the run go on, unless `strict=True`. A wrong key raises either way. Each makes its own client on first
281
+ use, failing open with a 10 second timeout, so importing one needs no key.
282
+
283
+ ## License
284
+
285
+ MIT
@@ -0,0 +1,254 @@
1
+ # Alexein for Python
2
+
3
+ Long-term memory for your app users and agents. Add what they say; search it later in plain language.
4
+
5
+ ```bash
6
+ pip install alexein
7
+ ```
8
+
9
+ Python 3.10 or newer. One dependency: `httpx`. The framework integrations are extras:
10
+ `alexein[langgraph]`, `alexein[openai-agents]`, `alexein[crewai]`.
11
+
12
+ ## Quickstart
13
+
14
+ ```python
15
+ from alexein import Memory
16
+
17
+ memory = Memory() # reads ALEXEIN_API_KEY
18
+ dana = memory.user("dana") # your own id for one of your app users
19
+
20
+ dana.add("I moved to Lisbon for the job at Acme.", role="user", session="onboarding")
21
+ dana.add({"id": "diet", "kind": "note", "text": "Vegetarian, allergic to peanuts."}, wait=True)
22
+
23
+ result = dana.search("Where does Dana live?")
24
+ print(result.context()) # ready to paste into a prompt
25
+ ```
26
+
27
+ Make a key in the console, under **API keys** (https://app.alexein.ai/console/keys). A key belongs to one
28
+ project; each app user you name with `memory.user(...)` has a memory of their own inside it. An app user id is
29
+ 1 to 35 letters, digits, `.`, `_` or `-`, starting with a letter or digit; another raises `ValueError` where
30
+ you named it.
31
+
32
+ ## Settings
33
+
34
+ | Setting | Argument | Environment variable | Default |
35
+ | ----------- | ------------- | -------------------- | ---------------------------- |
36
+ | API key | `api_key` | `ALEXEIN_API_KEY` | required |
37
+ | Base URL | `base_url` | `ALEXEIN_API_URL` | `https://api.alexein.ai` |
38
+ | Timeout | `timeout` | | 60 seconds per attempt |
39
+ | Retries | `max_retries` | | 2 |
40
+ | Fail open | `fail_open` | | `False` |
41
+ | Integration | `integration` | | none |
42
+ | HTTP client | `http_client` | | an `httpx.Client` of its own |
43
+
44
+ The base URL is the host: the client adds `/v2`, and `http://localhost:8210/v2/` means the same host as
45
+ `http://localhost:8210`. `integration="my-app"` adds one lowercase word to the `User-Agent`
46
+ (`alexein-python/0.3.0 my-app`), so the console's request logs show where requests came from.
47
+
48
+ ## Adding
49
+
50
+ `add` takes a string, an item dict, or a list of either. Keyword fields fill every item that does not set its
51
+ own.
52
+
53
+ ```python
54
+ dana.add(
55
+ [
56
+ {"text": "Can you book the same hotel as last time?", "role": "user"},
57
+ {"text": "Booked the Memmo Alfama again for 3-5 May.", "role": "assistant"},
58
+ ],
59
+ session="trip-may",
60
+ )
61
+ dana.add("Moving house on 1 December.", kind="note", expires="2026-12-31")
62
+ ```
63
+
64
+ | Item field | Meaning |
65
+ | ---------- | --------------------------------------------------------- |
66
+ | `text` | required |
67
+ | `kind` | `message` (default), `note`, or `document` |
68
+ | `id` | your id for it; the same id again is an edit of that item |
69
+ | `session` | groups messages into one conversation |
70
+ | `role` | `user`, `assistant`, ... |
71
+ | `author` | a name, or `{"id", "name", "email", "handle", "self"}` |
72
+ | `title` | a document's title |
73
+ | `about` | work it names, as `kind:name` (`project:atlas`) |
74
+ | `at` | when it was said, an ISO date or time |
75
+ | `expires` | when it stops being true; it is erased then |
76
+ | `metadata` | your own JSON |
77
+
78
+ An add is accepted at once and searchable a moment later; `wait=True` returns once it is searchable.
79
+ `redact=True` replaces phone numbers, cards, keys and the like before memory keeps any copy. Lists longer than
80
+ 500 items go in batches, and every request carries an `Idempotency-Key` that a retry repeats, so a retried add
81
+ never saves an item twice.
82
+
83
+ ## Searching
84
+
85
+ ```python
86
+ result = dana.search("What should I avoid cooking for Dana?")
87
+ result.notes # guidance for the model reading the answer
88
+ result.memories # in order: known lines, then facts, then excerpts
89
+ result.answerable # False when memory does not seem to hold the answer; None when not checked
90
+ result.refs # the ref of every fact and excerpt
91
+ result.context(max_chars=4000)
92
+ ```
93
+
94
+ Each memory is a `Known(text)`, a `Fact(ref, text, source)` or an `Excerpt(ref, text, date, added_at, session,
95
+ roles)`; `result.known`, `.facts` and `.excerpts` hold each kind. `dana.open(ref)` reads one whole: an
96
+ excerpt in full, or a fact with the excerpts it was learned from.
97
+
98
+ `context()` is the notes, a blank line, then each memory as `- <text>` (an excerpt after its date), cut at a
99
+ whole memory so it never exceeds `max_chars`; empty when memory found nothing.
100
+
101
+ `fast=True` answers sooner, a little less precisely, and free: use it where a reply cannot wait, like a voice
102
+ agent's turn. `before` and `after` (ISO dates), `filters`, `provider`, `source` and `kind` narrow a search.
103
+
104
+ ## Everything else
105
+
106
+ | Method | What it answers |
107
+ | ------------------------------------------------------------- | ------------------------------------------------- |
108
+ | `memory.me()` | the key: organisation, project, whether it writes |
109
+ | `memory.users(limit=, cursor=)` / `memory.iter_users()` | the project's app users with memory |
110
+ | `user.grep(pattern, limit=)` | excerpts holding an exact phrase |
111
+ | `user.latest(k=10, place=, after=, before=)` | the most recent excerpts |
112
+ | `user.around(ref, width=2)` | the excerpts either side of one |
113
+ | `user.open(ref)` | one memory whole |
114
+ | `user.history(subject, attribute=None)` | every value a subject was stated to have |
115
+ | `user.summary()` | counts, dates, connected apps and the known lines |
116
+ | `user.facts(limit=, cursor=)` / `user.iter_facts()` | the facts memory learned, newest first |
117
+ | `user.open_items(...)` | open issues and pull requests, and follow-ups |
118
+ | `user.sources()` | what each connected app delivered |
119
+ | `user.items(limit=, cursor=, filters=)` / `user.iter_items()` | the items you added, as you added them |
120
+ | `user.item(item_id)` | one item whole, or `None` |
121
+ | `user.status(ids)` | queued, stored, processed or unknown, per item |
122
+ | `user.forget(items=, sessions=, refs=)` | erase every copy, for good; returns a receipt |
123
+ | `user.correct(ref, replacement=None)` | retract one fact, optionally saying what is true |
124
+ | `user.versions(item_id)` | an item's versions, and when and why it went |
125
+ | `user.export(limit=, cursor=)` / `user.iter_export()` | everything memory holds for this app user |
126
+ | `user.delete()` | erase this app user whole |
127
+
128
+ Arguments left out are not sent, so the API's defaults apply. Every result is a frozen dataclass, and `.raw`
129
+ holds the JSON as the API sent it. The `iter_*` methods follow `next` page by page.
130
+
131
+ ## Async
132
+
133
+ ```python
134
+ from alexein import AsyncMemory
135
+
136
+ async with AsyncMemory() as memory:
137
+ dana = memory.user("dana")
138
+ await dana.add("Prefers green tea.")
139
+ result = await dana.search("What does Dana drink?", fast=True)
140
+ async for fact in dana.iter_facts():
141
+ print(fact.text)
142
+ ```
143
+
144
+ ## Errors
145
+
146
+ Every error is an `AlexeinError` with `status` (None when no response came), `detail` (the API's sentence),
147
+ `errors` (on a 422, each field as `FieldError(field, message)`), `request_id`, and `outage`: whether memory is
148
+ unavailable rather than the request or the key being wrong.
149
+
150
+ | Error | When |
151
+ | ------------------------- | ---------------------------------------------------------------------- |
152
+ | `AuthenticationError` | 401: no key, or one the service did not issue |
153
+ | `PermissionDeniedError` | 403: the key reads, and the call writes |
154
+ | `NotFoundError` | 404: nothing added for this app user yet, or a ref memory did not give |
155
+ | `InvalidRequestError` | 400, 413, 422 and any other 4xx |
156
+ | `RateLimitError` | 429, after retries |
157
+ | `ServiceUnavailableError` | 5xx, after retries |
158
+ | `APIConnectionError` | the service could not be reached, after retries |
159
+ | `APITimeoutError` | the service did not answer in time, after retries |
160
+
161
+ 429, 503 and connection failures are retried up to `max_retries` times, waiting as `Retry-After` says (one
162
+ over 60 seconds is raised at once) or backing off from half a second. Nothing else is retried.
163
+
164
+ ### Failing open
165
+
166
+ Memory being down should cost a reply its memory, not the reply. With `Memory(fail_open=True)`, an outage -
167
+ memory unreachable, timing out, or answering 429 or 5xx after its retries - does not raise from `search` or
168
+ `add`: `search` answers an empty result with `failed=True`, `add` saves nothing and answers `failed=True`, and
169
+ each logs a warning on the `alexein` logger. Every other method, and every other error (a wrong key, a wrong
170
+ request), still raises. Pair it with a short `timeout` where a reply cannot wait.
171
+
172
+ ## LangGraph
173
+
174
+ ```bash
175
+ pip install "alexein[langgraph]"
176
+ ```
177
+
178
+ ```python
179
+ from alexein.langgraph import AlexeinMemory, MemoryContext, MemoryState
180
+
181
+ memory = AlexeinMemory() # or AlexeinMemory(client, fast=False, max_chars=4000, strict=False)
182
+
183
+ builder = StateGraph(MemoryState, context_schema=MemoryContext)
184
+ builder.add_node("recall", memory.recall) # what memory holds for the latest message
185
+ builder.add_node("agent", agent) # put memory.prompt(state) in its system prompt
186
+ builder.add_node("remember", memory.remember) # save the turn
187
+ builder.add_edge(START, "recall")
188
+ builder.add_edge("recall", "agent")
189
+ builder.add_edge("agent", "remember")
190
+ graph = builder.compile(checkpointer=InMemorySaver())
191
+
192
+ graph.invoke(inputs, config={"configurable": {"thread_id": "t-1"}}, context={"user_id": "dana"})
193
+ ```
194
+
195
+ Or give a model tools to call when it decides to: `create_agent(model, tools=memory.tools(write=True),
196
+ context_schema=MemoryContext)` offers `memory_search`, `memory_open` and `memory_add`.
197
+
198
+ Whose memory is read is decided by your code, never by the model: the user id comes from the run's `context`
199
+ (a tool also reads `config["configurable"]["user_id"]`), and a run without one raises a `ValueError` saying how to pass
200
+ it. Each message is saved under its LangGraph id, so a turn run again edits rather than duplicates; the thread
201
+ id is the session. Tool calls and tool results are not saved. The nodes and tools run under `invoke` and
202
+ `ainvoke`; with an `AsyncMemory`, add `memory.arecall` and `memory.aremember`. Module-level `recall`,
203
+ `remember`, `memory_prompt` and `memory_tools` use one default memory. See `examples/langgraph_agent.py`.
204
+
205
+ ## OpenAI Agents SDK
206
+
207
+ ```bash
208
+ pip install "alexein[openai-agents]"
209
+ ```
210
+
211
+ ```python
212
+ from alexein.openai_agents import AlexeinMemory, MemoryContext
213
+
214
+ memory = AlexeinMemory()
215
+ agent = Agent(name="Assistant", tools=memory.tools())
216
+ result = await Runner.run(
217
+ agent,
218
+ message,
219
+ context=MemoryContext(user_id="dana", session_id="conv-7"),
220
+ run_config=RunConfig(call_model_input_filter=memory.recall),
221
+ )
222
+ await memory.remember(result)
223
+ ```
224
+
225
+ `recall` searches once a turn and adds what it finds to the instructions of every model call in that turn, so
226
+ it never lands in your session. `remember` saves the user's message and the final answer under the agent's
227
+ name. Any context with a `user_id` will do.
228
+
229
+ ## CrewAI
230
+
231
+ ```bash
232
+ pip install "alexein[crewai]"
233
+ ```
234
+
235
+ ```python
236
+ from alexein.crewai import AlexeinMemory
237
+
238
+ memory = AlexeinMemory()
239
+ crew = Crew(agents=..., tasks=..., memory=memory, before_kickoff_callbacks=[memory.whose])
240
+ crew.kickoff(inputs={"user_id": "dana", "city": "Porto"})
241
+ ```
242
+
243
+ `AlexeinMemory` is CrewAI's own memory, kept in Alexein: CrewAI recalls the user's memory before every task and
244
+ saves what each task found. `whose` takes the user from the kickoff's inputs and checks the key once; a run
245
+ that names no user stops before any task. For tools with the user fixed by your code,
246
+ `Agent(..., tools=memory.tools("dana"))`, and `memory.save_result("dana", output)` after a run.
247
+
248
+ In every integration, a memory outage answers the model "Memory is unavailable right now; carry on without it."
249
+ and lets the run go on, unless `strict=True`. A wrong key raises either way. Each makes its own client on first
250
+ use, failing open with a 10 second timeout, so importing one needs no key.
251
+
252
+ ## License
253
+
254
+ MIT