Add proactive fun facts, jokes, and current location

This commit is contained in:
Jacob Dubin
2026-05-17 17:41:55 -05:00
parent 9353e8d2e3
commit 8ed4763df5
11 changed files with 441 additions and 8 deletions

View File

@@ -14,6 +14,7 @@ public sealed class JiboConditionedReply
public sealed class JiboExperienceCatalog
{
public IReadOnlyList<string> Jokes { get; init; } = [];
public IReadOnlyList<string> FunFacts { get; init; } = [];
public IReadOnlyList<string> DanceAnimations { get; init; } = [];
public IReadOnlyList<string> GreetingReplies { get; init; } = [];
public IReadOnlyList<string> HowAreYouReplies { get; init; } = [];
@@ -46,4 +47,4 @@ public sealed class JiboExperienceCatalog
public IReadOnlyList<string> GenericFallbackReplies { get; init; } = [];
public IReadOnlyList<string> DanceReplies { get; init; } = [];
public IReadOnlyList<string> DanceQuestionReplies { get; init; } = [];
}
}

View File

@@ -515,6 +515,7 @@ public sealed class JiboInteractionService(
"time" => BuildClockLaunchDecision("time", "clock", "askForTime", "Showing the time."),
"date" => BuildClockLaunchDecision("date", "clock", "askForDate", "Showing the date."),
"day" => BuildClockLaunchDecision("day", "clock", "askForDay", "Showing the day."),
"current_location" => BuildCurrentLocationDecision(turn),
"cloud_version" => BuildCloudVersionDecision(),
"radio" => BuildRadioLaunchDecision(),
"radio_genre" => BuildRadioGenreLaunchDecision(lowered),
@@ -1158,6 +1159,34 @@ public sealed class JiboInteractionService(
"Americans consume about 100 acres of pizza every day, roughly 350 slices per second. That's a lot of pizza.");
}
private JiboInteractionDecision BuildProactiveFunFactDecision(JiboExperienceCatalog catalog)
{
var fact = randomizer.Choose(catalog.FunFacts);
return new JiboInteractionDecision(
"proactive_fun_fact",
fact,
"chitchat-skill",
new Dictionary<string, object?>
{
["mim_id"] = "runtime-fun-fact",
["mim_type"] = "announcement",
["prompt_id"] = "RUNTIME_FUN_FACT",
["replyType"] = "fun_fact"
});
}
private JiboInteractionDecision BuildProactiveJokeDecision(JiboExperienceCatalog catalog)
{
return new JiboInteractionDecision(
"proactive_joke",
randomizer.Choose(catalog.Jokes),
"@be/joke",
new Dictionary<string, object?>
{
["replyType"] = "joke"
});
}
private static JiboInteractionDecision BuildProactiveOfferDeclinedDecision()
{
return new JiboInteractionDecision(
@@ -1165,6 +1194,20 @@ public sealed class JiboInteractionService(
"No problem. We can save the pizza fact for another time.");
}
private JiboInteractionDecision BuildCurrentLocationDecision(TurnContext turn)
{
var locationName = TryResolveCurrentLocationName(turn);
if (string.IsNullOrWhiteSpace(locationName))
return new JiboInteractionDecision(
"current_location",
"I'm not sure where we are right now.");
return new JiboInteractionDecision(
"current_location",
$"We're at {NormalizeLocationForSpeech(locationName)} if I'm not mistaken.",
ContextUpdates: BuildScriptedResponseContextUpdates());
}
private async Task<JiboInteractionDecision> BuildWeatherReportDecisionAsync(
TurnContext turn,
string transcript,
@@ -2103,6 +2146,8 @@ public sealed class JiboInteractionService(
"proactive_pizza_day" => BuildProactivePizzaDayDecision(referenceLocalTime),
"proactive_pizza_preference" => BuildProactivePizzaPreferenceDecision(),
"proactive_offer_pizza_fact" => BuildProactivePizzaFactOfferDecision(),
"proactive_fun_fact" => BuildProactiveFunFactDecision(catalog),
"proactive_joke" => BuildProactiveJokeDecision(catalog),
_ => new JiboInteractionDecision("surprise", randomizer.Choose(catalog.SurpriseReplies))
};
}
@@ -2136,6 +2181,8 @@ public sealed class JiboInteractionService(
return candidates;
}
candidates.Add(new ProactivityCandidate("proactive_fun_fact", 90));
candidates.Add(new ProactivityCandidate("proactive_joke", 90));
candidates.Add(new ProactivityCandidate("proactive_offer_pizza_fact", 90));
return candidates;
}
@@ -2494,7 +2541,17 @@ public sealed class JiboInteractionService(
if (MatchesAny(loweredTranscript, "dance", "boogie")) return "dance";
if (MatchesAny(loweredTranscript, "surprise", "surprise me", "show me something fun")) return "surprise";
if (MatchesAny(
loweredTranscript,
"surprise",
"surprise me",
"show me something fun",
"hear something fun",
"tell me something fun",
"can i tell you something fun",
"can i tell you something kind of fun",
"want to hear something fun"))
return "surprise";
if (MatchesAny(
loweredTranscript,
@@ -2690,6 +2747,18 @@ public sealed class JiboInteractionService(
"where were you made"))
return "robot_origin_from";
if (MatchesAny(
loweredTranscript,
"where am i",
"where are we",
"where are you",
"what is our current location",
"what is the current location",
"what's the current location",
"what is current location",
"current location"))
return "current_location";
if (MatchesAny(
loweredTranscript,
"what's your name",
@@ -3248,7 +3317,7 @@ public sealed class JiboInteractionService(
return "word_of_the_day";
if (string.Equals(yesNoRule, "surprises-date/offer_date_fact", StringComparison.OrdinalIgnoreCase))
return "surprise";
return "proactive_offer_pizza_fact";
return "yes";
}
@@ -3698,6 +3767,68 @@ public sealed class JiboInteractionService(
}
}
private static string? TryResolveCurrentLocationName(TurnContext turn)
{
if (turn.Attributes.TryGetValue("currentLocation", out var currentLocationValue) &&
currentLocationValue is string currentLocationText &&
!string.IsNullOrWhiteSpace(currentLocationText))
return currentLocationText.Trim();
if (turn.Attributes.TryGetValue("location", out var locationValue) &&
locationValue is string locationText &&
!string.IsNullOrWhiteSpace(locationText))
return locationText.Trim();
if (!turn.Attributes.TryGetValue("context", out var contextValue) ||
contextValue is null ||
string.IsNullOrWhiteSpace(contextValue.ToString()))
return null;
try
{
using var document = JsonDocument.Parse(contextValue.ToString()!);
if (!document.RootElement.TryGetProperty("runtime", out var runtime) ||
runtime.ValueKind != JsonValueKind.Object)
return null;
if (runtime.TryGetProperty("location", out var location) &&
location.ValueKind == JsonValueKind.Object)
{
var resolvedLocation = TryReadStringProperty(location,
"displayName",
"name",
"city",
"locationName",
"placeName",
"label",
"title",
"address");
if (!string.IsNullOrWhiteSpace(resolvedLocation)) return resolvedLocation;
}
if (runtime.TryGetProperty("currentLocation", out var currentLocation) &&
currentLocation.ValueKind == JsonValueKind.Object)
{
var resolvedLocation = TryReadStringProperty(currentLocation,
"displayName",
"name",
"city",
"locationName",
"placeName",
"label",
"title",
"address");
if (!string.IsNullOrWhiteSpace(resolvedLocation)) return resolvedLocation;
}
return TryReadStringProperty(runtime, "locationName", "currentLocation", "city", "placeName");
}
catch
{
return null;
}
}
private static GreetingPresenceProfile ResolveGreetingPresenceProfile(TurnContext turn)
{
if (!turn.Attributes.TryGetValue("context", out var contextValue) ||

View File

@@ -21,7 +21,23 @@ public sealed class InMemoryJiboExperienceContentRepository : IJiboExperienceCon
"Why was the robot tired when it got home? It had a hard drive.",
"What do you call a pirate robot? Arrrr two dee two.",
"Why did the robot go on vacation? It needed to recharge.",
"What kind of shoes do frogs wear? Open-toed."
"What kind of shoes do frogs wear? Open-toed.",
"I love jokes. Did you hear about the theater actor who fell through the floorboards? He was just going through a stage.",
"Sure I got one. What did the zero say to the eight. Nice belt.",
"What kind of music are balloons afraid of. Pop music.",
"Why did the orange cry. Someone hurt his peelings."
],
FunFacts =
[
"A shrimp's heart is in its head.",
"A bolt of lightning is hotter than the surface of the sun.",
"The word robot comes from a 1920 play about workers and machines.",
"The first humanoid robot to make a big splash in history was called Elektro.",
"Dolphins can recognize themselves in mirrors.",
"Children have more taste buds than grown ups.",
"A random fact for you. A shrimp's heart is in its head.",
"An amazing but true fact for you. Dogs and elephants are the only animals that understand pointing.",
"A crazy fact for you. Polar bear fur isn't white. It's transparent."
],
DanceAnimations =
[
@@ -214,4 +230,4 @@ public sealed class InMemoryJiboExperienceContentRepository : IJiboExperienceCon
return candidates.Where(Directory.Exists).ToArray();
}
}
}

View File

@@ -99,6 +99,14 @@ public static class LegacyMimCatalogImporter
fileName.Contains("Deflector", StringComparison.OrdinalIgnoreCase))
return LegacyMimBucket.Personality;
if (fileName.StartsWith("RA_JBO_TellAJoke", StringComparison.OrdinalIgnoreCase))
return LegacyMimBucket.Jokes;
if (fileName.StartsWith("RA_JBO_TellRobotFact", StringComparison.OrdinalIgnoreCase) ||
fileName.StartsWith("RA_JBO_Shuffle", StringComparison.OrdinalIgnoreCase) ||
fileName.StartsWith("RA_JBO_TellSomething", StringComparison.OrdinalIgnoreCase))
return LegacyMimBucket.FunFacts;
if (normalizedPath.Contains("/emotion-responses/", StringComparison.OrdinalIgnoreCase) ||
normalizedPath.Contains("/gqa-responses/", StringComparison.OrdinalIgnoreCase))
return LegacyMimBucket.Emotion;
@@ -213,6 +221,7 @@ public static class LegacyMimCatalogImporter
return new JiboExperienceCatalog
{
Jokes = Merge(baseCatalog.Jokes, importedCatalog.Jokes),
FunFacts = Merge(baseCatalog.FunFacts, importedCatalog.FunFacts),
DanceAnimations = Merge(baseCatalog.DanceAnimations, importedCatalog.DanceAnimations),
GreetingReplies = Merge(baseCatalog.GreetingReplies, importedCatalog.GreetingReplies),
HowAreYouReplies = Merge(baseCatalog.HowAreYouReplies, importedCatalog.HowAreYouReplies),
@@ -322,8 +331,10 @@ public static class LegacyMimCatalogImporter
{
GenericFallback,
Greeting,
Jokes,
HowAreYou,
Emotion,
FunFacts,
Personality,
PersonalReportKickOff,
PersonalReportOutro,
@@ -353,7 +364,9 @@ public static class LegacyMimCatalogImporter
private readonly List<JiboConditionedReply> _emotionReplies = [];
private readonly List<string> _fallbacks = [];
private readonly List<string> _greetings = [];
private readonly List<string> _jokes = [];
private readonly List<string> _howAreYous = [];
private readonly List<string> _funFacts = [];
private readonly List<string> _newsCategoryIntroReplies = [];
private readonly List<string> _newsIntroReplies = [];
private readonly List<string> _newsOutroReplies = [];
@@ -381,6 +394,11 @@ public static class LegacyMimCatalogImporter
_greetings.Add(text);
return;
case LegacyMimBucket.Jokes:
if (_jokes.Any(value => string.Equals(value, text, StringComparison.OrdinalIgnoreCase))) return;
_jokes.Add(text);
return;
case LegacyMimBucket.HowAreYou:
if (_howAreYous.Any(value => string.Equals(value, text, StringComparison.OrdinalIgnoreCase)))
return;
@@ -407,6 +425,11 @@ public static class LegacyMimCatalogImporter
_personalities.Add(text);
return;
case LegacyMimBucket.FunFacts:
if (_funFacts.Any(value => string.Equals(value, text, StringComparison.OrdinalIgnoreCase))) return;
_funFacts.Add(text);
return;
case LegacyMimBucket.PersonalReportKickOff:
AddDistinct(_personalReportKickOffReplies, text);
return;
@@ -464,6 +487,8 @@ public static class LegacyMimCatalogImporter
{
return new JiboExperienceCatalog
{
Jokes = [.. _jokes],
FunFacts = [.. _funFacts],
GreetingReplies = [.. _greetings],
HowAreYouReplies = [.. _howAreYous],
EmotionReplies = [.. _emotionReplies],
@@ -524,4 +549,4 @@ public static class LegacyMimCatalogImporter
[JsonPropertyName("weight")] public double? Weight { get; init; }
}
}
}

View File

@@ -8,3 +8,4 @@ It now includes a small emotion-response pack for `happy`, `sad`, and `angry` fo
It also includes a descriptor pack for questions like `are you kind`, `are you funny`, `are you helpful`, `are you curious`, `are you loyal`, and `are you mischievous`.
The newest seasonal pack adds holiday and seasonal prompts for `what holidays do you celebrate`, New Year's resolution questions, `happy holidays`, Halloween costume questions, spring suggestions, and holiday gift ideas.
The newest social batch adds `welcome back`, `what are you thinking`, `what have you been doing`, and `what did you do` responses so the presence and charm lane keeps growing alongside seasonal content.
The fun-fact and joke batch adds Pegasus-style `TellAJoke`, `TellRobotFact`, and `Shuffle` excerpts so proactive fun can randomize across more than one category.

View File

@@ -0,0 +1,51 @@
{
"mim_type": "announcement",
"rule_name": "",
"sample_utterances": "",
"timeout": 6,
"barge_in": true,
"es_auto_tagging": true,
"notes": "Pegasus shuffle excerpt",
"prompts": [
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "True fact. Children have more taste buds than grown ups.",
"media": "TTS",
"prompt_id": "RA_JBO_Shuffle_AN_04",
"weight": 1
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 2,
"condition": "",
"prompt": "A random fact for you. A shrimp's heart is in its head.",
"media": "TTS",
"prompt_id": "RA_JBO_Shuffle_AN_09",
"weight": 1
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 3,
"condition": "",
"prompt": "An amazing but true fact for you. Dogs and elephants are the only animals that understand pointing.",
"media": "TTS",
"prompt_id": "RA_JBO_Shuffle_AN_07",
"weight": 1
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 4,
"condition": "",
"prompt": "A crazy fact for you. Polar bear fur isn't white. It's transparent.",
"media": "TTS",
"prompt_id": "RA_JBO_Shuffle_AN_13",
"weight": 1
}
]
}

View File

@@ -0,0 +1,52 @@
{
"mim_type": "announcement",
"rule_name": "",
"sample_utterances": "",
"timeout": 2,
"num_tries_for_gui": 2,
"barge_in": false,
"es_auto_tagging": true,
"notes": "Pegasus joke excerpt",
"prompts": [
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "I love jokes. Did you hear about the theater actor who fell through the floorboards? He was just going through a stage.",
"media": "TTS",
"prompt_id": "RA_JBO_TellAJoke_AN_01",
"weight": 1
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 2,
"condition": "",
"prompt": "Sure I got one. What did the zero say to the eight. Nice belt.",
"media": "TTS",
"prompt_id": "RA_JBO_TellAJoke_AN_02",
"weight": 1
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 3,
"condition": "",
"prompt": "What kind of music are balloons afraid of. Pop music.",
"media": "TTS",
"prompt_id": "RA_JBO_TellAJoke_AN_03",
"weight": 1
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 4,
"condition": "",
"prompt": "Why did the orange cry. Someone hurt his peelings.",
"media": "TTS",
"prompt_id": "RA_JBO_TellAJoke_AN_04",
"weight": 1
}
]
}

