membase-strands 0.1.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.
- membase_strands-0.1.0/.gitignore +5 -0
- membase_strands-0.1.0/LICENSE +21 -0
- membase_strands-0.1.0/PKG-INFO +186 -0
- membase_strands-0.1.0/README.md +159 -0
- membase_strands-0.1.0/examples/live_smoke.py +90 -0
- membase_strands-0.1.0/pyproject.toml +64 -0
- membase_strands-0.1.0/src/membase_strands/__init__.py +16 -0
- membase_strands-0.1.0/src/membase_strands/py.typed +0 -0
- membase_strands-0.1.0/src/membase_strands/store.py +527 -0
- membase_strands-0.1.0/tests/conftest.py +153 -0
- membase_strands-0.1.0/tests/test_store.py +244 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Unibase
|
|
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.
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: membase-strands
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Membase memory for Strands Agents: a MemoryStore with time-boxed recall and deliberate search/save/profile tools.
|
|
5
|
+
Project-URL: Homepage, https://membase.com
|
|
6
|
+
Project-URL: Documentation, https://github.com/unibaseio/membase/tree/main/integrations/strands#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/unibaseio/membase
|
|
8
|
+
Project-URL: Issues, https://github.com/unibaseio/membase/issues
|
|
9
|
+
Author: Unibase
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agents,long-term-memory,membase,memory,strands,strands-agents
|
|
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: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: strands-agents>=1.45.0
|
|
23
|
+
Requires-Dist: unibaseio-membase>=0.3.2
|
|
24
|
+
Provides-Extra: local
|
|
25
|
+
Requires-Dist: unibaseio-membase[local]>=0.3.2; extra == 'local'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# membase-strands
|
|
29
|
+
|
|
30
|
+
[Membase](https://membase.com) memory for [Strands Agents](https://strandsagents.com) (Python).
|
|
31
|
+
|
|
32
|
+
`MembaseMemoryStore` implements Strands' `MemoryStore` protocol, so it plugs into
|
|
33
|
+
`strands.memory.MemoryManager` like the other memory stores:
|
|
34
|
+
|
|
35
|
+
- **Recall before each user turn.** The manager's context injection calls `search`, and the
|
|
36
|
+
passages it returns are folded into that model call. Each one names the Membase memory
|
|
37
|
+
(container) it came from: `[Project decisions] We chose Postgres for the ledger.`
|
|
38
|
+
- **Time-boxed, fail-open recall.** A Membase search runs inside the account's own memory
|
|
39
|
+
container, and the first one after a quiet spell takes longer while it wakes. The
|
|
40
|
+
automatic recall gets 8 seconds by default (`recall_timeout`). If it runs out, the manager
|
|
41
|
+
logs it and the turn goes ahead without memory. The request keeps running in the
|
|
42
|
+
background, so the memory is awake for the next turn. While it is still running, later
|
|
43
|
+
recalls skip immediately instead of piling up.
|
|
44
|
+
- **Deliberate writes.** `get_tools()` gives the model `membase_search`, `membase_save` and
|
|
45
|
+
`membase_profile`. Saving is a tool the model calls when the user asked it to remember
|
|
46
|
+
something. Saving every turn automatically (`capture=True`) is opt-in.
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install membase-strands
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Quick start
|
|
53
|
+
|
|
54
|
+
Create a developer key in the Membase app under **Connect › Developer keys**. Use
|
|
55
|
+
*Read & write* if the agent should save, and tick the profile if it should read it.
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from strands import Agent
|
|
59
|
+
from strands.memory import MemoryManager
|
|
60
|
+
from membase_strands import MembaseMemoryStore
|
|
61
|
+
|
|
62
|
+
store = MembaseMemoryStore(api_key="mbk_...") # or set MEMBASE_API_KEY
|
|
63
|
+
|
|
64
|
+
agent = Agent(
|
|
65
|
+
memory_manager=MemoryManager(
|
|
66
|
+
stores=[store],
|
|
67
|
+
search_tool_config=False, # the model gets membase_search instead (see below)
|
|
68
|
+
),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
agent("Which database did we pick for the ledger?")
|
|
72
|
+
# Before the model call, the manager searches Membase and injects:
|
|
73
|
+
# <memory>
|
|
74
|
+
# <entry source="membase">[Project decisions] We chose Postgres for the ledger.</entry>
|
|
75
|
+
# </memory>
|
|
76
|
+
|
|
77
|
+
agent("Remember that we moved the launch to May.")
|
|
78
|
+
# The model calls membase_save(content="We moved the launch to May.", container="Project decisions")
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Why `search_tool_config=False`
|
|
82
|
+
|
|
83
|
+
The manager's built-in `search_memory` tool calls the same time-boxed `search` as injection.
|
|
84
|
+
On a cold start it would give up after 8 seconds and show the model an empty list. The
|
|
85
|
+
store's own `membase_search` tool waits for the SDK's full timeout (90 s), so a cold start
|
|
86
|
+
gets an answer. It also lets the model pick one Memory by name, and it says when a Memory
|
|
87
|
+
could not answer. You can keep both tools if you prefer.
|
|
88
|
+
|
|
89
|
+
## Tools
|
|
90
|
+
|
|
91
|
+
| Tool | What it does |
|
|
92
|
+
|---|---|
|
|
93
|
+
| `membase_search(query, container?, limit?)` | Searches every Memory in reach, or one Memory by name or id. Each passage is cited as `[Memory name] …`. A Memory that could not answer is listed as such, so it is not mistaken for "no memory". |
|
|
94
|
+
| `membase_save(content, container?, standing_fact?)` | Saves a note in the user's words. `standing_fact=true` records a lasting fact in the user's profile. If the key reaches several Memories and none is named, the tool returns their names so the model can ask which one. |
|
|
95
|
+
| `membase_profile(topic?)` | Reads the user's standing facts and recent changes. This needs the owner to have ticked the profile for the key. |
|
|
96
|
+
|
|
97
|
+
Pick the tools with `tools=("search", "profile")`. For example, drop `"save"` for a Read key.
|
|
98
|
+
There is no delete or forget tool, and the store never confirms a destructive operation for
|
|
99
|
+
the user.
|
|
100
|
+
|
|
101
|
+
## Options
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
MembaseMemoryStore(
|
|
105
|
+
client=None, # a ready membase.Membase client, or:
|
|
106
|
+
api_key=None, # MEMBASE_API_KEY
|
|
107
|
+
base_url=None, # MEMBASE_BASE_URL, default https://api.app.membase.com
|
|
108
|
+
local=None, # a directory for local mode (pip install 'membase-strands[local]')
|
|
109
|
+
container=None, # one Memory (name or id) to search and write to; default: all in reach
|
|
110
|
+
name="membase", # store name in the manager
|
|
111
|
+
max_search_results=None, # passages per search when the caller passes none (default 5)
|
|
112
|
+
recall_timeout=8.0, # budget for the automatic recall; None = SDK timeout
|
|
113
|
+
capture=False, # let the manager write (add_memory tool, extraction)
|
|
114
|
+
extraction=None, # the manager's extraction config; needs capture=True
|
|
115
|
+
capture_roles=("user",), # which messages captured turns keep
|
|
116
|
+
tools=("search", "save", "profile"),
|
|
117
|
+
)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Automatic capture (opt-in)
|
|
121
|
+
|
|
122
|
+
By default the store is not `writable`, so the manager never writes to Membase. The only way
|
|
123
|
+
to save is the model calling `membase_save` when the user asks. Membase learns from what it
|
|
124
|
+
is given, and a transcript of every turn would teach it the assistant's own answers as facts.
|
|
125
|
+
|
|
126
|
+
To capture anyway:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
store = MembaseMemoryStore(container="Conversations", capture=True, extraction=True)
|
|
130
|
+
agent = Agent(memory_manager=MemoryManager(stores=[store], search_tool_config=False))
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
With `extraction=True` the manager hands batches of turns to `add_messages`, by default every
|
|
134
|
+
5 turns and at the end of each synchronous `agent(...)` call. The store saves the user's
|
|
135
|
+
messages from the batch as one note in `container`, and Membase learns from it in the
|
|
136
|
+
background. `capture_roles=("user", "assistant")` keeps both sides. `capture=True` also lets
|
|
137
|
+
you enable the manager's own `add_memory` tool (`MemoryManager(..., add_tool_config=True)`).
|
|
138
|
+
A retried batch can be saved twice.
|
|
139
|
+
|
|
140
|
+
## One store per person
|
|
141
|
+
|
|
142
|
+
Membase has no `user_id`. One key, or one local directory, is one person's account. The
|
|
143
|
+
`MemoryStore` protocol does not pass a user or session to `search` either. So a store built
|
|
144
|
+
from a single key suits **a personal agent**. In a multi-user app, build the store and the
|
|
145
|
+
agent per user, from that user's own key or directory, and never route one user's turn
|
|
146
|
+
through another user's key:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
def agent_for(user) -> Agent:
|
|
150
|
+
store = MembaseMemoryStore(api_key=keys.get(user.id)) # that user's key
|
|
151
|
+
# or: MembaseMemoryStore(local=f"/data/membase/{user.id}") # one directory per user
|
|
152
|
+
return Agent(memory_manager=MemoryManager(stores=[store], search_tool_config=False))
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Errors
|
|
156
|
+
|
|
157
|
+
- **403** means the key's owner narrowed or revoked it. Automatic recall raises
|
|
158
|
+
`MembaseAccessError`; the manager logs it and continues without memory. The tools return
|
|
159
|
+
"this Membase key was narrowed or revoked…" so the model can tell the user. Nothing retries
|
|
160
|
+
with that key.
|
|
161
|
+
- **A Memory that could not answer** (no working model, dormant) is reported. Recall returns
|
|
162
|
+
an entry that says so, and `membase_search` lists it under "Not searched".
|
|
163
|
+
- **A recall that runs out of time** raises `MembaseRecallTimeout` (a `TimeoutError`); the
|
|
164
|
+
manager logs it and continues. The tools use the SDK timeout and, if that runs out too,
|
|
165
|
+
return a "try again in a minute" sentence.
|
|
166
|
+
- Captured writes (`add`, `add_messages`) raise on failure so the manager can report or retry
|
|
167
|
+
them. The tools turn errors into sentences.
|
|
168
|
+
|
|
169
|
+
The key is never logged.
|
|
170
|
+
|
|
171
|
+
## Compatibility
|
|
172
|
+
|
|
173
|
+
Python 3.10+, `strands-agents>=1.45.0` (the first release with `strands.memory`) and
|
|
174
|
+
`unibaseio-membase>=0.3.2`. The offline tests pass on strands-agents 1.45.0, 1.50.0, 1.55.0
|
|
175
|
+
and 1.61.1.
|
|
176
|
+
|
|
177
|
+
## Development
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
uv venv && uv pip install -e . pytest pytest-asyncio ruff
|
|
181
|
+
.venv/bin/python -m pytest -q # offline: real Strands Agent, Membase SDK on httpx.MockTransport
|
|
182
|
+
MEMBASE_API_KEY=mbk_... .venv/bin/python examples/live_smoke.py
|
|
183
|
+
uv build
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
MIT licence.
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# membase-strands
|
|
2
|
+
|
|
3
|
+
[Membase](https://membase.com) memory for [Strands Agents](https://strandsagents.com) (Python).
|
|
4
|
+
|
|
5
|
+
`MembaseMemoryStore` implements Strands' `MemoryStore` protocol, so it plugs into
|
|
6
|
+
`strands.memory.MemoryManager` like the other memory stores:
|
|
7
|
+
|
|
8
|
+
- **Recall before each user turn.** The manager's context injection calls `search`, and the
|
|
9
|
+
passages it returns are folded into that model call. Each one names the Membase memory
|
|
10
|
+
(container) it came from: `[Project decisions] We chose Postgres for the ledger.`
|
|
11
|
+
- **Time-boxed, fail-open recall.** A Membase search runs inside the account's own memory
|
|
12
|
+
container, and the first one after a quiet spell takes longer while it wakes. The
|
|
13
|
+
automatic recall gets 8 seconds by default (`recall_timeout`). If it runs out, the manager
|
|
14
|
+
logs it and the turn goes ahead without memory. The request keeps running in the
|
|
15
|
+
background, so the memory is awake for the next turn. While it is still running, later
|
|
16
|
+
recalls skip immediately instead of piling up.
|
|
17
|
+
- **Deliberate writes.** `get_tools()` gives the model `membase_search`, `membase_save` and
|
|
18
|
+
`membase_profile`. Saving is a tool the model calls when the user asked it to remember
|
|
19
|
+
something. Saving every turn automatically (`capture=True`) is opt-in.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install membase-strands
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick start
|
|
26
|
+
|
|
27
|
+
Create a developer key in the Membase app under **Connect › Developer keys**. Use
|
|
28
|
+
*Read & write* if the agent should save, and tick the profile if it should read it.
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from strands import Agent
|
|
32
|
+
from strands.memory import MemoryManager
|
|
33
|
+
from membase_strands import MembaseMemoryStore
|
|
34
|
+
|
|
35
|
+
store = MembaseMemoryStore(api_key="mbk_...") # or set MEMBASE_API_KEY
|
|
36
|
+
|
|
37
|
+
agent = Agent(
|
|
38
|
+
memory_manager=MemoryManager(
|
|
39
|
+
stores=[store],
|
|
40
|
+
search_tool_config=False, # the model gets membase_search instead (see below)
|
|
41
|
+
),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
agent("Which database did we pick for the ledger?")
|
|
45
|
+
# Before the model call, the manager searches Membase and injects:
|
|
46
|
+
# <memory>
|
|
47
|
+
# <entry source="membase">[Project decisions] We chose Postgres for the ledger.</entry>
|
|
48
|
+
# </memory>
|
|
49
|
+
|
|
50
|
+
agent("Remember that we moved the launch to May.")
|
|
51
|
+
# The model calls membase_save(content="We moved the launch to May.", container="Project decisions")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Why `search_tool_config=False`
|
|
55
|
+
|
|
56
|
+
The manager's built-in `search_memory` tool calls the same time-boxed `search` as injection.
|
|
57
|
+
On a cold start it would give up after 8 seconds and show the model an empty list. The
|
|
58
|
+
store's own `membase_search` tool waits for the SDK's full timeout (90 s), so a cold start
|
|
59
|
+
gets an answer. It also lets the model pick one Memory by name, and it says when a Memory
|
|
60
|
+
could not answer. You can keep both tools if you prefer.
|
|
61
|
+
|
|
62
|
+
## Tools
|
|
63
|
+
|
|
64
|
+
| Tool | What it does |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `membase_search(query, container?, limit?)` | Searches every Memory in reach, or one Memory by name or id. Each passage is cited as `[Memory name] …`. A Memory that could not answer is listed as such, so it is not mistaken for "no memory". |
|
|
67
|
+
| `membase_save(content, container?, standing_fact?)` | Saves a note in the user's words. `standing_fact=true` records a lasting fact in the user's profile. If the key reaches several Memories and none is named, the tool returns their names so the model can ask which one. |
|
|
68
|
+
| `membase_profile(topic?)` | Reads the user's standing facts and recent changes. This needs the owner to have ticked the profile for the key. |
|
|
69
|
+
|
|
70
|
+
Pick the tools with `tools=("search", "profile")`. For example, drop `"save"` for a Read key.
|
|
71
|
+
There is no delete or forget tool, and the store never confirms a destructive operation for
|
|
72
|
+
the user.
|
|
73
|
+
|
|
74
|
+
## Options
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
MembaseMemoryStore(
|
|
78
|
+
client=None, # a ready membase.Membase client, or:
|
|
79
|
+
api_key=None, # MEMBASE_API_KEY
|
|
80
|
+
base_url=None, # MEMBASE_BASE_URL, default https://api.app.membase.com
|
|
81
|
+
local=None, # a directory for local mode (pip install 'membase-strands[local]')
|
|
82
|
+
container=None, # one Memory (name or id) to search and write to; default: all in reach
|
|
83
|
+
name="membase", # store name in the manager
|
|
84
|
+
max_search_results=None, # passages per search when the caller passes none (default 5)
|
|
85
|
+
recall_timeout=8.0, # budget for the automatic recall; None = SDK timeout
|
|
86
|
+
capture=False, # let the manager write (add_memory tool, extraction)
|
|
87
|
+
extraction=None, # the manager's extraction config; needs capture=True
|
|
88
|
+
capture_roles=("user",), # which messages captured turns keep
|
|
89
|
+
tools=("search", "save", "profile"),
|
|
90
|
+
)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Automatic capture (opt-in)
|
|
94
|
+
|
|
95
|
+
By default the store is not `writable`, so the manager never writes to Membase. The only way
|
|
96
|
+
to save is the model calling `membase_save` when the user asks. Membase learns from what it
|
|
97
|
+
is given, and a transcript of every turn would teach it the assistant's own answers as facts.
|
|
98
|
+
|
|
99
|
+
To capture anyway:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
store = MembaseMemoryStore(container="Conversations", capture=True, extraction=True)
|
|
103
|
+
agent = Agent(memory_manager=MemoryManager(stores=[store], search_tool_config=False))
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
With `extraction=True` the manager hands batches of turns to `add_messages`, by default every
|
|
107
|
+
5 turns and at the end of each synchronous `agent(...)` call. The store saves the user's
|
|
108
|
+
messages from the batch as one note in `container`, and Membase learns from it in the
|
|
109
|
+
background. `capture_roles=("user", "assistant")` keeps both sides. `capture=True` also lets
|
|
110
|
+
you enable the manager's own `add_memory` tool (`MemoryManager(..., add_tool_config=True)`).
|
|
111
|
+
A retried batch can be saved twice.
|
|
112
|
+
|
|
113
|
+
## One store per person
|
|
114
|
+
|
|
115
|
+
Membase has no `user_id`. One key, or one local directory, is one person's account. The
|
|
116
|
+
`MemoryStore` protocol does not pass a user or session to `search` either. So a store built
|
|
117
|
+
from a single key suits **a personal agent**. In a multi-user app, build the store and the
|
|
118
|
+
agent per user, from that user's own key or directory, and never route one user's turn
|
|
119
|
+
through another user's key:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
def agent_for(user) -> Agent:
|
|
123
|
+
store = MembaseMemoryStore(api_key=keys.get(user.id)) # that user's key
|
|
124
|
+
# or: MembaseMemoryStore(local=f"/data/membase/{user.id}") # one directory per user
|
|
125
|
+
return Agent(memory_manager=MemoryManager(stores=[store], search_tool_config=False))
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Errors
|
|
129
|
+
|
|
130
|
+
- **403** means the key's owner narrowed or revoked it. Automatic recall raises
|
|
131
|
+
`MembaseAccessError`; the manager logs it and continues without memory. The tools return
|
|
132
|
+
"this Membase key was narrowed or revoked…" so the model can tell the user. Nothing retries
|
|
133
|
+
with that key.
|
|
134
|
+
- **A Memory that could not answer** (no working model, dormant) is reported. Recall returns
|
|
135
|
+
an entry that says so, and `membase_search` lists it under "Not searched".
|
|
136
|
+
- **A recall that runs out of time** raises `MembaseRecallTimeout` (a `TimeoutError`); the
|
|
137
|
+
manager logs it and continues. The tools use the SDK timeout and, if that runs out too,
|
|
138
|
+
return a "try again in a minute" sentence.
|
|
139
|
+
- Captured writes (`add`, `add_messages`) raise on failure so the manager can report or retry
|
|
140
|
+
them. The tools turn errors into sentences.
|
|
141
|
+
|
|
142
|
+
The key is never logged.
|
|
143
|
+
|
|
144
|
+
## Compatibility
|
|
145
|
+
|
|
146
|
+
Python 3.10+, `strands-agents>=1.45.0` (the first release with `strands.memory`) and
|
|
147
|
+
`unibaseio-membase>=0.3.2`. The offline tests pass on strands-agents 1.45.0, 1.50.0, 1.55.0
|
|
148
|
+
and 1.61.1.
|
|
149
|
+
|
|
150
|
+
## Development
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
uv venv && uv pip install -e . pytest pytest-asyncio ruff
|
|
154
|
+
.venv/bin/python -m pytest -q # offline: real Strands Agent, Membase SDK on httpx.MockTransport
|
|
155
|
+
MEMBASE_API_KEY=mbk_... .venv/bin/python examples/live_smoke.py
|
|
156
|
+
uv build
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
MIT licence.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Live smoke test against the real Membase API.
|
|
2
|
+
|
|
3
|
+
Needs MEMBASE_API_KEY (a developer key from Connect > Developer keys in the Membase app).
|
|
4
|
+
Without a model key it checks the store and its tools directly; with AWS credentials for
|
|
5
|
+
Bedrock (Strands' default model) or OPENAI_API_KEY plus `pip install 'strands-agents[openai]'`,
|
|
6
|
+
it also runs one agent turn so injection is exercised end to end.
|
|
7
|
+
|
|
8
|
+
MEMBASE_API_KEY=mbk_... python examples/live_smoke.py
|
|
9
|
+
MEMBASE_API_KEY=mbk_... MEMBASE_SMOKE_SAVE=1 python examples/live_smoke.py # also saves one note
|
|
10
|
+
|
|
11
|
+
Saving is off unless MEMBASE_SMOKE_SAVE=1, because it writes to a real person's memory.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import logging
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
import time
|
|
21
|
+
|
|
22
|
+
from membase_strands import MembaseMemoryStore
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def tool(store: MembaseMemoryStore, name: str):
|
|
26
|
+
return next(t for t in store.get_tools() if t.tool_name == name)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def main() -> int:
|
|
30
|
+
if not os.environ.get("MEMBASE_API_KEY"):
|
|
31
|
+
print("MEMBASE_API_KEY is not set; nothing to do.")
|
|
32
|
+
return 0
|
|
33
|
+
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
|
|
34
|
+
|
|
35
|
+
store = MembaseMemoryStore() # MEMBASE_API_KEY / MEMBASE_BASE_URL from the environment
|
|
36
|
+
containers = store.client.containers.list()["containers"]
|
|
37
|
+
print("Memories in reach:", [c["name"] for c in containers])
|
|
38
|
+
|
|
39
|
+
# 1. Explicit search through the tool (SDK timeout, so a cold start is waited out).
|
|
40
|
+
started = time.monotonic()
|
|
41
|
+
out = await tool(store, "membase_search")(query="what do I care about?")
|
|
42
|
+
print(f"\nmembase_search ({time.monotonic() - started:.1f}s):\n{out}")
|
|
43
|
+
|
|
44
|
+
# 2. Automatic recall path: time-boxed; now warm, it should answer within budget.
|
|
45
|
+
started = time.monotonic()
|
|
46
|
+
try:
|
|
47
|
+
entries = await store.search("what do I care about?")
|
|
48
|
+
print(f"\nstore.search ({time.monotonic() - started:.1f}s): {len(entries)} entries")
|
|
49
|
+
for e in entries:
|
|
50
|
+
print(" ", e.content[:160])
|
|
51
|
+
except TimeoutError as error:
|
|
52
|
+
print(f"\nstore.search timed out as designed: {error}")
|
|
53
|
+
|
|
54
|
+
# 3. Profile (403 if the owner did not share the profile with this key).
|
|
55
|
+
print("\nmembase_profile:\n" + await tool(store, "membase_profile")())
|
|
56
|
+
|
|
57
|
+
if os.environ.get("MEMBASE_SMOKE_SAVE") == "1":
|
|
58
|
+
target = containers[0]["name"] if containers else None
|
|
59
|
+
out = await tool(store, "membase_save")(
|
|
60
|
+
content="Membase Strands smoke test: this note can be deleted.", container=target
|
|
61
|
+
)
|
|
62
|
+
print("\nmembase_save:", out)
|
|
63
|
+
|
|
64
|
+
# 4. One real agent turn, if a model is configured.
|
|
65
|
+
model = None
|
|
66
|
+
if os.environ.get("OPENAI_API_KEY"):
|
|
67
|
+
from strands.models.openai import OpenAIModel
|
|
68
|
+
|
|
69
|
+
model = OpenAIModel(model_id=os.environ.get("OPENAI_MODEL", "gpt-4o-mini"))
|
|
70
|
+
elif os.environ.get("AWS_ACCESS_KEY_ID") or os.environ.get("AWS_PROFILE"):
|
|
71
|
+
model = None # Strands' default Bedrock model
|
|
72
|
+
else:
|
|
73
|
+
print("\nNo model credentials; skipping the agent turn.")
|
|
74
|
+
return 0
|
|
75
|
+
|
|
76
|
+
from strands import Agent
|
|
77
|
+
from strands.memory import MemoryManager
|
|
78
|
+
|
|
79
|
+
agent = Agent(
|
|
80
|
+
model=model,
|
|
81
|
+
memory_manager=MemoryManager(stores=[store], search_tool_config=False),
|
|
82
|
+
system_prompt="Answer from the user's memory when it helps, and name the Memory you used.",
|
|
83
|
+
)
|
|
84
|
+
result = await agent.invoke_async("What do you remember about me? One sentence.")
|
|
85
|
+
print("\nAgent:", result)
|
|
86
|
+
return 0
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
if __name__ == "__main__":
|
|
90
|
+
sys.exit(asyncio.run(main()))
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "membase-strands"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Membase memory for Strands Agents: a MemoryStore with time-boxed recall and deliberate search/save/profile tools."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Unibase" }]
|
|
14
|
+
keywords = ["strands", "strands-agents", "agents", "memory", "membase", "long-term-memory"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
24
|
+
]
|
|
25
|
+
# strands-agents>=1.45.0: first release with strands.memory (MemoryStore / MemoryManager,
|
|
26
|
+
# injection, get_tools). The offline tests pass on 1.45.0, 1.50.0, 1.55.0 and 1.61.1.
|
|
27
|
+
dependencies = [
|
|
28
|
+
"strands-agents>=1.45.0",
|
|
29
|
+
"unibaseio-membase>=0.3.2",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
local = ["unibaseio-membase[local]>=0.3.2"]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://membase.com"
|
|
37
|
+
Documentation = "https://github.com/unibaseio/membase/tree/main/integrations/strands#readme"
|
|
38
|
+
Repository = "https://github.com/unibaseio/membase"
|
|
39
|
+
Issues = "https://github.com/unibaseio/membase/issues"
|
|
40
|
+
|
|
41
|
+
[dependency-groups]
|
|
42
|
+
dev = [
|
|
43
|
+
"pytest>=8",
|
|
44
|
+
"pytest-asyncio>=0.25",
|
|
45
|
+
"ruff>=0.11",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.wheel]
|
|
49
|
+
packages = ["src/membase_strands"]
|
|
50
|
+
|
|
51
|
+
[tool.hatch.build.targets.sdist]
|
|
52
|
+
include = ["src", "tests", "examples", "README.md", "LICENSE", "pyproject.toml"]
|
|
53
|
+
|
|
54
|
+
[tool.pytest.ini_options]
|
|
55
|
+
testpaths = ["tests"]
|
|
56
|
+
pythonpath = ["src"]
|
|
57
|
+
asyncio_mode = "auto"
|
|
58
|
+
|
|
59
|
+
[tool.ruff]
|
|
60
|
+
line-length = 120
|
|
61
|
+
target-version = "py310"
|
|
62
|
+
|
|
63
|
+
[tool.ruff.lint]
|
|
64
|
+
select = ["E", "F", "I", "B", "UP"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Membase memory for Strands Agents: a ``MemoryStore`` plus deliberate search/save/profile tools."""
|
|
2
|
+
|
|
3
|
+
from .store import (
|
|
4
|
+
DEFAULT_RECALL_TIMEOUT,
|
|
5
|
+
MembaseAccessError,
|
|
6
|
+
MembaseMemoryStore,
|
|
7
|
+
MembaseRecallTimeout,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"DEFAULT_RECALL_TIMEOUT",
|
|
12
|
+
"MembaseAccessError",
|
|
13
|
+
"MembaseMemoryStore",
|
|
14
|
+
"MembaseRecallTimeout",
|
|
15
|
+
]
|
|
16
|
+
__version__ = "0.1.0"
|
|
File without changes
|