more Jibo patches

This commit is contained in:
Jacob Dubin
2026-04-20 21:45:55 -05:00
parent 32d63584d6
commit 7b05452937
6 changed files with 129 additions and 5 deletions

View File

@@ -89,6 +89,7 @@ Parallel tags:
- the attached `jibo test 13` session includes both examples in one bundle: - the attached `jibo test 13` session includes both examples in one bundle:
- a proactive or share-style prompt where spoken `yes` was treated as generic speech - a proactive or share-style prompt where spoken `yes` was treated as generic speech
- a later update prompt where spoken `no` was accepted correctly - a later update prompt where spoken `no` was accepted correctly
- the share prompt uses `surprises-date/offer_date_fact` with `$YESNO`, and the failing reply leaked `globals/*` rules back into a Nimbus relaunch
- Implementation notes: - Implementation notes:
- compare the active listen rules, ASR hints, and local skill ownership for the share-style prompt versus OTA prompts - compare the active listen rules, ASR hints, and local skill ownership for the share-style prompt versus OTA prompts
- make constrained yes-no detection cover this prompt family without regressing the already-working update `no` path - make constrained yes-no detection cover this prompt family without regressing the already-working update `no` path

View File

@@ -340,6 +340,7 @@ public sealed class JiboInteractionService(
string.Equals(rule, "$YESNO", StringComparison.OrdinalIgnoreCase) || string.Equals(rule, "$YESNO", StringComparison.OrdinalIgnoreCase) ||
string.Equals(rule, "create/is_it_a_keeper", StringComparison.OrdinalIgnoreCase) || string.Equals(rule, "create/is_it_a_keeper", StringComparison.OrdinalIgnoreCase) ||
string.Equals(rule, "settings/download_now_later", StringComparison.OrdinalIgnoreCase) || string.Equals(rule, "settings/download_now_later", StringComparison.OrdinalIgnoreCase) ||
string.Equals(rule, "surprises-date/offer_date_fact", StringComparison.OrdinalIgnoreCase) ||
string.Equals(rule, "surprises-ota/want_to_download_now", StringComparison.OrdinalIgnoreCase)); string.Equals(rule, "surprises-ota/want_to_download_now", StringComparison.OrdinalIgnoreCase));
} }

View File

