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:
|
||||
|
||||
@@ -628,4 +634,4 @@ For `1.0.20` and beyond:
|
||||
5. Loop advancement (family and friends) / multiple user recognition / multiple Jibo support so Jibo's can interact and communicate
|
||||
6. Advanced Jibo features such as pizza delivery, Uber/Lyft integration, calendar management, smart home control (Home Assistant), etc. can be added after the conversion process is smooth and stable, with a focus on features that leverage the new cloud capabilities and content personalization enabled by Open Jibo
|
||||
7. LLM integration for more natural dialog, question answering, and content generation can be explored as a longer-term goal after the core platform is stable and has a growing user base to provide feedback and use cases for LLM-powered features
|
||||
8. Tiered Jibo brain/orchestration plan from README.md can be implemented in parallel with the above, starting with the simplest cloud features and gradually adding more complex capabilities as the platform matures and user feedback is collected, always preserving his unique charm and original features.
|
||||
8. Tiered Jibo brain/orchestration plan from README.md can be implemented in parallel with the above, starting with the simplest cloud features and gradually adding more complex capabilities as the platform matures and user feedback is collected, always preserving his unique charm and original features.
|
||||
|
||||
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