Skip to content
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

[C#] feat: AssistantsPlanner support for authenticating Azure OpenAI using managed identity #1930

Merged
merged 19 commits into from
Aug 14, 2024
Merged
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
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Azure.AI.OpenAI;
using Azure.Core;
using Microsoft.Bot.Builder;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
Expand Down Expand Up @@ -43,20 +44,30 @@ public class AssistantsPlanner<TState> : IPlanner<TState>
public AssistantsPlanner(AssistantsPlannerOptions options, ILoggerFactory? loggerFactory = null)
{
Verify.ParamNotNull(options);
Verify.ParamNotNull(options.ApiKey, "AssistantsPlannerOptions.ApiKey");
Verify.ParamNotNull(options.AssistantId, "AssistantsPlannerOptions.AssistantId");

_options = new AssistantsPlannerOptions(options.ApiKey, options.AssistantId)
{
Organization = options.Organization,
PollingInterval = options.PollingInterval ?? DEFAULT_POLLING_INTERVAL
};
options.PollingInterval = options.PollingInterval ?? DEFAULT_POLLING_INTERVAL;

_options = options;
_logger = loggerFactory == null ? NullLogger.Instance : loggerFactory.CreateLogger<AssistantsPlanner<TState>>();
_client = _CreateClient(options.ApiKey, options.Endpoint);

if (options.TokenCredential != null)
{
Verify.ParamNotNull(options.Endpoint, "AssistantsPlannerOptions.Endpoint");
_client = _CreateClient(options.TokenCredential, options.Endpoint!);
}
else if (options.ApiKey != null)
{
_client = _CreateClient(options.ApiKey, options.Endpoint);
}
else
{
throw new ArgumentException("Either `AssistantsPlannerOptions.ApiKey` or `AssistantsPlannerOptions.TokenCredential` should be set.");
}
}

/// <summary>
/// Static helper method for programmatically creating an assistant.
/// Static helper method for programatically creating an assistant.
/// </summary>
/// <param name="apiKey">OpenAI or Azure OpenAI API key.</param>
/// <param name="request">Definition of the assistant to create.</param>
Expand All @@ -76,6 +87,28 @@ public static async Task<Assistant> CreateAssistantAsync(string apiKey, Assistan
return await client.CreateAssistantAsync(model, request, cancellationToken);
}

/// <summary>
/// Static helper method for programatically creating an assistant.
/// </summary>
/// <param name="tokenCredential">Azure token credential to authenticate requests</param>
/// <param name="request">Definition of the assistant to create.</param>
/// <param name="model">The underlying LLM model.</param>
/// <param name="endpoint">Azure OpenAI API Endpoint.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>The created assistant.</returns>
public static async Task<Assistant> CreateAssistantAsync(TokenCredential tokenCredential, AssistantCreationOptions request, string model, string endpoint, CancellationToken cancellationToken = default)
{
Verify.ParamNotNull(tokenCredential);
Verify.ParamNotNull(request);
Verify.ParamNotNull(model);
Verify.ParamNotNull(endpoint);

AssistantClient client = _CreateClient(tokenCredential, endpoint);

return await client.CreateAssistantAsync(model, request, cancellationToken);
}

/// <inheritdoc/>
public async Task<Plan> BeginTaskAsync(ITurnContext turnContext, TState turnState, AI<TState> ai, CancellationToken cancellationToken)
{
Expand Down Expand Up @@ -334,6 +367,15 @@ internal static AssistantClient _CreateClient(string apiKey, string? endpoint =
return new AssistantClient(apiKey);
}
}

internal static AssistantClient _CreateClient(TokenCredential tokenCredential, string endpoint)
{
Verify.ParamNotNull(tokenCredential);
Verify.ParamNotNull(endpoint);

AzureOpenAIClient azureOpenAI = new(new Uri(endpoint), tokenCredential);
return azureOpenAI.GetAssistantClient();
}
}
}
#pragma warning restore OPENAI001
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.Teams.AI.Utilities;
using Azure.Core;
using Microsoft.Teams.AI.Utilities;

