Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
bc6e38b
Add LocalChatClientWithTools sample — on-device Apple Intelligence wi…
mattleibow Mar 11, 2026
7cc87a0
Add streaming responses and real-time tool call visibility
mattleibow Mar 11, 2026
b865cc5
Use inline divider for tool call indicators instead of speech bubbles
mattleibow Mar 11, 2026
f7e84a1
Replace screenshots with 4 actual app captures
mattleibow Mar 11, 2026
e9cd0eb
Retake calculator and multi-tool screenshots (remove overlay)
mattleibow Mar 11, 2026
db8b4fb
Remove NuGet.config — no longer needed for preview feed
mattleibow Mar 12, 2026
ac4d1be
Pin Microsoft.Maui.Essentials.AI to 10.0.50-preview.1.26161.6
mattleibow Mar 12, 2026
bf36bd0
Pin Microsoft.Maui.Essentials.AI to 10.0.50-preview.1.26158.1
mattleibow Mar 12, 2026
6a84d2a
Refactor tools to AIFunctionFactory, redesign chat UI with template s…
mattleibow Mar 13, 2026
c4b5dc3
Fix code review issues and improve CalculatorTool robustness
mattleibow Mar 13, 2026
fb127e5
Add multi-turn conversation history and fix README
mattleibow Mar 13, 2026
56d5822
Fix CI build: use $(MauiVersion), remove MauiDevFlow references
mattleibow Mar 13, 2026
15bf43a
Delete 10.0/AI/LocalChatClientWithTools/src/Resources/Images/dotnet_b…
mattleibow Mar 13, 2026
e3721e8
Remove unused dotnet_bot.png template asset
mattleibow Mar 13, 2026
4935b6f
Update LocalChatClientWithTools.sln
mattleibow Mar 13, 2026
4a2a283
Fix CI CA2252 error, address PR review comments
mattleibow Mar 13, 2026
07b46e9
Use IHttpClientFactory instead of singleton HttpClient in WeatherTool
mattleibow Mar 13, 2026
84bc243
Fix Windows app.manifest assemblyIdentity to match project name
mattleibow Mar 13, 2026
2affe63
Merge branch 'main' into ai/local-chat-client-with-tools
mattleibow Mar 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions 10.0/AI/LocalChatClientWithTools/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
---
name: .NET MAUI - Local Chat Client with Tools
description: Chat UI demonstrating on-device Apple Intelligence function calling (tools) with weather, calculator, file listing, system info, and timers — no API keys or cloud services required.
page_type: sample
languages:
- csharp
- xaml
products:
- dotnet-maui
urlFragment: local-chat-client-with-tools
---

# LocalChatClientWithTools (MAUI + On-Device Apple Intelligence Tools)

A .NET MAUI sample showing how to enhance an on-device LLM with `Microsoft.Extensions.AI` function calling (tools) using Apple Intelligence. All inference runs locally — **no API keys, no cloud services, no cost**.

| Prompt buttons | Weather tool | Calculator tool | Multi-tool calls |
|:-:|:-:|:-:|:-:|
| ![prompts](images/prompts.png) | ![weather](images/weather.png) | ![calculator](images/calculator.png) | ![multi-tool](images/multi_tool.png) |

## What you'll learn

