beanis 0.3.0__tar.gz → 0.3.2__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.
- beanis-0.3.2/PKG-INFO +296 -0
- beanis-0.3.2/README.md +249 -0
- {beanis-0.3.0 → beanis-0.3.2}/beanis/__init__.py +1 -1
- beanis-0.3.2/beanis/cli.py +347 -0
- {beanis-0.3.0 → beanis-0.3.2}/beanis/mcp_server.py +3 -15
- {beanis-0.3.0 → beanis-0.3.2}/beanis/memory.py +82 -7
- {beanis-0.3.0 → beanis-0.3.2}/beanis/odm/indexes.py +2 -0
- {beanis-0.3.0 → beanis-0.3.2}/beanis/odm/utils/init.py +10 -3
- {beanis-0.3.0 → beanis-0.3.2}/pyproject.toml +3 -2
- beanis-0.3.0/PKG-INFO +0 -719
- beanis-0.3.0/README.md +0 -672
- {beanis-0.3.0 → beanis-0.3.2}/LICENSE +0 -0
- {beanis-0.3.0 → beanis-0.3.2}/beanis/exceptions.py +0 -0
- {beanis-0.3.0 → beanis-0.3.2}/beanis/odm/__init__.py +0 -0
- {beanis-0.3.0 → beanis-0.3.2}/beanis/odm/actions.py +0 -0
- {beanis-0.3.0 → beanis-0.3.2}/beanis/odm/custom_encoders.py +0 -0
- {beanis-0.3.0 → beanis-0.3.2}/beanis/odm/custom_types/__init__.py +0 -0
- {beanis-0.3.0 → beanis-0.3.2}/beanis/odm/custom_types/decimal.py +0 -0
- {beanis-0.3.0 → beanis-0.3.2}/beanis/odm/documents.py +0 -0
- {beanis-0.3.0 → beanis-0.3.2}/beanis/odm/fields.py +0 -0
- {beanis-0.3.0 → beanis-0.3.2}/beanis/odm/interfaces/__init__.py +0 -0
- {beanis-0.3.0 → beanis-0.3.2}/beanis/odm/interfaces/clone.py +0 -0
- {beanis-0.3.0 → beanis-0.3.2}/beanis/odm/interfaces/detector.py +0 -0
- {beanis-0.3.0 → beanis-0.3.2}/beanis/odm/interfaces/getters.py +0 -0
- {beanis-0.3.0 → beanis-0.3.2}/beanis/odm/interfaces/inheritance.py +0 -0
- {beanis-0.3.0 → beanis-0.3.2}/beanis/odm/interfaces/setters.py +0 -0
- {beanis-0.3.0 → beanis-0.3.2}/beanis/odm/registry.py +0 -0
- {beanis-0.3.0 → beanis-0.3.2}/beanis/odm/settings/__init__.py +0 -0
- {beanis-0.3.0 → beanis-0.3.2}/beanis/odm/settings/base.py +0 -0
- {beanis-0.3.0 → beanis-0.3.2}/beanis/odm/settings/document.py +0 -0
- {beanis-0.3.0 → beanis-0.3.2}/beanis/odm/utils/__init__.py +0 -0
- {beanis-0.3.0 → beanis-0.3.2}/beanis/odm/utils/dump.py +0 -0
- {beanis-0.3.0 → beanis-0.3.2}/beanis/odm/utils/encoder.py +0 -0
- {beanis-0.3.0 → beanis-0.3.2}/beanis/odm/utils/general.py +0 -0
- {beanis-0.3.0 → beanis-0.3.2}/beanis/odm/utils/parsing.py +0 -0
- {beanis-0.3.0 → beanis-0.3.2}/beanis/odm/utils/projection.py +0 -0
- {beanis-0.3.0 → beanis-0.3.2}/beanis/odm/utils/pydantic.py +0 -0
- {beanis-0.3.0 → beanis-0.3.2}/beanis/odm/utils/self_validation.py +0 -0
- {beanis-0.3.0 → beanis-0.3.2}/beanis/odm/utils/state.py +0 -0
- {beanis-0.3.0 → beanis-0.3.2}/beanis/odm/utils/typing.py +0 -0
- {beanis-0.3.0 → beanis-0.3.2}/beanis/py.typed +0 -0
- {beanis-0.3.0 → beanis-0.3.2}/beanis/pydantic_check.py +0 -0
- {beanis-0.3.0 → beanis-0.3.2}/beanis/test.py +0 -0
beanis-0.3.2/PKG-INFO
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: beanis
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Summary: Asynchronous Python ODM for Redis
|
|
5
|
+
Keywords: redis,odm,orm,pydantic,async,python
|
|
6
|
+
Author-email: Andrei Stefan Bejgu <stefan.bejgu@gmail.com>
|
|
7
|
+
Requires-Python: >=3.9,<4.0
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Topic :: Database
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: redis[hiredis]>=5.0.1
|
|
18
|
+
Requires-Dist: pydantic>=1.10,<3.0
|
|
19
|
+
Requires-Dist: lazy-model==0.2.0
|
|
20
|
+
Requires-Dist: msgspec>=0.18.0
|
|
21
|
+
Requires-Dist: typing-extensions>=4.7; python_version < '3.11'
|
|
22
|
+
Requires-Dist: Pygments>=2.8.0 ; extra == "doc"
|
|
23
|
+
Requires-Dist: Markdown>=3.3 ; extra == "doc"
|
|
24
|
+
Requires-Dist: pydoc-markdown>=4.8 ; extra == "doc"
|
|
25
|
+
Requires-Dist: mkdocs>=1.4 ; extra == "doc"
|
|
26
|
+
Requires-Dist: mkdocs-material>=9.0 ; extra == "doc"
|
|
27
|
+
Requires-Dist: jinja2>=3.0.3 ; extra == "doc"
|
|
28
|
+
Requires-Dist: mcp>=2.3 ; extra == "mcp" and ( python_version >= '3.10')
|
|
29
|
+
Requires-Dist: fastembed>=0.9 ; extra == "mcp" and ( python_version >= '3.10')
|
|
30
|
+
Requires-Dist: pytest>=6.0.0 ; extra == "test"
|
|
31
|
+
Requires-Dist: pytest-asyncio>=0.21.0 ; extra == "test"
|
|
32
|
+
Requires-Dist: pytest-cov>=2.8.1 ; extra == "test"
|
|
33
|
+
Requires-Dist: fakeredis[lua]>=2.20.0 ; extra == "test"
|
|
34
|
+
Requires-Dist: flake8>=3 ; extra == "test"
|
|
35
|
+
Requires-Dist: pyright>=0 ; extra == "test"
|
|
36
|
+
Requires-Dist: httpx>=0.23.0 ; extra == "test"
|
|
37
|
+
Requires-Dist: fastapi>=0.100 ; extra == "test"
|
|
38
|
+
Requires-Dist: pydantic-settings>=2 ; extra == "test"
|
|
39
|
+
Requires-Dist: pydantic-extra-types>=2 ; extra == "test"
|
|
40
|
+
Requires-Dist: pydantic[email] ; extra == "test"
|
|
41
|
+
Project-URL: homepage, https://github.com/andreim14/beanis
|
|
42
|
+
Project-URL: repository, https://github.com/andreim14/beanis
|
|
43
|
+
Provides-Extra: doc
|
|
44
|
+
Provides-Extra: mcp
|
|
45
|
+
Provides-Extra: test
|
|
46
|
+
|
|
47
|
+
<p align="center">
|
|
48
|
+
<picture>
|
|
49
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/andreim14/beanis/main/assets/logo/beanis-dark.svg">
|
|
50
|
+
<img src="https://raw.githubusercontent.com/andreim14/beanis/main/assets/logo/beanis.svg" alt="beanis" width="420">
|
|
51
|
+
</picture>
|
|
52
|
+
</p>
|
|
53
|
+
|
|
54
|
+
<div align="center">
|
|
55
|
+
<a href="https://pypi.python.org/pypi/beanis"><img src="https://img.shields.io/pypi/v/beanis?color=blue" alt="PyPI version"></a>
|
|
56
|
+
<a href="https://pypi.python.org/pypi/beanis"><img src="https://img.shields.io/pypi/pyversions/beanis?color=blue" alt="Python versions"></a>
|
|
57
|
+
<a href="https://github.com/andreim14/beanis/blob/main/LICENSE"><img src="https://img.shields.io/github/license/andreim14/beanis?color=blue" alt="License"></a>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
Beanis is an async object-document mapper for Redis, built on Pydantic. You define a model, and Beanis stores each instance as a Redis hash, keeps secondary indexes up to date, and gives you typed objects back.
|
|
61
|
+
|
|
62
|
+
It also does vector search on the vector sets that ship with Redis 8, and it includes long-term memory for AI agents, usable from Python, from the `beanis` command line, or as an MCP server for Claude Code and other agents.
|
|
63
|
+
|
|
64
|
+
It works on plain Redis. No RedisJSON, RediSearch or Redis Stack.
|
|
65
|
+
|
|
66
|
+
[Documentation](https://andreim14.github.io/beanis/) · [PyPI](https://pypi.org/project/beanis/) · [Changelog](docs/changelog.md)
|
|
67
|
+
|
|
68
|
+
## Install
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip install beanis # the ODM
|
|
72
|
+
pip install "beanis[mcp]" # plus agent memory: CLI embeddings and MCP server (Python 3.10+)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
You need a Redis server. Redis 5+ is enough for documents and indexes; vector search and memory need Redis 8.0+:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
docker run -d -p 6379:6379 redis:8 # or: brew install redis
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Quick start
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
import asyncio
|
|
85
|
+
|
|
86
|
+
from pydantic import BaseModel
|
|
87
|
+
from redis.asyncio import Redis
|
|
88
|
+
|
|
89
|
+
from beanis import Document, Indexed, init_beanis
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class Category(BaseModel):
|
|
93
|
+
name: str
|
|
94
|
+
description: str
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class Product(Document):
|
|
98
|
+
name: str
|
|
99
|
+
price: Indexed(float) # sorted-set index: range queries
|
|
100
|
+
category: Indexed(str) # set index: exact match
|
|
101
|
+
tags: list[str] = []
|
|
102
|
+
stock: int = 0
|
|
103
|
+
|
|
104
|
+
class Settings:
|
|
105
|
+
key_prefix = "Product"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
async def main():
|
|
109
|
+
redis = Redis(decode_responses=True)
|
|
110
|
+
await init_beanis(database=redis, document_models=[Product])
|
|
111
|
+
|
|
112
|
+
bar = Product(name="Dark chocolate", price=5.95, category="chocolate", stock=100)
|
|
113
|
+
await bar.insert()
|
|
114
|
+
|
|
115
|
+
same = await Product.get(bar.id)
|
|
116
|
+
cheap = await Product.find(price__lt=10)
|
|
117
|
+
chocolate = await Product.find(category="chocolate", price__gte=5)
|
|
118
|
+
|
|
119
|
+
await bar.update(price=6.50)
|
|
120
|
+
await bar.increment_field("stock", -1)
|
|
121
|
+
await bar.delete_self()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
asyncio.run(main())
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`decode_responses=True` is required. Each document is the hash `Product:<id>`; nested models, lists, dicts, `datetime`, `Decimal`, `UUID` and enums are encoded for you and decoded on read.
|
|
128
|
+
|
|
129
|
+
## Queries
|
|
130
|
+
|
|
131
|
+
`find()` takes keyword filters on indexed fields. Several filters are combined with AND.
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
await Product.find(category="chocolate") # exact match
|
|
135
|
+
await Product.find(price__gte=10, price__lte=50) # range: __gte, __lte, __gt, __lt
|
|
136
|
+
await Product.find(price=12.0) # exact match on a number
|
|
137
|
+
await Product.all(skip=0, limit=20) # every document, by insertion time
|
|
138
|
+
await Product.count()
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Filtering on a field that is not indexed raises `ValueError`. Sorting by other fields and full-text search are not built in; sort the returned list in Python, or use Redis's query engine directly if you need them.
|
|
142
|
+
|
|
143
|
+
Declare indexes with `Indexed(type)` or `Annotated[type, IndexedField()]`. Numbers get a sorted set, strings and other scalars get a set, `GeoPoint` gets a geo index (see the [geo tutorial](docs/tutorial/geo-spatial.md)).
|
|
144
|
+
|
|
145
|
+
## Vector search
|
|
146
|
+
|
|
147
|
+
Embeddings live in a Redis 8 vector set (`VADD` / `VSIM`), next to the document's short fields so you can filter a search:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from typing import List
|
|
151
|
+
|
|
152
|
+
from typing_extensions import Annotated
|
|
153
|
+
|
|
154
|
+
from beanis import Document, IndexedField, VectorField
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class Chunk(Document):
|
|
158
|
+
text: str
|
|
159
|
+
source: Annotated[str, IndexedField()]
|
|
160
|
+
embedding: Annotated[List[float], VectorField(dimensions=384)]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
await Chunk(text="Refunds take 5 days", source="faq", embedding=embed("Refunds take 5 days")).insert()
|
|
164
|
+
|
|
165
|
+
for chunk, score in await Chunk.vector_search(embed("how long is a refund?"), k=3, filter='.source == "faq"'):
|
|
166
|
+
print(round(score, 3), chunk.text) # score: cosine similarity in [0, 1]
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`embed` is any function from text to a list of floats. Vectors use HNSW with int8 quantization by default (`VectorField(quantization="NOQUANT")` keeps float32). Updating or deleting a document updates its vector. `init_beanis` raises at startup if a model has vector fields and the server has no vector sets (Redis 7, Valkey).
|
|
170
|
+
|
|
171
|
+
## Memory for agents
|
|
172
|
+
|
|
173
|
+
`Memory` stores what an agent should keep between conversations and finds it again by meaning. Memories are scoped by namespace, user, session and kind; a new memory that is nearly identical to an old one replaces it instead of piling up.
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
from beanis import Memory
|
|
177
|
+
from beanis.memory import fastembed_embedder
|
|
178
|
+
|
|
179
|
+
memory = Memory(redis, embed=fastembed_embedder(), namespace="support-bot")
|
|
180
|
+
|
|
181
|
+
await memory.remember("Prefers answers in Italian", user_id="u42", kind="preference")
|
|
182
|
+
for record, score in await memory.recall("which language should I answer in?", user_id="u42"):
|
|
183
|
+
print(round(score, 3), record.text)
|
|
184
|
+
await memory.recent(user_id="u42")
|
|
185
|
+
await memory.forget(record.id)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
`fastembed_embedder()` runs `BAAI/bge-small-en-v1.5` locally on the CPU (no API key). Any embedding function works, sync or async; use the same one for every writer of a namespace.
|
|
189
|
+
|
|
190
|
+
### From the command line
|
|
191
|
+
|
|
192
|
+
`pip install "beanis[mcp]"` installs a `beanis` command. Every command prints JSON, so shell scripts and coding agents can use it directly.
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
beanis doctor # checks Redis, vector sets, extras, stored memory
|
|
196
|
+
beanis memory remember "Deploys go out on Tuesdays" --kind decision
|
|
197
|
+
beanis memory recall "when do we deploy?" -k 3
|
|
198
|
+
beanis memory recent --limit 5
|
|
199
|
+
beanis memory forget <id>
|
|
200
|
+
beanis context # newest memories as plain text, for session hooks
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Configuration is by environment: `REDIS_URL` (default `redis://localhost:6379/0`, or pass `--url`), `BEANIS_MEMORY_NAMESPACE` (default `default`; `auto` gives each git project its own namespace), `BEANIS_EMBED_MODEL` (default `BAAI/bge-small-en-v1.5`).
|
|
204
|
+
|
|
205
|
+
### As an MCP server
|
|
206
|
+
|
|
207
|
+
`beanis-mcp` (or `beanis mcp`) serves the same memory over MCP stdio with four tools: `remember`, `recall`, `forget`, `recent`.
|
|
208
|
+
|
|
209
|
+
Claude Code, as a plugin (MCP server plus a skill that teaches Claude the Beanis API):
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
claude plugin marketplace add andreim14/beanis
|
|
213
|
+
claude plugin install beanis@beanis
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The plugin gives each project its own namespace and, at the start of every session, puts that project's newest memories into Claude's context, so a new session does not start from zero. Sessions and subagents share the store: what one remembers, the next one knows. It starts everything with `uvx`, so it needs [uv](https://docs.astral.sh/uv/) and a Redis 8 at `REDIS_URL`. Without the plugin:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
claude mcp add beanis-memory --env REDIS_URL=redis://localhost:6379/0 -- beanis-mcp
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Claude Desktop, Cursor and other MCP clients:
|
|
223
|
+
|
|
224
|
+
```json
|
|
225
|
+
{
|
|
226
|
+
"mcpServers": {
|
|
227
|
+
"beanis-memory": {
|
|
228
|
+
"command": "beanis-mcp",
|
|
229
|
+
"env": { "REDIS_URL": "redis://localhost:6379/0" }
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## More
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
await product.insert(ttl=3600) # expire after an hour
|
|
239
|
+
await product.set_ttl(7200); await product.get_ttl(); await product.persist()
|
|
240
|
+
|
|
241
|
+
price = await product.get_field("price") # read or write one field
|
|
242
|
+
await product.set_field("stock", 200)
|
|
243
|
+
|
|
244
|
+
await Product.insert_many(products) # pipelined batches
|
|
245
|
+
await Product.get_many(ids)
|
|
246
|
+
await Product.delete_many(ids)
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Event hooks run around inserts, updates and deletes:
|
|
250
|
+
|
|
251
|
+
```python
|
|
252
|
+
from beanis import Document, Insert, before_event
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class Product(Document):
|
|
256
|
+
name: str
|
|
257
|
+
price: float
|
|
258
|
+
|
|
259
|
+
@before_event(Insert)
|
|
260
|
+
def check_price(self):
|
|
261
|
+
if self.price < 0:
|
|
262
|
+
raise ValueError("price cannot be negative")
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Custom types are registered once with `register_type(cls, encoder=..., decoder=...)`; NumPy arrays are supported when NumPy is installed. See the [custom encoders tutorial](docs/tutorial/custom-encoders.md).
|
|
266
|
+
|
|
267
|
+
## When to use it
|
|
268
|
+
|
|
269
|
+
Use Beanis when you want typed models on Redis without running Redis modules: caches with structure, session and user state, job metadata, small catalogues, retrieval for RAG, and agent memory.
|
|
270
|
+
|
|
271
|
+
Use something else when you need complex queries across many fields, full-text search or aggregations: a document database, or Redis's query engine with [Redis OM](https://github.com/redis/redis-om-python) or [RedisVL](https://github.com/redis/redis-vl-python).
|
|
272
|
+
|
|
273
|
+
## Requirements
|
|
274
|
+
|
|
275
|
+
- Python 3.9+ (3.10+ for the `mcp` extra)
|
|
276
|
+
- Redis 5.0+; Redis 8.0+ for `VectorField`, `Memory`, the CLI memory commands and the MCP server
|
|
277
|
+
- Pydantic 1.10+ or 2.x
|
|
278
|
+
|
|
279
|
+
## Development
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
pip install -e ".[test,mcp]"
|
|
283
|
+
pytest # unit tests, on fakeredis
|
|
284
|
+
REDIS_PORT=6379 pytest tests/test_real_redis_regressions.py tests/test_memory_real_redis.py # needs Redis 8
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
See [AGENTS.md](AGENTS.md) for repository conventions.
|
|
288
|
+
|
|
289
|
+
## Credits
|
|
290
|
+
|
|
291
|
+
Beanis started as a fork of [Beanie](https://github.com/BeanieODM/beanie), the MongoDB ODM by Roman Right and contributors, rewritten for Redis.
|
|
292
|
+
|
|
293
|
+
## License
|
|
294
|
+
|
|
295
|
+
Apache License 2.0
|
|
296
|
+
|
beanis-0.3.2/README.md
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/andreim14/beanis/main/assets/logo/beanis-dark.svg">
|
|
4
|
+
<img src="https://raw.githubusercontent.com/andreim14/beanis/main/assets/logo/beanis.svg" alt="beanis" width="420">
|
|
5
|
+
</picture>
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
<div align="center">
|
|
9
|
+
<a href="https://pypi.python.org/pypi/beanis"><img src="https://img.shields.io/pypi/v/beanis?color=blue" alt="PyPI version"></a>
|
|
10
|
+
<a href="https://pypi.python.org/pypi/beanis"><img src="https://img.shields.io/pypi/pyversions/beanis?color=blue" alt="Python versions"></a>
|
|
11
|
+
<a href="https://github.com/andreim14/beanis/blob/main/LICENSE"><img src="https://img.shields.io/github/license/andreim14/beanis?color=blue" alt="License"></a>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
Beanis is an async object-document mapper for Redis, built on Pydantic. You define a model, and Beanis stores each instance as a Redis hash, keeps secondary indexes up to date, and gives you typed objects back.
|
|
15
|
+
|
|
16
|
+
It also does vector search on the vector sets that ship with Redis 8, and it includes long-term memory for AI agents, usable from Python, from the `beanis` command line, or as an MCP server for Claude Code and other agents.
|
|
17
|
+
|
|
18
|
+
It works on plain Redis. No RedisJSON, RediSearch or Redis Stack.
|
|
19
|
+
|
|
20
|
+
[Documentation](https://andreim14.github.io/beanis/) · [PyPI](https://pypi.org/project/beanis/) · [Changelog](docs/changelog.md)
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install beanis # the ODM
|
|
26
|
+
pip install "beanis[mcp]" # plus agent memory: CLI embeddings and MCP server (Python 3.10+)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
You need a Redis server. Redis 5+ is enough for documents and indexes; vector search and memory need Redis 8.0+:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
docker run -d -p 6379:6379 redis:8 # or: brew install redis
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick start
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import asyncio
|
|
39
|
+
|
|
40
|
+
from pydantic import BaseModel
|
|
41
|
+
from redis.asyncio import Redis
|
|
42
|
+
|
|
43
|
+
from beanis import Document, Indexed, init_beanis
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Category(BaseModel):
|
|
47
|
+
name: str
|
|
48
|
+
description: str
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Product(Document):
|
|
52
|
+
name: str
|
|
53
|
+
price: Indexed(float) # sorted-set index: range queries
|
|
54
|
+
category: Indexed(str) # set index: exact match
|
|
55
|
+
tags: list[str] = []
|
|
56
|
+
stock: int = 0
|
|
57
|
+
|
|
58
|
+
class Settings:
|
|
59
|
+
key_prefix = "Product"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
async def main():
|
|
63
|
+
redis = Redis(decode_responses=True)
|
|
64
|
+
await init_beanis(database=redis, document_models=[Product])
|
|
65
|
+
|
|
66
|
+
bar = Product(name="Dark chocolate", price=5.95, category="chocolate", stock=100)
|
|
67
|
+
await bar.insert()
|
|
68
|
+
|
|
69
|
+
same = await Product.get(bar.id)
|
|
70
|
+
cheap = await Product.find(price__lt=10)
|
|
71
|
+
chocolate = await Product.find(category="chocolate", price__gte=5)
|
|
72
|
+
|
|
73
|
+
await bar.update(price=6.50)
|
|
74
|
+
await bar.increment_field("stock", -1)
|
|
75
|
+
await bar.delete_self()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
asyncio.run(main())
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`decode_responses=True` is required. Each document is the hash `Product:<id>`; nested models, lists, dicts, `datetime`, `Decimal`, `UUID` and enums are encoded for you and decoded on read.
|
|
82
|
+
|
|
83
|
+
## Queries
|
|
84
|
+
|
|
85
|
+
`find()` takes keyword filters on indexed fields. Several filters are combined with AND.
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
await Product.find(category="chocolate") # exact match
|
|
89
|
+
await Product.find(price__gte=10, price__lte=50) # range: __gte, __lte, __gt, __lt
|
|
90
|
+
await Product.find(price=12.0) # exact match on a number
|
|
91
|
+
await Product.all(skip=0, limit=20) # every document, by insertion time
|
|
92
|
+
await Product.count()
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Filtering on a field that is not indexed raises `ValueError`. Sorting by other fields and full-text search are not built in; sort the returned list in Python, or use Redis's query engine directly if you need them.
|
|
96
|
+
|
|
97
|
+
Declare indexes with `Indexed(type)` or `Annotated[type, IndexedField()]`. Numbers get a sorted set, strings and other scalars get a set, `GeoPoint` gets a geo index (see the [geo tutorial](docs/tutorial/geo-spatial.md)).
|
|
98
|
+
|
|
99
|
+
## Vector search
|
|
100
|
+
|
|
101
|
+
Embeddings live in a Redis 8 vector set (`VADD` / `VSIM`), next to the document's short fields so you can filter a search:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from typing import List
|
|
105
|
+
|
|
106
|
+
from typing_extensions import Annotated
|
|
107
|
+
|
|
108
|
+
from beanis import Document, IndexedField, VectorField
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class Chunk(Document):
|
|
112
|
+
text: str
|
|
113
|
+
source: Annotated[str, IndexedField()]
|
|
114
|
+
embedding: Annotated[List[float], VectorField(dimensions=384)]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
await Chunk(text="Refunds take 5 days", source="faq", embedding=embed("Refunds take 5 days")).insert()
|
|
118
|
+
|
|
119
|
+
for chunk, score in await Chunk.vector_search(embed("how long is a refund?"), k=3, filter='.source == "faq"'):
|
|
120
|
+
print(round(score, 3), chunk.text) # score: cosine similarity in [0, 1]
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`embed` is any function from text to a list of floats. Vectors use HNSW with int8 quantization by default (`VectorField(quantization="NOQUANT")` keeps float32). Updating or deleting a document updates its vector. `init_beanis` raises at startup if a model has vector fields and the server has no vector sets (Redis 7, Valkey).
|
|
124
|
+
|
|
125
|
+
## Memory for agents
|
|
126
|
+
|
|
127
|
+
`Memory` stores what an agent should keep between conversations and finds it again by meaning. Memories are scoped by namespace, user, session and kind; a new memory that is nearly identical to an old one replaces it instead of piling up.
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from beanis import Memory
|
|
131
|
+
from beanis.memory import fastembed_embedder
|
|
132
|
+
|
|
133
|
+
memory = Memory(redis, embed=fastembed_embedder(), namespace="support-bot")
|
|
134
|
+
|
|
135
|
+
await memory.remember("Prefers answers in Italian", user_id="u42", kind="preference")
|
|
136
|
+
for record, score in await memory.recall("which language should I answer in?", user_id="u42"):
|
|
137
|
+
print(round(score, 3), record.text)
|
|
138
|
+
await memory.recent(user_id="u42")
|
|
139
|
+
await memory.forget(record.id)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`fastembed_embedder()` runs `BAAI/bge-small-en-v1.5` locally on the CPU (no API key). Any embedding function works, sync or async; use the same one for every writer of a namespace.
|
|
143
|
+
|
|
144
|
+
### From the command line
|
|
145
|
+
|
|
146
|
+
`pip install "beanis[mcp]"` installs a `beanis` command. Every command prints JSON, so shell scripts and coding agents can use it directly.
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
beanis doctor # checks Redis, vector sets, extras, stored memory
|
|
150
|
+
beanis memory remember "Deploys go out on Tuesdays" --kind decision
|
|
151
|
+
beanis memory recall "when do we deploy?" -k 3
|
|
152
|
+
beanis memory recent --limit 5
|
|
153
|
+
beanis memory forget <id>
|
|
154
|
+
beanis context # newest memories as plain text, for session hooks
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Configuration is by environment: `REDIS_URL` (default `redis://localhost:6379/0`, or pass `--url`), `BEANIS_MEMORY_NAMESPACE` (default `default`; `auto` gives each git project its own namespace), `BEANIS_EMBED_MODEL` (default `BAAI/bge-small-en-v1.5`).
|
|
158
|
+
|
|
159
|
+
### As an MCP server
|
|
160
|
+
|
|
161
|
+
`beanis-mcp` (or `beanis mcp`) serves the same memory over MCP stdio with four tools: `remember`, `recall`, `forget`, `recent`.
|
|
162
|
+
|
|
163
|
+
Claude Code, as a plugin (MCP server plus a skill that teaches Claude the Beanis API):
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
claude plugin marketplace add andreim14/beanis
|
|
167
|
+
claude plugin install beanis@beanis
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
The plugin gives each project its own namespace and, at the start of every session, puts that project's newest memories into Claude's context, so a new session does not start from zero. Sessions and subagents share the store: what one remembers, the next one knows. It starts everything with `uvx`, so it needs [uv](https://docs.astral.sh/uv/) and a Redis 8 at `REDIS_URL`. Without the plugin:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
claude mcp add beanis-memory --env REDIS_URL=redis://localhost:6379/0 -- beanis-mcp
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Claude Desktop, Cursor and other MCP clients:
|
|
177
|
+
|
|
178
|
+
```json
|
|
179
|
+
{
|
|
180
|
+
"mcpServers": {
|
|
181
|
+
"beanis-memory": {
|
|
182
|
+
"command": "beanis-mcp",
|
|
183
|
+
"env": { "REDIS_URL": "redis://localhost:6379/0" }
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## More
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
await product.insert(ttl=3600) # expire after an hour
|
|
193
|
+
await product.set_ttl(7200); await product.get_ttl(); await product.persist()
|
|
194
|
+
|
|
195
|
+
price = await product.get_field("price") # read or write one field
|
|
196
|
+
await product.set_field("stock", 200)
|
|
197
|
+
|
|
198
|
+
await Product.insert_many(products) # pipelined batches
|
|
199
|
+
await Product.get_many(ids)
|
|
200
|
+
await Product.delete_many(ids)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Event hooks run around inserts, updates and deletes:
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
from beanis import Document, Insert, before_event
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class Product(Document):
|
|
210
|
+
name: str
|
|
211
|
+
price: float
|
|
212
|
+
|
|
213
|
+
@before_event(Insert)
|
|
214
|
+
def check_price(self):
|
|
215
|
+
if self.price < 0:
|
|
216
|
+
raise ValueError("price cannot be negative")
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Custom types are registered once with `register_type(cls, encoder=..., decoder=...)`; NumPy arrays are supported when NumPy is installed. See the [custom encoders tutorial](docs/tutorial/custom-encoders.md).
|
|
220
|
+
|
|
221
|
+
## When to use it
|
|
222
|
+
|
|
223
|
+
Use Beanis when you want typed models on Redis without running Redis modules: caches with structure, session and user state, job metadata, small catalogues, retrieval for RAG, and agent memory.
|
|
224
|
+
|
|
225
|
+
Use something else when you need complex queries across many fields, full-text search or aggregations: a document database, or Redis's query engine with [Redis OM](https://github.com/redis/redis-om-python) or [RedisVL](https://github.com/redis/redis-vl-python).
|
|
226
|
+
|
|
227
|
+
## Requirements
|
|
228
|
+
|
|
229
|
+
- Python 3.9+ (3.10+ for the `mcp` extra)
|
|
230
|
+
- Redis 5.0+; Redis 8.0+ for `VectorField`, `Memory`, the CLI memory commands and the MCP server
|
|
231
|
+
- Pydantic 1.10+ or 2.x
|
|
232
|
+
|
|
233
|
+
## Development
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
pip install -e ".[test,mcp]"
|
|
237
|
+
pytest # unit tests, on fakeredis
|
|
238
|
+
REDIS_PORT=6379 pytest tests/test_real_redis_regressions.py tests/test_memory_real_redis.py # needs Redis 8
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
See [AGENTS.md](AGENTS.md) for repository conventions.
|
|
242
|
+
|
|
243
|
+
## Credits
|
|
244
|
+
|
|
245
|
+
Beanis started as a fork of [Beanie](https://github.com/BeanieODM/beanie), the MongoDB ODM by Roman Right and contributors, rewritten for Redis.
|
|
246
|
+
|
|
247
|
+
## License
|
|
248
|
+
|
|
249
|
+
Apache License 2.0
|