Your LLM agents often need to act upon or retrieve information that isn't directly available within their immediate programming environment. To truly extend their capabilities, your custom Python tools will frequently need to communicate with external services. The two most common types of external services you'll encounter are Application Programming Interfaces (APIs) and databases. Building tools that effectively interact with these resources enables your agents to fetch real-time data, access specialized functionalities, and work with persisted information.Connecting to External APIsAPIs serve as gateways to an array of functionalities and data sources on the web. Whether it's fetching the latest financial news, translating text, getting weather updates, or interacting with a proprietary business system, APIs allow your tools to programmatically access these services. The requests library is the de facto standard in Python for making HTTP requests, which is the underlying protocol for most web APIs.If you don't have it installed, you can add requests to your project using pip: pip install requestsMaking API RequestsMost API interactions involve sending an HTTP request to a specific URL (an endpoint) and then processing the response. The two most common HTTP methods you'll use are GET (to retrieve data) and POST (to send data or trigger an action).GET RequestsA GET request is used to retrieve information from an API. You might pass parameters as part of the URL's query string.import requests def get_current_weather(api_key, city_name): """Fetches current weather data for a given city using an example weather API.""" base_url = "http://api.exampleweather.com/v1/current.json" # Fictional API endpoint params = { "": api_key, "q": city_name } try: response = requests.get(base_url, params=params, timeout=10) # 10-second timeout response.raise_for_status() # Raises an HTTPError for bad responses (4XX or 5XX) weather_data = response.json() # Assumes the API returns JSON # Process and return relevant weather information description = weather_data.get("current", {}).get("condition", {}).get("text", "Not available") temp_c = weather_data.get("current", {}).get("temp_c", "N/A") return f"Current weather in {city_name}: {description}, Temperature: {temp_c}°C" except requests.exceptions.Timeout: return f"Error: The request to the weather API timed out." except requests.exceptions.HTTPError as http_err: return f"Error: HTTP error occurred: {http_err} - Status: {response.status_code}" except requests.exceptions.RequestException as e: return f"Error: An error occurred while fetching weather data: {e}" # Example usage (API key would be managed securely) # print(get_current_weather("YOUR_API_KEY", "London"))In this example, requests.get() sends the request. response.raise_for_status() is a convenient way to check if the request was successful (status codes 200-299); otherwise, it raises an exception. API responses are often in JSON format, which can be easily parsed into a Python dictionary using response.json().POST RequestsA POST request is used to send data to an API, perhaps to create a new resource or trigger an operation. Data is typically sent in the request body, often as JSON.import requests import json def submit_user_feedback(api_endpoint, user_id, feedback_text): """Submits user feedback to an example feedback API.""" payload = { "user_id": user_id, "feedback": feedback_text, "source": "LLM_Agent_Tool" } headers = { "Content-Type": "application/json" # "Authorization": "Bearer YOUR_ACCESS_TOKEN" # If authentication is needed } try: response = requests.post(api_endpoint, data=json.dumps(payload), headers=headers, timeout=10) response.raise_for_status() # Assuming the API returns a JSON response, e.g., with a submission ID return f"Feedback submitted successfully. Response: {response.json()}" except requests.exceptions.RequestException as e: return f"Error submitting feedback: {e}" # Example usage # feedback_api_url = "http://api.examplefeedback.com/v1/submit" # Fictional API endpoint # print(submit_user_feedback(feedback_api_url, "user123", "The agent was very helpful!"))Here, json.dumps(payload) serializes the Python dictionary into a JSON string, which is sent as the request body. The Content-Type header tells the server that we're sending JSON.Handling API Responses and AuthenticationAlways inspect the response.status_code to understand the outcome of your request. Common status codes include:200 OK: Request succeeded.201 Created: Request succeeded, and a new resource was created (often for POST/PUT).400 Bad Request: The server could not understand the request due to invalid syntax.401 Unauthorized: Authentication is required and has failed or has not yet been provided.403 Forbidden: The server understood the request, but is refusing to fulfill it.404 Not Found: The requested resource could not be found.429 Too Many Requests: You've hit a rate limit.500 Internal Server Error: The server encountered an unexpected condition.Many APIs require authentication. Common methods include API keys (sent as a header or query parameter) or OAuth tokens. Securely managing these credentials is very important; avoid hardcoding them directly in your tool. Use environment variables, configuration files, or dedicated secrets management services. Chapter 4, "Integrating External APIs as Tools," will cover authentication and other API integration aspects like rate limiting in more detail.Querying DatabasesDatabases are essential for tools that need to store, retrieve, and manage structured data persistently. Your LLM agent might need a tool to look up product information, retrieve customer history, or log interaction details.Python has a standard API for connecting to SQL databases, known as the DB-API 2.0 (PEP 249). Most Python database drivers for relational databases like PostgreSQL, MySQL, and SQLite adhere to this specification.Using sqlite3 for SimplicitySQLite is a lightweight, file-based database engine that's built into Python's standard library via the sqlite3 module, making it excellent for examples and simpler applications.Let's imagine a tool that needs to fetch customer details from a SQLite database.First, you'd establish a connection and create a cursor object. The cursor allows you to execute SQL queries.import sqlite3 def get_customer_details(db_path, customer_id): """Retrieves customer details from an SQLite database using their ID.""" conn = None # Initialize conn to None for the finally block try: conn = sqlite3.connect(db_path) cursor = conn.cursor() # Use parameterized queries to prevent SQL injection! query = "SELECT name, email, join_date FROM customers WHERE id = ?;" cursor.execute(query, (customer_id,)) # The tuple (customer_id,) is important customer_data = cursor.fetchone() # Fetches one row if customer_data: name, email, join_date = customer_data return f"Customer ID: {customer_id}\nName: {name}\nEmail: {email}\nJoined: {join_date}" else: return f"No customer found with ID {customer_id}." except sqlite3.Error as e: return f"Database error occurred: {e}" finally: if conn: conn.close() # Always close the connection # Example: Assume 'company.db' exists and has a 'customers' table. # To set up for testing: # def setup_dummy_db(db_file): # conn_setup = sqlite3.connect(db_file) # cursor_setup = conn_setup.cursor() # cursor_setup.execute("DROP TABLE IF EXISTS customers") # cursor_setup.execute(""" # CREATE TABLE customers ( # id INTEGER PRIMARY KEY, # name TEXT NOT NULL, # email TEXT NOT NULL UNIQUE, # join_date TEXT # ) # """) # cursor_setup.execute("INSERT INTO customers (id, name, email, join_date) VALUES (?, ?, ?, ?)", # (1, 'Alice Wonderland', 'alice@example.com', '2023-01-15')) # cursor_setup.execute("INSERT INTO customers (id, name, email, join_date) VALUES (?, ?, ?, ?)", # (2, 'Bob The Builder', 'bob@example.com', '2022-11-05')) # conn_setup.commit() # conn_setup.close() # setup_dummy_db('company.db') # print(get_customer_details('company.db', 1)) # print(get_customer_details('company.db', 3))A significant aspect highlighted here is the use of parameterized queries (cursor.execute(query, (customer_id,))). You should always use placeholders (like ? for sqlite3 or %s for some other drivers) and pass values as a separate tuple or dictionary. This practice is important for preventing SQL injection vulnerabilities, where malicious input could otherwise alter your SQL queries.After executing a SELECT query, you can retrieve results using methods like:fetchone(): Fetches the next row of a query result set, returning a single sequence, or None when no more data is available.fetchall(): Fetches all remaining rows of a query result, returning a list of sequences.fetchmany(size): Fetches the next set of rows, returning a list.For operations that modify the database (like INSERT, UPDATE, DELETE), you need to commit the transaction using conn.commit() for the changes to be saved. Always ensure connections are closed (typically in a finally block) to release resources.Working with Other Databases and ORMsWhile sqlite3 is convenient, for more applications you'll likely use other database systems like PostgreSQL or MySQL. The connection process and query execution are similar, but you'll need to install their respective Python drivers (e.g., psycopg2-binary for PostgreSQL, mysql-connector-python for MySQL).For complex applications, Object-Relational Mappers (ORMs) like SQLAlchemy or Django ORM can provide a higher-level abstraction over direct SQL. ORMs allow you to interact with your database using Python objects and methods, which can simplify development and improve code maintainability, especially for tools handling intricate data models.Diagram: Tool Interaction with an External APIThe following diagram illustrates a typical flow where an LLM agent uses a Python tool to interact with an external API:digraph G { rankdir=TB; graph [fontname="Arial"]; node [shape=box, style="rounded,filled", fillcolor="#e9ecef", fontname="Arial"]; edge [fontname="Arial", fontsize=10]; LLM_Agent [label="LLM Agent", fillcolor="#a5d8ff", shape=ellipse]; Python_Tool [label="Python Tool\n(e.g., GetStockPrice)", fillcolor="#96f2d7"]; External_API [label="External Stock API", shape=cylinder, fillcolor="#ffec99", style="filled"]; Tool_Processing [label="Tool Processes Data", fillcolor="#96f2d7", shape=parallelogram]; LLM_Agent -> Python_Tool [label="Invokes Tool\nInput: Stock Ticker 'XYZ'"]; Python_Tool -> External_API [label="Sends API Request\n(GET /price?ticker=XYZ)"]; External_API -> Tool_Processing [label="API Responds\n(JSON with price data)"]; Tool_Processing -> LLM_Agent [label="Returns Formatted Result\n(e.g., 'Price of XYZ is $150')"]; }This diagram shows an LLM agent requesting stock price information. The agent calls a Python tool, which in turn queries an external stock API. The tool processes the API's response and returns a structured result to the LLM.Design for External Interaction ToolsWhen building tools that interact with external services, keep these points in mind:Clear Inputs: Design your tool's input parameters to clearly map to what the API or database query needs. For example, if a tool queries a weather API, its inputs might be location and date.Data Transformation: Raw data from APIs or databases might be verbose or not in an ideal format for an LLM. Your tool should process and transform this data into a concise, understandable, and useful format for the agent. This links to the later section on "Structuring Complex Tool Outputs for LLMs."Error Handling: External services can fail. Network issues, API rate limits, database downtimes, or invalid API keys are common. Implement comprehensive error handling (e.g., try-except blocks) to catch these issues and return informative error messages to the LLM, allowing it to potentially retry or try an alternative approach.Timeouts: Network requests can sometimes hang indefinitely. Always set reasonable timeouts for API calls and database connections to prevent your tool from blocking the agent for too long.Idempotency: Where possible, design tools that interact with external systems to be idempotent. This means that making the same call multiple times with the same input parameters has the same effect as making it once. This is particularly important if the agent might retry a tool invocation after a temporary failure.Configuration: Externalize configurations like API endpoints, database connection strings, and credentials. Avoid hardcoding these directly in your tool's logic. Environment variables or configuration files are better alternatives.By mastering interactions with APIs and databases, you significantly broaden the scope of tasks your LLM agents can perform. These tools become the agent's senses and effectors in the digital world, allowing them to fetch up-to-date information and trigger actions in external systems. As you progress, you'll combine these foundational skills with more advanced techniques for error handling, input validation, and output structuring to build truly powerful and reliable tools.