Track durable greeting history for presence-aware greetings

This commit is contained in:
Jacob Dubin
2026-05-21 12:01:13 -05:00
parent 4989889608
commit 70b1b1547f
9 changed files with 220 additions and 10 deletions

View File

@@ -48,5 +48,7 @@ public interface ICloudStateStore
CommuteProfileRecord UpsertCommuteProfile(CommuteProfileRecord commuteProfile);
IReadOnlyList<CalendarEventRecord> GetCalendarEvents(string? loopId = null);
CalendarEventRecord UpsertCalendarEvent(CalendarEventRecord calendarEvent);
IReadOnlyList<GreetingPresenceRecord> GetGreetingPresences(string? loopId = null);
GreetingPresenceRecord UpsertGreetingPresence(GreetingPresenceRecord greetingPresence);
void UpdateRobot(DeviceRegistration registration);
}
}

View File

@@ -45,6 +45,7 @@ public sealed partial class JiboInteractionService
var presence = ResolveGreetingPresenceProfile(turn);
var displayName = ResolvePreferredGreetingName(turn, presence);
var replyText = BuildReactiveGreetingReply(greetingIntent, displayName, referenceLocalTime);
RecordGreetingPresence(turn, presence, "ReactiveGreeting", greetingIntent, displayName, proactive: false);
return new JiboInteractionDecision(
greetingIntent,
replyText,
@@ -61,6 +62,7 @@ public sealed partial class JiboInteractionService
var replyText = string.IsNullOrWhiteSpace(displayName)
? $"{greetingPrefix}. I am glad to see you."
: $"{greetingPrefix}, {displayName}. Welcome back.";
RecordGreetingPresence(turn, presence, "ProactiveGreeting", "proactive_greeting", displayName, proactive: true);
return new JiboInteractionDecision(
"proactive_greeting",
replyText,
@@ -113,7 +115,7 @@ public sealed partial class JiboInteractionService
: CultureInfo.InvariantCulture.TextInfo.ToTitleCase(trimmed);
}
private static bool ShouldHandleProactiveGreetingTrigger(
private bool ShouldHandleProactiveGreetingTrigger(
TurnContext turn,
string? triggerSource,
GreetingPresenceProfile presence)
@@ -122,10 +124,32 @@ public sealed partial class JiboInteractionService
if (!presence.HasKnownIdentity) return false;
var lastGreetingUtc = ReadTimestampAttribute(turn, LastProactiveGreetingUtcMetadataKey);
var lastGreetingUtc = ReadGreetingHistoryLastGreetedUtc(turn, presence);
return !lastGreetingUtc.HasValue || DateTimeOffset.UtcNow - lastGreetingUtc.Value >= ProactiveGreetingCooldown;
}
private DateTimeOffset? ReadGreetingHistoryLastGreetedUtc(TurnContext turn, GreetingPresenceProfile presence)
{
var historyIdentity = ResolveGreetingHistoryIdentity(presence);
if (!string.IsNullOrWhiteSpace(historyIdentity) && cloudStateStore is not null)
{
var loopId = ReadTenantAttribute(turn, "loopId") ?? "openjibo-default-loop";
var greetingHistory = cloudStateStore.GetGreetingPresences(loopId)
.FirstOrDefault(record =>
record.PersonId.Equals(historyIdentity, StringComparison.OrdinalIgnoreCase));
if (greetingHistory is not null && greetingHistory.LastGreetedUtc.HasValue)
return greetingHistory.LastGreetedUtc;
}
return ReadTimestampAttribute(turn, LastProactiveGreetingUtcMetadataKey);
}
private static string? ResolveGreetingHistoryIdentity(GreetingPresenceProfile presence)
{
if (!string.IsNullOrWhiteSpace(presence.PrimaryPersonId)) return presence.PrimaryPersonId;
return !string.IsNullOrWhiteSpace(presence.SpeakerId) ? presence.SpeakerId : null;
}
private static DateTimeOffset? ReadTimestampAttribute(TurnContext turn, string key)
{
if (!turn.Attributes.TryGetValue(key, out var value) || value is null) return null;
@@ -155,6 +179,35 @@ public sealed partial class JiboInteractionService
return updates;
}
private void RecordGreetingPresence(
TurnContext turn,
GreetingPresenceProfile presence,
string route,
string intentName,
string? preferredName,
bool proactive)
{
if (cloudStateStore is null) return;
var identityId = ResolveGreetingHistoryIdentity(presence);
if (string.IsNullOrWhiteSpace(identityId)) return;
var now = DateTimeOffset.UtcNow;
var tenantScope = ResolveTenantScope(turn, identityId);
cloudStateStore.UpsertGreetingPresence(new GreetingPresenceRecord
{
AccountId = tenantScope.AccountId,
LoopId = tenantScope.LoopId,
PersonId = identityId,
SpeakerId = presence.SpeakerId,
PreferredName = preferredName,
LastSeenUtc = now,
LastGreetedUtc = now,
LastGreetingRoute = route,
LastGreetingIntent = intentName
});
}
private static string ResolveTimeOfDayGreetingPrefix(DateTimeOffset? referenceLocalTime)
{
var hour = (referenceLocalTime ?? DateTimeOffset.UtcNow).Hour;

View File

@@ -0,0 +1,17 @@
namespace Jibo.Cloud.Domain.Models;
public sealed class GreetingPresenceRecord
{
public string Id { get; init; } = $"greeting-presence-{Guid.NewGuid():N}";
public string AccountId { get; init; } = "usr_openjibo_owner";
public string LoopId { get; init; } = "openjibo-default-loop";
public string PersonId { get; init; } = string.Empty;
public string? SpeakerId { get; init; }
public string? PreferredName { get; init; }
public DateTimeOffset LastSeenUtc { get; init; } = DateTimeOffset.UtcNow;
public DateTimeOffset? LastGreetedUtc { get; init; }
public string? LastGreetingRoute { get; init; }
public string? LastGreetingIntent { get; init; }
public DateTimeOffset CreatedUtc { get; init; } = DateTimeOffset.UtcNow;
public DateTimeOffset UpdatedUtc { get; init; } = DateTimeOffset.UtcNow;
}

View File

@@ -24,6 +24,7 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
private readonly IHolidayCalendarProvider _holidayCalendarProvider;
private readonly List<HolidayRecord> _holidayOverrides = [];
private readonly List<GreetingPresenceRecord> _greetingPresences = [];
private readonly ConcurrentDictionary<string, KeyRequestRecord>
_keyRequests = new(StringComparer.OrdinalIgnoreCase);
@@ -170,6 +171,9 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
_calendarEvents.Clear();
_calendarEvents.AddRange(snapshot.CalendarEvents ?? []);
_greetingPresences.Clear();
_greetingPresences.AddRange(snapshot.GreetingPresences ?? []);
_loops.Clear();
_loops.AddRange(snapshot.Loops ?? []);
@@ -225,6 +229,7 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
Backups = _backups.ToArray(),
CommuteProfiles = _commuteProfiles.ToArray(),
CalendarEvents = _calendarEvents.ToArray(),
GreetingPresences = _greetingPresences.ToArray(),
Loops = _loops.ToArray(),
Holidays = _holidayOverrides.ToArray(),
People = _people.ToArray()
@@ -540,6 +545,66 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
return resolvedCalendarEvent;
}
public IReadOnlyList<GreetingPresenceRecord> GetGreetingPresences(string? loopId = null)
{
var resolvedLoopId = string.IsNullOrWhiteSpace(loopId) ? ResolveDefaultLoopId() : loopId.Trim();
return _greetingPresences
.Where(greeting =>
string.Equals(greeting.LoopId, resolvedLoopId, StringComparison.OrdinalIgnoreCase))
.OrderByDescending(static greeting => greeting.LastGreetedUtc ?? greeting.LastSeenUtc)
.ThenByDescending(static greeting => greeting.UpdatedUtc)
.ToArray();
}
public GreetingPresenceRecord UpsertGreetingPresence(GreetingPresenceRecord greetingPresence)
{
var resolvedLoopId = string.IsNullOrWhiteSpace(greetingPresence.LoopId)
? ResolveDefaultLoopId()
: greetingPresence.LoopId.Trim();
var resolvedPersonId = string.IsNullOrWhiteSpace(greetingPresence.PersonId)
? greetingPresence.SpeakerId?.Trim() ?? "unknown-person"
: greetingPresence.PersonId.Trim();
var normalizedId = string.IsNullOrWhiteSpace(greetingPresence.Id)
? $"greeting-presence-{resolvedLoopId}-{Slugify(resolvedPersonId)}"
: greetingPresence.Id.Trim();
var now = DateTimeOffset.UtcNow;
var resolvedPresence = new GreetingPresenceRecord
{
Id = normalizedId,
AccountId = string.IsNullOrWhiteSpace(greetingPresence.AccountId)
? _account.AccountId
: greetingPresence.AccountId.Trim(),
LoopId = resolvedLoopId,
PersonId = resolvedPersonId,
SpeakerId = string.IsNullOrWhiteSpace(greetingPresence.SpeakerId) ? null : greetingPresence.SpeakerId.Trim(),
PreferredName = string.IsNullOrWhiteSpace(greetingPresence.PreferredName)
? null
: greetingPresence.PreferredName.Trim(),
LastSeenUtc = greetingPresence.LastSeenUtc == default ? now : greetingPresence.LastSeenUtc,
LastGreetedUtc = greetingPresence.LastGreetedUtc,
LastGreetingRoute = string.IsNullOrWhiteSpace(greetingPresence.LastGreetingRoute)
? null
: greetingPresence.LastGreetingRoute.Trim(),
LastGreetingIntent = string.IsNullOrWhiteSpace(greetingPresence.LastGreetingIntent)
? null
: greetingPresence.LastGreetingIntent.Trim(),
CreatedUtc = greetingPresence.CreatedUtc == default ? now : greetingPresence.CreatedUtc,
UpdatedUtc = now
};
var existingIndex = _greetingPresences.FindIndex(existing =>
string.Equals(existing.LoopId, resolvedLoopId, StringComparison.OrdinalIgnoreCase) &&
string.Equals(existing.PersonId, resolvedPersonId, StringComparison.OrdinalIgnoreCase));
if (existingIndex >= 0)
_greetingPresences[existingIndex] = resolvedPresence;
else
_greetingPresences.Add(resolvedPresence);
TouchState();
return resolvedPresence;
}
public bool ShouldCreateSymmetricKey(string loopId)
{
return !_symmetricKeys.ContainsKey(loopId);
@@ -905,6 +970,7 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
public BackupRecord[]? Backups { get; init; }
public CommuteProfileRecord[]? CommuteProfiles { get; init; }
public CalendarEventRecord[]? CalendarEvents { get; init; }
public GreetingPresenceRecord[]? GreetingPresences { get; init; }
public LoopRecord[]? Loops { get; init; }
public HolidayRecord[]? Holidays { get; init; }
public PersonRecord[]? People { get; init; }
@@ -952,4 +1018,4 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
};
}
}
}
}