Expand affinity parser guardrails with Pegasus phrases

This commit is contained in:
Jacob Dubin
2026-05-09 23:46:00 -05:00
parent ffb444e4f9
commit 8ae6d86a8c
9 changed files with 931 additions and 6 deletions

View File

@@ -20,6 +20,10 @@ public sealed class JiboInteractionServiceTests
private const string ChitchatStateKey = "chitchatState";
private const string ChitchatRouteKey = "chitchatRoute";
private const string ChitchatEmotionKey = "chitchatEmotion";
private const string GreetingRouteKey = "greetingsRoute";
private const string GreetingSpeakerKey = "greetingsSpeaker";
private const string GreetingLastProactiveUtcKey = "greetingsLastProactiveUtc";
private const string GreetingLastReactiveUtcKey = "greetingsLastReactiveUtc";
[Fact]
public async Task BuildDecisionAsync_Joke_UsesCatalogBackedRandomContent()
@@ -150,6 +154,103 @@ public sealed class JiboInteractionServiceTests
Assert.Equal("My birthday is March 22, 2026.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_GoodMorning_UsesReactiveGreetingWithRememberedName()
{
var memoryStore = new InMemoryPersonalMemoryStore();
memoryStore.SetName(new PersonalMemoryTenantScope("acct-a", "loop-a", "device-a"), "jake");
var service = CreateService(memoryStore);
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "good morning",
NormalizedTranscript = "good morning",
Attributes = new Dictionary<string, object?>
{
["accountId"] = "acct-a",
["loopId"] = "loop-a"
},
DeviceId = "device-a"
});
Assert.Equal("good_morning", decision.IntentName);
Assert.Equal("Good morning, Jake. It is great to see you.", decision.ReplyText);
Assert.NotNull(decision.ContextUpdates);
Assert.Equal("ReactiveGreeting", decision.ContextUpdates![GreetingRouteKey]);
Assert.Equal(string.Empty, decision.ContextUpdates[GreetingSpeakerKey]);
Assert.True(DateTimeOffset.TryParse(decision.ContextUpdates[GreetingLastReactiveUtcKey]?.ToString(), out _));
}
[Fact]
public async Task BuildDecisionAsync_TriggerWithKnownIdentity_BuildsProactiveGreetingAndContext()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = string.Empty,
NormalizedTranscript = string.Empty,
Attributes = new Dictionary<string, object?>
{
["messageType"] = "TRIGGER",
["triggerSource"] = "PRESENCE",
["context"] = """{"runtime":{"perception":{"speaker":"person-1","peoplePresent":[{"id":"person-1"}]},"loop":{"users":[{"id":"person-1","firstName":"jake"}]}}}"""
}
});
Assert.Equal("proactive_greeting", decision.IntentName);
Assert.Contains("Jake", decision.ReplyText, StringComparison.Ordinal);
Assert.NotNull(decision.ContextUpdates);
Assert.Equal("ProactiveGreeting", decision.ContextUpdates![GreetingRouteKey]);
Assert.Equal("person-1", decision.ContextUpdates[GreetingSpeakerKey]);
Assert.True(DateTimeOffset.TryParse(decision.ContextUpdates[GreetingLastProactiveUtcKey]?.ToString(), out _));
}
[Fact]
public async Task BuildDecisionAsync_TriggerFromSurprise_ReturnsSilentTriggerIgnoredDecision()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = string.Empty,
NormalizedTranscript = string.Empty,
Attributes = new Dictionary<string, object?>
{
["messageType"] = "TRIGGER",
["triggerSource"] = "SURPRISE",
["context"] = """{"runtime":{"perception":{"speaker":"person-1"},"loop":{"users":[{"id":"person-1","firstName":"jake"}]}}}"""
}
});
Assert.Equal("trigger_ignored", decision.IntentName);
Assert.Equal(string.Empty, decision.ReplyText);
Assert.Equal("chitchat-skill", decision.SkillName);
Assert.NotNull(decision.SkillPayload);
Assert.Equal("completion_only", decision.SkillPayload!["cloudResponseMode"]);
}
[Fact]
public async Task BuildDecisionAsync_TriggerWithinCooldown_IsIgnored()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = string.Empty,
NormalizedTranscript = string.Empty,
Attributes = new Dictionary<string, object?>
{
["messageType"] = "TRIGGER",
["triggerSource"] = "PRESENCE",
["context"] = """{"runtime":{"perception":{"speaker":"person-1"},"loop":{"users":[{"id":"person-1","firstName":"jake"}]}}}""",
[GreetingLastProactiveUtcKey] = DateTimeOffset.UtcNow.ToString("O")
}
});
Assert.Equal("trigger_ignored", decision.IntentName);
}
[Fact]
public async Task BuildDecisionAsync_DoYouHaveAPersonality_UsesCatalogBackedPersonalityReply()
{
@@ -691,6 +792,144 @@ public sealed class JiboInteractionServiceTests
Assert.Equal("Yes. You told me you like country music.", recallDecision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_AffinityMemory_PegasusWeLovePhrase_SetThenRecallWithinTenant()
{
var memoryStore = new InMemoryPersonalMemoryStore();
var service = CreateService(memoryStore);
var setDecision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "we love pizza",
NormalizedTranscript = "we love pizza",
Attributes = new Dictionary<string, object?>
{
["accountId"] = "acct-a",
["loopId"] = "loop-a"
},
DeviceId = "device-a"
});
Assert.Equal("memory_set_affinity", setDecision.IntentName);
Assert.Equal("Got it. I will remember you love pizza.", setDecision.ReplyText);
var recallDecision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "do i love pizza",
NormalizedTranscript = "do i love pizza",
Attributes = new Dictionary<string, object?>
{
["accountId"] = "acct-a",
["loopId"] = "loop-a"
},
DeviceId = "device-a"
});
Assert.Equal("memory_get_affinity", recallDecision.IntentName);
Assert.Equal("Yes. You told me you love pizza.", recallDecision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_AffinityMemory_PegasusLoathePhrase_SetThenRecallWithinTenant()
{
var memoryStore = new InMemoryPersonalMemoryStore();
var service = CreateService(memoryStore);
var setDecision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "I loathe celery",
NormalizedTranscript = "I loathe celery",
Attributes = new Dictionary<string, object?>
{
["accountId"] = "acct-a",
["loopId"] = "loop-a"
},
DeviceId = "device-a"
});
Assert.Equal("memory_set_affinity", setDecision.IntentName);
Assert.Equal("Got it. I will remember you dislike celery.", setDecision.ReplyText);
var recallDecision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "do i loathe celery",
NormalizedTranscript = "do i loathe celery",
Attributes = new Dictionary<string, object?>
{
["accountId"] = "acct-a",
["loopId"] = "loop-a"
},
DeviceId = "device-a"
});
Assert.Equal("memory_get_affinity", recallDecision.IntentName);
Assert.Equal("Yes. You told me you dislike celery.", recallDecision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_AffinityMemory_PegasusDoYouThinkLikeLookup_SetsAndRecalls()
{
var memoryStore = new InMemoryPersonalMemoryStore();
var service = CreateService(memoryStore);
await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "I enjoy country music",
NormalizedTranscript = "I enjoy country music",
Attributes = new Dictionary<string, object?>
{
["accountId"] = "acct-a",
["loopId"] = "loop-a"
},
DeviceId = "device-a"
});
var recallDecision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "do you think i like country music",
NormalizedTranscript = "do you think i like country music",
Attributes = new Dictionary<string, object?>
{
["accountId"] = "acct-a",
["loopId"] = "loop-a"
},
DeviceId = "device-a"
});
Assert.Equal("memory_get_affinity", recallDecision.IntentName);
Assert.Equal("Yes. You told me you like country music.", recallDecision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_AffinitySetAttemptWithoutItem_RoutesToAffinityPrompt()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "we like",
NormalizedTranscript = "we like"
});
Assert.Equal("memory_set_affinity", decision.IntentName);
Assert.Equal("I can remember it if you say, I like pizza or I dislike mushrooms.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_AffinityRecallAttemptWithoutItem_RoutesToRecallPrompt()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "do you think i like",
NormalizedTranscript = "do you think i like"
});
Assert.Equal("memory_get_affinity", decision.IntentName);
Assert.Equal("Ask me like this: do I like pizza?", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_PreferenceReversePhrase_ParsesFavoriteVariant()
{

View File

@@ -491,6 +491,75 @@ public sealed class JiboWebSocketServiceTests
Assert.Equal("i do like pizza", listenPayload.RootElement.GetProperty("data").GetProperty("asr").GetProperty("text").GetString());
}
[Fact]
public async Task BufferedAudio_WithIncompletePegasusWeAffinityHint_DefersThenFinalizesWhenContinuationArrives()
{
await _service.HandleMessageAsync(new WebSocketMessageEnvelope
{
HostName = "neo-hub.jibo.com",
Path = "/listen",
Kind = "neo-hub-listen",
Token = "hub-affinity-we-continuation-token",
Text = """{"type":"LISTEN","transID":"trans-affinity-we-continuation","data":{"rules":["launch"]}}"""
});
await _service.HandleMessageAsync(new WebSocketMessageEnvelope
{
HostName = "neo-hub.jibo.com",
Path = "/listen",
Kind = "neo-hub-listen",
Token = "hub-affinity-we-continuation-token",
Text = """{"type":"CONTEXT","transID":"trans-affinity-we-continuation","data":{"audioTranscriptHint":"we like"}}"""
});
for (var index = 0; index < 4; index += 1)
{
var chunkReplies = await _service.HandleMessageAsync(new WebSocketMessageEnvelope
{
HostName = "neo-hub.jibo.com",
Path = "/listen",
Kind = "neo-hub-listen",
Token = "hub-affinity-we-continuation-token",
Binary = new byte[3000]
});
Assert.Empty(chunkReplies);
}
var session = _store.FindSessionByToken("hub-affinity-we-continuation-token");
Assert.NotNull(session);
session.TurnState.FirstAudioReceivedUtc = DateTimeOffset.UtcNow - TimeSpan.FromSeconds(2);
var deferredReplies = await _service.HandleMessageAsync(new WebSocketMessageEnvelope
{
HostName = "neo-hub.jibo.com",
Path = "/listen",
Kind = "neo-hub-listen",
Token = "hub-affinity-we-continuation-token",
Binary = new byte[3000]
});
Assert.Empty(deferredReplies);
var finalizedReplies = await _service.HandleMessageAsync(new WebSocketMessageEnvelope
{
HostName = "neo-hub.jibo.com",
Path = "/listen",
Kind = "neo-hub-listen",
Token = "hub-affinity-we-continuation-token",
Text = """{"type":"CONTEXT","transID":"trans-affinity-we-continuation","data":{"audioTranscriptHint":"we like pizza"}}"""
});
Assert.Equal(3, finalizedReplies.Count);
Assert.Equal("LISTEN", ReadReplyType(finalizedReplies[0]));
Assert.Equal("EOS", ReadReplyType(finalizedReplies[1]));
Assert.Equal("SKILL_ACTION", ReadReplyType(finalizedReplies[2]));
using var listenPayload = JsonDocument.Parse(finalizedReplies[0].Text!);
Assert.Equal("memory_set_affinity", listenPayload.RootElement.GetProperty("data").GetProperty("nlu").GetProperty("intent").GetString());
Assert.Equal("we like pizza", listenPayload.RootElement.GetProperty("data").GetProperty("asr").GetProperty("text").GetString());
}
[Fact]
public async Task MultiChunkAudio_AccumulatesBufferedStateAcrossMessages()
{
@@ -3501,6 +3570,122 @@ public sealed class JiboWebSocketServiceTests
Assert.False(session.Metadata.ContainsKey("pendingProactivityOffer"));
}
[Fact]
public async Task TriggerPresence_WithIdentity_EmitsProactiveGreetingAndPersistsGreetingMetadata()
{
var token = _store.IssueRobotToken("trigger-greeting-device-a");
var listenSetupReplies = await _service.HandleMessageAsync(new WebSocketMessageEnvelope
{
HostName = "neo-hub.jibo.com",
Path = "/listen",
Kind = "neo-hub-listen",
Token = token,
Text = """{"type":"LISTEN","transID":"trans-greeting-trigger","data":{"rules":["launch","globals/global_commands_launch"],"mode":"CLIENT_NLU"}}"""
});
Assert.Empty(listenSetupReplies);
var contextReplies = await _service.HandleMessageAsync(new WebSocketMessageEnvelope
{
HostName = "neo-hub.jibo.com",
Path = "/listen",
Kind = "neo-hub-listen",
Token = token,
Text = """{"type":"CONTEXT","transID":"trans-greeting-trigger","data":{"runtime":{"perception":{"speaker":"person-1","peoplePresent":[{"id":"person-1"}]},"loop":{"users":[{"id":"person-1","firstName":"jake"}]}}}}"""
});
Assert.Empty(contextReplies);
var triggerReplies = await _service.HandleMessageAsync(new WebSocketMessageEnvelope
{
HostName = "neo-hub.jibo.com",
Path = "/listen",
Kind = "neo-hub-listen",
Token = token,
Text = """{"type":"TRIGGER","transID":"trans-greeting-trigger","data":{"triggerSource":"PRESENCE","triggerData":{"looperID":"person-1"}}}"""
});
Assert.Equal(3, triggerReplies.Count);
Assert.Equal("LISTEN", ReadReplyType(triggerReplies[0]));
Assert.Equal("EOS", ReadReplyType(triggerReplies[1]));
Assert.Equal("SKILL_ACTION", ReadReplyType(triggerReplies[2]));
using (var listenPayload = JsonDocument.Parse(triggerReplies[0].Text!))
{
Assert.Equal("proactive_greeting", listenPayload.RootElement.GetProperty("data").GetProperty("nlu").GetProperty("intent").GetString());
}
using (var skillPayload = JsonDocument.Parse(triggerReplies[2].Text!))
{
var esml = skillPayload.RootElement
.GetProperty("data")
.GetProperty("action")
.GetProperty("config")
.GetProperty("jcp")
.GetProperty("config")
.GetProperty("play")
.GetProperty("esml")
.GetString();
Assert.Contains("Jake", esml, StringComparison.Ordinal);
}
var session = _store.FindSessionByToken(token);
Assert.NotNull(session);
Assert.False(session.FollowUpOpen);
Assert.True(session.Metadata.TryGetValue("greetingsRoute", out var route));
Assert.Equal("ProactiveGreeting", route?.ToString());
Assert.True(session.Metadata.TryGetValue("greetingsSpeaker", out var speaker));
Assert.Equal("person-1", speaker?.ToString());
Assert.True(session.Metadata.TryGetValue("greetingsLastProactiveUtc", out var lastUtc));
Assert.True(DateTimeOffset.TryParse(lastUtc?.ToString(), out _));
}
[Fact]
public async Task TriggerSurprise_IsIgnoredWithoutLeavingMicOpen()
{
var token = _store.IssueRobotToken("trigger-greeting-device-b");
await _service.HandleMessageAsync(new WebSocketMessageEnvelope
{
HostName = "neo-hub.jibo.com",
Path = "/listen",
Kind = "neo-hub-listen",
Token = token,
Text = """{"type":"LISTEN","transID":"trans-greeting-trigger-ignore","data":{"rules":["launch"],"mode":"CLIENT_NLU"}}"""
});
await _service.HandleMessageAsync(new WebSocketMessageEnvelope
{
HostName = "neo-hub.jibo.com",
Path = "/listen",
Kind = "neo-hub-listen",
Token = token,
Text = """{"type":"CONTEXT","transID":"trans-greeting-trigger-ignore","data":{"runtime":{"perception":{"speaker":"person-1"},"loop":{"users":[{"id":"person-1","firstName":"jake"}]}}}}"""
});
var triggerReplies = await _service.HandleMessageAsync(new WebSocketMessageEnvelope
{
HostName = "neo-hub.jibo.com",
Path = "/listen",
Kind = "neo-hub-listen",
Token = token,
Text = """{"type":"TRIGGER","transID":"trans-greeting-trigger-ignore","data":{"triggerSource":"SURPRISE","triggerData":{"looperID":"person-1"}}}"""
});
Assert.Equal(3, triggerReplies.Count);
Assert.Equal("LISTEN", ReadReplyType(triggerReplies[0]));
Assert.Equal("EOS", ReadReplyType(triggerReplies[1]));
Assert.Equal("SKILL_ACTION", ReadReplyType(triggerReplies[2]));
using (var listenPayload = JsonDocument.Parse(triggerReplies[0].Text!))
{
Assert.Equal("trigger_ignored", listenPayload.RootElement.GetProperty("data").GetProperty("nlu").GetProperty("intent").GetString());
}
var session = _store.FindSessionByToken(token);
Assert.NotNull(session);
Assert.False(session.FollowUpOpen);
Assert.False(session.Metadata.ContainsKey("greetingsLastProactiveUtc"));
}
[Fact]
public async Task ClientAsrPersonalReport_StateMachinePersistsAcrossTurns()
{