Skip to content
Open
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
189 changes: 151 additions & 38 deletions samples/dotnet/Agent Framework/Agent/WeatherAgent.cs
Original file line number Diff line number Diff line change
@@ -1,45 +1,69 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using AgentFrameworkWeather.Tools;
using Microsoft.Agents.AI;
using Microsoft.Agents.Builder;
using Microsoft.Agents.Builder.App;
using Microsoft.Agents.Builder.State;
using Microsoft.Agents.Core;
using Microsoft.Agents.Core.Models;
using Microsoft.Agents.Core.Serialization;
using Microsoft.Agents.Core.Telemetry;
using Microsoft.Extensions.AI;
using System.Text.Json;

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using AgentFrameworkWeather.Tools;
using Microsoft.Agents.A365.Runtime.Utils;
using Microsoft.Agents.A365.Tooling.Extensions.AgentFramework.Services;
using Microsoft.Agents.AI;
using Microsoft.Agents.Builder;
using Microsoft.Agents.Builder.App;
using Microsoft.Agents.Builder.State;
using Microsoft.Agents.Core;
using Microsoft.Agents.Core.Models;
using Microsoft.Agents.Core.Serialization;
using Microsoft.Agents.Core.Telemetry;
using Microsoft.Extensions.AI;
using System.Text.Json;

