Build an AI Customer Support Agent

Deploy Azure OpenAI and build a grounded AI support agent with a chat UI.

Introduction

30 Second Summary

Every online store has a customer support chatbot these days. Some are helpful. Others confidently make up product details that do not exist, leaving customers frustrated and confused.

In this project, you will build your own AI customer support agent powered by Azure OpenAI. You will deploy a cloud AI model, ground it with real product data so it cannot hallucinate, and wrap it in a browser-based chat interface you can demo to anyone.

What You'll Build

You open your browser, type a question about headphones or a smartwatch, and get back an accurate, friendly response pulled from a real product catalog. Ask about something outside the catalog and the agent honestly says it does not know.

By the end of this project, you'll have:

  • A live AI chat where you type a product question and see an accurate answer appear in a styled message bubble, pulled directly from catalog data you control.
  • A grounded support agent that refuses to invent features or prices. Ask about a product not in the catalog and it tells you honestly that it does not have that information.
  • A multi-turn conversation where the agent remembers what you asked two messages ago, so you can follow up naturally without repeating yourself.
  • Secret Mission: Add smart suggested follow-up questions that appear as clickable chips after each agent response.

Are there any prerequisites?

You need basic Python knowledge (variables, functions, and imports) and an active Azure for Students subscription. The subscription gives you $100 in free credit with no credit card required.

Step 1 walks you through installing every tool you need, so no prior Azure or AI experience is required.

Before We Start

You are about to build an AI customer support agent that runs in your browser, powered by a model you deploy on Azure OpenAI. Before writing any AI code, you need the right tools installed and a live connection to your Azure subscription.

This step installs the Azure CLI, sets up a Python project with the packages your agent needs, and signs you in to Azure so your code can talk to cloud resources.

In this step, get ready to:
  • Install the Azure CLI and verify it works.
  • Create a Python project folder with a virtual environment and dependencies.
  • Sign in to Azure with your student subscription.
Install the Azure CLI

The Azure CLI is a command-line tool that lets you create and manage Azure resources from your terminal. You will use it throughout this project to deploy your AI model and configure cloud services.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
  • Install the Azure CLI by running the command for your operating system:

macOS

  • Install the Azure CLI with Homebrew by running:
brew update && brew install azure-cli

What if I don't have Homebrew?

Homebrew is the recommended package manager for macOS. If you don't have it, install it first by following the instructions at brew.sh.

Windows

  • Install the Azure CLI with WinGet by running:
winget install --exact --id Microsoft.AzureCLI
  • Close your PowerShell window completely after the install finishes.
  • Reopen PowerShell using the Windows key, typing PowerShell, and pressing Enter.

Why do I need to restart my terminal?

The installer adds the az command to your system PATH. Your terminal only picks up PATH changes when it starts a new session.

  • Before you run the next command, what do you expect to see if the install succeeded?
  • Verify the Azure CLI is installed by running:
az version

✔️ I see version information

You should see JSON output containing an azure-cli key with a version number. The Azure CLI is ready to use.

ⓧ Command not found

Your terminal cannot find the az command. Try these fixes:

  • Make sure you fully closed and reopened your terminal after installing.
  • On Windows, try opening a new PowerShell window as Administrator and running the install command again.
  • On macOS, confirm Homebrew itself is working by running brew --version. If that fails, reinstall Homebrew first.

Help me troubleshoot the Azure CLI installation

Create your project folder and install dependencies

Every Python project needs its own folder and virtual environment. The virtual environment keeps your packages isolated so they do not conflict with other projects on your machine.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir techgear-support
cd techgear-support
  • Confirm your folder exists by running:
ls

You should see the techgear-support folder listed if you are still in the Desktop, or an empty listing if you are already inside it.

  • Create a Python virtual environment by running:

macOS

python3 -m venv venv
source venv/bin/activate

Windows

python -m venv venv
venv\Scripts\activate

Your terminal prompt now shows (venv) at the beginning. This confirms the virtual environment is active.

Don't see (venv) in your prompt?

  • On Windows, if you see a permissions error, run Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned and try the activate command again.
  • On macOS, make sure you used source venv/bin/activate (not just venv/bin/activate).

Help me activate my Python virtual environment

  • Install the three packages your project needs by running:
pip install openai==2.46.0 flask==3.1.3 python-dotenv==1.1.0

What are these packages?

  • openai is the official Python SDK for calling OpenAI-compatible APIs, including Azure OpenAI.
  • flask is a lightweight web framework you will use to build the chat interface.
  • python-dotenv loads environment variables from a .env file so you can keep secrets out of your code.
  • Confirm the packages installed correctly by running:
pip list

You should see openai, flask, and python-dotenv in the output list with their version numbers.

Packages not showing up?

  • Make sure your virtual environment is active. You should see (venv) in your prompt before running pip install.
  • If you accidentally installed packages without the venv active, activate it and run the pip install command again.

Help me fix my pip install issue

Sign in to Azure

Your code will need permission to create and use Azure resources. The az login command links your terminal session to your Azure for Students subscription.

  • Sign in to Azure by running:
az login

What happens when I run this?

