// 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..]; } }