Where DataDesk stands
In b1 you built the agent loop by hand, then rebuilt it as DataDesk v0.1: one csv_stats tool inside create_agent. Tonight we open the hood on the three things that loop was juggling - the model interface, the message list, and the tool contract - because every later session (middleware, graphs, memory, evals) manipulates exactly these three objects.
One interface, every model 8 min live
LangChain's oldest promise is also its most defensible: one calling convention across providers. But the promise has fine print - the interface is standardized, the capabilities are not. Knowing exactly where the seam sits is what makes the one-line swap safe instead of naive.
Liveinit_chat_model and the provider-swap contract3 min▶
init_chat_model("claude-haiku-4-5-20251001") and init_chat_model("ollama:llama3.1") return objects with the same surface. What is actually in the contract:
- Standardized:
invoke(one call, full reply),stream(chunks as they generate),bind_tools(attach tool schemas), the message types in and out. Your agent code never changes. - Provider-specific, on purpose: what the model can DO. Strict tool schemas, extended thinking, prompt caching, PDF input - those are Claude features. Zero cost and total privacy - those are Ollama features. The swap changes capability, never syntax.
- The mental model: LangChain standardizes the plug, not the appliance. Design your agent against the plug; choose the appliance per environment - capability in dev, privacy in the confidential-data path.
init_chat_model("claude-haiku-4-5-20251001") ↔ init_chat_model("ollama:llama3.1"). Every demo tonight ends by running on the other engine. That habit is the course's spine.
LiveMessages anatomy and content_blocks3 min▶
Four message roles carry every conversation: system (standing orders), human (the user), ai (the model - text, but also tool calls and reasoning), and tool (results going back). The 1.0 upgrade that matters is message.content_blocks:
- The problem it solves: providers disagree about payload shape. Claude returns thinking blocks and citations one way, other providers another way. Pre-1.0 code that reached into raw
contentbroke on every provider swap. - The fix:
content_blocksis a provider-agnostic view - a list of typed blocks (text,reasoning,tool_call, citations) with the same shape regardless of engine. Iterate it and you never write provider-specific parsing again. - Why a data person should care: reasoning traces and tool calls are exactly what you log, audit, and eval later (b10). A stable schema for them is infrastructure, not sugar.
The dashboard that broke on a model upgrade. A team parsed Claude's raw response payload to display tool calls in their internal UI. A provider-side format tweak broke the display for a week. The rewrite to content_blocks was 20 lines - and survived the next three model swaps untouched.
Self-studyThe engine capability envelope3 min read▶
Same plug, different appliance. Know each engine's envelope before you promise features to stakeholders:
| Claude via langchain-anthropic | Local via langchain-ollama | |
|---|---|---|
| Tool calling | Excellent, with strict schema mode | Works ONLY on tool-tuned models (llama3.1 yes; many models no) |
| Extended thinking | Yes - visible as reasoning blocks | Model-dependent, often absent |
| Prompt caching | Yes - big savings on long system prompts | Not applicable (no per-token bill) |
| Image / PDF input | Yes | Model-dependent, usually no |
| Token-usage metadata | Full usage returned per call | None - budget dashboards go blind |
| Cost / privacy | Per token · Anthropic ToS | Free · nothing leaves the machine |
Design rule for DataDesk: features may light up on Claude and degrade on Ollama, but the code path stays identical. If a feature is envelope-dependent (say, PDF ingestion), guard it, do not fork the codebase.
Tools and structured output 7 min live
Tools are how the model reaches into your world; structured output is how its answers reach into your pipelines. Both are contracts, and both are written in the same place: your function signatures and Pydantic models.
LiveThe @tool decorator - schema from signature3 min▶
In b1 you hand-wrote a JSON schema for csv_stats. The @tool decorator from langchain.tools generates it: parameter names and types from the signature, the description from the docstring. Three consequences worth money:
- Good docstrings ARE prompt engineering. "Row count, columns and date range of a CSV file" tells the model exactly when to reach for this tool. A lazy docstring like "gets stats" produces wrong tool choices you will misdiagnose as model stupidity.
- Type hints are enforced upstream.
path: strmeans the model is told to send a string. Bad inputs get caught at the schema boundary, not deep in your pandas code. - One decorator, both engines. The same decorated function binds to Claude and to a tool-tuned Ollama model. Capability envelope applies: on a non-tool-tuned local model,
bind_toolssimply cannot work.
LiveStructured output - in the main loop, no second call3 min▶
Free text is for humans; your scripts need fields. Pass a Pydantic model as response_format to create_agent and the final answer arrives as a validated object:
- The 1.x win: the structured response is generated in the agent's main loop. The 0.x pattern - run the agent, then fire a second LLM call to reformat the answer - is gone. One loop, fewer tokens, no drift between the answer and its structured version.
- Validation is Pydantic's, not the model's: if the model emits a field that fails your types, you find out immediately, at the boundary, with a Python exception you can catch.
- Read it from
result["structured_response"]- it sits alongside the normal message list, so you keep the conversational trace too.
Self-studyWhen structured output beats free text2 min read▶
The decision rule is one sentence: if anything other than a human reads the answer, structure it.
| Consumer of the answer | Format | Example |
|---|---|---|
| A human, reading in chat | Free text | "Explain why signups dipped in March" |
| A script, a pipeline, a dashboard | Pydantic via response_format | DataRequest(dataset, metric, date_range) |
| A downstream tool call | Structured - always | Parsed request feeding csv_stats |
| An audit log or eval suite | Structured - always | Verdicts with reason codes, b10 |
Heuristic for data teams: every "can you pull me..." message from a stakeholder is secretly a form. Tonight's Demo 2 makes that literal.
X-ray a conversation ★ 12 min · everyone builds
You will send one message, bind one tool, and read the raw typed blocks the model actually returns - on both engines. After this, no LangChain response is ever opaque to you again.
In your DataDesk folder from b1, create xray.py with the script below. It uses your existing csv_stats function body.
Run it on the Claude engine. First X-ray: a plain reply - expect a single text block (and, if extended thinking is on, a reasoning block before it).
Second X-ray: the tool-armed call. Find the tool_call block - name and arguments, exactly the schema your docstring generated. This block is what the while-loop from b1 was pattern-matching on.
Now the swap: change one line to init_chat_model("ollama:llama3.1") and re-run. Diff what changes: block shapes identical, capability different - likely no reasoning block, and no token-usage metadata at all.
Say the conclusion out loud: content_blocks is the same language on both engines; the engines just have different vocabularies.
bind_tools only works on tool-tuned models. llama3.1 qualifies; many pulls do not. This is the capability envelope from Part 1, live.
DataDesk learns to fill forms ★ 10 min · build your own
A stakeholder writes "hey can u pull avg order value for the shop data, last quarter-ish? thx". DataDesk v0.2 turns that into a validated DataRequest object - dataset, metric, date_range - ready to feed a real pipeline.
Define the form: a Pydantic DataRequest model with dataset, metric, date_range. This class IS the contract between vague humans and exact pipelines.
Pass it to create_agent as response_format=DataRequest. No parsing prompt, no second LLM call - the structured answer is produced inside the main loop.
Invoke with the messy request. Print result["structured_response"] - three clean validated fields out of one sloppy sentence.
Validate the win: feed req.dataset into csv_stats directly. The agent's answer just became an argument to your code - that handoff is the whole point of structured output.
Engine-swap test: same file on ollama:llama3.1. Local models are sloppier at edge cases - watch whether "last quarter-ish" survives. That gap is eval material for b10.
The intake queue that stopped being a queue. A data team's #data-requests channel averaged two clarification round trips per ask. They put a structured-output parser in front: every request became a typed object or a single auto-generated question ("which dataset - orders or web analytics?"). Clarification round trips dropped by half before anyone touched a model bigger than Haiku.
Try it yourself - this week ◐ 30-45 min total
- Run both demos on BOTH engines if you did not finish live. The X-ray diff is the habit that makes b4's fallback middleware feel obvious.
- Rewrite the docstring of
csv_statsthree ways (terse, precise, over-long) and X-ray which one the model picks correctly for three different questions. Bring your winner to b3. - Extend
DataRequestwith an optionalgroup_byfield and test it on two real requests from your own backlog or inbox. - Skim your last five stakeholder asks: which were secretly forms? Note the fields - that list is DataDesk's future intake schema.
- Optional reading: the messages + content blocks pages at docs.langchain.com - now you can read them as documentation of things you have printed.
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:
Three questions before you go 🎯 ◐ 90 seconds
1 · What exactly does init_chat_model standardize across Claude and Ollama?
LangChain standardizes the plug, not the appliance. The swap changes capability, never syntax - which is why you test on both engines every session.
2 · Why read message.content_blocks instead of the raw provider payload?
Reasoning traces and tool calls differ per provider in the raw payload. content_blocks gives them one stable schema - which is also what you log and eval later.
3 · How does structured output work in LangChain 1.x agents?
The extra-call pattern is the 0.x museum. One loop, validated at the boundary by Pydantic, no drift between answer and structure.