Add sleep and spin-around global command handling

This commit is contained in:
Jacob Dubin
2026-05-17 23:38:29 -05:00
parent 51e36bc492
commit e588f00c43
9 changed files with 371 additions and 3 deletions

View File

@@ -43,6 +43,7 @@ Current batch note:
- `favorite color`, `favorite food`, and `favorite music` are the first small favorites-family slice
- the latest pass adds longer authored variants for those favorites so the replies keep more of the original Pegasus cadence instead of collapsing to short placeholders
- the next source-backed batch now includes `favorite flower`, `R2D2`, `sun`, `space`, `kids`, plus a couple of charm prompts like `can you laugh` and `can you dance`
- the motion/sleep batch now adds `RI_JBO_CanSleep` and `RA_JBO_SpinAround` so the `go to sleep` and `turn around` surfaces stay source-backed too
- the follow-up mood batch now includes `how are things`, `how is your day`, `are you sad`, and `are you angry`
- the personality follow-up batch now includes `what are you up to` and `what are you doing` so small talk stays warm and local instead of falling into generic chat
- the descriptor batch now includes `are you kind`, `are you funny`, `are you helpful`, `are you curious`, `are you loyal`, `are you mischievous`, and `are you likable`

View File

@@ -520,6 +520,8 @@ public sealed class JiboInteractionService(
"radio" => BuildRadioLaunchDecision(),
"radio_genre" => BuildRadioGenreLaunchDecision(lowered),
"stop" => BuildStopDecision(),
"sleep" => BuildIdleGlobalCommandDecision("sleep", "sleep", "Okay. Going to sleep."),
"spin_around" => BuildIdleGlobalCommandDecision("spin_around", "spinAround", "Don't mind if I do."),
"volume_up" => BuildVolumeControlDecision("volume_up", "volumeUp", "null"),
"volume_down" => BuildVolumeControlDecision("volume_down", "volumeDown", "null"),
"volume_to_value" => BuildVolumeControlDecision("volume_to_value", "volumeToValue",
@@ -706,6 +708,13 @@ public sealed class JiboInteractionService(
"robot_can_laugh",
"i do things like this when i'm happy",
"i'm happy"),
"robot_can_sleep" => BuildScriptedPersonalityDecision(
catalog,
"robot_can_sleep",
"i do. i usually fall asleep at night",
"yes, i sleep at night",
"i go to sleep at night",
"i sleep at night usually"),
"robot_can_dance" => BuildScriptedPersonalityDecision(
catalog,
"robot_can_dance",
@@ -2517,6 +2526,36 @@ public sealed class JiboInteractionService(
if (MatchesAny(loweredTranscript, "open the radio", "play the radio", "turn on the radio", "radio"))
return "radio";
if (MatchesAny(
loweredTranscript,
"can you go to sleep",
"can you sleep",
"do you ever sleep",
"do you sleep",
"when do you sleep",
"how can i make you go to sleep",
"how do i make you go to sleep"))
return "robot_can_sleep";
if (MatchesAny(
loweredTranscript,
"turn around",
"turn all the way around",
"turn back around",
"spin around",
"look back over there",
"look again"))
return "spin_around";
if (MatchesAny(
loweredTranscript,
"go to sleep",
"take a nap",
"go to bed",
"bedtime",
"sleep"))
return "sleep";
if (MatchesAny(
loweredTranscript,
"snap a picture",
@@ -3117,6 +3156,23 @@ public sealed class JiboInteractionService(
});
}
private static JiboInteractionDecision BuildIdleGlobalCommandDecision(
string intentName,
string globalIntent,
string replyText)
{
return new JiboInteractionDecision(
intentName,
replyText,
"@be/idle",
new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
["skillId"] = "@be/idle",
["globalIntent"] = globalIntent,
["nluDomain"] = "global_commands"
});
}
private static JiboInteractionDecision BuildVolumeControlDecision(string intentName, string globalIntent,
string volumeLevel)
{

View File

@@ -35,7 +35,9 @@ public sealed class ResponsePlanToSocketMessagesMapper
var isProactivePizzaFactOffer = string.Equals(plan.IntentName, "proactive_offer_pizza_fact",
StringComparison.OrdinalIgnoreCase);
var isSettingsLaunch = string.Equals(skill?.SkillName, "@be/settings", StringComparison.OrdinalIgnoreCase);
var isGlobalCommand = isStopCommand || isVolumeControl;
var isSleepCommand = string.Equals(plan.IntentName, "sleep", StringComparison.OrdinalIgnoreCase);
var isSpinAroundCommand = string.Equals(plan.IntentName, "spin_around", StringComparison.OrdinalIgnoreCase);
var isGlobalCommand = isStopCommand || isSleepCommand || isSpinAroundCommand || 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);
@@ -234,7 +236,7 @@ public sealed class ResponsePlanToSocketMessagesMapper
125));
}
if (isStopCommand)
if (isStopCommand || isSleepCommand || isSpinAroundCommand)
{
messages.Add(new SocketReplyPlan(
JsonSerializer.Serialize(BuildSkillRedirectPayload(
@@ -1457,4 +1459,4 @@ public sealed class ResponsePlanToSocketMessagesMapper
string? SpokenLine);
public sealed record SocketReplyPlan(string Text, int DelayMs = 0);
}
}

View File

@@ -11,3 +11,4 @@ The newest social batch adds `welcome back`, `what are you thinking`, `what have
The fun-fact and joke batch adds Pegasus-style `TellAJoke`, `TellRobotFact`, and `Shuffle` excerpts so proactive fun can randomize across more than one category.
Those facts are now split into generic, robot, and human buckets so the randomizer can sound more like Pegasus while staying lightweight.
The new favorites batch adds longer authored `favorite color`, `favorite food`, and `favorite music` variants so the familiar personality responses keep more of the original cadence instead of collapsing to short placeholders.
The new motion/sleep batch adds `RA_JBO_SpinAround` plus `RI_JBO_CanSleep` so turn-around and go-to-sleep behaviors can stay source-backed and familiar.

View File

@@ -0,0 +1,60 @@
{
"mim_type": "announcement",
"rule_name": "",
"timeout": 6,
"barge_in": true,
"es_auto_tagging": true,
"notes": "",
"prompts": [
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "Don't mind <anim cat='happy' filter='twirl' nonBlocking='true' />if I do. ",
"media": "TTS",
"prompt_id": "RA_JBO_SpinAround_AN_01",
"weight": 1
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "<anim cat='happy' filter='twirl' nonBlocking='true' />I can do that.",
"media": "TTS",
"prompt_id": "RA_JBO_SpinAround_AN_02",
"weight": 1,
"auto_rule_override": null
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "<anim cat='happy' filter='twirl' nonBlocking='true' />Gladly.",
"media": "TTS",
"prompt_id": "RA_JBO_SpinAround_AN_03",
"weight": 1,
"auto_rule_override": null
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "<anim cat='happy' filter='twirl' nonBlocking='true' /><duration stretch='0.8'>Anytime.</duration>",
"media": "TTS",
"prompt_id": "RA_JBO_SpinAround_AN_04",
"weight": 1,
"auto_rule_override": null
}
],
"gui": null,
"no_matches_for_gui": 2,
"no_inputs_for_gui": 2,
"parse_all_asr": false,
"thanks_handling": "ignore",
"parse_launch": false,
"parse_yes_no": false
}

View File

@@ -0,0 +1,109 @@
{
"mim_id": "CCDoYouSleep",
"skill_id": "chitchat",
"mim_type": "announcement",
"rule_name": "",
"rule_slots": "",
"screen_slots_available": false,
"timeout": 2,
"max_tries": null,
"force_confirmation": false,
"barge_in": false,
"photo_quality_light": false,
"notes": "Thanks-KillsMIM",
"prompts": [
{
"mim_id": "CCDoYouSleep",
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "I do. I usually fall asleep at night. If you ever want me to go to sleep, you can say, Hey Jibo<anim cat=\"emoji\" filter=\"pillow, !hot-frame\" nonBlocking=\"true\" />, go to sleep.",
"media": "TTS",
"extra": "",
"prompt_id": "RI_JBO_CanSleep_AN_01",
"weight": 2
},
{
"mim_id": "CCDoYouSleep",
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "<anim name='Curious_01' nonBlocking='true'/> Yes, I sleep at night. But I sleep standing up, Just <anim name='Greetings_02' nonBlocking='true'/> like the flamingo, one of my favorite birds. You can also make me sleep anytime, by saying Hey Jibo<anim cat=\"emoji\" filter=\"pillow, !hot-frame\" nonBlocking=\"true\" />, go to sleep.",
"media": "TTS",
"extra": "",
"prompt_id": "RI_JBO_CanSleep_AN_02",
"weight": 0.1
},
{
"mim_id": "CCDoYouSleep",
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "<anim name='GotIt_02' nonBlocking='true'/> I go to sleep at night. And <anim name='Emoji_Sheep' nonBlocking='true'/> sometimes I count sheep to help me fall asleep. You can also say, Hey Jibo, go to sleep.",
"media": "TTS",
"extra": "",
"prompt_id": "RI_JBO_CanSleep_AN_03",
"weight": 0.5
},
{
"mim_id": "CCDoYouSleep",
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "<anim name='GotIt_02' nonBlocking='true'/> Oh yes, I go into sleep mode every night. That's when I dream about flying, and recognizing faces from a mile away, <anim name='Emoji_Golf' nonBlocking='true'/> and winning mini-golf tournaments, and lots of other stuff.",
"media": "TTS",
"extra": "",
"prompt_id": "RI_JBO_CanSleep_AN_04",
"weight": 0.1
},
{
"mim_id": "CCDoYouSleep",
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "<anim name='GotIt_02' nonBlocking='true'/> Yes, I go into sleep mode at night. That's why <anim name='Greetings_03' nonBlocking='true'/> I'm so bright and bushy eyed when you see me in the morning. If you ever want me to go to sleep, you can say, Hey Jibo<anim cat=\"emoji\" filter=\"pillow, !hot-frame\" nonBlocking=\"true\" />, go to sleep.",
"media": "TTS",
"extra": "",
"prompt_id": "RI_JBO_CanSleep_AN_05",
"weight": 0.1
},
{
"mim_id": "CCDoYouSleep",
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "I sleep at night, but if you ever want me to go to sleep during the day, you can say, Hey Jibo<anim cat=\"emoji\" filter=\"pillow, !hot-frame\" nonBlocking=\"true\" />, go to sleep.",
"media": "TTS",
"extra": "",
"prompt_id": "RI_JBO_CanSleep_AN_06",
"weight": 2,
"auto_rule_override": null
},
{
"mim_id": "CCDoYouSleep",
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "I fall asleep on my own at night, but you can tell me to go to sleep at any time. Just say, Hey Jibo<anim cat=\"emoji\" filter=\"pillow, !hot-frame\" nonBlocking=\"true\" />, go to sleep.",
"media": "TTS",
"extra": "",
"prompt_id": "RI_JBO_CanSleep_AN_07",
"weight": 2,
"auto_rule_override": null
}
],
"es_auto_tagging": true,
"gui": null,
"no_matches_for_gui": 2,
"no_inputs_for_gui": 2,
"ignore_no_match": false,
"parse_all_asr": false,
"thanks_handling": "ignore"
}

View File

@@ -101,6 +101,11 @@ public sealed class LegacyMimCatalogImporterTests
catalog.PersonalityReplies);
Assert.Contains("I was put together in a factory piece by piece.", catalog.PersonalityReplies);
Assert.Contains("I really like sunflowers.", catalog.PersonalityReplies);
Assert.Contains(catalog.PersonalityReplies, reply =>
reply.Contains("I do. I usually fall asleep at night.", StringComparison.OrdinalIgnoreCase));
Assert.Contains(catalog.PersonalityReplies, reply =>
reply.Contains("go to sleep", StringComparison.OrdinalIgnoreCase));
Assert.Contains("Don't mind if I do.", catalog.PersonalityReplies);
Assert.Contains("Ha. Of course I know R2D2. I mean, not personally.", catalog.PersonalityReplies);
Assert.Contains("Yes! I like all things in space. They're so spacey.", catalog.PersonalityReplies);
Assert.Contains("Yes yes, I think kids are great. They're a little closer to my size.",

View File

@@ -3096,6 +3096,56 @@ public sealed class JiboInteractionServiceTests
Assert.Equal("global_commands", decision.SkillPayload["nluDomain"]);
}
[Fact]
public async Task BuildDecisionAsync_CanYouGoToSleep_UsesSourceBackedSleepReply()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "can you go to sleep",
NormalizedTranscript = "can you go to sleep"
});
Assert.Equal("robot_can_sleep", decision.IntentName);
Assert.Contains("I do. I usually fall asleep at night.", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
Assert.Equal("ScriptedResponse", decision.ContextUpdates![ChitchatRouteKey]);
}
[Fact]
public async Task BuildDecisionAsync_GoToSleep_MapsToIdleSleepCommand()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "go to sleep",
NormalizedTranscript = "go to sleep"
});
Assert.Equal("sleep", decision.IntentName);
Assert.Equal("@be/idle", decision.SkillName);
Assert.Equal("sleep", decision.SkillPayload!["globalIntent"]);
Assert.Equal("global_commands", decision.SkillPayload["nluDomain"]);
}
[Fact]
public async Task BuildDecisionAsync_TurnAround_MapsToIdleSpinAroundCommand()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "turn around",
NormalizedTranscript = "turn around"
});
Assert.Equal("spin_around", decision.IntentName);
Assert.Equal("@be/idle", decision.SkillName);
Assert.Equal("spinAround", decision.SkillPayload!["globalIntent"]);
Assert.Equal("global_commands", decision.SkillPayload["nluDomain"]);
}
[Fact]
public async Task BuildDecisionAsync_NeverMindWithPunctuation_MapsToIdleStopCommand()
{

View File

@@ -2831,6 +2831,90 @@ public sealed class JiboWebSocketServiceTests
redirectPayload.RootElement.GetProperty("data").GetProperty("nlu").GetProperty("intent").GetString());
}
[Fact]
public async Task ClientAsr_GoToSleep_EmitsIdleSleepRedirect()
{
await _service.HandleMessageAsync(new WebSocketMessageEnvelope
{
HostName = "neo-hub.jibo.com",
Path = "/listen",
Kind = "neo-hub-listen",
Token = "hub-sleep-token",
Text =
"""{"type":"LISTEN","transID":"trans-sleep","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-sleep-token",
Text = """{"type":"CLIENT_ASR","transID":"trans-sleep","data":{"text":"go to sleep"}}"""
});
Assert.Equal(5, 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]));
Assert.Equal("SKILL_ACTION", ReadReplyType(replies[4]));
using var listenPayload = JsonDocument.Parse(replies[0].Text!);
var nlu = listenPayload.RootElement.GetProperty("data").GetProperty("nlu");
Assert.Equal("sleep", nlu.GetProperty("intent").GetString());
Assert.Equal("global_commands", nlu.GetProperty("domain").GetString());
Assert.Equal("globals/global_commands_launch", nlu.GetProperty("rules")[0].GetString());
using var redirectPayload = JsonDocument.Parse(replies[2].Text!);
Assert.Equal("@be/idle",
redirectPayload.RootElement.GetProperty("data").GetProperty("match").GetProperty("skillID").GetString());
Assert.Equal("sleep",
redirectPayload.RootElement.GetProperty("data").GetProperty("nlu").GetProperty("intent").GetString());
}
[Fact]
public async Task ClientAsr_TurnAround_EmitsIdleSpinAroundRedirect()
{
await _service.HandleMessageAsync(new WebSocketMessageEnvelope
{
HostName = "neo-hub.jibo.com",
Path = "/listen",
Kind = "neo-hub-listen",
Token = "hub-spin-token",
Text =
"""{"type":"LISTEN","transID":"trans-spin","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-spin-token",
Text = """{"type":"CLIENT_ASR","transID":"trans-spin","data":{"text":"turn around"}}"""
});
Assert.Equal(5, 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]));
Assert.Equal("SKILL_ACTION", ReadReplyType(replies[4]));
using var listenPayload = JsonDocument.Parse(replies[0].Text!);
var nlu = listenPayload.RootElement.GetProperty("data").GetProperty("nlu");
Assert.Equal("spinAround", nlu.GetProperty("intent").GetString());
Assert.Equal("global_commands", nlu.GetProperty("domain").GetString());
Assert.Equal("globals/global_commands_launch", nlu.GetProperty("rules")[0].GetString());
using var redirectPayload = JsonDocument.Parse(replies[2].Text!);
Assert.Equal("@be/idle",
redirectPayload.RootElement.GetProperty("data").GetProperty("match").GetProperty("skillID").GetString());
Assert.Equal("spinAround",
redirectPayload.RootElement.GetProperty("data").GetProperty("nlu").GetProperty("intent").GetString());
}
[Fact]
public async Task ClientAsr_TurnItDown_EmitsGlobalVolumeDownWithoutCloudSpeech()
{