Skip to content

Commit 56703fd

Browse files
authored
Merge pull request #380 from deepgram/feat/agent-ium
feat: adds agent inject user message
2 parents c174175 + ecee058 commit 56703fd

File tree

6 files changed

+290
-2
lines changed

6 files changed

+290
-2
lines changed
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
// Copyright 2024 Deepgram .NET SDK contributors. All Rights Reserved.
2+
// Use of this source code is governed by a MIT license that can be found in the LICENSE file.
3+
// SPDX-License-Identifier: MIT
4+
5+
using Bogus;
6+
using FluentAssertions;
7+
using FluentAssertions.Execution;
8+
using NSubstitute;
9+
using System.Text.Json;
10+
using Deepgram.Models.Authenticate.v1;
11+
using Deepgram.Models.Agent.v2.WebSocket;
12+
using Deepgram.Clients.Agent.v2.WebSocket;
13+
14+
namespace Deepgram.Tests.UnitTests.ClientTests;
15+
16+
public class AgentClientTests
17+
{
18+
DeepgramWsClientOptions _options;
19+
string _apiKey;
20+
21+
[SetUp]
22+
public void Setup()
23+
{
24+
_apiKey = new Faker().Random.Guid().ToString();
25+
_options = new DeepgramWsClientOptions(_apiKey)
26+
{
27+
OnPrem = true,
28+
};
29+
}
30+
31+
[Test]
32+
public async Task SendInjectUserMessage_With_String_Should_Send_Message()
33+
{
34+
// Input and Output
35+
var content = "Hello! Can you hear me?";
36+
var agentClient = Substitute.For<Client>(_apiKey, _options);
37+
38+
// Mock the SendMessageImmediately method
39+
agentClient.When(x => x.SendMessageImmediately(Arg.Any<byte[]>(), Arg.Any<int>(), Arg.Any<CancellationTokenSource>()))
40+
.DoNotCallBase();
41+
42+
// Act
43+
await agentClient.SendInjectUserMessage(content);
44+
45+
// Assert
46+
await agentClient.Received(1).SendMessageImmediately(Arg.Any<byte[]>(), Arg.Any<int>(), Arg.Any<CancellationTokenSource>());
47+
}
48+
49+
[Test]
50+
public async Task SendInjectUserMessage_With_Schema_Should_Send_Message()
51+
{
52+
// Input and Output
53+
var schema = new InjectUserMessageSchema
54+
{
55+
Content = "Hello! Can you hear me?"
56+
};
57+
var agentClient = Substitute.For<Client>(_apiKey, _options);
58+
59+
// Mock the SendMessageImmediately method
60+
agentClient.When(x => x.SendMessageImmediately(Arg.Any<byte[]>(), Arg.Any<int>(), Arg.Any<CancellationTokenSource>()))
61+
.DoNotCallBase();
62+
63+
// Act
64+
await agentClient.SendInjectUserMessage(schema);
65+
66+
// Assert
67+
await agentClient.Received(1).SendMessageImmediately(Arg.Any<byte[]>(), Arg.Any<int>(), Arg.Any<CancellationTokenSource>());
68+
}
69+
70+
[Test]
71+
public async Task SendInjectUserMessage_With_Null_String_Should_Throw_ArgumentException()
72+
{
73+
// Input and Output
74+
string? content = null;
75+
var agentClient = new Client(_apiKey, _options);
76+
77+
// Act & Assert
78+
var exception = await agentClient.Invoking(y => y.SendInjectUserMessage(content!))
79+
.Should().ThrowAsync<ArgumentException>()
80+
.WithMessage("Content cannot be null or empty*");
81+
exception.And.ParamName.Should().Be("content");
82+
}
83+
84+
[Test]
85+
public async Task SendInjectUserMessage_With_Empty_String_Should_Throw_ArgumentException()
86+
{
87+
// Input and Output
88+
var content = "";
89+
var agentClient = new Client(_apiKey, _options);
90+
91+
// Act & Assert
92+
var exception = await agentClient.Invoking(y => y.SendInjectUserMessage(content))
93+
.Should().ThrowAsync<ArgumentException>()
94+
.WithMessage("Content cannot be null or empty*");
95+
exception.And.ParamName.Should().Be("content");
96+
}
97+
98+
[Test]
99+
public async Task SendInjectUserMessage_With_Whitespace_String_Should_Throw_ArgumentException()
100+
{
101+
// Input and Output
102+
var content = " ";
103+
var agentClient = new Client(_apiKey, _options);
104+
105+
// Act & Assert
106+
var exception = await agentClient.Invoking(y => y.SendInjectUserMessage(content))
107+
.Should().ThrowAsync<ArgumentException>()
108+
.WithMessage("Content cannot be null or empty*");
109+
exception.And.ParamName.Should().Be("content");
110+
}
111+
112+
[Test]
113+
public async Task SendInjectUserMessage_With_Null_Schema_Should_Throw_ArgumentNullException()
114+
{
115+
// Input and Output
116+
InjectUserMessageSchema? schema = null;
117+
var agentClient = new Client(_apiKey, _options);
118+
119+
// Act & Assert
120+
var exception = await agentClient.Invoking(y => y.SendInjectUserMessage(schema!))
121+
.Should().ThrowAsync<ArgumentNullException>();
122+
exception.And.ParamName.Should().Be("injectUserMessageSchema");
123+
}
124+
125+
[Test]
126+
public async Task SendInjectUserMessage_With_Schema_Null_Content_Should_Throw_ArgumentException()
127+
{
128+
// Input and Output
129+
var schema = new InjectUserMessageSchema
130+
{
131+
Content = null
132+
};
133+
var agentClient = new Client(_apiKey, _options);
134+
135+
// Act & Assert
136+
var exception = await agentClient.Invoking(y => y.SendInjectUserMessage(schema))
137+
.Should().ThrowAsync<ArgumentException>()
138+
.WithMessage("Content cannot be null or empty*");
139+
exception.And.ParamName.Should().Be("Content");
140+
}
141+
142+
[Test]
143+
public async Task SendInjectUserMessage_With_Schema_Empty_Content_Should_Throw_ArgumentException()
144+
{
145+
// Input and Output
146+
var schema = new InjectUserMessageSchema
147+
{
148+
Content = ""
149+
};
150+
var agentClient = new Client(_apiKey, _options);
151+
152+
// Act & Assert
153+
var exception = await agentClient.Invoking(y => y.SendInjectUserMessage(schema))
154+
.Should().ThrowAsync<ArgumentException>()
155+
.WithMessage("Content cannot be null or empty*");
156+
exception.And.ParamName.Should().Be("Content");
157+
}
158+
159+
[Test]
160+
public void InjectUserMessageSchema_Should_Have_Correct_Type()
161+
{
162+
// Input and Output
163+
var schema = new InjectUserMessageSchema
164+
{
165+
Content = "Test message"
166+
};
167+
168+
// Assert
169+
using (new AssertionScope())
170+
{
171+
schema.Type.Should().Be("InjectUserMessage");
172+
schema.Content.Should().Be("Test message");
173+
}
174+
}
175+
176+
[Test]
177+
public void InjectUserMessageSchema_ToString_Should_Return_Valid_Json()
178+
{
179+
// Input and Output
180+
var schema = new InjectUserMessageSchema
181+
{
182+
Content = "Hello! Can you hear me?"
183+
};
184+
185+
// Act
186+
var result = schema.ToString();
187+
188+
// Assert
189+
using (new AssertionScope())
190+
{
191+
result.Should().NotBeNull();
192+
result.Should().Contain("InjectUserMessage");
193+
result.Should().Contain("Hello! Can you hear me?");
194+
195+
// Verify it's valid JSON by parsing it
196+
var parsed = JsonDocument.Parse(result);
197+
parsed.RootElement.GetProperty("type").GetString().Should().Be("InjectUserMessage");
198+
parsed.RootElement.GetProperty("content").GetString().Should().Be("Hello! Can you hear me?");
199+
}
200+
}
201+
}

