diff --git a/OpenJibo/docs/feature-backlog.md b/OpenJibo/docs/feature-backlog.md index 33af42a..eeb8d51 100644 --- a/OpenJibo/docs/feature-backlog.md +++ b/OpenJibo/docs/feature-backlog.md @@ -857,6 +857,7 @@ Current release theme: - Seasonal charm work in flight: - source-backed holiday, New Year's, Halloween, spring, and gift prompts are now part of Build B - `RN_` holiday greeting files are now bucketed as greetings so seasonal replies stay visible in the catalog + - birthday celebration lines are now bucketed separately, and birthday memory writes a loop-scoped holiday record so personal dates can join the holiday list later - Presence and thought follow-ups in flight: - `welcome back`, `what are you thinking`, `what have you been doing`, and `what did you do` are now part of Build B - these keep the social surface lively while the memory and multitenant tracks keep advancing in parallel diff --git a/OpenJibo/docs/holiday-architecture.md b/OpenJibo/docs/holiday-architecture.md index ed84c41..f0d7c38 100644 --- a/OpenJibo/docs/holiday-architecture.md +++ b/OpenJibo/docs/holiday-architecture.md @@ -20,3 +20,4 @@ Notes: - `IsEnabled = false` can be used to suppress a holiday later - birthdays and other personal events can be added as loop-scoped custom records - the current system holiday source uses Nager.Date with a safe local fallback for uptime +- birthday memory authoring now upserts a holiday record so the same merged list can later drive celebration and reminder behavior diff --git a/OpenJibo/docs/release-1.0.19-plan.md b/OpenJibo/docs/release-1.0.19-plan.md index 530c097..a0d87e6 100644 --- a/OpenJibo/docs/release-1.0.19-plan.md +++ b/OpenJibo/docs/release-1.0.19-plan.md @@ -90,9 +90,11 @@ The goal is to port these in small batches, capture the source-backed phrasing w - port holiday-aware personality responses as a visible extension of the new persona slice - start with a small, source-backed set (for example birthdays/holidays already represented in legacy data paths) - ensure holiday responses feel characterful while still routing through stock-compatible payloads +- imported Build B holiday buckets now include holiday, holiday greeting, holiday gift, and birthday celebration lines - use a loop-scoped merged holiday list in the cloud protocol so system holidays and custom person holidays can coexist - source system holidays from a live holiday provider and keep `IsEnabled = false` records available for holiday suppression - keep birthday/custom holiday authoring aligned with person memory so future proactivity can suppress or promote holidays per loop +- birthday memory writes now create loop-scoped holiday records, which keeps the holiday list extensible without changing the protocol shape again ### 5. Multi-Tenant Memory Storage Foundation diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Abstractions/ICloudStateStore.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Abstractions/ICloudStateStore.cs index 09b7b37..84c5f26 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Abstractions/ICloudStateStore.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Abstractions/ICloudStateStore.cs @@ -43,5 +43,6 @@ public interface ICloudStateStore IReadOnlyList GetIncomingKeyRequests(); IReadOnlyList GetBinaryRequests(); IReadOnlyList GetHolidays(string? loopId = null); + HolidayRecord UpsertHoliday(HolidayRecord holiday); void UpdateRobot(DeviceRegistration registration); } 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 877a813..68b88ac 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 @@ -19,6 +19,11 @@ public sealed class JiboExperienceCatalog public IReadOnlyList FunFacts { get; init; } = []; public IReadOnlyList DanceAnimations { get; init; } = []; public IReadOnlyList GreetingReplies { get; init; } = []; + public IReadOnlyList HolidayReplies { get; init; } = []; + public IReadOnlyList HolidaySeasonReplies { get; init; } = []; + public IReadOnlyList HolidayGreetingReplies { get; init; } = []; + public IReadOnlyList HolidayGiftReplies { get; init; } = []; + public IReadOnlyList BirthdayCelebrationReplies { get; init; } = []; public IReadOnlyList HowAreYouReplies { get; init; } = []; public IReadOnlyList EmotionReplies { get; init; } = []; public IReadOnlyList PersonalityReplies { get; init; } = []; @@ -50,4 +55,4 @@ public sealed class JiboExperienceCatalog public IReadOnlyList GenericFallbackReplies { get; init; } = []; public IReadOnlyList DanceReplies { get; init; } = []; public IReadOnlyList DanceQuestionReplies { get; init; } = []; -} \ No newline at end of file +} 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 19cdbb0..894480e 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 @@ -2,6 +2,7 @@ using System.Globalization; using System.Text.Json; using System.Text.RegularExpressions; using Jibo.Cloud.Application.Abstractions; +using Jibo.Cloud.Domain.Models; using Jibo.Runtime.Abstractions; namespace Jibo.Cloud.Application.Services; @@ -12,7 +13,8 @@ public sealed class JiboInteractionService( IPersonalMemoryStore personalMemoryStore, IWeatherReportProvider? weatherReportProvider = null, ICommuteReportProvider? commuteReportProvider = null, - INewsBriefingProvider? newsBriefingProvider = null) + INewsBriefingProvider? newsBriefingProvider = null, + ICloudStateStore? cloudStateStore = null) { private const string GreetingRouteMetadataKey = "greetingsRoute"; private const string GreetingSpeakerMetadataKey = "greetingsSpeaker"; @@ -634,17 +636,25 @@ public sealed class JiboInteractionService( catalog, "robot_is_likable", "people like me"), - "seasonal_holiday_greeting" => BuildScriptedGreetingDecision( + "seasonal_holiday_greeting" => BuildScriptedHolidayGreetingDecision( catalog, "seasonal_holiday_greeting", - "It's a fun time of year", - "And to you too", - "Right back at you"), - "seasonal_holidays" => BuildScriptedPersonalityDecision( + "fun time of year", + "right back at you", + "and to you too"), + "seasonal_holidays" => BuildScriptedHolidayTemplateDecision( + turn, + greetingPresence, catalog, "seasonal_holidays", "official owner can tell me which ones we'll celebrate together", "going to the jibo's settings screen in the jibo app"), + "seasonal_holiday_season" => BuildScriptedHolidayDecision( + catalog.HolidaySeasonReplies, + "seasonal_holiday_season", + "holiday season", + "festive", + "celebrate"), "seasonal_new_years_resolution" => BuildScriptedPersonalityDecision( catalog, "seasonal_new_years_resolution", @@ -669,12 +679,18 @@ public sealed class JiboInteractionService( catalog, "seasonal_first_day_spring", "maybe enjoy some flowers and all things spring"), - "seasonal_holiday_gift" => BuildScriptedPersonalityDecision( - catalog, + "seasonal_holiday_gift" => BuildScriptedHolidayDecision( + catalog.HolidayGiftReplies, "seasonal_holiday_gift", "ask for a pet elephant", "experience as a present", "donate to charities in other people's names"), + "birthday_celebration" => BuildScriptedHolidayDecision( + catalog.BirthdayCelebrationReplies, + "birthday_celebration", + "another year older", + "can't wait to see what you got me", + "powered on for the first time today"), "robot_favorite_flower" => BuildScriptedPersonalityDecision( catalog, "robot_favorite_flower", @@ -974,7 +990,27 @@ public sealed class JiboInteractionService( "memory_set_birthday", "I can remember it if you say, my birthday is March 14."); - personalMemoryStore.SetBirthday(ResolveTenantScope(turn), birthday); + var tenantScope = ResolveTenantScope(turn); + personalMemoryStore.SetBirthday(tenantScope, birthday); + var birthdayDate = TryParseBirthdayDate(birthday); + if (birthdayDate is not null) + { + var birthdayLabel = ResolvePreferredBirthdayLabel(turn); + cloudStateStore?.UpsertHoliday(new HolidayRecord + { + EventId = $"birthday-{tenantScope.LoopId}-{tenantScope.PersonId ?? "loop"}", + Name = string.IsNullOrWhiteSpace(birthdayLabel) ? "Birthday" : $"{birthdayLabel}'s Birthday", + Category = "birthday", + Subcategory = "personal", + LoopId = tenantScope.LoopId, + MemberId = tenantScope.PersonId, + IsEnabled = true, + Date = birthdayDate.Value, + Source = "birthday", + CountryCode = "US" + }); + } + return new JiboInteractionDecision( "memory_set_birthday", $"Got it. I will remember your birthday is {birthday}."); @@ -992,6 +1028,78 @@ public sealed class JiboInteractionService( $"You told me your birthday is {birthday}."); } + private static DateOnly? TryParseBirthdayDate(string birthdayText) + { + if (string.IsNullOrWhiteSpace(birthdayText)) return null; + + var normalized = birthdayText.Trim().ToLowerInvariant(); + var match = Regex.Match( + normalized, + @"\b(?january|february|march|april|may|june|july|august|september|october|november|december)\s+(?\d{1,2})(?:st|nd|rd|th)?\b", + RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); + if (!match.Success) return null; + + var month = match.Groups["month"].Value.ToLowerInvariant() switch + { + "january" => 1, + "february" => 2, + "march" => 3, + "april" => 4, + "may" => 5, + "june" => 6, + "july" => 7, + "august" => 8, + "september" => 9, + "october" => 10, + "november" => 11, + "december" => 12, + _ => 0 + }; + if (month == 0) return null; + + if (!int.TryParse(match.Groups["day"].Value, out var day) || day is < 1 or > 31) return null; + + var today = DateOnly.FromDateTime(DateTime.UtcNow); + var year = today.Year; + if (day > DateTime.DaysInMonth(year, month)) return null; + + DateOnly birthday; + try + { + birthday = new DateOnly(year, month, day); + } + catch + { + return null; + } + + if (birthday < today) birthday = birthday.AddYears(1); + return birthday; + } + + private static string? ResolvePreferredBirthdayLabel(TurnContext turn) + { + var context = ResolveGreetingPresenceProfile(turn); + return !string.IsNullOrWhiteSpace(context.PrimaryPersonId) && + context.LoopUserFirstNames.TryGetValue(context.PrimaryPersonId, out var firstName) && + !string.IsNullOrWhiteSpace(firstName) + ? ToDisplayName(firstName) + : null; + } + + private string RenderHolidayTemplate(string template, TurnContext turn, GreetingPresenceProfile presence) + { + var ownerName = ResolvePreferredGreetingName(turn, presence); + var speakerName = !string.IsNullOrWhiteSpace(ownerName) ? ownerName : "you"; + return template + .Replace("${speaker}'s", $"{speakerName}'s", StringComparison.OrdinalIgnoreCase) + .Replace("${speaker}", speakerName, StringComparison.OrdinalIgnoreCase) + .Replace("${loop.owner}", string.IsNullOrWhiteSpace(ownerName) ? string.Empty : ownerName, + StringComparison.OrdinalIgnoreCase) + .Replace(" ", " ", StringComparison.Ordinal) + .Trim(); + } + private JiboInteractionDecision BuildRememberImportantDateDecision(TurnContext turn, string transcript) { var importantDate = TryExtractImportantDateSet(transcript); @@ -2360,6 +2468,42 @@ public sealed class JiboInteractionService( ContextUpdates: BuildScriptedResponseContextUpdates()); } + private JiboInteractionDecision BuildScriptedHolidayDecision( + IReadOnlyList replies, + string intentName, + params string[] preferredSnippets) + { + return new JiboInteractionDecision( + intentName, + SelectLegacyReply(replies, preferredSnippets), + ContextUpdates: BuildScriptedResponseContextUpdates()); + } + + private JiboInteractionDecision BuildScriptedHolidayGreetingDecision( + JiboExperienceCatalog catalog, + string intentName, + params string[] preferredSnippets) + { + return new JiboInteractionDecision( + intentName, + SelectLegacyReply(catalog.HolidayGreetingReplies, preferredSnippets), + ContextUpdates: BuildScriptedResponseContextUpdates()); + } + + private JiboInteractionDecision BuildScriptedHolidayTemplateDecision( + TurnContext turn, + GreetingPresenceProfile presence, + JiboExperienceCatalog catalog, + string intentName, + params string[] preferredSnippets) + { + var selected = SelectLegacyReply(catalog.HolidayReplies, preferredSnippets); + return new JiboInteractionDecision( + intentName, + RenderHolidayTemplate(selected, turn, presence), + ContextUpdates: BuildScriptedResponseContextUpdates()); + } + private static IDictionary BuildScriptedResponseContextUpdates() { return new Dictionary(StringComparer.OrdinalIgnoreCase) @@ -2381,7 +2525,7 @@ public sealed class JiboInteractionService( if (!string.IsNullOrWhiteSpace(match)) return match; } - return randomizer.Choose(catalog.PersonalityReplies); + return catalog.PersonalityReplies.Count == 0 ? string.Empty : randomizer.Choose(catalog.PersonalityReplies); } private string SelectLegacyGreetingReply(JiboExperienceCatalog catalog, params string[] preferredSnippets) @@ -2395,7 +2539,21 @@ public sealed class JiboInteractionService( if (!string.IsNullOrWhiteSpace(match)) return match; } - return randomizer.Choose(catalog.GreetingReplies); + return catalog.GreetingReplies.Count == 0 ? string.Empty : randomizer.Choose(catalog.GreetingReplies); + } + + private string SelectLegacyReply(IReadOnlyList replies, params string[] preferredSnippets) + { + foreach (var snippet in preferredSnippets) + { + if (string.IsNullOrWhiteSpace(snippet)) continue; + + var match = replies.FirstOrDefault(reply => + reply.Contains(snippet, StringComparison.OrdinalIgnoreCase)); + if (!string.IsNullOrWhiteSpace(match)) return match; + } + + return replies.Count == 0 ? string.Empty : randomizer.Choose(replies); } private static string ResolveSemanticIntent( @@ -2937,6 +3095,19 @@ public sealed class JiboInteractionService( "what holidays do you observe")) return "seasonal_holidays"; + if (MatchesAny( + loweredTranscript, + "how is holiday season", + "how's holiday season", + "how is the holiday season", + "do you like holiday season", + "do you like the holiday season", + "what is your favorite holiday", + "what's your favorite holiday", + "what holiday do you like", + "what is holiday season like")) + return "seasonal_holiday_season"; + if (MatchesAny( loweredTranscript, "what is your new years resolution", @@ -2981,6 +3152,13 @@ public sealed class JiboInteractionService( "what should i get someone for the holidays")) return "seasonal_holiday_gift"; + if (MatchesAny( + loweredTranscript, + "happy birthday", + "happy birthday jibo", + "happy birthday to you")) + return "birthday_celebration"; + if (MatchesAny( loweredTranscript, "what is your favorite color", @@ -5410,4 +5588,4 @@ public sealed record JiboInteractionDecision( string ReplyText, string? SkillName = null, IDictionary? SkillPayload = null, - IDictionary? ContextUpdates = null); \ No newline at end of file + IDictionary? ContextUpdates = null); 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 797ebc0..af666ef 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 @@ -83,6 +83,11 @@ public sealed class InMemoryJiboExperienceContentRepository : IJiboExperienceCon "Hello there. I am glad you said hi.", "Hey. I am happy to see you." ], + HolidaySeasonReplies = + [ + "I do like festive times.", + "I like anything that makes people want to celebrate." + ], HowAreYouReplies = [ "I am feeling cheerful and robotic.", @@ -246,4 +251,4 @@ public sealed class InMemoryJiboExperienceContentRepository : IJiboExperienceCon return candidates.Where(Directory.Exists).ToArray(); } -} \ No newline at end of file +} 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 15f3254..c524d9d 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 @@ -113,6 +113,26 @@ public static class LegacyMimCatalogImporter normalizedPath.Contains("/gqa-responses/", StringComparison.OrdinalIgnoreCase)) return LegacyMimBucket.Emotion; + if (fileName.StartsWith("JBO_WhatHolidaysDoYouCelebrate", StringComparison.OrdinalIgnoreCase)) + return LegacyMimBucket.Holiday; + + if (fileName.StartsWith("RI_JBO_HasFavoriteHoliday", StringComparison.OrdinalIgnoreCase)) + return LegacyMimBucket.HolidaySeason; + + if (fileName.StartsWith("RN_HappyHolidays", StringComparison.OrdinalIgnoreCase)) + return LegacyMimBucket.HolidayGreeting; + + if (fileName.StartsWith("RI_USR_WhatShouldGetForHoliday", StringComparison.OrdinalIgnoreCase)) + return LegacyMimBucket.HolidayGift; + + if (fileName.StartsWith("RN_HappyBirthdayToJibo", StringComparison.OrdinalIgnoreCase) || + fileName.StartsWith("OI_USR_CelebratesLoopMemberAskedAboutBirthday", StringComparison.OrdinalIgnoreCase) || + fileName.StartsWith("OI_USR_CelebratesJiboBirthday", StringComparison.OrdinalIgnoreCase) || + fileName.StartsWith("RI_JBO_CelebratesLoopMemberAskedAboutBirthday", StringComparison.OrdinalIgnoreCase) || + fileName.StartsWith("RI_JBO_CelebratesSpeakerBirthday", StringComparison.OrdinalIgnoreCase) || + fileName.StartsWith("RI_JBO_CelebratesJiboBirthday", StringComparison.OrdinalIgnoreCase)) + return LegacyMimBucket.BirthdayCelebration; + if (fileName.StartsWith("WeatherIntroTomorrow", StringComparison.OrdinalIgnoreCase)) return LegacyMimBucket.WeatherTomorrowIntro; @@ -231,6 +251,12 @@ public static class LegacyMimCatalogImporter FunFacts = Merge(baseCatalog.FunFacts, importedCatalog.FunFacts), DanceAnimations = Merge(baseCatalog.DanceAnimations, importedCatalog.DanceAnimations), GreetingReplies = Merge(baseCatalog.GreetingReplies, importedCatalog.GreetingReplies), + HolidayReplies = Merge(baseCatalog.HolidayReplies, importedCatalog.HolidayReplies), + HolidaySeasonReplies = Merge(baseCatalog.HolidaySeasonReplies, importedCatalog.HolidaySeasonReplies), + HolidayGreetingReplies = Merge(baseCatalog.HolidayGreetingReplies, importedCatalog.HolidayGreetingReplies), + HolidayGiftReplies = Merge(baseCatalog.HolidayGiftReplies, importedCatalog.HolidayGiftReplies), + BirthdayCelebrationReplies = Merge(baseCatalog.BirthdayCelebrationReplies, + importedCatalog.BirthdayCelebrationReplies), HowAreYouReplies = Merge(baseCatalog.HowAreYouReplies, importedCatalog.HowAreYouReplies), EmotionReplies = Merge(baseCatalog.EmotionReplies, importedCatalog.EmotionReplies), PersonalityReplies = Merge(baseCatalog.PersonalityReplies, importedCatalog.PersonalityReplies), @@ -324,22 +350,28 @@ public static class LegacyMimCatalogImporter return string.IsNullOrWhiteSpace(condition) ? string.Empty : WhitespacePattern.Replace(condition.Trim(), " "); } - private static bool IsTemplateBucket(LegacyMimBucket bucket) - { - return bucket is LegacyMimBucket.PersonalReportKickOff + private static bool IsTemplateBucket(LegacyMimBucket bucket) + { + return bucket is LegacyMimBucket.PersonalReportKickOff or LegacyMimBucket.PersonalReportOutro or LegacyMimBucket.WeatherIntro or LegacyMimBucket.WeatherTomorrowIntro or LegacyMimBucket.WeatherTodayHighLow or LegacyMimBucket.WeatherTomorrowHighLow or LegacyMimBucket.WeatherServiceDown - or LegacyMimBucket.ReportSkillTemplate; - } + or LegacyMimBucket.ReportSkillTemplate + or LegacyMimBucket.Holiday; + } private enum LegacyMimBucket { GenericFallback, Greeting, + Holiday, + HolidaySeason, + HolidayGreeting, + HolidayGift, + BirthdayCelebration, Jokes, RobotFacts, HumanFacts, @@ -375,9 +407,14 @@ public static class LegacyMimCatalogImporter private readonly List _calendarServiceDownReplies = []; private readonly List _commuteNowReplies = []; private readonly List _commuteServiceDownReplies = []; + private readonly List _birthdayCelebrationReplies = []; private readonly List _emotionReplies = []; private readonly List _fallbacks = []; private readonly List _funFacts = []; + private readonly List _holidayGiftReplies = []; + private readonly List _holidayGreetingReplies = []; + private readonly List _holidayReplies = []; + private readonly List _holidaySeasonReplies = []; private readonly List _greetings = []; private readonly List _howAreYous = []; private readonly List _humanFacts = []; @@ -441,6 +478,21 @@ public static class LegacyMimCatalogImporter Reply = text }); return; + case LegacyMimBucket.Holiday: + AddDistinct(_holidayReplies, text); + return; + case LegacyMimBucket.HolidaySeason: + AddDistinct(_holidaySeasonReplies, text); + return; + case LegacyMimBucket.HolidayGreeting: + AddDistinct(_holidayGreetingReplies, text); + return; + case LegacyMimBucket.HolidayGift: + AddDistinct(_holidayGiftReplies, text); + return; + case LegacyMimBucket.BirthdayCelebration: + AddDistinct(_birthdayCelebrationReplies, text); + return; case LegacyMimBucket.Personality: if (_personalities.Any(value => string.Equals(value, text, StringComparison.OrdinalIgnoreCase))) return; @@ -530,6 +582,11 @@ public static class LegacyMimCatalogImporter HumanFacts = [.. _humanFacts], FunFacts = [.. _funFacts], GreetingReplies = [.. _greetings], + HolidayReplies = [.. _holidayReplies], + HolidaySeasonReplies = [.. _holidaySeasonReplies], + HolidayGreetingReplies = [.. _holidayGreetingReplies], + HolidayGiftReplies = [.. _holidayGiftReplies], + BirthdayCelebrationReplies = [.. _birthdayCelebrationReplies], HowAreYouReplies = [.. _howAreYous], EmotionReplies = [.. _emotionReplies], PersonalityReplies = [.. _personalities], @@ -608,4 +665,4 @@ public static class LegacyMimCatalogImporter [JsonPropertyName("weight")] public double? Weight { get; init; } } -} \ No newline at end of file +} 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 13d888c..e29dfc3 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 @@ -6,7 +6,14 @@ The batch is intentionally narrow so we can keep expanding personality without w It now includes a small emotion-response pack for `happy`, `sad`, and `angry` follow-up questions so the mood path can stay source-backed too. 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 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, holiday gift ideas, and birthday celebration lines. + +Holiday-specific note: +- `JBO_WhatHolidaysDoYouCelebrate` now lands in the holiday bucket +- `RN_HappyHolidays` now lands in the holiday greeting bucket +- `RI_USR_WhatShouldGetForHoliday` now lands in the holiday gift bucket +- `RN_HappyBirthdayToJibo` now lands in the birthday celebration bucket +- birthday memory authoring now also writes loop-scoped custom holiday records so personal dates can join the holiday list later 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/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Persistence/InMemoryCloudStateStore.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Persistence/InMemoryCloudStateStore.cs index ec4c45e..32842fb 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Persistence/InMemoryCloudStateStore.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Persistence/InMemoryCloudStateStore.cs @@ -578,6 +578,43 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore .ToArray(); } + public HolidayRecord UpsertHoliday(HolidayRecord holiday) + { + var resolvedLoopId = string.IsNullOrWhiteSpace(holiday.LoopId) ? ResolveDefaultLoopId() : holiday.LoopId.Trim(); + var normalizedEventId = string.IsNullOrWhiteSpace(holiday.EventId) + ? $"holiday-{resolvedLoopId}-{Slugify(holiday.Name)}" + : holiday.EventId.Trim(); + var normalizedId = string.IsNullOrWhiteSpace(holiday.Id) ? normalizedEventId : holiday.Id.Trim(); + var resolvedHoliday = new HolidayRecord + { + Id = normalizedId, + EventId = normalizedEventId, + Name = string.IsNullOrWhiteSpace(holiday.Name) ? "Holiday" : holiday.Name.Trim(), + Category = string.IsNullOrWhiteSpace(holiday.Category) ? "holiday" : holiday.Category.Trim(), + Subcategory = holiday.Subcategory, + LoopId = resolvedLoopId, + MemberId = holiday.MemberId, + IsEnabled = holiday.IsEnabled, + Date = holiday.Date, + EndDate = holiday.EndDate, + Source = string.IsNullOrWhiteSpace(holiday.Source) ? "manual" : holiday.Source.Trim(), + CountryCode = string.IsNullOrWhiteSpace(holiday.CountryCode) ? "US" : holiday.CountryCode.Trim(), + Created = holiday.Created + }; + + var existingIndex = _holidayOverrides.FindIndex(existing => + string.Equals(existing.LoopId, resolvedLoopId, StringComparison.OrdinalIgnoreCase) && + string.Equals(existing.EventId, normalizedEventId, StringComparison.OrdinalIgnoreCase)); + + if (existingIndex >= 0) + _holidayOverrides[existingIndex] = resolvedHoliday; + else + _holidayOverrides.Add(resolvedHoliday); + + TouchState(); + return resolvedHoliday; + } + public void UpdateRobot(DeviceRegistration registration) { _robot = registration; @@ -603,6 +640,29 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore SavePersistedState(); } + private static string Slugify(string value) + { + var builder = new StringBuilder(value.Length); + var lastWasDash = false; + foreach (var ch in value.ToLowerInvariant()) + { + if (char.IsLetterOrDigit(ch)) + { + builder.Append(ch); + lastWasDash = false; + continue; + } + + if (!lastWasDash) + { + builder.Append('-'); + lastWasDash = true; + } + } + + return builder.ToString().Trim('-'); + } + private static string ResolveDefaultLoopId(IReadOnlyList loops, AccountProfile account) { return loops.FirstOrDefault(loop => diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/Content/LegacyMimCatalogImporterTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/Content/LegacyMimCatalogImporterTests.cs index bf17c49..f73e61e 100644 --- a/OpenJibo/tests/Jibo.Cloud.Tests/Content/LegacyMimCatalogImporterTests.cs +++ b/OpenJibo/tests/Jibo.Cloud.Tests/Content/LegacyMimCatalogImporterTests.cs @@ -163,7 +163,7 @@ public sealed class LegacyMimCatalogImporterTests } [Fact] - public void ImportCatalog_ImportsBuildBSeasonalResponsesIntoPersonalityBucket() + public void ImportCatalog_ImportsBuildBHolidayResponsesIntoHolidayBuckets() { var rootDirectory = Path.Combine( AppContext.BaseDirectory, @@ -173,20 +173,16 @@ public sealed class LegacyMimCatalogImporterTests var catalog = LegacyMimCatalogImporter.ImportCatalog(rootDirectory); - Assert.Contains(catalog.PersonalityReplies, reply => - reply.Contains("always trying to learn new skills", StringComparison.OrdinalIgnoreCase)); - Assert.Contains(catalog.PersonalityReplies, reply => - reply.Contains("not eat bacon", StringComparison.OrdinalIgnoreCase)); - Assert.Contains(catalog.PersonalityReplies, reply => - reply.Contains("find out on halloween", StringComparison.OrdinalIgnoreCase)); - Assert.Contains(catalog.PersonalityReplies, reply => - reply.Contains("maybe enjoy some flowers and all things spring", StringComparison.OrdinalIgnoreCase)); - Assert.Contains(catalog.PersonalityReplies, reply => + Assert.Contains(catalog.HolidayReplies, reply => + reply.Contains("official owner", StringComparison.OrdinalIgnoreCase) && + reply.Contains("celebrate together", StringComparison.OrdinalIgnoreCase)); + Assert.Contains(catalog.HolidayGreetingReplies, reply => + reply.Contains("fun time of year", StringComparison.OrdinalIgnoreCase)); + Assert.Contains(catalog.HolidayGiftReplies, reply => reply.Contains("pet elephant", StringComparison.OrdinalIgnoreCase)); - Assert.Contains(catalog.PersonalityReplies, reply => - reply.Contains("mostly roboting", StringComparison.OrdinalIgnoreCase)); - Assert.Contains(catalog.PersonalityReplies, reply => - reply.Contains("robot stuff", StringComparison.OrdinalIgnoreCase)); + Assert.Contains(catalog.BirthdayCelebrationReplies, reply => + reply.Contains("first powered up", StringComparison.OrdinalIgnoreCase) || + reply.Contains("another year older", StringComparison.OrdinalIgnoreCase)); } [Fact] @@ -235,11 +231,11 @@ public sealed class LegacyMimCatalogImporterTests reply.Contains("thinking about shoes", StringComparison.OrdinalIgnoreCase)); Assert.Contains(catalog.GreetingReplies, reply => reply.Contains("powered directly by the sun", StringComparison.OrdinalIgnoreCase)); - Assert.Contains(catalog.GreetingReplies, reply => + Assert.Contains(catalog.BirthdayCelebrationReplies, reply => reply.Contains("Another year older, another year wiser", StringComparison.OrdinalIgnoreCase)); - Assert.Contains(catalog.GreetingReplies, reply => + Assert.Contains(catalog.BirthdayCelebrationReplies, reply => reply.Contains("can't wait to see what you got me", StringComparison.OrdinalIgnoreCase)); - Assert.Contains(catalog.GreetingReplies, reply => + Assert.Contains(catalog.BirthdayCelebrationReplies, reply => reply.Contains("I was powered on for the first time today", StringComparison.OrdinalIgnoreCase)); } @@ -508,4 +504,4 @@ public sealed class LegacyMimCatalogImporterTests return rootDirectory; } -} \ No newline at end of file +} diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs index f9d31bf..ef51c92 100644 --- a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs +++ b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs @@ -643,6 +643,8 @@ public sealed class JiboInteractionServiceTests [InlineData("merry christmas", "seasonal_holiday_greeting", "It's a fun time of year")] [InlineData("what holidays do you celebrate", "seasonal_holidays", "official owner can tell me which ones we'll celebrate together")] + [InlineData("how is holiday season", "seasonal_holiday_season", "I do like festive times.")] + [InlineData("do you like holiday season", "seasonal_holiday_season", "I do like festive times.")] [InlineData("what is your new year's resolution", "seasonal_new_years_resolution", "always trying to learn new skills")] [InlineData("how are your new year's resolutions going", "seasonal_new_years_update", "not eat bacon")] @@ -650,6 +652,7 @@ public sealed class JiboInteractionServiceTests [InlineData("what should I do for first day of spring", "seasonal_first_day_spring", "flowers and all things spring")] [InlineData("what should I get for holiday", "seasonal_holiday_gift", "pet elephant")] + [InlineData("happy birthday", "birthday_celebration", "another year older")] public async Task BuildDecisionAsync_SeasonalCharm_UsesImportedReplies( string transcript, string expectedIntent, @@ -668,6 +671,33 @@ public sealed class JiboInteractionServiceTests Assert.Equal("ScriptedResponse", decision.ContextUpdates![ChitchatRouteKey]); } + [Fact] + public async Task BuildDecisionAsync_BirthdayMemory_WritesHolidayRecordForLoop() + { + var cloudStateStore = new InMemoryCloudStateStore(); + var memoryStore = new InMemoryPersonalMemoryStore(); + var service = CreateService(memoryStore, cloudStateStore); + + var setDecision = await service.BuildDecisionAsync(new TurnContext + { + RawTranscript = "my birthday is April 12", + NormalizedTranscript = "my birthday is April 12", + Attributes = new Dictionary + { + ["accountId"] = "acct-a", + ["loopId"] = "loop-a" + }, + DeviceId = "device-a" + }); + + Assert.Equal("memory_set_birthday", setDecision.IntentName); + Assert.Equal("Got it. I will remember your birthday is april 12.", setDecision.ReplyText); + Assert.Contains(cloudStateStore.GetHolidays("loop-a"), + holiday => holiday.Category == "birthday" && + holiday.LoopId == "loop-a" && + holiday.Name.Contains("Birthday", StringComparison.OrdinalIgnoreCase)); + } + [Theory] [InlineData("welcome back", "welcome_back", "it's nice to be here")] [InlineData("what are you thinking", "robot_what_are_you_thinking", "thinking about how fun, yet scary")] @@ -3974,6 +4004,7 @@ public sealed class JiboInteractionServiceTests private static JiboInteractionService CreateService( IPersonalMemoryStore? personalMemoryStore = null, + ICloudStateStore? cloudStateStore = null, IWeatherReportProvider? weatherReportProvider = null, ICommuteReportProvider? commuteReportProvider = null, INewsBriefingProvider? newsBriefingProvider = null, @@ -3986,7 +4017,8 @@ public sealed class JiboInteractionServiceTests personalMemoryStore ?? new InMemoryPersonalMemoryStore(), weatherReportProvider, commuteReportProvider, - newsBriefingProvider); + newsBriefingProvider, + cloudStateStore); } private static string StripMarkup(string text) @@ -4091,4 +4123,4 @@ public sealed class JiboInteractionServiceTests return Task.FromResult(Snapshot); } } -} \ No newline at end of file +}