Add birthday and holiday special-day greetings

This commit is contained in:
Jacob Dubin
2026-05-21 12:05:46 -05:00
parent 70b1b1547f
commit e8d7bafcd6
5 changed files with 142 additions and 8 deletions

View File

@@ -723,6 +723,8 @@ Current release theme:
- durable greeting-presence records now persist last-seen and last-greeted per person/loop - durable greeting-presence records now persist last-seen and last-greeted per person/loop
- proactive greeting gating now consults cloud greeting history when available - proactive greeting gating now consults cloud greeting history when available
- reactive and proactive greeting turns write back greeting-history records for later cooldown checks - reactive and proactive greeting turns write back greeting-history records for later cooldown checks
- birthday-aware proactive greetings now use stored birthday memory on matching dates
- holiday-aware proactive greetings now use loop holiday records on matching dates
- Exit criteria: - Exit criteria:
- presence-aware greetings are routed deterministically with tests - presence-aware greetings are routed deterministically with tests
- proactive greetings are frequency-bounded and do not trigger from surprise source when blocked by policy - proactive greetings are frequency-bounded and do not trigger from surprise source when blocked by policy

View File

@@ -64,7 +64,9 @@ Current implementation progress:
- runtime presence parsing now extracts speaker, people-present ids, and loop user first names - runtime presence parsing now extracts speaker, people-present ids, and loop user first names
- reactive and proactive greeting turns now write durable greeting-presence history into cloud state - reactive and proactive greeting turns now write durable greeting-presence history into cloud state
- proactive greeting gating now consults stored greeting history first, then falls back to the current turn metadata - proactive greeting gating now consults stored greeting history first, then falls back to the current turn metadata
- the remaining work is to broaden the presence policy surface so it can grow from reactive/proactive greeting split into richer day-part, birthday, and holiday behavior without reworking the storage seam again - birthday-aware proactive greetings now use the loop/person birthday memory when the current date matches
- holiday-aware proactive greetings now use the loop holiday calendar when the current date matches
- the remaining work is to broaden the presence policy surface so it can grow from reactive/proactive greeting split into richer day-part and return-visit behavior without reworking the storage seam again
## Implementation Slices ## Implementation Slices

View File

@@ -362,7 +362,7 @@ First completed slice in this personal-report parity track:
1. MIM import foundation for personality expansion 1. MIM import foundation for personality expansion
2. Dialog parsing expansion 2. Dialog parsing expansion
3. Presence-aware greetings and identity-triggered proactivity 3. Presence-aware greetings and identity-triggered proactivity
- in progress: durable greeting-presence history and per-person cooldown gating are now in place - in progress: durable greeting-presence history, per-person cooldown gating, and birthday/holiday-aware special-day greetings are now in place
4. Personal report parity slices 4. Personal report parity slices
5. Holidays and seasonal personality slice beyond pizza day 5. Holidays and seasonal personality slice beyond pizza day
6. Durable memory persistence path 6. Durable memory persistence path

View File

