using Microsoft.Extensions.AI;
using OpenAI;
using System.Text.Json;
namespace MicrosoftAgentFrameworkLesson.ConsoleApp.FunctionCalling;
/// <summary>
/// Demonstrates three strategies for handling invalid tool input from AI models:
/// 1. IncludeDetailedErrors — surfaces exception messages so the model can self-correct
/// 2. Custom FunctionInvoker — intercepts exceptions and returns structured error feedback
/// 3. Strict JSON Schema — constrains the model's output to match the function schema
/// Scenario: Fitness center assistant with court booking and workout logging.
/// </summary>
public static class InvalidToolInputDemo
{
// Throws on an invalid court number so we can demonstrate error-handling strategies.
private static string BookCourt(string memberId, string sport, int courtNumber)
{
if (courtNumber < 1 || courtNumber > 8)
throw new ArgumentOutOfRangeException(nameof(courtNumber),
$"Court number must be between 1 and 8. Received: {courtNumber}.");
return $"Member {memberId} booked court {courtNumber} for {sport}.";
}
public static async Task RunAsync()
{
var apiKey = Environment.GetEnvironmentVariable("OPEN_AI_KEY")
?? throw new InvalidOperationException("Set OPEN_AI_KEY environment variable.");
Console.WriteLine("====== Handle Invalid Tool Input — Fitness Center Assistant ======\n");
IChatClient baseClient = new OpenAIClient(apiKey)
.GetChatClient("gpt-4o-mini")
.AsIChatClient();
AIFunction bookCourt = AIFunctionFactory.Create(
BookCourt,
"BookCourt",
"Books a sports court. memberId is the member ID, sport is the sport name, " +
"courtNumber must be an integer between 1 and 8.");
// -----------------------------------------------------------------------
// Demo 1: IncludeDetailedErrors
// When a function throws, the full exception message is sent back to the model
// so it can see exactly what went wrong and retry with corrected arguments.
// -----------------------------------------------------------------------
Console.WriteLine("--- Demo 1: IncludeDetailedErrors ---");
var clientWithDetails = new FunctionInvokingChatClient(baseClient)
{
IncludeDetailedErrors = true
};
ChatResponse r1 = await clientWithDetails.GetResponseAsync(
"Book court 15 for member M001 to play tennis.",
new ChatOptions { Tools = [bookCourt] });
Console.WriteLine($"Assistant: {r1.Text}");
Console.WriteLine();
// -----------------------------------------------------------------------
// Demo 2: Custom FunctionInvoker
// Replaces the default invocation path with a try/catch that returns
// a descriptive error string instead of letting the exception propagate.
// The model receives the error text and can self-correct.
// -----------------------------------------------------------------------
Console.WriteLine("--- Demo 2: Custom FunctionInvoker ---");
var clientWithCustomInvoker = new FunctionInvokingChatClient(baseClient)
{
FunctionInvoker = async (context, cancellationToken) =>
{
try
{
return await context.Function.InvokeAsync(context.Arguments, cancellationToken);
}
catch (ArgumentOutOfRangeException ex)
{
return $"Invalid argument: {ex.Message} " +
"Please use a courtNumber in the range 1–8 and try again.";
}
catch (JsonException ex)
{
return $"Could not parse arguments: {ex.Message} " +
"Check parameter types and retry.";
}
catch (Exception ex)
{
return $"Function execution failed: {ex.Message}";
}
}
};
ChatResponse r2 = await clientWithCustomInvoker.GetResponseAsync(
"Book court 99 for member M002 for badminton.",
new ChatOptions { Tools = [bookCourt] });
Console.WriteLine($"Assistant: {r2.Text}");
Console.WriteLine();
// -----------------------------------------------------------------------
// Demo 3: Strict JSON Schema (OpenAI only)
// Setting Strict = true in AdditionalProperties tells the OpenAI model to
// adhere exactly to the declared parameter schema, reducing type mismatches.
// -----------------------------------------------------------------------
Console.WriteLine("--- Demo 3: Strict JSON Schema ---");
AIFunction logWorkout = AIFunctionFactory.Create(
(string exerciseType, double durationMinutes, int intensityLevel) =>
{
if (intensityLevel < 1 || intensityLevel > 5)
return $"Invalid intensity level '{intensityLevel}'. Must be 1–5.";
double calories = durationMinutes * intensityLevel * 5.5;
return $"{exerciseType} for {durationMinutes} min at intensity {intensityLevel}: " +
$"~{calories:N0} kcal burned.";
},
new AIFunctionFactoryOptions
{
Name = "LogWorkout",
Description = "Logs a workout session and estimates calories burned. " +
"intensityLevel must be an integer between 1 and 5.",
AdditionalProperties = new Dictionary<string, object?> { { "Strict", true } }
});
IChatClient strictClient = new ChatClientBuilder(baseClient)
.UseFunctionInvocation()
.Build();
ChatResponse r3 = await strictClient.GetResponseAsync(
"Log a 45-minute cycling session for member M003 at intensity level 4.",
new ChatOptions { Tools = [logWorkout] });
Console.WriteLine($"Assistant: {r3.Text}");
Console.WriteLine();
}
}