langchain-memorae 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.
- langchain_memorae-0.1.0/PKG-INFO +62 -0
- langchain_memorae-0.1.0/README.md +46 -0
- langchain_memorae-0.1.0/langchain_memorae/__init__.py +8 -0
- langchain_memorae-0.1.0/langchain_memorae/client.py +89 -0
- langchain_memorae-0.1.0/langchain_memorae/memory.py +51 -0
- langchain_memorae-0.1.0/langchain_memorae/retriever.py +30 -0
- langchain_memorae-0.1.0/langchain_memorae/types.py +26 -0
- langchain_memorae-0.1.0/langchain_memorae.egg-info/PKG-INFO +62 -0
- langchain_memorae-0.1.0/langchain_memorae.egg-info/SOURCES.txt +13 -0
- langchain_memorae-0.1.0/langchain_memorae.egg-info/dependency_links.txt +1 -0
- langchain_memorae-0.1.0/langchain_memorae.egg-info/requires.txt +11 -0
- langchain_memorae-0.1.0/langchain_memorae.egg-info/top_level.txt +1 -0
- langchain_memorae-0.1.0/pyproject.toml +18 -0
- langchain_memorae-0.1.0/setup.cfg +4 -0
- langchain_memorae-0.1.0/tests/test_integration.py +69 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: langchain-memorae
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MeMorae memory and retrieval adapters for LangChain and LangGraph
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: httpx<1,>=0.25
|
|
8
|
+
Requires-Dist: langchain-core<2,>=0.3
|
|
9
|
+
Requires-Dist: langchain-classic<2,>=1
|
|
10
|
+
Provides-Extra: test
|
|
11
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
12
|
+
Provides-Extra: examples
|
|
13
|
+
Requires-Dist: langchain-openai>=0.2; extra == "examples"
|
|
14
|
+
Requires-Dist: langgraph>=0.2; extra == "examples"
|
|
15
|
+
Requires-Dist: flask>=3; extra == "examples"
|
|
16
|
+
|
|
17
|
+
# langchain-memorae
|
|
18
|
+
|
|
19
|
+
Connect an existing LangChain, LangGraph, or local chatbot to MeMorae's project-key API. MeMorae handles ingestion, approval, and retrieval; this package only calls the existing endpoints. The MeMorae backend must already be running.
|
|
20
|
+
|
|
21
|
+
MCP exposes MeMorae tools to MCP clients. This package provides Python memory and retriever objects for application code using LangChain or LangGraph.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
From the repository root:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install -e integrations/langchain-memorae
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
When published, install with `pip install langchain-memorae`.
|
|
32
|
+
|
|
33
|
+
## Basic usage
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from langchain_memorae import MeMoraeMemory, MeMoraeRetriever
|
|
37
|
+
|
|
38
|
+
memory = MeMoraeMemory(api_key="MEMORAE_PROJECT_API_KEY",
|
|
39
|
+
base_url="http://localhost:8000",
|
|
40
|
+
subject_id="user_123", namespace="default")
|
|
41
|
+
context = memory.load_memory_variables({"input": "What food do I like?"})
|
|
42
|
+
memory.save_context({"input": "I love pizza"}, {"output": "Got it."})
|
|
43
|
+
|
|
44
|
+
retriever = MeMoraeRetriever(api_key="MEMORAE_PROJECT_API_KEY",
|
|
45
|
+
base_url="http://localhost:8000",
|
|
46
|
+
subject_id="user_123", namespace="default")
|
|
47
|
+
docs = retriever.invoke("What sports do I enjoy?")
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`context["history"]` is a compact string of retrieved memories. `docs` contains LangChain `Document` objects with `memory_id` and `source_ids` metadata. Ingestion is asynchronous on the server; a new message may take time to appear in search. Assistant output is saved only with `save_assistant=True`.
|
|
51
|
+
|
|
52
|
+
`subject_id` identifies the end user whose memories are recalled. `namespace` separates memory spaces for that user; use the same values for saving and searching. A project API key determines the project scope. Keep it on your server, load it from an environment variable, and never put it in browser code or logs.
|
|
53
|
+
|
|
54
|
+
## LangGraph
|
|
55
|
+
|
|
56
|
+
In a graph, call `memory.load_memory_variables({"input": question})` in a recall node before the answer node. Call `memory.save_context({"input": question}, {"output": answer})` in a save node afterward. See [langgraph_chatbot.py](examples/langgraph_chatbot.py) for a complete small graph.
|
|
57
|
+
|
|
58
|
+
## Local chatbot
|
|
59
|
+
|
|
60
|
+
Wrap your existing answer function: load `history`, pass it to the function as context, then save the question and answer. See [local_chatbot_flask.py](examples/local_chatbot_flask.py) for a Flask route and [basic_chatbot.py](examples/basic_chatbot.py) for ChatOpenAI. Examples read `MEMORAE_API_KEY`, `MEMORAE_BASE_URL`, and `MEMORAE_SUBJECT_ID` from the environment. The ChatOpenAI examples also need `OPENAI_API_KEY` and their optional dependencies.
|
|
61
|
+
|
|
62
|
+
`clear()` is a no-op because clearing remote memories is a separate MeMorae review action.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# langchain-memorae
|
|
2
|
+
|
|
3
|
+
Connect an existing LangChain, LangGraph, or local chatbot to MeMorae's project-key API. MeMorae handles ingestion, approval, and retrieval; this package only calls the existing endpoints. The MeMorae backend must already be running.
|
|
4
|
+
|
|
5
|
+
MCP exposes MeMorae tools to MCP clients. This package provides Python memory and retriever objects for application code using LangChain or LangGraph.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
From the repository root:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install -e integrations/langchain-memorae
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
When published, install with `pip install langchain-memorae`.
|
|
16
|
+
|
|
17
|
+
## Basic usage
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from langchain_memorae import MeMoraeMemory, MeMoraeRetriever
|
|
21
|
+
|
|
22
|
+
memory = MeMoraeMemory(api_key="MEMORAE_PROJECT_API_KEY",
|
|
23
|
+
base_url="http://localhost:8000",
|
|
24
|
+
subject_id="user_123", namespace="default")
|
|
25
|
+
context = memory.load_memory_variables({"input": "What food do I like?"})
|
|
26
|
+
memory.save_context({"input": "I love pizza"}, {"output": "Got it."})
|
|
27
|
+
|
|
28
|
+
retriever = MeMoraeRetriever(api_key="MEMORAE_PROJECT_API_KEY",
|
|
29
|
+
base_url="http://localhost:8000",
|
|
30
|
+
subject_id="user_123", namespace="default")
|
|
31
|
+
docs = retriever.invoke("What sports do I enjoy?")
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`context["history"]` is a compact string of retrieved memories. `docs` contains LangChain `Document` objects with `memory_id` and `source_ids` metadata. Ingestion is asynchronous on the server; a new message may take time to appear in search. Assistant output is saved only with `save_assistant=True`.
|
|
35
|
+
|
|
36
|
+
`subject_id` identifies the end user whose memories are recalled. `namespace` separates memory spaces for that user; use the same values for saving and searching. A project API key determines the project scope. Keep it on your server, load it from an environment variable, and never put it in browser code or logs.
|
|
37
|
+
|
|
38
|
+
## LangGraph
|
|
39
|
+
|
|
40
|
+
In a graph, call `memory.load_memory_variables({"input": question})` in a recall node before the answer node. Call `memory.save_context({"input": question}, {"output": answer})` in a save node afterward. See [langgraph_chatbot.py](examples/langgraph_chatbot.py) for a complete small graph.
|
|
41
|
+
|
|
42
|
+
## Local chatbot
|
|
43
|
+
|
|
44
|
+
Wrap your existing answer function: load `history`, pass it to the function as context, then save the question and answer. See [local_chatbot_flask.py](examples/local_chatbot_flask.py) for a Flask route and [basic_chatbot.py](examples/basic_chatbot.py) for ChatOpenAI. Examples read `MEMORAE_API_KEY`, `MEMORAE_BASE_URL`, and `MEMORAE_SUBJECT_ID` from the environment. The ChatOpenAI examples also need `OPENAI_API_KEY` and their optional dependencies.
|
|
45
|
+
|
|
46
|
+
`clear()` is a no-op because clearing remote memories is a separate MeMorae review action.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""MeMorae adapters for LangChain and LangGraph."""
|
|
2
|
+
from .client import MeMoraeClient
|
|
3
|
+
from .memory import MeMoraeMemory
|
|
4
|
+
from .retriever import MeMoraeRetriever
|
|
5
|
+
from .types import IngestResult, MemoryRecord, SearchResult
|
|
6
|
+
|
|
7
|
+
__all__ = ["MeMoraeClient", "MeMoraeMemory", "MeMoraeRetriever",
|
|
8
|
+
"IngestResult", "MemoryRecord", "SearchResult"]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""HTTP client for the existing MeMorae project-key integration API."""
|
|
2
|
+
from typing import Any
|
|
3
|
+
from uuid import uuid4
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from .types import IngestResult, MemoryRecord, SearchResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MeMoraeClient:
|
|
11
|
+
def __init__(self, api_key: str, base_url: str = "http://localhost:8000",
|
|
12
|
+
subject_id: str = "", namespace: str = "default", timeout: float = 30.0):
|
|
13
|
+
if not api_key or not api_key.strip():
|
|
14
|
+
raise ValueError("MeMorae project API key is required")
|
|
15
|
+
if not subject_id:
|
|
16
|
+
raise ValueError("subject_id is required")
|
|
17
|
+
if not namespace:
|
|
18
|
+
raise ValueError("namespace is required")
|
|
19
|
+
self._api_key = api_key
|
|
20
|
+
self.base_url = base_url.rstrip("/")
|
|
21
|
+
self.subject_id = subject_id
|
|
22
|
+
self.namespace = namespace
|
|
23
|
+
self.timeout = timeout
|
|
24
|
+
|
|
25
|
+
def __repr__(self) -> str:
|
|
26
|
+
return (f"MeMoraeClient(base_url={self.base_url!r}, "
|
|
27
|
+
f"subject_id={self.subject_id!r}, namespace={self.namespace!r})")
|
|
28
|
+
|
|
29
|
+
def _post(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
30
|
+
try:
|
|
31
|
+
with httpx.Client(timeout=self.timeout) as client:
|
|
32
|
+
response = client.post(
|
|
33
|
+
f"{self.base_url}{path}", json=payload,
|
|
34
|
+
headers={"Authorization": f"Bearer {self._api_key}"},
|
|
35
|
+
)
|
|
36
|
+
response.raise_for_status()
|
|
37
|
+
return response.json()
|
|
38
|
+
except httpx.HTTPStatusError as exc:
|
|
39
|
+
raise RuntimeError(f"MeMorae API returned HTTP {exc.response.status_code}") from None
|
|
40
|
+
except httpx.RequestError:
|
|
41
|
+
raise RuntimeError("MeMorae API request failed") from None
|
|
42
|
+
|
|
43
|
+
def ingest_message(self, text: str, role: str = "user", turn_id: str | None = None,
|
|
44
|
+
metadata: dict[str, Any] | None = None) -> IngestResult:
|
|
45
|
+
if role not in {"user", "assistant"}:
|
|
46
|
+
raise ValueError("role must be 'user' or 'assistant'")
|
|
47
|
+
if not text:
|
|
48
|
+
raise ValueError("text is required")
|
|
49
|
+
# The integration endpoint ingests user messages. The existing source
|
|
50
|
+
# endpoint accepts other roles and resolves its project from this key.
|
|
51
|
+
conversation_id = turn_id or str(uuid4())
|
|
52
|
+
payload = {"subject_id": self.subject_id, "namespace": self.namespace,
|
|
53
|
+
"conversation_id": conversation_id, "idempotency_key": str(uuid4()),
|
|
54
|
+
"content": text}
|
|
55
|
+
if role == "user":
|
|
56
|
+
data = self._post("/v1/integrations/messages", payload)
|
|
57
|
+
else:
|
|
58
|
+
scope = self._get_scope()
|
|
59
|
+
data = self._post("/v1/sources/messages", {
|
|
60
|
+
**payload, "project_id": scope["project_id"], "role": role,
|
|
61
|
+
})
|
|
62
|
+
# The API has no metadata field; it is intentionally not sent.
|
|
63
|
+
return IngestResult(id=str(data["id"]), status=data["status"], created=data["created"])
|
|
64
|
+
|
|
65
|
+
def _get_scope(self) -> dict[str, Any]:
|
|
66
|
+
try:
|
|
67
|
+
with httpx.Client(timeout=self.timeout) as client:
|
|
68
|
+
response = client.get(f"{self.base_url}/v1/integrations/scope",
|
|
69
|
+
headers={"Authorization": f"Bearer {self._api_key}"})
|
|
70
|
+
response.raise_for_status()
|
|
71
|
+
return response.json()
|
|
72
|
+
except httpx.HTTPStatusError as exc:
|
|
73
|
+
raise RuntimeError(f"MeMorae API returned HTTP {exc.response.status_code}") from None
|
|
74
|
+
except httpx.RequestError:
|
|
75
|
+
raise RuntimeError("MeMorae API request failed") from None
|
|
76
|
+
|
|
77
|
+
def search(self, query: str, limit: int = 8) -> SearchResult:
|
|
78
|
+
if not query:
|
|
79
|
+
raise ValueError("query is required")
|
|
80
|
+
data = self._post("/v1/integrations/memories/search", {
|
|
81
|
+
"subject_id": self.subject_id, "namespace": self.namespace,
|
|
82
|
+
"query": query, "limit": limit,
|
|
83
|
+
})
|
|
84
|
+
memories = [MemoryRecord(id=str(item["id"]), content=item["content"],
|
|
85
|
+
type=item.get("type"), sources=item.get("sources") or [],
|
|
86
|
+
retrieval_explanation=item.get("retrieval_explanation") or {})
|
|
87
|
+
for item in data.get("memories", [])]
|
|
88
|
+
return SearchResult(memories=memories, context=data.get("context"),
|
|
89
|
+
memory_state=data.get("memory_state"))
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""LangChain memory adapter."""
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
from langchain_core.memory import BaseMemory
|
|
6
|
+
except ImportError:
|
|
7
|
+
from langchain_classic.base_memory import BaseMemory
|
|
8
|
+
from pydantic import PrivateAttr, SecretStr
|
|
9
|
+
|
|
10
|
+
from .client import MeMoraeClient
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MeMoraeMemory(BaseMemory):
|
|
14
|
+
api_key: SecretStr
|
|
15
|
+
base_url: str = "http://localhost:8000"
|
|
16
|
+
subject_id: str
|
|
17
|
+
namespace: str = "default"
|
|
18
|
+
timeout: float = 30.0
|
|
19
|
+
memory_key: str = "history"
|
|
20
|
+
input_key: str = "input"
|
|
21
|
+
output_key: str = "output"
|
|
22
|
+
limit: int = 8
|
|
23
|
+
save_assistant: bool = False
|
|
24
|
+
_client: MeMoraeClient = PrivateAttr()
|
|
25
|
+
|
|
26
|
+
def model_post_init(self, __context: Any) -> None:
|
|
27
|
+
self._client = MeMoraeClient(self.api_key.get_secret_value(), self.base_url, self.subject_id,
|
|
28
|
+
self.namespace, self.timeout)
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def memory_variables(self) -> list[str]:
|
|
32
|
+
return [self.memory_key]
|
|
33
|
+
|
|
34
|
+
def load_memory_variables(self, inputs: dict[str, Any]) -> dict[str, str]:
|
|
35
|
+
query = str(inputs.get(self.input_key, "")).strip()
|
|
36
|
+
if not query:
|
|
37
|
+
return {self.memory_key: ""}
|
|
38
|
+
result = self._client.search(query, self.limit)
|
|
39
|
+
return {self.memory_key: "\n".join(item.content for item in result.memories)}
|
|
40
|
+
|
|
41
|
+
def save_context(self, inputs: dict[str, Any], outputs: dict[str, Any]) -> None:
|
|
42
|
+
user_text = str(inputs.get(self.input_key, "")).strip()
|
|
43
|
+
if user_text:
|
|
44
|
+
self._client.ingest_message(user_text)
|
|
45
|
+
if self.save_assistant:
|
|
46
|
+
assistant_text = str(outputs.get(self.output_key, "")).strip()
|
|
47
|
+
if assistant_text:
|
|
48
|
+
self._client.ingest_message(assistant_text, role="assistant")
|
|
49
|
+
|
|
50
|
+
def clear(self) -> None:
|
|
51
|
+
"""No remote deletion: MeMorae memories are managed by its own review API."""
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""LangChain retriever adapter."""
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from langchain_core.documents import Document
|
|
5
|
+
from langchain_core.retrievers import BaseRetriever
|
|
6
|
+
from pydantic import PrivateAttr, SecretStr
|
|
7
|
+
|
|
8
|
+
from .client import MeMoraeClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MeMoraeRetriever(BaseRetriever):
|
|
12
|
+
api_key: SecretStr
|
|
13
|
+
base_url: str = "http://localhost:8000"
|
|
14
|
+
subject_id: str
|
|
15
|
+
namespace: str = "default"
|
|
16
|
+
timeout: float = 30.0
|
|
17
|
+
limit: int = 8
|
|
18
|
+
_client: MeMoraeClient = PrivateAttr()
|
|
19
|
+
|
|
20
|
+
def model_post_init(self, __context: Any) -> None:
|
|
21
|
+
self._client = MeMoraeClient(self.api_key.get_secret_value(), self.base_url, self.subject_id,
|
|
22
|
+
self.namespace, self.timeout)
|
|
23
|
+
|
|
24
|
+
def _get_relevant_documents(self, query: str, *, run_manager: Any = None) -> list[Document]:
|
|
25
|
+
result = self._client.search(query, self.limit)
|
|
26
|
+
return [Document(page_content=item.content, metadata={
|
|
27
|
+
"memory_id": item.id, "type": item.type,
|
|
28
|
+
"source_ids": [str(source["source_id"]) for source in item.sources
|
|
29
|
+
if source.get("source_id") is not None],
|
|
30
|
+
}) for item in result.memories]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Small, framework-independent API result types."""
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True)
|
|
7
|
+
class MemoryRecord:
|
|
8
|
+
id: str
|
|
9
|
+
content: str
|
|
10
|
+
type: str | None = None
|
|
11
|
+
sources: list[dict[str, Any]] = field(default_factory=list)
|
|
12
|
+
retrieval_explanation: dict[str, Any] = field(default_factory=dict)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class SearchResult:
|
|
17
|
+
memories: list[MemoryRecord]
|
|
18
|
+
context: str | None = None
|
|
19
|
+
memory_state: str | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class IngestResult:
|
|
24
|
+
id: str
|
|
25
|
+
status: str
|
|
26
|
+
created: bool
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: langchain-memorae
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MeMorae memory and retrieval adapters for LangChain and LangGraph
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: httpx<1,>=0.25
|
|
8
|
+
Requires-Dist: langchain-core<2,>=0.3
|
|
9
|
+
Requires-Dist: langchain-classic<2,>=1
|
|
10
|
+
Provides-Extra: test
|
|
11
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
12
|
+
Provides-Extra: examples
|
|
13
|
+
Requires-Dist: langchain-openai>=0.2; extra == "examples"
|
|
14
|
+
Requires-Dist: langgraph>=0.2; extra == "examples"
|
|
15
|
+
Requires-Dist: flask>=3; extra == "examples"
|
|
16
|
+
|
|
17
|
+
# langchain-memorae
|
|
18
|
+
|
|
19
|
+
Connect an existing LangChain, LangGraph, or local chatbot to MeMorae's project-key API. MeMorae handles ingestion, approval, and retrieval; this package only calls the existing endpoints. The MeMorae backend must already be running.
|
|
20
|
+
|
|
21
|
+
MCP exposes MeMorae tools to MCP clients. This package provides Python memory and retriever objects for application code using LangChain or LangGraph.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
From the repository root:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install -e integrations/langchain-memorae
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
When published, install with `pip install langchain-memorae`.
|
|
32
|
+
|
|
33
|
+
## Basic usage
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from langchain_memorae import MeMoraeMemory, MeMoraeRetriever
|
|
37
|
+
|
|
38
|
+
memory = MeMoraeMemory(api_key="MEMORAE_PROJECT_API_KEY",
|
|
39
|
+
base_url="http://localhost:8000",
|
|
40
|
+
subject_id="user_123", namespace="default")
|
|
41
|
+
context = memory.load_memory_variables({"input": "What food do I like?"})
|
|
42
|
+
memory.save_context({"input": "I love pizza"}, {"output": "Got it."})
|
|
43
|
+
|
|
44
|
+
retriever = MeMoraeRetriever(api_key="MEMORAE_PROJECT_API_KEY",
|
|
45
|
+
base_url="http://localhost:8000",
|
|
46
|
+
subject_id="user_123", namespace="default")
|
|
47
|
+
docs = retriever.invoke("What sports do I enjoy?")
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`context["history"]` is a compact string of retrieved memories. `docs` contains LangChain `Document` objects with `memory_id` and `source_ids` metadata. Ingestion is asynchronous on the server; a new message may take time to appear in search. Assistant output is saved only with `save_assistant=True`.
|
|
51
|
+
|
|
52
|
+
`subject_id` identifies the end user whose memories are recalled. `namespace` separates memory spaces for that user; use the same values for saving and searching. A project API key determines the project scope. Keep it on your server, load it from an environment variable, and never put it in browser code or logs.
|
|
53
|
+
|
|
54
|
+
## LangGraph
|
|
55
|
+
|
|
56
|
+
In a graph, call `memory.load_memory_variables({"input": question})` in a recall node before the answer node. Call `memory.save_context({"input": question}, {"output": answer})` in a save node afterward. See [langgraph_chatbot.py](examples/langgraph_chatbot.py) for a complete small graph.
|
|
57
|
+
|
|
58
|
+
## Local chatbot
|
|
59
|
+
|
|
60
|
+
Wrap your existing answer function: load `history`, pass it to the function as context, then save the question and answer. See [local_chatbot_flask.py](examples/local_chatbot_flask.py) for a Flask route and [basic_chatbot.py](examples/basic_chatbot.py) for ChatOpenAI. Examples read `MEMORAE_API_KEY`, `MEMORAE_BASE_URL`, and `MEMORAE_SUBJECT_ID` from the environment. The ChatOpenAI examples also need `OPENAI_API_KEY` and their optional dependencies.
|
|
61
|
+
|
|
62
|
+
`clear()` is a no-op because clearing remote memories is a separate MeMorae review action.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
langchain_memorae/__init__.py
|
|
4
|
+
langchain_memorae/client.py
|
|
5
|
+
langchain_memorae/memory.py
|
|
6
|
+
langchain_memorae/retriever.py
|
|
7
|
+
langchain_memorae/types.py
|
|
8
|
+
langchain_memorae.egg-info/PKG-INFO
|
|
9
|
+
langchain_memorae.egg-info/SOURCES.txt
|
|
10
|
+
langchain_memorae.egg-info/dependency_links.txt
|
|
11
|
+
langchain_memorae.egg-info/requires.txt
|
|
12
|
+
langchain_memorae.egg-info/top_level.txt
|
|
13
|
+
tests/test_integration.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
langchain_memorae
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "langchain-memorae"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "MeMorae memory and retrieval adapters for LangChain and LangGraph"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
dependencies = ["httpx>=0.25,<1", "langchain-core>=0.3,<2", "langchain-classic>=1,<2"]
|
|
12
|
+
|
|
13
|
+
[project.optional-dependencies]
|
|
14
|
+
test = ["pytest>=8"]
|
|
15
|
+
examples = ["langchain-openai>=0.2", "langgraph>=0.2", "flask>=3"]
|
|
16
|
+
|
|
17
|
+
[tool.setuptools.packages.find]
|
|
18
|
+
include = ["langchain_memorae*"]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
from langchain_memorae import MeMoraeClient, MeMoraeMemory, MeMoraeRetriever
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def fake_http(monkeypatch, response_data):
|
|
8
|
+
calls = []
|
|
9
|
+
|
|
10
|
+
def handler(request):
|
|
11
|
+
calls.append(request)
|
|
12
|
+
return httpx.Response(200 if request.method == "GET" else 202, json=response_data)
|
|
13
|
+
|
|
14
|
+
original = httpx.Client
|
|
15
|
+
|
|
16
|
+
def client_factory(*args, **kwargs):
|
|
17
|
+
return original(*args, transport=httpx.MockTransport(handler), **kwargs)
|
|
18
|
+
|
|
19
|
+
monkeypatch.setattr(httpx, "Client", client_factory)
|
|
20
|
+
return calls
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_search_formatting(monkeypatch):
|
|
24
|
+
calls = fake_http(monkeypatch, {"memory_state": "ready", "context": "likes pizza",
|
|
25
|
+
"memories": [{"id": "m1", "content": "Likes pizza", "type": "preference",
|
|
26
|
+
"sources": [{"source_id": "s1"}]}]})
|
|
27
|
+
result = MeMoraeClient("memora_secret", subject_id="u1").search("food")
|
|
28
|
+
assert result.memories[0].content == "Likes pizza"
|
|
29
|
+
assert result.memories[0].sources[0]["source_id"] == "s1"
|
|
30
|
+
assert calls[0].url.path == "/v1/integrations/memories/search"
|
|
31
|
+
assert b'"limit":8' in calls[0].content
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_save_context_ingests_user_only(monkeypatch):
|
|
35
|
+
calls = fake_http(monkeypatch, {"id": "s1", "status": "pending", "created": True})
|
|
36
|
+
memory = MeMoraeMemory(api_key="memora_secret", subject_id="u1")
|
|
37
|
+
memory.save_context({"input": "I love pizza"}, {"output": "Got it"})
|
|
38
|
+
assert len(calls) == 1
|
|
39
|
+
assert calls[0].url.path == "/v1/integrations/messages"
|
|
40
|
+
assert b"I love pizza" in calls[0].content
|
|
41
|
+
assert b"Got it" not in calls[0].content
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_retriever_documents(monkeypatch):
|
|
45
|
+
fake_http(monkeypatch, {"memories": [{"id": "m1", "content": "Tennis",
|
|
46
|
+
"sources": [{"source_id": "s1"}]}]})
|
|
47
|
+
document = MeMoraeRetriever(api_key="memora_secret", subject_id="u1").invoke("sports")[0]
|
|
48
|
+
assert document.page_content == "Tennis"
|
|
49
|
+
assert document.metadata["memory_id"] == "m1"
|
|
50
|
+
assert document.metadata["source_ids"] == ["s1"]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_missing_key_and_no_leakage(monkeypatch):
|
|
54
|
+
with pytest.raises(ValueError, match="required"):
|
|
55
|
+
MeMoraeClient("", subject_id="u1")
|
|
56
|
+
with pytest.raises(ValueError, match="required"):
|
|
57
|
+
MeMoraeMemory(api_key="", subject_id="u1")
|
|
58
|
+
secret = "memora_very_secret"
|
|
59
|
+
assert secret not in repr(MeMoraeClient(secret, subject_id="u1"))
|
|
60
|
+
assert secret not in repr(MeMoraeMemory(api_key=secret, subject_id="u1"))
|
|
61
|
+
assert secret not in repr(MeMoraeRetriever(api_key=secret, subject_id="u1"))
|
|
62
|
+
# Error construction itself must not expose the Authorization header.
|
|
63
|
+
def failed(request):
|
|
64
|
+
return httpx.Response(401, json={"detail": "denied"})
|
|
65
|
+
original = httpx.Client
|
|
66
|
+
monkeypatch.setattr(httpx, "Client", lambda *a, **k: original(*a, transport=httpx.MockTransport(failed), **k))
|
|
67
|
+
with pytest.raises(RuntimeError) as error:
|
|
68
|
+
MeMoraeClient(secret, subject_id="u1").search("food")
|
|
69
|
+
assert secret not in str(error.value)
|