Table of Contents

Build a Function-Calling Agent

Function calling lets a language model decide which code to run based on a natural language prompt. Instead of parsing user intent manually, the model selects the right function and fills in the parameters. LM-Kit.NET's SingleFunctionCall class maps prompts to C# methods decorated with attributes, handling parameter extraction and invocation. This tutorial builds a function-calling system where the model routes requests to typed methods.


Why Local Function Calling Matters

Two enterprise problems that on-device function calling solves:

  1. Keep business logic private. Function calling sends user requests through your internal methods: database lookups, API calls, calculations. With a local model, the routing decision happens on your infrastructure. No external service sees which functions exist or what parameters are being passed.
  2. Deterministic execution with AI routing. The model decides what to call, but the actual function runs normal C# code. You get the flexibility of natural language input with the reliability of typed method execution. No hallucinated responses for operations that need precise results.

Prerequisites

Requirement Minimum
.NET SDK 8.0+
VRAM 4+ GB
Disk ~3 GB free for model download

Step 1: Create the Project

dotnet new console -n FunctionCallQuickstart
cd FunctionCallQuickstart
dotnet add package LM-Kit.NET

Step 2: Define Functions with Attributes

Create a class with methods decorated with [Description] attributes. The model uses these descriptions to match user intent:

using System.ComponentModel;
using System.Text;
using LMKit.Model;
using LMKit.FunctionCalling;

LMKit.Licensing.LicenseManager.SetLicenseKey("");

Console.InputEncoding = Encoding.UTF8;
Console.OutputEncoding = Encoding.UTF8;

// ──────────────────────────────────────
// 1. Define callable functions
// ──────────────────────────────────────
public class WeatherService
{
    [Description("Get the current weather for a city")]
    public static string GetWeather(
        [Description("The city name")] string city,
        [Description("Temperature unit: celsius or fahrenheit")] string unit = "celsius")
    {
        // In production, call a real weather API
        return $"Weather in {city}: 22 degrees {unit}, partly cloudy.";
    }

    [Description("Get the weather forecast for the next N days")]
    public static string GetForecast(
        [Description("The city name")] string city,
        [Description("Number of days to forecast")] int days = 3)
    {
        return $"{days}-day forecast for {city}: Sunny tomorrow, rain expected in 2 days.";
    }
}

public class CalculatorService
{
    [Description("Calculate the result of a mathematical expression")]
    public static double Calculate(
        [Description("First number")] double a,
        [Description("Mathematical operator: add, subtract, multiply, divide")] string operation,
        [Description("Second number")] double b)
    {
        return operation.ToLower() switch
        {
            "add" => a + b,
            "subtract" => a - b,
            "multiply" => a * b,
            "divide" => b != 0 ? a / b : double.NaN,
            _ => double.NaN
        };
    }
}

Step 3: Route Prompts to Functions

// ──────────────────────────────────────
// 2. Load model
// ──────────────────────────────────────
Console.WriteLine("Loading model...");
using LM model = LM.LoadFromModelID("gemma3:4b",
    downloadingProgress: (_, len, read) =>
    {
        if (len.HasValue) Console.Write($"\r  Downloading: {(double)read / len.Value * 100:F1}%   ");
        return true;
    },
    loadingProgress: p => { Console.Write($"\r  Loading: {p * 100:F0}%   "); return true; });
Console.WriteLine("\n");

// ──────────────────────────────────────
// 3. Register functions and call
// ──────────────────────────────────────
using var functionCall = new SingleFunctionCall(model);

functionCall.ImportFunctions<WeatherService>();
functionCall.ImportFunctions<CalculatorService>();

string[] prompts =
{
    "What's the weather in Paris?",
    "Give me a 5-day forecast for Tokyo",
    "What is 145 multiplied by 23?",
    "How much is 1000 divided by 7?"
};

foreach (string prompt in prompts)
{
    Console.ForegroundColor = ConsoleColor.Green;
    Console.Write("  Prompt:   ");
    Console.ResetColor();
    Console.WriteLine(prompt);

    FunctionCallResult result = functionCall.Submit(prompt);

    Console.ForegroundColor = ConsoleColor.Cyan;
    Console.Write("  Function: ");
    Console.ResetColor();
    Console.WriteLine(result.Method.Name);

    Console.ForegroundColor = ConsoleColor.DarkGray;
    Console.Write("  Params:   ");
    Console.ResetColor();
    Console.WriteLine(string.Join(", ", result.Parameters));

    Console.Write("  Result:   ");
    Console.WriteLine(result.Result);

    Console.ForegroundColor = ConsoleColor.DarkGray;
    Console.WriteLine($"  Confidence: {result.Confidence:P0}\n");
    Console.ResetColor();
}

Step 4: Instance Methods

Register methods from an object instance (for functions that need state or dependencies):

public class OrderService
{
    private readonly Dictionary<string, (string Product, double Price, string Status)> _orders = new()
    {
        ["ORD-001"] = ("Laptop Stand", 49.99, "Shipped"),
        ["ORD-002"] = ("USB-C Hub", 34.99, "Processing"),
        ["ORD-003"] = ("Mechanical Keyboard", 129.99, "Delivered")
    };

