learn-langchain-with-phoebe / Builder session 4 of 10
Learn LangChain with Phoebe · Builder track · Session 4 of 10

Middleware: the production layer

Everything that separates a demo agent from a deployable one - PII scrubbing, conversation summarization, retries, fallbacks, approval gates - is cross-cutting. LangChain 1.x ships them as composable layers around the model call. Tonight DataDesk grows armor, and you learn why we pin versions from a story where LangChain itself shipped the bug.

🟡 Builder track Practitioners: DA · DE · DS · engineers DataDesk v1 from b3 · langchain>=1.3,<2 pinned 45 minutes
0-3 · Recap 3-18 · Concepts: layers + the yank story 18-42 · Build-along: armor + fallback 42-45 · Q&A
Part 0

Where DataDesk stands

v1 chains tools, streams, and refuses to invent columns. But paste it a stakeholder email and it ships the sender's address straight to a cloud API; leave a thread running all afternoon and the context window fills; and if the Claude API hiccups, DataDesk simply dies. None of these are features of your agent - they are properties every production agent needs. That is exactly what middleware is for.

Live - presented in session Self-study - read after class ★ Try it now prompt Official docs + Academy covered
★ What you walk out with today DataDesk v1.1 wearing three layers of armor (PII scrub, summarization, model fallback), proof on screen that the redaction happened before the provider saw anything, a graceful-degrade demo where losing the API key downgrades quality instead of crashing - and a version-pinning reflex backed by a true story.
Part 1 · covers the middleware docs

Cross-cutting concerns, composably 9 min live

A concern is cross-cutting when every agent needs it and no agent's business logic should contain it. The 1.x insight: model them all as layers a request passes through on its way to the model - and back through on the way out.

PIIMiddleware - scrub before anything leaves SummarizationMiddleware - keep context in budget Fallback + retry - survive provider failure the model call Claude, or the local engine request passes IN through every ring response passes back OUT the same rings Order matters: PII sits outermost so nothing inside the onion ever sees the raw address.
🔍 Click to zoom - the middleware onion: every request passes through the rings around the model call
LiveWhy middleware exists at all3 min

In the 0.x era these concerns lived inside application code: a summarize-when-long branch here, a regex PII scrub there, retry logic copy-pasted around every model call. Three failures followed, everywhere, always:

  • Duplication: five agents meant five slightly different retry implementations, four of them wrong in different ways.
  • Tangling: business logic and plumbing in one function - unreviewable, untestable, and terrifying to touch.
  • Gaps: the one code path someone forgot to scrub is, by law of nature, the one the auditor finds.

1.x moves them into declared layers on create_agent(middleware=[...]). Your agent code stays business-only; the concerns compose in a list you can read, reorder, and diff in review. If you have used WSGI/ASGI middleware or Express, this is that idea, aimed at the model call.

The 0.x pattern this replacesdef ask_agent(question): question = scrub_pii_v2(question) # copy-pasted from the other agent if too_long(history): history = summarize(history) # different threshold than team B's for attempt in range(3): # retry logic, fourth variant this repo try: return agent.run(question) except ProviderError: continue # and the fallback? somebody's TODO
LiveThe built-in five3 min

All from langchain.agents.middleware, all attachable in one line each:

The built-in five: middleware you attach, not build SUMMARIZATION long chats blow context window DEMO 1 HUMAN-IN-LOOP some tool calls need a human yes b7 DEEP DIVE PII SCRUB personal data skips the API DEMO 1 TOOL RETRY tools fail transiently N RETRIES MODEL FALLBACK providers have bad days DEMO 2 Three of five wired into DataDesk tonight; order is the list order, privacy layers outermost.
🔍 Click to zoom - five cross-cutting concerns, attached in one line each
MiddlewareConcernDataDesk use
SummarizationMiddlewareLong chats blow the context windowAll-afternoon analysis threads keep working - Demo 1
HumanInTheLoopMiddlewareSome tool calls need a human yesApproval gates - the b7 deep dive builds on this
PIIMiddlewarePersonal data must not reach the providerScrub stakeholder pastes before send - Demo 1
Tool retryTools fail transientlyFlaky warehouse connection gets N retries, not a crash
Model fallbackProviders have bad daysClaude down → local Ollama engine - Demo 2

(Rate limiting ships as a built-in too - same shape, attach and forget.) The composition rule from the diagram: order is the list order, and privacy layers belong outermost.

Real world

The audit that took an afternoon instead of a quarter. A regulated team was asked to prove no customer emails reached their LLM vendor. Their answer was one line of code in review - PIIMiddleware("email", ...) outermost in the list - plus its tests. The sister team with hand-rolled scrubbing spent three months tracing code paths for the same question.

Self-studyCustom middleware - the three hooks3 min read

When no built-in fits, you write your own. Three hook styles, in order of power:

  • before_model - runs before each model call. Inspect or edit the messages going in. Use for: injecting today's date, enforcing input budgets, custom redaction.
  • after_model - runs on the way out. Inspect or edit the response. Use for: logging content_blocks (b2 pays off), blocking answers that violate policy, tagging outputs for evals.
  • wrap-style - wraps the model call itself, so you control both sides AND whether the call happens at all. Retries, fallbacks, caching and circuit breakers live here - it is how the built-in fallback works.