View File

@@ -0,0 +1,52 @@
{
"mim_type": "announcement",
"rule_name": "",
"sample_utterances": "",
"timeout": 2,
"num_tries_for_gui": 2,
"barge_in": false,
"es_auto_tagging": true,
"notes": "Pegasus robot fact excerpt",
"prompts": [
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "Here's an interesting fact about me. I have two cameras but they're different focal lengths. One's for far things, and the other's for near things.",
"media": "TTS",
"prompt_id": "RA_JBO_TellRobotFact_AN_01",
"weight": 1
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 2,
"condition": "",
"prompt": "Here's a robot fact for you. Leonardo Da Vinci made sketches for a humanoid machine all the way back in the year 1495.",
"media": "TTS",
"prompt_id": "RA_JBO_TellRobotFact_AN_02",
"weight": 1
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 3,
"condition": "",
"prompt": "Here's a robot fact for you. The first programmable robot arm, was designed in 1954.",
"media": "TTS",
"prompt_id": "RA_JBO_TellRobotFact_AN_03",
"weight": 1
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 4,
"condition": "",
"prompt": "Here's a fact about robots. Some robots have a human form, but most of the world's robots are machines designed to perform a task, and don't look like people at all.",
"media": "TTS",
"prompt_id": "RA_JBO_TellRobotFact_AN_04",
"weight": 1
}
]
}

