Update commit message generation

This commit is contained in:
Jacob Dubin
2026-05-10 20:31:07 -05:00
parent d434138f9b
commit 8c17ad4035
5 changed files with 162 additions and 20 deletions

View File

@@ -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"),

View File

@@ -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)
{

View File

@@ -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;

View File

@@ -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()
{

View File

@@ -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(