Skip to content

Commit

Permalink
.Net Agents - Refine client provider/factory (#10616)
Browse files Browse the repository at this point in the history
### Motivation and Context
<!-- Thank you for your contribution to the semantic-kernel repo!
Please help reviewers and future users, providing the following
information:
  1. Why is this change required?
  2. What problem does it solve?
  3. What scenario does it contribute to?
  4. If it fixes an open issue, please link to the issue here.
-->

Move away from the "client-provider" pattern and make client factory
more discoverable.

Fixes: #10582

### Description
<!-- Describe your changes, the overall approach, the underlying design.
These notes will help understanding how your code works. Thanks! -->

Expose ability to create an SDK client as a static factory methods on
the agent. This is more discoverable than poking around for the
client-provider and aligns with the Python approach.

- Organized factory code in a separate file from the core agent
abstractions.
- Updated samples

### Contribution Checklist
<!-- Before submitting this PR, please make sure: -->

- [X] The code builds clean without any errors or warnings
- [X] The PR follows the [SK Contribution
Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md)
and the [pre-submission formatting
script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts)
raises no violations
- [X] All unit tests pass, and I have added new tests where possible
- [X] I didn't break anyone 😄
  • Loading branch information
crickman authored Feb 20, 2025
1 parent 2984423 commit 82aafd3
Show file tree
Hide file tree
Showing 6 changed files with 196 additions and 12 deletions.
65 changes: 65 additions & 0 deletions dotnet/src/Agents/AzureAI/AzureAIAgent.ClientFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Net.Http;
using Azure.AI.Projects;
using Azure.Core;
using Azure.Core.Pipeline;
using Microsoft.SemanticKernel.Http;

namespace Microsoft.SemanticKernel.Agents.AzureAI;

/// <summary>
/// Provides an <see cref="AIProjectClient"/> for use by <see cref="AzureAIAgent"/>.
/// </summary>
public sealed partial class AzureAIAgent : KernelAgent
{
/// <summary>
/// Produces a <see cref="AIProjectClient"/>.
/// </summary>
/// <param name="connectionString">The Azure AI Foundry project connection string, in the form `endpoint;subscription_id;resource_group_name;project_name`.</param>
/// <param name="credential"> A credential used to authenticate to an Azure Service.</param>
/// <param name="httpClient">A custom <see cref="HttpClient"/> for HTTP requests.</param>
public static AIProjectClient CreateAzureAIClient(
string connectionString,
TokenCredential credential,
HttpClient? httpClient = null)
{
Verify.NotNullOrWhiteSpace(connectionString, nameof(connectionString));
Verify.NotNull(credential, nameof(credential));

AIProjectClientOptions clientOptions = CreateAzureClientOptions(httpClient);

return new AIProjectClient(connectionString, credential, clientOptions);
}

private static AIProjectClientOptions CreateAzureClientOptions(HttpClient? httpClient)
{
AIProjectClientOptions options =
new()
{
Diagnostics = {
ApplicationId = HttpHeaderConstant.Values.UserAgent,
}
};

options.AddPolicy(new SemanticKernelHeadersPolicy(), HttpPipelinePosition.PerCall);

if (httpClient is not null)
{
options.Transport = new HttpClientTransport(httpClient);
// Disable retry policy if and only if a custom HttpClient is provided.
options.RetryPolicy = new RetryPolicy(maxRetries: 0);
}

return options;
}

private class SemanticKernelHeadersPolicy : HttpPipelineSynchronousPolicy
{
public override void OnSendingRequest(HttpMessage message)
{
message.Request.Headers.Add(
HttpHeaderConstant.Names.SemanticKernelVersion,
HttpHeaderConstant.Values.GetAssemblyVersion(typeof(AzureAIAgent)));
}
}
}
2 changes: 1 addition & 1 deletion dotnet/src/Agents/AzureAI/AzureAIAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ namespace Microsoft.SemanticKernel.Agents.AzureAI;
/// <summary>
/// Provides a specialized <see cref="KernelAgent"/> based on an Azure AI agent.
/// </summary>
public sealed class AzureAIAgent : KernelAgent
public sealed partial class AzureAIAgent : KernelAgent
{
/// <summary>
/// Provides tool definitions used when associating a file attachment to an input message:
Expand Down
122 changes: 122 additions & 0 deletions dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.ClientFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.ClientModel;
using System.ClientModel.Primitives;
using System.Net.Http;
using System.Threading;
using Azure.AI.OpenAI;
using Azure.Core;
using Microsoft.SemanticKernel.Http;
using OpenAI;

namespace Microsoft.SemanticKernel.Agents.OpenAI;

public sealed partial class OpenAIAssistantAgent : KernelAgent
{
/// <summary>
/// Specifies a key that avoids an exception from OpenAI Client when a custom endpoint is provided without an API key.
/// </summary>
private const string SingleSpaceKey = " ";

/// <summary>
/// Produces an <see cref="AzureOpenAIClient"/>.
/// </summary>
/// <param name="apiKey">The API key.</param>
/// <param name="endpoint">The service endpoint.</param>
/// <param name="httpClient">A custom <see cref="HttpClient"/> for HTTP requests.</param>
public static AzureOpenAIClient CreateAzureOpenAIClient(ApiKeyCredential apiKey, Uri endpoint, HttpClient? httpClient = null)
{
Verify.NotNull(apiKey, nameof(apiKey));
Verify.NotNull(endpoint, nameof(endpoint));

AzureOpenAIClientOptions clientOptions = CreateAzureClientOptions(httpClient);

return new AzureOpenAIClient(endpoint, apiKey!, clientOptions);
}

/// <summary>
/// Produces an <see cref="AzureOpenAIClient"/>.
/// </summary>
/// <param name="credential">The credentials.</param>
/// <param name="endpoint">The service endpoint.</param>
/// <param name="httpClient">A custom <see cref="HttpClient"/> for HTTP requests.</param>
public static AzureOpenAIClient CreateAzureOpenAIClient(TokenCredential credential, Uri endpoint, HttpClient? httpClient = null)
{
Verify.NotNull(credential, nameof(credential));
Verify.NotNull(endpoint, nameof(endpoint));

AzureOpenAIClientOptions clientOptions = CreateAzureClientOptions(httpClient);

return new AzureOpenAIClient(endpoint, credential, clientOptions);
}

/// <summary>
/// Produces an <see cref="OpenAIClient"/>.
/// </summary>
/// <param name="endpoint">An optional endpoint.</param>
/// <param name="httpClient">A custom <see cref="HttpClient"/> for HTTP requests.</param>
public static OpenAIClient CreateOpenAIClient(Uri? endpoint = null, HttpClient? httpClient = null)
{
OpenAIClientOptions clientOptions = CreateOpenAIClientOptions(endpoint, httpClient);
return new OpenAIClient(new ApiKeyCredential(SingleSpaceKey), clientOptions);
}

/// <summary>
/// Produces an <see cref="OpenAIClient"/>.
/// </summary>
/// <param name="apiKey">The API key.</param>
/// <param name="endpoint">An optional endpoint.</param>
/// <param name="httpClient">A custom <see cref="HttpClient"/> for HTTP requests.</param>
public static OpenAIClient CreateOpenAIClient(ApiKeyCredential apiKey, Uri? endpoint = null, HttpClient? httpClient = null)
{
OpenAIClientOptions clientOptions = CreateOpenAIClientOptions(endpoint, httpClient);
return new OpenAIClient(apiKey, clientOptions);
}

private static AzureOpenAIClientOptions CreateAzureClientOptions(HttpClient? httpClient)
{
AzureOpenAIClientOptions options = new()
{
UserAgentApplicationId = HttpHeaderConstant.Values.UserAgent
};

ConfigureClientOptions(httpClient, options);

return options;
}

private static OpenAIClientOptions CreateOpenAIClientOptions(Uri? endpoint, HttpClient? httpClient)
{
OpenAIClientOptions options = new()
{
UserAgentApplicationId = HttpHeaderConstant.Values.UserAgent,
Endpoint = endpoint ?? httpClient?.BaseAddress,
};

ConfigureClientOptions(httpClient, options);

return options;
}

private static void ConfigureClientOptions(HttpClient? httpClient, ClientPipelineOptions options)
{
options.AddPolicy(CreateRequestHeaderPolicy(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(OpenAIAssistantAgent))), PipelinePosition.PerCall);

if (httpClient is not null)
{
options.Transport = new HttpClientPipelineTransport(httpClient);
options.RetryPolicy = new ClientRetryPolicy(maxRetries: 0); // Disable retry policy if and only if a custom HttpClient is provided.
options.NetworkTimeout = Timeout.InfiniteTimeSpan; // Disable default timeout
}
}

private static GenericActionPipelinePolicy CreateRequestHeaderPolicy(string headerName, string headerValue)
=>
new((message) =>
{
if (message?.Request?.Headers?.TryGetValue(headerName, out string? _) == false)
{
message.Request.Headers.Set(headerName, headerValue);
}
});
}
2 changes: 1 addition & 1 deletion dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ namespace Microsoft.SemanticKernel.Agents.OpenAI;
/// <summary>
/// Represents a <see cref="KernelAgent"/> specialization based on Open AI Assistant / GPT.
/// </summary>
public sealed class OpenAIAssistantAgent : KernelAgent
public sealed partial class OpenAIAssistantAgent : KernelAgent
{
/// <summary>
/// The metadata key that identifies code-interpreter content.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,14 @@ public abstract class BaseAssistantTest : BaseAgentsTest<OpenAIClient>
{
protected BaseAssistantTest(ITestOutputHelper output) : base(output)
{
var clientProvider =
this.Client =
this.UseOpenAIConfig ?
OpenAIClientProvider.ForOpenAI(new ApiKeyCredential(this.ApiKey ?? throw new ConfigurationNotFoundException("OpenAI:ApiKey"))) :
OpenAIAssistantAgent.CreateOpenAIClient(new ApiKeyCredential(this.ApiKey ?? throw new ConfigurationNotFoundException("OpenAI:ApiKey"))) :
!string.IsNullOrWhiteSpace(this.ApiKey) ?
OpenAIClientProvider.ForAzureOpenAI(new ApiKeyCredential(this.ApiKey), new Uri(this.Endpoint!)) :
OpenAIClientProvider.ForAzureOpenAI(new AzureCliCredential(), new Uri(this.Endpoint!));
OpenAIAssistantAgent.CreateAzureOpenAIClient(new ApiKeyCredential(this.ApiKey), new Uri(this.Endpoint!)) :
OpenAIAssistantAgent.CreateAzureOpenAIClient(new AzureCliCredential(), new Uri(this.Endpoint!));

this.Client = clientProvider.Client;
this.AssistantClient = clientProvider.AssistantClient;
this.AssistantClient = this.Client.GetAssistantClient();
}

/// <inheritdoc/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,8 @@ public abstract class BaseAzureAgentTest : BaseAgentsTest<AIProjectClient>
{
protected BaseAzureAgentTest(ITestOutputHelper output) : base(output)
{
var clientProvider = AzureAIClientProvider.FromConnectionString(TestConfiguration.AzureAI.ConnectionString, new AzureCliCredential());

this.Client = clientProvider.Client;
this.AgentsClient = clientProvider.AgentsClient;
this.Client = AzureAIAgent.CreateAzureAIClient(TestConfiguration.AzureAI.ConnectionString, new AzureCliCredential());
this.AgentsClient = this.Client.GetAgentsClient();
}

/// <inheritdoc/>
Expand Down

0 comments on commit 82aafd3

Please sign in to comment.