Skip to content

Commit

Permalink
[C#] feat: AssistantsPlanner support for authenticating Azure OpenA…
Browse files Browse the repository at this point in the history
…I using managed identity (#1930)

## Linked issues

closes: #minor (issue number)

#1918 C# implementaiton

## Details
* Authentication Azure OpenAI endpoint requests using managed identity
auth.

#### Change details
* Updated `MathBot` and `OrderBot` to authenticate using MI if api keys
are not set.
* Added `projectId` in the `teamsapp.yml` file.

**code snippets**:

**screenshots**:

## Attestation Checklist

- [x] My code follows the style guidelines of this project

- I have checked for/fixed spelling, linting, and other errors
- I have commented my code for clarity
- I have made corresponding changes to the documentation (updating the
doc strings in the code is sufficient)
- My changes generate no new warnings
- I have added tests that validates my changes, and provides sufficient
test coverage. I have tested with:
  - Local testing
  - E2E testing in Teams
- New and existing unit tests pass locally with my changes

### Additional information

> Feel free to add other relevant information below
  • Loading branch information
singhk97 authored Aug 14, 2024
1 parent 299fcd8 commit ba6e806
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 27 deletions.
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

1 comment on commit ba6e806

@htertery
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@singhk97 when can we expect this change to release in next nuget package version ?

Please sign in to comment.