Agentic AI Engineering with LangChain / Lab 00
Module 00 · Orientation

Lab 00 · Hello, agent

Build your first tool-calling agent: a Northwind support assistant that looks up order status and invoice totals.

30 minBeginnerAzure OpenAIcreate_agent@toolsystem prompt

What you will build

A small support agent for the fictional company Northwind. It answers questions like Where is A-100 and how much was it? by calling two Python functions, and it says so politely when an order does not exist instead of making something up.

What you will learn

  • How create_agent runs the model-and-tools loop for you
  • How @tool turns a Python function into something a model can call
  • Why tool docstrings matter: the model picks tools by name and description
  • How to connect LangChain to an Azure OpenAI deployment
1

Check your prerequisites

You need three things on your machine and three values from your instructor.

  • Python 3.11, 3.12 or 3.13. Check with the command below. On Windows, install it from python.org and tick Add python.exe to PATH.
  • A code editor. VS Code with the Python extension works well.
  • A terminal. PowerShell on Windows, Terminal on macOS or Linux.
PowerShell
python --version
i

From your instructor you get an Azure OpenAI endpoint, an API key and a deployment name. Keep the key private: never paste it into code or share it in chat.

2

Create the project folder and virtual environment

Create a new folder for this lab, open it in a terminal, and create a virtual environment. A virtual environment is a private copy of Python for this project, so the libraries you install here never clash with other projects.

PowerShell
mkdir C:\labs\lab-00-hello-agent
cd C:\labs\lab-00-hello-agent
python -m venv .venv
.venv\Scripts\Activate.ps1
✓

Keep lab folders on a short path such as C:\labs. Deep folders, for example inside OneDrive or Desktop, can exceed the Windows path limit. Some libraries then fail to load with The filename or extension is too long. OneDrive syncing can also lock files the labs write.

When activation works, your prompt starts with (.venv). Activate it again every time you open a new terminal for this lab. Open the folder in VS Code with code . if you like.

!

PowerShell says running scripts is disabled on this system? Run Set-ExecutionPolicy -Scope CurrentUser RemoteSigned, answer Y, and activate again.

This is what your folder will look like at the end of the lab:

lab-00-hello-agent/ ├── .env ├── hello_agent.py ├── model_setup.py └── requirements.txt
3

Install the libraries

Create a file named requirements.txt in your lab folder with the content below. The versions are pinned to the ones this lab was tested with.

requirements.txt
langchain==1.4.2
langchain-core==1.6.4
langchain-openai==1.6.2
python-dotenv==1.2.3

Then install everything into your virtual environment. It takes a minute or two.

PowerShell
python -m pip install --upgrade pip
pip install -r requirements.txt
4

Add your Azure OpenAI settings

Create a file named .env (the name starts with a dot) in your lab folder. Paste the template and replace the three placeholders with the values from your instructor.

.env
# Azure OpenAI settings. Your instructor gives you these three values.
AZURE_OPENAI_ENDPOINT=https://<your-resource>.openai.azure.com/openai/v1
AZURE_OPENAI_API_KEY=<paste-your-key-here>
AZURE_OPENAI_DEPLOYMENT=<your-chat-deployment-name>

# Only used when your endpoint does NOT end in /openai/v1.
OPENAI_API_VERSION=2025-04-01-preview
  • AZURE_OPENAI_ENDPOINT is your resource URL. If it ends in /openai/v1, keep it that way. If it is just https://<resource>.openai.azure.com/, paste that; the helper in the next step handles both.
  • AZURE_OPENAI_API_KEY is your secret key.
  • AZURE_OPENAI_DEPLOYMENT is the deployment name, for example gpt-5-mini. On Azure you call a deployment, not a model name.
!

Never commit .env to Git or share it. If you use Git, add a .gitignore file containing .env and .venv/.

5

Create the Azure OpenAI helper

Every lab talks to Azure OpenAI through one small helper file. Create model_setup.py and add the blocks below one after another. You write it once per lab folder; the lab code then only needs from model_setup import get_model.

Block 1Imports, .env loading and a console fix

load_dotenv() reads your .env file and puts every line into environment variables, so your key never appears in code.

The loop below it switches the console to UTF-8. Windows terminals default to an older encoding and crash when a model prints curly quotes or dashes.

model_setup.py (add to the end of the file)
"""Azure OpenAI model setup shared by every lab.

Import it in lab code as:  from model_setup import get_model, get_embeddings
"""

import asyncio
import os
import sys

from dotenv import load_dotenv
from langchain_core.embeddings import Embeddings
from langchain_core.language_models import BaseChatModel

# Read AZURE_OPENAI_* values from the .env file next to your lab code.
load_dotenv()

# Windows consoles default to cp1252 and crash on the curly quotes models love to print.
for _stream in (sys.stdout, sys.stderr):
    if hasattr(_stream, "reconfigure"):
        _stream.reconfigure(encoding="utf-8", errors="replace")

Block 2Read settings safely

_setting() fetches a value from the environment and stops with a friendly message if it is missing, instead of a long stack trace.

