From af76cbaee2e668c9f9ab9e66a799d4e0a7ed4011 Mon Sep 17 00:00:00 2001 From: Jacob Dubin Date: Sun, 17 May 2026 20:20:48 -0500 Subject: [PATCH] Fix personal report yes/no and weather hi/low handling --- .../Services/JiboInteractionService.cs | 7 --- .../Services/PersonalReportOrchestrator.cs | 11 +++++ .../Weather/OpenWeatherReportProvider.cs | 24 ++++------ .../Infrastructure/ProviderCachingTests.cs | 46 ++++++++++++++++--- .../WebSockets/JiboInteractionServiceTests.cs | 27 +++++++++++ 5 files changed, 87 insertions(+), 28 deletions(-) diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs index 5733cdc..7c3cdcc 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs @@ -942,13 +942,6 @@ public sealed class JiboInteractionService( var name = personalMemoryStore.GetName(personScope); 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); return string.IsNullOrWhiteSpace(name) diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/PersonalReportOrchestrator.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/PersonalReportOrchestrator.cs index 758ef7a..4dd06d4 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/PersonalReportOrchestrator.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/PersonalReportOrchestrator.cs @@ -103,6 +103,7 @@ internal static class PersonalReportOrchestrator return new JiboInteractionDecision( "personal_report_opt_in", reply, + SkillPayload: BuildYesNoPromptPayload(), ContextUpdates: contextUpdates); } @@ -119,6 +120,7 @@ internal static class PersonalReportOrchestrator return new JiboInteractionDecision( "personal_report_verify_user", $"I think this is {knownName}. Is that right?", + SkillPayload: BuildYesNoPromptPayload(), ContextUpdates: BuildContextUpdates( AwaitingIdentityConfirmationState, 0, @@ -147,6 +149,7 @@ internal static class PersonalReportOrchestrator return new JiboInteractionDecision( "personal_report_opt_in", $"{inlineToggleSummary} Would you like your personal report now?", + SkillPayload: BuildYesNoPromptPayload(), ContextUpdates: BuildContextUpdates( AwaitingOptInState, 0, @@ -425,6 +428,14 @@ internal static class PersonalReportOrchestrator }; } + private static IDictionary BuildYesNoPromptPayload() + { + return new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["listen_contexts"] = new[] { "shared/yes_no" } + }; + } + private static bool IsAffirmativeReply(string loweredTranscript) { return ContainsAnyPhrase(loweredTranscript, AffirmativePhrases); diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Weather/OpenWeatherReportProvider.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Weather/OpenWeatherReportProvider.cs index 33eb904..784c218 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Weather/OpenWeatherReportProvider.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Weather/OpenWeatherReportProvider.cs @@ -163,13 +163,11 @@ public sealed class OpenWeatherReportProvider( ?? TryReadInt(selectedDayTemp, "night") ?? TryReadInt(selectedDayTemp, "morn") ?? TryReadInt(selectedDayTemp, "eve"); - var high = TryReadInt(selectedDayTemp, "max"); - var low = TryReadInt(selectedDayTemp, "min"); - if (temperature is not null) - { - high = high is null ? temperature : Math.Max(high.Value, temperature.Value); - low = low is null ? temperature : Math.Min(low.Value, temperature.Value); - } + var currentDayHighLow = forecastDayOffset <= 0 + ? await TryResolveCurrentDayHighLowFromForecastAsync(location, useCelsius, cancellationToken) + : null; + var high = currentDayHighLow?.High ?? TryReadInt(selectedDayTemp, "max"); + var low = currentDayHighLow?.Low ?? TryReadInt(selectedDayTemp, "min"); 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 condition = TryReadWeatherCondition(root); var temperature = TryReadInt(main, "temp"); - var high = TryReadInt(main, "temp_max"); - var low = TryReadInt(main, "temp_min"); - if (temperature is not null) - { - high = high is null ? temperature : Math.Max(high.Value, temperature.Value); - low = low is null ? temperature : Math.Min(low.Value, temperature.Value); - } + var currentDayHighLow = await TryResolveCurrentDayHighLowFromForecastAsync(location, useCelsius, cancellationToken); + var high = currentDayHighLow?.High ?? TryReadInt(main, "temp_max"); + var low = currentDayHighLow?.Low ?? TryReadInt(main, "temp_min"); if (temperature is null && high is null && low is null) return null; @@ -515,4 +509,4 @@ public sealed class OpenWeatherReportProvider( int? LowTemperature, string? Summary, string? Condition); -} \ No newline at end of file +} diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/Infrastructure/ProviderCachingTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/Infrastructure/ProviderCachingTests.cs index b00da7a..6b27f3f 100644 --- a/OpenJibo/tests/Jibo.Cloud.Tests/Infrastructure/ProviderCachingTests.cs +++ b/OpenJibo/tests/Jibo.Cloud.Tests/Infrastructure/ProviderCachingTests.cs @@ -58,6 +58,9 @@ public sealed class ProviderCachingTests [Fact] 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 path = message.RequestUri?.AbsolutePath ?? string.Empty; @@ -71,12 +74,18 @@ public sealed class ProviderCachingTests "lat":38.8708, "lon":-94.1733, "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":[ - {"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) }; }); @@ -96,10 +105,11 @@ public sealed class ProviderCachingTests await provider.GetReportAsync(new WeatherReportRequest("Lone Jack,US", null, null, false, false, 0)); Assert.NotNull(report); - Assert.Equal(76, report!.Temperature); + Assert.Equal(83, report!.Temperature); 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/2.5/forecast")); } [Fact] @@ -153,6 +163,9 @@ public sealed class ProviderCachingTests [Fact] 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 path = message.RequestUri?.AbsolutePath ?? string.Empty; @@ -168,7 +181,13 @@ public sealed class ProviderCachingTests "application/json") }, "/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) }; }); @@ -192,6 +211,7 @@ public sealed class ProviderCachingTests Assert.Equal(66, report.LowTemperature); 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/forecast")); } [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 responseFactory) : HttpMessageHandler { @@ -422,4 +456,4 @@ public sealed class ProviderCachingTests return Task.FromResult(responseFactory(request)); } } -} \ No newline at end of file +} diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs index 204627a..1d85d26 100644 --- a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs +++ b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs @@ -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); } + [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 + { + ["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] public async Task BuildDecisionAsync_TriggerWithKnownIdentity_BuildsProactiveGreetingAndContext() { @@ -1712,6 +1737,7 @@ public sealed class JiboInteractionServiceTests Assert.Equal("personal_report_opt_in", decision.IntentName); Assert.Equal("Would you like your personal report now?", decision.ReplyText); + Assert.Equal("shared/yes_no", ((IReadOnlyList)decision.SkillPayload!["listen_contexts"])[0]); Assert.NotNull(decision.ContextUpdates); Assert.Equal("awaiting_opt_in", decision.ContextUpdates![PersonalReportStateKey]); Assert.Equal(true, decision.ContextUpdates[PersonalReportWeatherEnabledKey]); @@ -1742,6 +1768,7 @@ public sealed class JiboInteractionServiceTests Assert.Equal("personal_report_verify_user", decision.IntentName); Assert.Equal("I think this is alex. Is that right?", decision.ReplyText); + Assert.Equal("shared/yes_no", ((IReadOnlyList)decision.SkillPayload!["listen_contexts"])[0]); Assert.NotNull(decision.ContextUpdates); Assert.Equal("awaiting_identity_confirmation", decision.ContextUpdates![PersonalReportStateKey]); Assert.Equal("alex", decision.ContextUpdates[PersonalReportUserNameKey]);