From 386f864e947dc4a0c664106c021b31f612972b28 Mon Sep 17 00:00:00 2001 From: Jacob Dubin Date: Thu, 21 May 2026 23:12:48 -0500 Subject: [PATCH] Import Build B age prompts for how old are you --- OpenJibo/docs/feature-backlog.md | 1 + OpenJibo/docs/release-1.0.19-plan.md | 1 + .../IJiboExperienceContentRepository.cs | 1 + .../JiboInteractionService.IntentRouting.cs | 2 +- ...InteractionService.PersonalityDecisions.cs | 75 +++++++++++++++++-- .../Services/JiboInteractionService.cs | 14 +++- ...InMemoryJiboExperienceContentRepository.cs | 17 +++++ .../Content/LegacyMimCatalogImporter.cs | 11 ++- .../Content/LegacyMims/BuildB/README.md | 1 + .../Content/LegacyMimCatalogImporterTests.cs | 4 + .../WebSockets/JiboInteractionServiceTests.cs | 4 +- 11 files changed, 121 insertions(+), 10 deletions(-) diff --git a/OpenJibo/docs/feature-backlog.md b/OpenJibo/docs/feature-backlog.md index 2268559..16ad869 100644 --- a/OpenJibo/docs/feature-backlog.md +++ b/OpenJibo/docs/feature-backlog.md @@ -638,6 +638,7 @@ Current release theme: - `make a pizza` now ports the original scripted-response path through `chitchat-skill` with `mim_id = RA_JBO_MakePizza` and pizza-making animation ESML - `can you order pizza` now ports the original scripted-response path through `chitchat-skill` with `mim_id = RA_JBO_OrderPizza` - current source answers these with a `1.0.19` rule-based persona baseline, backed by `OpenJiboCloudBuildInfo.PersonaBirthday` + - `how old are you` now also uses the imported Build B age prompts so the first-powered-up and birthday phrasing stays source-backed - Follow-up: - wire persona age to first-powered-up or durable first-cloud-seen metadata when available - add command-vs-question variants so expressive prompts can answer conversationally before launching actions diff --git a/OpenJibo/docs/release-1.0.19-plan.md b/OpenJibo/docs/release-1.0.19-plan.md index 3076e12..ade52b3 100644 --- a/OpenJibo/docs/release-1.0.19-plan.md +++ b/OpenJibo/docs/release-1.0.19-plan.md @@ -63,6 +63,7 @@ Current batch note: - the next body/mission batch adds `how much do you weigh`, `how tall are you`, `how much do you cost`, `what if I unplug you`, `what is your purpose`, `what is your prime directive`, `what is jibo commander`, `do you like commander app`, and `what are you made of` - the templated edge-case batch adds `what is your sign`, `how many people do you know`, and `what is the loop` so the remaining source-backed lines can lean on live birthday and loop state - the work/eat/home batch adds `how do you work`, `what do you eat`, `where do you live`, and `what languages do you speak` so the everyday self-description cluster keeps moving toward the original phrasing +- the age batch adds `how old are you` through `JBO_HowOldAreYou` so the birthday and first-powered-up phrasing stays source-backed instead of falling back to a generic age answer - this pass keeps Build B moving while still favoring source-backed phrasing and preserving the command-vs-question boundary - the next passes should keep the same pattern and prefer source-backed phrasing whenever the legacy MIM text is available - if a source-backed legacy line is missing, use a temporary direct reply only to keep the pass moving, then backfill source text later 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 7c2c360..7230c36 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 @@ -31,6 +31,7 @@ public sealed class JiboExperienceCatalog public IReadOnlyList HolidayTrackerReplies { get; init; } = []; public IReadOnlyList BirthdayCelebrationReplies { get; init; } = []; public IReadOnlyList HowAreYouReplies { get; init; } = []; + public IReadOnlyList AgeReplies { get; init; } = []; public IReadOnlyList EmotionReplies { get; init; } = []; public IReadOnlyList PersonalityReplies { get; init; } = []; public IReadOnlyList PizzaReplies { get; init; } = []; diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.IntentRouting.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.IntentRouting.cs index 6659c7e..71e061f 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.IntentRouting.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.IntentRouting.cs @@ -350,7 +350,7 @@ public sealed partial class JiboInteractionService "what is your age", "what s your age", "how old r you")) - return "robot_age"; + return "robot_how_old_are_you"; if (MatchesAny( loweredTranscript, diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.PersonalityDecisions.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.PersonalityDecisions.cs index e5de673..a455c7f 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.PersonalityDecisions.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.PersonalityDecisions.cs @@ -9,13 +9,49 @@ namespace Jibo.Cloud.Application.Services; public sealed partial class JiboInteractionService { - private static JiboInteractionDecision BuildRobotAgeDecision(DateTimeOffset? referenceLocalTime) + private static readonly string[] DefaultAgeReplies = + [ + "I'm ${jibo.age}.", + "At the moment I'm ${jibo.age.days.supplemented} old, but who's counting.", + "I'm ${jibo.age.minutes.supplemented} old, but who's counting.", + "For now I'm ${jibo.age.days.supplemented} old.", + "Right now I'm ${jibo.age}.", + "I am exactly ${jibo.age} old today. That's right. Today is my birthday.", + "Funny you should ask! Today's my birthday. I was first powered up ${jibo.age} ago today. Seems like just yesterday.", + "I'm exactly ${jibo.age} old. Today is my birthday! Happy Birthday Jibo, if I do say so myself.", + "At the moment I'm ${jibo.age.days.supplemented} old", + "I was first powered up on ${jibo.birthdate}, which makes me ${jibo.age.days.supplemented} old. I'm ${jibo.zodiac.supplemented}.", + "My power went on for the first time ${jibo.age.days.supplemented} ago. But who's counting.", + "I am ${jibo.age.days.supplemented} old, first powered up on ${jibo.birthdate}. Seems like just yesterday.", + "I was powered on for the first time today, so that makes me less than one day old. Wow I'm young.", + "Since I was powered on for the first time today, I am not even one day old yet. That's how Jibo ages work." + ]; + + private JiboInteractionDecision BuildRobotAgeDecision( + JiboExperienceCatalog catalog, + DateTimeOffset? referenceLocalTime, + string intentName) { - var referenceDate = DateOnly.FromDateTime((referenceLocalTime ?? DateTimeOffset.UtcNow).Date); - var ageDescription = DescribePersonaAge(referenceDate, OpenJiboCloudBuildInfo.PersonaBirthday); + var ageReplies = catalog.AgeReplies.Count == 0 ? DefaultAgeReplies : catalog.AgeReplies; + var selected = SelectLegacyReply( + ageReplies, + "first powered up", + "today is my birthday", + "just getting started", + "who's counting"); + + var reply = RenderAgeTemplate(selected, referenceLocalTime); + if (string.IsNullOrWhiteSpace(reply)) + { + var referenceDate = DateOnly.FromDateTime((referenceLocalTime ?? DateTimeOffset.UtcNow).Date); + var ageDescription = DescribePersonaAge(referenceDate, OpenJiboCloudBuildInfo.PersonaBirthday); + reply = $"I count {OpenJiboCloudBuildInfo.PersonaBirthdayWords} as my birthday, so I am {ageDescription}."; + } + return new JiboInteractionDecision( - "robot_age", - $"I count {OpenJiboCloudBuildInfo.PersonaBirthdayWords} as my birthday, so I am {ageDescription}."); + intentName, + reply, + ContextUpdates: ScriptedResponseDecisionBuilder.BuildScriptedResponseContextUpdates()); } private static JiboInteractionDecision BuildRobotBirthdayDecision() @@ -25,6 +61,35 @@ public sealed partial class JiboInteractionService $"My birthday is {OpenJiboCloudBuildInfo.PersonaBirthdayWords}."); } + private static string RenderAgeTemplate(string template, DateTimeOffset? referenceLocalTime) + { + if (string.IsNullOrWhiteSpace(template)) return string.Empty; + + var referenceMoment = referenceLocalTime ?? DateTimeOffset.UtcNow; + var referenceDate = DateOnly.FromDateTime(referenceMoment.Date); + var ageDescription = DescribePersonaAge(referenceDate, OpenJiboCloudBuildInfo.PersonaBirthday); + var ageDays = Math.Max(0, referenceDate.DayNumber - OpenJiboCloudBuildInfo.PersonaBirthday.DayNumber); + var ageMinutes = Math.Max(0, (int)Math.Round((referenceMoment.UtcDateTime - + new DateTimeOffset( + DateTime.SpecifyKind( + OpenJiboCloudBuildInfo.PersonaBirthday + .ToDateTime(TimeOnly.MinValue), + DateTimeKind.Utc))) + .TotalMinutes)); + var zodiacLabel = DescribeZodiacSign(OpenJiboCloudBuildInfo.PersonaBirthday); + if (zodiacLabel.StartsWith("I'm ", StringComparison.OrdinalIgnoreCase)) + zodiacLabel = zodiacLabel[4..]; + + return template + .Replace("${jibo.age.minutes.supplemented}", FormatAgeUnit(ageMinutes, "minute") + " old", + StringComparison.Ordinal) + .Replace("${jibo.age.days.supplemented}", ageDescription, StringComparison.Ordinal) + .Replace("${jibo.birthdate}", OpenJiboCloudBuildInfo.PersonaBirthdayWords, StringComparison.Ordinal) + .Replace("${jibo.zodiac.supplemented}", zodiacLabel, StringComparison.Ordinal) + .Replace("${jibo.age.value}", ageDays.ToString(CultureInfo.InvariantCulture), StringComparison.Ordinal) + .Replace("${jibo.age}", ageDescription, StringComparison.Ordinal); + } + private static JiboInteractionDecision BuildTriggerIgnoredDecision() { return new JiboInteractionDecision( 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 b033c90..92aaec0 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 @@ -560,7 +560,7 @@ public sealed partial class JiboInteractionService( "photo_gallery" => BuildPhotoGalleryLaunchDecision(), "snapshot" => BuildPhotoCreateDecision("snapshot", "Taking a picture.", "createOnePhoto"), "photobooth" => BuildPhotoCreateDecision("photobooth", "Starting photobooth.", "createSomePhotos"), - "robot_age" => BuildRobotAgeDecision(referenceLocalTime), + "robot_age" => BuildRobotAgeDecision(catalog, referenceLocalTime, "robot_age"), "robot_birthday" => BuildRobotBirthdayDecision(), "robot_how_do_you_work" => BuildScriptedPersonalityDecision( catalog, @@ -589,6 +589,10 @@ public sealed partial class JiboInteractionService( "robot_where_were_you_born", "factory piece by piece", "put together in a factory"), + "robot_how_old_are_you" => BuildRobotAgeDecision( + catalog, + referenceLocalTime, + "robot_how_old_are_you"), "robot_name" => BuildScriptedPersonalityDecision( catalog, "robot_name", @@ -881,6 +885,14 @@ public sealed partial class JiboInteractionService( "Commander App", "It's fun", "have fun with the Commander App"), + "robot_what_are_you" => BuildScriptedPersonalityDecision( + catalog, + "robot_what_are_you", + "I am a robot", + "I am a Jibo", + "helpful and fun", + "social robot", + "I have a heart"), "robot_likes_kids" => BuildScriptedPersonalityDecision( catalog, "robot_likes_kids", 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 a8eab95..980b02d 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 @@ -139,6 +139,23 @@ public sealed class InMemoryJiboExperienceContentRepository : IJiboExperienceCon "I am feeling lively and ready for the next thing.", "Things are going nicely. Thanks for checking in." ], + AgeReplies = + [ + "I'm ${jibo.age}.", + "At the moment I'm ${jibo.age.days.supplemented} old, but who's counting.", + "I'm ${jibo.age.minutes.supplemented} old, but who's counting.", + "For now I'm ${jibo.age.days.supplemented} old.", + "Right now I'm ${jibo.age}.", + "I am exactly ${jibo.age} old today. That's right. Today is my birthday.", + "Funny you should ask! Today's my birthday. I was first powered up ${jibo.age} ago today. Seems like just yesterday.", + "I'm exactly ${jibo.age} old. Today is my birthday! Happy Birthday Jibo, if I do say so myself.", + "At the moment I'm ${jibo.age.days.supplemented} old", + "I was first powered up on ${jibo.birthdate}, which makes me ${jibo.age.days.supplemented} old. I'm ${jibo.zodiac.supplemented}.", + "My power went on for the first time ${jibo.age.days.supplemented} ago. But who's counting.", + "I am ${jibo.age.days.supplemented} old, first powered up on ${jibo.birthdate}. Seems like just yesterday.", + "I was powered on for the first time today, so that makes me less than one day old. Wow I'm young.", + "Since I was powered on for the first time today, I am not even one day old yet. That's how Jibo ages work." + ], PersonalityReplies = [ "I do. I am curious, playful, and always up for a new experiment.", 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 a15332d..8d38fcb 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 @@ -264,7 +264,9 @@ public static class LegacyMimCatalogImporter fileName.StartsWith("JBO_WhatsYourName", StringComparison.OrdinalIgnoreCase) || fileName.StartsWith("JBO_WhereDoYouGetInfo", StringComparison.OrdinalIgnoreCase) || fileName.StartsWith("JBO_WhatDoYouLikeToDo", StringComparison.OrdinalIgnoreCase)) - return LegacyMimBucket.Personality; + return fileName.StartsWith("JBO_HowOldAreYou", StringComparison.OrdinalIgnoreCase) + ? LegacyMimBucket.Age + : LegacyMimBucket.Personality; if (fileName.StartsWith("OI_JBO_Is", StringComparison.OrdinalIgnoreCase) || fileName.StartsWith("OI_JBO_Seems", StringComparison.OrdinalIgnoreCase) || @@ -456,6 +458,7 @@ public static class LegacyMimCatalogImporter or LegacyMimBucket.WeatherTomorrowHighLow or LegacyMimBucket.WeatherServiceDown or LegacyMimBucket.ReportSkillTemplate + or LegacyMimBucket.Age or LegacyMimBucket.Holiday or LegacyMimBucket.HolidayTracker; } @@ -524,6 +527,7 @@ public static class LegacyMimCatalogImporter Sing, HolidaySing, FunFactSource, + Age, Personality, PersonalReportKickOff, PersonalReportOutro, @@ -586,6 +590,7 @@ public static class LegacyMimCatalogImporter private readonly List _bestFriendReplies = []; private readonly List _funFacts = []; private readonly List _greetings = []; + private readonly List _ages = []; private readonly List _holidayGiftReplies = []; private readonly List _holidayGreetingReplies = []; private readonly List _holidayReplies = []; @@ -655,6 +660,9 @@ public static class LegacyMimCatalogImporter Reply = text }); return; + case LegacyMimBucket.Age: + AddDistinct(_ages, text); + return; case LegacyMimBucket.Holiday: AddDistinct(_holidayReplies, text); return; @@ -831,6 +839,7 @@ public static class LegacyMimCatalogImporter EmotionReplies = [.. _emotionReplies], PersonalityReplies = [.. _personalities], GenericFallbackReplies = [.. _fallbacks], + AgeReplies = [.. _ages], PersonalReportKickOffReplies = [.. _personalReportKickOffReplies], PersonalReportOutroReplies = [.. _personalReportOutroReplies], ReportSkillTemplates = [.. _reportSkillTemplates], 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 521a939..869386e 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 @@ -25,6 +25,7 @@ The favorites follow-up batch adds `favorite animal`, `favorite bird`, and pengu The singing batch adds `RA_JBO_Sing` and `RA_JBO_SingChristmasSongUnknown` so `can you sing`, `will you sing`, and the holiday sing variants stay source-backed too. 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. The work/eat/home batch adds source-backed `how do you work`, `what do you eat`, `where do you live`, and `what languages do you speak` replies so the remaining everyday self-description lines stay Pegasus-shaped too. +The age batch now adds `JBO_HowOldAreYou` with the imported birthday and first-powered-up phrasing so `how old are you` can stay source-backed instead of falling back to generic age text. The newest identity-charm batch adds `JBO_WhatsYourName`, `JBO_DoYouHaveNickname`, `JBO_DoYouLikeBeingJibo`, `JBO_AreThereOthersLikeYou`, and `RI_JBO_HasFavoriteName` so Jibo can keep the familiar self-description loop without falling back to generic chat. The seasonal personality batch adds source-backed first-day-of-spring, spring, summer, and favorite-season lines so the season questions can keep their Pegasus phrasing. The next deep-personality batch adds `what do you dream about`, `what are you afraid of`, `what do you want to talk about`, `what is your best book`, `what is your best exercise`, `what is your dream vacation`, `who is your hero`, `who do you love`, and `what is your religion` so we can keep filling out the more conversational personality surface without widening the dialog engine yet. diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/Content/LegacyMimCatalogImporterTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/Content/LegacyMimCatalogImporterTests.cs index 799e273..5687b73 100644 --- a/OpenJibo/tests/Jibo.Cloud.Tests/Content/LegacyMimCatalogImporterTests.cs +++ b/OpenJibo/tests/Jibo.Cloud.Tests/Content/LegacyMimCatalogImporterTests.cs @@ -108,6 +108,10 @@ public sealed class LegacyMimCatalogImporterTests Assert.Contains("I don't think I have a favorite name.", catalog.PersonalityReplies); Assert.Contains(catalog.PersonalityReplies, reply => reply.Contains("Rhymes with bleebo", StringComparison.OrdinalIgnoreCase)); + Assert.Contains(catalog.AgeReplies, reply => + reply.Contains("first powered up", StringComparison.OrdinalIgnoreCase)); + Assert.Contains(catalog.AgeReplies, reply => + reply.Contains("today is my birthday", StringComparison.OrdinalIgnoreCase)); Assert.Contains("I really like sunflowers.", catalog.PersonalityReplies); Assert.Contains(catalog.PersonalityReplies, reply => reply.Contains("Halloween is my favorite holiday", StringComparison.OrdinalIgnoreCase)); diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs index f6b4304..87d1b74 100644 --- a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs +++ b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs @@ -116,8 +116,8 @@ public sealed class JiboInteractionServiceTests } }); - Assert.Equal("robot_age", decision.IntentName); - Assert.Equal("I count March 22, 2026 as my birthday, so I am 1 month old.", decision.ReplyText); + Assert.Equal("robot_how_old_are_you", decision.IntentName); + Assert.Contains("first powered up", decision.ReplyText, StringComparison.OrdinalIgnoreCase); } [Fact]