Wrap personal report in report-skill payload

This commit is contained in:
Jacob Dubin
2026-05-21 00:28:05 -05:00
parent 884b2215c7
commit fff342fd18
2 changed files with 71 additions and 3 deletions

View File

@@ -269,11 +269,13 @@ internal static class PersonalReportOrchestrator
userName) userName)
}; };
var serviceError = string.Empty; var serviceError = string.Empty;
IDictionary<string, object?>? weatherSkillPayload = null;
if (toggles.WeatherEnabled) if (toggles.WeatherEnabled)
{ {
reportSections.Add("Weather.");
var weatherDecision = await buildWeatherDecisionAsync(turn, "weather", cancellationToken); var weatherDecision = await buildWeatherDecisionAsync(turn, "weather", cancellationToken);
weatherSkillPayload = weatherDecision.SkillPayload;
reportSections.Add("Weather.");
reportSections.Add(weatherDecision.ReplyText); reportSections.Add(weatherDecision.ReplyText);
if (IsWeatherErrorReply(weatherDecision.ReplyText)) serviceError = "weather"; if (IsWeatherErrorReply(weatherDecision.ReplyText)) serviceError = "weather";
} }
@@ -282,7 +284,10 @@ internal static class PersonalReportOrchestrator
reportSections.Add((await buildCalendarDecisionAsync(turn, cancellationToken)).ReplyText); reportSections.Add((await buildCalendarDecisionAsync(turn, cancellationToken)).ReplyText);
if (toggles.CommuteEnabled) if (toggles.CommuteEnabled)
reportSections.Add((await buildCommuteDecisionAsync(turn, cancellationToken)).ReplyText); {
var commuteReply = (await buildCommuteDecisionAsync(turn, cancellationToken)).ReplyText;
reportSections.Add(ChooseFirstSentence(commuteReply));
}
if (toggles.NewsEnabled) if (toggles.NewsEnabled)
{ {
@@ -310,9 +315,12 @@ internal static class PersonalReportOrchestrator
"And that's your report for the day. I hope you had as much fun as I did."), "And that's your report for the day. I hope you had as much fun as I did."),
userName)); userName));
var reportText = string.Join(" ", reportSections);
return new JiboInteractionDecision( return new JiboInteractionDecision(
"personal_report_delivered", "personal_report_delivered",
string.Join(" ", reportSections), reportText,
"report-skill",
BuildPersonalReportSkillPayload(reportText, weatherSkillPayload),
ContextUpdates: BuildContextUpdates( ContextUpdates: BuildContextUpdates(
IdleState, IdleState,
0, 0,
@@ -323,6 +331,38 @@ internal static class PersonalReportOrchestrator
serviceError)); serviceError));
} }
private static IDictionary<string, object?> BuildPersonalReportSkillPayload(
string reportText,
IDictionary<string, object?>? weatherSkillPayload)
{
var payload = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
["skillId"] = "report-skill",
["cloudSkill"] = "personal_report",
["mim_id"] = "runtime-personal-report",
["mim_type"] = "announcement",
["prompt_id"] = "PersonalReport_AN_01",
["prompt_sub_category"] = "AN",
["esml"] =
$"<speak><es cat='neutral' filter='!ssa-only, !sfx-only' endNeutral='true'>{EscapeForEsml(reportText)}</es></speak>",
["personal_report_report_text"] = reportText
};
if (weatherSkillPayload is null) return payload;
foreach (var (key, value) in weatherSkillPayload)
if (!string.Equals(key, "esml", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(key, "skillId", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(key, "cloudSkill", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(key, "mim_id", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(key, "mim_type", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(key, "prompt_id", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(key, "prompt_sub_category", StringComparison.OrdinalIgnoreCase))
payload[key] = value;
return payload;
}
private static JiboInteractionDecision BuildNoInputDecision( private static JiboInteractionDecision BuildNoInputDecision(
TurnContext turn, TurnContext turn,
string state, string state,
@@ -667,6 +707,16 @@ internal static class PersonalReportOrchestrator
return string.IsNullOrWhiteSpace(firstSentence) ? selected : firstSentence; return string.IsNullOrWhiteSpace(firstSentence) ? selected : firstSentence;
} }
private static string ChooseFirstSentence(string value)
{
if (string.IsNullOrWhiteSpace(value)) return string.Empty;
var firstSentence = value.Split(['.', '!', '?'], 2,
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.FirstOrDefault();
return string.IsNullOrWhiteSpace(firstSentence) ? value.Trim() : firstSentence;
}
private static string? ChooseShortestTemplate(IEnumerable<string> templates) private static string? ChooseShortestTemplate(IEnumerable<string> templates)
{ {
var selected = templates var selected = templates
@@ -686,6 +736,16 @@ internal static class PersonalReportOrchestrator
.Trim(); .Trim();
} }
private static string EscapeForEsml(string value)
{
return value
.Replace("&", "&amp;", StringComparison.Ordinal)
.Replace("<", "&lt;", StringComparison.Ordinal)
.Replace(">", "&gt;", StringComparison.Ordinal)
.Replace("\"", "&quot;", StringComparison.Ordinal)
.Replace("'", "&apos;", StringComparison.Ordinal);
}
private readonly record struct PersonalReportServiceToggles( private readonly record struct PersonalReportServiceToggles(
bool WeatherEnabled, bool WeatherEnabled,
bool CalendarEnabled, bool CalendarEnabled,

View File

@@ -1871,6 +1871,7 @@ public sealed class JiboInteractionServiceTests
}); });
Assert.Equal("personal_report_delivered", decision.IntentName); Assert.Equal("personal_report_delivered", decision.IntentName);
Assert.Equal("report-skill", decision.SkillName);
Assert.Contains("Sure alex. Here it is.", decision.ReplyText, StringComparison.OrdinalIgnoreCase); Assert.Contains("Sure alex. Here it is.", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
Assert.Contains("Weather.", decision.ReplyText, StringComparison.OrdinalIgnoreCase); Assert.Contains("Weather.", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
Assert.Contains( Assert.Contains(
@@ -1882,6 +1883,13 @@ public sealed class JiboInteractionServiceTests
Assert.True(StripMarkup(decision.ReplyText).Length < 500, Assert.True(StripMarkup(decision.ReplyText).Length < 500,
$"Personal report speech was still too long: {StripMarkup(decision.ReplyText).Length} chars."); $"Personal report speech was still too long: {StripMarkup(decision.ReplyText).Length} chars.");
Assert.Contains("alex", decision.ReplyText, StringComparison.OrdinalIgnoreCase); Assert.Contains("alex", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
Assert.NotNull(decision.SkillPayload);
Assert.Equal("report-skill", decision.SkillPayload!["skillId"]);
Assert.Equal("personal_report", decision.SkillPayload["cloudSkill"]);
Assert.Equal(true, decision.SkillPayload["weather_view_enabled"]);
Assert.Equal("runtime-personal-report", decision.SkillPayload["mim_id"]);
Assert.Contains("Weather. For your weather.", decision.SkillPayload["personal_report_report_text"]?.ToString(),
StringComparison.OrdinalIgnoreCase);
Assert.NotNull(decision.ContextUpdates); Assert.NotNull(decision.ContextUpdates);
Assert.Equal("idle", decision.ContextUpdates![PersonalReportStateKey]); Assert.Equal("idle", decision.ContextUpdates![PersonalReportStateKey]);
Assert.Equal(true, decision.ContextUpdates[PersonalReportUserVerifiedKey]); Assert.Equal(true, decision.ContextUpdates[PersonalReportUserVerifiedKey]);