-
Notifications
You must be signed in to change notification settings - Fork 48
Add W365 Computer Use sample agent #305
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
44 commits
Select commit
Hold shift + click to select a range
8a9aee7
initial commit
6a0f6bc
add dual model support - 5.4 and computer-use-preview
2581f90
fix nits
64b2121
add comment
7535418
Merge branch 'main' of https://github.com/microsoft/Agent365-Samples …
desmarest f11bfc3
feat: multi-session handling per conversation
78ddf83
Merge branch 'mabdelkader/cuaAgentSample' of https://github.com/Moham…
desmarest 407b43c
allow ending sessions and conversational messages, fix nuget and pack…
030efb0
add per agent user onedrive creation
8d49310
feat: use previous_response_id to avoid resending screenshots in CUA …
desmarest f7446b4
Merge pull request #1 from desmarest/bertd/cua-previous-response-id
MohamedAbdekader 9cb8229
fix: keep last screenshot pair between messages + session recovery fo…
desmarest 322daf1
fix: use W365 session ID for OneDrive screenshot folder names
desmarest e548392
refactor: improve variable names in history pruning logic
desmarest 09148bc
feat: multi-server MCP support and function tool integration
desmarest 8d8f009
Merge pull request #3 from desmarest/bertd/latest-fixes
MohamedAbdekader 5ea71c3
Restore OneDrive folder link, add mail tooling, logging, prompt tweaks
desmarest bcd331b
Merge remote-tracking branch 'myfork/mabdelkader/cuaAgentSample' into…
desmarest 908ef9e
Revert "Merge remote-tracking branch 'myfork/mabdelkader/cuaAgentSamp…
desmarest e5a1895
Port EndSession + session recovery + packaging hygiene from mabdelkader
desmarest c189f13
Fix intermittent Kestrel "Reading is already in progress" crash
desmarest ce58c6f
Expand ToolingManifest to 10 MCP servers (mail/calendar/teams/odsp/etc.)
desmarest 06795f0
Merge pull request #4 from MohamedAbdekader/users/bertd/multi-mcp-tools
MohamedAbdekader e134f5d
W365 sample agent: align with ATG remote-MCP server rename + trim log…
desmarest 5284e66
W365 sample agent: gitignore appsettings.Production.json
desmarest 3bc9263
Two-phase routing: skip W365 session for non-CUA messages; surface ac…
desmarest 0a2e255
Fix ordering: send 'Got it' acknowledgment before the streaming respo…
desmarest aeb7b67
Only show 'Acquiring session' status on cold start
desmarest 7240dcf
Route W365 tool-load errors through the streaming response
desmarest 803831a
Strip CUA history when calling model without computer tool
desmarest 06d52bc
Also strip OnTaskComplete/EndSession history pairs in non-CUA filter
desmarest 994d7b0
Save local screenshots into per-session subfolder
desmarest 24ef3f8
Sample agent: drop V1-leftover StartSessionAsync plumbing for V2 in-p…
desmarest 5427ed8
Sample agent: tighten EndSession/OnTaskComplete + handle gpt-5.4 reas…
desmarest 3a77944
Sample agent: add narrate() function tool for live progress updates
desmarest 96fd531
Sample agent: normalize OpenAI CUA action args before W365 MCP dispatch
desmarest 1a6cd42
Update W365 sample for explicit sessions
desmarest 704d24e
W365 sample: explicit-session MCP path + remove OBO handler
desmarest fe469b4
W365 sample: remove internal CustomEndpoint model provider
desmarest e9ebb83
W365 sample: address PR #305 review feedback
desmarest 4c18848
W365 sample: tighten system instructions
desmarest 868ef1c
W365 sample: address remaining style review comments
desmarest 3850c30
W365 sample: address second round of bot review comments
desmarest c410ae0
Merge branch 'main' into users/bertd/w365-computer-use-sample
desmarest File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| appsettings.Development.json | ||
| appsettings.Production.json | ||
| Screenshots/ | ||
| a365.config.json | ||
| a365.generated.config.json | ||
| app.zip | ||
| publish/ | ||
| .vscode/.env |
Large diffs are not rendered by default.
Oops, something went wrong.
179 changes: 179 additions & 0 deletions
179
dotnet/w365-computer-use/sample-agent/AspNetExtensions.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,179 @@ | ||
| // 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"); | ||
| } | ||
|
|
||
| var invalidAudiences = validationOptions.Audiences | ||
| .Where(audience => !Guid.TryParse(audience, out _)) | ||
| .ToList(); | ||
| if (invalidAudiences.Count > 0) | ||
| { | ||
| 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 = context => | ||
| { | ||
| string authorizationHeader = context.Request.Headers.Authorization.ToString(); | ||
|
|
||
| if (string.IsNullOrEmpty(authorizationHeader)) | ||
| { | ||
| context.Options.TokenValidationParameters.ConfigurationManager ??= options.ConfigurationManager as BaseConfigurationManager; | ||
| return Task.CompletedTask; | ||
| } | ||
|
|
||
| string[] parts = authorizationHeader.Split(' '); | ||
| if (parts.Length != 2 || !string.Equals(parts[0], "Bearer", StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| context.Options.TokenValidationParameters.ConfigurationManager ??= options.ConfigurationManager as BaseConfigurationManager; | ||
| return Task.CompletedTask; | ||
| } | ||
|
|
||
| string? issuer = null; | ||
| try | ||
| { | ||
| JwtSecurityToken token = new(parts[1]); | ||
| issuer = token.Claims.FirstOrDefault(claim => claim.Type == AuthenticationConstants.IssuerClaim)?.Value; | ||
| } | ||
| catch (ArgumentException) | ||
| { | ||
| // Malformed / opaque token — fall back to default configuration so the JwtBearer | ||
| // handler emits a 401 instead of a 500. | ||
| context.Options.TokenValidationParameters.ConfigurationManager ??= options.ConfigurationManager as BaseConfigurationManager; | ||
| return Task.CompletedTask; | ||
| } | ||
|
|
||
| if (validationOptions.AzureBotServiceTokenHandling && AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer)) | ||
| { | ||
| context.Options.TokenValidationParameters.ConfigurationManager = _openIdMetadataCache.GetOrAdd(validationOptions.AzureBotServiceOpenIdMetadataUrl, key => | ||
| { | ||
| return new ConfigurationManager<OpenIdConnectConfiguration>(validationOptions.AzureBotServiceOpenIdMetadataUrl, new OpenIdConnectConfigurationRetriever()) | ||
| { | ||
| AutomaticRefreshInterval = openIdMetadataRefresh | ||
| }; | ||
| }); | ||
| } | ||
| else | ||
| { | ||
| context.Options.TokenValidationParameters.ConfigurationManager = _openIdMetadataCache.GetOrAdd(validationOptions.OpenIdMetadataUrl, key => | ||
| { | ||
| return new ConfigurationManager<OpenIdConnectConfiguration>(validationOptions.OpenIdMetadataUrl, new OpenIdConnectConfigurationRetriever()) | ||
| { | ||
| AutomaticRefreshInterval = openIdMetadataRefresh | ||
| }; | ||
| }); | ||
| } | ||
|
|
||
| return Task.CompletedTask; | ||
| }, | ||
| 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; } | ||
| } | ||
| } | ||
64 changes: 64 additions & 0 deletions
64
dotnet/w365-computer-use/sample-agent/ComputerUse/AzureOpenAIModelProvider.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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}"; | ||
| } | ||
|
desmarest marked this conversation as resolved.
|
||
| } | ||
|
|
||
| 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); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.