Add holiday buckets and birthday authoring

This commit is contained in:
Jacob Dubin
2026-05-19 19:12:34 -05:00
parent 2bc6fec1bf
commit 5ad6d4e673
12 changed files with 386 additions and 41 deletions

View File

@@ -43,5 +43,6 @@ public interface ICloudStateStore
IReadOnlyList<KeyRequestRecord> GetIncomingKeyRequests();
IReadOnlyList<KeyRequestRecord> GetBinaryRequests();
IReadOnlyList<HolidayRecord> GetHolidays(string? loopId = null);
HolidayRecord UpsertHoliday(HolidayRecord holiday);
void UpdateRobot(DeviceRegistration registration);
}

View File

@@ -19,6 +19,11 @@ public sealed class JiboExperienceCatalog
public IReadOnlyList<string> FunFacts { get; init; } = [];
public IReadOnlyList<string> DanceAnimations { get; init; } = [];
public IReadOnlyList<string> GreetingReplies { get; init; } = [];
public IReadOnlyList<string> HolidayReplies { get; init; } = [];
public IReadOnlyList<string> HolidaySeasonReplies { get; init; } = [];
public IReadOnlyList<string> HolidayGreetingReplies { get; init; } = [];
public IReadOnlyList<string> HolidayGiftReplies { get; init; } = [];
public IReadOnlyList<string> BirthdayCelebrationReplies { get; init; } = [];
public IReadOnlyList<string> HowAreYouReplies { get; init; } = [];
public IReadOnlyList<JiboConditionedReply> EmotionReplies { get; init; } = [];
public IReadOnlyList<string> PersonalityReplies { get; init; } = [];
@@ -50,4 +55,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

@@ -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(?<month>january|february|march|april|may|june|july|august|september|october|november|december)\s+(?<day>\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<string> 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<string, object?> BuildScriptedResponseContextUpdates()
{
return new Dictionary<string, object?>(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<string> 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<string, object?>? SkillPayload = null,
IDictionary<string, object?>? ContextUpdates = null);
IDictionary<string, object?>? ContextUpdates = null);

View File

@@ -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();
}
}
}

View File

@@ -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<string> _calendarServiceDownReplies = [];
private readonly List<string> _commuteNowReplies = [];
private readonly List<string> _commuteServiceDownReplies = [];
private readonly List<string> _birthdayCelebrationReplies = [];
private readonly List<JiboConditionedReply> _emotionReplies = [];
private readonly List<string> _fallbacks = [];
private readonly List<string> _funFacts = [];
private readonly List<string> _holidayGiftReplies = [];
private readonly List<string> _holidayGreetingReplies = [];
private readonly List<string> _holidayReplies = [];
private readonly List<string> _holidaySeasonReplies = [];
private readonly List<string> _greetings = [];
private readonly List<string> _howAreYous = [];
private readonly List<string> _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; }
}
}
}

View File

@@ -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.

View File

@@ -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<LoopRecord> loops, AccountProfile account)
{
return loops.FirstOrDefault(loop =>