Skip to content

Lab 3.1: Tools - Error Handling & Validation

In this lab, you'll enhance your travel assistant's tools with proper error handling, input validation, and graceful fallbacks.

You'll learn how to build tools that handle edge cases, validate inputs, and provide helpful error messages to agents when things go wrong.

By the end of this lab, you will:

  • ✅ Add input validation to prevent invalid tool calls
  • ✅ Implement error handling for external dependencies (databases, APIs)
  • ✅ Create graceful fallbacks when tools fail
  • ✅ Return structured error messages that help the AI agent recover

Instructions

Step 1: Add Input Validation

Open FlightSearchTool class and locate the SearchFlights method.

Add these validation checks to catch common user input errors before making any external calls:

public async Task<string> SearchFlights(
    [Description("Departure city or airport (e.g., 'Melbourne', 'MEL')")] string origin,
    [Description("Destination city or airport (e.g., 'Tokyo', 'NRT', 'Paris', 'CDG')")] string destination,
    [Description("Departure date in YYYY-MM-DD format (optional)")] string? departureDate = null,
    [Description("Return date in YYYY-MM-DD format (optional)")] string? returnDate = null,
    [Description("Maximum budget in AUD (optional)")] decimal? maxBudget = null,
    [Description("User preferences for flight characteristics (e.g., 'comfortable flight with entertainment', 'budget-friendly', 'business travel') (optional)")] string? userPreferences = null)
{
    // VALIDATION 1: Check for missing required fields
    if (string.IsNullOrWhiteSpace(origin) || string.IsNullOrWhiteSpace(destination))
    {
        return JsonSerializer.Serialize(new 
        { 
            success = false,
            error = "Missing required fields",
            message = "Please provide both origin and destination cities.",
            suggestedAction = "Ask the user to specify both departure and destination cities."
        });
    }

    // VALIDATION 2: Check if searching same city
    if (origin.Equals(destination, StringComparison.OrdinalIgnoreCase))
    {
        return JsonSerializer.Serialize(new 
        { 
            success = false,
            error = "Invalid route",
            message = "Origin and destination cannot be the same city.",
            suggestedAction = "Clarify the intended destination with the user."
        });
    }

    // VALIDATION 3: Check date format if provided
    if (!string.IsNullOrEmpty(departureDate))
    {
        if (!DateTime.TryParse(departureDate, out var parsedDate))
        {
            return JsonSerializer.Serialize(new 
            { 
                success = false,
                error = "Invalid date format",
                message = $"The date '{departureDate}' is not valid. Please use YYYY-MM-DD format.",
                suggestedAction = "Ask the user for a valid date in YYYY-MM-DD format."
            });
        }

        // VALIDATION 4: Check if date is in the past
        if (parsedDate.Date < DateTime.Today)
        {
            return JsonSerializer.Serialize(new 
            { 
                success = false,
                error = "Invalid date",
                message = "Departure date cannot be in the past.",
                suggestedAction = "Ask the user for a future travel date."
            });
        }
    }

    // VALIDATION 5: Check return date is after departure date
    if (!string.IsNullOrEmpty(departureDate) && !string.IsNullOrEmpty(returnDate))
    {
        if (DateTime.TryParse(departureDate, out var depDate) && 
            DateTime.TryParse(returnDate, out var retDate))
        {
            if (retDate <= depDate)
            {
                return JsonSerializer.Serialize(new 
                { 
                    success = false,
                    error = "Invalid date range",
                    message = "Return date must be after departure date.",
                    suggestedAction = "Ask the user to provide valid travel dates."
                });
            }
        }
    }

    // VALIDATION 6: Validate budget is positive
    if (maxBudget.HasValue && maxBudget.Value <= 0)
    {
        return JsonSerializer.Serialize(new 
        { 
            success = false,
            error = "Invalid budget",
            message = "Budget must be a positive amount.",
            suggestedAction = "Ask the user for a realistic budget amount."
        });
    }

    // Continue with existing logic...
}

Step 2: Add Error Handling for External Dependencies

Wrap the database operations in try-catch blocks to handle different failure scenarios:

try
{
    // Query flights from Cosmos DB
    var container = _database.GetContainer(_config.CosmosDbFlightsContainer!);

    // Generate embedding for user preferences if provided
    float[]? preferenceVector = null;
    if (!string.IsNullOrEmpty(userPreferences))
    {
        try
        {
            var embeddingResponse = await _embeddingClient.GenerateEmbeddingAsync(userPreferences);
            preferenceVector = embeddingResponse.Value.ToFloats().ToArray();
        }
        catch (Exception ex)
        {
            // Graceful fallback: Continue without semantic search
            Console.WriteLine($"Failed to generate embedding: {ex.Message}");
            // Don't fail the entire search, just skip semantic matching
        }
    }

    // ... rest of existing query logic ...

}
catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
{
    // Handle rate limiting (429)
    return JsonSerializer.Serialize(new 
    { 
        success = false,
        error = "Service temporarily busy",
        message = "Flight search is experiencing high demand. Please try again in a moment.",
        suggestedAction = "Apologize and ask the user to try their search again in a few seconds.",
        retryable = true
    });
}
catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable)
{
    // Handle service unavailable (503)
    return JsonSerializer.Serialize(new 
    { 
        success = false,
        error = "Service unavailable",
        message = "The flight database is temporarily unavailable.",
        suggestedAction = "Apologize and suggest the user try again later or provide general travel advice.",
        retryable = true
    });
}
catch (CosmosException ex)
{
    // Handle other Cosmos DB errors
    Console.WriteLine($"Cosmos DB error: {ex.StatusCode} - {ex.Message}");
    return JsonSerializer.Serialize(new 
    { 
        success = false,
        error = "Database error",
        message = "Unable to search flights due to a technical issue.",
        suggestedAction = "Apologize and offer to help with other travel planning tasks.",
        technicalDetails = $"Status: {ex.StatusCode}"
    });
}
catch (Exception ex)
{
    // Handle unexpected errors
    Console.WriteLine($"Unexpected error in SearchFlights: {ex.Message}");
    return JsonSerializer.Serialize(new 
    { 
        success = false,
        error = "Unexpected error",
        message = "An unexpected error occurred while searching for flights.",
        suggestedAction = "Apologize and offer alternative assistance.",
        technicalDetails = ex.GetType().Name
    });
}

Step 3: Test the Error Handling

Run your backend and test these scenarios:

  1. Test 1: Same City Validation

    User: Find me flights from Tokyo to Tokyo
    

    Expected Response: Agent should recognize the validation error and ask for clarification.

  2. Test 2: Past Date Validation**

    User: Find me flights to Paris on 2025-01-01
    

    Expected Response:: Agent should ask for a future date.

  3. Test 3: Invalid Date Format

    User: Find me flights to Sydney departing 2026-03-15 and returning 2026-03-10
    

    Expected Response:: Agent should recognize the date logic error.