a couple more features for version 18

This commit is contained in:
Jacob Dubin
2026-04-26 20:19:16 -05:00
parent df78170aa2
commit acbba413db
9 changed files with 586 additions and 24 deletions

View File

@@ -74,6 +74,11 @@ public sealed class DemoConversationBroker(JiboInteractionService interactionSer
"word_of_the_day_guess" => false,
"radio" => false,
"radio_genre" => false,
"stop" => false,
"volume_up" => false,
"volume_down" => false,
"volume_to_value" => false,
"volume_query" => false,
"time" => false,
"date" => false,
"day" => false,

View File

@@ -51,6 +51,11 @@ public sealed class JiboInteractionService(
"cloud_version" => new JiboInteractionDecision("cloud_version", OpenJiboCloudBuildInfo.SpokenVersion),
"radio" => BuildRadioLaunchDecision(),
"radio_genre" => BuildRadioGenreLaunchDecision(lowered),
"stop" => BuildStopDecision(),
"volume_up" => BuildVolumeControlDecision("volume_up", "volumeUp", "null"),
"volume_down" => BuildVolumeControlDecision("volume_down", "volumeDown", "null"),
"volume_to_value" => BuildVolumeControlDecision("volume_to_value", "volumeToValue", ResolveVolumeLevel(lowered, clientEntities) ?? "7"),
"volume_query" => BuildSettingsVolumeDecision(),
"clock_open" => BuildClockLaunchDecision("clock_open", "clock", "askForTime", "Opening the clock."),
"clock_menu" => BuildClockLaunchDecision("clock_menu", "clock", "menu", "Opening the clock menu."),
"timer_menu" => BuildClockLaunchDecision("timer", "Opening the timer."),
@@ -309,6 +314,27 @@ public sealed class JiboInteractionService(
return "radio_genre";
}
if (TryResolveVolumeLevel(loweredTranscript) is not null ||
clientEntities.ContainsKey("volumeLevel"))
{
return "volume_to_value";
}
if (IsVolumeQueryRequest(loweredTranscript))
{
return "volume_query";
}
if (IsVolumeUpRequest(loweredTranscript))
{
return "volume_up";
}
if (IsVolumeDownRequest(loweredTranscript))
{
return "volume_down";
}
if (MatchesAny(loweredTranscript, "open the clock", "open clock", "show the clock", "show clock"))
{
return "clock_open";
@@ -346,6 +372,11 @@ public sealed class JiboInteractionService(
return "timer_delete";
}
if (IsGlobalStopRequest(loweredTranscript, clientIntent, clientEntities))
{
return "stop";
}
if (TryParseAlarmValue(loweredTranscript, isAlarmValueTurn, referenceLocalTime) is not null)
{
return "alarm_value";
@@ -535,6 +566,47 @@ public sealed class JiboInteractionService(
});
}
private static JiboInteractionDecision BuildStopDecision()
{
return new JiboInteractionDecision(
"stop",
"Stopping.",
"@be/idle",
new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
["skillId"] = "@be/idle",
["globalIntent"] = "stop",
["nluDomain"] = "global_commands"
});
}
private static JiboInteractionDecision BuildVolumeControlDecision(string intentName, string globalIntent, string volumeLevel)
{
return new JiboInteractionDecision(
intentName,
"Adjusting volume.",
"global_commands",
new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
["globalIntent"] = globalIntent,
["nluDomain"] = "global_commands",
["volumeLevel"] = volumeLevel
});
}
private static JiboInteractionDecision BuildSettingsVolumeDecision()
{
return new JiboInteractionDecision(
"volume_query",
"Opening volume controls.",
"@be/settings",
new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
["skillId"] = "@be/settings",
["localIntent"] = "volumeQuery"
});
}
private static JiboInteractionDecision BuildClockLaunchDecision(string intentName, string domain, string clockIntent, string replyText)
{
return new JiboInteractionDecision(
@@ -1120,6 +1192,144 @@ public sealed class JiboInteractionService(
loweredTranscript is "cancel" or "stop" or "never mind" or "nevermind";
}
private static bool IsGlobalStopRequest(
string loweredTranscript,
string? clientIntent,
IReadOnlyDictionary<string, string> clientEntities)
{
if (string.Equals(clientIntent, "stop", StringComparison.OrdinalIgnoreCase) &&
IsGlobalCommandsDomain(clientEntities))
{
return true;
}
return loweredTranscript is "stop" or "stop it" or "stop that" or "stop talking" or "be quiet" or "never mind" or "nevermind" or "forget it" ||
MatchesAny(loweredTranscript, "that s enough", "that's enough", "that will do", "that ll do", "that'll do", "cut it out", "cut that out");
}
private static bool IsVolumeQueryRequest(string loweredTranscript)
{
return MatchesAny(
loweredTranscript,
"volume controls",
"volume control",
"volume menu",
"volume level",
"show volume",
"show the volume",
"open volume",
"open the volume",
"what is your volume",
"what's your volume",
"how is your volume",
"how s your volume");
}
private static bool IsVolumeUpRequest(string loweredTranscript)
{
return MatchesAny(
loweredTranscript,
"turn it up",
"turn this up",
"turn that up",
"turn up the volume",
"turn the volume up",
"turn volume up",
"turn your volume up",
"increase the volume",
"increase your volume",
"raise the volume",
"raise your volume",
"make it louder",
"make that louder",
"speak louder",
"talk louder",
"be louder",
"louder");
}
private static bool IsVolumeDownRequest(string loweredTranscript)
{
return MatchesAny(
loweredTranscript,
"turn it down",
"turn this down",
"turn that down",
"turn down the volume",
"turn the volume down",
"turn volume down",
"turn your volume down",
"decrease the volume",
"decrease your volume",
"lower the volume",
"lower your volume",
"make it quieter",
"make that quieter",
"make it softer",
"speak quieter",
"talk quieter",
"be quieter",
"quieter",
"softer");
}
private static string? ResolveVolumeLevel(string loweredTranscript, IReadOnlyDictionary<string, string> clientEntities)
{
if (clientEntities.TryGetValue("volumeLevel", out var entityValue) &&
TryNormalizeVolumeLevel(entityValue) is { } structuredLevel)
{
return structuredLevel;
}
return TryResolveVolumeLevel(loweredTranscript);
}
private static string? TryResolveVolumeLevel(string loweredTranscript)
{
if (!loweredTranscript.Contains("volume", StringComparison.Ordinal) &&
!loweredTranscript.Contains("loudness", StringComparison.Ordinal))
{
return null;
}
if (MatchesAny(loweredTranscript, "max volume", "maximum volume", "volume max", "volume maximum"))
{
return "10";
}
if (MatchesAny(loweredTranscript, "min volume", "minimum volume", "volume min", "volume minimum"))
{
return "1";
}
var match = VolumeLevelPattern.Match(loweredTranscript);
if (!match.Success)
{
return null;
}
return TryNormalizeVolumeLevel(match.Groups["value"].Value);
}
private static string? TryNormalizeVolumeLevel(string token)
{
if (string.Equals(token, "null", StringComparison.OrdinalIgnoreCase))
{
return "null";
}
var parsed = ParseNumberToken(token);
return parsed is >= 1 and <= 10
? parsed.Value.ToString()
: null;
}
private static bool IsGlobalCommandsDomain(IReadOnlyDictionary<string, string> clientEntities)
{
return clientEntities.TryGetValue("domain", out var domain) &&
string.Equals(domain, "global_commands", StringComparison.OrdinalIgnoreCase);
}
private static bool IsClockTimerValueTurn(
IReadOnlyList<string> clientRules,
IReadOnlyList<string> listenRules)
@@ -1235,6 +1445,10 @@ public sealed class JiboInteractionService(
@"\b(?<compact>\d{3,4})\s*(?<ampm>a[\s\.]*m\.?|p[\s\.]*m\.?)?\b",
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled);
private static readonly Regex VolumeLevelPattern = new(
@"\b(?:volume|loudness)\s*(?:to|at|level|is)?\s*(?<value>10|\d|one|two|three|four|five|six|seven|eight|nine|ten)\b|\b(?:set|change|make|turn)\s+(?:the\s+|your\s+)?(?:volume|loudness)\s*(?:to|at)?\s*(?<value>10|\d|one|two|three|four|five|six|seven|eight|nine|ten)\b",
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled);
private static readonly (string Phrase, string Station)[] RadioGenreAliases =
[
("country music", "Country"),

View File

@@ -25,6 +25,12 @@ public sealed class ResponsePlanToSocketMessagesMapper
var isWordOfDayGuess = string.Equals(plan.IntentName, "word_of_the_day_guess", StringComparison.OrdinalIgnoreCase);
var isRadioLaunch = string.Equals(plan.IntentName, "radio", StringComparison.OrdinalIgnoreCase) ||
string.Equals(plan.IntentName, "radio_genre", StringComparison.OrdinalIgnoreCase);
var isStopCommand = string.Equals(plan.IntentName, "stop", StringComparison.OrdinalIgnoreCase);
var isVolumeControl = string.Equals(plan.IntentName, "volume_up", StringComparison.OrdinalIgnoreCase) ||
string.Equals(plan.IntentName, "volume_down", StringComparison.OrdinalIgnoreCase) ||
string.Equals(plan.IntentName, "volume_to_value", StringComparison.OrdinalIgnoreCase);
var isSettingsLaunch = string.Equals(skill?.SkillName, "@be/settings", StringComparison.OrdinalIgnoreCase);
var isGlobalCommand = isStopCommand || isVolumeControl;
var isPhotoGalleryLaunch = string.Equals(plan.IntentName, "photo_gallery", StringComparison.OrdinalIgnoreCase);
var isPhotoCreateLaunch = string.Equals(plan.IntentName, "snapshot", StringComparison.OrdinalIgnoreCase) ||
string.Equals(plan.IntentName, "photobooth", StringComparison.OrdinalIgnoreCase);
@@ -39,12 +45,19 @@ public sealed class ResponsePlanToSocketMessagesMapper
var alarmAmPm = ReadSkillPayloadString(skill, "ampm");
var radioStation = ReadSkillPayloadString(skill, "station");
var cloudSkill = ReadSkillPayloadString(skill, "cloudSkill");
var globalIntent = ReadSkillPayloadString(skill, "globalIntent");
var nluDomain = ReadSkillPayloadString(skill, "nluDomain");
var volumeLevel = ReadSkillPayloadString(skill, "volumeLevel");
var nluGuess = ReadClientEntity(turn, "guess");
var wordOfDayGuess = ResolveWordOfDayGuess(turn, transcript, nluGuess);
var outboundIntent = isWordOfDayLaunch
var outboundIntent = isGlobalCommand && !string.IsNullOrWhiteSpace(globalIntent)
? globalIntent
: isWordOfDayLaunch
? "menu"
: isRadioLaunch
? "menu"
: isSettingsLaunch && !string.IsNullOrWhiteSpace(localIntent)
? localIntent
: (isPhotoGalleryLaunch || isPhotoCreateLaunch) && !string.IsNullOrWhiteSpace(localIntent)
? localIntent
: isClockSkillLaunch && !string.IsNullOrWhiteSpace(clockIntent)
@@ -58,8 +71,12 @@ public sealed class ResponsePlanToSocketMessagesMapper
? wordOfDayGuess
: isWordOfDayLaunch
? string.Empty
: isGlobalCommand
? transcript
: isRadioLaunch
? transcript
: isSettingsLaunch
? transcript
: isPhotoGalleryLaunch || isPhotoCreateLaunch
? transcript
: isClockSkillLaunch
@@ -73,8 +90,12 @@ public sealed class ResponsePlanToSocketMessagesMapper
: transcript;
var outboundRules = isWordOfDayLaunch
? ["word-of-the-day/menu"]
: isGlobalCommand
? BuildGlobalCommandRules(rules)
: isRadioLaunch
? Array.Empty<string>()
: isSettingsLaunch
? string.Equals(messageType, "CLIENT_NLU", StringComparison.OrdinalIgnoreCase) ? rules : Array.Empty<string>()
: isPhotoGalleryLaunch || isPhotoCreateLaunch
? string.Equals(messageType, "CLIENT_NLU", StringComparison.OrdinalIgnoreCase) ? rules : Array.Empty<string>()
: isClockSkillLaunch
@@ -88,6 +109,8 @@ public sealed class ResponsePlanToSocketMessagesMapper
isYesNoTurn && isYesNoIntent,
ShouldIncludeCreateDomain(yesNoRule),
isWordOfDayLaunch,
isGlobalCommand,
volumeLevel,
isRadioLaunch,
isWordOfDayGuess,
wordOfDayGuess,
@@ -118,10 +141,12 @@ public sealed class ResponsePlanToSocketMessagesMapper
entities,
isWordOfDayLaunch ? "@be/word-of-the-day" :
isRadioLaunch ? "@be/radio" :
isSettingsLaunch ? "@be/settings" :
isPhotoGalleryLaunch ? "@be/gallery" :
isPhotoCreateLaunch ? "@be/create" :
isClockSkillLaunch ? "@be/clock" :
null),
null,
isGlobalCommand ? nluDomain ?? "global_commands" : null),
match = new
{
intent = outboundIntent,
@@ -177,6 +202,39 @@ public sealed class ResponsePlanToSocketMessagesMapper
DelayMs: 125));
}
if (isStopCommand)
{
messages.Add(new SocketReplyPlan(
JsonSerializer.Serialize(BuildSkillRedirectPayload(
transId,
"@be/idle",
outboundIntent,
outboundAsrText,
outboundRules,
entities)),
DelayMs: 75));
messages.Add(new SocketReplyPlan(
JsonSerializer.Serialize(BuildCompletionOnlySkillPayload(transId, "@be/idle")),
DelayMs: 125));
}
if (isSettingsLaunch &&
!string.Equals(messageType, "CLIENT_NLU", StringComparison.OrdinalIgnoreCase))
{
messages.Add(new SocketReplyPlan(
JsonSerializer.Serialize(BuildSkillRedirectPayload(
transId,
"@be/settings",
outboundIntent,
outboundAsrText,
outboundRules,
entities)),
DelayMs: 75));
messages.Add(new SocketReplyPlan(
JsonSerializer.Serialize(BuildCompletionOnlySkillPayload(transId, "@be/settings")),
DelayMs: 125));
}
if (isClockSkillLaunch &&
!string.Equals(messageType, "CLIENT_NLU", StringComparison.OrdinalIgnoreCase))
{
@@ -355,6 +413,8 @@ public sealed class ResponsePlanToSocketMessagesMapper
bool yesNoTurn,
bool includeCreateDomain,
bool wordOfDayLaunch,
bool globalCommand,
string? volumeLevel,
bool radioLaunch,
bool wordOfDayGuess,
string? guess,
@@ -389,6 +449,17 @@ public sealed class ResponsePlanToSocketMessagesMapper
};
}
if (globalCommand)
{
var entities = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
if (!string.IsNullOrWhiteSpace(volumeLevel))
{
entities["volumeLevel"] = volumeLevel;
}
return entities;
}
if (radioLaunch)
{
var entities = new Dictionary<string, object?>();
@@ -702,7 +773,8 @@ public sealed class ResponsePlanToSocketMessagesMapper
string outboundIntent,
IReadOnlyList<string> outboundRules,
object entities,
string? skillId)
string? skillId,
string? domain = null)
{
var payload = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
@@ -717,9 +789,21 @@ public sealed class ResponsePlanToSocketMessagesMapper
payload["skill"] = skillId;
}
if (!string.IsNullOrWhiteSpace(domain))
{
payload["domain"] = domain;
}
return payload;
}
private static IReadOnlyList<string> BuildGlobalCommandRules(IReadOnlyList<string> rules)
{
return rules.Any(static rule => string.Equals(rule, "globals/global_commands_launch", StringComparison.OrdinalIgnoreCase))
? ["globals/global_commands_launch"]
: Array.Empty<string>();
}
private static object BuildGenericFallbackSkillPayload(string transId)
{
return new

View File

@@ -504,6 +504,11 @@ public sealed class WebSocketTurnFinalizationService(
var emitSkillActions = !string.Equals(plan.IntentName, "word_of_the_day", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(plan.IntentName, "radio", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(plan.IntentName, "radio_genre", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(plan.IntentName, "stop", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(plan.IntentName, "volume_up", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(plan.IntentName, "volume_down", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(plan.IntentName, "volume_to_value", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(plan.IntentName, "volume_query", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(plan.IntentName, "time", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(plan.IntentName, "date", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(plan.IntentName, "day", StringComparison.OrdinalIgnoreCase) &&