Compare commits

...

2 Commits

Author SHA1 Message Date
Jacob Dubin
a94b7ec493 Merge branch 'main' of https://kevinblog.sytes.net/Code/Jibo-Revival-Group/JiboExperiments 2026-05-10 20:31:46 -05:00
Jacob Dubin
8c17ad4035 Update commit message generation 2026-05-10 20:31:07 -05:00
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."); "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"),

View File

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

View File

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

View File

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

View File

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