Use with Strands agents
Restore a Strands conversation through Neo4jSessionManager, then choose additional retrieval or tool surfaces only when your application needs them. The selected program verifies a fresh-process history restore before another model request.
1. Prepare the selected environment
Install the AWS CLI before running the identity check below.
Use Python 3.10+, a POSIX shell, a dedicated AuraDB instance and AWS credentials authorized for both the selected Bedrock chat model/inference profile and Titan V2 embeddings. The [strands] extra selects the SDK’s supported 1.x range. The TypeScript integration has different surfaces: see Strands for TypeScript.
Follow the Aura connection setup for a dedicated test instance, then copy its URI, username and password into the exports below.
Create a local folder and virtual environment for the examples on this page. The commands reuse an existing environment without changing its files:
mkdir -p ~/agent-memory-tutorials
cd ~/agent-memory-tutorials
if [ -e .venv ]; then
printf '%s\n' 'Using the existing virtual environment.'
else
python3 -m venv .venv
fi
source .venv/bin/activate
Expected: ~/agent-memory-tutorials is your working directory and its virtual environment is active. Install the published SDK with the command below.
Each complete code block labelled Save as names a file to create in this folder using your editor. Copy the entire block, including imports and the entry point. Expand each helper disclosure and use Copy code to copy its full source. Keep all files together so their imports resolve.
When continuing from another tutorial or guide, retain the existing environment, configuration, session files, and .tutorial-state/. Reuse unchanged helper files; compare an existing file before replacing it, and finish any pending cleanup or recovery before changing the code that owns its state.
python -m pip install 'neo4j-agent-memory[strands,bedrock]==0.7.0'
export AWS_REGION='us-east-1'
export AWS_DEFAULT_REGION="$AWS_REGION"
export BEDROCK_MODEL_ID='replace-with-an-authorized-model-or-inference-profile-id'
export NEO4J_URI='neo4j+s://<instance-id>.databases.neo4j.io'
export NEO4J_USERNAME='neo4j'
export NEO4J_PASSWORD='replace-with-your-Aura-password'
export NEO4J_DATABASE='neo4j'
aws sts get-caller-identity
2. Read the complete session recipe
Save both complete files below in agent-memory-tutorials/. The program imports aura_config from the helper in the same directory.
Open the helper below and save its complete source as aura_connection.py in ~/agent-memory-tutorials.
Show aura_connection.py
aura_connection.py"""Read the Aura connection exported by the documentation setup commands."""
import os
class AuraConfigurationError(ValueError):
"""Missing or invalid tutorial settings, with no credential values in errors."""
def aura_config():
required = ("NEO4J_URI", "NEO4J_USERNAME", "NEO4J_PASSWORD")
missing = [name for name in required if not os.environ.get(name, "").strip()]
if missing:
raise AuraConfigurationError("Export the Aura connection settings: " + ", ".join(missing))
uri = os.environ["NEO4J_URI"]
if not uri.startswith("neo4j+s://"):
raise AuraConfigurationError("NEO4J_URI must use the Aura neo4j+s:// connection scheme")
database = os.environ.get("NEO4J_DATABASE", "neo4j")
if not database.strip():
raise AuraConfigurationError("NEO4J_DATABASE must not be empty")
return {
"uri": uri,
"username": os.environ["NEO4J_USERNAME"],
"password": os.environ["NEO4J_PASSWORD"],
"database": database,
}
Complete strands_memory_tutorial.py
strands_memory_tutorial.py"""Two-process AuraDB conversation persistence using a Strands session manager."""
import argparse
import json
import os
from pathlib import Path
from uuid import uuid4
from aura_connection import aura_config
SESSION_FILE = Path("strands-tutorial-session.txt")
SENTINEL = "The workshop passphrase is cedar-lantern-47."
def inspect_session():
"""Read the saved session directly; never construct an agent or write memory."""
from neo4j import GraphDatabase
session_id = SESSION_FILE.read_text().strip()
if not session_id:
raise RuntimeError("The session file is empty; preserve it and follow recovery")
config = aura_config()
with GraphDatabase.driver(
config["uri"], auth=(config["username"], config["password"])
) as driver:
records, _, _ = driver.execute_query(
"MATCH (c:Conversation {session_id: $session_id}) "
"OPTIONAL MATCH (c)-[:HAS_MESSAGE]->(m:Message) "
"RETURN count(DISTINCT c) AS conversations, "
"collect(DISTINCT m { .id, .role, .content }) AS messages",
session_id=session_id,
database_=config["database"],
routing_="r",
)
if len(records) != 1 or records[0]["conversations"] > 1:
raise RuntimeError(
"Ambiguous session; preserve the file and inspect the dedicated instance"
)
messages = records[0]["messages"]
sentinel_stored = any(
message["role"] == "user" and message["content"] == SENTINEL for message in messages
)
result = {
"session_id": session_id,
"message_ids": [message["id"] for message in messages],
"message_count": len(messages),
"sentinel_stored": sentinel_stored,
}
print(json.dumps(result, indent=2))
print(
"Next: recall may make another model call and store another turn."
if sentinel_stored
else "Not ready for recall. Preserve the session file and follow the recovery instructions."
)
return result
def main():
parser = argparse.ArgumentParser()
parser.add_argument("phase", choices=["record", "inspect", "recall"])
phase = parser.parse_args().phase
if phase == "inspect":
inspect_session()
return
from strands import Agent
from neo4j_agent_memory import BoltSettings
from neo4j_agent_memory.integrations.strands import Neo4jSessionManager
model_id = os.environ.get("BEDROCK_MODEL_ID", "").strip()
if not model_id or model_id.startswith("replace-with-"):
raise ValueError("Export an accessible BEDROCK_MODEL_ID before recording a session")
settings = BoltSettings(
neo4j=aura_config(),
embedding="bedrock/amazon.titan-embed-text-v2:0",
extraction={"extractor_type": "none"},
)
if phase == "record":
if SESSION_FILE.exists():
raise RuntimeError(
"A session file exists; run inspect before choosing recall or recovery"
)
session_id = f"strands-docs-{uuid4().hex[:8]}"
with SESSION_FILE.open("x") as saved:
saved.write(session_id)
else:
session_id = SESSION_FILE.read_text().strip()
with Neo4jSessionManager(session_id, settings=settings, extract_entities=False) as manager:
agent = Agent(
model=model_id,
session_manager=manager,
system_prompt="Use the conversation history to answer. Do not invent a missing passphrase.",
)
restored_text = str(agent.messages)
if phase == "recall":
if SENTINEL not in restored_text:
raise RuntimeError("The stored message was not restored; no recall claim verified")
print("Verified: prior message restored before the model call")
prompt = SENTINEL if phase == "record" else "What workshop passphrase did I give you?"
response = agent(prompt)
print(response)
print(f"Session ID: {session_id}; phase={phase}")
print("Session manager closed after persisting the turn")
if __name__ == "__main__":
main()
The manager is constructed from settings, owns its client and closes it through with. A local state file carries the same actual session ID into the second process. Entity extraction is disabled for this persistence check.
3. Run and verify a fresh process
python strands_memory_tutorial.py record
python strands_memory_tutorial.py recall
Expected on recall: Verified: prior message restored before the model call. If that explicit check fails, a plausible model answer does not prove restoration. For Aura setup and cleanup, follow the complete Strands tutorial.
record refuses to run when strands-tutorial-session.txt already exists, for example after you finished the Strands tutorial in the same folder or after an interrupted first turn. Run the read-only check below, then follow the tutorial’s recovery table for its result:
python strands_memory_tutorial.py inspect
Troubleshooting and cleanup
Confirm account/region/model access separately from authentication. BedrockModel accepts a deployment-specific model ID; aliases do not authorize it. Use a database whose vector dimensions match Titan V2’s 1024 dimensions. Close the session manager to flush its buffered final message, and retain the state file when restarting. Remove the dedicated example database and local strands-tutorial-session.txt only when inspection is complete.
Choose retrieval and sharing behavior
Pick a surface before configuring either class: use Neo4jSessionManager to transparently persist and restore the whole conversation transcript, and add Neo4jMemoryStore only when the agent also needs to pull retrieved memory into its own reasoning loop through MemoryManager. context_graph_tools(…) is a third, pull-based option: the model decides when to call the tools. The subsections below cover each surface, how to combine them, how to share memory across agents and their limits.
Session manager options
Neo4jSessionManager persists and restores transcripts; it does not require the model to invoke a memory tool. One Strands session maps to one Conversation. Its constructor requires exactly one of settings= or memory_client=. Prefer settings-owned lifecycle for a synchronous Strands application.
| Argument | Default | Effect |
|---|---|---|
|
required |
Strands session identifier; maps to one |
|
|
Exactly one. With |
|
|
Scopes writes, and the preference lookup used for injection, to one user. |
|
|
A |
|
|
Runs entity extraction on stored messages (Bolt only; NAMS extracts server-side). |
|
|
Records observed tool-use blocks in reasoning memory for audit. |
|
|
Seconds to wait for each backend call made from Strands' synchronous hooks. |
|
|
Maximum messages restored into the agent. |
Neo4jSessionManager.for_nams(session_id, **kwargs) builds a manager for the hosted service from MEMORY_API_KEY and optional MEMORY_ENDPOINT; Neo4jMemoryStore.for_nams(config) does the same for a store. The Bolt program above uses settings instead.
Inject retrieved context per turn
Pass retrieval_config= to have the session manager search long-term memory on every user message and prepend the matches to that message inside a <user_context> block. The stored message stays the user’s original text, and nothing is prepended when no result passes min_score. Each turn costs one to three extra backend searches, which is why injection is off by default.
This partial snippet reuses session_id and settings from strands_memory_tutorial.py and shows only the manager construction.
from neo4j_agent_memory.integrations.strands import (
Neo4jRetrievalConfig,
Neo4jSessionManager,
)
manager = Neo4jSessionManager(
session_id,
settings=settings,
retrieval_config=Neo4jRetrievalConfig(
top_k=10, # results per memory kind
min_score=0.2, # similarity floor (Bolt only)
include_entities=True,
include_preferences=True,
include_facts=False,
context_tag="user_context", # wrapper tag
),
)
On NAMS only entity search runs: preferences and facts are skipped, and min_score is not applied.
Memory store options
Neo4jMemoryStore(Neo4jMemoryStoreConfig(…)) implements Strands' memory-store surface. Configure it on MemoryManager(stores=[store]) for agent-loop retrieval. Neo4jMemoryStoreConfig takes:
-
name(required) identifies the store, anddescriptiondescribes it to the model. -
Exactly one of
client(borrowed and left open) orsettings(the store constructs and closes its client). -
max_search_results,min_score(default0.2) and theinclude_entities,include_preferencesandinclude_factsswitches (allTrue) control retrieval. -
writable(defaultTrue) andextraction(defaultFalse) control writes. Store extraction is off by default, so a store with the defaults does not capture every turn. -
conversation_idselects the sink conversation for writes, anduser_idscopes them. -
graph_tools(defaultTrue) adds the store’s graph tools to the agent.
Reuse one configuration for several stores with dataclasses.replace(config, name="team"). See the maintained memory-store example for its lifecycle and verified settings.
Combine the store and the session manager
Choose one writer when combining a store and session manager. Leave store extraction disabled when the session manager owns transcript writes; otherwise a turn is written and extracted twice, and the session manager raises ValueError when it detects that pairing. Inject retrieved memory from one side only: either omit retrieval_config on the session manager, or pass injection=False to MemoryManager. The session manager logs a warning when both inject. Strands invocation-triggered extraction and backend entity extraction are separate stages; a successful write does not prove every derived entity is ready.
See the maintained session-manager example for two agents with separate sessions over one graph.
Add graph tools
context_graph_tools(…) returns four Strands tools for a Bolt database. Tool names describe available actions; the model can choose not to call them.
import os
from strands import Agent
from neo4j_agent_memory.integrations.strands import context_graph_tools
tools = context_graph_tools(
neo4j_uri=os.environ["NEO4J_URI"],
neo4j_user=os.environ["NEO4J_USERNAME"],
neo4j_password=os.environ["NEO4J_PASSWORD"],
neo4j_database=os.getenv("NEO4J_DATABASE", "neo4j"),
embedding_provider="bedrock", # or "openai", "vertex_ai"
embedding_model="amazon.titan-embed-text-v2:0",
aws_region=os.environ["AWS_REGION"],
)
agent = Agent(model=os.environ["BEDROCK_MODEL_ID"], tools=tools)
neo4j_uri and neo4j_password fall back to NEO4J_URI and NEO4J_PASSWORD. StrandsConfig.from_env() reads the whole configuration from the environment and is passed as context_graph_tools(**config.to_dict()). It reads NEO4J_USER, not NEO4J_USERNAME, and also NEO4J_DATABASE, EMBEDDING_PROVIDER, EMBEDDING_MODEL, AWS_REGION and AWS_PROFILE.
| Tool | Parameters and behavior |
|---|---|
|
|
|
|
|
|
|
|
The factory caches one client per URI, user and database; call clear_client_cache() when ending their lifecycle. nams_context_graph_tools(…) exposes hosted operations with a workspace key. bedrock_llm_model() and bedrock_embedding_model() return Bedrock model IDs, honoring BEDROCK_MODEL_ID and BEDROCK_EMBEDDING_MODEL_ID. llm_provider_from_strands(model) turns a Strands model ID into an LLMProvider when you want the same model for memory extraction.
Share memory across agents
Await writes, check the applicable extraction lifecycle, and apply workspace/database authorization before another agent retrieves results. A shared graph makes permitted records available through retrieval; it does not broadcast every fact immediately to every agent. user_id and session grouping do not provide universal entity isolation.
Limitations
-
One
Agentper session manager. AttachingNeo4jSessionManagerto a Strands Graph, Swarm orBidiAgentraisesNotImplementedErrorat the first dispatch; use one of Strands' repository-backed managers for those topologies. -
Text turns are restored; tool-use blocks and
agent.stateare not. Setrecord_tool_calls=Trueto keep an audit record of tool use. -
Pass
settings=or an unconnectedmemory_client=. A client already connected on another event loop cannot be driven from the manager’s background loop. -
Guardrail redaction rewrites the latest message before it is stored. After it is stored, Bolt deletes and re-adds the redacted message at the end of the history; NAMS cannot redact a stored message and logs a warning.
-
Hooks for
AfterInvocationEventregistered after the session manager run before the final turn is stored; read persisted state on the next turn.
See also
See Backend capabilities, Multi-agent sharing and Bedrock embeddings. Neo4j Agent Memory is community-supported Neo4j Labs software.