From f2826253d5b6ba9e93b988f082606912d640d148 Mon Sep 17 00:00:00 2001 From: Jacob Dubin Date: Sun, 17 May 2026 17:50:01 -0500 Subject: [PATCH] Expand surprise facts into robot and human categories --- .../IJiboExperienceContentRepository.cs | 2 + .../Services/JiboInteractionService.cs | 26 ++++++++- ...InMemoryJiboExperienceContentRepository.cs | 16 ++++++ .../Content/LegacyMimCatalogImporter.cs | 57 +++++++++++++++++-- .../Content/LegacyMims/BuildB/README.md | 1 + .../Content/LegacyMimCatalogImporterTests.cs | 12 +++- .../WebSockets/JiboInteractionServiceTests.cs | 43 +++++++++++++- 7 files changed, 146 insertions(+), 11 deletions(-) diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Abstractions/IJiboExperienceContentRepository.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Abstractions/IJiboExperienceContentRepository.cs index 38fba00..1626a18 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Abstractions/IJiboExperienceContentRepository.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Abstractions/IJiboExperienceContentRepository.cs @@ -14,6 +14,8 @@ public sealed class JiboConditionedReply public sealed class JiboExperienceCatalog { public IReadOnlyList Jokes { get; init; } = []; + public IReadOnlyList RobotFacts { get; init; } = []; + public IReadOnlyList HumanFacts { get; init; } = []; public IReadOnlyList FunFacts { get; init; } = []; public IReadOnlyList DanceAnimations { get; init; } = []; public IReadOnlyList GreetingReplies { get; init; } = []; diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs index 5a63481..5733cdc 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs @@ -1161,7 +1161,16 @@ public sealed class JiboInteractionService( private JiboInteractionDecision BuildProactiveFunFactDecision(JiboExperienceCatalog catalog) { - var fact = randomizer.Choose(catalog.FunFacts); + var categories = new List(); + AddProactiveFactCategory(categories, "fun_fact", catalog.FunFacts); + AddProactiveFactCategory(categories, "robot_fact", catalog.RobotFacts); + AddProactiveFactCategory(categories, "human_fact", catalog.HumanFacts); + + if (categories.Count == 0) + return new JiboInteractionDecision("proactive_fun_fact", randomizer.Choose(catalog.SurpriseReplies)); + + var selectedCategory = randomizer.Choose(categories); + var fact = randomizer.Choose(selectedCategory.Replies); return new JiboInteractionDecision( "proactive_fun_fact", fact, @@ -1171,10 +1180,21 @@ public sealed class JiboInteractionService( ["mim_id"] = "runtime-fun-fact", ["mim_type"] = "announcement", ["prompt_id"] = "RUNTIME_FUN_FACT", - ["replyType"] = "fun_fact" + ["replyType"] = "fun_fact", + ["factCategory"] = selectedCategory.CategoryName }); } + private static void AddProactiveFactCategory( + ICollection categories, + string categoryName, + IReadOnlyList replies) + { + if (replies.Count == 0) return; + + categories.Add(new ProactiveFactCategory(categoryName, replies)); + } + private JiboInteractionDecision BuildProactiveJokeDecision(JiboExperienceCatalog catalog) { return new JiboInteractionDecision( @@ -5204,6 +5224,8 @@ public sealed class JiboInteractionService( private sealed record ProactivityCandidate(string IntentName, int Weight); + private sealed record ProactiveFactCategory(string CategoryName, IReadOnlyList Replies); + private sealed record PizzaSignal(PersonalAffinity? Affinity); private sealed record GreetingPresenceProfile( diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/InMemoryJiboExperienceContentRepository.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/InMemoryJiboExperienceContentRepository.cs index 2ab2bb3..c61f137 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/InMemoryJiboExperienceContentRepository.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/InMemoryJiboExperienceContentRepository.cs @@ -27,6 +27,22 @@ public sealed class InMemoryJiboExperienceContentRepository : IJiboExperienceCon "What kind of music are balloons afraid of. Pop music.", "Why did the orange cry. Someone hurt his peelings." ], + RobotFacts = + [ + "Leonardo Da Vinci made sketches for a humanoid machine all the way back in the year 1495.", + "The world's first humanoid robot was called Elektro, and it debuted in 1939.", + "The English word robot comes from a 1920 play in Czechoslovakia, called Rossum's Universal Robots.", + "The first programmable robot arm was designed in 1954.", + "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." + ], + HumanFacts = + [ + "Every human being that has ever lived spent about 30 minutes as a single cell.", + "50 percent of a human's DNA is the same as a banana's.", + "Humans are the only animals that cry tears of emotion.", + "Six-year-olds laugh an average of 300 times a day. Grown ups only laugh 15 to 100 times a day.", + "Your nose can remember 50,000 different scents." + ], FunFacts = [ "A shrimp's heart is in its head.", diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMimCatalogImporter.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMimCatalogImporter.cs index 1e68266..59ffe2a 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMimCatalogImporter.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMimCatalogImporter.cs @@ -61,7 +61,7 @@ public static class LegacyMimCatalogImporter var text = NormalizePrompt(prompt.Prompt, IsTemplateBucket(bucket.Value)); if (string.IsNullOrWhiteSpace(text)) continue; - builder.Add(bucket.Value, prompt.Condition, text); + builder.Add(bucket.Value, prompt.Condition, text, prompt.Prompt); } } @@ -102,10 +102,12 @@ public static class LegacyMimCatalogImporter 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) || + if (fileName.StartsWith("RA_JBO_TellRobotFact", StringComparison.OrdinalIgnoreCase)) + return LegacyMimBucket.RobotFacts; + + if (fileName.StartsWith("RA_JBO_Shuffle", StringComparison.OrdinalIgnoreCase) || fileName.StartsWith("RA_JBO_TellSomething", StringComparison.OrdinalIgnoreCase)) - return LegacyMimBucket.FunFacts; + return LegacyMimBucket.FunFactSource; if (normalizedPath.Contains("/emotion-responses/", StringComparison.OrdinalIgnoreCase) || normalizedPath.Contains("/gqa-responses/", StringComparison.OrdinalIgnoreCase)) @@ -221,6 +223,8 @@ public static class LegacyMimCatalogImporter return new JiboExperienceCatalog { Jokes = Merge(baseCatalog.Jokes, importedCatalog.Jokes), + RobotFacts = Merge(baseCatalog.RobotFacts, importedCatalog.RobotFacts), + HumanFacts = Merge(baseCatalog.HumanFacts, importedCatalog.HumanFacts), FunFacts = Merge(baseCatalog.FunFacts, importedCatalog.FunFacts), DanceAnimations = Merge(baseCatalog.DanceAnimations, importedCatalog.DanceAnimations), GreetingReplies = Merge(baseCatalog.GreetingReplies, importedCatalog.GreetingReplies), @@ -332,9 +336,12 @@ public static class LegacyMimCatalogImporter GenericFallback, Greeting, Jokes, + RobotFacts, + HumanFacts, HowAreYou, Emotion, FunFacts, + FunFactSource, Personality, PersonalReportKickOff, PersonalReportOutro, @@ -365,6 +372,8 @@ public static class LegacyMimCatalogImporter private readonly List _fallbacks = []; private readonly List _greetings = []; private readonly List _jokes = []; + private readonly List _robotFacts = []; + private readonly List _humanFacts = []; private readonly List _howAreYous = []; private readonly List _funFacts = []; private readonly List _newsCategoryIntroReplies = []; @@ -380,7 +389,7 @@ public static class LegacyMimCatalogImporter private readonly List _weatherTomorrowHighLowReplies = []; private readonly List _weatherTomorrowIntroReplies = []; - public void Add(LegacyMimBucket bucket, string? condition, string text) + public void Add(LegacyMimBucket bucket, string? condition, string text, string? sourcePrompt = null) { switch (bucket) { @@ -399,6 +408,12 @@ public static class LegacyMimCatalogImporter _jokes.Add(text); return; + case LegacyMimBucket.RobotFacts: + AddDistinct(_robotFacts, text); + return; + case LegacyMimBucket.HumanFacts: + AddDistinct(_humanFacts, text); + return; case LegacyMimBucket.HowAreYou: if (_howAreYous.Any(value => string.Equals(value, text, StringComparison.OrdinalIgnoreCase))) return; @@ -425,6 +440,19 @@ public static class LegacyMimCatalogImporter _personalities.Add(text); return; + case LegacyMimBucket.FunFactSource: + switch (ResolveFunFactTarget(sourcePrompt ?? text)) + { + case LegacyMimBucket.RobotFacts: + AddDistinct(_robotFacts, text); + return; + case LegacyMimBucket.HumanFacts: + AddDistinct(_humanFacts, text); + return; + default: + AddDistinct(_funFacts, text); + return; + } case LegacyMimBucket.FunFacts: if (_funFacts.Any(value => string.Equals(value, text, StringComparison.OrdinalIgnoreCase))) return; @@ -488,6 +516,8 @@ public static class LegacyMimCatalogImporter return new JiboExperienceCatalog { Jokes = [.. _jokes], + RobotFacts = [.. _robotFacts], + HumanFacts = [.. _humanFacts], FunFacts = [.. _funFacts], GreetingReplies = [.. _greetings], HowAreYouReplies = [.. _howAreYous], @@ -519,6 +549,23 @@ public static class LegacyMimCatalogImporter target.Add(text); } + + private LegacyMimBucket ResolveFunFactTarget(string prompt) + { + var lowered = NormalizePrompt(prompt, false).ToLowerInvariant(); + if (ContainsAny(lowered, "robot", "humanoid", "machine", "about me", "my cameras", "turing", "deep blue", "rossum")) + return LegacyMimBucket.RobotFacts; + + if (ContainsAny(lowered, "human", "people", "grown ups", "human being", "humans")) + return LegacyMimBucket.HumanFacts; + + return LegacyMimBucket.FunFacts; + } + + private static bool ContainsAny(string text, params string[] values) + { + return values.Any(value => text.Contains(value, StringComparison.OrdinalIgnoreCase)); + } } private sealed class LegacyMimDefinition diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildB/README.md b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildB/README.md index b1d9e9f..294072d 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildB/README.md +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildB/README.md @@ -9,3 +9,4 @@ It also includes a descriptor pack for questions like `are you kind`, `are you f 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. +Those facts are now split into generic, robot, and human buckets so the randomizer can sound more like Pegasus while staying lightweight. diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/Content/LegacyMimCatalogImporterTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/Content/LegacyMimCatalogImporterTests.cs index d5edf6b..5404e60 100644 --- a/OpenJibo/tests/Jibo.Cloud.Tests/Content/LegacyMimCatalogImporterTests.cs +++ b/OpenJibo/tests/Jibo.Cloud.Tests/Content/LegacyMimCatalogImporterTests.cs @@ -192,9 +192,17 @@ public sealed class LegacyMimCatalogImporterTests 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.", + Assert.Contains(catalog.RobotFacts, reply => + reply.Contains("Leonardo Da Vinci made sketches", StringComparison.OrdinalIgnoreCase)); + Assert.Contains(catalog.RobotFacts, reply => + reply.Contains("first programmable robot arm", StringComparison.OrdinalIgnoreCase)); + Assert.Contains(catalog.RobotFacts, reply => + reply.Contains("robots have a human form", StringComparison.OrdinalIgnoreCase)); + Assert.Contains(catalog.RobotFacts, reply => + reply.Contains("two cameras but they're different focal lengths", StringComparison.OrdinalIgnoreCase)); + Assert.Contains("A random fact for you. A shrimp's heart is in its head.", catalog.FunFacts); + Assert.Contains("An amazing but true fact for you. Dogs and elephants are the only animals that understand pointing.", catalog.FunFacts); - Assert.Contains("True fact. Children have more taste buds than grown ups.", catalog.FunFacts); } [Fact] diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs index d8ccb36..204627a 100644 --- a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs +++ b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs @@ -2915,10 +2915,30 @@ public sealed class JiboInteractionServiceTests Assert.Equal("proactive_fun_fact", decision.IntentName); Assert.Equal("chitchat-skill", decision.SkillName); Assert.Equal("fun_fact", decision.SkillPayload!["replyType"]); + Assert.Equal("fun_fact", decision.SkillPayload["factCategory"]); Assert.NotNull(decision.ReplyText); Assert.NotEmpty(decision.ReplyText); } + [Fact] + public async Task BuildDecisionAsync_Surprise_UsesHumanFactWhenRandomizerChoosesLastCategory() + { + var service = CreateService(randomizer: new FactCategoryLastRandomizer()); + + 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.Equal("human_fact", decision.SkillPayload["factCategory"]); + Assert.NotNull(decision.ReplyText); + Assert.Contains("human", decision.ReplyText, StringComparison.OrdinalIgnoreCase); + } + [Fact] public async Task BuildDecisionAsync_WordOfDayOfferPrompt_WithNoisyAffirmation_MapsToWordOfDayLaunch() { @@ -3872,11 +3892,12 @@ public sealed class JiboInteractionServiceTests IPersonalMemoryStore? personalMemoryStore = null, IWeatherReportProvider? weatherReportProvider = null, INewsBriefingProvider? newsBriefingProvider = null, - IJiboExperienceContentRepository? contentRepository = null) + IJiboExperienceContentRepository? contentRepository = null, + IJiboRandomizer? randomizer = null) { return new JiboInteractionService( new JiboExperienceContentCache(contentRepository ?? new InMemoryJiboExperienceContentRepository()), - new FirstItemRandomizer(), + randomizer ?? new FirstItemRandomizer(), personalMemoryStore ?? new InMemoryPersonalMemoryStore(), weatherReportProvider, newsBriefingProvider); @@ -3915,6 +3936,24 @@ public sealed class JiboInteractionServiceTests } } + private sealed class LastItemRandomizer : IJiboRandomizer + { + public T Choose(IReadOnlyList items) + { + return items[^1]; + } + } + + private sealed class FactCategoryLastRandomizer : IJiboRandomizer + { + public T Choose(IReadOnlyList items) + { + return typeof(T).Name == "ProactiveFactCategory" + ? items[^1] + : items[0]; + } + } + private sealed class CapturingWeatherReportProvider : IWeatherReportProvider { public WeatherReportRequest? LastRequest { get; private set; }