mirror of
https://kevinblog.sytes.net/Code/Jibo-Revival-Group/JiboExperiments.git
synced 2026-06-16 18:56:23 +00:00
Expand weather forecast phrasing and day offsets
This commit is contained in:
@@ -12,7 +12,8 @@ public sealed record WeatherReportRequest(
|
|||||||
double? Latitude,
|
double? Latitude,
|
||||||
double? Longitude,
|
double? Longitude,
|
||||||
bool IsTomorrow,
|
bool IsTomorrow,
|
||||||
bool? UseCelsius);
|
bool? UseCelsius,
|
||||||
|
int? ForecastDayOffset = null);
|
||||||
|
|
||||||
public sealed record WeatherReportSnapshot(
|
public sealed record WeatherReportSnapshot(
|
||||||
string LocationName,
|
string LocationName,
|
||||||
|
|||||||
@@ -417,7 +417,8 @@ public sealed class JiboInteractionService(
|
|||||||
string transcript,
|
string transcript,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var dateEntity = TryResolveWeatherDateEntity(transcript);
|
var referenceLocalTime = TryResolveReferenceLocalTime(turn);
|
||||||
|
var weatherDate = ResolveWeatherDateEntity(turn, transcript, referenceLocalTime);
|
||||||
if (weatherReportProvider is null)
|
if (weatherReportProvider is null)
|
||||||
{
|
{
|
||||||
return new JiboInteractionDecision(
|
return new JiboInteractionDecision(
|
||||||
@@ -425,6 +426,13 @@ public sealed class JiboInteractionService(
|
|||||||
"I can check weather once my weather service is connected.");
|
"I can check weather once my weather service is connected.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (weatherDate.ForecastDayOffset > MaxWeatherForecastDayOffset)
|
||||||
|
{
|
||||||
|
return new JiboInteractionDecision(
|
||||||
|
"weather",
|
||||||
|
$"I can forecast up to {MaxWeatherForecastDayOffset} days ahead. Try tomorrow or another day this week.");
|
||||||
|
}
|
||||||
|
|
||||||
var locationQuery = TryResolveWeatherLocationQuery(transcript);
|
var locationQuery = TryResolveWeatherLocationQuery(transcript);
|
||||||
var weatherCoordinates = TryResolveWeatherCoordinates(turn);
|
var weatherCoordinates = TryResolveWeatherCoordinates(turn);
|
||||||
var useCelsius = ShouldUseCelsius(turn, transcript);
|
var useCelsius = ShouldUseCelsius(turn, transcript);
|
||||||
@@ -436,8 +444,9 @@ public sealed class JiboInteractionService(
|
|||||||
locationQuery,
|
locationQuery,
|
||||||
weatherCoordinates?.Latitude,
|
weatherCoordinates?.Latitude,
|
||||||
weatherCoordinates?.Longitude,
|
weatherCoordinates?.Longitude,
|
||||||
string.Equals(dateEntity, "tomorrow", StringComparison.OrdinalIgnoreCase),
|
string.Equals(weatherDate.DateEntity, "tomorrow", StringComparison.OrdinalIgnoreCase),
|
||||||
useCelsius),
|
useCelsius,
|
||||||
|
weatherDate.ForecastDayOffset),
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
}
|
}
|
||||||
catch (Exception) when (!cancellationToken.IsCancellationRequested)
|
catch (Exception) when (!cancellationToken.IsCancellationRequested)
|
||||||
@@ -452,8 +461,8 @@ public sealed class JiboInteractionService(
|
|||||||
"I couldn't fetch the weather right now. Please try again.");
|
"I couldn't fetch the weather right now. Please try again.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var spokenReply = BuildWeatherSpokenReply(snapshot, dateEntity);
|
var spokenReply = BuildWeatherSpokenReply(snapshot, weatherDate);
|
||||||
var weatherPayload = BuildWeatherSkillPayload(spokenReply, snapshot, TryResolveReferenceLocalTime(turn));
|
var weatherPayload = BuildWeatherSkillPayload(spokenReply, snapshot, referenceLocalTime);
|
||||||
return new JiboInteractionDecision(
|
return new JiboInteractionDecision(
|
||||||
"weather",
|
"weather",
|
||||||
spokenReply,
|
spokenReply,
|
||||||
@@ -463,7 +472,7 @@ public sealed class JiboInteractionService(
|
|||||||
|
|
||||||
private static string BuildWeatherSpokenReply(
|
private static string BuildWeatherSpokenReply(
|
||||||
WeatherReportSnapshot snapshot,
|
WeatherReportSnapshot snapshot,
|
||||||
string? dateEntity)
|
WeatherDateEntity weatherDate)
|
||||||
{
|
{
|
||||||
var unit = snapshot.UseCelsius ? "Celsius" : "Fahrenheit";
|
var unit = snapshot.UseCelsius ? "Celsius" : "Fahrenheit";
|
||||||
var summary = string.IsNullOrWhiteSpace(snapshot.Summary)
|
var summary = string.IsNullOrWhiteSpace(snapshot.Summary)
|
||||||
@@ -473,7 +482,7 @@ public sealed class JiboInteractionService(
|
|||||||
? "your area"
|
? "your area"
|
||||||
: snapshot.LocationName;
|
: snapshot.LocationName;
|
||||||
|
|
||||||
if (string.Equals(dateEntity, "tomorrow", StringComparison.OrdinalIgnoreCase))
|
if (weatherDate.ForecastDayOffset > 0)
|
||||||
{
|
{
|
||||||
var highText = snapshot.HighTemperature is null
|
var highText = snapshot.HighTemperature is null
|
||||||
? null
|
? null
|
||||||
@@ -486,7 +495,10 @@ public sealed class JiboInteractionService(
|
|||||||
: highText is not null && lowText is not null
|
: highText is not null && lowText is not null
|
||||||
? $" with {highText} and {lowText}"
|
? $" with {highText} and {lowText}"
|
||||||
: $" with {highText ?? lowText}";
|
: $" with {highText ?? lowText}";
|
||||||
return $"Tomorrow in {location}, expect {summary}{tempRange}.";
|
var forecastLeadIn = string.IsNullOrWhiteSpace(weatherDate.ForecastLeadIn)
|
||||||
|
? "Tomorrow"
|
||||||
|
: weatherDate.ForecastLeadIn;
|
||||||
|
return $"{forecastLeadIn} in {location}, expect {summary}{tempRange}.";
|
||||||
}
|
}
|
||||||
|
|
||||||
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}.";
|
||||||
@@ -1809,6 +1821,12 @@ public sealed class JiboInteractionService(
|
|||||||
|
|
||||||
private static bool IsWeatherRequest(string loweredTranscript)
|
private static bool IsWeatherRequest(string loweredTranscript)
|
||||||
{
|
{
|
||||||
|
var normalized = NormalizeCommandPhrase(loweredTranscript);
|
||||||
|
if (IsWeatherTopicQuestion(normalized))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (MatchesAny(
|
if (MatchesAny(
|
||||||
loweredTranscript,
|
loweredTranscript,
|
||||||
"weather",
|
"weather",
|
||||||
@@ -1864,6 +1882,41 @@ public sealed class JiboInteractionService(
|
|||||||
return WeatherConditionForecastPattern.IsMatch(loweredTranscript);
|
return WeatherConditionForecastPattern.IsMatch(loweredTranscript);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsWeatherTopicQuestion(string normalizedTranscript)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(normalizedTranscript))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var mentionsWeatherTopic =
|
||||||
|
normalizedTranscript.Contains("weather", StringComparison.Ordinal) ||
|
||||||
|
normalizedTranscript.Contains("forecast", StringComparison.Ordinal) ||
|
||||||
|
normalizedTranscript.Contains("temperature", StringComparison.Ordinal) ||
|
||||||
|
normalizedTranscript.Contains("humidity", StringComparison.Ordinal);
|
||||||
|
if (!mentionsWeatherTopic)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedTranscript.StartsWith("what ", StringComparison.Ordinal) ||
|
||||||
|
normalizedTranscript.StartsWith("how ", StringComparison.Ordinal) ||
|
||||||
|
normalizedTranscript.StartsWith("check ", StringComparison.Ordinal) ||
|
||||||
|
normalizedTranscript.StartsWith("show ", StringComparison.Ordinal) ||
|
||||||
|
normalizedTranscript.StartsWith("tell ", StringComparison.Ordinal) ||
|
||||||
|
normalizedTranscript.StartsWith("look up ", StringComparison.Ordinal) ||
|
||||||
|
normalizedTranscript.StartsWith("launch ", StringComparison.Ordinal) ||
|
||||||
|
normalizedTranscript.StartsWith("give me ", StringComparison.Ordinal) ||
|
||||||
|
normalizedTranscript.StartsWith("temperature ", StringComparison.Ordinal) ||
|
||||||
|
normalizedTranscript.StartsWith("forecast ", StringComparison.Ordinal) ||
|
||||||
|
normalizedTranscript.StartsWith("weather ", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return WeatherTopicLocationPattern.IsMatch(normalizedTranscript);
|
||||||
|
}
|
||||||
|
|
||||||
private static string? TryResolveWeatherLocationQuery(string transcript)
|
private static string? TryResolveWeatherLocationQuery(string transcript)
|
||||||
{
|
{
|
||||||
var normalized = NormalizeCommandPhrase(transcript);
|
var normalized = NormalizeCommandPhrase(transcript);
|
||||||
@@ -1975,15 +2028,247 @@ public sealed class JiboInteractionService(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? TryResolveWeatherDateEntity(string transcript)
|
private static WeatherDateEntity ResolveWeatherDateEntity(
|
||||||
|
TurnContext turn,
|
||||||
|
string transcript,
|
||||||
|
DateTimeOffset? referenceLocalTime)
|
||||||
{
|
{
|
||||||
var normalized = NormalizeCommandPhrase(transcript);
|
var entities = ReadEntities(turn);
|
||||||
if (MatchesAny(normalized, "tomorrow", "tomorrow s", "tomorrow's"))
|
if (TryResolveWeatherDateEntityFromClientEntities(entities, referenceLocalTime, out var entityFromClient))
|
||||||
{
|
{
|
||||||
return "tomorrow";
|
return entityFromClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
var normalized = NormalizeCommandPhrase(transcript);
|
||||||
|
if (string.IsNullOrWhiteSpace(normalized))
|
||||||
|
{
|
||||||
|
return WeatherDateEntity.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized.Contains("day after tomorrow", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return new WeatherDateEntity("day_after_tomorrow", 2, "The day after tomorrow");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MatchesAny(normalized, "tomorrow", "tomorrow s", "tomorrow's"))
|
||||||
|
{
|
||||||
|
return new WeatherDateEntity("tomorrow", 1, "Tomorrow");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (referenceLocalTime is not null &&
|
||||||
|
TryResolveWeatherTimeRangeOffset(normalized, referenceLocalTime.Value, out var rangeOffset, out var rangeLeadIn) &&
|
||||||
|
rangeOffset > 0)
|
||||||
|
{
|
||||||
|
return new WeatherDateEntity("range", rangeOffset, rangeLeadIn);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (referenceLocalTime is not null &&
|
||||||
|
TryResolveWeatherDayOfWeekOffset(normalized, referenceLocalTime.Value, out var dayOffset, out var dayName) &&
|
||||||
|
dayOffset > 0)
|
||||||
|
{
|
||||||
|
return new WeatherDateEntity("weekday", dayOffset, $"On {dayName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return WeatherDateEntity.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryResolveWeatherDateEntityFromClientEntities(
|
||||||
|
IReadOnlyDictionary<string, string> clientEntities,
|
||||||
|
DateTimeOffset? referenceLocalTime,
|
||||||
|
out WeatherDateEntity weatherDate)
|
||||||
|
{
|
||||||
|
weatherDate = WeatherDateEntity.None;
|
||||||
|
if (!TryReadClientWeatherDateValue(clientEntities, out var rawDateValue))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalizedDate = NormalizeCommandPhrase(rawDateValue);
|
||||||
|
if (normalizedDate.Contains("day after tomorrow", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
weatherDate = new WeatherDateEntity("day_after_tomorrow", 2, "The day after tomorrow");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MatchesAny(normalizedDate, "tomorrow", "tomorrow s", "tomorrow's"))
|
||||||
|
{
|
||||||
|
weatherDate = new WeatherDateEntity("tomorrow", 1, "Tomorrow");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (referenceLocalTime is not null &&
|
||||||
|
TryResolveWeatherTimeRangeOffset(normalizedDate, referenceLocalTime.Value, out var rangeOffset, out var rangeLeadIn) &&
|
||||||
|
rangeOffset > 0)
|
||||||
|
{
|
||||||
|
weatherDate = new WeatherDateEntity("range", rangeOffset, rangeLeadIn);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
DateOnly targetDate;
|
||||||
|
if (DateOnly.TryParse(rawDateValue, out var parsedDate))
|
||||||
|
{
|
||||||
|
targetDate = parsedDate;
|
||||||
|
}
|
||||||
|
else if (DateTimeOffset.TryParse(rawDateValue, out var parsedDateTimeOffset))
|
||||||
|
{
|
||||||
|
targetDate = DateOnly.FromDateTime(parsedDateTimeOffset.DateTime);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var referenceDate = DateOnly.FromDateTime((referenceLocalTime ?? DateTimeOffset.UtcNow).DateTime);
|
||||||
|
var dayOffset = targetDate.DayNumber - referenceDate.DayNumber;
|
||||||
|
if (dayOffset <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
weatherDate = dayOffset == 1
|
||||||
|
? new WeatherDateEntity("tomorrow", 1, "Tomorrow")
|
||||||
|
: new WeatherDateEntity(
|
||||||
|
"date",
|
||||||
|
dayOffset,
|
||||||
|
$"On {targetDate.ToDateTime(TimeOnly.MinValue).ToString("dddd", CultureInfo.InvariantCulture)}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadClientWeatherDateValue(
|
||||||
|
IReadOnlyDictionary<string, string> clientEntities,
|
||||||
|
out string dateValue)
|
||||||
|
{
|
||||||
|
foreach (var key in WeatherDateEntityKeys)
|
||||||
|
{
|
||||||
|
if (!clientEntities.TryGetValue(key, out var rawValue) ||
|
||||||
|
string.IsNullOrWhiteSpace(rawValue))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
dateValue = rawValue.Trim();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
dateValue = string.Empty;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryResolveWeatherDayOfWeekOffset(
|
||||||
|
string normalizedTranscript,
|
||||||
|
DateTimeOffset referenceLocalTime,
|
||||||
|
out int dayOffset,
|
||||||
|
out string dayName)
|
||||||
|
{
|
||||||
|
dayOffset = 0;
|
||||||
|
dayName = string.Empty;
|
||||||
|
|
||||||
|
var match = WeatherDayOfWeekPattern.Match(normalizedTranscript);
|
||||||
|
if (!match.Success)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dayToken = match.Groups["day"].Value;
|
||||||
|
if (!TryParseDayOfWeek(dayToken, out var targetDay))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentDay = referenceLocalTime.DayOfWeek;
|
||||||
|
dayOffset = ((int)targetDay - (int)currentDay + 7) % 7;
|
||||||
|
if (match.Groups["next"].Success)
|
||||||
|
{
|
||||||
|
dayOffset = dayOffset == 0 ? 7 : dayOffset + 7;
|
||||||
|
}
|
||||||
|
else if (match.Groups["this"].Success && dayOffset == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
dayName = CultureInfo.InvariantCulture.TextInfo.ToTitleCase(dayToken);
|
||||||
|
return dayOffset > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryResolveWeatherTimeRangeOffset(
|
||||||
|
string normalizedTranscript,
|
||||||
|
DateTimeOffset referenceLocalTime,
|
||||||
|
out int dayOffset,
|
||||||
|
out string leadIn)
|
||||||
|
{
|
||||||
|
dayOffset = 0;
|
||||||
|
leadIn = string.Empty;
|
||||||
|
if (string.IsNullOrWhiteSpace(normalizedTranscript))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasNextWeekend = normalizedTranscript.Contains("next weekend", StringComparison.Ordinal);
|
||||||
|
var hasThisWeekend =
|
||||||
|
normalizedTranscript.Contains("this weekend", StringComparison.Ordinal) ||
|
||||||
|
normalizedTranscript.Contains("the weekend", StringComparison.Ordinal) ||
|
||||||
|
normalizedTranscript.EndsWith("weekend", StringComparison.Ordinal);
|
||||||
|
if (hasNextWeekend || hasThisWeekend)
|
||||||
|
{
|
||||||
|
dayOffset = ((int)DayOfWeek.Saturday - (int)referenceLocalTime.DayOfWeek + 7) % 7;
|
||||||
|
if (hasNextWeekend)
|
||||||
|
{
|
||||||
|
dayOffset = dayOffset + 7;
|
||||||
|
leadIn = "Next weekend";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// If it's already Saturday, prefer forecasting Sunday for "this weekend".
|
||||||
|
if (dayOffset == 0 && referenceLocalTime.DayOfWeek == DayOfWeek.Saturday)
|
||||||
|
{
|
||||||
|
dayOffset = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
leadIn = "This weekend";
|
||||||
|
}
|
||||||
|
|
||||||
|
return dayOffset > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasNextWeek = normalizedTranscript.Contains("next week", StringComparison.Ordinal);
|
||||||
|
if (hasNextWeek)
|
||||||
|
{
|
||||||
|
dayOffset = 7;
|
||||||
|
leadIn = "Next week";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasThisWeek = normalizedTranscript.Contains("this week", StringComparison.Ordinal);
|
||||||
|
if (hasThisWeek)
|
||||||
|
{
|
||||||
|
dayOffset = referenceLocalTime.DayOfWeek == DayOfWeek.Saturday ? 1 : 2;
|
||||||
|
leadIn = "Later this week";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParseDayOfWeek(string dayToken, out DayOfWeek dayOfWeek)
|
||||||
|
{
|
||||||
|
dayOfWeek = DayOfWeek.Sunday;
|
||||||
|
return dayToken switch
|
||||||
|
{
|
||||||
|
"monday" => AssignDayOfWeek(DayOfWeek.Monday, out dayOfWeek),
|
||||||
|
"tuesday" => AssignDayOfWeek(DayOfWeek.Tuesday, out dayOfWeek),
|
||||||
|
"wednesday" => AssignDayOfWeek(DayOfWeek.Wednesday, out dayOfWeek),
|
||||||
|
"thursday" => AssignDayOfWeek(DayOfWeek.Thursday, out dayOfWeek),
|
||||||
|
"friday" => AssignDayOfWeek(DayOfWeek.Friday, out dayOfWeek),
|
||||||
|
"saturday" => AssignDayOfWeek(DayOfWeek.Saturday, out dayOfWeek),
|
||||||
|
"sunday" => AssignDayOfWeek(DayOfWeek.Sunday, out dayOfWeek),
|
||||||
|
_ => false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool AssignDayOfWeek(DayOfWeek value, out DayOfWeek target)
|
||||||
|
{
|
||||||
|
target = value;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? TryResolveWeatherConditionEntity(string transcript)
|
private static string? TryResolveWeatherConditionEntity(string transcript)
|
||||||
@@ -3018,6 +3303,11 @@ public sealed class JiboInteractionService(
|
|||||||
|
|
||||||
private sealed record PizzaSignal(PersonalAffinity? Affinity);
|
private sealed record PizzaSignal(PersonalAffinity? Affinity);
|
||||||
|
|
||||||
|
private sealed record WeatherDateEntity(string? DateEntity, int ForecastDayOffset, string? ForecastLeadIn)
|
||||||
|
{
|
||||||
|
public static WeatherDateEntity None { get; } = new(null, 0, null);
|
||||||
|
}
|
||||||
|
|
||||||
private static readonly Regex SplitAlarmPattern = new(
|
private static readonly Regex SplitAlarmPattern = new(
|
||||||
@"\b(?<hour>\d{1,2}|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve)(?:[:\s,-]+(?<minute>\d{2}|[a-z\-]+(?:\s+[a-z\-]+)?))?\s*(?<ampm>a[\s\.]*m\.?|p[\s\.]*m\.?)?\b",
|
@"\b(?<hour>\d{1,2}|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve)(?:[:\s,-]+(?<minute>\d{2}|[a-z\-]+(?:\s+[a-z\-]+)?))?\s*(?<ampm>a[\s\.]*m\.?|p[\s\.]*m\.?)?\b",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
||||||
@@ -3051,13 +3341,21 @@ public sealed class JiboInteractionService(
|
|||||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
||||||
|
|
||||||
private static readonly Regex WeatherLocationSuffixPattern = new(
|
private static readonly Regex WeatherLocationSuffixPattern = new(
|
||||||
@"\b(?:today|tonight|tomorrow|outside|right now|please|thanks|this weekend|next weekend|the weekend|weekend|this week|next week|on monday|on tuesday|on wednesday|on thursday|on friday|on saturday|on sunday|monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b",
|
@"\b(?:today|tonight|tomorrow|day after tomorrow|outside|right now|please|thanks|this weekend|next weekend|the weekend|weekend|this week|next week|on monday|on tuesday|on wednesday|on thursday|on friday|on saturday|on sunday|this monday|this tuesday|this wednesday|this thursday|this friday|this saturday|this sunday|next monday|next tuesday|next wednesday|next thursday|next friday|next saturday|next sunday|monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
||||||
|
|
||||||
private static readonly Regex WeatherConditionForecastPattern = new(
|
private static readonly Regex WeatherConditionForecastPattern = new(
|
||||||
@"\bwill it be\s+(sunny|cloudy|windy|foggy|stormy|rainy|snowy|hail|hailing)\b",
|
@"\bwill it be\s+(sunny|cloudy|windy|foggy|stormy|rainy|snowy|hail|hailing)\b",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
||||||
|
|
||||||
|
private static readonly Regex WeatherTopicLocationPattern = new(
|
||||||
|
@"\b(?:weather|forecast|temperature|humidity)\b.*\b(?:in|for|at)\s+[a-z]",
|
||||||
|
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
||||||
|
|
||||||
|
private static readonly Regex WeatherDayOfWeekPattern = new(
|
||||||
|
@"\b(?<next>next\s+)?(?<this>this\s+)?(?:on\s+)?(?<day>monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b",
|
||||||
|
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
||||||
|
|
||||||
private static readonly PizzaMimPrompt[] PizzaMimPrompts =
|
private static readonly PizzaMimPrompt[] PizzaMimPrompts =
|
||||||
[
|
[
|
||||||
new("RA_JBO_ShowPizzaMaking_AN_01", "<speak><anim cat='jiboji' filter='pizza-making'/></speak>"),
|
new("RA_JBO_ShowPizzaMaking_AN_01", "<speak><anim cat='jiboji' filter='pizza-making'/></speak>"),
|
||||||
@@ -3079,6 +3377,16 @@ public sealed class JiboInteractionService(
|
|||||||
" are my favourite "
|
" are my favourite "
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private static readonly string[] WeatherDateEntityKeys =
|
||||||
|
[
|
||||||
|
"date",
|
||||||
|
"sys.date",
|
||||||
|
"datetime",
|
||||||
|
"dateTime",
|
||||||
|
"date_time",
|
||||||
|
"day"
|
||||||
|
];
|
||||||
|
|
||||||
// Directly imported from Pegasus parser intent phrase families:
|
// Directly imported from Pegasus parser intent phrase families:
|
||||||
// userLikesThing / userDislikesThing / doesUserLikeThing / doesUserDislikeThing.
|
// userLikesThing / userDislikesThing / doesUserLikeThing / doesUserDislikeThing.
|
||||||
private static readonly (string Prefix, PersonalAffinity Affinity)[] PegasusUserAffinitySetPrefixes =
|
private static readonly (string Prefix, PersonalAffinity Affinity)[] PegasusUserAffinitySetPrefixes =
|
||||||
@@ -3152,6 +3460,8 @@ public sealed class JiboInteractionService(
|
|||||||
"our neighbourhood"
|
"our neighbourhood"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private const int MaxWeatherForecastDayOffset = 5;
|
||||||
|
|
||||||
private static readonly (string Phrase, string Station)[] RadioGenreAliases =
|
private static readonly (string Phrase, string Station)[] RadioGenreAliases =
|
||||||
[
|
[
|
||||||
("country music", "Country"),
|
("country music", "Country"),
|
||||||
|
|||||||
@@ -795,19 +795,20 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
var promptId = ReadPayloadString(skillPayload, "prompt_id") ?? "RUNTIME_PROMPT";
|
var promptId = ReadPayloadString(skillPayload, "prompt_id") ?? "RUNTIME_PROMPT";
|
||||||
var promptSubCategory = ReadPayloadString(skillPayload, "prompt_sub_category") ?? "AN";
|
var promptSubCategory = ReadPayloadString(skillPayload, "prompt_sub_category") ?? "AN";
|
||||||
var listenContexts = ReadPayloadStringArray(skillPayload, "listen_contexts");
|
var listenContexts = ReadPayloadStringArray(skillPayload, "listen_contexts");
|
||||||
|
var playConfig = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["esml"] = esml,
|
||||||
|
["meta"] = new
|
||||||
|
{
|
||||||
|
prompt_id = promptId,
|
||||||
|
prompt_sub_category = promptSubCategory,
|
||||||
|
mim_id = mimId,
|
||||||
|
mim_type = mimType
|
||||||
|
}
|
||||||
|
};
|
||||||
var jcpConfig = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
var jcpConfig = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
["play"] = new
|
["play"] = playConfig
|
||||||
{
|
|
||||||
esml,
|
|
||||||
meta = new
|
|
||||||
{
|
|
||||||
prompt_id = promptId,
|
|
||||||
prompt_sub_category = promptSubCategory,
|
|
||||||
mim_id = mimId,
|
|
||||||
mim_type = mimType
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (listenContexts.Count > 0)
|
if (listenContexts.Count > 0)
|
||||||
@@ -823,12 +824,16 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
var weatherHiLoView = BuildWeatherHiLoView(skillPayload);
|
var weatherHiLoView = BuildWeatherHiLoView(skillPayload);
|
||||||
if (weatherHiLoView is not null)
|
if (weatherHiLoView is not null)
|
||||||
{
|
{
|
||||||
jcpConfig["gui"] = new
|
var guiConfig = new
|
||||||
{
|
{
|
||||||
type = "Javascript",
|
type = "Javascript",
|
||||||
data = "views.weatherHiLo",
|
data = "views.weatherHiLo",
|
||||||
pause = true
|
pause = true
|
||||||
};
|
};
|
||||||
|
jcpConfig["gui"] = guiConfig;
|
||||||
|
playConfig["gui"] = guiConfig;
|
||||||
|
playConfig["no_matches_for_gui"] = 0;
|
||||||
|
playConfig["no_inputs_for_gui"] = 0;
|
||||||
jcpConfig["views"] = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
jcpConfig["views"] = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
["weatherHiLo"] = weatherHiLoView
|
["weatherHiLo"] = weatherHiLoView
|
||||||
@@ -1110,6 +1115,11 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var hiNumX = GetTemperatureLabelXPosition(370, high.Value);
|
||||||
|
var hiUnitX = GetTemperatureLabelXPosition(360, high.Value);
|
||||||
|
var loNumX = GetTemperatureLabelXPosition(1110, low.Value);
|
||||||
|
var loUnitX = GetTemperatureLabelXPosition(1100, low.Value);
|
||||||
|
|
||||||
return new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
return new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
["viewConfig"] = new
|
["viewConfig"] = new
|
||||||
@@ -1165,7 +1175,7 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
{
|
{
|
||||||
id = "hiNumLabel",
|
id = "hiNumLabel",
|
||||||
type = "Label",
|
type = "Label",
|
||||||
text = $"{high.Value}°",
|
text = $"{high.Value}\u00B0",
|
||||||
style = new
|
style = new
|
||||||
{
|
{
|
||||||
fontSize = "160",
|
fontSize = "160",
|
||||||
@@ -1174,7 +1184,7 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
fill = "#FFFFFF",
|
fill = "#FFFFFF",
|
||||||
align = "center"
|
align = "center"
|
||||||
},
|
},
|
||||||
position = new { x = 370, y = 430 },
|
position = new { x = hiNumX, y = 430 },
|
||||||
targetAnchor = new { x = 1, y = 1 }
|
targetAnchor = new { x = 1, y = 1 }
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
@@ -1190,14 +1200,14 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
fill = "#FFFFFF",
|
fill = "#FFFFFF",
|
||||||
align = "center"
|
align = "center"
|
||||||
},
|
},
|
||||||
position = new { x = 360, y = 418 },
|
position = new { x = hiUnitX, y = 418 },
|
||||||
targetAnchor = new { x = 0, y = 1 }
|
targetAnchor = new { x = 0, y = 1 }
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
id = "loNumLabel",
|
id = "loNumLabel",
|
||||||
type = "Label",
|
type = "Label",
|
||||||
text = $"{low.Value}°",
|
text = $"{low.Value}\u00B0",
|
||||||
style = new
|
style = new
|
||||||
{
|
{
|
||||||
fontSize = "160",
|
fontSize = "160",
|
||||||
@@ -1206,7 +1216,7 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
fill = "#FFFFFF",
|
fill = "#FFFFFF",
|
||||||
align = "center"
|
align = "center"
|
||||||
},
|
},
|
||||||
position = new { x = 1110, y = 430 },
|
position = new { x = loNumX, y = 430 },
|
||||||
targetAnchor = new { x = 1, y = 1 }
|
targetAnchor = new { x = 1, y = 1 }
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
@@ -1222,13 +1232,58 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
fill = "#FFFFFF",
|
fill = "#FFFFFF",
|
||||||
align = "center"
|
align = "center"
|
||||||
},
|
},
|
||||||
position = new { x = 1100, y = 418 },
|
position = new { x = loUnitX, y = 418 },
|
||||||
targetAnchor = new { x = 0, y = 1 }
|
targetAnchor = new { x = 0, y = 1 }
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
id = "hiTextLabel",
|
||||||
|
type = "Label",
|
||||||
|
text = "Hi",
|
||||||
|
style = new
|
||||||
|
{
|
||||||
|
fontSize = "60",
|
||||||
|
fontFamily = "Proxima Nova Light",
|
||||||
|
fill = "#FFFFFF",
|
||||||
|
align = "center"
|
||||||
|
},
|
||||||
|
position = new { x = 280, y = 496 },
|
||||||
|
targetAnchor = new { x = 0.5, y = 1 }
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
id = "loTextLabel",
|
||||||
|
type = "Label",
|
||||||
|
text = "Lo",
|
||||||
|
style = new
|
||||||
|
{
|
||||||
|
fontSize = "60",
|
||||||
|
fontFamily = "Proxima Nova Light",
|
||||||
|
fill = "#FFFFFF",
|
||||||
|
align = "center"
|
||||||
|
},
|
||||||
|
position = new { x = 990, y = 496 },
|
||||||
|
targetAnchor = new { x = 0.5, y = 1 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int GetTemperatureLabelXPosition(int baseX, int temperature)
|
||||||
|
{
|
||||||
|
const int xOffset = 70;
|
||||||
|
if (temperature < -9 || temperature > 99)
|
||||||
|
{
|
||||||
|
return baseX + xOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (temperature is >= 0 and < 10)
|
||||||
|
{
|
||||||
|
return baseX - xOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseX;
|
||||||
|
}
|
||||||
private static int? TryReadPayloadInt(IDictionary<string, object?>? payload, string key)
|
private static int? TryReadPayloadInt(IDictionary<string, object?>? payload, string key)
|
||||||
{
|
{
|
||||||
if (payload is null || !payload.TryGetValue(key, out var value) || value is null)
|
if (payload is null || !payload.TryGetValue(key, out var value) || value is null)
|
||||||
@@ -1279,3 +1334,4 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
|
|
||||||
public sealed record SocketReplyPlan(string Text, int DelayMs = 0);
|
public sealed record SocketReplyPlan(string Text, int DelayMs = 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,9 +29,18 @@ public sealed class OpenWeatherReportProvider(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var useCelsius = request.UseCelsius ?? options.UseCelsius;
|
var useCelsius = request.UseCelsius ?? options.UseCelsius;
|
||||||
return request.IsTomorrow
|
var forecastDayOffset = request.ForecastDayOffset ?? (request.IsTomorrow ? 1 : 0);
|
||||||
? await GetTomorrowForecastAsync(location.Value, useCelsius, cancellationToken)
|
if (forecastDayOffset <= 0)
|
||||||
: await GetCurrentWeatherAsync(location.Value, useCelsius, cancellationToken);
|
{
|
||||||
|
return await GetCurrentWeatherAsync(location.Value, useCelsius, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forecastDayOffset > MaxForecastDayOffset)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await GetForecastForDayOffsetAsync(location.Value, useCelsius, forecastDayOffset, cancellationToken);
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
@@ -134,9 +143,10 @@ public sealed class OpenWeatherReportProvider(
|
|||||||
useCelsius);
|
useCelsius);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<WeatherReportSnapshot?> GetTomorrowForecastAsync(
|
private async Task<WeatherReportSnapshot?> GetForecastForDayOffsetAsync(
|
||||||
LocationPoint location,
|
LocationPoint location,
|
||||||
bool useCelsius,
|
bool useCelsius,
|
||||||
|
int forecastDayOffset,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var forecastUri = BuildRequestUri(
|
var forecastUri = BuildRequestUri(
|
||||||
@@ -160,7 +170,7 @@ public sealed class OpenWeatherReportProvider(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var offset = TryReadForecastOffset(root);
|
var offset = TryReadForecastOffset(root);
|
||||||
var tomorrow = DateOnly.FromDateTime(DateTimeOffset.UtcNow.ToOffset(offset).DateTime.AddDays(1));
|
var targetDate = DateOnly.FromDateTime(DateTimeOffset.UtcNow.ToOffset(offset).DateTime.AddDays(forecastDayOffset));
|
||||||
var entries = new List<ForecastEntry>();
|
var entries = new List<ForecastEntry>();
|
||||||
foreach (var item in list.EnumerateArray())
|
foreach (var item in list.EnumerateArray())
|
||||||
{
|
{
|
||||||
@@ -170,7 +180,7 @@ public sealed class OpenWeatherReportProvider(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var localTimestamp = DateTimeOffset.FromUnixTimeSeconds(unixSeconds).ToOffset(offset);
|
var localTimestamp = DateTimeOffset.FromUnixTimeSeconds(unixSeconds).ToOffset(offset);
|
||||||
if (DateOnly.FromDateTime(localTimestamp.DateTime) != tomorrow)
|
if (DateOnly.FromDateTime(localTimestamp.DateTime) != targetDate)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -361,4 +371,6 @@ public sealed class OpenWeatherReportProvider(
|
|||||||
int? LowTemperature,
|
int? LowTemperature,
|
||||||
string? Summary,
|
string? Summary,
|
||||||
string? Condition);
|
string? Condition);
|
||||||
|
|
||||||
|
private const int MaxForecastDayOffset = 5;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1196,6 +1196,7 @@ public sealed class JiboInteractionServiceTests
|
|||||||
Assert.Equal("Right now in Boston, US, it is light rain and 61 degrees Fahrenheit.", decision.ReplyText);
|
Assert.Equal("Right now in Boston, US, it is light rain and 61 degrees Fahrenheit.", decision.ReplyText);
|
||||||
Assert.NotNull(provider.LastRequest);
|
Assert.NotNull(provider.LastRequest);
|
||||||
Assert.False(provider.LastRequest!.IsTomorrow);
|
Assert.False(provider.LastRequest!.IsTomorrow);
|
||||||
|
Assert.Equal(0, provider.LastRequest.ForecastDayOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -1216,6 +1217,7 @@ public sealed class JiboInteractionServiceTests
|
|||||||
Assert.Equal("weather", decision.IntentName);
|
Assert.Equal("weather", decision.IntentName);
|
||||||
Assert.Equal("Chicago", provider.LastRequest?.LocationQuery);
|
Assert.Equal("Chicago", provider.LastRequest?.LocationQuery);
|
||||||
Assert.True(provider.LastRequest?.IsTomorrow);
|
Assert.True(provider.LastRequest?.IsTomorrow);
|
||||||
|
Assert.Equal(1, provider.LastRequest?.ForecastDayOffset);
|
||||||
Assert.Equal("Tomorrow in Chicago, US, expect mostly cloudy with a high near 74 degrees Fahrenheit and a low around 60 degrees Fahrenheit.", decision.ReplyText);
|
Assert.Equal("Tomorrow in Chicago, US, expect mostly cloudy with a high near 74 degrees Fahrenheit and a low around 60 degrees Fahrenheit.", decision.ReplyText);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1237,6 +1239,7 @@ public sealed class JiboInteractionServiceTests
|
|||||||
Assert.Equal("weather", decision.IntentName);
|
Assert.Equal("weather", decision.IntentName);
|
||||||
Assert.Equal("Seattle", provider.LastRequest?.LocationQuery);
|
Assert.Equal("Seattle", provider.LastRequest?.LocationQuery);
|
||||||
Assert.False(provider.LastRequest?.IsTomorrow);
|
Assert.False(provider.LastRequest?.IsTomorrow);
|
||||||
|
Assert.Equal(0, provider.LastRequest?.ForecastDayOffset);
|
||||||
Assert.Equal("Right now in Seattle, US, it is light rain and 58 degrees Fahrenheit.", decision.ReplyText);
|
Assert.Equal("Right now in Seattle, US, it is light rain and 58 degrees Fahrenheit.", decision.ReplyText);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1258,9 +1261,231 @@ public sealed class JiboInteractionServiceTests
|
|||||||
Assert.Equal("weather", decision.IntentName);
|
Assert.Equal("weather", decision.IntentName);
|
||||||
Assert.Equal("Paris", provider.LastRequest?.LocationQuery);
|
Assert.Equal("Paris", provider.LastRequest?.LocationQuery);
|
||||||
Assert.False(provider.LastRequest?.IsTomorrow);
|
Assert.False(provider.LastRequest?.IsTomorrow);
|
||||||
|
Assert.Equal(0, provider.LastRequest?.ForecastDayOffset);
|
||||||
Assert.Equal("Right now in Paris, FR, it is overcast clouds and 66 degrees Fahrenheit.", decision.ReplyText);
|
Assert.Equal("Right now in Paris, FR, it is overcast clouds and 66 degrees Fahrenheit.", decision.ReplyText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BuildDecisionAsync_TemperatureLocationQuery_WithProvider_MapsToWeatherIntent()
|
||||||
|
{
|
||||||
|
var provider = new CapturingWeatherReportProvider
|
||||||
|
{
|
||||||
|
Snapshot = new WeatherReportSnapshot("Redmond, US", "clear sky", 63, 66, 52, "sunny", false)
|
||||||
|
};
|
||||||
|
var service = CreateService(weatherReportProvider: provider);
|
||||||
|
|
||||||
|
var decision = await service.BuildDecisionAsync(new TurnContext
|
||||||
|
{
|
||||||
|
RawTranscript = "what is the temperature in redmond oregon",
|
||||||
|
NormalizedTranscript = "what is the temperature in redmond oregon"
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal("weather", decision.IntentName);
|
||||||
|
Assert.Equal("Redmond Oregon", provider.LastRequest?.LocationQuery);
|
||||||
|
Assert.False(provider.LastRequest?.IsTomorrow);
|
||||||
|
Assert.Equal(0, provider.LastRequest?.ForecastDayOffset);
|
||||||
|
Assert.Equal("Right now in Redmond, US, it is clear sky and 63 degrees Fahrenheit.", decision.ReplyText);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BuildDecisionAsync_ForecastLocationQuery_WithProvider_MapsToWeatherIntent()
|
||||||
|
{
|
||||||
|
var provider = new CapturingWeatherReportProvider
|
||||||
|
{
|
||||||
|
Snapshot = new WeatherReportSnapshot("New York, US", "partly cloudy", 71, 76, 61, "cloudy", false)
|
||||||
|
};
|
||||||
|
var service = CreateService(weatherReportProvider: provider);
|
||||||
|
|
||||||
|
var decision = await service.BuildDecisionAsync(new TurnContext
|
||||||
|
{
|
||||||
|
RawTranscript = "forecast for new york city",
|
||||||
|
NormalizedTranscript = "forecast for new york city"
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BuildDecisionAsync_WeatherQueryWithClientDateEntity_UsesForecastDayOffset()
|
||||||
|
{
|
||||||
|
var provider = new CapturingWeatherReportProvider
|
||||||
|
{
|
||||||
|
Snapshot = new WeatherReportSnapshot("Portland, US", "scattered clouds", 64, 68, 53, "cloudy", false)
|
||||||
|
};
|
||||||
|
var service = CreateService(weatherReportProvider: provider);
|
||||||
|
|
||||||
|
var decision = await service.BuildDecisionAsync(new TurnContext
|
||||||
|
{
|
||||||
|
RawTranscript = "what's the weather",
|
||||||
|
NormalizedTranscript = "what's the weather",
|
||||||
|
Attributes = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["clientEntities"] = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["date"] = "2026-05-11"
|
||||||
|
},
|
||||||
|
["context"] = """{"runtime":{"location":{"iso":"2026-05-09T09:00:00-05:00"}}}"""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal("weather", decision.IntentName);
|
||||||
|
Assert.Equal(2, provider.LastRequest?.ForecastDayOffset);
|
||||||
|
Assert.False(provider.LastRequest?.IsTomorrow);
|
||||||
|
Assert.Equal("On Monday in Portland, US, expect scattered clouds with a high near 68 degrees Fahrenheit and a low around 53 degrees Fahrenheit.", decision.ReplyText);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BuildDecisionAsync_WeatherQueryWithWeekday_UsesForecastDayOffset()
|
||||||
|
{
|
||||||
|
var provider = new CapturingWeatherReportProvider
|
||||||
|
{
|
||||||
|
Snapshot = new WeatherReportSnapshot("Chicago, US", "light rain", 59, 63, 51, "rain", false)
|
||||||
|
};
|
||||||
|
var service = CreateService(weatherReportProvider: provider);
|
||||||
|
|
||||||
|
var decision = await service.BuildDecisionAsync(new TurnContext
|
||||||
|
{
|
||||||
|
RawTranscript = "what's the weather in chicago on tuesday",
|
||||||
|
NormalizedTranscript = "what's the weather in chicago on tuesday",
|
||||||
|
Attributes = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["context"] = """{"runtime":{"location":{"iso":"2026-04-20T08:00:00-05:00"}}}"""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal("weather", decision.IntentName);
|
||||||
|
Assert.Equal("Chicago", provider.LastRequest?.LocationQuery);
|
||||||
|
Assert.Equal(1, provider.LastRequest?.ForecastDayOffset);
|
||||||
|
Assert.Equal("On Tuesday in Chicago, US, expect light rain with a high near 63 degrees Fahrenheit and a low around 51 degrees Fahrenheit.", decision.ReplyText);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BuildDecisionAsync_WeatherQueryBeyondSupportedForecastRange_ReturnsGuardrailMessage()
|
||||||
|
{
|
||||||
|
var provider = new CapturingWeatherReportProvider
|
||||||
|
{
|
||||||
|
Snapshot = new WeatherReportSnapshot("Chicago, US", "light rain", 59, 63, 51, "rain", false)
|
||||||
|
};
|
||||||
|
var service = CreateService(weatherReportProvider: provider);
|
||||||
|
|
||||||
|
var decision = await service.BuildDecisionAsync(new TurnContext
|
||||||
|
{
|
||||||
|
RawTranscript = "what's the weather next saturday",
|
||||||
|
NormalizedTranscript = "what's the weather next saturday",
|
||||||
|
Attributes = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["context"] = """{"runtime":{"location":{"iso":"2026-04-20T08:00:00-05:00"}}}"""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal("weather", decision.IntentName);
|
||||||
|
Assert.Equal("I can forecast up to 5 days ahead. Try tomorrow or another day this week.", decision.ReplyText);
|
||||||
|
Assert.Null(provider.LastRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BuildDecisionAsync_WeatherThisWeekend_WithContext_UsesWeekendOffset()
|
||||||
|
{
|
||||||
|
var provider = new CapturingWeatherReportProvider
|
||||||
|
{
|
||||||
|
Snapshot = new WeatherReportSnapshot("Paris, FR", "overcast clouds", 66, 70, 60, "cloudy", false)
|
||||||
|
};
|
||||||
|
var service = CreateService(weatherReportProvider: provider);
|
||||||
|
|
||||||
|
var decision = await service.BuildDecisionAsync(new TurnContext
|
||||||
|
{
|
||||||
|
RawTranscript = "what's the weather in paris this weekend",
|
||||||
|
NormalizedTranscript = "what's the weather in paris this weekend",
|
||||||
|
Attributes = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["context"] = """{"runtime":{"location":{"iso":"2026-04-20T08:00:00-05:00"}}}"""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal("weather", decision.IntentName);
|
||||||
|
Assert.Equal("Paris", provider.LastRequest?.LocationQuery);
|
||||||
|
Assert.Equal(5, provider.LastRequest?.ForecastDayOffset);
|
||||||
|
Assert.Equal("This weekend in Paris, FR, expect overcast clouds with a high near 70 degrees Fahrenheit and a low around 60 degrees Fahrenheit.", decision.ReplyText);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BuildDecisionAsync_WeatherThisWeek_WithContext_UsesRangeOffset()
|
||||||
|
{
|
||||||
|
var provider = new CapturingWeatherReportProvider
|
||||||
|
{
|
||||||
|
Snapshot = new WeatherReportSnapshot("Seattle, US", "light rain", 58, 61, 52, "rain", false)
|
||||||
|
};
|
||||||
|
var service = CreateService(weatherReportProvider: provider);
|
||||||
|
|
||||||
|
var decision = await service.BuildDecisionAsync(new TurnContext
|
||||||
|
{
|
||||||
|
RawTranscript = "forecast for seattle this week",
|
||||||
|
NormalizedTranscript = "forecast for seattle this week",
|
||||||
|
Attributes = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["context"] = """{"runtime":{"location":{"iso":"2026-04-20T08:00:00-05:00"}}}"""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal("weather", decision.IntentName);
|
||||||
|
Assert.Equal("Seattle", provider.LastRequest?.LocationQuery);
|
||||||
|
Assert.Equal(2, provider.LastRequest?.ForecastDayOffset);
|
||||||
|
Assert.Equal("Later this week in Seattle, US, expect light rain with a high near 61 degrees Fahrenheit and a low around 52 degrees Fahrenheit.", decision.ReplyText);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BuildDecisionAsync_WeatherNextWeek_WithContext_ReturnsGuardrailMessage()
|
||||||
|
{
|
||||||
|
var provider = new CapturingWeatherReportProvider
|
||||||
|
{
|
||||||
|
Snapshot = new WeatherReportSnapshot("Seattle, US", "light rain", 58, 61, 52, "rain", false)
|
||||||
|
};
|
||||||
|
var service = CreateService(weatherReportProvider: provider);
|
||||||
|
|
||||||
|
var decision = await service.BuildDecisionAsync(new TurnContext
|
||||||
|
{
|
||||||
|
RawTranscript = "forecast for seattle next week",
|
||||||
|
NormalizedTranscript = "forecast for seattle next week",
|
||||||
|
Attributes = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["context"] = """{"runtime":{"location":{"iso":"2026-04-20T08:00:00-05:00"}}}"""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal("weather", decision.IntentName);
|
||||||
|
Assert.Equal("I can forecast up to 5 days ahead. Try tomorrow or another day this week.", decision.ReplyText);
|
||||||
|
Assert.Null(provider.LastRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BuildDecisionAsync_WeatherDayAfterTomorrow_WithContext_PassesDayOffsetAndLocation()
|
||||||
|
{
|
||||||
|
var provider = new CapturingWeatherReportProvider
|
||||||
|
{
|
||||||
|
Snapshot = new WeatherReportSnapshot("Chicago, US", "mostly cloudy", 72, 74, 60, "cloudy", false)
|
||||||
|
};
|
||||||
|
var service = CreateService(weatherReportProvider: provider);
|
||||||
|
|
||||||
|
var decision = await service.BuildDecisionAsync(new TurnContext
|
||||||
|
{
|
||||||
|
RawTranscript = "what's the weather in chicago day after tomorrow",
|
||||||
|
NormalizedTranscript = "what's the weather in chicago day after tomorrow",
|
||||||
|
Attributes = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["context"] = """{"runtime":{"location":{"iso":"2026-04-20T08:00:00-05:00"}}}"""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal("weather", decision.IntentName);
|
||||||
|
Assert.Equal("Chicago", provider.LastRequest?.LocationQuery);
|
||||||
|
Assert.Equal(2, provider.LastRequest?.ForecastDayOffset);
|
||||||
|
Assert.Equal("The day after tomorrow in Chicago, US, expect mostly cloudy with a high near 74 degrees Fahrenheit and a low around 60 degrees Fahrenheit.", decision.ReplyText);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task BuildDecisionAsync_ClientNluAskForDate_MapsToDateIntent()
|
public async Task BuildDecisionAsync_ClientNluAskForDate_MapsToDateIntent()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2035,6 +2035,12 @@ public sealed class JiboWebSocketServiceTests
|
|||||||
Assert.Equal("views.weatherHiLo", gui.GetProperty("data").GetString());
|
Assert.Equal("views.weatherHiLo", gui.GetProperty("data").GetString());
|
||||||
Assert.True(gui.GetProperty("pause").GetBoolean());
|
Assert.True(gui.GetProperty("pause").GetBoolean());
|
||||||
|
|
||||||
|
var play = jcpConfig.GetProperty("play");
|
||||||
|
Assert.True(play.TryGetProperty("gui", out var playGui));
|
||||||
|
Assert.Equal("views.weatherHiLo", playGui.GetProperty("data").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("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());
|
||||||
|
|||||||
Reference in New Issue
Block a user