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.
Files changed (74) hide show
  1. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/PKG-INFO +29 -3
  2. synapse_memory_engine-1.3.1/README.md +52 -0
  3. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/pyproject.toml +1 -1
  4. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/setup.py +4 -3
  5. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/__init__.py +1 -1
  6. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/api/fastapi_server.py +23 -12
  7. synapse_memory_engine-1.3.1/synapse_memory/client.py +200 -0
  8. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/core/embedder.py +14 -6
  9. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/core/embedding_provider.py +10 -2
  10. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/core/pgvector_db.py +194 -29
  11. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory_engine.egg-info/PKG-INFO +29 -3
  12. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory_engine.egg-info/SOURCES.txt +1 -0
  13. synapse_memory_engine-1.3.1/tests/test_client_retry_policy.py +116 -0
  14. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_db_role_provisioning.py +57 -2
  15. synapse_memory_engine-1.3.1/tests/test_f17_provider_dimension.py +106 -0
  16. synapse_memory_engine-1.3.1/tests/test_f18_pgvector_3072.py +114 -0
  17. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_helm_chart.py +19 -7
  18. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_p1_contract_regressions.py +12 -0
  19. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_pgvector_integration.py +3 -0
  20. synapse_memory_engine-1.3.0/README.md +0 -26
  21. synapse_memory_engine-1.3.0/synapse_memory/client.py +0 -127
  22. synapse_memory_engine-1.3.0/tests/test_f17_provider_dimension.py +0 -43
  23. synapse_memory_engine-1.3.0/tests/test_f18_pgvector_3072.py +0 -69
  24. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/setup.cfg +0 -0
  25. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/cli/synapse_cli.py +0 -0
  26. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/core/__init__.py +0 -0
  27. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/core/auth.py +0 -0
  28. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/core/decay_engine.py +0 -0
  29. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/core/deduplicator.py +0 -0
  30. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/core/encryption.py +0 -0
  31. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/core/hybrid_search.py +0 -0
  32. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/core/knapsack_packer.py +0 -0
  33. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/core/memory_manager.py +0 -0
  34. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/core/migrations.py +0 -0
  35. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/core/observability.py +0 -0
  36. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/core/pruner.py +0 -0
  37. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/core/rate_limiter.py +0 -0
  38. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/core/sqlite_db.py +0 -0
  39. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/examples/langchain_integration.py +0 -0
  40. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/examples/llamaindex_integration.py +0 -0
  41. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/integrations/__init__.py +0 -0
  42. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/integrations/langchain.py +0 -0
  43. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/integrations/llamaindex.py +0 -0
  44. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/main.py +0 -0
  45. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory/py.typed +0 -0
  46. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory_engine.egg-info/dependency_links.txt +0 -0
  47. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory_engine.egg-info/entry_points.txt +0 -0
  48. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory_engine.egg-info/requires.txt +0 -0
  49. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/synapse_memory_engine.egg-info/top_level.txt +0 -0
  50. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_auth_multitenancy.py +0 -0
  51. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_backup_restore_drill.py +0 -0
  52. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_decay.py +0 -0
  53. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_deduplicator.py +0 -0
  54. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_deterministic_embedding.py +0 -0
  55. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_encryption.py +0 -0
  56. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_f01_auth_escalation.py +0 -0
  57. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_f02_f07_probes.py +0 -0
  58. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_f05_f06_data_at_rest.py +0 -0
  59. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_f10_f15_hardening.py +0 -0
  60. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_integration.py +0 -0
  61. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_knapsack.py +0 -0
  62. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_langchain.py +0 -0
  63. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_llamaindex.py +0 -0
  64. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_load_resilience.py +0 -0
  65. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_max_record_limit.py +0 -0
  66. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_migrations.py +0 -0
  67. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_p0_safety_regressions.py +0 -0
  68. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_pgvector.py +0 -0
  69. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_phase_cd.py +0 -0
  70. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_pruner.py +0 -0
  71. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_rate_limit.py +0 -0
  72. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_required_auth_gateway.py +0 -0
  73. {synapse_memory_engine-1.3.0 → synapse_memory_engine-1.3.1}/tests/test_retrieval_eval.py +0 -0
  74. {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.0
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
- Core Python SDK and algorithms for the Synapse Cognitive Memory substrate.
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
- python3 -m unittest discover -s tests
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.0"
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.2.0",
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="synapse-memory@users.noreply.github.com",
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.0"
13
+ __version__ = "1.3.1"
14
14
  __api_compat_version__ = "v1"
15
15
 
16
16
  __all__ = [
@@ -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.2.0"
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 IngestRequest(BaseModel):
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(BaseModel):
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(BaseModel):
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(BaseModel):
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(BaseModel):
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(BaseModel):
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(BaseModel):
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(BaseModel):
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(BaseModel):
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(BaseModel):
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
- dim = int(os.getenv("SYNAPSE_EMBEDDING_DIMENSION", "128"))
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
- dim = int(os.getenv("SYNAPSE_EMBEDDING_DIMENSION", "128"))
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
- return GeminiEmbeddingProvider(self.gemini_key)
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
- dim = int(os.getenv("SYNAPSE_EMBEDDING_DIMENSION", "128"))
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
- return OpenAIEmbeddingProvider(self.openai_key)
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
- dim = int(os.getenv("SYNAPSE_EMBEDDING_DIMENSION", "128"))
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 = {"content": {"parts": [{"text": text}]}}
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"),