Skip to content

Learn Agentic AI by Building One — A Hands-On Guide - Lesson 5: Tools — Letting AI Call Functions

6 minute read
Content level: Intermediate
0

Introduction to how Agentic AI works behind the scene

This is where it gets interesting. Remember Lesson 1? The AI couldn't check the weather because it's just a text predictor.

TOOLS solve this. Here's the idea:

  1. You DESCRIBE functions to the AI (name, parameters, what they do)
  2. The AI decides WHEN to call them based on the user's question
  3. The AI outputs a "tool call" instead of text
  4. YOUR CODE executes the function and sends the result back
  5. The AI uses the result to form its final answer
User: "How many urgent cases do we have?"
     ↓
AI thinks: "I need the get_case_stats tool"
     ↓
AI outputs: tool_call(name="get_case_stats", args={"severity": "Urgent"})
     ↓
YOUR CODE runs get_case_stats("Urgent") → returns 5
     ↓
AI receives result → "You have 5 urgent cases."

The AI NEVER runs code. It just ASKS you to run it. You're the executor.

Step 1: Define Real Functions

First, some fake data simulating our support cases, and functions that query it:

import json
import boto3

bedrock = boto3.client("bedrock-runtime", region_name="us-east-1")

CASES_DB = [
    {"id": 1, "subject": "EC2 Instance Rebooted", "severity": "Urgent", "service": "EC2", "days_to_resolve": 10},
    {"id": 2, "subject": "RDS Connection Timeout", "severity": "Urgent", "service": "RDS", "days_to_resolve": 68},
    {"id": 3, "subject": "S3 Permission Denied", "severity": "High", "service": "S3", "days_to_resolve": 1},
    {"id": 4, "subject": "Lambda Memory Limit", "severity": "Low", "service": "Lambda", "days_to_resolve": 12},
    {"id": 5, "subject": "EFS Throughput Issue", "severity": "High", "service": "EFS", "days_to_resolve": 20},
    {"id": 6, "subject": "EKS Pods Failing", "severity": "Normal", "service": "EKS", "days_to_resolve": 11},
]


def get_case_count(severity: str = None) -> str:
    if severity:
        count = sum(1 for c in CASES_DB if c["severity"].lower() == severity.lower())
        return f"{count} cases with severity '{severity}'"
    return f"{len(CASES_DB)} total cases"


def get_avg_resolution(service: str = None, severity: str = None) -> str:
    cases = CASES_DB
    if service:
        cases = [c for c in cases if c["service"].lower() == service.lower()]
    if severity:
        cases = [c for c in cases if c["severity"].lower() == severity.lower()]
    if not cases:
        return "No matching cases found"
    avg = sum(c["days_to_resolve"] for c in cases) / len(cases)
    parts = [f"service={service}" if service else None, f"severity={severity}" if severity else None]
    label = " for " + ", ".join(p for p in parts if p) if any(parts) else ""
    return f"{avg:.1f} days average resolution{label}"


def list_cases(severity: str = None) -> str:
    cases = CASES_DB
    if severity:
        cases = [c for c in cases if c["severity"].lower() == severity.lower()]
    return json.dumps([{"subject": c["subject"], "severity": c["severity"], "service": c["service"]} for c in cases])


TOOL_FUNCTIONS = {
    "get_case_count": get_case_count,
    "get_avg_resolution": get_avg_resolution,
    "list_cases": list_cases,
}

Step 2: Describe the Tools to the AI

This is the "menu" — you tell the AI what's available. It uses these descriptions to decide which tool to call:

TOOLS = [
    {
        "name": "get_case_count",
        "description": "Count support cases, optionally filtered by severity level",
        "input_schema": {
            "type": "object",
            "properties": {
                "severity": {
                    "type": "string",
                    "description": "Filter by: Urgent, High, Normal, or Low",
                    "enum": ["Urgent", "High", "Normal", "Low"]
                }
            },
            "required": [],
        },
    },
    {
        "name": "get_avg_resolution",
        "description": "Get the average resolution time in days, optionally filtered by service and/or severity",
        "input_schema": {
            "type": "object",
            "properties": {
                "service": {"type": "string", "description": "AWS service name like EC2, RDS, S3"},
                "severity": {"type": "string", "description": "Filter by severity level", "enum": ["Urgent", "High", "Normal", "Low"]}
            },
            "required": [],
        },
    },
    {
        "name": "list_cases",
        "description": "List support cases with their details, optionally filtered by severity",
        "input_schema": {
            "type": "object",
            "properties": {
                "severity": {"type": "string", "description": "Filter by severity level"}
            },
            "required": [],
        },
    },
]

