Normalize transcripts and expand speech regressions
This commit is contained in:
@@ -38,13 +38,25 @@ public sealed class JiboInteractionService(
|
||||
@"\b(?:volume|loudness)\s+(?:2|two|to)\s+(?<value>10|\d|one|two|three|four|five|six|seven|eight|nine|ten)\b",
|
||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex CommandPhrasePattern = new(
|
||||
@"[^\w\s]",
|
||||
RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex CommandWhitespacePattern = new(
|
||||
@"\s+",
|
||||
RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
||||
private static readonly string[] CommandLeadPhrases =
|
||||
[
|
||||
"hey jibo",
|
||||
"hello jibo",
|
||||
"hi jibo",
|
||||
"jibo",
|
||||
"o",
|
||||
"oh",
|
||||
"so",
|
||||
"well",
|
||||
"um",
|
||||
"uh",
|
||||
"hmm",
|
||||
"erm",
|
||||
"ah",
|
||||
"please",
|
||||
"ok jibo",
|
||||
"okay jibo"
|
||||
];
|
||||
|
||||
private static readonly Regex AlarmDeletePattern = new(
|
||||
@"\b(?:cancel|delete|remove|stop|turn\s+off)\s+(?:the\s+)?(?:alarm|along|elo)\b",
|
||||
@@ -3543,11 +3555,15 @@ public sealed class JiboInteractionService(
|
||||
return normalized is
|
||||
"what is the date" or
|
||||
"what s the date" or
|
||||
"what's the date" or
|
||||
"what date is it" or
|
||||
"today s date" or
|
||||
"today date" or
|
||||
"what's today's date" or
|
||||
"what is today s date" or
|
||||
"what s today s date" or
|
||||
"what's today s date" or
|
||||
"what's todays date" or
|
||||
"what is todays date" or
|
||||
"what s todays date";
|
||||
}
|
||||
@@ -4311,11 +4327,13 @@ public sealed class JiboInteractionService(
|
||||
{
|
||||
var normalized = NormalizeCommandPhrase(loweredTranscript);
|
||||
return normalized.StartsWith("what is my favorite", StringComparison.Ordinal) ||
|
||||
normalized.StartsWith("what s my favorite", StringComparison.Ordinal) ||
|
||||
normalized.StartsWith("what is my favourite", StringComparison.Ordinal) ||
|
||||
normalized.StartsWith("what s my favourite", StringComparison.Ordinal) ||
|
||||
normalized.StartsWith("do you remember my favorite", StringComparison.Ordinal) ||
|
||||
normalized.StartsWith("do you remember my favourite", StringComparison.Ordinal);
|
||||
normalized.StartsWith("what s my favorite", StringComparison.Ordinal) ||
|
||||
normalized.StartsWith("what's my favorite", StringComparison.Ordinal) ||
|
||||
normalized.StartsWith("what is my favourite", StringComparison.Ordinal) ||
|
||||
normalized.StartsWith("what s my favourite", StringComparison.Ordinal) ||
|
||||
normalized.StartsWith("what's my favourite", StringComparison.Ordinal) ||
|
||||
normalized.StartsWith("do you remember my favorite", StringComparison.Ordinal) ||
|
||||
normalized.StartsWith("do you remember my favourite", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static string? TryExtractPreferenceLookupCategory(string transcript)
|
||||
@@ -4878,10 +4896,12 @@ public sealed class JiboInteractionService(
|
||||
|
||||
private static string NormalizeCommandPhrase(string value)
|
||||
{
|
||||
return CommandWhitespacePattern.Replace(
|
||||
CommandPhrasePattern.Replace(value.Trim().ToLowerInvariant(), " "),
|
||||
" ")
|
||||
.Trim();
|
||||
var normalized = TranscriptTextNormalizer.NormalizeLooseText(value);
|
||||
if (string.Equals(normalized, "uh huh", StringComparison.Ordinal) ||
|
||||
normalized.StartsWith("uh huh ", StringComparison.Ordinal))
|
||||
return normalized;
|
||||
|
||||
return TranscriptTextNormalizer.StripLeadingPhrases(normalized, CommandLeadPhrases);
|
||||
}
|
||||
|
||||
private static string? TryNormalizeVolumeLevel(string token)
|
||||
|
||||
@@ -41,6 +41,7 @@ internal static class PersonalReportOrchestrator
|
||||
"yeah",
|
||||
"yep",
|
||||
"yup",
|
||||
"uh huh",
|
||||
"sure",
|
||||
"ok",
|
||||
"okay",
|
||||
@@ -260,7 +261,7 @@ internal static class PersonalReportOrchestrator
|
||||
|
||||
if (toggles.WeatherEnabled)
|
||||
{
|
||||
reportSections.Add("First, your weather.");
|
||||
reportSections.Add("Weather.");
|
||||
var weatherDecision = await buildWeatherDecisionAsync(turn, "weather", cancellationToken);
|
||||
reportSections.Add(weatherDecision.ReplyText);
|
||||
if (IsWeatherErrorReply(weatherDecision.ReplyText)) serviceError = "weather";
|
||||
@@ -275,13 +276,6 @@ internal static class PersonalReportOrchestrator
|
||||
catalog.CalendarNothingReplies,
|
||||
"Looking at your calendar, I don't see anything scheduled today."),
|
||||
userName));
|
||||
reportSections.Add(
|
||||
RenderReportSkillTemplate(
|
||||
ChooseReportSkillTemplate(
|
||||
catalog.CalendarOutroReplies,
|
||||
[],
|
||||
"And that's it."),
|
||||
userName));
|
||||
}
|
||||
|
||||
if (toggles.CommuteEnabled)
|
||||
@@ -302,7 +296,7 @@ internal static class PersonalReportOrchestrator
|
||||
catalog.NewsCategoryIntroReplies,
|
||||
"Here's today's news, from the associated press."),
|
||||
userName));
|
||||
reportSections.Add(randomizer.Choose(catalog.NewsBriefings));
|
||||
reportSections.Add(ChooseShortestBriefing(catalog.NewsBriefings));
|
||||
reportSections.Add(
|
||||
RenderReportSkillTemplate(
|
||||
ChooseReportSkillTemplate(
|
||||
@@ -632,7 +626,8 @@ internal static class PersonalReportOrchestrator
|
||||
|
||||
var speakerAwareTemplate = usableTemplates.FirstOrDefault(static template =>
|
||||
template.Contains("${speaker}", StringComparison.OrdinalIgnoreCase));
|
||||
return speakerAwareTemplate ?? usableTemplates[0];
|
||||
return ChooseShortestTemplate(speakerAwareTemplate is not null ? [speakerAwareTemplate] : usableTemplates)
|
||||
?? fallback;
|
||||
}
|
||||
|
||||
private static string RenderPersonalReportTemplate(string template, string userName)
|
||||
@@ -649,13 +644,33 @@ internal static class PersonalReportOrchestrator
|
||||
IReadOnlyList<string> secondaryTemplates,
|
||||
string fallback)
|
||||
{
|
||||
var primary = primaryTemplates.FirstOrDefault(static template => !string.IsNullOrWhiteSpace(template));
|
||||
var primary = ChooseShortestTemplate(primaryTemplates);
|
||||
if (!string.IsNullOrWhiteSpace(primary)) return primary!;
|
||||
|
||||
var secondary = secondaryTemplates.FirstOrDefault(static template => !string.IsNullOrWhiteSpace(template));
|
||||
var secondary = ChooseShortestTemplate(secondaryTemplates);
|
||||
return !string.IsNullOrWhiteSpace(secondary) ? secondary! : fallback;
|
||||
}
|
||||
|
||||
private static string ChooseShortestBriefing(IReadOnlyList<string> briefings)
|
||||
{
|
||||
var selected = ChooseShortestTemplate(briefings);
|
||||
if (string.IsNullOrWhiteSpace(selected)) return string.Empty;
|
||||
|
||||
var firstSentence = selected.Split(['.', '!', '?'], 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.FirstOrDefault();
|
||||
return string.IsNullOrWhiteSpace(firstSentence) ? selected : firstSentence;
|
||||
}
|
||||
|
||||
private static string? ChooseShortestTemplate(IEnumerable<string> templates)
|
||||
{
|
||||
var selected = templates
|
||||
.Where(static template => !string.IsNullOrWhiteSpace(template))
|
||||
.OrderBy(static template => template.Length)
|
||||
.FirstOrDefault();
|
||||
|
||||
return selected;
|
||||
}
|
||||
|
||||
private static string RenderReportSkillTemplate(string template, string userName)
|
||||
{
|
||||
return template
|
||||
@@ -670,4 +685,4 @@ internal static class PersonalReportOrchestrator
|
||||
bool CalendarEnabled,
|
||||
bool CommuteEnabled,
|
||||
bool NewsEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Jibo.Cloud.Application.Services;
|
||||
|
||||
internal static partial class TranscriptTextNormalizer
|
||||
{
|
||||
private static readonly Regex PunctuationToSpaceRegex = new(
|
||||
@"[^\p{L}\p{N}\s']+",
|
||||
RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex WhitespaceRegex = new(
|
||||
@"\s+",
|
||||
RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
||||
|
||||
internal static string NormalizeLooseText(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value)) return string.Empty;
|
||||
|
||||
return WhitespaceRegex.Replace(
|
||||
PunctuationToSpaceRegex.Replace(value.Trim().ToLowerInvariant(), " "),
|
||||
" ")
|
||||
.Trim();
|
||||
}
|
||||
|
||||
internal static string StripLeadingPhrases(string value, params string[] phrases)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value) || phrases.Length == 0) return value;
|
||||
|
||||
var normalized = value;
|
||||
while (TryStripLeadingPhrase(normalized, phrases, out var trimmed))
|
||||
normalized = trimmed;
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static bool TryStripLeadingPhrase(string normalizedValue, IReadOnlyList<string> phrases, out string trimmed)
|
||||
{
|
||||
foreach (var phrase in phrases)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(phrase)) continue;
|
||||
|
||||
if (string.Equals(normalizedValue, phrase, StringComparison.Ordinal))
|
||||
{
|
||||
trimmed = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (normalizedValue.StartsWith($"{phrase} ", StringComparison.Ordinal))
|
||||
{
|
||||
trimmed = normalizedValue[(phrase.Length + 1)..].TrimStart();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
trimmed = normalizedValue;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -90,6 +90,8 @@ public sealed partial class WebSocketTurnFinalizationService(
|
||||
|
||||
private static readonly string[] TranscriptNoisePrefixes =
|
||||
[
|
||||
"o",
|
||||
"oh",
|
||||
"uh",
|
||||
"um",
|
||||
"hmm",
|
||||
@@ -1627,11 +1629,7 @@ public sealed partial class WebSocketTurnFinalizationService(
|
||||
|
||||
private static string NormalizeTranscript(string? transcript)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(transcript)) return string.Empty;
|
||||
|
||||
return TranscriptNormalizationRegex().Replace(transcript.Trim().ToLowerInvariant(), " ")
|
||||
.Replace(" ", " ", StringComparison.Ordinal)
|
||||
.Trim();
|
||||
return TranscriptTextNormalizer.NormalizeLooseText(transcript);
|
||||
}
|
||||
|
||||
private static string? ReadMessageType(TurnContext turn)
|
||||
@@ -1939,9 +1937,6 @@ public sealed partial class WebSocketTurnFinalizationService(
|
||||
};
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"[^\w\s]")]
|
||||
private static partial Regex TranscriptNormalizationRegex();
|
||||
|
||||
private enum YesNoReply
|
||||
{
|
||||
None = 0,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Text.Json;
|
||||
using System.Text;
|
||||
using Jibo.Cloud.Application.Abstractions;
|
||||
using Jibo.Cloud.Application.Services;
|
||||
using Jibo.Cloud.Infrastructure.Content;
|
||||
@@ -1748,19 +1749,16 @@ public sealed class JiboInteractionServiceTests
|
||||
|
||||
Assert.Equal("personal_report_delivered", decision.IntentName);
|
||||
Assert.Contains("Sure alex. Here it is.", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("First, your weather.", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("Weather.", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains(
|
||||
"For your weather. In Boston, U.S., it's light rain and 61 degrees Fahrenheit. Today's high is 65, and the low is 54.",
|
||||
decision.ReplyText, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("Looking at your calendar, I don't see anything scheduled today.", decision.ReplyText,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("Sorry, commute information isn't available right now.", decision.ReplyText,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("Here's today's news, from the associated press.", decision.ReplyText,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("And that's what's new in the news.", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("alex that wraps up your report for the day. Hope you have a good one.", decision.ReplyText,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("calendar", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("commute", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("news", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.True(StripMarkup(decision.ReplyText).Length < 500,
|
||||
$"Personal report speech was still too long: {StripMarkup(decision.ReplyText).Length} chars.");
|
||||
Assert.Contains("alex", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.NotNull(decision.ContextUpdates);
|
||||
Assert.Equal("idle", decision.ContextUpdates![PersonalReportStateKey]);
|
||||
Assert.Equal(true, decision.ContextUpdates[PersonalReportUserVerifiedKey]);
|
||||
@@ -3807,6 +3805,31 @@ public sealed class JiboInteractionServiceTests
|
||||
newsBriefingProvider);
|
||||
}
|
||||
|
||||
private static string StripMarkup(string text)
|
||||
{
|
||||
var builder = new StringBuilder(text.Length);
|
||||
var inTag = false;
|
||||
|
||||
foreach (var character in text)
|
||||
{
|
||||
if (character == '<')
|
||||
{
|
||||
inTag = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (character == '>')
|
||||
{
|
||||
inTag = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!inTag) builder.Append(character);
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private sealed class FirstItemRandomizer : IJiboRandomizer
|
||||
{
|
||||
public T Choose<T>(IReadOnlyList<T> items)
|
||||
@@ -3854,4 +3877,4 @@ public sealed class JiboInteractionServiceTests
|
||||
return Task.FromResult(catalog);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Text.Json;
|
||||
using System.Text;
|
||||
using Jibo.Cloud.Application.Abstractions;
|
||||
using Jibo.Cloud.Application.Services;
|
||||
using Jibo.Cloud.Domain.Models;
|
||||
@@ -1883,6 +1884,41 @@ public sealed class JiboWebSocketServiceTests
|
||||
listenPayload.RootElement.GetProperty("data").GetProperty("match").GetProperty("rule").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClientAsr_YesNoPromptFromAsrHints_StripsPunctuationMashupAndMapsYesIntent()
|
||||
{
|
||||
await _service.HandleMessageAsync(new WebSocketMessageEnvelope
|
||||
{
|
||||
HostName = "neo-hub.jibo.com",
|
||||
Path = "/listen",
|
||||
Kind = "neo-hub-listen",
|
||||
Token = "hub-yesno-punctuation-token",
|
||||
Text =
|
||||
"""{"type":"LISTEN","transID":"trans-yesno-punctuation","data":{"rules":["surprises-ota/want_to_download_now"],"asr":{"hints":["$YESNO"]}}}"""
|
||||
});
|
||||
|
||||
var replies = await _service.HandleMessageAsync(new WebSocketMessageEnvelope
|
||||
{
|
||||
HostName = "neo-hub.jibo.com",
|
||||
Path = "/listen",
|
||||
Kind = "neo-hub-listen",
|
||||
Token = "hub-yesno-punctuation-token",
|
||||
Text = """{"type":"CLIENT_ASR","transID":"trans-yesno-punctuation","data":{"text":"- Thank you. - Yes."}}"""
|
||||
});
|
||||
|
||||
Assert.Equal(3, replies.Count);
|
||||
|
||||
using var listenPayload = JsonDocument.Parse(replies[0].Text!);
|
||||
Assert.Equal("thank you yes",
|
||||
listenPayload.RootElement.GetProperty("data").GetProperty("asr").GetProperty("text").GetString());
|
||||
Assert.Equal("yes",
|
||||
listenPayload.RootElement.GetProperty("data").GetProperty("nlu").GetProperty("intent").GetString());
|
||||
Assert.Equal("surprises-ota/want_to_download_now",
|
||||
listenPayload.RootElement.GetProperty("data").GetProperty("nlu").GetProperty("rules")[0].GetString());
|
||||
Assert.Equal("surprises-ota/want_to_download_now",
|
||||
listenPayload.RootElement.GetProperty("data").GetProperty("match").GetProperty("rule").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClientAsr_SharedYesNoPrompt_StripsGlobalRulesAndStaysLocal()
|
||||
{
|
||||
@@ -2672,6 +2708,49 @@ public sealed class JiboWebSocketServiceTests
|
||||
completionPayload.RootElement.GetProperty("data").GetProperty("skill").GetProperty("id").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClientAsr_HotwordLeakageBeforeCommand_StripsNoiseAndRoutesToRadio()
|
||||
{
|
||||
await _service.HandleMessageAsync(new WebSocketMessageEnvelope
|
||||
{
|
||||
HostName = "neo-hub.jibo.com",
|
||||
Path = "/listen",
|
||||
Kind = "neo-hub-listen",
|
||||
Token = "hub-radio-noise-token",
|
||||
Text =
|
||||
"""{"type":"LISTEN","transID":"trans-radio-noise","data":{"hotphrase":true,"rules":["launch","globals/global_commands_launch"]}}"""
|
||||
});
|
||||
|
||||
var replies = await _service.HandleMessageAsync(new WebSocketMessageEnvelope
|
||||
{
|
||||
HostName = "neo-hub.jibo.com",
|
||||
Path = "/listen",
|
||||
Kind = "neo-hub-listen",
|
||||
Token = "hub-radio-noise-token",
|
||||
Text = """{"type":"CLIENT_ASR","transID":"trans-radio-noise","data":{"text":"Hey Jibo - so open the radio"}}"""
|
||||
});
|
||||
|
||||
Assert.Equal(4, replies.Count);
|
||||
Assert.Equal("LISTEN", ReadReplyType(replies[0]));
|
||||
Assert.Equal("EOS", ReadReplyType(replies[1]));
|
||||
Assert.Equal("SKILL_REDIRECT", ReadReplyType(replies[2]));
|
||||
Assert.Equal("SKILL_ACTION", ReadReplyType(replies[3]));
|
||||
|
||||
using var listenPayload = JsonDocument.Parse(replies[0].Text!);
|
||||
Assert.Equal("menu",
|
||||
listenPayload.RootElement.GetProperty("data").GetProperty("nlu").GetProperty("intent").GetString());
|
||||
Assert.Equal("@be/radio",
|
||||
listenPayload.RootElement.GetProperty("data").GetProperty("nlu").GetProperty("skill").GetString());
|
||||
Assert.Equal(0,
|
||||
listenPayload.RootElement.GetProperty("data").GetProperty("nlu").GetProperty("rules").GetArrayLength());
|
||||
|
||||
using var redirectPayload = JsonDocument.Parse(replies[2].Text!);
|
||||
Assert.Equal("@be/radio",
|
||||
redirectPayload.RootElement.GetProperty("data").GetProperty("match").GetProperty("skillID").GetString());
|
||||
Assert.True(redirectPayload.RootElement.GetProperty("data").GetProperty("match").GetProperty("launch")
|
||||
.GetBoolean());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClientAsr_PlayCountryMusic_EmitsRadioRedirectWithCountryStation()
|
||||
{
|
||||
@@ -4561,6 +4640,158 @@ public sealed class JiboWebSocketServiceTests
|
||||
Assert.Equal("idle", stateValue?.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Smoke_ClassicUserJourney_CoversGreetingMemoryPersonalityWeatherAndPersonalReport()
|
||||
{
|
||||
var customStore = new InMemoryCloudStateStore();
|
||||
var service = CreateService(
|
||||
customStore,
|
||||
new StubWeatherReportProvider(
|
||||
new WeatherReportSnapshot("Lone Jack, US", "overcast clouds", 79, 82, 78, "clouds", false)),
|
||||
new StubNewsBriefingProvider(
|
||||
new NewsBriefingSnapshot(
|
||||
[
|
||||
new NewsHeadline("Space missions are preparing for new launches"),
|
||||
new NewsHeadline("AI tools keep pushing into everyday products")
|
||||
],
|
||||
"NewsAPI")));
|
||||
|
||||
var token = "hub-smoke-journey-token";
|
||||
|
||||
var greetingReplies = await service.HandleMessageAsync(new WebSocketMessageEnvelope
|
||||
{
|
||||
HostName = "neo-hub.jibo.com",
|
||||
Path = "/listen",
|
||||
Kind = "neo-hub-listen",
|
||||
Token = token,
|
||||
Text = """{"type":"LISTEN","transID":"trans-smoke-greeting","data":{"text":"hello jibo","rules":["wake-word"]}}"""
|
||||
});
|
||||
|
||||
Assert.Equal(3, greetingReplies.Count);
|
||||
Assert.Equal("hello",
|
||||
JsonDocument.Parse(greetingReplies[0].Text!).RootElement.GetProperty("data").GetProperty("nlu")
|
||||
.GetProperty("intent").GetString());
|
||||
|
||||
var nameReplies = await service.HandleMessageAsync(new WebSocketMessageEnvelope
|
||||
{
|
||||
HostName = "neo-hub.jibo.com",
|
||||
Path = "/listen",
|
||||
Kind = "neo-hub-listen",
|
||||
Token = token,
|
||||
Text = """{"type":"CLIENT_ASR","transID":"trans-smoke-name","data":{"text":"my name is erin"}}"""
|
||||
});
|
||||
|
||||
Assert.Equal(3, nameReplies.Count);
|
||||
Assert.Equal("memory_set_name",
|
||||
JsonDocument.Parse(nameReplies[0].Text!).RootElement.GetProperty("data").GetProperty("nlu")
|
||||
.GetProperty("intent").GetString());
|
||||
|
||||
var session = customStore.FindSessionByToken(token);
|
||||
Assert.NotNull(session);
|
||||
|
||||
var memoryReplies = await service.HandleMessageAsync(new WebSocketMessageEnvelope
|
||||
{
|
||||
HostName = "neo-hub.jibo.com",
|
||||
Path = "/listen",
|
||||
Kind = "neo-hub-listen",
|
||||
Token = token,
|
||||
Text = """{"type":"CLIENT_ASR","transID":"trans-smoke-memory","data":{"text":"who am i"}}"""
|
||||
});
|
||||
|
||||
Assert.Equal(3, memoryReplies.Count);
|
||||
Assert.Equal("memory_get_name",
|
||||
JsonDocument.Parse(memoryReplies[0].Text!).RootElement.GetProperty("data").GetProperty("nlu")
|
||||
.GetProperty("intent").GetString());
|
||||
|
||||
var personalityReplies = await service.HandleMessageAsync(new WebSocketMessageEnvelope
|
||||
{
|
||||
HostName = "neo-hub.jibo.com",
|
||||
Path = "/listen",
|
||||
Kind = "neo-hub-listen",
|
||||
Token = token,
|
||||
Text = """{"type":"CLIENT_ASR","transID":"trans-smoke-color","data":{"text":"what is your favorite color"}}"""
|
||||
});
|
||||
|
||||
Assert.Equal(3, personalityReplies.Count);
|
||||
Assert.Equal("robot_favorite_color",
|
||||
JsonDocument.Parse(personalityReplies[0].Text!).RootElement.GetProperty("data").GetProperty("nlu")
|
||||
.GetProperty("intent").GetString());
|
||||
|
||||
var weatherReplies = await service.HandleMessageAsync(new WebSocketMessageEnvelope
|
||||
{
|
||||
HostName = "neo-hub.jibo.com",
|
||||
Path = "/listen",
|
||||
Kind = "neo-hub-listen",
|
||||
Token = token,
|
||||
Text = """{"type":"CLIENT_ASR","transID":"trans-smoke-weather","data":{"text":"how is the weather"}}"""
|
||||
});
|
||||
|
||||
Assert.Equal(3, weatherReplies.Count);
|
||||
Assert.Equal("weather",
|
||||
JsonDocument.Parse(weatherReplies[0].Text!).RootElement.GetProperty("data").GetProperty("nlu")
|
||||
.GetProperty("intent").GetString());
|
||||
|
||||
var reportStartReplies = await service.HandleMessageAsync(new WebSocketMessageEnvelope
|
||||
{
|
||||
HostName = "neo-hub.jibo.com",
|
||||
Path = "/listen",
|
||||
Kind = "neo-hub-listen",
|
||||
Token = token,
|
||||
Text = """{"type":"CLIENT_ASR","transID":"trans-smoke-report-start","data":{"text":"personal report"}}"""
|
||||
});
|
||||
|
||||
Assert.Equal(3, reportStartReplies.Count);
|
||||
Assert.Equal("personal_report_opt_in",
|
||||
JsonDocument.Parse(reportStartReplies[0].Text!).RootElement.GetProperty("data").GetProperty("nlu")
|
||||
.GetProperty("intent").GetString());
|
||||
|
||||
var reportVerifyReplies = await service.HandleMessageAsync(new WebSocketMessageEnvelope
|
||||
{
|
||||
HostName = "neo-hub.jibo.com",
|
||||
Path = "/listen",
|
||||
Kind = "neo-hub-listen",
|
||||
Token = token,
|
||||
Text = """{"type":"CLIENT_ASR","transID":"trans-smoke-report-verify","data":{"text":"uh huh"}}"""
|
||||
});
|
||||
|
||||
Assert.Equal(3, reportVerifyReplies.Count);
|
||||
Assert.Equal("personal_report_verify_user",
|
||||
JsonDocument.Parse(reportVerifyReplies[0].Text!).RootElement.GetProperty("data").GetProperty("nlu")
|
||||
.GetProperty("intent").GetString());
|
||||
|
||||
var reportReplies = await service.HandleMessageAsync(new WebSocketMessageEnvelope
|
||||
{
|
||||
HostName = "neo-hub.jibo.com",
|
||||
Path = "/listen",
|
||||
Kind = "neo-hub-listen",
|
||||
Token = token,
|
||||
Text = """{"type":"CLIENT_ASR","transID":"trans-smoke-report-deliver","data":{"text":"yes"}}"""
|
||||
});
|
||||
|
||||
Assert.Equal(3, reportReplies.Count);
|
||||
Assert.Equal("personal_report_delivered",
|
||||
JsonDocument.Parse(reportReplies[0].Text!).RootElement.GetProperty("data").GetProperty("nlu")
|
||||
.GetProperty("intent").GetString());
|
||||
|
||||
using var skillPayload = JsonDocument.Parse(reportReplies[2].Text!);
|
||||
var esml = skillPayload.RootElement
|
||||
.GetProperty("data")
|
||||
.GetProperty("action")
|
||||
.GetProperty("config")
|
||||
.GetProperty("jcp")
|
||||
.GetProperty("config")
|
||||
.GetProperty("play")
|
||||
.GetProperty("esml")
|
||||
.GetString();
|
||||
|
||||
Assert.NotNull(esml);
|
||||
var stripped = StripMarkup(esml!);
|
||||
Assert.Contains("weather", stripped, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("calendar", stripped, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("news", stripped, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.True(stripped.Length < 500, $"Personal report speech was still too long: {stripped.Length} chars.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClientAsrChitchatEmotionCommand_PersistsSplitRouteMetadata()
|
||||
{
|
||||
@@ -4836,6 +5067,31 @@ public sealed class JiboWebSocketServiceTests
|
||||
return payload.RootElement.GetProperty("type").GetString() ?? string.Empty;
|
||||
}
|
||||
|
||||
private static string StripMarkup(string text)
|
||||
{
|
||||
var builder = new StringBuilder(text.Length);
|
||||
var inTag = false;
|
||||
|
||||
foreach (var character in text)
|
||||
{
|
||||
if (character == '<')
|
||||
{
|
||||
inTag = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (character == '>')
|
||||
{
|
||||
inTag = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!inTag) builder.Append(character);
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private sealed class StubWeatherReportProvider(WeatherReportSnapshot snapshot) : IWeatherReportProvider
|
||||
{
|
||||
public Task<WeatherReportSnapshot?> GetReportAsync(
|
||||
|
||||
Reference in New Issue
Block a user