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
79 changes: 54 additions & 25 deletions dotnet/agent-framework/sample-agent/Agent/MyAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using Microsoft.Agents.Core.Serialization;
using Microsoft.Extensions.AI;
using System.Collections.Concurrent;
using System.Linq;
using System.Text.Json;

namespace Agent365AgentFrameworkSampleAgent.Agent
Expand Down Expand Up @@ -256,8 +257,12 @@ await A365OtelWrapper.InvokeObservedAgentOperation(
var userText = turnContext.Activity.Text?.Trim() ?? string.Empty;
var _agent = await GetClientAgent(turnContext, turnState, _toolService, ToolAuthHandlerName);

// Read or Create the conversation thread for this conversation.
AgentThread? thread = GetConversationThread(_agent, turnState);
// Snapshot the persisted thread before the run. If Purview blocks this turn
// we restore this snapshot so the blocked prompt is not baked into history.
string? threadSnapshot = turnState.Conversation.GetValue<string?>("conversation.threadInfo", () => null);

// Read or Create the conversation session for this conversation.
AgentSession? thread = await GetConversationSessionAsync(_agent, turnState, cancellationToken);

if (turnContext?.Activity?.Attachments?.Count > 0)
{
Expand All @@ -270,15 +275,43 @@ await A365OtelWrapper.InvokeObservedAgentOperation(
}
}

// Stream the response back to the user as we receive it from the agent.
// Stream the response back to the user. Purview wraps its block message in a
// PurviewTextContent / PurviewBinaryContent (Microsoft.Agents.AI.Purview namespace);
// detecting that content type tells us the turn was blocked by policy.
const string PurviewContentNamespace = "Microsoft.Agents.AI.Purview";
var purviewBlocked = false;
await foreach (var response in _agent!.RunStreamingAsync(userText, thread, cancellationToken: cancellationToken))
{
if (response.Role == ChatRole.Assistant && !string.IsNullOrEmpty(response.Text))
if (string.IsNullOrEmpty(response.Text))
{
continue;
}

turnContext?.StreamingResponse.QueueTextChunk(response.Text);
if (response.Contents is not null &&
response.Contents.Any(c => c.GetType().FullName?.StartsWith(PurviewContentNamespace, StringComparison.Ordinal) == true))
{
purviewBlocked = true;
}
}

// On a Purview block, restore the pre-turn snapshot so the blocked exchange is
// not persisted to history (otherwise Purview re-blocks every subsequent turn).
if (purviewBlocked)
{
if (threadSnapshot is null)
{
turnState.Conversation.DeleteValue("conversation.threadInfo");
}
else
{
turnContext?.StreamingResponse.QueueTextChunk(response.Text);
turnState.Conversation.SetValue("conversation.threadInfo", threadSnapshot);
}
}
turnState.Conversation.SetValue("conversation.threadInfo", ProtocolJsonSerializer.ToJson(thread.Serialize()));
else
{
turnState.Conversation.SetValue("conversation.threadInfo", ProtocolJsonSerializer.ToJson(await _agent!.SerializeSessionAsync(thread, jsonSerializerOptions: null, cancellationToken)));
}
}
finally
{
Expand Down Expand Up @@ -391,52 +424,48 @@ await A365OtelWrapper.InvokeObservedAgentOperation(
}
}

// Create Chat Options with tools:
// Create Chat Options with instructions, tools, and temperature.
var toolOptions = new ChatOptions
{
Instructions = GetAgentInstructions(displayName),
Temperature = (float?)0.2,
Tools = toolList
};

