Skip to content

Commit 5d439f5

Browse files
authored
Merge branch 'main' into memory-record-loop
2 parents a20e5d3 + 42fae73 commit 5d439f5

File tree

43 files changed

+2043
-575
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+2043
-575
lines changed

.github/workflows/node-pr.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ on:
88
pull_request:
99
branches: ["main"]
1010
paths:
11-
- 'samples/**'
11+
- 'samples/apps/**'
1212

1313
jobs:
1414
build:

.vscode/tasks.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@
121121
"type": "process",
122122
"args": [
123123
"test",
124+
"--results-directory",
125+
"${workspaceFolder}/dotnet/TestResults/",
124126
"--collect",
125127
"XPlat Code Coverage;Format=lcov",
126128
"--filter",
@@ -143,6 +145,8 @@
143145
"type": "process",
144146
"args": [
145147
"test",
148+
"--results-directory",
149+
"${workspaceFolder}/dotnet/TestResults/",
146150
"--collect",
147151
"XPlat Code Coverage;Format=lcov",
148152
"--filter",
@@ -159,12 +163,37 @@
159163
"cwd": "${workspaceFolder}/dotnet/src/Extensions/Extensions.UnitTests/"
160164
}
161165
},
166+
{
167+
"label": "Test - ALL (Code Coverage)",
168+
"command": "dotnet",
169+
"type": "process",
170+
"args": [
171+
"test",
172+
"--results-directory",
173+
"${workspaceFolder}/dotnet/TestResults/",
174+
"--collect",
175+
"XPlat Code Coverage;Format=lcov",
176+
"--filter",
177+
"${input:filter}"
178+
],
179+
"problemMatcher": "$msCompile",
180+
"group": "test",
181+
"presentation": {
182+
"reveal": "always",
183+
"panel": "shared"
184+
},
185+
"options": {
186+
"cwd": "${workspaceFolder}/dotnet/"
187+
}
188+
},
162189
{
163190
"label": "Test - Semantic-Kernel Integration (Code Coverage)",
164191
"command": "dotnet",
165192
"type": "process",
166193
"args": [
167194
"test",
195+
"--results-directory",
196+
"${workspaceFolder}/dotnet/TestResults/",
168197
"--collect",
169198
"XPlat Code Coverage;Format=lcov",
170199
"--filter",

dotnet/src/Connectors/Connectors.AI.OpenAI/CustomClient/OpenAIClientBase.cs

Lines changed: 70 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
using System;
44
using System.Collections.Generic;
5-
using System.Diagnostics.CodeAnalysis;
65
using System.Linq;
76
using System.Net.Http;
87
using System.Text;
@@ -21,39 +20,34 @@
2120

2221
namespace Microsoft.SemanticKernel.Connectors.AI.OpenAI.CustomClient;
2322

24-
/// <summary>
25-
/// An abstract OpenAI Client.
26-
/// </summary>
27-
[SuppressMessage("Design", "CA1054:URI-like parameters should not be strings", Justification = "OpenAI users use strings")]
23+
#pragma warning disable CA1063 // Class isn't publicly extensible and thus doesn't implement the full IDisposable pattern
24+
#pragma warning disable CA1816 // No derived types implement a finalizer
25+
26+
/// <summary>Base type for OpenAI clients.</summary>
2827
public abstract class OpenAIClientBase : IDisposable
2928
{
30-
protected static readonly HttpClientHandler DefaultHttpClientHandler = new() { CheckCertificateRevocationList = true };
31-
32-
/// <summary>
33-
/// Logger
34-
/// </summary>
35-
protected ILogger Log { get; } = NullLogger.Instance;
29+
/// <summary>Initialize the client.</summary>
30+
private protected OpenAIClientBase(HttpClient? httpClient = null, ILogger? logger = null)
31+
{
32+
this._httpClient = httpClient ?? new HttpClient(s_defaultHttpClientHandler, disposeHandler: false);
33+
this._disposeHttpClient = this._httpClient != httpClient; // dispose a non-shared client when this is disposed
3634

37-
/// <summary>
38-
/// HTTP client
39-
/// </summary>
40-
protected HttpClient HTTPClient { get; }
35+
this._log = logger ?? NullLogger.Instance;
36+
}
4137

42-
internal OpenAIClientBase(HttpClient? httpClient = null, ILogger? logger = null)
38+
/// <summary>Clean up resources used by this instance.</summary>
39+
public void Dispose()
4340
{
44-
this.Log = logger ?? this.Log;
45-
46-
if (httpClient == null)
47-
{
48-
this.HTTPClient = new HttpClient(DefaultHttpClientHandler, disposeHandler: false);
49-
this._disposeHttpClient = true; // If client is created internally, dispose it when done
50-
}
51-
else
41+
if (this._disposeHttpClient)
5242
{
53-
this.HTTPClient = httpClient;
43+
this._httpClient.Dispose();
5444
}
45+
}
5546

56-
this.HTTPClient.DefaultRequestHeaders.Add("User-Agent", HTTPUserAgent);
47+
/// <summary>Adds headers to use for OpenAI HTTP requests.</summary>
48+
private protected virtual void AddRequestHeaders(HttpRequestMessage request)
49+
{
50+
request.Headers.Add("User-Agent", HttpUserAgent);
5751
}
5852

5953
/// <summary>
@@ -64,112 +58,43 @@ internal OpenAIClientBase(HttpClient? httpClient = null, ILogger? logger = null)
6458
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
6559
/// <returns>List of text embeddings</returns>
6660
/// <exception cref="AIException">AIException thrown during the request.</exception>
67-
protected async Task<IList<Embedding<float>>> ExecuteTextEmbeddingRequestAsync(
61+
private protected async Task<IList<Embedding<float>>> ExecuteTextEmbeddingRequestAsync(
6862
string url,
6963
string requestBody,
7064
CancellationToken cancellationToken = default)
7165
{
72-
try
73-
{
74-
var result = await this.ExecutePostRequestAsync<TextEmbeddingResponse>(url, requestBody, cancellationToken).ConfigureAwait(false);
75-
if (result.Embeddings.Count < 1)
76-
{
77-
throw new AIException(
78-
AIException.ErrorCodes.InvalidResponseContent,
79-
"Embeddings not found");
80-
}
81-
82-
return result.Embeddings.Select(e => new Embedding<float>(e.Values)).ToList();
83-
}
84-
catch (Exception e) when (e is not AIException)
66+
var result = await this.ExecutePostRequestAsync<TextEmbeddingResponse>(url, requestBody, cancellationToken).ConfigureAwait(false);
67+
if (result.Embeddings is not { Count: >= 1 })
8568
{
8669
throw new AIException(
87-
AIException.ErrorCodes.UnknownError,
88-
$"Something went wrong: {e.Message}", e);
70+
AIException.ErrorCodes.InvalidResponseContent,
71+
"Embeddings not found");
8972
}
90-
}
9173

92-
/// <summary>
93-
/// Run the HTTP request to generate a list of images
94-
/// </summary>
95-
/// <param name="url">URL for the image generation request API</param>
96-
/// <param name="requestBody">Request payload</param>
97-
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
98-
/// <returns>List of image URLs</returns>
99-
/// <exception cref="AIException">AIException thrown during the request.</exception>
100-
protected async Task<IList<string>> ExecuteImageUrlGenerationRequestAsync(
101-
string url,
102-
string requestBody,
103-
CancellationToken cancellationToken = default)
104-
{
105-
try
106-
{
107-
var result = await this.ExecutePostRequestAsync<ImageGenerationResponse>(url, requestBody, cancellationToken).ConfigureAwait(false);
108-
return result.Images.Select(x => x.Url).ToList();
109-
}
110-
catch (Exception e) when (e is not AIException)
111-
{
112-
throw new AIException(
113-
AIException.ErrorCodes.UnknownError,
114-
$"Something went wrong: {e.Message}", e);
115-
}
74+
return result.Embeddings.Select(e => new Embedding<float>(e.Values)).ToList();
11675
}
11776

11877
/// <summary>
11978
/// Run the HTTP request to generate a list of images
12079
/// </summary>
12180
/// <param name="url">URL for the image generation request API</param>
12281
/// <param name="requestBody">Request payload</param>
82+
/// <param name="extractResponseFunc">Function to invoke to extract the desired portion of the image generation response.</param>
12383
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
124-
/// <returns>List of images serialized in base64</returns>
84+
/// <returns>List of image URLs</returns>
12585
/// <exception cref="AIException">AIException thrown during the request.</exception>
126-
protected async Task<IList<string>> ExecuteImageBase64GenerationRequestAsync(
86+
private protected async Task<IList<string>> ExecuteImageGenerationRequestAsync(
12787
string url,
12888
string requestBody,
89+
Func<ImageGenerationResponse.Image, string> extractResponseFunc,
12990
CancellationToken cancellationToken = default)
13091
{
131-
try
132-
{
133-
var result = await this.ExecutePostRequestAsync<ImageGenerationResponse>(url, requestBody, cancellationToken).ConfigureAwait(false);
134-
return result.Images.Select(x => x.AsBase64).ToList();
135-
}
136-
catch (Exception e) when (e is not AIException)
137-
{
138-
throw new AIException(
139-
AIException.ErrorCodes.UnknownError,
140-
$"Something went wrong: {e.Message}", e);
141-
}
142-
}
143-
144-
/// <summary>
145-
/// Explicit finalizer called by IDisposable
146-
/// </summary>
147-
public void Dispose()
148-
{
149-
this.Dispose(true);
150-
// Request CL runtime not to call the finalizer - reduce cost of GC
151-
GC.SuppressFinalize(this);
152-
}
153-
154-
/// <summary>
155-
/// Overridable finalizer for concrete classes
156-
/// </summary>
157-
/// <param name="disposing"></param>
158-
protected virtual void Dispose(bool disposing)
159-
{
160-
if (disposing & this._disposeHttpClient)
161-
{
162-
this.HTTPClient.Dispose();
163-
}
92+
var result = await this.ExecutePostRequestAsync<ImageGenerationResponse>(url, requestBody, cancellationToken).ConfigureAwait(false);
93+
return result.Images.Select(extractResponseFunc).ToList();
16494
}
16595

166-
protected virtual string? GetErrorMessageFromResponse(string? jsonResponsePayload)
96+
private protected virtual string? GetErrorMessageFromResponse(string jsonResponsePayload)
16797
{
168-
if (jsonResponsePayload is null)
169-
{
170-
return null;
171-
}
172-
17398
try
17499
{
175100
JsonNode? root = JsonSerializer.Deserialize<JsonNode>(jsonResponsePayload);
@@ -178,33 +103,48 @@ protected virtual void Dispose(bool disposing)
178103
}
179104
catch (Exception ex) when (ex is NotSupportedException or JsonException)
180105
{
181-
this.Log.LogTrace("Unable to extract error from response body content. Exception: {0}:{1}", ex.GetType(), ex.Message);
182-
return null;
106+
this._log.LogTrace("Unable to extract error from response body content. Exception: {0}:{1}", ex.GetType(), ex.Message);
183107
}
108+
109+
return null;
184110
}
185111

186112
#region private ================================================================================
187113

114+
// Shared singleton HttpClientHandler used when an existing HttpClient isn't provided
115+
private static readonly HttpClientHandler s_defaultHttpClientHandler = new() { CheckCertificateRevocationList = true };
116+
188117
// HTTP user agent sent to remote endpoints
189-
private const string HTTPUserAgent = "Microsoft-Semantic-Kernel";
118+
private const string HttpUserAgent = "Microsoft-Semantic-Kernel";
190119

191120
// Set to true to dispose of HttpClient when disposing. If HttpClient was passed in, then the caller can manage.
192-
private readonly bool _disposeHttpClient = false;
121+
private readonly bool _disposeHttpClient;
122+
123+
/// <summary>
124+
/// Logger
125+
/// </summary>
126+
private readonly ILogger _log;
127+
128+
/// <summary>
129+
/// The <see cref="_httpClient"/> to use for issuing requests.
130+
/// </summary>
131+
private readonly HttpClient _httpClient;
193132

194133
private async Task<T> ExecutePostRequestAsync<T>(string url, string requestBody, CancellationToken cancellationToken = default)
195134
{
196-
string responseJson;
197-
135+
HttpResponseMessage? response = null;
198136
try
199137
{
200-
using HttpContent content = new StringContent(requestBody, Encoding.UTF8, "application/json");
201-
202-
HttpResponseMessage response = await this.HTTPClient.PostAsync(url, content, cancellationToken).ConfigureAwait(false)
203-
?? throw new AIException(AIException.ErrorCodes.NoResponse);
138+
using (var request = new HttpRequestMessage(HttpMethod.Post, url))
139+
{
140+
this.AddRequestHeaders(request);
141+
request.Content = new StringContent(requestBody, Encoding.UTF8, "application/json");
142+
response = await this._httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
143+
}
204144

205-
this.Log.LogTrace("HTTP response: {0} {1}", (int)response.StatusCode, response.StatusCode.ToString("G"));
145+
this._log.LogTrace("HTTP response: {0} {1}", (int)response.StatusCode, response.StatusCode.ToString("G"));
206146

207-
responseJson = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
147+
string responseJson = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
208148
string? errorDetail = this.GetErrorMessageFromResponse(responseJson);
209149

210150
if (!response.IsSuccessStatusCode)
@@ -276,29 +216,27 @@ private async Task<T> ExecutePostRequestAsync<T>(string url, string requestBody,
276216
errorDetail);
277217
}
278218
}
279-
}
280-
catch (Exception e) when (e is not AIException)
281-
{
282-
throw new AIException(
283-
AIException.ErrorCodes.UnknownError,
284-
$"Something went wrong: {e.Message}", e);
285-
}
286219

287-
try
288-
{
289220
var result = Json.Deserialize<T>(responseJson);
290-
if (result != null) { return result; }
291-
292-
throw new AIException(
221+
if (result is null)
222+
{
223+
throw new AIException(
293224
AIException.ErrorCodes.InvalidResponseContent,
294225
"Response JSON parse error");
226+
}
227+
228+
return result;
295229
}
296230
catch (Exception e) when (e is not AIException)
297231
{
298232
throw new AIException(
299233
AIException.ErrorCodes.UnknownError,
300234
$"Something went wrong: {e.Message}", e);
301235
}
236+
finally
237+
{
238+
response?.Dispose();
239+
}
302240
}
303241

304242
#endregion

0 commit comments

Comments
 (0)