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.");
|
"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)
|
if (weatherDate.ForecastDayOffset > MaxWeatherForecastDayOffset)
|
||||||
{
|
{
|
||||||
return new JiboInteractionDecision(
|
return new JiboInteractionDecision(
|
||||||
"weather",
|
"weather",
|
||||||
$"I can forecast up to {MaxWeatherForecastDayOffset} days ahead. Try tomorrow or another day this week.");
|
$"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;
|
WeatherReportSnapshot? snapshot;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -688,6 +734,52 @@ 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 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)
|
private static bool ShouldDefaultForecastToTomorrow(string normalizedTranscript, WeatherDateEntity weatherDate)
|
||||||
{
|
{
|
||||||
if (weatherDate.ForecastDayOffset > 0 ||
|
if (weatherDate.ForecastDayOffset > 0 ||
|
||||||
@@ -946,7 +1038,11 @@ public sealed class JiboInteractionService(
|
|||||||
["skillId"] = "news",
|
["skillId"] = "news",
|
||||||
["cloudSkill"] = "news",
|
["cloudSkill"] = "news",
|
||||||
["mim_id"] = "runtime-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))
|
if (!string.IsNullOrWhiteSpace(sourceName))
|
||||||
@@ -4100,6 +4196,10 @@ public sealed class JiboInteractionService(
|
|||||||
("technology", "technology"),
|
("technology", "technology"),
|
||||||
("tech", "technology"),
|
("tech", "technology"),
|
||||||
("ai", "technology"),
|
("ai", "technology"),
|
||||||
|
("a i", "technology"),
|
||||||
|
("a eye", "technology"),
|
||||||
|
("aye eye", "technology"),
|
||||||
|
("artificial intelligence", "technology"),
|
||||||
("science", "science"),
|
("science", "science"),
|
||||||
("business", "business"),
|
("business", "business"),
|
||||||
("finance", "business"),
|
("finance", "business"),
|
||||||
|
|||||||
@@ -844,9 +844,10 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
["view"] = resolvedGuiConfig
|
["view"] = resolvedGuiConfig
|
||||||
};
|
};
|
||||||
|
|
||||||
playConfig["gui"] = resolvedGuiConfig;
|
jcpConfig["timeout"] = 6;
|
||||||
playConfig["no_matches_for_gui"] = 0;
|
jcpConfig["barge_in"] = true;
|
||||||
playConfig["no_inputs_for_gui"] = 0;
|
jcpConfig["no_matches_for_gui"] = 0;
|
||||||
|
jcpConfig["no_inputs_for_gui"] = 0;
|
||||||
|
|
||||||
var weatherViews = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
var weatherViews = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -668,6 +668,22 @@ public sealed partial class WebSocketTurnFinalizationService(
|
|||||||
UpdatePendingProactivityOffer(session, plan.IntentName);
|
UpdatePendingProactivityOffer(session, plan.IntentName);
|
||||||
await ApplyContextUpdatesAsync(session, plan.ContextUpdates, envelope, plan.IntentName, cancellationToken);
|
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
|
session.FollowUpExpiresUtc = plan.FollowUp.KeepMicOpen
|
||||||
? DateTimeOffset.UtcNow.Add(plan.FollowUp.Timeout)
|
? DateTimeOffset.UtcNow.Add(plan.FollowUp.Timeout)
|
||||||
: null;
|
: null;
|
||||||
|
|||||||
@@ -1761,7 +1761,7 @@ public sealed class JiboInteractionServiceTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task BuildDecisionAsync_WeatherNextWeek_WithContext_ReturnsGuardrailMessage()
|
public async Task BuildDecisionAsync_WeatherNextWeek_WithContext_ReturnsFiveDaySummary()
|
||||||
{
|
{
|
||||||
var provider = new CapturingWeatherReportProvider
|
var provider = new CapturingWeatherReportProvider
|
||||||
{
|
{
|
||||||
@@ -1780,8 +1780,12 @@ public sealed class JiboInteractionServiceTests
|
|||||||
});
|
});
|
||||||
|
|
||||||
Assert.Equal("weather", decision.IntentName);
|
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.Contains("next five-day forecast", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
|
||||||
Assert.Null(provider.LastRequest);
|
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]
|
[Fact]
|
||||||
@@ -2690,6 +2694,7 @@ public sealed class JiboInteractionServiceTests
|
|||||||
Assert.Equal("news", decision.SkillPayload!["skillId"]);
|
Assert.Equal("news", decision.SkillPayload!["skillId"]);
|
||||||
Assert.Equal("news", decision.SkillPayload["cloudSkill"]);
|
Assert.Equal("news", decision.SkillPayload["cloudSkill"]);
|
||||||
Assert.Equal("runtime-news", decision.SkillPayload["mim_id"]);
|
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("NewsAPI", decision.SkillPayload["news_source"]);
|
||||||
Assert.Equal(2, decision.SkillPayload["news_headline_count"]);
|
Assert.Equal(2, decision.SkillPayload["news_headline_count"]);
|
||||||
Assert.Contains("Local robotics team unveils weather-ready helper", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
|
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);
|
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]
|
[Fact]
|
||||||
public async Task BuildDecisionAsync_TellMeTheNews_WithMemoryPreference_UsesCategoryHints()
|
public async Task BuildDecisionAsync_TellMeTheNews_WithMemoryPreference_UsesCategoryHints()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2155,13 +2155,9 @@ 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.Equal(6, jcpConfig.GetProperty("timeout").GetInt32());
|
||||||
Assert.True(play.TryGetProperty("gui", out var playGui));
|
Assert.Equal(0, jcpConfig.GetProperty("no_matches_for_gui").GetInt32());
|
||||||
Assert.Equal("Javascript", playGui.GetProperty("type").GetString());
|
Assert.Equal(0, jcpConfig.GetProperty("no_inputs_for_gui").GetInt32());
|
||||||
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.True(jcpConfig.TryGetProperty("display", out var display));
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
|
|||||||
Reference in New Issue
Block a user