AI Course/week14_tool_use/tools.py

Course file

tools.py

week14_tool_use/tools.py

"""
Week 14: Tool Implementations
==============================
Real tool functions that the agent can call.
Each function takes simple arguments and returns a string result.
"""

import os
import urllib.request
import urllib.error
import html.parser


class HTMLTextExtractor(html.parser.HTMLParser):
    """Simple HTML parser that extracts visible text."""

    def __init__(self):
        super().__init__()
        self.result = []
        self._skip = False

    def handle_starttag(self, tag, attrs):
        if tag in ("script", "style", "head"):
            self._skip = True

    def handle_endtag(self, tag):
        if tag in ("script", "style", "head"):
            self._skip = False

    def handle_data(self, data):
        if not self._skip:
            text = data.strip()
            if text:
                self.result.append(text)

    def get_text(self):
        return "\n".join(self.result)


def web_search(url: str) -> str:
    """
    Fetches a URL and returns the text content.
    Strips HTML tags so Claude gets readable text.

    Args:
        url: The full URL to fetch (must start with http:// or https://)

    Returns:
        The text content of the page, or an error message.
    """
    try:
        if not url.startswith(("http://", "https://")):
            return f"Error: URL must start with http:// or https://. Got: {url}"

        req = urllib.request.Request(
            url,
            headers={"User-Agent": "Mozilla/5.0 (AI Learning Course Agent)"},
        )
        with urllib.request.urlopen(req, timeout=10) as response:
            raw = response.read().decode("utf-8", errors="replace")

        # Strip HTML tags to get readable text
        extractor = HTMLTextExtractor()
        extractor.feed(raw)
        text = extractor.get_text()

        # Truncate if too long (Claude has context limits)
        if len(text) > 5000:
            text = text[:5000] + "\n\n[... truncated — page was too long ...]"

        return text if text else "Page loaded but no readable text found."

    except urllib.error.HTTPError as e:
        return f"HTTP Error {e.code}: {e.reason}"
    except urllib.error.URLError as e:
        return f"URL Error: {e.reason}"
    except Exception as e:
        return f"Error fetching URL: {e}"


def read_file(file_path: str) -> str:
    """
    Reads a local file and returns its contents.

    Args:
        file_path: Path to the file to read.

    Returns:
        The file contents, or an error message.
    """
    try:
        # Security: don't allow reading sensitive files
        blocked = [".env", "id_rsa", "id_ed25519", ".ssh", "passwd", "shadow"]
        if any(b in file_path.lower() for b in blocked):
            return "Error: cannot read sensitive files (.env, SSH keys, etc.)"

        if not os.path.exists(file_path):
            return f"Error: file not found: {file_path}"

        with open(file_path, "r") as f:
            content = f.read()

        if len(content) > 5000:
            content = content[:5000] + "\n\n[... truncated ...]"

        return content

    except Exception as e:
        return f"Error reading file: {e}"


def calculate(expression: str) -> str:
    """
    Evaluates a math expression safely.

    Args:
        expression: A math expression like "42 * 67 + 13" or "2**10".

    Returns:
        The result as a string, or an error message.
    """
    allowed_chars = set("0123456789+-*/.() eE")
    if not all(c in allowed_chars for c in expression):
        return "Error: only numbers and math operators (+-*/.()) are allowed."

    try:
        result = eval(expression)
        return str(result)
    except Exception as e:
        return f"Error: {e}"


# TODO: Add your own tool function here!
# Some ideas:
#
# def list_directory(path: str) -> str:
#     """List files in a directory."""
#     ...
#
# def get_weather(city: str) -> str:
#     """Fetch weather from wttr.in."""
#     url = f"https://wttr.in/{city}?format=3"
#     ...