// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Security.Cryptography;
using System.Text.RegularExpressions;
using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
namespace Microsoft.Agents.AI.Hosting.OpenAI;
///
/// Generates IDs with partition keys.
///
internal sealed partial class IdGenerator
{
private readonly string _partitionId;
private readonly Random? _random;
#if NET9_0_OR_GREATER
[GeneratedRegex("^[A-Za-z0-9]+$")]
private static partial Regex WatermarkRegex();
#else
private static readonly Regex s_watermarkRegex = new("^[A-Za-z0-9]+$", RegexOptions.Compiled);
private static Regex WatermarkRegex() => s_watermarkRegex;
#endif
///
/// Initializes a new instance of the class.
///
/// The response ID.
/// The conversation ID.
/// Optional random seed for deterministic ID generation. When null, uses cryptographically secure random generation.
public IdGenerator(string? responseId, string? conversationId, int? randomSeed = null)
{
this._random = randomSeed.HasValue ? new Random(randomSeed.Value) : null;
this.ResponseId = responseId ?? NewId("resp", random: this._random);
this.ConversationId = conversationId ?? NewId("conv", random: this._random);
this._partitionId = GetPartitionIdOrDefault(this.ConversationId) ?? string.Empty;
}
///
/// Creates a new ID generator from a create response request.
///
/// The create response request.
/// A new ID generator.
public static IdGenerator From(CreateResponse request)
{
string? responseId = null;
request.Metadata?.TryGetValue("response_id", out responseId);
return new IdGenerator(responseId, request.Conversation?.Id);
}
///
/// Gets the response ID.
///
public string ResponseId { get; }
///
/// Gets the conversation ID.
///
public string ConversationId { get; }
///
/// Generates a new ID.
///
/// The optional category for the ID.
/// A generated ID string.
public string Generate(string? category = null)
{
var prefix = string.IsNullOrEmpty(category) ? "id" : category;
return NewId(prefix, partitionKey: this._partitionId, random: this._random);
}
///
/// Generates a function call ID.
///
/// A function call ID.
public string GenerateFunctionCallId() => this.Generate("func");
///
/// Generates a function output ID.
///
/// A function output ID.
public string GenerateFunctionOutputId() => this.Generate("funcout");
///
/// Generates a message ID.
///
/// A message ID.
public string GenerateMessageId() => this.Generate("msg");
///
/// Generates a reasoning ID.
///
/// A reasoning ID.
public string GenerateReasoningId() => this.Generate("rs");
///
/// Generates a new ID with a structured format that includes a partition key.
///
/// The prefix to add to the ID, typically indicating the resource type.
/// The length of the random entropy string in the ID.
/// The length of the partition key if generating a new one.
/// Optional additional text to insert between the prefix and the entropy.
/// Optional text to insert in the middle of the entropy string for traceability.
/// The delimiter character used to separate parts of the ID.
/// An explicit partition key to use. When provided, this value will be used instead of generating a new one.
/// An existing ID to extract the partition key from. When provided, the same partition key will be used instead of generating a new one.
/// The random number generator.
/// A new ID with format "{prefix}{delimiter}{infix}{entropy}{delimiter}{partitionKey}".
/// Thrown when the watermark contains non-alphanumeric characters.
public static string NewId(string prefix, int stringLength = 32, int partitionKeyLength = 16, string infix = "",
string watermark = "", string delimiter = "_", string? partitionKey = null, string partitionKeyHint = "",
Random? random = null)
{
ArgumentOutOfRangeException.ThrowIfLessThan(stringLength, 1);
var entropy = GetRandomString(stringLength, random);
string pKey = partitionKey ?? GetPartitionIdOrDefault(partitionKeyHint) ?? GetRandomString(partitionKeyLength, random);
if (!string.IsNullOrEmpty(watermark))
{
if (!WatermarkRegex().IsMatch(watermark))
{
throw new ArgumentException($"Only alphanumeric characters may be in watermark: {watermark}",
nameof(watermark));
}
entropy = $"{entropy[..(stringLength / 2)]}{watermark}{entropy[(stringLength / 2)..]}";
}
infix ??= "";
prefix = !string.IsNullOrEmpty(prefix) ? $"{prefix}{delimiter}" : "";
return $"{prefix}{infix}{entropy}{pKey}";
}
///
/// Generates a secure random alphanumeric string of the specified length.
/// When a random seed was provided to the constructor, uses deterministic generation.
///
/// The desired length of the random string.
/// The optional random number generator.
/// A random alphanumeric string.
/// Thrown when stringLength is less than 1.
private static string GetRandomString(int stringLength, Random? random)
{
const string Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
if (random is not null)
{
#if NET10_0_OR_GREATER
return random.GetString(Chars, stringLength);
#else
// Use deterministic random generation when seed is provided
return string.Create(stringLength, random, static (destination, random) =>
{
for (int i = 0; i < destination.Length; i++)
{
destination[i] = Chars[random.Next(Chars.Length)];
}
});
#endif
}
// Use cryptographically secure random generation when no seed is provided
return RandomNumberGenerator.GetString(Chars, stringLength);
}
///
/// Extracts the partition key from an existing ID, or returns null if extraction fails.
///
/// The ID to extract the partition key from.
/// The length of the random entropy string in the ID.
/// The length of the partition key if generating a new one.
/// The delimiter character used in the ID.
/// The partition key if successfully extracted; otherwise, null.
private static string? GetPartitionIdOrDefault(string? id, int stringLength = 32, int partitionKeyLength = 16,
string delimiter = "_")
{
if (string.IsNullOrEmpty(id))
{
return null;
}
var parts = id.Split([delimiter], StringSplitOptions.RemoveEmptyEntries);
if (parts.Length < 2)
{
return null;
}
if (parts[1].Length < stringLength + partitionKeyLength)
{
return null;
}
// get last partitionKeyLength characters from the last part as the partition key
return parts[1][^partitionKeyLength..];
}
}