Use NAMS (hosted backend)
How to configure MemoryClient to talk to the Neo4j Agent Memory Service (NAMS) — Neo4j’s hosted memory API — instead of a direct Neo4j cluster.
NAMS is a managed REST service backed by Neo4j. You don’t run the database; you just point MemoryClient at the service with an API key. The rest of the library API is unchanged. For the conceptual comparison with bolt, see Bolt vs NAMS.
Prerequisites
-
Python 3.10+
-
A NAMS API key — sign up at memory.neo4jlabs.com. Keys start with
nams_.
Install
uv pip install 'neo4j-agent-memory[nams]>=0.4.0'
The [nams] extra pulls in httpx. If you already have httpx (or any framework integration that depends on it), you don’t strictly need the extra — the import is lazy.
Configure
The shortest path: set one environment variable.
export MEMORY_API_KEY=nams_xxxxxxxxxxxxxxxx
That’s enough — MemoryClient() with no args picks up the env var and switches the backend to NAMS:
from neo4j_agent_memory import MemoryClient
async with MemoryClient() as client:
conversation = await client.short_term.create_conversation("session-1")
conversation_id = str(conversation.id)
await client.short_term.add_message(conversation_id, "user", "Hello!")
Or be explicit:
from pydantic import SecretStr
from neo4j_agent_memory import MemoryClient, MemorySettings, NamsConfig
settings = MemorySettings(
backend="nams",
nams=NamsConfig(api_key=SecretStr("nams_xxxxxxxxxxxx")),
)
async with MemoryClient(settings) as client:
conversation = await client.short_term.create_conversation("session-1")
conversation_id = str(conversation.id)
await client.short_term.add_message(conversation_id, "user", "Hello!")
NamsConfig fields
| Field | Default | Description |
|---|---|---|
|
Base URL. The |
|
|
|
Your NAMS API key ( |
|
|
HTTP request timeout in seconds. |
|
|
Retry budget for 429, 5xx, and network errors. |
|
|
Base exponential backoff (0.5, 1.0, 2.0, …). |
|
|
Extra HTTP headers (e.g. tracing IDs). |
|
|
If True, |
|
|
Override wire-protocol detection: |
Env var reference
| Variable | Purpose |
|---|---|
|
NAMS API key. When set and |
|
Override the default endpoint URL. |
|
Force the backend ( |
|
Per-field overrides on |
First request
import asyncio
from neo4j_agent_memory import MemoryClient
async def main() -> None:
async with MemoryClient() as client:
conversation = await client.short_term.create_conversation("first-request-demo")
conversation_id = str(conversation.id)
# Short-term
msg = await client.short_term.add_message(
conversation_id, "user", "I love Italian food."
)
print(f"Stored message {msg.id}")
# Long-term
entity = await client.long_term.add_entity(
"Alice", "PERSON", description="The user introducing themselves"
)
print(f"Stored entity {entity.name}")
# Reasoning
trace = await client.reasoning.start_trace(
conversation_id, "Recommend a restaurant"
)
await client.reasoning.complete_trace(
trace.id, outcome="Done", success=True
)
# Read-only Cypher (Platinum)
rows = await client.query.cypher(
"MATCH (e:Entity) RETURN e.name AS name LIMIT 5"
)
print(rows)
asyncio.run(main())
Custom endpoint (private deployments)
If you run NAMS on-prem or in a private cloud, override endpoint:
settings = MemorySettings(
backend="nams",
nams=NamsConfig(
endpoint="https://nams.internal.example.com/v1",
api_key=SecretStr("nams_xxx"),
),
)
Or set MEMORY_ENDPOINT in the environment.
Multi-tenancy
NAMS scopes data two ways:
-
Workspace — implicit in the API key. Different keys = different workspaces.
-
User — explicit per-call. Pass
user_identifier=oncreate_conversation,add_message,start_trace,set_entity_feedback, etc.; it’s forwarded to NAMS asuserId.
conversation = await client.short_term.create_conversation(
"session-alice",
user_identifier="alice-user-id",
)
conversation_id = str(conversation.id)
await client.short_term.add_message(
conversation_id,
"user",
"Hi from Alice.",
user_identifier="alice-user-id",
)
What’s not available on NAMS
These accessors / methods raise NotSupportedError because NAMS handles them server-side or doesn’t expose them at all:
-
client.graph— useclient.query.cypherinstead. -
client.users(UserMemory). -
client.buffered(BufferedWriter). -
client.consolidation(dedupe / archival). -
client.schema.adopt_existing_graph(). -
client.long_term.geocode_locations(),search_locations_near(),search_locations_in_bounding_box(). -
client.long_term.find_potential_duplicates(),merge_duplicate_entities(),review_duplicate(). -
client.get_stats(),client.get_graph(),client.get_locations().
If you configure client-side embedding, extraction, resolution, geocoding, or enrichment alongside backend="nams", you’ll see a single UserWarning at connect() time naming the inactive layers — the configs are otherwise ignored (NAMS manages these server-side).
The llm provider config does stay active on NAMS — it’s still used by client.short_term.get_conversation_summary() and any of your own client-side LLM workflows.
For the complete list of NAMS-side methods and their bolt counterparts, see Migrate to NAMS.
Common errors
| Error | When | Fix |
|---|---|---|
|
At construction time |
Set |
|
Probe / first request returns 401 |
Check your key is current; old keys may be revoked |
|
403 |
Key doesn’t have access to the workspace |
|
429 after retries exhausted |
Inspect |
|
Network errors or 5xx after retries |
Check connectivity; verify endpoint URL; service status |
|
Calling a bolt-only feature |
Switch to |
Observability
The NAMS transport emits OpenTelemetry-style spans for every request when a tracer is configured. Span name is nams.http.request; attributes include http.method, http.url, http.status_code, nams.method, nams.protocol, and (when retries fire) nams.retry_count. See the observability how-to.