_uses_v1_api() checks the shape of your endpoint. Azure offers two:

  • https://<resource>.openai.azure.com/openai/v1 is the newer OpenAI-compatible API.
  • https://<resource>.openai.azure.com/ is the classic API that needs an api_version.
model_setup.py (add to the end of the file)
def _setting(name: str) -> str:
    """Return a required setting from .env, or stop with a readable message."""
    value = os.getenv(name, "").strip()
    if not value:
        sys.exit(f"Missing {name} in your .env file. Open .env and fill it in.")
    return value


def _uses_v1_api(endpoint: str) -> bool:
    """True for the OpenAI-compatible Azure endpoint that ends in /openai/v1."""
    return endpoint.rstrip("/").endswith("/openai/v1")

Block 3Build the chat model

get_model() returns a LangChain chat model connected to your deployment.

  • With a /openai/v1 endpoint it uses ChatOpenAI with base_url. On Azure the deployment name goes in model.
  • With a classic endpoint it uses AzureChatOpenAI with azure_deployment and api_version.

Either way the lab code just calls get_model() and never cares which one it got.

model_setup.py (add to the end of the file)
def get_model(deployment: str | None = None, **kwargs) -> BaseChatModel:
    """Return a chat model for your Azure OpenAI deployment."""
    endpoint = _setting("AZURE_OPENAI_ENDPOINT")
    api_key = _setting("AZURE_OPENAI_API_KEY")
    name = deployment or _setting("AZURE_OPENAI_DEPLOYMENT")

    if _uses_v1_api(endpoint):
        from langchain_openai import ChatOpenAI

        return ChatOpenAI(model=name, base_url=endpoint, api_key=api_key, **kwargs)

    from langchain_openai import AzureChatOpenAI

    return AzureChatOpenAI(
        azure_deployment=name,
        azure_endpoint=endpoint,
        api_key=api_key,
        api_version=os.getenv("OPENAI_API_VERSION", "2025-04-01-preview"),
        **kwargs,
    )

Block 4Build the embeddings model

get_embeddings() does the same for embeddings, which turn text into vectors for search. Only the retrieval labs use it, but keeping one helper for every lab is simpler.

model_setup.py (add to the end of the file)
def get_embeddings(deployment: str | None = None) -> Embeddings:
    """Return an embeddings model for your Azure OpenAI embeddings deployment."""
    endpoint = _setting("AZURE_OPENAI_ENDPOINT")
    api_key = _setting("AZURE_OPENAI_API_KEY")
    name = deployment or _setting("AZURE_OPENAI_EMBEDDING_DEPLOYMENT")

    if _uses_v1_api(endpoint):
        from langchain_openai import OpenAIEmbeddings

        return OpenAIEmbeddings(model=name, base_url=endpoint, api_key=api_key)

    from langchain_openai import AzureOpenAIEmbeddings

    return AzureOpenAIEmbeddings(
        azure_deployment=name,
        azure_endpoint=endpoint,
        api_key=api_key,
        api_version=os.getenv("OPENAI_API_VERSION", "2025-04-01-preview"),
    )

Block 5A Windows fix for async code

use_stable_event_loop() matters only for the async MCP lab. The default Windows event loop can crash on exit after async network sessions close. On macOS and Linux it does nothing.

model_setup.py (add to the end of the file)
def use_stable_event_loop() -> None:
    """On Windows, use the Selector event loop so async MCP code exits cleanly."""
    if sys.platform == "win32":
        asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())

Check that the helper can reach your deployment with a one-line test:

PowerShell
python -c "from model_setup import get_model; print(get_model().invoke('Say hello in 5 words').text)"

You should see a short greeting. If you see an error instead, check the troubleshooting step at the end.

6

Write the agent

An agent is a loop: the model reads the conversation, decides whether to call a tool, LangChain runs the tool and hands the result back, and the loop repeats until the model answers in plain text. You provide the model, the tools and a system prompt; create_agent provides the loop.

Create an empty file named hello_agent.py in your lab folder. Then copy each block below into it, in order, one after another.

Block 1Imports

  • create_agent builds the agent loop.
  • tool is a decorator that turns a function into a tool.
  • get_model comes from the helper you wrote in the previous step and returns your Azure OpenAI model.
hello_agent.py (add to the end of the file)
"""Lab 00: a Northwind support agent with two tools.

Run:  python hello_agent.py
"""

from langchain.agents import create_agent
from langchain.tools import tool

from model_setup import get_model

Block 2A pretend order database

A real agent would call an order API. Here a Python dictionary stands in for it, so you can see exactly what data the tools return.

hello_agent.py (add to the end of the file)
ORDERS = {
    "A-100": {"status": "shipped via DHL, arriving 2026-09-15", "total": 249.00},
    "A-101": {"status": "processing, expected 2026-09-18", "total": 1299.50},
}

Block 3Tool 1: order status

The @tool decorator reads three things to describe the tool to the model: the function name, the type hints (order_id: str) and the docstring.

Notice that an unknown id returns a helpful string, not an exception. The model reads that message and can relay it to the user.

