AWS Strands Agents Integration
|
This is the Python guide, which covers both backends, the hosted NAMS and
the self-hosted Neo4j or Aura over Bolt. If you are using TypeScript, see
Strands Agents (TypeScript). That SDK
wires up different Strands surfaces ( |
This guide shows how to add Neo4j Context Graph memory to AWS Strands agents. The integration offers two complementary modes, usable independently or together on the same agent:
| Mode | What it does | Reach for it when |
|---|---|---|
Memory tools (pull-based) |
|
You want the agent in explicit control — deep queries, on-demand recall, storing extra-conversational facts. |
Session manager (push-based) |
|
You want zero-effort conversation capture and continuity — nothing depends on the model remembering to call a tool. |
Overview
Both modes write into the same knowledge graph, which is what enables the multi-agent "shared brain" shown above: what one agent learns, every other agent can retrieve (see The Shared-Brain Pattern).
Quick Start
This example combines both modes: the session manager captures and restores the conversation transparently, while the tools give the agent explicit memory operations.
from strands import Agent
from neo4j_agent_memory import MemorySettings
from neo4j_agent_memory.integrations.strands import (
Neo4jRetrievalConfig,
Neo4jSessionManager,
context_graph_tools,
)
# Pull-based tools — the agent decides when to search or store.
tools = context_graph_tools(
neo4j_uri="neo4j+s://xxx.databases.neo4j.io",
neo4j_password="your-password",
embedding_provider="bedrock",
)
# Push-based session manager — every turn persisted, history restored,
# relevant memories injected into each user message (opt-in).
manager = Neo4jSessionManager(
"support-42",
settings=MemorySettings(
neo4j={
"uri": "neo4j+s://xxx.databases.neo4j.io",
"password": "your-password",
},
),
retrieval_config=Neo4jRetrievalConfig(),
)
agent = Agent(
model="anthropic.claude-sonnet-4-20250514-v1:0",
tools=tools,
session_manager=manager,
)
agent("Remember that I prefer Python over JavaScript")
Using the hosted NAMS service instead of self-hosted Neo4j? Swap in the
NAMS variants — both read MEMORY_API_KEY (and optionally
MEMORY_ENDPOINT) from the environment:
from neo4j_agent_memory.integrations.strands import (
Neo4jRetrievalConfig,
Neo4jSessionManager,
nams_context_graph_tools,
)
manager = Neo4jSessionManager.for_nams(
"support-42",
retrieval_config=Neo4jRetrievalConfig(),
)
tools = nams_context_graph_tools()
|
Do not share a |
Each mode also works on its own — the next two sections cover them independently.
Memory Tools (Pull-Based)
The integration provides four memory tools that agents can use:
| Tool | Purpose |
|---|---|
|
Semantic search across memories and entities |
|
Explore relationships around an entity |
|
Store information with entity extraction |
|
Retrieve user preferences by category |
|
When a session manager is attached (see Session Manager (Push-Based)),
conversation capture is automatic — |
Configuration
Basic Configuration
from neo4j_agent_memory.integrations.strands import context_graph_tools
tools = context_graph_tools(
# Neo4j connection
neo4j_uri="neo4j+s://xxx.databases.neo4j.io",
neo4j_user="neo4j",
neo4j_password="your-password",
neo4j_database="neo4j",
# Embedding configuration
embedding_provider="bedrock", # or "openai", "vertex_ai"
embedding_model="amazon.titan-embed-text-v2:0",
# AWS configuration (for Bedrock)
aws_region="us-east-1",
)
Using StrandsConfig
from neo4j_agent_memory.integrations.strands import StrandsConfig, context_graph_tools
config = StrandsConfig(
neo4j_uri="neo4j+s://xxx.databases.neo4j.io",
neo4j_password="your-password",
embedding_provider="bedrock",
embedding_model="amazon.titan-embed-text-v2:0",
aws_region="us-east-1",
)
tools = context_graph_tools(**config.to_dict())
From Environment Variables
from neo4j_agent_memory.integrations.strands import StrandsConfig, context_graph_tools
# Reads from environment variables
config = StrandsConfig.from_env()
tools = context_graph_tools(**config.to_dict())
Required environment variables:
export NEO4J_URI=neo4j+s://xxx.databases.neo4j.io
export NEO4J_PASSWORD=your-password
export AWS_REGION=us-east-1
Pass-Through Your Strands Model
Strands agents typically use Bedrock model strings (e.g. "anthropic.claude-sonnet-4-20250514-v1:0"). llm_provider_from_strands accepts either a bare string or a model object and prepends bedrock/ when no provider prefix is present:
from neo4j_agent_memory import MemoryClient, MemorySettings
from neo4j_agent_memory.integrations.strands import (
llm_provider_from_strands,
)
provider = llm_provider_from_strands(
"anthropic.claude-sonnet-4-20250514-v1:0",
)
settings = MemorySettings(
neo4j={"password": "p"},
llm=provider,
embedding="bedrock/amazon.titan-embed-text-v2:0",
)
The context_graph_tools(…) helper already resolves the embedder via from_provider internally — the pass-through is only needed when you want the same model used by both the agent and memory extraction.
Tool Details
search_context
Search for relevant memories and entities:
# Agent calls this tool automatically
result = search_context(
query="What do I know about the Acme project?",
user_id="user-123",
top_k=10, # Max results
min_score=0.5, # Similarity threshold
include_relationships=True,
)
Returns:
-
Matching messages with similarity scores
-
Related entities
-
Entity relationships (if enabled)
get_entity_graph
Explore relationships around an entity:
result = get_entity_graph(
entity_name="Acme Corp",
user_id="user-123",
depth=2, # Relationship depth
relationship_types=None, # All types, or filter
)
Returns:
-
Entity details
-
Connected entities
-
Relationship types and properties
add_memory
Store information with automatic entity extraction:
result = add_memory(
content="John Smith from Acme Corp called about the Q4 project",
user_id="user-123",
session_id="session-456",
extract_entities=True, # Auto-extract entities
)
Returns:
-
Memory ID
-
Extracted entities (if enabled)
-
Entity relationships created
Agent System Prompts
Guide your agent to use memory effectively:
agent = Agent(
model="anthropic.claude-sonnet-4-20250514-v1:0",
tools=tools,
system_prompt="""You are an assistant with persistent memory.
MEMORY USAGE:
1. At the start of a conversation, use search_context to find relevant history
2. When the user mentions entities (people, companies), use get_entity_graph
3. Store important facts with add_memory
4. Check get_user_preferences for personalization
Always reference your memory when it's relevant to the conversation.""",
)
Error Handling
Tools return error information in results:
# If Neo4j is unavailable
{
"error": "ConnectionError",
"message": "Failed to connect to Neo4j",
"memories": [],
"entities": []
}
Configure the agent to handle errors gracefully:
agent = Agent(
tools=tools,
system_prompt="""...
If a memory tool returns an error, acknowledge it and continue
without that context. Don't expose internal errors to users.""",
)
Session Manager (Push-Based)
Neo4jSessionManager plugs into the Strands session lifecycle: pass it to
Agent(session_manager=…) and every turn is persisted automatically,
conversation history is restored on agent restart, and (opt-in) relevant
long-term memories are injected into each user message before the model sees
it. Nothing depends on the model deciding to call a tool.
Construction
For self-hosted Neo4j, pass settings= and the manager owns the client
lifecycle:
from neo4j_agent_memory import MemorySettings
from neo4j_agent_memory.integrations.strands import Neo4jSessionManager
settings = MemorySettings(
neo4j={"uri": "bolt://localhost:7687", "password": "password"},
)
manager = Neo4jSessionManager("support-42", settings=settings)
For the hosted NAMS service, for_nams reads MEMORY_API_KEY (and
optionally MEMORY_ENDPOINT) from the environment — the same conventions as
nams_context_graph_tools():
from neo4j_agent_memory.integrations.strands import Neo4jSessionManager
manager = Neo4jSessionManager.for_nams("support-42")
Alternatively, pass an already-constructed (but not yet connected)
MemoryClient via memory_client=; see the loop-binding note under
Limitations.
Retrieval Injection
When a Neo4jRetrievalConfig is supplied, each user message triggers
concurrent long-term searches (entities, preferences, and optionally facts).
Matching results are prepended inside a <user_context> block:
<user_context>
Relevant memory:
- [entity] Acme Corp (ORGANIZATION) — customer since 2024, owner: Jane Doe
- [preference] communication: prefers concise answers
</user_context>
{original user text}
The stored message is always the user’s original — injection is in-memory
only and idempotent per message. If nothing clears the min_score floor,
nothing is injected (no empty tags). The cost is 1–3 backend round-trips per
user turn (why it is opt-in).
|
On NAMS, only entity search is available — NAMS does not yet expose
preference or fact search endpoints, so |
from neo4j_agent_memory.integrations.strands import Neo4jRetrievalConfig
retrieval_config = Neo4jRetrievalConfig(
top_k=10, # max results per memory kind
min_score=0.2, # similarity floor
include_entities=True,
include_preferences=True,
include_facts=False, # set True to also search stored facts
context_tag="user_context", # XML wrapper tag (AgentCore-compatible default)
)
Constructor Reference
| Parameter | Default | Description |
|---|---|---|
|
(required) |
Strands session identifier (maps to one |
|
|
Pre-constructed |
|
|
|
|
|
Scopes writes to a specific user/tenant (multi-tenant deployments). |
|
|
Enable long-term memory injection. |
|
|
Run entity extraction on stored messages (bolt only; NAMS extracts server-side regardless). |
|
|
Mirror tool-use and tool-result blocks into reasoning memory for audit. |
|
|
Seconds to wait for each sync→async backend call before raising. |
|
|
Max messages loaded into the agent on restore; |
Limitations
-
Memory-grade persistence. Text turns are stored and restored; tool-use blocks are not replayed on restart (set
record_tool_calls=Trueto mirror them into reasoning memory for audit instead). -
agent.stateand conversation-manager window state do not survive restarts. No Strands-specific node types are written to the graph;sync_agentis a no-op — the message buffer is flushed by theAfterInvocationEventhook. -
Redaction. Fully supported within an invocation — the write-behind buffer lets guardrails rewrite the latest message before it ever reaches the backend. After the buffer has been flushed, falls back to delete-and-re-add on bolt (the redacted message moves to the end of restored history) and logs a warning on NAMS (no message-update endpoint on NAMS, so server-side redaction is not possible).
-
External
AfterInvocationEventhooks registered after the session manager run before the final turn is flushed (Strands dispatches that event in reverse registration order) — read persisted state on the next turn instead. -
Search scope. Long-term searches are workspace/database-scoped.
user_idscopes writes (multi-tenant), not searches — the search APIs take no user filter. -
One agent per session manager instance. Pass multiple agents their own managers (the shared-brain pattern). Strands Graph/Swarm orchestration persistence is not supported in v1.
-
Loop-bound transports. Pass
settings=(or an unconnectedmemory_client=). A client already connected on another event loop cannot be driven from the manager’s background loop — the transport is bound to the loop that performed the firstconnect(). -
Multiple conversations with the same session ID (NAMS). If external writers create more than one NAMS conversation with the same
strands_session_idmetadata, the manager resolves to the first one listed. Avoid duplicate session IDs in shared workspaces; resolution passesuser_idand an explicit page limit to narrow the scan.
Multi-User Support
Handle multiple users in the same application. The tools take a user_id
argument on each call; the session manager takes user_id at construction
(one manager per user session):
# Tools are configured per-invocation
# The agent passes user_id with each tool call
agent = Agent(
model="anthropic.claude-sonnet-4-20250514-v1:0",
tools=tools,
session_manager=Neo4jSessionManager(
f"support-{user_id}",
settings=settings,
user_id=user_id, # scopes writes in multi-tenant deployments
),
system_prompt="""The current user is {user_id}.
Always include user_id="{user_id}" in your memory tool calls.""",
)
# Inject user context
response = agent(
f"[User: user-123] What do you remember about me?",
)
The Shared-Brain Pattern
Each agent gets its own Neo4jSessionManager (its own session_id), all
pointing at the same graph. Conversations flow into one knowledge graph;
entities extracted from one session are searchable by all others — this is
the multi-agent architecture from the Overview diagrams.
from neo4j_agent_memory import MemorySettings
from neo4j_agent_memory.integrations.strands import (
Neo4jRetrievalConfig,
Neo4jSessionManager,
)
settings = MemorySettings(
neo4j={"uri": "bolt://localhost:7687", "password": "password"},
)
# Agent A — ingests from KYC conversations
manager_a = Neo4jSessionManager(
"kyc-session",
settings=settings,
extract_entities=True,
)
# Agent B — queries the shared graph with context injection
manager_b = Neo4jSessionManager(
"credit-session",
settings=settings,
retrieval_config=Neo4jRetrievalConfig(),
)
See examples/strands-session-manager/ for a runnable three-phase demo that
requires no LLM or API key.