From efdb5bcf01cfd8b5f5d46d07830e2b88c2ff6a41 Mon Sep 17 00:00:00 2001 From: Jacob Dubin Date: Mon, 20 Apr 2026 22:09:23 -0500 Subject: [PATCH] jibo news skill by voice --- OpenJibo/docs/development-plan.md | 6 +++ OpenJibo/docs/feature-backlog.md | 3 ++ .../IJiboExperienceContentRepository.cs | 1 + .../Services/DemoConversationBroker.cs | 1 + .../Services/JiboInteractionService.cs | 18 +++++++- .../ResponsePlanToSocketMessagesMapper.cs | 4 +- ...InMemoryJiboExperienceContentRepository.cs | 5 +++ .../WebSockets/JiboInteractionServiceTests.cs | 19 ++++++++ .../WebSockets/JiboWebSocketServiceTests.cs | 44 +++++++++++++++++++ 9 files changed, 99 insertions(+), 2 deletions(-) diff --git a/OpenJibo/docs/development-plan.md b/OpenJibo/docs/development-plan.md index 57c1623..5944bf4 100644 --- a/OpenJibo/docs/development-plan.md +++ b/OpenJibo/docs/development-plan.md @@ -165,6 +165,12 @@ Latest radio discovery findings: - `result.nlu.entities.station` is the genre selector, and `Country` is a real supported station key from the robot's `genres.json`. - The smallest stock-shaped cloud handoff for voice launch is therefore a local `SKILL_REDIRECT` to `@be/radio` with `nlu.intent = "menu"`, optional `entities.station`, and a silent completion to settle the hotphrase cloud response. +Latest news discovery findings: + +- Nimbus explicitly treats `match.cloudSkill === "news"` like the GQA path and waits on `cloudSkillResponse`. +- The first OpenJibo news pass should therefore use a real cloud-skill shape, not a generic placeholder chat reply. +- For now, the content can stay synthetic while the protocol is grounded: `match.cloudSkill = "news"` plus a supported `SLIM` announcement response is enough to validate the robot path before provider-backed headlines arrive later. + ## Speech, Animation, And ESML The current joke flow is only a small foothold into Jibo expressiveness. diff --git a/OpenJibo/docs/feature-backlog.md b/OpenJibo/docs/feature-backlog.md index d1d9687..b9f4a17 100644 --- a/OpenJibo/docs/feature-backlog.md +++ b/OpenJibo/docs/feature-backlog.md @@ -111,6 +111,9 @@ Parallel tags: - Implementation notes: - decide whether the first pass is a simple headline summary or a closer personal-report style payload - confirm whether stock OS expects `news` as a dedicated cloud skill or under the broader personal-report family +- Latest progress: + - first pass should use Nimbus's supported cloud path by setting `match.cloudSkill = news` and returning a supported `SLIM` announcement + - provider-backed headlines can follow later under the `Lasso / Knowledge And Event Aggregation` track - Exit criteria: - `tell me the news` reaches a non-placeholder live path - robot behavior feels Nimbus-native rather than generic chat playback diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Abstractions/IJiboExperienceContentRepository.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Abstractions/IJiboExperienceContentRepository.cs index 2c94456..58da2dc 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Abstractions/IJiboExperienceContentRepository.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Abstractions/IJiboExperienceContentRepository.cs @@ -17,5 +17,6 @@ public sealed class JiboExperienceCatalog public IReadOnlyList CalendarReplies { get; init; } = []; public IReadOnlyList CommuteReplies { get; init; } = []; public IReadOnlyList NewsReplies { get; init; } = []; + public IReadOnlyList NewsBriefings { get; init; } = []; public IReadOnlyList GenericFallbackReplies { get; init; } = []; } diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/DemoConversationBroker.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/DemoConversationBroker.cs index 0b44284..f17ac73 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/DemoConversationBroker.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/DemoConversationBroker.cs @@ -74,6 +74,7 @@ public sealed class DemoConversationBroker(JiboInteractionService interactionSer "word_of_the_day_guess" => false, "radio" => false, "radio_genre" => false, + "news" => false, _ => true }; } diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs index b5e46c1..9f4696a 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs @@ -42,7 +42,7 @@ public sealed class JiboInteractionService( "weather" => new JiboInteractionDecision("weather", randomizer.Choose(catalog.WeatherReplies)), "calendar" => new JiboInteractionDecision("calendar", randomizer.Choose(catalog.CalendarReplies)), "commute" => new JiboInteractionDecision("commute", randomizer.Choose(catalog.CommuteReplies)), - "news" => new JiboInteractionDecision("news", randomizer.Choose(catalog.NewsReplies)), + "news" => BuildNewsDecision(catalog), _ => new JiboInteractionDecision("chat", BuildGenericReply(catalog, transcript, lowered)) }; } @@ -75,6 +75,22 @@ public sealed class JiboInteractionService( }); } + private JiboInteractionDecision BuildNewsDecision(JiboExperienceCatalog catalog) + { + var briefing = randomizer.Choose(catalog.NewsBriefings); + return new JiboInteractionDecision( + "news", + briefing, + "news", + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["skillId"] = "news", + ["cloudSkill"] = "news", + ["mim_id"] = "runtime-news", + ["mim_type"] = "announcement" + }); + } + private string BuildGenericReply(JiboExperienceCatalog catalog, string transcript, string lowered) { if (string.IsNullOrWhiteSpace(transcript)) diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/ResponsePlanToSocketMessagesMapper.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/ResponsePlanToSocketMessagesMapper.cs index c748908..fdc9607 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/ResponsePlanToSocketMessagesMapper.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/ResponsePlanToSocketMessagesMapper.cs @@ -26,6 +26,7 @@ public sealed class ResponsePlanToSocketMessagesMapper var isRadioLaunch = string.Equals(plan.IntentName, "radio", StringComparison.OrdinalIgnoreCase) || string.Equals(plan.IntentName, "radio_genre", StringComparison.OrdinalIgnoreCase); var radioStation = ReadSkillPayloadString(skill, "station"); + var cloudSkill = ReadSkillPayloadString(skill, "cloudSkill"); var nluGuess = ReadClientEntity(turn, "guess"); var wordOfDayGuess = ResolveWordOfDayGuess(turn, transcript, nluGuess); var outboundIntent = isWordOfDayLaunch @@ -84,7 +85,8 @@ public sealed class ResponsePlanToSocketMessagesMapper { intent = outboundIntent, rule = outboundRules.FirstOrDefault() ?? string.Empty, - score = 0.95 + score = 0.95, + cloudSkill } } }; diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/InMemoryJiboExperienceContentRepository.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/InMemoryJiboExperienceContentRepository.cs index 6b6162a..49298ef 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/InMemoryJiboExperienceContentRepository.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/InMemoryJiboExperienceContentRepository.cs @@ -66,6 +66,11 @@ public sealed class InMemoryJiboExperienceContentRepository : IJiboExperienceCon "I heard your news request. That path is still a future cloud integration.", "News is recognized, but I do not have the full news service behind it yet." ], + NewsBriefings = + [ + "Here are your headlines. Space missions are preparing for new launches, climate and weather systems are staying active across the country, and AI tools keep pushing into everyday products.", + "Here is a quick news brief. Technology companies are still racing on AI, global leaders are trading policy updates, and science teams are sharing new research findings." + ], GenericFallbackReplies = [ "Okay. You said, {transcript}.", diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs index 5ef09fc..f7de49e 100644 --- a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs +++ b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs @@ -181,6 +181,25 @@ public sealed class JiboInteractionServiceTests Assert.Equal("Country", decision.SkillPayload!["station"]); } + [Fact] + public async Task BuildDecisionAsync_TellMeTheNews_UsesNimbusCloudSkillPath() + { + var service = CreateService(); + + var decision = await service.BuildDecisionAsync(new TurnContext + { + RawTranscript = "tell me the news", + NormalizedTranscript = "tell me the news" + }); + + Assert.Equal("news", decision.IntentName); + Assert.Equal("news", decision.SkillName); + Assert.Equal("news", decision.SkillPayload!["skillId"]); + Assert.Equal("news", decision.SkillPayload["cloudSkill"]); + Assert.Equal("runtime-news", decision.SkillPayload["mim_id"]); + Assert.DoesNotContain("future cloud integration", decision.ReplyText, StringComparison.OrdinalIgnoreCase); + } + [Fact] public async Task BuildDecisionAsync_WordOfDayGuess_UsesStructuredClientNluGuess() { diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboWebSocketServiceTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboWebSocketServiceTests.cs index 5e3cddc..da566b8 100644 --- a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboWebSocketServiceTests.cs +++ b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboWebSocketServiceTests.cs @@ -545,6 +545,50 @@ public sealed class JiboWebSocketServiceTests Assert.DoesNotContain("'", esml, StringComparison.Ordinal); } + [Fact] + public async Task ClientAsr_TellMeTheNews_EmitsNimbusCloudSkillMatchAndNewsSkillAction() + { + await _service.HandleMessageAsync(new WebSocketMessageEnvelope + { + HostName = "neo-hub.jibo.com", + Path = "/listen", + Kind = "neo-hub-listen", + Token = "hub-news-token", + Text = """{"type":"LISTEN","transID":"trans-news","data":{"hotphrase":true,"rules":["launch","globals/global_commands_launch"]}}""" + }); + + var replies = await _service.HandleMessageAsync(new WebSocketMessageEnvelope + { + HostName = "neo-hub.jibo.com", + Path = "/listen", + Kind = "neo-hub-listen", + Token = "hub-news-token", + Text = """{"type":"CLIENT_ASR","transID":"trans-news","data":{"text":"tell me the news"}}""" + }); + + Assert.Equal(3, replies.Count); + Assert.Equal("LISTEN", ReadReplyType(replies[0])); + Assert.Equal("EOS", ReadReplyType(replies[1])); + Assert.Equal("SKILL_ACTION", ReadReplyType(replies[2])); + + using var listenPayload = JsonDocument.Parse(replies[0].Text!); + Assert.Equal("news", listenPayload.RootElement.GetProperty("data").GetProperty("nlu").GetProperty("intent").GetString()); + Assert.Equal("news", listenPayload.RootElement.GetProperty("data").GetProperty("match").GetProperty("cloudSkill").GetString()); + + using var skillPayload = JsonDocument.Parse(replies[2].Text!); + Assert.Equal("news", skillPayload.RootElement.GetProperty("data").GetProperty("skill").GetProperty("id").GetString()); + var meta = skillPayload.RootElement + .GetProperty("data") + .GetProperty("action") + .GetProperty("config") + .GetProperty("jcp") + .GetProperty("config") + .GetProperty("play") + .GetProperty("meta"); + Assert.Equal("runtime-news", meta.GetProperty("mim_id").GetString()); + Assert.Equal("announcement", meta.GetProperty("mim_type").GetString()); + } + [Fact] public async Task ClientAsr_OpenTheRadio_EmitsRadioRedirectAndSilentCompletion() {