// Create the chat Client passing in agent instructions and tools:
return new ChatClientAgent(_chatClient!,
// Create the chat agent with the configured options.
return new ChatClientAgent(
_chatClient!,
new ChatClientAgentOptions
{
Instructions = GetAgentInstructions(displayName),
ChatOptions = toolOptions,
ChatMessageStoreFactory = ctx =>
{
#pragma warning disable MEAI001 // MessageCountingChatReducer is for evaluation purposes only and is subject to change or removal in future updates
return new InMemoryChatMessageStore(new MessageCountingChatReducer(10), ctx.SerializedState, ctx.JsonSerializerOptions);
#pragma warning restore MEAI001 // MessageCountingChatReducer is for evaluation purposes only and is subject to change or removal in future updates
}
})
.AsBuilder()
.UseOpenTelemetry(sourceName: AgentMetrics.SourceName, (cfg) => cfg.EnableSensitiveData = true)
.Build();
}

/// <summary>
/// Manage Agent threads against the conversation state.
/// Manage Agent sessions against the conversation state.
/// </summary>
/// <param name="agent">ChatAgent</param>
/// <param name="turnState">State Manager for the Agent.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns></returns>
private static AgentThread GetConversationThread(AIAgent? agent, ITurnState turnState)
private static async Task<AgentSession> GetConversationSessionAsync(AIAgent? agent, ITurnState turnState, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(agent);
AgentThread thread;
string? agentThreadInfo = turnState.Conversation.GetValue<string?>("conversation.threadInfo", () => null);
if (string.IsNullOrEmpty(agentThreadInfo))
AgentSession session;
string? agentSessionInfo = turnState.Conversation.GetValue<string?>("conversation.threadInfo", () => null);
if (string.IsNullOrEmpty(agentSessionInfo))
{
thread = agent.GetNewThread();
session = await agent.CreateSessionAsync(cancellationToken);
}
else
{
JsonElement ele = ProtocolJsonSerializer.ToObject<JsonElement>(agentThreadInfo);
thread = agent.DeserializeThread(ele);
JsonElement ele = ProtocolJsonSerializer.ToObject<JsonElement>(agentSessionInfo);
session = await agent.DeserializeSessionAsync(ele, jsonSerializerOptions: null, cancellationToken);
}
return thread;
return session;
}

private string GetToolCacheKey(ITurnState turnState)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>7a8f9d79-5c4c-495f-8d56-1db8168ef8bd</UserSecretsId>
<Nullable>enable</Nullable>
Expand All @@ -19,18 +19,21 @@
<PackageReference Include="Microsoft.Agents.A365.Notifications" Version="*-beta.*" />
<PackageReference Include="Microsoft.Agents.A365.Tooling.Extensions.AgentFramework" Version="*-beta.*" />

<!-- Purview Policy Enforcement -->
<PackageReference Include="Microsoft.Agents.AI.Purview" Version="1.3.*-*" />

<!-- Agent Framework Packages -->
<PackageReference Include="AdaptiveCards" Version="3.1.0" />
<PackageReference Include="Azure.AI.OpenAI" Version="2.5.0-beta.1" />
<PackageReference Include="Azure.Identity" Version="1.17.0" />
<PackageReference Include="Microsoft.Agents.AI" Version="1.0.0-preview.251113.1" />
<PackageReference Include="Azure.Identity" Version="1.21.0" />
<PackageReference Include="Microsoft.Agents.AI" Version="1.3.0" />
<PackageReference Include="Microsoft.Agents.Authentication.Msal" Version="1.3.*-*" />
<PackageReference Include="Microsoft.Agents.Hosting.AspNetCore" Version="1.3.*-*" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="9.10.0-preview.1.25513.3" />

<!-- Additional Packages -->
<PackageReference Include="OpenWeatherMapSharp" Version="4.1.0" />

</ItemGroup>

<ItemGroup>
Expand Down
35 changes: 32 additions & 3 deletions dotnet/agent-framework/sample-agent/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
using Agent365AgentFrameworkSampleAgent.telemetry;
using Azure;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.A365.Tooling.Extensions.AgentFramework.Services;
using Microsoft.Agents.A365.Tooling.Services;
using Microsoft.Agents.AI.Purview;
using Microsoft.Agents.Builder;
using Microsoft.Agents.Core;
using Microsoft.Agents.Hosting.AspNetCore;
Expand Down Expand Up @@ -79,13 +81,40 @@
var apiKeyCredential = new AzureKeyCredential(apiKey);

