Track durable greeting history for presence-aware greetings
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user