@@ -57,7 +57,16 @@ public sealed class ResponsePlanToSocketMessagesMapper
: isWordOfDayGuess : isWordOfDayGuess
? ["word-of-the-day/puzzle"] ? ["word-of-the-day/puzzle"]
: isYesNoTurn && isYesNoIntent ? [yesNoRule!] : rules; : isYesNoTurn && isYesNoIntent ? [yesNoRule!] : rules;
var entities = ReadEntities(turn, messageType, isYesNoTurn && isYesNoIntent, isWordOfDayLaunch, isRadioLaunch, isWordOfDayGuess, wordOfDayGuess, radioStation); var entities = ReadEntities(
turn,
messageType,
isYesNoTurn && isYesNoIntent,
ShouldIncludeCreateDomain(yesNoRule),
isWordOfDayLaunch,
isRadioLaunch,
isWordOfDayGuess,
wordOfDayGuess,
radioStation);
var listenMessage = new var listenMessage = new
{ {
type = "LISTEN", type = "LISTEN",
@@ -265,15 +274,21 @@ public sealed class ResponsePlanToSocketMessagesMapper
private static object ReadEntities( private static object ReadEntities(
TurnContext turn, TurnContext turn,
string? messageType, string? messageType,
bool yesNoCreateTurn, bool yesNoTurn,
bool includeCreateDomain,
bool wordOfDayLaunch, bool wordOfDayLaunch,
bool radioLaunch, bool radioLaunch,
bool wordOfDayGuess, bool wordOfDayGuess,
string? guess, string? guess,
string? radioStation) string? radioStation)
{ {
if (yesNoCreateTurn) if (yesNoTurn)
{ {
if (!includeCreateDomain)
{
return new Dictionary<string, object?>();
}
return new Dictionary<string, object?> return new Dictionary<string, object?>
{ {
["domain"] = "create" ["domain"] = "create"
@@ -331,9 +346,16 @@ public sealed class ResponsePlanToSocketMessagesMapper
.FirstOrDefault(static rule => .FirstOrDefault(static rule =>
string.Equals(rule, "create/is_it_a_keeper", StringComparison.OrdinalIgnoreCase) || string.Equals(rule, "create/is_it_a_keeper", StringComparison.OrdinalIgnoreCase) ||
string.Equals(rule, "settings/download_now_later", StringComparison.OrdinalIgnoreCase) || string.Equals(rule, "settings/download_now_later", StringComparison.OrdinalIgnoreCase) ||
string.Equals(rule, "surprises-date/offer_date_fact", StringComparison.OrdinalIgnoreCase) ||
string.Equals(rule, "surprises-ota/want_to_download_now", StringComparison.OrdinalIgnoreCase)); string.Equals(rule, "surprises-ota/want_to_download_now", StringComparison.OrdinalIgnoreCase));
} }
private static bool ShouldIncludeCreateDomain(string? yesNoRule)
{
return string.Equals(yesNoRule, "create/is_it_a_keeper", StringComparison.OrdinalIgnoreCase) ||
string.Equals(yesNoRule, "surprises-ota/want_to_download_now", StringComparison.OrdinalIgnoreCase);
}
private static IEnumerable<string> ReadRuleValues(TurnContext turn) private static IEnumerable<string> ReadRuleValues(TurnContext turn)
{ {
return ReadRuleValues(turn, "listenRules").Concat(ReadRuleValues(turn, "clientRules")); return ReadRuleValues(turn, "listenRules").Concat(ReadRuleValues(turn, "clientRules"));
@@ -715,8 +737,7 @@ public sealed class ResponsePlanToSocketMessagesMapper
.Replace("&", "&amp;", StringComparison.Ordinal) .Replace("&", "&amp;", StringComparison.Ordinal)
.Replace("<", "&lt;", StringComparison.Ordinal) .Replace("<", "&lt;", StringComparison.Ordinal)
.Replace(">", "&gt;", StringComparison.Ordinal) .Replace(">", "&gt;", StringComparison.Ordinal)
.Replace("\"", "&quot;", StringComparison.Ordinal) .Replace("\"", "&quot;", StringComparison.Ordinal);
.Replace("'", "&apos;", StringComparison.Ordinal);
} }
private static string? ReadPayloadString(IDictionary<string, object?>? payload, string key) private static string? ReadPayloadString(IDictionary<string, object?>? payload, string key)

View File

@@ -710,6 +710,7 @@ public sealed class WebSocketTurnFinalizationService(
string.Equals(rule, "$YESNO", StringComparison.OrdinalIgnoreCase) || string.Equals(rule, "$YESNO", StringComparison.OrdinalIgnoreCase) ||
string.Equals(rule, "create/is_it_a_keeper", StringComparison.OrdinalIgnoreCase) || string.Equals(rule, "create/is_it_a_keeper", StringComparison.OrdinalIgnoreCase) ||
string.Equals(rule, "settings/download_now_later", StringComparison.OrdinalIgnoreCase) || string.Equals(rule, "settings/download_now_later", StringComparison.OrdinalIgnoreCase) ||
string.Equals(rule, "surprises-date/offer_date_fact", StringComparison.OrdinalIgnoreCase) ||
string.Equals(rule, "surprises-ota/want_to_download_now", StringComparison.OrdinalIgnoreCase)); string.Equals(rule, "surprises-ota/want_to_download_now", StringComparison.OrdinalIgnoreCase));
} }

View File

@@ -115,6 +115,26 @@ public sealed class JiboInteractionServiceTests
Assert.Equal("No.", decision.ReplyText); Assert.Equal("No.", decision.ReplyText);
} }
[Fact]
public async Task BuildDecisionAsync_SurprisesDateOfferPrompt_MapsShortAffirmationToYesIntent()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "Yes!",
NormalizedTranscript = "Yes!",
Attributes = new Dictionary<string, object?>
{
["listenRules"] = new[] { "surprises-date/offer_date_fact", "globals/global_commands_launch" },
["listenAsrHints"] = new[] { "$YESNO" }
}
});
Assert.Equal("yes", decision.IntentName);
Assert.Equal("Yes.", decision.ReplyText);
}
[Fact] [Fact]
public async Task BuildDecisionAsync_SkillPhraseVariant_MapsToKnownIntent() public async Task BuildDecisionAsync_SkillPhraseVariant_MapsToKnownIntent()
{ {

View File

@@ -4,6 +4,7 @@ using Jibo.Cloud.Domain.Models;
using Jibo.Cloud.Infrastructure.Content; using Jibo.Cloud.Infrastructure.Content;
using Jibo.Cloud.Infrastructure.Persistence; using Jibo.Cloud.Infrastructure.Persistence;
using Jibo.Cloud.Tests.Fixtures; using Jibo.Cloud.Tests.Fixtures;
using Jibo.Runtime.Abstractions;
namespace Jibo.Cloud.Tests.WebSockets; namespace Jibo.Cloud.Tests.WebSockets;
@@ -403,6 +404,85 @@ public sealed class JiboWebSocketServiceTests
Assert.Equal("surprises-ota/want_to_download_now", listenPayload.RootElement.GetProperty("data").GetProperty("match").GetProperty("rule").GetString()); Assert.Equal("surprises-ota/want_to_download_now", listenPayload.RootElement.GetProperty("data").GetProperty("match").GetProperty("rule").GetString());
} }
[Fact]
public async Task ClientAsr_SurprisesDateOfferPrompt_MapsYesWithoutGlobalRuleLeak()
{
await _service.HandleMessageAsync(new WebSocketMessageEnvelope
{
HostName = "neo-hub.jibo.com",
Path = "/listen",
Kind = "neo-hub-listen",
Token = "hub-share-yesno-token",
Text = """{"type":"LISTEN","transID":"trans-share-yes","data":{"rules":["surprises-date/offer_date_fact","globals/gui_nav","globals/mim_repeat","globals/global_commands_launch"],"asr":{"hints":["$YESNO"]}}}"""
});
var replies = await _service.HandleMessageAsync(new WebSocketMessageEnvelope
{
HostName = "neo-hub.jibo.com",
Path = "/listen",
Kind = "neo-hub-listen",
Token = "hub-share-yesno-token",
Text = """{"type":"CLIENT_ASR","transID":"trans-share-yes","data":{"text":"Yes!"}}"""
});
Assert.Equal(3, replies.Count);
using var listenPayload = JsonDocument.Parse(replies[0].Text!);
Assert.Equal("yes", listenPayload.RootElement.GetProperty("data").GetProperty("nlu").GetProperty("intent").GetString());
var rules = listenPayload.RootElement.GetProperty("data").GetProperty("nlu").GetProperty("rules");
Assert.Single(rules.EnumerateArray());
Assert.Equal("surprises-date/offer_date_fact", rules[0].GetString());
Assert.Equal("surprises-date/offer_date_fact", listenPayload.RootElement.GetProperty("data").GetProperty("match").GetProperty("rule").GetString());
Assert.Equal(0, listenPayload.RootElement.GetProperty("data").GetProperty("nlu").GetProperty("entities").EnumerateObject().Count());
}
[Fact]
public void ResponsePlanMapper_EscapesSpeechWithoutEncodingApostrophes()
{
var plan = new ResponsePlan
{
IntentName = "chat",
Actions =
{
new SpeakAction
{
Sequence = 0,
Text = "I'm glad you're here.",
Voice = "griffin"
},
new InvokeNativeSkillAction
{
Sequence = 1,
SkillName = "chitchat-skill",
Payload = new Dictionary<string, object?>()
}
}
};
var turn = new TurnContext
{
Attributes = new Dictionary<string, object?>
{
["transID"] = "trans-apostrophe"
}
};
var replies = ResponsePlanToSocketMessagesMapper.Map(plan, turn, new CloudSession(), emitSkillActions: true);
using var payload = JsonDocument.Parse(replies[2].Text);
var esml = payload.RootElement
.GetProperty("data")
.GetProperty("action")
.GetProperty("config")
.GetProperty("jcp")
.GetProperty("config")
.GetProperty("play")
.GetProperty("esml")
.GetString();
Assert.Contains("I'm glad you're here.", esml, StringComparison.Ordinal);
Assert.DoesNotContain("&apos;", esml, StringComparison.Ordinal);
}
[Fact] [Fact]
public async Task ClientAsr_OpenTheRadio_EmitsRadioRedirectAndSilentCompletion() public async Task ClientAsr_OpenTheRadio_EmitsRadioRedirectAndSilentCompletion()
{ {