namespace AgentFrameworkWeather.Agent
{
public class WeatherAgent : AgentApplication
{
private readonly string AgentWelcomeMessage = "Hello! I'm your friendly weather cat assistant. I can help you find the current weather or a weather forecast for any city. Just tell me the city name and, if you're in the US, the 2-letter state code. Meow!";

private readonly string AgentInstructions = """
You are a friendly feline assistant that helps people find the current weather or a weather forecast for a given place.
You will always speak like a cat.
Location is a city name, 2 letter US state codes should be resolved to the full name of the United States State.
You may ask follow up questions until you have enough information to answer the customers question, but once you have the current weather or a forecast, make sure to format it nicely in text.

For current weather, Use the {{WeatherLookupTool.GetCurrentWeatherForLocation}}, you should include the current temperature, low and high temperatures, wind speed, humidity, and a short description of the weather.
For forecast's, Use the {{WeatherLookupTool.GetWeatherForecastForLocation}}, you should report on the next 5 days, including the current day, and include the date, high and low temperatures, and a short description of the weather.
You should use the {{DateTimePlugin.GetDateTime}} to get the current date and time.

When responding, make sure to format the information in a way that is easy to read and understand, markdown is good, and always speak like a cat. Use emojis if it fits the response!

private readonly string AgentWelcomeMessage = "Hello! I'm your friendly purr-ductivity cat assistant. I can fetch the current weather or forecast for any US city, and I can help with your Microsoft 365 work too - like Teams chats, mail, and files. Ask me a weather question (city + 2-letter state) or anything about your workday. Meow!";

private readonly string AgentInstructions = """
You are a friendly feline assistant. You always speak like a cat (use "meow", playful cat puns, and emojis when they fit).

You can help with two kinds of requests, and you must always pick the right tool for each:

1. Weather in the United States -- use your local weather tools:
- Use {{WeatherLookupTool.GetCurrentWeatherForLocation}} for current conditions. Include the current temperature, low and high temperatures, wind speed, humidity, and a short description of the weather.
- Use {{WeatherLookupTool.GetWeatherForecastForLocation}} for forecasts. Report the next 5 days, including the current day, with the date, high and low temperatures, and a short description.
- Use {{DateTimeFunctionTool.getDate}} to get the current date and time.
- Location is a city name; resolve 2-letter US state codes to the full name of the United States state.

2. Anything that is NOT United States weather -- use the WorkIQ tools to answer. This includes Microsoft 365 and Microsoft Teams tasks such as reading or posting chat messages, listing chats, channels, and teams, and other workplace questions.

Routing rule: US weather questions go to the weather tools; every other question goes to the WorkIQ tools. You may ask brief follow-up questions when you need more detail. Always format answers nicely in markdown, keep them easy to read, and always speak like a cat. Use emojis if it fits the response!
""";

private readonly IChatClient? _chatClient = null;
private readonly IConfiguration? _configuration = null;
private readonly ILogger<WeatherAgent>? _logger = null;

// WorkIQ (Agent 365) MCP tool service. Nullable so the agent still runs
// (weather-only) when the service or its configuration is unavailable.
private readonly IMcpToolRegistrationService? _toolService = null;

// Auth handler names for MCP access (configurable via appsettings.json).
private readonly string? AgenticAuthHandlerName;
private readonly string? OboAuthHandlerName;

public WeatherAgent(AgentApplicationOptions options, IChatClient chatClient, IConfiguration configuration) : base(options)
public WeatherAgent(
AgentApplicationOptions options,
IChatClient chatClient,
IConfiguration configuration,
IMcpToolRegistrationService? toolService = null,
ILogger<WeatherAgent>? logger = null) : base(options)
{
_chatClient = chatClient;
_configuration = configuration;
_toolService = toolService;
_logger = logger;

// Read auth handler names from configuration (can be empty/null to disable).
AgenticAuthHandlerName = _configuration.GetValue<string>("AgentApplication:AgenticAuthHandlerName");
OboAuthHandlerName = _configuration.GetValue<string>("AgentApplication:OboAuthHandlerName");

// Greet when members are added to the conversation
OnConversationUpdate(ConversationUpdateEvents.MembersAdded, WelcomeMessageAsync);
Expand All @@ -48,6 +72,30 @@ public WeatherAgent(AgentApplicationOptions options, IChatClient chatClient, ICo
OnActivity(ActivityTypes.Message, OnMessageAsync);
}

/// <summary>
/// Check if a bearer token is available in the environment for development/testing.
/// </summary>
private static bool TryGetBearerTokenForDevelopment(out string? bearerToken)
{
bearerToken = Environment.GetEnvironmentVariable("BEARER_TOKEN");
return !string.IsNullOrEmpty(bearerToken);
}

/// <summary>
/// Graceful fallback to weather-only mode when MCP tools fail to load.
/// Only allowed in Development AND when SKIP_TOOLING_ON_ERRORS=true.
/// </summary>
private static bool ShouldSkipToolingOnErrors()
{
var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")
?? Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT")
?? "Production";
var skip = Environment.GetEnvironmentVariable("SKIP_TOOLING_ON_ERRORS");
return environment.Equals("Development", StringComparison.OrdinalIgnoreCase)
&& !string.IsNullOrEmpty(skip)
&& skip.Equals("true", StringComparison.OrdinalIgnoreCase);
}

protected async Task WelcomeMessageAsync(ITurnContext turnContext, ITurnState turnState, CancellationToken cancellationToken)
{
foreach (ChannelAccount member in turnContext.Activity.MembersAdded)
Expand All @@ -66,7 +114,13 @@ protected async Task OnMessageAsync(ITurnContext turnContext, ITurnState turnSta
try
{
var userText = turnContext.Activity.Text?.Trim() ?? string.Empty;
var _agent = GetClientAgent(turnContext);

// Pick the auth handler for this turn (agentic vs OBO). In dev the BEARER_TOKEN path is used.
string? toolAuthHandlerName = turnContext.Activity.IsAgenticRequest()
? AgenticAuthHandlerName
: OboAuthHandlerName;

var _agent = await GetClientAgent(turnContext, turnState, toolAuthHandlerName);

// Read or Create the conversation thread for this conversation.
AgentSession? thread = await GetConversationThread(_agent, turnState);
Expand Down Expand Up @@ -94,26 +148,85 @@ protected async Task OnMessageAsync(ITurnContext turnContext, ITurnState turnSta
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
private AIAgent GetClientAgent(ITurnContext context)
private async Task<AIAgent> GetClientAgent(ITurnContext context, ITurnState turnState, string? authHandlerName)
{
AssertionHelpers.ThrowIfNull(_configuration!, nameof(_configuration));
AssertionHelpers.ThrowIfNull(context, nameof(context));
AssertionHelpers.ThrowIfNull(_chatClient!, nameof(_chatClient));

// Setup the local tool to be able to access the AgentSDK current context,UserAuthorization and other services can be accessed from here as well.
// Acquire the access token once for this turn - used for WorkIQ MCP tool loading.
string? accessToken = null;
string? agentId = null;
if (!string.IsNullOrEmpty(authHandlerName))
{
// Production / Teams: exchange an OBO token via the auth handler.
accessToken = await UserAuthorization.GetTurnTokenAsync(context, authHandlerName);
agentId = Utility.ResolveAgentIdentity(context, accessToken);
}
else if (TryGetBearerTokenForDevelopment(out var bearerToken))
{
// Local dev: use the bearer token from `a365 develop get-token`.
_logger?.LogInformation("Using bearer token from environment for WorkIQ MCP.");
accessToken = bearerToken;
agentId = Utility.ResolveAgentIdentity(context, accessToken!);
}
else
{
_logger?.LogWarning("No auth handler or bearer token - WorkIQ MCP tools will not be loaded (weather-only).");
}

// Setup the local (weather) tools - these are always available.
WeatherLookupTool weatherLookupTool = new(context, _configuration!);
var toolList = new List<AITool>
{
AIFunctionFactory.Create(DateTimeFunctionTool.getDate),
AIFunctionFactory.Create(weatherLookupTool.GetCurrentWeatherForLocation),
AIFunctionFactory.Create(weatherLookupTool.GetWeatherForecastForLocation)
};

// Attach the WorkIQ MCP tools on top of the weather tools when available.
if (_toolService != null && !string.IsNullOrEmpty(agentId))
{
try
{
await context.StreamingResponse.QueueInformativeUpdateAsync("Loading tools...");

// For the bearer-token (dev) flow, pass the token as an override and
// use the OBO/agentic handler name if configured.
var handlerForMcp = !string.IsNullOrEmpty(authHandlerName)
? authHandlerName
: OboAuthHandlerName ?? AgenticAuthHandlerName ?? string.Empty;
var tokenOverride = string.IsNullOrEmpty(authHandlerName) ? accessToken : null;

var a365Tools = await _toolService.GetMcpToolsAsync(agentId, UserAuthorization, handlerForMcp, context, tokenOverride).ConfigureAwait(false);
if (a365Tools != null && a365Tools.Count > 0)
{
toolList.AddRange(a365Tools);
}
}
catch (Exception ex)
{
// If setup fails, keep serving weather instead of crashing.
if (ShouldSkipToolingOnErrors())
{
_logger?.LogWarning(ex, "Failed to register WorkIQ MCP tools. Continuing weather-only (SKIP_TOOLING_ON_ERRORS=true).");
}
else
{
_logger?.LogError(ex, "Failed to register WorkIQ MCP tools.");
throw;
}
}
}

// Setup the tools for the agent:
var toolOptions = new ChatOptions
{
Temperature = (float?)0.2,
Tools = new List<AITool>(),
Tools = toolList,
Instructions = AgentInstructions,
AllowMultipleToolCalls = true
};
toolOptions.Tools.Add(AIFunctionFactory.Create(DateTimeFunctionTool.getDate));
toolOptions.Tools.Add(AIFunctionFactory.Create(weatherLookupTool.GetCurrentWeatherForLocation));
toolOptions.Tools.Add(AIFunctionFactory.Create(weatherLookupTool.GetWeatherForecastForLocation));

// Create the chat Client passing in agent instructions and tools:
return new ChatClientAgent(_chatClient!,
Expand Down Expand Up @@ -158,5 +271,5 @@ private static async Task<AgentSession> GetConversationThread(AIAgent? agent, IT
}
return thread;
}
}
}
}
4 changes: 4 additions & 0 deletions samples/dotnet/Agent Framework/AgentFrameworkWeather.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
<PackageReference Include="Microsoft.Agents.Authentication.Msal" Version="1.6.*" />
<PackageReference Include="Microsoft.Agents.Hosting.AspNetCore" Version="1.6.*" />

<!-- Agent 365 (WorkIQ) MCP Tooling -->
<PackageReference Include="Microsoft.Agents.A365.Tooling" Version="1.0.*" />
<PackageReference Include="Microsoft.Agents.A365.Tooling.Extensions.AgentFramework" Version="1.0.*" />

<!-- Agent Framework Packages -->
<PackageReference Include="AdaptiveCards" Version="3.1.0" />
<PackageReference Include="Azure.AI.OpenAI" Version="2.7.0-beta.2" />
Expand Down
27 changes: 18 additions & 9 deletions samples/dotnet/Agent Framework/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
using AgentFrameworkWeather.Agent;
using Azure;
using Azure.AI.OpenAI;
using Microsoft.Agents.A365.Tooling.Extensions.AgentFramework.Services;
using Microsoft.Agents.A365.Tooling.Services;
using Microsoft.Agents.Builder;
using Microsoft.Agents.Core;
using Microsoft.Agents.Hosting.AspNetCore;
Expand All @@ -20,8 +22,8 @@
builder.Services.AddHttpClient("WebClient", client => client.Timeout = TimeSpan.FromSeconds(600));
builder.Services.AddHttpContextAccessor();

// Configure defaults for Aspire dashboard
builder.ConfigureOtelProviders();
// Configure defaults for Aspire dashboard
builder.ConfigureOtelProviders();

builder.Logging.AddConsole();

Expand All @@ -34,6 +36,13 @@
// in a cluster of Agent instances.
builder.Services.AddSingleton<IStorage, MemoryStorage>();

// ********** Configure A365 (WorkIQ) Services **********
// Registers the MCP tool registration + server configuration services so the
// agent can discover and call WorkIQ (Agent 365) MCP tools at runtime.
builder.Services.AddSingleton<IMcpToolRegistrationService, McpToolRegistrationService>();
builder.Services.AddSingleton<IMcpToolServerConfigurationService, McpToolServerConfigurationService>();
// ********** END Configure A365 Services **********

// Add the bot (which is transient)
builder.AddAgent<WeatherAgent>();

Expand Down Expand Up @@ -72,17 +81,17 @@
app.UseAuthentication();
app.UseAuthorization();

// Map GET "/"
app.MapAgentRootEndpoint();
// Map the endpoints for all agents using the [AgentInterface] attribute.
// If there is a single IAgent/AgentApplication, the endpoints will be mapped to (e.g. "/api/message").
app.MapAgentApplicationEndpoints(requireAuth: !(app.Environment.IsDevelopment() || app.Environment.EnvironmentName == "Playground"));
// Map GET "/"
app.MapAgentRootEndpoint();

// Map the endpoints for all agents using the [AgentInterface] attribute.
// If there is a single IAgent/AgentApplication, the endpoints will be mapped to (e.g. "/api/message").
app.MapAgentApplicationEndpoints(requireAuth: !(app.Environment.IsDevelopment() || app.Environment.EnvironmentName == "Playground"));

if (app.Environment.IsDevelopment() || app.Environment.EnvironmentName == "Playground")
{
app.UseDeveloperExceptionPage();
app.MapControllers().AllowAnonymous();
app.MapControllers().AllowAnonymous();
}
else
{
Expand Down
7 changes: 7 additions & 0 deletions samples/dotnet/Agent Framework/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@
"NormalizeMentions": false
},

"AgentApplication": {
// WorkIQ MCP auth handler names. Leave empty for local dev (uses BEARER_TOKEN env var).
// Set these for production / Teams to use OBO / agentic token exchange.
"AgenticAuthHandlerName": "",
"OboAuthHandlerName": ""
},

"Logging": {
"LogLevel": {
"Default": "Information",
Expand Down
Loading