langchain-synapse 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_synapse-0.1.0/LICENSE +21 -0
- langchain_synapse-0.1.0/PKG-INFO +104 -0
- langchain_synapse-0.1.0/README.md +81 -0
- langchain_synapse-0.1.0/langchain_synapse/__init__.py +11 -0
- langchain_synapse-0.1.0/langchain_synapse/chat_history.py +101 -0
- langchain_synapse-0.1.0/langchain_synapse/checkpointer.py +189 -0
- langchain_synapse-0.1.0/langchain_synapse/memory.py +98 -0
- langchain_synapse-0.1.0/langchain_synapse/retriever.py +78 -0
- langchain_synapse-0.1.0/pyproject.toml +39 -0
- langchain_synapse-0.1.0/tests/__init__.py +0 -0
- langchain_synapse-0.1.0/tests/test_chat_history.py +43 -0
- langchain_synapse-0.1.0/tests/test_checkpointer.py +31 -0
- langchain_synapse-0.1.0/tests/test_memory.py +36 -0
- langchain_synapse-0.1.0/tests/test_retriever.py +23 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2026 Raghuram Parvataneni
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: langchain-synapse
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: An integration package connecting Synapse memory and LangChain. Privacy-first, zero API calls, pure Python.
|
|
5
|
+
Project-URL: Homepage, https://github.com/raghuram369/synapse
|
|
6
|
+
Project-URL: Repository, https://github.com/raghuram369/synapse
|
|
7
|
+
Project-URL: Issues, https://github.com/raghuram369/synapse/issues
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: langchain-core<1.0.0,>=0.3.0
|
|
21
|
+
Requires-Dist: synapse-ai-memory>=0.3.0
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# langchain-synapse
|
|
25
|
+
|
|
26
|
+
An integration package connecting [Synapse](https://github.com/raghuram369/synapse) memory and [LangChain](https://github.com/langchain-ai/langchain).
|
|
27
|
+
|
|
28
|
+
**Privacy-first AI memory** — all data stays local. Zero API calls for storage. Zero external dependencies beyond LangChain and Synapse.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install langchain-synapse
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Components
|
|
37
|
+
|
|
38
|
+
### SynapseMemory
|
|
39
|
+
|
|
40
|
+
Drop-in `BaseMemory` implementation for any LangChain chain:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from langchain_synapse import SynapseMemory
|
|
44
|
+
from synapse import Synapse
|
|
45
|
+
|
|
46
|
+
syn = Synapse("./agent_memory")
|
|
47
|
+
memory = SynapseMemory(synapse=syn)
|
|
48
|
+
|
|
49
|
+
# Use with any chain
|
|
50
|
+
chain = prompt | llm
|
|
51
|
+
chain.invoke({"input": "hello"}, config={"memory": memory})
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### SynapseChatMessageHistory
|
|
55
|
+
|
|
56
|
+
Persistent chat history with semantic recall:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from langchain_synapse import SynapseChatMessageHistory
|
|
60
|
+
from synapse import Synapse
|
|
61
|
+
|
|
62
|
+
syn = Synapse("./chat_memory")
|
|
63
|
+
history = SynapseChatMessageHistory(synapse=syn, session_id="user-123")
|
|
64
|
+
|
|
65
|
+
history.add_user_message("I love Italian food")
|
|
66
|
+
history.add_ai_message("Great! Any favorite dish?")
|
|
67
|
+
|
|
68
|
+
# Semantic search — find relevant messages, not just recent ones
|
|
69
|
+
relevant = history.search("What cuisine do they prefer?")
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### SynapseRetriever
|
|
73
|
+
|
|
74
|
+
Use Synapse as a retriever in RAG pipelines:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from langchain_synapse import SynapseRetriever
|
|
78
|
+
from synapse import Synapse
|
|
79
|
+
|
|
80
|
+
syn = Synapse("./knowledge_base")
|
|
81
|
+
syn.remember("Python was created by Guido van Rossum")
|
|
82
|
+
syn.remember("Rust was created by Graydon Hoare at Mozilla")
|
|
83
|
+
|
|
84
|
+
retriever = SynapseRetriever(synapse=syn, k=5)
|
|
85
|
+
|
|
86
|
+
# Use in a RAG chain
|
|
87
|
+
from langchain_core.runnables import RunnablePassthrough
|
|
88
|
+
chain = (
|
|
89
|
+
{"context": retriever, "question": RunnablePassthrough()}
|
|
90
|
+
| prompt
|
|
91
|
+
| llm
|
|
92
|
+
)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Why Synapse?
|
|
96
|
+
|
|
97
|
+
- **🔒 Privacy-first**: All memory stays on your machine. No cloud. No API calls for storage.
|
|
98
|
+
- **🧠 Neuroscience-inspired**: Memory strengthening, decay, and semantic recall — not just vector similarity.
|
|
99
|
+
- **⚡ Zero dependencies**: Synapse itself is pure Python with no external dependencies.
|
|
100
|
+
- **📦 Portable**: `.synapse` files can be shared, versioned, and federated across agents.
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
MIT
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# langchain-synapse
|
|
2
|
+
|
|
3
|
+
An integration package connecting [Synapse](https://github.com/raghuram369/synapse) memory and [LangChain](https://github.com/langchain-ai/langchain).
|
|
4
|
+
|
|
5
|
+
**Privacy-first AI memory** — all data stays local. Zero API calls for storage. Zero external dependencies beyond LangChain and Synapse.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install langchain-synapse
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Components
|
|
14
|
+
|
|
15
|
+
### SynapseMemory
|
|
16
|
+
|
|
17
|
+
Drop-in `BaseMemory` implementation for any LangChain chain:
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from langchain_synapse import SynapseMemory
|
|
21
|
+
from synapse import Synapse
|
|
22
|
+
|
|
23
|
+
syn = Synapse("./agent_memory")
|
|
24
|
+
memory = SynapseMemory(synapse=syn)
|
|
25
|
+
|
|
26
|
+
# Use with any chain
|
|
27
|
+
chain = prompt | llm
|
|
28
|
+
chain.invoke({"input": "hello"}, config={"memory": memory})
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### SynapseChatMessageHistory
|
|
32
|
+
|
|
33
|
+
Persistent chat history with semantic recall:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from langchain_synapse import SynapseChatMessageHistory
|
|
37
|
+
from synapse import Synapse
|
|
38
|
+
|
|
39
|
+
syn = Synapse("./chat_memory")
|
|
40
|
+
history = SynapseChatMessageHistory(synapse=syn, session_id="user-123")
|
|
41
|
+
|
|
42
|
+
history.add_user_message("I love Italian food")
|
|
43
|
+
history.add_ai_message("Great! Any favorite dish?")
|
|
44
|
+
|
|
45
|
+
# Semantic search — find relevant messages, not just recent ones
|
|
46
|
+
relevant = history.search("What cuisine do they prefer?")
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### SynapseRetriever
|
|
50
|
+
|
|
51
|
+
Use Synapse as a retriever in RAG pipelines:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from langchain_synapse import SynapseRetriever
|
|
55
|
+
from synapse import Synapse
|
|
56
|
+
|
|
57
|
+
syn = Synapse("./knowledge_base")
|
|
58
|
+
syn.remember("Python was created by Guido van Rossum")
|
|
59
|
+
syn.remember("Rust was created by Graydon Hoare at Mozilla")
|
|
60
|
+
|
|
61
|
+
retriever = SynapseRetriever(synapse=syn, k=5)
|
|
62
|
+
|
|
63
|
+
# Use in a RAG chain
|
|
64
|
+
from langchain_core.runnables import RunnablePassthrough
|
|
65
|
+
chain = (
|
|
66
|
+
{"context": retriever, "question": RunnablePassthrough()}
|
|
67
|
+
| prompt
|
|
68
|
+
| llm
|
|
69
|
+
)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Why Synapse?
|
|
73
|
+
|
|
74
|
+
- **🔒 Privacy-first**: All memory stays on your machine. No cloud. No API calls for storage.
|
|
75
|
+
- **🧠 Neuroscience-inspired**: Memory strengthening, decay, and semantic recall — not just vector similarity.
|
|
76
|
+
- **⚡ Zero dependencies**: Synapse itself is pure Python with no external dependencies.
|
|
77
|
+
- **📦 Portable**: `.synapse` files can be shared, versioned, and federated across agents.
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
|
|
81
|
+
MIT
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""LangChain integration for Synapse — privacy-first memory for AI agents."""
|
|
2
|
+
|
|
3
|
+
from langchain_synapse.chat_history import SynapseChatMessageHistory
|
|
4
|
+
from langchain_synapse.memory import SynapseMemory
|
|
5
|
+
from langchain_synapse.retriever import SynapseRetriever
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"SynapseChatMessageHistory",
|
|
9
|
+
"SynapseMemory",
|
|
10
|
+
"SynapseRetriever",
|
|
11
|
+
]
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""SynapseChatMessageHistory — LangChain chat history backed by Synapse.
|
|
2
|
+
|
|
3
|
+
Privacy-first: all messages stay local. No cloud storage.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
from typing import Any, Dict, List, Optional, Sequence
|
|
10
|
+
|
|
11
|
+
from langchain_core.chat_history import BaseChatMessageHistory
|
|
12
|
+
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage
|
|
13
|
+
from synapse import Synapse
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SynapseChatMessageHistory(BaseChatMessageHistory):
|
|
17
|
+
"""Persistent chat message history using Synapse as the backend.
|
|
18
|
+
|
|
19
|
+
Unlike simple list-based histories, Synapse provides semantic recall —
|
|
20
|
+
relevant past messages surface automatically based on context.
|
|
21
|
+
|
|
22
|
+
Example::
|
|
23
|
+
|
|
24
|
+
from synapse import Synapse
|
|
25
|
+
from langchain_synapse import SynapseChatMessageHistory
|
|
26
|
+
|
|
27
|
+
syn = Synapse("./chat_memory")
|
|
28
|
+
history = SynapseChatMessageHistory(synapse=syn, session_id="user-123")
|
|
29
|
+
|
|
30
|
+
history.add_user_message("I love Italian food")
|
|
31
|
+
history.add_ai_message("Great! Do you have a favorite dish?")
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
synapse: Optional[Synapse] = None,
|
|
37
|
+
path: str = ":memory:",
|
|
38
|
+
session_id: str = "default",
|
|
39
|
+
):
|
|
40
|
+
self.synapse = synapse or Synapse(path)
|
|
41
|
+
self.session_id = session_id
|
|
42
|
+
|
|
43
|
+
def _make_metadata(self, role: str) -> Dict[str, Any]:
|
|
44
|
+
return {
|
|
45
|
+
"role": role,
|
|
46
|
+
"session_id": self.session_id,
|
|
47
|
+
"source": "langchain_chat_history",
|
|
48
|
+
"timestamp": time.time(),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
def add_message(self, message: BaseMessage) -> None:
|
|
52
|
+
"""Add a LangChain BaseMessage to history."""
|
|
53
|
+
if isinstance(message, HumanMessage):
|
|
54
|
+
role = "human"
|
|
55
|
+
elif isinstance(message, AIMessage):
|
|
56
|
+
role = "ai"
|
|
57
|
+
elif isinstance(message, SystemMessage):
|
|
58
|
+
role = "system"
|
|
59
|
+
else:
|
|
60
|
+
role = "unknown"
|
|
61
|
+
|
|
62
|
+
self.synapse.remember(
|
|
63
|
+
str(message.content),
|
|
64
|
+
memory_type="observation",
|
|
65
|
+
metadata=self._make_metadata(role),
|
|
66
|
+
episode=f"chat-{self.session_id}",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def messages(self) -> List[BaseMessage]:
|
|
71
|
+
"""Return all messages in this session, ordered by time."""
|
|
72
|
+
all_memories = self.synapse.recall("", limit=10000)
|
|
73
|
+
session_memories = [
|
|
74
|
+
m for m in all_memories
|
|
75
|
+
if (m.metadata or {}).get("session_id") == self.session_id
|
|
76
|
+
]
|
|
77
|
+
session_memories.sort(key=lambda m: m.created_at)
|
|
78
|
+
|
|
79
|
+
messages: List[BaseMessage] = []
|
|
80
|
+
for mem in session_memories:
|
|
81
|
+
role = (mem.metadata or {}).get("role", "human")
|
|
82
|
+
if role == "ai":
|
|
83
|
+
messages.append(AIMessage(content=mem.content))
|
|
84
|
+
elif role == "system":
|
|
85
|
+
messages.append(SystemMessage(content=mem.content))
|
|
86
|
+
else:
|
|
87
|
+
messages.append(HumanMessage(content=mem.content))
|
|
88
|
+
return messages
|
|
89
|
+
|
|
90
|
+
def search(self, query: str, limit: int = 5) -> List[Any]:
|
|
91
|
+
"""Semantic search across chat history — Synapse's superpower."""
|
|
92
|
+
results = self.synapse.recall(query, limit=limit)
|
|
93
|
+
return [m for m in results
|
|
94
|
+
if (m.metadata or {}).get("session_id") == self.session_id]
|
|
95
|
+
|
|
96
|
+
def clear(self) -> None:
|
|
97
|
+
"""Clear all messages in this session."""
|
|
98
|
+
all_memories = self.synapse.recall("", limit=10000)
|
|
99
|
+
for mem in all_memories:
|
|
100
|
+
if (mem.metadata or {}).get("session_id") == self.session_id:
|
|
101
|
+
self.synapse.forget(mem.id)
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""SynapseCheckpointer — LangGraph checkpoint persistence backed by Synapse.
|
|
2
|
+
|
|
3
|
+
Saves and restores graph state between invocations.
|
|
4
|
+
All data stays local.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import time
|
|
11
|
+
from typing import Any, Dict, Iterator, Optional, Sequence, Tuple
|
|
12
|
+
|
|
13
|
+
from synapse import Synapse
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from langgraph.checkpoint.base import (
|
|
17
|
+
BaseCheckpointSaver,
|
|
18
|
+
Checkpoint,
|
|
19
|
+
CheckpointMetadata,
|
|
20
|
+
CheckpointTuple,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
_HAS_LANGGRAPH = True
|
|
24
|
+
except ImportError:
|
|
25
|
+
_HAS_LANGGRAPH = False
|
|
26
|
+
BaseCheckpointSaver = object # type: ignore
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SynapseCheckpointer(BaseCheckpointSaver): # type: ignore[misc]
|
|
30
|
+
"""LangGraph-compatible checkpointer using Synapse for persistence.
|
|
31
|
+
|
|
32
|
+
Example::
|
|
33
|
+
|
|
34
|
+
from synapse import Synapse
|
|
35
|
+
from langchain_synapse import SynapseCheckpointer
|
|
36
|
+
|
|
37
|
+
syn = Synapse("./agent_state")
|
|
38
|
+
checkpointer = SynapseCheckpointer(synapse=syn)
|
|
39
|
+
graph = builder.compile(checkpointer=checkpointer)
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, synapse: Optional[Synapse] = None, path: str = ":memory:"):
|
|
43
|
+
if _HAS_LANGGRAPH:
|
|
44
|
+
super().__init__()
|
|
45
|
+
self.synapse = synapse or Synapse(path)
|
|
46
|
+
self._index: Dict[str, list] = {}
|
|
47
|
+
self._rebuild_index()
|
|
48
|
+
|
|
49
|
+
def _rebuild_index(self) -> None:
|
|
50
|
+
all_memories = self.synapse.recall("", limit=10000)
|
|
51
|
+
for mem in all_memories:
|
|
52
|
+
meta = mem.metadata or {}
|
|
53
|
+
if meta.get("source") == "langgraph_checkpoint":
|
|
54
|
+
thread_id = meta.get("thread_id", "default")
|
|
55
|
+
checkpoint_id = meta.get("checkpoint_id", "")
|
|
56
|
+
self._index.setdefault(thread_id, []).append(
|
|
57
|
+
(checkpoint_id, mem.id, mem.created_at)
|
|
58
|
+
)
|
|
59
|
+
for thread_id in self._index:
|
|
60
|
+
self._index[thread_id].sort(key=lambda x: x[2])
|
|
61
|
+
|
|
62
|
+
def _make_checkpoint_id(self, thread_id: str) -> str:
|
|
63
|
+
count = len(self._index.get(thread_id, []))
|
|
64
|
+
return f"{thread_id}:{count}:{time.time()}"
|
|
65
|
+
|
|
66
|
+
def put(
|
|
67
|
+
self,
|
|
68
|
+
config: Dict[str, Any],
|
|
69
|
+
checkpoint: Dict[str, Any],
|
|
70
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
71
|
+
new_versions: Optional[Any] = None,
|
|
72
|
+
) -> Dict[str, Any]:
|
|
73
|
+
configurable = config.get("configurable", {})
|
|
74
|
+
thread_id = configurable.get("thread_id", "default")
|
|
75
|
+
checkpoint_ns = configurable.get("checkpoint_ns", "")
|
|
76
|
+
checkpoint_id = self._make_checkpoint_id(thread_id)
|
|
77
|
+
|
|
78
|
+
content = json.dumps(checkpoint, default=str)
|
|
79
|
+
mem = self.synapse.remember(
|
|
80
|
+
content,
|
|
81
|
+
memory_type="fact",
|
|
82
|
+
metadata={
|
|
83
|
+
"source": "langgraph_checkpoint",
|
|
84
|
+
"thread_id": thread_id,
|
|
85
|
+
"checkpoint_ns": checkpoint_ns,
|
|
86
|
+
"checkpoint_id": checkpoint_id,
|
|
87
|
+
"checkpoint_metadata": json.dumps(metadata or {}, default=str),
|
|
88
|
+
},
|
|
89
|
+
deduplicate=False,
|
|
90
|
+
)
|
|
91
|
+
self._index.setdefault(thread_id, []).append(
|
|
92
|
+
(checkpoint_id, mem.id, mem.created_at)
|
|
93
|
+
)
|
|
94
|
+
return {
|
|
95
|
+
"configurable": {
|
|
96
|
+
"thread_id": thread_id,
|
|
97
|
+
"checkpoint_ns": checkpoint_ns,
|
|
98
|
+
"checkpoint_id": checkpoint_id,
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
def put_writes(
|
|
103
|
+
self,
|
|
104
|
+
config: Dict[str, Any],
|
|
105
|
+
writes: Sequence[Tuple[str, Any]],
|
|
106
|
+
task_id: str,
|
|
107
|
+
) -> None:
|
|
108
|
+
configurable = config.get("configurable", {})
|
|
109
|
+
thread_id = configurable.get("thread_id", "default")
|
|
110
|
+
checkpoint_id = configurable.get("checkpoint_id", "")
|
|
111
|
+
content = json.dumps({"writes": writes, "task_id": task_id}, default=str)
|
|
112
|
+
self.synapse.remember(
|
|
113
|
+
content,
|
|
114
|
+
memory_type="fact",
|
|
115
|
+
metadata={
|
|
116
|
+
"source": "langgraph_writes",
|
|
117
|
+
"thread_id": thread_id,
|
|
118
|
+
"checkpoint_id": checkpoint_id,
|
|
119
|
+
"task_id": task_id,
|
|
120
|
+
},
|
|
121
|
+
deduplicate=False,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def get_tuple(self, config: Dict[str, Any]) -> Optional[Any]:
|
|
125
|
+
configurable = config.get("configurable", {})
|
|
126
|
+
thread_id = configurable.get("thread_id", "default")
|
|
127
|
+
checkpoint_id = configurable.get("checkpoint_id")
|
|
128
|
+
entries = self._index.get(thread_id, [])
|
|
129
|
+
if not entries:
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
target = (
|
|
133
|
+
next((e for e in entries if e[0] == checkpoint_id), None)
|
|
134
|
+
if checkpoint_id
|
|
135
|
+
else entries[-1]
|
|
136
|
+
)
|
|
137
|
+
if not target:
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
cp_id, mem_id, _ = target
|
|
141
|
+
memories = self.synapse.recall("", limit=10000)
|
|
142
|
+
mem = next((m for m in memories if m.id == mem_id), None)
|
|
143
|
+
if not mem:
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
checkpoint = json.loads(mem.content)
|
|
147
|
+
meta = mem.metadata or {}
|
|
148
|
+
checkpoint_metadata = json.loads(meta.get("checkpoint_metadata", "{}"))
|
|
149
|
+
|
|
150
|
+
if _HAS_LANGGRAPH:
|
|
151
|
+
return CheckpointTuple(
|
|
152
|
+
config={
|
|
153
|
+
"configurable": {
|
|
154
|
+
"thread_id": thread_id,
|
|
155
|
+
"checkpoint_ns": meta.get("checkpoint_ns", ""),
|
|
156
|
+
"checkpoint_id": cp_id,
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
checkpoint=checkpoint,
|
|
160
|
+
metadata=checkpoint_metadata,
|
|
161
|
+
parent_config=None,
|
|
162
|
+
pending_writes=[],
|
|
163
|
+
)
|
|
164
|
+
return {"config": config, "checkpoint": checkpoint, "metadata": checkpoint_metadata}
|
|
165
|
+
|
|
166
|
+
def list(
|
|
167
|
+
self,
|
|
168
|
+
config: Optional[Dict[str, Any]] = None,
|
|
169
|
+
*,
|
|
170
|
+
filter: Optional[Dict[str, Any]] = None,
|
|
171
|
+
before: Optional[Dict[str, Any]] = None,
|
|
172
|
+
limit: Optional[int] = None,
|
|
173
|
+
) -> Iterator[Any]:
|
|
174
|
+
if config:
|
|
175
|
+
thread_id = config.get("configurable", {}).get("thread_id", "default")
|
|
176
|
+
entries = self._index.get(thread_id, [])
|
|
177
|
+
else:
|
|
178
|
+
entries = [e for thread_entries in self._index.values() for e in thread_entries]
|
|
179
|
+
thread_id = "default"
|
|
180
|
+
|
|
181
|
+
entries.sort(key=lambda x: x[2], reverse=True)
|
|
182
|
+
if limit:
|
|
183
|
+
entries = entries[:limit]
|
|
184
|
+
for cp_id, mem_id, created_at in entries:
|
|
185
|
+
result = self.get_tuple(
|
|
186
|
+
{"configurable": {"thread_id": thread_id, "checkpoint_id": cp_id}}
|
|
187
|
+
)
|
|
188
|
+
if result:
|
|
189
|
+
yield result
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""SynapseMemory — LangChain BaseMemory backed by Synapse.
|
|
2
|
+
|
|
3
|
+
Privacy-first: all memory stays local. No API calls for storage.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
from langchain_core.memory import BaseMemory
|
|
11
|
+
from synapse import Synapse
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SynapseMemory(BaseMemory):
|
|
15
|
+
"""LangChain-compatible memory backed by Synapse.
|
|
16
|
+
|
|
17
|
+
Stores conversation history and relevant facts in a local Synapse
|
|
18
|
+
database. Recall is semantic — not just last-N messages.
|
|
19
|
+
|
|
20
|
+
Example::
|
|
21
|
+
|
|
22
|
+
from synapse import Synapse
|
|
23
|
+
from langchain_synapse import SynapseMemory
|
|
24
|
+
|
|
25
|
+
syn = Synapse("./agent_memory")
|
|
26
|
+
memory = SynapseMemory(synapse=syn)
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
synapse: Any # Synapse instance
|
|
30
|
+
memory_key: str = "history"
|
|
31
|
+
input_key: Optional[str] = None
|
|
32
|
+
output_key: Optional[str] = None
|
|
33
|
+
recall_limit: int = 10
|
|
34
|
+
store_inputs: bool = True
|
|
35
|
+
store_outputs: bool = True
|
|
36
|
+
return_messages: bool = False
|
|
37
|
+
|
|
38
|
+
class Config:
|
|
39
|
+
arbitrary_types_allowed = True
|
|
40
|
+
|
|
41
|
+
def __init__(self, synapse: Optional[Synapse] = None, path: str = ":memory:", **kwargs: Any):
|
|
42
|
+
super().__init__(synapse=synapse or Synapse(path), **kwargs)
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def memory_variables(self) -> List[str]:
|
|
46
|
+
return [self.memory_key]
|
|
47
|
+
|
|
48
|
+
def load_memory_variables(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
|
49
|
+
"""Recall relevant memories based on the current input."""
|
|
50
|
+
context_parts = [v for v in inputs.values() if isinstance(v, str)]
|
|
51
|
+
context = " ".join(context_parts) if context_parts else ""
|
|
52
|
+
|
|
53
|
+
memories = self.synapse.recall(context, limit=self.recall_limit)
|
|
54
|
+
|
|
55
|
+
if self.return_messages:
|
|
56
|
+
from langchain_core.messages import AIMessage, HumanMessage
|
|
57
|
+
|
|
58
|
+
messages = []
|
|
59
|
+
for mem in memories:
|
|
60
|
+
role = (mem.metadata or {}).get("role", "human")
|
|
61
|
+
if role == "ai":
|
|
62
|
+
messages.append(AIMessage(content=mem.content))
|
|
63
|
+
else:
|
|
64
|
+
messages.append(HumanMessage(content=mem.content))
|
|
65
|
+
return {self.memory_key: messages}
|
|
66
|
+
|
|
67
|
+
formatted = "\n".join(f"- {m.content}" for m in memories)
|
|
68
|
+
return {self.memory_key: formatted}
|
|
69
|
+
|
|
70
|
+
def save_context(self, inputs: Dict[str, Any], outputs: Dict[str, str]) -> None:
|
|
71
|
+
"""Store the conversation turn in Synapse."""
|
|
72
|
+
if self.store_inputs:
|
|
73
|
+
input_key = self.input_key or next(iter(inputs), None)
|
|
74
|
+
if input_key and input_key in inputs:
|
|
75
|
+
value = inputs[input_key]
|
|
76
|
+
if isinstance(value, str) and value.strip():
|
|
77
|
+
self.synapse.remember(
|
|
78
|
+
value,
|
|
79
|
+
memory_type="observation",
|
|
80
|
+
metadata={"role": "human", "source": "langchain"},
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if self.store_outputs:
|
|
84
|
+
output_key = self.output_key or next(iter(outputs), None)
|
|
85
|
+
if output_key and output_key in outputs:
|
|
86
|
+
value = outputs[output_key]
|
|
87
|
+
if isinstance(value, str) and value.strip():
|
|
88
|
+
self.synapse.remember(
|
|
89
|
+
value,
|
|
90
|
+
memory_type="observation",
|
|
91
|
+
metadata={"role": "ai", "source": "langchain"},
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def clear(self) -> None:
|
|
95
|
+
"""Clear all memories."""
|
|
96
|
+
all_memories = self.synapse.recall("", limit=10000)
|
|
97
|
+
for mem in all_memories:
|
|
98
|
+
self.synapse.forget(mem.id)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""SynapseRetriever — LangChain BaseRetriever backed by Synapse.
|
|
2
|
+
|
|
3
|
+
Privacy-first: all retrieval is local. No vector DB API calls.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
from langchain_core.callbacks import CallbackManagerForRetrieverRun
|
|
11
|
+
from langchain_core.documents import Document
|
|
12
|
+
from langchain_core.retrievers import BaseRetriever
|
|
13
|
+
from synapse import Synapse
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SynapseRetriever(BaseRetriever):
|
|
17
|
+
"""LangChain-compatible retriever using Synapse's semantic recall.
|
|
18
|
+
|
|
19
|
+
Synapse combines BM25, concept graphs, and optional local embeddings
|
|
20
|
+
for retrieval — all running locally with zero external API calls.
|
|
21
|
+
|
|
22
|
+
Example::
|
|
23
|
+
|
|
24
|
+
from synapse import Synapse
|
|
25
|
+
from langchain_synapse import SynapseRetriever
|
|
26
|
+
|
|
27
|
+
syn = Synapse("./knowledge_base")
|
|
28
|
+
syn.remember("Python was created by Guido van Rossum")
|
|
29
|
+
|
|
30
|
+
retriever = SynapseRetriever(synapse=syn, k=5)
|
|
31
|
+
docs = retriever.invoke("Who created Python?")
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
synapse: Any # Synapse instance
|
|
35
|
+
k: int = 5
|
|
36
|
+
memory_type: Optional[str] = None
|
|
37
|
+
min_strength: float = 0.01
|
|
38
|
+
metadata_filter: Dict[str, Any] = {}
|
|
39
|
+
|
|
40
|
+
class Config:
|
|
41
|
+
arbitrary_types_allowed = True
|
|
42
|
+
|
|
43
|
+
def __init__(self, synapse: Optional[Synapse] = None, path: str = ":memory:", **kwargs: Any):
|
|
44
|
+
super().__init__(synapse=synapse or Synapse(path), **kwargs)
|
|
45
|
+
|
|
46
|
+
def _get_relevant_documents(
|
|
47
|
+
self, query: str, *, run_manager: CallbackManagerForRetrieverRun
|
|
48
|
+
) -> List[Document]:
|
|
49
|
+
"""Retrieve relevant documents from Synapse."""
|
|
50
|
+
memories = self.synapse.recall(
|
|
51
|
+
query,
|
|
52
|
+
limit=self.k,
|
|
53
|
+
memory_type=self.memory_type,
|
|
54
|
+
min_strength=self.min_strength,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if self.metadata_filter:
|
|
58
|
+
memories = [
|
|
59
|
+
m for m in memories
|
|
60
|
+
if all(
|
|
61
|
+
(m.metadata or {}).get(k) == v
|
|
62
|
+
for k, v in self.metadata_filter.items()
|
|
63
|
+
)
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
return [
|
|
67
|
+
Document(
|
|
68
|
+
page_content=mem.content,
|
|
69
|
+
metadata={
|
|
70
|
+
"memory_id": mem.id,
|
|
71
|
+
"memory_type": mem.memory_type,
|
|
72
|
+
"strength": mem.effective_strength,
|
|
73
|
+
"created_at": mem.created_at,
|
|
74
|
+
**(mem.metadata or {}),
|
|
75
|
+
},
|
|
76
|
+
)
|
|
77
|
+
for mem in memories
|
|
78
|
+
]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "langchain-synapse"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "An integration package connecting Synapse memory and LangChain. Privacy-first, zero API calls, pure Python."
|
|
9
|
+
license = { text = "MIT" }
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 4 - Beta",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
21
|
+
]
|
|
22
|
+
requires-python = ">=3.10"
|
|
23
|
+
dependencies = [
|
|
24
|
+
"langchain-core>=0.3.0,<1.0.0",
|
|
25
|
+
"synapse-ai-memory>=0.3.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/raghuram369/synapse"
|
|
30
|
+
Repository = "https://github.com/raghuram369/synapse"
|
|
31
|
+
Issues = "https://github.com/raghuram369/synapse/issues"
|
|
32
|
+
|
|
33
|
+
[dependency-groups]
|
|
34
|
+
test = [
|
|
35
|
+
"pytest>=7.3.0,<9.0.0",
|
|
36
|
+
"pytest-asyncio>=0.21.1,<1.0.0",
|
|
37
|
+
"langchain-core",
|
|
38
|
+
]
|
|
39
|
+
dev = ["langchain-core"]
|
|
File without changes
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Tests for SynapseChatMessageHistory."""
|
|
2
|
+
|
|
3
|
+
from langchain_core.messages import AIMessage, HumanMessage
|
|
4
|
+
from synapse import Synapse
|
|
5
|
+
from langchain_synapse import SynapseChatMessageHistory
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_add_and_retrieve_messages():
|
|
9
|
+
syn = Synapse(":memory:")
|
|
10
|
+
history = SynapseChatMessageHistory(synapse=syn, session_id="test")
|
|
11
|
+
history.add_user_message("Hello")
|
|
12
|
+
history.add_ai_message("Hi there!")
|
|
13
|
+
messages = history.messages
|
|
14
|
+
assert len(messages) == 2
|
|
15
|
+
assert isinstance(messages[0], HumanMessage)
|
|
16
|
+
assert isinstance(messages[1], AIMessage)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_search():
|
|
20
|
+
syn = Synapse(":memory:")
|
|
21
|
+
history = SynapseChatMessageHistory(synapse=syn, session_id="test")
|
|
22
|
+
history.add_user_message("I love Italian food")
|
|
23
|
+
history.add_user_message("The weather is nice today")
|
|
24
|
+
results = history.search("cuisine preferences")
|
|
25
|
+
assert len(results) > 0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_clear():
|
|
29
|
+
syn = Synapse(":memory:")
|
|
30
|
+
history = SynapseChatMessageHistory(synapse=syn, session_id="test")
|
|
31
|
+
history.add_user_message("Hello")
|
|
32
|
+
history.clear()
|
|
33
|
+
assert len(history.messages) == 0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_session_isolation():
|
|
37
|
+
syn = Synapse(":memory:")
|
|
38
|
+
h1 = SynapseChatMessageHistory(synapse=syn, session_id="s1")
|
|
39
|
+
h2 = SynapseChatMessageHistory(synapse=syn, session_id="s2")
|
|
40
|
+
h1.add_user_message("Session 1 message")
|
|
41
|
+
h2.add_user_message("Session 2 message")
|
|
42
|
+
assert len(h1.messages) == 1
|
|
43
|
+
assert len(h2.messages) == 1
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Tests for SynapseCheckpointer."""
|
|
2
|
+
|
|
3
|
+
from synapse import Synapse
|
|
4
|
+
from langchain_synapse.checkpointer import SynapseCheckpointer
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_put_and_get():
|
|
8
|
+
syn = Synapse(":memory:")
|
|
9
|
+
cp = SynapseCheckpointer(synapse=syn)
|
|
10
|
+
config = {"configurable": {"thread_id": "t1"}}
|
|
11
|
+
checkpoint = {"v": 1, "ts": "2024-01-01", "channel_values": {"messages": []}}
|
|
12
|
+
|
|
13
|
+
result_config = cp.put(config, checkpoint, metadata={"step": 0})
|
|
14
|
+
assert "configurable" in result_config
|
|
15
|
+
assert result_config["configurable"]["thread_id"] == "t1"
|
|
16
|
+
|
|
17
|
+
# Retrieve
|
|
18
|
+
tup = cp.get_tuple(config)
|
|
19
|
+
assert tup is not None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_list_checkpoints():
|
|
23
|
+
syn = Synapse(":memory:")
|
|
24
|
+
cp = SynapseCheckpointer(synapse=syn)
|
|
25
|
+
config = {"configurable": {"thread_id": "t1"}}
|
|
26
|
+
|
|
27
|
+
cp.put(config, {"v": 1, "ts": "1"}, metadata={"step": 0})
|
|
28
|
+
cp.put(config, {"v": 1, "ts": "2"}, metadata={"step": 1})
|
|
29
|
+
|
|
30
|
+
items = list(cp.list(config))
|
|
31
|
+
assert len(items) == 2
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Tests for SynapseMemory."""
|
|
2
|
+
|
|
3
|
+
from synapse import Synapse
|
|
4
|
+
from langchain_synapse import SynapseMemory
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_memory_variables():
|
|
8
|
+
mem = SynapseMemory(synapse=Synapse(":memory:"))
|
|
9
|
+
assert mem.memory_variables == ["history"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_save_and_load():
|
|
13
|
+
syn = Synapse(":memory:")
|
|
14
|
+
mem = SynapseMemory(synapse=syn)
|
|
15
|
+
mem.save_context({"input": "I like pizza"}, {"output": "Great choice!"})
|
|
16
|
+
result = mem.load_memory_variables({"input": "food preferences"})
|
|
17
|
+
assert "history" in result
|
|
18
|
+
assert len(result["history"]) > 0
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_clear():
|
|
22
|
+
syn = Synapse(":memory:")
|
|
23
|
+
mem = SynapseMemory(synapse=syn)
|
|
24
|
+
mem.save_context({"input": "test"}, {"output": "response"})
|
|
25
|
+
mem.clear()
|
|
26
|
+
result = mem.load_memory_variables({"input": "test"})
|
|
27
|
+
assert result["history"] == ""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_return_messages():
|
|
31
|
+
syn = Synapse(":memory:")
|
|
32
|
+
mem = SynapseMemory(synapse=syn, return_messages=True)
|
|
33
|
+
mem.save_context({"input": "hello"}, {"output": "hi there"})
|
|
34
|
+
result = mem.load_memory_variables({"input": "greeting"})
|
|
35
|
+
messages = result["history"]
|
|
36
|
+
assert len(messages) > 0
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Tests for SynapseRetriever."""
|
|
2
|
+
|
|
3
|
+
from langchain_core.documents import Document
|
|
4
|
+
from synapse import Synapse
|
|
5
|
+
from langchain_synapse import SynapseRetriever
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_retrieve_documents():
|
|
9
|
+
syn = Synapse(":memory:")
|
|
10
|
+
syn.remember("Python was created by Guido van Rossum")
|
|
11
|
+
syn.remember("JavaScript was created by Brendan Eich")
|
|
12
|
+
|
|
13
|
+
retriever = SynapseRetriever(synapse=syn, k=5)
|
|
14
|
+
docs = retriever.invoke("Who created Python?")
|
|
15
|
+
assert len(docs) > 0
|
|
16
|
+
assert all(isinstance(d, Document) for d in docs)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_empty_retrieval():
|
|
20
|
+
syn = Synapse(":memory:")
|
|
21
|
+
retriever = SynapseRetriever(synapse=syn, k=5)
|
|
22
|
+
docs = retriever.invoke("anything")
|
|
23
|
+
assert docs == []
|