DataRobot + Neo4j Integration
Overview
This integration packages a Neo4j-backed research agent for DataRobot, built on
DataRobot’s official af-component-agent
copier template (base framework), with Neo4j-specific tools and memory applied on top.
|
Why this structure? An earlier version of this PR shipped three different,
partially-overlapping agent implementations side by side (a deprecated DRUM
custom-model entrypoint, an unwired LangGraph agent, and a standalone NAT
tool-calling workflow) — reviewed by the DataRobot team ( |
Architecture
Architecture at a glance:
This agent is built directly on top of DataRobot’s official af-component-agent template (scaffolded via copier, agent_template_framework: base), with Neo4j-specific pieces layered on top of the template’s structure rather than reimplemented from scratch:
-
`agent/register.py` wires
myagent.py’s `MyAgentinto NAT viaregister_per_user_function, matching the template’sregister_base.py.j2pattern — this is the single entry point NAT’s registration system routes requests to. -
`workflow.yaml` declares
general.front_end._type: dragent_fastapi, so the agent is served through DataRobot’sdragentruntime front end. -
`agent/neo4j_tools.py` and `agent/mcp_client.py` provide the Neo4j-specific tool-calling and MCP integration, applied on top of the template’s tool-binding conventions.
-
`agent/nat_memory.py` wraps NAMS-backed cross-session memory using NAT’s own
MemoryEditorinterface.
Request flow — memory (NAMS)
Memory is wired as part of workflow.yaml’s `streaming_memory_agent workflow type
(retrieve-context → enrich-prompt → run-agent → save-results), the same shape as
DataRobot’s own dr_mem0_memory reference implementation — it is never on the critical
path to a correct answer: any NAMS failure is caught, logged, and the request proceeds
with an empty/unsaved context.
Request flow — MCP tool loading (incl. hosted Neo4j Aura MCP)
Any failure anywhere in this chain (_discover_oauth_metadata, _fetch_oauth_token,
alist_tools) is caught and logged — mcp_tools_context() yields [] rather than
raising, so the agent always runs with at least its 7 built-in Neo4j tools.
Packaging & deployment flow
This mirrors the official template’s own task create-docker-context /
task build-docker-context flow exactly — no custom Dockerfile is checked into this
repo (the prior static Dockerfile, tied to the now-removed agent/server.py, was
deleted); the same Docker context that runs locally via task dev is what DataRobot’s
platform runs after deployment, since both invoke agent/workflow.yaml through the
dragent runtime.
Files
datarobot/
├── .env.example
├── README.md
├── requirements.txt ← single dependency set (Neo4j tools + NAT/dragent runtime)
├── pyproject.toml ← packages agent/ as an installable NAT plugin
├── Taskfile.yml ← task install / task dev / task test / task run / task validate
├── dev.py ← IDE-friendly dev server entrypoint (nat dragent serve, in-process)
├── datarobot_agent.ipynb ← (removed — was built entirely around the deprecated custom.py/
│ infra/agent.py deploy flow; no longer applicable)
├── agent/
│ ├── __init__.py
│ ├── myagent.py ← the single agent: LangGraph planner/writer + MCP tool loading
│ ├── register.py ← NAT registration wiring MyAgent into the dragent runtime
│ ├── workflow.yaml ← NAT/dragent workflow config (front end, LLM, memory, agent)
│ ├── neo4j_tools.py ← 7 LangChain-compatible Neo4j tools (parameterized Cypher)
│ ├── mcp_client.py ← OAuth-aware (RFC 9728) MCP client for external MCP servers
│ ├── memory.py ← low-level NAMS HTTP client (used by nat_memory.py)
│ ├── nat_memory.py ← NAT MemoryEditor plugin (neo4j_agent_memory), NAMS-backed
│ └── nat_tools.py ← optional: same Neo4j tools registered as native NAT functions
│ (independently MCP-servable; not wired into the primary agent)
├── scripts/
│ └── test_mcp_connection.py ← standalone MCP connectivity smoke test
└── tests/
├── test_mcp_oauth.py ← OAuth 2.0 client-credentials + RFC 9728 discovery (17 tests)
├── test_register.py ← NAT registration wiring/structure tests
└── test_myagent_mcp.py ← MCP tool-loading helper tests
Quick Start (local)
cd datarobot
task install # creates .venv, installs requirements.txt, pip install -e .
cp .env.example .env
# fill in DATAROBOT_ENDPOINT/DATAROBOT_API_TOKEN, Neo4j creds (demo DB works out of the box),
# optionally MEMORY_API_KEY/MEMORY_WORKSPACE_ID, MCP_SERVER_URL
task validate # schema-checks agent/workflow.yaml, no live calls
task run -- "Tell me about Neo4j the company" # one-shot run against a real input
task dev # nat dragent serve --reload, for iterative local development
Don’t have Task installed? Every task target is a thin wrapper
around a plain command — run them directly instead:
python3 -m venv .venv && source .venv/bin/activate
pip install --prefer-binary -r requirements.txt
pip install -e . # registers agent/register.py + nat_tools.py + nat_memory.py as a NAT plugin
nat validate --config_file agent/workflow.yaml
nat run --config_file agent/workflow.yaml --input "Tell me about Neo4j the company"
nat dragent serve --config_file agent/workflow.yaml --reload true --port 8842
|
`--prefer-binary` matters: |
|
Verified end-to-end (this session, against a real DataRobot org + live Neo4j Aura
credentials): |
|
Troubleshooting: NEO4J_URI=bolt+s://demo.neo4jlabs.com:7687 |
|
Troubleshooting:
This is an environment/network configuration issue, not something this repo’s code can fix. |
Neo4j Agent Memory (NAMS)
Memory is implemented as a NAT MemoryEditor plugin (agent/nat_memory.py, neo4j_agent_memory
type), wrapping the same NAMS session-cache logic as before, and wired into workflow.yaml’s
`streaming_memory_agent workflow — the retrieve-context → enrich-prompt → run-agent →
save-results flow, modeled on DataRobot’s own dr_mem0_memory reference implementation
(including its no-op fallback when unconfigured).
# Get a free key at https://memory.neo4jlabs.com
MEMORY_API_KEY=nams_... MEMORY_WORKSPACE_ID=<workspace-id> task run -- "Tell me about Apple"
# Next run in the same session/thread will have context from the first one
Memory is non-blocking — if MEMORY_API_KEY/MEMORY_WORKSPACE_ID are absent, or a NAMS call
fails, nat_memory.py logs a warning and the workflow completes normally without memory rather
than crashing.
|
`MEMORY_WORKSPACE_ID` is a real workspace ID issued by NAMS (sent as the |
MCP Integration
agent/myagent.py’s `mcp_tools_context() loads external MCP tools using this repo’s own
OAuth-aware client (agent/mcp_client.py), not `datarobot_genai’s built-in MCP adapter — the
built-in one expects auth headers already resolved by DataRobot’s own internal MCP
function-group mechanism, which doesn’t apply to an external server like a hosted Neo4j Aura
MCP endpoint.
# Neo4j MCP official server (uses NEO4J_USERNAME/PASSWORD for Basic auth automatically)
MCP_SERVER_URL=https://neo4j-mcp-official-1008050579172.us-central1.run.app/mcp \
task run -- "What schema does my Neo4j database have?"
# Any HTTP MCP server with a Bearer token
MCP_AUTH_TOKEN=my-bearer-token \
MCP_SERVER_URL=https://my-mcp-server.example.com/mcp \
task run -- "..."
# Hosted Neo4j Aura MCP (OAuth 2.0 client-credentials) — see below
MCP_SERVER_URL=<aura-agent-or-aura-database-mcp-endpoint> \
MCP_OAUTH_CLIENT_ID=<client-id> \
MCP_OAUTH_CLIENT_SECRET=<client-secret> \
task run -- "What tools does my Aura Agent expose?"
Supported transports (auto-detected): Streamable HTTP (tried first) → SSE (fallback) → stdio (any other string, treated as a shell command).
Authentication (checked in this priority order):
| Priority | Condition | Header sent |
|---|---|---|
1 |
|
OAuth 2.0 client-credentials grant → |
2 |
|
|
3 |
|
|
4 |
None of the above |
No auth header (open servers) |
Hosted Neo4j Aura MCP (Aura Agents / Aura hosted database)
Two hosted Aura paths are supported by pointing MCP_SERVER_URL at the right endpoint. Both
require OAuth 2.0 client-credentials — confirmed via live testing against a real Aura
hosted-database MCP instance that it rejects unauthenticated requests (401 with a
WWW-Authenticate: Bearer header). Neither path uses Basic auth / database username-password.
-
Aura Agents — an Aura Agent’s public MCP endpoint URL. Since the Aura Agents
/invokeAPI doesn’t publish RFC 9728 discovery metadata, the client falls back toMCP_OAUTH_TOKEN_URL(defaulthttps://api.neo4j.io/oauth/token). OverrideMCP_OAUTH_TOKEN_URL/MCP_OAUTH_SCOPE/MCP_OAUTH_AUDIENCEif your setup differs. -
Hosted database (URL from the Aura Console "Inspect" tab, e.g.
https://<id>.mcp-instances.neo4j.io) — the client auto-discovers the correct token endpoint and audience from RFC 9728 Protected Resource Metadata + OIDC discovery (a region-specific Auth0 tenant) — noMCP_OAUTH_TOKEN_URLoverride needed.
Both are covered by tests/test_mcp_oauth.py (17 tests, mocked HTTP — no live Aura access
needed to run them).
MCP is non-blocking — if MCP_SERVER_URL is absent or the mcp package isn’t importable
(requires Python ≥3.10), the agent runs with only its Neo4j tools.
Neo4j tools (agent/neo4j_tools.py)
| Tool | Description |
|---|---|
|
Execute any raw Cypher query |
|
Full-text company lookup |
|
Company profile — summary, industries, locations, leadership |
|
List all industry categories |
|
Companies in a specific industry |
|
Org-to-org graph traversal (depth 1–4) |
|
Executives and board members |
All parameterized (no string-interpolated Cypher) — verified with adversarial inputs against the
live database. agent/nat_tools.py registers the same logic as native NAT functions
(independently servable via nat mcp serve, not wired into the primary agent by default).
|
These tools assume the Neo4jLabs "companies" demo graph schema. If you point |
Runtime / environment variables
| Variable | Purpose | Default |
|---|---|---|
|
DataRobot API base URL |
|
|
DataRobot API token — used by |
(required) |
|
Neo4j connection string |
|
|
Neo4j username |
|
|
Neo4j password |
(required) |
|
Neo4j database |
|
|
NAMS key — leave blank to disable memory |
(optional) |
|
NAMS workspace ID (from the NAMS dashboard, not derived from the key) |
(optional) |
|
External MCP server URL — leave blank to skip |
(optional) |
|
OAuth client-credentials for hosted Aura MCP |
(optional) |
|
Static bearer token for other MCP servers |
(optional) |
|
Local dev server port ( |
|
See .env.example for the full annotated list, including MCP OAuth discovery overrides.
Notes
-
agent/register.py’s `NameError: name 'Streaming' is not definedbug, found and fixed during end-to-end testing this session: the file initially carriedfrom future import annotations(copy-pasted habit from other modules in this repo), which turnsAnnotated[…, Streaming(…)]into a string annotation. NAT’sFunctionInfo.from_fnresolves that string viatyping.get_type_hints()at registration time, butStreamingis only imported inside the function body (matching the official template exactly) — not at module scope — so the string couldn’t be resolved, andnat run/nat validatefailed withNameError: name 'Streaming' is not defined. Fixed by removing the future-annotations import, matching the template’s ownregister.py(which doesn’t use it either). Covered by a regression test intests/test_register.py. -
Full end-to-end validation performed this session against a real DataRobot org and live Neo4j Aura credentials:
nat validate --config_file agent/workflow.yamlpasses;nat runbuilds every component inworkflow.yaml(auth, middleware, LLM, memory, theneo4j_agentfunction, thestreaming_memory_agentworkflow) and reaches DataRobot’s live LLM Gateway with a correctly-authenticated request. The only failure encountered was theGENAI_EXPERIMENTATIONfeature-flag entitlement described above — a DataRobot account-level setting, not a code issue. -
The previously-existing
datarobot_agent.ipynbnotebook,run_local.py,infra/agent.py,infra/workload.py,agent/agent.py,agent/custom.py,agent/server.py,agent/model-metadata.yaml, and the standaloneDockerfilewere all built entirely around the now-removed DRUM/Workload API paths and have been deleted rather than kept as unmaintained dead code, to keep the codebase aligned with the single official-template architecture. -
neo4j-agent-memoryandmcpboth require Python ≥3.10 (same asdatarobot-genai[dragent]’s own `nvidia-natdependency, so this is not an additional constraint versus what the template already requires).