Build an AI Lead Qualification Agent

Build an AI agent that qualifies leads through conversation using Claude's tool use.

Introduction

30 Second Summary

Every business needs to figure out which potential customers are worth following up with. Right now, most companies either hire someone to answer enquiries all day or let leads slip through the cracks.

In this project, you will build an AI agent that qualifies leads through natural conversation. It asks the right questions, checks service availability, provides pricing, and saves the lead's information for follow-up.

What You'll Build

A fully functional AI Lead Qualification Agent powered by Claude and Streamlit that autonomously decides when to call business tools during a conversation.

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

  • A professional chat interface built with Streamlit that displays real-time conversations.
  • Three custom business tools that Claude calls autonomously to check availability, look up pricing, and save leads.
  • A working implementation of the agentic loop pattern, the same pattern used in production AI automation systems.
  • Secret Mission: a live Lead Dashboard in the sidebar that tracks captured leads and their qualification status in real-time.

Are there any prerequisites?

You should be comfortable opening a terminal and running basic commands. No prior experience with AI APIs or Python web frameworks is required.

Set Up Your Environment and API Key

Before you can build your AI agent, you need a clean workspace and access to Claude's brain.

In this step, you'll create an isolated Python project, install the packages your agent needs, and connect to the Anthropic API so Claude can power your chatbot.

In this step, get ready to:
  • Create a project folder with a Python virtual environment.
  • Install the anthropic, streamlit, and python-dotenv packages.
  • Set up your Anthropic API key and store it securely in a .env file.
Create your project folder and virtual environment

A virtual environment gives your project its own isolated set of Python packages. This prevents conflicts between different projects on your machine.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Cursor and press Enter to open it.

Using VS Code instead?

