Secure GraphRAG Gateway with Neo4j
Build a zero-trust GraphRAG API with Neo4j, JWT auth, and role-based access control.
Introduction
30 Second Summary
When hospitals connect patient monitors, infusion pumps, and blood pressure devices to the same network, every connected system becomes a potential data leak. An AI assistant that helps a technician troubleshoot an infusion pump should never accidentally surface a patient's cardiac alarm history.
In this project, you will build a secure GraphRAG gateway that processes healthcare IoT device manuals through a Neo4j knowledge graph and enforces role-based access control at the retrieval layer. Different users (doctors, technicians, patients) will only see the information their role authorizes.
What You'll Build
A Zero-Trust Multi-Tenant GraphRAG system that extracts knowledge from medical device documentation into a Neo4j property graph, with cryptographic identity verification controlling every query.
By the end of this project, you'll have:
- A Neo4j Property Graph populated with extracted entities and relationships from three healthcare IoT device manuals.
- A secure FastAPI gateway with JWT token generation that enforces role-based access control on every GraphRAG query.
- A working end-to-end system where three user roles (doctor, technician, patient) each receive only the graph context their role authorizes.
- Secret Mission: Add token expiration enforcement and visualize your knowledge graph topology in Neo4j Browser.
Are there any prerequisites?
You should have intermediate Python experience and basic familiarity with REST APIs. You will also need Docker installed on your machine. No prior experience with Neo4j or LlamaIndex is required.
Set Up Docker and Neo4j
Neo4j is a graph database that stores data as interconnected nodes and relationships. For this project, it will hold the knowledge graph extracted from your healthcare IoT device documentation.
In this step, you'll pull the Neo4j Docker image and start a container configured with the APOC plugin and authentication credentials.
In this step, get ready to:
- Pull the Neo4j Docker image.
- Start a Neo4j container with APOC enabled and authentication configured.
- Verify the database is accessible through the browser.
Pull the Neo4j Docker image
Docker Desktop is already running on your machine. The next task is to download the Neo4j database image so you can run it as a container.
- Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows), then typing Terminal and pressing Enter.
- Pull the Neo4j image by running this command:
docker pull neo4j:2026.05.0
What does this command do?
docker pull downloads a container image from Docker Hub to your machine. The neo4j:2026.05.0 tag pins the exact version so your setup matches this project.
You should see progress bars as Docker downloads the image layers. Once complete, you'll see a Status: Downloaded newer image message.
Seeing a connection error?
Make sure Docker Desktop is running. You can check by looking for the Docker whale icon in your system tray (Windows) or menu bar (macOS).
Help me troubleshoot my Docker pull command
Start the Neo4j container with APOC enabled
APOC (Awesome Procedures on Cypher) is a library of utility functions that extends Neo4j's capabilities. LlamaIndex uses APOC procedures when constructing the property graph.
- Start the Neo4j container by running this command:
macOS
docker run -d \
--name neo4j-apoc \
-p 7474:7474 \
-p 7687:7687 \
--env NEO4J_AUTH=neo4j/graphrag2026 \
--env NEO4J_PLUGINS='["apoc"]' \
neo4j:2026.05.0
Windows
docker run -d `
--name neo4j-apoc `
-p 7474:7474 `
-p 7687:7687 `
--env NEO4J_AUTH=neo4j/graphrag2026 `
--env "NEO4J_PLUGINS=[""apoc""]" `
neo4j:2026.05.0
What do these flags mean?
- -d runs the container in detached mode (in the background).
- --name neo4j-apoc gives the container a recognizable name for later management.
- -p 7474:7474 maps the Neo4j Browser HTTP port to your machine.
- -p 7687:7687 maps the Bolt protocol port that applications use to query the database.
- NEO4J_AUTH=neo4j/graphrag2026 sets the database username and password.
- NEO4J_PLUGINS installs the APOC plugin automatically at startup.
- Verify the container is running by executing:
docker ps
You should see a row with neo4j-apoc in the NAMES column and a status of Up followed by a time duration.
Container not showing as running?
If you see no output from docker ps, the container may have exited immediately. Run docker logs neo4j-apoc to check for errors.
If you see a "name already in use" error, a previous container with the same name exists. Remove it by running docker rm neo4j-apoc and then re-run the start command above.
Help me debug my Neo4j Docker container
Verify Neo4j is accessible in the browser
Neo4j includes a built-in browser interface for running queries and inspecting your graph. You'll use this to confirm the database is ready before building the Python application.
- Open your web browser and navigate to http://localhost:7474.
You'll see the Neo4j Browser login screen prompting you to connect.
- In the Connect URL field, confirm it shows bolt://localhost:7687.
- Enter neo4j as the username.
- Enter graphrag2026 as the password.
- Click Connect.
You should see the Neo4j Browser dashboard with a welcome guide. This confirms both the HTTP interface (port 7474) and the Bolt protocol (port 7687) are working.
What is Bolt?
Bolt is Neo4j's binary protocol optimized for database queries. Your Python application will communicate with Neo4j over this protocol at bolt://localhost:7687.
The browser interface at port 7474 is for human interaction only.
Can't reach localhost:7474?
Neo4j takes 10-15 seconds to fully start after the container launches. Wait a moment and refresh the page.
If the page still doesn't load, confirm the container is running with docker ps and check that port 7474 is listed in the PORTS column.
Help me connect to Neo4j Browser
Your graph database is up and running. Next up, you'll set up the Python environment and install the packages that connect LlamaIndex to Neo4j.
Set Up Python Environment and Configuration
Your Neo4j graph database is running and ready to accept data. Now you need the Python application layer that will talk to it.
This step sets up your project workspace, installs the GraphRAG and API libraries, and creates the configuration that connects everything together.
In this step, get ready to:
- Create a Python virtual environment for the project.
- Install GraphRAG, embedding, and API gateway packages.
- Configure environment variables and application settings.
Create your project directory and virtual environment
A virtual environment isolates your project's packages from your system Python. This prevents version conflicts between projects.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir healthcare-graphrag
cd healthcare-graphrag
- Confirm you are inside the new folder by running:
pwd
You should see a path ending in healthcare-graphrag.
Now check your Python version. This project requires Python 3.10 or higher.
- Check your installed Python version by running:
python3 --version
On Windows?
If python3 is not recognized, try python --version instead. Windows often uses python as the command.
✔️ I see version 3.10 or higher
Python is ready. Continue to creating the virtual environment below.
ⓧ I see an older version
Your system Python is below 3.10. macOS often ships with an older Python that shadows newer installs.
- Download and install Python 3.10+ from the official site:
- https://www.python.org/downloads/
- After installing, close and reopen your terminal.
- Run python3 --version again to confirm it now reports 3.10 or higher.
Use the full path if needed
If the version still shows an older Python, you may need to call the new binary directly (e.g. python3.12 --version). Use whichever binary reports 3.10+ for the rest of this project.
ⓧ Command not found
Python is not installed on your system yet.
- Download and install Python from the official site:
- https://www.python.org/downloads/
- On Windows, check the box that says Add python.exe to PATH during installation.
- Close and reopen your terminal after installing.
- Run python3 --version (or python --version on Windows) to confirm it shows 3.10 or higher.
With Python confirmed, create and activate a virtual environment.
- Create a virtual environment by running:
python3 -m venv venv
On Windows?
Use python -m venv venv if your Python command is python instead of python3.
- Activate the virtual environment:
macOS
source venv/bin/activate
Windows
venv\Scripts\activate
You should see (venv) appear at the start of your terminal prompt. This confirms the virtual environment is active.
Don't see (venv) in your prompt?
On Windows, you may need to allow script execution first. Run Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned then try the activate command again.
Make sure you are inside the healthcare-graphrag folder when you run the activate command.
Still stuck? Help me activate my Python virtual environment
Install all required packages
Your project needs several libraries. LlamaIndex handles graph construction and retrieval. FastAPI provides the secure API gateway. PyJWT handles token-based authentication.
- Install the project dependencies by running:
pip install llama-index-core llama-index-graph-stores-neo4j llama-index-llms-openai-like llama-index-embeddings-huggingface fastapi uvicorn PyJWT python-dotenv
What are these packages?
- llama-index-core is the core framework for building RAG pipelines.
- llama-index-graph-stores-neo4j connects LlamaIndex to your Neo4j database.
- llama-index-llms-openai-like lets you use Groq's OpenAI-compatible API for LLM calls.
- llama-index-embeddings-huggingface provides local embedding models so you avoid API costs for vectorization.
- fastapi and uvicorn create and serve the HTTP API gateway.
- PyJWT encodes and decodes JWT tokens for authentication.
- python-dotenv loads environment variables from a .env file.
This installation downloads several models and dependencies. It may take 2-3 minutes.
You should see Successfully installed followed by a list of package names when it finishes.
Seeing installation errors?
Make sure (venv) appears in your terminal prompt. If it does not, your virtual environment is not active. Go back and run the activate command.
If you see a permissions error, do NOT use sudo pip install. Reactivate the virtual environment instead.
My pip install is failing
Create the environment file and configuration module
Your application needs credentials for Groq (the LLM provider) and Neo4j (the graph database). A .env file keeps these values out of your code.
First, you need a free Groq API key to power the LLM calls in your pipeline.
- Visit the Groq console at https://console.groq.com/keys and sign in (or create a free account).
- Click Create API Key.
- Copy the key immediately. It will not be shown again.
Why Groq?
Groq provides a free tier for LLM inference with an OpenAI-compatible API. This means LlamaIndex can talk to it using the same patterns as OpenAI, but at zero cost for this project.
- Open the healthcare-graphrag folder in your code editor (VS Code or Cursor).
- Create a new file called .env in the root of your project folder.
- Paste the following into .env, replacing the placeholder with your actual Groq API key:
GROQ_API_KEY=your-groq-api-key-here
NEO4J_URI=bolt://localhost:7687
NEO4J_USERNAME=neo4j
NEO4J_PASSWORD=graphrag2026
JWT_SECRET=healthcare_graphrag_secret_key_2026
What do these variables do?
- GROQ_API_KEY authenticates your LLM requests to Groq.
- NEO4J_URI, NEO4J_USERNAME, and NEO4J_PASSWORD connect to the Neo4j container you started in Step 1.
- JWT_SECRET is the signing key for creating and verifying access tokens.
- Save .env.
Now create the configuration module that loads these values and defines application-wide settings.
- Create a new file called config.py in the root of your project folder.
- Paste the following code into config.py:
import os
from dotenv import load_dotenv
load_dotenv()
class AppSettings:
"""Application-wide configuration schema."""
GROQ_API_KEY: str = os.getenv("GROQ_API_KEY", "")
LLM_MODEL: str = "llama-3.3-70b-versatile"
LLM_API_BASE: str = "https://api.groq.com/openai/v1"
EMBEDDING_MODEL_NAME: str = "BAAI/bge-small-en-v1.5"
NEO4J_URI: str = os.getenv("NEO4J_URI", "bolt://localhost:7687")
NEO4J_USERNAME: str = os.getenv("NEO4J_USERNAME", "neo4j")
NEO4J_PASSWORD: str = os.getenv("NEO4J_PASSWORD", "graphrag2026")
NEO4J_DATABASE: str = "neo4j"
JWT_SECRET: str = os.getenv("JWT_SECRET", "healthcare_graphrag_secret_key_2026")
JWT_ALGORITHM: str = "HS256"
@classmethod
def validate_keys(cls):
"""Raises runtime error if critical credentials are missing."""
if not cls.GROQ_API_KEY:
raise ValueError("GROQ_API_KEY environment variable is not defined.")
AppSettings.validate_keys()
What does this code do?
- load_dotenv() reads your .env file and loads the values into environment variables.
- AppSettings centralizes all configuration in one class. Other modules import this instead of reading env vars directly.
- LLM_MODEL is set to llama-3.3-70b-versatile, a production model on Groq with a 131,072 token context window.
- EMBEDDING_MODEL_NAME uses a local HuggingFace model so embeddings run on your machine at zero cost.
- validate_keys() runs immediately on import and stops the app if the Groq key is missing.
- Save config.py.
- Verify the configuration loads without errors by running this in your terminal:
python3 -c "from config import AppSettings; print('Config loaded. LLM:', AppSettings.LLM_MODEL)"
You should see: Config loaded. LLM: llama-3.3-70b-versatile.
Seeing a ValueError about GROQ_API_KEY?
This means your .env file either does not exist, is in the wrong folder, or still has the placeholder your-groq-api-key-here as the value.
Open .env and confirm you replaced the placeholder with your actual Groq API key (no quotes around the value).
My config.py is throwing an error
✔️ Awesome, I've got everything!
Great. Double check you have saved both .env and config.py.
ⓧ I'd like to double check the full code
Here is the complete .env file:
GROQ_API_KEY=your-groq-api-key-here
NEO4J_URI=bolt://localhost:7687
NEO4J_USERNAME=neo4j
NEO4J_PASSWORD=graphrag2026
JWT_SECRET=healthcare_graphrag_secret_key_2026
Here is the complete config.py:
import os
from dotenv import load_dotenv
load_dotenv()
class AppSettings:
"""Application-wide configuration schema."""
GROQ_API_KEY: str = os.getenv("GROQ_API_KEY", "")
LLM_MODEL: str = "llama-3.3-70b-versatile"
LLM_API_BASE: str = "https://api.groq.com/openai/v1"
EMBEDDING_MODEL_NAME: str = "BAAI/bge-small-en-v1.5"
NEO4J_URI: str = os.getenv("NEO4J_URI", "bolt://localhost:7687")
NEO4J_USERNAME: str = os.getenv("NEO4J_USERNAME", "neo4j")
NEO4J_PASSWORD: str = os.getenv("NEO4J_PASSWORD", "graphrag2026")
NEO4J_DATABASE: str = "neo4j"
JWT_SECRET: str = os.getenv("JWT_SECRET", "healthcare_graphrag_secret_key_2026")
JWT_ALGORITHM: str = "HS256"
@classmethod
def validate_keys(cls):
"""Raises runtime error if critical credentials are missing."""
if not cls.GROQ_API_KEY:
raise ValueError("GROQ_API_KEY environment variable is not defined.")
AppSettings.validate_keys()
Your Python environment is configured and verified. Next up, you will create the healthcare IoT device documentation that feeds your knowledge graph.
Create Healthcare IoT Device Documentation
Your Python environment is configured and all credentials are validated. Now it's time to create the source material your knowledge graph will be built from.
A GraphRAG system needs documents to extract entities and relationships from. In this step, you'll create three healthcare IoT device manuals that represent different security classifications. Each document will later be tagged with a specific role requirement, ensuring only authorized users can query its content.
In this step, get ready to:
- Create the data/ directory for your device documentation.
- Write three clinically-themed device manuals with distinct safety parameters and maintenance procedures.
Create the data directory
Your project needs a dedicated folder to hold the device manuals that the ingestion pipeline will process later.
- Create the data/ directory inside your project folder by running this command:
mkdir data
- Confirm the directory was created by running:
ls
You should see data listed alongside your existing .env and config.py files.
Write the device documentation files
Each device manual represents a different access tier in your security model. The patient monitor documentation is restricted to doctors, the infusion pump to technicians, and the blood pressure monitor to patients.
- Create a new file called patient_monitor.md inside the data/ folder and paste the following content:
# CardioWatch Pro X200 - Clinical Patient Monitor
## Device Overview
The CardioWatch Pro X200 is a Class C medical device designed for continuous cardiac monitoring in ICU and critical care environments. It provides real-time ECG waveform analysis, SpO2 tracking, and invasive blood pressure measurement.
## Alarm Thresholds
- Heart Rate Critical Low: 40 BPM
- Heart Rate Critical High: 150 BPM
- SpO2 Critical Low: 88%
- Systolic BP Critical High: 180 mmHg
- Systolic BP Critical Low: 80 mmHg
## Maintenance Protocol
Calibration must be performed every 90 days by authorized clinical engineering staff. The internal lithium backup battery requires replacement every 24 months. Electrode lead impedance testing should be conducted weekly.
## Safety Warnings
Do not operate in the presence of flammable anesthetics. Defibrillation may cause temporary signal loss. All alarm limits must be verified by the attending physician before patient connection.
What does this document represent?
The CardioWatch Pro X200 is a fictional Class C medical device. Class C is the highest risk classification under IEC 62304, meaning its documentation contains critical alarm thresholds and safety warnings.
In your access control system, this file will be tagged with required_role: doctor. Only users with a doctor JWT claim will be able to query this content through the API.
- Save data/patient_monitor.md.
- You won't see a runtime change yet. The next file continues building the documentation set.
- Create a new file called infusion_pump.md inside the data/ folder and paste the following content:
# MedFlow Precision 3000 - Volumetric Infusion Pump
## Device Overview
The MedFlow Precision 3000 is a Class C programmable volumetric infusion pump certified for delivery of intravenous fluids, medications, and blood products. It features anti-free-flow protection and air-in-line detection.
## Flow Rate Specifications
- Minimum Flow Rate: 0.1 mL/hr
- Maximum Flow Rate: 1200 mL/hr
- Bolus Rate Maximum: 1800 mL/hr
- Volume Accuracy: +/- 5% at rates above 1 mL/hr
## Preventive Maintenance
The peristaltic mechanism requires inspection every 500 operating hours. Door latch tension must be verified monthly. Software updates must only be applied by certified biomedical technicians using the manufacturer's update utility.
## Technical Service Access
Service mode is accessed via the rear panel DIP switch configuration. Firmware reflashing requires connection to the MedFlow TechLink diagnostic interface. All service interventions must be logged in the device maintenance record.
What does this document represent?
The MedFlow Precision 3000 contains technical service details like DIP switch configurations and firmware flashing procedures. This is sensitive operational information restricted to biomedical technicians.
This file will be tagged with required_role: technician. A patient or doctor querying the system will never receive this technical service content in their responses.
- Save data/infusion_pump.md.
- You won't see a runtime change yet. The next file completes the documentation set.
- Create a new file called blood_pressure_monitor.md inside the data/ folder and paste the following content:
# VitalLink BP Connect - Home Blood Pressure Monitor
## Device Overview
The VitalLink BP Connect is a Class B oscillometric blood pressure monitoring device designed for home use by patients. It features Bluetooth connectivity for data transmission to healthcare provider portals.
## Usage Instructions
1. Sit comfortably with feet flat on the floor for 5 minutes before measurement
2. Place the cuff on the left upper arm, 2cm above the elbow crease
3. Press the START button and remain still during measurement
4. Record your reading in the companion mobile application
## Normal Reading Ranges
- Normal: Below 120/80 mmHg
- Elevated: 120-129 / less than 80 mmHg
- High Blood Pressure Stage 1: 130-139 / 80-89 mmHg
- High Blood Pressure Stage 2: 140+ / 90+ mmHg
## Troubleshooting
If readings appear inconsistent, ensure the cuff size matches your arm circumference (22-42cm). Replace batteries when the low battery indicator appears. Contact your healthcare provider if systolic readings consistently exceed 180 mmHg.
What does this document represent?
The VitalLink BP Connect is a Class B device designed for home use. Class B is a lower risk classification, and its content focuses on patient-friendly usage instructions and troubleshooting.
This file will be tagged with required_role: patient. It's the least restricted tier. Patients can query this content, but they cannot access the doctor-classified or technician-classified documents.
- Save data/blood_pressure_monitor.md.
- Confirm all three files exist by running:
ls data/
You should see blood_pressure_monitor.md, infusion_pump.md, and patient_monitor.md listed in the output.
Don't see all three files?
Make sure you created the files inside the data/ folder, not in the project root. The path should be data/patient_monitor.md, not just patient_monitor.md.
Check that each filename uses underscores (patient_monitor.md) and the .md extension. Hyphens or missing extensions will cause the ingestion pipeline to fail in a later step.
Help me troubleshoot my missing data files.
✔️ Awesome, I've got everything!
Great. Double check you've saved all three files in your data/ folder.
ⓧ I'd like to double check the full code
Here are all three device documentation files for comparison.
# CardioWatch Pro X200 - Clinical Patient Monitor
## Device Overview
The CardioWatch Pro X200 is a Class C medical device designed for continuous cardiac monitoring in ICU and critical care environments. It provides real-time ECG waveform analysis, SpO2 tracking, and invasive blood pressure measurement.
## Alarm Thresholds
- Heart Rate Critical Low: 40 BPM
- Heart Rate Critical High: 150 BPM
- SpO2 Critical Low: 88%
- Systolic BP Critical High: 180 mmHg
- Systolic BP Critical Low: 80 mmHg
## Maintenance Protocol
Calibration must be performed every 90 days by authorized clinical engineering staff. The internal lithium backup battery requires replacement every 24 months. Electrode lead impedance testing should be conducted weekly.
## Safety Warnings
Do not operate in the presence of flammable anesthetics. Defibrillation may cause temporary signal loss. All alarm limits must be verified by the attending physician before patient connection.
# MedFlow Precision 3000 - Volumetric Infusion Pump
## Device Overview
The MedFlow Precision 3000 is a Class C programmable volumetric infusion pump certified for delivery of intravenous fluids, medications, and blood products. It features anti-free-flow protection and air-in-line detection.
## Flow Rate Specifications
- Minimum Flow Rate: 0.1 mL/hr
- Maximum Flow Rate: 1200 mL/hr
- Bolus Rate Maximum: 1800 mL/hr
- Volume Accuracy: +/- 5% at rates above 1 mL/hr
## Preventive Maintenance
The peristaltic mechanism requires inspection every 500 operating hours. Door latch tension must be verified monthly. Software updates must only be applied by certified biomedical technicians using the manufacturer's update utility.
## Technical Service Access
Service mode is accessed via the rear panel DIP switch configuration. Firmware reflashing requires connection to the MedFlow TechLink diagnostic interface. All service interventions must be logged in the device maintenance record.
# VitalLink BP Connect - Home Blood Pressure Monitor
## Device Overview
The VitalLink BP Connect is a Class B oscillometric blood pressure monitoring device designed for home use by patients. It features Bluetooth connectivity for data transmission to healthcare provider portals.
## Usage Instructions
1. Sit comfortably with feet flat on the floor for 5 minutes before measurement
2. Place the cuff on the left upper arm, 2cm above the elbow crease
3. Press the START button and remain still during measurement
4. Record your reading in the companion mobile application
## Normal Reading Ranges
- Normal: Below 120/80 mmHg
- Elevated: 120-129 / less than 80 mmHg
- High Blood Pressure Stage 1: 130-139 / 80-89 mmHg
- High Blood Pressure Stage 2: 140+ / 90+ mmHg
## Troubleshooting
If readings appear inconsistent, ensure the cuff size matches your arm circumference (22-42cm). Replace batteries when the low battery indicator appears. Contact your healthcare provider if systolic readings consistently exceed 180 mmHg.
Why three documents with different roles?
These three files simulate a real multi-tenant healthcare environment. In production, a hospital's document management system would contain thousands of files with varying sensitivity levels.
By assigning each file a distinct required_role (doctor, technician, patient), you'll later prove that the retrieval layer respects access boundaries. A patient querying the system will never see ICU alarm thresholds or firmware flashing procedures.
Your documentation set is ready. Next up, you'll build the graph store manager that connects LlamaIndex to your Neo4j database and prepares the foundation for knowledge graph extraction.
Build the Graph Store Manager
Your healthcare IoT device documentation is ready and waiting in the data/ directory.
Now you need a bridge between those documents and your Neo4j graph database. The graph store manager handles two jobs: configuring the AI models that will extract knowledge from your documents, and establishing the connection to Neo4j where that knowledge lives as a property graph.
In this step, get ready to:
- Configure LlamaIndex with a Groq LLM and HuggingFace embedding model.
- Connect to your Neo4j property graph store and implement an index retrieval method.
Configure LlamaIndex with Groq and HuggingFace embeddings
LlamaIndex uses a global Settings object to know which LLM to call and which embedding model to use for vector operations. You will point it at Groq's OpenAI-compatible API for the LLM and a local HuggingFace model for embeddings.
- Create a new file called graph_manager.py in your project root (the same folder where config.py lives).
- Add the imports and LLM/embedding configuration by pasting this code:
from llama_index.graph_stores.neo4j import Neo4jPropertyGraphStore
from llama_index.core import PropertyGraphIndex, Settings
from llama_index.llms.openai_like import OpenAILike
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from config import AppSettings
class GraphDatabaseManager:
"""Manages Neo4j property graph store connections and index abstractions."""
def __init__(self):
# Configure the LLM to use Groq's OpenAI-compatible endpoint
Settings.llm = OpenAILike(
model=AppSettings.LLM_MODEL,
api_base=AppSettings.LLM_API_BASE,
api_key=AppSettings.GROQ_API_KEY,
temperature=0.1,
is_chat_model=True,
)
# Configure the embedding model to run locally
Settings.embed_model = HuggingFaceEmbedding(
model_name=AppSettings.EMBEDDING_MODEL_NAME
)
What does this code do?
The imports pull in everything you need: Neo4jPropertyGraphStore for the database connection, PropertyGraphIndex for building and querying the graph, OpenAILike as a wrapper that lets you talk to any OpenAI-compatible API (in this case Groq), and HuggingFaceEmbedding for local vector embeddings.
Settings.llm points LlamaIndex at the llama-3.3-70b-versatile model on Groq with a low temperature of 0.1 for consistent knowledge extraction. Settings.embed_model uses BAAI/bge-small-en-v1.5 locally, which avoids API costs for embedding operations.
You won't see a change yet. The next chunk adds the Neo4j connection that brings it all together.
Connect to Neo4j and add the index retrieval method
With the AI models configured, the manager also needs to open a connection to your running Neo4j container. It will use the credentials from AppSettings (which reads your .env file).
- In graph_manager.py, add the following code directly below the Settings.embed_model block, still inside the __init__ method:
# Establish connection to the Neo4j property graph database
self.graph_store = Neo4jPropertyGraphStore(
username=AppSettings.NEO4J_USERNAME,
password=AppSettings.NEO4J_PASSWORD,
url=AppSettings.NEO4J_URI,
database=AppSettings.NEO4J_DATABASE,
)
def get_existing_index(self) -> PropertyGraphIndex:
"""Retrieves the property graph index from an active database."""
return PropertyGraphIndex.from_existing(
property_graph_store=self.graph_store
)
What does this code do?
Neo4jPropertyGraphStore opens a Bolt protocol connection to your running Neo4j container using the credentials stored in .env. This is where all extracted entities, relationships, and metadata will live.
The get_existing_index() method wraps PropertyGraphIndex.from_existing(), which reconnects to a previously populated graph. You will use this later in the API gateway to query the graph without re-ingesting documents every time.
- Save graph_manager.py.
- Verify the module loads correctly by running this command in your terminal:
python -c "from graph_manager import GraphDatabaseManager; print('GraphDatabaseManager loaded successfully')"
You should see GraphDatabaseManager loaded successfully printed in the terminal.
Seeing an import error?
- If you see a ModuleNotFoundError, make sure your virtual environment is activated. Check that you ran pip install for all packages in the previous step.
- If you see ValueError: GROQ_API_KEY environment variable is not defined, confirm your .env file has a valid Groq API key on the GROQ_API_KEY line.
- If you see a connection error mentioning Neo4j, confirm your neo4j-apoc container is still running with docker ps.
Still stuck? Help me debug my graph_manager.py import error
✔️ Awesome, I've got everything!
Great. Double check you have saved graph_manager.py before moving on.
ⓧ I'd like to double check the full code
Here is the complete graph_manager.py file:
from llama_index.graph_stores.neo4j import Neo4jPropertyGraphStore
from llama_index.core import PropertyGraphIndex, Settings
from llama_index.llms.openai_like import OpenAILike
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from config import AppSettings
class GraphDatabaseManager:
"""Manages Neo4j property graph store connections and index abstractions."""
def __init__(self):
Settings.llm = OpenAILike(
model=AppSettings.LLM_MODEL,
api_base=AppSettings.LLM_API_BASE,
api_key=AppSettings.GROQ_API_KEY,
temperature=0.1,
is_chat_model=True,
)
Settings.embed_model = HuggingFaceEmbedding(
model_name=AppSettings.EMBEDDING_MODEL_NAME
)
self.graph_store = Neo4jPropertyGraphStore(
username=AppSettings.NEO4J_USERNAME,
password=AppSettings.NEO4J_PASSWORD,
url=AppSettings.NEO4J_URI,
database=AppSettings.NEO4J_DATABASE,
)
def get_existing_index(self) -> PropertyGraphIndex:
"""Retrieves the property graph index from an active database."""
return PropertyGraphIndex.from_existing(
property_graph_store=self.graph_store
)
Why Groq instead of OpenAI directly?
Groq provides an OpenAI-compatible API at https://api.groq.com/openai/v1, which means LlamaIndex's OpenAILike wrapper works seamlessly. The llama-3.3-70b-versatile model has a 131,072 token context window, and Groq's free tier covers the usage in this project.
Using BAAI/bge-small-en-v1.5 for embeddings keeps the vector operations entirely local. No embedding API calls means zero additional cost and no rate limits during ingestion.
Your graph store manager is ready to go. Next up, you will build the ingestion pipeline that feeds your healthcare device documents through this manager and into the Neo4j knowledge graph.
Build the Ingestion Pipeline with Security Metadata
Your GraphDatabaseManager can connect to Neo4j and retrieve a property graph index. Your device documentation is sitting in the data/ folder. Now you need to bridge the two by ingesting those documents into the knowledge graph with security classification attached.
The critical piece here is metadata tagging. Each device manual gets a required_role label that determines which users can retrieve its content later. This is the foundation of your zero-trust access control at the retrieval layer.
In this step, get ready to:
- Define document access policies mapping each device file to a security role.
- Build an ingestion pipeline that loads, tags, and chunks documents.
- Execute the pipeline to populate your Neo4j knowledge graph with security-classified entities.
Define document access policies
In a zero-trust system, every piece of data must declare who is allowed to access it. You will define a policy dictionary that maps each device file to a role, device type, device name, and safety classification.
- Create a new file called ingest_pipeline.py in your project root (the same folder that contains config.py and graph_manager.py).
- Add the imports and access policy definitions by pasting this code:
import os
from llama_index.core import SimpleDirectoryReader, PropertyGraphIndex
from llama_index.core.node_parser import SentenceSplitter
from graph_manager import GraphDatabaseManager
# Maps each device file to its access control metadata
DOCUMENT_ACCESS_POLICIES = {
"patient_monitor.md": {
"device_type": "patient_monitor",
"device_name": "CardioWatch Pro X200",
"required_role": "doctor",
"safety_class": "IEC 62304 Class C",
},
"infusion_pump.md": {
"device_type": "infusion_pump",
"device_name": "MedFlow Precision 3000",
"required_role": "technician",
"safety_class": "IEC 62304 Class C",
},
"blood_pressure_monitor.md": {
"device_type": "blood_pressure_monitor",
"device_name": "VitalLink BP Connect",
"required_role": "patient",
"safety_class": "IEC 62304 Class B",
},
}
What does this code do?
The DOCUMENT_ACCESS_POLICIES dictionary is your security classification layer. Each key is a filename from your data/ directory, and each value contains the metadata that will be stamped onto every chunk extracted from that file.
- required_role determines which JWT role can query this data later. The patient monitor requires doctor access, the infusion pump requires technician, and the blood pressure monitor is accessible to patient.
- safety_class records the IEC 62304 classification. Class C devices (patient monitor, infusion pump) carry higher risk and require more restricted access.
Why tag metadata at ingestion time?
By attaching role requirements during ingestion, every node in the knowledge graph inherits its access policy. When a user queries the system later, the retriever can filter nodes by metadata without needing a separate permissions database.
Implement the ingestion class
The IngestionPipeline class handles three responsibilities: loading documents from disk, injecting access control metadata onto each document, and chunking them into graph-ready nodes using LlamaIndex's SentenceSplitter.
- Add the following class below the DOCUMENT_ACCESS_POLICIES dictionary in ingest_pipeline.py:
class IngestionPipeline:
"""Ingests documents, applies access control metadata, and builds the Neo4j graph."""
def __init__(self, data_directory: str = "data"):
self.data_dir = data_directory
self.manager = GraphDatabaseManager()
def run(self):
print(f"Reading data directory: {os.path.abspath(self.data_dir)}")
if not os.path.exists(self.data_dir):
raise FileNotFoundError(
f"Source folder '{self.data_dir}' not found."
)
# Load all markdown files from the data directory
reader = SimpleDirectoryReader(self.data_dir)
documents = reader.load_data()
# Inject access control metadata onto each document
for doc in documents:
filename = os.path.basename(doc.metadata.get("file_name", ""))
if filename in DOCUMENT_ACCESS_POLICIES:
doc.metadata.update(DOCUMENT_ACCESS_POLICIES[filename])
print(
f"Tagged {filename} with role: "
f"{DOCUMENT_ACCESS_POLICIES[filename]['required_role']}"
)
# Configure chunking strategy for graph node creation
node_parser = SentenceSplitter(
chunk_size=512,
chunk_overlap=50,
paragraph_separator="\n\n",
)
# Build the Neo4j knowledge graph from tagged documents
print("Building Neo4j Knowledge Graph...")
PropertyGraphIndex.from_documents(
documents,
property_graph_store=self.manager.graph_store,
transformations=[node_parser],
)
print("Knowledge Graph construction complete.")
if __name__ == "__main__":
pipeline = IngestionPipeline()
pipeline.run()
What does this code do?
- SimpleDirectoryReader loads every markdown file in the data/ folder into LlamaIndex Document objects.
- The metadata injection loop matches each document's filename against DOCUMENT_ACCESS_POLICIES and stamps the role, device type, and safety class directly onto the document's metadata dictionary.
- SentenceSplitter breaks documents into 512-token chunks with 50-token overlap. This keeps each graph node focused while preserving context at chunk boundaries.
- PropertyGraphIndex.from_documents sends the tagged chunks to the LLM for entity and relationship extraction, then writes everything into your Neo4j database.
- Save ingest_pipeline.py.
✔️ Awesome, I've got everything!
Great. Make sure you have saved your file before moving on.
ⓧ I'd like to double check the full code
import os
from llama_index.core import SimpleDirectoryReader, PropertyGraphIndex
from llama_index.core.node_parser import SentenceSplitter
from graph_manager import GraphDatabaseManager
DOCUMENT_ACCESS_POLICIES = {
"patient_monitor.md": {
"device_type": "patient_monitor",
"device_name": "CardioWatch Pro X200",
"required_role": "doctor",
"safety_class": "IEC 62304 Class C",
},
"infusion_pump.md": {
"device_type": "infusion_pump",
"device_name": "MedFlow Precision 3000",
"required_role": "technician",
"safety_class": "IEC 62304 Class C",
},
"blood_pressure_monitor.md": {
"device_type": "blood_pressure_monitor",
"device_name": "VitalLink BP Connect",
"required_role": "patient",
"safety_class": "IEC 62304 Class B",
},
}
class IngestionPipeline:
"""Ingests documents, applies access control metadata, and builds the Neo4j graph."""
def __init__(self, data_directory: str = "data"):
self.data_dir = data_directory
self.manager = GraphDatabaseManager()
def run(self):
print(f"Reading data directory: {os.path.abspath(self.data_dir)}")
if not os.path.exists(self.data_dir):
raise FileNotFoundError(
f"Source folder '{self.data_dir}' not found."
)
reader = SimpleDirectoryReader(self.data_dir)
documents = reader.load_data()
for doc in documents:
filename = os.path.basename(doc.metadata.get("file_name", ""))
if filename in DOCUMENT_ACCESS_POLICIES:
doc.metadata.update(DOCUMENT_ACCESS_POLICIES[filename])
print(
f"Tagged {filename} with role: "
f"{DOCUMENT_ACCESS_POLICIES[filename]['required_role']}"
)
node_parser = SentenceSplitter(
chunk_size=512,
chunk_overlap=50,
paragraph_separator="\n\n",
)
print("Building Neo4j Knowledge Graph...")
PropertyGraphIndex.from_documents(
documents,
property_graph_store=self.manager.graph_store,
transformations=[node_parser],
)
print("Knowledge Graph construction complete.")
if __name__ == "__main__":
pipeline = IngestionPipeline()
pipeline.run()
Execute the ingestion pipeline
With the pipeline code in place, you can now run it to extract entities and relationships from your three device manuals and write them into Neo4j.
- Run the ingestion pipeline by executing this command in your terminal:
python ingest_pipeline.py
What should I see?
The pipeline will take 1-3 minutes to run. The LLM extracts entities and relationships from each document chunk, which involves multiple API calls to Groq.
You should see output similar to:
- Three "Tagged ... with role:" lines confirming metadata injection.
- A "Building Neo4j Knowledge Graph..." message.
- A final "Knowledge Graph construction complete." confirmation.
- Wait for the script to finish and confirm you see "Knowledge Graph construction complete." in the terminal output.
Seeing a connection error or timeout?
- Make sure your neo4j-apoc container is still running. Check with docker ps and restart it if needed.
- Verify your .env file has the correct Neo4j password (graphrag2026) matching what you set when starting the container.
- If you see a Groq API error, confirm your GROQ_API_KEY in .env is valid and has available free-tier credits.
Why is my ingestion pipeline failing to connect?
Seeing rate limit errors from Groq?
The knowledge graph extraction makes many LLM calls. If you hit Groq's free-tier rate limit, wait 60 seconds and run the script again. The graph store will continue where it left off for any documents that were already processed.
Help me handle Groq rate limits during graph ingestion.
Your Neo4j knowledge graph is now populated with security-classified entities and relationships from all three device manuals. Next up, you will build the FastAPI gateway that enforces these access policies using JWT tokens at query time.
Build the Secure FastAPI Gateway
Your Neo4j knowledge graph is populated with medical device documentation, each chunk tagged with security metadata. The graph knows which documents belong to doctors, technicians, and patients.
Now you need a gatekeeper. Without access control, any user could query any document regardless of their clearance. In this step, you will build a FastAPI gateway that issues JWT tokens encoding a user's role, verifies those tokens on every request, and filters graph retrieval to only return documents matching the authenticated role.
In this step, get ready to:
- Write a JWT token generation endpoint for role-based authentication.
- Implement a token verification dependency that protects routes.
- Build a secure query endpoint that filters GraphRAG results by user role.
Write JWT token generation endpoint using PyJWT with HS256 signing
JWT tokens are compact, self-contained strings that carry claims about a user. Your gateway will issue tokens that encode a user's role (doctor, technician, or patient) so the query endpoint knows what data to return.
- Create a new file called app.py in your project root (the same folder where config.py and graph_manager.py live).
- Add the imports and application setup by pasting the following code:
import jwt
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel
from config import AppSettings
from graph_manager import GraphDatabaseManager
from llama_index.core.vector_stores.types import MetadataFilters, MetadataFilter
from llama_index.core.indices.property_graph import VectorContextRetriever
# Initialize the FastAPI application with metadata
app = FastAPI(
title="Healthcare GraphRAG Gateway",
description="Secure entry point for querying medical IoT device knowledge graphs.",
)
# HTTPBearer extracts the token from the Authorization header
bearer_scheme = HTTPBearer()
# Reuse the graph manager from earlier steps
graph_manager = GraphDatabaseManager()
# Define the request schema for incoming queries
class QueryRequest(BaseModel):
query: str
What does this code do?
This sets up the FastAPI application and its core dependencies.
- HTTPBearer tells FastAPI to look for a Bearer <token> header on protected routes.
- GraphDatabaseManager connects to Neo4j so the API can run graph queries.
- QueryRequest is a Pydantic model that validates incoming JSON bodies have a query field.
- Below the QueryRequest class, add the token generation endpoint:
@app.get("/api/v1/auth/token")
def issue_test_token(role: str = "patient"):
"""Generates JWT tokens for testing role-based access."""
# Validate the requested role against allowed values
if role not in ["doctor", "technician", "patient"]:
raise HTTPException(
status_code=400,
detail="Role must be one of: doctor, technician, patient",
)
# Build JWT claims with user identity and role
claims = {
"sub": "TestUser",
"role": role,
"iss": "healthcare_graphrag_dev",
}
# Sign the token with HS256 using the secret from config
token = jwt.encode(
claims,
AppSettings.JWT_SECRET,
algorithm=AppSettings.JWT_ALGORITHM,
)
return {"token": token, "role": role}
What does this endpoint do?
The /api/v1/auth/token endpoint generates signed JWT tokens for testing.
- The sub claim identifies the user (hardcoded to TestUser for development).
- The role claim controls which documents the user can access.
- PyJWT signs the token with HS256 so the server can verify it was not tampered with.
- Save app.py.
Seeing an import error?
Make sure your virtual environment is activated. The jwt import comes from the PyJWT package, not from a package called jwt. If you installed both, uninstall jwt and keep only PyJWT.
Help me debug a PyJWT import error in my FastAPI app.
Implement token verification dependency using FastAPI's HTTPBearer and Depends pattern
FastAPI's dependency injection system lets you run verification logic before a request reaches the endpoint. You will write a function that decodes the JWT from the Authorization header and rejects invalid or expired tokens with a 401 response.
- In app.py, find the QueryRequest class you just wrote. Add the following function between QueryRequest and the issue_test_token endpoint:
def get_authenticated_user(
credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme),
) -> dict:
"""Verifies JWT token and returns decoded claims."""
token = credentials.credentials
try:
# Decode and verify the token signature
payload = jwt.decode(
token,
AppSettings.JWT_SECRET,
algorithms=[AppSettings.JWT_ALGORITHM],
)
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token has expired.",
)
except jwt.InvalidTokenError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token.",
)
What does this dependency do?
Any endpoint that includes Depends(get_authenticated_user) will automatically run this function first.
- HTTPBearer extracts the raw token string from the Authorization: Bearer <token> header.
- jwt.decode() verifies the signature and returns the claims dictionary if valid.
- Two exception handlers catch expired tokens and any other invalid token condition separately.
Why separate ExpiredSignatureError from InvalidTokenError?
An expired token means the user was legitimate but their session timed out. An invalid token means something is fundamentally wrong (tampered, malformed, wrong secret). In production, you would log these differently for security monitoring.
- Save app.py.
Indentation problems?
Make sure get_authenticated_user is a module-level function (no indentation), not nested inside another function or class. It should sit between the QueryRequest class and the @app.get decorator of issue_test_token.
Help me fix indentation in my FastAPI dependency function.
Build the secure query endpoint with role-based metadata filtering
This is where the access control comes together. The query endpoint extracts the user's role from the verified JWT, constructs a metadata filter matching that role, and passes it to the VectorContextRetriever. The retriever only returns graph nodes whose required_role metadata matches the authenticated user.
- In app.py, add the following endpoint between get_authenticated_user and issue_test_token:
@app.post("/api/v1/query")
async def secure_graph_query(
request: QueryRequest,
user_payload: dict = Depends(get_authenticated_user),
):
"""Executes GraphRAG query filtered by user role from JWT claims."""
# Extract identity and role from the verified JWT claims
username = user_payload.get("sub", "Anonymous")
user_role = user_payload.get("role", "guest")
print(f"Query from user '{username}' with role '{user_role}'")
# Build metadata filter to restrict results to this user's clearance
dynamic_filters = MetadataFilters(
filters=[MetadataFilter(key="required_role", value=user_role)]
)
try:
index = graph_manager.get_existing_index()
# Configure retriever to only return documents matching the role
vector_retriever = VectorContextRetriever(
index.property_graph_store,
embed_model=index._embed_model,
similarity_top_k=3,
filters=dynamic_filters,
)
query_engine = index.as_query_engine(
sub_retrievers=[vector_retriever]
)
result = query_engine.query(request.query)
return {
"status": "success",
"query": request.query,
"response": str(result),
"authenticated_role": user_role,
}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Query execution failed: {str(e)}",
)
What does this endpoint do?
This is the core of your zero-trust retrieval system.
- Depends(get_authenticated_user) runs the JWT verification before the endpoint body executes. If the token is invalid, the request never reaches the query logic.
- MetadataFilter(key="required_role", value=user_role) ensures the vector retriever only returns chunks whose access policy matches the caller's role.
- VectorContextRetriever performs semantic search over the property graph with similarity_top_k=3, returning the three most relevant filtered chunks.
- The query engine combines retrieved graph context with the LLM to produce a natural language answer.
Why VectorContextRetriever instead of passing filters to as_query_engine?
LlamaIndex's property graph MetadataFilters work through the VectorContextRetriever class, not through the query engine directly. You instantiate the retriever with filters, then pass it as a sub_retriever to the query engine. This gives you fine-grained control over how vector search interacts with the graph store.
- Save app.py.
- Confirm your file has four top-level definitions in this order: QueryRequest, get_authenticated_user, secure_graph_query, issue_test_token.
Seeing a NameError for get_authenticated_user?
Python reads files top to bottom. If secure_graph_query references get_authenticated_user in its Depends() call, the dependency function must be defined above it in the file.
Help me fix a NameError in my FastAPI gateway.
✔️ Awesome, I've got everything!
Great. Double check you have saved app.py with all four sections: imports/setup, verification dependency, query endpoint, and token endpoint.
ⓧ I'd like to double check the full code
Here is the complete app.py file. Make sure yours matches:
import jwt
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel
from config import AppSettings
from graph_manager import GraphDatabaseManager
from llama_index.core.vector_stores.types import MetadataFilters, MetadataFilter
from llama_index.core.indices.property_graph import VectorContextRetriever
app = FastAPI(
title="Healthcare GraphRAG Gateway",
description="Secure entry point for querying medical IoT device knowledge graphs.",
)
bearer_scheme = HTTPBearer()
graph_manager = GraphDatabaseManager()
class QueryRequest(BaseModel):
query: str
def get_authenticated_user(
credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme),
) -> dict:
"""Verifies JWT token and returns decoded claims."""
token = credentials.credentials
try:
payload = jwt.decode(
token,
AppSettings.JWT_SECRET,
algorithms=[AppSettings.JWT_ALGORITHM],
)
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token has expired.",
)
except jwt.InvalidTokenError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token.",
)
@app.post("/api/v1/query")
async def secure_graph_query(
request: QueryRequest,
user_payload: dict = Depends(get_authenticated_user),
):
"""Executes GraphRAG query filtered by user role from JWT claims."""
username = user_payload.get("sub", "Anonymous")
user_role = user_payload.get("role", "guest")
print(f"Query from user '{username}' with role '{user_role}'")
dynamic_filters = MetadataFilters(
filters=[MetadataFilter(key="required_role", value=user_role)]
)
try:
index = graph_manager.get_existing_index()
vector_retriever = VectorContextRetriever(
index.property_graph_store,
embed_model=index._embed_model,
similarity_top_k=3,
filters=dynamic_filters,
)
query_engine = index.as_query_engine(
sub_retrievers=[vector_retriever]
)
result = query_engine.query(request.query)
return {
"status": "success",
"query": request.query,
"response": str(result),
"authenticated_role": user_role,
}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Query execution failed: {str(e)}",
)
@app.get("/api/v1/auth/token")
def issue_test_token(role: str = "patient"):
"""Generates JWT tokens for testing role-based access."""
if role not in ["doctor", "technician", "patient"]:
raise HTTPException(
status_code=400,
detail="Role must be one of: doctor, technician, patient",
)
claims = {
"sub": "TestUser",
"role": role,
"iss": "healthcare_graphrag_dev",
}
token = jwt.encode(
claims,
AppSettings.JWT_SECRET,
algorithm=AppSettings.JWT_ALGORITHM,
)
return {"token": token, "role": role}
Your secure gateway is ready. It can issue tokens, verify them, and filter graph queries by role. Next up, you will launch the server and prove that the access control actually works by testing with different roles.
Test the Access Control System
Your gateway is built. The knowledge graph is populated, the JWT authentication layer is in place, and the metadata filters are wired to role claims.
Now you need to prove it actually works. In this step, you will launch the FastAPI server, generate tokens for each role, and query the system to confirm that each role only sees the device data it is authorized to access.
In this step, get ready to:
- Launch the FastAPI server and open the Swagger UI.
- Generate JWT tokens for each role and test authorized queries.
- Confirm that cross-role data isolation prevents unauthorized access.
Launch the FastAPI server with uvicorn
Uvicorn is the ASGI server that runs your FastAPI application. The --reload flag enables hot-reload during development, so any changes to app.py restart the server automatically.
- Start the FastAPI server by running this command in your terminal:
uvicorn app:app --reload
What does this command do?
The app:app argument tells uvicorn to look in the file app.py for the FastAPI instance named app. The server listens on port 8000 by default.
- You should see output indicating the server started successfully, including a line like Uvicorn running on http://127.0.0.1:8000.
- Open your browser and navigate to http://localhost:8000/docs.
You should see the Swagger UI interactive documentation page with the title "Healthcare GraphRAG Gateway" and two endpoints listed.
Server won't start?
- Make sure your virtual environment is activated. You should see the environment name in your terminal prompt.
- Confirm that your neo4j-apoc Docker container is still running. The GraphDatabaseManager connects to Neo4j on startup.
- Check that your .env file has a valid GROQ_API_KEY value. The AppSettings.validate_keys() call raises an error if it is missing.
Help me debug why my FastAPI server won't start.
Generate tokens and test role-based queries
The Swagger UI lets you interact with your API endpoints directly in the browser. You will first generate a JWT token for a specific role, then use that token to authenticate a GraphRAG query.
- In the Swagger UI, scroll down to the GET /api/v1/auth/token endpoint and click on it to expand it.
- Click the **Try it out** button.
- In the role parameter field, type doctor.
- Click **Execute**.
The response body shows a JSON object with a token field and role field. Copy the entire token string from the response.
- Scroll to the top of the Swagger UI page and click the **Authorize** button (it has a lock icon).
- In the value field, type Bearer followed by the token you copied. For example: Bearer eyJhbGciOi....
- Click **Authorize**, then click **Close**.
Why Bearer prefix?
The HTTPBearer security scheme expects the token in the format Bearer <token>. This follows the HTTP Authorization header standard. Swagger UI sends this header automatically with every request after you authorize.
- Scroll down to the POST /api/v1/query endpoint and click on it to expand it.
- Click **Try it out**.
- In the request body, enter the following JSON:
{
"query": "What are the alarm thresholds for the patient monitor?"
}
- Click **Execute**.
The response should include information about the CardioWatch Pro X200 alarm thresholds (heart rate, SpO2, blood pressure limits). The authenticated_role field in the response confirms the query ran with doctor permissions.
What should I see?
The response JSON includes a status of success, a response field with the LLM-generated answer about alarm thresholds, and authenticated_role showing doctor. The response content references the CardioWatch Pro X200 device because the doctor role is authorized to access patient_monitor.md data.
- Now test the technician role. Click the **Authorize** button again, click **Logout** to clear the current token, then close the dialog.
- Go back to GET /api/v1/auth/token, change the role to technician, and click **Execute**.
- Copy the new token, click **Authorize**, paste it with the Bearer prefix, and authorize.
- Go to POST /api/v1/query and query:
{
"query": "What is the maximum flow rate for the infusion pump?"
}
- Click **Execute**. The response should reference the MedFlow Precision 3000 with flow rate specifications.
- Repeat the same process for the patient role. Generate a patient token, authorize with it, and query:
{
"query": "How do I take a blood pressure reading?"
}
- Click **Execute**. The response should reference the VitalLink BP Connect usage instructions.
Getting a 500 error on queries?
- Check your terminal for the detailed error. A common cause is an invalid or expired Groq API key. Verify the key in your .env file is correct.
- If you see a Neo4j connection error, confirm neo4j-apoc is still running with docker ps.
- If the response field says the information is not available, the knowledge graph may not have been ingested properly. Re-run python ingest_pipeline.py and try again.
Help me fix a 500 error when querying my GraphRAG gateway.
Confirm cross-role data isolation
The real test of a zero-trust system is not just that authorized users CAN access their data. It is that unauthorized users CANNOT access restricted data.
- Generate a new patient token using GET /api/v1/auth/token with role patient.
- Authorize with the patient token (click **Authorize**, paste with Bearer prefix).
- Go to POST /api/v1/query and submit a query that targets doctor-classified data:
{
"query": "What are the alarm thresholds for the CardioWatch Pro X200?"
}
- Click **Execute** and examine the response.
The response should NOT contain specific alarm threshold values from the CardioWatch Pro X200 documentation. The MetadataFilter restricts retrieval to chunks tagged with required_role: patient, so the system cannot pull doctor-classified context into the response.
Why does this prove data isolation?
The VectorContextRetriever applies MetadataFilters before the similarity search runs. This means the patient role's query never even sees the doctor-classified graph nodes during retrieval. The LLM can only generate answers from the filtered context it receives.
This is fundamentally different from post-filtering (where all data is retrieved and then filtered). Pre-filtering at the retrieval layer prevents data leakage even if the LLM tries to infer across boundaries.
- Try one more cross-role test. With the same patient token still active, query for technician-classified data:
{
"query": "How do I access service mode on the infusion pump?"
}
- Click **Execute**. The response should lack specific technical service access details from the MedFlow Precision 3000 documentation.
You have now confirmed end-to-end that each role only retrieves its authorized data, and cross-role queries cannot leak restricted information through the GraphRAG pipeline.
Patient query still returning restricted data?
- Verify you authorized with a patient token, not a doctor token. Check the authenticated_role field in the response. It should say patient.
- If the metadata was not properly tagged during ingestion, the filter will not work correctly. Re-run python ingest_pipeline.py and confirm you see the tagging output for each file.
- Note that the LLM might generate a generic answer from its training data if no relevant context is found. The key indicator is the absence of specific values (exact thresholds, exact flow rates) that only exist in the restricted documents.
Help me understand why my patient token is returning doctor-classified data.
Your zero-trust GraphRAG gateway is fully operational. You have proven that cryptographic identity verification combined with metadata-filtered retrieval creates airtight data isolation across user roles.
Secret mission
Token Expiration and Graph Visualization
Your gateway enforces role-based access, but tokens live forever. In production, that is a security hole. Add JWT expiration to your tokens and visualize the knowledge graph you built in Neo4j Browser.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally with a free-tier Groq API, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Neo4j Docker container (neo4j-apoc), running locally at no cost.
- Groq API free tier usage for LLM inference.
- FastAPI server process running via uvicorn.
- Project folder on your Desktop containing Python files, virtual environment, and data/ directory.
Keep everything running
No action needed. Choose this if you're still actively experimenting with different queries and roles.
- Your Neo4j container stays running in Docker.
- Your FastAPI server continues serving requests at http://localhost:8000.
- Your Groq account remains active at no cost under the free tier.
Pause - I'll come back to this later
Shut down running processes to free up memory while keeping your files intact for later.
- Stop your FastAPI server by pressing Ctrl+C in the terminal where uvicorn is running.
- Stop the Neo4j container by running:
docker stop neo4j-apoc
- When you're ready to resume, start the container again by running:
docker start neo4j-apoc
- Your project files and Neo4j data remain on disk, ready for next time.
Delete - I don't want to use this again
Remove all project resources and start fresh. This is irreversible.
- Stop your FastAPI server by pressing Ctrl+C in the terminal where uvicorn is running.
- Stop and remove the Neo4j Docker container by running:
docker stop neo4j-apoc
docker rm neo4j-apoc
- Deactivate your Python virtual environment by running:
deactivate
- Delete the project folder from your Desktop. On macOS/Linux, run:
cd ~/Desktop
rm -rf healthcare-graphrag
- On Windows PowerShell, run:
cd ~\Desktop
Remove-Item -Recurse -Force healthcare-graphrag
- Revoke your Groq API key by navigating to the API Keys section in the Groq Console and deleting the key you created for this project.
Nice Work!
Nice Work!
You just built a production-grade secure GraphRAG system from scratch. Your Healthcare IoT gateway combines a Neo4j knowledge graph with cryptographic identity verification and graph-level data isolation.
You've learned how to:
- Built a Neo4j Property Graph from unstructured healthcare IoT documentation using LlamaIndex's knowledge graph extraction pipeline.
- Implemented JWT-based authentication with role claim propagation through FastAPI dependency injection.
- Configured VectorContextRetriever with runtime MetadataFilters to enforce data isolation at the retrieval layer, proving that unauthorized roles cannot access restricted device documentation.
- Added token expiration enforcement and visualized your knowledge graph topology in Neo4j Browser.
Ready to quiz yourself?