Deepgram/Clients/Agent/v2/Websocket/Client.cs

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,50 @@ public async Task SendKeepAlive()
463463
byte[] data = Encoding.ASCII.GetBytes(message.ToString());
464464
await SendMessageImmediately(data);
465465
}
466+
467+
/// <summary>
468+
/// Sends an InjectUserMessage to the agent
469+
/// </summary>
470+
/// <param name="content">The specific phrase or statement the agent should respond to</param>
471+
public async Task SendInjectUserMessage(string content)
472+
{
473+
if (string.IsNullOrWhiteSpace(content))
474+
{
475+
Log.Warning("SendInjectUserMessage", "Content cannot be null or empty");
476+
throw new ArgumentException("Content cannot be null or empty", nameof(content));
477+
}
478+
479+
var injectUserMessage = new InjectUserMessageSchema
480+
{
481+
Content = content
482+
};
483+
484+
await SendInjectUserMessage(injectUserMessage);
485+
}
486+
487+
/// <summary>
488+
/// Sends an InjectUserMessage to the agent using a schema object
489+
/// </summary>
490+
/// <param name="injectUserMessageSchema">The InjectUserMessage schema containing the message details</param>
491+
public async Task SendInjectUserMessage(InjectUserMessageSchema injectUserMessageSchema)
492+
{
493+
if (injectUserMessageSchema == null)
494+
{
495+
Log.Warning("SendInjectUserMessage", "InjectUserMessageSchema cannot be null");
496+
throw new ArgumentNullException(nameof(injectUserMessageSchema));
497+
}
498+
499+
if (string.IsNullOrWhiteSpace(injectUserMessageSchema.Content))
500+
{
501+
Log.Warning("SendInjectUserMessage", "Content cannot be null or empty");
502+
throw new ArgumentException("Content cannot be null or empty", nameof(injectUserMessageSchema.Content));
503+
}
504+
505+
Log.Debug("SendInjectUserMessage", $"Sending InjectUserMessage: {injectUserMessageSchema.Content}");
506+
507+
byte[] data = Encoding.UTF8.GetBytes(injectUserMessageSchema.ToString());
508+
await SendMessageImmediately(data);
509+
}
466510
/// <summary>
467511
/// Sends a Close message to Deepgram
468512
/// </summary>
@@ -831,7 +875,7 @@ internal override void ProcessTextMessage(WebSocketReceiveResult result, MemoryS
831875
#region Helpers
832876
/// <summary>
833877
/// Get the URI for the WebSocket connection
834-
/// </summary>
878+
/// </summary>
835879
internal static Uri GetUri(IDeepgramClientOptions options)
836880
{
837881
var baseAddress = options.BaseAddress;

Deepgram/Clients/Interfaces/v2/IAgentWebSocketClient.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,18 @@ public Task<bool> Subscribe(EventHandler<AgentStartedSpeakingResponse> eventHand
131131
/// </summary>
132132
public Task SendKeepAlive();
133133

134+
/// <summary>
135+
/// Sends an InjectUserMessage to the agent
136+
/// </summary>
137+
/// <param name="content">The specific phrase or statement the agent should respond to</param>
138+
public Task SendInjectUserMessage(string content);
139+
140+
/// <summary>
141+
/// Sends an InjectUserMessage to the agent using a schema object
142+
/// </summary>
143+
/// <param name="injectUserMessageSchema">The InjectUserMessage schema containing the message details</param>
144+
public Task SendInjectUserMessage(InjectUserMessageSchema injectUserMessageSchema);
145+
134146
/// <summary>
135147
/// Sends a Close message to Deepgram
136148
/// </summary>
@@ -176,7 +188,7 @@ public Task<bool> Subscribe(EventHandler<AgentStartedSpeakingResponse> eventHand
176188

177189
/// <summary>
178190
/// Indicates whether the WebSocket is connected
179-
/// </summary>
191+
/// </summary>
180192
/// <returns>Returns true if the WebSocket is connected</returns>
181193
public bool IsConnected();
182194
#endregion

Deepgram/Models/Agent/v2/WebSocket/AgentType.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public static class AgentClientTypes
3333
public const string UpdatePrompt = "UpdatePrompt";
3434
public const string UpdateSpeak = "UpdateSpeak";
3535
public const string InjectAgentMessage = "InjectAgentMessage";
36+
public const string InjectUserMessage = "InjectUserMessage";
3637
public const string FunctionCallResponse = "FunctionCallResponse";
3738
public const string KeepAlive = "KeepAlive";
3839
public const string Close = "Close";
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright 2024 Deepgram .NET SDK contributors. All Rights Reserved.
2+
// Use of this source code is governed by a MIT license that can be found in the LICENSE file.
3+
// SPDX-License-Identifier: MIT
4+
5+
namespace Deepgram.Models.Agent.v2.WebSocket;
6+
7+
public class InjectUserMessageSchema
8+
{
9+
/// <summary>
10+
/// InjectUserMessage event type.
11+
/// </summary>
12+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
13+
[JsonPropertyName("type")]
14+
public string? Type { get; } = AgentClientTypes.InjectUserMessage;
15+
16+
/// <summary>
17+
/// The specific phrase or statement the agent should respond to
18+
/// </summary>
19+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
20+
[JsonPropertyName("content")]
21+
public string? Content { get; set; }
22+
23+
/// <summary>
24+
/// Override ToString method to serialize the object
25+
/// </summary>
26+
public override string ToString()
27+
{
28+
return Regex.Unescape(JsonSerializer.Serialize(this, JsonSerializeOptions.DefaultOptions));
29+
}
30+
}

output_0.wav

-121 KB
Binary file not shown.

0 commit comments

Comments
 (0)