- Using `AppleIntelligenceChatClient` from `Microsoft.Maui.Essentials.AI` as a local `IChatClient`
- Supplying a set of `AIFunction` tools to `IChatClient` via `ChatOptions.Tools`
- Implementing strongly described tool schemas (JSON schema) for better argument selection
- Resolving user-friendly locations to coordinates with the free [open-meteo.com](https://open-meteo.com) API
- Building a MAUI chat UI with template selectors for different message types (text, tool calls)
- Running AI inference entirely on-device with no external API keys

## Prerequisites

- .NET 10 SDK
- iOS 26.0+ or macCatalyst 26.0+ device/simulator with Apple Intelligence enabled
- No API keys or cloud accounts needed

## Tools implemented

| Tool | Name | Purpose | Notes |
|------|------|---------|-------|
| WeatherTool | `get_weather` | Current conditions for a location | Geocodes via open-meteo.com, then fetches forecast — free, no API key |
| CalculatorTool | `calculate` | Evaluate arithmetic / percentages | Sanitizes expression & returns formatted result |
| FileOperationsTool | `list_files` | List files & folders in a common or given path | Limits count; resolves shortcuts (Documents, Desktop, Downloads) |
| SystemInfoTool | `get_system_info` | Battery, storage, memory, device info | Returns live device values on Apple platforms |
| TimerTool | `set_timer` | Create a one‑shot timer with title | Keeps in-memory timers; shows alert on completion |

Each tool is created via `AIFunctionFactory.Create` using strongly-typed methods and AOT-compatible JSON serialization via `ToolJsonContext`.

## Key files

- `MauiProgram.cs` – Registers `IChatClient` via `AppleIntelligenceChatClient` with `UseFunctionInvocation()` for automatic tool dispatch, and registers all tool functions.
- `Services/Tools/*.cs` – Tool implementations using `AIFunctionFactory.Create` for clean, factory-based tool registration.
- `ViewModels/ChatViewModel.cs` – Collects tools and invokes `_chatClient.GetStreamingResponseAsync` passing `ChatOptions.Tools`.
- `ViewModels/ChatMessageViewModel.cs` – Base view model for chat messages, with `TextMessageViewModel` and `ToolCallMessageViewModel` subclasses.
- `MainPage.xaml` – Chat layout with `ChatTemplateSelector`, EmptyView sample prompts, and a `Clear` toolbar item (resets to EmptyView).

## Sample prompts

Try:

- "What's the weather in Seattle?"
- "Calculate 15% tip on $47.50"
- "List the files in my Documents folder"
- "Show me current battery level and available storage"
- "Set a 5-minute timer for my coffee break"
- "What's 25 × 18 + 150?"
- "Check the weather in Tokyo and Cape Town"

## Run

```bash
# iOS simulator
dotnet build -f net10.0-ios -t:Run

# macCatalyst
dotnet build -f net10.0-maccatalyst -t:Run
```

Use a sample prompt or type your own. Use the toolbar **Clear** to reset and revisit the instructional prompt list.

> **Note:** This sample only runs on Apple platforms (iOS / macCatalyst) with Apple Intelligence. Other platforms will throw `PlatformNotSupportedException` at startup.

## Architecture highlights

### Tool invocation flow

1. User sends natural language input.
2. `ChatViewModel` builds `ChatOptions` with the registered tool objects.
3. `AppleIntelligenceChatClient` (wrapped with `UseFunctionInvocation()`) decides if any tool(s) should be invoked and executes them, passing JSON arguments that conform to each tool's declared schema.
4. Tool result objects surface in the assistant response and are bound to specialized UI cards.
5. UI shows structured cards or plain text depending on `ChatMessage` flags.

### On-device vs. cloud

This sample is a local-only variant of the `ChatClientWithTools` sample. The key differences:

| | ChatClientWithTools | LocalChatClientWithTools |
|---|---|---|
| **LLM provider** | Azure OpenAI (cloud) | Apple Intelligence (on-device) |
| **API keys** | Required (Azure + optional OpenWeatherMap) | None |
| **Weather API** | OpenWeatherMap (key required) | open-meteo.com (free, keyless) |
| **Platforms** | Windows, iOS, macCatalyst, Android | iOS, macCatalyst only |
| **NuGet packages** | Azure.AI.OpenAI, Microsoft.Extensions.AI.OpenAI | Microsoft.Maui.Essentials.AI |

### UI features

- EmptyView with categorized starter buttons (Weather / Calculator / Files / System Info / Timers).
- Rich result cards styled with color-coded borders.
- Toolbar "Clear" to wipe conversation state quickly (teaching scenario friendly).
- Auto-scroll to the latest message.

## Useful references

- Microsoft.Extensions.AI overview: <https://learn.microsoft.com/dotnet/ai/microsoft-extensions-ai>
- Apple Intelligence in .NET MAUI: <https://learn.microsoft.com/dotnet/maui/platform-integration/communication/ai>
- CommunityToolkit.Mvvm: <https://learn.microsoft.com/dotnet/communitytoolkit/mvvm/>

## Notes

- Requires iOS 26.0+ or macCatalyst 26.0+ with Apple Intelligence available and enabled.
- All AI inference runs on-device — no data leaves the device for LLM processing.
- Weather data comes from [open-meteo.com](https://open-meteo.com) (free, open-source weather API).
- Timers are in-memory only (lost on app exit) and surface completion via a `DisplayAlertAsync` dialog.
- File operations are intentionally conservative; expand with care for security.
- This sample emphasizes pedagogy over exhaustive production hardening.

---
For the cloud-hosted version of this sample, see `ChatClientWithTools` in the same folder tree.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions 10.0/AI/LocalChatClientWithTools/src/App.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version = "1.0" encoding = "UTF-8" ?>
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:LocalChatClientWithTools"
x:Class="LocalChatClientWithTools.App">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Styles/Colors.xaml" />
<ResourceDictionary Source="Resources/Styles/Styles.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
14 changes: 14 additions & 0 deletions 10.0/AI/LocalChatClientWithTools/src/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace LocalChatClientWithTools;

public partial class App : Application
{
public App()
{
InitializeComponent();
}

protected override Window CreateWindow(IActivationState? activationState)
{
return new Window(new AppShell());
}
}
14 changes: 14 additions & 0 deletions 10.0/AI/LocalChatClientWithTools/src/AppShell.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Shell
x:Class="LocalChatClientWithTools.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:LocalChatClientWithTools"
Title="LocalChatClientWithTools">

<ShellContent
Title="Home"
ContentTemplate="{DataTemplate local:MainPage}"
Route="MainPage" />

</Shell>
9 changes: 9 additions & 0 deletions 10.0/AI/LocalChatClientWithTools/src/AppShell.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace LocalChatClientWithTools;

public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
}
}
16 changes: 16 additions & 0 deletions 10.0/AI/LocalChatClientWithTools/src/ChatTemplateSelector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using LocalChatClientWithTools.ViewModels;

