Fix weather forecast routing and Hi/Lo rendering
This commit is contained in:
@@ -419,6 +419,12 @@ public sealed class JiboInteractionService(
|
|||||||
{
|
{
|
||||||
var referenceLocalTime = TryResolveReferenceLocalTime(turn);
|
var referenceLocalTime = TryResolveReferenceLocalTime(turn);
|
||||||
var weatherDate = ResolveWeatherDateEntity(turn, transcript, referenceLocalTime);
|
var weatherDate = ResolveWeatherDateEntity(turn, transcript, referenceLocalTime);
|
||||||
|
var normalizedTranscript = NormalizeCommandPhrase(transcript);
|
||||||
|
if (ShouldDefaultForecastToTomorrow(normalizedTranscript, weatherDate))
|
||||||
|
{
|
||||||
|
weatherDate = new WeatherDateEntity("tomorrow", 1, "Tomorrow");
|
||||||
|
}
|
||||||
|
|
||||||
if (weatherReportProvider is null)
|
if (weatherReportProvider is null)
|
||||||
{
|
{
|
||||||
return new JiboInteractionDecision(
|
return new JiboInteractionDecision(
|
||||||
@@ -434,7 +440,9 @@ public sealed class JiboInteractionService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var locationQuery = TryResolveWeatherLocationQuery(transcript);
|
var locationQuery = TryResolveWeatherLocationQuery(transcript);
|
||||||
var weatherCoordinates = TryResolveWeatherCoordinates(turn);
|
var weatherCoordinates = string.IsNullOrWhiteSpace(locationQuery)
|
||||||
|
? TryResolveWeatherCoordinates(turn)
|
||||||
|
: null;
|
||||||
var useCelsius = ShouldUseCelsius(turn, transcript);
|
var useCelsius = ShouldUseCelsius(turn, transcript);
|
||||||
WeatherReportSnapshot? snapshot;
|
WeatherReportSnapshot? snapshot;
|
||||||
try
|
try
|
||||||
@@ -504,6 +512,26 @@ public sealed class JiboInteractionService(
|
|||||||
return $"Right now in {location}, it is {summary} and {snapshot.Temperature} degrees {unit}.";
|
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<string, object?> BuildWeatherSkillPayload(
|
private static IDictionary<string, object?> BuildWeatherSkillPayload(
|
||||||
string spokenReply,
|
string spokenReply,
|
||||||
WeatherReportSnapshot snapshot,
|
WeatherReportSnapshot snapshot,
|
||||||
|
|||||||
@@ -824,20 +824,42 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
var weatherHiLoView = BuildWeatherHiLoView(skillPayload);
|
var weatherHiLoView = BuildWeatherHiLoView(skillPayload);
|
||||||
if (weatherHiLoView is not null)
|
if (weatherHiLoView is not null)
|
||||||
{
|
{
|
||||||
var guiConfig = new
|
var resolvedGuiConfig = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["type"] = "Javascript",
|
||||||
|
["data"] = weatherHiLoView,
|
||||||
|
["pause"] = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var legacyGuiConfig = new
|
||||||
{
|
{
|
||||||
type = "Javascript",
|
type = "Javascript",
|
||||||
data = "views.weatherHiLo",
|
data = "views.weatherHiLo",
|
||||||
pause = true
|
pause = true
|
||||||
};
|
};
|
||||||
jcpConfig["gui"] = guiConfig;
|
|
||||||
playConfig["gui"] = guiConfig;
|
jcpConfig["gui"] = legacyGuiConfig;
|
||||||
|
jcpConfig["display"] = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["view"] = resolvedGuiConfig
|
||||||
|
};
|
||||||
|
|
||||||
|
playConfig["gui"] = resolvedGuiConfig;
|
||||||
playConfig["no_matches_for_gui"] = 0;
|
playConfig["no_matches_for_gui"] = 0;
|
||||||
playConfig["no_inputs_for_gui"] = 0;
|
playConfig["no_inputs_for_gui"] = 0;
|
||||||
|
|
||||||
|
var weatherViews = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["weatherHiLo"] = weatherHiLoView
|
||||||
|
};
|
||||||
jcpConfig["views"] = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
jcpConfig["views"] = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
["weatherHiLo"] = weatherHiLoView
|
["weatherHiLo"] = weatherHiLoView
|
||||||
};
|
};
|
||||||
|
jcpConfig["local"] = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["views"] = weatherViews
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return new
|
return new
|
||||||
|
|||||||
@@ -53,14 +53,20 @@ public sealed class OpenWeatherReportProvider(
|
|||||||
WeatherReportRequest request,
|
WeatherReportRequest request,
|
||||||
CancellationToken cancellationToken)
|
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))
|
if (string.IsNullOrWhiteSpace(query))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1304,9 +1304,93 @@ public sealed class JiboInteractionServiceTests
|
|||||||
|
|
||||||
Assert.Equal("weather", decision.IntentName);
|
Assert.Equal("weather", decision.IntentName);
|
||||||
Assert.Equal("New York City", provider.LastRequest?.LocationQuery);
|
Assert.Equal("New York City", provider.LastRequest?.LocationQuery);
|
||||||
Assert.False(provider.LastRequest?.IsTomorrow);
|
Assert.True(provider.LastRequest?.IsTomorrow);
|
||||||
Assert.Equal(0, provider.LastRequest?.ForecastDayOffset);
|
Assert.Equal(1, provider.LastRequest?.ForecastDayOffset);
|
||||||
Assert.Equal("Right now in New York, US, it is partly cloudy and 71 degrees Fahrenheit.", decision.ReplyText);
|
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<string, object?>
|
||||||
|
{
|
||||||
|
["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]
|
[Fact]
|
||||||
|
|||||||
@@ -2037,14 +2037,26 @@ public sealed class JiboWebSocketServiceTests
|
|||||||
|
|
||||||
var play = jcpConfig.GetProperty("play");
|
var play = jcpConfig.GetProperty("play");
|
||||||
Assert.True(play.TryGetProperty("gui", out var playGui));
|
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_matches_for_gui").GetInt32());
|
||||||
Assert.Equal(0, play.GetProperty("no_inputs_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));
|
Assert.True(jcpConfig.TryGetProperty("views", out var views));
|
||||||
var weatherHiLo = views.GetProperty("weatherHiLo");
|
var weatherHiLo = views.GetProperty("weatherHiLo");
|
||||||
Assert.Equal("weatherTempView", weatherHiLo.GetProperty("viewConfig").GetProperty("id").GetString());
|
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!;
|
var payloadText = replies[2].Text!;
|
||||||
Assert.Contains("assets/personal-report-skill/weather/icons/rain_v01.crn", payloadText, StringComparison.Ordinal);
|
Assert.Contains("assets/personal-report-skill/weather/icons/rain_v01.crn", payloadText, StringComparison.Ordinal);
|
||||||
Assert.Contains("tempNormal_v01.crn", payloadText, StringComparison.Ordinal);
|
Assert.Contains("tempNormal_v01.crn", payloadText, StringComparison.Ordinal);
|
||||||
|
|||||||
Reference in New Issue
Block a user