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.
@@ -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
+ )
@@ -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
@@ -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)
@@ -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
+ [![PyPI version](https://img.shields.io/pypi/v/synapse-memory.svg)](https://pypi.org/project/synapse-memory/)
35
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,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