Learnixo

AI for .NET Developers · Lesson 4 of 6

Function Calling and Tool Use in AI Apps

How Function Calling Works

Traditional prompt:
  User: "What is the INR for patient MRN001?"
  AI: "I don't have access to patient records." (or worse, hallucinates a value)

With function calling:
  User: "What is the INR for patient MRN001?"
  AI: "I need to look that up. [calls get_patient_inr(mrn="MRN001")]"
  Your code: fetches real INR from the database, returns "2.4"
  AI: "The latest INR for patient MRN001 is 2.4, recorded on 14 March 2026."

Function calling enables:
  → AI answers questions with real data (not training data or hallucinations)
  → AI can take actions (create appointments, update records) when instructed
  → Complex multi-step reasoning: AI calls multiple functions and synthesises results
  → Grounded responses: the AI can only say what the functions return

Defining Functions with Semantic Kernel

C#
// The recommended approach in .NET — Semantic Kernel handles serialization

public sealed class ClinicalDataPlugin
{
    private readonly ILabResultsRepository _labRepository;

    public ClinicalDataPlugin(ILabResultsRepository labRepository) =>
        _labRepository = labRepository;

    [KernelFunction("get_latest_inr")]
    [Description("Retrieves the most recent INR blood test result for a patient.")]
    [return: Description("The latest INR value and when it was recorded, or null if no result exists.")]
    public async Task<InrResultDto?> GetLatestInrAsync(
        [Description("The patient's MRN (Medical Record Number), e.g. MRN-001")] string mrn,
        CancellationToken ct = default)
    {
        var result = await _labRepository.GetLatestInrAsync(PatientMrn.Of(mrn), ct);
        if (result is null) return null;

        return new InrResultDto(result.Value, result.RecordedAt, result.RecordedBy);
    }

    [KernelFunction("get_medication_list")]
    [Description("Returns all active medications for a patient including doses and frequencies.")]
    public async Task<IReadOnlyList<MedicationDto>> GetMedicationsAsync(
        [Description("Patient MRN")] string mrn,
        CancellationToken ct = default)
    {
        var prescriptions = await _labRepository.GetActivePrescriptionsAsync(
            PatientMrn.Of(mrn), ct);

        return prescriptions
            .Select(p => new MedicationDto(p.MedicationName.Value, p.DoseAmount, p.DoseUnit))
            .ToList();
    }
}

// Return types are automatically serialised to JSON for the AI:
public sealed record InrResultDto(decimal Value, DateTime RecordedAt, string RecordedBy);
public sealed record MedicationDto(string Name, decimal DoseAmount, string DoseUnit);

Multi-Step Function Calling

C#
// The AI can call multiple functions in sequence to answer a complex question
// "Is this patient's Warfarin dose appropriate given their recent INR?"

var chat    = kernel.GetRequiredService<IChatCompletionService>();
var history = new ChatHistory("""
    You are a clinical pharmacist assistant. You have access to patient data.
    When asked about medication appropriateness, check:
    1. The current medication and dose
    2. The recent INR history (last 3 results)
    3. Provide an assessment based on standard Warfarin dosing guidelines
    Always state that the prescriber must make the final clinical decision.
    """);

history.AddUserMessage(
    "Is the Warfarin dose appropriate for patient MRN-001 given their recent INR results?");

// With AutoInvokeKernelFunctions, the AI calls get_medication_list and get_latest_inr
// then synthesises a response
var settings = new OpenAIPromptExecutionSettings
{
    ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions,
    Temperature      = 0.1
};

var response = await chat.GetChatMessageContentAsync(history, settings, kernel, ct);

// AI response:
// "Patient MRN-001 is currently on Warfarin 5mg daily.
//  Their last three INR values: 2.3 (10 Mar), 1.8 (3 Mar), 2.1 (24 Feb).
//  The most recent INR of 2.3 is within the therapeutic range of 2.0–3.0.
//  The previous result of 1.8 was slightly sub-therapeutic.
//  Based on the trend toward the therapeutic range, the current dose appears appropriate.
//  However, the prescriber must review and make the final clinical decision."

