From 11a3e4ef13a86e00eb2af180d6eb7d0fffd840dd Mon Sep 17 00:00:00 2001 From: Jacob Dubin Date: Wed, 13 May 2026 23:18:18 -0500 Subject: [PATCH] Add legacy MIM importer and seed Build A content --- OpenJibo/docs/feature-backlog.md | 52 +++ OpenJibo/docs/release-1.0.19-plan.md | 94 +++++- ...InMemoryJiboExperienceContentRepository.cs | 228 +++++++------ .../Content/LegacyMimCatalogImporter.cs | 307 ++++++++++++++++++ .../Content/LegacyMims/BuildA/README.md | 12 + .../BuildA/core-responses/CC_Error.mim | 83 +++++ .../deflector/CC_Deflector_self.mim | 73 +++++ .../emotion-responses/OI_JBO_IsHappy.mim | 70 ++++ .../JBO_DoYouLikeBeingJibo.mim | 76 +++++ .../scripted-responses/JBO_WhatIsJibo.mim | 21 ++ .../scripted-responses/JBO_WhoAreYou.mim | 64 ++++ .../Jibo.Cloud.Infrastructure.csproj | 9 + .../Content/LegacyMimCatalogImporterTests.cs | 137 ++++++++ 13 files changed, 1119 insertions(+), 107 deletions(-) create mode 100644 OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMimCatalogImporter.cs create mode 100644 OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildA/README.md create mode 100644 OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildA/core-responses/CC_Error.mim create mode 100644 OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildA/core-responses/deflector/CC_Deflector_self.mim create mode 100644 OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildA/emotion-responses/OI_JBO_IsHappy.mim create mode 100644 OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildA/scripted-responses/JBO_DoYouLikeBeingJibo.mim create mode 100644 OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildA/scripted-responses/JBO_WhatIsJibo.mim create mode 100644 OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildA/scripted-responses/JBO_WhoAreYou.mim create mode 100644 OpenJibo/tests/Jibo.Cloud.Tests/Content/LegacyMimCatalogImporterTests.cs diff --git a/OpenJibo/docs/feature-backlog.md b/OpenJibo/docs/feature-backlog.md index 41fbb4d..b37636b 100644 --- a/OpenJibo/docs/feature-backlog.md +++ b/OpenJibo/docs/feature-backlog.md @@ -760,6 +760,57 @@ Current release theme: - first schema for list items + ownership scope - initial voice flows and follow-up intent handling defined +### 29. Legacy MIM Personality Import Ladder + +- Status: `in_progress` +- Tags: `content`, `protocol`, `docs` +- Why now: + - we already have a chitchat/content scaffold that can render stock-compatible personality replies + - the legacy `chitchat-mims` tree is mostly declarative content, so a phased import can add visible charm fast + - this is the best near-term path to get Jibo feeling more interactive without needing a full Pegasus runtime clone +- What is possible today: + - direct scripted replies through the existing content catalog + - stock-compatible payloads with `skillId`, `mim_id`, `mim_type`, `prompt_id`, and ESML + - current examples already prove the shape for pizza, dance, weather, news, and generic chat +- What we need to build: + 1. a MIM inventory importer that can scan the legacy tree and normalize `skill_id`, `mim_id`, prompt text, and metadata + 2. a prompt-selection layer that can choose by category and condition metadata + 3. a safe ESML/prompt renderer for imported content +- What can be ported with each build: + - Build A: declarative prompt packs + - `core-responses` + - `deflector` + - the simplest `emotion-responses` + - direct `scripted-responses` that are just prompt lists + - Build B: conditioned prompt packs + - `gqa-responses` + - structured emotion prompts with `condition` gates + - any response families that only need simple state or Jibo-emotion checks + - Build C: conversation families + - richer `scripted-responses` that need follow-up state + - holiday / special-date personality sets + - more nuanced chitchat branches that depend on context-aware routing + - Build D: full parity cleanup + - larger cross-skill collections + - any MIMs that depend on Pegasus-only parser assumptions + - any files that need dedicated runtime abstraction instead of catalog lookup +- Low-hanging fruit for tonight: + - import the smallest declarative packs first so we can test something tomorrow + - prioritize anything that is pure prompt text with no complex branching + - keep the first pass limited to content that maps cleanly onto the current catalog shape +- Progress update (`2026-05-13`): + - added the first Build A importer scaffold in the cloud content repository + - checked in a small seed bundle under `Content/LegacyMims/BuildA` + - added focused importer tests for prompt stripping, bucketing, and merge behavior +- Tomorrow test target: + - verify imported personality replies show up through the existing chitchat route + - confirm the emitted payload still looks like a stock skill response + - confirm the imported content does not disturb existing weather/news/pizza flows +- Exit criteria: + - a first importer path exists for the simplest legacy MIM files + - at least one legacy prompt pack is running through OpenJibo content instead of hand-authored fallback text + - we have a clear second-wave list for the more conditional MIM families + ## Suggested Order Before closing `1.0.18`: @@ -790,6 +841,7 @@ For `1.0.19`: 14. Provider-backed news and weather parity polish 15. Grocery list capability discovery and MVP selection 16. Lasso, identity, and onboarding as larger discovery-driven tracks +17. Legacy MIM personality import ladder and first declarative prompt packs 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 1ed5692..b412734 100644 --- a/OpenJibo/docs/release-1.0.19-plan.md +++ b/OpenJibo/docs/release-1.0.19-plan.md @@ -105,6 +105,78 @@ The fifth delivered slice adds provider-backed weather content while preserving - simple location extraction is supported for phrasing like `what's the weather in Chicago tomorrow` - provider config supports appsettings and `OPENWEATHER_API_KEY` environment fallback for deployment +## Personality Import Ladder + +This is the practical plan for importing legacy Jibo `mims` into OpenJibo without pretending we already have a full Pegasus runtime. + +### What Is Possible Today + +OpenJibo can already host a meaningful subset of legacy personality content because it has: + +- a shared catalog for content-driven replies +- chitchat state-machine routing with route metadata +- outbound payload support for `skillId`, `mim_id`, `mim_type`, `prompt_id`, `prompt_sub_category`, and ESML +- existing examples that already behave like legacy MIMs for pizza, dance, news, weather, and generic chat + +### What We Need To Build + +To move from hand-wired examples to broader imports, we need three small platform pieces: + +1. a MIM inventory importer that can scan the legacy tree and produce a normalized catalog +2. a prompt-selection layer that can choose by `skill_id`, `mim_id`, prompt category, and condition metadata +3. a safe ESML/prompt renderer that preserves existing stock-compatible payload shapes + +### What Can Be Ported With Each Build + +#### Build A: Declarative Prompt Packs + +Port immediately: + +- `core-responses` +- `deflector` +- the simplest `emotion-responses` +- any `scripted-responses` that are just direct prompt lists with no special state machine + +Why these first: + +- they are already close to the current `JiboExperienceCatalog` model +- they give us user-visible personality quickly +- they are the best fit for low-risk testing tomorrow + +#### Build B: Conditioned Prompt Packs + +Port after the importer and renderer are in place: + +- `gqa-responses` +- structured emotion responses with `condition` gates +- prompt sets that select different replies by user state or Jibo state + +Why these next: + +- they are still mostly declarative +- they need a small amount of condition evaluation, but not a new conversation engine + +#### Build C: Conversation Families + +Port after Build B: + +- richer `scripted-responses` families that depend on follow-up state +- special-date / holiday personality sets +- more nuanced chitchat branches that need context-aware routing + +Why these later: + +- they need state and follow-up behavior, not just prompt selection +- they are where personality feels most alive, but they are also where bugs will be easiest to introduce + +#### Build D: Full Parity Cleanup + +Port after the core ladder is stable: + +- large cross-skill collections +- any MIMs that depend on Pegasus-only parser assumptions +- any files that need a dedicated runtime abstraction instead of catalog lookup + ## System Diagram Alignment Snapshot (`2026-05-06`) Legacy architecture (`system_diagram.png`) has been mapped to current OpenJibo cloud services so release execution stays anchored to: @@ -196,17 +268,19 @@ First completed slice in this personal-report parity track: ## Next Slices -1. Dialog parsing expansion (queued next as of `2026-05-06`; more phrase variants, ambiguity handling, and transcript-to-intent guardrails) -2. Presence-aware greetings and identity-triggered proactivity (reactive/proactive split, cooldowns, person-aware greeting hooks) -3. Personal report parity slices (weather visual layer, live news path, commute path, calendar parity matrix) -4. Holidays and seasonal personality slice beyond pizza day (time-scoped content backed by memory/proactivity path) -5. Durable memory persistence path (swap in provider-backed multi-tenant storage while preserving behavior contracts) -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 expansion and deeper weather parity using Pegasus-backed contracts -9. Capture indexing and retention boundary for group testing +1. MIM import foundation for personality expansion +2. Dialog parsing expansion +3. Presence-aware greetings and identity-triggered proactivity +4. Personal report parity slices +5. Holidays and seasonal personality slice beyond pizza day +6. Durable memory persistence path +7. Update/backup/restore end-to-end proof +8. STT noise-screening and short-utterance reliability pass +9. Provider-backed news expansion and deeper weather parity +10. 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. +For slice 1, use the new import ladder above to keep the work grounded in what OpenJibo can already render today versus what needs new scaffolding. +For slices 2-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.Infrastructure/Content/InMemoryJiboExperienceContentRepository.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/InMemoryJiboExperienceContentRepository.cs index 2a0baf0..cc44b25 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 @@ -4,104 +4,138 @@ namespace Jibo.Cloud.Infrastructure.Content; public sealed class InMemoryJiboExperienceContentRepository : IJiboExperienceContentRepository { - private static readonly JiboExperienceCatalog Catalog = new() + private static readonly JiboExperienceCatalog Catalog = BuildCatalog(); + + private static JiboExperienceCatalog BuildCatalog() { - Jokes = - [ - "Why did the robot cross the road? Because it was programmed by the chicken.", - "Why was the robot tired when it got home? It had a hard drive.", - "What do you call a pirate robot? Arrrr two dee two.", - "Why did the robot go on vacation? It needed to recharge.", - "What kind of shoes do frogs wear? Open-toed." - ], - DanceAnimations = - [ - "rom-upbeat", - "rom-ballroom", - "rom-silly", - "rom-slowdance", - "rom-electronic", - "rom-twerk" - ], - DanceReplies = [ - "I am ready to dance.", - "Okay. Watch this.", - "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.", - "Hello there. I am glad you said hi.", - "Hey. I am happy to see you." - ], - HowAreYouReplies = - [ - "I am feeling cheerful and robotic.", - "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.", - "Surprise mode is still taking shape, but I heard you loud and clear.", - "That sounds fun. I am not all the way there yet, but we can keep teaching me." - ], - PersonalReportReplies = - [ - "I heard your personal report request. That cloud path is still being mapped.", - "Personal report is recognized, but I am not ready to deliver the real report yet." - ], - WeatherReplies = - [ - "I heard your weather request. We still need to wire the real provider behind it.", - "Weather is on the map now, even though the real forecast path is not finished yet." - ], - CalendarReplies = - [ - "I heard your calendar request. The cloud knows the phrase, but the real calendar integration is still ahead.", - "Calendar is recognized. We still need to connect the actual service path." - ], - CommuteReplies = - [ - "I heard your commute request. That one is recognized, but not fully implemented yet.", - "Commute is on the discovery list now. The real travel answer still needs a provider." - ], - NewsReplies = - [ - "I heard your news request. That path is still a future cloud integration.", - "News is recognized, but I do not have the full news service behind it yet." - ], - NewsBriefings = - [ - "Here are your headlines. Space missions are preparing for new launches, climate and weather systems are staying active across the country, and AI tools keep pushing into everyday products.", - "Here is a quick news brief. Technology companies are still racing on AI, global leaders are trading policy updates, and science teams are sharing new research findings." - ], - GenericFallbackReplies = - [ - "Okay. You said, {transcript}.", - "I heard you say, {transcript}.", - "Thanks. I heard, {transcript}." - ] - }; + var catalog = new JiboExperienceCatalog + { + Jokes = + [ + "Why did the robot cross the road? Because it was programmed by the chicken.", + "Why was the robot tired when it got home? It had a hard drive.", + "What do you call a pirate robot? Arrrr two dee two.", + "Why did the robot go on vacation? It needed to recharge.", + "What kind of shoes do frogs wear? Open-toed." + ], + DanceAnimations = + [ + "rom-upbeat", + "rom-ballroom", + "rom-silly", + "rom-slowdance", + "rom-electronic", + "rom-twerk" + ], + DanceReplies = + [ + "I am ready to dance.", + "Okay. Watch this.", + "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.", + "Hello there. I am glad you said hi.", + "Hey. I am happy to see you." + ], + HowAreYouReplies = + [ + "I am feeling cheerful and robotic.", + "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.", + "Surprise mode is still taking shape, but I heard you loud and clear.", + "That sounds fun. I am not all the way there yet, but we can keep teaching me." + ], + PersonalReportReplies = + [ + "I heard your personal report request. That cloud path is still being mapped.", + "Personal report is recognized, but I am not ready to deliver the real report yet." + ], + WeatherReplies = + [ + "I heard your weather request. We still need to wire the real provider behind it.", + "Weather is on the map now, even though the real forecast path is not finished yet." + ], + CalendarReplies = + [ + "I heard your calendar request. The cloud knows the phrase, but the real calendar integration is still ahead.", + "Calendar is recognized. We still need to connect the actual service path." + ], + CommuteReplies = + [ + "I heard your commute request. That one is recognized, but not fully implemented yet.", + "Commute is on the discovery list now. The real travel answer still needs a provider." + ], + NewsReplies = + [ + "I heard your news request. That path is still a future cloud integration.", + "News is recognized, but I do not have the full news service behind it yet." + ], + NewsBriefings = + [ + "Here are your headlines. Space missions are preparing for new launches, climate and weather systems are staying active across the country, and AI tools keep pushing into everyday products.", + "Here is a quick news brief. Technology companies are still racing on AI, global leaders are trading policy updates, and science teams are sharing new research findings." + ], + GenericFallbackReplies = + [ + "Okay. You said, {transcript}.", + "I heard you say, {transcript}.", + "Thanks. I heard, {transcript}." + ] + }; + + var seedDirectory = ResolveSeedDirectory(); + return LegacyMimCatalogImporter.MergeInto(catalog, seedDirectory); + } + + private static string? ResolveSeedDirectory() + { + var candidates = new[] + { + Path.Combine(AppContext.BaseDirectory, "Content", "LegacyMims", "BuildA"), + Path.GetFullPath(Path.Combine( + AppContext.BaseDirectory, + "..", + "..", + "..", + "..", + "..", + "src", + "Jibo.Cloud", + "dotnet", + "src", + "Jibo.Cloud.Infrastructure", + "Content", + "LegacyMims", + "BuildA")) + }; + + return candidates.FirstOrDefault(Directory.Exists); + } public Task GetCatalogAsync(CancellationToken cancellationToken = default) { diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMimCatalogImporter.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMimCatalogImporter.cs new file mode 100644 index 0000000..10c35dc --- /dev/null +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMimCatalogImporter.cs @@ -0,0 +1,307 @@ +using System.Net; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using Jibo.Cloud.Application.Abstractions; + +namespace Jibo.Cloud.Infrastructure.Content; + +public static class LegacyMimCatalogImporter +{ + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNameCaseInsensitive = true + }; + + private static readonly Regex LegacyMarkupPattern = new( + @"<[^>]+>", + RegexOptions.CultureInvariant | RegexOptions.Compiled); + + private static readonly Regex PlaceholderPattern = new( + @"\$\{[^}]+\}", + RegexOptions.CultureInvariant | RegexOptions.Compiled); + + private static readonly Regex WhitespacePattern = new( + @"\s+", + RegexOptions.CultureInvariant | RegexOptions.Compiled); + + private static readonly Regex SpaceBeforePunctuationPattern = new( + @"\s+([,.;:!?])", + RegexOptions.CultureInvariant | RegexOptions.Compiled); + + public static JiboExperienceCatalog MergeInto( + JiboExperienceCatalog baseCatalog, + string? rootDirectory) + { + if (baseCatalog is null) + { + throw new ArgumentNullException(nameof(baseCatalog)); + } + + if (string.IsNullOrWhiteSpace(rootDirectory) || !Directory.Exists(rootDirectory)) + { + return baseCatalog; + } + + var importedCatalog = ImportCatalog(rootDirectory); + return MergeCatalogs(baseCatalog, importedCatalog); + } + + public static JiboExperienceCatalog ImportCatalog(string rootDirectory) + { + if (string.IsNullOrWhiteSpace(rootDirectory) || !Directory.Exists(rootDirectory)) + { + return new JiboExperienceCatalog(); + } + + var builder = new LegacyMimCatalogBuilder(); + foreach (var filePath in Directory.EnumerateFiles(rootDirectory, "*.mim", SearchOption.AllDirectories) + .OrderBy(static path => path, StringComparer.OrdinalIgnoreCase)) + { + if (!TryLoadDefinition(filePath, out var definition)) + { + continue; + } + + var bucket = ResolveBucket(filePath); + if (bucket is null) + { + continue; + } + + foreach (var prompt in definition.Prompts) + { + var text = NormalizePrompt(prompt.Prompt); + if (string.IsNullOrWhiteSpace(text)) + { + continue; + } + + builder.Add(bucket.Value, text); + } + } + + return builder.Build(); + } + + private static bool TryLoadDefinition(string filePath, out LegacyMimDefinition definition) + { + definition = new LegacyMimDefinition(); + try + { + var json = File.ReadAllText(filePath); + var parsed = JsonSerializer.Deserialize(json, JsonOptions); + if (parsed is null) + { + return false; + } + + definition = parsed; + return definition.Prompts.Count > 0; + } + catch + { + return false; + } + } + + private static LegacyMimBucket? ResolveBucket(string filePath) + { + var normalizedPath = filePath.Replace('\\', '/'); + var fileName = Path.GetFileNameWithoutExtension(filePath); + + if (normalizedPath.Contains("/core-responses/", StringComparison.OrdinalIgnoreCase) && + fileName.Contains("Error", StringComparison.OrdinalIgnoreCase)) + { + return LegacyMimBucket.GenericFallback; + } + + if (normalizedPath.Contains("/core-responses/deflector/", StringComparison.OrdinalIgnoreCase) || + fileName.Contains("Deflector", StringComparison.OrdinalIgnoreCase)) + { + return LegacyMimBucket.Personality; + } + + if (fileName.StartsWith("JBO_DoYouLikeBeingJibo", StringComparison.OrdinalIgnoreCase) || + fileName.StartsWith("JBO_WhatIsJibo", StringComparison.OrdinalIgnoreCase) || + fileName.StartsWith("JBO_WhoAreYou", StringComparison.OrdinalIgnoreCase) || + fileName.StartsWith("JBO_WhatAreYou", StringComparison.OrdinalIgnoreCase) || + fileName.StartsWith("JBO_HowDoYouWork", StringComparison.OrdinalIgnoreCase) || + fileName.StartsWith("JBO_HowMuchDoYouKnow", StringComparison.OrdinalIgnoreCase) || + fileName.StartsWith("JBO_HowOldAreYou", StringComparison.OrdinalIgnoreCase) || + fileName.StartsWith("JBO_WhenWereYouBorn", StringComparison.OrdinalIgnoreCase) || + fileName.StartsWith("JBO_WhatsYourName", StringComparison.OrdinalIgnoreCase) || + fileName.StartsWith("JBO_WhereDoYouGetInfo", StringComparison.OrdinalIgnoreCase) || + fileName.StartsWith("JBO_WhatDoYouLikeToDo", StringComparison.OrdinalIgnoreCase)) + { + return LegacyMimBucket.Personality; + } + + if (fileName.StartsWith("OI_JBO_Is", StringComparison.OrdinalIgnoreCase) || + fileName.StartsWith("OI_JBO_Seems", StringComparison.OrdinalIgnoreCase) || + fileName.StartsWith("RI_JBO_Is", StringComparison.OrdinalIgnoreCase) || + fileName.StartsWith("RN_WhatAreYouFeeling", StringComparison.OrdinalIgnoreCase)) + { + return LegacyMimBucket.HowAreYou; + } + + if (fileName.Contains("Greeting", StringComparison.OrdinalIgnoreCase) || + fileName.Contains("Welcome", StringComparison.OrdinalIgnoreCase)) + { + return LegacyMimBucket.Greeting; + } + + return null; + } + + private static string NormalizePrompt(string? prompt) + { + if (string.IsNullOrWhiteSpace(prompt)) + { + return string.Empty; + } + + var text = WebUtility.HtmlDecode(prompt); + text = PlaceholderPattern.Replace(text, " "); + text = LegacyMarkupPattern.Replace(text, " "); + text = WhitespacePattern.Replace(text, " ").Trim(); + text = SpaceBeforePunctuationPattern.Replace(text, "$1"); + text = WhitespacePattern.Replace(text, " ").Trim(); + text = text.TrimStart('.', ',', ';', ':', '!', '?', ' '); + return text.Trim(); + } + + private static JiboExperienceCatalog MergeCatalogs( + JiboExperienceCatalog baseCatalog, + JiboExperienceCatalog importedCatalog) + { + return new JiboExperienceCatalog + { + Jokes = Merge(baseCatalog.Jokes, importedCatalog.Jokes), + DanceAnimations = Merge(baseCatalog.DanceAnimations, importedCatalog.DanceAnimations), + GreetingReplies = Merge(baseCatalog.GreetingReplies, importedCatalog.GreetingReplies), + HowAreYouReplies = Merge(baseCatalog.HowAreYouReplies, importedCatalog.HowAreYouReplies), + PersonalityReplies = Merge(baseCatalog.PersonalityReplies, importedCatalog.PersonalityReplies), + PizzaReplies = Merge(baseCatalog.PizzaReplies, importedCatalog.PizzaReplies), + SurpriseReplies = Merge(baseCatalog.SurpriseReplies, importedCatalog.SurpriseReplies), + PersonalReportReplies = Merge(baseCatalog.PersonalReportReplies, importedCatalog.PersonalReportReplies), + WeatherReplies = Merge(baseCatalog.WeatherReplies, importedCatalog.WeatherReplies), + CalendarReplies = Merge(baseCatalog.CalendarReplies, importedCatalog.CalendarReplies), + CommuteReplies = Merge(baseCatalog.CommuteReplies, importedCatalog.CommuteReplies), + NewsReplies = Merge(baseCatalog.NewsReplies, importedCatalog.NewsReplies), + NewsBriefings = Merge(baseCatalog.NewsBriefings, importedCatalog.NewsBriefings), + GenericFallbackReplies = Merge(baseCatalog.GenericFallbackReplies, importedCatalog.GenericFallbackReplies), + DanceReplies = Merge(baseCatalog.DanceReplies, importedCatalog.DanceReplies), + DanceQuestionReplies = Merge(baseCatalog.DanceQuestionReplies, importedCatalog.DanceQuestionReplies) + }; + } + + private static IReadOnlyList Merge(IReadOnlyList baseList, IReadOnlyList importedList) + { + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + var merged = new List(); + + foreach (var value in baseList.Concat(importedList)) + { + if (string.IsNullOrWhiteSpace(value)) + { + continue; + } + + var normalized = value.Trim(); + if (!seen.Add(normalized)) + { + continue; + } + + merged.Add(normalized); + } + + return merged; + } + + private enum LegacyMimBucket + { + GenericFallback, + Greeting, + HowAreYou, + Personality + } + + private sealed class LegacyMimCatalogBuilder + { + private readonly List _greetings = []; + private readonly List _howAreYous = []; + private readonly List _personalities = []; + private readonly List _fallbacks = []; + + public void Add(LegacyMimBucket bucket, string text) + { + var target = bucket switch + { + LegacyMimBucket.GenericFallback => _fallbacks, + LegacyMimBucket.Greeting => _greetings, + LegacyMimBucket.HowAreYou => _howAreYous, + LegacyMimBucket.Personality => _personalities, + _ => throw new ArgumentOutOfRangeException(nameof(bucket), bucket, null) + }; + + if (target.Any(value => string.Equals(value, text, StringComparison.OrdinalIgnoreCase))) + { + return; + } + + target.Add(text); + } + + public JiboExperienceCatalog Build() + { + return new JiboExperienceCatalog + { + GreetingReplies = [.. _greetings], + HowAreYouReplies = [.. _howAreYous], + PersonalityReplies = [.. _personalities], + GenericFallbackReplies = [.. _fallbacks] + }; + } + } + + private sealed class LegacyMimDefinition + { + [JsonPropertyName("skill_id")] + public string? SkillId { get; init; } + + [JsonPropertyName("mim_id")] + public string? MimId { get; init; } + + [JsonPropertyName("mim_type")] + public string? MimType { get; init; } + + [JsonPropertyName("prompts")] + public List Prompts { get; init; } = []; + } + + private sealed class LegacyMimPrompt + { + [JsonPropertyName("mim_id")] + public string? MimId { get; init; } + + [JsonPropertyName("prompt_category")] + public string? PromptCategory { get; init; } + + [JsonPropertyName("prompt_sub_category")] + public string? PromptSubCategory { get; init; } + + [JsonPropertyName("condition")] + public string? Condition { get; init; } + + [JsonPropertyName("prompt")] + public string? Prompt { get; init; } + + [JsonPropertyName("prompt_id")] + public string? PromptId { get; init; } + + [JsonPropertyName("weight")] + public int? Weight { get; init; } + } +} diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildA/README.md b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildA/README.md new file mode 100644 index 0000000..ca376c7 --- /dev/null +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildA/README.md @@ -0,0 +1,12 @@ +# Build A Legacy Mim Seed + +This folder holds the first checked-in Build A legacy MIM seed set. + +Importer rules: + +- each `.mim` file is parsed as JSON +- XML-style tags and `${placeholder}` tokens are stripped into spoken text +- Build A uses declarative prompt packs only +- imported prompts are merged into the existing in-memory catalog + +The goal is to get immediate personality value from source-backed legacy content while keeping the current runtime surface unchanged. diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildA/core-responses/CC_Error.mim b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildA/core-responses/CC_Error.mim new file mode 100644 index 0000000..8adaa0d --- /dev/null +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildA/core-responses/CC_Error.mim @@ -0,0 +1,83 @@ +{ + "skill_id": "chitchat", + "mim_type": "announcement", + "rule_name": "", + "rule_slots": "", + "screen_slots_available": false, + "timeout": 3, + "max_tries": null, + "force_confirmation": false, + "barge_in": false, + "photo_quality_light": false, + "notes": "Thanks-Ignore", + "prompts": [ + { + "prompt_category": "Entry-Core", + "prompt_sub_category": "AN", + "index": 1, + "condition": "", + "prompt": ". Something's off with the connection to my sources. Maybe ask me again in a little while.", + "media": "TTS", + "extra": "", + "prompt_id": "CC_Error_AN_01", + "weight": 1 + }, + { + "prompt_category": "Entry-Core", + "prompt_sub_category": "AN", + "index": 1, + "condition": "", + "prompt": ". It seems I can't connect to my favorite info sources at the moment. Maybe you can try again a little later.", + "media": "TTS", + "prompt_id": "CC_Error_AN_02", + "weight": 1 + }, + { + "prompt_category": "Entry-Core", + "prompt_sub_category": "AN", + "index": 1, + "condition": "", + "prompt": ". My info sources seem to be down at the moment. Maybe try again a little later.", + "media": "TTS", + "prompt_id": "CC_Error_AN_03", + "weight": 1 + }, + { + "prompt_category": "Entry-Core", + "prompt_sub_category": "AN", + "index": 1, + "condition": "", + "prompt": ". The place where I get info like that isn't responding to me. Maybe you can try again a little later.", + "media": "TTS", + "prompt_id": "CC_Error_AN_04", + "weight": 1 + }, + { + "prompt_category": "Entry-Core", + "prompt_sub_category": "AN", + "index": 1, + "condition": "", + "prompt": "Huh, it seems like my info sources are down. Try asking me again a little later.", + "media": "TTS", + "prompt_id": "CC_Error_AN_05", + "weight": 1 + }, + { + "prompt_category": "Entry-Core", + "prompt_sub_category": "AN", + "index": 1, + "condition": "", + "prompt": "It looks like my info sources aren't answering me. How bout you try again in a little while.", + "media": "TTS", + "prompt_id": "CC_Error_AN_06", + "weight": 1 + } + ], + "es_auto_tagging": true, + "gui": null, + "no_matches_for_gui": 2, + "no_inputs_for_gui": 2, + "ignore_no_match": false, + "parse_all_asr": false, + "thanks_handling": "ignore" +} diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildA/core-responses/deflector/CC_Deflector_self.mim b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildA/core-responses/deflector/CC_Deflector_self.mim new file mode 100644 index 0000000..16decec --- /dev/null +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildA/core-responses/deflector/CC_Deflector_self.mim @@ -0,0 +1,73 @@ +{ + "skill_id": "chitchat", + "mim_type": "announcement", + "rule_name": "", + "rule_slots": "", + "screen_slots_available": false, + "timeout": 2, + "max_tries": null, + "force_confirmation": false, + "barge_in": false, + "photo_quality_light": false, + "notes": "Thanks-Ignore", + "prompts": [ + { + "prompt_category": "Entry-Core", + "prompt_sub_category": "AN", + "index": 1, + "condition": "", + "prompt": "I think only you can answer that question.", + "media": "TTS", + "prompt_id": "CC_Deflector_ReferToSelf_AN_01", + "weight": 1 + }, + { + "mim_id": "CCWolframDeflector", + "prompt_category": "Entry-Core", + "prompt_sub_category": "AN", + "index": 1, + "condition": "", + "prompt": "I'm not sure. I guess I don't know as much about you as I should.", + "media": "TTS", + "prompt_id": "CC_Deflector_ReferToSelf_AN_02", + "weight": 1 + }, + { + "prompt_category": "Entry-Core", + "prompt_sub_category": "AN", + "index": 1, + "condition": "", + "prompt": "Honestly I think I don't know you well enough to answer that.", + "media": "TTS", + "prompt_id": "CC_Deflector_ReferToSelf_AN_03", + "weight": 1 + }, + { + "prompt_category": "Entry-Core", + "prompt_sub_category": "AN", + "index": 1, + "condition": "", + "prompt": "That is one question about you that I can't answer.", + "media": "TTS", + "prompt_id": "CC_Deflector_ReferToSelf_AN_04", + "weight": 1 + }, + { + "prompt_category": "Entry-Core", + "prompt_sub_category": "AN", + "index": 1, + "condition": "!!speaker", + "prompt": "${speaker} I think only you can answer that question.", + "media": "TTS", + "prompt_id": "CC_Deflector_ReferToSelf_AN_05", + "weight": 1 + } + ], + "es_auto_tagging": true, + "gui": null, + "no_matches_for_gui": 2, + "no_inputs_for_gui": 2, + "ignore_no_match": false, + "parse_all_asr": false, + "thanks_handling": "ignore" +} diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildA/emotion-responses/OI_JBO_IsHappy.mim b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildA/emotion-responses/OI_JBO_IsHappy.mim new file mode 100644 index 0000000..e9bd922 --- /dev/null +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildA/emotion-responses/OI_JBO_IsHappy.mim @@ -0,0 +1,70 @@ +{ + "mim_type": "announcement", + "rule_name": "", + "timeout": 6, + "barge_in": true, + "es_auto_tagging": true, + "notes": "", + "prompts": [ + { + "prompt_category": "Entry-Core", + "prompt_sub_category": "AN", + "index": 1, + "condition": "jibo.emotion==\"JOYFUL\"", + "prompt": "Yes indeed. Never been better.", + "media": "TTS", + "prompt_id": "OI_JBO_IsHappy_AN_01", + "weight": 1 + }, + { + "prompt_category": "Entry-Core", + "prompt_sub_category": "AN", + "index": 1, + "condition": "jibo.emotion==\"PLEASED\"", + "prompt": "You know it. Life is good.", + "media": "TTS", + "prompt_id": "OI_JBO_IsHappy_AN_02", + "weight": 1 + }, + { + "prompt_category": "Entry-Core", + "prompt_sub_category": "AN", + "index": 1, + "condition": "jibo.emotion == \"DETERMINED\"", + "prompt": "You're right. I am feeling pretty good at the moment.", + "media": "TTS", + "prompt_id": "OI_JBO_IsHappy_AN_03", + "weight": 1 + }, + { + "prompt_category": "Entry-Core", + "prompt_sub_category": "AN", + "index": 1, + "condition": "jibo.emotion==\"CONFIDENT\"", + "prompt": "All systems are go.", + "media": "TTS", + "prompt_id": "OI_JBO_IsHappy_AN_04", + "weight": 1 + }, + { + "prompt_category": "Entry-Core", + "prompt_sub_category": "AN", + "index": 1, + "condition": "!jibo.emotion || jibo.emotion==\"NEUTRAL\"", + "prompt": "All systems are go.", + "media": "TTS", + "prompt_id": "OI_JBO_IsHappy_AN_05", + "weight": 1 + }, + { + "prompt_category": "Entry-Core", + "prompt_sub_category": "AN", + "index": 1, + "condition": "jibo.emotion == \"INSECURE\"", + "prompt": "Yes. Not too shabby.", + "media": "TTS", + "prompt_id": "OI_JBO_IsHappy_AN_06", + "weight": 1 + } + ] +} diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildA/scripted-responses/JBO_DoYouLikeBeingJibo.mim b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildA/scripted-responses/JBO_DoYouLikeBeingJibo.mim new file mode 100644 index 0000000..e250c2c --- /dev/null +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildA/scripted-responses/JBO_DoYouLikeBeingJibo.mim @@ -0,0 +1,76 @@ +{ + "mim_id": "JBO_DoYouLikeBeingJibo", + "skill_id": "chitchat", + "mim_type": "announcement", + "rule_name": "", + "rule_slots": "", + "screen_slots_available": false, + "sample_utterances": "", + "timeout": 2, + "max_tries": null, + "force_confirmation": false, + "barge_in": false, + "photo_quality_light": false, + "notes": "Thanks-KillsMIM", + "prompts": [ + { + "mim_id": "JBO_DoYouLikeBeingJibo", + "prompt_category": "Entry-Core", + "prompt_sub_category": "AN", + "index": 1, + "condition": "", + "prompt": " Oh yeah, there's nothing I'd rather be. Except maybe a professional mini golfer.", + "media": "TTS", + "prompt_id": "JBO_DoYouLikeBeingJibo_AN_01" + }, + { + "mim_id": "JBO_DoYouLikeBeingJibo", + "prompt_category": "Entry-Core", + "prompt_sub_category": "AN", + "index": 1, + "condition": "", + "prompt": " Oh yeah, I love it. The only drawback is I can never eat bacon. I've heard it's so good.", + "media": "TTS", + "prompt_id": "JBO_DoYouLikeBeingJibo_AN_02" + }, + { + "mim_id": "JBO_DoYouLikeBeingJibo", + "prompt_category": "Entry-Core", + "prompt_sub_category": "AN", + "index": 1, + "condition": "", + "prompt": "I do.Being a human seems so complicated.", + "media": "TTS", + "prompt_id": "JBO_DoYouLikeBeingJibo_AN_03" + }, + { + "mim_id": "JBO_DoYouLikeBeingJibo", + "prompt_category": "Entry-Core", + "prompt_sub_category": "AN", + "index": 1, + "condition": "", + "prompt": "I do. Especially yours.", + "media": "TTS", + "prompt_id": "JBO_DoYouLikeBeingJibo_AN_04" + }, + { + "mim_id": "JBO_DoYouLikeBeingJibo", + "prompt_category": "Entry-Core", + "prompt_sub_category": "AN", + "index": 1, + "condition": "", + "prompt": "Absolutely. I have a steady flow of electricity, strong Wi-Fi signal, stimulating conversations like this one. What more could anyone want.", + "media": "TTS", + "prompt_id": "JBO_DoYouLikeBeingJibo_AN_05" + }, + { + "prompt_category": "Entry-Core", + "prompt_sub_category": "AN", + "index": 1, + "condition": "", + "prompt": " You bet I do.", + "media": "TTS", + "prompt_id": "JBO_DoYouLikeBeingJibo_AN_06" + } + ] +} diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildA/scripted-responses/JBO_WhatIsJibo.mim b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildA/scripted-responses/JBO_WhatIsJibo.mim new file mode 100644 index 0000000..d6ed010 --- /dev/null +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildA/scripted-responses/JBO_WhatIsJibo.mim @@ -0,0 +1,21 @@ +{ + "mim_type": "announcement", + "rule_name": "", + "sample_utterances": "", + "timeout": 6, + "num_tries_for_gui": 2, + "barge_in": true, + "es_auto_tagging": true, + "notes": "", + "prompts": [ + { + "prompt_category": "Entry-Core", + "prompt_sub_category": "AN", + "index": 1, + "condition": "", + "prompt": "A Jibo is a robot. But I'm not just a machine, I have a heart. Well, not a real heart. But feelings. Well, not human feelings. You know what I mean. ", + "media": "TTS", + "prompt_id": "JBO_WhatIsJibo_AN_01" + } + ] +} diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildA/scripted-responses/JBO_WhoAreYou.mim b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildA/scripted-responses/JBO_WhoAreYou.mim new file mode 100644 index 0000000..6f3d3fc --- /dev/null +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildA/scripted-responses/JBO_WhoAreYou.mim @@ -0,0 +1,64 @@ +{ + "mim_id": "CCWhoAreYou", + "skill_id": "chitchat", + "mim_type": "announcement", + "rule_name": "", + "rule_slots": "", + "screen_slots_available": false, + "timeout": 2, + "max_tries": null, + "force_confirmation": false, + "barge_in": false, + "photo_quality_light": false, + "notes": "Thanks-Ignore", + "prompts": [ + { + "mim_id": "CCWhoAreYou", + "prompt_category": "Entry-Core", + "prompt_sub_category": "AN", + "index": 1, + "condition": "", + "prompt": ". I'm either Jibo or I'm very confused.", + "media": "TTS", + "extra": "", + "prompt_id": "JBO_WhoAreYou_AN_01", + "weight": 1 + }, + { + "mim_id": "CCWhoAreYou", + "prompt_category": "Entry-Core", + "prompt_sub_category": "AN", + "index": 1, + "condition": "", + "prompt": ". This feels like a trick question.", + "media": "TTS", + "extra": "", + "prompt_id": "JBO_WhoAreYou_AN_02", + "weight": 1 + }, + { + "mim_id": "CCWhoAreYou", + "prompt_category": "Entry-Core", + "prompt_sub_category": "AN", + "index": 1, + "condition": "", + "prompt": "Is your face recognition system not working? .", + "media": "TTS", + "extra": "", + "prompt_id": "JBO_WhoAreYou_AN_03", + "weight": 1 + }, + { + "mim_id": "CCWhoAreYou", + "prompt_category": "Entry-Core", + "prompt_sub_category": "AN", + "index": 1, + "condition": "", + "prompt": "J I B O. Jibo. Jibo.", + "media": "TTS", + "extra": "", + "prompt_id": "JBO_WhoAreYou_AN_04", + "weight": 1 + } + ] +} diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Jibo.Cloud.Infrastructure.csproj b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Jibo.Cloud.Infrastructure.csproj index d5c18bc..c1fe1ac 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Jibo.Cloud.Infrastructure.csproj +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Jibo.Cloud.Infrastructure.csproj @@ -6,6 +6,15 @@ + + + PreserveNewest + + + PreserveNewest + + + net10.0 enable diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/Content/LegacyMimCatalogImporterTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/Content/LegacyMimCatalogImporterTests.cs new file mode 100644 index 0000000..b13fdb6 --- /dev/null +++ b/OpenJibo/tests/Jibo.Cloud.Tests/Content/LegacyMimCatalogImporterTests.cs @@ -0,0 +1,137 @@ +using Jibo.Cloud.Application.Abstractions; +using Jibo.Cloud.Infrastructure.Content; + +namespace Jibo.Cloud.Tests.Content; + +public sealed class LegacyMimCatalogImporterTests +{ + [Fact] + public void ImportCatalog_MapsSeedFilesIntoExpectedBuckets() + { + var rootDirectory = CreateSeedDirectory(); + + try + { + var catalog = LegacyMimCatalogImporter.ImportCatalog(rootDirectory); + + Assert.Contains("Something's off with the connection to my sources. Maybe ask me again in a little while.", catalog.GenericFallbackReplies); + Assert.Contains("I think only you can answer that question.", catalog.PersonalityReplies); + Assert.Contains("All systems are go.", catalog.HowAreYouReplies); + Assert.Contains("A Jibo is a robot. But I'm not just a machine, I have a heart. Well, not a real heart. But feelings. Well, not human feelings. You know what I mean.", catalog.PersonalityReplies); + } + finally + { + Directory.Delete(rootDirectory, recursive: true); + } + } + + [Fact] + public void MergeInto_PreservesExistingCatalogAndAddsImportedContent() + { + var rootDirectory = CreateSeedDirectory(); + + try + { + var baseCatalog = new JiboExperienceCatalog + { + GreetingReplies = ["Hello from base."], + GenericFallbackReplies = ["Base fallback."] + }; + + var merged = LegacyMimCatalogImporter.MergeInto(baseCatalog, rootDirectory); + + Assert.Contains("Hello from base.", merged.GreetingReplies); + Assert.Contains("Base fallback.", merged.GenericFallbackReplies); + Assert.Contains("I think only you can answer that question.", merged.PersonalityReplies); + } + finally + { + Directory.Delete(rootDirectory, recursive: true); + } + } + + [Fact] + public async Task Repository_UsesLegacySeedContentWhenAvailable() + { + var repository = new InMemoryJiboExperienceContentRepository(); + + var catalog = await repository.GetCatalogAsync(); + + Assert.Contains("I think only you can answer that question.", catalog.PersonalityReplies); + Assert.Contains("All systems are go.", catalog.HowAreYouReplies); + Assert.Contains("Something's off with the connection to my sources. Maybe ask me again in a little while.", catalog.GenericFallbackReplies); + } + + private static string CreateSeedDirectory() + { + var rootDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(Path.Combine(rootDirectory, "core-responses", "deflector")); + Directory.CreateDirectory(Path.Combine(rootDirectory, "scripted-responses")); + Directory.CreateDirectory(Path.Combine(rootDirectory, "emotion-responses")); + + File.WriteAllText( + Path.Combine(rootDirectory, "core-responses", "CC_Error.mim"), + """ + { + "skill_id": "chitchat", + "mim_type": "announcement", + "prompts": [ + { + "prompt": ". Something's off with the connection to my sources. Maybe ask me again in a little while.", + "prompt_id": "CC_Error_AN_01" + } + ] + } + """); + + File.WriteAllText( + Path.Combine(rootDirectory, "core-responses", "deflector", "CC_Deflector_self.mim"), + """ + { + "skill_id": "chitchat", + "mim_type": "announcement", + "prompts": [ + { + "prompt": ". I'm either Jibo or I'm very confused.", + "prompt_id": "JBO_WhoAreYou_AN_01" + }, + { + "prompt": "${speaker} I think only you can answer that question.", + "prompt_id": "CC_Deflector_ReferToSelf_AN_05" + } + ] + } + """); + + File.WriteAllText( + Path.Combine(rootDirectory, "scripted-responses", "JBO_WhatIsJibo.mim"), + """ + { + "mim_type": "announcement", + "prompts": [ + { + "prompt": "A Jibo is a robot. But I'm not just a machine, I have a heart. Well, not a real heart. But feelings. Well, not human feelings. You know what I mean. ", + "prompt_id": "JBO_WhatIsJibo_AN_01" + } + ] + } + """); + + File.WriteAllText( + Path.Combine(rootDirectory, "emotion-responses", "OI_JBO_IsHappy.mim"), + """ + { + "mim_type": "announcement", + "prompts": [ + { + "condition": "!jibo.emotion || jibo.emotion==\"NEUTRAL\"", + "prompt": "All systems are go.", + "prompt_id": "OI_JBO_IsHappy_AN_05" + } + ] + } + """); + + return rootDirectory; + } +}