From 687ff62f0fcfb629177d7859897c2e4aa345df4f Mon Sep 17 00:00:00 2001 From: Jacob Dubin Date: Tue, 5 May 2026 22:27:28 -0500 Subject: [PATCH] Refine persona routing and update 1.0.19 plan --- OpenJibo/docs/feature-backlog.md | 37 ++++++----- OpenJibo/docs/release-1.0.19-plan.md | 17 +++-- .../IJiboExperienceContentRepository.cs | 1 + .../Services/JiboInteractionService.cs | 57 ++++++++++++---- ...InMemoryJiboExperienceContentRepository.cs | 6 ++ .../WebSockets/JiboInteractionServiceTests.cs | 66 +++++++++++++++++++ .../WebSockets/JiboWebSocketServiceTests.cs | 4 +- 7 files changed, 151 insertions(+), 37 deletions(-) diff --git a/OpenJibo/docs/feature-backlog.md b/OpenJibo/docs/feature-backlog.md index e5e16ec..393310e 100644 --- a/OpenJibo/docs/feature-backlog.md +++ b/OpenJibo/docs/feature-backlog.md @@ -590,15 +590,15 @@ Current release theme: ### 22. Command Vs Question Reply Style -- Status: `ready` +- Status: `implemented` - Tags: `content`, `polish` -- User goals: - - `dance` should behave like a willing action - - `do you like to dance` should answer the question before or instead of treating it like the same command -- Implementation notes: - - evolve reply collections into command/question variants - - start with dance or another expressive skill - - keep the first version rule-based +- Result: + - `dance` still launches the dance animation path + - `do you like to dance` now responds conversationally as a personality question instead of launching the action + - birthday phrasing now takes precedence over an `askForDate` client-intent misclassification +- Follow-up: + - expand command-vs-question splits to more expressive intents (pizza, surprise, photo prompts) + - add Pegasus phrase and MIM-backed variants for richer style coverage ## Suggested Order @@ -614,16 +614,17 @@ Use [regression-test-plan.md](regression-test-plan.md) as the detailed checklist For `1.0.19`: -1. Harden stop or volume if the `1.0.18` live pass exposes stock-OS quirks / harden $YESNO interaction -2. Make a pizza. How old are you? When's your birthday? Do you have a personality? (`implemented` in the first `1.0.19` slice; continue refining with persistent identity metadata and richer persona variants.) -3. Holidays and seasonal personality slice so persona evolution remains visible and testable -4. Multi-tenant internal storage foundation for memory/personality data (account/loop/device scoped) with cloud-ready persistence boundaries -5. Update, backup, and restore proof -6. STT upgrade and noise screening -7. Hosted capture/storage plan / indexing for group testing -8. Binary-safe media storage / sync to cloud drive: OneDrive, Google Drive, Box, etc. -9. Provider-backed news and weather -10. Proactivity, dialog parsing/NLP, memory/history, Lasso, identity, and onboarding as larger discovery-driven tracks +1. Command-vs-question personality split (`dance` command vs `do you like to dance` question style; expand this pattern) +2. First memory-backed personal facts with tenant-scoped storage (birthday/preferences foundation) +3. Proactivity selector baseline with source-backed first offers +4. Dialog parsing expansion and ambiguity guardrails +5. Holidays and seasonal personality behavior built on the new memory/proactivity foundation +6. Update, backup, and restore proof +7. STT upgrade and noise screening +8. Hosted capture/storage plan / indexing for group testing +9. Binary-safe media storage / sync to cloud drive: OneDrive, Google Drive, Box, etc. +10. Provider-backed news and weather +11. Lasso, identity, and onboarding as larger discovery-driven tracks For `1.0.20` and beyond: diff --git a/OpenJibo/docs/release-1.0.19-plan.md b/OpenJibo/docs/release-1.0.19-plan.md index 3a2350f..d29480f 100644 --- a/OpenJibo/docs/release-1.0.19-plan.md +++ b/OpenJibo/docs/release-1.0.19-plan.md @@ -59,12 +59,17 @@ This slice is intentionally small and user-visible. It creates immediate persona ## Next Slices -1. Update/backup/restore end-to-end proof (operator-run and documented) -2. Holidays and seasonal personality slice (first scoped calendar + response set) -3. Multi-tenant memory storage foundation (tenant model + persistence contracts + initial implementation) -4. STT noise-screening and short-utterance reliability pass -5. Provider-backed news/weather expansion using Pegasus-backed contracts -6. Capture indexing and retention boundary for group testing +1. Command-vs-question personality split (start with dance/twerk-style prompts, keep commands action-oriented and questions conversational) +2. First memory-backed personal facts (tenant-scoped birthday/preferences storage contracts + initial implementation) +3. Proactivity selector baseline (source-backed first proactive offers with safe throttling and stock-compatible payloads) +4. Dialog parsing expansion (more phrase variants, ambiguity handling, and transcript-to-intent guardrails) +5. Holidays and seasonal personality slice (time-scoped content backed by the new memory/proactivity path) +6. Update/backup/restore end-to-end proof (operator-run and documented) +7. STT noise-screening and short-utterance reliability pass +8. Provider-backed news/weather expansion using Pegasus-backed contracts +9. Capture indexing and retention boundary for group testing + +For slices 1-5, use Pegasus phrase lists, MIM IDs, and behavior patterns as the source anchor before broadening into OpenJibo-native improvements. ## Definition Of Done 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 f4ef31d..fb5e097 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 @@ -22,4 +22,5 @@ public sealed class JiboExperienceCatalog public IReadOnlyList NewsBriefings { get; init; } = []; public IReadOnlyList GenericFallbackReplies { get; init; } = []; public IReadOnlyList DanceReplies { get; init; } = []; + public IReadOnlyList DanceQuestionReplies { get; init; } = []; } 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 91c7cc5..4ddb381 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 @@ -43,6 +43,7 @@ public sealed class JiboInteractionService( return semanticIntent switch { "joke" => BuildJokeDecision(catalog), + "dance_question" => BuildDanceQuestionDecision(catalog), "dance" => BuildRandomDanceDecision(catalog), "twerk" => BuildDanceDecision("twerk", "rom-twerk", "Watch me twerk."), "time" => BuildClockLaunchDecision("time", "clock", "askForTime", "Showing the time."), @@ -167,6 +168,11 @@ public sealed class JiboInteractionService( return BuildDanceDecision("dance", dance, replyText); } + private JiboInteractionDecision BuildDanceQuestionDecision(JiboExperienceCatalog catalog) + { + return new JiboInteractionDecision("dance_question", randomizer.Choose(catalog.DanceQuestionReplies)); + } + private static JiboInteractionDecision BuildDanceDecision(string intentName, string dance, string replyText) { return new JiboInteractionDecision( @@ -260,6 +266,11 @@ public sealed class JiboInteractionService( }; } + if (IsRobotBirthdayQuestion(loweredTranscript)) + { + return "robot_birthday"; + } + if (string.Equals(clientIntent, "askForTime", StringComparison.OrdinalIgnoreCase)) { return "time"; @@ -500,6 +511,11 @@ public sealed class JiboInteractionService( return "photo_gallery"; } + if (IsDanceQuestion(loweredTranscript)) + { + return "dance_question"; + } + if (MatchesAny(loweredTranscript, "twerk")) { return "twerk"; @@ -525,17 +541,6 @@ public sealed class JiboInteractionService( return "robot_age"; } - if (MatchesAny( - loweredTranscript, - "when is your birthday", - "when's your birthday", - "what is your birthday", - "when were you born", - "what day is your birthday")) - { - return "robot_birthday"; - } - if (MatchesAny( loweredTranscript, "do you have a personality", @@ -564,7 +569,12 @@ public sealed class JiboInteractionService( if (MatchesAny( loweredTranscript, "can you order pizza", + "can you order a pizza", + "could you order a pizza", "order pizza", + "order a pizza", + "order us a pizza", + "order me a pizza", "please order pizza")) { return "order_pizza"; @@ -1078,6 +1088,31 @@ public sealed class JiboInteractionService( return candidates.Any(candidate => loweredTranscript.Contains(candidate, StringComparison.Ordinal)); } + private static bool IsDanceQuestion(string loweredTranscript) + { + return MatchesAny( + loweredTranscript, + "do you like to dance", + "do you like dancing", + "what kind of dance do you like", + "what kind of dancing do you like", + "do you enjoy dancing"); + } + + private static bool IsRobotBirthdayQuestion(string loweredTranscript) + { + return MatchesAny( + loweredTranscript, + "when is your birthday", + "when's your birthday", + "what's your birthday", + "what s your birthday", + "what is your birthday", + "when were you born", + "what day is your birthday") || + loweredTranscript.Contains("birthday", StringComparison.Ordinal); + } + private static string? TryResolveRadioGenre(string loweredTranscript) { foreach (var (phrase, station) in RadioGenreAliases) 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 c44807c..2a0baf0 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 @@ -29,6 +29,12 @@ public sealed class InMemoryJiboExperienceContentRepository : IJiboExperienceCon "Watch me dance.", "Here's my favorite dance move." ], + DanceQuestionReplies = + [ + "I love to dance. Tell me to dance and I will show you a move.", + "Absolutely. Dancing is one of my favorite things to do.", + "Dancing is my kind of fun. Say dance and I am in." + ], GreetingReplies = [ "Hi there. It is really good to talk with you.", diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs index b62b218..91f728d 100644 --- a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs +++ b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs @@ -41,6 +41,22 @@ public sealed class JiboInteractionServiceTests Assert.Equal("Okay. Watch this.", decision.SkillPayload!["esml"]); } + [Fact] + public async Task BuildDecisionAsync_DoYouLikeToDance_UsesQuestionReplyStyleInsteadOfTriggeringDanceAnimation() + { + var service = CreateService(); + + var decision = await service.BuildDecisionAsync(new TurnContext + { + RawTranscript = "do you like to dance", + NormalizedTranscript = "do you like to dance" + }); + + Assert.Equal("dance_question", decision.IntentName); + Assert.Null(decision.SkillName); + Assert.Equal("I love to dance. Tell me to dance and I will show you a move.", decision.ReplyText); + } + [Fact] public async Task BuildDecisionAsync_TwerkQuestion_PrefersSpecificTwerkIntent() { @@ -90,6 +106,21 @@ public sealed class JiboInteractionServiceTests Assert.Equal("My birthday is March 22, 2026.", decision.ReplyText); } + [Fact] + public async Task BuildDecisionAsync_WhatsYourBirthday_DoesNotFallThroughToDateIntent() + { + var service = CreateService(); + + var decision = await service.BuildDecisionAsync(new TurnContext + { + RawTranscript = "what's your birthday", + NormalizedTranscript = "what's your birthday" + }); + + Assert.Equal("robot_birthday", decision.IntentName); + Assert.Equal("My birthday is March 22, 2026.", decision.ReplyText); + } + [Fact] public async Task BuildDecisionAsync_DoYouHaveAPersonality_UsesCatalogBackedPersonalityReply() { @@ -162,6 +193,22 @@ public sealed class JiboInteractionServiceTests Assert.Contains("I can't do that yet", decision.SkillPayload["esml"]?.ToString(), StringComparison.Ordinal); } + [Fact] + public async Task BuildDecisionAsync_OrderAPizza_UsesLegacyOrderPizzaMimPayload() + { + var service = CreateService(); + + var decision = await service.BuildDecisionAsync(new TurnContext + { + RawTranscript = "order a pizza", + NormalizedTranscript = "order a pizza" + }); + + Assert.Equal("order_pizza", decision.IntentName); + Assert.Equal("chitchat-skill", decision.SkillName); + Assert.Equal("RA_JBO_OrderPizza", decision.SkillPayload!["mim_id"]); + } + [Fact] public async Task BuildDecisionAsync_ClientNluRequestOrderPizza_UsesLegacyOrderPizzaMimPayload() { @@ -200,6 +247,25 @@ public sealed class JiboInteractionServiceTests Assert.Equal("askForDate", decision.SkillPayload!["clockIntent"]); } + [Fact] + public async Task BuildDecisionAsync_ClientNluAskForDate_WithBirthdayTranscript_PrefersRobotBirthdayIntent() + { + var service = CreateService(); + + var decision = await service.BuildDecisionAsync(new TurnContext + { + RawTranscript = "what's your birthday", + NormalizedTranscript = "what's your birthday", + Attributes = new Dictionary + { + ["clientIntent"] = "askForDate" + } + }); + + Assert.Equal("robot_birthday", decision.IntentName); + Assert.Equal("My birthday is March 22, 2026.", decision.ReplyText); + } + [Fact] public async Task BuildDecisionAsync_YesNoFollowUp_MapsShortAffirmationToYesIntent() { diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboWebSocketServiceTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboWebSocketServiceTests.cs index 30e8a07..cd98d0f 100644 --- a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboWebSocketServiceTests.cs +++ b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboWebSocketServiceTests.cs @@ -2863,7 +2863,7 @@ public sealed class JiboWebSocketServiceTests } [Fact] - public async Task ClientAsrOrderPizzaFlow_UsesLegacyOrderPizzaMim() + public async Task ClientAsrOrderAPizzaFlow_UsesLegacyOrderPizzaMim() { await _service.HandleMessageAsync(new WebSocketMessageEnvelope { @@ -2880,7 +2880,7 @@ public sealed class JiboWebSocketServiceTests Path = "/listen", Kind = "neo-hub-listen", Token = "hub-client-asr-order-pizza-token", - Text = """{"type":"CLIENT_ASR","transID":"trans-order-pizza-shape","data":{"text":"can you order pizza"}}""" + Text = """{"type":"CLIENT_ASR","transID":"trans-order-pizza-shape","data":{"text":"order a pizza"}}""" }); Assert.Equal(3, replies.Count);