Low-Level OpenAI Function Calling (Without Semantic Kernel)

C#
// Direct OpenAI SDK if you prefer more control
// NuGet: Azure.AI.OpenAI

var tools = new List<ChatTool>
{
    ChatTool.CreateFunctionTool(
        functionName:        "get_latest_inr",
        functionDescription: "Gets the most recent INR result for a patient",
        functionParameters: BinaryData.FromString("""
            {
              "type": "object",
              "properties": {
                "mrn": {
                  "type": "string",
                  "description": "Patient MRN"
                }
              },
              "required": ["mrn"]
            }
            """))
};

var messages = new List<ChatRequestMessage>
{
    new ChatRequestSystemMessage("You are a pharmacist assistant with access to patient data."),
    new ChatRequestUserMessage("What is the INR for MRN-001?")
};

var response = await openAiClient.GetChatCompletionsAsync(
    new ChatCompletionsOptions("gpt-4o", messages)
    {
        Tools = tools,
        ToolChoice = ChatCompletionsToolChoice.Auto
    });

// Check if the AI wants to call a function
var completion = response.Value.Choices[0];
if (completion.FinishReason == CompletionsFinishReason.ToolCalls)
{
    var toolCall = completion.Message.ToolCalls[0];
    var args     = JsonDocument.Parse(toolCall.Function.Arguments);
    var mrn      = args.RootElement.GetProperty("mrn").GetString()!;

    // Call your function:
    var inr = await labRepository.GetLatestInrAsync(PatientMrn.Of(mrn), ct);
    var result = inr?.ToString() ?? "No INR result found";

    // Return result to the AI:
    messages.Add(new ChatRequestAssistantMessage(completion.Message));
    messages.Add(new ChatRequestToolMessage(result, toolCall.Id));

    // Get the final response:
    var finalResponse = await openAiClient.GetChatCompletionsAsync(...);
}

Safety Considerations for Clinical Function Calling

Read-only vs Write functions:

Read-only (safe to auto-invoke):
  get_latest_inr, get_medication_list, get_patient_summary
  → AI can call these automatically with no risk
  → Wrong answer from AI is corrected by what the function returns

Write functions (require explicit confirmation):
  create_prescription, update_dose, discharge_patient
  → NEVER auto-invoke without explicit user confirmation
  → Use a two-step pattern: AI proposes the action, user confirms, action executes

Example safe pattern for write actions:
  1. User: "Can you create a Warfarin prescription for MRN-001 at 5mg daily?"
  2. AI: "I can create a Warfarin 5mg daily prescription for MRN-001.
          Please confirm this action." (does NOT call the function yet)
  3. User: "Confirm"
  4. System: validates confirmation, calls create_prescription

Prompt instruction:
  "For any action that creates, modifies, or deletes clinical data,
   describe the action and ask for explicit confirmation before executing.
   Never execute write actions without a clear confirmation from the user."

Production issue I've seen: A team deployed a function-calling AI assistant with write access to the prescription database. The AI was configured with ToolCallBehavior.AutoInvokeKernelFunctions — it would invoke any function automatically. A pharmacist asked "What would the dose be if we increased Warfarin to 7mg for MRN-001?" The AI interpreted this as an instruction, called update_prescription_dose(mrn="MRN-001", dose=7), and updated the prescription. The pharmacist had asked a hypothetical question — they were not authorising the change. Separating read and write functions, and requiring explicit confirmation steps for writes, prevents this class of unintended actions.


Key Takeaway

Function calling lets the AI call your .NET functions to retrieve real data or take actions. Define functions with [KernelFunction] and [Description] attributes — the AI reads descriptions to decide when to call each function. Use AutoInvokeKernelFunctions for read-only functions. Never auto-invoke write functions (create, update, delete) — always require explicit user confirmation before executing. The AI's answer is only as reliable as the data your functions return — grounded responses eliminate hallucination of clinical data.