Skip to content

Update sample code for the article on LangChain chatbot - #778

Open
lpozo wants to merge 18 commits into
masterfrom
langchain-rag-app
Open

lpozo wants to merge 18 commits into
masterfrom
langchain-rag-app

Conversation

@lpozo

@lpozo lpozo commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Updates the langchain-rag-app sample code to match the refreshed Build an LLM RAG Chatbot With LangChain tutorial.

What changed

  • Modernized all step code to LangChain 1.x (create_agent, langchain-neo4j, …) and the gpt-5.5 models, matching the updated article step by step (source_code_step_1source_code_final).
  • De-duplicated the data: extracted the shared hospital CSVs into a single top-level data/ folder and removed the per-step copies. Step 1 keeps its own data/reviews.csv (it only needs the reviews). CSV URLs in the article and README now point to langchain-rag-app/data/.
  • Removed the committed chroma_data/ vector-store artifact (it's regenerated by create_retriever.py) and added a .gitignore.
  • requirements.txt in every step, pinned consistently with each pyproject.toml and the article, using the latest releases (openai==2.41.0, uvicorn==0.49.0, polars==1.41.2, httpx==0.28.1).
  • README updated (CSV URLs, model names, CHATBOT_URL).
  • ruff format and ruff check both pass.

🤖 Generated with Claude Code

Update: article split into two tutorials

The article was split in two, so the LangChain fundamentals now live in their own tutorial, LangChain Tutorial: Build Your First Chains and Agents.

  • Removed source_code_step_1/ (langchain_intro/ plus its local data/reviews.csv). That code backs the new fundamentals tutorial, not this one.
  • Renumbered the remaining folders to match the article's new table of contents: old step_2step_5 become step_1step_4. Each folder is byte-identical to the one it replaces; only the names changed. data/ and source_code_final/ are untouched.
  • Switched the models to gpt-5.6-luna, matching the fundamentals tutorial.
  • README: added a folder-to-step map, updated the model names, and replaced the stale Neo4j AuraDB setup with the local Community Edition instance that Compose now starts. Credentials stay as placeholders.
  • ruff format --check and ruff check both pass on the folder.
Folder Article step
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

Needs a follow-up before merge

The fundamentals tutorial still links to langchain-rag-app/source_code_step_1/data/reviews.csv, which this PR deletes. That link will 404 once this merges, so the intro code needs its own folder (a langchain-tutorial/ PR) and the article link needs repointing.

lpozo and others added 18 commits June 5, 2026 10:33
Align the run command with the tutorial, which uses the
`docker compose` (v2) form rather than the deprecated `docker-compose`.
The LangChain fundamentals content now lives in its own tutorial, so the
langchain_intro/ sample code no longer belongs with this article. Renumber
the remaining step folders to match the article's new table of contents,
and update the README with the new layout, the gpt-5.6-luna model names,
and the local Neo4j Community Edition setup that replaced AuraDB.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Docker publishes to all host interfaces by default, which exposed the
unauthenticated chatbot API, the Neo4j Bolt and Browser ports, and the
Streamlit frontend to anything that could reach the host. Combined with
the LLM-generated Cypher the API runs, a reachable client could spend
API credits or modify the database.

Technical review finding 11.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Finding 7: the North Carolina example counted Visit-to-Review matches
while its question asked for patients, so the question now describes what
the query computes. The query is unchanged.

Finding 8: the Cigna billing example grouped by phy.name, which merges
physicians who share a name. physicians.csv has 16 duplicated names, all
of which collide on Cigna, so the example now groups by the physician
node and returns both id and name.

Finding 9: duration.between() splits an interval into months and
remaining days, so .days is not total elapsed days. Added an instruction
to use duration.inDays() when counting days between two dates.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MERGE matches the entire pattern it is given, so folding every property
into the MERGE made a renamed hospital or a corrected physician record
look like a new node. The uniqueness constraint on id then rejected it,
and @Retry(tries=100, delay=10) spent roughly sixteen minutes retrying a
permanent failure. Re-running the ETL against updated CSVs now updates
the existing nodes instead.

All six node loaders change. The Visit loader's ON CREATE SET / ON MATCH
SET pairs collapse into the same SET, since merging on the ID alone
makes the two branches identical.

COVERED_BY used ON CREATE SET, so a corrected billing amount never
reached an existing relationship. It now uses SET.

Technical review finding 15.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
OpenAIEmbeddings() silently defaulted to text-embedding-ada-002 while
every chat model was pinned through .env. The embedding model now follows
the same pattern and names text-embedding-3-small, which scores higher on
MTEB (62.3% vs 61.0%), costs a fifth as much per token, and keeps the
same 1536 dimensions, so the vector index definition is unchanged.

Switching models does require regenerating the stored vectors:
from_existing_graph() only embeds nodes where the embedding property is
null, so existing reviews keep their old vectors otherwise.

Technical review finding 20.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The prompt tells the model twice to filter denominators to be non-zero,
then shows an example that divides by count_2022 without checking it. In
Cypher, dividing a float by zero yields Infinity rather than an error, so
a state with 2023 visits and no 2022 visits would sort straight to the
top of ORDER BY percent_increase DESC LIMIT 1 and win silently.

No state in the supplied data has zero visits in 2022 for either payer,
so this changes no result today: Medicaid still gives TX at 8.823529%.
The guard is defensive, and it stops the example from contradicting the
instructions the model is asked to follow.

Technical review finding 23.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
_get_current_hospitals() constructed a new Neo4jGraph on every call, and
get_most_available_hospital() calls it once per hospital to validate
names it just read from the database. On the supplied data that meant 31
drivers, 124 APOC schema queries, and 31 list queries to produce 30
random integers.

The graph is now built once at module level, named and shaped the same
way as the one in hospital_cypher_chain.py. It also passes
refresh_schema=False: these functions only run a single Cypher query and
never touch Neo4jGraph.query's schema, so the four apoc.meta.data and
apoc.schema.nodes calls per construction were pure overhead.

The name validation is unchanged. get_current_wait_times() takes
arbitrary input from the agent and still needs it.

Technical review finding 25.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@async_retry wrapped the entire agent invocation, so any failure reran
create_agent from scratch. Measured against langchain 1.3.4: a tool
raising RuntimeError propagates out of ainvoke, and max_retries=10 turned
one failed request into ten agent runs and ten billable model calls
before raising ValueError("Failed after 10 attempts") with __cause__ set
to None. A malformed generated query cost the same as a dropped
connection, and validate_cypher does not catch syntax errors, so that
case is reachable.

The retry now sits on the chain call itself, where it belongs. It reruns
only ServiceUnavailable, which is what Neo4j raises when a connection
drops and what a second attempt can actually fix, and retrying the query
pulls a fresh connection from the driver's pool, which is what the
article claimed retries did all along. Everything else fails on the first
attempt.

The wrapper then catches whatever is left and returns a sentence, so the
agent reports the problem to the user instead of the request dying. One
agent run per request, either way.

utils/async_utils.py is deleted. Nothing called it once the tools handled
their own failures.

Technical review finding 27.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The UI checked response.status_code for errors, but that branch only
runs when a response arrives. An unreachable API raises
requests.exceptions.ConnectionError before there is any status to check,
so the friendly message the code already contained was unreachable in
the most likely failure and the user got a Streamlit traceback instead.

This is easy to hit: chatbot_frontend depends on chatbot_api with no
health condition, so the UI accepts input while the API is still
importing chains and building the vector index.

requests also has no default timeout, so a stalled connection hung the
interface indefinitely.

Technical review finding 28.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
These two scripts lived in tests/ but tested nothing: no assertions, no
framework, just a printed duration. The article used them to compare 68
seconds of synchronous requests against 18 asynchronous ones, which
side-tracked the deployment narrative into a benchmarking demo, and the
two files duplicated the same fourteen-question list verbatim.

They also reported success on total failure. Neither script checked
response status, so fourteen fast error responses printed a run time as
though fourteen questions had been answered, and the synchronous script
had no timeout.

The article keeps its explanation of why FastAPI serves requests
asynchronously, which is the part that bears on the project.

Technical review finding 29.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
chatbot_frontend/src/entrypoint.sh said "Run the ETL script" above the
line that starts Streamlit. Carried over from the ETL entrypoint it was
copied from.

Found while reading the article end to end for leftovers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The @Retry(ServiceUnavailable) decorators added for finding 27 duplicated
work the driver already does. Neo4jGraph.query calls
driver.execute_query, which runs the query through session.execute_read
or execute_write. The driver documents these managed transactions as
providing "a retry-mechanism for appropriate errors", and
ServiceUnavailable.is_retryable() returns True, so the driver already
retries with exponential backoff for up to max_transaction_retry_time,
30 seconds by default. Retrying three more times on top of that bought
nothing.

The wrappers keep the part that mattered: catching what retrying cannot
fix, such as a malformed generated query, and returning a sentence so the
agent reports the problem instead of the request dying.

This removes the retry dependency from chatbot_api, which the ETL still
uses for its own purpose of waiting out Neo4j startup.

httpx==0.28.1 also goes. It existed only for the request-timing scripts
removed in the previous commit and is now unused project-wide.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gpt-5.6-luna rejects function tools alongside reasoning_effort on
/v1/chat/completions, so the agent returned a 400 BadRequestError on
every query. Bind the agent model to /v1/responses instead, which
supports reasoning and function tools together.

The Responses API returns message content as a list of blocks rather
than a plain string, so read the final message with .text, which
flattens the blocks and keeps HospitalQueryOutput.output a valid str.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants