Skip to content
Closed
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
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -398,3 +398,13 @@ devTools/
node_modules/
*.tsbuildinfo


# a365 CLI local/generated config (tenant/app identifiers - do not commit)
a365.config.json
a365.generated.config.json

# a365-generated Teams app package
**/appPackage/

# Slack extension per-conversation state (may contain API tokens)
samples/dotnet/Agent Framework/slack/
282 changes: 235 additions & 47 deletions samples/dotnet/Agent Framework/Agent/WeatherAgent.cs

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions samples/dotnet/Agent Framework/AgentFrameworkWeather.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@
<PackageReference Include="Microsoft.Agents.Authentication.Msal" Version="1.6.*" />
<PackageReference Include="Microsoft.Agents.Hosting.AspNetCore" Version="1.6.*" />

<!-- Slack channel extension (Slack Blocks / interactive messages) -->
<PackageReference Include="Microsoft.Agents.Extensions.Slack" 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
12 changes: 12 additions & 0 deletions samples/dotnet/Agent Framework/ToolingManifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"mcpServers": [
{
"mcpServerName": "mcp_TeamsServer",
"mcpServerUniqueName": "mcp_TeamsServer",
"url": "https://agent365.svc.cloud.microsoft/agents/servers/mcp_TeamsServer",
"scope": "Tools.ListInvoke.All",
"audience": "ce5029ee-c1d3-45c0-bdcc-efb5a4245687",
"publisher": "Microsoft"
}
]
}
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
34 changes: 34 additions & 0 deletions samples/dotnet/discordagent/DiscordAgent.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UserSecretsId>discord-agent-vinchen</UserSecretsId>
</PropertyGroup>

<ItemGroup>
<!-- Discord API wrapper (Gateway + REST). There is no Azure Bot Service Slack-style
channel for Discord, so we host the bot directly with Discord.Net. -->
<PackageReference Include="Discord.Net.WebSocket" Version="3.*" />

<!-- Configuration (user-secrets for the bot token, env vars). -->
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.*" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.*" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.*" />

<!-- Agent Framework + Azure OpenAI (same stack as the weather sample). -->
<PackageReference Include="Azure.AI.OpenAI" Version="2.7.0-beta.2" />
<PackageReference Include="Microsoft.Agents.AI" Version="1.10.0" />
<PackageReference Include="Microsoft.Extensions.AI" Version="10.7.0" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.7.0" />

<!-- WorkIQ (Agent 365) MCP client - connect directly to mcp_TeamsServer with a dev bearer token. -->
<PackageReference Include="ModelContextProtocol.Core" Version="0.2.0-preview.3" />

<!-- Weather data source. -->
<PackageReference Include="OpenWeatherMapSharp" Version="4.2.0" />
</ItemGroup>

</Project>
206 changes: 206 additions & 0 deletions samples/dotnet/discordagent/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Collections.Concurrent;
using System.Text;
using Azure;
using Azure.AI.OpenAI;
using Discord;
using Discord.WebSocket;
using DiscordAgent.Tools;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using ModelContextProtocol.Client;

// Discord host for the weather (+ WorkIQ, next step) agent.
//
// Discord has no first-party Azure Bot Service channel, so we connect directly to the
// Discord Gateway with Discord.Net and drive the Agent Framework agent ourselves:
// Discord message -> agent.RunStreamingAsync -> reply as a Discord embed (Discord's card).

var config = new ConfigurationBuilder()
.AddUserSecrets(typeof(Program).Assembly)
.AddEnvironmentVariables()
.Build();

// Bot token from user-secret "Discord:BotToken" or env var DISCORD_BOT_TOKEN.
var token = config["Discord:BotToken"] ?? Environment.GetEnvironmentVariable("DISCORD_BOT_TOKEN");
if (string.IsNullOrWhiteSpace(token))
{
Console.Error.WriteLine(
"Missing Discord bot token. Set it with:\n" +
" dotnet user-secrets set \"Discord:BotToken\" \"<token>\"\n" +
"or the DISCORD_BOT_TOKEN environment variable.");
return;
}

// ---- Build the agent (same stack as the weather sample) ----
var endpoint = config["AIServices:AzureOpenAI:Endpoint"];
var apiKey = config["AIServices:AzureOpenAI:ApiKey"];
var deployment = config["AIServices:AzureOpenAI:DeploymentName"];
var openWeatherApiKey = config["OpenWeatherApiKey"];

if (string.IsNullOrWhiteSpace(endpoint) || string.IsNullOrWhiteSpace(apiKey) ||
string.IsNullOrWhiteSpace(deployment) || string.IsNullOrWhiteSpace(openWeatherApiKey))
{
Console.Error.WriteLine(
"Missing AI/weather config. Set user-secrets:\n" +
" AIServices:AzureOpenAI:Endpoint, AIServices:AzureOpenAI:ApiKey, AIServices:AzureOpenAI:DeploymentName, OpenWeatherApiKey");
return;
}

const string instructions = """
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:
1. United States weather -- use your weather tools:
- current-weather tool for current conditions (temperature, low/high, wind, humidity, short description).
- forecast tool for the next 5 days (date, high/low, short description).
- date tool to resolve "today". Location is a city name; resolve 2-letter US state codes to the full US state name.
2. Anything that is NOT United States weather -- use the WorkIQ tools (Microsoft 365 / Teams: chats, channels, teams, messages).

Routing rule: US weather questions go to the weather tools; every other question goes to the WorkIQ tools.
Format answers nicely in markdown, keep them easy to read, and always speak like a cat. Use emojis if it fits!
""";

IChatClient chatClient = new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(apiKey))
.GetChatClient(deployment)
.AsIChatClient();

var weatherTool = new WeatherTool(openWeatherApiKey);
var tools = new List<AITool>
{
AIFunctionFactory.Create(DateTimeTool.GetDate),
AIFunctionFactory.Create(weatherTool.GetCurrentWeatherForLocation),
AIFunctionFactory.Create(weatherTool.GetWeatherForecastForLocation),
};

// ---- WorkIQ (Agent 365) MCP tools ----
// Discord has no OBO sign-in channel, so we use a dev bearer token for the Teams MCP server.
// Token from user-secret "WorkIQ:McpTeamsServerToken" or env BEARER_TOKEN_MCP_TEAMSSERVER
// (refresh with: a365 develop get-token ...). If absent, the agent runs weather-only.
var mcpToken = config["WorkIQ:McpTeamsServerToken"] ?? Environment.GetEnvironmentVariable("BEARER_TOKEN_MCP_TEAMSSERVER");
if (!string.IsNullOrWhiteSpace(mcpToken))
{
try
{
var mcpTransport = new SseClientTransport(new SseClientTransportOptions
{
Endpoint = new Uri("https://agent365.svc.cloud.microsoft/agents/servers/mcp_TeamsServer"),
AdditionalHeaders = new Dictionary<string, string> { ["Authorization"] = $"Bearer {mcpToken}" },
TransportMode = HttpTransportMode.AutoDetect,
Name = "mcp_TeamsServer",
});
var mcpClient = await McpClientFactory.CreateAsync(mcpTransport);
var mcpTools = await mcpClient.ListToolsAsync();
tools.AddRange(mcpTools);
Console.WriteLine($"WorkIQ MCP tools loaded from mcp_TeamsServer: {mcpTools.Count}");
}
catch (Exception ex)
{
Console.Error.WriteLine($"WorkIQ MCP tools failed to load (continuing weather-only): {ex.Message}");
}
}
else
{
Console.WriteLine("No WorkIQ MCP token (BEARER_TOKEN_MCP_TEAMSSERVER) - running weather-only.");
}

AIAgent agent = new ChatClientAgent(chatClient, new ChatClientAgentOptions
{
Name = "Purrfect Weather Agent",
ChatOptions = new ChatOptions
{
Temperature = 0.2f,
Tools = tools,
Instructions = instructions,
AllowMultipleToolCalls = true,
},
});

// One conversation session per Discord channel, so context is preserved within a channel.
var sessions = new ConcurrentDictionary<ulong, AgentSession>();

// ---- Discord wiring ----
var socketConfig = new DiscordSocketConfig
{
// MessageContent is a privileged intent - enable it in the Discord Developer Portal
// (Bot -> Privileged Gateway Intents -> Message Content Intent).
GatewayIntents =
GatewayIntents.Guilds |
GatewayIntents.GuildMessages |
GatewayIntents.DirectMessages |
GatewayIntents.MessageContent,
LogLevel = LogSeverity.Info
};

var client = new DiscordSocketClient(socketConfig);

client.Log += msg =>
{
Console.WriteLine(msg.ToString());
return Task.CompletedTask;
};

client.Ready += () =>
{
Console.WriteLine($"Discord bot ready as {client.CurrentUser}");
return Task.CompletedTask;
};

client.MessageReceived += async (SocketMessage message) =>
{
// Only handle real user messages; ignore system messages and other bots (incl. ourselves).
if (message is not SocketUserMessage userMessage) return;
if (message.Author.IsBot) return;

var userText = message.Content?.Trim() ?? string.Empty;
if (string.IsNullOrEmpty(userText)) return;

try
{
using (userMessage.Channel.EnterTypingState())
{
var session = sessions.GetOrAdd(message.Channel.Id, _ => agent.CreateSessionAsync().GetAwaiter().GetResult());

// Run the agent and collect the full answer.
var sb = new StringBuilder();
await foreach (var update in agent.RunStreamingAsync(userText, session))
{
if (update.Role == ChatRole.Assistant && !string.IsNullOrEmpty(update.Text))
{
sb.Append(update.Text);
}
}

var answer = sb.ToString();
if (string.IsNullOrWhiteSpace(answer)) answer = "_(no response)_";
if (answer.Length > 4000) answer = answer[..4000] + "…";

// Discord embed = Discord's equivalent of a Slack Block Kit card.
var embed = new EmbedBuilder()
.WithColor(new Color(0x2E, 0x9B, 0xF5))
.WithAuthor("🐾 Purrfect Assistant")
.WithDescription(answer)
.WithFooter("Agent Framework + WorkIQ · Discord adapter")
.WithCurrentTimestamp()
.Build();

await userMessage.Channel.SendMessageAsync(embed: embed);
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error handling message: {ex}");
await userMessage.Channel.SendMessageAsync("Sorry, I hit an error while processing that. 🐾");
}
};

await client.LoginAsync(TokenType.Bot, token);
await client.StartAsync();

Console.WriteLine("Discord agent host started. Press Ctrl+C to exit.");

// Keep the process running.
await Task.Delay(Timeout.Infinite);
15 changes: 15 additions & 0 deletions samples/dotnet/discordagent/Tools/DateTimeTool.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.ComponentModel;

namespace DiscordAgent.Tools;

/// <summary>
/// Simple date/time tool so the agent can resolve "today" for forecasts.
/// </summary>
public static class DateTimeTool
{
[Description("Gets the current date and time (UTC).")]
public static string GetDate() => DateTimeOffset.UtcNow.ToString("f");
}
Loading
Loading