// Create and return the AzureOpenAIClient's ChatClient
return new AzureOpenAIClient(endpointUri, apiKeyCredential)
var chatClientBuilder = new AzureOpenAIClient(endpointUri, apiKeyCredential)
.GetChatClient(deployment)
.AsIChatClient()
.AsBuilder()
.AsBuilder();

// Add Purview middleware if configured
var purviewClientAppId = confSvc["Purview:ClientAppId"];
var purviewAppName = confSvc["Purview:AppName"];
var purviewTenantId = confSvc["Purview:TenantId"];
var purviewClientSecret = confSvc["Purview:ClientSecret"];
var purviewUserId = confSvc["Purview:UserId"];

if (!string.IsNullOrEmpty(purviewClientAppId) &&
!string.IsNullOrEmpty(purviewAppName) &&
!string.IsNullOrEmpty(purviewTenantId) &&
!string.IsNullOrEmpty(purviewClientSecret))
{
// Stamp each user message with the Purview userId so the Purview middleware
// can identify the user when authenticating with app-level credentials.
if (!string.IsNullOrEmpty(purviewUserId))
{
chatClientBuilder = chatClientBuilder.Use((innerClient) =>
new PurviewUserIdStampingClient(innerClient, purviewUserId));
}

var purviewCredential = new ClientSecretCredential(purviewTenantId, purviewClientAppId, purviewClientSecret);
var purviewSettings = new PurviewSettings(purviewAppName);
chatClientBuilder = chatClientBuilder.WithPurview(purviewCredential, purviewSettings);
}

return chatClientBuilder
.UseFunctionInvocation()
.UseOpenTelemetry(sourceName: AgentMetrics.SourceName, configure: (cfg) => cfg.EnableSensitiveData = true)
.Build();
.Build();
});

// Uncomment to add transcript logging middleware to log all conversations to files
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.Extensions.AI;

namespace Agent365AgentFrameworkSampleAgent;

/// <summary>
/// A delegating <see cref="IChatClient"/> that stamps each user message with a
/// Purview userId before forwarding the call to the inner client.
/// This is required when authenticating to Purview with app-level credentials
/// (e.g. ClientSecretCredential) since the userId cannot be inferred from the token.
/// </summary>
internal sealed class PurviewUserIdStampingClient(IChatClient innerClient, string userId)
: DelegatingChatClient(innerClient)
{
private const string UserIdKey = "userId";

public override Task<ChatResponse> GetResponseAsync(
IEnumerable<ChatMessage> messages,
ChatOptions? options = null,
CancellationToken cancellationToken = default)
{
StampMessages(messages);
return base.GetResponseAsync(messages, options, cancellationToken);
}

public override IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
IEnumerable<ChatMessage> messages,
ChatOptions? options = null,
CancellationToken cancellationToken = default)
{
StampMessages(messages);
return base.GetStreamingResponseAsync(messages, options, cancellationToken);
}