    [Description("Look up an order by its order ID")]
    public string GetOrderStatus(
        [Description("The order ID (e.g., ORD-001)")] string orderId)
    {
        if (_orders.TryGetValue(orderId.ToUpper(), out var order))
            return $"Order {orderId}: {order.Product} (${order.Price}) - Status: {order.Status}";
        return $"Order {orderId} not found.";
    }

    [Description("Get the total value of all orders")]
    public string GetOrderTotal()
    {
        double total = _orders.Values.Sum(o => o.Price);
        return $"Total across {_orders.Count} orders: ${total:F2}";
    }
}

// Register with instance
var orderService = new OrderService();

using var functionCall = new SingleFunctionCall(model);
functionCall.ImportFunctions(orderService);

FunctionCallResult result = functionCall.Submit("What's the status of order ORD-002?");
Console.WriteLine(result.Result);

Step 5: Before-Invoke Validation

Intercept function calls before execution to log, validate, or block them:

using var functionCall = new SingleFunctionCall(model);
functionCall.ImportFunctions<WeatherService>();
functionCall.ImportFunctions<CalculatorService>();

functionCall.BeforeMethodInvoke += (sender, e) =>
{
    Console.ForegroundColor = ConsoleColor.DarkYellow;
    Console.WriteLine($"  [Intercepted] About to call: {e.Method.Name}");
    Console.WriteLine($"  [Intercepted] Parameters: {string.Join(", ", e.Parameters)}");
    Console.ResetColor();

    // Block specific functions if needed
    // e.Cancel = true;
};

FunctionCallResult result = functionCall.Submit("What's the weather in London?");
Console.WriteLine($"  Result: {result.Result}");

Step 6: Force Function Selection

By default, the model can decline to call any function if no match is found. Use ForceFunctionSelection to always route to the closest function:

// Default: model may not select a function for irrelevant prompts
using var flexible = new SingleFunctionCall(model)
{
    InvokeFunctions = true,
    ForceFunctionSelection = false
};
flexible.ImportFunctions<WeatherService>();

// Forced: always selects the closest function, even for edge cases
using var forced = new SingleFunctionCall(model)
{
    InvokeFunctions = true,
    ForceFunctionSelection = true
};
forced.ImportFunctions<WeatherService>();

Set InvokeFunctions = false to see which function the model would select without actually executing it:

using var dryRun = new SingleFunctionCall(model)
{
    InvokeFunctions = false  // Don't execute, just select
};
dryRun.ImportFunctions<WeatherService>();
dryRun.ImportFunctions<CalculatorService>();

FunctionCallResult result = dryRun.Submit("Multiply 5 by 10");

Console.WriteLine($"Would call: {result.Method.Name}");
Console.WriteLine($"With params: {string.Join(", ", result.Parameters)}");
Console.WriteLine($"Result is null (not executed): {result.Result == null}");

Step 7: Interactive Function-Calling Loop

using var functionCall = new SingleFunctionCall(model);
functionCall.ImportFunctions<WeatherService>();
functionCall.ImportFunctions<CalculatorService>();
functionCall.ImportFunctions(new OrderService());

Console.WriteLine("Available functions: GetWeather, GetForecast, Calculate, GetOrderStatus, GetOrderTotal");
Console.WriteLine("Ask anything in natural language (or 'quit' to exit):\n");

while (true)
{
    Console.ForegroundColor = ConsoleColor.Green;
    Console.Write("You: ");
    Console.ResetColor();

    string? input = Console.ReadLine();
    if (string.IsNullOrWhiteSpace(input) || input.Equals("quit", StringComparison.OrdinalIgnoreCase))
        break;

    try
    {
        FunctionCallResult result = functionCall.Submit(input);

        Console.ForegroundColor = ConsoleColor.Cyan;
        Console.Write($"  [{result.Method.Name}] ");
        Console.ResetColor();
        Console.WriteLine(result.Result);
        Console.ForegroundColor = ConsoleColor.DarkGray;
        Console.WriteLine($"  Confidence: {result.Confidence:P0}\n");
        Console.ResetColor();
    }
    catch (Exception ex)
    {
        Console.ForegroundColor = ConsoleColor.Red;
        Console.WriteLine($"  Error: {ex.Message}\n");
        Console.ResetColor();
    }
}

Common Issues

Problem Cause Fix
Wrong function selected Descriptions too similar Make [Description] attributes more distinct; add parameter descriptions
Parameters parsed incorrectly Ambiguous parameter types Use specific types (int, double) instead of string where possible
No function selected Prompt too unrelated to any function Enable ForceFunctionSelection = true or add a catch-all function
Low confidence Model uncertain about the match Use a larger model; improve function descriptions
Method not found Static methods not imported correctly Use ImportFunctions<T>() for static methods, ImportFunctions(instance) for instance methods

Next Steps