Rule of thumb: reach for before/after first; wrap only when you need to own the call. And check the built-ins list again before writing anything - the whole point of this session is that someone already wrote the boring layer, tested it, and maintains it.

★ Try it now (any chat AI)My LLM agent framework offers before_model, after_model and wrap-style middleware hooks. Here are 4 requirements from my team: [list yours - e.g. log every tool call, block answers naming customers, add a cost cap, cache repeated questions]. Map each to the right hook and say why in one sentence.
Part 2 · covers the changelog + release history

Trust, with a lockfile 6 min live

The middleware system is also where LangChain shipped its most instructive recent bug. This part is short, true, and worth more than most tutorials: it is why professionals pin.

LiveThe yanked-releases story4 min

In the 1.x line, releases 1.3.5 and 1.2.5 were yanked from PyPI - pulled after publication - because a change to SummarizationMiddleware's signature broke code that used it. The same layer you attach tonight.

  • What yanking means: the files stay downloadable if explicitly pinned to that exact version, but installers skip yanked releases when resolving ranges. It is the ecosystem's recall notice: "we shipped this; do not take it."
  • Why pip install -U on a Friday is a bad habit: an unpinned upgrade window is exactly how a yanked-grade regression walks into production between your last test run and your deploy. Nobody diffed anything; the resolver just grabbed newest.
  • The reflex: pin langchain>=1.3,<2 and langgraph>=1.2,<2, upgrade deliberately, and re-run your own smoke tests after every bump. The no-breaking-changes-until-2.0 promise is real - and yanks are what "real but human" looks like.
The honest framing This story is a point FOR the 1.x era, not against it: the regression was caught, recalled, and documented in public. Your job is just to not be the team that auto-upgraded during the bad window. Trust, with a lockfile.
Real world

Two teams, one bad window. When the regression shipped, the team with pinned ranges and a smoke-test script never noticed - their resolver skipped the yanked version on the next deliberate bump. The team with unpinned requirements rebuilt an image that Friday, picked up the broken release, and spent a weekend bisecting "middleware suddenly raises TypeError". Same ecosystem event, entirely different weekends.

Same yanked release, two different weekends ✗ UNPINNED REQUIREMENTS Rebuilt image that Friday Picked up the broken release Spent a weekend bisecting TypeError ✓ PINNED RANGES + SMOKE TEST Resolver skipped yanked version Never noticed the regression Shipped on schedule Same ecosystem event, entirely different weekends - pinning is the difference.
🔍 Click to zoom - same ecosystem event, entirely different weekends
Self-studyReading the changelog like an operator2 min read

A 10-minute monthly ritual that replaces upgrade anxiety:

  • Skim the changelog at docs.langchain.com before any bump - middleware and agent-surface entries matter most to DataDesk; note anything touching classes you attach.
  • Check PyPI for yank flags on versions between your pin and the target - yanked releases are labeled on the release history page.
  • Keep a smoke-test file: one script that runs DataDesk's core flows (tool chain, structured output, each middleware) end to end. Green after upgrade = ship the new pin. This file grows into the b10 eval suite - same idea, more rigor.
  • Upgrade on a Tuesday morning, not a Friday evening. Not a technology rule - a being-on-call rule.
Demo 1 of 2

DataDesk grows armor ★ 12 min · everyone builds

Two rings onto v1: a PII scrub for the stakeholder pastes DataDesk lives on, and summarization so long analysis threads stop dying at the context ceiling. Then the important part - we PROVE the scrub happened, on screen.

Import the layers and add a middleware=[...] list to your v1 create_agent call - PII outermost, exactly like the onion diagram. The rest of v1 is untouched.

Seed the test: invoke with a message containing a fake address - "Request from jane.doe@example.com: how many rows in the orders data?". Fake, always - we are demonstrating a scrubber, not feeding it.

Prove the scrub: print the message stack from the result and find the human message the model actually received. The address is redacted; the question survived. This screenshot is your future audit answer.

Now the summarizer: loop 15+ questions through one thread and watch the older turns collapse into a summary message instead of the context window overflowing. Ask a question that depends on an early turn - note what survives summarization and what blurs.

Review the diff from v1: the entire production upgrade is one list. Read it aloud in the words you would use in design review - that list IS the review.

