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
52 changes: 37 additions & 15 deletions dotnet/agent-framework/sample-agent/Agent/MyAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
using Agent365AgentFrameworkSampleAgent.telemetry;
using Agent365AgentFrameworkSampleAgent.Tools;
using Microsoft.Agents.A365.Observability.Hosting.Caching;
using Microsoft.Agents.A365.Observability.Hosting.Extensions;
using Microsoft.Agents.A365.Observability.Runtime.Tracing.Contracts;
using Microsoft.Agents.A365.Observability.Runtime.Tracing.Scopes;
using Microsoft.Agents.A365.Runtime.Utils;
using Microsoft.Agents.A365.Tooling.Extensions.AgentFramework.Services;
using Microsoft.Agents.AI;
Expand Down Expand Up @@ -254,10 +257,18 @@ await A365OtelWrapper.InvokeObservedAgentOperation(
try
{
var userText = turnContext.Activity.Text?.Trim() ?? string.Empty;
using var invokeScope = InvokeAgentScope.Start(
request: new Request(userText),
scopeDetails: new InvokeAgentScopeDetails(endpoint: new Uri("http://localhost:3978")),
Comment on lines +260 to +262
agentDetails: BuildAgentDetails())
.FromTurnContext(turnContext);

invokeScope.RecordInputMessages(new[] { userText });

var _agent = await GetClientAgent(turnContext, turnState, _toolService, ToolAuthHandlerName);

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

Comment on lines +270 to 272
if (turnContext?.Activity?.Attachments?.Count > 0)
{
Expand All @@ -270,15 +281,19 @@ await A365OtelWrapper.InvokeObservedAgentOperation(
}
}

var collectedOutput = new System.Text.StringBuilder();
// Stream the response back to the user as we receive it from the agent.
await foreach (var response in _agent!.RunStreamingAsync(userText, thread, cancellationToken: cancellationToken))
{
if (response.Role == ChatRole.Assistant && !string.IsNullOrEmpty(response.Text))
{
turnContext?.StreamingResponse.QueueTextChunk(response.Text);
collectedOutput.Append(response.Text);
}
}
turnState.Conversation.SetValue("conversation.threadInfo", ProtocolJsonSerializer.ToJson(thread.Serialize()));
invokeScope.RecordOutputMessages(new[] { collectedOutput.ToString() });
var serializedSession = await _agent!.SerializeSessionAsync(thread!);
Comment on lines +284 to +295
turnState.Conversation.SetValue("conversation.threadInfo", ProtocolJsonSerializer.ToJson(serializedSession));
}
finally
{
Expand Down Expand Up @@ -340,7 +355,8 @@ await A365OtelWrapper.InvokeObservedAgentOperation(
// Create the local tools:
var toolList = new List<AITool>();
WeatherLookupTool weatherLookupTool = new(context, _configuration!);
toolList.Add(AIFunctionFactory.Create(DateTimeFunctionTool.getDate));
DateTimeFunctionTool dateTimeTool = new(_configuration!);
toolList.Add(AIFunctionFactory.Create(dateTimeTool.GetCurrentDateTime));
toolList.Add(AIFunctionFactory.Create(weatherLookupTool.GetCurrentWeatherForLocation));
toolList.Add(AIFunctionFactory.Create(weatherLookupTool.GetWeatherForecastForLocation));

Expand Down Expand Up @@ -391,25 +407,25 @@ await A365OtelWrapper.InvokeObservedAgentOperation(
}
}

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

// Create the chat Client passing in agent instructions and tools:
return new ChatClientAgent(_chatClient!,
new ChatClientAgentOptions
{
Instructions = GetAgentInstructions(displayName),
ChatOptions = toolOptions,
ChatMessageStoreFactory = ctx =>
ChatHistoryProvider = new InMemoryChatHistoryProvider(new InMemoryChatHistoryProviderOptions
{
#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);
ChatReducer = new MessageCountingChatReducer(10)
#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)
Expand All @@ -422,21 +438,19 @@ await A365OtelWrapper.InvokeObservedAgentOperation(
/// <param name="agent">ChatAgent</param>
/// <param name="turnState">State Manager for the Agent.</param>
/// <returns></returns>
private static AgentThread GetConversationThread(AIAgent? agent, ITurnState turnState)
private static async Task<AgentSession> GetConversationSessionAsync(AIAgent? agent, ITurnState turnState, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(agent);
AgentThread thread;
string? agentThreadInfo = turnState.Conversation.GetValue<string?>("conversation.threadInfo", () => null);
if (string.IsNullOrEmpty(agentThreadInfo))
{
thread = agent.GetNewThread();
return await agent.CreateSessionAsync(cancellationToken);
}
else
{
JsonElement ele = ProtocolJsonSerializer.ToObject<JsonElement>(agentThreadInfo);
thread = agent.DeserializeThread(ele);
return await agent.DeserializeSessionAsync(ele, cancellationToken: cancellationToken);
}
return thread;
}

private string GetToolCacheKey(ITurnState turnState)
Expand All @@ -450,5 +464,13 @@ private string GetToolCacheKey(ITurnState turnState)
}
return userToolCacheKey;
}

private AgentDetails BuildAgentDetails() =>
new AgentDetails(
agentId: _configuration?["Agent365Observability:AgentId"] ?? "local-dev",
agentName: _configuration?["Agent365Observability:AgentName"] ?? "my-agent",
agentDescription: _configuration?["Agent365Observability:AgentDescription"] ?? "",
agentBlueprintId: _configuration?["Agent365Observability:AgentBlueprintId"] ?? "",
tenantId: _configuration?["Agent365Observability:TenantId"] ?? "local-dev");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,20 @@

<ItemGroup>
<!-- Microsoft OpenTelemetry Distro -->
<PackageReference Include="Microsoft.OpenTelemetry" Version="1.0.0-alpha.3" />
<PackageReference Include="Microsoft.OpenTelemetry" Version="1.0.0" />

<!-- A365 SDK Packages -->
<PackageReference Include="Microsoft.Agents.A365.Notifications" Version="*-beta.*" />
<PackageReference Include="Microsoft.Agents.A365.Tooling.Extensions.AgentFramework" Version="*-beta.*" />
<PackageReference Include="Microsoft.Agents.A365.Notifications" Version="1.0.0" />
<PackageReference Include="Microsoft.Agents.A365.Tooling.Extensions.AgentFramework" Version="1.0.0" />

<!-- 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="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" />
<PackageReference Include="Azure.AI.OpenAI" Version="2.7.0-beta.2" />
<PackageReference Include="Azure.Identity" Version="1.17.1" />
<PackageReference Include="Microsoft.Agents.AI" Version="1.1.0" />
<PackageReference Include="Microsoft.Agents.Authentication.Msal" Version="1.4.83" />
<PackageReference Include="Microsoft.Agents.Hosting.AspNetCore" Version="1.4.83" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.0.1-preview.1.25571.5" />

<!-- Additional Packages -->
<PackageReference Include="OpenWeatherMapSharp" Version="4.1.0" />
Expand Down
5 changes: 5 additions & 0 deletions dotnet/agent-framework/sample-agent/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using Microsoft.Agents.Storage.Transcript;
using Microsoft.Extensions.AI;
using Microsoft.OpenTelemetry;
using OpenTelemetry;
using System.Reflection;


Expand All @@ -29,6 +30,10 @@
: ExportTarget.Agent365;
});

// Register custom activity source so spans from AgentMetrics are captured by the TracerProvider
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing.AddSource(AgentMetrics.SourceName));
Comment on lines +33 to +35

builder.Configuration.AddUserSecrets(Assembly.GetExecutingAssembly());
builder.Services.AddControllers();
builder.Services.AddHttpClient("WebClient", client => client.Timeout = TimeSpan.FromSeconds(600));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,41 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Microsoft.Agents.A365.Observability.Runtime.Tracing.Contracts;
using Microsoft.Agents.A365.Observability.Runtime.Tracing.Scopes;
using System.ComponentModel;

namespace Agent365AgentFrameworkSampleAgent.Tools
{
public static class DateTimeFunctionTool
public class DateTimeFunctionTool(IConfiguration configuration)
{
[Description("Use this tool to get the current date and time")]
public static string getDate(string input)
public string GetCurrentDateTime()
{
var toolCallDetails = new ToolCallDetails(
toolName: nameof(GetCurrentDateTime),
arguments: "{}",
toolCallId: Guid.NewGuid().ToString(),
description: "Returns the current date and time",
toolType: "function",
endpoint: new Uri("local://datetime")
);
using var toolScope = ExecuteToolScope.Start(
request: new Request("Get current date and time"),
details: toolCallDetails,
agentDetails: BuildAgentDetails());

string date = DateTimeOffset.Now.ToString("D", null);
toolScope.RecordResponse(date);
return date;
}

private AgentDetails BuildAgentDetails() =>
new AgentDetails(
agentId: configuration["Agent365Observability:AgentId"] ?? "local-dev",
agentName: configuration["Agent365Observability:AgentName"] ?? "my-agent",
agentDescription: configuration["Agent365Observability:AgentDescription"] ?? "",
agentBlueprintId: configuration["Agent365Observability:AgentBlueprintId"] ?? "",
tenantId: configuration["Agent365Observability:TenantId"] ?? "local-dev");
Comment on lines +33 to +39
}
}
40 changes: 40 additions & 0 deletions dotnet/agent-framework/sample-agent/Tools/WeatherLookupTool.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Microsoft.Agents.A365.Observability.Runtime.Tracing.Contracts;
using Microsoft.Agents.A365.Observability.Runtime.Tracing.Scopes;
using Microsoft.Agents.Builder;
using Microsoft.Agents.Core;
using Microsoft.Agents.Core.Models;
Expand Down Expand Up @@ -39,6 +41,19 @@ public class WeatherLookupTool(ITurnContext turnContext, IConfiguration configur
{
AssertionHelpers.ThrowIfNull(turnContext, nameof(turnContext));

var toolCallDetails = new ToolCallDetails(
toolName: nameof(GetCurrentWeatherForLocation),
arguments: $"{{\"location\":\"{location}\",\"state\":\"{state}\"}}",
toolCallId: Guid.NewGuid().ToString(),
Comment on lines +44 to +47
description: "Retrieves current weather for a city/state",
toolType: "function",
endpoint: new Uri("https://api.openweathermap.org")
);
using var toolScope = ExecuteToolScope.Start(
request: new Request($"Get current weather for {location}, {state}"),
details: toolCallDetails,
agentDetails: BuildAgentDetails());

// Notify the user that we are looking up the weather
Console.WriteLine($"Looking up the Current Weather in {location}");

Expand Down Expand Up @@ -80,16 +95,26 @@ await turnContext.SendActivityAsync(
if (weather.IsSuccess)
{
WeatherRoot wInfo = weather.Response;
toolScope.RecordResponse(System.Text.Json.JsonSerializer.Serialize(wInfo));
return wInfo;
}
}
else
{
System.Diagnostics.Trace.WriteLine($"Failed to complete API Call to OpenWeather: {openWeatherLocation!.Error}");
}
toolScope.RecordResponse("null");
return null;
}

private AgentDetails BuildAgentDetails() =>
new AgentDetails(
agentId: configuration["Agent365Observability:AgentId"] ?? "local-dev",
agentName: configuration["Agent365Observability:AgentName"] ?? "my-agent",
agentDescription: configuration["Agent365Observability:AgentDescription"] ?? "",
agentBlueprintId: configuration["Agent365Observability:AgentBlueprintId"] ?? "",
tenantId: configuration["Agent365Observability:TenantId"] ?? "local-dev");

/// <summary>
/// Retrieves the weather forecast for a specified location.
/// This method uses the OpenWeatherMap API to fetch the weather forecast data for a given city and state.
Expand All @@ -115,6 +140,19 @@ await turnContext.SendActivityAsync(
[Description("Retrieves the Weather forecast for a location, location is a city name")]
public async Task<List<ForecastItem>?> GetWeatherForecastForLocation(string location, string state)
{
var toolCallDetails = new ToolCallDetails(
toolName: nameof(GetWeatherForecastForLocation),
arguments: $"{{\"location\":\"{location}\",\"state\":\"{state}\"}}",
toolCallId: Guid.NewGuid().ToString(),
description: "Retrieves weather forecast for a city/state",
toolType: "function",
endpoint: new Uri("https://api.openweathermap.org")
);
using var toolScope = ExecuteToolScope.Start(
request: new Request($"Get weather forecast for {location}, {state}"),
details: toolCallDetails,
agentDetails: BuildAgentDetails());

// Notify the user that we are looking up the weather
Console.WriteLine($"Looking up the Weather Forecast in {location}");

Expand Down Expand Up @@ -145,13 +183,15 @@ await turnContext.SendActivityAsync(
if (weather.IsSuccess)
{
var result = weather.Response.Items;
toolScope.RecordResponse(System.Text.Json.JsonSerializer.Serialize(result));
return result;
}
}
else
{
System.Diagnostics.Trace.WriteLine($"Failed to complete API Call to OpenWeather: {openWeatherLocation!.Error}");
}
toolScope.RecordResponse("null");
return null;
}
}
Expand Down
30 changes: 21 additions & 9 deletions dotnet/agent-framework/sample-agent/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"Settings": {
"Scopes": [
"https://graph.microsoft.com/.default"
]
],
"AlternateBlueprintConnectionName": "ServiceConnection"
}
}
// To use OBO auth instead, uncomment the following lines.
Expand All @@ -35,7 +36,7 @@

"TokenValidation": {
"Audiences": [
"{{ClientId}}" // this is the Client ID used for the Azure Bot
"{{BOT_ID}}" // Agent Identity App ID (Enterprise App in Entra, NOT the Blueprint)
]
},

Expand All @@ -54,10 +55,11 @@
"Settings": {
"AuthType": "UserManagedIdentity", // this is the AuthType for the connection, valid values can be found in Microsoft.Agents.Authentication.Msal.Model.AuthTypes.
"AuthorityEndpoint": "https://login.microsoftonline.com/{{BOT_TENANT_ID}}",
"ClientId": "{{BOT_ID}}", // this is the BluePrint Client ID used for the connection.
"ClientId": "{{BLUEPRINT_ID}}", // Blueprint App ID — from a365.generated.config.json: agentBlueprintId
"Scopes": [
"5a807f24-c9de-44ee-a3a7-329e88a00ffc/.default"
]
],
"AgentId": "{{BOT_ID}}" // Agent Identity App ID — Enterprise App in Entra (different from Blueprint above)
}
}
},
Expand All @@ -69,10 +71,20 @@
],
"AIServices": {
"AzureOpenAI": {
"DeploymentName": "----", // This is the Deployment (as opposed to model) Name of the Azure OpenAI model
"Endpoint": "----", // This is the Endpoint of the Azure OpenAI model deployment
"ApiKey": "----" // This is the API Key of the Azure OpenAI model deployment
"DeploymentName": "gpt-4o", // This is the Deployment (as opposed to model) Name of the Azure OpenAI model
"Endpoint": "<<YOUR_AZURE_OPENAI_ENDPOINT>>", // This is the Endpoint of the Azure OpenAI model deployment
"ApiKey": "<<YOUR_AZURE_OPENAI_API_KEY>>" // 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).
"EnableAgent365Exporter": true,
"Agent365Observability": {
"AgentId": "{{BOT_ID}}", // this is the Agent ID used for observability reporting
"AgentName": "My Agent",
"AgentDescription": "My agent description",
"TenantId": "{{BOT_TENANT_ID}}",
"AgentBlueprintId": "{{BLUEPRINT_ID}}", // this is the Blueprint ID for the agent
"ClientId": "{{BLUEPRINT_ID}}", // Blueprint App ID — used by ObservabilityTokenService to acquire tokens via the FMI chain
"ClientSecret": "<<PLACEHOLDER>>"
}
}
Loading