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

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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);
} }

View File

@@ -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;

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 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; }
@@ -952,4 +1018,4 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
}; };
} }
} }
} }

View File

@@ -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());
@@ -189,4 +204,4 @@ public sealed class PersistenceStoreTests
Saves.Add(snapshot); Saves.Add(snapshot);
} }
} }
} }

View File

@@ -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]