Skip to content

Lab 018: Function Calling & Tool UseΒΆ

Level: L100 Path: βš™οΈ Pro Code Agents Β· 🧠 Semantic Kernel Time: ~35 min πŸ’° Cost: GitHub Free β€” Free GitHub account, no credit card

What You'll LearnΒΆ

  • What function calling (tool use) is and how it works at the API level
  • How to define tools that the LLM can call
  • How to parse and execute tool calls from the model's response
  • How to implement a tool execution loop (the agent loop)
  • Common patterns: parallel tools, required tools, tool error handling
  • The difference between function calling and Semantic Kernel plugins

IntroductionΒΆ

Function calling (also called "tool use") is the mechanism that transforms an LLM from a text generator into an agent. Instead of just producing text, the model can say: "I need to call get_weather("Seattle") before I can answer."

Your code then executes that function, returns the result, and the model uses it to generate a grounded answer.

This is the foundation of every AI agent:

Agent Tool-Calling Loop


How Function Calling WorksΒΆ

1. You define tools as JSON schemasΒΆ

tools = [
    {
        "type": "function",
        "function": {
            "name": "search_products",
            "description": "Search OutdoorGear products by criteria",
            "parameters": {
                "type": "object",
                "properties": {
                    "category": {
                        "type": "string",
                        "description": "Product category (e.g., 'tent', 'sleeping bag', 'backpack')"
                    },
                    "max_price": {
                        "type": "number",
                        "description": "Maximum price in USD"
                    },
                    "in_stock": {
                        "type": "boolean",
                        "description": "If true, only return in-stock items"
                    }
                },
                "required": ["category"]
            }
        }
    }
]

2. The LLM responds with a tool call (not text)ΒΆ

{
  "role": "assistant",
  "tool_calls": [
    {
      "id": "call_abc123",
      "type": "function",
      "function": {
        "name": "search_products",
        "arguments": "{\"category\": \"tent\", \"max_price\": 200}"
      }
    }
  ]
}

3. You execute the function and return the resultΒΆ

result = search_products(category="tent", max_price=200)
# Add result to messages as a "tool" role message

4. The LLM generates the final answer using the tool resultΒΆ


Step 1: Set UpΒΆ

pip install openai
export GITHUB_TOKEN=your_github_token

Step 2: Define Your Tool FunctionsΒΆ

import json
from typing import Optional

# Simulated OutdoorGear product database
PRODUCTS = [
    {"id": "P001", "name": "TrailBlazer Tent 2P", "category": "tent", "price": 189.99, "in_stock": True, "weight_kg": 1.8},
    {"id": "P002", "name": "Summit Dome 4P",      "category": "tent", "price": 349.99, "in_stock": True, "weight_kg": 3.2},
    {"id": "P003", "name": "UltraLight Solo",      "category": "tent", "price": 249.99, "in_stock": False, "weight_kg": 0.9},
    {"id": "P004", "name": "ArcticDown -20Β°C",     "category": "sleeping bag", "price": 299.99, "in_stock": True, "weight_kg": 1.5},
    {"id": "P005", "name": "ThreeSeasons 0Β°C",     "category": "sleeping bag", "price": 149.99, "in_stock": True, "weight_kg": 1.1},
    {"id": "P006", "name": "Osprey Atmos 65L",     "category": "backpack",     "price": 279.99, "in_stock": True, "weight_kg": 2.1},
    {"id": "P007", "name": "DayHiker 22L",          "category": "backpack",     "price": 89.99,  "in_stock": True, "weight_kg": 0.8},
]


def search_products(category: str, max_price: Optional[float] = None, in_stock: Optional[bool] = None) -> list:
    """Search products by category, price, and availability."""
    results = [p for p in PRODUCTS if category.lower() in p["category"].lower()]
    if max_price is not None:
        results = [p for p in results if p["price"] <= max_price]
    if in_stock is not None:
        results = [p for p in results if p["in_stock"] == in_stock]
    return results


def get_product_details(product_id: str) -> dict:
    """Get full details for a specific product by ID."""
    for product in PRODUCTS:
        if product["id"] == product_id:
            return product
    return {"error": f"Product {product_id} not found"}


