Class 1: you talked to a model. Class 2: you give it hands. You'll meet LangChain, snap your first chain together, then build an agent that decides — on its own — when to use a tool. Watch it happen live on the right. 👉
.env
Shipped a Summarizer + an LLM Arena
Everything today is the same OpenAI call you already know, organised more cleverly. Keep your Class 1 folder open; we build directly on top of it.
Warm-up, prompting vs RAG vs fine-tuning
Template · model · parser — what each one IS, before any code
Snap-it-together builder, then the real code, line by line
Why models forget & how LangChain remembers — decoded
One tool, line by line, + watch the message list grow
Prove you can think like the agent
Chat UI + public link
Caption + post — today 🎉
Homework + the road ahead (ends ~2:30)
Before we build: the entire field of AI engineering boils down to three ways of steering a pre-trained model. You'll live in the first, visit the second soon, and almost never need the third.
Just tell it clearly. Free, instant, no training. 90% of real work lives here — including everything today.
Hand it your documents at question-time so it answers from real data. A coming class.
Actually re-train on examples. Powerful, costly, rarely needed. Reach for it last.
Chains, tools, agents — it's all still option 1, organised cleverly. Nothing new to fear. Let's build.
In Class 1 you called the API by hand — perfect for one call. The moment you want reusable prompts, multi-step pipelines, and memory, you'd be rebuilding the same plumbing forever. LangChain is that plumbing, pre-built.
The raw OpenAI call is a Lego brick. LangChain is the box of connectors that snaps bricks into machines.
$ pip install langchain langchain-openai
LangChain code uses three names that look scary. They aren't. Read these before we touch code — each one is a thing you already understand:
A normal prompt where some parts are left as {blanks} to fill in later. Like a wedding-invite
template: "Dear {name}, join us on {date}". Write once, reuse for every guest.
The exact same GPT you called in Class 1 — just wrapped so it can snap onto other LangChain pieces. Same
model=, same temperature=.
The model doesn't return plain text — it returns a package (text + metadata like token counts). This piece opens the package and hands you just the string. ("Str" = string, i.e. plain text.)
Because it builds prompts in the chat format you already know from Class 1 — system / user / assistant messages. Same grammar, now reusable.
This is all a template is — blanks you fill. Pick values and watch the final prompt assemble. The dict you pass
to LangChain ({"tone": "witty", ...}) is just "here's what goes in each blank":
Here's the bit nobody explains. When the model replies, you don't get plain text — you get a package
called an AIMessage with the text inside it, plus bookkeeping. Press the button to see
the package, and what the parser does to it:
Without the parser you'd
write response.content by hand every time (in Class 1 it was
response.choices[0].message.content — remember that mouthful?). The parser does it for you,
forever.
A chain is the three pieces joined by | (the pipe — read it as "then"): prompt
then model then parser. Click the pieces in the right order, then run data through:
Rebuilding your Class 1 summarizer the LangChain way. The numbered comments match the decoder below — nothing here is mystery code:
from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser from dotenv import load_dotenv from scraper import fetch_website_contents # reuse Class 1's scraper load_dotenv() prompt = ChatPromptTemplate.from_template( # ① "Give a short, friendly summary of this website:\n\n{website}") model = ChatOpenAI(model="gpt-4o-mini", temperature=0.3) # ② parser = StrOutputParser() # ③ chain = prompt | model | parser # ④ def summarize(url): return chain.invoke({"website": fetch_website_contents(url)}) # ⑤ print(summarize("https://anthropic.com"))
from_template(...) turns my text into a reusable prompt. The
{website} part is the blank — it will be filled in later, just like the playground above.
The same model from Class 1, wrapped for LangChain. temperature=0.3 = mostly
focused (summaries shouldn't be wildly creative).
The package-opener you just saw in the animation: takes the model's AIMessage
package, hands back plain text.
The pipe | means "then". Read aloud: "the prompt, then the model,
then the parser." Data flows left → right, exactly like the builder.
invoke means "run it". The dict {"website": ...} says which
blank gets what — the key "website" matches the {website} blank by name.
chain is now a reusable building block. New task? Swap the template.
Different model? Swap line ②. Hindi summaries? Add one word to the prompt. That composability is LangChain's
entire point.
Class 1 truth: models forget everything between calls. The fix is simple — re-send the old messages every time. LangChain gives that a tidy home. Two tiny new words first:
Just a way to store "the human said X" and "the AI replied Y" — the same user/assistant roles from Class 1, as Python objects.
A blank in your prompt that holds a list of past messages instead of one word. "Insert the whole conversation so far, right here."
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder from langchain_core.messages import HumanMessage, AIMessage prompt = ChatPromptTemplate.from_messages([ ("system", "You are a friendly tutor."), # ① personality, like Class 1 MessagesPlaceholder("history"), # ② past turns park here ("human", "{question}"), # ③ the new question ]) chain = prompt | model history = [HumanMessage("My name is Aarav."), AIMessage("Hi Aarav!")] print(chain.invoke({"history": history, "question": "What's my name?"}).content) # → "Your name is Aarav." ✅ it "remembered" — because WE re-sent the history
The model didn't magically remember. We re-sent the old messages, and the placeholder
slotted them in. All "chatbot memory" everywhere is exactly this trick. (One thing: this invoke returns the
package — that's why we wrote .content. Add | parser to the chain and you
wouldn't need it. See how the pieces connect?)
Run the summarizer live and change the template in front of them ("now make it snarky"). The target feeling: "oh — it's just my Class 1 code, tidied up."
Everything so far talks. An agent is a model with a tool and the freedom to decide when to use it. We'll build the smallest one possible — a shop assistant with exactly one skill: looking up real prices.
The model thinks "do I need a tool here?" → if yes, calls it → reads the result → answers. Nobody hard-codes when. That decision is the entire difference between a chatbot and an agent.
LLMs are great with language, terrible with facts they don't have — today's price, live stock, exact math. A tool lets the model fetch truth instead of guessing. Tools cure "confident but wrong."
import json from openai import OpenAI from dotenv import load_dotenv load_dotenv() client = OpenAI() PRICES = {"shoes": 799, "hat": 399, "bag": 1420, "shorts": 1299, "pants": 1699} def get_price(item): print(f"🔧 tool called: get_price({item})") # so you SEE it happen return f"₹{PRICES.get(item.lower(), 'unknown')}"
Two tiny notes for the Python: PRICES is an ordinary
dict standing in for a database, and .get(item, 'unknown') means "look it up, and if it's not
there, say unknown instead of crashing." That's the entire tool — any function you can write
can become an agent's tool.
The model can't see your Python. You hand it a menu card describing the tool — written as a dict (the nested braces look busy, but it's only four facts). Decoder below:
tools = [{ "type": "function", # ① "function": { "name": "get_price", # ② "description": "Get the price of a shop item the user asks about.", # ③ "parameters": { # ④ "type": "object", "properties": {"item": {"type": "string", "description": "the item name"}}, "required": ["item"], }, }, }]
What kind of tool? A function. (That's the only kind you'll use for a long time — just write this line as-is.)
Its name — must exactly match your Python function's name, so we can find it when the model asks for it.
When to use it — written for the model to read. This sentence is literally how the model decides whether to call your tool. Write it clearly!
What inputs it needs — one input called item, which is text
("string"), and it's required. That's all the nesting says.
Line ③ is prompt engineering in disguise. A vague description ("does stuff with items") → the model misuses the tool. A clear one → it behaves. Your words steer the machine, even inside JSON.
def agent(user_message): messages = [{"role": "user", "content": user_message}] response = client.chat.completions.create( # ① send message + tools menu model="gpt-4o-mini", messages=messages, tools=tools) msg = response.choices[0].message if msg.tool_calls: # ② did it ask for a tool? messages.append(msg) for call in msg.tool_calls: args = json.loads(call.function.arguments) # ③ read its request, run it result = get_price(args["item"]) messages.append({"role": "tool", "tool_call_id": call.id, "content": result}) response = client.chat.completions.create( # ④ send it all back → nice answer model="gpt-4o-mini", messages=messages) msg = response.choices[0].message return msg.content print(agent("How much are the shoes?")) # → tool fires → "₹799" print(agent("Hi! What can you help with?")) # → no tool → just chats
We send the user's message plus our tools menu. The model now knows a tool exists and may ask to use it.
msg.tool_calls = "did the model ask to run a tool?" If it did, this holds which tool
and with what input — e.g. get_price, item="shoes". If not, it's empty
and we skip straight to the answer.
The model's request arrives as text, so json.loads(...) converts it into a Python dict we can
read — then we run the real function. (Important: the model never runs code itself. It
asks; your Python does.)
We append the tool's result to the conversation with role: "tool" (a third role, joining
system/user/assistant!) and send everything back, so the model can write a friendly final answer using the
real data.
The whole agent is just a list of messages getting longer. Step through one question and watch
each card get appended — this is exactly what your code's messages.append(...) lines do:
if msg.tool_calls is the entire secret
The model asked to run get_price("shoes") on its own. Give it ten tools and it picks
among them. You now understand how Cursor, support bots, and every "AI agent" headline actually works — same
pattern, bigger toolbox.
Before you trust the agent, prove you understand it. For each question, predict: will the model call the tool or answer directly? Get 5 in a row:
Play this as a class — hands up for tool vs chat before revealing. The "ooh" on question 4 (the trick one) is reliably the best moment of the session.
Same agent() from Block 11 — we give it a face. Gradio has a ready-made chat
interface, so this takes four lines:
import gradio as gr from agent import agent # the function you just wrote def chat(message, history): # ① Gradio fills these two in for you return agent(message) gr.ChatInterface(fn=chat, title="🛍️ Smart Shop Assistant").launch(share=True) # ②
Gradio's chat box calls your function for you, handing it the user's new message and the chat
history (a list of past turns). We only need the message today; your homework hint:
pass history into the agent and it gains memory — exactly the trick from the memory section.
ChatInterface = a ready-made chat UI (bubbles, input box, send button) around any function.
share=True = also give me a public link.
$ pip install openai gradio python-dotenv $ python app.py Running on local URL: http://127.0.0.1:7860 Running on public URL: https://shop-xyz.gradio.live # ← your shareable agent!
"How much are the pants?" — watch your terminal print 🔧 tool called: get_price(pants). Your
agent used its tool!
"What's your return policy?" — no tool fires. It's deciding, not following a script.
15 seconds: one tool question, one chat question, terminal visible. That's your LinkedIn clip.
A live, in-browser version with the real chat feel — typing dots, tool-call chips, the works. Watch when the 🔧 appears (and when it doesn't):
⚙️ Simulated in-browser (keyword matching plays
the "model") so it runs key-free. Your real agent.py lets GPT make that decision far more flexibly
— same loop, same trace.
Add tools for your database, email and calendar and this becomes a real assistant: "find my order, refund it, email the customer." Every production agent — coding agents included — is this exact loop with a bigger toolbox. You now own the core pattern.
"I built an AI agent" lands harder than "I built a chatbot" — because most people have no idea how simple the trick is. Post the clip, use this caption, and hit the big button below when you're live. 😄
👉 Tag linkedin.com/in/shivankagrawal and @Scaler so we can reshare and boost your reach.
You're officially "building in public." Recruiters notice consistency far more than one flashy post. Keep the streak alive.
Prompting, RAG, fine-tuning — you'll mostly prompt.
prompt | model | parser — chains you can snap together.
LLM + tool + loop. The model decides. You saw it.
A working tool-using agent, in public. 🎉
Add check_stock(item) or apply_discount(item) and watch the agent pick the
right one. Post the clip!
The next big build: a model that answers from your documents.
Several agents handing work to each other — once one agent feels easy.
We're deliberately deferring the deep "how models are built" topics — attention, training, scaling — until you're comfortable building. They'll land as "oh, that's why it works" instead of abstract lecture.