Compare commits
2 Commits
383c272d9a
...
a94b7ec493
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a94b7ec493 | ||
|
|
8c17ad4035 |
@@ -608,18 +608,64 @@ public sealed class JiboInteractionService(
|
||||
"I can check weather once my weather service is connected.");
|
||||
}
|
||||
|
||||
var locationQuery = TryResolveWeatherLocationQuery(transcript);
|
||||
var weatherCoordinates = string.IsNullOrWhiteSpace(locationQuery)
|
||||
? TryResolveWeatherCoordinates(turn)
|
||||
: null;
|
||||
var useCelsius = ShouldUseCelsius(turn, transcript);
|
||||
var isNextWeekForecast = IsNextWeekForecastRequest(normalizedTranscript);
|
||||
|
||||
if (isNextWeekForecast)
|
||||
{
|
||||
var weeklySnapshots = new List<(int DayOffset, WeatherReportSnapshot Snapshot)>();
|
||||
for (var offset = 1; offset <= MaxWeatherForecastDayOffset; offset += 1)
|
||||
{
|
||||
WeatherReportSnapshot? weeklySnapshot;
|
||||
try
|
||||
{
|
||||
weeklySnapshot = await weatherReportProvider.GetReportAsync(
|
||||
new WeatherReportRequest(
|
||||
locationQuery,
|
||||
weatherCoordinates?.Latitude,
|
||||
weatherCoordinates?.Longitude,
|
||||
IsTomorrow: offset == 1,
|
||||
useCelsius,
|
||||
ForecastDayOffset: offset),
|
||||
cancellationToken);
|
||||
}
|
||||
catch (Exception) when (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
weeklySnapshot = null;
|
||||
}
|
||||
|
||||
if (weeklySnapshot is not null)
|
||||
{
|
||||
weeklySnapshots.Add((offset, weeklySnapshot));
|
||||
}
|
||||
}
|
||||
|
||||
if (weeklySnapshots.Count == 0)
|
||||
{
|
||||
return new JiboInteractionDecision(
|
||||
"weather",
|
||||
"I couldn't fetch the weather right now. Please try again.");
|
||||
}
|
||||
|
||||
var weeklySpokenReply = BuildNextWeekForecastSpokenReply(weeklySnapshots, referenceLocalTime);
|
||||
var weeklyWeatherPayload = BuildWeatherSkillPayload(weeklySpokenReply, weeklySnapshots[0].Snapshot, referenceLocalTime);
|
||||
return new JiboInteractionDecision(
|
||||
"weather",
|
||||
weeklySpokenReply,
|
||||
"chitchat-skill",
|
||||
SkillPayload: weeklyWeatherPayload);
|
||||
}
|
||||
|
||||
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 weatherCoordinates = string.IsNullOrWhiteSpace(locationQuery)
|
||||
? TryResolveWeatherCoordinates(turn)
|
||||
: null;
|
||||
var useCelsius = ShouldUseCelsius(turn, transcript);
|
||||
WeatherReportSnapshot? snapshot;
|
||||
try
|
||||
{
|
||||
@@ -688,6 +734,52 @@ public sealed class JiboInteractionService(
|
||||
return $"Right now in {location}, it is {summary} and {snapshot.Temperature} degrees {unit}.";
|
||||
}
|
||||
|
||||
private static string BuildNextWeekForecastSpokenReply(
|
||||
IReadOnlyList<(int DayOffset, WeatherReportSnapshot Snapshot)> snapshots,
|
||||
DateTimeOffset? referenceLocalTime)
|
||||
{
|
||||
if (snapshots.Count == 0)
|
||||
{
|
||||
return "I couldn't build a forecast right now.";
|
||||
}
|
||||
|
||||
var location = snapshots[0].Snapshot.LocationName;
|
||||
if (string.IsNullOrWhiteSpace(location))
|
||||
{
|
||||
location = "your area";
|
||||
}
|
||||
|
||||
var unit = snapshots[0].Snapshot.UseCelsius ? "Celsius" : "Fahrenheit";
|
||||
var referenceDate = (referenceLocalTime ?? DateTimeOffset.UtcNow).Date;
|
||||
var segments = snapshots
|
||||
.OrderBy(static item => item.DayOffset)
|
||||
.Take(MaxWeatherForecastDayOffset)
|
||||
.Select(item =>
|
||||
{
|
||||
var dayName = referenceDate.AddDays(item.DayOffset).ToString("dddd", CultureInfo.InvariantCulture);
|
||||
var summary = string.IsNullOrWhiteSpace(item.Snapshot.Summary)
|
||||
? "partly cloudy"
|
||||
: item.Snapshot.Summary.Trim().TrimEnd('.');
|
||||
var high = item.Snapshot.HighTemperature ?? item.Snapshot.Temperature;
|
||||
var low = item.Snapshot.LowTemperature ?? item.Snapshot.Temperature;
|
||||
return $"{dayName}: {summary}, high {high}, low {low}.";
|
||||
});
|
||||
|
||||
return $"I can share the next five-day forecast in {location}. {string.Join(" ", segments)} Temperatures are in {unit}.";
|
||||
}
|
||||
|
||||
private static bool IsNextWeekForecastRequest(string normalizedTranscript)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(normalizedTranscript) ||
|
||||
!normalizedTranscript.Contains("next week", StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return normalizedTranscript.Contains("forecast", StringComparison.Ordinal) ||
|
||||
normalizedTranscript.Contains("weather", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static bool ShouldDefaultForecastToTomorrow(string normalizedTranscript, WeatherDateEntity weatherDate)
|
||||
{
|
||||
if (weatherDate.ForecastDayOffset > 0 ||
|
||||
@@ -946,7 +1038,11 @@ public sealed class JiboInteractionService(
|
||||
["skillId"] = "news",
|
||||
["cloudSkill"] = "news",
|
||||
["mim_id"] = "runtime-news",
|
||||
["mim_type"] = "announcement"
|
||||
["mim_type"] = "announcement",
|
||||
["prompt_id"] = "NewsHeadline_AN_01",
|
||||
["prompt_sub_category"] = "AN",
|
||||
["esml"] =
|
||||
$"<speak><anim cat='news' meta='news-stinger' nonBlocking='true' /><break size='0.35'/><es cat='neutral' filter='!ssa-only, !sfx-only' endNeutral='true'>{EscapeForEsml(spokenBriefing)}</es></speak>"
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(sourceName))
|
||||
@@ -4100,6 +4196,10 @@ public sealed class JiboInteractionService(
|
||||
("technology", "technology"),
|
||||
("tech", "technology"),
|
||||
("ai", "technology"),
|
||||
("a i", "technology"),
|
||||
("a eye", "technology"),
|
||||
("aye eye", "technology"),
|
||||
("artificial intelligence", "technology"),
|
||||
("science", "science"),
|
||||
("business", "business"),
|
||||
("finance", "business"),
|
||||
|
||||
@@ -844,9 +844,10 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
||||
["view"] = resolvedGuiConfig
|
||||
};
|
||||
|
||||
playConfig["gui"] = resolvedGuiConfig;
|
||||
playConfig["no_matches_for_gui"] = 0;
|
||||
playConfig["no_inputs_for_gui"] = 0;
|
||||
jcpConfig["timeout"] = 6;
|
||||
jcpConfig["barge_in"] = true;
|
||||
jcpConfig["no_matches_for_gui"] = 0;
|
||||
jcpConfig["no_inputs_for_gui"] = 0;
|
||||
|
||||
var weatherViews = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
|
||||
@@ -668,6 +668,22 @@ public sealed partial class WebSocketTurnFinalizationService(
|
||||
UpdatePendingProactivityOffer(session, plan.IntentName);
|
||||
await ApplyContextUpdatesAsync(session, plan.ContextUpdates, envelope, plan.IntentName, cancellationToken);
|
||||
|
||||
var invokedSkillAction = plan.Actions.OfType<InvokeNativeSkillAction>().FirstOrDefault();
|
||||
if ((string.Equals(plan.IntentName, "weather", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(plan.IntentName, "news", StringComparison.OrdinalIgnoreCase)) &&
|
||||
invokedSkillAction is not null)
|
||||
{
|
||||
await sink.RecordTurnDiagnosticAsync(
|
||||
"skill_payload_summary",
|
||||
BuildTurnDiagnosticSnapshot(session, envelope, new Dictionary<string, object?>
|
||||
{
|
||||
["intent"] = plan.IntentName,
|
||||
["skillName"] = invokedSkillAction.SkillName,
|
||||
["payload"] = invokedSkillAction.Payload
|
||||
}),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
session.FollowUpExpiresUtc = plan.FollowUp.KeepMicOpen
|
||||
? DateTimeOffset.UtcNow.Add(plan.FollowUp.Timeout)
|
||||
: null;
|
||||
|
||||
@@ -1761,7 +1761,7 @@ public sealed class JiboInteractionServiceTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildDecisionAsync_WeatherNextWeek_WithContext_ReturnsGuardrailMessage()
|
||||
public async Task BuildDecisionAsync_WeatherNextWeek_WithContext_ReturnsFiveDaySummary()
|
||||
{
|
||||
var provider = new CapturingWeatherReportProvider
|
||||
{
|
||||
@@ -1780,8 +1780,12 @@ public sealed class JiboInteractionServiceTests
|
||||
});
|
||||
|
||||
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);
|
||||
Assert.Contains("next five-day forecast", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("Seattle, US", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("Temperatures are in Fahrenheit.", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.NotNull(provider.LastRequest);
|
||||
Assert.Equal("Seattle", provider.LastRequest!.LocationQuery);
|
||||
Assert.Equal(5, provider.LastRequest.ForecastDayOffset);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -2690,6 +2694,7 @@ public sealed class JiboInteractionServiceTests
|
||||
Assert.Equal("news", decision.SkillPayload!["skillId"]);
|
||||
Assert.Equal("news", decision.SkillPayload["cloudSkill"]);
|
||||
Assert.Equal("runtime-news", decision.SkillPayload["mim_id"]);
|
||||
Assert.Contains("news-stinger", decision.SkillPayload["esml"]?.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Equal("NewsAPI", decision.SkillPayload["news_source"]);
|
||||
Assert.Equal(2, decision.SkillPayload["news_headline_count"]);
|
||||
Assert.Contains("Local robotics team unveils weather-ready helper", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
|
||||
@@ -2697,6 +2702,30 @@ public sealed class JiboInteractionServiceTests
|
||||
Assert.Equal(3, provider.LastRequest!.MaxHeadlines);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildDecisionAsync_TellMeTheNews_WithAIAlias_UsesTechnologyCategory()
|
||||
{
|
||||
var provider = new CapturingNewsBriefingProvider
|
||||
{
|
||||
Snapshot = new NewsBriefingSnapshot(
|
||||
[
|
||||
new NewsHeadline("AI labs unveil new home companion behaviors")
|
||||
],
|
||||
"NewsAPI")
|
||||
};
|
||||
var service = CreateService(newsBriefingProvider: provider);
|
||||
|
||||
var decision = await service.BuildDecisionAsync(new TurnContext
|
||||
{
|
||||
RawTranscript = "tell me the a i news",
|
||||
NormalizedTranscript = "tell me the a i news"
|
||||
});
|
||||
|
||||
Assert.Equal("news", decision.IntentName);
|
||||
Assert.NotNull(provider.LastRequest);
|
||||
Assert.Contains("technology", provider.LastRequest!.PreferredCategories, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildDecisionAsync_TellMeTheNews_WithMemoryPreference_UsesCategoryHints()
|
||||
{
|
||||
|
||||
@@ -2155,13 +2155,9 @@ public sealed class JiboWebSocketServiceTests
|
||||
Assert.Equal("views.weatherHiLo", gui.GetProperty("data").GetString());
|
||||
Assert.True(gui.GetProperty("pause").GetBoolean());
|
||||
|
||||
var play = jcpConfig.GetProperty("play");
|
||||
Assert.True(play.TryGetProperty("gui", out var playGui));
|
||||
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.Equal(6, jcpConfig.GetProperty("timeout").GetInt32());
|
||||
Assert.Equal(0, jcpConfig.GetProperty("no_matches_for_gui").GetInt32());
|
||||
Assert.Equal(0, jcpConfig.GetProperty("no_inputs_for_gui").GetInt32());
|
||||
|
||||
Assert.True(jcpConfig.TryGetProperty("display", out var display));
|
||||
Assert.Equal(
|
||||
|
||||
Reference in New Issue
Block a user