diff --git a/Deepgram.Tests/UnitTests/ClientTests/AgentClientTests.cs b/Deepgram.Tests/UnitTests/ClientTests/AgentClientTests.cs index 0e2d1de..8cc4e18 100644 --- a/Deepgram.Tests/UnitTests/ClientTests/AgentClientTests.cs +++ b/Deepgram.Tests/UnitTests/ClientTests/AgentClientTests.cs @@ -644,4 +644,157 @@ public void Agent_Should_Not_Have_Tags_Property() } #endregion + + #region History Tests + + [Test] + public void HistoryResponse_Should_Have_History_AgentType() + { + // Arrange & Act + var response = new HistoryResponse(); + + // Assert + response.Type.Should().Be(AgentType.History); + } + + [Test] + public void HistoryResponse_With_Role_And_Content_Should_Serialize_Correctly() + { + // Arrange + var response = new HistoryResponse + { + Role = "user", + Content = "Hello, how are you?" + }; + + // Act + var result = response.ToString(); + + // Assert + using (new AssertionScope()) + { + result.Should().NotBeNull(); + result.Should().Contain("History"); + result.Should().Contain("user"); + result.Should().Contain("Hello, how are you?"); + + var parsed = JsonDocument.Parse(result); + parsed.RootElement.GetProperty("type").GetString().Should().Be("History"); + parsed.RootElement.GetProperty("role").GetString().Should().Be("user"); + parsed.RootElement.GetProperty("content").GetString().Should().Be("Hello, how are you?"); + } + } + + [Test] + public void HistoryResponse_With_FunctionCalls_Should_Serialize_Correctly() + { + // Arrange + var response = new HistoryResponse + { + Role = "assistant", + FunctionCalls = new List + { + new HistoryFunctionCall + { + Id = "call_123", + Name = "get_weather", + ClientSide = false, + Arguments = "location=Seattle", + Response = "temperature=55" + } + } + }; + + // Act + var result = response.ToString(); + + // Assert + using (new AssertionScope()) + { + result.Should().NotBeNull(); + result.Should().Contain("function_calls"); + result.Should().Contain("call_123"); + result.Should().Contain("get_weather"); + + var parsed = JsonDocument.Parse(result); + var functionCalls = parsed.RootElement.GetProperty("function_calls"); + functionCalls.ValueKind.Should().Be(JsonValueKind.Array); + functionCalls.GetArrayLength().Should().Be(1); + + var call = functionCalls[0]; + call.GetProperty("id").GetString().Should().Be("call_123"); + call.GetProperty("name").GetString().Should().Be("get_weather"); + call.GetProperty("client_side").GetBoolean().Should().BeFalse(); + } + } + + [Test] + public void HistoryResponse_Null_Fields_Should_Not_Serialize() + { + // Arrange + var response = new HistoryResponse(); + + // Act + var result = response.ToString(); + + // Assert + using (new AssertionScope()) + { + result.Should().NotBeNull(); + + var parsed = JsonDocument.Parse(result); + parsed.RootElement.TryGetProperty("role", out _).Should().BeFalse(); + parsed.RootElement.TryGetProperty("content", out _).Should().BeFalse(); + parsed.RootElement.TryGetProperty("function_calls", out _).Should().BeFalse(); + } + } + + [Test] + public void HistoryFunctionCall_Should_Serialize_All_Fields() + { + // Arrange & Act + var response = new HistoryResponse + { + FunctionCalls = new List + { + new HistoryFunctionCall + { + Id = "call_abc", + Name = "search", + ClientSide = true, + Arguments = "query=test", + Response = "results=none" + } + } + }; + var result = response.ToString(); + + // Assert + using (new AssertionScope()) + { + var parsed = JsonDocument.Parse(result); + var callJson = parsed.RootElement.GetProperty("function_calls")[0]; + callJson.GetProperty("id").GetString().Should().Be("call_abc"); + callJson.GetProperty("name").GetString().Should().Be("search"); + callJson.GetProperty("client_side").GetBoolean().Should().BeTrue(); + callJson.GetProperty("arguments").GetString().Should().Be("query=test"); + callJson.GetProperty("response").GetString().Should().Be("results=none"); + } + } + + [Test] + public void AgentType_Should_Include_History() + { + // Assert + Enum.IsDefined(typeof(AgentType), AgentType.History).Should().BeTrue(); + } + + [Test] + public void AgentClientTypes_History_Constant_Should_Be_History() + { + // Assert + AgentClientTypes.History.Should().Be("History"); + } + + #endregion } \ No newline at end of file diff --git a/Deepgram/Abstractions/v2/AbstractWebSocketClient.cs b/Deepgram/Abstractions/v2/AbstractWebSocketClient.cs index 1493cc8..979dbd7 100644 --- a/Deepgram/Abstractions/v2/AbstractWebSocketClient.cs +++ b/Deepgram/Abstractions/v2/AbstractWebSocketClient.cs @@ -115,7 +115,16 @@ public async Task Connect(string uri, CancellationTokenSource? cancelToken Log.Debug("Connect", $"uri: {uri}"); Log.Debug("Connect", "Connecting to Deepgram API..."); +#if NET5_0_OR_GREATER + // Use SocketsHttpHandler via HttpMessageInvoker to prevent Content-Length: 0 from + // being added to the WebSocket upgrade request, which violates WebSocket protocol + // and causes failures with strict proxies such as Azure API Management (APIM). + using var socketHandler = new System.Net.Http.SocketsHttpHandler(); + using var invoker = new System.Net.Http.HttpMessageInvoker(socketHandler); + await _clientWebSocket.ConnectAsync(myUri, invoker, cancelToken.Token).ConfigureAwait(false); +#else await _clientWebSocket.ConnectAsync(myUri, cancelToken.Token).ConfigureAwait(false); +#endif if (!IsConnected()) { diff --git a/Deepgram/Clients/Agent/v2/Websocket/Client.cs b/Deepgram/Clients/Agent/v2/Websocket/Client.cs index 0118155..417ec78 100644 --- a/Deepgram/Clients/Agent/v2/Websocket/Client.cs +++ b/Deepgram/Clients/Agent/v2/Websocket/Client.cs @@ -45,6 +45,7 @@ public Client(string? apiKey = null, IDeepgramClientOptions? options = null) : b private event EventHandler? _injectionRefusedReceived; private event EventHandler? _promptUpdatedReceived; private event EventHandler? _speakUpdatedReceived; + private event EventHandler? _historyReceived; #endregion /// @@ -383,6 +384,24 @@ public async Task Subscribe(EventHandler eventHandle return true; } + /// + /// Subscribe to a History event from the Deepgram API + /// + /// True if successful + public async Task Subscribe(EventHandler eventHandler) + { + await _mutexSubscribe.WaitAsync(); + try + { + _historyReceived += (sender, e) => eventHandler(sender, e); + } + finally + { + _mutexSubscribe.Release(); + } + return true; + } + /// /// Subscribe to an Close event from the Deepgram API /// @@ -849,6 +868,24 @@ internal override void ProcessTextMessage(WebSocketReceiveResult result, MemoryS Log.Debug("ProcessTextMessage", $"Invoking SpeakUpdatedResponse. event: {speakUpdatedResponse}"); InvokeParallel(_speakUpdatedReceived, speakUpdatedResponse); break; + case AgentType.History: + var historyResponse = data.Deserialize(); + if (_historyReceived == null) + { + Log.Debug("ProcessTextMessage", "_historyReceived has no listeners"); + Log.Verbose("ProcessTextMessage", "LEAVE"); + return; + } + if (historyResponse == null) + { + Log.Warning("ProcessTextMessage", "HistoryResponse is invalid"); + Log.Verbose("ProcessTextMessage", "LEAVE"); + return; + } + + Log.Debug("ProcessTextMessage", $"Invoking HistoryResponse. event: {historyResponse}"); + InvokeParallel(_historyReceived, historyResponse); + break; default: Log.Debug("ProcessTextMessage", "Calling base.ProcessTextMessage..."); base.ProcessTextMessage(result, ms); diff --git a/Deepgram/Clients/Interfaces/v2/IAgentWebSocketClient.cs b/Deepgram/Clients/Interfaces/v2/IAgentWebSocketClient.cs index ac576f7..c2ca863 100644 --- a/Deepgram/Clients/Interfaces/v2/IAgentWebSocketClient.cs +++ b/Deepgram/Clients/Interfaces/v2/IAgentWebSocketClient.cs @@ -123,6 +123,12 @@ public Task Subscribe(EventHandler eventHand /// /// True if successful public Task Subscribe(EventHandler eventHandler); + + /// + /// Subscribe to a History event from the Deepgram API + /// + /// True if successful + public Task Subscribe(EventHandler eventHandler); #endregion #region Send Functions diff --git a/Deepgram/Models/Agent/v2/WebSocket/AgentType.cs b/Deepgram/Models/Agent/v2/WebSocket/AgentType.cs index b415b38..ae6b2f9 100644 --- a/Deepgram/Models/Agent/v2/WebSocket/AgentType.cs +++ b/Deepgram/Models/Agent/v2/WebSocket/AgentType.cs @@ -24,6 +24,7 @@ public enum AgentType SettingsApplied, PromptUpdated, SpeakUpdated, + History, } public static class AgentClientTypes @@ -37,4 +38,5 @@ public static class AgentClientTypes public const string FunctionCallResponse = "FunctionCallResponse"; public const string KeepAlive = "KeepAlive"; public const string Close = "Close"; + public const string History = "History"; } diff --git a/Deepgram/Models/Agent/v2/WebSocket/HistoryResponse.cs b/Deepgram/Models/Agent/v2/WebSocket/HistoryResponse.cs new file mode 100644 index 0000000..6051d7e --- /dev/null +++ b/Deepgram/Models/Agent/v2/WebSocket/HistoryResponse.cs @@ -0,0 +1,56 @@ +// Copyright 2024 Deepgram .NET SDK contributors. All Rights Reserved. +// Use of this source code is governed by a MIT license that can be found in the LICENSE file. +// SPDX-License-Identifier: MIT + +namespace Deepgram.Models.Agent.v2.WebSocket; + +public record HistoryFunctionCall +{ + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("id")] + public string? Id { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("client_side")] + public bool? ClientSide { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("arguments")] + public string? Arguments { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("response")] + public string? Response { get; set; } +} + +public record HistoryResponse +{ + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("type")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public AgentType? Type { get; } = AgentType.History; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("role")] + public string? Role { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("content")] + public string? Content { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("function_calls")] + public List? FunctionCalls { get; set; } + + /// + /// Override ToString method to serialize the object + /// + public override string ToString() + { + return Regex.Unescape(JsonSerializer.Serialize(this, JsonSerializeOptions.DefaultOptions)); + } +}