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 +12 -0
- memnos_sdk/client.py +137 -0
- memnos_sdk/integrations/__init__.py +1 -0
- memnos_sdk/integrations/langchain.py +51 -0
- memnos_sdk/integrations/langgraph.py +103 -0
- memnos_sdk/integrations/llamaindex.py +52 -0
- memnos_sdk-0.1.0.dist-info/METADATA +109 -0
- memnos_sdk-0.1.0.dist-info/RECORD +10 -0
- memnos_sdk-0.1.0.dist-info/WHEEL +5 -0
- memnos_sdk-0.1.0.dist-info/top_level.txt +1 -0
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 @@
|
|
|
1
|
+
memnos_sdk
|