// Assistants API is currently in beta and is subject to change.
#pragma warning disable IDE0130 // Namespace does not match folder structure
Expand All @@ -13,13 +14,18 @@ public class AssistantsPlannerOptions
/// <summary>
/// OpenAI API key or Azure OpenAI API key.
/// </summary>
public string ApiKey { get; set; }
public string? ApiKey { get; set; }

/// <summary>
/// Optional. Azure OpenAI Endpoint.
/// </summary>
public string? Endpoint { get; set; }

/// <summary>
/// Optional. The token credential to use when making requests to Azure OpenAI.
/// </summary>
public TokenCredential? TokenCredential { get; set; }

/// <summary>
/// The Assistant ID.
/// </summary>
Expand Down Expand Up @@ -51,5 +57,21 @@ public AssistantsPlannerOptions(string apiKey, string assistantId, string? endpo
AssistantId = assistantId;
Endpoint = endpoint;
}

/// <summary>
/// Create an instance of the AsssistantsPlannerOptions class.
/// </summary>
/// <param name="tokenCredential">The token credential object. This can be set to DefaultAzureCredential to use managed identity auth.</param>
/// <param name="assistantId">The Assistant ID.</param>
/// <param name="endpoint">Optional. The Azure OpenAI Endpoint</param>
public AssistantsPlannerOptions(TokenCredential tokenCredential, string assistantId, string? endpoint = null)
{
Verify.ParamNotNull(tokenCredential);
Verify.ParamNotNull(assistantId);

TokenCredential = tokenCredential;
AssistantId = assistantId;
Endpoint = endpoint;
}
}
}
1 change: 1 addition & 0 deletions dotnet/samples/06.assistants.a.mathBot/MathBot.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Azure.Identity" Version="1.12.0" />
<PackageReference Include="Microsoft.Bot.Builder.Integration.AspNet.Core" Version="4.22.7" />
<PackageReference Include="Microsoft.Teams.AI" Version="1.5.*" />
<PackageReference Include="Azure.AI.OpenAI.Assistants" Version="1.0.0-beta.3" />
Expand Down
54 changes: 45 additions & 9 deletions dotnet/samples/06.assistants.a.mathBot/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
using Microsoft.Teams.AI.AI;
using Microsoft.Teams.AI.AI.Planners.Experimental;
using Microsoft.Teams.AI.AI.Planners;

using MathBot;
using OpenAI.Assistants;
using Azure.Core;
using Azure.Identity;
using System.Runtime.CompilerServices;

var builder = WebApplication.CreateBuilder(args);

Expand All @@ -17,19 +19,29 @@

// Load configuration
var config = builder.Configuration.Get<ConfigOptions>()!;
var isAzureCredentialsSet = config.Azure != null && !string.IsNullOrEmpty(config.Azure.OpenAIApiKey) && !string.IsNullOrEmpty(config.Azure.OpenAIEndpoint);
var isAzureCredentialsSet = config.Azure != null && !string.IsNullOrEmpty(config.Azure.OpenAIEndpoint);
var isOpenAICredentialsSet = config.OpenAI != null && !string.IsNullOrEmpty(config.OpenAI.ApiKey);

string apiKey = "";
string? apiKey = null;
TokenCredential? tokenCredential = null;
string? endpoint = null;
string? assistantId = "";

