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:
- 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

View File

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

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

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 =>

View File

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

View File

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