Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Updates the
langchain-rag-appsample code to match the refreshed Build an LLM RAG Chatbot With LangChain tutorial.What changed
create_agent,langchain-neo4j, …) and thegpt-5.5models, matching the updated article step by step (source_code_step_1…source_code_final).data/folder and removed the per-step copies. Step 1 keeps its owndata/reviews.csv(it only needs the reviews). CSV URLs in the article and README now point tolangchain-rag-app/data/.chroma_data/vector-store artifact (it's regenerated bycreate_retriever.py) and added a.gitignore.requirements.txtin every step, pinned consistently with eachpyproject.tomland the article, using the latest releases (openai==2.41.0,uvicorn==0.49.0,polars==1.41.2,httpx==0.28.1).CHATBOT_URL).ruff formatandruff checkboth 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.
source_code_step_1/(langchain_intro/plus its localdata/reviews.csv). That code backs the new fundamentals tutorial, not this one.step_2…step_5becomestep_1…step_4. Each folder is byte-identical to the one it replaces; only the names changed.data/andsource_code_final/are untouched.gpt-5.6-luna, matching the fundamentals tutorial.ruff format --checkandruff checkboth pass on the folder.source_code_step_1/source_code_step_2/source_code_step_3/source_code_step_4/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 (alangchain-tutorial/PR) and the article link needs repointing.