Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2ba004d
Update sample code for the article on LangChain chatbot
lpozo Jun 5, 2026
a7bd0d3
Use Docker Compose v2 command in README
lpozo Jun 5, 2026
ea858ed
Merge branch 'master' into langchain-rag-app
martin-martin Jul 3, 2026
f71ac4f
Merge branch 'master' into langchain-rag-app
lpozo Sep 21, 2026
7ffcf22
Remove the LangChain intro code and renumber the step folders
lpozo Sep 21, 2026
e6004e7
Bind published Compose ports to 127.0.0.1
lpozo Sep 22, 2026
6f1f888
Correct three few-shot examples in the Cypher prompt
lpozo Sep 22, 2026
5827c9d
MERGE nodes on their ID, then SET the other properties
lpozo Sep 22, 2026
413cf75
Pin the embedding model via HOSPITAL_EMBEDDING_MODEL
lpozo Sep 22, 2026
77bd509
Guard the denominator in the percent-increase example
lpozo Sep 22, 2026
26c98ba
Build the wait-times graph once and skip its schema refresh
lpozo Sep 22, 2026
1ca50f8
Retry inside the tools instead of rerunning the whole agent
lpozo Sep 22, 2026
7dc46be
Give the Streamlit request a timeout and catch transport errors
lpozo Sep 22, 2026
02f5256
Drop the request-timing scripts
lpozo Sep 22, 2026
898461e
Fix the copy-pasted comment in the frontend entrypoint
lpozo Sep 22, 2026
c2e3cf3
Drop the redundant tool-level retry and the unused httpx pin
lpozo Sep 22, 2026
f83a662
Use the Responses API for the hospital RAG agent
lpozo Sep 22, 2026
2daed73
Merge branch 'master' into langchain-rag-app
lpozo Sep 22, 2026
f0e73b6
Merge branch 'master' into langchain-rag-app
lpozo Sep 24, 2026
8090e27
Refactor example questions into a tuple and loop
lpozo Sep 24, 2026
8baec3e
Require CHATBOT_URL instead of a localhost default
lpozo Sep 24, 2026
5213ccb
Group OpenAI settings in the README's .env example
lpozo Sep 24, 2026
330b0ce
Merge branch 'master' into langchain-rag-app
lpozo Sep 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions langchain-rag-app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
__pycache__/
*.pyc
.env
chroma_data/
47 changes: 31 additions & 16 deletions langchain-rag-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,56 @@ This repo contains the source code for [Build an LLM RAG Chatbot With LangChain]

To run the final application that you'll build in this tutorial, you can use the code provided in `source_code_final/`.

## Project Layout

Each folder holds the code as it stands at the end of the matching step in the tutorial:

| Folder | Tutorial step |
| --- | --- |
| `data/` | The hospital system CSV files used throughout the tutorial |
| `source_code_step_1/` | Step 1: Understand the Business Requirements and Data |
| `source_code_step_2/` | Step 2: Set Up a Neo4j Graph Database |
| `source_code_step_3/` | Step 3: Build a Graph RAG Chatbot in LangChain |
| `source_code_step_4/` | Step 4: Deploy the LangChain Agent |
| `source_code_final/` | The finished application |

The LangChain basics that this tutorial builds on live in a separate tutorial, [LangChain Tutorial: Build Your First Chains and Agents](https://realpython.com/langchain-tutorial/), along with its own sample code.

## Setup

Create a `.env` file in the root directory and add the following environment variables:

```.env
OPENAI_API_KEY=<YOUR_OPENAI_API_KEY>

NEO4J_URI=<YOUR_NEO4J_URI>
NEO4J_USERNAME=<YOUR_NEO4J_USERNAME>
NEO4J_PASSWORD=<YOUR_NEO4J_PASSWORD>

HOSPITALS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/hospitals.csv
PAYERS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/payers.csv
PHYSICIANS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/physicians.csv
PATIENTS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/patients.csv
VISITS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/visits.csv
REVIEWS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/reviews.csv
HOSPITALS_CSV_PATH=https://raw.githubusercontent.com/realpython/materials/refs/heads/master/langchain-rag-app/data/hospitals.csv
PAYERS_CSV_PATH=https://raw.githubusercontent.com/realpython/materials/refs/heads/master/langchain-rag-app/data/payers.csv
PHYSICIANS_CSV_PATH=https://raw.githubusercontent.com/realpython/materials/refs/heads/master/langchain-rag-app/data/physicians.csv
PATIENTS_CSV_PATH=https://raw.githubusercontent.com/realpython/materials/refs/heads/master/langchain-rag-app/data/patients.csv
VISITS_CSV_PATH=https://raw.githubusercontent.com/realpython/materials/refs/heads/master/langchain-rag-app/data/visits.csv
REVIEWS_CSV_PATH=https://raw.githubusercontent.com/realpython/materials/refs/heads/master/langchain-rag-app/data/reviews.csv

HOSPITAL_AGENT_MODEL=gpt-3.5-turbo-1106
HOSPITAL_CYPHER_MODEL=gpt-3.5-turbo-1106
HOSPITAL_QA_MODEL=gpt-3.5-turbo-0125
OPENAI_API_KEY=<YOUR_OPENAI_API_KEY>
HOSPITAL_AGENT_MODEL=gpt-5.6-luna
HOSPITAL_CYPHER_MODEL=gpt-5.6-luna
HOSPITAL_QA_MODEL=gpt-5.6-luna
HOSPITAL_EMBEDDING_MODEL=text-embedding-3-small

CHATBOT_URL=http://host.docker.internal:8000/hospital-rag-agent
CHATBOT_URL=http://chatbot_api:8000/hospital-rag-agent
```

The chatbot uses OpenAI LLMs, so you'll need to create an [OpenAI API key](https://realpython.com/generate-images-with-dalle-openai-api/#get-your-openai-api-key) and store it as `OPENAI_API_KEY`.

The three `NEO4J_` variables are used to connect to your Neo4j AuraDB instance. Follow the directions [here](https://neo4j.com/cloud/platform/aura-graph-database/?ref=docs-nav-get-started) to create a free instance.
The three `NEO4J_` variables configure the Neo4j Community Edition instance that Docker Compose runs for you, so there's no cloud account to set up. Point `NEO4J_URI` at the Bolt endpoint on your machine, use the default `neo4j` user, and choose your own password of at least eight characters. Compose reads `NEO4J_PASSWORD` when it creates the database container, and the ETL and chatbot services reach Neo4j over the Compose network instead of `localhost`.

Once you have a running Neo4j instance, and have filled out all the environment variables in `.env`, you can run the entire project with [Docker Compose](https://docs.docker.com/compose/). You can install Docker Compose by following [these directions](https://docs.docker.com/compose/install/).
Keep your real credentials in `.env` only. That file is listed in `.gitignore`, so it stays out of version control.

Once you've filled in all of the environment variables, set up a Neo4j AuraDB instance, and installed Docker Compose, open a terminal and run:
Once you've filled in all of the environment variables and installed [Docker Compose](https://docs.docker.com/compose/install/), open a terminal and run:

```console
$ docker-compose up --build
$ docker compose up --build
```

After each container finishes building, you'll be able to access the chatbot API at `http://localhost:8000/docs` and the Streamlit app at `http://localhost:8501/`.
8 changes: 3 additions & 5 deletions langchain-rag-app/source_code_final/chatbot_api/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
# chatbot_api/Dockerfile

FROM python:3.11-slim
FROM python:3.14-slim

WORKDIR /app
COPY ./src/ /app

COPY ./pyproject.toml /code/pyproject.toml
RUN pip install /code/.
RUN python -m pip install /code/.

EXPOSE 8000
CMD ["sh", "entrypoint.sh"]
CMD ["sh", "entrypoint.sh"]
22 changes: 10 additions & 12 deletions langchain-rag-app/source_code_final/chatbot_api/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,16 @@
name = "chatbot_api"
version = "0.1"
dependencies = [
"asyncio==3.4.3",
"fastapi==0.109.0",
"langchain==0.1.0",
"langchain-openai==0.0.2",
"langchainhub==0.1.14",
"neo4j==5.14.1",
"numpy==1.26.2",
"openai==1.7.2",
"opentelemetry-api==1.22.0",
"pydantic==2.5.1",
"uvicorn==0.25.0"
"fastapi==0.136.3",
"langchain==1.3.4",
"langchain-openai==1.2.2",
"langchain-neo4j==0.9.0",
"neo4j==6.2.0",
"numpy==2.4.6",
"openai==2.41.0",
"pydantic==2.13.4",
"uvicorn==0.49.0"
]

[project.optional-dependencies]
dev = ["black", "flake8"]
dev = ["ruff"]
Original file line number Diff line number Diff line change
@@ -1,23 +1,46 @@
import os

from langchain.agents import create_agent
from langchain_core.tools import Tool
from langchain_openai import ChatOpenAI

from chains.hospital_cypher_chain import hospital_cypher_chain
from chains.hospital_review_chain import reviews_vector_chain
from langchain import hub
from langchain.agents import AgentExecutor, Tool, create_openai_functions_agent
from langchain_openai import ChatOpenAI
from tools.wait_times import (
get_current_wait_times,
get_most_available_hospital,
)

HOSPITAL_AGENT_MODEL = os.getenv("HOSPITAL_AGENT_MODEL")

hospital_agent_prompt = hub.pull("hwchase17/openai-functions-agent")
agent_system_prompt = (
"You are a helpful assistant for a hospital system. Use the tools "
"available to you to answer the user's questions about patients, "
"visits, physicians, hospitals, insurance payers, patient reviews, "
"and current wait times."
)


def query_reviews(query: str) -> str:
"""Answer questions about patient experiences from their reviews."""
try:
return reviews_vector_chain.invoke(query)
except Exception as e:
return f"The patient reviews are unavailable right now: {e}"


def query_graph(query: str) -> str:
"""Answer questions by querying the hospital graph database."""
try:
return hospital_cypher_chain.invoke(query)["result"]
except Exception as e:
return f"The hospital database couldn't answer that: {e}"


tools = [
Tool(
name="Experiences",
func=reviews_vector_chain.invoke,
func=query_reviews,
description="""Useful when you need to answer questions
about patient experiences, feelings, or any other qualitative
question that could be answered about a patient using semantic
Expand All @@ -30,7 +53,7 @@
),
Tool(
name="Graph",
func=hospital_cypher_chain.invoke,
func=query_graph,
description="""Useful for answering questions about patients,
physicians, hospitals, insurance payers, patient review
statistics, and hospital visit details. Use the entire prompt as
Expand Down Expand Up @@ -63,20 +86,10 @@
),
]

chat_model = ChatOpenAI(
model=HOSPITAL_AGENT_MODEL,
temperature=0,
)

hospital_rag_agent = create_openai_functions_agent(
llm=chat_model,
prompt=hospital_agent_prompt,
tools=tools,
)
chat_model = ChatOpenAI(model=HOSPITAL_AGENT_MODEL, use_responses_api=True)

hospital_rag_agent_executor = AgentExecutor(
agent=hospital_rag_agent,
hospital_rag_agent_executor = create_agent(
model=chat_model,
tools=tools,
return_intermediate_steps=True,
verbose=True,
system_prompt=agent_system_prompt,
)
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import os

from langchain.chains import GraphCypherQAChain
from langchain.prompts import PromptTemplate
from langchain_community.graphs import Neo4jGraph
from langchain_core.prompts import PromptTemplate
from langchain_neo4j import GraphCypherQAChain, Neo4jGraph
from langchain_openai import ChatOpenAI

HOSPITAL_QA_MODEL = os.getenv("HOSPITAL_QA_MODEL")
Expand Down Expand Up @@ -50,7 +49,8 @@
# Which physician has billed the least to Cigna
MATCH (p:Payer)<-[c:COVERED_BY]-(v:Visit)-[t:TREATS]-(phy:Physician)
WHERE p.name = 'Cigna'
RETURN phy.name AS physician_name, SUM(c.billing_amount) AS total_billed
RETURN phy.id AS physician_id, phy.name AS physician_name,
SUM(c.billing_amount) AS total_billed
ORDER BY total_billed
LIMIT 1

Expand All @@ -64,33 +64,37 @@
v.admission_date < '2023-01-01' THEN 1 ELSE 0 END) AS count_2022,
SUM(CASE WHEN v.admission_date >= '2023-01-01' AND
v.admission_date < '2024-01-01' THEN 1 ELSE 0 END) AS count_2023
WHERE count_2022 > 0
WITH state, visit_count, count_2022, count_2023,
(toFloat(count_2023) - toFloat(count_2022)) / toFloat(count_2022) * 100
AS percent_increase
RETURN state, percent_increase
ORDER BY percent_increase DESC
LIMIT 1

# How many non-emergency patients in North Carolina have written reviews?
match (r:Review)<-[:WRITES]-(v:Visit)-[:AT]->(h:Hospital)
where h.state_name = 'NC' and v.admission_type <> 'Emergency'
return count(*)
# How many reviews are there for non-emergency visits at North Carolina
# hospitals?
MATCH (r:Review)<-[:WRITES]-(v:Visit)-[:AT]->(h:Hospital)
WHERE h.state_name = 'NC' and v.admission_type <> 'Emergency'
RETURN count(*)

String category values:
Test results are one of: 'Inconclusive', 'Normal', 'Abnormal'
Visit statuses are one of: 'OPEN', 'DISCHARGED'
Admission Types are one of: 'Elective', 'Emergency', 'Urgent'
Payer names are one of: 'Cigna', 'Blue Cross', 'UnitedHealthcare', 'Medicare',
Payer names are one of: 'Cigna', 'Blue Cross', 'UnitedHealthcare', 'Medicaid',
'Aetna'

A visit is considered open if its status is 'OPEN' and the discharge date is
missing.
Use abbreviations when
filtering on hospital states (e.g. "Texas" is "TX",
"Colorado" is "CO", "North Carolina" is "NC",
"Florida" is "FL", "Georgia" is "GA, etc.)
"Florida" is "FL", "Georgia" is "GA", etc.)

Make sure to use IS NULL or IS NOT NULL when analyzing missing properties.
Use duration.inDays(date1, date2).days when counting the number of days
between two dates.
Never return embedding properties in your queries. You must never include the
statement "GROUP BY" in your query. Make sure to alias all statements that
follow as with statement (e.g. WITH v as visit, c.billing_amount as
Expand All @@ -109,7 +113,7 @@
qa_generation_template = """You are an assistant that takes the results
from a Neo4j Cypher query and forms a human-readable response. The
query results section contains the results of a Cypher query that was
generated based on a users natural language question. The provided
generated based on a user's natural language question. The provided
information is authoritative, you must never doubt it or try to use
your internal knowledge to correct it. Make the answer sound like a
response to the question.
Expand All @@ -128,15 +132,14 @@
results are in units of days unless otherwise specified.

When names are provided in the query results, such as hospital names,
beware of any names that have commas or other punctuation in them.
beware of any names that have commas or other punctuation in them.
For instance, 'Jones, Brown and Murray' is a single hospital name,
not multiple hospitals. Make sure you return any list of names in
a way that isn't ambiguous and allows someone to tell what the full
names are.

Never say you don't have the right information if there is data in
the query results. Make sure to show all the relevant query results
if you're asked.
the query results. Always use the data in the query results.

Helpful Answer:
"""
Expand All @@ -146,12 +149,13 @@
)

hospital_cypher_chain = GraphCypherQAChain.from_llm(
cypher_llm=ChatOpenAI(model=HOSPITAL_CYPHER_MODEL, temperature=0),
qa_llm=ChatOpenAI(model=HOSPITAL_QA_MODEL, temperature=0),
cypher_llm=ChatOpenAI(model=HOSPITAL_CYPHER_MODEL),
qa_llm=ChatOpenAI(model=HOSPITAL_QA_MODEL),
graph=graph,
verbose=True,
qa_prompt=qa_generation_prompt,
cypher_prompt=cypher_generation_prompt,
validate_cypher=True,
top_k=100,
allow_dangerous_requests=True,
)
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
import logging
import os

from langchain.chains import RetrievalQA
from langchain.prompts import (
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import (
ChatPromptTemplate,
HumanMessagePromptTemplate,
PromptTemplate,
SystemMessagePromptTemplate,
)
from langchain.vectorstores.neo4j_vector import Neo4jVector
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_core.runnables import RunnablePassthrough
from langchain_neo4j import Neo4jVector
from langchain_openai import ChatOpenAI
from langchain_openai import OpenAIEmbeddings

# Silence Neo4j's deprecation notice for db.index.vector.queryNodes
logging.getLogger("neo4j.notifications").setLevel(logging.ERROR)

HOSPITAL_QA_MODEL = os.getenv("HOSPITAL_QA_MODEL")
HOSPITAL_EMBEDDING_MODEL = os.getenv("HOSPITAL_EMBEDDING_MODEL")

neo4j_vector_index = Neo4jVector.from_existing_graph(
embedding=OpenAIEmbeddings(),
embedding=OpenAIEmbeddings(model=HOSPITAL_EMBEDDING_MODEL),
url=os.getenv("NEO4J_URI"),
username=os.getenv("NEO4J_USERNAME"),
password=os.getenv("NEO4J_PASSWORD"),
Expand All @@ -29,11 +36,10 @@
)

review_template = """Your job is to use patient
reviews to answer questions about their experience at
a hospital. Use the following context to answer questions.
Be as detailed as possible, but don't make up any information
that's not from the context. If you don't know an answer,
say you don't know.
reviews to answer questions about their experience at a hospital. Use
the following context to answer questions. Be as detailed as possible,
but don't make up any information that's not from the context. If you
don't know an answer, say you don't know.
{context}
"""

Expand All @@ -52,9 +58,12 @@
input_variables=["context", "question"], messages=messages
)

reviews_vector_chain = RetrievalQA.from_chain_type(
llm=ChatOpenAI(model=HOSPITAL_QA_MODEL, temperature=0),
chain_type="stuff",
retriever=neo4j_vector_index.as_retriever(k=12),
reviews_retriever = neo4j_vector_index.as_retriever(search_kwargs={"k": 12})
review_chat_model = ChatOpenAI(model=HOSPITAL_QA_MODEL)

reviews_vector_chain = (
{"context": reviews_retriever, "question": RunnablePassthrough()}
| review_prompt
| review_chat_model
| StrOutputParser()
)
reviews_vector_chain.combine_documents_chain.llm_chain.prompt = review_prompt
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
echo "Starting hospital RAG FastAPI service..."

# Start the main application
uvicorn main:app --host 0.0.0.0 --port 8000
uvicorn main:app --host 0.0.0.0 --port 8000
Loading
Loading