diff --git a/src/Observability/Runtime/AgentSettings/AgentSettingTemplate.cs b/src/Observability/Runtime/AgentSettings/AgentSettingTemplate.cs new file mode 100644 index 00000000..39892d6c --- /dev/null +++ b/src/Observability/Runtime/AgentSettings/AgentSettingTemplate.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; + +namespace Microsoft.Agents.A365.Observability.Runtime.AgentSettings +{ + /// + /// Represents an agent setting template for a specific agent type. + /// + public class AgentSettingTemplate + { + /// + /// Gets or sets the agent type identifier. + /// + public string AgentType { get; set; } = string.Empty; + + /// + /// Gets or sets the settings as key-value pairs. + /// + public Dictionary Settings { get; set; } = new Dictionary(); + + /// + /// Gets or sets optional metadata for the template. + /// + public Dictionary? Metadata { get; set; } + } +} diff --git a/src/Observability/Runtime/AgentSettings/AgentSettings.cs b/src/Observability/Runtime/AgentSettings/AgentSettings.cs new file mode 100644 index 00000000..ca918106 --- /dev/null +++ b/src/Observability/Runtime/AgentSettings/AgentSettings.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; + +namespace Microsoft.Agents.A365.Observability.Runtime.AgentSettings +{ + /// + /// Represents agent settings for a specific agent instance. + /// + public class AgentSettings + { + /// + /// Gets or sets the agent instance identifier. + /// + public string AgentInstanceId { get; set; } = string.Empty; + + /// + /// Gets or sets the agent type identifier. + /// + public string AgentType { get; set; } = string.Empty; + + /// + /// Gets or sets the settings as key-value pairs. + /// + public Dictionary Settings { get; set; } = new Dictionary(); + + /// + /// Gets or sets optional metadata for the settings. + /// + public Dictionary? Metadata { get; set; } + } +} diff --git a/src/Observability/Runtime/AgentSettings/AgentSettingsService.cs b/src/Observability/Runtime/AgentSettings/AgentSettingsService.cs new file mode 100644 index 00000000..45a46d1e --- /dev/null +++ b/src/Observability/Runtime/AgentSettings/AgentSettingsService.cs @@ -0,0 +1,201 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Microsoft.Agents.A365.Observability.Runtime.Common; + +namespace Microsoft.Agents.A365.Observability.Runtime.AgentSettings +{ + /// + /// Service for managing agent configuration templates and instance-specific settings. + /// + public class AgentSettingsService + { + private readonly PowerPlatformApiDiscovery _apiDiscovery; + private readonly string _tenantId; + private readonly HttpClient _httpClient; + private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true + }; + + /// + /// Initializes a new instance of the class. + /// + /// The Power Platform API discovery service. + /// The tenant identifier. + /// Optional HttpClient instance. If not provided, a new instance will be created. + /// For production use, it is recommended to provide an HttpClient instance managed by an IHttpClientFactory + /// to avoid socket exhaustion issues. + public AgentSettingsService( + PowerPlatformApiDiscovery apiDiscovery, + string tenantId, + HttpClient? httpClient = null) + { + _apiDiscovery = apiDiscovery ?? throw new ArgumentNullException(nameof(apiDiscovery)); + _tenantId = tenantId ?? throw new ArgumentNullException(nameof(tenantId)); + _httpClient = httpClient ?? new HttpClient(); + } + + /// + /// Gets the agent setting template for a specific agent type. + /// + /// The agent type identifier. + /// The authentication token. + /// The agent setting template. + public async Task GetAgentSettingTemplateAsync(string agentType, string token) + { + if (string.IsNullOrEmpty(agentType)) + { + throw new ArgumentNullException(nameof(agentType)); + } + + if (string.IsNullOrEmpty(token)) + { + throw new ArgumentNullException(nameof(token)); + } + + var endpoint = _apiDiscovery.GetTenantEndpoint(_tenantId); + var url = $"https://{endpoint}/agents/v1.0/settings/templates/{Uri.EscapeDataString(agentType)}"; + + using (var request = new HttpRequestMessage(HttpMethod.Get, url)) + { + request.Headers.Add("Authorization", $"Bearer {token}"); + request.Headers.Add("Accept", "application/json"); + + var response = await _httpClient.SendAsync(request).ConfigureAwait(false); + + if (response.StatusCode == System.Net.HttpStatusCode.NotFound) + { + return null; + } + + response.EnsureSuccessStatusCode(); + + var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + return JsonSerializer.Deserialize(content, JsonOptions); + } + } + + /// + /// Sets the agent setting template for a specific agent type. + /// + /// The agent setting template to set. + /// The authentication token. + /// A task representing the asynchronous operation. + public async Task SetAgentSettingTemplateAsync(AgentSettingTemplate template, string token) + { + if (template == null) + { + throw new ArgumentNullException(nameof(template)); + } + + if (string.IsNullOrEmpty(template.AgentType)) + { + throw new ArgumentException("AgentType cannot be null or empty.", nameof(template)); + } + + if (string.IsNullOrEmpty(token)) + { + throw new ArgumentNullException(nameof(token)); + } + + var endpoint = _apiDiscovery.GetTenantEndpoint(_tenantId); + var url = $"https://{endpoint}/agents/v1.0/settings/templates/{Uri.EscapeDataString(template.AgentType)}"; + + var json = JsonSerializer.Serialize(template); + using (var request = new HttpRequestMessage(HttpMethod.Put, url)) + { + request.Headers.Add("Authorization", $"Bearer {token}"); + request.Content = new StringContent(json, Encoding.UTF8, "application/json"); + + var response = await _httpClient.SendAsync(request).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + } + } + + /// + /// Gets the agent settings for a specific agent instance. + /// + /// The agent instance identifier. + /// The authentication token. + /// The agent settings. + public async Task GetAgentSettingsAsync(string agentInstanceId, string token) + { + if (string.IsNullOrEmpty(agentInstanceId)) + { + throw new ArgumentNullException(nameof(agentInstanceId)); + } + + if (string.IsNullOrEmpty(token)) + { + throw new ArgumentNullException(nameof(token)); + } + + var endpoint = _apiDiscovery.GetTenantEndpoint(_tenantId); + var url = $"https://{endpoint}/agents/v1.0/settings/instances/{Uri.EscapeDataString(agentInstanceId)}"; + + using (var request = new HttpRequestMessage(HttpMethod.Get, url)) + { + request.Headers.Add("Authorization", $"Bearer {token}"); + request.Headers.Add("Accept", "application/json"); + + var response = await _httpClient.SendAsync(request).ConfigureAwait(false); + + if (response.StatusCode == System.Net.HttpStatusCode.NotFound) + { + return null; + } + + response.EnsureSuccessStatusCode(); + + var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + return JsonSerializer.Deserialize(content, JsonOptions); + } + } + + /// + /// Sets the agent settings for a specific agent instance. + /// + /// The agent settings to set. + /// The authentication token. + /// A task representing the asynchronous operation. + public async Task SetAgentSettingsAsync(AgentSettings settings, string token) + { + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + + if (string.IsNullOrEmpty(settings.AgentInstanceId)) + { + throw new ArgumentException("AgentInstanceId cannot be null or empty.", nameof(settings)); + } + + if (string.IsNullOrEmpty(token)) + { + throw new ArgumentNullException(nameof(token)); + } + + var endpoint = _apiDiscovery.GetTenantEndpoint(_tenantId); + var url = $"https://{endpoint}/agents/v1.0/settings/instances/{Uri.EscapeDataString(settings.AgentInstanceId)}"; + + var json = JsonSerializer.Serialize(settings); + using (var request = new HttpRequestMessage(HttpMethod.Put, url)) + { + request.Headers.Add("Authorization", $"Bearer {token}"); + request.Content = new StringContent(json, Encoding.UTF8, "application/json"); + + var response = await _httpClient.SendAsync(request).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + } + } + } +} diff --git a/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/AgentSettings/AgentSettingsServiceTests.cs b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/AgentSettings/AgentSettingsServiceTests.cs new file mode 100644 index 00000000..14e7a035 --- /dev/null +++ b/src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/AgentSettings/AgentSettingsServiceTests.cs @@ -0,0 +1,361 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Agents.A365.Observability.Runtime.AgentSettings; +using Microsoft.Agents.A365.Observability.Runtime.Common; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Moq.Protected; + +namespace Microsoft.Agents.A365.Observability.Runtime.Tests.AgentSettings.Tests +{ + [TestClass] + public class AgentSettingsServiceTests + { + private const string TestTenantId = "e3064512-cc6d-4703-be71-a2ecaecaa98a"; + private const string TestAgentType = "test-agent"; + private const string TestAgentInstanceId = "test-instance-123"; + private const string TestToken = "test-token"; + + [TestMethod] + public void Constructor_WithValidParameters_Succeeds() + { + // Arrange + var apiDiscovery = new PowerPlatformApiDiscovery("prod"); + + // Act + var service = new AgentSettingsService(apiDiscovery, TestTenantId); + + // Assert + Assert.IsNotNull(service); + } + + [TestMethod] + public void Constructor_WithNullApiDiscovery_ThrowsArgumentNullException() + { + // Act & Assert + Assert.ThrowsException(() => + new AgentSettingsService(null!, TestTenantId)); + } + + [TestMethod] + public void Constructor_WithNullTenantId_ThrowsArgumentNullException() + { + // Arrange + var apiDiscovery = new PowerPlatformApiDiscovery("prod"); + + // Act & Assert + Assert.ThrowsException(() => + new AgentSettingsService(apiDiscovery, null!)); + } + + [TestMethod] + public async Task GetAgentSettingTemplateAsync_WithValidResponse_ReturnsTemplate() + { + // Arrange + var expectedTemplate = new AgentSettingTemplate + { + AgentType = TestAgentType, + Settings = new Dictionary { ["key1"] = "value1" } + }; + + var responseContent = JsonSerializer.Serialize(expectedTemplate); + var httpClient = CreateMockHttpClient(HttpStatusCode.OK, responseContent); + var apiDiscovery = new PowerPlatformApiDiscovery("prod"); + var service = new AgentSettingsService(apiDiscovery, TestTenantId, httpClient); + + // Act + var result = await service.GetAgentSettingTemplateAsync(TestAgentType, TestToken); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(TestAgentType, result.AgentType); + Assert.IsTrue(result.Settings.ContainsKey("key1")); + } + + [TestMethod] + public async Task GetAgentSettingTemplateAsync_WithNotFoundResponse_ReturnsNull() + { + // Arrange + var httpClient = CreateMockHttpClient(HttpStatusCode.NotFound, string.Empty); + var apiDiscovery = new PowerPlatformApiDiscovery("prod"); + var service = new AgentSettingsService(apiDiscovery, TestTenantId, httpClient); + + // Act + var result = await service.GetAgentSettingTemplateAsync(TestAgentType, TestToken); + + // Assert + Assert.IsNull(result); + } + + [TestMethod] + public async Task GetAgentSettingTemplateAsync_WithNullAgentType_ThrowsArgumentNullException() + { + // Arrange + var apiDiscovery = new PowerPlatformApiDiscovery("prod"); + var service = new AgentSettingsService(apiDiscovery, TestTenantId); + + // Act & Assert + await Assert.ThrowsExceptionAsync(() => + service.GetAgentSettingTemplateAsync(null!, TestToken)); + } + + [TestMethod] + public async Task GetAgentSettingTemplateAsync_WithNullToken_ThrowsArgumentNullException() + { + // Arrange + var apiDiscovery = new PowerPlatformApiDiscovery("prod"); + var service = new AgentSettingsService(apiDiscovery, TestTenantId); + + // Act & Assert + await Assert.ThrowsExceptionAsync(() => + service.GetAgentSettingTemplateAsync(TestAgentType, null!)); + } + + [TestMethod] + public async Task SetAgentSettingTemplateAsync_WithValidTemplate_Succeeds() + { + // Arrange + var template = new AgentSettingTemplate + { + AgentType = TestAgentType, + Settings = new Dictionary { ["key1"] = "value1" } + }; + + var httpClient = CreateMockHttpClient(HttpStatusCode.OK, string.Empty); + var apiDiscovery = new PowerPlatformApiDiscovery("prod"); + var service = new AgentSettingsService(apiDiscovery, TestTenantId, httpClient); + + // Act + await service.SetAgentSettingTemplateAsync(template, TestToken); + + // Assert - no exception thrown + } + + [TestMethod] + public async Task SetAgentSettingTemplateAsync_WithNullTemplate_ThrowsArgumentNullException() + { + // Arrange + var apiDiscovery = new PowerPlatformApiDiscovery("prod"); + var service = new AgentSettingsService(apiDiscovery, TestTenantId); + + // Act & Assert + await Assert.ThrowsExceptionAsync(() => + service.SetAgentSettingTemplateAsync(null!, TestToken)); + } + + [TestMethod] + public async Task SetAgentSettingTemplateAsync_WithEmptyAgentType_ThrowsArgumentException() + { + // Arrange + var template = new AgentSettingTemplate + { + AgentType = string.Empty, + Settings = new Dictionary { ["key1"] = "value1" } + }; + + var apiDiscovery = new PowerPlatformApiDiscovery("prod"); + var service = new AgentSettingsService(apiDiscovery, TestTenantId); + + // Act & Assert + await Assert.ThrowsExceptionAsync(() => + service.SetAgentSettingTemplateAsync(template, TestToken)); + } + + [TestMethod] + public async Task GetAgentSettingsAsync_WithValidResponse_ReturnsSettings() + { + // Arrange + var expectedSettings = new Runtime.AgentSettings.AgentSettings + { + AgentInstanceId = TestAgentInstanceId, + AgentType = TestAgentType, + Settings = new Dictionary { ["key1"] = "value1" } + }; + + var responseContent = JsonSerializer.Serialize(expectedSettings); + var httpClient = CreateMockHttpClient(HttpStatusCode.OK, responseContent); + var apiDiscovery = new PowerPlatformApiDiscovery("prod"); + var service = new AgentSettingsService(apiDiscovery, TestTenantId, httpClient); + + // Act + var result = await service.GetAgentSettingsAsync(TestAgentInstanceId, TestToken); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(TestAgentInstanceId, result.AgentInstanceId); + Assert.AreEqual(TestAgentType, result.AgentType); + Assert.IsTrue(result.Settings.ContainsKey("key1")); + } + + [TestMethod] + public async Task GetAgentSettingsAsync_WithNotFoundResponse_ReturnsNull() + { + // Arrange + var httpClient = CreateMockHttpClient(HttpStatusCode.NotFound, string.Empty); + var apiDiscovery = new PowerPlatformApiDiscovery("prod"); + var service = new AgentSettingsService(apiDiscovery, TestTenantId, httpClient); + + // Act + var result = await service.GetAgentSettingsAsync(TestAgentInstanceId, TestToken); + + // Assert + Assert.IsNull(result); + } + + [TestMethod] + public async Task SetAgentSettingsAsync_WithValidSettings_Succeeds() + { + // Arrange + var settings = new Runtime.AgentSettings.AgentSettings + { + AgentInstanceId = TestAgentInstanceId, + AgentType = TestAgentType, + Settings = new Dictionary { ["key1"] = "value1" } + }; + + var httpClient = CreateMockHttpClient(HttpStatusCode.OK, string.Empty); + var apiDiscovery = new PowerPlatformApiDiscovery("prod"); + var service = new AgentSettingsService(apiDiscovery, TestTenantId, httpClient); + + // Act + await service.SetAgentSettingsAsync(settings, TestToken); + + // Assert - no exception thrown + } + + [TestMethod] + public async Task SetAgentSettingsAsync_WithNullSettings_ThrowsArgumentNullException() + { + // Arrange + var apiDiscovery = new PowerPlatformApiDiscovery("prod"); + var service = new AgentSettingsService(apiDiscovery, TestTenantId); + + // Act & Assert + await Assert.ThrowsExceptionAsync(() => + service.SetAgentSettingsAsync(null!, TestToken)); + } + + [TestMethod] + public async Task SetAgentSettingsAsync_WithEmptyAgentInstanceId_ThrowsArgumentException() + { + // Arrange + var settings = new Runtime.AgentSettings.AgentSettings + { + AgentInstanceId = string.Empty, + AgentType = TestAgentType, + Settings = new Dictionary { ["key1"] = "value1" } + }; + + var apiDiscovery = new PowerPlatformApiDiscovery("prod"); + var service = new AgentSettingsService(apiDiscovery, TestTenantId); + + // Act & Assert + await Assert.ThrowsExceptionAsync(() => + service.SetAgentSettingsAsync(settings, TestToken)); + } + + [TestMethod] + public async Task GetAgentSettingTemplateAsync_ConstructsCorrectUrl() + { + // Arrange + HttpRequestMessage? capturedRequest = null; + var mockHandler = new Mock(); + mockHandler + .Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .Callback((req, ct) => capturedRequest = req) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(JsonSerializer.Serialize(new AgentSettingTemplate + { + AgentType = TestAgentType, + Settings = new Dictionary() + })) + }); + + var httpClient = new HttpClient(mockHandler.Object); + var apiDiscovery = new PowerPlatformApiDiscovery("prod"); + var service = new AgentSettingsService(apiDiscovery, TestTenantId, httpClient); + + // Act + await service.GetAgentSettingTemplateAsync(TestAgentType, TestToken); + + // Assert + Assert.IsNotNull(capturedRequest); + var expectedEndpoint = apiDiscovery.GetTenantEndpoint(TestTenantId); + var expectedUrl = $"https://{expectedEndpoint}/agents/v1.0/settings/templates/{TestAgentType}"; + Assert.AreEqual(expectedUrl, capturedRequest.RequestUri?.ToString()); + Assert.AreEqual(HttpMethod.Get, capturedRequest.Method); + } + + [TestMethod] + public async Task GetAgentSettingsAsync_ConstructsCorrectUrl() + { + // Arrange + HttpRequestMessage? capturedRequest = null; + var mockHandler = new Mock(); + mockHandler + .Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .Callback((req, ct) => capturedRequest = req) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(JsonSerializer.Serialize(new Runtime.AgentSettings.AgentSettings + { + AgentInstanceId = TestAgentInstanceId, + AgentType = TestAgentType, + Settings = new Dictionary() + })) + }); + + var httpClient = new HttpClient(mockHandler.Object); + var apiDiscovery = new PowerPlatformApiDiscovery("prod"); + var service = new AgentSettingsService(apiDiscovery, TestTenantId, httpClient); + + // Act + await service.GetAgentSettingsAsync(TestAgentInstanceId, TestToken); + + // Assert + Assert.IsNotNull(capturedRequest); + var expectedEndpoint = apiDiscovery.GetTenantEndpoint(TestTenantId); + var expectedUrl = $"https://{expectedEndpoint}/agents/v1.0/settings/instances/{TestAgentInstanceId}"; + Assert.AreEqual(expectedUrl, capturedRequest.RequestUri?.ToString()); + Assert.AreEqual(HttpMethod.Get, capturedRequest.Method); + } + + private static HttpClient CreateMockHttpClient(HttpStatusCode statusCode, string content) + { + var mockHandler = new Mock(); + mockHandler + .Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = statusCode, + Content = new StringContent(content, Encoding.UTF8, "application/json") + }); + + return new HttpClient(mockHandler.Object); + } + } +}