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.
- alexein-0.3.0/.gitignore +6 -0
- alexein-0.3.0/CHANGELOG.md +37 -0
- alexein-0.3.0/LICENSE +21 -0
- alexein-0.3.0/PKG-INFO +285 -0
- alexein-0.3.0/README.md +254 -0
- alexein-0.3.0/pyproject.toml +92 -0
- alexein-0.3.0/src/alexein/__init__.py +111 -0
- alexein-0.3.0/src/alexein/_async_user.py +259 -0
- alexein-0.3.0/src/alexein/_calls.py +200 -0
- alexein-0.3.0/src/alexein/_client.py +165 -0
- alexein-0.3.0/src/alexein/_errors.py +123 -0
- alexein-0.3.0/src/alexein/_http.py +172 -0
- alexein-0.3.0/src/alexein/_integration.py +115 -0
- alexein-0.3.0/src/alexein/_memories.py +206 -0
- alexein-0.3.0/src/alexein/_results.py +305 -0
- alexein-0.3.0/src/alexein/_types.py +45 -0
- alexein-0.3.0/src/alexein/_user.py +258 -0
- alexein-0.3.0/src/alexein/_version.py +1 -0
- alexein-0.3.0/src/alexein/_writes.py +154 -0
- alexein-0.3.0/src/alexein/crewai.py +334 -0
- alexein-0.3.0/src/alexein/langgraph.py +334 -0
- alexein-0.3.0/src/alexein/openai_agents.py +303 -0
- alexein-0.3.0/src/alexein/py.typed +0 -0
alexein-0.3.0/.gitignore
ADDED
|
@@ -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
|
alexein-0.3.0/README.md
ADDED
|
@@ -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
|