private void StampMessages(IEnumerable<ChatMessage> messages)
{
foreach (var message in messages)
{
if (message.Role == ChatRole.User)
{
message.AdditionalProperties ??= new AdditionalPropertiesDictionary();
if (!message.AdditionalProperties.ContainsKey(UserIdKey))
{
message.AdditionalProperties[UserIdKey] = userId;
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,12 @@
"ApiKey": "---" // This is the API Key of the Azure OpenAI model deployment
}
},
"OpenWeatherApiKey": "---" //https://openweathermap.org/api - You will need to create a free account to get an API key.
"OpenWeatherApiKey": "---", //https://openweathermap.org/api - You will need to create a free account to get an API key.
"Purview": {
"ClientAppId": "---",
"AppName": "---",
"TenantId": "---",
"ClientSecret": "---",
"UserId": "---"
}
}
9 changes: 8 additions & 1 deletion dotnet/agent-framework/sample-agent/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,12 @@
"ApiKey": "----" // This is the API Key of the Azure OpenAI model deployment
}
},
"OpenWeatherApiKey": "----" //https://openweathermap.org/price - You will need to create a free account to get an API key (its at the bottom of the page).
"OpenWeatherApiKey": "----", //https://openweathermap.org/price - You will need to create a free account to get an API key (its at the bottom of the page).
"Purview": {
"ClientAppId": "<<ClientId>>",
"AppName": "<<YOUR_APP_NAME>>",
"TenantId": "<<TenantId>>",
"ClientSecret": "<<ClientSecret>>",
"UserId": "<<EntraUserObjectId>>"
}
}
26 changes: 25 additions & 1 deletion python/agent-framework/sample-agent/.env.template
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# This is a demo .env file
# Replace with your actual OpenAI API key
# Replace placeholders with your actual values
OPENAI_API_KEY=
MCP_SERVER_HOST=
MCP_PLATFORM_ENDPOINT=
Expand All @@ -20,6 +20,11 @@ OPENAI_MODEL=

USE_AGENTIC_AUTH=true

# Set DISABLE_MCP=true to skip MCP tool registration. Useful for local
# Microsoft 365 Agents Playground runs where the cloud MCP servers reject
# requests because the playground has no real M365 tenant context.
DISABLE_MCP=false

# Agent 365 Agentic Authentication Configuration
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID=
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET=
Expand Down Expand Up @@ -50,3 +55,22 @@ PYTHON_ENVIRONMENT=development
# Enable otel logs on AgentFramework SDK. Required for auto instrumentation
ENABLE_OTEL=true
ENABLE_SENSITIVE_DATA=true

# Purview Policy Enforcement (Optional)
# Enables Microsoft Purview policy evaluation on agent prompts and responses.
# Set PURVIEW_CLIENT_APP_ID to enable Purview; leave empty to skip.
#
# Auth priority (first match wins):
# 1. Client Secret — set PURVIEW_CLIENT_SECRET + PURVIEW_TENANT_ID
# 2. Certificate — set PURVIEW_USE_CERT_AUTH=true + PURVIEW_CERT_PATH
# 3. Interactive Browser — fallback (opens browser for sign-in)
PURVIEW_CLIENT_APP_ID=
PURVIEW_CLIENT_SECRET=
PURVIEW_TENANT_ID=
PURVIEW_APP_NAME=A365 Agent Framework Sample App
PURVIEW_DEFAULT_USER_ID=

# Certificate auth only (set PURVIEW_USE_CERT_AUTH=true to use)
PURVIEW_USE_CERT_AUTH=false
PURVIEW_CERT_PATH=
PURVIEW_CERT_PASSWORD=
16 changes: 8 additions & 8 deletions python/agent-framework/sample-agent/AGENT-CODE-WALKTHROUGH.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ Each section follows this pattern:

```python
# AgentFramework SDK
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework import ChatAgent
from agent_framework.openai import OpenAIChatClient
from agent_framework import Agent
from azure.identity import AzureCliCredential

# Agent Interface
Expand Down Expand Up @@ -119,11 +119,11 @@ def _create_chat_client(self):
if not api_version:
raise ValueError("AZURE_OPENAI_API_VERSION environment variable is required")

self.chat_client = AzureOpenAIChatClient(
endpoint=endpoint,
self.chat_client = OpenAIChatClient(
model=deployment,
azure_endpoint=endpoint,
api_version=api_version,
credential=AzureCliCredential(),
deployment_name=deployment,
api_version=api_version
)
```

Expand All @@ -149,8 +149,8 @@ def _create_agent(self):
try:
logger.info("Creating AgentFramework agent...")

self.agent = ChatAgent(
chat_client=self.chat_client,
self.agent = Agent(
client=self.chat_client,
instructions="You are a helpful assistant with access to tools.",
tools=[] # Tools will be added dynamically by MCP setup
)
Expand Down
Loading
Loading