★ DataDesk v1.1 - the armor difffrom langchain.agents import create_agent from langchain.agents.middleware import PIIMiddleware, SummarizationMiddleware from langchain.chat_models import init_chat_model agent = create_agent( model=init_chat_model("claude-haiku-4-5-20251001"), tools=[csv_stats, column_mean, list_datasets], # unchanged from v1 system_prompt=SYSTEM, # unchanged from v1 middleware=[ PIIMiddleware("email", strategy="redact"), # outermost: scrub before send SummarizationMiddleware( model="claude-haiku-4-5-20251001", max_tokens_before_summary=4000, # long threads collapse, not crash ), ], ) out = agent.invoke({"messages": [{"role": "user", "content": "Request from jane.doe@example.com: how many rows in the orders data?"}]}) for msg in out["messages"]: print(msg.type, "·", str(msg.content)[:100]) # find the redacted human turn
Scrub names too? PIIMiddleware takes a type per instance - attach one for "email", another for names or custom patterns, and stack them in the list. Start with email tonight; extend as homework.
Demo 2 of 2

The graceful degrade ★ 10 min · build your own

The two-engine setup you have maintained since b1 pays its dividend: model fallback from Claude to the local Ollama engine means a provider outage degrades DataDesk's answers instead of killing them. We simulate the outage live.

Add ModelFallbackMiddleware("ollama:llama3.1") to the middleware list. One line: if the primary model call fails, retry the call on the local engine.

Baseline run with the key present - confirm a normal Claude-quality answer to the v1 two-tool question.

Simulate the outage: in a fresh shell, unset ANTHROPIC_API_KEY and run the same script. The primary call fails, the fallback ring catches it, and DataDesk answers from llama3.1 - slower, plainer, alive.

Compare the two answers honestly: tool choice usually survives, nuance and phrasing degrade. Write one sentence on whether that trade is acceptable for YOUR users during an outage - that sentence is an SLA decision.

Land the reframe: the fallback is also the privacy mode. The same one line means "confidential-data path runs local" and "outage path runs local" - one architecture, two policies. Restore your key before b5.

Choose fallbacks by failure mode The middleware accepts any model string, so Claude-to-hosted-Claude is also valid - it survives a model incident but not a provider outage or a network-isolated environment. The local engine survives all three, at a quality cost. Write the choice down where your on-call can find it.
★ The one-line degrade pathfrom langchain.agents.middleware import (PIIMiddleware, SummarizationMiddleware, ModelFallbackMiddleware) middleware=[ PIIMiddleware("email", strategy="redact"), SummarizationMiddleware(model="claude-haiku-4-5-20251001", max_tokens_before_summary=4000), ModelFallbackMiddleware("ollama:llama3.1"), # Claude down -> local engine, not a crash ] # Outage drill, in your shell: # unset ANTHROPIC_API_KEY # python datadesk_v1.py # same file, same question - local answer
Real world

The outage that nobody escalated. During a provider incident, a data team's assistant quietly fell back to its local model for two hours. Answers got terser; a banner said "running in degraded local mode". Zero pages, zero lost afternoon - and the incident review was one sentence. The neighboring team without a fallback ring filed the outage as a Sev-2.

Homework

Try it yourself - this week ◐ 30-45 min total

Source material

Official sources covered

This track teaches from the official docs and the free LangChain Academy curricula (login required for lesson content; certificates stay with the Academy - all free). This page covers:

LangChain docs: middleware built-insPart 1 + both demos · Summarization, HITL, PII, retry, fallback, rate limiting
LangChain docs: custom middleware hooksbefore_model / after_model / wrap covered as self-study; you write one when a real need appears
Changelog + PyPI release history (yanked 1.3.5 / 1.2.5)Part 2 · the pin-your-versions lesson, from the primary sources
Check yourself

Three questions before you go 🎯 ◐ 90 seconds

1 · Why does middleware exist as a first-class concept in LangChain 1.x?

Duplication, tangling and gaps were the 0.x pattern. One middleware list per agent is the fix - and the list itself becomes your audit answer.

2 · langchain 1.3.5 and 1.2.5 were yanked from PyPI. What does that mean, and what is the lesson?

Yanking is the ecosystem's recall notice. Pin langchain>=1.3,<2, read the changelog, and never let Friday's pip install -U pick your production version.

3 · What does ModelFallbackMiddleware("ollama:llama3.1") buy DataDesk?

One line, two policies: outage resilience and a local path for confidential data. This is the b1 two-engine discipline paying its dividend.

Builder session 4 cheat sheet · pin this

The ideaCross-cutting concerns = layers around the model call. Declared in create_agent(middleware=[...]), ordered, reviewable.
Built-insSummarization · HumanInTheLoop (b7) · PII · tool retry · model fallback (+ rate limiting). Import from langchain.agents.middleware.
Onion orderList order = ring order. Privacy (PII) outermost, so nothing inside ever sees the raw data.
Custom hooksbefore_model (edit inputs) · after_model (edit/inspect outputs) · wrap (own the call: retries, caching). Built-ins first.
The yank lesson1.3.5 / 1.2.5 recalled for a middleware signature regression. Pin langchain>=1.3,<2 · langgraph>=1.2,<2.
Upgrade ritualChangelog → PyPI yank check → smoke tests green → new pin. Tuesday morning, never Friday night.
Graceful degradeModelFallbackMiddleware("ollama:llama3.1") - outage path and privacy path are the same line of code.
Running projectDataDesk v1.1 wears armor. Next: b5 opens the LangGraph layer the whole harness runs on.