memnos-sdk 0.1.0__py3-none-any.whl

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.
memnos_sdk/__init__.py ADDED
@@ -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__"]
memnos_sdk/client.py ADDED
@@ -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,10 @@
1
+ memnos_sdk/__init__.py,sha256=y9VKTyGO1692ay3M2yd3Xod2MO4AcVGPVKCzQcNAEr4,609
2
+ memnos_sdk/client.py,sha256=etorgZPdcYwhJw2UNLsG6fAScDW_fpg_y22g5eHiqc0,5734
3
+ memnos_sdk/integrations/__init__.py,sha256=REDXaovbT4vPkp382xHt99LQy9VpkwJ0B1Xb9bDAewI,75
4
+ memnos_sdk/integrations/langchain.py,sha256=XOz0cnZdqf44O0x9kTSLaRp3djwR5wGe02aHQ0s1qPk,2054
5
+ memnos_sdk/integrations/langgraph.py,sha256=AmgzgRkCtq5qqfwW0ZldStZksHl_wqKI0UyXybPiLY4,4616
6
+ memnos_sdk/integrations/llamaindex.py,sha256=OhQvpXPFoS1uaBQGZzIseVCzPbbMJpoWzB5jOxW4N1Q,2276
7
+ memnos_sdk-0.1.0.dist-info/METADATA,sha256=rA4vZACJZC7y6fpOKqi-8FUopHcyqAnqLMvUcp4vsn8,3973
8
+ memnos_sdk-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
9
+ memnos_sdk-0.1.0.dist-info/top_level.txt,sha256=2pE0i6SWCUuJQl4NPrGhauh36f5I-XSPEkW6MdKoVzg,11
10
+ memnos_sdk-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ memnos_sdk