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 f302d32..d0f7b74 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 @@ -419,6 +419,12 @@ public sealed class JiboInteractionService( { var referenceLocalTime = TryResolveReferenceLocalTime(turn); var weatherDate = ResolveWeatherDateEntity(turn, transcript, referenceLocalTime); + var normalizedTranscript = NormalizeCommandPhrase(transcript); + if (ShouldDefaultForecastToTomorrow(normalizedTranscript, weatherDate)) + { + weatherDate = new WeatherDateEntity("tomorrow", 1, "Tomorrow"); + } + if (weatherReportProvider is null) { return new JiboInteractionDecision( @@ -434,7 +440,9 @@ public sealed class JiboInteractionService( } var locationQuery = TryResolveWeatherLocationQuery(transcript); - var weatherCoordinates = TryResolveWeatherCoordinates(turn); + var weatherCoordinates = string.IsNullOrWhiteSpace(locationQuery) + ? TryResolveWeatherCoordinates(turn) + : null; var useCelsius = ShouldUseCelsius(turn, transcript); WeatherReportSnapshot? snapshot; try @@ -504,6 +512,26 @@ public sealed class JiboInteractionService( return $"Right now in {location}, it is {summary} and {snapshot.Temperature} degrees {unit}."; } + private static bool ShouldDefaultForecastToTomorrow(string normalizedTranscript, WeatherDateEntity weatherDate) + { + if (weatherDate.ForecastDayOffset > 0 || + string.IsNullOrWhiteSpace(normalizedTranscript) || + !normalizedTranscript.Contains("forecast", StringComparison.Ordinal)) + { + return false; + } + + return !MatchesAny( + normalizedTranscript, + "today", + "today s", + "today's", + "tonight", + "right now", + "current weather", + "currently"); + } + private static IDictionary BuildWeatherSkillPayload( string spokenReply, WeatherReportSnapshot snapshot, diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/ResponsePlanToSocketMessagesMapper.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/ResponsePlanToSocketMessagesMapper.cs index 3157f1b..073a37e 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/ResponsePlanToSocketMessagesMapper.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/ResponsePlanToSocketMessagesMapper.cs @@ -824,20 +824,42 @@ public sealed class ResponsePlanToSocketMessagesMapper var weatherHiLoView = BuildWeatherHiLoView(skillPayload); if (weatherHiLoView is not null) { - var guiConfig = new + var resolvedGuiConfig = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["type"] = "Javascript", + ["data"] = weatherHiLoView, + ["pause"] = true + }; + + var legacyGuiConfig = new { type = "Javascript", data = "views.weatherHiLo", pause = true }; - jcpConfig["gui"] = guiConfig; - playConfig["gui"] = guiConfig; + + jcpConfig["gui"] = legacyGuiConfig; + jcpConfig["display"] = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["view"] = resolvedGuiConfig + }; + + playConfig["gui"] = resolvedGuiConfig; playConfig["no_matches_for_gui"] = 0; playConfig["no_inputs_for_gui"] = 0; + + var weatherViews = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["weatherHiLo"] = weatherHiLoView + }; jcpConfig["views"] = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["weatherHiLo"] = weatherHiLoView }; + jcpConfig["local"] = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["views"] = weatherViews + }; } return new 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 a1ca235..78dd8dd 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 @@ -53,14 +53,20 @@ public sealed class OpenWeatherReportProvider( WeatherReportRequest request, CancellationToken cancellationToken) { - if (request is { Latitude: not null, Longitude: not null }) + var query = string.IsNullOrWhiteSpace(request.LocationQuery) + ? null + : request.LocationQuery.Trim(); + + if (string.IsNullOrWhiteSpace(query)) { - return new LocationPoint(request.Latitude.Value, request.Longitude.Value, null); + if (request is { Latitude: not null, Longitude: not null }) + { + return new LocationPoint(request.Latitude.Value, request.Longitude.Value, null); + } + + query = options.DefaultLocation; } - var query = string.IsNullOrWhiteSpace(request.LocationQuery) - ? options.DefaultLocation - : request.LocationQuery.Trim(); if (string.IsNullOrWhiteSpace(query)) { return null; diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs index e15a0c2..8db1278 100644 --- a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs +++ b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs @@ -1304,9 +1304,93 @@ public sealed class JiboInteractionServiceTests Assert.Equal("weather", decision.IntentName); Assert.Equal("New York City", provider.LastRequest?.LocationQuery); - Assert.False(provider.LastRequest?.IsTomorrow); - Assert.Equal(0, provider.LastRequest?.ForecastDayOffset); - Assert.Equal("Right now in New York, US, it is partly cloudy and 71 degrees Fahrenheit.", decision.ReplyText); + Assert.True(provider.LastRequest?.IsTomorrow); + Assert.Equal(1, provider.LastRequest?.ForecastDayOffset); + Assert.Equal("Tomorrow in New York, US, expect partly cloudy with a high near 76 degrees Fahrenheit and a low around 61 degrees Fahrenheit.", decision.ReplyText); + } + + [Fact] + public async Task BuildDecisionAsync_ForecastWithoutDate_WithProvider_DefaultsToTomorrow() + { + var provider = new CapturingWeatherReportProvider + { + Snapshot = new WeatherReportSnapshot("Kansas City, US", "clear sky", 72, 79, 63, "sunny", false) + }; + var service = CreateService(weatherReportProvider: provider); + + var decision = await service.BuildDecisionAsync(new TurnContext + { + RawTranscript = "what's the forecast", + NormalizedTranscript = "what's the forecast" + }); + + Assert.Equal("weather", decision.IntentName); + Assert.Null(provider.LastRequest?.LocationQuery); + Assert.True(provider.LastRequest?.IsTomorrow); + Assert.Equal(1, provider.LastRequest?.ForecastDayOffset); + Assert.Equal("Tomorrow in Kansas City, US, expect clear sky with a high near 79 degrees Fahrenheit and a low around 63 degrees Fahrenheit.", decision.ReplyText); + } + + [Fact] + public async Task BuildDecisionAsync_WeatherLocationQuery_IgnoresRuntimeCoordinates() + { + var provider = new CapturingWeatherReportProvider + { + Snapshot = new WeatherReportSnapshot("Chicago, US", "mostly cloudy", 70, 75, 62, "cloudy", false) + }; + var service = CreateService(weatherReportProvider: provider); + + var decision = await service.BuildDecisionAsync(new TurnContext + { + RawTranscript = "what's the weather in chicago", + NormalizedTranscript = "what's the weather in chicago", + Attributes = new Dictionary + { + ["context"] = """{"runtime":{"location":{"lat":39.0997,"lng":-94.5786,"iso":"2026-05-09T09:00:00-05:00"}}}""" + } + }); + + Assert.Equal("weather", decision.IntentName); + Assert.Equal("Chicago", provider.LastRequest?.LocationQuery); + Assert.Null(provider.LastRequest?.Latitude); + Assert.Null(provider.LastRequest?.Longitude); + Assert.Equal("Right now in Chicago, US, it is mostly cloudy and 70 degrees Fahrenheit.", decision.ReplyText); + } + + [Theory] + [InlineData("how is the weather", null, 0, false)] + [InlineData("what's the forecast", null, 1, true)] + [InlineData("forecast for new york city", "New York City", 1, true)] + [InlineData("what's today's forecast", null, 0, false)] + [InlineData("what's the weather in chicago", "Chicago", 0, false)] + [InlineData("what's the weather in chicago tomorrow", "Chicago", 1, true)] + [InlineData("what is the temperature in redmond oregon", "Redmond Oregon", 0, false)] + [InlineData("will it rain tomorrow", null, 1, true)] + public async Task BuildDecisionAsync_WeatherPromptRegression_MatchesExpectedRouting( + string transcript, + string? expectedLocationQuery, + int expectedForecastOffset, + bool expectedIsTomorrow) + { + var provider = new CapturingWeatherReportProvider + { + Snapshot = new WeatherReportSnapshot("Test City, US", "light rain", 62, 66, 55, "rain", false) + }; + var service = CreateService(weatherReportProvider: provider); + + var decision = await service.BuildDecisionAsync(new TurnContext + { + RawTranscript = transcript, + NormalizedTranscript = transcript + }); + + Assert.Equal("weather", decision.IntentName); + Assert.NotNull(provider.LastRequest); + Assert.Equal(expectedLocationQuery, provider.LastRequest!.LocationQuery); + Assert.Equal(expectedForecastOffset, provider.LastRequest.ForecastDayOffset); + Assert.Equal(expectedIsTomorrow, provider.LastRequest.IsTomorrow); + Assert.Equal("chitchat-skill", decision.SkillName); + Assert.Equal(true, decision.SkillPayload?["weather_view_enabled"]); } [Fact] diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboWebSocketServiceTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboWebSocketServiceTests.cs index 39b926c..8355eef 100644 --- a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboWebSocketServiceTests.cs +++ b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboWebSocketServiceTests.cs @@ -2037,14 +2037,26 @@ public sealed class JiboWebSocketServiceTests var play = jcpConfig.GetProperty("play"); Assert.True(play.TryGetProperty("gui", out var playGui)); - Assert.Equal("views.weatherHiLo", playGui.GetProperty("data").GetString()); + Assert.Equal("Javascript", playGui.GetProperty("type").GetString()); + Assert.True(playGui.GetProperty("pause").GetBoolean()); + Assert.Equal("weatherTempView", playGui.GetProperty("data").GetProperty("viewConfig").GetProperty("id").GetString()); Assert.Equal(0, play.GetProperty("no_matches_for_gui").GetInt32()); Assert.Equal(0, play.GetProperty("no_inputs_for_gui").GetInt32()); + Assert.True(jcpConfig.TryGetProperty("display", out var display)); + Assert.Equal( + "weatherTempView", + display.GetProperty("view").GetProperty("data").GetProperty("viewConfig").GetProperty("id").GetString()); + Assert.True(jcpConfig.TryGetProperty("views", out var views)); var weatherHiLo = views.GetProperty("weatherHiLo"); Assert.Equal("weatherTempView", weatherHiLo.GetProperty("viewConfig").GetProperty("id").GetString()); + Assert.True(jcpConfig.TryGetProperty("local", out var local)); + Assert.Equal( + "weatherTempView", + local.GetProperty("views").GetProperty("weatherHiLo").GetProperty("viewConfig").GetProperty("id").GetString()); + var payloadText = replies[2].Text!; Assert.Contains("assets/personal-report-skill/weather/icons/rain_v01.crn", payloadText, StringComparison.Ordinal); Assert.Contains("tempNormal_v01.crn", payloadText, StringComparison.Ordinal);