A browser window opens and asks you to sign in with your Microsoft account. After you authenticate, the browser shows a success message and you can close it.

Back in your terminal, you will see JSON output listing your subscription details. Look for your Azure for Students subscription name in the output.

  • Confirm your student subscription appears in the terminal output. You should see a JSON block with "name" showing your subscription name and "state": "Enabled".

Subscription not showing up?

  • Make sure you are signing in with the same Microsoft account that has your Azure for Students subscription.
  • If you see multiple subscriptions and are unsure which is correct, look for one with "Students" in the name or check the Azure portal at portal.azure.com under Subscriptions.

Help me sign in to Azure with my student subscription

Your environment is ready. You have the Azure CLI installed, a Python project with all the right packages, and a live connection to your Azure subscription. Next up, you will deploy your first AI model in the cloud.

Deploy Your AI Model on Azure

Your local environment is ready. Now you need something to talk to.

Before writing any AI code, you need a real model deployed in the cloud that your Python scripts can call over the network. In this step you will provision real Azure OpenAI infrastructure using the Azure CLI.

In this step, get ready to:
  • Create a resource group and Azure OpenAI resource via the CLI.
  • Deploy the gpt-4o-mini model to your resource.
  • Retrieve your endpoint and API key, then store them in a local .env file.
Create a resource group and Azure OpenAI resource

Azure organises resources into groups. You will create one group to hold everything for this project, then create an Azure OpenAI resource inside it.

Azure for Students subscriptions restrict which regions you can deploy to. You need to check your allowed regions first.

  • List your available regions by running this command:
az account list-locations --output table

You will see a table of region names. Look for regions like eastus, eastus2, swedencentral, or australiaeast. These commonly support Azure OpenAI.

  • Create a resource group by running this command (pick a region from your list that you saw above):
az group create --name techgear-rg --location eastus

What is a resource group?

A resource group is a container that holds related Azure resources together. It makes it easy to manage and clean up everything at once when you are finished with the project.

You should see JSON output with provisioningState set to Succeeded. That confirms your resource group is ready.

  • Create the Azure OpenAI resource inside your resource group by running:
az cognitiveservices account create --name techgear-openai --resource-group techgear-rg --location eastus --kind OpenAI --sku s0 --custom-domain techgear-openai

What does this command do?

This provisions an Azure OpenAI service account. --kind OpenAI tells Azure you want the OpenAI service specifically. --custom-domain sets the subdomain for your endpoint URL.

The --custom-domain value must be globally unique. If techgear-openai is already taken, add your initials or a number (e.g. techgear-openai-jd42).

This command can take 1-2 minutes to complete. Wait for the JSON output confirming the resource was created.

Seeing a region policy error?

Azure for Students subscriptions may block certain regions. If you see an error mentioning policy restrictions or a disallowed location, try a different region from your az account list-locations output. Common alternatives that work for students include eastus2, swedencentral, or australiaeast.

Re-run both the az group create and az cognitiveservices account create commands with the new --location value.

Help me fix a region policy error

Deploy the gpt-4o-mini model

Your Azure OpenAI resource is an empty container until you deploy a model into it. You will deploy gpt-4o-mini, a lightweight model confirmed to work on Azure for Students subscriptions.

  • Deploy the model by running this command:
az cognitiveservices account deployment create --name techgear-openai --resource-group techgear-rg --deployment-name gpt-4o-mini --model-name gpt-4o-mini --model-format OpenAI --sku-capacity 1 --sku-name Standard

What does this command do?

This deploys the gpt-4o-mini model into your resource with a Standard pricing tier. --deployment-name gpt-4o-mini is the name your code will use when making API calls.

The --sku-capacity 1 sets throughput to 1K tokens per minute, which is what Azure for Students allows.

Wait for the command to finish. You should see JSON output with provisioningState set to Succeeded.

Deployment failing or stuck?

If you see a quota or capacity error, confirm you used --sku-capacity 1 (not a higher number). Student subscriptions have a 1K TPM cap for gpt-4o-mini.

If the model name is not found, double check that your resource was created with --kind OpenAI (not AIServices).

Help me troubleshoot my model deployment

Retrieve your endpoint and API key

