Skip to content

Commit 82aafd3

Browse files
authored
.Net Agents - Refine client provider/factory (#10616)
### 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 😄
1 parent 2984423 commit 82aafd3

File tree

6 files changed

+196
-12
lines changed

6 files changed

+196
-12
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
using System.Net.Http;
3+
using Azure.AI.Projects;
4+
using Azure.Core;
5+
using Azure.Core.Pipeline;
6+
using Microsoft.SemanticKernel.Http;
7+
8+
namespace Microsoft.SemanticKernel.Agents.AzureAI;
9+
10+
/// <summary>
11+
/// Provides an <see cref="AIProjectClient"/> for use by <see cref="AzureAIAgent"/>.
12+
/// </summary>
13+
public sealed partial class AzureAIAgent : KernelAgent
14+
{
15+
/// <summary>
16+
/// Produces a <see cref="AIProjectClient"/>.
17+
/// </summary>
18+
/// <param name="connectionString">The Azure AI Foundry project connection string, in the form `endpoint;subscription_id;resource_group_name;project_name`.</param>
19+
/// <param name="credential"> A credential used to authenticate to an Azure Service.</param>
20+
/// <param name="httpClient">A custom <see cref="HttpClient"/> for HTTP requests.</param>
21+
public static AIProjectClient CreateAzureAIClient(
22+
string connectionString,
23+
TokenCredential credential,
24+
HttpClient? httpClient = null)
25+
{
26+
Verify.NotNullOrWhiteSpace(connectionString, nameof(connectionString));
27+
Verify.NotNull(credential, nameof(credential));
28+
29+
AIProjectClientOptions clientOptions = CreateAzureClientOptions(httpClient);
30+
31+
return new AIProjectClient(connectionString, credential, clientOptions);
32+
}
33+
34+
private static AIProjectClientOptions CreateAzureClientOptions(HttpClient? httpClient)
35+
{
36+
AIProjectClientOptions options =
37+
new()
38+
{
39+
Diagnostics = {
40+
ApplicationId = HttpHeaderConstant.Values.UserAgent,
41+
}
42+
};
43+
44+
options.AddPolicy(new SemanticKernelHeadersPolicy(), HttpPipelinePosition.PerCall);
45+
46+
if (httpClient is not null)
47+
{
48+
options.Transport = new HttpClientTransport(httpClient);
49+
// Disable retry policy if and only if a custom HttpClient is provided.
50+
options.RetryPolicy = new RetryPolicy(maxRetries: 0);
51+
}
52+
53+
return options;
54+
}
55+
56+
private class SemanticKernelHeadersPolicy : HttpPipelineSynchronousPolicy
57+
{
58+
public override void OnSendingRequest(HttpMessage message)
59+
{
60+
message.Request.Headers.Add(
61+
HttpHeaderConstant.Names.SemanticKernelVersion,
62+
HttpHeaderConstant.Values.GetAssemblyVersion(typeof(AzureAIAgent)));
63+
}
64+
}
65+
}

dotnet/src/Agents/AzureAI/AzureAIAgent.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ namespace Microsoft.SemanticKernel.Agents.AzureAI;
1515
/// <summary>
1616
/// Provides a specialized <see cref="KernelAgent"/> based on an Azure AI agent.
1717
/// </summary>
18-
public sealed class AzureAIAgent : KernelAgent
18+
public sealed partial class AzureAIAgent : KernelAgent
1919
{
2020
/// <summary>
2121
/// Provides tool definitions used when associating a file attachment to an input message:
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
using System;
3+
using System.ClientModel;
4+
using System.ClientModel.Primitives;
5+
using System.Net.Http;
6+
using System.Threading;
7+
using Azure.AI.OpenAI;
8+
using Azure.Core;
9+
using Microsoft.SemanticKernel.Http;
10+
using OpenAI;
11+
12+
namespace Microsoft.SemanticKernel.Agents.OpenAI;
13+
14+
public sealed partial class OpenAIAssistantAgent : KernelAgent
15+
{
16+
/// <summary>
17+
/// Specifies a key that avoids an exception from OpenAI Client when a custom endpoint is provided without an API key.
18+
/// </summary>
19+
private const string SingleSpaceKey = " ";
20+
21+
/// <summary>
22+
/// Produces an <see cref="AzureOpenAIClient"/>.
23+
/// </summary>
24+
/// <param name="apiKey">The API key.</param>
25+
/// <param name="endpoint">The service endpoint.</param>
26+
/// <param name="httpClient">A custom <see cref="HttpClient"/> for HTTP requests.</param>
27+
public static AzureOpenAIClient CreateAzureOpenAIClient(ApiKeyCredential apiKey, Uri endpoint, HttpClient? httpClient = null)
28+
{
29+
Verify.NotNull(apiKey, nameof(apiKey));
30+
Verify.NotNull(endpoint, nameof(endpoint));
31+
32+
AzureOpenAIClientOptions clientOptions = CreateAzureClientOptions(httpClient);
33+
34+
return new AzureOpenAIClient(endpoint, apiKey!, clientOptions);
35+
}
36+
37+
/// <summary>
38+
/// Produces an <see cref="AzureOpenAIClient"/>.
39+
/// </summary>
40+
/// <param name="credential">The credentials.</param>
41+
/// <param name="endpoint">The service endpoint.</param>
42+
/// <param name="httpClient">A custom <see cref="HttpClient"/> for HTTP requests.</param>
43+
public static AzureOpenAIClient CreateAzureOpenAIClient(TokenCredential credential, Uri endpoint, HttpClient? httpClient = null)
44+
{
45+
Verify.NotNull(credential, nameof(credential));
46+
Verify.NotNull(endpoint, nameof(endpoint));
47+
48+
AzureOpenAIClientOptions clientOptions = CreateAzureClientOptions(httpClient);
49+
50+
return new AzureOpenAIClient(endpoint, credential, clientOptions);
51+
}
52+
53+
/// <summary>
54+
/// Produces an <see cref="OpenAIClient"/>.
55+
/// </summary>
56+
/// <param name="endpoint">An optional endpoint.</param>
57+
/// <param name="httpClient">A custom <see cref="HttpClient"/> for HTTP requests.</param>
58+
public static OpenAIClient CreateOpenAIClient(Uri? endpoint = null, HttpClient? httpClient = null)
59+
{
60+
OpenAIClientOptions clientOptions = CreateOpenAIClientOptions(endpoint, httpClient);
61+
return new OpenAIClient(new ApiKeyCredential(SingleSpaceKey), clientOptions);
62+
}
63+
64+
/// <summary>
65+
/// Produces an <see cref="OpenAIClient"/>.
66+
/// </summary>
67+
/// <param name="apiKey">The API key.</param>
68+
/// <param name="endpoint">An optional endpoint.</param>
69+
/// <param name="httpClient">A custom <see cref="HttpClient"/> for HTTP requests.</param>
70+
public static OpenAIClient CreateOpenAIClient(ApiKeyCredential apiKey, Uri? endpoint = null, HttpClient? httpClient = null)
71+
{
72+
OpenAIClientOptions clientOptions = CreateOpenAIClientOptions(endpoint, httpClient);
73+
return new OpenAIClient(apiKey, clientOptions);
74+
}
75+
76+
private static AzureOpenAIClientOptions CreateAzureClientOptions(HttpClient? httpClient)
77+
{
78+
AzureOpenAIClientOptions options = new()
79+
{
80+
UserAgentApplicationId = HttpHeaderConstant.Values.UserAgent
81+
};
82+
83+
ConfigureClientOptions(httpClient, options);
84+
85+
return options;
86+
}
87+
88+
private static OpenAIClientOptions CreateOpenAIClientOptions(Uri? endpoint, HttpClient? httpClient)
89+
{
90+
OpenAIClientOptions options = new()
91+
{
92+
UserAgentApplicationId = HttpHeaderConstant.Values.UserAgent,
93+
Endpoint = endpoint ?? httpClient?.BaseAddress,
94+
};
95+
96+
ConfigureClientOptions(httpClient, options);
97+
98+
return options;
99+
}
100+
101+
private static void ConfigureClientOptions(HttpClient? httpClient, ClientPipelineOptions options)
102+
{
103+
options.AddPolicy(CreateRequestHeaderPolicy(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(OpenAIAssistantAgent))), PipelinePosition.PerCall);
104+
105+
if (httpClient is not null)
106+
{
107+
options.Transport = new HttpClientPipelineTransport(httpClient);
108+
options.RetryPolicy = new ClientRetryPolicy(maxRetries: 0); // Disable retry policy if and only if a custom HttpClient is provided.
109+
options.NetworkTimeout = Timeout.InfiniteTimeSpan; // Disable default timeout
110+
}
111+
}
112+
113+
private static GenericActionPipelinePolicy CreateRequestHeaderPolicy(string headerName, string headerValue)
114+
=>
115+
new((message) =>
116+
{
117+
if (message?.Request?.Headers?.TryGetValue(headerName, out string? _) == false)
118+
{
119+
message.Request.Headers.Set(headerName, headerValue);
120+
}
121+
});
122+
}

dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ namespace Microsoft.SemanticKernel.Agents.OpenAI;
1919
/// <summary>
2020
/// Represents a <see cref="KernelAgent"/> specialization based on Open AI Assistant / GPT.
2121
/// </summary>
22-
public sealed class OpenAIAssistantAgent : KernelAgent
22+
public sealed partial class OpenAIAssistantAgent : KernelAgent
2323
{
2424
/// <summary>
2525
/// The metadata key that identifies code-interpreter content.

dotnet/src/InternalUtilities/samples/AgentUtilities/BaseAssistantTest.cs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,14 @@ public abstract class BaseAssistantTest : BaseAgentsTest<OpenAIClient>
1717
{
1818
protected BaseAssistantTest(ITestOutputHelper output) : base(output)
1919
{
20-
var clientProvider =
20+
this.Client =
2121
this.UseOpenAIConfig ?
22-
OpenAIClientProvider.ForOpenAI(new ApiKeyCredential(this.ApiKey ?? throw new ConfigurationNotFoundException("OpenAI:ApiKey"))) :
22+
OpenAIAssistantAgent.CreateOpenAIClient(new ApiKeyCredential(this.ApiKey ?? throw new ConfigurationNotFoundException("OpenAI:ApiKey"))) :
2323
!string.IsNullOrWhiteSpace(this.ApiKey) ?
24-
OpenAIClientProvider.ForAzureOpenAI(new ApiKeyCredential(this.ApiKey), new Uri(this.Endpoint!)) :
25-
OpenAIClientProvider.ForAzureOpenAI(new AzureCliCredential(), new Uri(this.Endpoint!));
24+
OpenAIAssistantAgent.CreateAzureOpenAIClient(new ApiKeyCredential(this.ApiKey), new Uri(this.Endpoint!)) :
25+
OpenAIAssistantAgent.CreateAzureOpenAIClient(new AzureCliCredential(), new Uri(this.Endpoint!));
2626

27-
this.Client = clientProvider.Client;
28-
this.AssistantClient = clientProvider.AssistantClient;
27+
this.AssistantClient = this.Client.GetAssistantClient();
2928
}
3029

3130
/// <inheritdoc/>

dotnet/src/InternalUtilities/samples/AgentUtilities/BaseAzureTest.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,8 @@ public abstract class BaseAzureAgentTest : BaseAgentsTest<AIProjectClient>
1414
{
1515
protected BaseAzureAgentTest(ITestOutputHelper output) : base(output)
1616
{
17-
var clientProvider = AzureAIClientProvider.FromConnectionString(TestConfiguration.AzureAI.ConnectionString, new AzureCliCredential());
18-
19-
this.Client = clientProvider.Client;
20-
this.AgentsClient = clientProvider.AgentsClient;
17+
this.Client = AzureAIAgent.CreateAzureAIClient(TestConfiguration.AzureAI.ConnectionString, new AzureCliCredential());
18+
this.AgentsClient = this.Client.GetAgentsClient();
2119
}
2220

2321
/// <inheritdoc/>

0 commit comments

Comments
 (0)