Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
Build an AI assistant that goes beyond conversation — it can call functions to perform actions. The assistant decides when a function is needed, you execute it, and feed the result back. Everything runs locally with the Foundry Local SDK.
In this tutorial, you learn how to:
- Set up a project and install the Foundry Local SDK
- Define tools the assistant can call
- Send a message that triggers tool use
- Execute the tool and return results to the model
- Handle the complete tool calling loop
- Clean up resources
Prerequisites
- A Windows, macOS, or Linux computer with at least 8 GB of RAM.
Samples repository
You can find the complete sample code for this article in the Foundry samples GitHub repository. To clone the repository and navigate to the sample, use:
git clone https://github.com/microsoft-foundry/foundry-samples.git
cd foundry-samples/samples/csharp/foundry-local/tutorial-tool-calling
Install packages
dotnet add package Microsoft.AI.Foundry.Local
dotnet add package OpenAI
The C# samples in the GitHub repository are preconfigured projects. If you're building from scratch, you should read the Foundry Local SDK reference for more details on how to set up your C# project with Foundry Local.
Define tools
Tool calling lets the model request that your code runs a function and returns the result. You define the available tools as a list of JSON schemas that describe each function's name, purpose, and parameters.
Open
Program.csand add the following tool definitions:// --- Tool definitions and implementations --- var toolDefinitions = new[] { new { Name = "get_weather", Description = "Get the current weather for a location", Schema = """ { "type": "object", "properties": { "location": { "type": "string", "description": "The city or location" }, "unit": { "type": "string", "description": "Temperature unit", "enum": ["celsius", "fahrenheit"] } }, "required": ["location"] } """ }, new { Name = "calculate", Description = "Perform a math calculation", Schema = """ { "type": "object", "properties": { "expression": { "type": "string", "description": "The math expression to evaluate" } }, "required": ["expression"] } """ } }; string ExecuteTool(string functionName, JsonElement arguments) { switch (functionName) { case "get_weather": var location = arguments.GetProperty("location").GetString() ?? "unknown"; var unit = arguments.TryGetProperty("unit", out var u) ? u.GetString() ?? "celsius" : "celsius"; var temp = unit == "celsius" ? 22 : 72; return JsonSerializer.Serialize(new { location, temperature = temp, unit, condition = "Sunny" }); case "calculate": var expression = arguments.GetProperty("expression").GetString() ?? ""; try { var result = new System.Data.DataTable().Compute(expression, null); return JsonSerializer.Serialize(new { expression, result = result?.ToString() }); } catch (Exception ex) { return JsonSerializer.Serialize(new { error = ex.Message }); } default: return JsonSerializer.Serialize(new { error = $"Unknown function: {functionName}" }); } }Each tool definition includes a
name, adescriptionthat helps the model decide when to use it, and aparametersschema that describes the expected input.Add the C# methods that implement each tool:
// --- Tool definitions and implementations --- var toolDefinitions = new[] { new { Name = "get_weather", Description = "Get the current weather for a location", Schema = """ { "type": "object", "properties": { "location": { "type": "string", "description": "The city or location" }, "unit": { "type": "string", "description": "Temperature unit", "enum": ["celsius", "fahrenheit"] } }, "required": ["location"] } """ }, new { Name = "calculate", Description = "Perform a math calculation", Schema = """ { "type": "object", "properties": { "expression": { "type": "string", "description": "The math expression to evaluate" } }, "required": ["expression"] } """ } }; string ExecuteTool(string functionName, JsonElement arguments) { switch (functionName) { case "get_weather": var location = arguments.GetProperty("location").GetString() ?? "unknown"; var unit = arguments.TryGetProperty("unit", out var u) ? u.GetString() ?? "celsius" : "celsius"; var temp = unit == "celsius" ? 22 : 72; return JsonSerializer.Serialize(new { location, temperature = temp, unit, condition = "Sunny" }); case "calculate": var expression = arguments.GetProperty("expression").GetString() ?? ""; try { var result = new System.Data.DataTable().Compute(expression, null); return JsonSerializer.Serialize(new { expression, result = result?.ToString() }); } catch (Exception ex) { return JsonSerializer.Serialize(new { error = ex.Message }); } default: return JsonSerializer.Serialize(new { error = $"Unknown function: {functionName}" }); } }The model doesn't run these functions directly. It returns a tool call request with the function name and arguments, and your code executes the function.
Send a message that triggers tool use
Initialize the Foundry Local SDK, load a model, and send a message that the model can answer by calling a tool.
// --- Main application ---
var config = new Configuration
{
AppName = "foundry_local_samples",
LogLevel = Microsoft.AI.Foundry.Local.LogLevel.Information
};
using var loggerFactory = LoggerFactory.Create(builder =>
{
builder.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Information);
});
var logger = loggerFactory.CreateLogger<Program>();
await FoundryLocalManager.CreateAsync(config, logger);
var mgr = FoundryLocalManager.Instance;
var currentEp = "";
await mgr.DownloadAndRegisterEpsAsync((epName, percent) =>
{
if (epName != currentEp)
{
if (currentEp != "") Console.WriteLine();
currentEp = epName;
}
Console.Write($"\r {epName.PadRight(30)} {percent,6:F1}%");
});
if (currentEp != "") Console.WriteLine();
var catalog = await mgr.GetCatalogAsync();
var model = await catalog.GetModelAsync("qwen2.5-0.5b")
?? throw new Exception("Model not found");
await model.DownloadAsync(progress =>
{
Console.Write($"\rDownloading model: {progress:F2}%");
if (progress >= 100f) Console.WriteLine();
});
await model.LoadAsync();
Console.WriteLine("Model loaded and ready.");
using var chatSession = new ChatSession(model);
chatSession.SetOptions(new RequestOptions { ToolChoice = ToolChoice.Auto });
foreach (var tool in toolDefinitions)
{
chatSession.AddToolDefinition(tool.Name, tool.Description, tool.Schema);
}
When the model determines that a tool is needed, the response contains ToolCalls instead of a regular text message. The next step shows how to detect and handle these calls.
Execute the tool and return results
After the model responds with a tool call, you extract the function name and arguments, run the function, and send the result back.
Console.WriteLine("\nTool-calling assistant ready! Type 'quit' to exit.\n");
while (true)
{
Console.Write("You: ");
var userInput = Console.ReadLine();
if (string.IsNullOrWhiteSpace(userInput) ||
userInput.Equals("quit", StringComparison.OrdinalIgnoreCase) ||
userInput.Equals("exit", StringComparison.OrdinalIgnoreCase))
{
break;
}
using var request = new Request();
if (chatSession.TurnCount == 0)
{
request.AddItem(MessageItem.System(
"You are a helpful assistant with access to tools. Use them when needed."));
}
request.AddItem(MessageItem.User(userInput));
using var response = await chatSession.ProcessRequestAsync(request, ct);
var toolCalls = response.OfType<ToolCallItem>()
.Select(call => (call.CallId, call.Name, call.Arguments))
.ToList();
if (toolCalls.Count > 0)
{
using var toolRequest = new Request();
foreach (var toolCall in toolCalls)
{
using var argumentsDocument = JsonDocument.Parse(toolCall.Arguments);
var arguments = argumentsDocument.RootElement;
Console.WriteLine($" Tool call: {toolCall.Name}({arguments})");
var result = ExecuteTool(toolCall.Name, arguments);
toolRequest.AddItem(new ToolResultItem(toolCall.CallId, result));
}
using var finalResponse = await chatSession.ProcessRequestAsync(toolRequest, ct);
var answer = GetResponseText(finalResponse);
Console.WriteLine($"Assistant: {answer}\n");
}
else
{
Console.WriteLine($"Assistant: {GetResponseText(response)}\n");
}
}
chatSession.Dispose();
await model.UnloadAsync();
Console.WriteLine("Model unloaded. Goodbye!");
The key steps in the tool calling loop are:
- Detect tool calls — check
response.Choices[0].Message.ToolCalls. - Execute the function — parse the arguments and call your local function.
- Return the result — add a message with role
tooland the matchingToolCallId. - Get the final answer — the model uses the tool result to generate a natural response.
Handle the complete tool calling loop
Here's the complete application that combines tool definitions, SDK initialization, and the tool calling loop into a single runnable file.
Replace the contents of Program.cs with the following complete code:
using System.Text.Json;
using Microsoft.AI.Foundry.Local;
using Microsoft.Extensions.Logging;
CancellationToken ct = CancellationToken.None;
// --- Tool definitions and implementations ---
var toolDefinitions = new[]
{
new
{
Name = "get_weather",
Description = "Get the current weather for a location",
Schema =
"""
{
"type": "object",
"properties": {
"location": { "type": "string", "description": "The city or location" },
"unit": {
"type": "string",
"description": "Temperature unit",
"enum": ["celsius", "fahrenheit"]
}
},
"required": ["location"]
}
"""
},
new
{
Name = "calculate",
Description = "Perform a math calculation",
Schema =
"""
{
"type": "object",
"properties": {
"expression": { "type": "string", "description": "The math expression to evaluate" }
},
"required": ["expression"]
}
"""
}
};
string ExecuteTool(string functionName, JsonElement arguments)
{
switch (functionName)
{
case "get_weather":
var location = arguments.GetProperty("location").GetString() ?? "unknown";
var unit = arguments.TryGetProperty("unit", out var u)
? u.GetString() ?? "celsius"
: "celsius";
var temp = unit == "celsius" ? 22 : 72;
return JsonSerializer.Serialize(new
{
location,
temperature = temp,
unit,
condition = "Sunny"
});
case "calculate":
var expression = arguments.GetProperty("expression").GetString() ?? "";
try
{
var result = new System.Data.DataTable().Compute(expression, null);
return JsonSerializer.Serialize(new { expression, result = result?.ToString() });
}
catch (Exception ex)
{
return JsonSerializer.Serialize(new { error = ex.Message });
}
default:
return JsonSerializer.Serialize(new { error = $"Unknown function: {functionName}" });
}
}
// --- Main application ---
var config = new Configuration
{
AppName = "foundry_local_samples",
LogLevel = Microsoft.AI.Foundry.Local.LogLevel.Information
};
using var loggerFactory = LoggerFactory.Create(builder =>
{
builder.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Information);
});
var logger = loggerFactory.CreateLogger<Program>();
await FoundryLocalManager.CreateAsync(config, logger);
var mgr = FoundryLocalManager.Instance;
var currentEp = "";
await mgr.DownloadAndRegisterEpsAsync((epName, percent) =>
{
if (epName != currentEp)
{
if (currentEp != "") Console.WriteLine();
currentEp = epName;
}
Console.Write($"\r {epName.PadRight(30)} {percent,6:F1}%");
});
if (currentEp != "") Console.WriteLine();
var catalog = await mgr.GetCatalogAsync();
var model = await catalog.GetModelAsync("qwen2.5-0.5b")
?? throw new Exception("Model not found");
await model.DownloadAsync(progress =>
{
Console.Write($"\rDownloading model: {progress:F2}%");
if (progress >= 100f) Console.WriteLine();
});
await model.LoadAsync();
Console.WriteLine("Model loaded and ready.");
using var chatSession = new ChatSession(model);
chatSession.SetOptions(new RequestOptions { ToolChoice = ToolChoice.Auto });
foreach (var tool in toolDefinitions)
{
chatSession.AddToolDefinition(tool.Name, tool.Description, tool.Schema);
}
Console.WriteLine("\nTool-calling assistant ready! Type 'quit' to exit.\n");
while (true)
{
Console.Write("You: ");
var userInput = Console.ReadLine();
if (string.IsNullOrWhiteSpace(userInput) ||
userInput.Equals("quit", StringComparison.OrdinalIgnoreCase) ||
userInput.Equals("exit", StringComparison.OrdinalIgnoreCase))
{
break;
}
using var request = new Request();
if (chatSession.TurnCount == 0)
{
request.AddItem(MessageItem.System(
"You are a helpful assistant with access to tools. Use them when needed."));
}
request.AddItem(MessageItem.User(userInput));
using var response = await chatSession.ProcessRequestAsync(request, ct);
var toolCalls = response.OfType<ToolCallItem>()
.Select(call => (call.CallId, call.Name, call.Arguments))
.ToList();
if (toolCalls.Count > 0)
{
using var toolRequest = new Request();
foreach (var toolCall in toolCalls)
{
using var argumentsDocument = JsonDocument.Parse(toolCall.Arguments);
var arguments = argumentsDocument.RootElement;
Console.WriteLine($" Tool call: {toolCall.Name}({arguments})");
var result = ExecuteTool(toolCall.Name, arguments);
toolRequest.AddItem(new ToolResultItem(toolCall.CallId, result));
}
using var finalResponse = await chatSession.ProcessRequestAsync(toolRequest, ct);
var answer = GetResponseText(finalResponse);
Console.WriteLine($"Assistant: {answer}\n");
}
else
{
Console.WriteLine($"Assistant: {GetResponseText(response)}\n");
}
}
chatSession.Dispose();
await model.UnloadAsync();
Console.WriteLine("Model unloaded. Goodbye!");
Run the tool-calling assistant:
dotnet run
You see output similar to:
Downloading model: 100.00%
Model loaded and ready.
Tool-calling assistant ready! Type 'quit' to exit.
You: What's the weather like today?
Tool call: get_weather({"location":"current location"})
Assistant: The weather today is sunny with a temperature of 22°C.
You: What is 245 * 38?
Tool call: calculate({"expression":"245 * 38"})
Assistant: 245 multiplied by 38 equals 9,310.
You: quit
Model unloaded. Goodbye!
The model decides when to call a tool based on the user's message. For a weather question it calls get_weather, for math it calls calculate, and for general questions it responds directly without any tool calls.
Samples repository
The complete sample code for this article is available in the foundry-samples GitHub repository. To clone the repository and navigate to the sample use:
git clone https://github.com/microsoft-foundry/foundry-samples.git
cd foundry-samples/samples/javascript/foundry-local/tutorial-tool-calling
Install packages
npm install foundry-local-sdk openai
Define tools
Tool calling lets the model request that your code runs a function and returns the result. You define the available tools as a list of JSON schemas that describe each function's name, purpose, and parameters.
Create a file called
index.js.Add the following tool definitions:
// --- Tool definitions --- const tools = [ { name: 'get_weather', description: 'Get the current weather for a location', jsonSchema: JSON.stringify({ type: 'object', properties: { location: { type: 'string', description: 'The city or location' }, unit: { type: 'string', enum: ['celsius', 'fahrenheit'], description: 'Temperature unit' } }, required: ['location'] }) }, { name: 'calculate', description: 'Perform a math calculation', jsonSchema: JSON.stringify({ type: 'object', properties: { expression: { type: 'string', description: 'The math expression to evaluate' } }, required: ['expression'] }) } ]; // --- Tool implementations --- function getWeather(location, unit = 'celsius') { return { location, temperature: unit === 'celsius' ? 22 : 72, unit, condition: 'Sunny' }; } function calculate(expression) { // Input is validated against a strict allowlist of numeric/math characters, // making this safe from code injection in this tutorial context. const allowed = /^[0-9+\-*/(). ]+$/; if (!allowed.test(expression)) { return { error: 'Invalid expression' }; } try { const result = Function( `"use strict"; return (${expression})` )(); return { expression, result }; } catch (err) { return { error: err.message }; } } const toolFunctions = { get_weather: (args) => getWeather(args.location, args.unit), calculate: (args) => calculate(args.expression) };Each tool definition includes a
name, adescriptionthat helps the model decide when to use it, and aparametersschema that describes the expected input.Add the JavaScript functions that implement each tool:
// --- Tool definitions --- const tools = [ { name: 'get_weather', description: 'Get the current weather for a location', jsonSchema: JSON.stringify({ type: 'object', properties: { location: { type: 'string', description: 'The city or location' }, unit: { type: 'string', enum: ['celsius', 'fahrenheit'], description: 'Temperature unit' } }, required: ['location'] }) }, { name: 'calculate', description: 'Perform a math calculation', jsonSchema: JSON.stringify({ type: 'object', properties: { expression: { type: 'string', description: 'The math expression to evaluate' } }, required: ['expression'] }) } ]; // --- Tool implementations --- function getWeather(location, unit = 'celsius') { return { location, temperature: unit === 'celsius' ? 22 : 72, unit, condition: 'Sunny' }; } function calculate(expression) { // Input is validated against a strict allowlist of numeric/math characters, // making this safe from code injection in this tutorial context. const allowed = /^[0-9+\-*/(). ]+$/; if (!allowed.test(expression)) { return { error: 'Invalid expression' }; } try { const result = Function( `"use strict"; return (${expression})` )(); return { expression, result }; } catch (err) { return { error: err.message }; } } const toolFunctions = { get_weather: (args) => getWeather(args.location, args.unit), calculate: (args) => calculate(args.expression) };The model doesn't run these functions directly. It returns a tool call request with the function name and arguments, and your code executes the function.
Send a message that triggers tool use
Initialize the Foundry Local SDK, load a model, and send a message that the model can answer by calling a tool.
// --- Main application ---
const manager = FoundryLocalManager.create({
appName: 'foundry_local_samples',
logLevel: 'info'
});
// Download and register all execution providers.
let currentEp = '';
await manager.downloadAndRegisterEps((epName, percent) => {
if (epName !== currentEp) {
if (currentEp !== '') process.stdout.write('\n');
currentEp = epName;
}
process.stdout.write(`\r ${epName.padEnd(30)} ${percent.toFixed(1).padStart(5)}%`);
});
if (currentEp !== '') process.stdout.write('\n');
const model = await manager.catalog.getModel('qwen2.5-0.5b');
await model.download((progress) => {
process.stdout.write(
`\rDownloading model: ${progress.toFixed(2)}%`
);
});
console.log('\nModel downloaded.');
await model.load();
console.log('Model loaded and ready.');
const chatSession = new ChatSession(model);
for (const tool of tools) {
chatSession.addToolDefinition(tool);
}
let firstTurn = true;
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const askQuestion = (prompt) =>
new Promise((resolve) => {
if (rl.closed) return resolve('quit');
const onClose = () => resolve('quit');
rl.once('close', onClose);
rl.question(prompt, (answer) => {
rl.off('close', onClose);
resolve(answer);
});
});
console.log(
'\nTool-calling assistant ready! Type \'quit\' to exit.\n'
);
while (true) {
const userInput = await askQuestion('You: ');
if (
userInput.trim().toLowerCase() === 'quit' ||
userInput.trim().toLowerCase() === 'exit'
) {
break;
}
const request = new Request().setOptions({ toolChoice: 'auto' });
if (firstTurn) {
request.addItem(Item.systemMessage(
'You are a helpful assistant with access to tools. ' +
'Use them when needed to answer questions accurately.'
));
firstTurn = false;
}
request.addItem(Item.userMessage(userInput));
const response = await chatSession.processRequest(request);
const answer = await processToolCalls(
response, chatSession
);
console.log(`Assistant: ${answer}\n`);
}
chatSession.dispose();
await model.unload();
console.log('Model unloaded. Goodbye!');
rl.close();
When the model determines that a tool is needed, the response contains tool_calls instead of a regular text message. The next step shows how to detect and handle these calls.
Execute the tool and return results
After the model responds with a tool call, you extract the function name and arguments, run the function, and send the result back.
async function processToolCalls(response, chatSession) {
let currentResponse = response;
while (currentResponse.finishReason === 'toolCalls') {
const toolResults = [];
for (const toolCall of currentResponse.output.filter(
(item) => item.type === 'toolCall'
)) {
const functionName = toolCall.name;
const args = JSON.parse(toolCall.arguments);
console.log(
` Tool call: ${functionName}` +
`(${JSON.stringify(args)})`
);
const implementation = toolFunctions[functionName];
const result = implementation
? implementation(args)
: { error: `Unknown tool: ${functionName}` };
toolResults.push(Item.toolResult(
toolCall.callId,
JSON.stringify(result)
));
}
if (toolResults.length === 0) {
throw new Error('The model requested tools but returned no tool calls.');
}
const followUp = new Request()
.setOptions({ toolChoice: 'auto' });
for (const result of toolResults) {
followUp.addItem(result);
}
currentResponse = await chatSession.processRequest(followUp);
}
return currentResponse.output
.map((item) => item.type === 'message'
? item.content ?? item.parts?.map((part) => part.text ?? '').join('') ?? ''
: item.type === 'text' ? item.text : '')
.join('');
}
The key steps in the tool calling loop are:
- Detect tool calls — check
response.choices[0]?.message?.tool_calls. - Execute the function — parse the arguments and call your local function.
- Return the result — add a message with role
tooland the matchingtool_call_id. - Get the final answer — the model uses the tool result to generate a natural response.
Handle the complete tool calling loop
Here's the complete application that combines tool definitions, SDK initialization, and the tool calling loop into a single runnable file.
Create a file named index.js and add the following complete code:
import {
ChatSession,
FoundryLocalManager,
Item,
Request
} from 'foundry-local-sdk';
import * as readline from 'readline';
// --- Tool definitions ---
const tools = [
{
name: 'get_weather',
description: 'Get the current weather for a location',
jsonSchema: JSON.stringify({
type: 'object',
properties: {
location: {
type: 'string',
description: 'The city or location'
},
unit: {
type: 'string',
enum: ['celsius', 'fahrenheit'],
description: 'Temperature unit'
}
},
required: ['location']
})
},
{
name: 'calculate',
description: 'Perform a math calculation',
jsonSchema: JSON.stringify({
type: 'object',
properties: {
expression: {
type: 'string',
description: 'The math expression to evaluate'
}
},
required: ['expression']
})
}
];
// --- Tool implementations ---
function getWeather(location, unit = 'celsius') {
return {
location,
temperature: unit === 'celsius' ? 22 : 72,
unit,
condition: 'Sunny'
};
}
function calculate(expression) {
// Input is validated against a strict allowlist of numeric/math characters,
// making this safe from code injection in this tutorial context.
const allowed = /^[0-9+\-*/(). ]+$/;
if (!allowed.test(expression)) {
return { error: 'Invalid expression' };
}
try {
const result = Function(
`"use strict"; return (${expression})`
)();
return { expression, result };
} catch (err) {
return { error: err.message };
}
}
const toolFunctions = {
get_weather: (args) => getWeather(args.location, args.unit),
calculate: (args) => calculate(args.expression)
};
async function processToolCalls(response, chatSession) {
let currentResponse = response;
while (currentResponse.finishReason === 'toolCalls') {
const toolResults = [];
for (const toolCall of currentResponse.output.filter(
(item) => item.type === 'toolCall'
)) {
const functionName = toolCall.name;
const args = JSON.parse(toolCall.arguments);
console.log(
` Tool call: ${functionName}` +
`(${JSON.stringify(args)})`
);
const implementation = toolFunctions[functionName];
const result = implementation
? implementation(args)
: { error: `Unknown tool: ${functionName}` };
toolResults.push(Item.toolResult(
toolCall.callId,
JSON.stringify(result)
));
}
if (toolResults.length === 0) {
throw new Error('The model requested tools but returned no tool calls.');
}
const followUp = new Request()
.setOptions({ toolChoice: 'auto' });
for (const result of toolResults) {
followUp.addItem(result);
}
currentResponse = await chatSession.processRequest(followUp);
}
return currentResponse.output
.map((item) => item.type === 'message'
? item.content ?? item.parts?.map((part) => part.text ?? '').join('') ?? ''
: item.type === 'text' ? item.text : '')
.join('');
}
// --- Main application ---
const manager = FoundryLocalManager.create({
appName: 'foundry_local_samples',
logLevel: 'info'
});
// Download and register all execution providers.
let currentEp = '';
await manager.downloadAndRegisterEps((epName, percent) => {
if (epName !== currentEp) {
if (currentEp !== '') process.stdout.write('\n');
currentEp = epName;
}
process.stdout.write(`\r ${epName.padEnd(30)} ${percent.toFixed(1).padStart(5)}%`);
});
if (currentEp !== '') process.stdout.write('\n');
const model = await manager.catalog.getModel('qwen2.5-0.5b');
await model.download((progress) => {
process.stdout.write(
`\rDownloading model: ${progress.toFixed(2)}%`
);
});
console.log('\nModel downloaded.');
await model.load();
console.log('Model loaded and ready.');
const chatSession = new ChatSession(model);
for (const tool of tools) {
chatSession.addToolDefinition(tool);
}
let firstTurn = true;
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const askQuestion = (prompt) =>
new Promise((resolve) => {
if (rl.closed) return resolve('quit');
const onClose = () => resolve('quit');
rl.once('close', onClose);
rl.question(prompt, (answer) => {
rl.off('close', onClose);
resolve(answer);
});
});
console.log(
'\nTool-calling assistant ready! Type \'quit\' to exit.\n'
);
while (true) {
const userInput = await askQuestion('You: ');
if (
userInput.trim().toLowerCase() === 'quit' ||
userInput.trim().toLowerCase() === 'exit'
) {
break;
}
const request = new Request().setOptions({ toolChoice: 'auto' });
if (firstTurn) {
request.addItem(Item.systemMessage(
'You are a helpful assistant with access to tools. ' +
'Use them when needed to answer questions accurately.'
));
firstTurn = false;
}
request.addItem(Item.userMessage(userInput));
const response = await chatSession.processRequest(request);
const answer = await processToolCalls(
response, chatSession
);
console.log(`Assistant: ${answer}\n`);
}
chatSession.dispose();
await model.unload();
console.log('Model unloaded. Goodbye!');
rl.close();
Run the tool-calling assistant:
node index.js
You see output similar to:
Downloading model: 100.00%
Model downloaded.
Model loaded and ready.
Tool-calling assistant ready! Type 'quit' to exit.
You: What's the weather like today?
Tool call: get_weather({"location":"current location"})
Assistant: The weather today is sunny with a temperature of 22°C.
You: What is 245 * 38?
Tool call: calculate({"expression":"245 * 38"})
Assistant: 245 multiplied by 38 equals 9,310.
You: quit
Model unloaded. Goodbye!
The model decides when to call a tool based on the user's message. For a weather question it calls get_weather, for math it calls calculate, and for general questions it responds directly without any tool calls.
Samples repository
The complete sample code for this article is available in the foundry-samples GitHub repository. To clone the repository and navigate to the sample use:
git clone https://github.com/microsoft-foundry/foundry-samples.git
cd foundry-samples/samples/python/foundry-local/tutorial-tool-calling
Install packages
pip install foundry-local-sdk openai
Define tools
Tool calling lets the model request that your code runs a function and returns the result. You define the available tools as a list of JSON schemas that describe each function's name, purpose, and parameters.
Create a file called main.py and add the following tool definitions:
# --- Tool definitions ---
tools = [
{
"name": "get_weather",
"description": "Get the current weather for a location",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city or location",
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit",
},
},
"required": ["location"],
},
},
{
"name": "calculate",
"description": "Perform a math calculation",
"parameters": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "The math expression to evaluate",
}
},
"required": ["expression"],
},
},
]
# --- Tool implementations ---
def get_weather(location, unit="celsius"):
"""Simulate a weather lookup."""
return {
"location": location,
"temperature": 22 if unit == "celsius" else 72,
"unit": unit,
"condition": "Sunny",
}
def calculate(expression):
"""Evaluate a math expression safely."""
allowed = set("0123456789+-*/(). ")
if not all(character in allowed for character in expression):
return {"error": "Invalid expression"}
try:
result = eval(expression, {"__builtins__": {}}, {})
return {"expression": expression, "result": result}
except Exception as error:
return {"error": str(error)}
tool_functions = {"get_weather": get_weather, "calculate": calculate}
Each tool definition includes a name, a description that helps the model decide when to use it, and a parameters schema that describes the expected input.
Next, add the Python functions that implement each tool:
# --- Tool definitions ---
tools = [
{
"name": "get_weather",
"description": "Get the current weather for a location",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city or location",
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit",
},
},
"required": ["location"],
},
},
{
"name": "calculate",
"description": "Perform a math calculation",
"parameters": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "The math expression to evaluate",
}
},
"required": ["expression"],
},
},
]
# --- Tool implementations ---
def get_weather(location, unit="celsius"):
"""Simulate a weather lookup."""
return {
"location": location,
"temperature": 22 if unit == "celsius" else 72,
"unit": unit,
"condition": "Sunny",
}
def calculate(expression):
"""Evaluate a math expression safely."""
allowed = set("0123456789+-*/(). ")
if not all(character in allowed for character in expression):
return {"error": "Invalid expression"}
try:
result = eval(expression, {"__builtins__": {}}, {})
return {"expression": expression, "result": result}
except Exception as error:
return {"error": str(error)}
tool_functions = {"get_weather": get_weather, "calculate": calculate}
The model doesn't run these functions directly. It returns a tool call request with the function name and arguments, and your code executes the function.
Send a message that triggers tool use
Initialize the Foundry Local SDK, load a model, and send a message that the model can answer by calling a tool.
def main():
# Initialize the Foundry Local SDK
config = Configuration(app_name="foundry_local_samples")
FoundryLocalManager.initialize(config)
manager = FoundryLocalManager.instance
# Discover and register execution providers that are not already available.
missing_eps = [ep.name for ep in manager.discover_eps() if not ep.is_registered]
if missing_eps:
manager.download_and_register_eps(missing_eps)
# Select and load a tool-capable model.
model = manager.catalog.get_model("qwen2.5-0.5b")
if model is None:
raise RuntimeError("Model 'qwen2.5-0.5b' was not found in the catalog.")
if not model.is_cached:
model.download(
lambda progress: print(
f"\rDownloading model: {progress:.2f}%", end="", flush=True
)
)
print()
model.load()
print("Model loaded and ready.")
try:
with ChatSession(model) as session:
session.set_options(
RequestOptions(
search=SearchOptions(temperature=0.0, max_output_tokens=512)
)
)
for tool in tools:
session.add_tool_definition(
tool["name"],
tool["description"],
json.dumps(tool["parameters"]),
)
system_prompt = (
"You are a helpful assistant with access to tools. "
"Use them when needed to answer questions accurately."
)
first_turn = True
print("\nTool-calling assistant ready! Type 'quit' to exit.\n")
while True:
user_input = input("You: ")
if user_input.strip().lower() in ("quit", "exit"):
break
with Request() as request:
if first_turn:
request.add_item(MessageItem.system(system_prompt))
first_turn = False
request.add_item(MessageItem.user(user_input))
with session.process_request(request) as response:
answer, pending_calls = response_content(response)
answer = process_tool_calls(session, answer, pending_calls)
print(f"Assistant: {answer}\n")
finally:
# Clean up
if model.is_loaded:
model.unload()
manager.close()
print("Model unloaded. Goodbye!")
When the model determines that a tool is needed, the response contains tool_calls instead of a regular text message. The next step shows how to detect and handle these calls.
Execute the tool and return results
After the model responds with a tool call, you extract the function name and arguments, run the function, and send the result back.
def process_tool_calls(session, answer, pending_calls):
"""Handle tool calls until the stateful session produces a final answer."""
while pending_calls:
with Request() as follow_up:
for call_id, function_name, arguments_json in pending_calls:
arguments = json.loads(arguments_json)
print(f" Tool call: {function_name}({arguments})")
result = tool_functions[function_name](**arguments)
follow_up.add_item(
ToolResultItem(call_id=call_id, result=json.dumps(result))
)
with session.process_request(follow_up) as next_response:
answer, pending_calls = response_content(next_response)
return answer
The key steps in the tool calling loop are:
- Detect tool calls — check
response.choices[0].message.tool_calls. - Execute the function — parse the arguments and call your local function.
- Return the result — add a message with role
tooland the matchingtool_call_id. - Get the final answer — the model uses the tool result to generate a natural response.
Handle the complete tool calling loop
Here's the complete application that combines tool definitions, SDK initialization, and the tool calling loop into a single runnable file.
Create a file named main.py and add the following complete code:
import json
from foundry_local_sdk import (
ChatSession,
Configuration,
FoundryLocalManager,
MessageItem,
Request,
RequestOptions,
SearchOptions,
TextItem,
ToolCallItem,
ToolResultItem,
)
# --- Tool definitions ---
tools = [
{
"name": "get_weather",
"description": "Get the current weather for a location",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city or location",
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit",
},
},
"required": ["location"],
},
},
{
"name": "calculate",
"description": "Perform a math calculation",
"parameters": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "The math expression to evaluate",
}
},
"required": ["expression"],
},
},
]
# --- Tool implementations ---
def get_weather(location, unit="celsius"):
"""Simulate a weather lookup."""
return {
"location": location,
"temperature": 22 if unit == "celsius" else 72,
"unit": unit,
"condition": "Sunny",
}
def calculate(expression):
"""Evaluate a math expression safely."""
allowed = set("0123456789+-*/(). ")
if not all(character in allowed for character in expression):
return {"error": "Invalid expression"}
try:
result = eval(expression, {"__builtins__": {}}, {})
return {"expression": expression, "result": result}
except Exception as error:
return {"error": str(error)}
tool_functions = {"get_weather": get_weather, "calculate": calculate}
def response_content(response):
text_parts = []
tool_calls = []
for item in response:
parts = item.parts if isinstance(item, MessageItem) else [item]
for part in parts:
if isinstance(part, TextItem):
text_parts.append(part.text)
elif isinstance(part, ToolCallItem):
tool_calls.append((part.call_id, part.name, part.arguments))
return "".join(text_parts), tool_calls
def process_tool_calls(session, answer, pending_calls):
"""Handle tool calls until the stateful session produces a final answer."""
while pending_calls:
with Request() as follow_up:
for call_id, function_name, arguments_json in pending_calls:
arguments = json.loads(arguments_json)
print(f" Tool call: {function_name}({arguments})")
result = tool_functions[function_name](**arguments)
follow_up.add_item(
ToolResultItem(call_id=call_id, result=json.dumps(result))
)
with session.process_request(follow_up) as next_response:
answer, pending_calls = response_content(next_response)
return answer
def main():
# Initialize the Foundry Local SDK
config = Configuration(app_name="foundry_local_samples")
FoundryLocalManager.initialize(config)
manager = FoundryLocalManager.instance
# Discover and register execution providers that are not already available.
missing_eps = [ep.name for ep in manager.discover_eps() if not ep.is_registered]
if missing_eps:
manager.download_and_register_eps(missing_eps)
# Select and load a tool-capable model.
model = manager.catalog.get_model("qwen2.5-0.5b")
if model is None:
raise RuntimeError("Model 'qwen2.5-0.5b' was not found in the catalog.")
if not model.is_cached:
model.download(
lambda progress: print(
f"\rDownloading model: {progress:.2f}%", end="", flush=True
)
)
print()
model.load()
print("Model loaded and ready.")
try:
with ChatSession(model) as session:
session.set_options(
RequestOptions(
search=SearchOptions(temperature=0.0, max_output_tokens=512)
)
)
for tool in tools:
session.add_tool_definition(
tool["name"],
tool["description"],
json.dumps(tool["parameters"]),
)
system_prompt = (
"You are a helpful assistant with access to tools. "
"Use them when needed to answer questions accurately."
)
first_turn = True
print("\nTool-calling assistant ready! Type 'quit' to exit.\n")
while True:
user_input = input("You: ")
if user_input.strip().lower() in ("quit", "exit"):
break
with Request() as request:
if first_turn:
request.add_item(MessageItem.system(system_prompt))
first_turn = False
request.add_item(MessageItem.user(user_input))
with session.process_request(request) as response:
answer, pending_calls = response_content(response)
answer = process_tool_calls(session, answer, pending_calls)
print(f"Assistant: {answer}\n")
finally:
# Clean up
if model.is_loaded:
model.unload()
manager.close()
print("Model unloaded. Goodbye!")
if __name__ == "__main__":
main()
Run the tool-calling assistant:
python main.py
You see output similar to:
Downloading model: 100.00%
Model loaded and ready.
Tool-calling assistant ready! Type 'quit' to exit.
You: What's the weather like today?
Tool call: get_weather({'location': 'current location'})
Assistant: The weather today is sunny with a temperature of 22°C.
You: What is 245 * 38?
Tool call: calculate({'expression': '245 * 38'})
Assistant: 245 multiplied by 38 equals 9,310.
You: quit
Model unloaded. Goodbye!
The model decides when to call a tool based on the user's message. For a weather question it calls get_weather, for math it calls calculate, and for general questions it responds directly without any tool calls.
Samples repository
The complete sample code for this article is available in the foundry-samples GitHub repository. To clone the repository and navigate to the sample use:
git clone https://github.com/microsoft-foundry/foundry-samples.git
cd foundry-samples/samples/rust/foundry-local/tutorial-tool-calling
Install packages
cargo add foundry-local-sdk
cargo add tokio --features full
cargo add tokio-stream anyhow
Define tools
Tool calling lets the model request that your code runs a function and returns the result. You define the available tools as a list of JSON schemas that describe each function's name, purpose, and parameters.
Add the
serde_jsondependency for JSON handling:cargo add serde_jsonOpen
src/main.rsand add the following tool definitions:let weather_schema = json!({ "type": "object", "properties": { "location": {"type": "string", "description": "The city or location"}, "unit": { "type": "string", "enum": ["celsius", "fahrenheit"], "description": "Temperature unit" } }, "required": ["location"] }); let calculator_schema = json!({ "type": "object", "properties": { "expression": { "type": "string", "description": "The math expression to evaluate" } }, "required": ["expression"] });Each tool definition includes a
name, adescriptionthat helps the model decide when to use it, and aparametersschema that describes the expected input.Add the Rust functions that implement each tool:
let weather_schema = json!({ "type": "object", "properties": { "location": {"type": "string", "description": "The city or location"}, "unit": { "type": "string", "enum": ["celsius", "fahrenheit"], "description": "Temperature unit" } }, "required": ["location"] }); let calculator_schema = json!({ "type": "object", "properties": { "expression": { "type": "string", "description": "The math expression to evaluate" } }, "required": ["expression"] });The model doesn't run these functions directly. It returns a tool call request with the function name and arguments, and your code executes the function.
Send a message that triggers tool use
Initialize the Foundry Local SDK, load a model, and send a message that the model can answer by calling a tool.
let manager = FoundryLocalManager::create(FoundryLocalConfig::new("foundry_local_samples"))?;
manager
.download_and_register_eps_with_progress(None, {
let mut current_ep = String::new();
move |ep_name: &str, percent: f64| {
if ep_name != current_ep {
if !current_ep.is_empty() {
println!();
}
current_ep = ep_name.to_string();
}
print!("\r {ep_name:<30} {percent:5.1}%");
io::stdout().flush().ok();
}
})
.await?;
println!();
let model = manager.catalog().get_model("qwen2.5-0.5b").await?;
if !model.is_cached().await? {
println!("Downloading model...");
model
.download(Some(|progress: f64| {
print!("\r {progress:.1}%");
io::stdout().flush().ok();
}))
.await?;
println!();
}
model.load().await?;
let session = ChatSession::new(&model).await?;
session
.set_options(RequestOptions {
search: SearchOptions {
temperature: Some(0.7),
max_output_tokens: Some(512),
..Default::default()
},
tool_choice: Some(ToolChoice::Auto),
..Default::default()
})
.await?;
session
.add_tool_definition(
ToolDefinition::new("get_weather", weather_schema.to_string())
.with_description("Get the current weather for a location."),
)
.await?;
session
.add_tool_definition(
ToolDefinition::new("calculate", calculator_schema.to_string())
.with_description("Perform a math calculation."),
)
.await?;
When the model determines that a tool is needed, the response contains tool_calls instead of a regular text message. The next step shows how to detect and handle these calls.
Execute the tool and return results
After the model responds with a tool call, you extract the function name and arguments, run the function, and send the result back.
println!("\nTool-calling assistant ready! Type 'quit' to exit.\n");
let stdin = io::stdin();
loop {
print!("You: ");
io::stdout().flush()?;
let mut input = String::new();
stdin.lock().read_line(&mut input)?;
let input = input.trim();
if input.eq_ignore_ascii_case("quit") || input.eq_ignore_ascii_case("exit") {
break;
}
if input.is_empty() {
continue;
}
let mut items = Vec::new();
if session.turn_count() == 0 {
items.push(Item::system_message(vec![Item::text(
"You are a helpful assistant with access to tools. Use them when needed.",
)]));
}
items.push(Item::user_message(vec![Item::text(input)]));
loop {
let response = session
.process_request(Request::from_items(items).with_options(RequestOptions {
search: SearchOptions {
temperature: Some(0.7),
max_output_tokens: Some(512),
..Default::default()
},
tool_choice: Some(ToolChoice::Auto),
..Default::default()
}))
.await?;
let mut tool_results = Vec::new();
let mut answer = String::new();
for item in &response.items {
if let Some(call) = item.as_tool_call() {
let arguments: Value =
serde_json::from_str(&call.arguments).unwrap_or_else(|_| json!({}));
println!(" Tool call: {}({arguments})", call.name);
let result = execute_tool(&call.name, &arguments);
tool_results.push(Item::tool_result(call.call_id.clone(), result.to_string()));
} else if let Some(message) = item.as_message() {
answer.push_str(&message.text());
} else if let Some(text) = item.as_text() {
answer.push_str(text);
}
}
if tool_results.is_empty() {
println!("Assistant: {answer}\n");
break;
}
items = tool_results;
}
}
drop(session);
model.unload().await?;
println!("Model unloaded. Goodbye!");
The key steps in the tool calling loop are:
- Detect tool calls — check
response.choices[0].message.tool_calls. - Execute the function — parse the arguments and call your local function.
- Return the result — add a message with role
tooland the matching tool call ID. - Get the final answer — the model uses the tool result to generate a natural response.
Handle the complete tool calling loop
Here's the complete application that combines tool definitions, SDK initialization, and the tool calling loop into a single runnable file.
Replace the contents of src/main.rs with the following complete code:
use foundry_local_sdk::{
ChatSession, FoundryLocalConfig, FoundryLocalManager, Item, Request, RequestOptions,
SearchOptions, ToolChoice, ToolDefinition,
};
use serde_json::{json, Value};
use std::io::{self, BufRead, Write};
fn execute_tool(name: &str, arguments: &Value) -> Value {
match name {
"get_weather" => {
let location = arguments["location"].as_str().unwrap_or("unknown");
let unit = arguments["unit"].as_str().unwrap_or("celsius");
let temp = if unit == "celsius" { 22 } else { 72 };
json!({
"location": location,
"temperature": temp,
"unit": unit,
"condition": "Sunny"
})
}
"calculate" => {
let expression = arguments["expression"].as_str().unwrap_or("");
let is_valid = expression
.chars()
.all(|character| "0123456789+-*/(). ".contains(character));
if !is_valid {
return json!({"error": "Invalid expression"});
}
match eval_expression(expression) {
Ok(result) => json!({"expression": expression, "result": result}),
Err(error) => json!({"error": error}),
}
}
_ => json!({"error": format!("Unknown function: {name}")}),
}
}
fn eval_expression(expr: &str) -> Result<f64, String> {
let expr = expr.replace(' ', "");
let chars: Vec<char> = expr.chars().collect();
let mut pos = 0;
let result = parse_add(&chars, &mut pos)?;
if pos < chars.len() {
return Err("Unexpected character".to_string());
}
Ok(result)
}
fn parse_add(chars: &[char], pos: &mut usize) -> Result<f64, String> {
let mut result = parse_mul(chars, pos)?;
while *pos < chars.len() && (chars[*pos] == '+' || chars[*pos] == '-') {
let op = chars[*pos];
*pos += 1;
let right = parse_mul(chars, pos)?;
result = if op == '+' {
result + right
} else {
result - right
};
}
Ok(result)
}
fn parse_mul(chars: &[char], pos: &mut usize) -> Result<f64, String> {
let mut result = parse_atom(chars, pos)?;
while *pos < chars.len() && (chars[*pos] == '*' || chars[*pos] == '/') {
let op = chars[*pos];
*pos += 1;
let right = parse_atom(chars, pos)?;
result = if op == '*' {
result * right
} else {
result / right
};
}
Ok(result)
}
fn parse_atom(chars: &[char], pos: &mut usize) -> Result<f64, String> {
if *pos < chars.len() && chars[*pos] == '(' {
*pos += 1;
let result = parse_add(chars, pos)?;
if *pos < chars.len() && chars[*pos] == ')' {
*pos += 1;
}
return Ok(result);
}
let start = *pos;
while *pos < chars.len() && (chars[*pos].is_ascii_digit() || chars[*pos] == '.') {
*pos += 1;
}
if start == *pos {
return Err("Expected number".to_string());
}
chars[start..*pos]
.iter()
.collect::<String>()
.parse::<f64>()
.map_err(|error| error.to_string())
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let weather_schema = json!({
"type": "object",
"properties": {
"location": {"type": "string", "description": "The city or location"},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit"
}
},
"required": ["location"]
});
let calculator_schema = json!({
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "The math expression to evaluate"
}
},
"required": ["expression"]
});
let manager = FoundryLocalManager::create(FoundryLocalConfig::new("foundry_local_samples"))?;
manager
.download_and_register_eps_with_progress(None, {
let mut current_ep = String::new();
move |ep_name: &str, percent: f64| {
if ep_name != current_ep {
if !current_ep.is_empty() {
println!();
}
current_ep = ep_name.to_string();
}
print!("\r {ep_name:<30} {percent:5.1}%");
io::stdout().flush().ok();
}
})
.await?;
println!();
let model = manager.catalog().get_model("qwen2.5-0.5b").await?;
if !model.is_cached().await? {
println!("Downloading model...");
model
.download(Some(|progress: f64| {
print!("\r {progress:.1}%");
io::stdout().flush().ok();
}))
.await?;
println!();
}
model.load().await?;
let session = ChatSession::new(&model).await?;
session
.set_options(RequestOptions {
search: SearchOptions {
temperature: Some(0.7),
max_output_tokens: Some(512),
..Default::default()
},
tool_choice: Some(ToolChoice::Auto),
..Default::default()
})
.await?;
session
.add_tool_definition(
ToolDefinition::new("get_weather", weather_schema.to_string())
.with_description("Get the current weather for a location."),
)
.await?;
session
.add_tool_definition(
ToolDefinition::new("calculate", calculator_schema.to_string())
.with_description("Perform a math calculation."),
)
.await?;
println!("\nTool-calling assistant ready! Type 'quit' to exit.\n");
let stdin = io::stdin();
loop {
print!("You: ");
io::stdout().flush()?;
let mut input = String::new();
stdin.lock().read_line(&mut input)?;
let input = input.trim();
if input.eq_ignore_ascii_case("quit") || input.eq_ignore_ascii_case("exit") {
break;
}
if input.is_empty() {
continue;
}
let mut items = Vec::new();
if session.turn_count() == 0 {
items.push(Item::system_message(vec![Item::text(
"You are a helpful assistant with access to tools. Use them when needed.",
)]));
}
items.push(Item::user_message(vec![Item::text(input)]));
loop {
let response = session
.process_request(Request::from_items(items).with_options(RequestOptions {
search: SearchOptions {
temperature: Some(0.7),
max_output_tokens: Some(512),
..Default::default()
},
tool_choice: Some(ToolChoice::Auto),
..Default::default()
}))
.await?;
let mut tool_results = Vec::new();
let mut answer = String::new();
for item in &response.items {
if let Some(call) = item.as_tool_call() {
let arguments: Value =
serde_json::from_str(&call.arguments).unwrap_or_else(|_| json!({}));
println!(" Tool call: {}({arguments})", call.name);
let result = execute_tool(&call.name, &arguments);
tool_results.push(Item::tool_result(call.call_id.clone(), result.to_string()));
} else if let Some(message) = item.as_message() {
answer.push_str(&message.text());
} else if let Some(text) = item.as_text() {
answer.push_str(text);
}
}
if tool_results.is_empty() {
println!("Assistant: {answer}\n");
break;
}
items = tool_results;
}
}
drop(session);
model.unload().await?;
println!("Model unloaded. Goodbye!");
Ok(())
}
Run the tool-calling assistant:
cargo run
You see output similar to:
Downloading model: 100.00%
Model loaded and ready.
Tool-calling assistant ready! Type 'quit' to exit.
You: What's the weather like today?
Tool call: get_weather({"location":"current location"})
Assistant: The weather today is sunny with a temperature of 22°C.
You: What is 245 * 38?
Tool call: calculate({"expression":"245 * 38"})
Assistant: 245 multiplied by 38 equals 9,310.
You: quit
Model unloaded. Goodbye!
The model decides when to call a tool based on the user's message. For a weather question it calls get_weather, for math it calls calculate, and for general questions it responds directly without any tool calls.
Samples repository
Clone the Foundry samples GitHub repository and go to the C++ sample:
git clone https://github.com/microsoft-foundry/foundry-samples.git
cd foundry-samples/samples/cpp/foundry-local/tutorial-tool-calling-assistant
Install packages
The C++ samples require:
Configure and build the sample:
cmake -S . -B build
cmake --build build --config Release
During configuration, the sample restores version 2.2.1 of the
Microsoft.AI.Foundry.Local.Runtime NuGet package to build/_packages. The
package contains the C++ headers and native libraries for Windows, Linux, and
macOS. You don't need a separate WinML package.
The CMake configuration selects the runtime assets for your operating system
and architecture and copies the required native libraries beside the
executable. On macOS and Linux, run the executable from build. On Windows,
run the .exe file from build.
Define tools
Define the available tools and the local functions that implement them:
constexpr const char* kTimeSchema = R"({
"type": "object",
"properties": {
"timezone": {"type": "string", "description": "Requested timezone label"}
},
"required": ["timezone"]
})";
std::string GetLocalTime() {
const auto now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
std::tm local_time{};
#ifdef _WIN32
localtime_s(&local_time, &now);
#else
localtime_r(&now, &local_time);
#endif
std::ostringstream output;
output << std::put_time(&local_time, "%Y-%m-%d %H:%M:%S");
return R"({"timezone":"local","time":")" + output.str() + R"("})";
}
Send a message that triggers tool use
Initialize the SDK, load a model, and send a request that can use a tool:
Configuration config("foundry_local_cpp_tool_assistant");
Manager manager(std::move(config));
auto model = manager.GetCatalog().GetModel("qwen2.5-0.5b");
if (!model) {
throw std::runtime_error("Model 'qwen2.5-0.5b' was not found in the catalog.");
}
if (!model->IsCached()) {
model->Download([](float progress) {
std::cout << "\rDownloading model: " << static_cast<int>(progress) << "%" << std::flush;
return 0;
});
std::cout << "\n";
}
model->Load();
Execute the tool and return results
Detect the tool call, execute the local function, return its result, and ask the model for the final answer:
Request request{
SystemMessage("Use the available tool when current time is requested."),
UserMessage("What time is it?"),
};
request.SetOptions({
.search = {.temperature = 0.0F, .max_output_tokens = 256, .seed = 42},
.tool_choice = FOUNDRY_LOCAL_TOOL_CHOICE_REQUIRED,
});
Response response = session.ProcessRequest(request);
for (int iteration = 0; iteration < 8; ++iteration) {
bool called_tool = false;
for (const Item& item : response.GetItems()) {
if (item.GetType() == FOUNDRY_LOCAL_ITEM_MESSAGE) {
std::cout << "Assistant: ";
PrintAssistant(item);
continue;
}
if (item.GetType() != FOUNDRY_LOCAL_ITEM_TOOL_CALL) {
continue;
}
const auto call = item.GetToolCall();
if (call.name != "get_current_time") {
throw std::runtime_error("The model requested an unknown tool.");
}
Request tool_result;
tool_result.AddItem(Item::ToolResult(std::string(call.call_id), GetLocalTime()));
tool_result.SetOptions({
.search = {.temperature = 0.0F, .max_output_tokens = 256, .seed = 42},
.tool_choice = FOUNDRY_LOCAL_TOOL_CHOICE_NONE,
});
response = session.ProcessRequest(tool_result);
called_tool = true;
break;
}
if (!called_tool) {
break;
}
}
Complete code
#include <foundry_local/foundry_local_cpp.h>
#include <chrono>
#include <ctime>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <string>
using namespace foundry_local;
constexpr const char* kTimeSchema = R"({
"type": "object",
"properties": {
"timezone": {"type": "string", "description": "Requested timezone label"}
},
"required": ["timezone"]
})";
std::string GetLocalTime() {
const auto now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
std::tm local_time{};
#ifdef _WIN32
localtime_s(&local_time, &now);
#else
localtime_r(&now, &local_time);
#endif
std::ostringstream output;
output << std::put_time(&local_time, "%Y-%m-%d %H:%M:%S");
return R"({"timezone":"local","time":")" + output.str() + R"("})";
}
void PrintAssistant(const Item& message) {
for (const Item& part : message.GetMessage().parts) {
if (part.GetType() == FOUNDRY_LOCAL_ITEM_TEXT) {
std::cout << part.GetText().text;
}
}
std::cout << "\n";
}
int main() {
try {
Configuration config("foundry_local_cpp_tool_assistant");
Manager manager(std::move(config));
auto model = manager.GetCatalog().GetModel("qwen2.5-0.5b");
if (!model) {
throw std::runtime_error("Model 'qwen2.5-0.5b' was not found in the catalog.");
}
if (!model->IsCached()) {
model->Download([](float progress) {
std::cout << "\rDownloading model: " << static_cast<int>(progress) << "%" << std::flush;
return 0;
});
std::cout << "\n";
}
model->Load();
{
ChatSession session(*model);
session.AddToolDefinition(ToolDefinition{
"get_current_time",
"Get the current local date and time.",
kTimeSchema,
});
Request request{
SystemMessage("Use the available tool when current time is requested."),
UserMessage("What time is it?"),
};
request.SetOptions({
.search = {.temperature = 0.0F, .max_output_tokens = 256, .seed = 42},
.tool_choice = FOUNDRY_LOCAL_TOOL_CHOICE_REQUIRED,
});
Response response = session.ProcessRequest(request);
for (int iteration = 0; iteration < 8; ++iteration) {
bool called_tool = false;
for (const Item& item : response.GetItems()) {
if (item.GetType() == FOUNDRY_LOCAL_ITEM_MESSAGE) {
std::cout << "Assistant: ";
PrintAssistant(item);
continue;
}
if (item.GetType() != FOUNDRY_LOCAL_ITEM_TOOL_CALL) {
continue;
}
const auto call = item.GetToolCall();
if (call.name != "get_current_time") {
throw std::runtime_error("The model requested an unknown tool.");
}
Request tool_result;
tool_result.AddItem(Item::ToolResult(std::string(call.call_id), GetLocalTime()));
tool_result.SetOptions({
.search = {.temperature = 0.0F, .max_output_tokens = 256, .seed = 42},
.tool_choice = FOUNDRY_LOCAL_TOOL_CHOICE_NONE,
});
response = session.ProcessRequest(tool_result);
called_tool = true;
break;
}
if (!called_tool) {
break;
}
}
}
model->Unload();
manager.Shutdown();
} catch (const Error& error) {
std::cerr << "Foundry Local error [" << error.Code() << "]: " << error.what() << "\n";
return 1;
} catch (const std::exception& error) {
std::cerr << "Error: " << error.what() << "\n";
return 1;
}
return 0;
}
Run the assistant:
./build/tutorial-tool-calling-assistant
Clean up resources
The model weights remain in your local cache after you unload a model. This means the next time you run the application, the download step is skipped and the model loads faster. No extra cleanup is needed unless you want to reclaim disk space.