Add birthday and holiday special-day greetings
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.");
|
||||||
|
|||||||
@@ -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()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user