Fix personal report yes/no and weather hi/low handling
This commit is contained in:
@@ -942,13 +942,6 @@ public sealed class JiboInteractionService(
|
|||||||
var name = personalMemoryStore.GetName(personScope);
|
var name = personalMemoryStore.GetName(personScope);
|
||||||
if (string.IsNullOrWhiteSpace(name)) name = personalMemoryStore.GetName(ResolveTenantScope(turn));
|
if (string.IsNullOrWhiteSpace(name)) name = personalMemoryStore.GetName(ResolveTenantScope(turn));
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(name) &&
|
|
||||||
presence is not null &&
|
|
||||||
!string.IsNullOrWhiteSpace(presence.PrimaryPersonId) &&
|
|
||||||
presence.LoopUserFirstNames.TryGetValue(presence.PrimaryPersonId, out var firstName) &&
|
|
||||||
!string.IsNullOrWhiteSpace(firstName))
|
|
||||||
name = ToDisplayName(firstName);
|
|
||||||
|
|
||||||
name = ToDisplayName(name ?? string.Empty);
|
name = ToDisplayName(name ?? string.Empty);
|
||||||
|
|
||||||
return string.IsNullOrWhiteSpace(name)
|
return string.IsNullOrWhiteSpace(name)
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ internal static class PersonalReportOrchestrator
|
|||||||
return new JiboInteractionDecision(
|
return new JiboInteractionDecision(
|
||||||
"personal_report_opt_in",
|
"personal_report_opt_in",
|
||||||
reply,
|
reply,
|
||||||
|
SkillPayload: BuildYesNoPromptPayload(),
|
||||||
ContextUpdates: contextUpdates);
|
ContextUpdates: contextUpdates);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,6 +120,7 @@ internal static class PersonalReportOrchestrator
|
|||||||
return new JiboInteractionDecision(
|
return new JiboInteractionDecision(
|
||||||
"personal_report_verify_user",
|
"personal_report_verify_user",
|
||||||
$"I think this is {knownName}. Is that right?",
|
$"I think this is {knownName}. Is that right?",
|
||||||
|
SkillPayload: BuildYesNoPromptPayload(),
|
||||||
ContextUpdates: BuildContextUpdates(
|
ContextUpdates: BuildContextUpdates(
|
||||||
AwaitingIdentityConfirmationState,
|
AwaitingIdentityConfirmationState,
|
||||||
0,
|
0,
|
||||||
@@ -147,6 +149,7 @@ internal static class PersonalReportOrchestrator
|
|||||||
return new JiboInteractionDecision(
|
return new JiboInteractionDecision(
|
||||||
"personal_report_opt_in",
|
"personal_report_opt_in",
|
||||||
$"{inlineToggleSummary} Would you like your personal report now?",
|
$"{inlineToggleSummary} Would you like your personal report now?",
|
||||||
|
SkillPayload: BuildYesNoPromptPayload(),
|
||||||
ContextUpdates: BuildContextUpdates(
|
ContextUpdates: BuildContextUpdates(
|
||||||
AwaitingOptInState,
|
AwaitingOptInState,
|
||||||
0,
|
0,
|
||||||
@@ -425,6 +428,14 @@ internal static class PersonalReportOrchestrator
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static IDictionary<string, object?> BuildYesNoPromptPayload()
|
||||||
|
{
|
||||||
|
return new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["listen_contexts"] = new[] { "shared/yes_no" }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private static bool IsAffirmativeReply(string loweredTranscript)
|
private static bool IsAffirmativeReply(string loweredTranscript)
|
||||||
{
|
{
|
||||||
return ContainsAnyPhrase(loweredTranscript, AffirmativePhrases);
|
return ContainsAnyPhrase(loweredTranscript, AffirmativePhrases);
|
||||||
|
|||||||
@@ -163,13 +163,11 @@ public sealed class OpenWeatherReportProvider(
|
|||||||
?? TryReadInt(selectedDayTemp, "night")
|
?? TryReadInt(selectedDayTemp, "night")
|
||||||
?? TryReadInt(selectedDayTemp, "morn")
|
?? TryReadInt(selectedDayTemp, "morn")
|
||||||
?? TryReadInt(selectedDayTemp, "eve");
|
?? TryReadInt(selectedDayTemp, "eve");
|
||||||
var high = TryReadInt(selectedDayTemp, "max");
|
var currentDayHighLow = forecastDayOffset <= 0
|
||||||
var low = TryReadInt(selectedDayTemp, "min");
|
? await TryResolveCurrentDayHighLowFromForecastAsync(location, useCelsius, cancellationToken)
|
||||||
if (temperature is not null)
|
: null;
|
||||||
{
|
var high = currentDayHighLow?.High ?? TryReadInt(selectedDayTemp, "max");
|
||||||
high = high is null ? temperature : Math.Max(high.Value, temperature.Value);
|
var low = currentDayHighLow?.Low ?? TryReadInt(selectedDayTemp, "min");
|
||||||
low = low is null ? temperature : Math.Min(low.Value, temperature.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (temperature is null && high is null && low is null) return null;
|
if (temperature is null && high is null && low is null) return null;
|
||||||
|
|
||||||
@@ -220,13 +218,9 @@ public sealed class OpenWeatherReportProvider(
|
|||||||
var summary = TryReadWeatherSummary(root);
|
var summary = TryReadWeatherSummary(root);
|
||||||
var condition = TryReadWeatherCondition(root);
|
var condition = TryReadWeatherCondition(root);
|
||||||
var temperature = TryReadInt(main, "temp");
|
var temperature = TryReadInt(main, "temp");
|
||||||
var high = TryReadInt(main, "temp_max");
|
var currentDayHighLow = await TryResolveCurrentDayHighLowFromForecastAsync(location, useCelsius, cancellationToken);
|
||||||
var low = TryReadInt(main, "temp_min");
|
var high = currentDayHighLow?.High ?? TryReadInt(main, "temp_max");
|
||||||
if (temperature is not null)
|
var low = currentDayHighLow?.Low ?? TryReadInt(main, "temp_min");
|
||||||
{
|
|
||||||
high = high is null ? temperature : Math.Max(high.Value, temperature.Value);
|
|
||||||
low = low is null ? temperature : Math.Min(low.Value, temperature.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (temperature is null && high is null && low is null) return null;
|
if (temperature is null && high is null && low is null) return null;
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,9 @@ public sealed class ProviderCachingTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task OpenWeatherReportProvider_UsesCurrentHiLoForCurrentDay_WhenCurrentBandDiffers()
|
public async Task OpenWeatherReportProvider_UsesCurrentHiLoForCurrentDay_WhenCurrentBandDiffers()
|
||||||
{
|
{
|
||||||
|
var offset = TimeSpan.FromHours(-5);
|
||||||
|
var localNow = DateTimeOffset.UtcNow.ToOffset(offset);
|
||||||
|
var todayNoon = new DateTimeOffset(localNow.Date.AddHours(12), offset);
|
||||||
var handler = new CountingHttpMessageHandler(message =>
|
var handler = new CountingHttpMessageHandler(message =>
|
||||||
{
|
{
|
||||||
var path = message.RequestUri?.AbsolutePath ?? string.Empty;
|
var path = message.RequestUri?.AbsolutePath ?? string.Empty;
|
||||||
@@ -71,12 +74,18 @@ public sealed class ProviderCachingTests
|
|||||||
"lat":38.8708,
|
"lat":38.8708,
|
||||||
"lon":-94.1733,
|
"lon":-94.1733,
|
||||||
"timezone":"America/Chicago",
|
"timezone":"America/Chicago",
|
||||||
"current":{"dt":1710000000,"temp":76.0,"weather":[{"main":"Clouds","description":"overcast clouds"}]},
|
"current":{"dt":1710000000,"temp":83.0,"weather":[{"main":"Clouds","description":"overcast clouds"}]},
|
||||||
"daily":[
|
"daily":[
|
||||||
{"dt":1710000000,"temp":{"day":76.0,"min":78.0,"max":82.0},"weather":[{"main":"Clouds","description":"overcast clouds"}]}
|
{"dt":1710000000,"temp":{"day":83.0,"min":82.0,"max":83.0},"weather":[{"main":"Clouds","description":"overcast clouds"}]}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
"""),
|
"""),
|
||||||
|
"/data/2.5/forecast" => JsonResponse(
|
||||||
|
BuildForecastResponseJson(
|
||||||
|
"Lone Jack",
|
||||||
|
"US",
|
||||||
|
-18000,
|
||||||
|
[(todayNoon, 82, 82, 78, "Clouds", "overcast clouds")])),
|
||||||
_ => new HttpResponseMessage(HttpStatusCode.NotFound)
|
_ => new HttpResponseMessage(HttpStatusCode.NotFound)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -96,10 +105,11 @@ public sealed class ProviderCachingTests
|
|||||||
await provider.GetReportAsync(new WeatherReportRequest("Lone Jack,US", null, null, false, false, 0));
|
await provider.GetReportAsync(new WeatherReportRequest("Lone Jack,US", null, null, false, false, 0));
|
||||||
|
|
||||||
Assert.NotNull(report);
|
Assert.NotNull(report);
|
||||||
Assert.Equal(76, report!.Temperature);
|
Assert.Equal(83, report!.Temperature);
|
||||||
Assert.Equal(82, report.HighTemperature);
|
Assert.Equal(82, report.HighTemperature);
|
||||||
Assert.Equal(76, report.LowTemperature);
|
Assert.Equal(78, report.LowTemperature);
|
||||||
Assert.Equal(1, handler.GetCallCount("/data/3.0/onecall"));
|
Assert.Equal(1, handler.GetCallCount("/data/3.0/onecall"));
|
||||||
|
Assert.Equal(1, handler.GetCallCount("/data/2.5/forecast"));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -153,6 +163,9 @@ public sealed class ProviderCachingTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task OpenWeatherReportProvider_FallsBackToLegacyWeatherWhenOneCallIsUnauthorized()
|
public async Task OpenWeatherReportProvider_FallsBackToLegacyWeatherWhenOneCallIsUnauthorized()
|
||||||
{
|
{
|
||||||
|
var offset = TimeSpan.FromHours(-5);
|
||||||
|
var localNow = DateTimeOffset.UtcNow.ToOffset(offset);
|
||||||
|
var todayNoon = new DateTimeOffset(localNow.Date.AddHours(12), offset);
|
||||||
var handler = new CountingHttpMessageHandler(message =>
|
var handler = new CountingHttpMessageHandler(message =>
|
||||||
{
|
{
|
||||||
var path = message.RequestUri?.AbsolutePath ?? string.Empty;
|
var path = message.RequestUri?.AbsolutePath ?? string.Empty;
|
||||||
@@ -168,7 +181,13 @@ public sealed class ProviderCachingTests
|
|||||||
"application/json")
|
"application/json")
|
||||||
},
|
},
|
||||||
"/data/2.5/weather" => JsonResponse(
|
"/data/2.5/weather" => JsonResponse(
|
||||||
"""{"name":"Boston","weather":[{"main":"Clouds","description":"overcast clouds"}],"main":{"temp":70.0,"temp_max":72.0,"temp_min":66.0}}"""),
|
"""{"name":"Boston","weather":[{"main":"Clouds","description":"overcast clouds"}],"main":{"temp":70.0,"temp_max":83.0,"temp_min":82.0}}"""),
|
||||||
|
"/data/2.5/forecast" => JsonResponse(
|
||||||
|
BuildForecastResponseJson(
|
||||||
|
"Boston",
|
||||||
|
"US",
|
||||||
|
-18000,
|
||||||
|
[(todayNoon, 70, 72, 66, "Clouds", "overcast clouds")])),
|
||||||
_ => new HttpResponseMessage(HttpStatusCode.NotFound)
|
_ => new HttpResponseMessage(HttpStatusCode.NotFound)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -192,6 +211,7 @@ public sealed class ProviderCachingTests
|
|||||||
Assert.Equal(66, report.LowTemperature);
|
Assert.Equal(66, report.LowTemperature);
|
||||||
Assert.Equal(1, handler.GetCallCount("/data/3.0/onecall"));
|
Assert.Equal(1, handler.GetCallCount("/data/3.0/onecall"));
|
||||||
Assert.Equal(1, handler.GetCallCount("/data/2.5/weather"));
|
Assert.Equal(1, handler.GetCallCount("/data/2.5/weather"));
|
||||||
|
Assert.Equal(1, handler.GetCallCount("/data/2.5/forecast"));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -395,6 +415,20 @@ public sealed class ProviderCachingTests
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string BuildForecastResponseJson(
|
||||||
|
string cityName,
|
||||||
|
string country,
|
||||||
|
int timezoneSeconds,
|
||||||
|
IReadOnlyList<(DateTimeOffset Timestamp, int Temp, int High, int Low, string Main, string Description)> entries)
|
||||||
|
{
|
||||||
|
var list = string.Join(
|
||||||
|
",",
|
||||||
|
entries.Select(entry =>
|
||||||
|
$$"""{"dt":{{entry.Timestamp.ToUnixTimeSeconds()}},"main":{"temp":{{entry.Temp}},"temp_min":{{entry.Low}},"temp_max":{{entry.High}}},"weather":[{"main":"{{entry.Main}}","description":"{{entry.Description}}"}]}"""));
|
||||||
|
|
||||||
|
return $$"""{"city":{"name":"{{cityName}}","country":"{{country}}","timezone":{{timezoneSeconds}}},"list":[{{list}}]}""";
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class CountingHttpMessageHandler(Func<HttpRequestMessage, HttpResponseMessage> responseFactory)
|
private sealed class CountingHttpMessageHandler(Func<HttpRequestMessage, HttpResponseMessage> responseFactory)
|
||||||
: HttpMessageHandler
|
: HttpMessageHandler
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -282,6 +282,31 @@ public sealed class JiboInteractionServiceTests
|
|||||||
Assert.Equal("I do not know your name yet. You can say, my name is Alex.", decision.ReplyText);
|
Assert.Equal("I do not know your name yet. You can say, my name is Alex.", decision.ReplyText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BuildDecisionAsync_IdentityFollowUp_DoesNotGuessFromLoopFirstNameWhenMemoryIsMissing()
|
||||||
|
{
|
||||||
|
var service = CreateService();
|
||||||
|
|
||||||
|
var decision = await service.BuildDecisionAsync(new TurnContext
|
||||||
|
{
|
||||||
|
RawTranscript = "who am i",
|
||||||
|
NormalizedTranscript = "who am i",
|
||||||
|
Attributes = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["accountId"] = "acct-d",
|
||||||
|
["loopId"] = "loop-d",
|
||||||
|
["context"] =
|
||||||
|
"""
|
||||||
|
{"runtime":{"perception":{"speaker":"person-9"},"loop":{"users":[{"id":"person-9","firstName":"hi"}]}}}
|
||||||
|
"""
|
||||||
|
},
|
||||||
|
DeviceId = "device-d"
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal("memory_get_name", decision.IntentName);
|
||||||
|
Assert.Equal("I do not know your name yet. You can say, my name is Alex.", decision.ReplyText);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task BuildDecisionAsync_TriggerWithKnownIdentity_BuildsProactiveGreetingAndContext()
|
public async Task BuildDecisionAsync_TriggerWithKnownIdentity_BuildsProactiveGreetingAndContext()
|
||||||
{
|
{
|
||||||
@@ -1712,6 +1737,7 @@ public sealed class JiboInteractionServiceTests
|
|||||||
|
|
||||||
Assert.Equal("personal_report_opt_in", decision.IntentName);
|
Assert.Equal("personal_report_opt_in", decision.IntentName);
|
||||||
Assert.Equal("Would you like your personal report now?", decision.ReplyText);
|
Assert.Equal("Would you like your personal report now?", decision.ReplyText);
|
||||||
|
Assert.Equal("shared/yes_no", ((IReadOnlyList<string>)decision.SkillPayload!["listen_contexts"])[0]);
|
||||||
Assert.NotNull(decision.ContextUpdates);
|
Assert.NotNull(decision.ContextUpdates);
|
||||||
Assert.Equal("awaiting_opt_in", decision.ContextUpdates![PersonalReportStateKey]);
|
Assert.Equal("awaiting_opt_in", decision.ContextUpdates![PersonalReportStateKey]);
|
||||||
Assert.Equal(true, decision.ContextUpdates[PersonalReportWeatherEnabledKey]);
|
Assert.Equal(true, decision.ContextUpdates[PersonalReportWeatherEnabledKey]);
|
||||||
@@ -1742,6 +1768,7 @@ public sealed class JiboInteractionServiceTests
|
|||||||
|
|
||||||
Assert.Equal("personal_report_verify_user", decision.IntentName);
|
Assert.Equal("personal_report_verify_user", decision.IntentName);
|
||||||
Assert.Equal("I think this is alex. Is that right?", decision.ReplyText);
|
Assert.Equal("I think this is alex. Is that right?", decision.ReplyText);
|
||||||
|
Assert.Equal("shared/yes_no", ((IReadOnlyList<string>)decision.SkillPayload!["listen_contexts"])[0]);
|
||||||
Assert.NotNull(decision.ContextUpdates);
|
Assert.NotNull(decision.ContextUpdates);
|
||||||
Assert.Equal("awaiting_identity_confirmation", decision.ContextUpdates![PersonalReportStateKey]);
|
Assert.Equal("awaiting_identity_confirmation", decision.ContextUpdates![PersonalReportStateKey]);
|
||||||
Assert.Equal("alex", decision.ContextUpdates[PersonalReportUserNameKey]);
|
Assert.Equal("alex", decision.ContextUpdates[PersonalReportUserNameKey]);
|
||||||
|
|||||||
Reference in New Issue
Block a user