Skip to content
Open
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
153 changes: 153 additions & 0 deletions Deepgram.Tests/UnitTests/ClientTests/AgentClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<HistoryFunctionCall>
{
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<HistoryFunctionCall>
{
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
}
9 changes: 9 additions & 0 deletions Deepgram/Abstractions/v2/AbstractWebSocketClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,16 @@ public async Task<bool> 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())
{
Expand Down
37 changes: 37 additions & 0 deletions Deepgram/Clients/Agent/v2/Websocket/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public Client(string? apiKey = null, IDeepgramClientOptions? options = null) : b
private event EventHandler<InjectionRefusedResponse>? _injectionRefusedReceived;
private event EventHandler<PromptUpdatedResponse>? _promptUpdatedReceived;
private event EventHandler<SpeakUpdatedResponse>? _speakUpdatedReceived;
private event EventHandler<HistoryResponse>? _historyReceived;
#endregion

/// <summary>
Expand Down Expand Up @@ -383,6 +384,24 @@ public async Task<bool> Subscribe(EventHandler<SpeakUpdatedResponse> eventHandle
return true;
}

/// <summary>
/// Subscribe to a History event from the Deepgram API
/// </summary>
/// <returns>True if successful</returns>
public async Task<bool> Subscribe(EventHandler<HistoryResponse> eventHandler)
{
await _mutexSubscribe.WaitAsync();
try
{
_historyReceived += (sender, e) => eventHandler(sender, e);
}
finally
{
_mutexSubscribe.Release();
}
return true;
}

/// <summary>
/// Subscribe to an Close event from the Deepgram API
/// </summary>
Expand Down Expand Up @@ -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<HistoryResponse>();
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);
Expand Down
6 changes: 6 additions & 0 deletions Deepgram/Clients/Interfaces/v2/IAgentWebSocketClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,12 @@ public Task<bool> Subscribe(EventHandler<AgentStartedSpeakingResponse> eventHand
/// </summary>
/// <returns>True if successful</returns>
public Task<bool> Subscribe(EventHandler<SpeakUpdatedResponse> eventHandler);

/// <summary>
/// Subscribe to a History event from the Deepgram API
/// </summary>
/// <returns>True if successful</returns>
public Task<bool> Subscribe(EventHandler<HistoryResponse> eventHandler);
#endregion

#region Send Functions
Expand Down
2 changes: 2 additions & 0 deletions Deepgram/Models/Agent/v2/WebSocket/AgentType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public enum AgentType
SettingsApplied,
PromptUpdated,
SpeakUpdated,
History,
}

public static class AgentClientTypes
Expand All @@ -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";
}
56 changes: 56 additions & 0 deletions Deepgram/Models/Agent/v2/WebSocket/HistoryResponse.cs
Original file line number Diff line number Diff line change
@@ -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<HistoryFunctionCall>? FunctionCalls { get; set; }

/// <summary>
/// Override ToString method to serialize the object
/// </summary>
public override string ToString()
{
return Regex.Unescape(JsonSerializer.Serialize(this, JsonSerializeOptions.DefaultOptions));
}
}
Loading