Skip to content

Commit a2e659c

Browse files
feat: add memoria python sdk (#206)
## What type of PR is this? - [x] feat (new feature) - [ ] fix (bug fix) - [ ] docs (documentation) - [ ] style (formatting, no code change) - [ ] refactor (code change that neither fixes a bug nor adds a feature) - [ ] perf (performance improvement) - [ ] test (adding or updating tests) - [ ] chore (maintenance, tooling) - [ ] build / ci (build or CI changes) ## Which issue(s) this PR fixes Fixes # #205 ## What this PR does / why we need it add python sdk for memoria docs: https://github.com/loveRhythm1990/Memoria/blob/8776c5cc3581bfd6dd4323fbb96e0c793090d47d/sdk/python/docs/python_sdk_design.md
1 parent 7b2b372 commit a2e659c

30 files changed

Lines changed: 4112 additions & 0 deletions
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
name: Python SDK Release
2+
3+
on:
4+
push:
5+
tags:
6+
- "python-sdk-v*"
7+
8+
jobs:
9+
release:
10+
runs-on: ubuntu-latest
11+
permissions:
12+
contents: write
13+
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- uses: actions/setup-python@v5
18+
with:
19+
python-version: "3.10"
20+
21+
- name: Verify tag matches package version
22+
run: |
23+
TAG="${GITHUB_REF_NAME#python-sdk-v}"
24+
VERSION=$(python -c "import tomllib; print(tomllib.load(open('sdk/python/pyproject.toml','rb'))['project']['version'])")
25+
echo "Tag version: $TAG"
26+
echo "Package version: $VERSION"
27+
if [ "$TAG" != "$VERSION" ]; then
28+
echo "❌ Tag version ($TAG) does not match pyproject.toml version ($VERSION)"
29+
exit 1
30+
fi
31+
echo "✅ Versions match: $VERSION"
32+
33+
- name: Install build tools
34+
run: pip install build twine
35+
36+
- name: Build wheel and sdist
37+
working-directory: sdk/python
38+
run: python -m build
39+
40+
- name: Validate package metadata (PyPI compatibility check)
41+
run: twine check sdk/python/dist/*
42+
43+
- name: Run unit tests
44+
working-directory: sdk/python
45+
run: |
46+
pip install -e ".[dev]"
47+
python -m pytest tests/unit/ -v
48+
49+
- name: Create GitHub Release and upload assets
50+
uses: softprops/action-gh-release@v2
51+
with:
52+
tag_name: ${{ github.ref_name }}
53+
generate_release_notes: true
54+
prerelease: ${{ contains(github.ref_name, '-rc') || contains(github.ref_name, '-beta') || contains(github.ref_name, '-alpha') }}
55+
files: |
56+
sdk/python/dist/*.whl
57+
sdk/python/dist/*.tar.gz
58+
59+
- uses: pypa/gh-action-pypi-publish@release/v1
60+
with:
61+
packages-dir: sdk/python/dist/
62+
password: ${{ secrets.PYPI_API_TOKEN }}

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,5 @@ scripts/migrate_embedding_dim.sh
4444
dev-signing-key.b64
4545
.npmrc
4646
nohup.out
47+
sdk/python/.venv/
48+
sdk/python/uv.lock

Makefile

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
dev build build-local check \
44
test test-unit test-integration test-e2e test-e2e-mcp bench dev-bench \
55
new-key list-keys revoke-keys \
6+
python-sdk-test python-sdk-test-unit \
67
clean reset
78

89
# Load .env if present
@@ -340,6 +341,29 @@ revoke-keys: check-env
340341
-H "Authorization: Bearer $$MASTER_KEY" && echo "" || \
341342
echo "❌ Failed — is the API running?"
342343

344+
# ── Python SDK Tests ────────────────────────────────────────────────
345+
346+
SDK_PYTHON_DIR = sdk/python
347+
MEMORIA_BASE_URL ?= http://localhost:$${API_PORT:-8100}
348+
# Resolve Python interpreter: prefer .venv inside the SDK dir, fall back to python3
349+
SDK_PYTHON_ABS := $(abspath $(SDK_PYTHON_DIR))
350+
SDK_PYTHON := $(shell [ -f "$(SDK_PYTHON_ABS)/.venv/bin/python" ] && echo "$(SDK_PYTHON_ABS)/.venv/bin/python" || echo python3)
351+
352+
# Integration tests: unit + integration (needs `make up` first)
353+
python-sdk-test: check-env
354+
@API_PORT=$${API_PORT:-8100}; \
355+
curl -sf --noproxy localhost http://localhost:$$API_PORT/health \
356+
> /dev/null 2>&1 || \
357+
(echo "❌ Memoria API not running — run: make up"; exit 1)
358+
@cd $(SDK_PYTHON_DIR) && \
359+
MEMORIA_BASE_URL=$(MEMORIA_BASE_URL) \
360+
MEMORIA_MASTER_KEY=$${MEMORIA_MASTER_KEY} \
361+
$(SDK_PYTHON) -m pytest tests/unit/ tests/integration/ -v
362+
363+
# Unit tests only: no running API needed, uses mock HTTP
364+
python-sdk-test-unit:
365+
@cd $(SDK_PYTHON_DIR) && $(SDK_PYTHON) -m pytest tests/unit/ -v
366+
343367
# ── Clean ───────────────────────────────────────────────────────────
344368

345369
clean:

sdk/python/CHANGELOG.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Changelog
2+
3+
## [Unreleased]
4+
5+
### Fixed
6+
- `ping()` no longer wraps `MemoriaAuthError` / `MemoriaNotFoundError` and other API errors
7+
into `MemoriaConnectionError`; callers can now distinguish network failures from API errors.
8+
- `_map_error`: empty response body no longer produces duplicate status code in the error
9+
message (e.g. `"HTTP 404: HTTP 404"``"HTTP 404: Not Found"`).
10+
11+
## [1.0.0] - 2026-05-25
12+
13+
### Added
14+
- Initial release of the Memoria Python SDK
15+
- `MemoriaClient` (sync) and `AsyncMemoriaClient` (async) with identical interfaces
16+
- Full memories resource: store, store_batch, retrieve, search, list, correct, correct_by_query,
17+
delete, purge, feedback
18+
- observe endpoint for session memory extraction
19+
- profile.me()
20+
- snapshots: create, list, rollback, delete (single + bulk/prefix/date)
21+
- branches: create, list, checkout, diff, diff_items, merge, delete, apply, pick
22+
- governance: run, consolidate, reflect
23+
- ping / health check
24+
- Context-manager support (`with` / `async with`) for connection lifecycle
25+
- Structured exception hierarchy: MemoriaAuthError, MemoriaForbiddenError,
26+
MemoriaNotFoundError, MemoriaUnprocessableError, MemoriaServerError, MemoriaConnectionError
27+
- Exponential-backoff retry on 5xx and network errors (configurable max_retries)
28+
- dataclasses response models — zero extra dependencies beyond httpx

sdk/python/README.md

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
# Memoria Python SDK
2+
3+
Official Python SDK for the [Memoria](https://github.com/matrixorigin/Memoria) memory engine.
4+
5+
## Installation
6+
7+
**v1 — GitHub Release wheel:**
8+
9+
```bash
10+
pip install https://github.com/matrixorigin/Memoria/releases/download/python-sdk-v1.0.0/memoria_client-1.0.0-py3-none-any.whl
11+
```
12+
13+
**Offline / air-gapped:**
14+
15+
```bash
16+
pip install ./memoria_client-1.0.0-py3-none-any.whl
17+
```
18+
19+
**From source (development):**
20+
21+
```bash
22+
pip install -e ".[dev]"
23+
```
24+
25+
## Quick Start
26+
27+
```python
28+
from memoria import MemoriaClient
29+
30+
with MemoriaClient(base_url="http://localhost:8100", api_key="sk-...") as client:
31+
# Store a memory
32+
mem = client.memories.store(content="Prefers concise answers", memory_type="profile")
33+
34+
# Retrieve relevant memories
35+
result = client.memories.retrieve(query="answer style", top_k=5)
36+
for item in result.items:
37+
print(item.content)
38+
39+
# Observe a conversation turn (auto-extracts memories)
40+
client.observe(messages=[
41+
{"role": "user", "content": "What is the capital of France?"},
42+
{"role": "assistant", "content": "Paris."},
43+
])
44+
```
45+
46+
### Async usage
47+
48+
```python
49+
from memoria import AsyncMemoriaClient
50+
51+
async with AsyncMemoriaClient(base_url="http://localhost:8100", api_key="sk-...") as client:
52+
await client.ping()
53+
mem = await client.memories.store(content="...", memory_type="semantic")
54+
result = await client.memories.retrieve(query="...")
55+
```
56+
57+
## Resources
58+
59+
### memories
60+
61+
```python
62+
# Store
63+
mem = client.memories.store(content="...", memory_type="semantic")
64+
65+
# Batch store (max 100 items, max 32 KiB per item)
66+
mems = client.memories.store_batch([{"content": "a"}, {"content": "b"}])
67+
68+
# Retrieve (hybrid vector + fulltext)
69+
result = client.memories.retrieve(query="...", top_k=5)
70+
71+
# Search
72+
result = client.memories.search(query="...", top_k=10)
73+
74+
# List with pagination
75+
page = client.memories.list(limit=100, cursor=None)
76+
# page.next_cursor — pass as cursor= to get the next page
77+
78+
# Correct by ID
79+
mem = client.memories.correct("mem_id", new_content="...", reason="...")
80+
81+
# Correct by semantic query
82+
mem = client.memories.correct_by_query(query="old content", new_content="new content")
83+
84+
# Delete
85+
client.memories.delete("mem_id", reason="done")
86+
87+
# Purge (choose one selector)
88+
result = client.memories.purge(memory_ids=["id1", "id2"], reason="cleanup")
89+
result = client.memories.purge(topic="debug session", reason="done")
90+
result = client.memories.purge(session_id="sess_x", memory_types=["working"], reason="end")
91+
92+
# Feedback
93+
client.memories.feedback("mem_id", signal="useful") # useful|irrelevant|outdated|wrong
94+
```
95+
96+
### snapshots
97+
98+
```python
99+
snap = client.snapshots.create(name="before-cleanup")
100+
snaps = client.snapshots.list(limit=20)
101+
client.snapshots.rollback("before-cleanup")
102+
103+
client.snapshots.delete("before-cleanup") # single
104+
client.snapshots.delete(names=["s1", "s2"]) # multiple
105+
client.snapshots.delete(prefix="pre_") # by prefix
106+
client.snapshots.delete(older_than="2026-01-01") # by date
107+
```
108+
109+
### branches
110+
111+
```python
112+
client.branches.create(name="experiment-1")
113+
branches = client.branches.list()
114+
client.branches.checkout(name="experiment-1")
115+
116+
diff = client.branches.diff("experiment-1") # summary stats
117+
items = client.branches.diff_items("experiment-1", limit=50) # per-entry
118+
119+
client.branches.merge("experiment-1", strategy="accept")
120+
client.branches.apply("experiment-1", adds=["mem_id_1"])
121+
client.branches.pick("experiment-1", selector={"type": "key_list", "keys": ["mem_id_1"]})
122+
client.branches.delete("experiment-1")
123+
```
124+
125+
### governance
126+
127+
```python
128+
result = client.governance.run()
129+
if result.skipped:
130+
print(f"Cooldown: {result.cooldown_remaining_s}s remaining")
131+
else:
132+
print(f"Cleaned {result.cleaned_stale} stale memories")
133+
134+
result = client.governance.consolidate()
135+
result = client.governance.reflect(mode="auto") # auto|candidates|internal
136+
result = client.governance.reflect(mode="candidates") # never on cooldown
137+
138+
# Bypass cooldown
139+
client.governance.run(force=True)
140+
```
141+
142+
## Error Handling
143+
144+
```python
145+
from memoria import (
146+
MemoriaConnectionError, # network unreachable / timeout
147+
MemoriaAPIError, # base class for all HTTP error responses
148+
MemoriaAuthError, # 401 — invalid key or rate-limit exceeded
149+
MemoriaForbiddenError, # 403 — e.g. write to main in multi-member group mode
150+
MemoriaNotFoundError, # 404
151+
MemoriaUnprocessableError,# 422 — server-side validation (empty content, bad type, etc.)
152+
MemoriaServerError, # 5xx
153+
MemoriaValidationError, # local validation (request not sent)
154+
)
155+
156+
try:
157+
mem = client.memories.store(content="")
158+
except MemoriaUnprocessableError as e:
159+
print(f"Validation failed: {e.detail}")
160+
except MemoriaAuthError:
161+
print("Check your API key, or you may have hit the rate limit")
162+
```
163+
164+
`ping()` raises `MemoriaConnectionError` for network failures and `MemoriaAPIError` (or a
165+
subclass) for HTTP error responses — callers can distinguish the two:
166+
167+
```python
168+
from memoria import MemoriaAPIError, MemoriaConnectionError
169+
170+
try:
171+
client.ping()
172+
except MemoriaConnectionError:
173+
print("Cannot reach the server")
174+
except MemoriaAPIError as e:
175+
print(f"Server returned HTTP {e.status_code}: {e.detail}")
176+
```
177+
178+
## Compatibility Matrix
179+
180+
| SDK version | Memoria API | Python |
181+
|-------------|-------------|--------|
182+
| 1.0.x | >= 0.2.3 | >= 3.10 |
183+
184+
## Development
185+
186+
```bash
187+
cd sdk/python
188+
pip install -e ".[dev]"
189+
pytest tests/unit/ -v # unit tests (no API needed)
190+
make python-sdk-test # full integration tests (needs make up)
191+
```

0 commit comments

Comments
 (0)