synapse-memory-engine 1.3.0__tar.gz → 1.3.1__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.
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/PKG-INFO +29 -3
- synapse_memory_engine-1.3.1/README.md +52 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/pyproject.toml +1 -1
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/setup.py +4 -3
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/__init__.py +1 -1
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/api/fastapi_server.py +23 -12
- synapse_memory_engine-1.3.1/synapse_memory/client.py +200 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/core/embedder.py +14 -6
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/core/embedding_provider.py +10 -2
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/core/pgvector_db.py +194 -29
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory_engine.egg-info/PKG-INFO +29 -3
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory_engine.egg-info/SOURCES.txt +1 -0
- synapse_memory_engine-1.3.1/tests/test_client_retry_policy.py +116 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_db_role_provisioning.py +57 -2
- synapse_memory_engine-1.3.1/tests/test_f17_provider_dimension.py +106 -0
- synapse_memory_engine-1.3.1/tests/test_f18_pgvector_3072.py +114 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_helm_chart.py +19 -7
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_p1_contract_regressions.py +12 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_pgvector_integration.py +3 -0
- synapse_memory_engine-1.3.0/README.md +0 -26
- synapse_memory_engine-1.3.0/synapse_memory/client.py +0 -127
- synapse_memory_engine-1.3.0/tests/test_f17_provider_dimension.py +0 -43
- synapse_memory_engine-1.3.0/tests/test_f18_pgvector_3072.py +0 -69
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/setup.cfg +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/cli/synapse_cli.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/core/__init__.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/core/auth.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/core/decay_engine.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/core/deduplicator.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/core/encryption.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/core/hybrid_search.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/core/knapsack_packer.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/core/memory_manager.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/core/migrations.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/core/observability.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/core/pruner.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/core/rate_limiter.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/core/sqlite_db.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/examples/langchain_integration.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/examples/llamaindex_integration.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/integrations/__init__.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/integrations/langchain.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/integrations/llamaindex.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/main.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/py.typed +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory_engine.egg-info/dependency_links.txt +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory_engine.egg-info/entry_points.txt +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory_engine.egg-info/requires.txt +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory_engine.egg-info/top_level.txt +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_auth_multitenancy.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_backup_restore_drill.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_decay.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_deduplicator.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_deterministic_embedding.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_encryption.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_f01_auth_escalation.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_f02_f07_probes.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_f05_f06_data_at_rest.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_f10_f15_hardening.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_integration.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_knapsack.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_langchain.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_llamaindex.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_load_resilience.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_max_record_limit.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_migrations.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_p0_safety_regressions.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_pgvector.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_phase_cd.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_pruner.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_rate_limit.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_required_auth_gateway.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_retrieval_eval.py +0 -0
- {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_synapse.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: synapse-memory-engine
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.1
|
|
4
4
|
Summary: Production-grade active RAG & cognitive memory engine with multi-tenancy, pgvector HNSW indexing, semantic deduplication, decay-based pruning, and token-aware context packing.
|
|
5
5
|
Home-page: https://github.com/721189/Synapse-Memory
|
|
6
6
|
Author: Synapse Cognitive Systems
|
|
@@ -49,7 +49,7 @@ Dynamic: requires-python
|
|
|
49
49
|
|
|
50
50
|
# Synapse Memory Python SDK
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
Python distribution for the Synapse Memory engine. The distribution name is `synapse-memory-engine`; the import package is `synapse_memory`.
|
|
53
53
|
|
|
54
54
|
## Features
|
|
55
55
|
|
|
@@ -68,8 +68,34 @@ pip install synapse-memory-engine # from PyPI
|
|
|
68
68
|
pip install -e .
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
+
## REST client quick start
|
|
72
|
+
|
|
73
|
+
Use `SynapseClient` when connecting an application to a running Synapse API:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from synapse_memory import SynapseClient, SynapseError
|
|
77
|
+
|
|
78
|
+
with SynapseClient(
|
|
79
|
+
"http://127.0.0.1:8008",
|
|
80
|
+
api_key="syn_live_your_key",
|
|
81
|
+
tenant_id="your-tenant",
|
|
82
|
+
) as client:
|
|
83
|
+
created = client.create_memory("Customer selected the annual plan.", category="fact")
|
|
84
|
+
result = client.retrieve("Which plan did the customer select?")
|
|
85
|
+
print(created.id, result.fused_context)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The client automatically retries idempotent `GET`/`HEAD`/`OPTIONS` requests and honours `Retry-After`. It does not automatically replay `POST` requests (including `/query`, which updates access metadata) after an ambiguous failure; callers should reconcile the operation before manually retrying, or provide their own idempotency mechanism.
|
|
89
|
+
|
|
71
90
|
## Testing
|
|
72
91
|
|
|
92
|
+
From the repository root:
|
|
93
|
+
|
|
73
94
|
```bash
|
|
74
|
-
|
|
95
|
+
pip install -e "sdk/python[pgvector,langchain,llamaindex,dev]"
|
|
96
|
+
PYTHONPATH=sdk/python pytest sdk/python/tests -v
|
|
75
97
|
```
|
|
98
|
+
|
|
99
|
+
Database-gated tests require a disposable PostgreSQL/pgvector database and explicitly documented environment flags. Passing the offline suite alone is not production verification.
|
|
100
|
+
|
|
101
|
+
**Licence:** proprietary. Installation does not grant unrestricted usage or deployment rights; obtain written permission under [`../../LICENSE`](../../LICENSE) before using this package outside the permissions granted by the copyright holder.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Synapse Memory Python SDK
|
|
2
|
+
|
|
3
|
+
Python distribution for the Synapse Memory engine. The distribution name is `synapse-memory-engine`; the import package is `synapse_memory`.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Encrypted SQLite Storage**: Fernet AES-128-CBC with HMAC-SHA256 authenticated encryption at rest.
|
|
8
|
+
- **Dynamic Programming Knapsack Packing**: Exact 0/1 Knapsack optimizer for packing high-relevance memories into strict token envelopes.
|
|
9
|
+
- **Ebbinghaus Decay Engine**: Continuous temporal decay with positive/negative reinforcement factors.
|
|
10
|
+
- **Semantic Deduplication**: Dual lexical Jaccard and dense cosine similarity detection to prevent node fragmentation.
|
|
11
|
+
- **Hybrid Retrieval**: BM25 sparse lexical ranking fused with dense vector embeddings via Reciprocal Rank Fusion (RRF).
|
|
12
|
+
- **Framework Integrations**: True drop-in adapters for LangChain (`BaseMemory`) and LlamaIndex (`BaseMemory`).
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install synapse-memory-engine # from PyPI
|
|
18
|
+
# or, from a checkout of this repository:
|
|
19
|
+
pip install -e .
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## REST client quick start
|
|
23
|
+
|
|
24
|
+
Use `SynapseClient` when connecting an application to a running Synapse API:
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from synapse_memory import SynapseClient, SynapseError
|
|
28
|
+
|
|
29
|
+
with SynapseClient(
|
|
30
|
+
"http://127.0.0.1:8008",
|
|
31
|
+
api_key="syn_live_your_key",
|
|
32
|
+
tenant_id="your-tenant",
|
|
33
|
+
) as client:
|
|
34
|
+
created = client.create_memory("Customer selected the annual plan.", category="fact")
|
|
35
|
+
result = client.retrieve("Which plan did the customer select?")
|
|
36
|
+
print(created.id, result.fused_context)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The client automatically retries idempotent `GET`/`HEAD`/`OPTIONS` requests and honours `Retry-After`. It does not automatically replay `POST` requests (including `/query`, which updates access metadata) after an ambiguous failure; callers should reconcile the operation before manually retrying, or provide their own idempotency mechanism.
|
|
40
|
+
|
|
41
|
+
## Testing
|
|
42
|
+
|
|
43
|
+
From the repository root:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install -e "sdk/python[pgvector,langchain,llamaindex,dev]"
|
|
47
|
+
PYTHONPATH=sdk/python pytest sdk/python/tests -v
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Database-gated tests require a disposable PostgreSQL/pgvector database and explicitly documented environment flags. Passing the offline suite alone is not production verification.
|
|
51
|
+
|
|
52
|
+
**Licence:** proprietary. Installation does not grant unrestricted usage or deployment rights; obtain written permission under [`../../LICENSE`](../../LICENSE) before using this package outside the permissions granted by the copyright holder.
|
|
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
|
|
7
7
|
# owned by an unrelated third-party project. Import package stays `synapse_memory`.
|
|
8
8
|
# See docs/publishing.md.
|
|
9
9
|
name = "synapse-memory-engine"
|
|
10
|
-
version = "1.3.
|
|
10
|
+
version = "1.3.1"
|
|
11
11
|
authors = [{name = "Synapse Cognitive Systems", email = "singhshivam20009@gmail.com"}]
|
|
12
12
|
description = "Production-grade active RAG & cognitive memory engine with multi-tenancy, pgvector HNSW indexing, semantic deduplication, decay-based pruning, and token-aware context packing."
|
|
13
13
|
readme = "README.md"
|
|
@@ -2,10 +2,10 @@ from setuptools import setup, find_packages
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name="synapse-memory-engine",
|
|
5
|
-
version="1.
|
|
5
|
+
version="1.3.1",
|
|
6
6
|
description="Sovereign, active RAG & cognitive long-term memory engine for AI agents",
|
|
7
7
|
author="Synapse Cognitive Systems",
|
|
8
|
-
author_email="
|
|
8
|
+
author_email="singhshivam20009@gmail.com",
|
|
9
9
|
url="https://github.com/721189/Synapse-Memory",
|
|
10
10
|
license="All Rights Reserved",
|
|
11
11
|
packages=find_packages(include=["synapse_memory*"]),
|
|
@@ -16,6 +16,7 @@ setup(
|
|
|
16
16
|
"uvicorn>=0.34.0",
|
|
17
17
|
"cryptography>=42.0.0",
|
|
18
18
|
"requests>=2.31.0",
|
|
19
|
+
"urllib3>=2.8.0",
|
|
19
20
|
"numpy>=1.26.0",
|
|
20
21
|
],
|
|
21
22
|
extras_require={
|
|
@@ -29,13 +30,13 @@ setup(
|
|
|
29
30
|
"pgvector>=0.4.0",
|
|
30
31
|
"langchain-core>=0.3.0",
|
|
31
32
|
"llama-index-core>=0.13.0",
|
|
33
|
+
"redis>=5.0.0",
|
|
32
34
|
],
|
|
33
35
|
"dev": [
|
|
34
36
|
"flake8>=7.0.0",
|
|
35
37
|
"mypy>=1.13.0",
|
|
36
38
|
"bandit>=1.8.0",
|
|
37
39
|
"pytest>=8.0.0",
|
|
38
|
-
"types-cryptography>=42.0.0",
|
|
39
40
|
"types-requests>=2.32.0",
|
|
40
41
|
],
|
|
41
42
|
},
|
|
@@ -10,7 +10,7 @@ from synapse_memory.integrations.langchain import SynapseLangChainMemory
|
|
|
10
10
|
from synapse_memory.integrations.llamaindex import SynapseLlamaIndexMemory
|
|
11
11
|
from synapse_memory.client import SynapseClient, SynapseError, CreateResult, QueryResult
|
|
12
12
|
|
|
13
|
-
__version__ = "1.3.
|
|
13
|
+
__version__ = "1.3.1"
|
|
14
14
|
__api_compat_version__ = "v1"
|
|
15
15
|
|
|
16
16
|
__all__ = [
|
{synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/api/fastapi_server.py
RENAMED
|
@@ -6,7 +6,7 @@ import hashlib
|
|
|
6
6
|
import logging
|
|
7
7
|
import threading
|
|
8
8
|
from typing import List, Dict, Any, Optional, Tuple
|
|
9
|
-
from pydantic import BaseModel, Field
|
|
9
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
10
10
|
import uvicorn
|
|
11
11
|
from fastapi import FastAPI, HTTPException, status, Header, Request, Depends
|
|
12
12
|
from fastapi.middleware.cors import CORSMiddleware
|
|
@@ -233,7 +233,7 @@ def _rate_limit_principal(request: Request) -> str:
|
|
|
233
233
|
app = FastAPI(
|
|
234
234
|
title="SynapseMemory Enterprise Cognitive Gateway",
|
|
235
235
|
description="Sovereign Active Long-Term Memory & Context Budgeting REST API with Multi-Tenancy and Database-Level RLS.",
|
|
236
|
-
version="1.
|
|
236
|
+
version="1.3.1"
|
|
237
237
|
)
|
|
238
238
|
|
|
239
239
|
|
|
@@ -415,21 +415,32 @@ def envelope(data: Any, request: Optional[Request] = None) -> Dict[str, Any]:
|
|
|
415
415
|
|
|
416
416
|
# --- PYDANTIC SCHEMAS ---
|
|
417
417
|
|
|
418
|
-
class
|
|
418
|
+
class SynapseAPIModel(BaseModel):
|
|
419
|
+
"""Request/response models accept canonical aliases and Python field names.
|
|
420
|
+
|
|
421
|
+
Clients in Python naturally serialize fields as snake_case (tenant_id,
|
|
422
|
+
max_token_budget), while the public JSON API also documents camelCase aliases
|
|
423
|
+
(tenantId, maxTokens). Pydantic otherwise silently ignores the snake_case key
|
|
424
|
+
when an alias is configured, which can silently change a request's tenant.
|
|
425
|
+
"""
|
|
426
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
class IngestRequest(SynapseAPIModel):
|
|
419
430
|
content: str = Field(..., min_length=1, max_length=32000, description="Raw text representation of the memory segment")
|
|
420
431
|
category: str = Field("interaction", min_length=1, max_length=64, pattern=r"^[a-z0-9_\-]+$", description="Cognitive category (e.g., interaction, preference, system)")
|
|
421
432
|
confidence: float = Field(0.95, ge=0.0, le=1.0, description="Confidence alignment coefficient")
|
|
422
433
|
tenant_id: Optional[str] = Field(None, alias="tenantId", min_length=1, max_length=128, pattern=r"^[A-Za-z0-9_\-\.]+$", description="Target tenant ID for isolation")
|
|
423
434
|
|
|
424
435
|
|
|
425
|
-
class IngestResponse(
|
|
436
|
+
class IngestResponse(SynapseAPIModel):
|
|
426
437
|
id: str
|
|
427
438
|
status: str
|
|
428
439
|
token_cost: int
|
|
429
440
|
tenant_id: str
|
|
430
441
|
|
|
431
442
|
|
|
432
|
-
class QueryRequest(
|
|
443
|
+
class QueryRequest(SynapseAPIModel):
|
|
433
444
|
prompt: Optional[str] = Field(None, min_length=1, max_length=8000, description="Active user prompt")
|
|
434
445
|
query: Optional[str] = Field(None, min_length=1, max_length=8000, description="Alternative alias for prompt")
|
|
435
446
|
max_token_budget: int = Field(1500, ge=50, le=8000, description="Max token window allowed for context packing", alias="maxTokens")
|
|
@@ -437,20 +448,20 @@ class QueryRequest(BaseModel):
|
|
|
437
448
|
tenant_id: Optional[str] = Field(None, alias="tenantId", min_length=1, max_length=128, pattern=r"^[A-Za-z0-9_\-\.]+$", description="Target tenant ID")
|
|
438
449
|
|
|
439
450
|
|
|
440
|
-
class QueryResponse(
|
|
451
|
+
class QueryResponse(SynapseAPIModel):
|
|
441
452
|
fused_context: str
|
|
442
453
|
injected_nodes_count: int
|
|
443
454
|
injected_nodes: List[Dict[str, Any]]
|
|
444
455
|
active_storage_backend: str
|
|
445
456
|
|
|
446
457
|
|
|
447
|
-
class FeedbackRequest(
|
|
458
|
+
class FeedbackRequest(SynapseAPIModel):
|
|
448
459
|
memory_id: str = Field(..., min_length=1, max_length=256, description="Memory node ID")
|
|
449
460
|
feedback_type: str = Field(..., pattern=r"^(positive|negative)$", description="Must be either 'positive' (boost) or 'negative' (hallucination alert)")
|
|
450
461
|
tenant_id: Optional[str] = Field(None, alias="tenantId", min_length=1, max_length=128, pattern=r"^[A-Za-z0-9_\-\.]+$")
|
|
451
462
|
|
|
452
463
|
|
|
453
|
-
class CreateKeyRequest(
|
|
464
|
+
class CreateKeyRequest(SynapseAPIModel):
|
|
454
465
|
name: str = Field(..., min_length=1, max_length=128)
|
|
455
466
|
organization_id: str = Field("org_default", min_length=1, max_length=128, pattern=r"^[A-Za-z0-9_\-\.]+$")
|
|
456
467
|
project_id: str = Field("proj_default", min_length=1, max_length=128, pattern=r"^[A-Za-z0-9_\-\.]+$")
|
|
@@ -467,7 +478,7 @@ class CreateKeyRequest(BaseModel):
|
|
|
467
478
|
ttl_days: Optional[int] = Field(365, ge=1, le=3650)
|
|
468
479
|
|
|
469
480
|
|
|
470
|
-
class RevokeKeyRequest(
|
|
481
|
+
class RevokeKeyRequest(SynapseAPIModel):
|
|
471
482
|
key_id: str = Field(..., min_length=1, max_length=128)
|
|
472
483
|
|
|
473
484
|
|
|
@@ -1079,7 +1090,7 @@ def prune_audit_trail(
|
|
|
1079
1090
|
return envelope({"deleted": deleted, "older_than_days": days}, request)
|
|
1080
1091
|
|
|
1081
1092
|
|
|
1082
|
-
class RotateKeyRequest(
|
|
1093
|
+
class RotateKeyRequest(SynapseAPIModel):
|
|
1083
1094
|
key_id: str
|
|
1084
1095
|
|
|
1085
1096
|
|
|
@@ -1135,13 +1146,13 @@ def rotate_api_key(
|
|
|
1135
1146
|
)
|
|
1136
1147
|
|
|
1137
1148
|
|
|
1138
|
-
class BulkIngestItem(
|
|
1149
|
+
class BulkIngestItem(SynapseAPIModel):
|
|
1139
1150
|
content: str = Field(..., min_length=1, max_length=32000)
|
|
1140
1151
|
category: str = Field("interaction", min_length=1, max_length=64, pattern=r"^[a-z0-9_\-]+$")
|
|
1141
1152
|
confidence: float = Field(0.95, ge=0.0, le=1.0)
|
|
1142
1153
|
|
|
1143
1154
|
|
|
1144
|
-
class BulkIngestRequest(
|
|
1155
|
+
class BulkIngestRequest(SynapseAPIModel):
|
|
1145
1156
|
items: List[BulkIngestItem] = Field(..., min_length=1, max_length=100)
|
|
1146
1157
|
tenant_id: Optional[str] = Field(None, alias="tenantId", min_length=1, max_length=128, pattern=r"^[A-Za-z0-9_\-\.]+$")
|
|
1147
1158
|
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Canonical SynapseMemory Python client (Phase 6.1).
|
|
2
|
+
|
|
3
|
+
Small, typed surface over the REST gateway — the supported path for
|
|
4
|
+
external applications. Handles auth headers, timeouts, retries on
|
|
5
|
+
retryable (429/5xx) failures, and the canonical {success, data} envelope.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import time
|
|
10
|
+
from email.utils import parsedate_to_datetime
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Any, Dict, List, Optional
|
|
14
|
+
|
|
15
|
+
import requests
|
|
16
|
+
|
|
17
|
+
__all__ = ["SynapseClient", "SynapseError", "CreateResult", "QueryResult"]
|
|
18
|
+
|
|
19
|
+
SDK_VERSION = "1.3.1"
|
|
20
|
+
API_COMPAT_VERSION = "v1"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SynapseError(Exception):
|
|
24
|
+
def __init__(self, message: str, *, status: Optional[int] = None, retryable: bool = False):
|
|
25
|
+
super().__init__(message)
|
|
26
|
+
self.status = status
|
|
27
|
+
self.retryable = retryable
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class CreateResult:
|
|
32
|
+
id: str
|
|
33
|
+
status: str
|
|
34
|
+
token_cost: int
|
|
35
|
+
tenant_id: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class QueryResult:
|
|
40
|
+
fused_context: str
|
|
41
|
+
injected_nodes_count: int
|
|
42
|
+
injected_nodes: List[Dict[str, Any]]
|
|
43
|
+
storage_backend: str
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SynapseClient:
|
|
47
|
+
"""Synchronous client; also usable as a context manager."""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
base_url: str = "http://127.0.0.1:8008",
|
|
52
|
+
api_key: Optional[str] = None,
|
|
53
|
+
tenant_id: str = "default",
|
|
54
|
+
timeout: float = 10.0,
|
|
55
|
+
max_retries: int = 3,
|
|
56
|
+
):
|
|
57
|
+
if timeout <= 0:
|
|
58
|
+
raise ValueError("timeout must be greater than zero")
|
|
59
|
+
if max_retries < 0:
|
|
60
|
+
raise ValueError("max_retries must be zero or greater")
|
|
61
|
+
self.base_url = base_url.rstrip("/")
|
|
62
|
+
self.api_key = api_key
|
|
63
|
+
self.tenant_id = tenant_id
|
|
64
|
+
self.timeout = timeout
|
|
65
|
+
self.max_retries = max_retries
|
|
66
|
+
self._session = requests.Session()
|
|
67
|
+
|
|
68
|
+
def __enter__(self) -> "SynapseClient":
|
|
69
|
+
return self
|
|
70
|
+
|
|
71
|
+
def __exit__(self, *exc: Any) -> None:
|
|
72
|
+
self.close()
|
|
73
|
+
|
|
74
|
+
def close(self) -> None:
|
|
75
|
+
self._session.close()
|
|
76
|
+
|
|
77
|
+
def _headers(self) -> Dict[str, str]:
|
|
78
|
+
headers = {"Content-Type": "application/json", "X-Tenant-ID": self.tenant_id}
|
|
79
|
+
if self.api_key:
|
|
80
|
+
headers["X-Synapse-API-Key"] = self.api_key
|
|
81
|
+
return headers
|
|
82
|
+
|
|
83
|
+
def _request(
|
|
84
|
+
self,
|
|
85
|
+
method: str,
|
|
86
|
+
path: str,
|
|
87
|
+
payload: Optional[Dict[str, Any]] = None,
|
|
88
|
+
*,
|
|
89
|
+
retry_safe: Optional[bool] = None,
|
|
90
|
+
) -> Dict[str, Any]:
|
|
91
|
+
"""Perform a request without automatically replaying ambiguous writes.
|
|
92
|
+
|
|
93
|
+
GET/HEAD/OPTIONS are retry-safe by default. POST requests are not
|
|
94
|
+
retried unless the caller explicitly opts in with a proven idempotency
|
|
95
|
+
contract. This matters for /query too: retrieval updates access counts and
|
|
96
|
+
last-access timestamps. Mutation requests are not replayed after an
|
|
97
|
+
ambiguous timeout or 5xx response.
|
|
98
|
+
"""
|
|
99
|
+
url = f"{self.base_url}{path}"
|
|
100
|
+
method = method.upper()
|
|
101
|
+
safe = method in {"GET", "HEAD", "OPTIONS"} if retry_safe is None else retry_safe
|
|
102
|
+
max_attempts = self.max_retries + 1 if safe else 1
|
|
103
|
+
last_error: Optional[SynapseError] = None
|
|
104
|
+
|
|
105
|
+
for attempt in range(max_attempts):
|
|
106
|
+
retry_after: Optional[float] = None
|
|
107
|
+
try:
|
|
108
|
+
resp = self._session.request(
|
|
109
|
+
method, url, json=payload, headers=self._headers(), timeout=self.timeout
|
|
110
|
+
)
|
|
111
|
+
except requests.RequestException as exc:
|
|
112
|
+
if not safe:
|
|
113
|
+
raise SynapseError(
|
|
114
|
+
f"Connection failed during {method} {path}; the write outcome is unknown "
|
|
115
|
+
"and was not automatically retried.",
|
|
116
|
+
retryable=False,
|
|
117
|
+
) from exc
|
|
118
|
+
last_error = SynapseError(f"Connection failed: {exc}", retryable=True)
|
|
119
|
+
else:
|
|
120
|
+
if resp.status_code in (429, 502, 503, 504):
|
|
121
|
+
if not safe:
|
|
122
|
+
raise SynapseError(
|
|
123
|
+
f"Transient HTTP {resp.status_code} for non-replay-safe {method} {path}; "
|
|
124
|
+
"automatic retry suppressed to avoid duplicate mutations.",
|
|
125
|
+
status=resp.status_code,
|
|
126
|
+
retryable=True,
|
|
127
|
+
)
|
|
128
|
+
last_error = SynapseError(
|
|
129
|
+
f"Retryable {resp.status_code}: {resp.text[:200]}",
|
|
130
|
+
status=resp.status_code,
|
|
131
|
+
retryable=True,
|
|
132
|
+
)
|
|
133
|
+
header = resp.headers.get("Retry-After")
|
|
134
|
+
if header:
|
|
135
|
+
try:
|
|
136
|
+
retry_after = max(0.0, float(header))
|
|
137
|
+
except ValueError:
|
|
138
|
+
try:
|
|
139
|
+
retry_after = max(
|
|
140
|
+
0.0,
|
|
141
|
+
(parsedate_to_datetime(header) - datetime.now(timezone.utc)).total_seconds(),
|
|
142
|
+
)
|
|
143
|
+
except (TypeError, ValueError, OverflowError):
|
|
144
|
+
retry_after = None
|
|
145
|
+
elif not resp.ok:
|
|
146
|
+
raise SynapseError(
|
|
147
|
+
f"Request failed {resp.status_code}: {resp.text[:500]}",
|
|
148
|
+
status=resp.status_code,
|
|
149
|
+
)
|
|
150
|
+
else:
|
|
151
|
+
try:
|
|
152
|
+
body = resp.json()
|
|
153
|
+
except ValueError as exc:
|
|
154
|
+
raise SynapseError(
|
|
155
|
+
f"API returned invalid JSON for {method} {path}",
|
|
156
|
+
status=resp.status_code,
|
|
157
|
+
) from exc
|
|
158
|
+
if isinstance(body, dict) and "success" in body and "data" in body:
|
|
159
|
+
if not body["success"]:
|
|
160
|
+
raise SynapseError(
|
|
161
|
+
f"API error: {body.get('error')}", status=resp.status_code
|
|
162
|
+
)
|
|
163
|
+
return body["data"]
|
|
164
|
+
return body
|
|
165
|
+
|
|
166
|
+
if attempt < max_attempts - 1:
|
|
167
|
+
backoff = min(2.0, 0.25 * (2 ** attempt))
|
|
168
|
+
time.sleep(retry_after if retry_after is not None else backoff)
|
|
169
|
+
|
|
170
|
+
raise last_error or SynapseError("Request failed")
|
|
171
|
+
|
|
172
|
+
def create_memory(self, content: str, category: str = "interaction", confidence: float = 0.95) -> CreateResult:
|
|
173
|
+
data = self._request("POST", "/ingest", {"content": content, "category": category, "confidence": confidence})
|
|
174
|
+
return CreateResult(id=data["id"], status=data["status"], token_cost=data["token_cost"], tenant_id=data["tenant_id"])
|
|
175
|
+
|
|
176
|
+
def retrieve(self, query: str, max_tokens: int = 1500) -> QueryResult:
|
|
177
|
+
data = self._request(
|
|
178
|
+
"POST", "/query", {"prompt": query, "maxTokens": max_tokens}
|
|
179
|
+
)
|
|
180
|
+
return QueryResult(
|
|
181
|
+
fused_context=data.get("fused_context", ""),
|
|
182
|
+
injected_nodes_count=data.get("injected_nodes_count", 0),
|
|
183
|
+
injected_nodes=data.get("injected_nodes", []),
|
|
184
|
+
storage_backend=data.get("active_storage_backend", "unknown"),
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def list_memories(self, limit: int = 50, offset: int = 0) -> Dict[str, Any]:
|
|
188
|
+
return self._request("GET", f"/api/memories/list?limit={limit}&offset={offset}")
|
|
189
|
+
|
|
190
|
+
def delete_memory(self, memory_id: str) -> Dict[str, Any]:
|
|
191
|
+
return self._request("DELETE", f"/api/memories/{memory_id}")
|
|
192
|
+
|
|
193
|
+
def feedback(self, memory_id: str, feedback_type: str) -> Dict[str, Any]:
|
|
194
|
+
return self._request("POST", "/feedback", {"memory_id": memory_id, "feedback_type": feedback_type})
|
|
195
|
+
|
|
196
|
+
def health(self) -> Dict[str, Any]:
|
|
197
|
+
return self._request("GET", "/health")
|
|
198
|
+
|
|
199
|
+
def readiness(self) -> Dict[str, Any]:
|
|
200
|
+
return self._request("GET", "/readiness")
|
|
@@ -66,7 +66,8 @@ class SynapseEmbedder:
|
|
|
66
66
|
"(gemini, openai, sentence-transformers) or opt in with "
|
|
67
67
|
"SYNAPSE_ALLOW_DETERMINISTIC_EMBEDDING=true."
|
|
68
68
|
)
|
|
69
|
-
|
|
69
|
+
raw_dimension = os.getenv("SYNAPSE_EMBEDDING_DIMENSION", "").strip()
|
|
70
|
+
dim = int(raw_dimension) if raw_dimension else 128
|
|
70
71
|
return LocalEmbeddingProvider(dim=dim)
|
|
71
72
|
elif self.provider_name == "gemini":
|
|
72
73
|
if not self.gemini_key:
|
|
@@ -74,26 +75,32 @@ class SynapseEmbedder:
|
|
|
74
75
|
logger.warning(
|
|
75
76
|
"GEMINI_API_KEY not set; falling back to deterministic hash provider."
|
|
76
77
|
)
|
|
77
|
-
|
|
78
|
+
raw_dimension = os.getenv("SYNAPSE_EMBEDDING_DIMENSION", "").strip()
|
|
79
|
+
dim = int(raw_dimension) if raw_dimension else 128
|
|
78
80
|
return LocalEmbeddingProvider(dim=dim)
|
|
79
81
|
raise RuntimeError(
|
|
80
82
|
"SYNAPSE_EMBEDDING_PROVIDER=gemini but GEMINI_API_KEY is not set. "
|
|
81
83
|
"Set GEMINI_API_KEY or switch to provider=deterministic."
|
|
82
84
|
)
|
|
83
|
-
|
|
85
|
+
raw_dimension = os.getenv("SYNAPSE_EMBEDDING_DIMENSION", "").strip()
|
|
86
|
+
dimension = int(raw_dimension) if raw_dimension else 3072
|
|
87
|
+
return GeminiEmbeddingProvider(self.gemini_key, dim=dimension)
|
|
84
88
|
elif self.provider_name == "openai":
|
|
85
89
|
if not self.openai_key:
|
|
86
90
|
if allow_fallback:
|
|
87
91
|
logger.warning(
|
|
88
92
|
"OPENAI_API_KEY not set; falling back to deterministic hash provider."
|
|
89
93
|
)
|
|
90
|
-
|
|
94
|
+
raw_dimension = os.getenv("SYNAPSE_EMBEDDING_DIMENSION", "").strip()
|
|
95
|
+
dim = int(raw_dimension) if raw_dimension else 128
|
|
91
96
|
return LocalEmbeddingProvider(dim=dim)
|
|
92
97
|
raise RuntimeError(
|
|
93
98
|
"SYNAPSE_EMBEDDING_PROVIDER=openai but OPENAI_API_KEY is not set. "
|
|
94
99
|
"Set OPENAI_API_KEY or switch to provider=deterministic."
|
|
95
100
|
)
|
|
96
|
-
|
|
101
|
+
raw_dimension = os.getenv("SYNAPSE_EMBEDDING_DIMENSION", "").strip()
|
|
102
|
+
dimension = int(raw_dimension) if raw_dimension else 1536
|
|
103
|
+
return OpenAIEmbeddingProvider(self.openai_key, dim=dimension)
|
|
97
104
|
elif self.provider_name == "sentence-transformers":
|
|
98
105
|
# Explicit opt-in: requires [local-models] extra and model availability.
|
|
99
106
|
return SentenceTransformerEmbeddingProvider(
|
|
@@ -105,7 +112,8 @@ class SynapseEmbedder:
|
|
|
105
112
|
logger.warning(
|
|
106
113
|
f"Unknown embedding provider '{self.provider_name}'; falling back to deterministic hash provider."
|
|
107
114
|
)
|
|
108
|
-
|
|
115
|
+
raw_dimension = os.getenv("SYNAPSE_EMBEDDING_DIMENSION", "").strip()
|
|
116
|
+
dim = int(raw_dimension) if raw_dimension else 128
|
|
109
117
|
return LocalEmbeddingProvider(dim=dim)
|
|
110
118
|
raise RuntimeError(
|
|
111
119
|
f"Unknown embedding provider '{self.provider_name}'. "
|
|
@@ -171,7 +171,11 @@ class GeminiEmbeddingProvider(EmbeddingProvider):
|
|
|
171
171
|
f"gemini-embedding-001:embedContent?key={self.api_key}"
|
|
172
172
|
)
|
|
173
173
|
headers = {"Content-Type": "application/json"}
|
|
174
|
-
payload = {
|
|
174
|
+
payload = {
|
|
175
|
+
"content": {"parts": [{"text": text}]},
|
|
176
|
+
# Prefer EmbedContentConfig over the deprecated top-level field.
|
|
177
|
+
"embedContentConfig": {"outputDimensionality": self._dim},
|
|
178
|
+
}
|
|
175
179
|
|
|
176
180
|
req = urllib.request.Request(
|
|
177
181
|
url,
|
|
@@ -208,10 +212,14 @@ class OpenAIEmbeddingProvider(EmbeddingProvider):
|
|
|
208
212
|
"Content-Type": "application/json",
|
|
209
213
|
"Authorization": f"Bearer {self.api_key}"
|
|
210
214
|
}
|
|
211
|
-
payload = {
|
|
215
|
+
payload: dict[str, object] = {
|
|
212
216
|
"input": text,
|
|
213
217
|
"model": self.model
|
|
214
218
|
}
|
|
219
|
+
# text-embedding-3 models support an explicit output width. Older model
|
|
220
|
+
# families do not accept the dimensions field and keep their native size.
|
|
221
|
+
if self.model.startswith("text-embedding-3"):
|
|
222
|
+
payload["dimensions"] = self._dim
|
|
215
223
|
req = urllib.request.Request(
|
|
216
224
|
url,
|
|
217
225
|
data=json.dumps(payload).encode("utf-8"),
|