mnemoverse 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.
@@ -0,0 +1,12 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .eggs/
8
+ .mypy_cache/
9
+ .ruff_cache/
10
+ .pytest_cache/
11
+ .venv/
12
+ *.so
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mnemoverse
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,104 @@
1
+ Metadata-Version: 2.4
2
+ Name: mnemoverse
3
+ Version: 0.1.0
4
+ Summary: Python SDK for Mnemoverse Memory API — persistent memory for AI agents
5
+ Project-URL: Homepage, https://mnemoverse.com
6
+ Project-URL: Documentation, https://mnemoverse.com/docs/api/python-sdk
7
+ Project-URL: Repository, https://github.com/mnemoverse/mnemoverse-sdk-python
8
+ Project-URL: Issues, https://github.com/mnemoverse/mnemoverse-sdk-python/issues
9
+ Author-email: Edward Izgorodin <helloworld@uinside.org>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: agents,ai,cognitive,hebbian,llm,mcp,memory
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: httpx>=0.25.0
25
+ Requires-Dist: pydantic>=2.0.0
26
+ Provides-Extra: dev
27
+ Requires-Dist: mypy>=1.10; extra == 'dev'
28
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
29
+ Requires-Dist: pytest-httpx>=0.30; extra == 'dev'
30
+ Requires-Dist: pytest>=8.0; extra == 'dev'
31
+ Requires-Dist: ruff>=0.4; extra == 'dev'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # Mnemoverse Python SDK
35
+
36
+ Persistent memory for AI agents. Not vector search — statistical learning.
37
+
38
+ ## Installation
39
+
40
+ ```bash
41
+ pip install mnemoverse
42
+ ```
43
+
44
+ ## Quick Start
45
+
46
+ ```python
47
+ from mnemoverse import MnemoClient
48
+
49
+ client = MnemoClient(api_key="mk_live_YOUR_KEY")
50
+
51
+ # Store a memory
52
+ result = client.write(
53
+ "Retry with exponential backoff fixed the timeout issue",
54
+ concepts=["retry", "backoff", "timeout"]
55
+ )
56
+
57
+ # Query — Hebbian associations expand "timeout" → "retry", "backoff"
58
+ memories = client.read("how to handle timeouts?")
59
+
60
+ # Report outcome — the system learns what works
61
+ client.feedback(
62
+ atom_ids=[item.atom_id for item in memories.items],
63
+ outcome=1.0,
64
+ query_concepts=memories.query_concepts
65
+ )
66
+ ```
67
+
68
+ ## Async Client
69
+
70
+ ```python
71
+ from mnemoverse import AsyncMnemoClient
72
+
73
+ async with AsyncMnemoClient(api_key="mk_live_YOUR_KEY") as client:
74
+ result = await client.write("async memory", concepts=["async"])
75
+ memories = await client.read("what about async?")
76
+ ```
77
+
78
+ ## Features
79
+
80
+ - **Circuit breaker** — 5 failures → open → 30s half-open → probe
81
+ - **Retry with backoff** — 3 attempts, rate-limit-aware
82
+ - **Sync + async** — `MnemoClient` for scripts, `AsyncMnemoClient` for FastAPI
83
+ - **Type-safe** — Pydantic models, full type hints
84
+
85
+ ## Methods
86
+
87
+ | Method | Description |
88
+ |--------|-------------|
89
+ | `write(content, concepts, domain, metadata)` | Store a memory |
90
+ | `write_batch(items)` | Store up to 500 memories |
91
+ | `read(query, top_k, domain)` | Query with Hebbian expansion |
92
+ | `feedback(atom_ids, outcome)` | Report success/failure |
93
+ | `stats()` | Memory statistics |
94
+ | `health()` | API health check |
95
+
96
+ ## Documentation
97
+
98
+ - [Getting Started](https://mnemoverse.com/docs/api/getting-started)
99
+ - [API Reference](https://mnemoverse.com/docs/api/reference)
100
+ - [Python SDK Docs](https://mnemoverse.com/docs/api/python-sdk)
101
+
102
+ ## License
103
+
104
+ MIT
@@ -0,0 +1,71 @@
1
+ # Mnemoverse Python SDK
2
+
3
+ Persistent memory for AI agents. Not vector search — statistical learning.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install mnemoverse
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```python
14
+ from mnemoverse import MnemoClient
15
+
16
+ client = MnemoClient(api_key="mk_live_YOUR_KEY")
17
+
18
+ # Store a memory
19
+ result = client.write(
20
+ "Retry with exponential backoff fixed the timeout issue",
21
+ concepts=["retry", "backoff", "timeout"]
22
+ )
23
+
24
+ # Query — Hebbian associations expand "timeout" → "retry", "backoff"
25
+ memories = client.read("how to handle timeouts?")
26
+
27
+ # Report outcome — the system learns what works
28
+ client.feedback(
29
+ atom_ids=[item.atom_id for item in memories.items],
30
+ outcome=1.0,
31
+ query_concepts=memories.query_concepts
32
+ )
33
+ ```
34
+
35
+ ## Async Client
36
+
37
+ ```python
38
+ from mnemoverse import AsyncMnemoClient
39
+
40
+ async with AsyncMnemoClient(api_key="mk_live_YOUR_KEY") as client:
41
+ result = await client.write("async memory", concepts=["async"])
42
+ memories = await client.read("what about async?")
43
+ ```
44
+
45
+ ## Features
46
+
47
+ - **Circuit breaker** — 5 failures → open → 30s half-open → probe
48
+ - **Retry with backoff** — 3 attempts, rate-limit-aware
49
+ - **Sync + async** — `MnemoClient` for scripts, `AsyncMnemoClient` for FastAPI
50
+ - **Type-safe** — Pydantic models, full type hints
51
+
52
+ ## Methods
53
+
54
+ | Method | Description |
55
+ |--------|-------------|
56
+ | `write(content, concepts, domain, metadata)` | Store a memory |
57
+ | `write_batch(items)` | Store up to 500 memories |
58
+ | `read(query, top_k, domain)` | Query with Hebbian expansion |
59
+ | `feedback(atom_ids, outcome)` | Report success/failure |
60
+ | `stats()` | Memory statistics |
61
+ | `health()` | API health check |
62
+
63
+ ## Documentation
64
+
65
+ - [Getting Started](https://mnemoverse.com/docs/api/getting-started)
66
+ - [API Reference](https://mnemoverse.com/docs/api/reference)
67
+ - [Python SDK Docs](https://mnemoverse.com/docs/api/python-sdk)
68
+
69
+ ## License
70
+
71
+ MIT
@@ -0,0 +1,34 @@
1
+ """Mnemoverse Python SDK — persistent memory for AI agents."""
2
+
3
+ from mnemoverse.client import MnemoClient
4
+ from mnemoverse._async_client import AsyncMnemoClient
5
+ from mnemoverse.errors import MnemoError, MnemoAuthError, MnemoRateLimitError, MnemoUnavailableError
6
+ from mnemoverse.types import (
7
+ WriteResponse,
8
+ WriteBatchResponse,
9
+ WriteBatchItemResult,
10
+ ReadResponse,
11
+ MemoryItem,
12
+ FeedbackResponse,
13
+ StatsResponse,
14
+ HealthResponse,
15
+ )
16
+
17
+ __version__ = "0.1.0"
18
+
19
+ __all__ = [
20
+ "MnemoClient",
21
+ "AsyncMnemoClient",
22
+ "MnemoError",
23
+ "MnemoAuthError",
24
+ "MnemoRateLimitError",
25
+ "MnemoUnavailableError",
26
+ "WriteResponse",
27
+ "WriteBatchResponse",
28
+ "WriteBatchItemResult",
29
+ "ReadResponse",
30
+ "MemoryItem",
31
+ "FeedbackResponse",
32
+ "StatsResponse",
33
+ "HealthResponse",
34
+ ]
@@ -0,0 +1,237 @@
1
+ """Async Mnemoverse client using httpx."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+ from uuid import UUID
7
+
8
+ import httpx
9
+
10
+ from mnemoverse._retry import CircuitBreaker, retry_with_backoff
11
+ from mnemoverse.errors import (
12
+ MnemoAuthError,
13
+ MnemoError,
14
+ MnemoRateLimitError,
15
+ MnemoUnavailableError,
16
+ )
17
+ from mnemoverse.types import (
18
+ FeedbackResponse,
19
+ HealthResponse,
20
+ ReadResponse,
21
+ StatsResponse,
22
+ WriteBatchResponse,
23
+ WriteResponse,
24
+ )
25
+
26
+ _DEFAULT_BASE_URL = "https://api.mnemoverse.com"
27
+
28
+
29
+ class AsyncMnemoClient:
30
+ """Async client for the Mnemoverse Memory API.
31
+
32
+ Features:
33
+ - Circuit breaker (5 failures → open → 30s half-open)
34
+ - Timeout (10s default)
35
+ - Retry with exponential backoff (3 attempts, rate-limit-aware)
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ api_key: str,
41
+ base_url: str = _DEFAULT_BASE_URL,
42
+ timeout: float = 10.0,
43
+ max_retries: int = 3,
44
+ ) -> None:
45
+ self._api_key = api_key
46
+ self._base_url = base_url.rstrip("/")
47
+ self._timeout = timeout
48
+ self._max_retries = max_retries
49
+ self._cb = CircuitBreaker(failure_threshold=5, reset_timeout=30.0)
50
+ self._client: httpx.AsyncClient | None = None
51
+
52
+ def _get_client(self) -> httpx.AsyncClient:
53
+ if self._client is None or self._client.is_closed:
54
+ self._client = httpx.AsyncClient(
55
+ base_url=self._base_url,
56
+ headers={
57
+ "X-Api-Key": self._api_key,
58
+ "Content-Type": "application/json",
59
+ "Accept": "application/json",
60
+ },
61
+ timeout=self._timeout,
62
+ )
63
+ return self._client
64
+
65
+ async def close(self) -> None:
66
+ if self._client and not self._client.is_closed:
67
+ await self._client.aclose()
68
+
69
+ async def __aenter__(self) -> AsyncMnemoClient:
70
+ return self
71
+
72
+ async def __aexit__(self, *args: Any) -> None:
73
+ await self.close()
74
+
75
+ # --- Public API ---
76
+
77
+ async def write(
78
+ self,
79
+ content: str,
80
+ *,
81
+ concepts: list[str] | None = None,
82
+ domain: str = "general",
83
+ metadata: dict[str, Any] | None = None,
84
+ external_ref: str | None = None,
85
+ ) -> WriteResponse:
86
+ """Store a single memory atom."""
87
+ body: dict[str, Any] = {"content": content, "domain": domain}
88
+ if concepts:
89
+ body["concepts"] = concepts
90
+ if metadata:
91
+ body["metadata"] = metadata
92
+ if external_ref:
93
+ body["external_ref"] = external_ref
94
+ data = await self._request("POST", "/api/v1/memory/write", json=body)
95
+ return WriteResponse.model_validate(data)
96
+
97
+ async def write_batch(
98
+ self,
99
+ items: list[dict[str, Any]],
100
+ ) -> WriteBatchResponse:
101
+ """Store up to 500 atoms in one request."""
102
+ data = await self._request("POST", "/api/v1/memory/write-batch", json={"items": items})
103
+ return WriteBatchResponse.model_validate(data)
104
+
105
+ async def read(
106
+ self,
107
+ query: str,
108
+ *,
109
+ top_k: int = 10,
110
+ domain: str | None = None,
111
+ min_relevance: float = 0.3,
112
+ include_associations: bool = True,
113
+ concepts: list[str] | None = None,
114
+ ) -> ReadResponse:
115
+ """Query memory with semantic search + Hebbian expansion."""
116
+ body: dict[str, Any] = {
117
+ "query": query,
118
+ "top_k": top_k,
119
+ "min_relevance": min_relevance,
120
+ "include_associations": include_associations,
121
+ }
122
+ if domain:
123
+ body["domain"] = domain
124
+ if concepts:
125
+ body["concepts"] = concepts
126
+ data = await self._request("POST", "/api/v1/memory/read", json=body)
127
+ return ReadResponse.model_validate(data)
128
+
129
+ async def feedback(
130
+ self,
131
+ atom_ids: list[UUID | str],
132
+ outcome: float,
133
+ *,
134
+ concepts: list[str] | None = None,
135
+ query_concepts: list[str] | None = None,
136
+ domain: str = "general",
137
+ ) -> FeedbackResponse:
138
+ """Report outcome (success/failure) for memories."""
139
+ body: dict[str, Any] = {
140
+ "atom_ids": [str(aid) for aid in atom_ids],
141
+ "outcome": outcome,
142
+ "domain": domain,
143
+ }
144
+ if concepts:
145
+ body["concepts"] = concepts
146
+ if query_concepts:
147
+ body["query_concepts"] = query_concepts
148
+ data = await self._request("POST", "/api/v1/memory/feedback", json=body)
149
+ return FeedbackResponse.model_validate(data)
150
+
151
+ async def stats(self) -> StatsResponse:
152
+ """Get memory statistics."""
153
+ data = await self._request("GET", "/api/v1/memory/stats")
154
+ return StatsResponse.model_validate(data)
155
+
156
+ async def health(self) -> HealthResponse:
157
+ """Check API health."""
158
+ data = await self._request("GET", "/api/v1/health")
159
+ return HealthResponse.model_validate(data)
160
+
161
+ # --- Internal ---
162
+
163
+ async def _request(
164
+ self,
165
+ method: str,
166
+ path: str,
167
+ json: dict[str, Any] | None = None,
168
+ ) -> Any:
169
+ if not self._cb.can_execute():
170
+ raise MnemoUnavailableError(
171
+ f"Circuit breaker open (state: {self._cb.state})"
172
+ )
173
+
174
+ def is_retryable(e: Exception) -> bool:
175
+ if isinstance(e, MnemoRateLimitError):
176
+ return True
177
+ if isinstance(e, MnemoError) and e.status and e.status >= 500:
178
+ return True
179
+ if isinstance(e, (httpx.ConnectError, httpx.TimeoutException)):
180
+ return True
181
+ return False
182
+
183
+ async def attempt() -> Any:
184
+ return await self._single_request(method, path, json)
185
+
186
+ try:
187
+ result = await retry_with_backoff(
188
+ attempt,
189
+ max_retries=self._max_retries,
190
+ retryable_check=is_retryable,
191
+ )
192
+ self._cb.on_success()
193
+ return result
194
+ except (MnemoAuthError, MnemoError) as e:
195
+ if isinstance(e, MnemoAuthError):
196
+ raise
197
+ self._cb.on_failure()
198
+ raise
199
+
200
+ async def _single_request(
201
+ self,
202
+ method: str,
203
+ path: str,
204
+ json: dict[str, Any] | None = None,
205
+ ) -> Any:
206
+ client = self._get_client()
207
+ try:
208
+ response = await client.request(method, path, json=json)
209
+ except httpx.TimeoutException as e:
210
+ raise MnemoUnavailableError(f"Request timeout after {self._timeout}s", e)
211
+ except httpx.ConnectError as e:
212
+ raise MnemoUnavailableError(f"Connection error: {e}", e)
213
+
214
+ if response.status_code == 401 or response.status_code == 403:
215
+ raise MnemoAuthError(self._extract_detail(response))
216
+
217
+ if response.status_code == 429:
218
+ retry_after = response.headers.get("Retry-After")
219
+ raise MnemoRateLimitError(
220
+ self._extract_detail(response),
221
+ retry_after=float(retry_after) if retry_after else None,
222
+ )
223
+
224
+ if response.status_code >= 400:
225
+ raise MnemoError(self._extract_detail(response), status=response.status_code)
226
+
227
+ return response.json()
228
+
229
+ @staticmethod
230
+ def _extract_detail(response: httpx.Response) -> str:
231
+ try:
232
+ data = response.json()
233
+ if isinstance(data, dict):
234
+ return str(data.get("detail") or data.get("message") or data)
235
+ return str(data)
236
+ except Exception:
237
+ return f"HTTP {response.status_code}"
@@ -0,0 +1,74 @@
1
+ """Retry with exponential backoff and circuit breaker."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import time
7
+ from typing import Any
8
+
9
+
10
+ class CircuitBreaker:
11
+ """Simple circuit breaker: closed → open (after N failures) → half-open (after timeout)."""
12
+
13
+ def __init__(self, failure_threshold: int = 5, reset_timeout: float = 30.0) -> None:
14
+ self._failure_threshold = failure_threshold
15
+ self._reset_timeout = reset_timeout
16
+ self._failures = 0
17
+ self._last_failure_time = 0.0
18
+ self._state: str = "closed" # closed | open | half-open
19
+
20
+ @property
21
+ def state(self) -> str:
22
+ return self._state
23
+
24
+ def can_execute(self) -> bool:
25
+ if self._state == "closed":
26
+ return True
27
+ if self._state == "open":
28
+ if time.monotonic() - self._last_failure_time >= self._reset_timeout:
29
+ self._state = "half-open"
30
+ return True
31
+ return False
32
+ return False # half-open: block until probe completes
33
+
34
+ def on_success(self) -> None:
35
+ self._failures = 0
36
+ self._state = "closed"
37
+
38
+ def on_failure(self) -> None:
39
+ self._failures += 1
40
+ self._last_failure_time = time.monotonic()
41
+ if self._failures >= self._failure_threshold:
42
+ self._state = "open"
43
+
44
+
45
+ async def retry_with_backoff(
46
+ coro_factory: Any,
47
+ max_retries: int = 3,
48
+ base_delay: float = 0.1,
49
+ max_delay: float = 2.0,
50
+ retryable_check: Any = None,
51
+ ) -> Any:
52
+ """Execute an async callable with exponential backoff.
53
+
54
+ Args:
55
+ coro_factory: Async callable (called each attempt).
56
+ max_retries: Max retry attempts (total attempts = max_retries + 1).
57
+ base_delay: Initial backoff delay in seconds.
58
+ max_delay: Maximum backoff delay in seconds.
59
+ retryable_check: Optional callable(exception) -> bool.
60
+ """
61
+ last_error: Exception | None = None
62
+
63
+ for attempt in range(max_retries + 1):
64
+ try:
65
+ return await coro_factory()
66
+ except Exception as e:
67
+ last_error = e
68
+ if retryable_check and not retryable_check(e):
69
+ raise
70
+ if attempt < max_retries:
71
+ delay = min(base_delay * (2 ** attempt), max_delay)
72
+ await asyncio.sleep(delay)
73
+
74
+ raise last_error # type: ignore[misc]
@@ -0,0 +1,128 @@
1
+ """Synchronous Mnemoverse client — wraps AsyncMnemoClient with asyncio.run()."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from typing import Any
7
+ from uuid import UUID
8
+
9
+ from mnemoverse._async_client import AsyncMnemoClient
10
+ from mnemoverse.types import (
11
+ FeedbackResponse,
12
+ HealthResponse,
13
+ ReadResponse,
14
+ StatsResponse,
15
+ WriteBatchResponse,
16
+ WriteResponse,
17
+ )
18
+
19
+
20
+ class MnemoClient:
21
+ """Synchronous client for the Mnemoverse Memory API.
22
+
23
+ Wraps AsyncMnemoClient for use in scripts, notebooks, and sync applications.
24
+ For async applications (FastAPI, Discord bots), use AsyncMnemoClient directly.
25
+
26
+ Usage:
27
+ client = MnemoClient(api_key="mk_live_...")
28
+ result = client.write("Caching reduces latency", concepts=["caching"])
29
+ memories = client.read("how to reduce latency?")
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ api_key: str,
35
+ base_url: str = "https://api.mnemoverse.com",
36
+ timeout: float = 10.0,
37
+ max_retries: int = 3,
38
+ ) -> None:
39
+ self._async_client = AsyncMnemoClient(
40
+ api_key=api_key,
41
+ base_url=base_url,
42
+ timeout=timeout,
43
+ max_retries=max_retries,
44
+ )
45
+
46
+ def _run(self, coro: Any) -> Any:
47
+ try:
48
+ loop = asyncio.get_running_loop()
49
+ except RuntimeError:
50
+ loop = None
51
+
52
+ if loop and loop.is_running():
53
+ # Inside an existing event loop (e.g., Jupyter notebook)
54
+ import concurrent.futures
55
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
56
+ return pool.submit(asyncio.run, coro).result()
57
+ else:
58
+ return asyncio.run(coro)
59
+
60
+ def write(
61
+ self,
62
+ content: str,
63
+ *,
64
+ concepts: list[str] | None = None,
65
+ domain: str = "general",
66
+ metadata: dict[str, Any] | None = None,
67
+ external_ref: str | None = None,
68
+ ) -> WriteResponse:
69
+ """Store a single memory atom."""
70
+ return self._run(
71
+ self._async_client.write(
72
+ content, concepts=concepts, domain=domain,
73
+ metadata=metadata, external_ref=external_ref,
74
+ )
75
+ )
76
+
77
+ def write_batch(self, items: list[dict[str, Any]]) -> WriteBatchResponse:
78
+ """Store up to 500 atoms in one request."""
79
+ return self._run(self._async_client.write_batch(items))
80
+
81
+ def read(
82
+ self,
83
+ query: str,
84
+ *,
85
+ top_k: int = 10,
86
+ domain: str | None = None,
87
+ min_relevance: float = 0.3,
88
+ include_associations: bool = True,
89
+ concepts: list[str] | None = None,
90
+ ) -> ReadResponse:
91
+ """Query memory with semantic search + Hebbian expansion."""
92
+ return self._run(
93
+ self._async_client.read(
94
+ query, top_k=top_k, domain=domain,
95
+ min_relevance=min_relevance,
96
+ include_associations=include_associations,
97
+ concepts=concepts,
98
+ )
99
+ )
100
+
101
+ def feedback(
102
+ self,
103
+ atom_ids: list[UUID | str],
104
+ outcome: float,
105
+ *,
106
+ concepts: list[str] | None = None,
107
+ query_concepts: list[str] | None = None,
108
+ domain: str = "general",
109
+ ) -> FeedbackResponse:
110
+ """Report outcome (success/failure) for memories."""
111
+ return self._run(
112
+ self._async_client.feedback(
113
+ atom_ids, outcome, concepts=concepts,
114
+ query_concepts=query_concepts, domain=domain,
115
+ )
116
+ )
117
+
118
+ def stats(self) -> StatsResponse:
119
+ """Get memory statistics."""
120
+ return self._run(self._async_client.stats())
121
+
122
+ def health(self) -> HealthResponse:
123
+ """Check API health."""
124
+ return self._run(self._async_client.health())
125
+
126
+ def close(self) -> None:
127
+ """Close the underlying HTTP client."""
128
+ self._run(self._async_client.close())
@@ -0,0 +1,35 @@
1
+ """Mnemoverse SDK error types."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class MnemoError(Exception):
7
+ """Base error for all Mnemoverse API errors."""
8
+
9
+ def __init__(self, message: str, status: int | None = None) -> None:
10
+ super().__init__(message)
11
+ self.message = message
12
+ self.status = status
13
+
14
+
15
+ class MnemoAuthError(MnemoError):
16
+ """Invalid or missing API key (401/403)."""
17
+
18
+ def __init__(self, message: str = "Invalid or missing API key") -> None:
19
+ super().__init__(message, status=401)
20
+
21
+
22
+ class MnemoRateLimitError(MnemoError):
23
+ """Rate limit exceeded (429)."""
24
+
25
+ def __init__(self, message: str = "Rate limit exceeded", retry_after: float | None = None) -> None:
26
+ super().__init__(message, status=429)
27
+ self.retry_after = retry_after
28
+
29
+
30
+ class MnemoUnavailableError(MnemoError):
31
+ """Service unreachable — circuit breaker open, network error, or timeout."""
32
+
33
+ def __init__(self, message: str, cause: Exception | None = None) -> None:
34
+ super().__init__(message, status=None)
35
+ self.__cause__ = cause
@@ -0,0 +1,92 @@
1
+ """Pydantic models matching mnemoverse-core REST API schemas.
2
+
3
+ Source of truth: mnemoverse-core/src/mnemo/api/schemas.py
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any
9
+ from uuid import UUID
10
+
11
+ from pydantic import BaseModel
12
+
13
+
14
+ # --- Write ---
15
+
16
+
17
+ class WriteResponse(BaseModel):
18
+ stored: bool
19
+ atom_id: UUID | None = None
20
+ importance: float = 0.0
21
+ reason: str = ""
22
+
23
+
24
+ class WriteBatchItemResult(BaseModel):
25
+ index: int
26
+ stored: bool
27
+ atom_id: UUID | None = None
28
+ importance: float = 0.0
29
+ error: str | None = None
30
+
31
+
32
+ class WriteBatchResponse(BaseModel):
33
+ total_count: int
34
+ stored_count: int
35
+ results: list[WriteBatchItemResult]
36
+
37
+
38
+ # --- Read ---
39
+
40
+
41
+ class MemoryItem(BaseModel):
42
+ atom_id: UUID
43
+ content: str
44
+ relevance: float
45
+ similarity: float
46
+ valence: float
47
+ importance: float
48
+ source: str
49
+ concepts: list[str]
50
+ domain: str
51
+ metadata: dict[str, Any] = {}
52
+
53
+
54
+ class ReadResponse(BaseModel):
55
+ items: list[MemoryItem]
56
+ episodic_hit: bool
57
+ query_concepts: list[str]
58
+ expanded_concepts: list[str]
59
+ search_time_ms: float
60
+
61
+
62
+ # --- Feedback ---
63
+
64
+
65
+ class FeedbackResponse(BaseModel):
66
+ updated_count: int
67
+ avg_valence: float
68
+ coactivation_edges: int = 0
69
+
70
+
71
+ # --- Stats ---
72
+
73
+
74
+ class StatsResponse(BaseModel):
75
+ total_atoms: int
76
+ episodes: int
77
+ prototypes: int
78
+ singletons: int
79
+ hebbian_edges: int
80
+ episodic_fingerprints: int
81
+ domains: list[str]
82
+ avg_valence: float
83
+ avg_importance: float
84
+
85
+
86
+ # --- Health ---
87
+
88
+
89
+ class HealthResponse(BaseModel):
90
+ status: str
91
+ database: bool
92
+ version: str
@@ -0,0 +1,57 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "mnemoverse"
7
+ version = "0.1.0"
8
+ description = "Python SDK for Mnemoverse Memory API — persistent memory for AI agents"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ { name = "Edward Izgorodin", email = "helloworld@uinside.org" },
14
+ ]
15
+ keywords = ["ai", "memory", "agents", "llm", "mcp", "hebbian", "cognitive"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
26
+ "Typing :: Typed",
27
+ ]
28
+ dependencies = [
29
+ "httpx>=0.25.0",
30
+ "pydantic>=2.0.0",
31
+ ]
32
+
33
+ [project.urls]
34
+ Homepage = "https://mnemoverse.com"
35
+ Documentation = "https://mnemoverse.com/docs/api/python-sdk"
36
+ Repository = "https://github.com/mnemoverse/mnemoverse-sdk-python"
37
+ Issues = "https://github.com/mnemoverse/mnemoverse-sdk-python/issues"
38
+
39
+ [project.optional-dependencies]
40
+ dev = [
41
+ "pytest>=8.0",
42
+ "pytest-asyncio>=0.24",
43
+ "pytest-httpx>=0.30",
44
+ "ruff>=0.4",
45
+ "mypy>=1.10",
46
+ ]
47
+
48
+ [tool.ruff]
49
+ target-version = "py310"
50
+ line-length = 100
51
+
52
+ [tool.mypy]
53
+ python_version = "3.10"
54
+ strict = true
55
+
56
+ [tool.pytest.ini_options]
57
+ asyncio_mode = "auto"
File without changes
@@ -0,0 +1,143 @@
1
+ """Tests for MnemoClient and AsyncMnemoClient."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+ import httpx
7
+ from pytest_httpx import HTTPXMock
8
+
9
+ from mnemoverse import AsyncMnemoClient, MnemoAuthError, MnemoRateLimitError
10
+
11
+
12
+ @pytest.fixture
13
+ def client():
14
+ return AsyncMnemoClient(
15
+ api_key="mk_test_abc123",
16
+ base_url="https://test.api.mnemoverse.com",
17
+ timeout=5.0,
18
+ max_retries=0, # no retries in tests
19
+ )
20
+
21
+
22
+ async def test_write(client: AsyncMnemoClient, httpx_mock: HTTPXMock):
23
+ httpx_mock.add_response(
24
+ url="https://test.api.mnemoverse.com/api/v1/memory/write",
25
+ json={
26
+ "stored": True,
27
+ "atom_id": "550e8400-e29b-41d4-a716-446655440000",
28
+ "importance": 0.85,
29
+ "reason": "novel insight",
30
+ },
31
+ )
32
+
33
+ result = await client.write("test memory", concepts=["test"])
34
+
35
+ assert result.stored is True
36
+ assert str(result.atom_id) == "550e8400-e29b-41d4-a716-446655440000"
37
+ assert result.importance == 0.85
38
+
39
+
40
+ async def test_read(client: AsyncMnemoClient, httpx_mock: HTTPXMock):
41
+ httpx_mock.add_response(
42
+ url="https://test.api.mnemoverse.com/api/v1/memory/read",
43
+ json={
44
+ "items": [
45
+ {
46
+ "atom_id": "550e8400-e29b-41d4-a716-446655440000",
47
+ "content": "test memory",
48
+ "relevance": 0.92,
49
+ "similarity": 0.87,
50
+ "valence": 0.5,
51
+ "importance": 0.85,
52
+ "source": "semantic",
53
+ "concepts": ["test"],
54
+ "domain": "general",
55
+ "metadata": {},
56
+ }
57
+ ],
58
+ "episodic_hit": False,
59
+ "query_concepts": ["test"],
60
+ "expanded_concepts": ["test"],
61
+ "search_time_ms": 12.5,
62
+ },
63
+ )
64
+
65
+ result = await client.read("test query")
66
+
67
+ assert len(result.items) == 1
68
+ assert result.items[0].content == "test memory"
69
+ assert result.search_time_ms == 12.5
70
+
71
+
72
+ async def test_feedback(client: AsyncMnemoClient, httpx_mock: HTTPXMock):
73
+ httpx_mock.add_response(
74
+ url="https://test.api.mnemoverse.com/api/v1/memory/feedback",
75
+ json={"updated_count": 1, "avg_valence": 0.8, "coactivation_edges": 3},
76
+ )
77
+
78
+ result = await client.feedback(
79
+ atom_ids=["550e8400-e29b-41d4-a716-446655440000"],
80
+ outcome=1.0,
81
+ )
82
+
83
+ assert result.updated_count == 1
84
+ assert result.avg_valence == 0.8
85
+
86
+
87
+ async def test_stats(client: AsyncMnemoClient, httpx_mock: HTTPXMock):
88
+ httpx_mock.add_response(
89
+ url="https://test.api.mnemoverse.com/api/v1/memory/stats",
90
+ json={
91
+ "total_atoms": 100,
92
+ "episodes": 80,
93
+ "prototypes": 15,
94
+ "singletons": 5,
95
+ "hebbian_edges": 250,
96
+ "episodic_fingerprints": 10,
97
+ "domains": ["general", "engineering"],
98
+ "avg_valence": 0.3,
99
+ "avg_importance": 0.6,
100
+ },
101
+ )
102
+
103
+ result = await client.stats()
104
+
105
+ assert result.total_atoms == 100
106
+ assert "engineering" in result.domains
107
+
108
+
109
+ async def test_health(client: AsyncMnemoClient, httpx_mock: HTTPXMock):
110
+ httpx_mock.add_response(
111
+ url="https://test.api.mnemoverse.com/api/v1/health",
112
+ json={"status": "ok", "database": True, "version": "1.0.0"},
113
+ )
114
+
115
+ result = await client.health()
116
+
117
+ assert result.status == "ok"
118
+ assert result.database is True
119
+
120
+
121
+ async def test_auth_error(client: AsyncMnemoClient, httpx_mock: HTTPXMock):
122
+ httpx_mock.add_response(
123
+ url="https://test.api.mnemoverse.com/api/v1/memory/read",
124
+ status_code=401,
125
+ json={"detail": "Invalid API key"},
126
+ )
127
+
128
+ with pytest.raises(MnemoAuthError):
129
+ await client.read("test")
130
+
131
+
132
+ async def test_rate_limit_error(client: AsyncMnemoClient, httpx_mock: HTTPXMock):
133
+ httpx_mock.add_response(
134
+ url="https://test.api.mnemoverse.com/api/v1/memory/read",
135
+ status_code=429,
136
+ json={"detail": "Rate limit exceeded"},
137
+ headers={"Retry-After": "60"},
138
+ )
139
+
140
+ with pytest.raises(MnemoRateLimitError) as exc_info:
141
+ await client.read("test")
142
+
143
+ assert exc_info.value.retry_after == 60.0