From fff342fd18729c417c8c644cf3a2319b9e96fa38 Mon Sep 17 00:00:00 2001 From: Jacob Dubin Date: Thu, 21 May 2026 00:28:05 -0500 Subject: [PATCH] Wrap personal report in report-skill payload --- .../Services/PersonalReportOrchestrator.cs | 66 ++++++++++++++++++- .../WebSockets/JiboInteractionServiceTests.cs | 8 +++ 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/PersonalReportOrchestrator.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/PersonalReportOrchestrator.cs index 217b712..c577f76 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/PersonalReportOrchestrator.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/PersonalReportOrchestrator.cs @@ -269,11 +269,13 @@ internal static class PersonalReportOrchestrator userName) }; var serviceError = string.Empty; + IDictionary? weatherSkillPayload = null; if (toggles.WeatherEnabled) { - reportSections.Add("Weather."); var weatherDecision = await buildWeatherDecisionAsync(turn, "weather", cancellationToken); + weatherSkillPayload = weatherDecision.SkillPayload; + reportSections.Add("Weather."); reportSections.Add(weatherDecision.ReplyText); if (IsWeatherErrorReply(weatherDecision.ReplyText)) serviceError = "weather"; } @@ -282,7 +284,10 @@ internal static class PersonalReportOrchestrator reportSections.Add((await buildCalendarDecisionAsync(turn, cancellationToken)).ReplyText); if (toggles.CommuteEnabled) - reportSections.Add((await buildCommuteDecisionAsync(turn, cancellationToken)).ReplyText); + { + var commuteReply = (await buildCommuteDecisionAsync(turn, cancellationToken)).ReplyText; + reportSections.Add(ChooseFirstSentence(commuteReply)); + } 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."), userName)); + var reportText = string.Join(" ", reportSections); return new JiboInteractionDecision( "personal_report_delivered", - string.Join(" ", reportSections), + reportText, + "report-skill", + BuildPersonalReportSkillPayload(reportText, weatherSkillPayload), ContextUpdates: BuildContextUpdates( IdleState, 0, @@ -323,6 +331,38 @@ internal static class PersonalReportOrchestrator serviceError)); } + private static IDictionary BuildPersonalReportSkillPayload( + string reportText, + IDictionary? weatherSkillPayload) + { + var payload = new Dictionary(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"] = + $"{EscapeForEsml(reportText)}", + ["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( TurnContext turn, string state, @@ -667,6 +707,16 @@ internal static class PersonalReportOrchestrator 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 templates) { var selected = templates @@ -686,6 +736,16 @@ internal static class PersonalReportOrchestrator .Trim(); } + private static string EscapeForEsml(string value) + { + return value + .Replace("&", "&", StringComparison.Ordinal) + .Replace("<", "<", StringComparison.Ordinal) + .Replace(">", ">", StringComparison.Ordinal) + .Replace("\"", """, StringComparison.Ordinal) + .Replace("'", "'", StringComparison.Ordinal); + } + private readonly record struct PersonalReportServiceToggles( bool WeatherEnabled, bool CalendarEnabled, diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs index 4779441..150d219 100644 --- a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs +++ b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs @@ -1871,6 +1871,7 @@ public sealed class JiboInteractionServiceTests }); 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("Weather.", decision.ReplyText, StringComparison.OrdinalIgnoreCase); Assert.Contains( @@ -1882,6 +1883,13 @@ public sealed class JiboInteractionServiceTests Assert.True(StripMarkup(decision.ReplyText).Length < 500, $"Personal report speech was still too long: {StripMarkup(decision.ReplyText).Length} chars."); 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.Equal("idle", decision.ContextUpdates![PersonalReportStateKey]); Assert.Equal(true, decision.ContextUpdates[PersonalReportUserVerifiedKey]);