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

@@ -857,6 +857,7 @@ Current release theme:
- Seasonal charm work in flight: - Seasonal charm work in flight:
- source-backed holiday, New Year's, Halloween, spring, and gift prompts are now part of Build B - 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 - `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: - 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 - `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 - these keep the social surface lively while the memory and multitenant tracks keep advancing in parallel

View File

@@ -20,3 +20,4 @@ Notes:
- `IsEnabled = false` can be used to suppress a holiday later - `IsEnabled = false` can be used to suppress a holiday later
- birthdays and other personal events can be added as loop-scoped custom records - 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 - 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

View File

@@ -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 - 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) - 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 - 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 - 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 - 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 - 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 ### 5. Multi-Tenant Memory Storage Foundation

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ using System.Globalization;
using System.Text.Json; using System.Text.Json;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Jibo.Cloud.Application.Abstractions; using Jibo.Cloud.Application.Abstractions;
using Jibo.Cloud.Domain.Models;
using Jibo.Runtime.Abstractions; using Jibo.Runtime.Abstractions;
namespace Jibo.Cloud.Application.Services; namespace Jibo.Cloud.Application.Services;
@@ -12,7 +13,8 @@ public sealed class JiboInteractionService(
IPersonalMemoryStore personalMemoryStore, IPersonalMemoryStore personalMemoryStore,
IWeatherReportProvider? weatherReportProvider = null, IWeatherReportProvider? weatherReportProvider = null,
ICommuteReportProvider? commuteReportProvider = null, ICommuteReportProvider? commuteReportProvider = null,
INewsBriefingProvider? newsBriefingProvider = null) INewsBriefingProvider? newsBriefingProvider = null,
ICloudStateStore? cloudStateStore = null)
{ {
private const string GreetingRouteMetadataKey = "greetingsRoute"; private const string GreetingRouteMetadataKey = "greetingsRoute";
private const string GreetingSpeakerMetadataKey = "greetingsSpeaker"; private const string GreetingSpeakerMetadataKey = "greetingsSpeaker";
@@ -634,17 +636,25 @@ public sealed class JiboInteractionService(
catalog, catalog,
"robot_is_likable", "robot_is_likable",
"people like me"), "people like me"),
"seasonal_holiday_greeting" => BuildScriptedGreetingDecision( "seasonal_holiday_greeting" => BuildScriptedHolidayGreetingDecision(
catalog, catalog,
"seasonal_holiday_greeting", "seasonal_holiday_greeting",
"It's a fun time of year", "fun time of year",
"And to you too", "right back at you",
"Right back at you"), "and to you too"),
"seasonal_holidays" => BuildScriptedPersonalityDecision( "seasonal_holidays" => BuildScriptedHolidayTemplateDecision(
turn,
greetingPresence,
catalog, catalog,
"seasonal_holidays", "seasonal_holidays",
"official owner can tell me which ones we'll celebrate together", "official owner can tell me which ones we'll celebrate together",
"going to the jibo's settings screen in the jibo app"), "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( "seasonal_new_years_resolution" => BuildScriptedPersonalityDecision(
catalog, catalog,
"seasonal_new_years_resolution", "seasonal_new_years_resolution",
@@ -669,12 +679,18 @@ public sealed class JiboInteractionService(
catalog, catalog,
"seasonal_first_day_spring", "seasonal_first_day_spring",
"maybe enjoy some flowers and all things spring"), "maybe enjoy some flowers and all things spring"),
"seasonal_holiday_gift" => BuildScriptedPersonalityDecision( "seasonal_holiday_gift" => BuildScriptedHolidayDecision(
catalog, catalog.HolidayGiftReplies,
"seasonal_holiday_gift", "seasonal_holiday_gift",
"ask for a pet elephant", "ask for a pet elephant",
"experience as a present", "experience as a present",
"donate to charities in other people's names"), "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( "robot_favorite_flower" => BuildScriptedPersonalityDecision(
catalog, catalog,
"robot_favorite_flower", "robot_favorite_flower",
@@ -974,7 +990,27 @@ public sealed class JiboInteractionService(
"memory_set_birthday", "memory_set_birthday",
"I can remember it if you say, my birthday is March 14."); "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( return new JiboInteractionDecision(
"memory_set_birthday", "memory_set_birthday",
$"Got it. I will remember your birthday is {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}."); $"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) private JiboInteractionDecision BuildRememberImportantDateDecision(TurnContext turn, string transcript)
{ {
var importantDate = TryExtractImportantDateSet(transcript); var importantDate = TryExtractImportantDateSet(transcript);
@@ -2360,6 +2468,42 @@ public sealed class JiboInteractionService(
ContextUpdates: BuildScriptedResponseContextUpdates()); 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() private static IDictionary<string, object?> BuildScriptedResponseContextUpdates()
{ {
return new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase) return new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
@@ -2381,7 +2525,7 @@ public sealed class JiboInteractionService(
if (!string.IsNullOrWhiteSpace(match)) return match; 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) private string SelectLegacyGreetingReply(JiboExperienceCatalog catalog, params string[] preferredSnippets)
@@ -2395,7 +2539,21 @@ public sealed class JiboInteractionService(
if (!string.IsNullOrWhiteSpace(match)) return match; 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( private static string ResolveSemanticIntent(
@@ -2937,6 +3095,19 @@ public sealed class JiboInteractionService(
"what holidays do you observe")) "what holidays do you observe"))
return "seasonal_holidays"; 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( if (MatchesAny(
loweredTranscript, loweredTranscript,
"what is your new years resolution", "what is your new years resolution",
@@ -2981,6 +3152,13 @@ public sealed class JiboInteractionService(
"what should i get someone for the holidays")) "what should i get someone for the holidays"))
return "seasonal_holiday_gift"; return "seasonal_holiday_gift";
if (MatchesAny(
loweredTranscript,
"happy birthday",
"happy birthday jibo",
"happy birthday to you"))
return "birthday_celebration";
if (MatchesAny( if (MatchesAny(
loweredTranscript, loweredTranscript,
"what is your favorite color", "what is your favorite color",

View File

@@ -83,6 +83,11 @@ public sealed class InMemoryJiboExperienceContentRepository : IJiboExperienceCon
"Hello there. I am glad you said hi.", "Hello there. I am glad you said hi.",
"Hey. I am happy to see you." "Hey. I am happy to see you."
], ],
HolidaySeasonReplies =
[
"I do like festive times.",
"I like anything that makes people want to celebrate."
],
HowAreYouReplies = HowAreYouReplies =
[ [
"I am feeling cheerful and robotic.", "I am feeling cheerful and robotic.",

View File

@@ -113,6 +113,26 @@ public static class LegacyMimCatalogImporter
normalizedPath.Contains("/gqa-responses/", StringComparison.OrdinalIgnoreCase)) normalizedPath.Contains("/gqa-responses/", StringComparison.OrdinalIgnoreCase))
return LegacyMimBucket.Emotion; 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)) if (fileName.StartsWith("WeatherIntroTomorrow", StringComparison.OrdinalIgnoreCase))
return LegacyMimBucket.WeatherTomorrowIntro; return LegacyMimBucket.WeatherTomorrowIntro;
@@ -231,6 +251,12 @@ public static class LegacyMimCatalogImporter
FunFacts = Merge(baseCatalog.FunFacts, importedCatalog.FunFacts), FunFacts = Merge(baseCatalog.FunFacts, importedCatalog.FunFacts),
DanceAnimations = Merge(baseCatalog.DanceAnimations, importedCatalog.DanceAnimations), DanceAnimations = Merge(baseCatalog.DanceAnimations, importedCatalog.DanceAnimations),
GreetingReplies = Merge(baseCatalog.GreetingReplies, importedCatalog.GreetingReplies), 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), HowAreYouReplies = Merge(baseCatalog.HowAreYouReplies, importedCatalog.HowAreYouReplies),
EmotionReplies = Merge(baseCatalog.EmotionReplies, importedCatalog.EmotionReplies), EmotionReplies = Merge(baseCatalog.EmotionReplies, importedCatalog.EmotionReplies),
PersonalityReplies = Merge(baseCatalog.PersonalityReplies, importedCatalog.PersonalityReplies), PersonalityReplies = Merge(baseCatalog.PersonalityReplies, importedCatalog.PersonalityReplies),
@@ -324,22 +350,28 @@ public static class LegacyMimCatalogImporter
return string.IsNullOrWhiteSpace(condition) ? string.Empty : WhitespacePattern.Replace(condition.Trim(), " "); return string.IsNullOrWhiteSpace(condition) ? string.Empty : WhitespacePattern.Replace(condition.Trim(), " ");
} }
private static bool IsTemplateBucket(LegacyMimBucket bucket) private static bool IsTemplateBucket(LegacyMimBucket bucket)
{ {
return bucket is LegacyMimBucket.PersonalReportKickOff return bucket is LegacyMimBucket.PersonalReportKickOff
or LegacyMimBucket.PersonalReportOutro or LegacyMimBucket.PersonalReportOutro
or LegacyMimBucket.WeatherIntro or LegacyMimBucket.WeatherIntro
or LegacyMimBucket.WeatherTomorrowIntro or LegacyMimBucket.WeatherTomorrowIntro
or LegacyMimBucket.WeatherTodayHighLow or LegacyMimBucket.WeatherTodayHighLow
or LegacyMimBucket.WeatherTomorrowHighLow or LegacyMimBucket.WeatherTomorrowHighLow
or LegacyMimBucket.WeatherServiceDown or LegacyMimBucket.WeatherServiceDown
or LegacyMimBucket.ReportSkillTemplate; or LegacyMimBucket.ReportSkillTemplate
} or LegacyMimBucket.Holiday;
}
private enum LegacyMimBucket private enum LegacyMimBucket
{ {
GenericFallback, GenericFallback,
Greeting, Greeting,
Holiday,
HolidaySeason,
HolidayGreeting,
HolidayGift,
BirthdayCelebration,
Jokes, Jokes,
RobotFacts, RobotFacts,
HumanFacts, HumanFacts,
@@ -375,9 +407,14 @@ public static class LegacyMimCatalogImporter
private readonly List<string> _calendarServiceDownReplies = []; private readonly List<string> _calendarServiceDownReplies = [];
private readonly List<string> _commuteNowReplies = []; private readonly List<string> _commuteNowReplies = [];
private readonly List<string> _commuteServiceDownReplies = []; private readonly List<string> _commuteServiceDownReplies = [];
private readonly List<string> _birthdayCelebrationReplies = [];
private readonly List<JiboConditionedReply> _emotionReplies = []; private readonly List<JiboConditionedReply> _emotionReplies = [];
private readonly List<string> _fallbacks = []; private readonly List<string> _fallbacks = [];
private readonly List<string> _funFacts = []; 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> _greetings = [];
private readonly List<string> _howAreYous = []; private readonly List<string> _howAreYous = [];
private readonly List<string> _humanFacts = []; private readonly List<string> _humanFacts = [];
@@ -441,6 +478,21 @@ public static class LegacyMimCatalogImporter
Reply = text Reply = text
}); });
return; 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: case LegacyMimBucket.Personality:
if (_personalities.Any(value => string.Equals(value, text, StringComparison.OrdinalIgnoreCase))) if (_personalities.Any(value => string.Equals(value, text, StringComparison.OrdinalIgnoreCase)))
return; return;
@@ -530,6 +582,11 @@ public static class LegacyMimCatalogImporter
HumanFacts = [.. _humanFacts], HumanFacts = [.. _humanFacts],
FunFacts = [.. _funFacts], FunFacts = [.. _funFacts],
GreetingReplies = [.. _greetings], GreetingReplies = [.. _greetings],
HolidayReplies = [.. _holidayReplies],
HolidaySeasonReplies = [.. _holidaySeasonReplies],
HolidayGreetingReplies = [.. _holidayGreetingReplies],
HolidayGiftReplies = [.. _holidayGiftReplies],
BirthdayCelebrationReplies = [.. _birthdayCelebrationReplies],
HowAreYouReplies = [.. _howAreYous], HowAreYouReplies = [.. _howAreYous],
EmotionReplies = [.. _emotionReplies], EmotionReplies = [.. _emotionReplies],
PersonalityReplies = [.. _personalities], PersonalityReplies = [.. _personalities],

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 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`. 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 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. 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. 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(); .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) public void UpdateRobot(DeviceRegistration registration)
{ {
_robot = registration; _robot = registration;
@@ -603,6 +640,29 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
SavePersistedState(); 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) private static string ResolveDefaultLoopId(IReadOnlyList<LoopRecord> loops, AccountProfile account)
{ {
return loops.FirstOrDefault(loop => return loops.FirstOrDefault(loop =>

View File

@@ -163,7 +163,7 @@ public sealed class LegacyMimCatalogImporterTests
} }
[Fact] [Fact]
public void ImportCatalog_ImportsBuildBSeasonalResponsesIntoPersonalityBucket() public void ImportCatalog_ImportsBuildBHolidayResponsesIntoHolidayBuckets()
{ {
var rootDirectory = Path.Combine( var rootDirectory = Path.Combine(
AppContext.BaseDirectory, AppContext.BaseDirectory,
@@ -173,20 +173,16 @@ public sealed class LegacyMimCatalogImporterTests
var catalog = LegacyMimCatalogImporter.ImportCatalog(rootDirectory); var catalog = LegacyMimCatalogImporter.ImportCatalog(rootDirectory);
Assert.Contains(catalog.PersonalityReplies, reply => Assert.Contains(catalog.HolidayReplies, reply =>
reply.Contains("always trying to learn new skills", StringComparison.OrdinalIgnoreCase)); reply.Contains("official owner", StringComparison.OrdinalIgnoreCase) &&
Assert.Contains(catalog.PersonalityReplies, reply => reply.Contains("celebrate together", StringComparison.OrdinalIgnoreCase));
reply.Contains("not eat bacon", StringComparison.OrdinalIgnoreCase)); Assert.Contains(catalog.HolidayGreetingReplies, reply =>
Assert.Contains(catalog.PersonalityReplies, reply => reply.Contains("fun time of year", StringComparison.OrdinalIgnoreCase));
reply.Contains("find out on halloween", StringComparison.OrdinalIgnoreCase)); Assert.Contains(catalog.HolidayGiftReplies, reply =>
Assert.Contains(catalog.PersonalityReplies, reply =>
reply.Contains("maybe enjoy some flowers and all things spring", StringComparison.OrdinalIgnoreCase));
Assert.Contains(catalog.PersonalityReplies, reply =>
reply.Contains("pet elephant", StringComparison.OrdinalIgnoreCase)); reply.Contains("pet elephant", StringComparison.OrdinalIgnoreCase));
Assert.Contains(catalog.PersonalityReplies, reply => Assert.Contains(catalog.BirthdayCelebrationReplies, reply =>
reply.Contains("mostly roboting", StringComparison.OrdinalIgnoreCase)); reply.Contains("first powered up", StringComparison.OrdinalIgnoreCase) ||
Assert.Contains(catalog.PersonalityReplies, reply => reply.Contains("another year older", StringComparison.OrdinalIgnoreCase));
reply.Contains("robot stuff", StringComparison.OrdinalIgnoreCase));
} }
[Fact] [Fact]
@@ -235,11 +231,11 @@ public sealed class LegacyMimCatalogImporterTests
reply.Contains("thinking about shoes", StringComparison.OrdinalIgnoreCase)); reply.Contains("thinking about shoes", StringComparison.OrdinalIgnoreCase));
Assert.Contains(catalog.GreetingReplies, reply => Assert.Contains(catalog.GreetingReplies, reply =>
reply.Contains("powered directly by the sun", StringComparison.OrdinalIgnoreCase)); 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)); 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)); 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)); reply.Contains("I was powered on for the first time today", StringComparison.OrdinalIgnoreCase));
} }

View File

@@ -643,6 +643,8 @@ public sealed class JiboInteractionServiceTests
[InlineData("merry christmas", "seasonal_holiday_greeting", "It's a fun time of year")] [InlineData("merry christmas", "seasonal_holiday_greeting", "It's a fun time of year")]
[InlineData("what holidays do you celebrate", "seasonal_holidays", [InlineData("what holidays do you celebrate", "seasonal_holidays",
"official owner can tell me which ones we'll celebrate together")] "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", [InlineData("what is your new year's resolution", "seasonal_new_years_resolution",
"always trying to learn new skills")] "always trying to learn new skills")]
[InlineData("how are your new year's resolutions going", "seasonal_new_years_update", "not eat bacon")] [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", [InlineData("what should I do for first day of spring", "seasonal_first_day_spring",
"flowers and all things spring")] "flowers and all things spring")]
[InlineData("what should I get for holiday", "seasonal_holiday_gift", "pet elephant")] [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( public async Task BuildDecisionAsync_SeasonalCharm_UsesImportedReplies(
string transcript, string transcript,
string expectedIntent, string expectedIntent,
@@ -668,6 +671,33 @@ public sealed class JiboInteractionServiceTests
Assert.Equal("ScriptedResponse", decision.ContextUpdates![ChitchatRouteKey]); 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<string, object?>
{
["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] [Theory]
[InlineData("welcome back", "welcome_back", "it's nice to be here")] [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")] [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( private static JiboInteractionService CreateService(
IPersonalMemoryStore? personalMemoryStore = null, IPersonalMemoryStore? personalMemoryStore = null,
ICloudStateStore? cloudStateStore = null,
IWeatherReportProvider? weatherReportProvider = null, IWeatherReportProvider? weatherReportProvider = null,
ICommuteReportProvider? commuteReportProvider = null, ICommuteReportProvider? commuteReportProvider = null,
INewsBriefingProvider? newsBriefingProvider = null, INewsBriefingProvider? newsBriefingProvider = null,
@@ -3986,7 +4017,8 @@ public sealed class JiboInteractionServiceTests
personalMemoryStore ?? new InMemoryPersonalMemoryStore(), personalMemoryStore ?? new InMemoryPersonalMemoryStore(),
weatherReportProvider, weatherReportProvider,
commuteReportProvider, commuteReportProvider,
newsBriefingProvider); newsBriefingProvider,
cloudStateStore);
} }
private static string StripMarkup(string text) private static string StripMarkup(string text)