@@ -59,14 +59,21 @@ public sealed partial class JiboInteractionService
{ {
var displayName = ResolvePreferredGreetingName(turn, presence); var displayName = ResolvePreferredGreetingName(turn, presence);
var greetingPrefix = ResolveTimeOfDayGreetingPrefix(referenceLocalTime); var greetingPrefix = ResolveTimeOfDayGreetingPrefix(referenceLocalTime);
var replyText = string.IsNullOrWhiteSpace(displayName) var specialGreeting = ResolveSpecialGreetingPrefix(turn, presence, referenceLocalTime);
var route = specialGreeting?.Route ?? "ProactiveGreeting";
var intentName = specialGreeting?.IntentName ?? "proactive_greeting";
var replyText = specialGreeting is null
? string.IsNullOrWhiteSpace(displayName)
? $"{greetingPrefix}. I am glad to see you." ? $"{greetingPrefix}. I am glad to see you."
: $"{greetingPrefix}, {displayName}. Welcome back."; : $"{greetingPrefix}, {displayName}. Welcome back."
RecordGreetingPresence(turn, presence, "ProactiveGreeting", "proactive_greeting", displayName, proactive: true); : string.IsNullOrWhiteSpace(displayName)
? $"{specialGreeting.Prefix}. I am glad to see you."
: $"{specialGreeting.Prefix}, {displayName}. It is nice to celebrate with you.";
RecordGreetingPresence(turn, presence, route, intentName, displayName, proactive: true);
return new JiboInteractionDecision( return new JiboInteractionDecision(
"proactive_greeting", intentName,
replyText, replyText,
ContextUpdates: BuildGreetingContextUpdates("ProactiveGreeting", presence.PrimaryPersonId, true)); ContextUpdates: BuildGreetingContextUpdates(route, presence.PrimaryPersonId, true));
} }
private static string BuildReactiveGreetingReply( private static string BuildReactiveGreetingReply(
@@ -208,6 +215,8 @@ public sealed partial class JiboInteractionService
}); });
} }
private sealed record SpecialGreetingPrefix(string Route, string IntentName, string Prefix);
private static string ResolveTimeOfDayGreetingPrefix(DateTimeOffset? referenceLocalTime) private static string ResolveTimeOfDayGreetingPrefix(DateTimeOffset? referenceLocalTime)
{ {
var hour = (referenceLocalTime ?? DateTimeOffset.UtcNow).Hour; var hour = (referenceLocalTime ?? DateTimeOffset.UtcNow).Hour;
@@ -219,6 +228,58 @@ public sealed partial class JiboInteractionService
}; };
} }
private SpecialGreetingPrefix? ResolveSpecialGreetingPrefix(
TurnContext turn,
GreetingPresenceProfile presence,
DateTimeOffset? referenceLocalTime)
{
var today = DateOnly.FromDateTime((referenceLocalTime ?? DateTimeOffset.UtcNow).Date);
var birthday = ResolveBirthdayGreeting(turn, presence, today);
if (birthday is not null) return birthday;
return ResolveHolidayGreeting(turn, today);
}
private SpecialGreetingPrefix? ResolveBirthdayGreeting(
TurnContext turn,
GreetingPresenceProfile presence,
DateOnly today)
{
var identityScope = !string.IsNullOrWhiteSpace(presence.PrimaryPersonId)
? ResolveTenantScope(turn, presence.PrimaryPersonId)
: ResolveTenantScope(turn);
var birthdayText = personalMemoryStore.GetBirthday(identityScope) ??
personalMemoryStore.GetBirthday(ResolveTenantScope(turn));
if (string.IsNullOrWhiteSpace(birthdayText)) return null;
var birthdayDate = TryParseBirthdayDate(birthdayText);
if (birthdayDate is null) return null;
return birthdayDate.Value.Month == today.Month && birthdayDate.Value.Day == today.Day
? new SpecialGreetingPrefix("ProactiveBirthdayGreeting", "proactive_birthday_greeting",
"Happy birthday")
: null;
}
private SpecialGreetingPrefix? ResolveHolidayGreeting(TurnContext turn, DateOnly today)
{
if (cloudStateStore is null) return null;
var loopId = ReadTenantAttribute(turn, "loopId") ?? "openjibo-default-loop";
var holiday = cloudStateStore.GetHolidays(loopId)
.FirstOrDefault(item =>
item.IsEnabled &&
item.Category != "birthday" &&
item.Date.Month == today.Month &&
item.Date.Day == today.Day);
return holiday is null
? null
: new SpecialGreetingPrefix("ProactiveHolidayGreeting", "proactive_holiday_greeting",
"Happy holidays");
}
private JiboInteractionDecision BuildPizzaDecision() private JiboInteractionDecision BuildPizzaDecision()
{ {
return BuildPizzaAnimationDecision("pizza", "One pizza, coming right up."); return BuildPizzaAnimationDecision("pizza", "One pizza, coming right up.");

View File

@@ -348,6 +348,75 @@ public sealed class JiboInteractionServiceTests
greeting.LastGreetingIntent == "proactive_greeting"); greeting.LastGreetingIntent == "proactive_greeting");
} }
[Fact]
public async Task BuildDecisionAsync_TriggerOnBirthday_BuildsBirthdayGreeting()
{
var memoryStore = new InMemoryPersonalMemoryStore();
memoryStore.SetName(new PersonalMemoryTenantScope("acct-bday", "loop-bday", "device-bday", "person-7"),
"jake");
memoryStore.SetBirthday(new PersonalMemoryTenantScope("acct-bday", "loop-bday", "device-bday", "person-7"),
"March 14");
var cloudStateStore = new InMemoryCloudStateStore();
var service = CreateService(memoryStore, cloudStateStore);
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = string.Empty,
NormalizedTranscript = string.Empty,
Attributes = new Dictionary<string, object?>
{
["accountId"] = "acct-bday",
["loopId"] = "loop-bday",
["messageType"] = "TRIGGER",
["triggerSource"] = "PRESENCE",
["context"] =
"""{"runtime":{"location":{"iso":"2026-03-14T09:00:00-05:00"},"perception":{"speaker":"person-7","peoplePresent":[{"id":"person-7"}]},"loop":{"users":[{"id":"person-7","firstName":"jake"}]}}}"""
},
DeviceId = "device-bday"
});
Assert.Equal("proactive_birthday_greeting", decision.IntentName);
Assert.Contains("Happy birthday", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
Assert.Contains("Jake", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
Assert.Equal("ProactiveBirthdayGreeting", decision.ContextUpdates![GreetingRouteKey]);
}
[Fact]
public async Task BuildDecisionAsync_TriggerOnHoliday_BuildsHolidayGreeting()
{
var cloudStateStore = new InMemoryCloudStateStore();
cloudStateStore.UpsertHoliday(new HolidayRecord
{
LoopId = "loop-holiday",
Name = "Christmas",
Category = "holiday",
Date = new DateOnly(2026, 12, 25),
IsEnabled = true,
Source = "manual",
CountryCode = "US"
});
var service = CreateService(cloudStateStore: cloudStateStore);
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = string.Empty,
NormalizedTranscript = string.Empty,
Attributes = new Dictionary<string, object?>
{
["accountId"] = "acct-holiday",
["loopId"] = "loop-holiday",
["messageType"] = "TRIGGER",
["triggerSource"] = "PRESENCE",
["context"] =
"""{"runtime":{"location":{"iso":"2026-12-25T09:00:00-05:00"},"perception":{"speaker":"person-8","peoplePresent":[{"id":"person-8"}]},"loop":{"users":[{"id":"person-8","firstName":"jake"}]}}}"""
}
});
Assert.Equal("proactive_holiday_greeting", decision.IntentName);
Assert.Contains("Happy holidays", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
Assert.Equal("ProactiveHolidayGreeting", decision.ContextUpdates![GreetingRouteKey]);
}
[Fact] [Fact]
public async Task BuildDecisionAsync_TriggerWithKnownIdentity_SuppressesRepeatGreetingFromCloudHistory() public async Task BuildDecisionAsync_TriggerWithKnownIdentity_SuppressesRepeatGreetingFromCloudHistory()
{ {