diff --git a/clients/dotnet/WebClient/MemoryWebClient.cs b/clients/dotnet/WebClient/MemoryWebClient.cs
index 10d3eea83..281f9b5cf 100644
--- a/clients/dotnet/WebClient/MemoryWebClient.cs
+++ b/clients/dotnet/WebClient/MemoryWebClient.cs
@@ -17,21 +17,39 @@ namespace Microsoft.KernelMemory;
#pragma warning disable CA2234 // using string URIs is ok
+///
+/// Kernel Memory web service client
+///
public sealed class MemoryWebClient : IKernelMemory
{
private static readonly JsonSerializerOptions s_caseInsensitiveJsonOptions = new() { PropertyNameCaseInsensitive = true };
private readonly HttpClient _client;
+ ///
+ /// New instance of web client to use Kernel Memory web service
+ ///
+ /// Kernel Memory web service endpoint
+ /// Kernel Memory web service API Key (if configured)
+ /// Name of HTTP header to use to send API Key
public MemoryWebClient(string endpoint, string? apiKey = "", string apiKeyHeader = "Authorization")
: this(endpoint, new HttpClient(), apiKey: apiKey, apiKeyHeader: apiKeyHeader)
{
}
+ ///
+ /// New instance of web client to use Kernel Memory web service
+ ///
+ /// Kernel Memory web service endpoint
+ /// Custom HTTP Client to use (note: BaseAddress is overwritten)
+ /// Kernel Memory web service API Key (if configured)
+ /// Name of HTTP header to use to send API Key
public MemoryWebClient(string endpoint, HttpClient client, string? apiKey = "", string apiKeyHeader = "Authorization")
{
+ ArgumentNullExceptionEx.ThrowIfNullOrWhiteSpace(endpoint, nameof(endpoint), "Kernel Memory endpoint is empty");
+
this._client = client;
- this._client.BaseAddress = new Uri(endpoint);
+ this._client.BaseAddress = new Uri(endpoint.CleanBaseAddress());
if (!string.IsNullOrEmpty(apiKey))
{
@@ -101,16 +119,19 @@ public async Task ImportTextAsync(
IEnumerable? steps = null,
CancellationToken cancellationToken = default)
{
- using Stream content = new MemoryStream(Encoding.UTF8.GetBytes(text));
- return await this.ImportDocumentAsync(
- content,
- fileName: "content.txt",
- documentId: documentId,
- tags,
- index: index,
- steps: steps,
- cancellationToken)
- .ConfigureAwait(false);
+ Stream content = new MemoryStream(Encoding.UTF8.GetBytes(text));
+ await using (content.ConfigureAwait(false))
+ {
+ return await this.ImportDocumentAsync(
+ content,
+ fileName: "content.txt",
+ documentId: documentId,
+ tags,
+ index: index,
+ steps: steps,
+ cancellationToken)
+ .ConfigureAwait(false);
+ }
}
///
@@ -125,23 +146,26 @@ public async Task ImportWebPageAsync(
var uri = new Uri(url);
Verify.ValidateUrl(uri.AbsoluteUri, requireHttps: false, allowReservedIp: false, allowQuery: true);
- using Stream content = new MemoryStream(Encoding.UTF8.GetBytes(uri.AbsoluteUri));
- return await this.ImportDocumentAsync(
- content,
- fileName: "content.url",
- documentId: documentId,
- tags,
- index: index,
- steps: steps,
- cancellationToken)
- .ConfigureAwait(false);
+ Stream content = new MemoryStream(Encoding.UTF8.GetBytes(uri.AbsoluteUri));
+ await using (content.ConfigureAwait(false))
+ {
+ return await this.ImportDocumentAsync(
+ content,
+ fileName: "content.url",
+ documentId: documentId,
+ tags,
+ index: index,
+ steps: steps,
+ cancellationToken)
+ .ConfigureAwait(false);
+ }
}
///
public async Task> ListIndexesAsync(CancellationToken cancellationToken = default)
{
- const string URL = Constants.HttpIndexesEndpoint;
- HttpResponseMessage? response = await this._client.GetAsync(URL, cancellationToken).ConfigureAwait(false);
+ var url = Constants.HttpIndexesEndpoint.CleanUrlPath();
+ HttpResponseMessage response = await this._client.GetAsync(url, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
@@ -154,8 +178,10 @@ public async Task> ListIndexesAsync(CancellationToken
///
public async Task DeleteIndexAsync(string? index = null, CancellationToken cancellationToken = default)
{
- var url = Constants.HttpDeleteIndexEndpointWithParams.Replace(Constants.HttpIndexPlaceholder, index, StringComparison.OrdinalIgnoreCase);
- HttpResponseMessage? response = await this._client.DeleteAsync(url, cancellationToken).ConfigureAwait(false);
+ var url = Constants.HttpDeleteIndexEndpointWithParams
+ .Replace(Constants.HttpIndexPlaceholder, index, StringComparison.OrdinalIgnoreCase)
+ .CleanUrlPath();
+ HttpResponseMessage response = await this._client.DeleteAsync(url, cancellationToken).ConfigureAwait(false);
// No error if the index doesn't exist
if (response.StatusCode == HttpStatusCode.NotFound)
@@ -183,8 +209,9 @@ public async Task DeleteDocumentAsync(string documentId, string? index = null, C
var url = Constants.HttpDeleteDocumentEndpointWithParams
.Replace(Constants.HttpIndexPlaceholder, index, StringComparison.OrdinalIgnoreCase)
- .Replace(Constants.HttpDocumentIdPlaceholder, documentId, StringComparison.OrdinalIgnoreCase);
- HttpResponseMessage? response = await this._client.DeleteAsync(url, cancellationToken).ConfigureAwait(false);
+ .Replace(Constants.HttpDocumentIdPlaceholder, documentId, StringComparison.OrdinalIgnoreCase)
+ .CleanUrlPath();
+ HttpResponseMessage response = await this._client.DeleteAsync(url, cancellationToken).ConfigureAwait(false);
// No error if the document doesn't exist
if (response.StatusCode == HttpStatusCode.NotFound)
@@ -220,7 +247,8 @@ public async Task IsDocumentReadyAsync(
{
var url = Constants.HttpUploadStatusEndpointWithParams
.Replace(Constants.HttpIndexPlaceholder, index, StringComparison.OrdinalIgnoreCase)
- .Replace(Constants.HttpDocumentIdPlaceholder, documentId, StringComparison.OrdinalIgnoreCase);
+ .Replace(Constants.HttpDocumentIdPlaceholder, documentId, StringComparison.OrdinalIgnoreCase)
+ .CleanUrlPath();
HttpResponseMessage? response = await this._client.GetAsync(url, cancellationToken).ConfigureAwait(false);
if (response.StatusCode == HttpStatusCode.NotFound)
{
@@ -242,12 +270,13 @@ public async Task ExportFileAsync(
string? index = null,
CancellationToken cancellationToken = default)
{
- string requestUri = Constants.HttpDownloadEndpointWithParams
+ var url = Constants.HttpDownloadEndpointWithParams
.Replace(Constants.HttpIndexPlaceholder, index, StringComparison.OrdinalIgnoreCase)
.Replace(Constants.HttpDocumentIdPlaceholder, documentId, StringComparison.OrdinalIgnoreCase)
- .Replace(Constants.HttpFilenamePlaceholder, fileName, StringComparison.OrdinalIgnoreCase);
+ .Replace(Constants.HttpFilenamePlaceholder, fileName, StringComparison.OrdinalIgnoreCase)
+ .CleanUrlPath();
- HttpResponseMessage httpResponse = await this._client.GetAsync(requestUri, cancellationToken).ConfigureAwait(false);
+ HttpResponseMessage httpResponse = await this._client.GetAsync(url, cancellationToken).ConfigureAwait(false);
ArgumentNullExceptionEx.ThrowIfNull(httpResponse, nameof(httpResponse), "KernelMemory HTTP response is NULL");
httpResponse.EnsureSuccessStatusCode();
@@ -288,7 +317,8 @@ public async Task SearchAsync(
};
using StringContent content = new(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json");
- HttpResponseMessage? response = await this._client.PostAsync(Constants.HttpSearchEndpoint, content, cancellationToken).ConfigureAwait(false);
+ var url = Constants.HttpSearchEndpoint.CleanUrlPath();
+ HttpResponseMessage response = await this._client.PostAsync(url, content, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
@@ -320,7 +350,8 @@ public async Task AskAsync(
};
using StringContent content = new(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json");
- HttpResponseMessage? response = await this._client.PostAsync(Constants.HttpAskEndpoint, content, cancellationToken).ConfigureAwait(false);
+ var url = Constants.HttpAskEndpoint.CleanUrlPath();
+ HttpResponseMessage response = await this._client.PostAsync(url, content, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
@@ -382,7 +413,7 @@ private async Task ImportInternalAsync(
using StringContent indexContent = new(index);
using (StringContent documentIdContent = new(uploadRequest.DocumentId))
{
- List disposables = new();
+ List disposables = [];
formData.Add(indexContent, Constants.WebServiceIndexField);
formData.Add(documentIdContent, Constants.WebServiceDocumentIdField);
@@ -422,7 +453,8 @@ private async Task ImportInternalAsync(
// Send HTTP request
try
{
- HttpResponseMessage? response = await this._client.PostAsync("/upload", formData, cancellationToken).ConfigureAwait(false);
+ var url = Constants.HttpUploadEndpoint.CleanUrlPath();
+ HttpResponseMessage response = await this._client.PostAsync(url, formData, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
}
catch (HttpRequestException e) when (e.Data.Contains("StatusCode"))
@@ -435,7 +467,6 @@ private async Task ImportInternalAsync(
}
finally
{
- formData.Dispose();
foreach (var disposable in disposables)
{
disposable.Dispose();
diff --git a/clients/dotnet/WebClient/StringExtensions.cs b/clients/dotnet/WebClient/StringExtensions.cs
new file mode 100644
index 000000000..2c797db83
--- /dev/null
+++ b/clients/dotnet/WebClient/StringExtensions.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace Microsoft.KernelMemory;
+
+internal static class StringExtensions
+{
+ public static string CleanBaseAddress(this string endpoint)
+ {
+ ArgumentNullExceptionEx.ThrowIfNull(endpoint, nameof(endpoint), "Kernel Memory API endpoint is NULL");
+
+ return endpoint.TrimEnd('/') + '/';
+ }
+
+ public static string CleanUrlPath(this string path)
+ {
+ if (string.IsNullOrWhiteSpace(path)) { path = "/"; }
+
+ return path.TrimStart('/');
+ }
+}