def calculate_total(product_ids: list, discount_percent: float = 0) -> dict:
    """Calculate total price for a list of products with optional discount."""
    total = 0.0
    items = []
    for pid in product_ids:
        product = get_product_details(pid)
        if "error" not in product:
            items.append({"name": product["name"], "price": product["price"]})
            total += product["price"]
    discount = total * (discount_percent / 100)
    return {
        "items": items,
        "subtotal": round(total, 2),
        "discount": round(discount, 2),
        "total": round(total - discount, 2)
    }

Step 3: Define Tool SchemasΒΆ

TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "search_products",
            "description": "Search OutdoorGear products by category, price, and availability.",
            "parameters": {
                "type": "object",
                "properties": {
                    "category": {
                        "type": "string",
                        "description": "Product category: 'tent', 'sleeping bag', or 'backpack'"
                    },
                    "max_price": {
                        "type": "number",
                        "description": "Maximum price in USD. Omit if no price limit."
                    },
                    "in_stock": {
                        "type": "boolean",
                        "description": "Set to true to only return products currently in stock."
                    }
                },
                "required": ["category"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "get_product_details",
            "description": "Get full details (price, weight, stock) for a specific product by its ID.",
            "parameters": {
                "type": "object",
                "properties": {
                    "product_id": {
                        "type": "string",
                        "description": "The product ID, e.g. 'P001'"
                    }
                },
                "required": ["product_id"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "calculate_total",
            "description": "Calculate the total price for a list of products, with optional discount.",
            "parameters": {
                "type": "object",
                "properties": {
                    "product_ids": {
                        "type": "array",
                        "items": {"type": "string"},
                        "description": "List of product IDs to include in the total"
                    },
                    "discount_percent": {
                        "type": "number",
                        "description": "Discount percentage to apply (0-100). Default: 0"
                    }
                },
                "required": ["product_ids"]
            }
        }
    }
]

Step 4: The Tool Execution LoopΒΆ

This is the core of every function-calling agent:

import os
import json
from openai import OpenAI

client = OpenAI(
    base_url="https://models.inference.ai.azure.com",
    api_key=os.environ["GITHUB_TOKEN"],
)

# Map function names to actual Python functions
TOOL_FUNCTIONS = {
    "search_products": search_products,
    "get_product_details": get_product_details,
    "calculate_total": calculate_total,
}


def run_agent(user_message: str) -> str:
    """Run the agent loop: chat β†’ tool calls β†’ results β†’ final answer."""
    messages = [
        {
            "role": "system",
            "content": (
                "You are a helpful shopping assistant for OutdoorGear Inc. "
                "Use the provided tools to answer questions about products. "
                "Never invent product data β€” always use tool results."
            )
        },
        {"role": "user", "content": user_message}
    ]

    while True:
        # Call the LLM
        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=messages,
            tools=TOOLS,
            tool_choice="auto",  # LLM decides whether to call a tool
        )

        message = response.choices[0].message
        messages.append(message)  # always append LLM's response to history

        # Check if the LLM wants to call a tool
        if response.choices[0].finish_reason == "tool_calls":
            # Execute each requested tool
            for tool_call in message.tool_calls:
                func_name = tool_call.function.name
                func_args = json.loads(tool_call.function.arguments)

                print(f"  πŸ”§ Calling: {func_name}({func_args})")

                # Execute the function
                if func_name in TOOL_FUNCTIONS:
                    result = TOOL_FUNCTIONS[func_name](**func_args)
                else:
                    result = {"error": f"Unknown function: {func_name}"}

                # Add tool result to messages
                messages.append({
                    "role": "tool",
                    "tool_call_id": tool_call.id,
                    "content": json.dumps(result),
                })
        else:
            # No more tool calls β€” return the final answer
            return message.content


# Test it!
if __name__ == "__main__":
    questions = [
        "What tents do you have under $250 that are in stock?",
        "Show me the details for product P004 and calculate what it costs with a 10% discount.",
        "I need a lightweight tent and a sleeping bag for 0Β°C camping. What would be the total cost?",
    ]

    for q in questions:
        print(f"\n{'='*60}")
        print(f"User: {q}")
        print(f"{'='*60}")
        answer = run_agent(q)
        print(f"Agent: {answer}")

Step 5: Parallel Tool CallsΒΆ

The LLM can request multiple tool calls in a single response. Handle them all before returning:

