Add personal report state machine and turn telemetry
This commit is contained in:
@@ -9,6 +9,15 @@ namespace Jibo.Cloud.Tests.WebSockets;
|
||||
|
||||
public sealed class JiboInteractionServiceTests
|
||||
{
|
||||
private const string PersonalReportStateKey = "personalReportState";
|
||||
private const string PersonalReportNoMatchCountKey = "personalReportNoMatchCount";
|
||||
private const string PersonalReportUserNameKey = "personalReportUserName";
|
||||
private const string PersonalReportUserVerifiedKey = "personalReportUserVerified";
|
||||
private const string PersonalReportWeatherEnabledKey = "personalReportWeatherEnabled";
|
||||
private const string PersonalReportCalendarEnabledKey = "personalReportCalendarEnabled";
|
||||
private const string PersonalReportCommuteEnabledKey = "personalReportCommuteEnabled";
|
||||
private const string PersonalReportNewsEnabledKey = "personalReportNewsEnabled";
|
||||
|
||||
[Fact]
|
||||
public async Task BuildDecisionAsync_Joke_UsesCatalogBackedRandomContent()
|
||||
{
|
||||
@@ -641,6 +650,138 @@ public sealed class JiboInteractionServiceTests
|
||||
Assert.Equal("RA_JBO_OrderPizza", decision.SkillPayload!["mim_id"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildDecisionAsync_PersonalReport_StartsOptInStateMachine()
|
||||
{
|
||||
var service = CreateService();
|
||||
|
||||
var decision = await service.BuildDecisionAsync(new TurnContext
|
||||
{
|
||||
RawTranscript = "personal report",
|
||||
NormalizedTranscript = "personal report"
|
||||
});
|
||||
|
||||
Assert.Equal("personal_report_opt_in", decision.IntentName);
|
||||
Assert.Equal("Would you like your personal report now?", decision.ReplyText);
|
||||
Assert.NotNull(decision.ContextUpdates);
|
||||
Assert.Equal("awaiting_opt_in", decision.ContextUpdates![PersonalReportStateKey]);
|
||||
Assert.Equal(true, decision.ContextUpdates[PersonalReportWeatherEnabledKey]);
|
||||
Assert.Equal(true, decision.ContextUpdates[PersonalReportCalendarEnabledKey]);
|
||||
Assert.Equal(true, decision.ContextUpdates[PersonalReportCommuteEnabledKey]);
|
||||
Assert.Equal(true, decision.ContextUpdates[PersonalReportNewsEnabledKey]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildDecisionAsync_PersonalReport_OptInYesWithKnownName_AsksForIdentityConfirmation()
|
||||
{
|
||||
var memoryStore = new InMemoryPersonalMemoryStore();
|
||||
memoryStore.SetName(new PersonalMemoryTenantScope("acct-a", "loop-a", "device-a"), "alex");
|
||||
var service = CreateService(memoryStore);
|
||||
|
||||
var decision = await service.BuildDecisionAsync(new TurnContext
|
||||
{
|
||||
RawTranscript = "yes",
|
||||
NormalizedTranscript = "yes",
|
||||
DeviceId = "device-a",
|
||||
Attributes = new Dictionary<string, object?>
|
||||
{
|
||||
["accountId"] = "acct-a",
|
||||
["loopId"] = "loop-a",
|
||||
[PersonalReportStateKey] = "awaiting_opt_in"
|
||||
}
|
||||
});
|
||||
|
||||
Assert.Equal("personal_report_verify_user", decision.IntentName);
|
||||
Assert.Equal("I think this is alex. Is that right?", decision.ReplyText);
|
||||
Assert.NotNull(decision.ContextUpdates);
|
||||
Assert.Equal("awaiting_identity_confirmation", decision.ContextUpdates![PersonalReportStateKey]);
|
||||
Assert.Equal("alex", decision.ContextUpdates[PersonalReportUserNameKey]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildDecisionAsync_PersonalReport_VerifiedIdentity_DeliversReportAndResetsState()
|
||||
{
|
||||
var provider = new CapturingWeatherReportProvider
|
||||
{
|
||||
Snapshot = new WeatherReportSnapshot("Boston, US", "light rain", 61, 65, 54, "rain", false)
|
||||
};
|
||||
var service = CreateService(weatherReportProvider: provider);
|
||||
|
||||
var decision = await service.BuildDecisionAsync(new TurnContext
|
||||
{
|
||||
RawTranscript = "yes",
|
||||
NormalizedTranscript = "yes",
|
||||
Attributes = new Dictionary<string, object?>
|
||||
{
|
||||
[PersonalReportStateKey] = "awaiting_identity_confirmation",
|
||||
[PersonalReportUserNameKey] = "alex"
|
||||
}
|
||||
});
|
||||
|
||||
Assert.Equal("personal_report_delivered", decision.IntentName);
|
||||
Assert.Contains("Great, alex. Here is your personal report.", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("Right now in Boston, US, it is light rain and 61 degrees Fahrenheit.", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("That is your personal report.", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.NotNull(decision.ContextUpdates);
|
||||
Assert.Equal("idle", decision.ContextUpdates![PersonalReportStateKey]);
|
||||
Assert.Equal(true, decision.ContextUpdates[PersonalReportUserVerifiedKey]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildDecisionAsync_PersonalReport_NoMatchRetriesThenDeclines()
|
||||
{
|
||||
var service = CreateService();
|
||||
|
||||
var firstDecision = await service.BuildDecisionAsync(new TurnContext
|
||||
{
|
||||
RawTranscript = "maybe",
|
||||
NormalizedTranscript = "maybe",
|
||||
Attributes = new Dictionary<string, object?>
|
||||
{
|
||||
[PersonalReportStateKey] = "awaiting_opt_in",
|
||||
[PersonalReportNoMatchCountKey] = 0
|
||||
}
|
||||
});
|
||||
|
||||
Assert.Equal("personal_report_no_match", firstDecision.IntentName);
|
||||
Assert.NotNull(firstDecision.ContextUpdates);
|
||||
Assert.Equal(1, firstDecision.ContextUpdates![PersonalReportNoMatchCountKey]);
|
||||
|
||||
var secondDecision = await service.BuildDecisionAsync(new TurnContext
|
||||
{
|
||||
RawTranscript = "maybe",
|
||||
NormalizedTranscript = "maybe",
|
||||
Attributes = new Dictionary<string, object?>
|
||||
{
|
||||
[PersonalReportStateKey] = "awaiting_opt_in",
|
||||
[PersonalReportNoMatchCountKey] = 1
|
||||
}
|
||||
});
|
||||
|
||||
Assert.Equal("personal_report_declined", secondDecision.IntentName);
|
||||
Assert.NotNull(secondDecision.ContextUpdates);
|
||||
Assert.Equal("idle", secondDecision.ContextUpdates![PersonalReportStateKey]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildDecisionAsync_PersonalReport_StartCanApplyToggleHints()
|
||||
{
|
||||
var service = CreateService();
|
||||
|
||||
var decision = await service.BuildDecisionAsync(new TurnContext
|
||||
{
|
||||
RawTranscript = "personal report without weather and no news",
|
||||
NormalizedTranscript = "personal report without weather and no news"
|
||||
});
|
||||
|
||||
Assert.Equal("personal_report_opt_in", decision.IntentName);
|
||||
Assert.NotNull(decision.ContextUpdates);
|
||||
Assert.Equal(false, decision.ContextUpdates![PersonalReportWeatherEnabledKey]);
|
||||
Assert.Equal(false, decision.ContextUpdates[PersonalReportNewsEnabledKey]);
|
||||
Assert.Equal(true, decision.ContextUpdates[PersonalReportCalendarEnabledKey]);
|
||||
Assert.Equal(true, decision.ContextUpdates[PersonalReportCommuteEnabledKey]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildDecisionAsync_WeatherQuery_WithoutProvider_UsesSpokenFallback()
|
||||
{
|
||||
|
||||
@@ -3174,6 +3174,73 @@ public sealed class JiboWebSocketServiceTests
|
||||
Assert.False(session.Metadata.ContainsKey("pendingProactivityOffer"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClientAsrPersonalReport_StateMachinePersistsAcrossTurns()
|
||||
{
|
||||
const string stateKey = "personalReportState";
|
||||
var token = _store.IssueRobotToken("personal-report-device-a");
|
||||
|
||||
var startReplies = await _service.HandleMessageAsync(new WebSocketMessageEnvelope
|
||||
{
|
||||
HostName = "neo-hub.jibo.com",
|
||||
Path = "/listen",
|
||||
Kind = "neo-hub-listen",
|
||||
Token = token,
|
||||
Text = """{"type":"CLIENT_ASR","transID":"trans-personal-report-start","data":{"text":"personal report"}}"""
|
||||
});
|
||||
|
||||
Assert.Equal(3, startReplies.Count);
|
||||
using (var startListenPayload = JsonDocument.Parse(startReplies[0].Text!))
|
||||
{
|
||||
Assert.Equal("personal_report_opt_in", startListenPayload.RootElement.GetProperty("data").GetProperty("nlu").GetProperty("intent").GetString());
|
||||
}
|
||||
|
||||
var session = _store.FindSessionByToken(token);
|
||||
Assert.NotNull(session);
|
||||
Assert.True(session.Metadata.TryGetValue(stateKey, out var stateValue));
|
||||
Assert.Equal("awaiting_opt_in", stateValue?.ToString());
|
||||
|
||||
var optInReplies = await _service.HandleMessageAsync(new WebSocketMessageEnvelope
|
||||
{
|
||||
HostName = "neo-hub.jibo.com",
|
||||
Path = "/listen",
|
||||
Kind = "neo-hub-listen",
|
||||
Token = token,
|
||||
Text = """{"type":"CLIENT_ASR","transID":"trans-personal-report-optin","data":{"text":"yes"}}"""
|
||||
});
|
||||
|
||||
Assert.Equal(3, optInReplies.Count);
|
||||
using (var optInListenPayload = JsonDocument.Parse(optInReplies[0].Text!))
|
||||
{
|
||||
Assert.Equal("personal_report_request_name", optInListenPayload.RootElement.GetProperty("data").GetProperty("nlu").GetProperty("intent").GetString());
|
||||
}
|
||||
|
||||
session = _store.FindSessionByToken(token);
|
||||
Assert.NotNull(session);
|
||||
Assert.True(session.Metadata.TryGetValue(stateKey, out stateValue));
|
||||
Assert.Equal("awaiting_identity_name", stateValue?.ToString());
|
||||
|
||||
var identifyReplies = await _service.HandleMessageAsync(new WebSocketMessageEnvelope
|
||||
{
|
||||
HostName = "neo-hub.jibo.com",
|
||||
Path = "/listen",
|
||||
Kind = "neo-hub-listen",
|
||||
Token = token,
|
||||
Text = """{"type":"CLIENT_ASR","transID":"trans-personal-report-name","data":{"text":"my name is alex"}}"""
|
||||
});
|
||||
|
||||
Assert.Equal(3, identifyReplies.Count);
|
||||
using (var identifyListenPayload = JsonDocument.Parse(identifyReplies[0].Text!))
|
||||
{
|
||||
Assert.Equal("personal_report_delivered", identifyListenPayload.RootElement.GetProperty("data").GetProperty("nlu").GetProperty("intent").GetString());
|
||||
}
|
||||
|
||||
session = _store.FindSessionByToken(token);
|
||||
Assert.NotNull(session);
|
||||
Assert.True(session.Metadata.TryGetValue(stateKey, out stateValue));
|
||||
Assert.Equal("idle", stateValue?.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FollowUpTurn_UsesNewTurnStateWithoutLeakingBufferedAudio()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user