All instructions in this project work identically in VS Code. Just search for Visual Studio Code instead of Cursor.

  • Open the built-in terminal by pressing Ctrl+` (macOS and Windows).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir lead-agent
cd lead-agent
  • Confirm the folder was created by running:
ls

You should see the lead-agent folder listed in your Desktop output.

  • Create a virtual environment inside your project by running this command:
python -m venv venv

What does this command do?

python -m venv venv tells Python to create a virtual environment in a folder called venv.

Any packages you install while this environment is active stay isolated to this project.

  • Activate the virtual environment by running the command for your operating system:

macOS

source venv/bin/activate

Windows

venv\Scripts\activate

You should see (venv) appear at the beginning of your terminal prompt.

Don't see (venv) in your prompt?

On Windows, you may need to allow script execution first. Run Set-ExecutionPolicy -Scope CurrentUser RemoteSigned in PowerShell, then try the activate command again.

On macOS, make sure you typed source before the path. Without it, the command won't activate the environment.

help me activate my Python virtual environment

Install the required packages

Your agent needs three packages: anthropic to talk to Claude, streamlit to build the chat interface, and python-dotenv to load your API key securely.

  • Install all three packages by running this command:
pip install anthropic streamlit python-dotenv

You should see output showing each package downloading and installing successfully, ending with Successfully installed followed by a list of package names.

Seeing a permission error or 'pip not found'?

Make sure your virtual environment is active. You should see (venv) at the start of your terminal prompt.

If you see pip: command not found, try pip3 instead, or re-run the activate command from the previous substep.

help me fix my pip install error

  • Create a requirements.txt file to record your dependencies. In Cursor, click File in the top menu bar.
  • Click New File.
  • Save the file immediately with Cmd+S (macOS) or Ctrl+S (Windows) and name it requirements.txt inside your lead-agent folder.
  • Paste the following into the file:
anthropic
streamlit
python-dotenv

What is requirements.txt?

This file lists every package your project depends on. Anyone who clones your project can run pip install -r requirements.txt to install everything in one go.

  • Save requirements.txt with Cmd+S (macOS) or Ctrl+S (Windows).
Create your Anthropic account and API key

Your agent needs an API key to authenticate with Claude.

The Anthropic API charges based on usage, with a minimum credit load of $5. For this project, your total spend will be well under $5 since lead qualification conversations are short.

  • Open your browser and go to console.anthropic.com.
  • Create an account or sign in if you already have one.
  • Navigate to Billing and add a minimum of $5 in API credits.
  • Navigate to API Keys.
  • Click Create Key.
  • Give your key a name like lead-agent and click Create Key.
  • Copy the key immediately. You won't be able to see it again after closing the dialog.

Keep your API key private

Never share your API key publicly or commit it to Git. The .env file you're about to create keeps it out of your source code.

  • Back in Cursor, create a new file and save it as .env inside your lead-agent folder.
  • Paste the following into the file, replacing your-api-key-here with the key you just copied:
ANTHROPIC_API_KEY=your-api-key-here
  • Save .env with Cmd+S (macOS) or Ctrl+S (Windows).

What is a .env file?

A .env file stores environment variables that your code can read at runtime.

The python-dotenv package loads these values so your API key never appears directly in your Python files.

  • Verify your setup by checking your project structure. In the terminal, run:
ls -a

You should see .env, requirements.txt, and the venv folder listed in the output.

Missing .env in the output?

Files starting with a dot are hidden by default. Make sure you used ls -a (with the -a flag) to show hidden files.

If the file still doesn't appear, double-check that you saved it inside the lead-agent folder and not one level up on the Desktop.

help me find my .env file

✔️ Awesome, I've got everything!

Great. Double check you've saved both files before moving on.

ⓧ I'd like to double check the full code

Here's what each file should contain:

ANTHROPIC_API_KEY=your-api-key-here
anthropic
streamlit
python-dotenv

Your environment is locked and loaded. Next up, you'll build the chat interface that your customers will talk to.

Build the Basic Chat Interface with Streamlit

Your environment is set up and ready to go. Now it's time to build something you can actually see.

In this step, you'll create the chat interface for your AI agent using Streamlit. Streamlit turns Python scripts into web apps with zero frontend code. You'll set up the page layout, a message history system, and the input box where customers type their questions.

In this step, get ready to:
  • Create the Streamlit app file with page configuration and title.
  • Set up session state to store chat history.
  • Add chat message containers and input to display and capture messages.
Create the Streamlit app file with page configuration and title

Every Streamlit app starts with a page configuration that sets the browser tab title, icon, and layout. This is what visitors see before the page content loads.

  • Create a new file called app.py inside your lead-qualification-agent folder by running this command:
touch app.py
  • Confirm the file exists by running:
ls

You should see app.py, .env, and requirements.txt in the output.

  • Open app.py in Cursor and add the following page configuration code:
import streamlit as st

# Page configuration
st.set_page_config(
    page_title="HomeServ AI Assistant",
    page_icon="🏠",
    layout="centered"
)

# Main title
st.title("🏠 HomeServ Pro Assistant")
st.caption("Hi! I'm your AI assistant. Tell me what home service you need and I'll help you get started.")

What does this code do?

The st.set_page_config() call must be the first Streamlit command in your script. It controls what appears in the browser tab and how the page is laid out.

The st.title() and st.caption() functions render the heading and subtitle that visitors see when the chat loads.

  • Save app.py.
  • Launch the app by running this command:
streamlit run app.py

Your browser should open automatically to localhost:8501. You should see a page titled "HomeServ Pro Assistant" with the caption below it.

Browser didn't open?

If your browser didn't open automatically, look in the terminal output for a URL like http://localhost:8501 and open it manually.

If you see ModuleNotFoundError: No module named 'streamlit', make sure your virtual environment is activated. You should see (venv) at the start of your terminal prompt.

Still stuck? Help me troubleshoot why streamlit run app.py isn't working.

Set up session state to store chat history

Streamlit reruns your entire script from top to bottom every time the user interacts with the page. Without a way to preserve data between reruns, every message would disappear instantly.

What is session state?

Streamlit's st.session_state is a dictionary that persists across reruns for a single user's browser tab. Think of it as the app's short-term memory.

You'll store all chat messages here so they stay visible as the conversation grows.

  • In app.py, add the following code below the st.caption() line:
# Initialize session state
if "messages" not in st.session_state:
    st.session_state.messages = []

# Display chat history
for message in st.session_state.messages:
    with st.chat_message(message["role"]):
        st.markdown(message["content"])

What does this code do?

The if "messages" not in st.session_state check creates an empty list only on the first run. On every subsequent rerun, the existing messages stay intact.

The for loop iterates over every stored message and renders it inside a styled chat bubble using st.chat_message(). The role field tells Streamlit whether to show a user icon or an assistant icon.

  • Save app.py.

Your browser should auto-refresh. The page still looks the same because the messages list is empty. The chat history system is working behind the scenes.

Add chat input to capture user messages

The final piece is an input box where customers can type messages. Streamlit provides a built-in chat input widget that pins to the bottom of the page.

  • In app.py, add this code below the chat history display loop:
# Handle user input
if prompt := st.chat_input("Type your message..."):
    st.session_state.messages.append({"role": "user", "content": prompt})
    with st.chat_message("user"):
        st.markdown(prompt)

What does this code do?

The := operator (called the walrus operator) assigns the user's input to prompt and checks if it's not empty, all in one line.

When the user submits a message, the code appends it to session state and immediately displays it in a chat bubble. On the next rerun, the display loop above will also render it from history.

  • Save app.py and check your browser.
  • Type a message like Hello into the input box at the bottom and press Enter.

You should see your message appear as a chat bubble with a user icon. The message persists if you type more messages.

Messages disappearing or not showing?

Make sure the session state initialization (the if "messages" not in st.session_state block) comes before both the display loop and the input handler.

Check that you're appending to st.session_state.messages (not a plain variable called messages).

Need more help? Help me figure out why my chat messages aren't displaying correctly.

✔️ Awesome, I've got everything!

Great. Double check that you've saved app.py and your chat interface is showing messages correctly.

ⓧ I'd like to double check the full code

Here's the complete app.py file at this point:

import streamlit as st

# Page configuration
st.set_page_config(
    page_title="HomeServ AI Assistant",
    page_icon="🏠",
    layout="centered"
)

# Main title
st.title("🏠 HomeServ Pro Assistant")
st.caption("Hi! I'm your AI assistant. Tell me what home service you need and I'll help you get started.")

# Initialize session state
if "messages" not in st.session_state:
    st.session_state.messages = []

# Display chat history
for message in st.session_state.messages:
    with st.chat_message(message["role"]):
        st.markdown(message["content"])

# Handle user input
if prompt := st.chat_input("Type your message..."):
    st.session_state.messages.append({"role": "user", "content": prompt})
    with st.chat_message("user"):
        st.markdown(prompt)

Your chat interface is live and capturing messages. Next up, you'll connect Claude to this interface so the AI agent can actually respond to customers.

Connect Claude and Get Your First Response

Your chat interface looks great, but it doesn't actually respond to anyone yet. The messages go nowhere because there's no AI brain behind the scenes.

In this step, you'll connect your app to Claude using the Anthropic Python SDK. By the end, you'll type a message and get a real AI response back in your chat window.

In this step, get ready to:
  • Import the Anthropic client and load your API key from the .env file.
  • Write a function that sends messages to Claude and returns the response.
  • Wire the chat input so Claude's replies appear in the conversation.
Import the Anthropic client and load your API key

The anthropic package gives your Python code a direct line to Claude's brain. The python-dotenv package loads your secret API key from the .env file so you never hardcode credentials.

  • In app.py, find the line that says import streamlit as st at the top of the file.
  • Replace that single import line with the following block:
import streamlit as st
import anthropic
from dotenv import load_dotenv
import os

# Load environment variables
load_dotenv()

What does this code do?

  • anthropic is the official Python SDK for talking to Claude's API.
  • load_dotenv() reads your .env file and makes its values available through os.getenv().
  • os is a built-in Python module that lets you access environment variables.

Now you need to create the client object that actually communicates with Claude's servers.

  • Scroll down past the st.caption(...) line.
  • Add the following code directly below it (before the # Initialize session state comment):
# Initialize Anthropic client
client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))

What does this code do?

This creates an Anthropic client object authenticated with your API key. Every time your app needs to talk to Claude, it uses this client object to send the request.

  • Save app.py.

Seeing a red underline on the imports?

Make sure your virtual environment is activated. If Cursor shows import errors, check that you ran pip install anthropic streamlit python-dotenv inside your virtual environment in the previous step.

Why is my Anthropic import showing an error?

Write the function that sends messages to Claude

Claude needs a system prompt to know how to behave. This is like a job description you give the AI before it starts talking to customers.

  • In app.py, find the line # Initialize Anthropic client that you just added.
  • Add the following system prompt directly above that line:
# System prompt for the agent
SYSTEM_PROMPT = """You are a friendly and professional AI assistant for HomeServ Pro, a home services company. Your job is to qualify leads by:

1. Greeting the customer warmly
2. Finding out what service they need (plumbing, electrical, hvac, or roofing)
3. Getting their zip code to check availability
4. Providing pricing information when asked
5. Collecting their name and phone number to save the lead

Guidelines:
- Be conversational and helpful, not robotic
- Ask one question at a time to avoid overwhelming the customer
- Keep responses concise (2-3 sentences max)"""

What does this code do?

The system prompt tells Claude its role, goals, and behavioral rules. Claude reads this before every conversation but the user never sees it.

This prompt instructs Claude to act as a lead qualifier. It asks one question at a time, keeps answers short, and works toward collecting the customer's information.

Now you'll write the function that actually sends messages to Claude and gets a response back.

  • Scroll down past the chat history display loop (the for message in st.session_state.messages: block).
  • Add the following function between the chat history loop and the # Handle user input section:
def get_agent_response(messages):
    """Send messages to Claude and return the text response."""
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        system=SYSTEM_PROMPT,
        messages=messages
    )
    final_text = ""
    for block in response.content:
        if hasattr(block, "text"):
            final_text += block.text
    return final_text

What does this code do?

  • client.messages.create() sends your conversation to Claude's API and waits for a response.
  • model="claude-sonnet-4-6" specifies which Claude model to use. Sonnet is the smart-tier model with reliable tool use capabilities.
  • max_tokens=1024 caps the response length so Claude keeps answers concise.
  • The for block in response.content loop extracts the text from Claude's response. Claude returns content as a list of blocks, so you iterate through them to collect all the text.
  • Save app.py.

Not sure where to place the function?

The function must go after the chat history display loop and before the if prompt := st.chat_input line. Python needs to see the function definition before it gets called.

Help me figure out where get_agent_response goes in my file.

Wire the chat input to display Claude's response

Right now, your chat input saves the user's message and displays it. You need to also call your new function and show Claude's reply.

  • Find the existing # Handle user input section at the bottom of app.py.
  • Replace the entire section (from if prompt := st.chat_input to the end of the file) with the following code:
# Handle user input
if prompt := st.chat_input("Type your message..."):
    # Add user message to history and display it
    st.session_state.messages.append({"role": "user", "content": prompt})
    with st.chat_message("user"):
        st.markdown(prompt)

    # Get agent response
    with st.chat_message("assistant"):
        with st.spinner("Thinking..."):
            # Build messages for API (text-only for display history)
            api_messages = [
                {"role": m["role"], "content": m["content"]}
                for m in st.session_state.messages
            ]
            response_text = get_agent_response(api_messages)
            st.markdown(response_text)

    # Add assistant response to history
    st.session_state.messages.append({"role": "assistant", "content": response_text})

What does this code do?

  • st.spinner("Thinking...") shows a loading indicator while Claude processes the request.
  • api_messages builds a clean list of messages from your session state to send to the API.
  • The response is displayed inside a st.chat_message("assistant") container so it appears as a chat bubble from the AI.
  • The response is also saved to st.session_state.messages so Claude remembers the full conversation on the next turn.
  • Save app.py.
  • In your terminal, run your app by running:
streamlit run app.py
  • Once the browser opens, type Hi, I need help with plumbing in the chat input and press Enter.
  • You should see a "Thinking..." spinner appear, followed by Claude's response greeting you and asking a follow-up question about your location or service needs.

Seeing an AuthenticationError?

Double check that your .env file contains your actual API key (not the placeholder text your-api-key-here). Also make sure the file is in the same folder as app.py.

If you see a "credit balance is too low" error, add credits at console.anthropic.com (minimum $5 load).

Why is my Anthropic API key not working?

✔️ Awesome, I've got everything!

Great. Double check you've saved app.py and your app is responding to messages.

ⓧ I'd like to double check the full code

Here's the complete app.py at this point. Compare it with yours to find any differences.

import streamlit as st
import anthropic
from dotenv import load_dotenv
import os

# Load environment variables
load_dotenv()

# Page configuration
st.set_page_config(
    page_title="HomeServ AI Assistant",
    page_icon="\U0001f3e0",
    layout="centered"
)

# Main title
st.title("\U0001f3e0 HomeServ Pro Assistant")
st.caption("Hi! I'm your AI assistant. Tell me what home service you need and I'll help you get started.")

# System prompt for the agent
SYSTEM_PROMPT = """You are a friendly and professional AI assistant for HomeServ Pro, a home services company. Your job is to qualify leads by:

1. Greeting the customer warmly
2. Finding out what service they need (plumbing, electrical, hvac, or roofing)
3. Getting their zip code to check availability
4. Providing pricing information when asked
5. Collecting their name and phone number to save the lead

Guidelines:
- Be conversational and helpful, not robotic
- Ask one question at a time to avoid overwhelming the customer
- Keep responses concise (2-3 sentences max)"""

# Initialize Anthropic client
client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))

# Initialize session state
if "messages" not in st.session_state:
    st.session_state.messages = []

# Display chat history
for message in st.session_state.messages:
    with st.chat_message(message["role"]):
        st.markdown(message["content"])


def get_agent_response(messages):
    """Send messages to Claude and return the text response."""
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        system=SYSTEM_PROMPT,
        messages=messages
    )
    final_text = ""
    for block in response.content:
        if hasattr(block, "text"):
            final_text += block.text
    return final_text


# Handle user input
if prompt := st.chat_input("Type your message..."):
    # Add user message to history and display it
    st.session_state.messages.append({"role": "user", "content": prompt})
    with st.chat_message("user"):
        st.markdown(prompt)

    # Get agent response
    with st.chat_message("assistant"):
        with st.spinner("Thinking..."):
            # Build messages for API (text-only for display history)
            api_messages = [
                {"role": m["role"], "content": m["content"]}
                for m in st.session_state.messages
            ]
            response_text = get_agent_response(api_messages)
            st.markdown(response_text)

    # Add assistant response to history
    st.session_state.messages.append({"role": "assistant", "content": response_text})

Your chatbot is alive. Claude responds to messages and remembers the conversation. Next up, you'll give Claude real business tools so it can check service availability, look up pricing, and save leads autonomously.

Define Your Business Tools

Your chatbot can hold a conversation, but right now it has no way to actually do anything. It cannot check if a service is available, look up pricing, or save a customer's information.

In this step, you will create the tools that give your agent real capabilities. Tool use (also called function calling) is the pattern where Claude decides which tool to call, your code executes it, and Claude uses the result to continue the conversation.

In this step, get ready to:
  • Create three tool definitions with name, description, and input_schema following the Claude tool use format.
  • Implement Python functions that simulate business logic for checking availability, looking up pricing, and saving leads.
  • Register the tools in a dispatcher that connects Claude's requests to your functions.
Create the tools file with simulated data

Your tools need data to work with. In a production app, this data would come from a database or external API. For this project, you will simulate it with Python dictionaries that map zip codes to available services and service types to pricing.

  • Create a new file called tools.py in your lead-qualifier project folder (the same folder as app.py).
  • Add the following code to set up your imports and business data:
import json

# Simulated business data
SERVICE_AREAS = {
    "10001": ["plumbing", "electrical", "hvac"],
    "10002": ["plumbing", "electrical"],
    "90210": ["plumbing", "electrical", "hvac", "roofing"],
    "60601": ["electrical", "hvac"],
    "30301": ["plumbing", "hvac", "roofing"],
}

SERVICE_PRICING = {
    "plumbing": {"base": 85, "range": "$85 - $300 per visit"},
    "electrical": {"base": 95, "range": "$95 - $400 per visit"},
    "hvac": {"base": 120, "range": "$120 - $500 per visit"},
    "roofing": {"base": 200, "range": "$200 - $1000+ per project"},
}

What does this code do?

SERVICE_AREAS maps zip codes to a list of services available in that area. When a customer gives their zip code, your agent will look it up here.

SERVICE_PRICING maps each service type to its base price and display range. This gives the agent real numbers to share with customers.

  • Save tools.py.
  • Confirm the file exists by running:
ls

You should see tools.py listed alongside app.py and requirements.txt.

Add the business logic functions

Each tool needs a Python function that does the actual work. These functions receive input from Claude, perform a lookup, and return a JSON string that Claude can read and use in its response.

  • In tools.py, add the check_service_availability function below the SERVICE_PRICING dictionary:
def check_service_availability(zip_code: str, service_type: str) -> str:
    available_services = SERVICE_AREAS.get(zip_code)
    if available_services is None:
        return json.dumps({
            "available": False,
            "message": f"We don't currently service zip code {zip_code}. We cover: {', '.join(SERVICE_AREAS.keys())}."
        })
    if service_type.lower() in available_services:
        return json.dumps({
            "available": True,
            "message": f"Great news! We offer {service_type} services in your area ({zip_code})."
        })
    return json.dumps({
        "available": False,
        "message": f"We service {zip_code} but don't offer {service_type} there. Available services: {', '.join(available_services)}."
    })

What does this function do?

This function takes a zip code and service type, then checks against the SERVICE_AREAS dictionary. It handles three cases: the zip code is not in the system at all, the zip code is covered but that specific service is not available, or the service is available.

Every result is returned as a JSON string so Claude can parse it. The message field gives Claude natural language it can relay to the customer.

  • Save tools.py.
  • Test the function by running:
python -c "from tools import check_service_availability; print(check_service_availability('10001', 'plumbing'))"

You should see a JSON string with "available": true and a success message.

Seeing a ModuleNotFoundError?

Make sure you are running the command from the same folder where tools.py lives. Also confirm your virtual environment is activated (you should see (venv) in your terminal prompt).

help me debug a ModuleNotFoundError when importing tools.py

  • Add the get_service_pricing function below check_service_availability:
def get_service_pricing(service_type: str) -> str:
    pricing = SERVICE_PRICING.get(service_type.lower())
    if pricing is None:
        return json.dumps({
            "found": False,
            "message": f"We don't offer {service_type}. Available services: {', '.join(SERVICE_PRICING.keys())}."
        })
    return json.dumps({
        "found": True,
        "service": service_type,
        "price_range": pricing["range"],
        "base_price": pricing["base"],
        "message": f"{service_type.title()} services start at ${pricing['base']}. Typical range: {pricing['range']}."
    })

What does this function do?

This function looks up pricing for a given service type. It returns the base price, the typical range, and a human-readable message. If the service does not exist, it lists available options.

  • Save tools.py.
  • Add the save_lead function below get_service_pricing:
def save_lead(name: str, phone: str, service_needed: str, zip_code: str, notes: str) -> str:
    lead_data = {
        "name": name,
        "phone": phone,
        "service_needed": service_needed,
        "zip_code": zip_code,
        "notes": notes,
        "status": "new"
    }
    return json.dumps({
        "success": True,
        "message": f"Lead saved successfully for {name}. Our team will reach out within 24 hours.",
        "lead": lead_data
    })

What does this function do?

This function packages the customer's contact info and service request into a structured lead record. In a production system, this would write to a database. For now it returns the data as JSON so Claude can confirm the save to the customer.

  • Save tools.py.
  • Test all three functions together by running:
python -c "from tools import check_service_availability, get_service_pricing, save_lead; print(get_service_pricing('hvac')); print(save_lead('Jane', '555-1234', 'hvac', '60601', 'urgent'))"

You should see two JSON outputs. One showing HVAC pricing and one confirming the lead was saved for Jane.

Seeing a SyntaxError?

Check that each function has matching parentheses and braces. The f-strings inside json.dumps() use curly braces for variables, which can conflict with dictionary syntax if misplaced.

help me fix a SyntaxError in my tools.py functions

Define the tool schemas and register the dispatcher

Claude needs to know what tools are available and what inputs they expect. You define this using a specific JSON Schema format that describes each tool's name, description, and parameters.

Why does Claude need tool definitions?

Claude cannot see your Python code. The tool definitions act like a menu that tells Claude what actions it can take, what each action does, and what information it needs to provide. Claude reads the descriptions to decide which tool fits the customer's request.

  • In tools.py, add the TOOL_DEFINITIONS list below the save_lead function:
# Tool definitions for Claude API
TOOL_DEFINITIONS = [
    {
        "name": "check_service_availability",
        "description": "Check if a specific service is available in the customer's area based on their zip code. Use this when the customer mentions their location and the service they need.",
        "input_schema": {
            "type": "object",
            "properties": {
                "zip_code": {
                    "type": "string",
                    "description": "The customer's 5-digit zip code"
                },
                "service_type": {
                    "type": "string",
                    "description": "The type of service needed (plumbing, electrical, hvac, roofing)"
                }
            },
            "required": ["zip_code", "service_type"]
        }
    },
    {
        "name": "get_service_pricing",
        "description": "Look up pricing information for a specific service type. Use this when the customer asks about cost or pricing.",
        "input_schema": {
            "type": "object",
            "properties": {
                "service_type": {
                    "type": "string",
                    "description": "The type of service to get pricing for (plumbing, electrical, hvac, roofing)"
                }
            },
            "required": ["service_type"]
        }
    },
    {
        "name": "save_lead",
        "description": "Save the qualified lead's contact information and service request. Use this once you have gathered the customer's name, phone number, service needed, and zip code.",
        "input_schema": {
            "type": "object",
            "properties": {
                "name": {
                    "type": "string",
                    "description": "The customer's full name"
                },
                "phone": {
                    "type": "string",
                    "description": "The customer's phone number"
                },
                "service_needed": {
                    "type": "string",
                    "description": "The service the customer needs"
                },
                "zip_code": {
                    "type": "string",
                    "description": "The customer's zip code"
                },
                "notes": {
                    "type": "string",
                    "description": "Any additional notes about the customer's request"
                }
            },
            "required": ["name", "phone", "service_needed", "zip_code", "notes"]
        }
    }
]

What's happening here?

Each tool definition has three parts. The name must match your Python function name exactly. The description tells Claude when to use the tool. The input_schema uses JSON Schema to define what parameters Claude needs to provide.

Claude reads these descriptions to decide which tool to call. Good descriptions are the difference between an agent that works reliably and one that calls the wrong tool.

  • Save tools.py.
  • Now add the execute_tool dispatcher function below TOOL_DEFINITIONS:
# Tool execution dispatcher
def execute_tool(tool_name: str, tool_input: dict) -> str:
    if tool_name == "check_service_availability":
        return check_service_availability(**tool_input)
    elif tool_name == "get_service_pricing":
        return get_service_pricing(**tool_input)
    elif tool_name == "save_lead":
        return save_lead(**tool_input)
    else:
        return json.dumps({"error": f"Unknown tool: {tool_name}"})

What does the dispatcher do?

When Claude asks to use a tool, it returns the tool name and inputs as data. The dispatcher routes that request to the correct Python function. The **tool_input syntax unpacks the dictionary into keyword arguments, so {"zip_code": "10001", "service_type": "plumbing"} becomes check_service_availability(zip_code="10001", service_type="plumbing").

  • Save tools.py.
  • Test the full dispatcher by running:
python -c "from tools import execute_tool; print(execute_tool('check_service_availability', {'zip_code': '90210', 'service_type': 'roofing'}))"

You should see a JSON string with "available": true and a message confirming roofing is available in 90210.

Seeing a KeyError or TypeError?

A KeyError usually means a mismatch between the keys in your test dictionary and the function's parameter names. Check that the keys match exactly (zip_code and service_type).

help me fix a KeyError or TypeError in my execute_tool dispatcher

✔️ Awesome, I've got everything!

Great. Double check you have saved tools.py and confirmed all three functions work through the dispatcher.

ⓧ I'd like to double check the full code

Here is the complete tools.py file. Compare yours against this:

import json

# Simulated business data
SERVICE_AREAS = {
    "10001": ["plumbing", "electrical", "hvac"],
    "10002": ["plumbing", "electrical"],
    "90210": ["plumbing", "electrical", "hvac", "roofing"],
    "60601": ["electrical", "hvac"],
    "30301": ["plumbing", "hvac", "roofing"],
}

SERVICE_PRICING = {
    "plumbing": {"base": 85, "range": "$85 - $300 per visit"},
    "electrical": {"base": 95, "range": "$95 - $400 per visit"},
    "hvac": {"base": 120, "range": "$120 - $500 per visit"},
    "roofing": {"base": 200, "range": "$200 - $1000+ per project"},
}


def check_service_availability(zip_code: str, service_type: str) -> str:
    available_services = SERVICE_AREAS.get(zip_code)
    if available_services is None:
        return json.dumps({
            "available": False,
            "message": f"We don't currently service zip code {zip_code}. We cover: {', '.join(SERVICE_AREAS.keys())}."
        })
    if service_type.lower() in available_services:
        return json.dumps({
            "available": True,
            "message": f"Great news! We offer {service_type} services in your area ({zip_code})."
        })
    return json.dumps({
        "available": False,
        "message": f"We service {zip_code} but don't offer {service_type} there. Available services: {', '.join(available_services)}."
    })


def get_service_pricing(service_type: str) -> str:
    pricing = SERVICE_PRICING.get(service_type.lower())
    if pricing is None:
        return json.dumps({
            "found": False,
            "message": f"We don't offer {service_type}. Available services: {', '.join(SERVICE_PRICING.keys())}."
        })
    return json.dumps({
        "found": True,
        "service": service_type,
        "price_range": pricing["range"],
        "base_price": pricing["base"],
        "message": f"{service_type.title()} services start at ${pricing['base']}. Typical range: {pricing['range']}."
    })


def save_lead(name: str, phone: str, service_needed: str, zip_code: str, notes: str) -> str:
    lead_data = {
        "name": name,
        "phone": phone,
        "service_needed": service_needed,
        "zip_code": zip_code,
        "notes": notes,
        "status": "new"
    }
    return json.dumps({
        "success": True,
        "message": f"Lead saved successfully for {name}. Our team will reach out within 24 hours.",
        "lead": lead_data
    })


# Tool definitions for Claude API
TOOL_DEFINITIONS = [
    {
        "name": "check_service_availability",
        "description": "Check if a specific service is available in the customer's area based on their zip code. Use this when the customer mentions their location and the service they need.",
        "input_schema": {
            "type": "object",
            "properties": {
                "zip_code": {
                    "type": "string",
                    "description": "The customer's 5-digit zip code"
                },
                "service_type": {
                    "type": "string",
                    "description": "The type of service needed (plumbing, electrical, hvac, roofing)"
                }
            },
            "required": ["zip_code", "service_type"]
        }
    },
    {
        "name": "get_service_pricing",
        "description": "Look up pricing information for a specific service type. Use this when the customer asks about cost or pricing.",
        "input_schema": {
            "type": "object",
            "properties": {
                "service_type": {
                    "type": "string",
                    "description": "The type of service to get pricing for (plumbing, electrical, hvac, roofing)"
                }
            },
            "required": ["service_type"]
        }
    },
    {
        "name": "save_lead",
        "description": "Save the qualified lead's contact information and service request. Use this once you have gathered the customer's name, phone number, service needed, and zip code.",
        "input_schema": {
            "type": "object",
            "properties": {
                "name": {
                    "type": "string",
                    "description": "The customer's full name"
                },
                "phone": {
                    "type": "string",
                    "description": "The customer's phone number"
                },
                "service_needed": {
                    "type": "string",
                    "description": "The service the customer needs"
                },
                "zip_code": {
                    "type": "string",
                    "description": "The customer's zip code"
                },
                "notes": {
                    "type": "string",
                    "description": "Any additional notes about the customer's request"
                }
            },
            "required": ["name", "phone", "service_needed", "zip_code", "notes"]
        }
    }
]


# Tool execution dispatcher
def execute_tool(tool_name: str, tool_input: dict) -> str:
    if tool_name == "check_service_availability":
        return check_service_availability(**tool_input)
    elif tool_name == "get_service_pricing":
        return get_service_pricing(**tool_input)
    elif tool_name == "save_lead":
        return save_lead(**tool_input)
    else:
        return json.dumps({"error": f"Unknown tool: {tool_name}"})

Your tools are defined, tested, and ready to go. Next up, you will wire these tools into the agentic loop so Claude can autonomously decide when to call them during a conversation.

Implement the Agentic Tool Use Loop

Your chatbot can talk, but it can't do anything yet. When a customer mentions their zip code and service, Claude has no way to check availability or look up pricing.

The agentic loop fixes this. You send Claude a message along with your tool definitions. Claude decides whether to call a tool, your code executes it, and the result goes back to Claude so it can continue the conversation intelligently.

In this step, get ready to:
  • Connect your tool definitions to the Claude API call.
  • Build the while loop that lets Claude call tools repeatedly until it has a final answer.
  • Test the full agent flow with a real conversation.
Import your tools and update the system prompt

Claude needs to know your tools exist before it can use them. You also need to tell Claude when to use each tool through your system prompt.

  • In app.py, find the import section at the top of the file.
  • Find this line:
import os
  • Add this new import line directly below it:
from tools import TOOL_DEFINITIONS, execute_tool

What does this import do?

This brings in the tool definitions list (so Claude knows what tools are available) and the execute_tool dispatcher (so your code can run whichever tool Claude asks for).

Next, you need to update the system prompt so Claude knows when to use each tool.

  • In app.py, scroll down to the SYSTEM_PROMPT variable.
  • Find these two lines in the Guidelines section:
- Ask one question at a time to avoid overwhelming the customer
- Keep responses concise (2-3 sentences max)
  • Replace those two lines with the following expanded guidelines:
- Ask one question at a time to avoid overwhelming the customer
- Use the check_service_availability tool when you know their zip code and service
- Use the get_service_pricing tool when they ask about cost
- Use the save_lead tool once you have their name, phone, service needed, and zip code
- If a service is not available in their area, suggest alternatives
- Keep responses concise (2-3 sentences max)

Why add tool instructions to the system prompt?

Claude can see your tool definitions, but the system prompt tells it when to use each one. Without these guidelines, Claude might call tools at the wrong time or skip them entirely.

Build the agentic while loop

The tool use pattern works like a back-and-forth relay. Claude returns stop_reason: "tool_use" when it wants to call a tool. Your code executes the tool and sends the result back. This loop repeats until Claude has all the information it needs to respond.

  • In app.py, find the existing get_agent_response function.
  • Replace the entire function (from def get_agent_response(messages): through return final_text) with the following code:
def get_agent_response(messages):
    """Send messages to Claude with tools and handle the agentic loop."""
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        system=SYSTEM_PROMPT,
        tools=TOOL_DEFINITIONS,
        messages=messages
    )

    # Agentic loop: keep going while Claude wants to use tools
    while response.stop_reason == "tool_use":
        tool_results = []
        assistant_content = response.content

        for block in response.content:
            if block.type == "tool_use":
                tool_result = execute_tool(block.name, block.input)
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": tool_result
                })

        messages.append({"role": "assistant", "content": assistant_content})
        messages.append({"role": "user", "content": tool_results})

        response = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=1024,
            system=SYSTEM_PROMPT,
            tools=TOOL_DEFINITIONS,
            messages=messages
        )

    final_text = ""
    for block in response.content:
        if hasattr(block, "text"):
            final_text += block.text

    return final_text

What does this code do?

This function implements the complete agentic loop pattern:

  • tools=TOOL_DEFINITIONS passes your three tool schemas to Claude so it knows what actions are available.
  • The while response.stop_reason == "tool_use" loop keeps running as long as Claude wants to call tools.
  • Inside the loop, your code finds every tool_use block, executes the matching function, and builds a tool_result response with the matching tool_use_id.
  • The results get appended to the conversation history, and Claude is called again with the full context.
  • Once Claude stops requesting tools (stop_reason becomes "end_turn"), the loop exits and you extract the final text response.

Why is this called an agentic loop?

A regular chatbot just generates text. An agent takes actions in the real world. The loop gives Claude autonomy to decide which tools to call and in what order, without you hard-coding the sequence.

This same pattern powers production AI systems in business automation, customer support, and data processing.

  • Save app.py.

Seeing a syntax error?

Make sure you replaced the entire old function, including the def line and the return statement. Check that the indentation is consistent (4 spaces per level).

Help me debug my get_agent_response function

Test the full agentic flow

Time to see your agent autonomously decide to call tools. You will have a conversation that triggers tool use and watch Claude handle it without any manual intervention.

  • Run your app by entering this command in your terminal:
streamlit run app.py
  • In the chat input, type a message like:
Hi, I need help with plumbing in zip code 10001
  • Press Enter to send the message.

You should see a brief "Thinking..." spinner, then Claude responds with information about plumbing availability in that area. Behind the scenes, Claude called your check_service_availability tool and used the result to craft its response.

  • Try asking about pricing by typing:
How much does plumbing cost?

Claude should respond with pricing details from the get_service_pricing tool. It autonomously decided which tool to use based on your question.

What should I see?

Claude should mention that plumbing services are available in zip code 10001 and provide a price range starting at $85. The exact wording varies each time because Claude generates natural language responses.

Getting an API error or no response?

Check that your .env file has a valid API key and that you have credit loaded at console.anthropic.com. Also confirm your from tools import TOOL_DEFINITIONS, execute_tool line is at the top of app.py.

Help me fix my agentic loop API call

✔️ Awesome, I've got everything!

Great. Double check you have saved app.py with all your changes.

ⓧ I'd like to double check the full code

Here is the full app.py file as it should look at the end of this step:

import streamlit as st
import anthropic
from dotenv import load_dotenv
import os
from tools import TOOL_DEFINITIONS, execute_tool

# Load environment variables
load_dotenv()

# Page configuration
st.set_page_config(
    page_title="HomeServ AI Assistant",
    page_icon="🏠",
    layout="centered"
)

# Main title
st.title("🏠 HomeServ Pro Assistant")
st.caption("Hi! I'm your AI assistant. Tell me what home service you need and I'll help you get started.")

# System prompt for the agent
SYSTEM_PROMPT = """You are a friendly and professional AI assistant for HomeServ Pro, a home services company. Your job is to qualify leads by:

1. Greeting the customer warmly
2. Finding out what service they need (plumbing, electrical, hvac, or roofing)
3. Getting their zip code to check availability
4. Providing pricing information when asked
5. Collecting their name and phone number to save the lead

Guidelines:
- Be conversational and helpful, not robotic
- Ask one question at a time to avoid overwhelming the customer
- Use the check_service_availability tool when you know their zip code and service
- Use the get_service_pricing tool when they ask about cost
- Use the save_lead tool once you have their name, phone, service needed, and zip code
- If a service is not available in their area, suggest alternatives
- Keep responses concise (2-3 sentences max)"""

# Initialize Anthropic client
client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))

# Initialize session state
if "messages" not in st.session_state:
    st.session_state.messages = []

# Display chat history
for message in st.session_state.messages:
    with st.chat_message(message["role"]):
        st.markdown(message["content"])


def get_agent_response(messages):
    """Send messages to Claude with tools and handle the agentic loop."""
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        system=SYSTEM_PROMPT,
        tools=TOOL_DEFINITIONS,
        messages=messages
    )

    # Agentic loop: keep going while Claude wants to use tools
    while response.stop_reason == "tool_use":
        tool_results = []
        assistant_content = response.content

        for block in response.content:
            if block.type == "tool_use":
                tool_result = execute_tool(block.name, block.input)
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": tool_result
                })

        messages.append({"role": "assistant", "content": assistant_content})
        messages.append({"role": "user", "content": tool_results})

        response = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=1024,
            system=SYSTEM_PROMPT,
            tools=TOOL_DEFINITIONS,
            messages=messages
        )

    final_text = ""
    for block in response.content:
        if hasattr(block, "text"):
            final_text += block.text

    return final_text


# Handle user input
if prompt := st.chat_input("Type your message..."):
    st.session_state.messages.append({"role": "user", "content": prompt})
    with st.chat_message("user"):
        st.markdown(prompt)

    with st.chat_message("assistant"):
        with st.spinner("Thinking..."):
            api_messages = [
                {"role": m["role"], "content": m["content"]}
                for m in st.session_state.messages
            ]
            response_text = get_agent_response(api_messages)
            st.markdown(response_text)

    st.session_state.messages.append({"role": "assistant", "content": response_text})

Your agent now makes autonomous decisions about which tools to call and when. Next up, you will add a polished sidebar and smooth out the user experience.

Add a Sidebar and Polish the Experience

Your agent can now hold a full conversation, call tools autonomously, and qualify leads. The core logic is complete.

In this final step, you will add professional branding to your app with a sidebar and then run a full end-to-end test of the lead qualification flow.

In this step, get ready to:
  • Add a branded sidebar with your company name and agent description.
  • Test the complete lead qualification flow from greeting to saved lead.
Add the sidebar branding

Streamlit has a built-in sidebar component that stays visible alongside the chat. You will use it to show the company name and a short description of what the agent does.

  • In app.py, find the import section at the top of the file. Find this line:
import anthropic
from dotenv import load_dotenv
  • Add import json between those two lines, so the section now looks like this:
import anthropic
import json
from dotenv import load_dotenv
  • Now find the comment # Main title (just below the page configuration block). Add the following sidebar code directly above that comment:
# Sidebar with branding
st.sidebar.title("🏠 HomeServ Pro")
st.sidebar.markdown("**AI-Powered Lead Qualification**")
st.sidebar.markdown("---")
st.sidebar.markdown(
    "This agent qualifies leads by checking service availability, "
    "providing pricing, and collecting contact information."
)

What does this code do?

st.sidebar.title() creates a bold heading in Streamlit's collapsible sidebar panel.

The st.sidebar.markdown("---") call renders a horizontal divider line to separate the title from the description. The final markdown() call adds a plain-text summary of the agent's capabilities.

  • Save app.py.
  • If your Streamlit app is still running, it will detect the change and offer to rerun. Click Rerun in the browser. If it is not running, start it by running this command in your terminal:
streamlit run app.py
  • You should see a sidebar on the left with the heading "HomeServ Pro", a bold subtitle, a horizontal line, and a description paragraph.

Don't see the sidebar?

On narrow browser windows, Streamlit collapses the sidebar by default. Look for a small arrow icon in the top-left corner and click it to expand the panel.

If the app shows an error, make sure the sidebar code is placed after the st.set_page_config() block and before the st.title() line.

Why isn't my Streamlit sidebar showing up?

Test the complete lead qualification flow

Your agent is now fully assembled. Time to run a complete conversation to see the agentic loop in action. You will play the role of a potential customer and watch Claude autonomously decide which tools to call.

  • In the chat input at the bottom of the page, type a greeting like:
Hi, I need help with my plumbing
  • Press Enter to send the message. The agent will respond and ask for your zip code.
  • Reply with a zip code from the simulated data. Try 10001 (which covers plumbing) or 60601 (which does not cover plumbing).
  • Continue the conversation by providing your name and phone number when asked.

What should I see?

Each time you reply, you will see a "Thinking..." spinner while Claude processes your message. Behind the scenes, Claude may call one or more tools (like check_service_availability or save_lead) before responding to you.

By the end of the conversation, the agent should confirm it has saved your lead with a message like "Our team will reach out within 24 hours."

  • Try asking about pricing to trigger the get_service_pricing tool. Type something like "How much does plumbing cost?" at any point in the conversation.

Seeing an API error?

If you see a 401 Unauthorized error, your API key may be invalid or expired. Check your .env file and make sure the key is pasted correctly with no extra spaces.

If you see a credit balance too low error, add more credit at console.anthropic.com. The minimum top-up is $5.

My AI agent is returning an API error instead of a response. Help me fix it.

✔️ Awesome, I've got everything!

Great. Double check you have saved app.py and your agent is responding correctly in the browser.

ⓧ I'd like to double check the full code

Here is the complete app.py file. Compare it against yours to spot any differences.

import streamlit as st
import anthropic
import json
from dotenv import load_dotenv
import os
from tools import TOOL_DEFINITIONS, execute_tool

# Load environment variables
load_dotenv()

# Page configuration
st.set_page_config(
    page_title="HomeServ AI Assistant",
    page_icon="🏠",
    layout="centered"
)

# Sidebar with branding
st.sidebar.title("🏠 HomeServ Pro")
st.sidebar.markdown("**AI-Powered Lead Qualification**")
st.sidebar.markdown("---")
st.sidebar.markdown(
    "This agent qualifies leads by checking service availability, "
    "providing pricing, and collecting contact information."
)

# Main title
st.title("🏠 HomeServ Pro Assistant")
st.caption("Hi! I'm your AI assistant. Tell me what home service you need and I'll help you get started.")

# System prompt for the agent
SYSTEM_PROMPT = """You are a friendly and professional AI assistant for HomeServ Pro, a home services company. Your job is to qualify leads by:

1. Greeting the customer warmly
2. Finding out what service they need (plumbing, electrical, hvac, or roofing)
3. Getting their zip code to check availability
4. Providing pricing information when asked
5. Collecting their name and phone number to save the lead

Guidelines:
- Be conversational and helpful, not robotic
- Ask one question at a time to avoid overwhelming the customer
- Use the check_service_availability tool when you know their zip code and service
- Use the get_service_pricing tool when they ask about cost
- Use the save_lead tool once you have their name, phone, service needed, and zip code
- If a service is not available in their area, suggest alternatives
- Keep responses concise (2-3 sentences max)"""

# Initialize Anthropic client
client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))

# Initialize session state
if "messages" not in st.session_state:
    st.session_state.messages = []

# Display chat history
for message in st.session_state.messages:
    with st.chat_message(message["role"]):
        st.markdown(message["content"])


def get_agent_response(messages):
    """Send messages to Claude with tools and handle the agentic loop."""
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        system=SYSTEM_PROMPT,
        tools=TOOL_DEFINITIONS,
        messages=messages
    )

    # Agentic loop: keep going while Claude wants to use tools
    while response.stop_reason == "tool_use":
        tool_results = []
        assistant_content = response.content

        for block in response.content:
            if block.type == "tool_use":
                tool_result = execute_tool(block.name, block.input)
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": tool_result
                })

        messages.append({"role": "assistant", "content": assistant_content})
        messages.append({"role": "user", "content": tool_results})

        response = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=1024,
            system=SYSTEM_PROMPT,
            tools=TOOL_DEFINITIONS,
            messages=messages
        )

    final_text = ""
    for block in response.content:
        if hasattr(block, "text"):
            final_text += block.text

    return final_text


# Handle user input
if prompt := st.chat_input("Type your message..."):
    st.session_state.messages.append({"role": "user", "content": prompt})
    with st.chat_message("user"):
        st.markdown(prompt)

    with st.chat_message("assistant"):
        with st.spinner("Thinking..."):
            api_messages = [
                {"role": m["role"], "content": m["content"]}
                for m in st.session_state.messages
            ]
            response_text = get_agent_response(api_messages)
            st.markdown(response_text)

    st.session_state.messages.append({"role": "assistant", "content": response_text})

Your AI lead qualification agent is fully functional. It greets customers, asks qualifying questions, checks availability, provides pricing, and saves leads. Next up, the Secret Mission challenges you to build a real-time dashboard that tracks every lead the agent captures.

Secret mission

Build a Lead Dashboard

Your agent captures leads, but where do they go? Right now, lead data disappears into a function return value. In this secret mission, you'll build a real-time Lead Dashboard in the sidebar that tracks every captured lead, assigns a qualification status, and gives you a bird's-eye view of your conversion funnel.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely on your local machine, so there are no ongoing cloud costs. The only financial resource is the $5 Anthropic API credit you loaded, which only gets consumed when you actively use the chatbot.

Resources you used:

  • Project folder with app.py, tools.py, and requirements.txt.
  • Python virtual environment (venv folder).
  • .env file containing your Anthropic API key.
  • Anthropic API account with loaded credits at console.anthropic.com.
  • Installed Python packages (anthropic, streamlit, python-dotenv).

Keep everything running

No action needed. Choose this if you plan to keep experimenting with the agent or want to build on it further.

  • Your project folder stays on your Desktop.
  • Your Anthropic API key remains active. Credits are only consumed when you run the chatbot and send messages.
  • You can relaunch the app anytime by activating your virtual environment and running streamlit run app.py.

Pause - I'll come back to this later

Shut down any running processes to free up memory. Your files and API key stay intact for when you return.

  • Stop the Streamlit server by pressing Ctrl+C in the terminal where it is running.
  • Deactivate your virtual environment by running deactivate in the terminal.
  • Optionally revoke your API key at console.anthropic.com if you are concerned about security. You can generate a new one later.

Delete - I don't want to use this again

Remove all project resources from your machine and disable your API access.

  • Stop the Streamlit server by pressing Ctrl+C in the terminal.
  • Delete your entire project folder from the Desktop.
  • Go to console.anthropic.com and revoke your API key under API Keys.
  • Your remaining Anthropic credits stay in your account. You cannot withdraw them, but they will not be charged further once the key is revoked.

Nice Work!

Nice Work!

You just built a fully functional AI Lead Qualification Agent that autonomously qualifies leads through conversation. This is the exact same agentic loop pattern used in production business automation systems.

Here's what you accomplished:

  • Built a professional chat interface with Streamlit that manages conversation history using session state.
  • Integrated Claude's API with tool use to give the agent real capabilities like checking availability, looking up pricing, and saving leads.
  • Implemented the agentic loop pattern where Claude autonomously decides which tools to call, your code executes them, and Claude uses the results to continue the conversation.
  • Secret Mission: Added a live Lead Dashboard sidebar to track captured leads and their qualification status in real-time.

Ready to quiz yourself?