Track durable greeting history for presence-aware greetings
This commit is contained in:
@@ -702,7 +702,7 @@ Current release theme:
|
|||||||
|
|
||||||
### 26. Presence-Aware Greetings And Identity Proactivity
|
### 26. Presence-Aware Greetings And Identity Proactivity
|
||||||
|
|
||||||
- Status: `ready`
|
- Status: `in_progress`
|
||||||
- Tags: `protocol`, `content`, `storage`, `docs`
|
- Tags: `protocol`, `content`, `storage`, `docs`
|
||||||
- Why now:
|
- Why now:
|
||||||
- this is the next personality-charm expansion after parser guardrail and weather bring-up
|
- this is the next personality-charm expansion after parser guardrail and weather bring-up
|
||||||
@@ -719,6 +719,10 @@ Current release theme:
|
|||||||
- add greeting intent families and state-machine split for reactive vs proactive greeting routes
|
- add greeting intent families and state-machine split for reactive vs proactive greeting routes
|
||||||
- add cooldown and trigger-source guardrails for proactive greetings
|
- add cooldown and trigger-source guardrails for proactive greetings
|
||||||
- start person-aware greeting hooks (name-aware greeting, morning greeting policy, return greeting policy)
|
- start person-aware greeting hooks (name-aware greeting, morning greeting policy, return greeting policy)
|
||||||
|
- Shipped so far:
|
||||||
|
- durable greeting-presence records now persist last-seen and last-greeted per person/loop
|
||||||
|
- proactive greeting gating now consults cloud greeting history when available
|
||||||
|
- reactive and proactive greeting turns write back greeting-history records for later cooldown checks
|
||||||
- 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
|
||||||
|
|||||||
@@ -59,6 +59,13 @@ Main gap:
|
|||||||
|
|
||||||
- no first-class presence/identity perception extraction from runtime context for greeting policy decisions
|
- no first-class presence/identity perception extraction from runtime context for greeting policy decisions
|
||||||
|
|
||||||
|
Current implementation progress:
|
||||||
|
|
||||||
|
- 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
|
||||||
|
- 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
|
||||||
|
|
||||||
## Implementation Slices
|
## Implementation Slices
|
||||||
|
|
||||||
### Slice G1: Presence Context Extraction And Session Snapshot
|
### Slice G1: Presence Context Extraction And Session Snapshot
|
||||||
|
|||||||
@@ -362,6 +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
|
||||||
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
|
||||||
|
|||||||
@@ -48,5 +48,7 @@ public interface ICloudStateStore
|
|||||||
CommuteProfileRecord UpsertCommuteProfile(CommuteProfileRecord commuteProfile);
|
CommuteProfileRecord UpsertCommuteProfile(CommuteProfileRecord commuteProfile);
|
||||||
IReadOnlyList<CalendarEventRecord> GetCalendarEvents(string? loopId = null);
|
IReadOnlyList<CalendarEventRecord> GetCalendarEvents(string? loopId = null);
|
||||||
CalendarEventRecord UpsertCalendarEvent(CalendarEventRecord calendarEvent);
|
CalendarEventRecord UpsertCalendarEvent(CalendarEventRecord calendarEvent);
|
||||||
|
IReadOnlyList<GreetingPresenceRecord> GetGreetingPresences(string? loopId = null);
|
||||||
|
GreetingPresenceRecord UpsertGreetingPresence(GreetingPresenceRecord greetingPresence);
|
||||||
void UpdateRobot(DeviceRegistration registration);
|
void UpdateRobot(DeviceRegistration registration);
|
||||||
}
|
}
|
||||||
@@ -45,6 +45,7 @@ public sealed partial class JiboInteractionService
|
|||||||
var presence = ResolveGreetingPresenceProfile(turn);
|
var presence = ResolveGreetingPresenceProfile(turn);
|
||||||
var displayName = ResolvePreferredGreetingName(turn, presence);
|
var displayName = ResolvePreferredGreetingName(turn, presence);
|
||||||
var replyText = BuildReactiveGreetingReply(greetingIntent, displayName, referenceLocalTime);
|
var replyText = BuildReactiveGreetingReply(greetingIntent, displayName, referenceLocalTime);
|
||||||
|
RecordGreetingPresence(turn, presence, "ReactiveGreeting", greetingIntent, displayName, proactive: false);
|
||||||
return new JiboInteractionDecision(
|
return new JiboInteractionDecision(
|
||||||
greetingIntent,
|
greetingIntent,
|
||||||
replyText,
|
replyText,
|
||||||
@@ -61,6 +62,7 @@ public sealed partial class JiboInteractionService
|
|||||||
var replyText = string.IsNullOrWhiteSpace(displayName)
|
var replyText = 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);
|
||||||
return new JiboInteractionDecision(
|
return new JiboInteractionDecision(
|
||||||
"proactive_greeting",
|
"proactive_greeting",
|
||||||
replyText,
|
replyText,
|
||||||
@@ -113,7 +115,7 @@ public sealed partial class JiboInteractionService
|
|||||||
: CultureInfo.InvariantCulture.TextInfo.ToTitleCase(trimmed);
|
: CultureInfo.InvariantCulture.TextInfo.ToTitleCase(trimmed);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool ShouldHandleProactiveGreetingTrigger(
|
private bool ShouldHandleProactiveGreetingTrigger(
|
||||||
TurnContext turn,
|
TurnContext turn,
|
||||||
string? triggerSource,
|
string? triggerSource,
|
||||||
GreetingPresenceProfile presence)
|
GreetingPresenceProfile presence)
|
||||||
@@ -122,10 +124,32 @@ public sealed partial class JiboInteractionService
|
|||||||
|
|
||||||
if (!presence.HasKnownIdentity) return false;
|
if (!presence.HasKnownIdentity) return false;
|
||||||
|
|
||||||
var lastGreetingUtc = ReadTimestampAttribute(turn, LastProactiveGreetingUtcMetadataKey);
|
var lastGreetingUtc = ReadGreetingHistoryLastGreetedUtc(turn, presence);
|
||||||
return !lastGreetingUtc.HasValue || DateTimeOffset.UtcNow - lastGreetingUtc.Value >= ProactiveGreetingCooldown;
|
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)
|
private static DateTimeOffset? ReadTimestampAttribute(TurnContext turn, string key)
|
||||||
{
|
{
|
||||||
if (!turn.Attributes.TryGetValue(key, out var value) || value is null) return null;
|
if (!turn.Attributes.TryGetValue(key, out var value) || value is null) return null;
|
||||||
@@ -155,6 +179,35 @@ public sealed partial class JiboInteractionService
|
|||||||
return updates;
|
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)
|
private static string ResolveTimeOfDayGreetingPrefix(DateTimeOffset? referenceLocalTime)
|
||||||
{
|
{
|
||||||
var hour = (referenceLocalTime ?? DateTimeOffset.UtcNow).Hour;
|
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 IHolidayCalendarProvider _holidayCalendarProvider;
|
||||||
private readonly List<HolidayRecord> _holidayOverrides = [];
|
private readonly List<HolidayRecord> _holidayOverrides = [];
|
||||||
|
private readonly List<GreetingPresenceRecord> _greetingPresences = [];
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<string, KeyRequestRecord>
|
private readonly ConcurrentDictionary<string, KeyRequestRecord>
|
||||||
_keyRequests = new(StringComparer.OrdinalIgnoreCase);
|
_keyRequests = new(StringComparer.OrdinalIgnoreCase);
|
||||||
@@ -170,6 +171,9 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
|
|||||||
_calendarEvents.Clear();
|
_calendarEvents.Clear();
|
||||||
_calendarEvents.AddRange(snapshot.CalendarEvents ?? []);
|
_calendarEvents.AddRange(snapshot.CalendarEvents ?? []);
|
||||||
|
|
||||||
|
_greetingPresences.Clear();
|
||||||
|
_greetingPresences.AddRange(snapshot.GreetingPresences ?? []);
|
||||||
|
|
||||||
_loops.Clear();
|
_loops.Clear();
|
||||||
_loops.AddRange(snapshot.Loops ?? []);
|
_loops.AddRange(snapshot.Loops ?? []);
|
||||||
|
|
||||||
@@ -225,6 +229,7 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
|
|||||||
Backups = _backups.ToArray(),
|
Backups = _backups.ToArray(),
|
||||||
CommuteProfiles = _commuteProfiles.ToArray(),
|
CommuteProfiles = _commuteProfiles.ToArray(),
|
||||||
CalendarEvents = _calendarEvents.ToArray(),
|
CalendarEvents = _calendarEvents.ToArray(),
|
||||||
|
GreetingPresences = _greetingPresences.ToArray(),
|
||||||
Loops = _loops.ToArray(),
|
Loops = _loops.ToArray(),
|
||||||
Holidays = _holidayOverrides.ToArray(),
|
Holidays = _holidayOverrides.ToArray(),
|
||||||
People = _people.ToArray()
|
People = _people.ToArray()
|
||||||
@@ -540,6 +545,66 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
|
|||||||
return resolvedCalendarEvent;
|
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)
|
public bool ShouldCreateSymmetricKey(string loopId)
|
||||||
{
|
{
|
||||||
return !_symmetricKeys.ContainsKey(loopId);
|
return !_symmetricKeys.ContainsKey(loopId);
|
||||||
@@ -905,6 +970,7 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
|
|||||||
public BackupRecord[]? Backups { get; init; }
|
public BackupRecord[]? Backups { get; init; }
|
||||||
public CommuteProfileRecord[]? CommuteProfiles { get; init; }
|
public CommuteProfileRecord[]? CommuteProfiles { get; init; }
|
||||||
public CalendarEventRecord[]? CalendarEvents { get; init; }
|
public CalendarEventRecord[]? CalendarEvents { get; init; }
|
||||||
|
public GreetingPresenceRecord[]? GreetingPresences { get; init; }
|
||||||
public LoopRecord[]? Loops { get; init; }
|
public LoopRecord[]? Loops { get; init; }
|
||||||
public HolidayRecord[]? Holidays { get; init; }
|
public HolidayRecord[]? Holidays { get; init; }
|
||||||
public PersonRecord[]? People { get; init; }
|
public PersonRecord[]? People { get; init; }
|
||||||
|
|||||||
@@ -119,6 +119,17 @@ public sealed class PersistenceStoreTests
|
|||||||
TimeLabel = "at 6:00 p.m.",
|
TimeLabel = "at 6:00 p.m.",
|
||||||
Date = DateOnly.FromDateTime(DateTime.UtcNow)
|
Date = DateOnly.FromDateTime(DateTime.UtcNow)
|
||||||
});
|
});
|
||||||
|
var greetingPresence = firstStore.UpsertGreetingPresence(new GreetingPresenceRecord
|
||||||
|
{
|
||||||
|
LoopId = "openjibo-default-loop",
|
||||||
|
PersonId = "person-1",
|
||||||
|
SpeakerId = "person-1",
|
||||||
|
PreferredName = "Jake",
|
||||||
|
LastSeenUtc = DateTimeOffset.UtcNow.AddMinutes(-5),
|
||||||
|
LastGreetedUtc = DateTimeOffset.UtcNow.AddMinutes(-4),
|
||||||
|
LastGreetingRoute = "ProactiveGreeting",
|
||||||
|
LastGreetingIntent = "proactive_greeting"
|
||||||
|
});
|
||||||
var sessionToken = firstStore.IssueRobotToken("robot-123");
|
var sessionToken = firstStore.IssueRobotToken("robot-123");
|
||||||
var device = firstStore.GetOrCreateDevice("robot-123", "3.2.1", "4.5.6");
|
var device = firstStore.GetOrCreateDevice("robot-123", "3.2.1", "4.5.6");
|
||||||
firstStore.SavePersistedState();
|
firstStore.SavePersistedState();
|
||||||
@@ -137,6 +148,10 @@ public sealed class PersistenceStoreTests
|
|||||||
item => item.Id == commute.Id && item.Mode == commute.Mode);
|
item => item.Id == commute.Id && item.Mode == commute.Mode);
|
||||||
Assert.Contains(secondStore.GetCalendarEvents("openjibo-default-loop"),
|
Assert.Contains(secondStore.GetCalendarEvents("openjibo-default-loop"),
|
||||||
item => item.Id == calendarEvent.Id && item.Summary == calendarEvent.Summary);
|
item => item.Id == calendarEvent.Id && item.Summary == calendarEvent.Summary);
|
||||||
|
Assert.Contains(secondStore.GetGreetingPresences("openjibo-default-loop"),
|
||||||
|
item => item.PersonId == greetingPresence.PersonId &&
|
||||||
|
item.PreferredName == greetingPresence.PreferredName &&
|
||||||
|
item.LastGreetingRoute == greetingPresence.LastGreetingRoute);
|
||||||
Assert.NotNull(secondStore.FindSessionByToken(sessionToken));
|
Assert.NotNull(secondStore.FindSessionByToken(sessionToken));
|
||||||
Assert.Equal("3.2.1", secondStore.GetOrCreateDevice(device.DeviceId, null, null).FirmwareVersion);
|
Assert.Equal("3.2.1", secondStore.GetOrCreateDevice(device.DeviceId, null, null).FirmwareVersion);
|
||||||
Assert.NotEmpty(secondStore.GetPeople());
|
Assert.NotEmpty(secondStore.GetPeople());
|
||||||
|
|||||||
@@ -168,8 +168,9 @@ public sealed class JiboInteractionServiceTests
|
|||||||
public async Task BuildDecisionAsync_GoodMorning_UsesReactiveGreetingWithRememberedName()
|
public async Task BuildDecisionAsync_GoodMorning_UsesReactiveGreetingWithRememberedName()
|
||||||
{
|
{
|
||||||
var memoryStore = new InMemoryPersonalMemoryStore();
|
var memoryStore = new InMemoryPersonalMemoryStore();
|
||||||
|
var cloudStateStore = new InMemoryCloudStateStore();
|
||||||
memoryStore.SetName(new PersonalMemoryTenantScope("acct-a", "loop-a", "device-a"), "jake");
|
memoryStore.SetName(new PersonalMemoryTenantScope("acct-a", "loop-a", "device-a"), "jake");
|
||||||
var service = CreateService(memoryStore);
|
var service = CreateService(memoryStore, cloudStateStore);
|
||||||
|
|
||||||
var decision = await service.BuildDecisionAsync(new TurnContext
|
var decision = await service.BuildDecisionAsync(new TurnContext
|
||||||
{
|
{
|
||||||
@@ -178,7 +179,9 @@ public sealed class JiboInteractionServiceTests
|
|||||||
Attributes = new Dictionary<string, object?>
|
Attributes = new Dictionary<string, object?>
|
||||||
{
|
{
|
||||||
["accountId"] = "acct-a",
|
["accountId"] = "acct-a",
|
||||||
["loopId"] = "loop-a"
|
["loopId"] = "loop-a",
|
||||||
|
["context"] =
|
||||||
|
"""{"runtime":{"perception":{"speaker":"person-a","peoplePresent":[{"id":"person-a"}]},"loop":{"users":[{"id":"person-a","firstName":"jake"}]}}}"""
|
||||||
},
|
},
|
||||||
DeviceId = "device-a"
|
DeviceId = "device-a"
|
||||||
});
|
});
|
||||||
@@ -187,8 +190,12 @@ public sealed class JiboInteractionServiceTests
|
|||||||
Assert.Equal("Good morning, Jake. It is great to see you.", decision.ReplyText);
|
Assert.Equal("Good morning, Jake. It is great to see you.", decision.ReplyText);
|
||||||
Assert.NotNull(decision.ContextUpdates);
|
Assert.NotNull(decision.ContextUpdates);
|
||||||
Assert.Equal("ReactiveGreeting", decision.ContextUpdates![GreetingRouteKey]);
|
Assert.Equal("ReactiveGreeting", decision.ContextUpdates![GreetingRouteKey]);
|
||||||
Assert.Equal(string.Empty, decision.ContextUpdates[GreetingSpeakerKey]);
|
Assert.Equal("person-a", decision.ContextUpdates[GreetingSpeakerKey]);
|
||||||
Assert.True(DateTimeOffset.TryParse(decision.ContextUpdates[GreetingLastReactiveUtcKey]?.ToString(), out _));
|
Assert.True(DateTimeOffset.TryParse(decision.ContextUpdates[GreetingLastReactiveUtcKey]?.ToString(), out _));
|
||||||
|
Assert.Contains(cloudStateStore.GetGreetingPresences("loop-a"),
|
||||||
|
greeting => greeting.PersonId == "person-a" &&
|
||||||
|
greeting.LastGreetingRoute == "ReactiveGreeting" &&
|
||||||
|
greeting.LastGreetingIntent == "good_morning");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -313,7 +320,8 @@ public sealed class JiboInteractionServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task BuildDecisionAsync_TriggerWithKnownIdentity_BuildsProactiveGreetingAndContext()
|
public async Task BuildDecisionAsync_TriggerWithKnownIdentity_BuildsProactiveGreetingAndContext()
|
||||||
{
|
{
|
||||||
var service = CreateService();
|
var cloudStateStore = new InMemoryCloudStateStore();
|
||||||
|
var service = CreateService(cloudStateStore: cloudStateStore);
|
||||||
|
|
||||||
var decision = await service.BuildDecisionAsync(new TurnContext
|
var decision = await service.BuildDecisionAsync(new TurnContext
|
||||||
{
|
{
|
||||||
@@ -334,6 +342,43 @@ public sealed class JiboInteractionServiceTests
|
|||||||
Assert.Equal("ProactiveGreeting", decision.ContextUpdates![GreetingRouteKey]);
|
Assert.Equal("ProactiveGreeting", decision.ContextUpdates![GreetingRouteKey]);
|
||||||
Assert.Equal("person-1", decision.ContextUpdates[GreetingSpeakerKey]);
|
Assert.Equal("person-1", decision.ContextUpdates[GreetingSpeakerKey]);
|
||||||
Assert.True(DateTimeOffset.TryParse(decision.ContextUpdates[GreetingLastProactiveUtcKey]?.ToString(), out _));
|
Assert.True(DateTimeOffset.TryParse(decision.ContextUpdates[GreetingLastProactiveUtcKey]?.ToString(), out _));
|
||||||
|
Assert.Contains(cloudStateStore.GetGreetingPresences("openjibo-default-loop"),
|
||||||
|
greeting => greeting.PersonId == "person-1" &&
|
||||||
|
greeting.LastGreetingRoute == "ProactiveGreeting" &&
|
||||||
|
greeting.LastGreetingIntent == "proactive_greeting");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BuildDecisionAsync_TriggerWithKnownIdentity_SuppressesRepeatGreetingFromCloudHistory()
|
||||||
|
{
|
||||||
|
var cloudStateStore = new InMemoryCloudStateStore();
|
||||||
|
cloudStateStore.UpsertGreetingPresence(new GreetingPresenceRecord
|
||||||
|
{
|
||||||
|
LoopId = "loop-history",
|
||||||
|
PersonId = "person-1",
|
||||||
|
SpeakerId = "person-1",
|
||||||
|
LastSeenUtc = DateTimeOffset.UtcNow.AddMinutes(-10),
|
||||||
|
LastGreetedUtc = DateTimeOffset.UtcNow,
|
||||||
|
LastGreetingRoute = "ProactiveGreeting",
|
||||||
|
LastGreetingIntent = "proactive_greeting"
|
||||||
|
});
|
||||||
|
var service = CreateService(cloudStateStore: cloudStateStore);
|
||||||
|
|
||||||
|
var decision = await service.BuildDecisionAsync(new TurnContext
|
||||||
|
{
|
||||||
|
RawTranscript = string.Empty,
|
||||||
|
NormalizedTranscript = string.Empty,
|
||||||
|
Attributes = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["messageType"] = "TRIGGER",
|
||||||
|
["triggerSource"] = "PRESENCE",
|
||||||
|
["loopId"] = "loop-history",
|
||||||
|
["context"] =
|
||||||
|
"""{"runtime":{"perception":{"speaker":"person-1","peoplePresent":[{"id":"person-1"}]},"loop":{"users":[{"id":"person-1","firstName":"jake"}]}}}"""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal("trigger_ignored", decision.IntentName);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
Reference in New Issue
Block a user