synapse-memory-engine 1.3.1__tar.gz → 1.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/PKG-INFO +65 -1
- synapse_memory_engine-1.4.0/README.md +116 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/pyproject.toml +1 -1
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/setup.py +1 -1
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/__init__.py +29 -1
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/api/fastapi_server.py +200 -6
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/client.py +51 -1
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/core/auth.py +3 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/core/knapsack_packer.py +13 -3
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/core/migrations.py +130 -1
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/core/pgvector_db.py +55 -0
- synapse_memory_engine-1.4.0/synapse_memory/core/run_trace_store.py +547 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/core/sqlite_db.py +1 -0
- synapse_memory_engine-1.4.0/synapse_memory/runtime.py +540 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory_engine.egg-info/PKG-INFO +65 -1
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory_engine.egg-info/SOURCES.txt +11 -0
- synapse_memory_engine-1.4.0/tests/test_agent_run_api.py +160 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_db_role_provisioning.py +43 -0
- synapse_memory_engine-1.4.0/tests/test_developer_relevance_scoring.py +108 -0
- synapse_memory_engine-1.4.0/tests/test_developer_review_packet.py +57 -0
- synapse_memory_engine-1.4.0/tests/test_first_run_http_e2e.py +154 -0
- synapse_memory_engine-1.4.0/tests/test_knapsack_score_precision.py +55 -0
- synapse_memory_engine-1.4.0/tests/test_longmemeval_benchmark.py +142 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_migrations.py +19 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_pgvector_integration.py +74 -0
- synapse_memory_engine-1.4.0/tests/test_retrieval_baseline_benchmark.py +247 -0
- synapse_memory_engine-1.4.0/tests/test_run_trace_store.py +202 -0
- synapse_memory_engine-1.4.0/tests/test_runtime.py +280 -0
- synapse_memory_engine-1.3.1/README.md +0 -52
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/setup.cfg +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/cli/synapse_cli.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/core/__init__.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/core/decay_engine.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/core/deduplicator.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/core/embedder.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/core/embedding_provider.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/core/encryption.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/core/hybrid_search.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/core/memory_manager.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/core/observability.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/core/pruner.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/core/rate_limiter.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/examples/langchain_integration.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/examples/llamaindex_integration.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/integrations/__init__.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/integrations/langchain.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/integrations/llamaindex.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/main.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/py.typed +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory_engine.egg-info/dependency_links.txt +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory_engine.egg-info/entry_points.txt +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory_engine.egg-info/requires.txt +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory_engine.egg-info/top_level.txt +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_auth_multitenancy.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_backup_restore_drill.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_client_retry_policy.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_decay.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_deduplicator.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_deterministic_embedding.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_encryption.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_f01_auth_escalation.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_f02_f07_probes.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_f05_f06_data_at_rest.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_f10_f15_hardening.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_f17_provider_dimension.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_f18_pgvector_3072.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_helm_chart.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_integration.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_knapsack.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_langchain.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_llamaindex.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_load_resilience.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_max_record_limit.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_p0_safety_regressions.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_p1_contract_regressions.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_pgvector.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_phase_cd.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_pruner.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_rate_limit.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_required_auth_gateway.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_retrieval_eval.py +0 -0
- {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/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
|
+
Version: 1.4.0
|
|
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
|
|
@@ -59,6 +59,8 @@ Python distribution for the Synapse Memory engine. The distribution name is `syn
|
|
|
59
59
|
- **Semantic Deduplication**: Dual lexical Jaccard and dense cosine similarity detection to prevent node fragmentation.
|
|
60
60
|
- **Hybrid Retrieval**: BM25 sparse lexical ranking fused with dense vector embeddings via Reciprocal Rank Fusion (RRF).
|
|
61
61
|
- **Framework Integrations**: True drop-in adapters for LangChain (`BaseMemory`) and LlamaIndex (`BaseMemory`).
|
|
62
|
+
- **Agent reliability runtime**: Opt-in run budgets, repeated failed-call blocking, versioned event serialization, and persistent tenant-scoped run traces.
|
|
63
|
+
- **Opt-in agent run guard**: Bounded steps/tool calls, cooperative deadlines, repeated-failure blocking, and privacy-minimized in-memory events.
|
|
62
64
|
|
|
63
65
|
## Installation
|
|
64
66
|
|
|
@@ -87,6 +89,68 @@ with SynapseClient(
|
|
|
87
89
|
|
|
88
90
|
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
91
|
|
|
92
|
+
## Opt-in Python run guard
|
|
93
|
+
|
|
94
|
+
The runtime only guards tool calls explicitly routed through `SynapseRun`; it does not intercept arbitrary framework calls. Events stay in memory until the application exports or persists them. Event records contain tool names and keyed argument fingerprints, not raw arguments, prompts, tool results, or exception messages.
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from synapse_memory import RunPolicy, SynapseRun
|
|
98
|
+
|
|
99
|
+
policy = RunPolicy(
|
|
100
|
+
max_steps=12,
|
|
101
|
+
max_tool_calls=8,
|
|
102
|
+
timeout_seconds=30,
|
|
103
|
+
max_repeated_failures=2,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
with SynapseRun(policy=policy) as run:
|
|
107
|
+
customer = run.call_tool(
|
|
108
|
+
"lookup_customer",
|
|
109
|
+
{"customer_id": "cust_123"},
|
|
110
|
+
lookup_customer, # callable accepts arguments as keyword arguments
|
|
111
|
+
)
|
|
112
|
+
run.record_step() # account for non-tool work
|
|
113
|
+
print(customer)
|
|
114
|
+
|
|
115
|
+
# Inspect metadata-only events after the context exits.
|
|
116
|
+
for event in run.events:
|
|
117
|
+
print(event.event_type.value, event.reason, event.tool_fingerprint)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Limits are checked before admitting each step/tool call. The deadline is cooperative: a synchronous tool already in progress cannot be forcibly interrupted. A run guard is not a sandbox, does not authorize tools, does not intercept unwrapped calls, or enforce provider token/cost budgets. Repeated-call blocking applies after the configured number of consecutive failures for the same tool and equivalent JSON arguments; a successful call resets that failure count. No automatic retries are performed.
|
|
121
|
+
|
|
122
|
+
## Persistent run traces and retrieval provenance
|
|
123
|
+
|
|
124
|
+
Trace persistence is an explicit opt-in operation. The event contract is versioned (schema_version=1) and records only run lifecycle metadata, tool name/status, a per-run keyed argument fingerprint, typed failure reasons, and retrieval evidence. It does not record prompts, raw tool arguments, tool results, memory content, or the retrieval query. Retrieval evidence must be supplied by the application from its own retrieval/packing pipeline; Synapse does not silently instrument arbitrary framework calls or automatically attach every /query response to a run.
|
|
125
|
+
|
|
126
|
+
To persist a completed SynapseRun, use a key with agent-runs:read and agent-runs:write scopes, plus agent-runs:delete only when deletion is needed:
|
|
127
|
+
|
|
128
|
+
~~~python
|
|
129
|
+
from synapse_memory import RunPolicy, SynapseClient, SynapseRun
|
|
130
|
+
|
|
131
|
+
with SynapseRun(policy=RunPolicy(max_steps=12, max_tool_calls=8)) as run:
|
|
132
|
+
# The app can query Synapse or another retriever here.
|
|
133
|
+
candidates = [
|
|
134
|
+
{"id": "memory_01", "relevance_score": 0.91, "token_cost": 18},
|
|
135
|
+
{"id": "memory_02", "relevance_score": 0.43, "token_cost": 16},
|
|
136
|
+
]
|
|
137
|
+
packed_ids = ["memory_01"]
|
|
138
|
+
run.record_memory_retrieval(candidates, packed_memory_ids=packed_ids)
|
|
139
|
+
answer = run.call_tool("lookup_customer", {"customer_id": "cust_123"}, lookup_customer)
|
|
140
|
+
|
|
141
|
+
with SynapseClient(
|
|
142
|
+
"https://your-synapse-api",
|
|
143
|
+
api_key="syn_live_your_key",
|
|
144
|
+
tenant_id="your-tenant",
|
|
145
|
+
) as client:
|
|
146
|
+
receipt = client.upload_agent_run(run)
|
|
147
|
+
print(receipt["events_inserted"], receipt["events_deduplicated"])
|
|
148
|
+
~~~
|
|
149
|
+
|
|
150
|
+
The client uploads the run's ordered events after the run ends. Stable event IDs make retrying a partially completed upload idempotent. Event records are tenant-bound by the authenticated API key; a caller-supplied tenant field in an event is rejected. Available endpoints are documented in the API contract (../../docs/api-reference.md). Each tenant can list and delete only its own trace records; event pages are bounded and ordered by sequence number.
|
|
151
|
+
|
|
152
|
+
This first persistence layer is not yet a live streaming sink, a full observability dashboard, an outcome verifier, or a retention scheduler. Deployments must apply schema migration v6 before starting a PostgreSQL least-privilege runtime role. Use the API's deletion endpoint or a separately configured retention job to remove stored runs.
|
|
153
|
+
|
|
90
154
|
## Testing
|
|
91
155
|
|
|
92
156
|
From the repository root:
|
|
@@ -0,0 +1,116 @@
|
|
|
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
|
+
- **Agent reliability runtime**: Opt-in run budgets, repeated failed-call blocking, versioned event serialization, and persistent tenant-scoped run traces.
|
|
14
|
+
- **Opt-in agent run guard**: Bounded steps/tool calls, cooperative deadlines, repeated-failure blocking, and privacy-minimized in-memory events.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install synapse-memory-engine # from PyPI
|
|
20
|
+
# or, from a checkout of this repository:
|
|
21
|
+
pip install -e .
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## REST client quick start
|
|
25
|
+
|
|
26
|
+
Use `SynapseClient` when connecting an application to a running Synapse API:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from synapse_memory import SynapseClient, SynapseError
|
|
30
|
+
|
|
31
|
+
with SynapseClient(
|
|
32
|
+
"http://127.0.0.1:8008",
|
|
33
|
+
api_key="syn_live_your_key",
|
|
34
|
+
tenant_id="your-tenant",
|
|
35
|
+
) as client:
|
|
36
|
+
created = client.create_memory("Customer selected the annual plan.", category="fact")
|
|
37
|
+
result = client.retrieve("Which plan did the customer select?")
|
|
38
|
+
print(created.id, result.fused_context)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
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.
|
|
42
|
+
|
|
43
|
+
## Opt-in Python run guard
|
|
44
|
+
|
|
45
|
+
The runtime only guards tool calls explicitly routed through `SynapseRun`; it does not intercept arbitrary framework calls. Events stay in memory until the application exports or persists them. Event records contain tool names and keyed argument fingerprints, not raw arguments, prompts, tool results, or exception messages.
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from synapse_memory import RunPolicy, SynapseRun
|
|
49
|
+
|
|
50
|
+
policy = RunPolicy(
|
|
51
|
+
max_steps=12,
|
|
52
|
+
max_tool_calls=8,
|
|
53
|
+
timeout_seconds=30,
|
|
54
|
+
max_repeated_failures=2,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
with SynapseRun(policy=policy) as run:
|
|
58
|
+
customer = run.call_tool(
|
|
59
|
+
"lookup_customer",
|
|
60
|
+
{"customer_id": "cust_123"},
|
|
61
|
+
lookup_customer, # callable accepts arguments as keyword arguments
|
|
62
|
+
)
|
|
63
|
+
run.record_step() # account for non-tool work
|
|
64
|
+
print(customer)
|
|
65
|
+
|
|
66
|
+
# Inspect metadata-only events after the context exits.
|
|
67
|
+
for event in run.events:
|
|
68
|
+
print(event.event_type.value, event.reason, event.tool_fingerprint)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Limits are checked before admitting each step/tool call. The deadline is cooperative: a synchronous tool already in progress cannot be forcibly interrupted. A run guard is not a sandbox, does not authorize tools, does not intercept unwrapped calls, or enforce provider token/cost budgets. Repeated-call blocking applies after the configured number of consecutive failures for the same tool and equivalent JSON arguments; a successful call resets that failure count. No automatic retries are performed.
|
|
72
|
+
|
|
73
|
+
## Persistent run traces and retrieval provenance
|
|
74
|
+
|
|
75
|
+
Trace persistence is an explicit opt-in operation. The event contract is versioned (schema_version=1) and records only run lifecycle metadata, tool name/status, a per-run keyed argument fingerprint, typed failure reasons, and retrieval evidence. It does not record prompts, raw tool arguments, tool results, memory content, or the retrieval query. Retrieval evidence must be supplied by the application from its own retrieval/packing pipeline; Synapse does not silently instrument arbitrary framework calls or automatically attach every /query response to a run.
|
|
76
|
+
|
|
77
|
+
To persist a completed SynapseRun, use a key with agent-runs:read and agent-runs:write scopes, plus agent-runs:delete only when deletion is needed:
|
|
78
|
+
|
|
79
|
+
~~~python
|
|
80
|
+
from synapse_memory import RunPolicy, SynapseClient, SynapseRun
|
|
81
|
+
|
|
82
|
+
with SynapseRun(policy=RunPolicy(max_steps=12, max_tool_calls=8)) as run:
|
|
83
|
+
# The app can query Synapse or another retriever here.
|
|
84
|
+
candidates = [
|
|
85
|
+
{"id": "memory_01", "relevance_score": 0.91, "token_cost": 18},
|
|
86
|
+
{"id": "memory_02", "relevance_score": 0.43, "token_cost": 16},
|
|
87
|
+
]
|
|
88
|
+
packed_ids = ["memory_01"]
|
|
89
|
+
run.record_memory_retrieval(candidates, packed_memory_ids=packed_ids)
|
|
90
|
+
answer = run.call_tool("lookup_customer", {"customer_id": "cust_123"}, lookup_customer)
|
|
91
|
+
|
|
92
|
+
with SynapseClient(
|
|
93
|
+
"https://your-synapse-api",
|
|
94
|
+
api_key="syn_live_your_key",
|
|
95
|
+
tenant_id="your-tenant",
|
|
96
|
+
) as client:
|
|
97
|
+
receipt = client.upload_agent_run(run)
|
|
98
|
+
print(receipt["events_inserted"], receipt["events_deduplicated"])
|
|
99
|
+
~~~
|
|
100
|
+
|
|
101
|
+
The client uploads the run's ordered events after the run ends. Stable event IDs make retrying a partially completed upload idempotent. Event records are tenant-bound by the authenticated API key; a caller-supplied tenant field in an event is rejected. Available endpoints are documented in the API contract (../../docs/api-reference.md). Each tenant can list and delete only its own trace records; event pages are bounded and ordered by sequence number.
|
|
102
|
+
|
|
103
|
+
This first persistence layer is not yet a live streaming sink, a full observability dashboard, an outcome verifier, or a retention scheduler. Deployments must apply schema migration v6 before starting a PostgreSQL least-privilege runtime role. Use the API's deletion endpoint or a separately configured retention job to remove stored runs.
|
|
104
|
+
|
|
105
|
+
## Testing
|
|
106
|
+
|
|
107
|
+
From the repository root:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
pip install -e "sdk/python[pgvector,langchain,llamaindex,dev]"
|
|
111
|
+
PYTHONPATH=sdk/python pytest sdk/python/tests -v
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Database-gated tests require a disposable PostgreSQL/pgvector database and explicitly documented environment flags. Passing the offline suite alone is not production verification.
|
|
115
|
+
|
|
116
|
+
**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.
|
|
10
|
+
version = "1.4.0"
|
|
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,7 +2,7 @@ from setuptools import setup, find_packages
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name="synapse-memory-engine",
|
|
5
|
-
version="1.
|
|
5
|
+
version="1.4.0",
|
|
6
6
|
description="Sovereign, active RAG & cognitive long-term memory engine for AI agents",
|
|
7
7
|
author="Synapse Cognitive Systems",
|
|
8
8
|
author_email="singhshivam20009@gmail.com",
|
|
@@ -9,8 +9,23 @@ from synapse_memory.core.decay_engine import DecayEngine
|
|
|
9
9
|
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
|
+
from synapse_memory.runtime import (
|
|
13
|
+
RunPolicy,
|
|
14
|
+
RunStatus,
|
|
15
|
+
RunEventType,
|
|
16
|
+
RunEvent,
|
|
17
|
+
MemoryEvidence,
|
|
18
|
+
SynapseRun,
|
|
19
|
+
RunGuardError,
|
|
20
|
+
RunStateError,
|
|
21
|
+
RunLimitExceeded,
|
|
22
|
+
RunStepLimitExceeded,
|
|
23
|
+
RunToolCallLimitExceeded,
|
|
24
|
+
RunDeadlineExceeded,
|
|
25
|
+
RepeatedToolCallError,
|
|
26
|
+
)
|
|
12
27
|
|
|
13
|
-
__version__ = "1.
|
|
28
|
+
__version__ = "1.4.0"
|
|
14
29
|
__api_compat_version__ = "v1"
|
|
15
30
|
|
|
16
31
|
__all__ = [
|
|
@@ -28,4 +43,17 @@ __all__ = [
|
|
|
28
43
|
"SynapseError",
|
|
29
44
|
"CreateResult",
|
|
30
45
|
"QueryResult",
|
|
46
|
+
"RunPolicy",
|
|
47
|
+
"RunStatus",
|
|
48
|
+
"RunEventType",
|
|
49
|
+
"RunEvent",
|
|
50
|
+
"MemoryEvidence",
|
|
51
|
+
"SynapseRun",
|
|
52
|
+
"RunGuardError",
|
|
53
|
+
"RunStateError",
|
|
54
|
+
"RunLimitExceeded",
|
|
55
|
+
"RunStepLimitExceeded",
|
|
56
|
+
"RunToolCallLimitExceeded",
|
|
57
|
+
"RunDeadlineExceeded",
|
|
58
|
+
"RepeatedToolCallError",
|
|
31
59
|
]
|
{synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/api/fastapi_server.py
RENAMED
|
@@ -5,10 +5,10 @@ import datetime
|
|
|
5
5
|
import hashlib
|
|
6
6
|
import logging
|
|
7
7
|
import threading
|
|
8
|
-
from typing import List, Dict, Any, Optional, Tuple
|
|
9
|
-
from pydantic import BaseModel, ConfigDict, Field
|
|
8
|
+
from typing import List, Dict, Any, Optional, Tuple, Literal
|
|
9
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
10
10
|
import uvicorn
|
|
11
|
-
from fastapi import FastAPI, HTTPException, status, Header, Request, Depends
|
|
11
|
+
from fastapi import FastAPI, HTTPException, status, Header, Request, Depends, Path
|
|
12
12
|
from fastapi.middleware.cors import CORSMiddleware
|
|
13
13
|
|
|
14
14
|
# Adjust path to enable absolute imports when running as standalone script
|
|
@@ -21,6 +21,10 @@ from synapse_memory.core.memory_manager import DEFAULT_MAX_RECORD_LIMIT, MemoryM
|
|
|
21
21
|
from synapse_memory.core.knapsack_packer import KnapsackPacker
|
|
22
22
|
from synapse_memory.core.decay_engine import DecayEngine
|
|
23
23
|
from synapse_memory.core.hybrid_search import HybridSearch
|
|
24
|
+
from synapse_memory.runtime import RunEventType, RunStatus
|
|
25
|
+
from synapse_memory.core.run_trace_store import (
|
|
26
|
+
RunTraceStore, RunTraceError, RunNotFoundError, RunConflictError,
|
|
27
|
+
)
|
|
24
28
|
from synapse_memory.core.auth import (
|
|
25
29
|
SecurityManager,
|
|
26
30
|
APIKeyRecord,
|
|
@@ -106,6 +110,7 @@ else:
|
|
|
106
110
|
ACTIVE_BACKEND = "sqlite"
|
|
107
111
|
|
|
108
112
|
manager = MemoryManager(store=store, embedder=embedder, max_record_limit=DEFAULT_MAX_RECORD_LIMIT)
|
|
113
|
+
run_trace_store = RunTraceStore(store)
|
|
109
114
|
logger.info(
|
|
110
115
|
f"MemoryManager capacity limit: {manager.max_record_limit} records "
|
|
111
116
|
f"(override with SYNAPSE_MAX_RECORD_LIMIT; historical default is 50)"
|
|
@@ -233,7 +238,7 @@ def _rate_limit_principal(request: Request) -> str:
|
|
|
233
238
|
app = FastAPI(
|
|
234
239
|
title="SynapseMemory Enterprise Cognitive Gateway",
|
|
235
240
|
description="Sovereign Active Long-Term Memory & Context Budgeting REST API with Multi-Tenancy and Database-Level RLS.",
|
|
236
|
-
version="1.
|
|
241
|
+
version="1.4.0"
|
|
237
242
|
)
|
|
238
243
|
|
|
239
244
|
|
|
@@ -244,9 +249,11 @@ async def add_request_id_and_rate_limit(request: Request, call_next): # type: i
|
|
|
244
249
|
request_id = request.headers.get("X-Request-ID") or f"req_{_uuid.uuid4().hex[:12]}"
|
|
245
250
|
start = time.time()
|
|
246
251
|
# Rate-limit only mutating + heavy retrieval endpoints
|
|
247
|
-
if request.url.path.startswith(("/ingest", "/api/memories", "/query", "/api/retrieval")):
|
|
252
|
+
if request.url.path.startswith(("/ingest", "/api/memories", "/query", "/api/retrieval", "/api/agent-runs")):
|
|
253
|
+
# Avoid unbounded per-run limiter buckets: all run/event URLs share one bucket.
|
|
254
|
+
rate_route = "/api/agent-runs" if request.url.path.startswith("/api/agent-runs") else request.url.path
|
|
248
255
|
try:
|
|
249
|
-
_check_rate_limit(f"{_rate_limit_principal(request)}:{
|
|
256
|
+
_check_rate_limit(f"{_rate_limit_principal(request)}:{rate_route}")
|
|
250
257
|
except HTTPException as e:
|
|
251
258
|
from fastapi.responses import JSONResponse
|
|
252
259
|
|
|
@@ -455,6 +462,62 @@ class QueryResponse(SynapseAPIModel):
|
|
|
455
462
|
active_storage_backend: str
|
|
456
463
|
|
|
457
464
|
|
|
465
|
+
class RunCreateRequest(SynapseAPIModel):
|
|
466
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
467
|
+
run_id: str = Field(..., min_length=1, max_length=128, pattern=r"^[A-Za-z0-9._:-]+$")
|
|
468
|
+
schema_version: Literal[1] = 1
|
|
469
|
+
tenant_id: Optional[str] = Field(
|
|
470
|
+
None, alias="tenantId", min_length=1, max_length=128,
|
|
471
|
+
pattern=r"^[A-Za-z0-9_\-\.]+$",
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
class MemoryEvidenceRequest(SynapseAPIModel):
|
|
476
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
477
|
+
memory_id: str = Field(..., min_length=1, max_length=128, pattern=r"^[A-Za-z0-9._:-]+$")
|
|
478
|
+
rank: int = Field(..., ge=1, le=10000)
|
|
479
|
+
score_components: Dict[str, float] = Field(default_factory=dict, max_length=8)
|
|
480
|
+
token_cost: int = Field(0, ge=0, le=100000)
|
|
481
|
+
packed: bool = False
|
|
482
|
+
created_at: Optional[float] = Field(None, ge=0, allow_inf_nan=False)
|
|
483
|
+
supersedes: Optional[str] = Field(None, min_length=1, max_length=128, pattern=r"^[A-Za-z0-9._:-]+$")
|
|
484
|
+
superseded_by: Optional[str] = Field(None, min_length=1, max_length=128, pattern=r"^[A-Za-z0-9._:-]+$")
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
class AgentRunEventRequest(SynapseAPIModel):
|
|
488
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
489
|
+
schema_version: Literal[1] = 1
|
|
490
|
+
event_id: str = Field(..., min_length=1, max_length=128, pattern=r"^[A-Za-z0-9._:-]+$")
|
|
491
|
+
run_id: str = Field(..., min_length=1, max_length=128, pattern=r"^[A-Za-z0-9._:-]+$")
|
|
492
|
+
event_type: RunEventType
|
|
493
|
+
timestamp: datetime.datetime
|
|
494
|
+
run_status: RunStatus
|
|
495
|
+
step_index: Optional[int] = Field(None, ge=1)
|
|
496
|
+
tool_call_index: Optional[int] = Field(None, ge=1)
|
|
497
|
+
tool_name: Optional[str] = Field(None, min_length=1, max_length=128, pattern=r"^[A-Za-z][A-Za-z0-9_.:-]*$")
|
|
498
|
+
tool_fingerprint: Optional[str] = Field(None, pattern=r"^[a-fA-F0-9]{64}$")
|
|
499
|
+
reason: Optional[str] = Field(None, min_length=1, max_length=128, pattern=r"^[a-z][a-z0-9_]*$")
|
|
500
|
+
error_type: Optional[str] = Field(None, min_length=1, max_length=128, pattern=r"^[A-Za-z][A-Za-z0-9_.:-]*$")
|
|
501
|
+
retrieval_id: Optional[str] = Field(None, min_length=1, max_length=128, pattern=r"^[A-Za-z0-9._:-]+$")
|
|
502
|
+
memory_evidence: List[MemoryEvidenceRequest] = Field(default_factory=list, max_length=100)
|
|
503
|
+
|
|
504
|
+
@field_validator("timestamp")
|
|
505
|
+
@classmethod
|
|
506
|
+
def timestamp_must_be_timezone_aware(cls, value: datetime.datetime) -> datetime.datetime:
|
|
507
|
+
if value.tzinfo is None or value.utcoffset() is None:
|
|
508
|
+
raise ValueError("timestamp must include a timezone")
|
|
509
|
+
return value
|
|
510
|
+
|
|
511
|
+
@model_validator(mode="after")
|
|
512
|
+
def retrieval_metadata_must_match_event_type(self):
|
|
513
|
+
if self.event_type is RunEventType.MEMORY_RETRIEVAL:
|
|
514
|
+
if not self.retrieval_id:
|
|
515
|
+
raise ValueError("memory.retrieval requires retrieval_id")
|
|
516
|
+
elif self.retrieval_id is not None or self.memory_evidence:
|
|
517
|
+
raise ValueError("retrieval metadata is only valid for memory.retrieval")
|
|
518
|
+
return self
|
|
519
|
+
|
|
520
|
+
|
|
458
521
|
class FeedbackRequest(SynapseAPIModel):
|
|
459
522
|
memory_id: str = Field(..., min_length=1, max_length=256, description="Memory node ID")
|
|
460
523
|
feedback_type: str = Field(..., pattern=r"^(positive|negative)$", description="Must be either 'positive' (boost) or 'negative' (hallucination alert)")
|
|
@@ -791,6 +854,137 @@ def query_memory(
|
|
|
791
854
|
)
|
|
792
855
|
|
|
793
856
|
|
|
857
|
+
@app.post("/api/agent-runs", tags=["Agent Reliability"])
|
|
858
|
+
def create_agent_run(
|
|
859
|
+
request: Request,
|
|
860
|
+
payload: RunCreateRequest,
|
|
861
|
+
principal: APIKeyRecord = Depends(require_scope("agent-runs:write")),
|
|
862
|
+
):
|
|
863
|
+
"""Create a tenant-bound run before streaming events (or before manual upload)."""
|
|
864
|
+
effective_tenant = resolve_tenant(principal, payload.tenant_id)
|
|
865
|
+
try:
|
|
866
|
+
result = run_trace_store.create_run(
|
|
867
|
+
effective_tenant, payload.run_id, schema_version=payload.schema_version,
|
|
868
|
+
)
|
|
869
|
+
except RunTraceError as exc:
|
|
870
|
+
raise HTTPException(status_code=503, detail="Agent trace storage unavailable.") from exc
|
|
871
|
+
except ValueError as exc:
|
|
872
|
+
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
873
|
+
return envelope(result, request)
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
@app.post("/api/agent-runs/{run_id}/events", tags=["Agent Reliability"])
|
|
877
|
+
def append_agent_run_event(
|
|
878
|
+
request: Request,
|
|
879
|
+
payload: AgentRunEventRequest,
|
|
880
|
+
run_id: str = Path(..., min_length=1, max_length=128, pattern=r"^[A-Za-z0-9._:-]+$"),
|
|
881
|
+
tenant_id: Optional[str] = None,
|
|
882
|
+
principal: APIKeyRecord = Depends(require_scope("agent-runs:write")),
|
|
883
|
+
):
|
|
884
|
+
"""Append one versioned event; retries with the same event ID are idempotent."""
|
|
885
|
+
if payload.run_id != run_id:
|
|
886
|
+
raise HTTPException(status_code=422, detail="event run_id does not match the path run_id.")
|
|
887
|
+
effective_tenant = resolve_tenant(principal, tenant_id)
|
|
888
|
+
event = payload.model_dump(exclude_none=True)
|
|
889
|
+
event["event_type"] = payload.event_type.value
|
|
890
|
+
event["run_status"] = payload.run_status.value
|
|
891
|
+
event["memory_evidence"] = [
|
|
892
|
+
item.model_dump(exclude_none=True) for item in payload.memory_evidence
|
|
893
|
+
]
|
|
894
|
+
try:
|
|
895
|
+
result = run_trace_store.append_event(effective_tenant, run_id, event)
|
|
896
|
+
except RunNotFoundError as exc:
|
|
897
|
+
raise HTTPException(status_code=404, detail="Run not found for the authenticated tenant.") from exc
|
|
898
|
+
except RunConflictError as exc:
|
|
899
|
+
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
|
900
|
+
except RunTraceError as exc:
|
|
901
|
+
raise HTTPException(status_code=503, detail="Agent trace storage unavailable.") from exc
|
|
902
|
+
except (ValueError, TypeError) as exc:
|
|
903
|
+
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
904
|
+
return envelope(result, request)
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
@app.get("/api/agent-runs", tags=["Agent Reliability"])
|
|
908
|
+
def list_agent_runs(
|
|
909
|
+
request: Request,
|
|
910
|
+
tenant_id: Optional[str] = None,
|
|
911
|
+
limit: int = 50,
|
|
912
|
+
offset: int = 0,
|
|
913
|
+
principal: APIKeyRecord = Depends(require_scope("agent-runs:read")),
|
|
914
|
+
):
|
|
915
|
+
"""List recent run summaries for the authenticated tenant."""
|
|
916
|
+
effective_tenant = resolve_tenant(principal, tenant_id)
|
|
917
|
+
try:
|
|
918
|
+
runs = run_trace_store.list_runs(effective_tenant, limit=limit, offset=offset)
|
|
919
|
+
except RunTraceError as exc:
|
|
920
|
+
raise HTTPException(status_code=503, detail="Agent trace storage unavailable.") from exc
|
|
921
|
+
except ValueError as exc:
|
|
922
|
+
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
923
|
+
return envelope({"runs": runs, "limit": limit, "offset": offset}, request)
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
@app.get("/api/agent-runs/{run_id}", tags=["Agent Reliability"])
|
|
927
|
+
def get_agent_run(
|
|
928
|
+
request: Request,
|
|
929
|
+
run_id: str = Path(..., min_length=1, max_length=128, pattern=r"^[A-Za-z0-9._:-]+$"),
|
|
930
|
+
tenant_id: Optional[str] = None,
|
|
931
|
+
principal: APIKeyRecord = Depends(require_scope("agent-runs:read")),
|
|
932
|
+
):
|
|
933
|
+
"""Fetch one run summary without returning event payloads by default."""
|
|
934
|
+
effective_tenant = resolve_tenant(principal, tenant_id)
|
|
935
|
+
try:
|
|
936
|
+
result = run_trace_store.get_run(effective_tenant, run_id)
|
|
937
|
+
except RunNotFoundError as exc:
|
|
938
|
+
raise HTTPException(status_code=404, detail="Run not found for the authenticated tenant.") from exc
|
|
939
|
+
except RunTraceError as exc:
|
|
940
|
+
raise HTTPException(status_code=503, detail="Agent trace storage unavailable.") from exc
|
|
941
|
+
except ValueError as exc:
|
|
942
|
+
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
943
|
+
return envelope(result, request)
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
@app.get("/api/agent-runs/{run_id}/events", tags=["Agent Reliability"])
|
|
947
|
+
def list_agent_run_events(
|
|
948
|
+
request: Request,
|
|
949
|
+
run_id: str = Path(..., min_length=1, max_length=128, pattern=r"^[A-Za-z0-9._:-]+$"),
|
|
950
|
+
tenant_id: Optional[str] = None,
|
|
951
|
+
limit: int = 100,
|
|
952
|
+
offset: int = 0,
|
|
953
|
+
principal: APIKeyRecord = Depends(require_scope("agent-runs:read")),
|
|
954
|
+
):
|
|
955
|
+
"""Read a bounded, chronological metadata-only event page."""
|
|
956
|
+
effective_tenant = resolve_tenant(principal, tenant_id)
|
|
957
|
+
try:
|
|
958
|
+
events = run_trace_store.list_events(
|
|
959
|
+
effective_tenant, run_id, limit=limit, offset=offset,
|
|
960
|
+
)
|
|
961
|
+
except RunNotFoundError as exc:
|
|
962
|
+
raise HTTPException(status_code=404, detail="Run not found for the authenticated tenant.") from exc
|
|
963
|
+
except RunTraceError as exc:
|
|
964
|
+
raise HTTPException(status_code=503, detail="Agent trace storage unavailable.") from exc
|
|
965
|
+
except ValueError as exc:
|
|
966
|
+
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
967
|
+
return envelope({"run_id": run_id, "events": events, "limit": limit, "offset": offset}, request)
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
@app.delete("/api/agent-runs/{run_id}", tags=["Agent Reliability"])
|
|
971
|
+
def delete_agent_run(
|
|
972
|
+
request: Request,
|
|
973
|
+
run_id: str = Path(..., min_length=1, max_length=128, pattern=r"^[A-Za-z0-9._:-]+$"),
|
|
974
|
+
tenant_id: Optional[str] = None,
|
|
975
|
+
principal: APIKeyRecord = Depends(require_scope("agent-runs:delete")),
|
|
976
|
+
):
|
|
977
|
+
"""Delete a run and its events for data lifecycle/retention workflows."""
|
|
978
|
+
effective_tenant = resolve_tenant(principal, tenant_id)
|
|
979
|
+
try:
|
|
980
|
+
deleted = run_trace_store.delete_run(effective_tenant, run_id)
|
|
981
|
+
except RunTraceError as exc:
|
|
982
|
+
raise HTTPException(status_code=503, detail="Agent trace storage unavailable.") from exc
|
|
983
|
+
except ValueError as exc:
|
|
984
|
+
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
985
|
+
return envelope({"run_id": run_id, "status": "DELETED" if deleted else "NOT_FOUND"}, request)
|
|
986
|
+
|
|
987
|
+
|
|
794
988
|
@app.post("/feedback", tags=["Reinforcement"])
|
|
795
989
|
def submit_feedback(
|
|
796
990
|
request: Request,
|
|
@@ -7,6 +7,7 @@ retryable (429/5xx) failures, and the canonical {success, data} envelope.
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import time
|
|
10
|
+
import re
|
|
10
11
|
from email.utils import parsedate_to_datetime
|
|
11
12
|
from datetime import datetime, timezone
|
|
12
13
|
from dataclasses import dataclass
|
|
@@ -16,7 +17,7 @@ import requests
|
|
|
16
17
|
|
|
17
18
|
__all__ = ["SynapseClient", "SynapseError", "CreateResult", "QueryResult"]
|
|
18
19
|
|
|
19
|
-
SDK_VERSION = "1.
|
|
20
|
+
SDK_VERSION = "1.4.0"
|
|
20
21
|
API_COMPAT_VERSION = "v1"
|
|
21
22
|
|
|
22
23
|
|
|
@@ -193,6 +194,55 @@ class SynapseClient:
|
|
|
193
194
|
def feedback(self, memory_id: str, feedback_type: str) -> Dict[str, Any]:
|
|
194
195
|
return self._request("POST", "/feedback", {"memory_id": memory_id, "feedback_type": feedback_type})
|
|
195
196
|
|
|
197
|
+
def create_agent_run(self, run_id: str) -> Dict[str, Any]:
|
|
198
|
+
"""Create (idempotently) a tenant-bound persistent run trace."""
|
|
199
|
+
if not isinstance(run_id, str) or not re.fullmatch(r"[A-Za-z0-9._:-]{1,128}", run_id):
|
|
200
|
+
raise ValueError("run_id must match [A-Za-z0-9._:-] and be 1–128 characters")
|
|
201
|
+
return self._request(
|
|
202
|
+
"POST", "/api/agent-runs", {"run_id": run_id, "schema_version": 1}, retry_safe=True
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
def append_agent_run_event(self, run_id: str, event: Any) -> Dict[str, Any]:
|
|
206
|
+
"""Append one versioned event. Stable event IDs make retry safe."""
|
|
207
|
+
if not isinstance(run_id, str) or not re.fullmatch(r"[A-Za-z0-9._:-]{1,128}", run_id):
|
|
208
|
+
raise ValueError("run_id must match [A-Za-z0-9._:-] and be 1–128 characters")
|
|
209
|
+
if hasattr(event, "to_dict") and callable(event.to_dict):
|
|
210
|
+
payload = event.to_dict()
|
|
211
|
+
elif isinstance(event, dict):
|
|
212
|
+
payload = dict(event)
|
|
213
|
+
else:
|
|
214
|
+
raise TypeError("event must be a RunEvent or JSON-compatible event dictionary")
|
|
215
|
+
if payload.get("run_id") != run_id:
|
|
216
|
+
raise ValueError("event run_id does not match run_id")
|
|
217
|
+
if not isinstance(payload.get("event_id"), str) or not payload["event_id"]:
|
|
218
|
+
raise ValueError("event must include a stable event_id")
|
|
219
|
+
return self._request(
|
|
220
|
+
"POST", f"/api/agent-runs/{run_id}/events", payload, retry_safe=True
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
def upload_agent_run(self, run: Any) -> Dict[str, Any]:
|
|
224
|
+
"""Persist an in-memory SynapseRun after it finishes.
|
|
225
|
+
|
|
226
|
+
The API appends events in the run's original order. Re-uploading the same
|
|
227
|
+
run is idempotent because create-run and event IDs are stable. A partial
|
|
228
|
+
upload can be retried by calling this method again.
|
|
229
|
+
"""
|
|
230
|
+
run_id = getattr(run, "run_id", None)
|
|
231
|
+
events = getattr(run, "events", None)
|
|
232
|
+
if not isinstance(run_id, str) or not run_id or events is None:
|
|
233
|
+
raise TypeError("run must expose run_id and events (for example, SynapseRun)")
|
|
234
|
+
summary = self.create_agent_run(run_id)
|
|
235
|
+
uploaded = []
|
|
236
|
+
for event in events:
|
|
237
|
+
uploaded.append(self.append_agent_run_event(run_id, event))
|
|
238
|
+
return {
|
|
239
|
+
"run": summary,
|
|
240
|
+
"events_submitted": len(uploaded),
|
|
241
|
+
"events_inserted": sum(1 for result in uploaded if not result.get("duplicate", False)),
|
|
242
|
+
"events_deduplicated": sum(1 for result in uploaded if result.get("duplicate", False)),
|
|
243
|
+
"last_sequence_number": uploaded[-1]["sequence_number"] if uploaded else 0,
|
|
244
|
+
}
|
|
245
|
+
|
|
196
246
|
def health(self) -> Dict[str, Any]:
|
|
197
247
|
return self._request("GET", "/health")
|
|
198
248
|
|
{synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/core/knapsack_packer.py
RENAMED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from typing import List, Dict, Any, Optional
|
|
2
2
|
import logging
|
|
3
|
+
import math
|
|
3
4
|
|
|
4
5
|
logger = logging.getLogger("KnapsackPacker")
|
|
5
6
|
|
|
@@ -62,9 +63,18 @@ class KnapsackPacker:
|
|
|
62
63
|
|
|
63
64
|
n = len(memories)
|
|
64
65
|
weights = [max(1, int(m.get("token_cost", 1))) for m in memories]
|
|
65
|
-
values
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
values: List[float] = []
|
|
67
|
+
for memory in memories:
|
|
68
|
+
score = float(memory.get("relevance_score", 0.0))
|
|
69
|
+
# Non-finite values are not useful selection signals. Treat them as
|
|
70
|
+
# zero rather than allowing NaN/inf to poison DP comparisons.
|
|
71
|
+
values.append(max(0.0, score) if math.isfinite(score) else 0.0)
|
|
72
|
+
|
|
73
|
+
# Preserve score precision. Hybrid RRF scores are commonly around
|
|
74
|
+
# 0.01-0.03, with rank-to-rank differences below 0.001. Converting to
|
|
75
|
+
# int(score * 1000) collapses distinct scores into ties (or zero), so
|
|
76
|
+
# the chosen set can change when every score is rescaled.
|
|
77
|
+
dp = [0.0] * (budget + 1)
|
|
68
78
|
items_included: List[List[int]] = [[] for _ in range(budget + 1)]
|
|
69
79
|
|
|
70
80
|
for i in range(n):
|