Port pizza MIMs and update 1.0.19 planning
This commit is contained in:
@@ -6,18 +6,19 @@ This document is the current working plan for the OpenJibo hosted cloud.
|
||||
|
||||
The production lane is the `.NET` cloud in `src/Jibo.Cloud/dotnet`. The Node server remains the protocol oracle, capture harness, and fast reverse-engineering lab, but it is no longer the long-term hosted architecture.
|
||||
|
||||
Day-to-day feature sequencing lives in [feature-backlog.md](feature-backlog.md). Live closeout checks live in [regression-test-plan.md](regression-test-plan.md). This file tracks release shape, current code truth, evidence sources, and the boundary between `1.0.18` closeout work and `1.0.19` follow-up work.
|
||||
Day-to-day feature sequencing lives in [feature-backlog.md](feature-backlog.md). Live closeout checks live in [regression-test-plan.md](regression-test-plan.md). The `1.0.19` release shape is detailed in [release-1.0.19-plan.md](release-1.0.19-plan.md), while this file keeps the broader evidence and architecture context.
|
||||
|
||||
## Current Release Snapshot
|
||||
|
||||
- Current OpenJibo Cloud release constant: `1.0.18`
|
||||
- Current OpenJibo Cloud release constant: `1.0.19`
|
||||
- Source of truth: [OpenJiboCloudBuildInfo.cs](../src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/OpenJiboCloudBuildInfo.cs)
|
||||
- Spoken diagnostic: `Open Jibo Cloud version 1 dot 0 dot 18.`
|
||||
- Spoken diagnostic: `Cloud version 1 dot 0 dot 19.`
|
||||
- HTTP diagnostic: `/health` returns the same version
|
||||
- Startup diagnostic: the API logs the same version on boot
|
||||
- .NET target framework: `net10.0` across the cloud projects and cloud test project
|
||||
- First `1.0.19` shipped slice: persona prompts (`how old are you`, `when's your birthday`, `do you have a personality`, `make a pizza`)
|
||||
|
||||
Release `1.0.18` is now in feature-hardening. Its main bug-fix theme is alarm and photo/gallery behavior on stock OS `1.9`, with a few small feature slices added while the test loop is warm.
|
||||
Release `1.0.19` is now in feature kickoff. The `1.0.18` alarm/photo/gallery closeout evidence remains below as historical context while we execute the next feature slices.
|
||||
|
||||
## Latest Live Evidence
|
||||
|
||||
@@ -200,6 +201,8 @@ These are not blockers for calling `1.0.18` complete unless the live test shows
|
||||
After `1.0.18` is tested and tagged, `1.0.19` should move back into feature work:
|
||||
|
||||
- harden whichever stop/volume behavior is not fully proven by the `1.0.18` live pass, or pick the next lightweight device/persona slice
|
||||
- extend persona with holidays and seasonal content as a first-class character track
|
||||
- build multi-tenant internal memory storage (account/loop/device/user scoped) so new personality and history features persist safely
|
||||
- end-to-end update/backup/restore proof
|
||||
- STT reliability improvements, including noise screening and a managed STT comparison
|
||||
- provider-backed first content path, likely news or weather
|
||||
|
||||
@@ -8,6 +8,8 @@ Use it as the working queue when picking the next feature or bug-fix slice. The
|
||||
|
||||
The live regression checklist for release closeout is [regression-test-plan.md](regression-test-plan.md).
|
||||
|
||||
The active `1.0.19` execution shape is tracked in [release-1.0.19-plan.md](release-1.0.19-plan.md). This file keeps the full `1.0.18` evidence trail for parity reference.
|
||||
|
||||
Status key:
|
||||
|
||||
- `implemented`: present in current source and covered by focused tests
|
||||
@@ -24,9 +26,9 @@ Tags:
|
||||
- `stt`: transcript reliability
|
||||
- `storage`: persistence, media, backups, or hosted export
|
||||
|
||||
## Current `1.0.18` Snapshot
|
||||
## Historical `1.0.18` Snapshot
|
||||
|
||||
Current cloud version: `1.0.18`
|
||||
Historical cloud version at closeout boundary: `1.0.18`
|
||||
|
||||
Runtime truth:
|
||||
|
||||
@@ -573,16 +575,18 @@ Current release theme:
|
||||
|
||||
### 21. How Old Are You / Robot Age Persona
|
||||
|
||||
- Status: `discovery`
|
||||
- Status: `implemented`
|
||||
- Tags: `protocol`, `content`
|
||||
- User goals:
|
||||
- Result:
|
||||
- `how old are you`
|
||||
- answer from stored first-powered-up or first-cloud-seen metadata
|
||||
- optional zodiac/personality flavor when available
|
||||
- Questions:
|
||||
- where stock Jibo stores first-power-up or birthdate metadata
|
||||
- whether a stock persona path exists
|
||||
- whether first OpenJibo pass should use first-cloud-seen metadata if stock data is unavailable
|
||||
- `when's your birthday`
|
||||
- `do you have a personality`
|
||||
- `make a pizza` now ports the original scripted-response path through `chitchat-skill` with `mim_id = RA_JBO_MakePizza` and pizza-making animation ESML
|
||||
- `can you order pizza` now ports the original scripted-response path through `chitchat-skill` with `mim_id = RA_JBO_OrderPizza`
|
||||
- current source answers these with a `1.0.19` rule-based persona baseline, backed by `OpenJiboCloudBuildInfo.PersonaBirthday`
|
||||
- Follow-up:
|
||||
- wire persona age to first-powered-up or durable first-cloud-seen metadata when available
|
||||
- add command-vs-question variants so expressive prompts can answer conversationally before launching actions
|
||||
|
||||
### 22. Command Vs Question Reply Style
|
||||
|
||||
@@ -611,13 +615,15 @@ 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? (This is a fun one that can be implemented quickly and adds a lot of character, so it should be early in the queue to start showing off the new content capabilities.)
|
||||
3. Update, backup, and restore proof
|
||||
4. STT upgrade and noise screening
|
||||
5. Hosted capture/storage plan / indexing for group testing
|
||||
6. Binary-safe media storage / sync to cloud drive: OneDrive, Google Drive, Box, etc.
|
||||
7. Provider-backed news and weather
|
||||
8. Proactivity, dialog parsing/NLP, memory/history, Lasso, identity, and onboarding as larger discovery-driven tracks
|
||||
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
|
||||
|
||||
For `1.0.20` and beyond:
|
||||
|
||||
|
||||
76
OpenJibo/docs/release-1.0.19-plan.md
Normal file
76
OpenJibo/docs/release-1.0.19-plan.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Release `1.0.19` Plan
|
||||
|
||||
## Purpose
|
||||
|
||||
This release starts the shift from `1.0.18` hardening to visible feature growth.
|
||||
|
||||
The goal is to keep compatibility work steady while shipping personality and capability slices that make OpenJibo feel less like a placeholder cloud and more like a real assistant platform.
|
||||
|
||||
## Snapshot
|
||||
|
||||
- Kickoff date: `2026-05-05`
|
||||
- Cloud version source of truth: [OpenJiboCloudBuildInfo.cs](../src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/OpenJiboCloudBuildInfo.cs)
|
||||
- Active release constant: `1.0.19`
|
||||
|
||||
## Scope
|
||||
|
||||
### 1. Persona And Identity Surface
|
||||
|
||||
- add natural voice responses for robot identity/personality prompts
|
||||
- start building reusable content hooks for question-vs-command style responses
|
||||
- keep first implementation rule-based and test-backed
|
||||
|
||||
### 2. Reliability And Device Proof
|
||||
|
||||
- complete update/backup/restore proof path with captures and operator docs
|
||||
- continue alarm/gallery/yes-no cleanup from `1.0.18` evidence where regressions are still open
|
||||
- improve short-turn STT reliability and low-signal screening
|
||||
|
||||
### 3. Pegasus-To-Cloud Platform Porting
|
||||
|
||||
- prioritize small source-backed slices from Pegasus/JiboOS that can be shipped safely
|
||||
- keep Nimbus and stock payload compatibility as the release guardrail
|
||||
- avoid broad subsystem rewrites without tests and live-capture evidence
|
||||
|
||||
### 4. Holidays And Seasonal Personality
|
||||
|
||||
- port holiday-aware personality responses as a visible extension of the new persona slice
|
||||
- start with a small, source-backed set (for example birthdays/holidays already represented in legacy data paths)
|
||||
- ensure holiday responses feel characterful while still routing through stock-compatible payloads
|
||||
|
||||
### 5. Multi-Tenant Memory Storage Foundation
|
||||
|
||||
- define tenant boundaries across account, loop, device, and person-memory records
|
||||
- add storage abstractions that can move from in-memory/local JSON to hosted SQL/Blob without reworking behavior layers
|
||||
- implement memory-ready schemas and repository contracts for user facts (names, birthdays, personal dates, preferences) with strict tenant scoping
|
||||
|
||||
## First Implemented Slice In `1.0.19`
|
||||
|
||||
The first delivered slice in this release is persona expansion:
|
||||
|
||||
- `how old are you`
|
||||
- `when's your birthday`
|
||||
- `do you have a personality`
|
||||
- `make a pizza`
|
||||
|
||||
`make a pizza` is now wired to the legacy scripted-response identity (`RA_JBO_MakePizza`) with pizza-making animation ESML, based on the original skill manifests.
|
||||
|
||||
This slice is intentionally small and user-visible. It creates immediate personality gains while we keep deeper platform work in parallel.
|
||||
|
||||
## 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
|
||||
|
||||
## Definition Of Done
|
||||
|
||||
Release `1.0.19` is complete when:
|
||||
|
||||
- planned slices have focused tests and updated docs
|
||||
- regression checklist passes for the existing stock-OS compatibility paths
|
||||
- live runs confirm no critical regressions in alarms, gallery, yes/no, and cloud-version diagnostics
|
||||
- memory/personality storage proves tenant isolation by account/loop/device boundaries and is compatible with the target hosted cloud footprint
|
||||
36228
OpenJibo/docs/resources/SDK-SDK---ESML-121023-203758.pdf
Normal file
36228
OpenJibo/docs/resources/SDK-SDK---ESML-121023-203758.pdf
Normal file
File diff suppressed because one or more lines are too long
@@ -6,7 +6,7 @@
|
||||
|
||||
This is the production-oriented path for restoring device connectivity and creating a foundation for future runtime, AI, and OTA work.
|
||||
|
||||
Current spoken cloud version: `Open Jibo Cloud version 1.0.18.`
|
||||
Current spoken cloud version: `Cloud version 1.0.19.`
|
||||
|
||||
Release hygiene reminder:
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ public sealed class JiboExperienceCatalog
|
||||
public IReadOnlyList<string> DanceAnimations { get; init; } = [];
|
||||
public IReadOnlyList<string> GreetingReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> HowAreYouReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> PersonalityReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> PizzaReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> SurpriseReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> PersonalReportReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> WeatherReplies { get; init; } = [];
|
||||
|
||||
@@ -73,6 +73,11 @@ public sealed class JiboInteractionService(
|
||||
"photobooth" => BuildPhotoCreateDecision("photobooth", "Starting photobooth.", "createSomePhotos"),
|
||||
"hello" => new JiboInteractionDecision("hello", randomizer.Choose(catalog.GreetingReplies)),
|
||||
"how_are_you" => new JiboInteractionDecision("how_are_you", randomizer.Choose(catalog.HowAreYouReplies)),
|
||||
"robot_age" => BuildRobotAgeDecision(referenceLocalTime),
|
||||
"robot_birthday" => BuildRobotBirthdayDecision(),
|
||||
"robot_personality" => new JiboInteractionDecision("robot_personality", randomizer.Choose(catalog.PersonalityReplies)),
|
||||
"pizza" => BuildPizzaDecision(),
|
||||
"order_pizza" => BuildOrderPizzaDecision(),
|
||||
"yes" => new JiboInteractionDecision("yes", "Yes."),
|
||||
"no" => new JiboInteractionDecision("no", "No."),
|
||||
"word_of_the_day" => BuildWordOfTheDayLaunchDecision(),
|
||||
@@ -93,6 +98,55 @@ public sealed class JiboInteractionService(
|
||||
SkillPayload: new Dictionary<string, object?> { ["esml"] = OpenJiboCloudBuildInfo.EsmlVersion });
|
||||
}
|
||||
|
||||
private static JiboInteractionDecision BuildRobotAgeDecision(DateTimeOffset? referenceLocalTime)
|
||||
{
|
||||
var referenceDate = DateOnly.FromDateTime((referenceLocalTime ?? DateTimeOffset.UtcNow).Date);
|
||||
var ageDescription = DescribePersonaAge(referenceDate, OpenJiboCloudBuildInfo.PersonaBirthday);
|
||||
return new JiboInteractionDecision(
|
||||
"robot_age",
|
||||
$"I count {OpenJiboCloudBuildInfo.PersonaBirthdayWords} as my birthday, so I am {ageDescription}.");
|
||||
}
|
||||
|
||||
private static JiboInteractionDecision BuildRobotBirthdayDecision()
|
||||
{
|
||||
return new JiboInteractionDecision(
|
||||
"robot_birthday",
|
||||
$"My birthday is {OpenJiboCloudBuildInfo.PersonaBirthdayWords}.");
|
||||
}
|
||||
|
||||
private JiboInteractionDecision BuildPizzaDecision()
|
||||
{
|
||||
var prompt = randomizer.Choose(PizzaMimPrompts);
|
||||
return new JiboInteractionDecision(
|
||||
"pizza",
|
||||
"One pizza, coming right up.",
|
||||
"chitchat-skill",
|
||||
new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["esml"] = prompt.Esml,
|
||||
["mim_id"] = "RA_JBO_MakePizza",
|
||||
["mim_type"] = "announcement",
|
||||
["prompt_id"] = prompt.PromptId,
|
||||
["prompt_sub_category"] = "AN"
|
||||
});
|
||||
}
|
||||
|
||||
private static JiboInteractionDecision BuildOrderPizzaDecision()
|
||||
{
|
||||
return new JiboInteractionDecision(
|
||||
"order_pizza",
|
||||
"I can't do that yet, but I bet I'll be able to do that sometime in the near future.",
|
||||
"chitchat-skill",
|
||||
new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["esml"] = "<speak>I can't do that yet, but I bet I'll be able to do that sometime in the near future.</speak>",
|
||||
["mim_id"] = "RA_JBO_OrderPizza",
|
||||
["mim_type"] = "announcement",
|
||||
["prompt_id"] = "RA_JBO_OrderPizza_AN_01",
|
||||
["prompt_sub_category"] = "AN"
|
||||
});
|
||||
}
|
||||
|
||||
private JiboInteractionDecision BuildJokeDecision(JiboExperienceCatalog catalog)
|
||||
{
|
||||
var joke = randomizer.Choose(catalog.Jokes);
|
||||
@@ -231,6 +285,16 @@ public sealed class JiboInteractionService(
|
||||
return "alarm_value";
|
||||
}
|
||||
|
||||
if (string.Equals(clientIntent, "requestMakePizza", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "pizza";
|
||||
}
|
||||
|
||||
if (string.Equals(clientIntent, "requestOrderPizza", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "order_pizza";
|
||||
}
|
||||
|
||||
if (IsCancelRequest(clientIntent, loweredTranscript))
|
||||
{
|
||||
if (isAlarmValueTurn)
|
||||
@@ -451,6 +515,61 @@ public sealed class JiboInteractionService(
|
||||
return "surprise";
|
||||
}
|
||||
|
||||
if (MatchesAny(
|
||||
loweredTranscript,
|
||||
"how old are you",
|
||||
"what is your age",
|
||||
"what s your age",
|
||||
"how old r you"))
|
||||
{
|
||||
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",
|
||||
"what is your personality",
|
||||
"what's your personality",
|
||||
"what s your personality",
|
||||
"describe your personality"))
|
||||
{
|
||||
return "robot_personality";
|
||||
}
|
||||
|
||||
if (MatchesAny(
|
||||
loweredTranscript,
|
||||
"can you cook us a pizza",
|
||||
"flip a pizza",
|
||||
"make a pizza",
|
||||
"make pizza",
|
||||
"show pizza",
|
||||
"can you make pizza",
|
||||
"let's make pizza",
|
||||
"lets make pizza"))
|
||||
{
|
||||
return "pizza";
|
||||
}
|
||||
|
||||
if (MatchesAny(
|
||||
loweredTranscript,
|
||||
"can you order pizza",
|
||||
"order pizza",
|
||||
"please order pizza"))
|
||||
{
|
||||
return "order_pizza";
|
||||
}
|
||||
|
||||
if (MatchesAny(loweredTranscript, "personal report", "my report", "daily report", "my update"))
|
||||
{
|
||||
return "personal_report";
|
||||
@@ -841,6 +960,44 @@ public sealed class JiboInteractionService(
|
||||
return previous[right.Length];
|
||||
}
|
||||
|
||||
private static string DescribePersonaAge(DateOnly referenceDate, DateOnly birthday)
|
||||
{
|
||||
if (referenceDate < birthday)
|
||||
{
|
||||
return "just getting started";
|
||||
}
|
||||
|
||||
var totalDays = referenceDate.DayNumber - birthday.DayNumber;
|
||||
if (totalDays <= 31)
|
||||
{
|
||||
return $"{FormatAgeUnit(totalDays, "day")} old";
|
||||
}
|
||||
|
||||
var totalMonths = (referenceDate.Year - birthday.Year) * 12 + referenceDate.Month - birthday.Month;
|
||||
if (referenceDate.Day < birthday.Day)
|
||||
{
|
||||
totalMonths -= 1;
|
||||
}
|
||||
|
||||
totalMonths = Math.Max(totalMonths, 0);
|
||||
if (totalMonths < 12)
|
||||
{
|
||||
return $"{FormatAgeUnit(totalMonths, "month")} old";
|
||||
}
|
||||
|
||||
var years = totalMonths / 12;
|
||||
var months = totalMonths % 12;
|
||||
return months == 0
|
||||
? $"{FormatAgeUnit(years, "year")} old"
|
||||
: $"{FormatAgeUnit(years, "year")} and {FormatAgeUnit(months, "month")} old";
|
||||
}
|
||||
|
||||
private static string FormatAgeUnit(int value, string singular)
|
||||
{
|
||||
var plural = value == 1 ? singular : $"{singular}s";
|
||||
return $"{value} {plural}";
|
||||
}
|
||||
|
||||
private static IEnumerable<string> ReadRules(TurnContext turn, string key)
|
||||
{
|
||||
if (!turn.Attributes.TryGetValue(key, out var value) || value is null)
|
||||
@@ -1503,6 +1660,8 @@ public sealed class JiboInteractionService(
|
||||
|
||||
private sealed record ClockAlarmValue(string Time, string AmPm);
|
||||
|
||||
private sealed record PizzaMimPrompt(string PromptId, string Esml);
|
||||
|
||||
private static readonly Regex SplitAlarmPattern = new(
|
||||
@"\b(?<hour>\d{1,2}|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve)(?:[:\s,-]+(?<minute>\d{2}|[a-z\-]+(?:\s+[a-z\-]+)?))?\s*(?<ampm>a[\s\.]*m\.?|p[\s\.]*m\.?)?\b",
|
||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
||||
@@ -1531,6 +1690,13 @@ public sealed class JiboInteractionService(
|
||||
@"\b(?:cancel|delete|remove|stop|turn\s+off)\s+(?:the\s+)?(?:alarm|along|elo)\b",
|
||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
||||
|
||||
private static readonly PizzaMimPrompt[] PizzaMimPrompts =
|
||||
[
|
||||
new("RA_JBO_ShowPizzaMaking_AN_01", "<speak><anim cat='jiboji' filter='pizza-making'/></speak>"),
|
||||
new("RA_JBO_ShowPizzaMaking_AN_02", "<speak><anim cat='jiboji' filter='pizza-making' nonBlocking='true'/><pitch mult='1.2'>One </pitch> pizza, coming right up.</speak>"),
|
||||
new("RA_JBO_ShowPizzaMaking_AN_03", "<speak><anim cat='jiboji' filter='pizza-making' nonBlocking='true'/>My <pitch mult='1.2'>specialty </pitch>.</speak>")
|
||||
];
|
||||
|
||||
private static readonly (string Phrase, string Station)[] RadioGenreAliases =
|
||||
[
|
||||
("country music", "Country"),
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace Jibo.Cloud.Application.Services;
|
||||
|
||||
public static class OpenJiboCloudBuildInfo
|
||||
{
|
||||
public const string Version = "1.0.18";
|
||||
public const string Version = "1.0.19";
|
||||
public static readonly DateOnly PersonaBirthday = new(2026, 3, 22);
|
||||
|
||||
public static string VersionWords => Version.Replace(".", " dot ");
|
||||
public static string PersonaBirthdayWords => PersonaBirthday.ToString("MMMM d, yyyy", CultureInfo.InvariantCulture);
|
||||
|
||||
public static string SpokenVersion => $"Cloud version {VersionWords}.";
|
||||
|
||||
|
||||
@@ -743,6 +743,8 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
||||
: $"<speak><es cat='neutral' filter='!ssa-only, !sfx-only' endNeutral='true'>{EscapeXml(speak.Text)}</es></speak>");
|
||||
var mimId = ReadPayloadString(skillPayload, "mim_id") ?? (isJoke ? "runtime-joke" : "runtime-chat");
|
||||
var mimType = ReadPayloadString(skillPayload, "mim_type") ?? "announcement";
|
||||
var promptId = ReadPayloadString(skillPayload, "prompt_id") ?? "RUNTIME_PROMPT";
|
||||
var promptSubCategory = ReadPayloadString(skillPayload, "prompt_sub_category") ?? "AN";
|
||||
|
||||
return new
|
||||
{
|
||||
@@ -770,8 +772,8 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
||||
esml,
|
||||
meta = new
|
||||
{
|
||||
prompt_id = "RUNTIME_PROMPT",
|
||||
prompt_sub_category = "AN",
|
||||
prompt_id = promptId,
|
||||
prompt_sub_category = promptSubCategory,
|
||||
mim_id = mimId,
|
||||
mim_type = mimType
|
||||
}
|
||||
|
||||
@@ -41,6 +41,18 @@ public sealed class InMemoryJiboExperienceContentRepository : IJiboExperienceCon
|
||||
"I am doing great. Thanks for asking.",
|
||||
"I am feeling bright-eyed and ready to help."
|
||||
],
|
||||
PersonalityReplies =
|
||||
[
|
||||
"I do. I am curious, playful, and always up for a new experiment.",
|
||||
"Absolutely. I am friendly, curious, and a little goofy on purpose.",
|
||||
"Yes. My personality is part helper, part curious robot sidekick."
|
||||
],
|
||||
PizzaReplies =
|
||||
[
|
||||
"I cannot bake yet, but I can help design the perfect pizza plan.",
|
||||
"I am still cloud-side for now, so no oven control yet. But I can help pick toppings.",
|
||||
"Pizza mission accepted in spirit. I can help with the recipe while you handle the baking."
|
||||
],
|
||||
SurpriseReplies =
|
||||
[
|
||||
"I can definitely surprise you. We are still mapping that path, but I am ready for the next experiment.",
|
||||
|
||||
@@ -56,6 +56,132 @@ public sealed class JiboInteractionServiceTests
|
||||
Assert.Equal("chitchat-skill", decision.SkillName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildDecisionAsync_HowOldAreYou_UsesPersonaBirthdayForAgeReply()
|
||||
{
|
||||
var service = CreateService();
|
||||
|
||||
var decision = await service.BuildDecisionAsync(new TurnContext
|
||||
{
|
||||
RawTranscript = "how old are you",
|
||||
NormalizedTranscript = "how old are you",
|
||||
Attributes = new Dictionary<string, object?>
|
||||
{
|
||||
["context"] = """{"runtime":{"location":{"iso":"2026-05-05T19:00:00-05:00"}}}"""
|
||||
}
|
||||
});
|
||||
|
||||
Assert.Equal("robot_age", decision.IntentName);
|
||||
Assert.Equal("I count March 22, 2026 as my birthday, so I am 1 month old.", decision.ReplyText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildDecisionAsync_WhenIsYourBirthday_UsesPersonaBirthdayReply()
|
||||
{
|
||||
var service = CreateService();
|
||||
|
||||
var decision = await service.BuildDecisionAsync(new TurnContext
|
||||
{
|
||||
RawTranscript = "when's your birthday",
|
||||
NormalizedTranscript = "when'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()
|
||||
{
|
||||
var service = CreateService();
|
||||
|
||||
var decision = await service.BuildDecisionAsync(new TurnContext
|
||||
{
|
||||
RawTranscript = "do you have a personality",
|
||||
NormalizedTranscript = "do you have a personality"
|
||||
});
|
||||
|
||||
Assert.Equal("robot_personality", decision.IntentName);
|
||||
Assert.Equal("I do. I am curious, playful, and always up for a new experiment.", decision.ReplyText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildDecisionAsync_MakePizza_UsesOriginalMimStylePayload()
|
||||
{
|
||||
var service = CreateService();
|
||||
|
||||
var decision = await service.BuildDecisionAsync(new TurnContext
|
||||
{
|
||||
RawTranscript = "make a pizza",
|
||||
NormalizedTranscript = "make a pizza"
|
||||
});
|
||||
|
||||
Assert.Equal("pizza", decision.IntentName);
|
||||
Assert.Equal("chitchat-skill", decision.SkillName);
|
||||
Assert.Equal("One pizza, coming right up.", decision.ReplyText);
|
||||
Assert.Equal("RA_JBO_MakePizza", decision.SkillPayload!["mim_id"]);
|
||||
Assert.Equal("RA_JBO_ShowPizzaMaking_AN_01", decision.SkillPayload["prompt_id"]);
|
||||
Assert.Contains("pizza-making", decision.SkillPayload["esml"]?.ToString(), StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildDecisionAsync_ClientNluRequestMakePizza_UsesOriginalMimStylePayload()
|
||||
{
|
||||
var service = CreateService();
|
||||
|
||||
var decision = await service.BuildDecisionAsync(new TurnContext
|
||||
{
|
||||
RawTranscript = "requestMakePizza",
|
||||
NormalizedTranscript = "requestMakePizza",
|
||||
Attributes = new Dictionary<string, object?>
|
||||
{
|
||||
["clientIntent"] = "requestMakePizza"
|
||||
}
|
||||
});
|
||||
|
||||
Assert.Equal("pizza", decision.IntentName);
|
||||
Assert.Equal("chitchat-skill", decision.SkillName);
|
||||
Assert.Equal("RA_JBO_MakePizza", decision.SkillPayload!["mim_id"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildDecisionAsync_CanYouOrderPizza_UsesLegacyOrderPizzaMimPayload()
|
||||
{
|
||||
var service = CreateService();
|
||||
|
||||
var decision = await service.BuildDecisionAsync(new TurnContext
|
||||
{
|
||||
RawTranscript = "can you order pizza",
|
||||
NormalizedTranscript = "can you order pizza"
|
||||
});
|
||||
|
||||
Assert.Equal("order_pizza", decision.IntentName);
|
||||
Assert.Equal("chitchat-skill", decision.SkillName);
|
||||
Assert.Equal("RA_JBO_OrderPizza", decision.SkillPayload!["mim_id"]);
|
||||
Assert.Equal("RA_JBO_OrderPizza_AN_01", decision.SkillPayload["prompt_id"]);
|
||||
Assert.Contains("I can't do that yet", decision.SkillPayload["esml"]?.ToString(), StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildDecisionAsync_ClientNluRequestOrderPizza_UsesLegacyOrderPizzaMimPayload()
|
||||
{
|
||||
var service = CreateService();
|
||||
|
||||
var decision = await service.BuildDecisionAsync(new TurnContext
|
||||
{
|
||||
RawTranscript = "requestOrderPizza",
|
||||
NormalizedTranscript = "requestOrderPizza",
|
||||
Attributes = new Dictionary<string, object?>
|
||||
{
|
||||
["clientIntent"] = "requestOrderPizza"
|
||||
}
|
||||
});
|
||||
|
||||
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_ClientNluAskForDate_MapsToDateIntent()
|
||||
{
|
||||
|
||||
@@ -2817,6 +2817,95 @@ public sealed class JiboWebSocketServiceTests
|
||||
Assert.Equal("chitchat-skill", skillPayload.RootElement.GetProperty("data").GetProperty("skill").GetProperty("id").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClientAsrMakePizzaFlow_UsesLegacyPizzaMimAndAnimation()
|
||||
{
|
||||
await _service.HandleMessageAsync(new WebSocketMessageEnvelope
|
||||
{
|
||||
HostName = "neo-hub.jibo.com",
|
||||
Path = "/listen",
|
||||
Kind = "neo-hub-listen",
|
||||
Token = "hub-client-asr-pizza-token",
|
||||
Text = """{"type":"LISTEN","transID":"trans-pizza-shape","data":{"rules":["wake-word"]}}"""
|
||||
});
|
||||
|
||||
var replies = await _service.HandleMessageAsync(new WebSocketMessageEnvelope
|
||||
{
|
||||
HostName = "neo-hub.jibo.com",
|
||||
Path = "/listen",
|
||||
Kind = "neo-hub-listen",
|
||||
Token = "hub-client-asr-pizza-token",
|
||||
Text = """{"type":"CLIENT_ASR","transID":"trans-pizza-shape","data":{"text":"make a pizza"}}"""
|
||||
});
|
||||
|
||||
Assert.Equal(3, replies.Count);
|
||||
Assert.Equal("SKILL_ACTION", ReadReplyType(replies[2]));
|
||||
|
||||
using var skillPayload = JsonDocument.Parse(replies[2].Text!);
|
||||
Assert.Equal("chitchat-skill", skillPayload.RootElement.GetProperty("data").GetProperty("skill").GetProperty("id").GetString());
|
||||
|
||||
var play = skillPayload.RootElement
|
||||
.GetProperty("data")
|
||||
.GetProperty("action")
|
||||
.GetProperty("config")
|
||||
.GetProperty("jcp")
|
||||
.GetProperty("config")
|
||||
.GetProperty("play");
|
||||
|
||||
var esml = play.GetProperty("esml").GetString();
|
||||
Assert.Contains("pizza-making", esml, StringComparison.Ordinal);
|
||||
|
||||
var meta = play.GetProperty("meta");
|
||||
Assert.Equal("RA_JBO_MakePizza", meta.GetProperty("mim_id").GetString());
|
||||
Assert.Equal("announcement", meta.GetProperty("mim_type").GetString());
|
||||
Assert.StartsWith("RA_JBO_ShowPizzaMaking_AN_", meta.GetProperty("prompt_id").GetString(), StringComparison.Ordinal);
|
||||
Assert.Equal("AN", meta.GetProperty("prompt_sub_category").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClientAsrOrderPizzaFlow_UsesLegacyOrderPizzaMim()
|
||||
{
|
||||
await _service.HandleMessageAsync(new WebSocketMessageEnvelope
|
||||
{
|
||||
HostName = "neo-hub.jibo.com",
|
||||
Path = "/listen",
|
||||
Kind = "neo-hub-listen",
|
||||
Token = "hub-client-asr-order-pizza-token",
|
||||
Text = """{"type":"LISTEN","transID":"trans-order-pizza-shape","data":{"rules":["wake-word"]}}"""
|
||||
});
|
||||
|
||||
var replies = await _service.HandleMessageAsync(new WebSocketMessageEnvelope
|
||||
{
|
||||
HostName = "neo-hub.jibo.com",
|
||||
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"}}"""
|
||||
});
|
||||
|
||||
Assert.Equal(3, replies.Count);
|
||||
Assert.Equal("SKILL_ACTION", ReadReplyType(replies[2]));
|
||||
|
||||
using var skillPayload = JsonDocument.Parse(replies[2].Text!);
|
||||
Assert.Equal("chitchat-skill", skillPayload.RootElement.GetProperty("data").GetProperty("skill").GetProperty("id").GetString());
|
||||
|
||||
var play = skillPayload.RootElement
|
||||
.GetProperty("data")
|
||||
.GetProperty("action")
|
||||
.GetProperty("config")
|
||||
.GetProperty("jcp")
|
||||
.GetProperty("config")
|
||||
.GetProperty("play");
|
||||
|
||||
Assert.Contains("I can't do that yet", play.GetProperty("esml").GetString(), StringComparison.Ordinal);
|
||||
|
||||
var meta = play.GetProperty("meta");
|
||||
Assert.Equal("RA_JBO_OrderPizza", meta.GetProperty("mim_id").GetString());
|
||||
Assert.Equal("announcement", meta.GetProperty("mim_type").GetString());
|
||||
Assert.Equal("RA_JBO_OrderPizza_AN_01", meta.GetProperty("prompt_id").GetString());
|
||||
Assert.Equal("AN", meta.GetProperty("prompt_sub_category").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FollowUpTurn_UsesNewTurnStateWithoutLeakingBufferedAudio()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user