Step 3: Send a Question and Handle Tool Calls

This is the orchestration — send the question, check if the AI wants a tool, execute it, send the result back:

def ask_with_tools(question: str) -> str:
    print(f"\n🧑 User: {question}")

    # First call — AI decides if it needs a tool
    response = bedrock.invoke_model(
        modelId="us.anthropic.claude-sonnet-4-20250514-v1:0",
        contentType="application/json",
        accept="application/json",
        body=json.dumps({
            "anthropic_version": "bedrock-2023-05-31",
            "max_tokens": 1024,
            "system": "You are a support case analyst. Use the provided tools to answer questions.",
            "tools": TOOLS,
            "messages": [{"role": "user", "content": question}],
        }),
    )
    result = json.loads(response["body"].read())

    # Check if the AI wants to use a tool
    if result["stop_reason"] == "tool_use":
        tool_block = next(b for b in result["content"] if b["type"] == "tool_use")
        tool_name = tool_block["name"]
        tool_args = tool_block["input"]

        print(f"   🔧 AI wants to call: {tool_name}({tool_args})")

        # YOUR CODE executes the function — not the AI!
        func = TOOL_FUNCTIONS[tool_name]
        tool_result = func(**tool_args)

        print(f"   📊 Tool returned: {tool_result}")

        # Send the result back to the AI for a final answer
        response2 = bedrock.invoke_model(
            modelId="us.anthropic.claude-sonnet-4-20250514-v1:0",
            contentType="application/json",
            accept="application/json",
            body=json.dumps({
                "anthropic_version": "bedrock-2023-05-31",
                "max_tokens": 1024,
                "tools": TOOLS,
                "messages": [
                    {"role": "user", "content": question},
                    {"role": "assistant", "content": result["content"]},
                    {"role": "user", "content": [{"type": "tool_result", "tool_use_id": tool_block["id"], "content": tool_result}]},
                ],
            }),
        )
        result2 = json.loads(response2["body"].read())
        final = next(b["text"] for b in result2["content"] if b["type"] == "text")
    else:
        final = next(b["text"] for b in result["content"] if b["type"] == "text")

    print(f"   🤖 AI: {final}")
    return final

Running It

ask_with_tools("How many urgent cases do we have?")
ask_with_tools("What's the average resolution time for RDS cases?")
ask_with_tools("List all high severity cases")
ask_with_tools("What does SLA stand for?")  # No tool needed

Output:

🧑 User: How many urgent cases do we have?
🔧 AI wants to call: get_case_count({'severity': 'Urgent'})
📊 Tool returned: 2 cases with severity 'Urgent'
🤖 AI: You currently have 2 urgent cases.

🧑 User: What's the average resolution time for RDS cases?
🔧 AI wants to call: get_avg_resolution({'service': 'RDS'})
📊 Tool returned: 68.0 days average resolution for RDS
🤖 AI: The average resolution time for RDS cases is 68.0 days.

🧑 User: List all high severity cases
🔧 AI wants to call: list_cases({'severity': 'High'})
📊 Tool returned: [{"subject": "S3 Permission Denied", "severity": "High", "service": "S3"}, {"subject": "EFS Throughput Issue", "severity": "High", "service": "EFS"}]
🤖 AI: Here are all high severity cases:

  1. S3 Permission Denied - Service: S3, Severity: High
  2. EFS Throughput Issue - Service: EFS, Severity: High

🧑 User: What does SLA stand for?
🤖 AI: SLA stands for "Service Level Agreement." It's a contract or agreement that defines the expected level of service between a service provider and customer, including metrics like response times, resolution times, uptime guarantees, and quality standards.

In the context of support cases, SLAs typically specify how quickly different severity levels of issues should be acknowledged and resolved.

Notice how the last question ("What does SLA stand for?") didn't trigger any tool — the AI knew it could answer directly. It only uses tools when it needs external data.

Key Takeaway

Tools let AI interact with the real world through YOUR functions. The AI decides WHAT to call. Your code decides HOW to execute it. This is the fundamental pattern behind every AI assistant that can search the web, query databases, or take actions.

Next up: Lesson 6 — The agent loop: chaining multiple tool calls automatically →
Previous: Lesson 4 — Giving AI memory so it remembers the conversation →