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.
@@ -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,2 @@
1
+ [nat.components]
2
+ synap_memory = synap_nemo_agent_toolkit.register
@@ -0,0 +1,7 @@
1
+ maximem-synap>=0.2.0
2
+ synap-integrations-common>=0.1.0
3
+ nvidia-nat-core>=1.0
4
+
5
+ [dev]
6
+ pytest>=7.0
7
+ pytest-asyncio>=0.21
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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