hello_agent.py (add to the end of the file)
@tool
def get_order_status(order_id: str) -> str:
    """Look up the shipping status of an order, such as A-100."""
    order = ORDERS.get(order_id.upper())
    if not order:
        return f"No order found with id {order_id}."
    return f"Order {order_id.upper()} is {order['status']}."

Block 4Tool 2: invoice total

The docstring says when to use the tool: "Use for questions about price or total." That sentence is what makes the model pick this tool for money questions. If a model ever ignores your tool, improve the docstring first.

:.2f formats the number with two decimals, and the result names the currency, so the model never has to guess it.

hello_agent.py (add to the end of the file)
@tool
def get_invoice_total(order_id: str) -> str:
    """Return the amount charged for an order. Use for questions about price or total."""
    order = ORDERS.get(order_id.upper())
    if not order:
        return f"No order found with id {order_id}."
    return f"Order {order_id.upper()} total: {order['total']:.2f} USD."

Block 5The system prompt

The system prompt is the agent's standing instructions. The key rule is "never guess a status or an amount", which pushes the model to call a tool instead of inventing numbers.

hello_agent.py (add to the end of the file)
SYSTEM_PROMPT = (
    "You are the Northwind support assistant. Answer briefly and only about orders. "
    "Never guess a status or an amount: always call the matching tool."
)

Block 6Assemble the agent

create_agent takes the model, the list of tools and the system prompt, and returns a ready-to-run agent. The optional model argument lets you swap in a different model later, for example a fake one in tests.

hello_agent.py (add to the end of the file)
def build_agent(model=None):
    """Build the agent. Pass a model to override the Azure OpenAI default."""
    return create_agent(
        model=model or get_model(),
        tools=[get_order_status, get_invoice_total],
        system_prompt=SYSTEM_PROMPT,
    )

Block 7Ask two questions

This block only runs when you start the file directly.

  • agent.invoke(...) takes the conversation as a list of messages and returns the final state, which holds every message of the run.
  • The tools_used line collects the tool calls the model made, so you can see which tools it chose.
  • result['messages'][-1].text is the agent's final answer.

The second question uses an order id that does not exist, to show the friendly error path.

hello_agent.py (add to the end of the file)
if __name__ == "__main__":
    agent = build_agent()

    questions = [
        "Where is A-100 and how much was it?",
        "What is the status of order Z-999?",
    ]
    for question in questions:
        result = agent.invoke({"messages": [{"role": "user", "content": question}]})
        tools_used = [
            call["name"] for message in result["messages"] for call in getattr(message, "tool_calls", [])
        ]
        print(f"You:   {question}")
        print(f"Tools: {', '.join(tools_used) or 'none'}")
        print(f"Agent: {result['messages'][-1].text}\n")
7

Run the agent

PowerShell
python hello_agent.py

Your output should look similar to this. Model answers vary a little from run to run, so the wording will not match exactly.

Expected output
You:   Where is A-100 and how much was it?
Tools: get_order_status, get_invoice_total
Agent: Order A-100 is shipped via DHL and arriving 2026-09-15. Total charged: 249.00 USD.

You:   What is the status of order Z-999?
Tools: get_order_status
Agent: I couldn’t find an order with ID Z-999. Please confirm the order number and I’ll check the status.

Look at the Tools line for the first question. The model called both tools, possibly in parallel, then combined the two results into one answer. For the second question it called get_order_status, got No order found, and passed that on instead of inventing a status.

8

Check your work

You are done when

  • The first answer mentions DHL, the 2026-09-15 arrival date and 249.00 USD.
  • The Tools line for the first question shows get_order_status and get_invoice_total.
  • The answer about Z-999 says the order was not found, instead of inventing a status.

Troubleshooting

The model answers without calling a tool

Re-read the tool docstrings and the system prompt. The model chooses tools from their names and descriptions, so a vague docstring leads to skipped tools.

401 Unauthorized or 'Access denied due to invalid subscription key'

The key in .env is wrong or has spaces around it. Copy it again from your instructor, with no quotes.

404 'DeploymentNotFound' or 'The API deployment for this resource does not exist'

AZURE_OPENAI_DEPLOYMENT must be the deployment name exactly as your instructor gave it, and the endpoint must be the same resource.

429 Too Many Requests

The shared deployment is busy. Wait 30 seconds and run again.

ModuleNotFoundError: No module named 'model_setup' (or another package)

Run the command from inside your lab folder, with the virtual environment activated. Reinstall with pip install -r requirements.txt if a library is missing.

'python' is not recognized, or the wrong Python runs

Activate the virtual environment first. On Windows you can also try py -3.11 instead of python when creating the venv.

Go further

  • Add a third tool, get_delivery_address(order_id), and ask a question that needs all three.
  • Remove the word total from the second docstring and see whether the model still finds the tool.
  • Optional: create a free LangSmith account, add LANGSMITH_TRACING=true and LANGSMITH_API_KEY=... to .env, and run again to see every model and tool call as a trace.

Stuck?

Download the finished project and compare it with yours. It contains every file from this guide except .env and .venv, so you still add your own settings.