namespace LocalChatClientWithTools;

public class ChatTemplateSelector : DataTemplateSelector
{
public DataTemplate? TextTemplate { get; set; }
public DataTemplate? ToolCallTemplate { get; set; }

protected override DataTemplate OnSelectTemplate(object item, BindableObject container) => item switch
{
ToolCallMessageViewModel => ToolCallTemplate!,
TextMessageViewModel => TextTemplate!,
_ => TextTemplate!
};
}
3 changes: 3 additions & 0 deletions 10.0/AI/LocalChatClientWithTools/src/GlobalXmlns.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[assembly: XmlnsDefinition("http://schemas.microsoft.com/dotnet/maui/global", "LocalChatClientWithTools")]
[assembly: XmlnsDefinition("http://schemas.microsoft.com/dotnet/maui/global", "LocalChatClientWithTools.ViewModels")]
[assembly: XmlnsDefinition("http://schemas.microsoft.com/dotnet/maui/global", "Syncfusion.Maui.Toolkit.TextInputLayout", AssemblyName = "Syncfusion.Maui.Toolkit")]
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net10.0-android;net10.0-ios;net10.0-maccatalyst</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net10.0-windows10.0.19041.0</TargetFrameworks>

<!-- Note for MacCatalyst:
The default runtime is maccatalyst-x64, except in Release config, in which case the default is maccatalyst-x64;maccatalyst-arm64.
When specifying both architectures, use the plural <RuntimeIdentifiers> instead of the singular <RuntimeIdentifier>.
The Mac App Store will NOT accept apps with ONLY maccatalyst-arm64 indicated;
either BOTH runtimes must be indicated or ONLY macatalyst-x64. -->
<!-- For example: <RuntimeIdentifiers>maccatalyst-x64;maccatalyst-arm64</RuntimeIdentifiers> -->

<OutputType>Exe</OutputType>
<RootNamespace>LocalChatClientWithTools</RootNamespace>
<UseMaui>true</UseMaui>
<SingleProject>true</SingleProject>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<NoWarn>$(NoWarn);MAUIAI0001;CA2252</NoWarn>

<!-- Enable implicit + global XAML xmlns for .NET 10 previews -->
<DefineConstants>$(DefineConstants);MauiAllowImplicitXmlnsDeclaration</DefineConstants>
<EnablePreviewFeatures>true</EnablePreviewFeatures>

<!-- Display name -->
<ApplicationTitle>LocalChatClientWithTools</ApplicationTitle>

<!-- App Identifier -->
<ApplicationId>com.companyname.localchatclientwithtools</ApplicationId>

<!-- Versions -->
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion>

<!-- To develop, package, and publish an app to the Microsoft Store, see: https://aka.ms/MauiTemplateUnpackaged -->
<WindowsPackageType>None</WindowsPackageType>

<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">26.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">26.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">21.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
</PropertyGroup>

<ItemGroup>
<!-- App Icon -->
<MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4" />

<!-- Splash Screen -->
<MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#512BD4" BaseSize="128,128" />

<!-- Images -->
<MauiImage Include="Resources\Images\*" />

<!-- Custom Fonts -->
<MauiFont Include="Resources\Fonts\*" />

<!-- Raw Assets (also remove the "Resources\Raw" prefix) -->
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.AI" Version="10.3.0" />
<PackageReference Include="Microsoft.Maui.Essentials.AI" Version="10.0.50-preview.1.26158.1" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="10.0.0" />
<PackageReference Include="Syncfusion.Maui.Toolkit" Version="1.0.6" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.0" />
</ItemGroup>

</Project>
22 changes: 22 additions & 0 deletions 10.0/AI/LocalChatClientWithTools/src/LocalChatClientWithTools.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LocalChatClientWithTools", "LocalChatClientWithTools.csproj", "{9FB143A1-5BC5-40A3-B332-BA816CF1CE04}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{9FB143A1-5BC5-40A3-B332-BA816CF1CE04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9FB143A1-5BC5-40A3-B332-BA816CF1CE04}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9FB143A1-5BC5-40A3-B332-BA816CF1CE04}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9FB143A1-5BC5-40A3-B332-BA816CF1CE04}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
Loading
Loading