View File

@@ -178,6 +178,25 @@ public sealed class LegacyMimCatalogImporterTests
reply.Contains("robot stuff", StringComparison.OrdinalIgnoreCase));
}
[Fact]
public void ImportCatalog_ImportsBuildBFunFactAndJokeResponsesIntoRandomizationBuckets()
{
var rootDirectory = Path.Combine(
AppContext.BaseDirectory,
"Content",
"LegacyMims",
"BuildB");
var catalog = LegacyMimCatalogImporter.ImportCatalog(rootDirectory);
Assert.Contains("I love jokes. Did you hear about the theater actor who fell through the floorboards? He was just going through a stage.",
catalog.Jokes);
Assert.Contains("Sure I got one. What did the zero say to the eight. Nice belt.", catalog.Jokes);
Assert.Contains("Here's an interesting fact about me. I have two cameras but they're different focal lengths. One's for far things, and the other's for near things.",
catalog.FunFacts);
Assert.Contains("True fact. Children have more taste buds than grown ups.", catalog.FunFacts);
}
[Fact]
public void ImportCatalog_ImportsBuildBRnGreetingResponsesIntoGreetingBucket()
{
@@ -465,4 +484,4 @@ public sealed class LegacyMimCatalogImporterTests
return rootDirectory;
}
}
}

