Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
8a9aee7
initial commit
Mar 27, 2026
6a0f6bc
add dual model support - 5.4 and computer-use-preview
Mar 30, 2026
2581f90
fix nits
Mar 30, 2026
64b2121
add comment
Apr 2, 2026
7535418
Merge branch 'main' of https://github.com/microsoft/Agent365-Samples …
desmarest Apr 6, 2026
f11bfc3
feat: multi-session handling per conversation
Apr 6, 2026
78ddf83
Merge branch 'mabdelkader/cuaAgentSample' of https://github.com/Moham…
desmarest Apr 6, 2026
407b43c
allow ending sessions and conversational messages, fix nuget and pack…
Apr 6, 2026
030efb0
add per agent user onedrive creation
Apr 7, 2026
8d49310
feat: use previous_response_id to avoid resending screenshots in CUA …
desmarest Apr 7, 2026
f7446b4
Merge pull request #1 from desmarest/bertd/cua-previous-response-id
MohamedAbdekader Apr 8, 2026
9cb8229
fix: keep last screenshot pair between messages + session recovery fo…
desmarest Apr 8, 2026
322daf1
fix: use W365 session ID for OneDrive screenshot folder names
desmarest Apr 8, 2026
e548392
refactor: improve variable names in history pruning logic
desmarest Apr 8, 2026
09148bc
feat: multi-server MCP support and function tool integration
desmarest Apr 16, 2026
8d8f009
Merge pull request #3 from desmarest/bertd/latest-fixes
MohamedAbdekader Apr 17, 2026
5ea71c3
Restore OneDrive folder link, add mail tooling, logging, prompt tweaks
desmarest Apr 17, 2026
bcd331b
Merge remote-tracking branch 'myfork/mabdelkader/cuaAgentSample' into…
desmarest Apr 17, 2026
908ef9e
Revert "Merge remote-tracking branch 'myfork/mabdelkader/cuaAgentSamp…
desmarest Apr 17, 2026
e5a1895
Port EndSession + session recovery + packaging hygiene from mabdelkader
desmarest Apr 17, 2026
c189f13
Fix intermittent Kestrel "Reading is already in progress" crash
desmarest Apr 17, 2026
ce58c6f
Expand ToolingManifest to 10 MCP servers (mail/calendar/teams/odsp/etc.)
desmarest Apr 17, 2026
06795f0
Merge pull request #4 from MohamedAbdekader/users/bertd/multi-mcp-tools
MohamedAbdekader Apr 17, 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
25 changes: 25 additions & 0 deletions dotnet/w365-computer-use/W365ComputerUseSample.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.14.36623.8
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "W365ComputerUseSample", "sample-agent\W365ComputerUseSample.csproj", "{B72D1A3E-4F8C-9E56-A1B2-C3D4E5F60789}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{B72D1A3E-4F8C-9E56-A1B2-C3D4E5F60789}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B72D1A3E-4F8C-9E56-A1B2-C3D4E5F60789}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B72D1A3E-4F8C-9E56-A1B2-C3D4E5F60789}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B72D1A3E-4F8C-9E56-A1B2-C3D4E5F60789}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D4E5F6A7-B8C9-0D1E-2F3A-4B5C6D7E8F90}
EndGlobalSection
EndGlobal
6 changes: 6 additions & 0 deletions dotnet/w365-computer-use/sample-agent/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
appsettings.Development.json
Screenshots/
a365.config.json
a365.generated.config.json
app.zip
publish/
369 changes: 369 additions & 0 deletions dotnet/w365-computer-use/sample-agent/Agent/MyAgent.cs

Large diffs are not rendered by default.

170 changes: 170 additions & 0 deletions dotnet/w365-computer-use/sample-agent/AspNetExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.Agents.Authentication;
using Microsoft.Agents.Core;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using Microsoft.IdentityModel.Validators;
using System.Collections.Concurrent;
using System.Globalization;
using System.IdentityModel.Tokens.Jwt;

namespace W365ComputerUseSample;