# The loop above already handles this β€” message.tool_calls is a list
# When LLM calls two tools at once, you'll see:
# πŸ”§ Calling: search_products({'category': 'tent'})
# πŸ”§ Calling: search_products({'category': 'sleeping bag'})
# (both in the same iteration)

Try asking: "Compare all tents and sleeping bags under $300" β€” you'll see two parallel tool calls.


Step 6: Controlling Tool ChoiceΒΆ

# Auto (default): LLM decides whether and which tool to call
tool_choice="auto"

# Required: LLM MUST call at least one tool
tool_choice="required"

# Force a specific tool:
tool_choice={"type": "function", "function": {"name": "search_products"}}

# No tools (force text response):
tool_choice="none"

Step 7: πŸ§ͺ Interactive Challenge β€” Fix the Broken Tool DefinitionΒΆ

The schema below has 3 bugs that will cause the tool to fail or behave incorrectly. Find and fix them:

# BROKEN β€” find the 3 bugs
broken_tool = {
    "type": "functions",                    # Bug 1: wrong type
    "function": {
        "name": "get_inventory",
        "description": "",                  # Bug 2: empty description
        "parameters": {
            "type": "object",
            "properties": {
                "warehouse_id": {
                    "type": "int",          # Bug 3: wrong JSON Schema type
                    "description": "Warehouse identifier"
                }
            }
            # Missing "required" key β€” also a bug (but not counted)
        }
    }
}
Show the fixes

Bug 1: "type": "functions" β†’ should be "type": "function" (singular)

Bug 2: Empty description β€” the LLM uses descriptions to decide when to call a tool. Without it, the LLM won't know what the tool does and may never call it (or call it inappropriately).

Bug 3: "type": "int" β†’ should be "type": "integer" β€” JSON Schema uses integer, not int.

Bonus bug: The required key is missing. Add "required": ["warehouse_id"] to ensure the LLM always passes a warehouse ID.


Function Calling vs. Semantic Kernel PluginsΒΆ

Direct Function Calling Semantic Kernel Plugin
Level Low-level API High-level abstraction
Schema You write JSON manually Inferred from Python type hints
Languages Any OpenAI-compatible client Python, C#, Java
Flexibility Full control Less boilerplate
When to use Learning, custom control Production SK agents

In practice, SK plugins generate the JSON schema automatically from your Python function signatures and docstrings. Under the hood, it's the same API call.


🧠 Knowledge Check¢

Q1 (Multiple Choice): When the LLM returns finish_reason='tool_calls', what should your agent loop do next?
  • A) Return the partial answer to the user and wait for confirmation
  • B) Execute the requested function(s), add results as role: tool messages, then call the LLM again
  • C) Discard the response and retry with a different prompt
  • D) Switch to a different model that supports the tool
βœ… Reveal Answer

Correct: B

finish_reason='tool_calls' means the LLM needs external data before it can answer. Your loop must: (1) read response.choices[0].message.tool_calls, (2) call each requested function with the provided arguments, (3) add the LLM's message AND tool results to history with role: tool, then (4) call the LLM again. Repeat until finish_reason == 'stop'.

Q2 (Run the Lab): Using the search_products function defined in Step 2, how many tents are currently in stock?

Run the search manually or trace through the product list in Step 2. Count tents where in_stock == True.

βœ… Reveal Answer

2 tents are in stock: P001 (TrailBlazer Tent 2P, $189.99) and P002 (Summit Dome 4P, $349.99)

P003 (UltraLight Solo) is marked "in_stock": False. So search_products("tent", in_stock=True) returns exactly 2 items.

Q3 (Run the Lab): What does calculate_total(["P001", "P007"]) return as the total field? (No discount applied)

Look up the prices for P001 and P007 in the PRODUCTS list and add them together.

βœ… Reveal Answer

$279.98

P001 (TrailBlazer Tent 2P) = $189.99 + P007 (DayHiker 22L) = $89.99 = $279.98. The function applies no discount when discount_percent=0, so total == subtotal == 279.98.


SummaryΒΆ

Concept Key takeaway
Tool schema JSON object with name, description, and parameters
finish_reason "tool_calls" = LLM wants to call a function; "stop" = final answer
Tool result Added as role: "tool" message with matching tool_call_id
Agent loop Keep calling LLM until finish_reason == "stop"
Parallel tools One response can contain multiple tool calls β€” handle them all

Next StepsΒΆ