Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Threading;
using System.Threading.Tasks;
using Azure.Core;
using Microsoft.Agents.AI.CosmosNoSql;
using Microsoft.Azure.Cosmos;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
Expand Down Expand Up @@ -126,6 +127,7 @@ public CosmosChatHistoryProvider(
Throw.IfNull(stateInitializer),
stateKey ?? this.GetType().Name);
this._cosmosClient = Throw.IfNull(cosmosClient);
CosmosOptionsHelper.EnsureApplicationName(this._cosmosClient, nameof(CosmosChatHistoryProvider));
this.DatabaseId = Throw.IfNullOrWhitespace(databaseId);
this.ContainerId = Throw.IfNullOrWhitespace(containerId);
this._container = this._cosmosClient.GetContainer(databaseId, containerId);
Expand Down Expand Up @@ -157,7 +159,7 @@ public CosmosChatHistoryProvider(
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? provideOutputMessageFilter = null,
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? storeInputRequestMessageFilter = null,
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? storeInputResponseMessageFilter = null)
: this(new CosmosClient(Throw.IfNullOrWhitespace(connectionString)), databaseId, containerId, stateInitializer, ownsClient: true, stateKey, provideOutputMessageFilter, storeInputRequestMessageFilter, storeInputResponseMessageFilter)
: this(new CosmosClient(Throw.IfNullOrWhitespace(connectionString), CosmosOptionsHelper.CreateOptions(nameof(CosmosChatHistoryProvider))), databaseId, containerId, stateInitializer, ownsClient: true, stateKey, provideOutputMessageFilter, storeInputRequestMessageFilter, storeInputResponseMessageFilter)
{
}