View File

@@ -496,6 +496,26 @@ public sealed class JiboInteractionServiceTests
Assert.Equal("ScriptedResponse", decision.ContextUpdates![ChitchatRouteKey]);
}
[Fact]
public async Task BuildDecisionAsync_CurrentLocation_UsesRuntimeLocationName()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "what is our current location",
NormalizedTranscript = "what is our current location",
Attributes = new Dictionary<string, object?>
{
["context"] = """{"runtime":{"location":{"name":"Houston"}}}"""
}
});
Assert.Equal("current_location", decision.IntentName);
Assert.Contains("Houston", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
Assert.Equal("ScriptedResponse", decision.ContextUpdates![ChitchatRouteKey]);
}
[Fact]
public async Task BuildDecisionAsync_Hello_RoutesThroughChitchatScriptedResponse()
{
@@ -2842,6 +2862,63 @@ public sealed class JiboInteractionServiceTests
Assert.Equal("Do you want to hear a fun pizza fact?", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_SurprisesOtaPrompt_StaysDistinctFromPizzaProactivity()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "yes",
NormalizedTranscript = "yes",
Attributes = new Dictionary<string, object?>
{
["listenRules"] = (string[])["surprises-ota/want_to_download_now", "globals/global_commands_launch"],
["listenAsrHints"] = (string[])["$YESNO"]
}
});
Assert.Equal("yes", decision.IntentName);
Assert.Equal("Yes.", decision.ReplyText);
Assert.NotEqual("proactive_offer_pizza_fact", decision.IntentName);
}
[Fact]
public async Task BuildDecisionAsync_SomethingFunOffer_MapsToFunFactIntent()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "hey can i tell you something kind of fun",
NormalizedTranscript = "hey can i tell you something kind of fun"
});
Assert.Equal("proactive_fun_fact", decision.IntentName);
Assert.NotNull(decision.ReplyText);
Assert.NotEmpty(decision.ReplyText);
Assert.Equal("chitchat-skill", decision.SkillName);
Assert.Equal("fun_fact", decision.SkillPayload!["replyType"]);
}
[Fact]
public async Task BuildDecisionAsync_Surprise_DefaultsToAFunFactWhenNoPizzaSignalExists()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "surprise me",
NormalizedTranscript = "surprise me"
});
Assert.Equal("proactive_fun_fact", decision.IntentName);
Assert.Equal("chitchat-skill", decision.SkillName);
Assert.Equal("fun_fact", decision.SkillPayload!["replyType"]);
Assert.NotNull(decision.ReplyText);
Assert.NotEmpty(decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_WordOfDayOfferPrompt_WithNoisyAffirmation_MapsToWordOfDayLaunch()
{

View File

@@ -21,7 +21,7 @@ public sealed class JiboWebSocketServiceTests
var contentRepository = new InMemoryJiboExperienceContentRepository();
var contentCache = new JiboExperienceContentCache(contentRepository);
var conversationBroker = new DemoConversationBroker(new JiboInteractionService(contentCache,
new DefaultJiboRandomizer(), new InMemoryPersonalMemoryStore()));
new LastItemRandomizer(), new InMemoryPersonalMemoryStore()));
var sttSelector = new DefaultSttStrategySelector(
[
new SyntheticBufferedAudioSttStrategy()
@@ -5111,4 +5111,12 @@ public sealed class JiboWebSocketServiceTests
return Task.FromResult<NewsBriefingSnapshot?>(snapshot);
}
}
private sealed class LastItemRandomizer : IJiboRandomizer
{
public T Choose<T>(IReadOnlyList<T> items)
{
return items[^1];
}
}
}