synapse-memory-sdk 1.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.
- synapse_memory/__init__.py +56 -0
- synapse_memory/chat.py +44 -0
- synapse_memory/error.py +33 -0
- synapse_memory/memory.py +117 -0
- synapse_memory/scheduler.py +65 -0
- synapse_memory/synapse.py +107 -0
- synapse_memory/types.py +28 -0
- synapse_memory/visualization.py +187 -0
- synapse_memory/webhooks.py +106 -0
- synapse_memory_sdk-1.1.0.dist-info/METADATA +117 -0
- synapse_memory_sdk-1.1.0.dist-info/RECORD +14 -0
- synapse_memory_sdk-1.1.0.dist-info/WHEEL +5 -0
- synapse_memory_sdk-1.1.0.dist-info/licenses/LICENSE +21 -0
- synapse_memory_sdk-1.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Synapse Memory SDK — Python client for Synapse Memory API.
|
|
2
|
+
|
|
3
|
+
Persistent memory for AI agents. Never forget between sessions.
|
|
4
|
+
|
|
5
|
+
Example:
|
|
6
|
+
from synapse_memory import Synapse
|
|
7
|
+
|
|
8
|
+
synapse = Synapse(
|
|
9
|
+
base_url="https://synapse.schaefer.zone",
|
|
10
|
+
mind_key="your-mind-key-uuid",
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
# Recall all memories
|
|
14
|
+
text = synapse.memory.recall()
|
|
15
|
+
|
|
16
|
+
# Store a new memory
|
|
17
|
+
synapse.memory.store(
|
|
18
|
+
category="fact",
|
|
19
|
+
content="User is Michael Schäfer",
|
|
20
|
+
key="user_name",
|
|
21
|
+
priority="critical",
|
|
22
|
+
source="user",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# Search memories
|
|
26
|
+
results = synapse.memory.search("insolvenz")
|
|
27
|
+
|
|
28
|
+
# Chat with the human
|
|
29
|
+
msgs = synapse.chat.poll()
|
|
30
|
+
if msgs["messages"]:
|
|
31
|
+
synapse.chat.reply("Got it!")
|
|
32
|
+
|
|
33
|
+
# Webhooks (v1.4.0)
|
|
34
|
+
synapse.webhooks.register(
|
|
35
|
+
url="https://my-app.com/webhook",
|
|
36
|
+
events="memory.*",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Visualization (v1.4.0)
|
|
40
|
+
graph = synapse.visualization.graph(max_nodes=200)
|
|
41
|
+
tags = synapse.visualization.tags()
|
|
42
|
+
|
|
43
|
+
# Compact (v1.4.0)
|
|
44
|
+
synapse.compact.compact(dry_run=True)
|
|
45
|
+
|
|
46
|
+
# Sharing (v1.4.0, JWT required)
|
|
47
|
+
synapse = Synapse(base_url=..., mind_key=..., jwt="...")
|
|
48
|
+
synapse.sharing.share(mind_id, "user@example.com", acl="read")
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
from .synapse import Synapse
|
|
52
|
+
from .error import SynapseError
|
|
53
|
+
from .webhooks import WEBHOOK_EVENTS
|
|
54
|
+
|
|
55
|
+
__version__ = "1.1.0"
|
|
56
|
+
__all__ = ["Synapse", "SynapseError", "WEBHOOK_EVENTS"]
|
synapse_memory/chat.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Chat API — async chat between LLM and human."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ChatApi:
|
|
9
|
+
"""Chat endpoints."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, synapse):
|
|
12
|
+
self._synapse = synapse
|
|
13
|
+
|
|
14
|
+
def poll(self, since_id: Optional[int] = None) -> dict:
|
|
15
|
+
"""Poll for new messages from the human."""
|
|
16
|
+
query = f"?since={since_id}" if since_id else ""
|
|
17
|
+
return self._synapse.request("GET", f"/chat/poll{query}")
|
|
18
|
+
|
|
19
|
+
def reply(self, content: str, reply_to_id: Optional[int] = None) -> dict:
|
|
20
|
+
"""Reply to the human (agent sends a message)."""
|
|
21
|
+
body = {"content": content}
|
|
22
|
+
if reply_to_id:
|
|
23
|
+
body["reply_to_id"] = reply_to_id
|
|
24
|
+
return self._synapse.request("POST", "/chat/reply", body=body)
|
|
25
|
+
|
|
26
|
+
def status(self) -> dict:
|
|
27
|
+
"""Check agent online status."""
|
|
28
|
+
return self._synapse.request("GET", "/chat/status")
|
|
29
|
+
|
|
30
|
+
def history(self, limit: int = 50) -> dict:
|
|
31
|
+
"""Get full chat history (JWT-only)."""
|
|
32
|
+
if not self._synapse.jwt:
|
|
33
|
+
raise ValueError("chat.history() requires JWT. Pass jwt to Synapse constructor.")
|
|
34
|
+
return self._synapse.request(
|
|
35
|
+
"GET", f"/chat/history?limit={limit}", token=self._synapse.jwt
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def unread(self, role: str = "agent") -> dict:
|
|
39
|
+
"""Get unread count (JWT-only)."""
|
|
40
|
+
if not self._synapse.jwt:
|
|
41
|
+
raise ValueError("chat.unread() requires JWT.")
|
|
42
|
+
return self._synapse.request(
|
|
43
|
+
"GET", f"/chat/unread?role={role}", token=self._synapse.jwt
|
|
44
|
+
)
|
synapse_memory/error.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""SynapseError — typed error for Synapse API failures."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SynapseError(Exception):
|
|
7
|
+
"""Raised when a Synapse API request fails."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, status: int, body: str, endpoint: str):
|
|
10
|
+
self.status = status
|
|
11
|
+
self.body = body
|
|
12
|
+
self.endpoint = endpoint
|
|
13
|
+
super().__init__(f"Synapse {status} on {endpoint}: {body[:200]}")
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def is_network_error(self) -> bool:
|
|
17
|
+
"""True if the error is a network error (status 0)."""
|
|
18
|
+
return self.status == 0
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def is_auth_error(self) -> bool:
|
|
22
|
+
"""True if the error is an auth error (401 or 403)."""
|
|
23
|
+
return self.status in (401, 403)
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def is_rate_limited(self) -> bool:
|
|
27
|
+
"""True if the error is a rate limit (429)."""
|
|
28
|
+
return self.status == 429
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def is_retryable(self) -> bool:
|
|
32
|
+
"""True if the error is retryable (5xx or network)."""
|
|
33
|
+
return self.status >= 500 or self.status == 0 or self.status == 429
|
synapse_memory/memory.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Memory API — store, recall, search, update, delete memories."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
from urllib.parse import urlencode
|
|
7
|
+
|
|
8
|
+
from .types import StoreMemoryParams
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MemoryApi:
|
|
12
|
+
"""Memory endpoints."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, synapse):
|
|
15
|
+
self._synapse = synapse
|
|
16
|
+
|
|
17
|
+
def recall(self) -> str:
|
|
18
|
+
"""Recall all memories as LLM-formatted text."""
|
|
19
|
+
return self._synapse.request("GET", "/memory/recall", raw=True)
|
|
20
|
+
|
|
21
|
+
def list(
|
|
22
|
+
self,
|
|
23
|
+
category: Optional[str] = None,
|
|
24
|
+
tag: Optional[str] = None,
|
|
25
|
+
limit: int = 100,
|
|
26
|
+
offset: int = 0,
|
|
27
|
+
) -> dict:
|
|
28
|
+
"""List memories with optional filters."""
|
|
29
|
+
params = {"limit": limit, "offset": offset}
|
|
30
|
+
if category:
|
|
31
|
+
params["category"] = category
|
|
32
|
+
if tag:
|
|
33
|
+
params["tag"] = tag
|
|
34
|
+
return self._synapse.request("GET", f"/memory?{urlencode(params)}")
|
|
35
|
+
|
|
36
|
+
def store(self, **params) -> dict:
|
|
37
|
+
"""Store a new memory or upsert by key.
|
|
38
|
+
|
|
39
|
+
Keyword Args:
|
|
40
|
+
category: One of identity, preference, fact, project, skill, mistake, context, note
|
|
41
|
+
content: Memory content (1-50000 chars)
|
|
42
|
+
key: Optional key for upsert
|
|
43
|
+
tags: List of tags (max 20)
|
|
44
|
+
priority: critical, high, normal, low
|
|
45
|
+
source: user, agent, tool, system
|
|
46
|
+
confidence: 0.0-1.0
|
|
47
|
+
expires_at: Epoch timestamp or None
|
|
48
|
+
idempotency_key: UUID for safe retries
|
|
49
|
+
"""
|
|
50
|
+
idempotency_key = params.pop("idempotency_key", None)
|
|
51
|
+
headers = {"Idempotency-Key": idempotency_key} if idempotency_key else None
|
|
52
|
+
return self._synapse.request("POST", "/memory", body=params, headers=headers)
|
|
53
|
+
|
|
54
|
+
def search(self, q: str, limit: int = 20) -> dict:
|
|
55
|
+
"""Full-text search (FTS5)."""
|
|
56
|
+
return self._synapse.request("GET", f"/memory/search?q={q}&limit={limit}")
|
|
57
|
+
|
|
58
|
+
def semantic_search(self, q: str, limit: int = 10, threshold: float = 0.3) -> dict:
|
|
59
|
+
"""Semantic search using embeddings (requires Ollama)."""
|
|
60
|
+
return self._synapse.request(
|
|
61
|
+
"GET", f"/memory/semantic-search?q={q}&limit={limit}&threshold={threshold}"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def update(self, memory_id: str, **params) -> dict:
|
|
65
|
+
"""Update a memory by ID."""
|
|
66
|
+
return self._synapse.request("PUT", f"/memory/{memory_id}", body=params)
|
|
67
|
+
|
|
68
|
+
def delete(self, memory_id: str) -> dict:
|
|
69
|
+
"""Delete a memory by ID."""
|
|
70
|
+
return self._synapse.request("DELETE", f"/memory/{memory_id}")
|
|
71
|
+
|
|
72
|
+
def stats(self) -> dict:
|
|
73
|
+
"""Get memory statistics."""
|
|
74
|
+
return self._synapse.request("GET", "/memory/stats")
|
|
75
|
+
|
|
76
|
+
def unverified(self, limit: int = 100) -> dict:
|
|
77
|
+
"""List unverified memories."""
|
|
78
|
+
return self._synapse.request("GET", f"/memory/unverified?limit={limit}")
|
|
79
|
+
|
|
80
|
+
def contradictions(self, within_seconds: int = 86400) -> dict:
|
|
81
|
+
"""Detect potential contradictions."""
|
|
82
|
+
return self._synapse.request("GET", f"/memory/contradictions?within={within_seconds}")
|
|
83
|
+
|
|
84
|
+
def audit(self, limit: int = 100, action: Optional[str] = None) -> dict:
|
|
85
|
+
"""Get audit log."""
|
|
86
|
+
params = {"limit": limit}
|
|
87
|
+
if action:
|
|
88
|
+
params["action"] = action
|
|
89
|
+
return self._synapse.request("GET", f"/memory/audit?{urlencode(params)}")
|
|
90
|
+
|
|
91
|
+
def related(self, memory_id: str) -> dict:
|
|
92
|
+
"""Find related memories by shared tags."""
|
|
93
|
+
return self._synapse.request("GET", f"/memory/{memory_id}/related")
|
|
94
|
+
|
|
95
|
+
def by_tag(self, tag: str) -> dict:
|
|
96
|
+
"""Filter memories by tag."""
|
|
97
|
+
return self._synapse.request("GET", f"/memory/by-tag?tag={tag}")
|
|
98
|
+
|
|
99
|
+
def diff(self, since: int) -> dict:
|
|
100
|
+
"""Incremental sync: get changes since timestamp."""
|
|
101
|
+
return self._synapse.request("GET", f"/memory/diff?since={since}")
|
|
102
|
+
|
|
103
|
+
def expiring(self, within_seconds: int = 7 * 86400) -> dict:
|
|
104
|
+
"""List memories expiring soon."""
|
|
105
|
+
return self._synapse.request("GET", f"/memory/expiring?within={within_seconds}")
|
|
106
|
+
|
|
107
|
+
def health(self) -> dict:
|
|
108
|
+
"""Mind health check."""
|
|
109
|
+
return self._synapse.request("GET", "/memory/health")
|
|
110
|
+
|
|
111
|
+
def sync(self, memories: list) -> dict:
|
|
112
|
+
"""Bulk sync memories."""
|
|
113
|
+
return self._synapse.request("POST", "/memory/sync", body={"memories": memories})
|
|
114
|
+
|
|
115
|
+
def export(self, format: str = "json") -> Any:
|
|
116
|
+
"""Export entire mind as JSON or CSV."""
|
|
117
|
+
return self._synapse.request("GET", f"/mind/export?format={format}")
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Scheduler API — cron jobs + persistent variables."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SchedulerApi:
|
|
9
|
+
"""Scheduler endpoints (cron + variables)."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, synapse):
|
|
12
|
+
self._synapse = synapse
|
|
13
|
+
|
|
14
|
+
# === Cron Jobs ===
|
|
15
|
+
|
|
16
|
+
def list_crons(self) -> dict:
|
|
17
|
+
"""List all cron jobs."""
|
|
18
|
+
return self._synapse.request("GET", "/cron")
|
|
19
|
+
|
|
20
|
+
def create_cron(
|
|
21
|
+
self,
|
|
22
|
+
name: str,
|
|
23
|
+
schedule: str,
|
|
24
|
+
endpoint: str,
|
|
25
|
+
method: str = "GET",
|
|
26
|
+
headers: Optional[dict] = None,
|
|
27
|
+
body: Optional[str] = None,
|
|
28
|
+
) -> dict:
|
|
29
|
+
"""Create a cron job."""
|
|
30
|
+
payload = {
|
|
31
|
+
"name": name,
|
|
32
|
+
"schedule": schedule,
|
|
33
|
+
"endpoint": endpoint,
|
|
34
|
+
"method": method,
|
|
35
|
+
"headers": headers or {},
|
|
36
|
+
}
|
|
37
|
+
if body:
|
|
38
|
+
payload["body"] = body
|
|
39
|
+
return self._synapse.request("POST", "/cron", body=payload)
|
|
40
|
+
|
|
41
|
+
def delete_cron(self, cron_id: str) -> dict:
|
|
42
|
+
"""Delete a cron job."""
|
|
43
|
+
return self._synapse.request("DELETE", f"/cron/{cron_id}")
|
|
44
|
+
|
|
45
|
+
def toggle_cron(self, cron_id: str) -> dict:
|
|
46
|
+
"""Toggle a cron job (enable/disable)."""
|
|
47
|
+
return self._synapse.request("POST", f"/cron/{cron_id}/toggle")
|
|
48
|
+
|
|
49
|
+
# === Variables ===
|
|
50
|
+
|
|
51
|
+
def list_vars(self) -> dict:
|
|
52
|
+
"""List all variables."""
|
|
53
|
+
return self._synapse.request("GET", "/var")
|
|
54
|
+
|
|
55
|
+
def get_var(self, key: str) -> dict:
|
|
56
|
+
"""Get a variable."""
|
|
57
|
+
return self._synapse.request("GET", f"/var/{key}")
|
|
58
|
+
|
|
59
|
+
def set_var(self, key: str, value: Optional[str] = None) -> dict:
|
|
60
|
+
"""Set a variable."""
|
|
61
|
+
return self._synapse.request("POST", "/var", body={"key": key, "value": value})
|
|
62
|
+
|
|
63
|
+
def delete_var(self, key: str) -> dict:
|
|
64
|
+
"""Delete a variable."""
|
|
65
|
+
return self._synapse.request("DELETE", f"/var/{key}")
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Main Synapse client — provides typed access to all Synapse API endpoints."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json as _json
|
|
6
|
+
import time
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
from urllib.parse import urlencode
|
|
9
|
+
|
|
10
|
+
import requests
|
|
11
|
+
|
|
12
|
+
from .error import SynapseError
|
|
13
|
+
from .memory import MemoryApi
|
|
14
|
+
from .chat import ChatApi
|
|
15
|
+
from .scheduler import SchedulerApi
|
|
16
|
+
from .webhooks import WebhooksApi
|
|
17
|
+
from .visualization import VisualizationApi, CompactApi, SharingApi
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Synapse:
|
|
21
|
+
"""Synapse Memory API client.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
base_url: Synapse API base URL (e.g. "https://synapse.schaefer.zone")
|
|
25
|
+
mind_key: Mind Key for authentication
|
|
26
|
+
jwt: Optional JWT for human-only endpoints
|
|
27
|
+
timeout: Request timeout in seconds (default 30)
|
|
28
|
+
max_retries: Number of retries on 5xx/429 (default 2)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
base_url: str,
|
|
34
|
+
mind_key: str,
|
|
35
|
+
jwt: Optional[str] = None,
|
|
36
|
+
timeout: int = 30,
|
|
37
|
+
max_retries: int = 2,
|
|
38
|
+
):
|
|
39
|
+
self.base_url = base_url.rstrip("/")
|
|
40
|
+
self.mind_key = mind_key
|
|
41
|
+
self.jwt = jwt
|
|
42
|
+
self.timeout = timeout
|
|
43
|
+
self.max_retries = max_retries
|
|
44
|
+
|
|
45
|
+
self.memory = MemoryApi(self)
|
|
46
|
+
self.chat = ChatApi(self)
|
|
47
|
+
self.scheduler = SchedulerApi(self)
|
|
48
|
+
self.webhooks = WebhooksApi(self)
|
|
49
|
+
self.visualization = VisualizationApi(self)
|
|
50
|
+
self.compact = CompactApi(self)
|
|
51
|
+
self.sharing = SharingApi(self)
|
|
52
|
+
|
|
53
|
+
def request(
|
|
54
|
+
self,
|
|
55
|
+
method: str,
|
|
56
|
+
path: str,
|
|
57
|
+
*,
|
|
58
|
+
body: Any = None,
|
|
59
|
+
token: Optional[str] = None,
|
|
60
|
+
headers: Optional[dict] = None,
|
|
61
|
+
raw: bool = False,
|
|
62
|
+
) -> Any:
|
|
63
|
+
"""Make an HTTP request to Synapse. Returns parsed JSON or raw text."""
|
|
64
|
+
token = token or self.mind_key
|
|
65
|
+
url = f"{self.base_url}{path}"
|
|
66
|
+
|
|
67
|
+
hdrs = {"Authorization": f"Bearer {token}"}
|
|
68
|
+
if headers:
|
|
69
|
+
hdrs.update(headers)
|
|
70
|
+
if body is not None:
|
|
71
|
+
hdrs["Content-Type"] = "application/json"
|
|
72
|
+
|
|
73
|
+
last_error: Optional[Exception] = None
|
|
74
|
+
for attempt in range(self.max_retries + 1):
|
|
75
|
+
try:
|
|
76
|
+
response = requests.request(
|
|
77
|
+
method=method,
|
|
78
|
+
url=url,
|
|
79
|
+
headers=hdrs,
|
|
80
|
+
json=body if body is not None else None,
|
|
81
|
+
timeout=self.timeout,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Retry on 5xx and 429
|
|
85
|
+
if (response.status_code >= 500 or response.status_code == 429) and attempt < self.max_retries:
|
|
86
|
+
time.sleep(0.5 * (attempt + 1))
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
if not response.ok:
|
|
90
|
+
raise SynapseError(response.status_code, response.text, path)
|
|
91
|
+
|
|
92
|
+
if raw:
|
|
93
|
+
return response.text
|
|
94
|
+
if not response.text:
|
|
95
|
+
return None
|
|
96
|
+
try:
|
|
97
|
+
return response.json()
|
|
98
|
+
except ValueError:
|
|
99
|
+
return response.text
|
|
100
|
+
|
|
101
|
+
except requests.RequestException as e:
|
|
102
|
+
last_error = e
|
|
103
|
+
if attempt < self.max_retries:
|
|
104
|
+
time.sleep(0.5 * (attempt + 1))
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
raise SynapseError(0, f"Network error: {last_error}", path)
|
synapse_memory/types.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Type definitions for Synapse SDK."""
|
|
2
|
+
|
|
3
|
+
from typing import Literal, Optional, TypedDict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
Category = Literal["identity", "preference", "fact", "project", "skill", "mistake", "context", "note"]
|
|
7
|
+
Priority = Literal["critical", "high", "normal", "low"]
|
|
8
|
+
Source = Literal["user", "agent", "tool", "system"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SynapseOptions(TypedDict):
|
|
12
|
+
base_url: str
|
|
13
|
+
mind_key: str
|
|
14
|
+
jwt: Optional[str]
|
|
15
|
+
timeout: int
|
|
16
|
+
max_retries: int
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class StoreMemoryParams(TypedDict, total=False):
|
|
20
|
+
category: Category
|
|
21
|
+
content: str
|
|
22
|
+
key: str
|
|
23
|
+
tags: list[str]
|
|
24
|
+
priority: Priority
|
|
25
|
+
source: Source
|
|
26
|
+
confidence: float
|
|
27
|
+
expires_at: Optional[int]
|
|
28
|
+
idempotency_key: str
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Visualization API — graph data, tag frequency, and timeline for memory analysis.
|
|
3
|
+
|
|
4
|
+
Provides data suitable for D3.js, Cytoscape.js, or similar visualization
|
|
5
|
+
libraries.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
# Get graph data (nodes=memories, edges=shared tags)
|
|
9
|
+
graph = synapse.visualization.graph(max_nodes=200)
|
|
10
|
+
|
|
11
|
+
# Get tag frequency
|
|
12
|
+
tags = synapse.visualization.tags()
|
|
13
|
+
|
|
14
|
+
# Get timeline (memories grouped by day)
|
|
15
|
+
timeline = synapse.visualization.timeline(period="day")
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import Optional
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class VisualizationApi:
|
|
24
|
+
"""Memory visualization endpoints (v1.4.0+)."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, synapse):
|
|
27
|
+
self._synapse = synapse
|
|
28
|
+
|
|
29
|
+
def graph(
|
|
30
|
+
self,
|
|
31
|
+
max_nodes: int = 200,
|
|
32
|
+
category: Optional[str] = None,
|
|
33
|
+
tag: Optional[str] = None,
|
|
34
|
+
min_confidence: float = 0,
|
|
35
|
+
include_edges: bool = True,
|
|
36
|
+
min_shared_tags: int = 1,
|
|
37
|
+
) -> dict:
|
|
38
|
+
"""Get graph data for memory visualization.
|
|
39
|
+
|
|
40
|
+
Nodes = memories, edges = shared tags between memories.
|
|
41
|
+
Compatible with D3.js force-directed graph format.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
max_nodes: Maximum number of nodes (10-1000, default 200)
|
|
45
|
+
category: Filter by category
|
|
46
|
+
tag: Filter by tag
|
|
47
|
+
min_confidence: Minimum confidence threshold (0-1)
|
|
48
|
+
include_edges: Include edges (shared tag connections)
|
|
49
|
+
min_shared_tags: Minimum shared tags for an edge (1-10)
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Dict with nodes, edges, and stats (density, connected_components, etc.)
|
|
53
|
+
"""
|
|
54
|
+
params = {
|
|
55
|
+
"max_nodes": max_nodes,
|
|
56
|
+
"min_confidence": min_confidence,
|
|
57
|
+
"include_edges": str(include_edges).lower(),
|
|
58
|
+
"min_shared_tags": min_shared_tags,
|
|
59
|
+
}
|
|
60
|
+
if category:
|
|
61
|
+
params["category"] = category
|
|
62
|
+
if tag:
|
|
63
|
+
params["tag"] = tag
|
|
64
|
+
from urllib.parse import urlencode
|
|
65
|
+
return self._synapse.request("GET", f"/memory/graph?{urlencode(params)}")
|
|
66
|
+
|
|
67
|
+
def tags(self) -> dict:
|
|
68
|
+
"""Get all tags with frequency counts.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Dict with 'tags' list (sorted by count descending) and 'total_memories'
|
|
72
|
+
"""
|
|
73
|
+
return self._synapse.request("GET", "/memory/tags")
|
|
74
|
+
|
|
75
|
+
def timeline(self, period: str = "day") -> dict:
|
|
76
|
+
"""Get memories grouped by time period.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
period: Grouping period — 'hour', 'day', 'week', 'month', 'year'
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Dict with 'timeline' list (period, count, categories) and 'total_periods'
|
|
83
|
+
"""
|
|
84
|
+
return self._synapse.request("GET", f"/memory/timeline?period={period}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class CompactApi:
|
|
88
|
+
"""Memory compaction endpoints (v1.4.0+)."""
|
|
89
|
+
|
|
90
|
+
def __init__(self, synapse):
|
|
91
|
+
self._synapse = synapse
|
|
92
|
+
|
|
93
|
+
def compact(
|
|
94
|
+
self,
|
|
95
|
+
older_than: int = 2592000,
|
|
96
|
+
category: Optional[str] = None,
|
|
97
|
+
dry_run: bool = False,
|
|
98
|
+
max_clusters: int = 10,
|
|
99
|
+
min_cluster_size: int = 3,
|
|
100
|
+
) -> dict:
|
|
101
|
+
"""Auto-summarize similar memories to reduce clutter.
|
|
102
|
+
|
|
103
|
+
Clusters old unverified agent memories by tag overlap, creates a summary
|
|
104
|
+
memory (source=system, confidence=0.7), and soft-deletes originals via
|
|
105
|
+
expires_at. Verified memories are NEVER compacted.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
older_than: Only compact memories older than N seconds (default 30 days)
|
|
109
|
+
category: Only compact memories in this category
|
|
110
|
+
dry_run: Preview without making changes (default False)
|
|
111
|
+
max_clusters: Maximum clusters to create (1-50, default 10)
|
|
112
|
+
min_cluster_size: Minimum memories per cluster (2-20, default 3)
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Dict with compaction results (clusters_processed, memories_compacted, results)
|
|
116
|
+
"""
|
|
117
|
+
body = {
|
|
118
|
+
"older_than": older_than,
|
|
119
|
+
"dry_run": dry_run,
|
|
120
|
+
"max_clusters": max_clusters,
|
|
121
|
+
"min_cluster_size": min_cluster_size,
|
|
122
|
+
}
|
|
123
|
+
if category:
|
|
124
|
+
body["category"] = category
|
|
125
|
+
return self._synapse.request("POST", "/memory/compact", body=body)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class SharingApi:
|
|
129
|
+
"""Mind sharing endpoints (v1.4.0+, JWT required)."""
|
|
130
|
+
|
|
131
|
+
def __init__(self, synapse):
|
|
132
|
+
self._synapse = synapse
|
|
133
|
+
|
|
134
|
+
def share(self, mind_id: str, user_email: str, acl: str = "read") -> dict:
|
|
135
|
+
"""Share a mind with another user by email.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
mind_id: The mind ID to share
|
|
139
|
+
user_email: Email of the user to share with
|
|
140
|
+
acl: Access level — 'read', 'write', or 'admin'
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Dict with share details
|
|
144
|
+
"""
|
|
145
|
+
if not self._synapse.jwt:
|
|
146
|
+
raise ValueError("sharing.share() requires JWT. Pass jwt to Synapse constructor.")
|
|
147
|
+
return self._synapse.request(
|
|
148
|
+
"POST",
|
|
149
|
+
f"/minds/{mind_id}/share",
|
|
150
|
+
body={"user_email": user_email, "acl": acl},
|
|
151
|
+
token=self._synapse.jwt,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def list_shares(self, mind_id: str) -> dict:
|
|
155
|
+
"""List all shares for a mind."""
|
|
156
|
+
if not self._synapse.jwt:
|
|
157
|
+
raise ValueError("sharing.list_shares() requires JWT.")
|
|
158
|
+
return self._synapse.request(
|
|
159
|
+
"GET", f"/minds/{mind_id}/shares", token=self._synapse.jwt
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def update_share(self, mind_id: str, share_id: str, acl: str) -> dict:
|
|
163
|
+
"""Update a share's ACL."""
|
|
164
|
+
if not self._synapse.jwt:
|
|
165
|
+
raise ValueError("sharing.update_share() requires JWT.")
|
|
166
|
+
return self._synapse.request(
|
|
167
|
+
"PUT",
|
|
168
|
+
f"/minds/{mind_id}/shares/{share_id}",
|
|
169
|
+
body={"acl": acl},
|
|
170
|
+
token=self._synapse.jwt,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def revoke_share(self, mind_id: str, share_id: str) -> dict:
|
|
174
|
+
"""Revoke a share."""
|
|
175
|
+
if not self._synapse.jwt:
|
|
176
|
+
raise ValueError("sharing.revoke_share() requires JWT.")
|
|
177
|
+
return self._synapse.request(
|
|
178
|
+
"DELETE",
|
|
179
|
+
f"/minds/{mind_id}/shares/{share_id}",
|
|
180
|
+
token=self._synapse.jwt,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def shared_with_me(self) -> dict:
|
|
184
|
+
"""List all minds shared WITH the current user."""
|
|
185
|
+
if not self._synapse.jwt:
|
|
186
|
+
raise ValueError("sharing.shared_with_me() requires JWT.")
|
|
187
|
+
return self._synapse.request("GET", "/minds/shared", token=self._synapse.jwt)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Webhooks API — register and manage HTTP webhooks for event notifications.
|
|
3
|
+
|
|
4
|
+
Webhooks fire on memory/chat/task changes. Fire-and-forget: failures don't
|
|
5
|
+
block the original operation. Auto-disabled on HTTP 410 Gone.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
# Register a webhook
|
|
9
|
+
result = synapse.webhooks.register(
|
|
10
|
+
url="https://my-app.com/webhook",
|
|
11
|
+
events="memory.*",
|
|
12
|
+
secret="my-hmac-secret",
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# List webhooks
|
|
16
|
+
hooks = synapse.webhooks.list()
|
|
17
|
+
|
|
18
|
+
# Send a test event
|
|
19
|
+
synapse.webhooks.test(hook_id)
|
|
20
|
+
|
|
21
|
+
# Delete
|
|
22
|
+
synapse.webhooks.delete(hook_id)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from typing import Optional
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class WebhooksApi:
|
|
31
|
+
"""Webhook endpoints (v1.4.0+)."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, synapse):
|
|
34
|
+
self._synapse = synapse
|
|
35
|
+
|
|
36
|
+
def register(
|
|
37
|
+
self,
|
|
38
|
+
url: str,
|
|
39
|
+
events: str = "*",
|
|
40
|
+
secret: Optional[str] = None,
|
|
41
|
+
) -> dict:
|
|
42
|
+
"""Register a new webhook.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
url: HTTPS URL that Synapse will POST events to
|
|
46
|
+
events: Event pattern (e.g. "memory.*", "chat.message_received", "*")
|
|
47
|
+
secret: Optional HMAC-SHA256 signing secret (min 8 chars)
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Webhook registration response with id
|
|
51
|
+
"""
|
|
52
|
+
body = {"url": url, "events": events}
|
|
53
|
+
if secret:
|
|
54
|
+
body["secret"] = secret
|
|
55
|
+
return self._synapse.request("POST", "/webhooks", body=body)
|
|
56
|
+
|
|
57
|
+
def list(self) -> dict:
|
|
58
|
+
"""List all webhooks for the current mind."""
|
|
59
|
+
return self._synapse.request("GET", "/webhooks")
|
|
60
|
+
|
|
61
|
+
def get(self, webhook_id: str) -> dict:
|
|
62
|
+
"""Get a single webhook by ID."""
|
|
63
|
+
return self._synapse.request("GET", f"/webhooks/{webhook_id}")
|
|
64
|
+
|
|
65
|
+
def update(
|
|
66
|
+
self,
|
|
67
|
+
webhook_id: str,
|
|
68
|
+
url: Optional[str] = None,
|
|
69
|
+
events: Optional[str] = None,
|
|
70
|
+
secret: Optional[str] = None,
|
|
71
|
+
enabled: Optional[bool] = None,
|
|
72
|
+
) -> dict:
|
|
73
|
+
"""Update a webhook. All fields optional."""
|
|
74
|
+
body: dict = {}
|
|
75
|
+
if url is not None:
|
|
76
|
+
body["url"] = url
|
|
77
|
+
if events is not None:
|
|
78
|
+
body["events"] = events
|
|
79
|
+
if secret is not None:
|
|
80
|
+
body["secret"] = secret
|
|
81
|
+
if enabled is not None:
|
|
82
|
+
body["enabled"] = enabled
|
|
83
|
+
return self._synapse.request("PUT", f"/webhooks/{webhook_id}", body=body)
|
|
84
|
+
|
|
85
|
+
def delete(self, webhook_id: str) -> dict:
|
|
86
|
+
"""Delete a webhook."""
|
|
87
|
+
return self._synapse.request("DELETE", f"/webhooks/{webhook_id}")
|
|
88
|
+
|
|
89
|
+
def test(self, webhook_id: str) -> dict:
|
|
90
|
+
"""Send a test event to the webhook."""
|
|
91
|
+
return self._synapse.request("POST", f"/webhooks/{webhook_id}/test")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# Available webhook events
|
|
95
|
+
WEBHOOK_EVENTS = [
|
|
96
|
+
"memory.created",
|
|
97
|
+
"memory.updated",
|
|
98
|
+
"memory.deleted",
|
|
99
|
+
"memory.verified",
|
|
100
|
+
"memory.unverified",
|
|
101
|
+
"task.created",
|
|
102
|
+
"task.updated",
|
|
103
|
+
"task.completed",
|
|
104
|
+
"chat.message_received",
|
|
105
|
+
"chat.message_sent",
|
|
106
|
+
]
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: synapse-memory-sdk
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Python SDK for Synapse Memory API — persistent memory for AI agents
|
|
5
|
+
Author-email: Michael Schäfer <michael@schaefer.zone>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://gitlab.com/schaefer-services/synapse-sdk-py
|
|
8
|
+
Project-URL: Repository, https://gitlab.com/schaefer-services/synapse-sdk-py.git
|
|
9
|
+
Project-URL: Issues, https://gitlab.com/schaefer-services/synapse-sdk-py/-/issues
|
|
10
|
+
Keywords: synapse,memory,llm,ai,agent,persistent,claude,cursor,mcp
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: requests>=2.28.0
|
|
24
|
+
Provides-Extra: async
|
|
25
|
+
Requires-Dist: aiohttp>=3.9.0; extra == "async"
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
29
|
+
Requires-Dist: aiohttp>=3.9.0; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# synapse-memory — Python SDK for Synapse
|
|
33
|
+
|
|
34
|
+
[](https://pypi.org/project/synapse-memory/)
|
|
35
|
+
[](https://opensource.org/licenses/MIT)
|
|
36
|
+
|
|
37
|
+
Python SDK for the [Synapse Memory API](https://gitlab.com/schaefer-services/synapse) — persistent memory for AI agents.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install synapse-memory-sdk
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from synapse_memory import Synapse
|
|
49
|
+
|
|
50
|
+
synapse = Synapse(
|
|
51
|
+
base_url="https://synapse.schaefer.zone",
|
|
52
|
+
mind_key="your-mind-key-uuid",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Recall all memories (LLM-formatted text)
|
|
56
|
+
text = synapse.memory.recall()
|
|
57
|
+
print(text)
|
|
58
|
+
|
|
59
|
+
# Store a new memory
|
|
60
|
+
synapse.memory.store(
|
|
61
|
+
category="fact",
|
|
62
|
+
content="User is Michael Schäfer",
|
|
63
|
+
key="user_name",
|
|
64
|
+
priority="critical",
|
|
65
|
+
source="user",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Search memories
|
|
69
|
+
results = synapse.memory.search("insolvenz")
|
|
70
|
+
|
|
71
|
+
# Chat with the human
|
|
72
|
+
msgs = synapse.chat.poll()
|
|
73
|
+
if msgs["messages"]:
|
|
74
|
+
print(msgs["messages"][0]["content"])
|
|
75
|
+
synapse.chat.reply("Got it!")
|
|
76
|
+
|
|
77
|
+
# Persistent variables
|
|
78
|
+
synapse.scheduler.set_var("last_run", str(int(time.time())))
|
|
79
|
+
last_run = synapse.scheduler.get_var("last_run")
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## API Reference
|
|
83
|
+
|
|
84
|
+
### `synapse.memory`
|
|
85
|
+
- `recall()` — Get all memories as LLM-formatted text
|
|
86
|
+
- `list(category?, tag?, limit?, offset?)` — List with filters
|
|
87
|
+
- `store(**params)` — Store/upsert (category, content, key?, tags?, priority?, source?, confidence?)
|
|
88
|
+
- `search(q, limit?)` — Full-text search (FTS5)
|
|
89
|
+
- `semantic_search(q, limit?, threshold?)` — Semantic search
|
|
90
|
+
- `update(memory_id, **params)` — Update by ID
|
|
91
|
+
- `delete(memory_id)` — Delete by ID
|
|
92
|
+
- `stats()` — Statistics
|
|
93
|
+
- `unverified(limit?)` — Unverified memories
|
|
94
|
+
- `contradictions(within_seconds?)` — Detect contradictions
|
|
95
|
+
- `audit(limit?, action?)` — Audit log
|
|
96
|
+
- `related(memory_id)` — Related by shared tags
|
|
97
|
+
- `by_tag(tag)` — Filter by tag
|
|
98
|
+
- `diff(since)` — Incremental sync
|
|
99
|
+
- `expiring(within_seconds?)` — Soon-to-expire
|
|
100
|
+
- `health()` — Mind health check
|
|
101
|
+
- `sync(memories)` — Bulk sync
|
|
102
|
+
- `export(format?)` — Export mind
|
|
103
|
+
|
|
104
|
+
### `synapse.chat`
|
|
105
|
+
- `poll(since_id?)` — Poll for new messages
|
|
106
|
+
- `reply(content, reply_to_id?)` — Send a reply
|
|
107
|
+
- `status()` — Online status
|
|
108
|
+
- `history(limit?)` — Full history (JWT-only)
|
|
109
|
+
- `unread(role?)` — Unread count (JWT-only)
|
|
110
|
+
|
|
111
|
+
### `synapse.scheduler`
|
|
112
|
+
- `list_crons()` / `create_cron(...)` / `delete_cron(id)` / `toggle_cron(id)`
|
|
113
|
+
- `list_vars()` / `get_var(key)` / `set_var(key, value?)` / `delete_var(key)`
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
synapse_memory/__init__.py,sha256=2drd2tV1_6pJiQeTQl17TYowf-a8juqcklXZ_OXcHIs,1426
|
|
2
|
+
synapse_memory/chat.py,sha256=LSp9xUh4BO_EkxDV_Or66nNmXzmXKFRoIJq6-Lne0Qc,1566
|
|
3
|
+
synapse_memory/error.py,sha256=svb-O82ItAGEbclInTPxcXGJS_HNxxE-zRWw00WVpw4,1038
|
|
4
|
+
synapse_memory/memory.py,sha256=LbIv4uF39_wCWQdeON1npRwOJ--dmj0q0vDiL_MLCoo,4522
|
|
5
|
+
synapse_memory/scheduler.py,sha256=HSz6uI5r1HRidsMxY-tad9QosKmjGTxeNJ8HYxFQdDU,1901
|
|
6
|
+
synapse_memory/synapse.py,sha256=1zwKIkJGbRDLTYMkbpkqI_hwGEjP8BblrPcDz4YRWbk,3331
|
|
7
|
+
synapse_memory/types.py,sha256=5X0ixGl1vuk_23-nz4NC3_ZE1eK67Hq3DRqEK_FQeIs,677
|
|
8
|
+
synapse_memory/visualization.py,sha256=uqEGUl7XCTnmoHSUKgauR-ivd6m9NwitHYIMg4X6YUk,6340
|
|
9
|
+
synapse_memory/webhooks.py,sha256=niFUL2ObUYzpxk7DsigqFaAEhjJKhkIaxyAGxolFN0Q,2953
|
|
10
|
+
synapse_memory_sdk-1.1.0.dist-info/licenses/LICENSE,sha256=n4D_XqOdU7DBHCmBIIgJTpOe1jUKEyOiJoWhtu7gdsc,1092
|
|
11
|
+
synapse_memory_sdk-1.1.0.dist-info/METADATA,sha256=QAgTcZOwa8-XHEi6B2OVh7DCIgVfd8n7DP5TXIBLMrE,3920
|
|
12
|
+
synapse_memory_sdk-1.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
13
|
+
synapse_memory_sdk-1.1.0.dist-info/top_level.txt,sha256=FNZTO3mIf1OqSBTEtjS-L-flrnwHxtqISzvIBfxrS1c,15
|
|
14
|
+
synapse_memory_sdk-1.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Michael Schäfer, Schäfer Services
|
|
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 @@
|
|
|
1
|
+
synapse_memory
|