Your Python code needs two pieces of information to connect to the model: the endpoint URL and an API key. You will grab these from the Azure Portal and store them in a .env file so your code can load them securely.

  • Open the Azure Portal at portal.azure.com in your browser.
  • In the search bar at the top, type techgear-openai and select your OpenAI resource from the results.
  • In the left sidebar, expand Resource Management, then click Keys and Endpoint.
  • Copy the Endpoint value (it looks like https://techgear-openai.openai.azure.com/).
  • Copy KEY 1 (either key works, but use KEY 1 for this project).

Why use a .env file?

Storing secrets in a .env file keeps them out of your source code. The python-dotenv package you installed earlier loads these values into environment variables at runtime.

Never commit your .env file to version control.

  • Switch back to VS Code. Create a new file called .env in the root of your techgear-support folder and paste the following, replacing the placeholder values with what you just copied:
AZURE_OPENAI_ENDPOINT=your-endpoint-here
AZURE_OPENAI_API_KEY=your-api-key-here
AZURE_OPENAI_DEPLOYMENT_NAME=gpt-4o-mini

What does each variable do?

  • AZURE_OPENAI_ENDPOINT is the base URL your code sends requests to.
  • AZURE_OPENAI_API_KEY authenticates your requests so Azure knows it is you.
  • AZURE_OPENAI_DEPLOYMENT_NAME tells the SDK which model deployment to use.
  • Save the .env file.
  • Confirm the file exists by running:
ls .env

You should see .env printed in the output, confirming the file was saved.

Not sure if you copied the right values?

Your endpoint should start with https:// and end with .openai.azure.com/. Your API key is a long string of letters and numbers. If either looks off, go back to the Keys and Endpoint page in the Azure Portal and re-copy.

Help me verify my endpoint and key format

Before you move on, let's verify the deployment in the portal. What do you think you will see on the deployments page?

  • Back in the Azure Portal, navigate to your techgear-openai resource.
  • In the left sidebar, expand Resource Management, then click Model Deployments.
  • Confirm you see gpt-4o-mini listed with a status of Succeeded.

✔️ I see gpt-4o-mini with status Succeeded

Your model is live and ready to receive API calls. Your .env file is saved with the right credentials. You are all set for the next step.

ⓧ I don't see the deployment or it shows a different status

  • If the status shows Creating or Updating, wait a minute and refresh the page.
  • If it shows Failed, delete the deployment and re-run the az cognitiveservices account deployment create command from earlier.
  • If you see no deployments at all, make sure you are looking at the correct resource (the one named techgear-openai or whatever custom domain name you used).

Help me troubleshoot my deployment status

✔️ Awesome, I've got everything!

Great. Double check you have saved your .env file with all three variables filled in.

ⓧ I'd like to double check the full code

Your .env file should look exactly like this (with your own values replacing the placeholders):

AZURE_OPENAI_ENDPOINT=your-endpoint-here
AZURE_OPENAI_API_KEY=your-api-key-here
AZURE_OPENAI_DEPLOYMENT_NAME=gpt-4o-mini

You just deployed real AI infrastructure on Azure. Next up, you will write Python code that calls this model and gets your first AI-generated response.

Make Your First AI Call

Your model is deployed and your credentials are stored. Now you need proof that everything actually works end-to-end.

A single Python script that sends one question to the cloud and gets an answer back tells you that authentication, networking, and the model are all functioning. In this step, you will write that script and see what happens when you ask the model something it has no real knowledge about.

In this step, get ready to:
  • Write a test script that calls your deployed Azure OpenAI model and prints a response.
  • Observe how the model handles questions about products it knows nothing about.
Create your test script

Your script needs to load the credentials from your .env file, create a connection to Azure OpenAI, send a simple question, and print the response.

  • In VS Code, create a new file called test_call.py inside your techgear-support folder.
  • Add the following code to test_call.py:
import os
from openai import AzureOpenAI
from dotenv import load_dotenv

# Load credentials from .env file
load_dotenv()

# Create a connection to your Azure OpenAI deployment
client = AzureOpenAI(
    azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
    api_key=os.getenv("AZURE_OPENAI_API_KEY"),
    api_version="2024-02-01"
)

# Send a simple question to the model
response = client.chat.completions.create(
    model=os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME"),
    messages=[
        {"role": "user", "content": "What is the capital of France?"}
    ]
)

# Print the model's reply
print(response.choices[0].message.content)

What does this code do?

load_dotenv() reads your .env file and loads your endpoint, API key, and deployment name into environment variables so they never appear in your code.

AzureOpenAI(...) creates a client object that knows how to talk to your specific Azure deployment. The api_version tells Azure which version of its API to use.

client.chat.completions.create() sends a message to the model and returns the response. The messages list is how you structure a conversation with the model.

  • Save test_call.py.

File not saving?

Make sure you created the file inside the techgear-support folder, not outside it. Your virtual environment must be active (you should see (venv) in your terminal prompt).

help me set up my file in the right location

Run the script and test with product questions

Time to see if your cloud AI connection actually works.

  • Run your test script by entering this command in your terminal:
python test_call.py

You should see the model respond with something like "The capital of France is Paris" printed in your terminal. The exact wording varies each time, but it will clearly answer your question.

Seeing an authentication or connection error?

  • Open your .env file and double check that AZURE_OPENAI_ENDPOINT starts with https:// and AZURE_OPENAI_API_KEY has no extra spaces.
  • Confirm AZURE_OPENAI_DEPLOYMENT_NAME matches the deployment name you used in the Azure Portal (it should be gpt-4o-mini).
  • Make sure your virtual environment is active. If you see ModuleNotFoundError, your packages are installed inside the venv but you are running outside it.

help me debug my Azure OpenAI connection error

The script works for general knowledge questions. Now try asking something specific to TechGear, the fictional company whose support agent you are building.

  • In test_call.py, find this line:
{"role": "user", "content": "What is the capital of France?"}
  • Replace "What is the capital of France?" with "What are the features of the TechGear Pro headphones?", so the line now reads:
{"role": "user", "content": "What are the features of the TechGear Pro headphones?"}
  • Save test_call.py.

Before you run this, what do you think the model will say about TechGear Pro headphones? TechGear is a company you just made up for this project. The model has never seen any TechGear product data.

  • Run the script again:
python test_call.py

The model responds confidently with detailed features, pricing, and specs for "TechGear Pro headphones." Every single detail it mentions is completely fabricated.

What just happened?

This is hallucination. The model does not know it lacks information about TechGear. Instead of saying "I don't know," it generates plausible-sounding product details drawn from patterns in its training data.

For a customer support agent, this is dangerous. A customer could receive completely wrong information about pricing, features, or warranty terms. In the next step, you will fix this by giving the model real product data and strict instructions to only answer from that data.

  • Change the message in test_call.py back to "What is the capital of France?" and save the file.

✔️ Awesome, I've got everything!

Great. Double check you have saved test_call.py with the original France question.

ⓧ I'd like to double check the full code

import os
from openai import AzureOpenAI
from dotenv import load_dotenv

load_dotenv()

client = AzureOpenAI(
    azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
    api_key=os.getenv("AZURE_OPENAI_API_KEY"),
    api_version="2024-02-01"
)

response = client.chat.completions.create(
    model=os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME"),
    messages=[
        {"role": "user", "content": "What is the capital of France?"}
    ]
)

print(response.choices[0].message.content)

You have proven your Azure OpenAI connection works, and you have seen firsthand why a raw model cannot be trusted as a customer support agent. Next up, you will engineer a system prompt and inject real product data so the agent only answers from verified information.

Engineer the Support Agent

In the previous step, you asked the model about TechGear products and it confidently invented features, prices, and details that don't exist. That's hallucination in action.

Now you'll fix it by giving the model real product data and strict rules about when to speak and when to stay silent. This technique is called grounding, and it's the core skill that separates a toy demo from a production-ready AI agent.

In this step, get ready to:
  • Create a product catalog that gives the model verified facts to draw from.
  • Build an agent script with a system prompt that locks the model to catalog-only answers.
  • Test multi-turn conversation and confirm the agent refuses to invent information.
Create the product catalog

The model needs a source of truth. You'll create a JSON file containing TechGear's products, prices, features, and company policies.

  • Create a new file called products.json in your techgear-support folder and paste the following product catalog:
{
  "products": [
    {
      "name": "TechGear Pro Wireless Headphones",
      "price": "$79.99",
      "features": ["Active Noise Cancellation", "40-hour battery life", "Bluetooth 5.3", "Foldable design"],
      "warranty": "2 years",
      "return_policy": "30-day money-back guarantee"
    },
    {
      "name": "TechGear Ultra Smartwatch",
      "price": "$199.99",
      "features": ["Heart rate monitor", "GPS tracking", "5ATM water resistance", "7-day battery"],
      "warranty": "1 year",
      "return_policy": "30-day money-back guarantee"
    },
    {
      "name": "TechGear PowerBank 20000",
      "price": "$49.99",
      "features": ["20000mAh capacity", "USB-C PD 65W", "Charges 3 devices simultaneously", "LED display"],
      "warranty": "18 months",
      "return_policy": "30-day money-back guarantee"
    }
  ],
  "policies": {
    "shipping": "Free shipping on orders over $50. Standard delivery 3-5 business days.",
    "returns": "30-day money-back guarantee on all products. Items must be in original packaging.",
    "support_hours": "Monday-Friday 9AM-6PM EST"
  }
}

What does this file do?

This catalog is the agent's entire knowledge base. It contains three products with real prices, features, and warranty terms, plus company-wide policies for shipping, returns, and support hours.

When you inject this into the system prompt later, the model can only reference facts from this file. Anything not listed here becomes unanswerable.

  • Save products.json.
  • Confirm the file exists by running:
ls

You should see products.json alongside your existing test_call.py and .env files.

Build the agent script

Now you'll create agent.py, a script that loads the catalog, builds a system prompt with grounding rules, and runs an interactive conversation loop in your terminal.

  • Create a new file called agent.py in your techgear-support folder and paste the following setup code:
import os
import json
from openai import AzureOpenAI
from dotenv import load_dotenv

load_dotenv()

# Connect to Azure OpenAI using your .env credentials
client = AzureOpenAI(
    azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
    api_key=os.getenv("AZURE_OPENAI_API_KEY"),
    api_version="2024-02-01"
)

DEPLOYMENT_NAME = os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME", "gpt-4o-mini")

# Load the product catalog as the agent's knowledge base
with open("products.json", "r") as f:
    catalog = json.load(f)

What does this code do?

This block sets up the same AzureOpenAI client you used in test_call.py. The new part is loading products.json into a catalog variable so you can inject it into the system prompt next.

  • Below the catalog loading code, add the system prompt that defines the agent's persona and grounding rules:
SYSTEM_PROMPT = f"""You are a friendly and helpful customer support agent for TechGear, an electronics company.

Your role:
- Answer questions about TechGear products, pricing, warranties, and policies
- Help customers troubleshoot basic issues
- Be concise, professional, and empathetic
- If you don't know something or it's outside your scope, say so honestly

Product Catalog:
{json.dumps(catalog, indent=2)}

Rules:
- Only recommend products from the catalog above
- Never make up features, prices, or policies that aren't listed
- For complex technical issues, suggest the customer contact support@techgear.com
- Keep responses under 150 words unless the customer asks for detail"""

Why is this the key to stopping hallucination?

The system prompt does two things. First, it injects the entire product catalog using an f-string, so the model has all verified facts in its context window.

Second, the Rules section explicitly forbids inventing information. The model now has both the data to answer correctly and the instruction to stay silent when it doesn't know something.

  • Below the system prompt, add the conversation loop that handles multi-turn memory:
# Store conversation history for multi-turn memory
conversation_history = [
    {"role": "system", "content": SYSTEM_PROMPT}
]

print("TechGear Support Agent (type 'quit' to exit)")
print("-" * 45)

while True:
    user_input = input("\nYou: ")
    if user_input.lower() == "quit":
        break

    conversation_history.append({"role": "user", "content": user_input})

    response = client.chat.completions.create(
        model=DEPLOYMENT_NAME,
        messages=conversation_history,
        temperature=0.7,
        max_tokens=300
    )

    assistant_message = response.choices[0].message.content
    conversation_history.append(
        {"role": "assistant", "content": assistant_message}
    )
    print(f"\nAgent: {assistant_message}")

How does conversation memory work?

The conversation_history list starts with the system prompt, then accumulates every user message and assistant reply. Each API call sends the full history, so the model knows what was discussed earlier.

This means if you ask about headphones, then say "what about the warranty?", the model knows "the warranty" refers to the headphones from your first question.

  • Save agent.py.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file, then move on to testing.

ⓧ I'd like to double check the full code

Your agent.py should contain three sections in this order:

  • Imports, client setup, and catalog loading (the first code block above).
  • The SYSTEM_PROMPT f-string with persona, catalog injection, and rules (the second code block above).
  • The conversation_history list and the while True loop (the third code block above).

Scroll up and confirm each section is present in order with no gaps between them.

Test the grounded agent

Time to see whether grounding actually works. You'll ask the agent questions it can answer from the catalog, then ask something outside its knowledge to confirm it refuses to hallucinate.

  • Before you run the script, think about this: if you ask "What headphones do you sell?", do you expect the agent to invent features like it did in test_call.py, or stick to the catalog?
  • Run the agent by executing:
python agent.py

You'll see the prompt TechGear Support Agent (type 'quit' to exit) appear, waiting for your input.

  • Type this first question and press Enter:
What headphones do you sell?

The agent responds with details about the TechGear Pro Wireless Headphones, citing the exact price of $79.99 and features like Active Noise Cancellation and 40-hour battery life from your catalog.

  • Now test multi-turn memory by asking a follow-up:
What's the warranty on those?

The agent knows "those" refers to the headphones and responds with the 2-year warranty from the catalog.

  • Finally, test the grounding boundary by asking about a product that doesn't exist:
Do you sell gaming keyboards?

The agent says it doesn't have information about gaming keyboards, or that TechGear doesn't carry them. No invented specs, no hallucinated prices.

  • Type quit and press Enter to exit the agent.

What just happened?

Compare this to test_call.py from the previous step, where the model invented TechGear product details with complete confidence. The same model, with a grounding prompt and real data, now answers accurately and admits when it doesn't know something.

That's the power of prompt engineering. You didn't retrain the model or change its weights. You changed the instructions and gave it a source of truth.

Agent not responding or showing errors?

  • If you see a FileNotFoundError, make sure products.json is saved in the same folder as agent.py.
  • If you get an authentication error, confirm your .env file has the correct endpoint and API key from the Azure Portal.
  • If the agent invents product information, double check that your SYSTEM_PROMPT includes the line Never make up features, prices, or policies that aren't listed.

Still stuck? Help me debug my agent.py script that isn't responding correctly to product questions.

Your agent is grounded, accurate, and remembers context across turns. Next up, you'll wrap it in a web interface so you can demo it in a browser instead of a terminal.

Build the Chat Web Interface

Your support agent works in the terminal, but a terminal chatbot is not something you can demo in an interview or screenshot for LinkedIn.

In this step, you will wrap your agent logic in a Flask web application and build a browser-based chat UI. When you are done, anyone can open a URL, type a question, and get a grounded answer in a polished message bubble.

In this step, get ready to:
  • Create a Flask app with a route that serves your chat page and an API endpoint that handles messages.
  • Build an HTML chat interface with JavaScript that sends and receives messages in real time.
  • Style the interface with distinct message bubbles and a branded header.
Create the Flask application

Your Flask app needs two routes: one to serve the HTML page and one to accept chat messages as JSON. It also needs the same Azure OpenAI client and system prompt you built in the previous step.

  • In VS Code, create a new file called app.py in your techgear-support folder.
  • Paste the following imports and setup code:
import os
import json
from flask import Flask, render_template, request, jsonify
from openai import AzureOpenAI
from dotenv import load_dotenv

# Load environment variables from .env
load_dotenv()

# Create the Flask web application
app = Flask(__name__)

# Set up the Azure OpenAI client
client = AzureOpenAI(
    azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
    api_key=os.getenv("AZURE_OPENAI_API_KEY"),
    api_version="2024-02-01"
)

DEPLOYMENT_NAME = os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME", "gpt-4o-mini")

# Load the product catalog for grounding
with open("products.json", "r") as f:
    catalog = json.load(f)

What does this code do?

This sets up the same Azure OpenAI connection you used in agent.py, but inside a Flask web app.

  • Flask creates a web server that can serve HTML pages and handle API requests.
  • render_template serves HTML files from a templates folder.
  • The catalog is loaded once at startup so the system prompt can reference it.
  • Below the code you just pasted, add the system prompt and the index route:
SYSTEM_PROMPT = f"""You are a friendly and helpful customer support agent for TechGear, an electronics company.

Your role:
- Answer questions about TechGear products, pricing, warranties, and policies
- Help customers troubleshoot basic issues
- Be concise, professional, and empathetic
- If you don't know something or it's outside your scope, say so honestly

Product Catalog:
{json.dumps(catalog, indent=2)}

Rules:
- Only recommend products from the catalog above
- Never make up features, prices, or policies that aren't listed
- For complex technical issues, suggest the customer contact support@techgear.com
- Keep responses under 150 words unless the customer asks for detail"""

# Store conversation history per session
conversation_histories = {}


@app.route("/")
def index():
    return render_template("index.html")

What does this code do?

The SYSTEM_PROMPT is the same grounding prompt from your terminal agent, injected with the full product catalog.

The conversation_histories dictionary stores chat history per session so the agent remembers earlier messages. The / route serves the HTML page you will create next.

  • Finally, add the chat endpoint and the main block at the bottom of app.py:
@app.route("/chat", methods=["POST"])
def chat():
    data = request.get_json()
    user_message = data.get("message", "")
    session_id = data.get("session_id", "default")

    # Initialize conversation with system prompt for new sessions
    if session_id not in conversation_histories:
        conversation_histories[session_id] = [
            {"role": "system", "content": SYSTEM_PROMPT}
        ]

    # Add the user's message to history
    conversation_histories[session_id].append(
        {"role": "user", "content": user_message}
    )

    # Call Azure OpenAI with the full conversation
    response = client.chat.completions.create(
        model=DEPLOYMENT_NAME,
        messages=conversation_histories[session_id],
        temperature=0.7,
        max_tokens=300
    )

    # Extract and store the assistant's reply
    assistant_message = response.choices[0].message.content
    conversation_histories[session_id].append(
        {"role": "assistant", "content": assistant_message}
    )

    return jsonify({"reply": assistant_message})


if __name__ == "__main__":
    app.run(debug=True, port=5000)

What does this code do?

The /chat endpoint accepts a POST request with a JSON body containing message and session_id.

  • It builds a conversation history (including the system prompt) so the model has full context.
  • It calls Azure OpenAI with the full history and returns the assistant's reply as JSON.
  • The if __name__ == "__main__" block starts the server on port 5000 when you run the file directly.
  • Save app.py.

Why not reuse agent.py directly?

Flask needs everything in one module so it can manage the server lifecycle. Consolidating the client setup, system prompt, and routing into app.py keeps the web app self-contained and easy to run with a single command.

Create the HTML chat interface

Flask looks for HTML files in a folder called templates and static assets (CSS, images) in a folder called static.

  • Create both directories by running these commands in your terminal:
mkdir templates
mkdir static
  • Confirm the folders exist by running:
ls

You should see templates and static listed alongside your existing Python files.

  • Create a new file called index.html inside the templates folder.
  • Paste the following HTML structure into it:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>TechGear Support</title>
    <link rel="stylesheet" href="/static/style.css">
</head>
<body>
    <div class="chat-container">
        <div class="chat-header">
            <h1>TechGear Support</h1>
            <p>Hi! How can I help you today?</p>
        </div>
        <div class="chat-messages" id="chatMessages">
            <div class="message assistant">
                <p>Welcome to TechGear support! I can help you with product information, orders, warranties, and more. What can I do for you?</p>
            </div>
        </div>
        <div class="chat-input">
            <input type="text" id="userInput" placeholder="Type your message..." autocomplete="off">
            <button id="sendButton" onclick="sendMessage()">Send</button>
        </div>
    </div>
    <script>
    </script>
</body>
</html>

What does this HTML do?

  • The chat-container div wraps the entire chat interface.
  • chatMessages is the scrollable area where message bubbles appear.
  • The input and button at the bottom let the user type and send messages.
  • The empty <script> tag is where you will add JavaScript next.

Now you need JavaScript to send messages to your Flask backend and display the responses.

  • In templates/index.html, find the empty <script> tags and paste the following code between them:
        const sessionId = crypto.randomUUID();

        // Send message when Enter key is pressed
        document.getElementById("userInput").addEventListener("keypress", function(e) {
            if (e.key === "Enter") sendMessage();
        });

        // Send the user's message to the backend
        async function sendMessage() {
            const input = document.getElementById("userInput");
            const message = input.value.trim();
            if (!message) return;

            appendMessage("user", message);
            input.value = "";
            document.getElementById("sendButton").disabled = true;

            try {
                const response = await fetch("/chat", {
                    method: "POST",
                    headers: {"Content-Type": "application/json"},
                    body: JSON.stringify({message: message, session_id: sessionId})
                });
                const data = await response.json();
                appendMessage("assistant", data.reply);
            } catch (error) {
                appendMessage("assistant", "Sorry, something went wrong. Please try again.");
            }

            document.getElementById("sendButton").disabled = false;
        }

What does this JavaScript do?

  • crypto.randomUUID() generates a unique session ID so the backend can track conversation history per browser tab.
  • sendMessage() reads the input field, posts the message to /chat, and displays the reply.
  • The send button is disabled during the request so the user cannot double-send.
  • Directly below the sendMessage function (still inside the <script> tags), paste the helper that renders message bubbles:
        // Render a message bubble in the chat area
        function appendMessage(role, content) {
            const container = document.getElementById("chatMessages");
            const div = document.createElement("div");
            div.className = "message " + role;
            div.innerHTML = "<p>" + content.replace(/\n/g, "<br>") + "</p>";
            container.appendChild(div);
            container.scrollTop = container.scrollHeight;
        }

What does appendMessage do?

It creates a new <div> with the class message user or message assistant, appends it to the chat area, and scrolls to the bottom so the latest message is always visible.

  • Save templates/index.html.

Before you think about styling, test that the chat actually works. What do you think will happen when you load the page without any CSS?

  • Start the Flask server by running:
python app.py

You should see output indicating the server is running on http://127.0.0.1:5000.

  • Open your browser and go to http://localhost:5000.
  • Type a question like "What headphones do you sell?" and click Send (or press Enter).

You should see your message appear, followed by the agent's grounded reply. The page looks unstyled and plain, but the functionality works.

Page not loading or seeing an error?

  • If you see a jinja2.exceptions.TemplateNotFound error in your terminal, make sure index.html is inside the templates folder (not in the project root).
  • If the send button does nothing, open your browser's developer console (F12) and check for JavaScript errors. A missing closing brace in the script is a common cause.
  • If you get a connection error from the agent, verify your .env file has the correct endpoint and key.

help me debug my Flask chat app

  • Stop the server by pressing Ctrl+C in your terminal.
Style the chat interface

The chat works, but it looks like a plain 1990s web page. CSS will transform it into a modern chat interface with distinct message bubbles and a branded header.

  • Create a new file called style.css inside the static folder.
  • Paste the layout foundation styles:
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    background-color: #f0f2f5;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
}