// If both credentials are set then the Azure credentials will be used.
if (isAzureCredentialsSet)
{
apiKey = config.Azure!.OpenAIApiKey!;
endpoint = config.Azure.OpenAIEndpoint;
endpoint = config.Azure!.OpenAIEndpoint;
assistantId = config.Azure.OpenAIAssistantId;

if (config.Azure!.OpenAIApiKey != string.Empty)
{
apiKey = config.Azure!.OpenAIApiKey!;
}
else
{
// Using managed identity authentication
tokenCredential = new DefaultAzureCredential();
}
}
else if (isOpenAICredentialsSet)
{
Expand All @@ -45,13 +57,24 @@
// Missing Assistant ID, create new Assistant
if (string.IsNullOrEmpty(assistantId))
{
Console.WriteLine("No Assistant ID configured, creating new Assistant...");
AssistantCreationOptions assistantCreateParams = new()
AssistantCreationOptions assistantCreationOptions = new()
{
Name = "Math Tutor",
Instructions = "You are a personal math tutor. Write and run code to answer math questions."
};
assistantCreateParams.Tools.Add(new CodeInterpreterToolDefinition());

assistantCreationOptions.Tools.Add(new CodeInterpreterToolDefinition());

string newAssistantId = "";
if (apiKey != null)
{
newAssistantId = AssistantsPlanner<AssistantsState>.CreateAssistantAsync(apiKey, assistantCreationOptions, "gpt-4o-mini", endpoint).Result.Id;
}
else
{
// use token credential for authentication
newAssistantId = AssistantsPlanner<AssistantsState>.CreateAssistantAsync(tokenCredential!, assistantCreationOptions, "gpt-4o-mini", endpoint!).Result.Id;
}

string newAssistantId = AssistantsPlanner<AssistantsState>.CreateAssistantAsync(apiKey, assistantCreateParams, "gpt-4", endpoint).Result.Id;
Console.WriteLine($"Created a new assistant with an ID of: {newAssistantId}");
Expand All @@ -77,7 +100,20 @@
builder.Services.AddSingleton<BotAdapter>(sp => sp.GetService<TeamsAdapter>()!);

builder.Services.AddSingleton<IStorage, MemoryStorage>();
builder.Services.AddSingleton(_ => new AssistantsPlannerOptions(apiKey, assistantId) { Endpoint = endpoint });
builder.Services.AddSingleton(_ => {
if (apiKey != null)
{
return new AssistantsPlannerOptions(apiKey, assistantId, endpoint);
}
else if (tokenCredential != null)
{
return new AssistantsPlannerOptions(tokenCredential, assistantId, endpoint);
}
else
{
throw new ArgumentException("The `apiKey` or `tokenCredential` needs to be set");
}
});

// Create the Application.
builder.Services.AddTransient<IBot>(sp =>
Expand Down
1 change: 1 addition & 0 deletions dotnet/samples/06.assistants.a.mathBot/teamsapp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,4 @@ deploy:
# You can replace it with an existing Azure Resource ID or other
# custom environment variable.
resourceId: ${{BOT_AZURE_APP_SERVICE_RESOURCE_ID}}
projectId: 562123aa-6256-4018-bd3f-ca91d1cbd4d9
1 change: 1 addition & 0 deletions dotnet/samples/06.assistants.b.orderBot/OrderBot.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI.Assistants" Version="1.0.0-beta.3" />
<PackageReference Include="Azure.Identity" Version="1.12.0" />
<PackageReference Include="Microsoft.Bot.Builder.Integration.AspNet.Core" Version="4.22.7" />
<PackageReference Include="OpenAI" Version="2.0.0-beta.7" />
<PackageReference Include="Azure.AI.OpenAI" Version="2.0.0-beta.2" />
Expand Down
48 changes: 40 additions & 8 deletions dotnet/samples/06.assistants.b.orderBot/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
using OrderBot;
using OrderBot.Models;
using OpenAI.Assistants;
using Azure.Core;
using Azure.Identity;
using System.Runtime.CompilerServices;

var builder = WebApplication.CreateBuilder(args);

Expand All @@ -17,19 +20,28 @@

// Load configuration
var config = builder.Configuration.Get<ConfigOptions>()!;
var isAzureCredentialsSet = config.Azure != null && !string.IsNullOrEmpty(config.Azure.OpenAIApiKey) && !string.IsNullOrEmpty(config.Azure.OpenAIEndpoint);
var isAzureCredentialsSet = config.Azure != null && !string.IsNullOrEmpty(config.Azure.OpenAIEndpoint);
var isOpenAICredentialsSet = config.OpenAI != null && !string.IsNullOrEmpty(config.OpenAI.ApiKey);

string apiKey = "";
string? apiKey = null;
TokenCredential? tokenCredential = null;
string? endpoint = null;
string? assistantId = "";

// If both credentials are set then the Azure credentials will be used.
if (isAzureCredentialsSet)
{
apiKey = config.Azure!.OpenAIApiKey!;
endpoint = config.Azure.OpenAIEndpoint;
endpoint = config.Azure!.OpenAIEndpoint;
assistantId = config.Azure.OpenAIAssistantId;

if (config.Azure!.OpenAIApiKey != string.Empty)
{
apiKey = config.Azure!.OpenAIApiKey!;
} else
{
// Using managed identity authentication
tokenCredential = new DefaultAzureCredential();
}
}
else if (isOpenAICredentialsSet)
{
Expand Down Expand Up @@ -59,7 +71,16 @@

assistantCreationOptions.Tools.Add(new FunctionToolDefinition("place_order", "Creates or updates a food order.", new BinaryData(OrderParameters.GetSchema())));

string newAssistantId = AssistantsPlanner<AssistantsState>.CreateAssistantAsync(apiKey, assistantCreationOptions, "gpt-4", endpoint).Result.Id;
string newAssistantId = "";
if (apiKey != null)
{
newAssistantId = AssistantsPlanner<AssistantsState>.CreateAssistantAsync(apiKey, assistantCreationOptions, "gpt-4o-mini", endpoint).Result.Id;
}
else
{
// use token credential for authentication
newAssistantId = AssistantsPlanner<AssistantsState>.CreateAssistantAsync(tokenCredential!, assistantCreationOptions, "gpt-4o-mini", endpoint!).Result.Id;
}

Console.WriteLine($"Created a new assistant with an ID of: {newAssistantId}");
Console.WriteLine("Copy and save above ID, and set `OpenAI:AssistantId` in appsettings.Development.json.");
Expand All @@ -70,8 +91,8 @@

// Prepare Configuration for ConfigurationBotFrameworkAuthentication
builder.Configuration["MicrosoftAppType"] = "MultiTenant";
builder.Configuration["MicrosoftAppId"] = config.BOT_ID;
builder.Configuration["MicrosoftAppPassword"] = config.BOT_PASSWORD;
builder.Configuration["MicrosoftAppId"] = ""; // config.BOT_ID;
builder.Configuration["MicrosoftAppPassword"] = ""; // config.BOT_PASSWORD;

// Create the Bot Framework Authentication to be used with the Bot Adapter.
builder.Services.AddSingleton<BotFrameworkAuthentication, ConfigurationBotFrameworkAuthentication>();
Expand All @@ -84,7 +105,18 @@
builder.Services.AddSingleton<BotAdapter>(sp => sp.GetService<TeamsAdapter>()!);

builder.Services.AddSingleton<IStorage, MemoryStorage>();
builder.Services.AddSingleton(_ => new AssistantsPlannerOptions(apiKey, assistantId, endpoint));
builder.Services.AddSingleton(_ => {
if (apiKey != null)
{
return new AssistantsPlannerOptions(apiKey, assistantId, endpoint);
} else if (tokenCredential != null)
{
return new AssistantsPlannerOptions(tokenCredential, assistantId, endpoint);
} else
{
throw new ArgumentException("The `apiKey` or `tokenCredential` needs to be set");
}
});

// Create the Application.
builder.Services.AddTransient<IBot>(sp =>
Expand Down
1 change: 1 addition & 0 deletions dotnet/samples/06.assistants.b.orderBot/teamsapp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,4 @@ deploy:
# You can replace it with an existing Azure Resource ID or other
# custom environment variable.
resourceId: ${{BOT_AZURE_APP_SERVICE_RESOURCE_ID}}
projectId: e9c61418-40b8-4559-914f-6267cf343d93
Loading