Refine persona routing and update 1.0.19 plan

This commit is contained in:
Jacob Dubin
2026-05-05 22:27:28 -05:00
parent 4b6b744688
commit 687ff62f0f
7 changed files with 151 additions and 37 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -22,4 +22,5 @@ public sealed class JiboExperienceCatalog
public IReadOnlyList<string> NewsBriefings { get; init; } = [];
public IReadOnlyList<string> GenericFallbackReplies { get; init; } = [];
public IReadOnlyList<string> DanceReplies { get; init; } = [];
public IReadOnlyList<string> DanceQuestionReplies { get; init; } = [];
}

View File

@@ -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)

View File

@@ -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.",

View File

@@ -41,6 +41,22 @@ public sealed class JiboInteractionServiceTests
Assert.Equal("<speak>Okay.<break size='0.2'/> Watch this.<anim cat='dance' filter='music, rom-upbeat' /></speak>", 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<string, object?>
{
["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()
{

View File

@@ -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);