Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
43 changes: 20 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,13 @@

Sample clients for the [Work IQ](https://learn.microsoft.com/en-us/microsoft-365/copilot/extensibility/workiq-overview) API — Microsoft's AI-native interface to Microsoft 365 work intelligence.

| Sample | Language | Protocol | Description |
|--------|----------|----------|-------------|
| [**dotnet/a2a/**](dotnet/a2a/) | C# | [A2A (Agent-to-Agent)](https://a2a-protocol.org) | Interactive agent session using the open A2A protocol over JSON-RPC |
| [**dotnet/rest/**](dotnet/rest/) | C# | REST | Interactive chat using the [Copilot Chat API](https://learn.microsoft.com/en-us/microsoft-365-copilot/extensibility/api/ai-services/chat/overview) with sync and streaming modes |
| [**rust/a2a/**](rust/a2a/) | Rust | [A2A (Agent-to-Agent)](https://a2a-protocol.org) | Interactive agent session with device code auth, token caching, and SSE streaming |

## Swift Samples (`swift/`)

| Sample | Protocol | Description |
|--------|----------|-------------|
| [**swift/a2a/**](swift/a2a/) | [A2A (Agent-to-Agent)](https://a2a-protocol.org) | SwiftUI iOS/iPadOS chat app using A2A v0.3 with streaming responses |

## Rust Samples (`rust/`)

| Sample | Protocol | Description |
|--------|----------|-------------|
| [**rust/a2a/**](rust/a2a/) | [A2A (Agent-to-Agent)](https://a2a-protocol.org) | Interactive agent session with device code auth, token caching, and SSE streaming |
| Sample | Language | Platform | Protocol | Description |
|--------|----------|----------|----------|-------------|
| [**dotnet/a2a/**](dotnet/a2a/) | C# | Windows, macOS, Linux | [A2A](https://a2a-protocol.org) | Interactive agent session using the A2A protocol over JSON-RPC |
| [**dotnet/a2a-raw/**](dotnet/a2a-raw/) | C# | Windows, macOS, Linux | [A2A](https://a2a-protocol.org) | Same, but with raw `HttpClient` + JSON (no A2A SDK) |
| [**dotnet/rest/**](dotnet/rest/) | C# | Windows, macOS, Linux | REST | Interactive chat using the [Copilot Chat API](https://learn.microsoft.com/en-us/microsoft-365-copilot/extensibility/api/ai-services/chat/overview) |
| [**rust/a2a/**](rust/a2a/) | Rust | Windows, macOS, Linux | [A2A](https://a2a-protocol.org) | Interactive agent session with device code auth and token caching |
| [**swift/a2a/**](swift/a2a/) | Swift | iOS/iPadOS (macOS to build) | [A2A](https://a2a-protocol.org) | SwiftUI chat app with streaming responses |

> **Current state**: Work IQ is accessed through the Microsoft Graph API at `graph.microsoft.com`. All samples use Graph endpoints and Graph authentication today.
>
Expand Down Expand Up @@ -54,12 +44,15 @@ After adding permissions, click **Grant admin consent for [your tenant]**.

### 3. Language-specific SDKs

- **dotnet/** samples: [.NET 8.0 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) or later
- **dotnet/** samples: [.NET 10.0 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) or later
- **rust/** samples: [Rust toolchain](https://rustup.rs/) (stable)
- **swift/** samples: [Xcode 26+](https://developer.apple.com/xcode/) (macOS only)

## Authentication

### WAM (Windows Account Manager) — recommended on Windows (.NET only)
All samples support multiple authentication methods. Choose the one that fits your platform:

### WAM (Windows Account Manager) — Windows only, .NET samples

Uses the Windows broker for silent SSO. No browser popup for returning users.

Expand All @@ -68,20 +61,24 @@ cd dotnet/a2a
dotnet run -- --graph --token WAM --appid <your-app-client-id>
```

### Device code flow — any platform (Rust)
> **Note:** WAM is only available on Windows. On macOS and Linux, use a pre-obtained JWT token instead (see below).

### Device code flow — all platforms (Rust, Swift)

The Rust CLI and Swift app use device code flow, which works on any platform with a web browser.

```bash
cd rust/a2a
cargo run -- --appid <your-app-client-id>
# Follow the on-screen instructions to authenticate in a browser
```

### Pre-obtained JWT token — any platform
### Pre-obtained JWT token — all platforms, all samples

Acquire a token externally (e.g., via [Graph Explorer](https://developer.microsoft.com/en-us/graph/graph-explorer), `az account get-access-token`, or your own MSAL code) and pass it directly:
Acquire a token externally (e.g., via [Graph Explorer](https://developer.microsoft.com/en-us/graph/graph-explorer), `az account get-access-token`, or your own MSAL code) and pass it directly. This works on Windows, macOS, and Linux.

```bash
# .NET
# .NET samples
cd dotnet/a2a
dotnet run -- --graph --token eyJ0eXAiOiJKV1Qi...

Expand Down
124 changes: 124 additions & 0 deletions dotnet/WorkIQSamples.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "a2a-raw", "a2a-raw", "{CCFF03C2-3201-45FB-2E8F-00A04B2D057D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "workiq-a2a-raw", "a2a-raw\workiq-a2a-raw.csproj", "{1FDE52B4-ADE3-45C1-92B1-7FBDED0AF791}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "a2a-raw.tests", "a2a-raw.tests", "{ECE5971F-AE9E-BA12-55AA-4929917FAF06}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "workiq-a2a-raw-tests", "a2a-raw.tests\workiq-a2a-raw-tests.csproj", "{64FD8CEC-4C54-4873-B4E9-CA3F6BDA108A}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "rest", "rest", "{CAF2E94B-1F3E-339F-03CF-980AEBF14E62}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "workiq-rest-sample", "rest\workiq-rest-sample.csproj", "{521D7579-B37C-47B5-AE2C-6ACFCC1AD790}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "rest.tests", "rest.tests", "{99F3A7AA-53A2-3584-6717-8D7F80AF0238}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "workiq-rest-tests", "rest.tests\workiq-rest-tests.csproj", "{44DDF8F5-469B-4650-A170-721FFACB4B08}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "a2a", "a2a", "{DF537D00-43B0-169F-440F-F716863E989D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "workiq-a2a-sample", "a2a\workiq-a2a-sample.csproj", "{709CB2BA-85F0-41E1-8D28-F46CAC8BF783}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "a2a.tests", "a2a.tests", "{80F48381-A032-BB77-AC00-2E31FC5BD3EE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "workiq-a2a-tests", "a2a.tests\workiq-a2a-tests.csproj", "{F1B663D7-DBB9-4FB0-B8D5-60A8A077CD4F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{1FDE52B4-ADE3-45C1-92B1-7FBDED0AF791}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1FDE52B4-ADE3-45C1-92B1-7FBDED0AF791}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1FDE52B4-ADE3-45C1-92B1-7FBDED0AF791}.Debug|x64.ActiveCfg = Debug|Any CPU
{1FDE52B4-ADE3-45C1-92B1-7FBDED0AF791}.Debug|x64.Build.0 = Debug|Any CPU
{1FDE52B4-ADE3-45C1-92B1-7FBDED0AF791}.Debug|x86.ActiveCfg = Debug|Any CPU
{1FDE52B4-ADE3-45C1-92B1-7FBDED0AF791}.Debug|x86.Build.0 = Debug|Any CPU
{1FDE52B4-ADE3-45C1-92B1-7FBDED0AF791}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1FDE52B4-ADE3-45C1-92B1-7FBDED0AF791}.Release|Any CPU.Build.0 = Release|Any CPU
{1FDE52B4-ADE3-45C1-92B1-7FBDED0AF791}.Release|x64.ActiveCfg = Release|Any CPU
{1FDE52B4-ADE3-45C1-92B1-7FBDED0AF791}.Release|x64.Build.0 = Release|Any CPU
{1FDE52B4-ADE3-45C1-92B1-7FBDED0AF791}.Release|x86.ActiveCfg = Release|Any CPU
{1FDE52B4-ADE3-45C1-92B1-7FBDED0AF791}.Release|x86.Build.0 = Release|Any CPU
{64FD8CEC-4C54-4873-B4E9-CA3F6BDA108A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{64FD8CEC-4C54-4873-B4E9-CA3F6BDA108A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{64FD8CEC-4C54-4873-B4E9-CA3F6BDA108A}.Debug|x64.ActiveCfg = Debug|Any CPU
{64FD8CEC-4C54-4873-B4E9-CA3F6BDA108A}.Debug|x64.Build.0 = Debug|Any CPU
{64FD8CEC-4C54-4873-B4E9-CA3F6BDA108A}.Debug|x86.ActiveCfg = Debug|Any CPU
{64FD8CEC-4C54-4873-B4E9-CA3F6BDA108A}.Debug|x86.Build.0 = Debug|Any CPU
{64FD8CEC-4C54-4873-B4E9-CA3F6BDA108A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{64FD8CEC-4C54-4873-B4E9-CA3F6BDA108A}.Release|Any CPU.Build.0 = Release|Any CPU
{64FD8CEC-4C54-4873-B4E9-CA3F6BDA108A}.Release|x64.ActiveCfg = Release|Any CPU
{64FD8CEC-4C54-4873-B4E9-CA3F6BDA108A}.Release|x64.Build.0 = Release|Any CPU
{64FD8CEC-4C54-4873-B4E9-CA3F6BDA108A}.Release|x86.ActiveCfg = Release|Any CPU
{64FD8CEC-4C54-4873-B4E9-CA3F6BDA108A}.Release|x86.Build.0 = Release|Any CPU
{521D7579-B37C-47B5-AE2C-6ACFCC1AD790}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{521D7579-B37C-47B5-AE2C-6ACFCC1AD790}.Debug|Any CPU.Build.0 = Debug|Any CPU
{521D7579-B37C-47B5-AE2C-6ACFCC1AD790}.Debug|x64.ActiveCfg = Debug|Any CPU
{521D7579-B37C-47B5-AE2C-6ACFCC1AD790}.Debug|x64.Build.0 = Debug|Any CPU
{521D7579-B37C-47B5-AE2C-6ACFCC1AD790}.Debug|x86.ActiveCfg = Debug|Any CPU
{521D7579-B37C-47B5-AE2C-6ACFCC1AD790}.Debug|x86.Build.0 = Debug|Any CPU
{521D7579-B37C-47B5-AE2C-6ACFCC1AD790}.Release|Any CPU.ActiveCfg = Release|Any CPU
{521D7579-B37C-47B5-AE2C-6ACFCC1AD790}.Release|Any CPU.Build.0 = Release|Any CPU
{521D7579-B37C-47B5-AE2C-6ACFCC1AD790}.Release|x64.ActiveCfg = Release|Any CPU
{521D7579-B37C-47B5-AE2C-6ACFCC1AD790}.Release|x64.Build.0 = Release|Any CPU
{521D7579-B37C-47B5-AE2C-6ACFCC1AD790}.Release|x86.ActiveCfg = Release|Any CPU
{521D7579-B37C-47B5-AE2C-6ACFCC1AD790}.Release|x86.Build.0 = Release|Any CPU
{44DDF8F5-469B-4650-A170-721FFACB4B08}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{44DDF8F5-469B-4650-A170-721FFACB4B08}.Debug|Any CPU.Build.0 = Debug|Any CPU
{44DDF8F5-469B-4650-A170-721FFACB4B08}.Debug|x64.ActiveCfg = Debug|Any CPU
{44DDF8F5-469B-4650-A170-721FFACB4B08}.Debug|x64.Build.0 = Debug|Any CPU
{44DDF8F5-469B-4650-A170-721FFACB4B08}.Debug|x86.ActiveCfg = Debug|Any CPU
{44DDF8F5-469B-4650-A170-721FFACB4B08}.Debug|x86.Build.0 = Debug|Any CPU
{44DDF8F5-469B-4650-A170-721FFACB4B08}.Release|Any CPU.ActiveCfg = Release|Any CPU
{44DDF8F5-469B-4650-A170-721FFACB4B08}.Release|Any CPU.Build.0 = Release|Any CPU
{44DDF8F5-469B-4650-A170-721FFACB4B08}.Release|x64.ActiveCfg = Release|Any CPU
{44DDF8F5-469B-4650-A170-721FFACB4B08}.Release|x64.Build.0 = Release|Any CPU
{44DDF8F5-469B-4650-A170-721FFACB4B08}.Release|x86.ActiveCfg = Release|Any CPU
{44DDF8F5-469B-4650-A170-721FFACB4B08}.Release|x86.Build.0 = Release|Any CPU
{709CB2BA-85F0-41E1-8D28-F46CAC8BF783}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{709CB2BA-85F0-41E1-8D28-F46CAC8BF783}.Debug|Any CPU.Build.0 = Debug|Any CPU
{709CB2BA-85F0-41E1-8D28-F46CAC8BF783}.Debug|x64.ActiveCfg = Debug|Any CPU
{709CB2BA-85F0-41E1-8D28-F46CAC8BF783}.Debug|x64.Build.0 = Debug|Any CPU
{709CB2BA-85F0-41E1-8D28-F46CAC8BF783}.Debug|x86.ActiveCfg = Debug|Any CPU
{709CB2BA-85F0-41E1-8D28-F46CAC8BF783}.Debug|x86.Build.0 = Debug|Any CPU
{709CB2BA-85F0-41E1-8D28-F46CAC8BF783}.Release|Any CPU.ActiveCfg = Release|Any CPU
{709CB2BA-85F0-41E1-8D28-F46CAC8BF783}.Release|Any CPU.Build.0 = Release|Any CPU
{709CB2BA-85F0-41E1-8D28-F46CAC8BF783}.Release|x64.ActiveCfg = Release|Any CPU
{709CB2BA-85F0-41E1-8D28-F46CAC8BF783}.Release|x64.Build.0 = Release|Any CPU
{709CB2BA-85F0-41E1-8D28-F46CAC8BF783}.Release|x86.ActiveCfg = Release|Any CPU
{709CB2BA-85F0-41E1-8D28-F46CAC8BF783}.Release|x86.Build.0 = Release|Any CPU
{F1B663D7-DBB9-4FB0-B8D5-60A8A077CD4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F1B663D7-DBB9-4FB0-B8D5-60A8A077CD4F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F1B663D7-DBB9-4FB0-B8D5-60A8A077CD4F}.Debug|x64.ActiveCfg = Debug|Any CPU
{F1B663D7-DBB9-4FB0-B8D5-60A8A077CD4F}.Debug|x64.Build.0 = Debug|Any CPU
{F1B663D7-DBB9-4FB0-B8D5-60A8A077CD4F}.Debug|x86.ActiveCfg = Debug|Any CPU
{F1B663D7-DBB9-4FB0-B8D5-60A8A077CD4F}.Debug|x86.Build.0 = Debug|Any CPU
{F1B663D7-DBB9-4FB0-B8D5-60A8A077CD4F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F1B663D7-DBB9-4FB0-B8D5-60A8A077CD4F}.Release|Any CPU.Build.0 = Release|Any CPU
{F1B663D7-DBB9-4FB0-B8D5-60A8A077CD4F}.Release|x64.ActiveCfg = Release|Any CPU
{F1B663D7-DBB9-4FB0-B8D5-60A8A077CD4F}.Release|x64.Build.0 = Release|Any CPU
{F1B663D7-DBB9-4FB0-B8D5-60A8A077CD4F}.Release|x86.ActiveCfg = Release|Any CPU
{F1B663D7-DBB9-4FB0-B8D5-60A8A077CD4F}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{1FDE52B4-ADE3-45C1-92B1-7FBDED0AF791} = {CCFF03C2-3201-45FB-2E8F-00A04B2D057D}
{64FD8CEC-4C54-4873-B4E9-CA3F6BDA108A} = {ECE5971F-AE9E-BA12-55AA-4929917FAF06}
{521D7579-B37C-47B5-AE2C-6ACFCC1AD790} = {CAF2E94B-1F3E-339F-03CF-980AEBF14E62}
{44DDF8F5-469B-4650-A170-721FFACB4B08} = {99F3A7AA-53A2-3584-6717-8D7F80AF0238}
{709CB2BA-85F0-41E1-8D28-F46CAC8BF783} = {DF537D00-43B0-169F-440F-F716863E989D}
{F1B663D7-DBB9-4FB0-B8D5-60A8A077CD4F} = {80F48381-A032-BB77-AC00-2E31FC5BD3EE}
EndGlobalSection
EndGlobal
92 changes: 92 additions & 0 deletions dotnet/a2a-raw.tests/ArgParsingTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using WorkIQ.A2ARaw;
using Xunit;

namespace WorkIQ.A2ARaw.Tests;

/// <summary>
/// Tests for <see cref="Helpers.ParseArgs"/> — the arg-parsing logic
/// used by the a2a-raw sample app.
/// </summary>
public class ArgParsingTests
{
[Fact]
public void ValidArgs_AllParsedCorrectly()
{
var r = Helpers.ParseArgs(["--endpoint", "https://example.com", "--token", "abc", "--appid", "id1", "--stream"]);
Assert.Null(r.Error);
Assert.Equal("https://example.com", r.Endpoint);
Assert.Equal("abc", r.Token);
Assert.Equal("id1", r.AppId);
Assert.True(r.Stream);
}

[Fact]
public void ShortFlags_Work()
{
var r = Helpers.ParseArgs(["-e", "https://example.com", "-t", "tok", "-a", "app"]);
Assert.Null(r.Error);
Assert.Equal("https://example.com", r.Endpoint);
Assert.Equal("tok", r.Token);
Assert.Equal("app", r.AppId);
}

[Fact]
public void UnknownFlag_ReturnsError()
{
var r = Helpers.ParseArgs(["--unknown"]);
Assert.NotNull(r.Error);
Assert.Contains("Unknown flag", r.Error);
}

[Fact]
public void AllHeadersFlag_Parsed()
{
var r = Helpers.ParseArgs(["--all-headers", "--endpoint", "url", "--token", "t"]);
Assert.True(r.AllHeaders);
}

[Fact]
public void AccountFlag_Parsed()
{
var r = Helpers.ParseArgs(["--endpoint", "url", "--token", "t", "--account", "user@example.com"]);
Assert.Equal("user@example.com", r.Account);
}

[Fact]
public void MissingValueAfterToken_ReturnsError()
{
var r = Helpers.ParseArgs(["--token"]);
Assert.NotNull(r.Error);
Assert.Contains("Missing value", r.Error);
Assert.Contains("--token", r.Error);
}

[Fact]
public void MissingValueAfterEndpoint_ReturnsError()
{
var r = Helpers.ParseArgs(["--endpoint"]);
Assert.NotNull(r.Error);
Assert.Contains("Missing value", r.Error);
Assert.Contains("--endpoint", r.Error);
}

[Fact]
public void MissingValueAfterShortFlag_ReturnsError()
{
var r = Helpers.ParseArgs(["-t"]);
Assert.NotNull(r.Error);
Assert.Contains("Missing value", r.Error);
}

[Fact]
public void EmptyArgs_NoError()
{
var r = Helpers.ParseArgs([]);
Assert.Null(r.Error);
Assert.Null(r.Endpoint);
Assert.Null(r.Token);
}
}
Loading