diff --git a/OpenJibo/docs/feature-backlog.md b/OpenJibo/docs/feature-backlog.md index 98aba48..2268559 100644 --- a/OpenJibo/docs/feature-backlog.md +++ b/OpenJibo/docs/feature-backlog.md @@ -893,6 +893,7 @@ Current release theme: - deeper personality follow-ups like `what do you dream about`, `what are you afraid of`, `what do you want to talk about`, `what is your best book`, `what is your best exercise`, `what is your dream vacation`, `who is your hero`, `who do you love`, and `what is your religion`; `what is your sign` stays deferred until templated placeholder rendering exists - the next identity / knowledge wave adds `are you god`, `are you here`, `do you have super powers`, `how much do you know`, `what does jibo mean`, `where do you get info`, `what are you forbidden to do`, `what color are you`, and `what do you do when alone` - additional legacy source-backed `RI_USR` prompts where the text is short and the behavior is easy to verify + - templated edge cases like `what is your sign`, `how many people do you know`, and `what is the loop` where live birthday and loop state are part of the line instead of a plain canned response - Exit criteria: - a stable checklist exists for the original persona surface - each pass can be scoped to a small batch of prompts diff --git a/OpenJibo/docs/release-1.0.19-plan.md b/OpenJibo/docs/release-1.0.19-plan.md index 16145db..6123e1d 100644 --- a/OpenJibo/docs/release-1.0.19-plan.md +++ b/OpenJibo/docs/release-1.0.19-plan.md @@ -61,6 +61,7 @@ Current batch note: - the next deep-personality batch adds `what do you dream about`, `what are you afraid of`, `what do you want to talk about`, `what is your best book`, `what is your best exercise`, `what is your dream vacation`, `who is your hero`, `who do you love`, and `what is your religion`; `what is your sign` is still deferred until we add templated placeholder rendering - the next identity/knowledge batch adds `are you god`, `are you here`, `do you have super powers`, `how much do you know`, `what does jibo mean`, `where do you get info`, `what are you forbidden to do`, `what color are you`, and `what do you do when alone` - the next body/mission batch adds `how much do you weigh`, `how tall are you`, `how much do you cost`, `what if I unplug you`, `what is your purpose`, `what is your prime directive`, `what is jibo commander`, `do you like commander app`, and `what are you made of` +- the templated edge-case batch adds `what is your sign`, `how many people do you know`, and `what is the loop` so the remaining source-backed lines can lean on live birthday and loop state - this pass keeps Build B moving while still favoring source-backed phrasing and preserving the command-vs-question boundary - the next passes should keep the same pattern and prefer source-backed phrasing whenever the legacy MIM text is available - if a source-backed legacy line is missing, use a temporary direct reply only to keep the pass moving, then backfill source text later diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.IntentRouting.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.IntentRouting.cs index e9c5523..6659c7e 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.IntentRouting.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.IntentRouting.cs @@ -622,6 +622,28 @@ public sealed partial class JiboInteractionService "do you have a religion")) return "robot_what_is_your_religion"; + if (MatchesAny( + loweredTranscript, + "what is your sign", + "what's your sign", + "what sign are you")) + return "robot_what_is_your_sign"; + + if (MatchesAny( + loweredTranscript, + "how many people do you know", + "how many people are in your loop", + "how many people are in the loop", + "how many people do you know in your loop")) + return "robot_how_many_people_do_you_know"; + + if (MatchesAny( + loweredTranscript, + "what is the loop", + "what's the loop", + "tell me about the loop")) + return "robot_what_is_the_loop"; + if (MatchesAny( loweredTranscript, "what are you doing for christmas", diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.PersonalityDecisions.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.PersonalityDecisions.cs index f70e3f5..e5de673 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.PersonalityDecisions.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.PersonalityDecisions.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Globalization; +using System.Linq; using Jibo.Cloud.Application.Abstractions; using Jibo.Cloud.Domain.Models; using Jibo.Runtime.Abstractions; @@ -429,6 +430,95 @@ public sealed partial class JiboInteractionService "No problem. We can save the pizza fact for another time."); } + private JiboInteractionDecision BuildWhatIsYourSignDecision() + { + var today = DateOnly.FromDateTime(DateTimeOffset.UtcNow.Date); + var birthday = OpenJiboCloudBuildInfo.PersonaBirthday; + var zodiac = DescribeZodiacSign(birthday); + var reply = birthday.Month == today.Month && birthday.Day == today.Day + ? $"{zodiac}. Today is my birthday." + : $"{zodiac}. I was first powered up on {OpenJiboCloudBuildInfo.PersonaBirthdayWords}."; + + return new JiboInteractionDecision( + "robot_what_is_your_sign", + reply, + ContextUpdates: ScriptedResponseDecisionBuilder.BuildScriptedResponseContextUpdates()); + } + + private JiboInteractionDecision BuildHowManyPeopleDoYouKnowDecision(TurnContext turn) + { + var people = GetLoopPeople(turn); + var speaker = ResolvePreferredGreetingName(turn, ResolveGreetingPresenceProfile(turn)); + var reply = people.Count switch + { + 0 => "Well if we're talking about people in my Loop, I do not know anyone yet.", + 1 when string.IsNullOrWhiteSpace(speaker) => + "Well if we're talking about people in my Loop, I know 1 person.", + 1 => $"Well there is 1 person in our Loop. And it's you {speaker}.", + _ when string.IsNullOrWhiteSpace(speaker) => + $"Well if we're talking about people in my Loop, I know {people.Count} people.", + _ => $"Well there are {people.Count} people in our Loop." + }; + + return new JiboInteractionDecision( + "robot_how_many_people_do_you_know", + reply, + ContextUpdates: ScriptedResponseDecisionBuilder.BuildScriptedResponseContextUpdates()); + } + + private JiboInteractionDecision BuildWhatIsTheLoopDecision(TurnContext turn) + { + var people = GetLoopPeople(turn); + var reply = people.Count == 0 + ? "The Loop is the people I know, and whose faces and voices I can learn to recognize. There can be up to 16 people in the Loop." + : $"The Loop is the group of people I know. They're the people whose voices and faces I can learn. Right now, my Loop is {JoinWithAnd(people.Select(person => person.DisplayName).ToArray())}."; + + return new JiboInteractionDecision( + "robot_what_is_the_loop", + reply, + ContextUpdates: ScriptedResponseDecisionBuilder.BuildScriptedResponseContextUpdates()); + } + + private IReadOnlyList GetLoopPeople(TurnContext turn) + { + if (cloudStateStore is null) return []; + + var loopId = ReadTenantAttribute(turn, "loopId") ?? "openjibo-default-loop"; + return cloudStateStore.GetPeople() + .Where(person => string.Equals(person.LoopId, loopId, StringComparison.OrdinalIgnoreCase)) + .OrderBy(person => person.IsPrimary ? 0 : 1) + .ThenBy(person => person.DisplayName, StringComparer.OrdinalIgnoreCase) + .ToArray(); + } + + private static string JoinWithAnd(IReadOnlyList values) + { + if (values.Count == 0) return string.Empty; + if (values.Count == 1) return values[0]; + if (values.Count == 2) return $"{values[0]} and {values[1]}"; + + return $"{string.Join(", ", values.Take(values.Count - 1))}, and {values[^1]}"; + } + + private static string DescribeZodiacSign(DateOnly birthday) + { + return (birthday.Month, birthday.Day) switch + { + (3, >= 21) or (4, <= 19) => "I'm Aries", + (4, >= 20) or (5, <= 20) => "I'm Taurus", + (5, >= 21) or (6, <= 20) => "I'm Gemini", + (6, >= 21) or (7, <= 22) => "I'm Cancer", + (7, >= 23) or (8, <= 22) => "I'm Leo", + (8, >= 23) or (9, <= 22) => "I'm Virgo", + (9, >= 23) or (10, <= 22) => "I'm Libra", + (10, >= 23) or (11, <= 21) => "I'm Scorpio", + (11, >= 22) or (12, <= 21) => "I'm Sagittarius", + (12, >= 22) or (1, <= 19) => "I'm Capricorn", + (1, >= 20) or (2, <= 18) => "I'm Aquarius", + _ => "I'm Pisces" + }; + } + private string BuildGenericReply(JiboExperienceCatalog catalog, string transcript, string lowered) { if (string.IsNullOrWhiteSpace(transcript)) return "I am listening."; 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 a31dad7..0c8e0b2 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 @@ -672,6 +672,9 @@ public sealed partial class JiboInteractionService( "robot_what_is_your_religion", "bring people together", "energy from the universe"), + "robot_what_is_your_sign" => BuildWhatIsYourSignDecision(), + "robot_how_many_people_do_you_know" => BuildHowManyPeopleDoYouKnowDecision(turn), + "robot_what_is_the_loop" => BuildWhatIsTheLoopDecision(turn), "robot_what_are_you_thinking" => BuildScriptedGreetingDecision( catalog, "robot_what_are_you_thinking", diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildB/README.md b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildB/README.md index f7461f1..2f4eb35 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildB/README.md +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildB/README.md @@ -30,3 +30,4 @@ The next deep-personality batch adds `what do you dream about`, `what are you af `what is your sign` is still deferred because the current importer strips the birthday/zodiac placeholders that Pegasus uses there, so that one needs a templating pass instead of a plain scripted-reply import. The next identity/knowledge batch adds `are you god`, `are you here`, `do you have super powers`, `how much do you know`, `what does jibo mean`, `where do you get info`, `what are you forbidden to do`, `what color are you`, and `what do you do when alone` so the old self-description and capability loop keeps coming back in source-backed form. The next body/mission batch adds `how much do you weigh`, `how tall are you`, `how much do you cost`, `what if I unplug you`, `what is your purpose`, `what is your prime directive`, `what is jibo commander`, `do you like commander app`, and `what are you made of` so the physical self-description and capability answers stay closer to Pegasus too. +The templated edge-case batch adds `what is your sign`, `how many people do you know`, and `what is the loop` so the remaining source-backed lines can use live birthday and loop state instead of falling back to static text. diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs index 9cd9c9e..b017d51 100644 --- a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs +++ b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs @@ -715,6 +715,48 @@ public sealed class JiboInteractionServiceTests Assert.Equal("ScriptedResponse", decision.ContextUpdates![ChitchatRouteKey]); } + [Theory] + [InlineData("what is your sign", "robot_what_is_your_sign", "I'm Aries")] + [InlineData("what's your sign", "robot_what_is_your_sign", "March 22, 2026")] + public async Task BuildDecisionAsync_SignTemplatedMim_UsesPersonaBirthday( + string transcript, + string expectedIntent, + string expectedReplySnippet) + { + var service = CreateService(); + + var decision = await service.BuildDecisionAsync(new TurnContext + { + RawTranscript = transcript, + NormalizedTranscript = transcript + }); + + Assert.Equal(expectedIntent, decision.IntentName); + Assert.Contains(expectedReplySnippet, decision.ReplyText, StringComparison.OrdinalIgnoreCase); + Assert.Equal("ScriptedResponse", decision.ContextUpdates![ChitchatRouteKey]); + } + + [Theory] + [InlineData("how many people do you know", "robot_how_many_people_do_you_know", "I know 2 people")] + [InlineData("what is the loop", "robot_what_is_the_loop", "Jibo Owner and OpenJibo Household Member")] + public async Task BuildDecisionAsync_LoopTemplatedMims_UseLiveLoopState( + string transcript, + string expectedIntent, + string expectedReplySnippet) + { + var service = CreateService(cloudStateStore: new InMemoryCloudStateStore()); + + var decision = await service.BuildDecisionAsync(new TurnContext + { + RawTranscript = transcript, + NormalizedTranscript = transcript + }); + + Assert.Equal(expectedIntent, decision.IntentName); + Assert.Contains(expectedReplySnippet, decision.ReplyText, StringComparison.OrdinalIgnoreCase); + Assert.Equal("ScriptedResponse", decision.ContextUpdates![ChitchatRouteKey]); + } + [Theory] [InlineData("how much do you know", "robot_knowledge", "I know a lot")] [InlineData("what do you know", "robot_knowledge", "I know a lot")]