memnos-sdk 1.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,13 @@
1
+ Metadata-Version: 2.4
2
+ Name: memnos-sdk
3
+ Version: 1.1.0
4
+ Summary: memnos SDK — memory layer for AI agents
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: httpx>=0.27
7
+ Requires-Dist: pydantic>=2.0
8
+ Provides-Extra: langchain
9
+ Requires-Dist: langchain-core>=0.2; extra == "langchain"
10
+ Provides-Extra: llamaindex
11
+ Requires-Dist: llama-index-core>=0.10; extra == "llamaindex"
12
+ Provides-Extra: all
13
+ Requires-Dist: memnos-sdk[langchain,llamaindex]; extra == "all"
@@ -0,0 +1,241 @@
1
+ # memnos-sdk
2
+
3
+ Python SDK for [memnos](https://github.com/thameema/memnos) — persistent memory for AI agents.
4
+
5
+ ```python
6
+ from memnos_sdk import MemnosClient
7
+
8
+ client = MemnosClient(base_url="http://localhost:8766", api_key="your-key")
9
+
10
+ client.write("Chose PostgreSQL for the event store — ACID guarantees required",
11
+ namespace="org:myproject",
12
+ memory_type="decision",
13
+ affects=["event-store", "payment-service"],
14
+ rationale="Transactional writes across tables need ACID; Redis/Kafka can't provide this.")
15
+
16
+ results = client.search("event store choice", namespace="org:myproject")
17
+ for r in results:
18
+ print(r.content)
19
+ ```
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ pip install memnos-sdk
25
+ ```
26
+
27
+ With LangChain or LlamaIndex integrations:
28
+
29
+ ```bash
30
+ pip install "memnos-sdk[langchain]"
31
+ pip install "memnos-sdk[llamaindex]"
32
+ pip install "memnos-sdk[all]"
33
+ ```
34
+
35
+ Requires Python 3.10+.
36
+
37
+ ## Quick start
38
+
39
+ ### Sync client
40
+
41
+ ```python
42
+ from memnos_sdk import MemnosClient
43
+
44
+ client = MemnosClient(
45
+ base_url="http://localhost:8766",
46
+ api_key="memnos-local-dev-key",
47
+ )
48
+
49
+ # Write a memory
50
+ mem = client.write(
51
+ content="Use ArcadeDB as the sole graph store — single process handles vector + graph + docs",
52
+ namespace="org:myproject",
53
+ memory_type="decision",
54
+ affects=["storage", "arcadedb"],
55
+ rationale="Eliminates three-service ops complexity (Neo4j + Qdrant + Graphiti).",
56
+ tags=["architecture", "adr"],
57
+ )
58
+ print(mem.id)
59
+
60
+ # Search across all accessible namespaces
61
+ results = client.search("storage architecture decision")
62
+ for r in results:
63
+ print(f"[{r.score:.2f}] {r.content[:80]}")
64
+
65
+ # Get a specific memory
66
+ mem = client.get(mem.id)
67
+
68
+ # Delete a memory
69
+ client.delete(mem.id)
70
+ ```
71
+
72
+ ### Async client
73
+
74
+ ```python
75
+ import asyncio
76
+ from memnos_sdk import AsyncMemnosClient
77
+
78
+ async def main():
79
+ async with AsyncMemnosClient(base_url="http://localhost:8766", api_key="key") as client:
80
+ await client.write("Chose gRPC for inter-service comms", namespace="org:svc",
81
+ memory_type="decision", affects=["api-gateway"])
82
+ results = await client.search("gRPC decision")
83
+ for r in results:
84
+ print(r.content)
85
+
86
+ asyncio.run(main())
87
+ ```
88
+
89
+ ## Memory types
90
+
91
+ | Type | When to use |
92
+ |------|-------------|
93
+ | `decision` | Architectural choices — what was decided and why |
94
+ | `constraint` | Rules that apply to a component — enforced at code review |
95
+ | `adr` | Architecture Decision Records |
96
+ | `fact` | Context, session notes, anything else |
97
+ | `session` | Automated session summaries from hooks |
98
+
99
+ ```python
100
+ from memnos_sdk import MemoryType
101
+
102
+ client.write("...", namespace="org:x", memory_type=MemoryType.DECISION)
103
+ ```
104
+
105
+ ## Corpus — architecture docs as a CI gate
106
+
107
+ Register a folder of markdown ADRs/decision docs and query them as constraints:
108
+
109
+ ```python
110
+ from memnos_sdk import MemnosClient
111
+
112
+ client = MemnosClient(base_url="http://localhost:8766", api_key="key")
113
+
114
+ # Register a corpus (one-time setup)
115
+ corpus = client.corpus.register(
116
+ name="backend-adrs",
117
+ source="./docs/architecture",
118
+ namespace="org:myproject",
119
+ )
120
+
121
+ # Check a code change against registered constraints
122
+ result = client.corpus.check(
123
+ corpus_id=corpus.id,
124
+ component="payment-service",
125
+ code_snippet="db.execute('INSERT INTO payments ...')",
126
+ )
127
+ for hit in result.violations:
128
+ print(f"[{hit.severity}] {hit.rule}")
129
+ ```
130
+
131
+ ## LangChain integration
132
+
133
+ ```python
134
+ from langchain_openai import ChatOpenAI
135
+ from langchain.chains import ConversationChain
136
+ from memnos_sdk.integrations.langchain import MemnosMemory
137
+
138
+ memory = MemnosMemory(
139
+ base_url="http://localhost:8766",
140
+ api_key="your-key",
141
+ namespace="org:myproject",
142
+ top_k=5,
143
+ )
144
+
145
+ chain = ConversationChain(llm=ChatOpenAI(), memory=memory)
146
+ response = chain.predict(input="What storage decisions have we made?")
147
+ ```
148
+
149
+ `MemnosMemory` loads the top-k most relevant memories as conversation context and writes new exchanges back to memnos automatically.
150
+
151
+ ## LlamaIndex integration
152
+
153
+ ```python
154
+ from llama_index.core import VectorStoreIndex
155
+ from memnos_sdk.integrations.llamaindex import MemnosReader
156
+
157
+ reader = MemnosReader(base_url="http://localhost:8766", api_key="your-key")
158
+ documents = reader.load_data(namespace="org:myproject", query="architecture decisions", top_k=20)
159
+
160
+ index = VectorStoreIndex.from_documents(documents)
161
+ query_engine = index.as_query_engine()
162
+ print(query_engine.query("Which storage system did we pick and why?"))
163
+ ```
164
+
165
+ ## Governance queries
166
+
167
+ ```python
168
+ # Get all decisions governing a component
169
+ decisions = client.get_governing_decisions(namespace="org:myproject", component="auth-service")
170
+ for d in decisions:
171
+ print(f"[{d.memory_type}] {d.content[:80]}")
172
+ print(f" WHY: {d.rationale}")
173
+
174
+ # Get all constraints in a namespace
175
+ constraints = client.get_constraints(namespace="org:myproject")
176
+ ```
177
+
178
+ ## Namespace export / import
179
+
180
+ ```python
181
+ # Export
182
+ data = client.export_namespace("org:myproject")
183
+ import json
184
+ json.dump(data, open("backup.json", "w"), indent=2)
185
+
186
+ # Import into a new namespace
187
+ with open("backup.json") as f:
188
+ client.import_namespace("org:newproject", json.load(f))
189
+ ```
190
+
191
+ ## Error handling
192
+
193
+ ```python
194
+ from memnos_sdk.exceptions import NotFoundError, AuthenticationError, MemnosError
195
+
196
+ try:
197
+ mem = client.get("nonexistent-id")
198
+ except NotFoundError:
199
+ print("Memory not found")
200
+ except AuthenticationError:
201
+ print("Check your API key")
202
+ except MemnosError as e:
203
+ print(f"Memnos error: {e}")
204
+ ```
205
+
206
+ | Exception | When raised |
207
+ |-----------|-------------|
208
+ | `MemnosError` | Base class for all SDK errors |
209
+ | `AuthenticationError` | Invalid or missing API key |
210
+ | `NotFoundError` | Memory or corpus not found |
211
+ | `ValidationError` | Malformed request (bad namespace, etc.) |
212
+ | `ServerError` | 5xx from the memnos server |
213
+ | `ConnectionError` | Server unreachable |
214
+
215
+ ## Configuration
216
+
217
+ ```python
218
+ client = MemnosClient(
219
+ base_url="http://localhost:8766", # or MEMNOS_API env var
220
+ api_key="your-key", # or MEMNOS_KEY env var
221
+ timeout=15.0, # per-request timeout in seconds
222
+ )
223
+ ```
224
+
225
+ Environment variables:
226
+
227
+ | Variable | Default | Description |
228
+ |----------|---------|-------------|
229
+ | `MEMNOS_API` | `http://localhost:8766` | Server URL |
230
+ | `MEMNOS_KEY` | — | API key |
231
+
232
+ ## Running memnos locally
233
+
234
+ ```bash
235
+ git clone https://github.com/thameema/memnos
236
+ cd memnos
237
+ docker compose up -d
238
+ # Server is at http://localhost:8766, key: memnos-local-dev-key
239
+ ```
240
+
241
+ See the [main README](../../README.md) for full setup.
@@ -0,0 +1,52 @@
1
+ """memnos-sdk — memory layer for AI agents."""
2
+ from memnos_sdk.client import AsyncMemnosClient, MemnosClient
3
+ from memnos_sdk.models import (
4
+ Memory,
5
+ MemoryType,
6
+ SearchResult,
7
+ HealthStatus,
8
+ CorpusInfo,
9
+ CorpusStatus,
10
+ ConstraintHit,
11
+ CheckResult,
12
+ )
13
+ from memnos_sdk.corpus import AsyncCorpusClient, SyncCorpusClient
14
+ from memnos_sdk.exceptions import (
15
+ MemnosError,
16
+ AuthenticationError,
17
+ NotFoundError,
18
+ ValidationError,
19
+ ServerError,
20
+ ConnectionError,
21
+ )
22
+
23
+ __version__ = "1.1.0"
24
+ SCHEMA_VERSION = "1.0"
25
+
26
+ __all__ = [
27
+ # Clients
28
+ "MemnosClient",
29
+ "AsyncMemnosClient",
30
+ # Memory models
31
+ "Memory",
32
+ "MemoryType",
33
+ "SearchResult",
34
+ "HealthStatus",
35
+ # Corpus models
36
+ "CorpusInfo",
37
+ "CorpusStatus",
38
+ "ConstraintHit",
39
+ "CheckResult",
40
+ # Corpus sub-clients (advanced use)
41
+ "AsyncCorpusClient",
42
+ "SyncCorpusClient",
43
+ # Exceptions
44
+ "MemnosError",
45
+ "AuthenticationError",
46
+ "NotFoundError",
47
+ "ValidationError",
48
+ "ServerError",
49
+ "ConnectionError",
50
+ "__version__",
51
+ "SCHEMA_VERSION",
52
+ ]
@@ -0,0 +1,96 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import httpx
6
+
7
+ from memnos_sdk.exceptions import (
8
+ AuthenticationError,
9
+ ConnectionError,
10
+ NotFoundError,
11
+ ServerError,
12
+ ValidationError,
13
+ )
14
+
15
+
16
+ def _raise_for_status(response: httpx.Response) -> None:
17
+ code = response.status_code
18
+ if code in (401, 403):
19
+ raise AuthenticationError(f"HTTP {code}: {response.text}")
20
+ if code == 404:
21
+ raise NotFoundError(f"HTTP 404: {response.text}")
22
+ if code == 422:
23
+ raise ValidationError(f"HTTP 422: {response.text}")
24
+ if code >= 500:
25
+ raise ServerError(f"HTTP {code}: {response.text}")
26
+ response.raise_for_status()
27
+
28
+
29
+ class _SyncTransport:
30
+ def __init__(self, base_url: str, api_key: str, timeout: float = 30.0) -> None:
31
+ self._client = httpx.Client(
32
+ base_url=base_url,
33
+ headers={"Authorization": f"Bearer {api_key}"},
34
+ timeout=timeout,
35
+ )
36
+
37
+ def get(self, path: str, params: dict[str, Any] | None = None) -> dict:
38
+ try:
39
+ response = self._client.get(path, params=params)
40
+ except httpx.TransportError as exc:
41
+ raise ConnectionError(str(exc)) from exc
42
+ _raise_for_status(response)
43
+ return response.json()
44
+
45
+ def post(self, path: str, json: dict | None = None) -> dict:
46
+ try:
47
+ response = self._client.post(path, json=json)
48
+ except httpx.TransportError as exc:
49
+ raise ConnectionError(str(exc)) from exc
50
+ _raise_for_status(response)
51
+ return response.json()
52
+
53
+ def delete(self, path: str) -> None:
54
+ try:
55
+ response = self._client.delete(path)
56
+ except httpx.TransportError as exc:
57
+ raise ConnectionError(str(exc)) from exc
58
+ _raise_for_status(response)
59
+
60
+ def close(self) -> None:
61
+ self._client.close()
62
+
63
+
64
+ class _AsyncTransport:
65
+ def __init__(self, base_url: str, api_key: str, timeout: float = 30.0) -> None:
66
+ self._client = httpx.AsyncClient(
67
+ base_url=base_url,
68
+ headers={"Authorization": f"Bearer {api_key}"},
69
+ timeout=timeout,
70
+ )
71
+
72
+ async def get(self, path: str, params: dict[str, Any] | None = None) -> dict:
73
+ try:
74
+ response = await self._client.get(path, params=params)
75
+ except httpx.TransportError as exc:
76
+ raise ConnectionError(str(exc)) from exc
77
+ _raise_for_status(response)
78
+ return response.json()
79
+
80
+ async def post(self, path: str, json: dict | None = None) -> dict:
81
+ try:
82
+ response = await self._client.post(path, json=json)
83
+ except httpx.TransportError as exc:
84
+ raise ConnectionError(str(exc)) from exc
85
+ _raise_for_status(response)
86
+ return response.json()
87
+
88
+ async def delete(self, path: str) -> None:
89
+ try:
90
+ response = await self._client.delete(path)
91
+ except httpx.TransportError as exc:
92
+ raise ConnectionError(str(exc)) from exc
93
+ _raise_for_status(response)
94
+
95
+ async def aclose(self) -> None:
96
+ await self._client.aclose()