A hands-on workshop with LangChain, Google Gemini and DuckDuckGo search. Non-technical readers learn what an AI agent is and why it matters; developers build one, step by step, in about an hour.
A large language model (LLM) such as Gemini learned from text up to a certain date. Ask it "What is the latest iPhone and what does it cost?" and it answers from memory, which may be months or years out of date. It cannot look anything up by itself.
An AI agent fixes this. It is the same LLM, plus a set of tools it is allowed to use, plus a loop that lets it use those tools as many times as it needs before answering. In this workshop the tool is a web search engine.
The LLM is a very well-read assistant sitting at a desk. Without tools, they answer from what they remember. An agent is the same assistant with a phone and a browser. When you ask about today's prices, they decide to search, read the results, maybe search again, and only then reply. You still ask one question and get one answer; the extra work happens in between.
This is the exact flow of the agent we build. Each step shows what happens in plain words and, in green, the matching piece of code.
The key idea: Gemini only decides and writes. LangChain executes. This split is what makes agents controllable: you choose which tools exist, and the model can only ask for those.
| Term | Plain meaning | In this workshop |
|---|---|---|
| LLM | An AI model that reads and writes text. | Google Gemini (gemini-3.5-flash-lite) |
| Agent | An LLM that can use tools in a loop until a task is done. | create_agent(...) |
| Tool | Any function the agent is allowed to call: search, calculator, database, email. | DuckDuckGoSearchRun |
| Tool call | The model's request: "please run this tool with these inputs". | msg.tool_calls |
| LangChain | An open-source Python library that connects LLMs, tools and data. | langchain 1.x |
| API key | A secret password that lets your code use a paid or metered service. | GOOGLE_API_KEY in a .env file |
| Prompt | The instructions and question sent to the model. | Your question, plus an optional system prompt |
| ReAct | A pattern where the model alternates Reason → Act → Observe. | The idea behind the loop in section 2 |
"Compare the three cheapest flights to Dubai next Friday." The agent searches, compares and summarises.
Swap web search for your help-centre search and order database. The agent looks up the order before replying.
"Brief me on this company before my 3 pm call." Search for news, funding and leadership changes.
Point the tool at company documents instead of the web, and staff can ask policy questions in plain language.
The pattern stays the same in every case. Only the tools change. That is why learning this one small agent is a big step.
Versions used when this was tested: langchain 1.4.3, langchain-core 1.6.7, langgraph 1.2.14, langchain-google-genai 4.4.0, langchain-community 0.4.2, ddgs 9.16.0. AI libraries change quickly; if something breaks, compare your versions with these first.
# Windows (PowerShell) - macOS/Linux: use "python3" and "source .venv/bin/activate"
mkdir ai_workshop; cd ai_workshop
python -m venv .venv
.venv\Scripts\activate
pip install -U langchain langgraph langchain-core langchain-community langchain-google-genai ddgs python-dotenv jupyterA virtual environment keeps this project's packages separate from everything else on your computer. ddgs is the current name of the DuckDuckGo search package; the older duckduckgo-search name is being replaced.
# File name: .env (same folder as your notebook - never commit this file to GitHub)
GOOGLE_API_KEY=paste-your-key-from-google-ai-studio-hereKeeping the key in a .env file means it never appears in your code, screenshots or GitHub. Add .env to .gitignore.
import os
from dotenv import load_dotenv
load_dotenv() # reads .env into environment variables
api_key = os.getenv("GOOGLE_API_KEY")
print("API key loaded:", bool(api_key)) # True = good. Never print the key itself.If this prints False, every later step fails with "API key required". Fix it here first.
from google import genai
import os
client = genai.Client(api_key=os.getenv("GOOGLE_API_KEY"))
# Model names change often. Ask Google which ones YOUR key can use today.
for model in client.models.list():
print(model.name)Model names change every few months and old ones get switched off. In our run, gemini-2.5-flash returned "no longer available to new users", so we listed the models and picked one from the list.
from langchain_google_genai import ChatGoogleGenerativeAI
llm = ChatGoogleGenerativeAI(
model="gemini-3.5-flash-lite", # pick a name from the list in step 4
timeout=60, # give up after 60 s instead of hanging
max_retries=3, # retry temporary errors such as 503 "high demand"
)
response = llm.invoke("Say hello in one short sentence.")
# Newer Gemini models return a LIST of content blocks, not a plain string.
def text_of(content):
if isinstance(content, list):
return "".join(part.get("text", "") for part in content if isinstance(part, dict))
return content
print(text_of(response.content))This proves the key, network and model all work before you add more moving parts. The text_of helper handles a real surprise from our run: the newer model returned a list of content blocks instead of a plain string.
from langchain_community.tools import DuckDuckGoSearchRun
search = DuckDuckGoSearchRun() # free web search, no API key needed
tools = [search]
print("Tool ready:", search.name) # -> duckduckgo_search
print(search.description) # this text is what Gemini reads to decide WHEN to use itThe tool's name and description are sent to Gemini. That is all Gemini knows about the tool, so a clear description directly improves when and how the agent uses it.
from langchain.agents import create_agent
agent = create_agent(
model=llm,
tools=tools,
)
question = ("What is the latest iPhone model and its price in USD? "
"Give me the price in UK currency.")
response = agent.invoke({
"messages": [{"role": "user", "content": question}]
})
print(text_of(response["messages"][-1].content))Output from our run:
The latest flagship smartphone lineup from Apple is the iPhone 16 series
(including the iPhone 16, iPhone 16 Plus, iPhone 16 Pro, and iPhone 16 Pro Max).
Taking the base model (iPhone 16 with 128GB of storage) as the standard reference:
* Price in USD: $799 (US prices do not include state/local sales tax)
* Price in UK Currency (GBP): £799 (UK prices automatically include 20% VAT)
(If you are looking at the entry-level Pro model, the iPhone 16 Pro starts at $999 USD / £999 GBP).# Look inside the loop: every step the agent took is in response["messages"]
for msg in response["messages"]:
kind = type(msg).__name__ # HumanMessage / AIMessage / ToolMessage
if kind == "AIMessage" and msg.tool_calls:
for call in msg.tool_calls:
print(f"🧠 Gemini decided to call {call['name']} with {call['args']}")
elif kind == "ToolMessage":
print(f"🔎 Tool returned: {str(msg.content)[:200]}...")
elif kind == "AIMessage":
print(f"✅ Final answer: {text_of(msg.content)[:200]}...")
else:
print(f"🙋 You asked: {msg.content}")The response holds every message in the loop: your question, each tool call Gemini asked for, each search result, and the final answer. Printing them shows you whether the agent really searched or answered from memory. This matters, as section 7 explains.
from datetime import date
from langchain.agents import create_agent
from langchain.tools import tool
@tool
def usd_to_gbp(amount_usd: float, rate: float) -> str:
"""Convert US dollars to British pounds. Always find today's USD->GBP rate with web search first."""
return f"{amount_usd} USD = {amount_usd * rate:.2f} GBP (rate {rate})"
agent = create_agent(
model=llm,
tools=[search, usd_to_gbp],
system_prompt=(
f"Today is {date.today():%d %B %Y}. "
"For anything that changes over time (products, prices, exchange rates, news) "
"you MUST use web search instead of your own memory. "
"Name the sources you used. If the search fails, say so."
),
)Two upgrades. A system prompt tells the agent today's date and forces it to search for anything time-sensitive. A second custom tool, built with @tool, does the currency maths exactly, so the model does not guess a rate. The function's docstring becomes the tool description Gemini reads.
Our agent ran without errors and still gave a doubtful answer. It was run in October 2026 and said the latest iPhone is the iPhone 16. Apple released the iPhone 17 range in September 2025, so that answer was out of date.
A second run converted $799 using an "approximate" rate of 0.75 GBP per USD. The model chose that rate itself; it did not look it up. It also correctly noted that Apple's UK price is set separately (£799 including VAT) and is not a straight currency conversion.
Lessons for everyone, technical or not:
Every one of these happened while building this workshop. Expect to meet some of them.
| Error message | What it means | Fix |
|---|---|---|
| 404 NOT_FOUND · "model is no longer available to new users" | That model name has been retired for new keys. | Run step 4 and choose a model from the list. |
| 404 · "not found for API version v1beta" | The model name is misspelled or doesn't exist (we tried gemini-3.8-flash-lite). | Copy the name exactly from the step 4 list, without the models/ prefix. |
| "API key required for Gemini Developer API" | The key wasn't loaded into the environment. | Check .env location and spelling, call load_dotenv(), re-run step 3. |
| 503 UNAVAILABLE · "high demand" | Google's servers are busy. It's temporary and not your fault. | Set max_retries=3 and timeout=60, wait, or switch to a lighter model. |
| UserWarning: "temperature will be ignored" | Some newer models use fixed sampling settings. | Harmless. Remove temperature to silence it. |
Output looks like [{'type': 'text', 'text': ...}] | The model returned content blocks, not a string. | Use the text_of() helper from step 5. |
| DDGSException: DNSError … wt.wikipedia.org | The search library's automatic backend tried a Wikipedia address that does not exist. A temporary search-side failure. | Re-run; update with pip install -U ddgs. In production, catch search errors and let the agent say the search failed. |
NameError: name 'uuid' is not defined (in create_agent) | Appeared once in our notebook; most likely a stale import after upgrading packages without restarting the kernel. | Restart the Jupyter kernel after any pip install, then run cells top to bottom. |
| "Direct use of automatic function calling (AFC) … not recommended" | An informational notice from Google's SDK. | Safe to ignore for this workshop. |
Older LangChain tutorials, and an early cell of our notebook, build agents with a ReAct prompt like this one:
Answer the following questions as best you can. You have access to the following tools:
{tools}
Use the following format:
Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [{tool_names}]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input questionThe model writes "Action: …" as plain text and the framework parses it. That works, but a small formatting slip breaks the parser.
create_agent in LangChain 1.x uses native tool calling instead. Gemini returns a structured tool_calls field with the tool name and JSON arguments, so there is nothing to parse and far fewer format errors. The idea is still ReAct: reason, act, observe, repeat. Only the mechanics have improved. Under the hood, create_agent runs on LangGraph, which is why we installed langgraph.
| ReAct text prompt | Native tool calling (create_agent) | |
|---|---|---|
| How the model asks for a tool | Writes "Action: search" in text | Returns a structured tool_calls object |
| Parsing errors | Common | Rare |
| Works with | Almost any LLM | Models that support tool calling (Gemini, GPT, Claude, …) |
| Best for | Learning the concept | Real projects |
| Topic | What to know |
|---|---|
| Accuracy | Agents can still be wrong or out of date (section 7). Keep a human check on anything important. |
| Cost | Each loop step is another model call. A question that needs three searches costs about four calls. Set a limit on steps in production. |
| Speed | Every search and model call adds seconds. Agents are slower than a plain chatbot. |
| Rate limits | Free tiers limit requests per minute, and search providers can block heavy use. |
| Security | Never put API keys in code. Give agents only the tools they need: read-only first, and actions like "send email" only with human approval. |
| Untrusted content | Web pages can contain text that tries to give the model instructions (prompt injection). Treat search results as information, never as commands. |
@tool that returns today's date and time, and remove the date from the system prompt.thread_id, so follow-up questions like "and in euros?" work.LangChain. Gemini only asks for the search through a tool call; LangChain runs the tool and sends the results back.
When Gemini replies without any tool calls, that reply is treated as the final answer.
Inspect the tool calls to see whether it really searched (step 8), and give it today's date plus a rule to always search for current facts (step 9).
So the secret never appears in your code, notebooks, screenshots or Git history.
The tool's name and description, which LangChain sends to the model with every request, plus the question and system prompt.
Google's servers are temporarily overloaded. Retry with max_retries, wait, or switch to a lighter model.
An AI agent is an LLM with tools and a loop. You ask a question, LangChain hands it to Gemini with a list of tools, and Gemini decides whether to search. LangChain runs the search and returns the results, and Gemini repeats this until it can answer. With about 30 lines of Python you now have a working research assistant.
The harder skill is judging the output: check that tools were used, give the agent today's date, use real tools for numbers, and keep a human in the loop for anything that matters.