Add personal report state machine and turn telemetry

This commit is contained in:
Jacob Dubin
2026-05-06 14:50:37 -05:00
parent 60b8616239
commit 0ccfa5db68
7 changed files with 1098 additions and 2 deletions

View File

@@ -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()
{

View File

@@ -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()
{