memnos-sdk 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,109 @@
1
+ Metadata-Version: 2.4
2
+ Name: memnos-sdk
3
+ Version: 0.1.0
4
+ Summary: Lightweight client + LangChain/LangGraph adapters for memnos — backend memory for AI agents.
5
+ Author: memnos
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://memnos.net
8
+ Project-URL: Source, https://github.com/thameema/memnos
9
+ Keywords: memnos,memory,AI agents,LangChain,LangGraph,RAG,MCP
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: httpx>=0.27
13
+ Provides-Extra: langchain
14
+ Requires-Dist: langchain-core>=0.3; extra == "langchain"
15
+ Provides-Extra: langgraph
16
+ Requires-Dist: langgraph>=0.2; extra == "langgraph"
17
+ Provides-Extra: llamaindex
18
+ Requires-Dist: llama-index-core>=0.10; extra == "llamaindex"
19
+ Provides-Extra: all
20
+ Requires-Dist: langchain-core>=0.3; extra == "all"
21
+ Requires-Dist: langgraph>=0.2; extra == "all"
22
+ Requires-Dist: llama-index-core>=0.10; extra == "all"
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=8; extra == "dev"
25
+ Requires-Dist: langchain-core>=0.3; extra == "dev"
26
+ Requires-Dist: langgraph>=0.2; extra == "dev"
27
+ Requires-Dist: llama-index-core>=0.10; extra == "dev"
28
+
29
+ # memnos-sdk
30
+
31
+ Lightweight Python client for [memnos](https://memnos.net) — governed, vendor-neutral
32
+ **backend memory for AI agents**. Use it directly, or as a **LangChain** retriever, a
33
+ **LangGraph** long-term-memory store, or a **LlamaIndex** retriever.
34
+
35
+ `httpx`-only (no server deps). Talks to a running memnos server over REST.
36
+
37
+ ```bash
38
+ pip install memnos-sdk # core client
39
+ pip install 'memnos-sdk[langchain]' # + LangChain retriever
40
+ pip install 'memnos-sdk[langgraph]' # + LangGraph BaseStore
41
+ pip install 'memnos-sdk[llamaindex]' # + LlamaIndex retriever
42
+ pip install 'memnos-sdk[all]' # everything
43
+ ```
44
+
45
+ ## Core client (sync + async)
46
+
47
+ ```python
48
+ from memnos_sdk import MemnosClient
49
+
50
+ with MemnosClient(base_url="http://127.0.0.1:8900", token="mnk_...", namespace="org:acme") as mem:
51
+ mem.remember("We chose PostgreSQL + pgvector for the memory store")
52
+ ctx = mem.context("what database did we choose?") # ready-to-inject; no LLM at query time
53
+ rows = mem.recall("database decision")["memories"] # ranked memories w/ scores + dates
54
+ ```
55
+
56
+ ```python
57
+ from memnos_sdk import AsyncMemnosClient
58
+
59
+ async with AsyncMemnosClient(token="mnk_...", namespace="org:acme") as mem:
60
+ await mem.remember("...")
61
+ print(await mem.context("..."))
62
+ ```
63
+
64
+ A **token** + **namespace** come from your memnos admin (`memnos token <principal>`,
65
+ `memnos grant <principal> <namespace>`). Every call is namespace-scoped and audited
66
+ server-side.
67
+
68
+ ## LangChain
69
+
70
+ ```python
71
+ from memnos_sdk import MemnosClient
72
+ from memnos_sdk.integrations.langchain import MemnosRetriever
73
+
74
+ retriever = MemnosRetriever(client=MemnosClient(token="mnk_...", namespace="org:acme"))
75
+ docs = retriever.invoke("auth token expiry policy") # drop into any RAG chain
76
+ retriever.save("JWT tokens expire after 15 minutes in prod")
77
+ ```
78
+
79
+ ## LangGraph (long-term memory)
80
+
81
+ ```python
82
+ from memnos_sdk import MemnosClient
83
+ from memnos_sdk.integrations.langgraph import MemnosStore
84
+
85
+ store = MemnosStore(MemnosClient(token="mnk_..."))
86
+ graph = builder.compile(store=store)
87
+ # in a node: store.search(("org","acme"), query="...") · store.put(("org","acme"), key, {"text": "..."})
88
+ ```
89
+
90
+ memnos is *semantic* memory: `put`→remember, `search`→hybrid+reranked recall. Exact-key
91
+ `get` is best-effort (use `search`).
92
+
93
+ ## LlamaIndex
94
+
95
+ ```python
96
+ from memnos_sdk import MemnosClient
97
+ from memnos_sdk.integrations.llamaindex import MemnosRetriever
98
+
99
+ retriever = MemnosRetriever(client=MemnosClient(token="mnk_...", namespace="org:acme"))
100
+ nodes = retriever.retrieve("auth token expiry policy") # NodeWithScore[]; drop into a query engine
101
+ retriever.save("JWT tokens expire after 15 minutes in prod")
102
+ ```
103
+
104
+ ## API surface
105
+ `remember(text)` · `recall(query) -> {memories, context}` · `context(query) -> str` ·
106
+ `consolidate()` · `feedback(query, helpful)` · `healthy()`. Async mirror on
107
+ `AsyncMemnosClient`.
108
+
109
+ Apache-2.0.
@@ -0,0 +1,81 @@
1
+ # memnos-sdk
2
+
3
+ Lightweight Python client for [memnos](https://memnos.net) — governed, vendor-neutral
4
+ **backend memory for AI agents**. Use it directly, or as a **LangChain** retriever, a
5
+ **LangGraph** long-term-memory store, or a **LlamaIndex** retriever.
6
+
7
+ `httpx`-only (no server deps). Talks to a running memnos server over REST.
8
+
9
+ ```bash
10
+ pip install memnos-sdk # core client
11
+ pip install 'memnos-sdk[langchain]' # + LangChain retriever
12
+ pip install 'memnos-sdk[langgraph]' # + LangGraph BaseStore
13
+ pip install 'memnos-sdk[llamaindex]' # + LlamaIndex retriever
14
+ pip install 'memnos-sdk[all]' # everything
15
+ ```
16
+
17
+ ## Core client (sync + async)
18
+
19
+ ```python
20
+ from memnos_sdk import MemnosClient
21
+
22
+ with MemnosClient(base_url="http://127.0.0.1:8900", token="mnk_...", namespace="org:acme") as mem:
23
+ mem.remember("We chose PostgreSQL + pgvector for the memory store")
24
+ ctx = mem.context("what database did we choose?") # ready-to-inject; no LLM at query time
25
+ rows = mem.recall("database decision")["memories"] # ranked memories w/ scores + dates
26
+ ```
27
+
28
+ ```python
29
+ from memnos_sdk import AsyncMemnosClient
30
+
31
+ async with AsyncMemnosClient(token="mnk_...", namespace="org:acme") as mem:
32
+ await mem.remember("...")
33
+ print(await mem.context("..."))
34
+ ```
35
+
36
+ A **token** + **namespace** come from your memnos admin (`memnos token <principal>`,
37
+ `memnos grant <principal> <namespace>`). Every call is namespace-scoped and audited
38
+ server-side.
39
+
40
+ ## LangChain
41
+
42
+ ```python
43
+ from memnos_sdk import MemnosClient
44
+ from memnos_sdk.integrations.langchain import MemnosRetriever
45
+
46
+ retriever = MemnosRetriever(client=MemnosClient(token="mnk_...", namespace="org:acme"))
47
+ docs = retriever.invoke("auth token expiry policy") # drop into any RAG chain
48
+ retriever.save("JWT tokens expire after 15 minutes in prod")
49
+ ```
50
+
51
+ ## LangGraph (long-term memory)
52
+
53
+ ```python
54
+ from memnos_sdk import MemnosClient
55
+ from memnos_sdk.integrations.langgraph import MemnosStore
56
+
57
+ store = MemnosStore(MemnosClient(token="mnk_..."))
58
+ graph = builder.compile(store=store)
59
+ # in a node: store.search(("org","acme"), query="...") · store.put(("org","acme"), key, {"text": "..."})
60
+ ```
61
+
62
+ memnos is *semantic* memory: `put`→remember, `search`→hybrid+reranked recall. Exact-key
63
+ `get` is best-effort (use `search`).
64
+
65
+ ## LlamaIndex
66
+
67
+ ```python
68
+ from memnos_sdk import MemnosClient
69
+ from memnos_sdk.integrations.llamaindex import MemnosRetriever
70
+
71
+ retriever = MemnosRetriever(client=MemnosClient(token="mnk_...", namespace="org:acme"))
72
+ nodes = retriever.retrieve("auth token expiry policy") # NodeWithScore[]; drop into a query engine
73
+ retriever.save("JWT tokens expire after 15 minutes in prod")
74
+ ```
75
+
76
+ ## API surface
77
+ `remember(text)` · `recall(query) -> {memories, context}` · `context(query) -> str` ·
78
+ `consolidate()` · `feedback(query, helpful)` · `healthy()`. Async mirror on
79
+ `AsyncMemnosClient`.
80
+
81
+ Apache-2.0.
@@ -0,0 +1,12 @@
1
+ """memnos SDK — backend memory for agentic apps (LangChain / LangGraph / any).
2
+
3
+ from memnos_sdk import MemnosClient, AsyncMemnosClient
4
+
5
+ Framework adapters are optional extras (import only if the framework is installed):
6
+ from memnos_sdk.integrations.langchain import MemnosRetriever # pip install 'memnos-sdk[langchain]'
7
+ from memnos_sdk.integrations.langgraph import MemnosStore # pip install 'memnos-sdk[langgraph]'
8
+ """
9
+ from .client import AsyncMemnosClient, MemnosClient, MemnosError
10
+
11
+ __version__ = "0.1.0"
12
+ __all__ = ["MemnosClient", "AsyncMemnosClient", "MemnosError", "__version__"]
@@ -0,0 +1,137 @@
1
+ """memnos SDK — typed REST client (sync + async) for app developers.
2
+
3
+ Lightweight (httpx only) so agentic apps can `pip install memnos-sdk` without the server's
4
+ heavy deps. Talks to a running memnos server. Auth = a bearer token; every call is scoped
5
+ to a namespace (set once on the client or per call).
6
+
7
+ from memnos_sdk import MemnosClient
8
+ mem = MemnosClient(base_url="http://127.0.0.1:8900", token="mnk_...", namespace="org:acme")
9
+ mem.remember("We chose Postgres for the memory store")
10
+ print(mem.context("what db did we choose?")) # ready-to-inject context, no LLM at query time
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import httpx
15
+
16
+ DEFAULT_URL = "http://127.0.0.1:8900"
17
+
18
+
19
+ class MemnosError(RuntimeError):
20
+ def __init__(self, status, detail):
21
+ super().__init__(f"memnos {status}: {detail}")
22
+ self.status, self.detail = status, detail
23
+
24
+
25
+ def _ns(namespace, default):
26
+ ns = namespace or default
27
+ if not ns:
28
+ raise ValueError("namespace required (set MemnosClient(namespace=...) or pass namespace=)")
29
+ return ns
30
+
31
+
32
+ def _raise(r):
33
+ if r.status_code >= 400:
34
+ try:
35
+ detail = r.json().get("error", r.text)
36
+ except Exception:
37
+ detail = r.text
38
+ raise MemnosError(r.status_code, detail)
39
+ return r.json()
40
+
41
+
42
+ class MemnosClient:
43
+ """Synchronous client. Use as a context manager or call .close()."""
44
+
45
+ def __init__(self, base_url=DEFAULT_URL, token=None, namespace=None, timeout=30.0, transport=None):
46
+ self.namespace = namespace
47
+ self._h = {"Authorization": f"Bearer {token}"} if token else {}
48
+ self._c = httpx.Client(base_url=base_url.rstrip("/"), timeout=timeout, headers=self._h,
49
+ transport=transport)
50
+
51
+ def remember(self, text, *, namespace=None, speaker=None, session_id=None) -> dict:
52
+ return _raise(self._c.post("/remember", json={
53
+ "namespace": _ns(namespace, self.namespace), "text": text,
54
+ "speaker": speaker, "session_id": session_id}))
55
+
56
+ def recall(self, query, *, namespace=None, raw_quota=None, fact_quota=None, max_chars=None) -> dict:
57
+ body = {"namespace": _ns(namespace, self.namespace), "query": query}
58
+ for k, v in (("raw_quota", raw_quota), ("fact_quota", fact_quota), ("max_chars", max_chars)):
59
+ if v is not None:
60
+ body[k] = v
61
+ return _raise(self._c.post("/recall", json=body))
62
+
63
+ def ingest_file(self, filename, text, *, namespace=None, extract=False) -> dict:
64
+ """Chunk a document's text into memory under `filename`. Pass extracted text
65
+ (md/txt/code, or text pulled from a PDF/DOCX)."""
66
+ return _raise(self._c.post("/ingest/file", json={
67
+ "namespace": _ns(namespace, self.namespace), "filename": filename,
68
+ "text": text, "extract": extract}))
69
+
70
+ def context(self, query, *, namespace=None, **kw) -> str:
71
+ """Just the ready-to-inject context string from recall()."""
72
+ return self.recall(query, namespace=namespace, **kw).get("context", "")
73
+
74
+ def consolidate(self, *, namespace=None) -> dict:
75
+ return _raise(self._c.post("/consolidate", json={"namespace": _ns(namespace, self.namespace)}))
76
+
77
+ def feedback(self, query, helpful, *, namespace=None, note=None) -> dict:
78
+ return _raise(self._c.post("/feedback", json={
79
+ "namespace": _ns(namespace, self.namespace), "query": query,
80
+ "helpful": bool(helpful), "note": note}))
81
+
82
+ def healthy(self) -> bool:
83
+ try:
84
+ return self._c.get("/healthz").status_code == 200
85
+ except httpx.HTTPError:
86
+ return False
87
+
88
+ def close(self):
89
+ self._c.close()
90
+
91
+ def __enter__(self):
92
+ return self
93
+
94
+ def __exit__(self, *a):
95
+ self.close()
96
+
97
+
98
+ class AsyncMemnosClient:
99
+ """Async client (httpx.AsyncClient). Use `async with` or call .aclose()."""
100
+
101
+ def __init__(self, base_url=DEFAULT_URL, token=None, namespace=None, timeout=30.0, transport=None):
102
+ self.namespace = namespace
103
+ self._h = {"Authorization": f"Bearer {token}"} if token else {}
104
+ self._c = httpx.AsyncClient(base_url=base_url.rstrip("/"), timeout=timeout, headers=self._h,
105
+ transport=transport)
106
+
107
+ async def remember(self, text, *, namespace=None, speaker=None, session_id=None) -> dict:
108
+ return _raise(await self._c.post("/remember", json={
109
+ "namespace": _ns(namespace, self.namespace), "text": text,
110
+ "speaker": speaker, "session_id": session_id}))
111
+
112
+ async def recall(self, query, *, namespace=None, raw_quota=None, fact_quota=None, max_chars=None) -> dict:
113
+ body = {"namespace": _ns(namespace, self.namespace), "query": query}
114
+ for k, v in (("raw_quota", raw_quota), ("fact_quota", fact_quota), ("max_chars", max_chars)):
115
+ if v is not None:
116
+ body[k] = v
117
+ return _raise(await self._c.post("/recall", json=body))
118
+
119
+ async def context(self, query, *, namespace=None, **kw) -> str:
120
+ return (await self.recall(query, namespace=namespace, **kw)).get("context", "")
121
+
122
+ async def consolidate(self, *, namespace=None) -> dict:
123
+ return _raise(await self._c.post("/consolidate", json={"namespace": _ns(namespace, self.namespace)}))
124
+
125
+ async def feedback(self, query, helpful, *, namespace=None, note=None) -> dict:
126
+ return _raise(await self._c.post("/feedback", json={
127
+ "namespace": _ns(namespace, self.namespace), "query": query,
128
+ "helpful": bool(helpful), "note": note}))
129
+
130
+ async def aclose(self):
131
+ await self._c.aclose()
132
+
133
+ async def __aenter__(self):
134
+ return self
135
+
136
+ async def __aexit__(self, *a):
137
+ await self.aclose()
@@ -0,0 +1 @@
1
+ # memnos SDK framework adapters (optional). Import the submodule you need.
@@ -0,0 +1,51 @@
1
+ """LangChain adapter — memnos as a retriever (and a tiny save helper).
2
+
3
+ pip install 'memnos-sdk[langchain]'
4
+ from memnos_sdk import MemnosClient
5
+ from memnos_sdk.integrations.langchain import MemnosRetriever
6
+
7
+ mem = MemnosClient(token="mnk_...", namespace="org:acme")
8
+ retriever = MemnosRetriever(client=mem) # drop into any chain / RAG pipeline
9
+ docs = retriever.invoke("what database did we choose?")
10
+
11
+ Built on `langchain_core` (stable) — no heavy langchain deps. Retrieval uses memnos's
12
+ hybrid + reranked recall (no LLM at query time); each memory becomes a Document.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ from typing import List
17
+
18
+ try:
19
+ from langchain_core.callbacks import CallbackManagerForRetrieverRun
20
+ from langchain_core.documents import Document
21
+ from langchain_core.retrievers import BaseRetriever
22
+ except ImportError as e: # pragma: no cover
23
+ raise ImportError("LangChain integration needs langchain_core: pip install 'memnos-sdk[langchain]'") from e
24
+
25
+ from ..client import MemnosClient
26
+
27
+
28
+ class MemnosRetriever(BaseRetriever):
29
+ """A LangChain retriever backed by memnos recall."""
30
+
31
+ client: MemnosClient
32
+ namespace: str | None = None
33
+ k: int = 8
34
+
35
+ class Config:
36
+ arbitrary_types_allowed = True
37
+
38
+ def _get_relevant_documents(self, query: str, *, run_manager: "CallbackManagerForRetrieverRun" = None
39
+ ) -> List[Document]:
40
+ rows = self.client.recall(query, namespace=self.namespace).get("memories", [])
41
+ docs = []
42
+ for r in rows[: self.k]:
43
+ md = {"kind": r.get("kind"), "score": r.get("score")}
44
+ if r.get("date"):
45
+ md["date"] = r["date"]
46
+ docs.append(Document(page_content=r.get("content", ""), metadata=md))
47
+ return docs
48
+
49
+ def save(self, text: str, *, namespace: str | None = None) -> dict:
50
+ """Convenience: write a memory (LangChain has no standard 'write' on retrievers)."""
51
+ return self.client.remember(text, namespace=namespace or self.namespace)
@@ -0,0 +1,103 @@
1
+ """LangGraph adapter — memnos as a long-term-memory `BaseStore`.
2
+
3
+ pip install 'memnos-sdk[langgraph]'
4
+ from memnos_sdk import MemnosClient
5
+ from memnos_sdk.integrations.langgraph import MemnosStore
6
+
7
+ store = MemnosStore(MemnosClient(token="mnk_..."))
8
+ graph = builder.compile(store=store) # nodes get long-term memory via store.search/put
9
+
10
+ Mapping (memnos is SEMANTIC memory, not a KV store):
11
+ • Put(namespace, key, value) -> remember(value) (key kept inline so it's searchable)
12
+ • Search(namespace, query) -> recall(query) (hybrid + reranked, no LLM at query time)
13
+ • Get(namespace, key) -> best-effort via search; None if not found
14
+ • ListNamespaces -> [] (memnos doesn't enumerate per-key namespaces here)
15
+
16
+ LangGraph namespace tuples are joined with ':' into a memnos namespace.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ from datetime import datetime, timezone
22
+
23
+ try:
24
+ from langgraph.store.base import (BaseStore, GetOp, Item, ListNamespacesOp, PutOp,
25
+ SearchItem, SearchOp)
26
+ except ImportError as e: # pragma: no cover
27
+ raise ImportError("LangGraph integration needs langgraph: pip install 'memnos-sdk[langgraph]'") from e
28
+
29
+ from ..client import AsyncMemnosClient, MemnosClient
30
+
31
+
32
+ def _ns(tup):
33
+ return ":".join(str(p) for p in tup) if tup else "default"
34
+
35
+
36
+ def _text(value):
37
+ return value if isinstance(value, str) else json.dumps(value, default=str)
38
+
39
+
40
+ class MemnosStore(BaseStore):
41
+ """A LangGraph BaseStore backed by memnos semantic memory."""
42
+
43
+ def __init__(self, client: MemnosClient, async_client: AsyncMemnosClient | None = None):
44
+ self.client = client
45
+ self.aclient = async_client
46
+
47
+ # --- sync ---
48
+ def batch(self, ops):
49
+ out = []
50
+ for op in ops:
51
+ if isinstance(op, PutOp):
52
+ if op.value is None: # delete — memnos has no key delete; no-op
53
+ out.append(None)
54
+ else:
55
+ self.client.remember(f"[{op.key}] {_text(op.value)}", namespace=_ns(op.namespace))
56
+ out.append(None)
57
+ elif isinstance(op, SearchOp):
58
+ rows = self.client.recall(op.query or "", namespace=_ns(op.namespace_prefix)).get("memories", [])
59
+ out.append(self._to_items(op.namespace_prefix, rows, getattr(op, "limit", 10)))
60
+ elif isinstance(op, GetOp):
61
+ rows = self.client.recall(str(op.key), namespace=_ns(op.namespace)).get("memories", [])
62
+ out.append(self._to_items(op.namespace, rows, 1)[0] if rows else None)
63
+ elif isinstance(op, ListNamespacesOp):
64
+ out.append([])
65
+ else:
66
+ out.append(None)
67
+ return out
68
+
69
+ # --- async ---
70
+ async def abatch(self, ops):
71
+ if self.aclient is None:
72
+ return self.batch(ops)
73
+ out = []
74
+ for op in ops:
75
+ if isinstance(op, PutOp):
76
+ if op.value is not None:
77
+ await self.aclient.remember(f"[{op.key}] {_text(op.value)}", namespace=_ns(op.namespace))
78
+ out.append(None)
79
+ elif isinstance(op, SearchOp):
80
+ r = await self.aclient.recall(op.query or "", namespace=_ns(op.namespace_prefix))
81
+ out.append(self._to_items(op.namespace_prefix, r.get("memories", []), getattr(op, "limit", 10)))
82
+ elif isinstance(op, GetOp):
83
+ r = await self.aclient.recall(str(op.key), namespace=_ns(op.namespace))
84
+ rows = r.get("memories", [])
85
+ out.append(self._to_items(op.namespace, rows, 1)[0] if rows else None)
86
+ elif isinstance(op, ListNamespacesOp):
87
+ out.append([])
88
+ else:
89
+ out.append(None)
90
+ return out
91
+
92
+ def _to_items(self, namespace, rows, limit):
93
+ now = datetime.now(timezone.utc)
94
+ items = []
95
+ for i, r in enumerate(rows[: (limit or 10)]):
96
+ try:
97
+ items.append(SearchItem(namespace=tuple(namespace), key=str(i),
98
+ value={"content": r.get("content", ""), "kind": r.get("kind")},
99
+ created_at=now, updated_at=now, score=r.get("score")))
100
+ except TypeError: # SearchItem signature drift across langgraph versions
101
+ items.append(Item(namespace=tuple(namespace), key=str(i),
102
+ value={"content": r.get("content", "")}, created_at=now, updated_at=now))
103
+ return items
@@ -0,0 +1,52 @@
1
+ """LlamaIndex adapter — memnos as a retriever (and a tiny save helper).
2
+
3
+ pip install 'memnos-sdk[llamaindex]'
4
+ from memnos_sdk import MemnosClient
5
+ from memnos_sdk.integrations.llamaindex import MemnosRetriever
6
+
7
+ mem = MemnosClient(token="mnk_...", namespace="org:acme")
8
+ retriever = MemnosRetriever(client=mem) # drop into any LlamaIndex query engine
9
+ nodes = retriever.retrieve("what database did we choose?")
10
+
11
+ Built on `llama_index.core` (stable). Retrieval uses memnos's hybrid + reranked recall
12
+ (no LLM at query time); each memory becomes a TextNode with its score + metadata.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ from typing import List, Optional
17
+
18
+ try:
19
+ from llama_index.core.retrievers import BaseRetriever
20
+ from llama_index.core.schema import NodeWithScore, QueryBundle, TextNode
21
+ except ImportError as e: # pragma: no cover
22
+ raise ImportError("LlamaIndex integration needs llama-index-core: "
23
+ "pip install 'memnos-sdk[llamaindex]'") from e
24
+
25
+ from ..client import MemnosClient
26
+
27
+
28
+ class MemnosRetriever(BaseRetriever):
29
+ """A LlamaIndex retriever backed by memnos recall."""
30
+
31
+ def __init__(self, client: MemnosClient, namespace: Optional[str] = None, k: int = 8,
32
+ callback_manager=None):
33
+ self.client = client
34
+ self.namespace = namespace
35
+ self.k = k
36
+ super().__init__(callback_manager=callback_manager)
37
+
38
+ def _retrieve(self, query_bundle: "QueryBundle") -> List["NodeWithScore"]:
39
+ q = query_bundle.query_str if hasattr(query_bundle, "query_str") else str(query_bundle)
40
+ rows = self.client.recall(q, namespace=self.namespace).get("memories", [])
41
+ nodes: List[NodeWithScore] = []
42
+ for r in rows[: self.k]:
43
+ md = {"kind": r.get("kind")}
44
+ if r.get("date"):
45
+ md["date"] = r["date"]
46
+ node = TextNode(text=r.get("content", ""), metadata=md)
47
+ nodes.append(NodeWithScore(node=node, score=r.get("score")))
48
+ return nodes
49
+
50
+ def save(self, text: str, *, namespace: Optional[str] = None) -> dict:
51
+ """Convenience: write a memory (LlamaIndex retrievers have no standard 'write')."""
52
+ return self.client.remember(text, namespace=namespace or self.namespace)
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.4
2
+ Name: memnos-sdk
3
+ Version: 0.1.0
4
+ Summary: Lightweight client + LangChain/LangGraph adapters for memnos — backend memory for AI agents.
5
+ Author: memnos
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://memnos.net
8
+ Project-URL: Source, https://github.com/thameema/memnos
9
+ Keywords: memnos,memory,AI agents,LangChain,LangGraph,RAG,MCP
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: httpx>=0.27
13
+ Provides-Extra: langchain
14
+ Requires-Dist: langchain-core>=0.3; extra == "langchain"
15
+ Provides-Extra: langgraph
16
+ Requires-Dist: langgraph>=0.2; extra == "langgraph"
17
+ Provides-Extra: llamaindex
18
+ Requires-Dist: llama-index-core>=0.10; extra == "llamaindex"
19
+ Provides-Extra: all
20
+ Requires-Dist: langchain-core>=0.3; extra == "all"
21
+ Requires-Dist: langgraph>=0.2; extra == "all"
22
+ Requires-Dist: llama-index-core>=0.10; extra == "all"
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=8; extra == "dev"
25
+ Requires-Dist: langchain-core>=0.3; extra == "dev"
26
+ Requires-Dist: langgraph>=0.2; extra == "dev"
27
+ Requires-Dist: llama-index-core>=0.10; extra == "dev"
28
+
29
+ # memnos-sdk
30
+
31
+ Lightweight Python client for [memnos](https://memnos.net) — governed, vendor-neutral
32
+ **backend memory for AI agents**. Use it directly, or as a **LangChain** retriever, a
33
+ **LangGraph** long-term-memory store, or a **LlamaIndex** retriever.
34
+
35
+ `httpx`-only (no server deps). Talks to a running memnos server over REST.
36
+
37
+ ```bash
38
+ pip install memnos-sdk # core client
39
+ pip install 'memnos-sdk[langchain]' # + LangChain retriever
40
+ pip install 'memnos-sdk[langgraph]' # + LangGraph BaseStore
41
+ pip install 'memnos-sdk[llamaindex]' # + LlamaIndex retriever
42
+ pip install 'memnos-sdk[all]' # everything
43
+ ```
44
+
45
+ ## Core client (sync + async)
46
+
47
+ ```python
48
+ from memnos_sdk import MemnosClient
49
+
50
+ with MemnosClient(base_url="http://127.0.0.1:8900", token="mnk_...", namespace="org:acme") as mem:
51
+ mem.remember("We chose PostgreSQL + pgvector for the memory store")
52
+ ctx = mem.context("what database did we choose?") # ready-to-inject; no LLM at query time
53
+ rows = mem.recall("database decision")["memories"] # ranked memories w/ scores + dates
54
+ ```
55
+
56
+ ```python
57
+ from memnos_sdk import AsyncMemnosClient
58
+
59
+ async with AsyncMemnosClient(token="mnk_...", namespace="org:acme") as mem:
60
+ await mem.remember("...")
61
+ print(await mem.context("..."))
62
+ ```
63
+
64
+ A **token** + **namespace** come from your memnos admin (`memnos token <principal>`,
65
+ `memnos grant <principal> <namespace>`). Every call is namespace-scoped and audited
66
+ server-side.
67
+
68
+ ## LangChain
69
+
70
+ ```python
71
+ from memnos_sdk import MemnosClient
72
+ from memnos_sdk.integrations.langchain import MemnosRetriever
73
+
74
+ retriever = MemnosRetriever(client=MemnosClient(token="mnk_...", namespace="org:acme"))
75
+ docs = retriever.invoke("auth token expiry policy") # drop into any RAG chain
76
+ retriever.save("JWT tokens expire after 15 minutes in prod")
77
+ ```
78
+
79
+ ## LangGraph (long-term memory)
80
+
81
+ ```python
82
+ from memnos_sdk import MemnosClient
83
+ from memnos_sdk.integrations.langgraph import MemnosStore
84
+
85
+ store = MemnosStore(MemnosClient(token="mnk_..."))
86
+ graph = builder.compile(store=store)
87
+ # in a node: store.search(("org","acme"), query="...") · store.put(("org","acme"), key, {"text": "..."})
88
+ ```
89
+
90
+ memnos is *semantic* memory: `put`→remember, `search`→hybrid+reranked recall. Exact-key
91
+ `get` is best-effort (use `search`).
92
+
93
+ ## LlamaIndex
94
+
95
+ ```python
96
+ from memnos_sdk import MemnosClient
97
+ from memnos_sdk.integrations.llamaindex import MemnosRetriever
98
+
99
+ retriever = MemnosRetriever(client=MemnosClient(token="mnk_...", namespace="org:acme"))
100
+ nodes = retriever.retrieve("auth token expiry policy") # NodeWithScore[]; drop into a query engine
101
+ retriever.save("JWT tokens expire after 15 minutes in prod")
102
+ ```
103
+
104
+ ## API surface
105
+ `remember(text)` · `recall(query) -> {memories, context}` · `context(query) -> str` ·
106
+ `consolidate()` · `feedback(query, helpful)` · `healthy()`. Async mirror on
107
+ `AsyncMemnosClient`.
108
+
109
+ Apache-2.0.
@@ -0,0 +1,15 @@
1
+ README.md
2
+ pyproject.toml
3
+ memnos_sdk/__init__.py
4
+ memnos_sdk/client.py
5
+ memnos_sdk.egg-info/PKG-INFO
6
+ memnos_sdk.egg-info/SOURCES.txt
7
+ memnos_sdk.egg-info/dependency_links.txt
8
+ memnos_sdk.egg-info/requires.txt
9
+ memnos_sdk.egg-info/top_level.txt
10
+ memnos_sdk/integrations/__init__.py
11
+ memnos_sdk/integrations/langchain.py
12
+ memnos_sdk/integrations/langgraph.py
13
+ memnos_sdk/integrations/llamaindex.py
14
+ tests/test_client.py
15
+ tests/test_integrations.py
@@ -0,0 +1,21 @@
1
+ httpx>=0.27
2
+
3
+ [all]
4
+ langchain-core>=0.3
5
+ langgraph>=0.2
6
+ llama-index-core>=0.10
7
+
8
+ [dev]
9
+ pytest>=8
10
+ langchain-core>=0.3
11
+ langgraph>=0.2
12
+ llama-index-core>=0.10
13
+
14
+ [langchain]
15
+ langchain-core>=0.3
16
+
17
+ [langgraph]
18
+ langgraph>=0.2
19
+
20
+ [llamaindex]
21
+ llama-index-core>=0.10
@@ -0,0 +1 @@
1
+ memnos_sdk
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "memnos-sdk"
7
+ version = "0.1.0"
8
+ description = "Lightweight client + LangChain/LangGraph adapters for memnos — backend memory for AI agents."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "Apache-2.0" }
12
+ authors = [{ name = "memnos" }]
13
+ keywords = ["memnos", "memory", "AI agents", "LangChain", "LangGraph", "RAG", "MCP"]
14
+ dependencies = ["httpx>=0.27"]
15
+
16
+ [project.optional-dependencies]
17
+ langchain = ["langchain-core>=0.3"]
18
+ langgraph = ["langgraph>=0.2"]
19
+ llamaindex = ["llama-index-core>=0.10"]
20
+ all = ["langchain-core>=0.3", "langgraph>=0.2", "llama-index-core>=0.10"]
21
+ dev = ["pytest>=8", "langchain-core>=0.3", "langgraph>=0.2", "llama-index-core>=0.10"]
22
+
23
+ [project.urls]
24
+ Homepage = "https://memnos.net"
25
+ Source = "https://github.com/thameema/memnos"
26
+
27
+ [tool.setuptools]
28
+ packages = ["memnos_sdk", "memnos_sdk.integrations"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,102 @@
1
+ """memnos-sdk tests.
2
+
3
+ Unit tests use an httpx MockTransport (no server needed) — deterministic. A live smoke
4
+ test runs only if MEMNOS_TOKEN + MEMNOS_NS env are set and a server is reachable.
5
+ Run: python -m pytest sdk/tests -q (or: python sdk/tests/test_client.py)
6
+ """
7
+ import os
8
+ import sys
9
+
10
+ import httpx
11
+
12
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
13
+ from memnos_sdk import AsyncMemnosClient, MemnosClient, MemnosError
14
+
15
+ PASS = FAIL = 0
16
+
17
+
18
+ def check(name, cond):
19
+ global PASS, FAIL
20
+ print(f" {'PASS' if cond else 'FAIL'} {name}")
21
+ PASS += cond; FAIL += (not cond)
22
+
23
+
24
+ def _handler(request):
25
+ import json
26
+ body = json.loads(request.content or b"{}")
27
+ if request.url.path == "/remember":
28
+ assert body["namespace"] and body["text"]
29
+ return httpx.Response(200, json={"turn_id": 1, "facts": 2, "superseded": 0})
30
+ if request.url.path == "/recall":
31
+ return httpx.Response(200, json={"memories": [{"content": "we chose postgres", "kind": "fact",
32
+ "score": 0.9, "date": "2026-06-07"}],
33
+ "context": "- (fact) we chose postgres"})
34
+ if request.url.path == "/consolidate":
35
+ return httpx.Response(200, json={"dossiers": 3})
36
+ if request.url.path == "/feedback":
37
+ return httpx.Response(200, json={"ok": True})
38
+ if request.url.path == "/healthz":
39
+ return httpx.Response(200, json={"ok": True})
40
+ return httpx.Response(404, json={"error": "not found"})
41
+
42
+
43
+ def main():
44
+ print("=== memnos-sdk (mock transport) ===")
45
+ mem = MemnosClient(token="mnk_test", namespace="org:acme", transport=httpx.MockTransport(_handler))
46
+ check("remember returns ids", mem.remember("we chose postgres")["facts"] == 2)
47
+ check("recall returns memories", mem.recall("db?")["memories"][0]["content"] == "we chose postgres")
48
+ check("context returns string", mem.context("db?").startswith("- (fact)"))
49
+ check("consolidate", mem.consolidate()["dossiers"] == 3)
50
+ check("feedback", mem.feedback("db?", True)["ok"] is True)
51
+ check("healthy()", mem.healthy() is True)
52
+ # namespace required if neither client-default nor per-call
53
+ try:
54
+ MemnosClient(token="x", transport=httpx.MockTransport(_handler)).remember("y")
55
+ check("missing namespace raises", False)
56
+ except ValueError:
57
+ check("missing namespace raises", True)
58
+ # error mapping
59
+ def err_handler(req):
60
+ return httpx.Response(403, json={"error": "forbidden for namespace"})
61
+ em = MemnosClient(token="x", namespace="n", transport=httpx.MockTransport(err_handler))
62
+ try:
63
+ em.recall("q"); check("4xx -> MemnosError", False)
64
+ except MemnosError as e:
65
+ check("4xx -> MemnosError", e.status == 403)
66
+ mem.close(); em.close()
67
+
68
+ # async
69
+ import asyncio
70
+ async def _a():
71
+ async with AsyncMemnosClient(token="x", namespace="n", transport=httpx.MockTransport(_handler)) as am:
72
+ r = await am.recall("db?")
73
+ return r["memories"][0]["content"]
74
+ check("async recall", asyncio.run(_a()) == "we chose postgres")
75
+
76
+ # adapters import (skip if frameworks absent)
77
+ try:
78
+ from memnos_sdk.integrations.langchain import MemnosRetriever # noqa
79
+ check("langchain adapter imports", True)
80
+ except ImportError:
81
+ print(" SKIP langchain adapter (langchain_core not installed)")
82
+ try:
83
+ from memnos_sdk.integrations.langgraph import MemnosStore # noqa
84
+ check("langgraph adapter imports", True)
85
+ except ImportError:
86
+ print(" SKIP langgraph adapter (langgraph not installed)")
87
+
88
+ # live smoke (optional)
89
+ tok, ns = os.environ.get("MEMNOS_TOKEN"), os.environ.get("MEMNOS_NS")
90
+ if tok and ns:
91
+ live = MemnosClient(token=tok, namespace=ns)
92
+ if live.healthy():
93
+ live.remember("sdk live smoke: postgres chosen")
94
+ check("LIVE recall", "context" in live.recall("what was chosen?"))
95
+ live.close()
96
+
97
+ print(f"\n{PASS} passed, {FAIL} failed")
98
+ sys.exit(1 if FAIL else 0)
99
+
100
+
101
+ if __name__ == "__main__":
102
+ main()
@@ -0,0 +1,108 @@
1
+ """Adapter tests for memnos_sdk integrations (LangChain, LangGraph, LlamaIndex), driven
2
+ by an httpx MockTransport — no live server. Each adapter section is skipped if its
3
+ framework isn't installed, so this runs standalone. Exits non-zero on any failure.
4
+
5
+ python sdk/tests/test_integrations.py
6
+ """
7
+ import json
8
+ import sys
9
+
10
+ import httpx
11
+
12
+ sys.path.insert(0, "sdk")
13
+ sys.path.insert(0, ".")
14
+ from memnos_sdk import MemnosClient
15
+
16
+ PASS = FAIL = SKIP = 0
17
+
18
+
19
+ def check(name, cond):
20
+ global PASS, FAIL
21
+ print(f" {'PASS' if cond else 'FAIL'} {name}")
22
+ PASS += bool(cond); FAIL += (not cond)
23
+
24
+
25
+ def skip(name):
26
+ global SKIP
27
+ print(f" SKIP {name}")
28
+ SKIP += 1
29
+
30
+
31
+ # canned server responses
32
+ REMEMBERED = []
33
+
34
+
35
+ def handler(request):
36
+ path = request.url.path
37
+ if path == "/recall":
38
+ return httpx.Response(200, json={"memories": [
39
+ {"id": 1, "content": "We chose PostgreSQL + pgvector over a graph DB", "kind": "fact", "score": 0.92, "date": "2026-01-01"},
40
+ {"id": 2, "content": "Redis OOM on prod-02 fixed with allkeys-lru", "kind": "incident", "score": 0.71}],
41
+ "context": "..."})
42
+ if path == "/remember":
43
+ REMEMBERED.append(json.loads(request.content))
44
+ return httpx.Response(200, json={"turn_id": 99, "facts": 1, "superseded": 0})
45
+ return httpx.Response(404, json={"error": "not found"})
46
+
47
+
48
+ def make_client():
49
+ return MemnosClient(base_url="http://test", token="mnk_x", namespace="org:acme",
50
+ transport=httpx.MockTransport(handler))
51
+
52
+
53
+ def test_langchain():
54
+ try:
55
+ from memnos_sdk.integrations.langchain import MemnosRetriever
56
+ except ImportError:
57
+ return skip("langchain adapter (langchain_core not installed)")
58
+ r = MemnosRetriever(client=make_client(), k=8)
59
+ docs = r.invoke("what database did we choose?")
60
+ check("langchain: returns documents", len(docs) == 2)
61
+ check("langchain: content mapped", "PostgreSQL" in docs[0].page_content)
62
+ check("langchain: metadata kind+score", docs[0].metadata.get("kind") == "fact" and docs[0].metadata.get("score") == 0.92)
63
+ REMEMBERED.clear()
64
+ r.save("We use JWT with 24h expiry")
65
+ check("langchain: save -> remember", REMEMBERED and REMEMBERED[0]["text"] == "We use JWT with 24h expiry")
66
+
67
+
68
+ def test_llamaindex():
69
+ try:
70
+ from memnos_sdk.integrations.llamaindex import MemnosRetriever
71
+ except ImportError:
72
+ return skip("llamaindex adapter (llama-index-core not installed)")
73
+ r = MemnosRetriever(client=make_client(), k=8)
74
+ nodes = r.retrieve("what database did we choose?")
75
+ check("llamaindex: returns nodes-with-score", len(nodes) == 2)
76
+ check("llamaindex: text mapped", "PostgreSQL" in nodes[0].node.get_content())
77
+ check("llamaindex: score mapped", nodes[0].score == 0.92)
78
+ check("llamaindex: metadata kind", nodes[0].node.metadata.get("kind") == "fact")
79
+ REMEMBERED.clear()
80
+ r.save("Prefer Go for backends")
81
+ check("llamaindex: save -> remember", REMEMBERED and REMEMBERED[0]["text"] == "Prefer Go for backends")
82
+
83
+
84
+ def test_langgraph():
85
+ try:
86
+ from memnos_sdk.integrations.langgraph import MemnosStore
87
+ except ImportError:
88
+ return skip("langgraph adapter (langgraph not installed)")
89
+ store = MemnosStore(make_client())
90
+ items = store.search(("org", "acme"), query="database")
91
+ check("langgraph: search -> items from recall", len(items) >= 1)
92
+ check("langgraph: item content mapped", "PostgreSQL" in items[0].value.get("content", ""))
93
+ REMEMBERED.clear()
94
+ store.put(("org", "acme"), "decision-1", {"text": "use pgvector"})
95
+ check("langgraph: put -> remember", bool(REMEMBERED) and "decision-1" in REMEMBERED[0]["text"])
96
+
97
+
98
+ def main():
99
+ print("=== memnos_sdk framework adapters ===")
100
+ test_langchain()
101
+ test_llamaindex()
102
+ test_langgraph()
103
+ print(f"\n{PASS} passed, {FAIL} failed, {SKIP} skipped")
104
+ sys.exit(1 if FAIL else 0)
105
+
106
+
107
+ if __name__ == "__main__":
108
+ main()