maximem-synap-nemo-agent-toolkit 0.1.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.
- maximem_synap_nemo_agent_toolkit-0.1.1/PKG-INFO +101 -0
- maximem_synap_nemo_agent_toolkit-0.1.1/README.md +85 -0
- maximem_synap_nemo_agent_toolkit-0.1.1/maximem_synap_nemo_agent_toolkit.egg-info/PKG-INFO +101 -0
- maximem_synap_nemo_agent_toolkit-0.1.1/maximem_synap_nemo_agent_toolkit.egg-info/SOURCES.txt +12 -0
- maximem_synap_nemo_agent_toolkit-0.1.1/maximem_synap_nemo_agent_toolkit.egg-info/dependency_links.txt +1 -0
- maximem_synap_nemo_agent_toolkit-0.1.1/maximem_synap_nemo_agent_toolkit.egg-info/entry_points.txt +2 -0
- maximem_synap_nemo_agent_toolkit-0.1.1/maximem_synap_nemo_agent_toolkit.egg-info/requires.txt +7 -0
- maximem_synap_nemo_agent_toolkit-0.1.1/maximem_synap_nemo_agent_toolkit.egg-info/top_level.txt +1 -0
- maximem_synap_nemo_agent_toolkit-0.1.1/pyproject.toml +38 -0
- maximem_synap_nemo_agent_toolkit-0.1.1/setup.cfg +4 -0
- maximem_synap_nemo_agent_toolkit-0.1.1/synap_nemo_agent_toolkit/__init__.py +27 -0
- maximem_synap_nemo_agent_toolkit-0.1.1/synap_nemo_agent_toolkit/editor.py +219 -0
- maximem_synap_nemo_agent_toolkit-0.1.1/synap_nemo_agent_toolkit/plugin.py +108 -0
- maximem_synap_nemo_agent_toolkit-0.1.1/synap_nemo_agent_toolkit/register.py +13 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: maximem-synap-nemo-agent-toolkit
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Synap memory plugin for NVIDIA NeMo Agent Toolkit (NAT)
|
|
5
|
+
Author: Synap Team
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Keywords: synap,memory,nemo,nvidia,nat,agents,ai
|
|
8
|
+
Requires-Python: <3.14,>=3.11
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: maximem-synap>=0.2.0
|
|
11
|
+
Requires-Dist: synap-integrations-common>=0.1.0
|
|
12
|
+
Requires-Dist: nvidia-nat-core>=1.0
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
15
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
16
|
+
|
|
17
|
+
# synap-nemo-agent-toolkit
|
|
18
|
+
|
|
19
|
+
Synap memory plugin for **NVIDIA NeMo Agent Toolkit (NAT)**.
|
|
20
|
+
|
|
21
|
+
Implements `nat.memory.interfaces.MemoryEditor` so that NAT workflows can
|
|
22
|
+
use Synap as long-term memory out of the box — either programmatically,
|
|
23
|
+
or via a single YAML line:
|
|
24
|
+
|
|
25
|
+
```yaml
|
|
26
|
+
memory:
|
|
27
|
+
synap:
|
|
28
|
+
_type: synap_memory
|
|
29
|
+
customer_id: "acme" # optional scope
|
|
30
|
+
mode: "accurate" # or "fast"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install synap-nemo-agent-toolkit
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Depends on `maximem-synap>=0.2.0`, `synap-integrations-common>=0.1.0`,
|
|
40
|
+
and `nvidia-nat-core>=1.0`.
|
|
41
|
+
|
|
42
|
+
## Use (programmatic)
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from maximem_synap import MaximemSynapSDK
|
|
46
|
+
from nat.memory.models import MemoryItem
|
|
47
|
+
from synap_nemo_agent_toolkit import SynapMemoryEditor
|
|
48
|
+
|
|
49
|
+
sdk = MaximemSynapSDK(api_key="...")
|
|
50
|
+
await sdk.initialize()
|
|
51
|
+
|
|
52
|
+
editor = SynapMemoryEditor(sdk=sdk, customer_id="acme", mode="accurate")
|
|
53
|
+
|
|
54
|
+
# Write
|
|
55
|
+
await editor.add_items([
|
|
56
|
+
MemoryItem(
|
|
57
|
+
user_id="alice",
|
|
58
|
+
memory="Prefers tea over coffee",
|
|
59
|
+
tags=["beverage", "preference"],
|
|
60
|
+
),
|
|
61
|
+
])
|
|
62
|
+
|
|
63
|
+
# Read — user_id is required on every search
|
|
64
|
+
hits = await editor.search("beverage preference", top_k=5, user_id="alice")
|
|
65
|
+
for hit in hits:
|
|
66
|
+
print(hit.memory, hit.tags, hit.similarity_score)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Use (YAML / NAT workflow)
|
|
70
|
+
|
|
71
|
+
Set `SYNAP_API_KEY` (optionally `SYNAP_INSTANCE_ID`) in the environment
|
|
72
|
+
and declare the memory provider in the workflow config:
|
|
73
|
+
|
|
74
|
+
```yaml
|
|
75
|
+
memory:
|
|
76
|
+
synap:
|
|
77
|
+
_type: synap_memory
|
|
78
|
+
customer_id: "acme"
|
|
79
|
+
mode: "accurate"
|
|
80
|
+
# Optional — falls back to SYNAP_API_KEY env if omitted
|
|
81
|
+
# api_key: "${SYNAP_API_KEY}"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
NAT's plugin loader picks up `synap_memory` via the `nat.components`
|
|
85
|
+
entry-point registered in this package's `pyproject.toml`.
|
|
86
|
+
|
|
87
|
+
## Error policy
|
|
88
|
+
|
|
89
|
+
| Operation | Synap failure | Why |
|
|
90
|
+
|-----------------|------------------------|-------------------------------------------|
|
|
91
|
+
| `add_items` | `SynapIntegrationError`| Writes must be observable |
|
|
92
|
+
| `search` | Returns `[]` | A read blip must never crash an agent turn |
|
|
93
|
+
| `remove_items` | Warn once, no-op | Synap has no public delete API |
|
|
94
|
+
|
|
95
|
+
Every write is tagged with an internal marker so `search` filters out
|
|
96
|
+
non-NAT memories that happen to share the same user scope — the
|
|
97
|
+
`MemoryItem` stream stays NAT-owned.
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
Apache-2.0.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# synap-nemo-agent-toolkit
|
|
2
|
+
|
|
3
|
+
Synap memory plugin for **NVIDIA NeMo Agent Toolkit (NAT)**.
|
|
4
|
+
|
|
5
|
+
Implements `nat.memory.interfaces.MemoryEditor` so that NAT workflows can
|
|
6
|
+
use Synap as long-term memory out of the box — either programmatically,
|
|
7
|
+
or via a single YAML line:
|
|
8
|
+
|
|
9
|
+
```yaml
|
|
10
|
+
memory:
|
|
11
|
+
synap:
|
|
12
|
+
_type: synap_memory
|
|
13
|
+
customer_id: "acme" # optional scope
|
|
14
|
+
mode: "accurate" # or "fast"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install synap-nemo-agent-toolkit
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Depends on `maximem-synap>=0.2.0`, `synap-integrations-common>=0.1.0`,
|
|
24
|
+
and `nvidia-nat-core>=1.0`.
|
|
25
|
+
|
|
26
|
+
## Use (programmatic)
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from maximem_synap import MaximemSynapSDK
|
|
30
|
+
from nat.memory.models import MemoryItem
|
|
31
|
+
from synap_nemo_agent_toolkit import SynapMemoryEditor
|
|
32
|
+
|
|
33
|
+
sdk = MaximemSynapSDK(api_key="...")
|
|
34
|
+
await sdk.initialize()
|
|
35
|
+
|
|
36
|
+
editor = SynapMemoryEditor(sdk=sdk, customer_id="acme", mode="accurate")
|
|
37
|
+
|
|
38
|
+
# Write
|
|
39
|
+
await editor.add_items([
|
|
40
|
+
MemoryItem(
|
|
41
|
+
user_id="alice",
|
|
42
|
+
memory="Prefers tea over coffee",
|
|
43
|
+
tags=["beverage", "preference"],
|
|
44
|
+
),
|
|
45
|
+
])
|
|
46
|
+
|
|
47
|
+
# Read — user_id is required on every search
|
|
48
|
+
hits = await editor.search("beverage preference", top_k=5, user_id="alice")
|
|
49
|
+
for hit in hits:
|
|
50
|
+
print(hit.memory, hit.tags, hit.similarity_score)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Use (YAML / NAT workflow)
|
|
54
|
+
|
|
55
|
+
Set `SYNAP_API_KEY` (optionally `SYNAP_INSTANCE_ID`) in the environment
|
|
56
|
+
and declare the memory provider in the workflow config:
|
|
57
|
+
|
|
58
|
+
```yaml
|
|
59
|
+
memory:
|
|
60
|
+
synap:
|
|
61
|
+
_type: synap_memory
|
|
62
|
+
customer_id: "acme"
|
|
63
|
+
mode: "accurate"
|
|
64
|
+
# Optional — falls back to SYNAP_API_KEY env if omitted
|
|
65
|
+
# api_key: "${SYNAP_API_KEY}"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
NAT's plugin loader picks up `synap_memory` via the `nat.components`
|
|
69
|
+
entry-point registered in this package's `pyproject.toml`.
|
|
70
|
+
|
|
71
|
+
## Error policy
|
|
72
|
+
|
|
73
|
+
| Operation | Synap failure | Why |
|
|
74
|
+
|-----------------|------------------------|-------------------------------------------|
|
|
75
|
+
| `add_items` | `SynapIntegrationError`| Writes must be observable |
|
|
76
|
+
| `search` | Returns `[]` | A read blip must never crash an agent turn |
|
|
77
|
+
| `remove_items` | Warn once, no-op | Synap has no public delete API |
|
|
78
|
+
|
|
79
|
+
Every write is tagged with an internal marker so `search` filters out
|
|
80
|
+
non-NAT memories that happen to share the same user scope — the
|
|
81
|
+
`MemoryItem` stream stays NAT-owned.
|
|
82
|
+
|
|
83
|
+
## License
|
|
84
|
+
|
|
85
|
+
Apache-2.0.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: maximem-synap-nemo-agent-toolkit
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Synap memory plugin for NVIDIA NeMo Agent Toolkit (NAT)
|
|
5
|
+
Author: Synap Team
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Keywords: synap,memory,nemo,nvidia,nat,agents,ai
|
|
8
|
+
Requires-Python: <3.14,>=3.11
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: maximem-synap>=0.2.0
|
|
11
|
+
Requires-Dist: synap-integrations-common>=0.1.0
|
|
12
|
+
Requires-Dist: nvidia-nat-core>=1.0
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
15
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
16
|
+
|
|
17
|
+
# synap-nemo-agent-toolkit
|
|
18
|
+
|
|
19
|
+
Synap memory plugin for **NVIDIA NeMo Agent Toolkit (NAT)**.
|
|
20
|
+
|
|
21
|
+
Implements `nat.memory.interfaces.MemoryEditor` so that NAT workflows can
|
|
22
|
+
use Synap as long-term memory out of the box — either programmatically,
|
|
23
|
+
or via a single YAML line:
|
|
24
|
+
|
|
25
|
+
```yaml
|
|
26
|
+
memory:
|
|
27
|
+
synap:
|
|
28
|
+
_type: synap_memory
|
|
29
|
+
customer_id: "acme" # optional scope
|
|
30
|
+
mode: "accurate" # or "fast"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install synap-nemo-agent-toolkit
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Depends on `maximem-synap>=0.2.0`, `synap-integrations-common>=0.1.0`,
|
|
40
|
+
and `nvidia-nat-core>=1.0`.
|
|
41
|
+
|
|
42
|
+
## Use (programmatic)
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from maximem_synap import MaximemSynapSDK
|
|
46
|
+
from nat.memory.models import MemoryItem
|
|
47
|
+
from synap_nemo_agent_toolkit import SynapMemoryEditor
|
|
48
|
+
|
|
49
|
+
sdk = MaximemSynapSDK(api_key="...")
|
|
50
|
+
await sdk.initialize()
|
|
51
|
+
|
|
52
|
+
editor = SynapMemoryEditor(sdk=sdk, customer_id="acme", mode="accurate")
|
|
53
|
+
|
|
54
|
+
# Write
|
|
55
|
+
await editor.add_items([
|
|
56
|
+
MemoryItem(
|
|
57
|
+
user_id="alice",
|
|
58
|
+
memory="Prefers tea over coffee",
|
|
59
|
+
tags=["beverage", "preference"],
|
|
60
|
+
),
|
|
61
|
+
])
|
|
62
|
+
|
|
63
|
+
# Read — user_id is required on every search
|
|
64
|
+
hits = await editor.search("beverage preference", top_k=5, user_id="alice")
|
|
65
|
+
for hit in hits:
|
|
66
|
+
print(hit.memory, hit.tags, hit.similarity_score)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Use (YAML / NAT workflow)
|
|
70
|
+
|
|
71
|
+
Set `SYNAP_API_KEY` (optionally `SYNAP_INSTANCE_ID`) in the environment
|
|
72
|
+
and declare the memory provider in the workflow config:
|
|
73
|
+
|
|
74
|
+
```yaml
|
|
75
|
+
memory:
|
|
76
|
+
synap:
|
|
77
|
+
_type: synap_memory
|
|
78
|
+
customer_id: "acme"
|
|
79
|
+
mode: "accurate"
|
|
80
|
+
# Optional — falls back to SYNAP_API_KEY env if omitted
|
|
81
|
+
# api_key: "${SYNAP_API_KEY}"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
NAT's plugin loader picks up `synap_memory` via the `nat.components`
|
|
85
|
+
entry-point registered in this package's `pyproject.toml`.
|
|
86
|
+
|
|
87
|
+
## Error policy
|
|
88
|
+
|
|
89
|
+
| Operation | Synap failure | Why |
|
|
90
|
+
|-----------------|------------------------|-------------------------------------------|
|
|
91
|
+
| `add_items` | `SynapIntegrationError`| Writes must be observable |
|
|
92
|
+
| `search` | Returns `[]` | A read blip must never crash an agent turn |
|
|
93
|
+
| `remove_items` | Warn once, no-op | Synap has no public delete API |
|
|
94
|
+
|
|
95
|
+
Every write is tagged with an internal marker so `search` filters out
|
|
96
|
+
non-NAT memories that happen to share the same user scope — the
|
|
97
|
+
`MemoryItem` stream stays NAT-owned.
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
Apache-2.0.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
maximem_synap_nemo_agent_toolkit.egg-info/PKG-INFO
|
|
4
|
+
maximem_synap_nemo_agent_toolkit.egg-info/SOURCES.txt
|
|
5
|
+
maximem_synap_nemo_agent_toolkit.egg-info/dependency_links.txt
|
|
6
|
+
maximem_synap_nemo_agent_toolkit.egg-info/entry_points.txt
|
|
7
|
+
maximem_synap_nemo_agent_toolkit.egg-info/requires.txt
|
|
8
|
+
maximem_synap_nemo_agent_toolkit.egg-info/top_level.txt
|
|
9
|
+
synap_nemo_agent_toolkit/__init__.py
|
|
10
|
+
synap_nemo_agent_toolkit/editor.py
|
|
11
|
+
synap_nemo_agent_toolkit/plugin.py
|
|
12
|
+
synap_nemo_agent_toolkit/register.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
maximem_synap_nemo_agent_toolkit-0.1.1/maximem_synap_nemo_agent_toolkit.egg-info/top_level.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
synap_nemo_agent_toolkit
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "maximem-synap-nemo-agent-toolkit"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Synap memory plugin for NVIDIA NeMo Agent Toolkit (NAT)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11,<3.14"
|
|
11
|
+
license = "Apache-2.0"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Synap Team"}
|
|
14
|
+
]
|
|
15
|
+
keywords = ["synap", "memory", "nemo", "nvidia", "nat", "agents", "ai"]
|
|
16
|
+
|
|
17
|
+
dependencies = [
|
|
18
|
+
"maximem-synap>=0.2.0",
|
|
19
|
+
"synap-integrations-common>=0.1.0",
|
|
20
|
+
"nvidia-nat-core>=1.0",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
dev = [
|
|
25
|
+
"pytest>=7.0",
|
|
26
|
+
"pytest-asyncio>=0.21",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.entry-points."nat.components"]
|
|
30
|
+
synap_memory = "synap_nemo_agent_toolkit.register"
|
|
31
|
+
|
|
32
|
+
[tool.setuptools.packages.find]
|
|
33
|
+
where = ["."]
|
|
34
|
+
include = ["synap_nemo_agent_toolkit*"]
|
|
35
|
+
|
|
36
|
+
[tool.pytest.ini_options]
|
|
37
|
+
testpaths = ["tests"]
|
|
38
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Synap memory plugin for NVIDIA NeMo Agent Toolkit (NAT).
|
|
2
|
+
|
|
3
|
+
Two integration paths:
|
|
4
|
+
|
|
5
|
+
1. **Programmatic** — construct a :class:`MaximemSynapSDK` yourself and
|
|
6
|
+
wrap it in :class:`SynapMemoryEditor` to plug into any NAT surface
|
|
7
|
+
that expects a :class:`nat.memory.interfaces.MemoryEditor`.
|
|
8
|
+
|
|
9
|
+
2. **YAML-wired** — declare ``_type: synap_memory`` in a NAT workflow's
|
|
10
|
+
``memory:`` block. NAT's plugin loader imports
|
|
11
|
+
:mod:`synap_nemo_agent_toolkit.register` via the ``nat.components``
|
|
12
|
+
entry-point, which pulls in :class:`SynapMemoryClientConfig` and the
|
|
13
|
+
``@register_memory`` factory.
|
|
14
|
+
|
|
15
|
+
Error policy (matches every other Synap integration):
|
|
16
|
+
|
|
17
|
+
- Reads (``search``) degrade gracefully — a Synap blip returns ``[]``
|
|
18
|
+
rather than crashing the agent turn.
|
|
19
|
+
- Writes (``add_items``) surface as
|
|
20
|
+
:class:`synap_integrations_common.SynapIntegrationError`.
|
|
21
|
+
- Deletes (``remove_items``) warn once and no-op — Synap has no public
|
|
22
|
+
delete API.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from synap_nemo_agent_toolkit.editor import SynapMemoryEditor
|
|
26
|
+
|
|
27
|
+
__all__ = ["SynapMemoryEditor"]
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""SynapMemoryEditor — NAT :class:`MemoryEditor` implementation backed by Synap.
|
|
2
|
+
|
|
3
|
+
NAT's memory contract is three async methods on
|
|
4
|
+
:class:`nat.memory.interfaces.MemoryEditor`:
|
|
5
|
+
|
|
6
|
+
- ``add_items(items)`` → writes
|
|
7
|
+
- ``search(query, top_k, **kw)`` → reads
|
|
8
|
+
- ``remove_items(**kw)`` → deletes (no-op on Synap; see below)
|
|
9
|
+
|
|
10
|
+
Error policy mirrors every other Synap integration:
|
|
11
|
+
|
|
12
|
+
- **Writes** (``add_items``): failures surface as
|
|
13
|
+
:class:`SynapIntegrationError` so ingestion outages are observable.
|
|
14
|
+
- **Reads** (``search``): failures degrade gracefully — log at ERROR and
|
|
15
|
+
return ``[]``. A Synap blip should never crash an agent turn.
|
|
16
|
+
- **Deletes** (``remove_items``): Synap has no public delete API. We warn
|
|
17
|
+
once and no-op, same contract used by synap-crewai and synap-agno.
|
|
18
|
+
|
|
19
|
+
We tag every Synap memory we create with a marker key so that future
|
|
20
|
+
NAT searches can distinguish NAT-originated records from other
|
|
21
|
+
memories written to the same user's scope.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import asyncio
|
|
27
|
+
import logging
|
|
28
|
+
from typing import Any, Optional
|
|
29
|
+
|
|
30
|
+
from maximem_synap import MaximemSynapSDK
|
|
31
|
+
from nat.memory.interfaces import MemoryEditor
|
|
32
|
+
from nat.memory.models import MemoryItem
|
|
33
|
+
|
|
34
|
+
from synap_integrations_common import wrap_sdk_errors_async
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
# Metadata marker — every Synap memory NAT creates carries this, so we
|
|
39
|
+
# can filter them back out on search and avoid leaking non-NAT facts
|
|
40
|
+
# into NAT's MemoryItem stream.
|
|
41
|
+
_MARKER = "nat_memory_item"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SynapMemoryEditor(MemoryEditor):
|
|
45
|
+
"""Synap-backed NAT :class:`MemoryEditor`.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
sdk: Configured :class:`MaximemSynapSDK`.
|
|
49
|
+
customer_id: Optional customer/org scope. Empty string means
|
|
50
|
+
customer-less (forwarded to the SDK as ``None``).
|
|
51
|
+
mode: Synap fetch mode — ``"accurate"`` (default) or ``"fast"``.
|
|
52
|
+
document_type: ``document_type`` stamped on every ``memories.create``
|
|
53
|
+
write. Defaults to ``"ai-chat-conversation"`` to match the
|
|
54
|
+
other integrations.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
sdk: MaximemSynapSDK,
|
|
60
|
+
*,
|
|
61
|
+
customer_id: str = "",
|
|
62
|
+
mode: str = "accurate",
|
|
63
|
+
document_type: str = "ai-chat-conversation",
|
|
64
|
+
) -> None:
|
|
65
|
+
if sdk is None:
|
|
66
|
+
raise ValueError("SynapMemoryEditor requires a non-None sdk")
|
|
67
|
+
self.sdk = sdk
|
|
68
|
+
self.customer_id = customer_id
|
|
69
|
+
self.mode = mode
|
|
70
|
+
self.document_type = document_type
|
|
71
|
+
self._delete_warned = False
|
|
72
|
+
|
|
73
|
+
# ── writes ─────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
async def add_items(self, items: list[MemoryItem]) -> None:
|
|
76
|
+
"""Insert one ``memories.create`` per ``MemoryItem``.
|
|
77
|
+
|
|
78
|
+
Each item's ``user_id`` is required (Synap memory is user-scoped).
|
|
79
|
+
We write ``item.memory`` when present, otherwise fall back to a
|
|
80
|
+
joined string of the conversation's ``content`` fields — NAT's
|
|
81
|
+
Mem0 path accepts conversation-only items, so we must too.
|
|
82
|
+
"""
|
|
83
|
+
if not items:
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
async def _one(item: MemoryItem) -> None:
|
|
87
|
+
if not item.user_id:
|
|
88
|
+
raise ValueError("MemoryItem.user_id is required for Synap writes")
|
|
89
|
+
document = item.memory or _conversation_to_text(item.conversation)
|
|
90
|
+
if not document:
|
|
91
|
+
# Nothing worth recording — skip rather than writing empty
|
|
92
|
+
# documents that pollute fetch results.
|
|
93
|
+
return
|
|
94
|
+
metadata: dict[str, Any] = {
|
|
95
|
+
_MARKER: True,
|
|
96
|
+
"tags": list(item.tags or []),
|
|
97
|
+
**(item.metadata or {}),
|
|
98
|
+
}
|
|
99
|
+
if item.conversation:
|
|
100
|
+
metadata["conversation"] = list(item.conversation)
|
|
101
|
+
|
|
102
|
+
async with wrap_sdk_errors_async(
|
|
103
|
+
"nemo_agent_toolkit.add_items",
|
|
104
|
+
logger,
|
|
105
|
+
user_id=item.user_id,
|
|
106
|
+
tags=item.tags,
|
|
107
|
+
):
|
|
108
|
+
await self.sdk.memories.create(
|
|
109
|
+
document=document,
|
|
110
|
+
user_id=item.user_id,
|
|
111
|
+
customer_id=self.customer_id or "",
|
|
112
|
+
document_type=self.document_type,
|
|
113
|
+
metadata=metadata,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Fan out concurrently — mirrors Mem0Editor's asyncio.gather shape
|
|
117
|
+
# so throughput on bulk add_items matches the reference plugin.
|
|
118
|
+
await asyncio.gather(*(_one(item) for item in items))
|
|
119
|
+
|
|
120
|
+
# ── reads ──────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
async def search(self, query: str, top_k: int = 5, **kwargs) -> list[MemoryItem]:
|
|
123
|
+
"""Query Synap and map hits to :class:`MemoryItem`.
|
|
124
|
+
|
|
125
|
+
The ``user_id`` kwarg is required — Synap memory is user-scoped.
|
|
126
|
+
On SDK failure we log and return an empty list so the agent turn
|
|
127
|
+
can continue without long-term memory.
|
|
128
|
+
"""
|
|
129
|
+
user_id = kwargs.pop("user_id", None)
|
|
130
|
+
if not user_id:
|
|
131
|
+
# Mem0Editor also requires user_id in kwargs; be consistent.
|
|
132
|
+
raise ValueError(
|
|
133
|
+
"SynapMemoryEditor.search requires user_id in kwargs"
|
|
134
|
+
)
|
|
135
|
+
customer_id = kwargs.pop("customer_id", None) or self.customer_id or None
|
|
136
|
+
include_conversation_context = bool(
|
|
137
|
+
kwargs.pop("include_conversation_context", False)
|
|
138
|
+
)
|
|
139
|
+
tag_filter = kwargs.pop("tag_filter", None)
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
response = await self.sdk.fetch(
|
|
143
|
+
user_id=user_id,
|
|
144
|
+
customer_id=customer_id,
|
|
145
|
+
search_query=[query] if query else None,
|
|
146
|
+
max_results=max(int(top_k), 1),
|
|
147
|
+
mode=self.mode,
|
|
148
|
+
include_conversation_context=include_conversation_context,
|
|
149
|
+
)
|
|
150
|
+
except Exception as exc: # noqa: BLE001 — read-side graceful degrade
|
|
151
|
+
logger.error(
|
|
152
|
+
"SynapMemoryEditor.search: sdk.fetch failed user_id=%s error=%s",
|
|
153
|
+
user_id, exc, exc_info=True,
|
|
154
|
+
)
|
|
155
|
+
return []
|
|
156
|
+
|
|
157
|
+
facts = list(getattr(response, "facts", None) or [])
|
|
158
|
+
items: list[MemoryItem] = []
|
|
159
|
+
for fact in facts:
|
|
160
|
+
# Synap does not echo back custom metadata written with
|
|
161
|
+
# memories.create(), so _MARKER-based filtering is not possible.
|
|
162
|
+
# All facts for the user/customer scope are returned as-is.
|
|
163
|
+
md = getattr(fact, "metadata", None) or {}
|
|
164
|
+
if tag_filter:
|
|
165
|
+
fact_tags = md.get("tags") or []
|
|
166
|
+
if not any(t in fact_tags for t in tag_filter):
|
|
167
|
+
continue
|
|
168
|
+
items.append(_fact_to_memory_item(fact, user_id=user_id))
|
|
169
|
+
return items
|
|
170
|
+
|
|
171
|
+
# ── deletes (warn + no-op) ─────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
async def remove_items(self, **kwargs) -> None:
|
|
174
|
+
"""Warn once, then no-op — Synap has no public delete surface."""
|
|
175
|
+
if not self._delete_warned:
|
|
176
|
+
logger.warning(
|
|
177
|
+
"SynapMemoryEditor: Synap has no public delete API. "
|
|
178
|
+
"remove_items is a no-op. This warning fires once."
|
|
179
|
+
)
|
|
180
|
+
self._delete_warned = True
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# ── helpers ────────────────────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _conversation_to_text(conversation: Optional[list[dict[str, str]]]) -> str:
|
|
187
|
+
if not conversation:
|
|
188
|
+
return ""
|
|
189
|
+
lines = []
|
|
190
|
+
for msg in conversation:
|
|
191
|
+
role = msg.get("role", "?")
|
|
192
|
+
content = msg.get("content", "")
|
|
193
|
+
if content:
|
|
194
|
+
lines.append(f"{role}: {content}")
|
|
195
|
+
return "\n".join(lines)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _fact_to_memory_item(fact: Any, *, user_id: str) -> MemoryItem:
|
|
199
|
+
md = getattr(fact, "metadata", None) or {}
|
|
200
|
+
tags = md.get("tags") or []
|
|
201
|
+
if not isinstance(tags, list):
|
|
202
|
+
tags = []
|
|
203
|
+
conversation = md.get("conversation")
|
|
204
|
+
if conversation is not None and not isinstance(conversation, list):
|
|
205
|
+
conversation = None
|
|
206
|
+
# Strip our internal marker + conversation copy so downstream consumers
|
|
207
|
+
# don't see implementation detail in MemoryItem.metadata.
|
|
208
|
+
clean_metadata = {k: v for k, v in md.items() if k not in (_MARKER, "tags", "conversation")}
|
|
209
|
+
return MemoryItem(
|
|
210
|
+
conversation=conversation,
|
|
211
|
+
user_id=user_id,
|
|
212
|
+
memory=getattr(fact, "content", None) or None,
|
|
213
|
+
tags=list(tags),
|
|
214
|
+
metadata=clean_metadata,
|
|
215
|
+
similarity_score=getattr(fact, "confidence", None),
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
__all__ = ["SynapMemoryEditor"]
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""NAT ``register_memory`` factory for YAML-wired Synap memory.
|
|
2
|
+
|
|
3
|
+
Drop ``_type: synap_memory`` into a NAT workflow's ``memory:`` block and
|
|
4
|
+
the toolkit will construct a :class:`SynapMemoryEditor` from env vars
|
|
5
|
+
(``SYNAP_API_KEY`` / optional ``SYNAP_INSTANCE_ID``). Config fields let
|
|
6
|
+
operators override the scope, fetch mode, and document type from YAML.
|
|
7
|
+
|
|
8
|
+
Example YAML::
|
|
9
|
+
|
|
10
|
+
memory:
|
|
11
|
+
synap:
|
|
12
|
+
_type: synap_memory
|
|
13
|
+
customer_id: "acme"
|
|
14
|
+
mode: "accurate"
|
|
15
|
+
|
|
16
|
+
Programmatic callers that want to share a pre-constructed SDK should
|
|
17
|
+
instantiate :class:`SynapMemoryEditor` directly instead.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
import os
|
|
24
|
+
from typing import Optional
|
|
25
|
+
|
|
26
|
+
from pydantic import Field
|
|
27
|
+
|
|
28
|
+
from nat.builder.builder import Builder
|
|
29
|
+
from nat.cli.register_workflow import register_memory
|
|
30
|
+
from nat.data_models.common import OptionalSecretStr, get_secret_value
|
|
31
|
+
from nat.data_models.memory import MemoryBaseConfig
|
|
32
|
+
|
|
33
|
+
from maximem_synap import MaximemSynapSDK
|
|
34
|
+
|
|
35
|
+
from synap_nemo_agent_toolkit.editor import SynapMemoryEditor
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SynapMemoryClientConfig(MemoryBaseConfig, name="synap_memory"):
|
|
41
|
+
"""YAML-wired config for the Synap memory provider."""
|
|
42
|
+
|
|
43
|
+
customer_id: str = Field(
|
|
44
|
+
default="",
|
|
45
|
+
description="Optional customer/org scope. Empty means customer-less.",
|
|
46
|
+
)
|
|
47
|
+
mode: str = Field(
|
|
48
|
+
default="accurate",
|
|
49
|
+
description='Synap fetch mode: "accurate" or "fast".',
|
|
50
|
+
)
|
|
51
|
+
document_type: str = Field(
|
|
52
|
+
default="ai-chat-conversation",
|
|
53
|
+
description="document_type stamped on every Synap memories.create write.",
|
|
54
|
+
)
|
|
55
|
+
api_key: OptionalSecretStr = Field(
|
|
56
|
+
default=None,
|
|
57
|
+
description=(
|
|
58
|
+
"Synap API key. When omitted, falls back to SYNAP_API_KEY env var."
|
|
59
|
+
),
|
|
60
|
+
)
|
|
61
|
+
instance_id: str = Field(
|
|
62
|
+
default="",
|
|
63
|
+
description=(
|
|
64
|
+
"Optional Synap instance ID. When empty the SDK resolves it from "
|
|
65
|
+
"the API key via /auth/whoami. Falls back to SYNAP_INSTANCE_ID env."
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@register_memory(config_type=SynapMemoryClientConfig)
|
|
71
|
+
async def synap_memory_client(config: SynapMemoryClientConfig, builder: Builder):
|
|
72
|
+
"""Construct + initialize a Synap SDK, yield a :class:`SynapMemoryEditor`."""
|
|
73
|
+
api_key: Optional[str] = get_secret_value(config.api_key) or os.environ.get(
|
|
74
|
+
"SYNAP_API_KEY"
|
|
75
|
+
)
|
|
76
|
+
if api_key is None:
|
|
77
|
+
raise RuntimeError(
|
|
78
|
+
"Synap API key is not set. Provide it via SynapMemoryClientConfig.api_key "
|
|
79
|
+
"or the SYNAP_API_KEY environment variable."
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
sdk = MaximemSynapSDK(
|
|
83
|
+
instance_id=config.instance_id or os.environ.get("SYNAP_INSTANCE_ID", ""),
|
|
84
|
+
api_key=api_key,
|
|
85
|
+
_force_new=True,
|
|
86
|
+
)
|
|
87
|
+
await sdk.initialize()
|
|
88
|
+
|
|
89
|
+
editor = SynapMemoryEditor(
|
|
90
|
+
sdk=sdk,
|
|
91
|
+
customer_id=config.customer_id,
|
|
92
|
+
mode=config.mode,
|
|
93
|
+
document_type=config.document_type,
|
|
94
|
+
)
|
|
95
|
+
try:
|
|
96
|
+
yield editor
|
|
97
|
+
finally:
|
|
98
|
+
close = getattr(sdk, "close", None)
|
|
99
|
+
if close is not None:
|
|
100
|
+
try:
|
|
101
|
+
await close()
|
|
102
|
+
except Exception: # noqa: BLE001 — teardown must not mask workflow errors
|
|
103
|
+
logger.exception(
|
|
104
|
+
"synap_memory_client: SDK teardown raised (suppressing)"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
__all__ = ["SynapMemoryClientConfig", "synap_memory_client"]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""NAT plugin entry point.
|
|
2
|
+
|
|
3
|
+
Importing this module has the side effect of registering
|
|
4
|
+
:class:`SynapMemoryClientConfig` + the ``synap_memory`` YAML ``_type``
|
|
5
|
+
with NAT's :class:`GlobalTypeRegistry`. Wired via the ``nat.components``
|
|
6
|
+
entry-point in ``pyproject.toml`` so NAT picks it up automatically on
|
|
7
|
+
workflow load.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
# flake8: noqa — side-effectful import; don't remove
|
|
11
|
+
# isort:skip_file
|
|
12
|
+
|
|
13
|
+
from . import plugin # noqa: F401 — registers @register_memory
|