public static class AspNetExtensions
{
private static readonly ConcurrentDictionary<string, ConfigurationManager<OpenIdConnectConfiguration>> _openIdMetadataCache = new();

public static void AddAgentAspNetAuthentication(this IServiceCollection services, IConfiguration configuration, string tokenValidationSectionName = "TokenValidation")
{
IConfigurationSection tokenValidationSection = configuration.GetSection(tokenValidationSectionName);

if (!tokenValidationSection.Exists() || !tokenValidationSection.GetValue("Enabled", true))
{
System.Diagnostics.Trace.WriteLine("AddAgentAspNetAuthentication: Auth disabled");
return;
}

services.AddAgentAspNetAuthentication(tokenValidationSection.Get<TokenValidationOptions>()!);
}

public static void AddAgentAspNetAuthentication(this IServiceCollection services, TokenValidationOptions validationOptions)
{
AssertionHelpers.ThrowIfNull(validationOptions, nameof(validationOptions));

if (validationOptions.Audiences == null || validationOptions.Audiences.Count == 0)
{
throw new ArgumentException($"{nameof(TokenValidationOptions)}:Audiences requires at least one ClientId");
}

foreach (var audience in validationOptions.Audiences)
{
if (!Guid.TryParse(audience, out _))
{
throw new ArgumentException($"{nameof(TokenValidationOptions)}:Audiences values must be a GUID");
}
}

if (validationOptions.ValidIssuers == null || validationOptions.ValidIssuers.Count == 0)
{
validationOptions.ValidIssuers =
[
"https://api.botframework.com",
"https://sts.windows.net/d6d49420-f39b-4df7-a1dc-d59a935871db/",
"https://login.microsoftonline.com/d6d49420-f39b-4df7-a1dc-d59a935871db/v2.0",
"https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/",
"https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0",
"https://sts.windows.net/69e9b82d-4842-4902-8d1e-abc5b98a55e8/",
"https://login.microsoftonline.com/69e9b82d-4842-4902-8d1e-abc5b98a55e8/v2.0",
];

if (!string.IsNullOrEmpty(validationOptions.TenantId) && Guid.TryParse(validationOptions.TenantId, out _))
{
validationOptions.ValidIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidTokenIssuerUrlTemplateV1, validationOptions.TenantId));
validationOptions.ValidIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidTokenIssuerUrlTemplateV2, validationOptions.TenantId));
}
}

if (string.IsNullOrEmpty(validationOptions.AzureBotServiceOpenIdMetadataUrl))
{
validationOptions.AzureBotServiceOpenIdMetadataUrl = validationOptions.IsGov ? AuthenticationConstants.GovAzureBotServiceOpenIdMetadataUrl : AuthenticationConstants.PublicAzureBotServiceOpenIdMetadataUrl;
}

if (string.IsNullOrEmpty(validationOptions.OpenIdMetadataUrl))
{
validationOptions.OpenIdMetadataUrl = validationOptions.IsGov ? AuthenticationConstants.GovOpenIdMetadataUrl : AuthenticationConstants.PublicOpenIdMetadataUrl;
}

var openIdMetadataRefresh = validationOptions.OpenIdMetadataRefresh ?? BaseConfigurationManager.DefaultAutomaticRefreshInterval;

_ = services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(5),
ValidIssuers = validationOptions.ValidIssuers,
ValidAudiences = validationOptions.Audiences,
ValidateIssuerSigningKey = true,
RequireSignedTokens = true,
};

options.TokenValidationParameters.EnableAadSigningKeyIssuerValidation();