Expand Down Expand Up @@ -185,7 +187,7 @@ public CosmosChatHistoryProvider(
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? provideOutputMessageFilter = null,
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? storeInputRequestMessageFilter = null,
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? storeInputResponseMessageFilter = null)
: this(new CosmosClient(Throw.IfNullOrWhitespace(accountEndpoint), Throw.IfNull(tokenCredential)), databaseId, containerId, stateInitializer, ownsClient: true, stateKey, provideOutputMessageFilter, storeInputRequestMessageFilter, storeInputResponseMessageFilter)
: this(new CosmosClient(Throw.IfNullOrWhitespace(accountEndpoint), Throw.IfNull(tokenCredential), CosmosOptionsHelper.CreateOptions(nameof(CosmosChatHistoryProvider))), databaseId, containerId, stateInitializer, ownsClient: true, stateKey, provideOutputMessageFilter, storeInputRequestMessageFilter, storeInputResponseMessageFilter)
{
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Text.Json;
using System.Threading.Tasks;
using Azure.Core;
using Microsoft.Agents.AI.CosmosNoSql;
using Microsoft.Azure.Cosmos;
using Microsoft.Shared.Diagnostics;
using Newtonsoft.Json;
Expand Down Expand Up @@ -37,7 +38,7 @@ public class CosmosCheckpointStore<T> : JsonCheckpointStore, IDisposable
/// <exception cref="ArgumentException">Thrown when any string parameter is null or whitespace.</exception>
public CosmosCheckpointStore(string connectionString, string databaseId, string containerId)
{
var cosmosClientOptions = new CosmosClientOptions();
var cosmosClientOptions = CosmosOptionsHelper.CreateOptions(nameof(CosmosCheckpointStore));

this._cosmosClient = new CosmosClient(Throw.IfNullOrWhitespace(connectionString), cosmosClientOptions);
this._container = this._cosmosClient.GetContainer(Throw.IfNullOrWhitespace(databaseId), Throw.IfNullOrWhitespace(containerId));
Expand All @@ -55,12 +56,10 @@ public CosmosCheckpointStore(string connectionString, string databaseId, string
/// <exception cref="ArgumentException">Thrown when any string parameter is null or whitespace.</exception>
public CosmosCheckpointStore(string accountEndpoint, TokenCredential tokenCredential, string databaseId, string containerId)
{
var cosmosClientOptions = new CosmosClientOptions
var cosmosClientOptions = CosmosOptionsHelper.CreateOptions(nameof(CosmosCheckpointStore));
cosmosClientOptions.SerializerOptions = new CosmosSerializationOptions
{
SerializerOptions = new CosmosSerializationOptions
{
PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase
}
PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase
};

this._cosmosClient = new CosmosClient(Throw.IfNullOrWhitespace(accountEndpoint), Throw.IfNull(tokenCredential), cosmosClientOptions);
Expand All @@ -79,6 +78,7 @@ public CosmosCheckpointStore(string accountEndpoint, TokenCredential tokenCreden
public CosmosCheckpointStore(CosmosClient cosmosClient, string databaseId, string containerId)
{
this._cosmosClient = Throw.IfNull(cosmosClient);
CosmosOptionsHelper.EnsureApplicationName(this._cosmosClient, nameof(CosmosCheckpointStore));

this._container = this._cosmosClient.GetContainer(Throw.IfNullOrWhitespace(databaseId), Throw.IfNullOrWhitespace(containerId));
this._ownsClient = false;
Expand Down
80 changes: 80 additions & 0 deletions dotnet/src/Microsoft.Agents.AI.CosmosNoSql/CosmosOptionsHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Reflection;
using Microsoft.Azure.Cosmos;

namespace Microsoft.Agents.AI.CosmosNoSql;

/// <summary>
/// Provides shared Cosmos DB client configuration for Agent Framework Cosmos NoSQL integrations.
/// Ensures all internally-created <see cref="CosmosClient"/> instances carry a consistent
/// <see cref="CosmosClientOptions.ApplicationName"/> for telemetry and diagnostics.
/// </summary>
internal static class CosmosOptionsHelper
Comment thread
westey-m marked this conversation as resolved.
{
/// <summary>
/// Maximum length allowed by the Cosmos DB .NET SDK for <see cref="CosmosClientOptions.ApplicationName"/>.
/// </summary>
private const int MaxApplicationNameLength = 64;

private static readonly string s_version = GetVersion();

/// <summary>
/// Creates a <see cref="CosmosClientOptions"/> instance pre-configured with the
/// Agent Framework application name for User-Agent identification.
/// </summary>
/// <param name="component">The fully-qualified component class name (e.g. "CosmosChatHistoryProvider").</param>
/// <returns>A new <see cref="CosmosClientOptions"/> with <see cref="CosmosClientOptions.ApplicationName"/> set.</returns>
public static CosmosClientOptions CreateOptions(string component)
{
return new CosmosClientOptions
{
ApplicationName = BuildApplicationName(component)
};
}

/// <summary>
/// Ensures the given <see cref="CosmosClient"/> has an <see cref="CosmosClientOptions.ApplicationName"/> set.
/// If the client already has a non-empty ApplicationName, it is not overridden.
/// </summary>
/// <param name="cosmosClient">The client to apply the application name to.</param>
/// <param name="component">The fully-qualified component class name (e.g. "CosmosChatHistoryProvider").</param>
public static void EnsureApplicationName(CosmosClient cosmosClient, string component)
{
if (string.IsNullOrWhiteSpace(cosmosClient.ClientOptions.ApplicationName))
{
cosmosClient.ClientOptions.ApplicationName = BuildApplicationName(component);
}
}

private static string BuildApplicationName(string component)
{
var applicationName = $"Microsoft.Agents.AI.CosmosNoSql.{component}/{s_version}";

if (applicationName.Length > MaxApplicationNameLength)
{
applicationName = applicationName.Substring(0, MaxApplicationNameLength);
}

return applicationName;
}
Comment thread
TheovanKraay marked this conversation as resolved.

private static string GetVersion()
{
if (typeof(CosmosOptionsHelper).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion is string version)
{
int pos = version.IndexOf('+', System.StringComparison.Ordinal);
if (pos >= 0)
{
version = version.Substring(0, pos);
}

if (version.Length > 0)
{
return version;
}
}

return "unknown";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright (c) Microsoft. All rights reserved.

using Microsoft.Agents.AI.CosmosNoSql;
using Microsoft.Azure.Cosmos;

namespace Microsoft.Agents.AI.CosmosNoSql.UnitTests;

public sealed class CosmosOptionsHelperTests
{
[Fact]
public void CreateOptions_SetsApplicationName_WithComponentAndVersion()
{
// Act
var options = CosmosOptionsHelper.CreateOptions("CosmosChatHistoryProvider");

// Assert
Assert.NotNull(options.ApplicationName);
Assert.StartsWith("Microsoft.Agents.AI.CosmosNoSql.CosmosChatHistoryProvider/", options.ApplicationName);
}

[Fact]
public void CreateOptions_DifferentComponents_ProduceDifferentNames()
{
// Act
var chatOptions = CosmosOptionsHelper.CreateOptions("CosmosChatHistoryProvider");
var checkpointOptions = CosmosOptionsHelper.CreateOptions("CosmosCheckpointStore");

// Assert
Assert.NotEqual(chatOptions.ApplicationName, checkpointOptions.ApplicationName);
Assert.Contains("CosmosChatHistoryProvider", chatOptions.ApplicationName);
Assert.Contains("CosmosCheckpointStore", checkpointOptions.ApplicationName);
}

[Fact]
public void CreateOptions_ApplicationName_DoesNotExceedMaxLength()
{
// Use a deliberately long component name to trigger truncation
var longComponent = new string('X', 100);

// Act
var options = CosmosOptionsHelper.CreateOptions(longComponent);

// Assert
Assert.True(options.ApplicationName!.Length <= 64,
$"ApplicationName length {options.ApplicationName.Length} exceeds max 64");
}

[Fact]
public void EnsureApplicationName_SetsName_WhenClientHasNone()
{
// Arrange
var clientOptions = new CosmosClientOptions();
Assert.Null(clientOptions.ApplicationName);

// Act
var options = CosmosOptionsHelper.CreateOptions("CosmosChatHistoryProvider");

// Assert - verify the returned options have ApplicationName set
Assert.NotNull(options.ApplicationName);
Assert.NotEmpty(options.ApplicationName);
}

[Fact]
public void CreateOptions_ApplicationName_ContainsVersion()
{
// Act
var options = CosmosOptionsHelper.CreateOptions("CosmosChatHistoryProvider");

// Assert - should contain a "/" followed by version info
Assert.Contains("/", options.ApplicationName);
var parts = options.ApplicationName!.Split('/');
Assert.Equal(2, parts.Length);
Assert.False(string.IsNullOrWhiteSpace(parts[1]), "Version portion should not be empty");
}
}
Loading