.chat-container {
    width: 100%;
    max-width: 600px;
    height: 90vh;
    background: white;
    border-radius: 12px;
    box-shadow: 0 2px 20px rgba(0, 0, 0, 0.1);
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

What does this CSS do?

  • The * reset removes default browser margins so spacing is predictable.
  • body uses flexbox to center the chat container on the page.
  • .chat-container creates a 600px-wide card with rounded corners and a subtle shadow.
  • Below the container styles, add the header and message area styling:
.chat-header {
    background: #0078d4;
    color: white;
    padding: 20px;
    text-align: center;
}

.chat-header h1 {
    font-size: 1.3rem;
    margin-bottom: 4px;
}

.chat-header p {
    font-size: 0.85rem;
    opacity: 0.9;
}

.chat-messages {
    flex: 1;
    overflow-y: auto;
    padding: 20px;
    display: flex;
    flex-direction: column;
    gap: 12px;
}

What does this CSS do?

The header gets a blue background (#0078d4) matching Azure's brand color. The messages area uses flex: 1 to fill all remaining vertical space and scrolls when messages overflow.

  • Next, add the message bubble styles:
.message {
    max-width: 80%;
    padding: 12px 16px;
    border-radius: 12px;
    line-height: 1.4;
    font-size: 0.9rem;
}

.message.user {
    align-self: flex-end;
    background: #0078d4;
    color: white;
    border-bottom-right-radius: 4px;
}

.message.assistant {
    align-self: flex-start;
    background: #e9ecef;
    color: #333;
    border-bottom-left-radius: 4px;
}

What does this CSS do?

User messages are blue and align to the right. Assistant messages are gray and align to the left. The flattened bottom corner on each side creates the classic chat bubble shape.

  • Save static/style.css, restart your server with python app.py, and refresh your browser.

You should see the centered white card with a blue header and the welcome message in a gray bubble.

  • Now add the input area and button styles to complete the interface. Paste these below your existing CSS:
.chat-input {
    display: flex;
    padding: 16px;
    border-top: 1px solid #e9ecef;
    gap: 8px;
}

.chat-input input {
    flex: 1;
    padding: 12px 16px;
    border: 1px solid #ddd;
    border-radius: 24px;
    font-size: 0.9rem;
    outline: none;
}

.chat-input input:focus {
    border-color: #0078d4;
}

What does this CSS do?

The input area sits at the bottom with a subtle top border. The text input gets a pill shape (border-radius: 24px) and highlights blue when focused.

  • Add the final button styles at the bottom of static/style.css:
.chat-input button {
    padding: 12px 24px;
    background: #0078d4;
    color: white;
    border: none;
    border-radius: 24px;
    cursor: pointer;
    font-size: 0.9rem;
    font-weight: 500;
}

.chat-input button:hover {
    background: #106ebe;
}

.chat-input button:disabled {
    background: #ccc;
    cursor: not-allowed;
}

What does this CSS do?

The send button matches the blue theme and darkens on hover. When disabled (during a pending request), it grays out so the user knows to wait.

  • Save static/style.css.
  • Stop the server (Ctrl+C) and restart it:
python app.py

Before you refresh, what do you expect the chat page to look like now that all the CSS is in place?

  • Refresh http://localhost:5000 in your browser.
  • Send a message like "Tell me about the smartwatch" and watch the response appear in a styled chat bubble.

You should see a polished chat interface: a blue header reading "TechGear Support", your message in a blue bubble on the right, and the agent's grounded reply in a gray bubble on the left.

Styles not appearing?

  • Make sure style.css is inside the static folder, not templates or the project root.
  • Try a hard refresh (Ctrl+Shift+R or Cmd+Shift+R) to clear cached CSS.
  • Check your browser's developer console (F12 > Network tab) and confirm style.css loads with status 200.

help me fix my CSS not loading in Flask

✔️ Awesome, I've got everything!

Great. Double check you have saved all three files: app.py, templates/index.html, and static/style.css.

ⓧ I'd like to double check the full code

Scroll up and confirm each section is present in your files, in order:

  • app.py: imports and setup, then SYSTEM_PROMPT with conversation_histories and the index route, then the chat endpoint and main block.
  • templates/index.html: the HTML structure with chat-container, chat-messages, and chat-input divs, then the JavaScript with sendMessage() and appendMessage() inside the script tags.
  • static/style.css: reset and body, chat-container, header group, messages and bubbles, input area, and button states.

Your AI customer support agent now has a polished browser interface. Anyone can open the URL, ask a product question, and get an accurate, grounded response in a clean chat bubble.

Secret mission

Smart Suggested Questions

Real customer support chatbots show clickable follow-up suggestions after each response. Can you make your agent smart enough to predict what the customer wants to ask next?

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project uses pay-per-token pricing from your Azure for Students $100 credit, but the usage from this project costs only a few cents.

Cost warning

Your Azure OpenAI deployment charges per token whenever an API call is made. The project's usage is minimal (a few cents total), but leaving the resource active means any future API calls will continue to draw from your $100 student credit.

If you are not planning to build on this project further, delete the resource group to avoid any unexpected credit usage.

Resources you used:

  • Azure Resource Group containing your Azure OpenAI resource and gpt-4o-mini deployment.
  • Local techgear-support project folder with Python virtual environment and application files.
  • Azure CLI installed via WinGet.

Keep everything running

No action needed. Choose this if you are still actively building on this project or plan to extend it soon.

  • Your Azure OpenAI resource only costs money when API calls are made. No calls means no charges.
  • Monitor your remaining credit balance in the Azure Education Hub at https://portal.azure.com under **Education**.
  • Your local project files and Azure CLI remain on your machine.

Pause - I'll come back to this later

Shut down running processes to free up memory. Your Azure resources only charge per API call, so there is nothing to pause on the cloud side.

  • Stop the Flask development server if it is still running by pressing Ctrl+C in the terminal.
  • Deactivate your Python virtual environment by running:
  • Your Azure resources stay deployed but cost nothing while idle. You only pay when you make API calls.
deactivate

When you return, navigate back to your techgear-support folder and reactivate with venv\Scripts\activate.

Delete - I don't want to use this again

Remove all project resources and start fresh. This deletes your Azure OpenAI deployment, local files, and the Azure CLI.

  • Delete your Azure Resource Group (this removes the OpenAI resource and model deployment) by running:
az group delete --name [[RG_NAME="your-resource-group-name"]] --yes

What does this command do?

This single command deletes the resource group and every resource inside it, including your Azure OpenAI account and the gpt-4o-mini deployment. The --yes flag skips the confirmation prompt.

  • Delete your local project folder. Navigate to your Desktop and run:
cd ~/Desktop
Remove-Item -Recurse -Force techgear-support
  • Uninstall the Azure CLI. Open Settings on your computer.
  • Select Apps, then Installed apps (Windows 11) or Apps & Features (Windows 10).
  • Search for Azure CLI in the search bar. It appears as Microsoft CLI 2.0 for Azure.
  • Select it and click Uninstall.

Nice Work!

Nice Work!

You just built a fully functional AI customer support agent powered by Azure OpenAI, from deploying cloud infrastructure to serving real-time conversations in your browser.

You've learned how to:

  • Deploy a GPT-4o-mini model on Azure OpenAI using the Azure CLI, and experienced AI hallucination firsthand before fixing it with prompt engineering and knowledge injection.
  • Build a Python-based customer support agent with grounded knowledge and conversation memory that refuses to make up information outside its product catalog.
  • Create a polished Flask web chat interface with real-time AI responses, styled message bubbles, and enter-to-send functionality.
  • Secret Mission: Added smart contextual follow-up suggestions that appear as clickable chips after each agent response, enhancing the user experience.

Ready to quiz yourself?