options.Events = new JwtBearerEvents
{
OnMessageReceived = async context =>
{
string authorizationHeader = context.Request.Headers.Authorization.ToString();

if (string.IsNullOrEmpty(authorizationHeader))
{
context.Options.TokenValidationParameters.ConfigurationManager ??= options.ConfigurationManager as BaseConfigurationManager;
await Task.CompletedTask.ConfigureAwait(false);
return;
}

string[] parts = authorizationHeader?.Split(' ')!;
if (parts.Length != 2 || parts[0] != "Bearer")
{
context.Options.TokenValidationParameters.ConfigurationManager ??= options.ConfigurationManager as BaseConfigurationManager;
await Task.CompletedTask.ConfigureAwait(false);
return;
}

JwtSecurityToken token = new(parts[1]);
string issuer = token.Claims.FirstOrDefault(claim => claim.Type == AuthenticationConstants.IssuerClaim)?.Value!;

if (validationOptions.AzureBotServiceTokenHandling && AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer))
{
context.Options.TokenValidationParameters.ConfigurationManager = _openIdMetadataCache.GetOrAdd(validationOptions.AzureBotServiceOpenIdMetadataUrl, key =>
{
return new ConfigurationManager<OpenIdConnectConfiguration>(validationOptions.AzureBotServiceOpenIdMetadataUrl, new OpenIdConnectConfigurationRetriever(), new HttpClient())
{
AutomaticRefreshInterval = openIdMetadataRefresh
};
});
}
else
{
context.Options.TokenValidationParameters.ConfigurationManager = _openIdMetadataCache.GetOrAdd(validationOptions.OpenIdMetadataUrl, key =>
{
return new ConfigurationManager<OpenIdConnectConfiguration>(validationOptions.OpenIdMetadataUrl, new OpenIdConnectConfigurationRetriever(), new HttpClient())
{
AutomaticRefreshInterval = openIdMetadataRefresh
};
});
}

await Task.CompletedTask.ConfigureAwait(false);
},
OnTokenValidated = context => Task.CompletedTask,
OnForbidden = context => Task.CompletedTask,
OnAuthenticationFailed = context => Task.CompletedTask
};
});
}

public class TokenValidationOptions
{
public IList<string>? Audiences { get; set; }
public string? TenantId { get; set; }
public IList<string>? ValidIssuers { get; set; }
public bool IsGov { get; set; } = false;
public string? AzureBotServiceOpenIdMetadataUrl { get; set; }
public string? OpenIdMetadataUrl { get; set; }
public bool AzureBotServiceTokenHandling { get; set; } = true;
public TimeSpan? OpenIdMetadataRefresh { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Text;

namespace W365ComputerUseSample.ComputerUse;

/// <summary>
/// Sends CUA model requests to Azure OpenAI using an API key.
/// This is the default provider for external customers.
/// </summary>
public class AzureOpenAIModelProvider : ICuaModelProvider
{
private readonly HttpClient _httpClient;
private readonly string _url;
private readonly string _apiKey;
private readonly ILogger<AzureOpenAIModelProvider> _logger;

public string ModelName { get; }

public AzureOpenAIModelProvider(IHttpClientFactory httpClientFactory, IConfiguration configuration, ILogger<AzureOpenAIModelProvider> logger)
{
_httpClient = httpClientFactory.CreateClient("WebClient");
_logger = logger;
var endpoint = configuration["AIServices:AzureOpenAI:Endpoint"]
?? throw new InvalidOperationException("AIServices:AzureOpenAI:Endpoint is required.");
_apiKey = configuration["AIServices:AzureOpenAI:ApiKey"]
?? throw new InvalidOperationException("AIServices:AzureOpenAI:ApiKey is required.");
var apiVersion = configuration["AIServices:AzureOpenAI:ApiVersion"] ?? "2025-04-01-preview";

// DeploymentName = deployment-based URL; ModelName = model-based URL (model sent in body)
var deploymentName = configuration["AIServices:AzureOpenAI:DeploymentName"];
ModelName = configuration["AIServices:AzureOpenAI:ModelName"]
?? deploymentName
?? "computer-use-preview";

if (!string.IsNullOrEmpty(deploymentName))
{
_url = $"{endpoint.TrimEnd('/')}/openai/deployments/{deploymentName}/responses?api-version={apiVersion}";
}
else
{
// Model-based endpoint — model name goes in the request body, not the URL
_url = $"{endpoint.TrimEnd('/')}/openai/responses?api-version={apiVersion}";
}
}

public async Task<string> SendAsync(string requestBody, CancellationToken cancellationToken)
{
_logger.LogInformation("Azure OpenAI request URL: {Url}", _url);
using var req = new HttpRequestMessage(HttpMethod.Post, _url);
req.Headers.Add("api-key", _apiKey);
req.Content = new StringContent(requestBody, Encoding.UTF8, "application/json");

var resp = await _httpClient.SendAsync(req, cancellationToken);
if (!resp.IsSuccessStatusCode)
{
var err = await resp.Content.ReadAsStringAsync(cancellationToken);
throw new HttpRequestException($"Azure OpenAI returned {resp.StatusCode}: {err}");
}

return await resp.Content.ReadAsStringAsync(cancellationToken);
}
}
Loading
Loading