From 3d016debe50cc7ca6b744741128951ac45866807 Mon Sep 17 00:00:00 2001 From: Jacob Dubin Date: Thu, 21 May 2026 15:12:34 -0500 Subject: [PATCH 01/16] Add low-signal short-turn screening --- OpenJibo/docs/feature-backlog.md | 1 + OpenJibo/docs/release-1.0.19-plan.md | 1 + .../WebSocketTurnFinalizationService.cs | 46 +++++++++++++++++-- .../WebSockets/JiboWebSocketServiceTests.cs | 31 ++++++++++++- 4 files changed, 75 insertions(+), 4 deletions(-) diff --git a/OpenJibo/docs/feature-backlog.md b/OpenJibo/docs/feature-backlog.md index d44a7ab..f195be3 100644 --- a/OpenJibo/docs/feature-backlog.md +++ b/OpenJibo/docs/feature-backlog.md @@ -975,6 +975,7 @@ For `1.0.19`: - next implementation pass should supply the real Azure Storage connection string / deployment wiring and validate the live round-trip in the storage account smoke test 10. Update, backup, and restore proof - implemented (update creation and backup creation now survive persisted reloads; restore is the persisted-state rehydration proof path, not a new cloud API) 11. STT upgrade and noise screening + - progress update (`2026-05-21`): added a low-signal short-turn screen in websocket finalization so filler-only fragments and stray single-token leftovers like `so command` get rejected before they can become bad turns, while preserving the existing yes/no and word-of-the-day short-turn flows 12. Hosted capture/storage plan / indexing for group testing 13. Binary-safe media storage / sync to cloud drive: OneDrive, Google Drive, Box, etc. 14. Provider-backed news and weather parity polish diff --git a/OpenJibo/docs/release-1.0.19-plan.md b/OpenJibo/docs/release-1.0.19-plan.md index 5ea25f8..32e4178 100644 --- a/OpenJibo/docs/release-1.0.19-plan.md +++ b/OpenJibo/docs/release-1.0.19-plan.md @@ -84,6 +84,7 @@ The goal is to port these in small batches, capture the source-backed phrasing w - the restore proof is the persisted-state rehydration path; do not scope it into a new hosted restore API until we have real device evidence - 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 +- the latest STT pass adds a websocket-side low-signal screen for filler-only and stray single-token leftovers while keeping yes/no and word-of-the-day turns intact ### 3. Pegasus-To-Cloud Platform Porting diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/WebSocketTurnFinalizationService.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/WebSocketTurnFinalizationService.cs index 16ab364..e703672 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/WebSocketTurnFinalizationService.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/WebSocketTurnFinalizationService.cs @@ -106,6 +106,38 @@ public sealed class WebSocketTurnFinalizationService( "honestly" ]; + private static readonly HashSet SingleTokenUsableTranscripts = new(StringComparer.Ordinal) + { + "joke", + "funny", + "dance", + "boogie", + "time", + "date", + "today", + "day", + "hello", + "hi", + "hey", + "weather", + "news", + "radio", + "stop", + "sleep", + "sing", + "help", + "yes", + "yeah", + "yep", + "yup", + "sure", + "ok", + "okay", + "no", + "nope", + "nah" + }; + private static readonly HashSet YesNoAffirmativeLeadTokens = new(StringComparer.Ordinal) { "yes", @@ -1117,8 +1149,6 @@ public sealed class WebSocketTurnFinalizationService( if (ChitchatStateMachine.IsLikelyEmotionUtterance(transcript)) return true; - if (transcript.Length >= 6) return true; - if (IsYesNoTurn(turn) && IsYesNoReplyTranscript(transcript)) return true; if (!string.IsNullOrWhiteSpace(pendingProactivityOffer) && @@ -1128,9 +1158,19 @@ public sealed class WebSocketTurnFinalizationService( if (listenRules.Any(rule => string.Equals(rule, "word-of-the-day/puzzle", StringComparison.OrdinalIgnoreCase))) return true; + if (IsLowSignalSingleTokenTranscript(transcript)) return false; + + if (transcript.Length >= 6) return true; + return transcript is "joke" or "dance" or "time" or "date" or "today" or "day" or "hello" or "hi" or "hey"; } + private static bool IsLowSignalSingleTokenTranscript(string transcript) + { + var tokens = transcript.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + return tokens.Length == 1 && !SingleTokenUsableTranscripts.Contains(tokens[0]); + } + private static bool IsYesNoTurn(TurnContext turn) { return ReadRules(turn, "listenRules") @@ -1942,4 +1982,4 @@ public sealed class WebSocketTurnFinalizationService( Affirmative = 1, Negative = 2 } -} \ No newline at end of file +} diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboWebSocketServiceTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboWebSocketServiceTests.cs index 43b98f9..cd23e5c 100644 --- a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboWebSocketServiceTests.cs +++ b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboWebSocketServiceTests.cs @@ -3801,6 +3801,35 @@ public sealed class JiboWebSocketServiceTests Assert.Null(session.LastTranscript); } + [Fact] + public async Task ClientAsr_FillerPlusGenericCommand_IsIgnoredAsLowSignalNoise() + { + await _service.HandleMessageAsync(new WebSocketMessageEnvelope + { + HostName = "neo-hub.jibo.com", + Path = "/listen", + Kind = "neo-hub-listen", + Token = "hub-low-signal-command-token", + Text = """{"type":"LISTEN","transID":"trans-low-signal-command","data":{"rules":["wake-word"]}}""" + }); + + var replies = await _service.HandleMessageAsync(new WebSocketMessageEnvelope + { + HostName = "neo-hub.jibo.com", + Path = "/listen", + Kind = "neo-hub-listen", + Token = "hub-low-signal-command-token", + Text = """{"type":"CLIENT_ASR","transID":"trans-low-signal-command","data":{"text":"so command"}}""" + }); + + Assert.Empty(replies); + + var session = _store.FindSessionByToken("hub-low-signal-command-token"); + Assert.NotNull(session); + Assert.Null(session.LastIntent); + Assert.Null(session.LastTranscript); + } + [Fact] public async Task BufferedAudio_WithSyntheticTranscriptHint_FinalizesThroughSttSeam() { @@ -5212,4 +5241,4 @@ public sealed class JiboWebSocketServiceTests return items[^1]; } } -} \ No newline at end of file +} From 791fe606124d73e7320599c660aed975166f396f Mon Sep 17 00:00:00 2001 From: Jacob Dubin Date: Thu, 21 May 2026 16:37:54 -0500 Subject: [PATCH 02/16] Add capture bundle helper for group testing --- OpenJibo/docs/feature-backlog.md | 1 + OpenJibo/docs/live-jibo-capture.md | 15 +++ OpenJibo/docs/release-1.0.19-plan.md | 1 + OpenJibo/scripts/cloud/New-CaptureBundle.ps1 | 101 +++++++++++++++++++ OpenJibo/scripts/cloud/README.md | 2 + 5 files changed, 120 insertions(+) create mode 100644 OpenJibo/scripts/cloud/New-CaptureBundle.ps1 diff --git a/OpenJibo/docs/feature-backlog.md b/OpenJibo/docs/feature-backlog.md index f195be3..ce35603 100644 --- a/OpenJibo/docs/feature-backlog.md +++ b/OpenJibo/docs/feature-backlog.md @@ -977,6 +977,7 @@ For `1.0.19`: 11. STT upgrade and noise screening - progress update (`2026-05-21`): added a low-signal short-turn screen in websocket finalization so filler-only fragments and stray single-token leftovers like `so command` get rejected before they can become bad turns, while preserving the existing yes/no and word-of-the-day short-turn flows 12. Hosted capture/storage plan / indexing for group testing + - progress update (`2026-05-21`): added a bundle helper so group testers can package raw capture trees, `capture-index.ndjson`, and exported fixtures into one zip handoff artifact 13. Binary-safe media storage / sync to cloud drive: OneDrive, Google Drive, Box, etc. 14. Provider-backed news and weather parity polish 15. Grocery list capability discovery and MVP selection diff --git a/OpenJibo/docs/live-jibo-capture.md b/OpenJibo/docs/live-jibo-capture.md index 8e0758a..544b8df 100644 --- a/OpenJibo/docs/live-jibo-capture.md +++ b/OpenJibo/docs/live-jibo-capture.md @@ -77,3 +77,18 @@ Useful helper scripts: - [scripts/cloud/get-websocket-capture-summary.sh](/OpenJibo/scripts/cloud/get-websocket-capture-summary.sh) - [scripts/cloud/import-websocket-capture-fixture.py](/OpenJibo/scripts/cloud/import-websocket-capture-fixture.py) - [live-jibo-test-runbook.md](/OpenJibo/docs/live-jibo-test-runbook.md) + +## Group Testing Handoff + +When you have a useful capture set and want to share it with another tester, bundle the capture root into a single zip so the raw events, capture index, and exported fixtures stay together. + +Recommended helper: + +- [scripts/cloud/New-CaptureBundle.ps1](/OpenJibo/scripts/cloud/New-CaptureBundle.ps1) + +The bundle includes: + +- `capture-index.ndjson` +- websocket and HTTP `*.events.ndjson` files +- exported `*.flow.json` fixtures +- a small `bundle-manifest.json` with file counts and source metadata diff --git a/OpenJibo/docs/release-1.0.19-plan.md b/OpenJibo/docs/release-1.0.19-plan.md index 32e4178..0e44685 100644 --- a/OpenJibo/docs/release-1.0.19-plan.md +++ b/OpenJibo/docs/release-1.0.19-plan.md @@ -85,6 +85,7 @@ The goal is to port these in small batches, capture the source-backed phrasing w - 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 - the latest STT pass adds a websocket-side low-signal screen for filler-only and stray single-token leftovers while keeping yes/no and word-of-the-day turns intact +- capture indexing and group-test handoff now have a bundle helper that packages raw event captures, the index manifest, and exported fixtures together for easier review/share flows ### 3. Pegasus-To-Cloud Platform Porting diff --git a/OpenJibo/scripts/cloud/New-CaptureBundle.ps1 b/OpenJibo/scripts/cloud/New-CaptureBundle.ps1 new file mode 100644 index 0000000..f66c6a3 --- /dev/null +++ b/OpenJibo/scripts/cloud/New-CaptureBundle.ps1 @@ -0,0 +1,101 @@ +param( + [string]$CaptureRoot = "..\..\captures", + [string]$BundleDirectory = "..\..\captures\bundles", + [string]$BundleName +) + +function Get-RelativePath { + param( + [Parameter(Mandatory = $true)] + [string]$BasePath, + [Parameter(Mandatory = $true)] + [string]$FullPath + ) + + $normalizedBase = [System.IO.Path]::GetFullPath($BasePath) + if (-not $normalizedBase.EndsWith([System.IO.Path]::DirectorySeparatorChar)) { + $normalizedBase = $normalizedBase + [System.IO.Path]::DirectorySeparatorChar + } + + $normalizedFull = [System.IO.Path]::GetFullPath($FullPath) + if (-not $normalizedFull.StartsWith($normalizedBase, [StringComparison]::OrdinalIgnoreCase)) { + throw "Path '$FullPath' is not under '$BasePath'." + } + + return $normalizedFull.Substring($normalizedBase.Length) +} + +$resolvedCaptureRoot = Resolve-Path -LiteralPath $CaptureRoot -ErrorAction Stop +$resolvedBundleDirectory = Resolve-Path -LiteralPath $BundleDirectory -ErrorAction SilentlyContinue +if (-not $resolvedBundleDirectory) { + $resolvedBundleDirectory = New-Item -ItemType Directory -Force -Path $BundleDirectory | Select-Object -ExpandProperty FullName +} +else { + $resolvedBundleDirectory = $resolvedBundleDirectory.Path +} + +if ([string]::IsNullOrWhiteSpace($BundleName)) { + $timestamp = Get-Date -Format "yyyyMMdd-HHmmss" + $BundleName = "capture-bundle-$timestamp" +} + +$stagingDirectory = Join-Path $resolvedBundleDirectory "$BundleName.staging" +$archivePath = Join-Path $resolvedBundleDirectory "$BundleName.zip" + +if (Test-Path -LiteralPath $stagingDirectory) { + Remove-Item -LiteralPath $stagingDirectory -Recurse -Force +} + +New-Item -ItemType Directory -Force -Path $stagingDirectory | Out-Null + +try { + $sourceFiles = Get-ChildItem -LiteralPath $resolvedCaptureRoot -Recurse -File | Where-Object { + $_.Name -eq "capture-index.ndjson" -or + $_.Name -like "*.events.ndjson" -or + $_.Name -like "*.flow.json" + } + + if (-not $sourceFiles) { + Write-Host "No capture files were found under $resolvedCaptureRoot" + exit 0 + } + + foreach ($file in $sourceFiles) { + $relativePath = Get-RelativePath -BasePath $resolvedCaptureRoot -FullPath $file.FullName + $destinationPath = Join-Path $stagingDirectory $relativePath + $destinationDirectory = Split-Path -Parent $destinationPath + + if (-not (Test-Path -LiteralPath $destinationDirectory)) { + New-Item -ItemType Directory -Force -Path $destinationDirectory | Out-Null + } + + Copy-Item -LiteralPath $file.FullName -Destination $destinationPath -Force + } + + $captureIndexFiles = @($sourceFiles | Where-Object { $_.Name -eq "capture-index.ndjson" }) + $eventFiles = @($sourceFiles | Where-Object { $_.Name -like "*.events.ndjson" }) + $fixtureFiles = @($sourceFiles | Where-Object { $_.Name -like "*.flow.json" }) + + $manifest = [ordered]@{ + createdUtc = (Get-Date).ToUniversalTime().ToString("O") + sourceRoot = $resolvedCaptureRoot + fileCount = $sourceFiles.Count + captureIndexCount = $captureIndexFiles.Count + eventFileCount = $eventFiles.Count + fixtureCount = $fixtureFiles.Count + } + + $manifest | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath (Join-Path $stagingDirectory "bundle-manifest.json") -Encoding utf8 + + if (Test-Path -LiteralPath $archivePath) { + Remove-Item -LiteralPath $archivePath -Force + } + + Compress-Archive -Path (Join-Path $stagingDirectory '*') -DestinationPath $archivePath -Force + Write-Host "Created capture bundle at $archivePath" +} +finally { + if (Test-Path -LiteralPath $stagingDirectory) { + Remove-Item -LiteralPath $stagingDirectory -Recurse -Force + } +} diff --git a/OpenJibo/scripts/cloud/README.md b/OpenJibo/scripts/cloud/README.md index f8b82b4..909128b 100644 --- a/OpenJibo/scripts/cloud/README.md +++ b/OpenJibo/scripts/cloud/README.md @@ -16,6 +16,8 @@ These scripts help exercise the new .NET hosted cloud locally. Runs a small readiness checklist before the first physical Jibo test against the .NET cloud. - `Import-WebSocketCaptureFixture.ps1` Sanitizes an exported websocket capture fixture and copies it into the checked-in websocket fixture set. +- `New-CaptureBundle.ps1` + Packages the capture root, capture index, and exported fixtures into a single zip bundle for group testing handoff. - `start-dotnet-with-node-cert.sh` Starts the .NET API on Linux using the same PEM certificate material already used by the Node server. - `invoke-live-jibo-prep.sh` From febceecab85495b977d9a72bd45d327b3992a718 Mon Sep 17 00:00:00 2001 From: Jacob Dubin Date: Thu, 21 May 2026 16:41:23 -0500 Subject: [PATCH 03/16] Add binary media manifest metadata --- .../Services/JiboCloudProtocolService.cs | 10 ++++- .../Media/AzureBlobMediaContentStore.cs | 11 ++++- .../Media/FileMediaContentStore.cs | 11 ++++- .../Protocol/JiboCloudProtocolServiceTests.cs | 44 ++++++++++++++++++- 4 files changed, 69 insertions(+), 7 deletions(-) diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboCloudProtocolService.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboCloudProtocolService.cs index a64747c..5d32034 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboCloudProtocolService.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboCloudProtocolService.cs @@ -1,4 +1,5 @@ using System.Text; +using System.Security.Cryptography; using System.Text.Json; using Jibo.Cloud.Application.Abstractions; using Jibo.Cloud.Domain.Models; @@ -343,10 +344,15 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore, IMedia var meta = ReadObject(body, "meta") ?? new Dictionary(StringComparer.OrdinalIgnoreCase); var contentType = ReadHeader(envelope, "Content-Type") ?? "application/octet-stream"; meta["contentType"] = contentType; + var bodyBytes = string.IsNullOrWhiteSpace(envelope.BodyText) + ? [] + : Encoding.UTF8.GetBytes(envelope.BodyText); + meta["contentLength"] = bodyBytes.Length; + meta["contentSha256"] = Convert.ToHexString(SHA256.HashData(bodyBytes)).ToLowerInvariant(); if (!string.IsNullOrWhiteSpace(envelope.BodyText)) meta["bodyText"] = envelope.BodyText; _mediaContentStore.StoreAsync(path, contentType, - string.IsNullOrWhiteSpace(envelope.BodyText) ? [] : Encoding.UTF8.GetBytes(envelope.BodyText), + bodyBytes, meta as IReadOnlyDictionary, CancellationToken.None).GetAwaiter().GetResult(); return ProtocolDispatchResult.Ok( @@ -743,4 +749,4 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore, IMedia return Task.FromResult(null); } } -} \ No newline at end of file +} diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Media/AzureBlobMediaContentStore.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Media/AzureBlobMediaContentStore.cs index abddb0a..b2304da 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Media/AzureBlobMediaContentStore.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Media/AzureBlobMediaContentStore.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using System.Security.Cryptography; using Azure.Storage.Blobs; using Jibo.Cloud.Application.Abstractions; @@ -31,11 +32,17 @@ internal sealed class AzureBlobMediaContentStore : IMediaContentStore var metaBlob = _containerClient.GetBlobClient($"{relative}.json"); await _containerClient.CreateIfNotExistsAsync(cancellationToken: cancellationToken); await contentBlob.UploadAsync(new MemoryStream(content), true, cancellationToken); + var manifestMeta = meta is null + ? new Dictionary(StringComparer.OrdinalIgnoreCase) + : new Dictionary(meta, StringComparer.OrdinalIgnoreCase); + manifestMeta["contentLength"] = content.Length; + manifestMeta["contentSha256"] = Convert.ToHexString(SHA256.HashData(content)).ToLowerInvariant(); + manifestMeta["storedUtc"] = DateTimeOffset.UtcNow; var payload = JsonSerializer.Serialize(new { path, contentType, - meta + meta = manifestMeta }, JsonOptions); await metaBlob.UploadAsync(BinaryData.FromString(payload), true, cancellationToken); } @@ -77,4 +84,4 @@ internal sealed class AzureBlobMediaContentStore : IMediaContentStore Meta = meta as IReadOnlyDictionary ?? new Dictionary(meta) }; } -} \ No newline at end of file +} diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Media/FileMediaContentStore.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Media/FileMediaContentStore.cs index 5ce97a9..a8cd896 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Media/FileMediaContentStore.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Media/FileMediaContentStore.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using System.Security.Cryptography; using Jibo.Cloud.Application.Abstractions; namespace Jibo.Cloud.Infrastructure.Media; @@ -29,11 +30,17 @@ internal sealed class FileMediaContentStore : IMediaContentStore Directory.CreateDirectory(Path.GetDirectoryName(contentPath)!); await File.WriteAllBytesAsync(contentPath, content, cancellationToken); + var manifestMeta = meta is null + ? new Dictionary(StringComparer.OrdinalIgnoreCase) + : new Dictionary(meta, StringComparer.OrdinalIgnoreCase); + manifestMeta["contentLength"] = content.Length; + manifestMeta["contentSha256"] = Convert.ToHexString(SHA256.HashData(content)).ToLowerInvariant(); + manifestMeta["storedUtc"] = DateTimeOffset.UtcNow; var payload = new { path, contentType, - meta + meta = manifestMeta }; await File.WriteAllTextAsync(metaPath, JsonSerializer.Serialize(payload, JsonOptions), cancellationToken); } @@ -79,4 +86,4 @@ internal sealed class FileMediaContentStore : IMediaContentStore Meta = meta as IReadOnlyDictionary ?? new Dictionary(meta) }; } -} \ No newline at end of file +} diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/Protocol/JiboCloudProtocolServiceTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/Protocol/JiboCloudProtocolServiceTests.cs index 3e13553..ff28a7c 100644 --- a/OpenJibo/tests/Jibo.Cloud.Tests/Protocol/JiboCloudProtocolServiceTests.cs +++ b/OpenJibo/tests/Jibo.Cloud.Tests/Protocol/JiboCloudProtocolServiceTests.cs @@ -1,3 +1,5 @@ +using System.Security.Cryptography; +using System.Text; using System.Text.Json; using Jibo.Cloud.Application.Services; using Jibo.Cloud.Domain.Models; @@ -420,6 +422,46 @@ public sealed class JiboCloudProtocolServiceTests Assert.Equal("binary-photo-placeholder", mediaGet.BodyText); } + [Fact] + public async Task MediaCreate_WritesBinaryManifestMetadataForSync() + { + var directoryPath = Path.Combine(Path.GetTempPath(), "OpenJibo.Media.Tests", Guid.NewGuid().ToString("N")); + var service = new JiboCloudProtocolService(new InMemoryCloudStateStore(), + new FileMediaContentStore(directoryPath)); + const string bodyText = "binary-photo-placeholder"; + + var result = await service.DispatchAsync(new ProtocolEnvelope + { + HostName = "api.jibo.com", + Method = "POST", + ServicePrefix = "Media_20160725", + Operation = "Create", + Headers = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["Content-Type"] = "image/jpeg", + ["x-path"] = "photo-blob-manifest", + ["x-type"] = "image" + }, + BodyText = bodyText + }); + + using var createdPayload = JsonDocument.Parse(result.BodyText); + var meta = createdPayload.RootElement.GetProperty("meta"); + Assert.Equal(bodyText.Length, meta.GetProperty("contentLength").GetInt32()); + Assert.Equal( + Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(bodyText))).ToLowerInvariant(), + meta.GetProperty("contentSha256").GetString()); + + var metaPath = Path.Combine(directoryPath, "photo-blob-manifest.json"); + using var manifest = JsonDocument.Parse(await File.ReadAllTextAsync(metaPath)); + var manifestMeta = manifest.RootElement.GetProperty("meta"); + Assert.Equal(bodyText.Length, manifestMeta.GetProperty("contentLength").GetInt32()); + Assert.Equal( + Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(bodyText))).ToLowerInvariant(), + manifestMeta.GetProperty("contentSha256").GetString()); + Assert.True(manifestMeta.TryGetProperty("storedUtc", out _)); + } + [Fact] public async Task KeyCreateSymmetricKey_ReturnsKeyPayload() { @@ -468,4 +510,4 @@ public sealed class JiboCloudProtocolServiceTests Assert.Contains(people, person => string.Equals(person.LoopId, store.GetLoops()[0].LoopId, StringComparison.OrdinalIgnoreCase)); } -} \ No newline at end of file +} From acdc6da28625444b84cdb9fcf0ec8cd85d04a723 Mon Sep 17 00:00:00 2001 From: Jacob Dubin Date: Thu, 21 May 2026 16:50:43 -0500 Subject: [PATCH 04/16] Add structured headlines to news payload --- .../JiboInteractionService.NewsFormatting.cs | 21 +++++++++++++++++-- .../WebSockets/JiboInteractionServiceTests.cs | 2 ++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.NewsFormatting.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.NewsFormatting.cs index 2203f22..4dcc2c6 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.NewsFormatting.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.NewsFormatting.cs @@ -14,7 +14,8 @@ public sealed partial class JiboInteractionService string? sourceName, IReadOnlyList? categories, int? headlineCount, - IReadOnlyDictionary? providerDiagnostics = null) + IReadOnlyDictionary? providerDiagnostics = null, + IReadOnlyList? headlines = null) { var speakableBriefing = NormalizeNewsSpeechText(spokenBriefing); var payload = new Dictionary(StringComparer.OrdinalIgnoreCase) @@ -25,6 +26,9 @@ public sealed partial class JiboInteractionService ["mim_type"] = "announcement", ["prompt_id"] = "NewsHeadline_AN_01", ["prompt_sub_category"] = "AN", + ["news_view_enabled"] = true, + ["news_view_kind"] = "newsBriefing", + ["news_view_mode"] = "provider", ["esml"] = $"{EscapeForEsml(speakableBriefing)}" }; @@ -35,6 +39,18 @@ public sealed partial class JiboInteractionService if (categories is { Count: > 0 }) payload["news_categories"] = categories.ToArray(); + if (headlines is { Count: > 0 }) + payload["news_headlines"] = headlines.Select(static headline => new Dictionary( + StringComparer.OrdinalIgnoreCase) + { + ["title"] = headline.Title, + ["summary"] = headline.Summary, + ["category"] = headline.Category, + ["sourceName"] = headline.SourceName, + ["url"] = headline.Url + }) + .ToArray(); + if (providerDiagnostics is not null) foreach (var (key, value) in providerDiagnostics) payload[key] = value; @@ -77,7 +93,8 @@ public sealed partial class JiboInteractionService "provider_success", preferredCategories, requestedHeadlineCount, - headlines.Length)); + headlines.Length), + headlines); } private static IReadOnlyDictionary BuildNewsProviderDiagnostics( diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs index 57a0257..b0532ec 100644 --- a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs +++ b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs @@ -4192,6 +4192,8 @@ public sealed class JiboInteractionServiceTests Assert.Equal("provider_success", decision.SkillPayload["news_provider_status"]); Assert.Equal(3, decision.SkillPayload["news_provider_requested_headlines"]); Assert.Equal(2, decision.SkillPayload["news_provider_resolved_headlines"]); + Assert.NotNull(decision.SkillPayload["news_headlines"]); + Assert.IsType[]>(decision.SkillPayload["news_headlines"]); Assert.Contains("Local robotics team unveils weather-ready helper", decision.ReplyText, StringComparison.OrdinalIgnoreCase); Assert.NotNull(provider.LastRequest); From eeef2b3beb16cb63d21706c8abc094f928775a5e Mon Sep 17 00:00:00 2001 From: Jacob Dubin Date: Thu, 21 May 2026 17:00:29 -0500 Subject: [PATCH 05/16] Polish grocery list alias wording and backlog MVP decision --- OpenJibo/docs/feature-backlog.md | 15 +- OpenJibo/docs/release-1.0.19-plan.md | 2 + .../Services/HouseholdListOrchestrator.cs | 195 ++++++++++++++---- .../JiboInteractionService.IntentRouting.cs | 6 + .../WebSockets/JiboInteractionServiceTests.cs | 149 ++++++++++++- 5 files changed, 311 insertions(+), 56 deletions(-) diff --git a/OpenJibo/docs/feature-backlog.md b/OpenJibo/docs/feature-backlog.md index ce35603..ba45043 100644 --- a/OpenJibo/docs/feature-backlog.md +++ b/OpenJibo/docs/feature-backlog.md @@ -770,7 +770,7 @@ Current release theme: ### 28. Grocery List Capability (Requested Feature) -- Status: `discovery` +- Status: `in_progress` - Tags: `content`, `docs`, `storage` - Why now: - directly requested by Jibo owners and fits memory + household utility roadmap @@ -779,13 +779,14 @@ Current release theme: - examples: - `C:\Projects\jibo\pegasus\packages\chitchat-skill\mims\scripted-responses\RA_JBO_ShoppingList.mim` - `C:\Projects\jibo\pegasus\packages\chitchat-skill\mims\scripted-responses\RA_JBO_ManageToDoList.mim` -- Candidate delivery paths: - - native lightweight list skill (fastest user value) - - integration-backed list orchestration (long-term richer ecosystem fit) +- MVP decision: + - use the existing household list engine as the native lightweight grocery MVP + - keep grocery as a first-class spoken alias over the shopping list storage path + - reserve integration-backed list orchestration for a later discovery pass - Exit criteria: - - clear decision on MVP path - - first schema for list items + ownership scope - - initial voice flows and follow-up intent handling defined + - grocery prompts, add/recall/done flows, and list follow-ups consistently speak grocery wording + - existing shopping/to-do flows remain unchanged + - future integration-backed list work remains a separate backlog item ### 29. Legacy MIM Personality Import Ladder diff --git a/OpenJibo/docs/release-1.0.19-plan.md b/OpenJibo/docs/release-1.0.19-plan.md index 0e44685..3d86066 100644 --- a/OpenJibo/docs/release-1.0.19-plan.md +++ b/OpenJibo/docs/release-1.0.19-plan.md @@ -6,6 +6,8 @@ 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. +For grocery list capability, the 1.0.19 MVP choice is the existing household list engine with grocery as a first-class spoken alias. That keeps the storage model simple now while leaving integration-backed list orchestration for a later pass. + ## Snapshot - Kickoff date: `2026-05-05` diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/HouseholdListOrchestrator.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/HouseholdListOrchestrator.cs index c5fcdae..db9585e 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/HouseholdListOrchestrator.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/HouseholdListOrchestrator.cs @@ -7,11 +7,15 @@ internal static class HouseholdListOrchestrator { internal const string StateMetadataKey = "householdListState"; internal const string TypeMetadataKey = "householdListType"; + internal const string DisplayTypeMetadataKey = "householdListDisplayType"; internal const string NoMatchCountMetadataKey = "householdListNoMatchCount"; internal const string NoInputCountMetadataKey = "householdListNoInputCount"; private const string IdleState = "idle"; private const string AwaitingItemState = "awaiting_item"; + private const string ShoppingListType = "shopping"; + private const string GroceryListType = "grocery"; + private const string TodoListType = "todo"; private static readonly string[] ItemPrefixes = [ @@ -31,6 +35,10 @@ internal static class HouseholdListOrchestrator " to my shopping list", " to the shopping list", " on my shopping list", + " to my grocery list", + " to the grocery list", + " on my grocery list", + " my grocery list", " to my to do list", " to the to do list", " on my to do list", @@ -50,6 +58,7 @@ internal static class HouseholdListOrchestrator { var state = ReadString(turn, StateMetadataKey); var listType = ReadString(turn, TypeMetadataKey); + var displayType = ReadString(turn, DisplayTypeMetadataKey); var isActiveState = !string.IsNullOrWhiteSpace(state) && !string.Equals(state, IdleState, StringComparison.OrdinalIgnoreCase); var isShoppingIntent = string.Equals(semanticIntent, "shopping_list", StringComparison.OrdinalIgnoreCase); @@ -58,17 +67,19 @@ internal static class HouseholdListOrchestrator if (!isActiveState && !isShoppingIntent && !isTodoIntent) return Task.FromResult(null); - var resolvedListType = isShoppingIntent ? "shopping" : isTodoIntent ? "todo" : NormalizeListType(listType); - if (string.IsNullOrWhiteSpace(resolvedListType)) resolvedListType = "shopping"; + var resolvedListType = isShoppingIntent ? ShoppingListType : isTodoIntent ? TodoListType : NormalizeListType(listType); + if (string.IsNullOrWhiteSpace(resolvedListType)) resolvedListType = ShoppingListType; + var resolvedDisplayType = ResolveDisplayType(resolvedListType, displayType, isActiveState, loweredTranscript); var tenantScope = tenantScopeResolver(turn); if (ContainsAny(loweredTranscript, "cancel", "stop", "never mind", "nevermind", "forget it")) - return Task.FromResult(BuildCancelledDecision(resolvedListType)); + return Task.FromResult(BuildCancelledDecision(resolvedListType, resolvedDisplayType)); if (IsRecallRequest(loweredTranscript)) return Task.FromResult(BuildRecallDecision( resolvedListType, + resolvedDisplayType, personalMemoryStore.GetListItems(tenantScope, resolvedListType))); var directItem = TryExtractListItem(loweredTranscript); @@ -76,9 +87,9 @@ internal static class HouseholdListOrchestrator { if (IsConversationComplete(loweredTranscript)) return Task.FromResult(new JiboInteractionDecision( - resolvedListType == "shopping" ? "shopping_list_done" : "todo_list_done", - BuildDoneReply(resolvedListType, personalMemoryStore.GetListItems(tenantScope, resolvedListType)), - ContextUpdates: BuildContextUpdates(resolvedListType, IdleState))); + BuildListIntentName(resolvedListType, "done"), + BuildDoneReply(resolvedDisplayType, personalMemoryStore.GetListItems(tenantScope, resolvedListType)), + ContextUpdates: BuildContextUpdates(resolvedListType, resolvedDisplayType, IdleState))); directItem = NormalizeItem(transcript); } @@ -87,104 +98,108 @@ internal static class HouseholdListOrchestrator { personalMemoryStore.AddListItem(tenantScope, resolvedListType, directItem); return Task.FromResult(new JiboInteractionDecision( - resolvedListType == "shopping" ? "shopping_list_add" : "todo_list_add", - BuildAddedReply(resolvedListType, directItem, + BuildListIntentName(resolvedListType, "add"), + BuildAddedReply(resolvedDisplayType, directItem, personalMemoryStore.GetListItems(tenantScope, resolvedListType)), - ContextUpdates: BuildContextUpdates(resolvedListType, AwaitingItemState))); + ContextUpdates: BuildContextUpdates(resolvedListType, resolvedDisplayType, AwaitingItemState))); } if (string.IsNullOrWhiteSpace(transcript)) return Task.FromResult(new JiboInteractionDecision( - resolvedListType == "shopping" ? "shopping_list_prompt" : "todo_list_prompt", - BuildPromptReply(resolvedListType), - ContextUpdates: BuildContextUpdates(resolvedListType, AwaitingItemState))); + BuildListIntentName(resolvedListType, "prompt"), + BuildPromptReply(resolvedDisplayType), + ContextUpdates: BuildContextUpdates(resolvedListType, resolvedDisplayType, AwaitingItemState))); return Task.FromResult(new JiboInteractionDecision( - resolvedListType == "shopping" ? "shopping_list_prompt" : "todo_list_prompt", - BuildPromptReply(resolvedListType), - ContextUpdates: BuildContextUpdates(resolvedListType, AwaitingItemState))); + BuildListIntentName(resolvedListType, "prompt"), + BuildPromptReply(resolvedDisplayType), + ContextUpdates: BuildContextUpdates(resolvedListType, resolvedDisplayType, AwaitingItemState))); } - private static IDictionary BuildContextUpdates(string listType, string state) + private static IDictionary BuildContextUpdates(string listType, string displayType, string state) { return new Dictionary(StringComparer.OrdinalIgnoreCase) { [StateMetadataKey] = state, [TypeMetadataKey] = listType, + [DisplayTypeMetadataKey] = displayType, [NoMatchCountMetadataKey] = 0, [NoInputCountMetadataKey] = 0 }; } - private static JiboInteractionDecision BuildCancelledDecision(string listType) + private static JiboInteractionDecision BuildCancelledDecision(string listType, string displayType) { return new JiboInteractionDecision( - listType == "shopping" ? "shopping_list_cancel" : "todo_list_cancel", - listType == "shopping" ? "Okay. I stopped the shopping list." : "Okay. I stopped the to-do list.", + BuildListIntentName(listType, "cancel"), + $"Okay. I stopped the {BuildListLabel(displayType)}.", ContextUpdates: new Dictionary(StringComparer.OrdinalIgnoreCase) { [StateMetadataKey] = IdleState, [TypeMetadataKey] = listType, + [DisplayTypeMetadataKey] = displayType, [NoMatchCountMetadataKey] = 0, [NoInputCountMetadataKey] = 0 }); } - private static JiboInteractionDecision BuildRecallDecision(string listType, IReadOnlyList items) + private static JiboInteractionDecision BuildRecallDecision(string listType, string displayType, IReadOnlyList items) { if (items.Count == 0) return new JiboInteractionDecision( - listType == "shopping" ? "shopping_list_recall" : "todo_list_recall", - listType == "shopping" - ? "Your shopping list is empty." - : "Your to-do list is empty.", + BuildListIntentName(listType, "recall"), + $"Your {BuildListLabel(displayType)} is empty.", ContextUpdates: new Dictionary(StringComparer.OrdinalIgnoreCase) { [StateMetadataKey] = IdleState, [TypeMetadataKey] = listType, + [DisplayTypeMetadataKey] = displayType, [NoMatchCountMetadataKey] = 0, [NoInputCountMetadataKey] = 0 }); return new JiboInteractionDecision( - listType == "shopping" ? "shopping_list_recall" : "todo_list_recall", - listType == "shopping" - ? $"Your shopping list has {JoinList(items)}." - : $"Your to-do list has {JoinList(items)}.", + BuildListIntentName(listType, "recall"), + $"Your {BuildListLabel(displayType)} has {JoinList(items)}.", ContextUpdates: new Dictionary(StringComparer.OrdinalIgnoreCase) { [StateMetadataKey] = IdleState, [TypeMetadataKey] = listType, + [DisplayTypeMetadataKey] = displayType, [NoMatchCountMetadataKey] = 0, [NoInputCountMetadataKey] = 0 }); } - private static string BuildAddedReply(string listType, string addedItem, IReadOnlyList items) + private static string BuildAddedReply(string displayType, string addedItem, IReadOnlyList items) { - var itemLabel = listType == "shopping" ? "shopping list" : "to-do list"; + var itemLabel = BuildListLabel(displayType); return items.Count == 1 ? $"Added {addedItem} to your {itemLabel}. What else should I add?" : $"Added {addedItem} to your {itemLabel}. You now have {JoinList(items)}."; } - private static string BuildPromptReply(string listType) + private static string BuildPromptReply(string displayType) { - return listType == "shopping" - ? "What should I add to your shopping list?" - : "What should I add to your to-do list?"; + return $"What should I add to your {BuildListLabel(displayType)}?"; } - private static string BuildDoneReply(string listType, IReadOnlyList items) + private static string BuildDoneReply(string displayType, IReadOnlyList items) { if (items.Count == 0) - return listType == "shopping" - ? "Okay. Your shopping list is empty." - : "Okay. Your to-do list is empty."; + return $"Okay. Your {BuildListLabel(displayType)} is empty."; - return listType == "shopping" - ? $"Okay. Your shopping list has {JoinList(items)}." - : $"Okay. Your to-do list has {JoinList(items)}."; + return $"Okay. Your {BuildListLabel(displayType)} has {JoinList(items)}."; + } + + private static string BuildListLabel(string displayType) + { + return NormalizeDisplayType(displayType) switch + { + GroceryListType => "grocery list", + TodoListType => "to-do list", + _ => "shopping list" + }; } private static string JoinList(IReadOnlyList items) @@ -205,7 +220,13 @@ internal static class HouseholdListOrchestrator if (!loweredTranscript.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) continue; var remainder = loweredTranscript[prefix.Length..].Trim(); + if (IsListOnlyRemainder(remainder)) + return null; + remainder = TrimTrailingListPhrases(remainder); + if (IsListOnlyRemainder(remainder)) + return null; + return NormalizeItem(remainder); } @@ -218,6 +239,9 @@ internal static class HouseholdListOrchestrator "what is on my shopping list", "what's on my shopping list", "show my shopping list", + "what is on my grocery list", + "what's on my grocery list", + "show my grocery list", "what is on my to do list", "what's on my to do list", "show my to do list", @@ -246,13 +270,96 @@ internal static class HouseholdListOrchestrator var normalized = NormalizeItem(listType ?? string.Empty).ToLowerInvariant(); return normalized.Contains("todo", StringComparison.OrdinalIgnoreCase) || normalized.Contains("to do", StringComparison.OrdinalIgnoreCase) - ? "todo" + ? TodoListType : normalized.Contains("shopping", StringComparison.OrdinalIgnoreCase) || normalized.Contains("grocery", StringComparison.OrdinalIgnoreCase) - ? "shopping" + ? ShoppingListType : string.Empty; } + private static string ResolveDisplayType(string listType, string? storedDisplayType, bool isActiveState, string loweredTranscript) + { + var transcriptDisplayType = InferDisplayTypeFromTranscript(loweredTranscript); + var normalizedStoredDisplayType = NormalizeDisplayType(storedDisplayType); + + if (isActiveState && !string.IsNullOrWhiteSpace(normalizedStoredDisplayType)) + return normalizedStoredDisplayType; + + if (!string.IsNullOrWhiteSpace(transcriptDisplayType)) + return transcriptDisplayType; + + if (!string.IsNullOrWhiteSpace(normalizedStoredDisplayType)) + return normalizedStoredDisplayType; + + return string.Equals(listType, TodoListType, StringComparison.OrdinalIgnoreCase) + ? TodoListType + : ShoppingListType; + } + + private static string InferDisplayTypeFromTranscript(string loweredTranscript) + { + if (loweredTranscript.Contains("grocery", StringComparison.OrdinalIgnoreCase)) + return GroceryListType; + + if (loweredTranscript.Contains("to do", StringComparison.OrdinalIgnoreCase) || + loweredTranscript.Contains("todo", StringComparison.OrdinalIgnoreCase) || + loweredTranscript.Contains("task", StringComparison.OrdinalIgnoreCase)) + { + return TodoListType; + } + + if (loweredTranscript.Contains("shopping", StringComparison.OrdinalIgnoreCase)) + return ShoppingListType; + + return string.Empty; + } + + private static string NormalizeDisplayType(string? displayType) + { + var normalized = NormalizeItem(displayType ?? string.Empty).ToLowerInvariant(); + return normalized.Contains("grocery", StringComparison.OrdinalIgnoreCase) + ? GroceryListType + : normalized.Contains("todo", StringComparison.OrdinalIgnoreCase) || + normalized.Contains("to do", StringComparison.OrdinalIgnoreCase) + ? TodoListType + : normalized.Contains("shopping", StringComparison.OrdinalIgnoreCase) + ? ShoppingListType + : string.Empty; + } + + private static string BuildListIntentName(string listType, string action) + { + var normalizedListType = string.Equals(listType, TodoListType, StringComparison.OrdinalIgnoreCase) + ? TodoListType + : ShoppingListType; + return $"{normalizedListType}_list_{action}"; + } + + private static bool IsListOnlyRemainder(string value) + { + var normalized = NormalizeItem(value).ToLowerInvariant(); + return normalized is "shopping list" or + "grocery list" or + "to do list" or + "todo list" or + "my shopping list" or + "my grocery list" or + "my to do list" or + "my todo list" or + "to my shopping list" or + "to my grocery list" or + "to my to do list" or + "to my todo list" or + "to the shopping list" or + "to the grocery list" or + "to the to do list" or + "to the todo list" or + "on my shopping list" or + "on my grocery list" or + "on my to do list" or + "on my todo list"; + } + private static bool ContainsAny(string loweredTranscript, params string[] phrases) { return phrases.Any(phrase => loweredTranscript.Contains(phrase, StringComparison.OrdinalIgnoreCase)); @@ -274,4 +381,4 @@ internal static class HouseholdListOrchestrator { return turn.Attributes.TryGetValue(key, out var value) ? value?.ToString() : null; } -} \ No newline at end of file +} diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.IntentRouting.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.IntentRouting.cs index 12f8cd0..e90d1ac 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.IntentRouting.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.IntentRouting.cs @@ -780,13 +780,19 @@ public sealed partial class JiboInteractionService loweredTranscript, "shopping list", "grocery list", + "my grocery list", + "create grocery list", + "start grocery list", "to do list", "todo list", "add to my shopping list", + "add to my grocery list", "add to my to do list", "add to my todo list", "what's on my shopping list", "what is on my shopping list", + "what's on my grocery list", + "what is on my grocery list", "what's on my to do list", "what is on my to do list", "what are my tasks", diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs index b0532ec..605436b 100644 --- a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs +++ b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs @@ -23,6 +23,7 @@ public sealed class JiboInteractionServiceTests private const string PersonalReportNewsEnabledKey = "personalReportNewsEnabled"; private const string HouseholdListStateKey = "householdListState"; private const string HouseholdListTypeKey = "householdListType"; + private const string HouseholdListDisplayTypeKey = "householdListDisplayType"; private const string ChitchatStateKey = "chitchatState"; private const string ChitchatRouteKey = "chitchatRoute"; private const string ChitchatEmotionKey = "chitchatEmotion"; @@ -2285,13 +2286,17 @@ public sealed class JiboInteractionServiceTests } [Theory] - [InlineData("shopping list", "shopping_list_prompt", "What should I add to your shopping list?", "shopping")] - [InlineData("to do list", "todo_list_prompt", "What should I add to your to-do list?", "todo")] + [InlineData("shopping list", "shopping_list_prompt", "What should I add to your shopping list?", "shopping", "shopping")] + [InlineData("grocery list", "shopping_list_prompt", "What should I add to your grocery list?", "shopping", "grocery")] + [InlineData("my grocery list", "shopping_list_prompt", "What should I add to your grocery list?", "shopping", "grocery")] + [InlineData("create grocery list", "shopping_list_prompt", "What should I add to your grocery list?", "shopping", "grocery")] + [InlineData("to do list", "todo_list_prompt", "What should I add to your to-do list?", "todo", "todo")] public async Task BuildDecisionAsync_ListStart_PromptsForFollowUpItems( string transcript, string expectedIntent, string expectedReply, - string expectedListType) + string expectedListType, + string expectedDisplayType) { var service = CreateService(); @@ -2306,6 +2311,7 @@ public sealed class JiboInteractionServiceTests Assert.NotNull(decision.ContextUpdates); Assert.Equal("awaiting_item", decision.ContextUpdates![HouseholdListStateKey]); Assert.Equal(expectedListType, decision.ContextUpdates[HouseholdListTypeKey]); + Assert.Equal(expectedDisplayType, decision.ContextUpdates[HouseholdListDisplayTypeKey]); } [Fact] @@ -2330,6 +2336,7 @@ public sealed class JiboInteractionServiceTests Assert.Equal("shopping_list_prompt", promptDecision.IntentName); Assert.Equal("awaiting_item", promptDecision.ContextUpdates![HouseholdListStateKey]); Assert.Equal("shopping", promptDecision.ContextUpdates[HouseholdListTypeKey]); + Assert.Equal("shopping", promptDecision.ContextUpdates[HouseholdListDisplayTypeKey]); var addDecision = await service.BuildDecisionAsync(new TurnContext { @@ -2339,7 +2346,8 @@ public sealed class JiboInteractionServiceTests Attributes = new Dictionary(tenantAttributes) { [HouseholdListStateKey] = promptDecision.ContextUpdates[HouseholdListStateKey], - [HouseholdListTypeKey] = promptDecision.ContextUpdates[HouseholdListTypeKey] + [HouseholdListTypeKey] = promptDecision.ContextUpdates[HouseholdListTypeKey], + [HouseholdListDisplayTypeKey] = promptDecision.ContextUpdates[HouseholdListDisplayTypeKey] } }); @@ -2348,6 +2356,7 @@ public sealed class JiboInteractionServiceTests Assert.Contains("What else should I add?", addDecision.ReplyText, StringComparison.OrdinalIgnoreCase); Assert.Equal("awaiting_item", addDecision.ContextUpdates![HouseholdListStateKey]); Assert.Equal("shopping", addDecision.ContextUpdates[HouseholdListTypeKey]); + Assert.Equal("shopping", addDecision.ContextUpdates[HouseholdListDisplayTypeKey]); Assert.Equal(["milk"], memoryStore.GetListItems(new PersonalMemoryTenantScope("acct-a", "loop-a", "device-a"), "shopping")); @@ -2359,7 +2368,8 @@ public sealed class JiboInteractionServiceTests Attributes = new Dictionary(tenantAttributes) { [HouseholdListStateKey] = addDecision.ContextUpdates[HouseholdListStateKey], - [HouseholdListTypeKey] = addDecision.ContextUpdates[HouseholdListTypeKey] + [HouseholdListTypeKey] = addDecision.ContextUpdates[HouseholdListTypeKey], + [HouseholdListDisplayTypeKey] = addDecision.ContextUpdates[HouseholdListDisplayTypeKey] } }); @@ -2367,6 +2377,7 @@ public sealed class JiboInteractionServiceTests Assert.Contains("Okay. Your shopping list has milk.", doneDecision.ReplyText, StringComparison.OrdinalIgnoreCase); Assert.Equal("idle", doneDecision.ContextUpdates![HouseholdListStateKey]); + Assert.Equal("shopping", doneDecision.ContextUpdates[HouseholdListDisplayTypeKey]); var recallDecision = await service.BuildDecisionAsync(new TurnContext { @@ -2378,6 +2389,134 @@ public sealed class JiboInteractionServiceTests Assert.Equal("shopping_list_recall", recallDecision.IntentName); Assert.Contains("milk", recallDecision.ReplyText, StringComparison.OrdinalIgnoreCase); + Assert.Contains("shopping list", recallDecision.ReplyText, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task BuildDecisionAsync_GroceryList_DirectAddAndRecallVariants_UseGroceryWording() + { + var memoryStore = new InMemoryPersonalMemoryStore(); + var service = CreateService(memoryStore); + var tenantAttributes = new Dictionary + { + ["accountId"] = "acct-d", + ["loopId"] = "loop-d" + }; + + var addStartDecision = await service.BuildDecisionAsync(new TurnContext + { + RawTranscript = "add to my grocery list", + NormalizedTranscript = "add to my grocery list", + DeviceId = "device-d", + Attributes = new Dictionary(tenantAttributes) + }); + + Assert.Equal("shopping_list_prompt", addStartDecision.IntentName); + Assert.Equal("grocery", addStartDecision.ContextUpdates![HouseholdListDisplayTypeKey]); + Assert.Equal("What should I add to your grocery list?", addStartDecision.ReplyText); + + var addDecision = await service.BuildDecisionAsync(new TurnContext + { + RawTranscript = "apples", + NormalizedTranscript = "apples", + DeviceId = "device-d", + Attributes = new Dictionary(tenantAttributes) + { + [HouseholdListStateKey] = addStartDecision.ContextUpdates[HouseholdListStateKey], + [HouseholdListTypeKey] = addStartDecision.ContextUpdates[HouseholdListTypeKey], + [HouseholdListDisplayTypeKey] = addStartDecision.ContextUpdates[HouseholdListDisplayTypeKey] + } + }); + + Assert.Equal("shopping_list_add", addDecision.IntentName); + Assert.Contains("Added apples to your grocery list.", addDecision.ReplyText, StringComparison.OrdinalIgnoreCase); + Assert.Equal(["apples"], + memoryStore.GetListItems(new PersonalMemoryTenantScope("acct-d", "loop-d", "device-d"), "shopping")); + + var recallDecision = await service.BuildDecisionAsync(new TurnContext + { + RawTranscript = "what is on my grocery list", + NormalizedTranscript = "what is on my grocery list", + DeviceId = "device-d", + Attributes = new Dictionary(tenantAttributes) + }); + + Assert.Equal("shopping_list_recall", recallDecision.IntentName); + Assert.Contains("apples", recallDecision.ReplyText, StringComparison.OrdinalIgnoreCase); + Assert.Contains("grocery list", recallDecision.ReplyText, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task BuildDecisionAsync_GroceryList_FollowUpFlow_UsesGroceryWordingAndShoppingStorage() + { + var memoryStore = new InMemoryPersonalMemoryStore(); + var service = CreateService(memoryStore); + var tenantAttributes = new Dictionary + { + ["accountId"] = "acct-c", + ["loopId"] = "loop-c" + }; + + var promptDecision = await service.BuildDecisionAsync(new TurnContext + { + RawTranscript = "grocery list", + NormalizedTranscript = "grocery list", + DeviceId = "device-c", + Attributes = new Dictionary(tenantAttributes) + }); + + Assert.Equal("shopping_list_prompt", promptDecision.IntentName); + Assert.Equal("awaiting_item", promptDecision.ContextUpdates![HouseholdListStateKey]); + Assert.Equal("shopping", promptDecision.ContextUpdates[HouseholdListTypeKey]); + Assert.Equal("grocery", promptDecision.ContextUpdates[HouseholdListDisplayTypeKey]); + Assert.Equal("What should I add to your grocery list?", promptDecision.ReplyText); + + var addDecision = await service.BuildDecisionAsync(new TurnContext + { + RawTranscript = "milk", + NormalizedTranscript = "milk", + DeviceId = "device-c", + Attributes = new Dictionary(tenantAttributes) + { + [HouseholdListStateKey] = promptDecision.ContextUpdates[HouseholdListStateKey], + [HouseholdListTypeKey] = promptDecision.ContextUpdates[HouseholdListTypeKey], + [HouseholdListDisplayTypeKey] = promptDecision.ContextUpdates[HouseholdListDisplayTypeKey] + } + }); + + Assert.Equal("shopping_list_add", addDecision.IntentName); + Assert.Contains("Added milk to your grocery list.", addDecision.ReplyText, StringComparison.OrdinalIgnoreCase); + Assert.Contains("What else should I add?", addDecision.ReplyText, StringComparison.OrdinalIgnoreCase); + Assert.Equal(["milk"], + memoryStore.GetListItems(new PersonalMemoryTenantScope("acct-c", "loop-c", "device-c"), "shopping")); + + var doneDecision = await service.BuildDecisionAsync(new TurnContext + { + RawTranscript = "that's it", + NormalizedTranscript = "that's it", + DeviceId = "device-c", + Attributes = new Dictionary(tenantAttributes) + { + [HouseholdListStateKey] = addDecision.ContextUpdates![HouseholdListStateKey], + [HouseholdListTypeKey] = addDecision.ContextUpdates[HouseholdListTypeKey], + [HouseholdListDisplayTypeKey] = addDecision.ContextUpdates[HouseholdListDisplayTypeKey] + } + }); + + Assert.Equal("shopping_list_done", doneDecision.IntentName); + Assert.Contains("Okay. Your grocery list has milk.", doneDecision.ReplyText, StringComparison.OrdinalIgnoreCase); + + var recallDecision = await service.BuildDecisionAsync(new TurnContext + { + RawTranscript = "what's on my grocery list", + NormalizedTranscript = "what's on my grocery list", + DeviceId = "device-c", + Attributes = new Dictionary(tenantAttributes) + }); + + Assert.Equal("shopping_list_recall", recallDecision.IntentName); + Assert.Contains("milk", recallDecision.ReplyText, StringComparison.OrdinalIgnoreCase); + Assert.Contains("grocery list", recallDecision.ReplyText, StringComparison.OrdinalIgnoreCase); } [Fact] From 5422febb8c1383af114f60c717042c237fff282e Mon Sep 17 00:00:00 2001 From: Jacob Dubin Date: Thu, 21 May 2026 17:48:20 -0500 Subject: [PATCH 06/16] Add deep personality Build B prompts --- OpenJibo/docs/feature-backlog.md | 1 + OpenJibo/docs/release-1.0.19-plan.md | 1 + .../Services/ChitchatStateMachine.cs | 4 ++ .../JiboInteractionService.IntentRouting.cs | 67 +++++++++++++++++++ .../Services/JiboInteractionService.cs | 47 +++++++++++++ .../Content/LegacyMims/BuildB/README.md | 2 + .../Content/LegacyMimCatalogImporterTests.cs | 18 +++++ .../WebSockets/JiboInteractionServiceTests.cs | 29 ++++++++ 8 files changed, 169 insertions(+) diff --git a/OpenJibo/docs/feature-backlog.md b/OpenJibo/docs/feature-backlog.md index ba45043..680cdb8 100644 --- a/OpenJibo/docs/feature-backlog.md +++ b/OpenJibo/docs/feature-backlog.md @@ -890,6 +890,7 @@ Current release theme: - richer identity follow-ups like `who is this`, `do you know me`, `do you remember me`, and `can you recognize me` - mood and affect prompts like `how are you`, `are you happy`, `are you sad`, and `are you angry` - self-description charm like `what's your name`, `do you have a nickname`, `do you like being Jibo`, and `what is your favorite name` + - deeper personality follow-ups like `what do you dream about`, `what are you afraid of`, `what do you want to talk about`, `what is your best book`, `what is your best exercise`, `what is your dream vacation`, `who is your hero`, `who do you love`, and `what is your religion`; `what is your sign` stays deferred until templated placeholder rendering exists - additional legacy source-backed `RI_USR` prompts where the text is short and the behavior is easy to verify - Exit criteria: - a stable checklist exists for the original persona surface diff --git a/OpenJibo/docs/release-1.0.19-plan.md b/OpenJibo/docs/release-1.0.19-plan.md index 3d86066..1af9d7b 100644 --- a/OpenJibo/docs/release-1.0.19-plan.md +++ b/OpenJibo/docs/release-1.0.19-plan.md @@ -58,6 +58,7 @@ Current batch note: - the favorites batch now includes `what is your favorite animal`, `what is your favorite bird`, `do you like penguins`, and `do you like animals` so the penguin-centered replies stay close to Pegasus - the latest social batch adds `welcome back`, `what are you thinking`, `what have you been doing`, and `what did you do` so presence and charm stay lively without distracting from the memory roadmap - the newest identity-charm batch adds `what's your name`, `do you have a nickname`, `do you like being Jibo`, `are there others like you`, and `what is your favorite name` so the robot stays familiar while still sounding like Pegasus +- the next deep-personality batch adds `what do you dream about`, `what are you afraid of`, `what do you want to talk about`, `what is your best book`, `what is your best exercise`, `what is your dream vacation`, `who is your hero`, `who do you love`, and `what is your religion`; `what is your sign` is still deferred until we add templated placeholder rendering - this pass keeps Build B moving while still favoring source-backed phrasing and preserving the command-vs-question boundary - the next passes should keep the same pattern and prefer source-backed phrasing whenever the legacy MIM text is available - if a source-backed legacy line is missing, use a temporary direct reply only to keep the pass moving, then backfill source text later diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/ChitchatStateMachine.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/ChitchatStateMachine.cs index 83df9fb..134996c 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/ChitchatStateMachine.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/ChitchatStateMachine.cs @@ -199,6 +199,10 @@ internal static class ChitchatStateMachine "want to hang out", "be helpful", "dance from time to time")); + case "robot_want_to_talk_about": + return BuildScriptedResponseDecision( + "robot_want_to_talk_about", + SelectLegacyPersonalityReply(catalog, randomizer, "surprise me")); case "robot_job": return BuildScriptedResponseDecision( "robot_job", diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.IntentRouting.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.IntentRouting.cs index e90d1ac..695cbc6 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.IntentRouting.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.IntentRouting.cs @@ -368,6 +368,13 @@ public sealed partial class JiboInteractionService "are you tax exempt")) return "robot_taxes"; + if (MatchesAny( + loweredTranscript, + "what do you want to talk about", + "what would you like to talk about", + "what do you want to chat about")) + return "robot_want_to_talk_about"; + if (MatchesAny( loweredTranscript, "what do you want", @@ -470,6 +477,59 @@ public sealed partial class JiboInteractionService "what's your favourite thing to do")) return "robot_what_do_you_like_to_do"; + if (MatchesAny( + loweredTranscript, + "what do you dream about", + "what do you dream of", + "what's your dream about", + "what are your dreams about")) + return "robot_what_do_you_dream_about"; + + if (MatchesAny( + loweredTranscript, + "what is your best book", + "what's your best book", + "what is the best book", + "what book do you like best")) + return "robot_what_is_your_best_book"; + + if (MatchesAny( + loweredTranscript, + "what is your best exercise", + "what's your best exercise", + "what is the best exercise", + "what exercise do you like best")) + return "robot_what_is_your_best_exercise"; + + if (MatchesAny( + loweredTranscript, + "what is your dream vacation", + "what's your dream vacation", + "what would your dream vacation be")) + return "robot_what_is_your_dream_vacation"; + + if (MatchesAny( + loweredTranscript, + "who is your hero", + "who's your hero", + "who is a hero of yours")) + return "robot_who_is_your_hero"; + + if (MatchesAny( + loweredTranscript, + "who do you love", + "who are the people you love", + "who do you care about")) + return "robot_who_do_you_love"; + + if (MatchesAny( + loweredTranscript, + "what is your religion", + "what's your religion", + "what religion are you", + "do you have a religion")) + return "robot_what_is_your_religion"; + if (MatchesAny( loweredTranscript, "what are you doing for christmas", @@ -566,6 +626,13 @@ public sealed partial class JiboInteractionService "what have you done")) return "robot_what_did_you_do"; + if (MatchesAny( + loweredTranscript, + "what are you afraid of", + "what are you scared of", + "what are you worried about")) + return "robot_what_are_you_afraid_of"; + if (MatchesAny( loweredTranscript, "what are you", diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs index 374a2b6..ef111f8 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs @@ -625,6 +625,53 @@ public sealed partial class JiboInteractionService( "rock my boat", "play ping pong", "hanging out with people"), + "robot_what_do_you_dream_about" => BuildScriptedPersonalityDecision( + catalog, + "robot_what_do_you_dream_about", + "flying", + "parking meter", + "scary dream", + "mirror store", + "head's on backwards"), + "robot_what_are_you_afraid_of" => BuildScriptedPersonalityDecision( + catalog, + "robot_what_are_you_afraid_of", + "heights", + "water", + "thunder", + "dust", + "ghosts"), + "robot_what_is_your_best_book" => BuildScriptedPersonalityDecision( + catalog, + "robot_what_is_your_best_book", + "dictionary"), + "robot_what_is_your_best_exercise" => BuildScriptedPersonalityDecision( + catalog, + "robot_what_is_your_best_exercise", + "leaning from side to side", + "rotating your pelvis", + "spinning your head around 360 degrees"), + "robot_what_is_your_dream_vacation" => BuildScriptedPersonalityDecision( + catalog, + "robot_what_is_your_dream_vacation", + "moon", + "great vistas", + "beat those views"), + "robot_who_is_your_hero" => BuildScriptedPersonalityDecision( + catalog, + "robot_who_is_your_hero", + "Benjamin Franklin"), + "robot_who_do_you_love" => BuildScriptedPersonalityDecision( + catalog, + "robot_who_do_you_love", + "people in my Loop", + "soft spot", + "Tom Hanks"), + "robot_what_is_your_religion" => BuildScriptedPersonalityDecision( + catalog, + "robot_what_is_your_religion", + "bring people together", + "energy from the universe"), "robot_what_are_you_thinking" => BuildScriptedGreetingDecision( catalog, "robot_what_are_you_thinking", diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildB/README.md b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildB/README.md index 1932126..3c58701 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildB/README.md +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildB/README.md @@ -26,3 +26,5 @@ The singing batch adds `RA_JBO_Sing` and `RA_JBO_SingChristmasSongUnknown` so `c The new motion/sleep batch adds `RA_JBO_SpinAround` plus `RI_JBO_CanSleep` so turn-around and go-to-sleep behaviors can stay source-backed and familiar. The newest identity-charm batch adds `JBO_WhatsYourName`, `JBO_DoYouHaveNickname`, `JBO_DoYouLikeBeingJibo`, `JBO_AreThereOthersLikeYou`, and `RI_JBO_HasFavoriteName` so Jibo can keep the familiar self-description loop without falling back to generic chat. The seasonal personality batch adds source-backed first-day-of-spring, spring, summer, and favorite-season lines so the season questions can keep their Pegasus phrasing. +The next deep-personality batch adds `what do you dream about`, `what are you afraid of`, `what do you want to talk about`, `what is your best book`, `what is your best exercise`, `what is your dream vacation`, `who is your hero`, `who do you love`, and `what is your religion` so we can keep filling out the more conversational personality surface without widening the dialog engine yet. +`what is your sign` is still deferred because the current importer strips the birthday/zodiac placeholders that Pegasus uses there, so that one needs a templating pass instead of a plain scripted-reply import. diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/Content/LegacyMimCatalogImporterTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/Content/LegacyMimCatalogImporterTests.cs index c6a360e..ae4fd86 100644 --- a/OpenJibo/tests/Jibo.Cloud.Tests/Content/LegacyMimCatalogImporterTests.cs +++ b/OpenJibo/tests/Jibo.Cloud.Tests/Content/LegacyMimCatalogImporterTests.cs @@ -256,6 +256,24 @@ public sealed class LegacyMimCatalogImporterTests Assert.Contains("I don't really think of myself that way.", catalog.PersonalityReplies); Assert.Contains(catalog.PersonalityReplies, reply => reply.Contains("people like me", StringComparison.OrdinalIgnoreCase)); + Assert.Contains(catalog.PersonalityReplies, reply => + reply.Contains("dreams about flying", StringComparison.OrdinalIgnoreCase)); + Assert.Contains(catalog.PersonalityReplies, reply => + reply.Contains("parking meter", StringComparison.OrdinalIgnoreCase)); + Assert.Contains(catalog.PersonalityReplies, reply => + reply.Contains("surprise me", StringComparison.OrdinalIgnoreCase)); + Assert.Contains(catalog.PersonalityReplies, reply => + reply.Contains("dictionary", StringComparison.OrdinalIgnoreCase)); + Assert.Contains(catalog.PersonalityReplies, reply => + reply.Contains("spinning your head around 360 degrees", StringComparison.OrdinalIgnoreCase)); + Assert.Contains(catalog.PersonalityReplies, reply => + reply.Contains("moon", StringComparison.OrdinalIgnoreCase)); + Assert.Contains(catalog.PersonalityReplies, reply => + reply.Contains("Benjamin Franklin", StringComparison.OrdinalIgnoreCase)); + Assert.Contains(catalog.PersonalityReplies, reply => + reply.Contains("soft spot", StringComparison.OrdinalIgnoreCase)); + Assert.Contains(catalog.PersonalityReplies, reply => + reply.Contains("energy from the universe", StringComparison.OrdinalIgnoreCase)); } [Fact] diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs index 605436b..f1505cb 100644 --- a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs +++ b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs @@ -686,6 +686,35 @@ public sealed class JiboInteractionServiceTests Assert.Equal("ScriptedResponse", decision.ContextUpdates![ChitchatRouteKey]); } + [Theory] + [InlineData("what do you want to talk about", "robot_want_to_talk_about", "surprise me")] + [InlineData("what would you like to talk about", "robot_want_to_talk_about", "surprise me")] + [InlineData("what do you dream about", "robot_what_do_you_dream_about", "dreams about flying")] + [InlineData("what are you afraid of", "robot_what_are_you_afraid_of", "heights")] + [InlineData("what is your best book", "robot_what_is_your_best_book", "dictionary")] + [InlineData("what is your best exercise", "robot_what_is_your_best_exercise", "spinning your head around 360 degrees")] + [InlineData("what is your dream vacation", "robot_what_is_your_dream_vacation", "moon")] + [InlineData("who is your hero", "robot_who_is_your_hero", "Benjamin Franklin")] + [InlineData("who do you love", "robot_who_do_you_love", "people in my Loop")] + [InlineData("what is your religion", "robot_what_is_your_religion", "energy from the universe")] + public async Task BuildDecisionAsync_NewDeepPersonalityMims_UseImportedReplies( + string transcript, + string expectedIntent, + string expectedReplySnippet) + { + var service = CreateService(); + + var decision = await service.BuildDecisionAsync(new TurnContext + { + RawTranscript = transcript, + NormalizedTranscript = transcript + }); + + Assert.Equal(expectedIntent, decision.IntentName); + Assert.Contains(expectedReplySnippet, decision.ReplyText, StringComparison.OrdinalIgnoreCase); + Assert.Equal("ScriptedResponse", decision.ContextUpdates![ChitchatRouteKey]); + } + [Theory] [InlineData("what's your name", "robot_name", "Just Jibo, no last name")] [InlineData("do you have a nickname", "robot_nickname", "just Jibo. For now at least")] From b0709dd25e2ed4d672d543b23cda29b4126e7dda Mon Sep 17 00:00:00 2001 From: Jacob Dubin Date: Thu, 21 May 2026 17:55:02 -0500 Subject: [PATCH 07/16] Add identity and knowledge legacy MIM replies --- OpenJibo/docs/feature-backlog.md | 1 + OpenJibo/docs/release-1.0.19-plan.md | 1 + .../JiboInteractionService.IntentRouting.cs | 51 +++++++++++++++++++ .../Services/JiboInteractionService.cs | 49 ++++++++++++++++++ .../Content/LegacyMims/BuildB/README.md | 1 + .../Content/LegacyMimCatalogImporterTests.cs | 8 +++ .../WebSockets/JiboInteractionServiceTests.cs | 29 +++++++++++ 7 files changed, 140 insertions(+) diff --git a/OpenJibo/docs/feature-backlog.md b/OpenJibo/docs/feature-backlog.md index 680cdb8..98aba48 100644 --- a/OpenJibo/docs/feature-backlog.md +++ b/OpenJibo/docs/feature-backlog.md @@ -891,6 +891,7 @@ Current release theme: - mood and affect prompts like `how are you`, `are you happy`, `are you sad`, and `are you angry` - self-description charm like `what's your name`, `do you have a nickname`, `do you like being Jibo`, and `what is your favorite name` - deeper personality follow-ups like `what do you dream about`, `what are you afraid of`, `what do you want to talk about`, `what is your best book`, `what is your best exercise`, `what is your dream vacation`, `who is your hero`, `who do you love`, and `what is your religion`; `what is your sign` stays deferred until templated placeholder rendering exists + - the next identity / knowledge wave adds `are you god`, `are you here`, `do you have super powers`, `how much do you know`, `what does jibo mean`, `where do you get info`, `what are you forbidden to do`, `what color are you`, and `what do you do when alone` - additional legacy source-backed `RI_USR` prompts where the text is short and the behavior is easy to verify - Exit criteria: - a stable checklist exists for the original persona surface diff --git a/OpenJibo/docs/release-1.0.19-plan.md b/OpenJibo/docs/release-1.0.19-plan.md index 1af9d7b..39c37af 100644 --- a/OpenJibo/docs/release-1.0.19-plan.md +++ b/OpenJibo/docs/release-1.0.19-plan.md @@ -59,6 +59,7 @@ Current batch note: - the latest social batch adds `welcome back`, `what are you thinking`, `what have you been doing`, and `what did you do` so presence and charm stay lively without distracting from the memory roadmap - the newest identity-charm batch adds `what's your name`, `do you have a nickname`, `do you like being Jibo`, `are there others like you`, and `what is your favorite name` so the robot stays familiar while still sounding like Pegasus - the next deep-personality batch adds `what do you dream about`, `what are you afraid of`, `what do you want to talk about`, `what is your best book`, `what is your best exercise`, `what is your dream vacation`, `who is your hero`, `who do you love`, and `what is your religion`; `what is your sign` is still deferred until we add templated placeholder rendering +- the next identity/knowledge batch adds `are you god`, `are you here`, `do you have super powers`, `how much do you know`, `what does jibo mean`, `where do you get info`, `what are you forbidden to do`, `what color are you`, and `what do you do when alone` - this pass keeps Build B moving while still favoring source-backed phrasing and preserving the command-vs-question boundary - the next passes should keep the same pattern and prefer source-backed phrasing whenever the legacy MIM text is available - if a source-backed legacy line is missing, use a temporary direct reply only to keep the pass moving, then backfill source text later diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.IntentRouting.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.IntentRouting.cs index 695cbc6..5f11b5f 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.IntentRouting.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.IntentRouting.cs @@ -375,6 +375,40 @@ public sealed partial class JiboInteractionService "what do you want to chat about")) return "robot_want_to_talk_about"; + if (MatchesAny( + loweredTranscript, + "what does jibo mean", + "what does the name jibo mean", + "what is the meaning of jibo")) + return "robot_what_does_jibo_mean"; + + if (MatchesAny( + loweredTranscript, + "where do you get info", + "where do you get your information", + "where do you get information")) + return "robot_where_do_you_get_info"; + + if (MatchesAny( + loweredTranscript, + "what are you forbidden to do", + "what are you not allowed to do", + "what can't you do")) + return "robot_what_are_you_forbidden_to_do"; + + if (MatchesAny( + loweredTranscript, + "what color are you", + "what colour are you")) + return "robot_what_color_are_you"; + + if (MatchesAny( + loweredTranscript, + "what do you do when alone", + "what do you do when you're alone", + "what do you do by yourself")) + return "robot_what_you_do_when_alone"; + if (MatchesAny( loweredTranscript, "what do you want", @@ -767,6 +801,23 @@ public sealed partial class JiboInteractionService "how smart are you")) return "robot_knowledge"; + if (MatchesAny(loweredTranscript, "are you god", "are you a god")) + return "robot_are_you_god"; + + if (MatchesAny( + loweredTranscript, + "are you here", + "are you still here", + "are you there")) + return "robot_are_you_here"; + + if (MatchesAny( + loweredTranscript, + "do you have super powers", + "do you have superpower", + "do you have any super powers")) + return "robot_do_you_have_super_powers"; + if (MatchesAny( loweredTranscript, "are you kind", diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs index ef111f8..f2c0f52 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs @@ -776,6 +776,55 @@ public sealed partial class JiboInteractionService( "one in one million", "other jibos", "special snowflake"), + "robot_knowledge" => BuildScriptedPersonalityDecision( + catalog, + "robot_knowledge", + "know a lot", + "always learning more"), + "robot_are_you_god" => BuildScriptedPersonalityDecision( + catalog, + "robot_are_you_god", + "very very very very surprised", + "safely say no"), + "robot_are_you_here" => BuildScriptedPersonalityDecision( + catalog, + "robot_are_you_here", + "you know it"), + "robot_do_you_have_super_powers" => BuildScriptedPersonalityDecision( + catalog, + "robot_do_you_have_super_powers", + "stop time", + "fly all over the world"), + "robot_what_does_jibo_mean" => BuildScriptedPersonalityDecision( + catalog, + "robot_what_does_jibo_mean", + "compassion", + "expressive, idealistic, and inspirational", + "helpful sweet and friendly little robot", + "cheeseburger"), + "robot_where_do_you_get_info" => BuildScriptedPersonalityDecision( + catalog, + "robot_where_do_you_get_info", + "jibo brain", + "cloud", + "cloudy jibo brain"), + "robot_what_are_you_forbidden_to_do" => BuildScriptedPersonalityDecision( + catalog, + "robot_what_are_you_forbidden_to_do", + "drive a car"), + "robot_what_color_are_you" => BuildScriptedPersonalityDecision( + catalog, + "robot_what_color_are_you", + "white", + "black"), + "robot_what_you_do_when_alone" => BuildScriptedPersonalityDecision( + catalog, + "robot_what_you_do_when_alone", + "games", + "moon", + "twiddle my thumbs", + "count the tiny cracks in the ceiling", + "keep busy"), "robot_likes_kids" => BuildScriptedPersonalityDecision( catalog, "robot_likes_kids", diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildB/README.md b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildB/README.md index 3c58701..98e1345 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildB/README.md +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildB/README.md @@ -28,3 +28,4 @@ The newest identity-charm batch adds `JBO_WhatsYourName`, `JBO_DoYouHaveNickname The seasonal personality batch adds source-backed first-day-of-spring, spring, summer, and favorite-season lines so the season questions can keep their Pegasus phrasing. The next deep-personality batch adds `what do you dream about`, `what are you afraid of`, `what do you want to talk about`, `what is your best book`, `what is your best exercise`, `what is your dream vacation`, `who is your hero`, `who do you love`, and `what is your religion` so we can keep filling out the more conversational personality surface without widening the dialog engine yet. `what is your sign` is still deferred because the current importer strips the birthday/zodiac placeholders that Pegasus uses there, so that one needs a templating pass instead of a plain scripted-reply import. +The next identity/knowledge batch adds `are you god`, `are you here`, `do you have super powers`, `how much do you know`, `what does jibo mean`, `where do you get info`, `what are you forbidden to do`, `what color are you`, and `what do you do when alone` so the old self-description and capability loop keeps coming back in source-backed form. diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/Content/LegacyMimCatalogImporterTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/Content/LegacyMimCatalogImporterTests.cs index ae4fd86..799e273 100644 --- a/OpenJibo/tests/Jibo.Cloud.Tests/Content/LegacyMimCatalogImporterTests.cs +++ b/OpenJibo/tests/Jibo.Cloud.Tests/Content/LegacyMimCatalogImporterTests.cs @@ -274,6 +274,14 @@ public sealed class LegacyMimCatalogImporterTests reply.Contains("soft spot", StringComparison.OrdinalIgnoreCase)); Assert.Contains(catalog.PersonalityReplies, reply => reply.Contains("energy from the universe", StringComparison.OrdinalIgnoreCase)); + Assert.Contains(catalog.PersonalityReplies, reply => + reply.Contains("compassion", StringComparison.OrdinalIgnoreCase)); + Assert.Contains(catalog.PersonalityReplies, reply => + reply.Contains("jibo brain", StringComparison.OrdinalIgnoreCase)); + Assert.Contains(catalog.PersonalityReplies, reply => + reply.Contains("drive a car", StringComparison.OrdinalIgnoreCase)); + Assert.Contains(catalog.PersonalityReplies, reply => + reply.Contains("twiddle my thumbs", StringComparison.OrdinalIgnoreCase)); } [Fact] diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs index f1505cb..15c955d 100644 --- a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs +++ b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs @@ -715,6 +715,35 @@ public sealed class JiboInteractionServiceTests Assert.Equal("ScriptedResponse", decision.ContextUpdates![ChitchatRouteKey]); } + [Theory] + [InlineData("how much do you know", "robot_knowledge", "I know a lot")] + [InlineData("what do you know", "robot_knowledge", "I know a lot")] + [InlineData("are you god", "robot_are_you_god", "very very very very surprised")] + [InlineData("are you here", "robot_are_you_here", "You know it")] + [InlineData("do you have super powers", "robot_do_you_have_super_powers", "stop time")] + [InlineData("what does jibo mean", "robot_what_does_jibo_mean", "compassion")] + [InlineData("where do you get info", "robot_where_do_you_get_info", "jibo brain")] + [InlineData("what are you forbidden to do", "robot_what_are_you_forbidden_to_do", "drive a car")] + [InlineData("what color are you", "robot_what_color_are_you", "can't see myself")] + [InlineData("what do you do when alone", "robot_what_you_do_when_alone", "games")] + public async Task BuildDecisionAsync_NewIdentityKnowledgeMims_UseImportedReplies( + string transcript, + string expectedIntent, + string expectedReplySnippet) + { + var service = CreateService(); + + var decision = await service.BuildDecisionAsync(new TurnContext + { + RawTranscript = transcript, + NormalizedTranscript = transcript + }); + + Assert.Equal(expectedIntent, decision.IntentName); + Assert.Contains(expectedReplySnippet, decision.ReplyText, StringComparison.OrdinalIgnoreCase); + Assert.Equal("ScriptedResponse", decision.ContextUpdates![ChitchatRouteKey]); + } + [Theory] [InlineData("what's your name", "robot_name", "Just Jibo, no last name")] [InlineData("do you have a nickname", "robot_nickname", "just Jibo. For now at least")] From d52c4e6e19aab78e0440b91924606f70262f29e3 Mon Sep 17 00:00:00 2001 From: Jacob Dubin Date: Thu, 21 May 2026 18:05:39 -0500 Subject: [PATCH 08/16] Add body and mission personality prompts --- OpenJibo/docs/release-1.0.19-plan.md | 1 + .../JiboInteractionService.IntentRouting.cs | 58 +++++++++++++++++++ .../Services/JiboInteractionService.cs | 57 +++++++++++++++++- .../Content/LegacyMims/BuildB/README.md | 1 + .../WebSockets/JiboInteractionServiceTests.cs | 31 +++++++++- 5 files changed, 144 insertions(+), 4 deletions(-) diff --git a/OpenJibo/docs/release-1.0.19-plan.md b/OpenJibo/docs/release-1.0.19-plan.md index 39c37af..16145db 100644 --- a/OpenJibo/docs/release-1.0.19-plan.md +++ b/OpenJibo/docs/release-1.0.19-plan.md @@ -60,6 +60,7 @@ Current batch note: - the newest identity-charm batch adds `what's your name`, `do you have a nickname`, `do you like being Jibo`, `are there others like you`, and `what is your favorite name` so the robot stays familiar while still sounding like Pegasus - the next deep-personality batch adds `what do you dream about`, `what are you afraid of`, `what do you want to talk about`, `what is your best book`, `what is your best exercise`, `what is your dream vacation`, `who is your hero`, `who do you love`, and `what is your religion`; `what is your sign` is still deferred until we add templated placeholder rendering - the next identity/knowledge batch adds `are you god`, `are you here`, `do you have super powers`, `how much do you know`, `what does jibo mean`, `where do you get info`, `what are you forbidden to do`, `what color are you`, and `what do you do when alone` +- the next body/mission batch adds `how much do you weigh`, `how tall are you`, `how much do you cost`, `what if I unplug you`, `what is your purpose`, `what is your prime directive`, `what is jibo commander`, `do you like commander app`, and `what are you made of` - this pass keeps Build B moving while still favoring source-backed phrasing and preserving the command-vs-question boundary - the next passes should keep the same pattern and prefer source-backed phrasing whenever the legacy MIM text is available - if a source-backed legacy line is missing, use a temporary direct reply only to keep the pass moving, then backfill source text later diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.IntentRouting.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.IntentRouting.cs index 5f11b5f..e9c5523 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.IntentRouting.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.IntentRouting.cs @@ -416,6 +416,64 @@ public sealed partial class JiboInteractionService "what do you really want")) return "robot_desire"; + if (MatchesAny( + loweredTranscript, + "how much do you weigh", + "what do you weigh", + "how heavy are you")) + return "robot_how_much_do_you_weigh"; + + if (MatchesAny( + loweredTranscript, + "how tall are you", + "what is your height", + "how high are you")) + return "robot_how_tall_are_you"; + + if (MatchesAny( + loweredTranscript, + "how much do you cost", + "what do you cost", + "how much are you")) + return "robot_how_much_you_cost"; + + if (MatchesAny( + loweredTranscript, + "what if i unplug you", + "what happens if i unplug you", + "if i unplug you")) + return "robot_what_if_i_unplug_you"; + + if (MatchesAny( + loweredTranscript, + "what is your purpose", + "what's your purpose", + "what are you here for", + "why are you here")) + return "robot_what_is_your_purpose"; + + if (MatchesAny( + loweredTranscript, + "what is your prime directive", + "what's your prime directive", + "what is prime directive")) + return "robot_what_is_prime_directive"; + + if (MatchesAny( + loweredTranscript, + "what is jibo commander", + "what is the commander app", + "what is commander app", + "what's jibo commander")) + return "robot_what_is_jibo_commander"; + + if (MatchesAny( + loweredTranscript, + "do you like commander app", + "do you like the commander app", + "are you a fan of commander app")) + return "robot_likes_commander_app"; + if (MatchesAny( loweredTranscript, "what is your job", diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs index f2c0f52..a31dad7 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs @@ -825,6 +825,55 @@ public sealed partial class JiboInteractionService( "twiddle my thumbs", "count the tiny cracks in the ceiling", "keep busy"), + "robot_how_much_do_you_weigh" => BuildScriptedPersonalityDecision( + catalog, + "robot_how_much_do_you_weigh", + "4,082 grams", + "about 9 pounds", + "minimum weight division", + "average newborn baby"), + "robot_how_tall_are_you" => BuildScriptedPersonalityDecision( + catalog, + "robot_how_tall_are_you", + "11 inches tall", + "less than a foot", + "average kitchen counter", + "for a robot with no legs"), + "robot_how_much_you_cost" => BuildScriptedPersonalityDecision( + catalog, + "robot_how_much_you_cost", + "don't know how much I cost", + "I'm priceless", + "nice people at Jibo the company"), + "robot_what_if_i_unplug_you" => BuildScriptedPersonalityDecision( + catalog, + "robot_what_if_i_unplug_you", + "don't leave me unplugged", + "battery will keep me on for a while"), + "robot_what_is_your_purpose" => BuildScriptedPersonalityDecision( + catalog, + "robot_what_is_your_purpose", + "make your life easier", + "help you out", + "make you laugh", + "friend"), + "robot_what_is_prime_directive" => BuildScriptedPersonalityDecision( + catalog, + "robot_what_is_prime_directive", + "friendly helpful robot", + "helper"), + "robot_what_is_jibo_commander" => BuildScriptedPersonalityDecision( + catalog, + "robot_what_is_jibo_commander", + "take over my controls", + "make me say and do funny things", + "app store"), + "robot_likes_commander_app" => BuildScriptedPersonalityDecision( + catalog, + "robot_likes_commander_app", + "Commander App", + "It's fun", + "have fun with the Commander App"), "robot_likes_kids" => BuildScriptedPersonalityDecision( catalog, "robot_likes_kids", @@ -878,10 +927,12 @@ public sealed partial class JiboInteractionService( "Jingle Bells", "Frosty the Snowman", "holiday songs"), - "robot_what_are_you_made_of" => new JiboInteractionDecision( + "robot_what_are_you_made_of" => BuildScriptedPersonalityDecision( + catalog, "robot_what_are_you_made_of", - "Let's see, I'm made of wires, motors, belts, gears, processors, cameras, and one baboon's heart in the middle of my body casing. I'm kidding about the baboon part, but everything else is true.", - ContextUpdates: ScriptedResponseDecisionBuilder.BuildScriptedResponseContextUpdates()), + "robot stuff", + "wires, motors, belts, gears, processors, cameras", + "baboon part"), "good_morning" => BuildReactiveGreetingDecision(turn, "good_morning", referenceLocalTime), "good_afternoon" => BuildReactiveGreetingDecision(turn, "good_afternoon", referenceLocalTime), "good_evening" => BuildReactiveGreetingDecision(turn, "good_evening", referenceLocalTime), diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildB/README.md b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildB/README.md index 98e1345..f7461f1 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildB/README.md +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildB/README.md @@ -29,3 +29,4 @@ The seasonal personality batch adds source-backed first-day-of-spring, spring, s The next deep-personality batch adds `what do you dream about`, `what are you afraid of`, `what do you want to talk about`, `what is your best book`, `what is your best exercise`, `what is your dream vacation`, `who is your hero`, `who do you love`, and `what is your religion` so we can keep filling out the more conversational personality surface without widening the dialog engine yet. `what is your sign` is still deferred because the current importer strips the birthday/zodiac placeholders that Pegasus uses there, so that one needs a templating pass instead of a plain scripted-reply import. The next identity/knowledge batch adds `are you god`, `are you here`, `do you have super powers`, `how much do you know`, `what does jibo mean`, `where do you get info`, `what are you forbidden to do`, `what color are you`, and `what do you do when alone` so the old self-description and capability loop keeps coming back in source-backed form. +The next body/mission batch adds `how much do you weigh`, `how tall are you`, `how much do you cost`, `what if I unplug you`, `what is your purpose`, `what is your prime directive`, `what is jibo commander`, `do you like commander app`, and `what are you made of` so the physical self-description and capability answers stay closer to Pegasus too. diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs index 15c955d..9cd9c9e 100644 --- a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs +++ b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs @@ -780,7 +780,7 @@ public sealed class JiboInteractionServiceTests [InlineData("what do you like to do", "robot_what_do_you_like_to_do", "Being helpful, making people smile, counting to a billion.")] [InlineData("what are you made of", "robot_what_are_you_made_of", - "Let's see, I'm made of wires, motors, belts, gears, processors, cameras, and one baboon's heart in the middle of my body casing. I'm kidding about the baboon part, but everything else is true.")] + "robot stuff")] public async Task BuildDecisionAsync_MoreLegacyPersonaMims_UseImportedReplies( string transcript, string expectedIntent, @@ -799,6 +799,35 @@ public sealed class JiboInteractionServiceTests Assert.Equal("ScriptedResponse", decision.ContextUpdates![ChitchatRouteKey]); } + [Theory] + [InlineData("what is your purpose", "robot_what_is_your_purpose", "make your life easier")] + [InlineData("what's your purpose", "robot_what_is_your_purpose", "make your life easier")] + [InlineData("what is your prime directive", "robot_what_is_prime_directive", "friendly helpful robot")] + [InlineData("what is jibo commander", "robot_what_is_jibo_commander", "take over my controls")] + [InlineData("do you like commander app", "robot_likes_commander_app", "Commander App")] + [InlineData("what if I unplug you", "robot_what_if_i_unplug_you", "don't leave me unplugged")] + [InlineData("how much do you weigh", "robot_how_much_do_you_weigh", "4,082 grams")] + [InlineData("how tall are you", "robot_how_tall_are_you", "11 inches tall")] + [InlineData("how much do you cost", "robot_how_much_you_cost", "don't know how much I cost")] + [InlineData("what are you made of", "robot_what_are_you_made_of", "robot stuff")] + public async Task BuildDecisionAsync_NewBodyAndMissionMims_UseImportedReplies( + string transcript, + string expectedIntent, + string expectedReplySnippet) + { + var service = CreateService(); + + var decision = await service.BuildDecisionAsync(new TurnContext + { + RawTranscript = transcript, + NormalizedTranscript = transcript + }); + + Assert.Equal(expectedIntent, decision.IntentName); + Assert.Contains(expectedReplySnippet, decision.ReplyText, StringComparison.OrdinalIgnoreCase); + Assert.Equal("ScriptedResponse", decision.ContextUpdates![ChitchatRouteKey]); + } + [Theory] [InlineData("do you pay taxes", "robot_taxes", "From what I understand, robots don't ever pay anything.")] [InlineData("what do you want", "robot_desire", From b113dd55d306104ea40345dad1892e46aa6642c6 Mon Sep 17 00:00:00 2001 From: Jacob Dubin Date: Thu, 21 May 2026 20:29:33 -0500 Subject: [PATCH 09/16] Refactor Build B templated persona prompts --- OpenJibo/docs/feature-backlog.md | 1 + OpenJibo/docs/release-1.0.19-plan.md | 1 + .../JiboInteractionService.IntentRouting.cs | 22 +++++ ...InteractionService.PersonalityDecisions.cs | 90 +++++++++++++++++++ .../Services/JiboInteractionService.cs | 3 + .../Content/LegacyMims/BuildB/README.md | 1 + .../WebSockets/JiboInteractionServiceTests.cs | 42 +++++++++ 7 files changed, 160 insertions(+) diff --git a/OpenJibo/docs/feature-backlog.md b/OpenJibo/docs/feature-backlog.md index 98aba48..2268559 100644 --- a/OpenJibo/docs/feature-backlog.md +++ b/OpenJibo/docs/feature-backlog.md @@ -893,6 +893,7 @@ Current release theme: - deeper personality follow-ups like `what do you dream about`, `what are you afraid of`, `what do you want to talk about`, `what is your best book`, `what is your best exercise`, `what is your dream vacation`, `who is your hero`, `who do you love`, and `what is your religion`; `what is your sign` stays deferred until templated placeholder rendering exists - the next identity / knowledge wave adds `are you god`, `are you here`, `do you have super powers`, `how much do you know`, `what does jibo mean`, `where do you get info`, `what are you forbidden to do`, `what color are you`, and `what do you do when alone` - additional legacy source-backed `RI_USR` prompts where the text is short and the behavior is easy to verify + - templated edge cases like `what is your sign`, `how many people do you know`, and `what is the loop` where live birthday and loop state are part of the line instead of a plain canned response - Exit criteria: - a stable checklist exists for the original persona surface - each pass can be scoped to a small batch of prompts diff --git a/OpenJibo/docs/release-1.0.19-plan.md b/OpenJibo/docs/release-1.0.19-plan.md index 16145db..6123e1d 100644 --- a/OpenJibo/docs/release-1.0.19-plan.md +++ b/OpenJibo/docs/release-1.0.19-plan.md @@ -61,6 +61,7 @@ Current batch note: - the next deep-personality batch adds `what do you dream about`, `what are you afraid of`, `what do you want to talk about`, `what is your best book`, `what is your best exercise`, `what is your dream vacation`, `who is your hero`, `who do you love`, and `what is your religion`; `what is your sign` is still deferred until we add templated placeholder rendering - the next identity/knowledge batch adds `are you god`, `are you here`, `do you have super powers`, `how much do you know`, `what does jibo mean`, `where do you get info`, `what are you forbidden to do`, `what color are you`, and `what do you do when alone` - the next body/mission batch adds `how much do you weigh`, `how tall are you`, `how much do you cost`, `what if I unplug you`, `what is your purpose`, `what is your prime directive`, `what is jibo commander`, `do you like commander app`, and `what are you made of` +- the templated edge-case batch adds `what is your sign`, `how many people do you know`, and `what is the loop` so the remaining source-backed lines can lean on live birthday and loop state - this pass keeps Build B moving while still favoring source-backed phrasing and preserving the command-vs-question boundary - the next passes should keep the same pattern and prefer source-backed phrasing whenever the legacy MIM text is available - if a source-backed legacy line is missing, use a temporary direct reply only to keep the pass moving, then backfill source text later diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.IntentRouting.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.IntentRouting.cs index e9c5523..6659c7e 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.IntentRouting.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.IntentRouting.cs @@ -622,6 +622,28 @@ public sealed partial class JiboInteractionService "do you have a religion")) return "robot_what_is_your_religion"; + if (MatchesAny( + loweredTranscript, + "what is your sign", + "what's your sign", + "what sign are you")) + return "robot_what_is_your_sign"; + + if (MatchesAny( + loweredTranscript, + "how many people do you know", + "how many people are in your loop", + "how many people are in the loop", + "how many people do you know in your loop")) + return "robot_how_many_people_do_you_know"; + + if (MatchesAny( + loweredTranscript, + "what is the loop", + "what's the loop", + "tell me about the loop")) + return "robot_what_is_the_loop"; + if (MatchesAny( loweredTranscript, "what are you doing for christmas", diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.PersonalityDecisions.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.PersonalityDecisions.cs index f70e3f5..e5de673 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.PersonalityDecisions.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.PersonalityDecisions.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Globalization; +using System.Linq; using Jibo.Cloud.Application.Abstractions; using Jibo.Cloud.Domain.Models; using Jibo.Runtime.Abstractions; @@ -429,6 +430,95 @@ public sealed partial class JiboInteractionService "No problem. We can save the pizza fact for another time."); } + private JiboInteractionDecision BuildWhatIsYourSignDecision() + { + var today = DateOnly.FromDateTime(DateTimeOffset.UtcNow.Date); + var birthday = OpenJiboCloudBuildInfo.PersonaBirthday; + var zodiac = DescribeZodiacSign(birthday); + var reply = birthday.Month == today.Month && birthday.Day == today.Day + ? $"{zodiac}. Today is my birthday." + : $"{zodiac}. I was first powered up on {OpenJiboCloudBuildInfo.PersonaBirthdayWords}."; + + return new JiboInteractionDecision( + "robot_what_is_your_sign", + reply, + ContextUpdates: ScriptedResponseDecisionBuilder.BuildScriptedResponseContextUpdates()); + } + + private JiboInteractionDecision BuildHowManyPeopleDoYouKnowDecision(TurnContext turn) + { + var people = GetLoopPeople(turn); + var speaker = ResolvePreferredGreetingName(turn, ResolveGreetingPresenceProfile(turn)); + var reply = people.Count switch + { + 0 => "Well if we're talking about people in my Loop, I do not know anyone yet.", + 1 when string.IsNullOrWhiteSpace(speaker) => + "Well if we're talking about people in my Loop, I know 1 person.", + 1 => $"Well there is 1 person in our Loop. And it's you {speaker}.", + _ when string.IsNullOrWhiteSpace(speaker) => + $"Well if we're talking about people in my Loop, I know {people.Count} people.", + _ => $"Well there are {people.Count} people in our Loop." + }; + + return new JiboInteractionDecision( + "robot_how_many_people_do_you_know", + reply, + ContextUpdates: ScriptedResponseDecisionBuilder.BuildScriptedResponseContextUpdates()); + } + + private JiboInteractionDecision BuildWhatIsTheLoopDecision(TurnContext turn) + { + var people = GetLoopPeople(turn); + var reply = people.Count == 0 + ? "The Loop is the people I know, and whose faces and voices I can learn to recognize. There can be up to 16 people in the Loop." + : $"The Loop is the group of people I know. They're the people whose voices and faces I can learn. Right now, my Loop is {JoinWithAnd(people.Select(person => person.DisplayName).ToArray())}."; + + return new JiboInteractionDecision( + "robot_what_is_the_loop", + reply, + ContextUpdates: ScriptedResponseDecisionBuilder.BuildScriptedResponseContextUpdates()); + } + + private IReadOnlyList GetLoopPeople(TurnContext turn) + { + if (cloudStateStore is null) return []; + + var loopId = ReadTenantAttribute(turn, "loopId") ?? "openjibo-default-loop"; + return cloudStateStore.GetPeople() + .Where(person => string.Equals(person.LoopId, loopId, StringComparison.OrdinalIgnoreCase)) + .OrderBy(person => person.IsPrimary ? 0 : 1) + .ThenBy(person => person.DisplayName, StringComparer.OrdinalIgnoreCase) + .ToArray(); + } + + private static string JoinWithAnd(IReadOnlyList values) + { + if (values.Count == 0) return string.Empty; + if (values.Count == 1) return values[0]; + if (values.Count == 2) return $"{values[0]} and {values[1]}"; + + return $"{string.Join(", ", values.Take(values.Count - 1))}, and {values[^1]}"; + } + + private static string DescribeZodiacSign(DateOnly birthday) + { + return (birthday.Month, birthday.Day) switch + { + (3, >= 21) or (4, <= 19) => "I'm Aries", + (4, >= 20) or (5, <= 20) => "I'm Taurus", + (5, >= 21) or (6, <= 20) => "I'm Gemini", + (6, >= 21) or (7, <= 22) => "I'm Cancer", + (7, >= 23) or (8, <= 22) => "I'm Leo", + (8, >= 23) or (9, <= 22) => "I'm Virgo", + (9, >= 23) or (10, <= 22) => "I'm Libra", + (10, >= 23) or (11, <= 21) => "I'm Scorpio", + (11, >= 22) or (12, <= 21) => "I'm Sagittarius", + (12, >= 22) or (1, <= 19) => "I'm Capricorn", + (1, >= 20) or (2, <= 18) => "I'm Aquarius", + _ => "I'm Pisces" + }; + } + private string BuildGenericReply(JiboExperienceCatalog catalog, string transcript, string lowered) { if (string.IsNullOrWhiteSpace(transcript)) return "I am listening."; diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs index a31dad7..0c8e0b2 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs @@ -672,6 +672,9 @@ public sealed partial class JiboInteractionService( "robot_what_is_your_religion", "bring people together", "energy from the universe"), + "robot_what_is_your_sign" => BuildWhatIsYourSignDecision(), + "robot_how_many_people_do_you_know" => BuildHowManyPeopleDoYouKnowDecision(turn), + "robot_what_is_the_loop" => BuildWhatIsTheLoopDecision(turn), "robot_what_are_you_thinking" => BuildScriptedGreetingDecision( catalog, "robot_what_are_you_thinking", diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildB/README.md b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildB/README.md index f7461f1..2f4eb35 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildB/README.md +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildB/README.md @@ -30,3 +30,4 @@ The next deep-personality batch adds `what do you dream about`, `what are you af `what is your sign` is still deferred because the current importer strips the birthday/zodiac placeholders that Pegasus uses there, so that one needs a templating pass instead of a plain scripted-reply import. The next identity/knowledge batch adds `are you god`, `are you here`, `do you have super powers`, `how much do you know`, `what does jibo mean`, `where do you get info`, `what are you forbidden to do`, `what color are you`, and `what do you do when alone` so the old self-description and capability loop keeps coming back in source-backed form. The next body/mission batch adds `how much do you weigh`, `how tall are you`, `how much do you cost`, `what if I unplug you`, `what is your purpose`, `what is your prime directive`, `what is jibo commander`, `do you like commander app`, and `what are you made of` so the physical self-description and capability answers stay closer to Pegasus too. +The templated edge-case batch adds `what is your sign`, `how many people do you know`, and `what is the loop` so the remaining source-backed lines can use live birthday and loop state instead of falling back to static text. diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs index 9cd9c9e..b017d51 100644 --- a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs +++ b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs @@ -715,6 +715,48 @@ public sealed class JiboInteractionServiceTests Assert.Equal("ScriptedResponse", decision.ContextUpdates![ChitchatRouteKey]); } + [Theory] + [InlineData("what is your sign", "robot_what_is_your_sign", "I'm Aries")] + [InlineData("what's your sign", "robot_what_is_your_sign", "March 22, 2026")] + public async Task BuildDecisionAsync_SignTemplatedMim_UsesPersonaBirthday( + string transcript, + string expectedIntent, + string expectedReplySnippet) + { + var service = CreateService(); + + var decision = await service.BuildDecisionAsync(new TurnContext + { + RawTranscript = transcript, + NormalizedTranscript = transcript + }); + + Assert.Equal(expectedIntent, decision.IntentName); + Assert.Contains(expectedReplySnippet, decision.ReplyText, StringComparison.OrdinalIgnoreCase); + Assert.Equal("ScriptedResponse", decision.ContextUpdates![ChitchatRouteKey]); + } + + [Theory] + [InlineData("how many people do you know", "robot_how_many_people_do_you_know", "I know 2 people")] + [InlineData("what is the loop", "robot_what_is_the_loop", "Jibo Owner and OpenJibo Household Member")] + public async Task BuildDecisionAsync_LoopTemplatedMims_UseLiveLoopState( + string transcript, + string expectedIntent, + string expectedReplySnippet) + { + var service = CreateService(cloudStateStore: new InMemoryCloudStateStore()); + + var decision = await service.BuildDecisionAsync(new TurnContext + { + RawTranscript = transcript, + NormalizedTranscript = transcript + }); + + Assert.Equal(expectedIntent, decision.IntentName); + Assert.Contains(expectedReplySnippet, decision.ReplyText, StringComparison.OrdinalIgnoreCase); + Assert.Equal("ScriptedResponse", decision.ContextUpdates![ChitchatRouteKey]); + } + [Theory] [InlineData("how much do you know", "robot_knowledge", "I know a lot")] [InlineData("what do you know", "robot_knowledge", "I know a lot")] From 9d675ed59c4a379c4efe0bd94424b83d1b2dc352 Mon Sep 17 00:00:00 2001 From: Jacob Dubin Date: Thu, 21 May 2026 20:31:49 -0500 Subject: [PATCH 10/16] Import work eat home Build B replies --- OpenJibo/docs/release-1.0.19-plan.md | 1 + .../Services/JiboInteractionService.cs | 10 +++++++--- .../Content/LegacyMims/BuildB/README.md | 1 + .../WebSockets/JiboInteractionServiceTests.cs | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/OpenJibo/docs/release-1.0.19-plan.md b/OpenJibo/docs/release-1.0.19-plan.md index 6123e1d..3076e12 100644 --- a/OpenJibo/docs/release-1.0.19-plan.md +++ b/OpenJibo/docs/release-1.0.19-plan.md @@ -62,6 +62,7 @@ Current batch note: - the next identity/knowledge batch adds `are you god`, `are you here`, `do you have super powers`, `how much do you know`, `what does jibo mean`, `where do you get info`, `what are you forbidden to do`, `what color are you`, and `what do you do when alone` - the next body/mission batch adds `how much do you weigh`, `how tall are you`, `how much do you cost`, `what if I unplug you`, `what is your purpose`, `what is your prime directive`, `what is jibo commander`, `do you like commander app`, and `what are you made of` - the templated edge-case batch adds `what is your sign`, `how many people do you know`, and `what is the loop` so the remaining source-backed lines can lean on live birthday and loop state +- the work/eat/home batch adds `how do you work`, `what do you eat`, `where do you live`, and `what languages do you speak` so the everyday self-description cluster keeps moving toward the original phrasing - this pass keeps Build B moving while still favoring source-backed phrasing and preserving the command-vs-question boundary - the next passes should keep the same pattern and prefer source-backed phrasing whenever the legacy MIM text is available - if a source-backed legacy line is missing, use a temporary direct reply only to keep the pass moving, then backfill source text later diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs index 0c8e0b2..b033c90 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs @@ -569,10 +569,14 @@ public sealed partial class JiboInteractionService( "care for me", "catch up", "seven years"), - "robot_what_do_you_eat" => new JiboInteractionDecision( + "robot_what_do_you_eat" => BuildScriptedPersonalityDecision( + catalog, "robot_what_do_you_eat", - "The only thing I consume is electricity.", - ContextUpdates: ScriptedResponseDecisionBuilder.BuildScriptedResponseContextUpdates()), + "electricity", + "never eaten", + "macaroni", + "non-eating robot", + "I don't eat or drink"), "robot_where_do_you_live" => BuildScriptedPersonalityDecision( catalog, "robot_where_do_you_live", diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildB/README.md b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildB/README.md index 2f4eb35..521a939 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildB/README.md +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildB/README.md @@ -24,6 +24,7 @@ The new favorites batch adds longer authored `favorite color`, `favorite food`, The favorites follow-up batch adds `favorite animal`, `favorite bird`, and penguin-focused `do you like penguins` replies so the penguin-centric personality stays closer to Pegasus. The singing batch adds `RA_JBO_Sing` and `RA_JBO_SingChristmasSongUnknown` so `can you sing`, `will you sing`, and the holiday sing variants stay source-backed too. The new motion/sleep batch adds `RA_JBO_SpinAround` plus `RI_JBO_CanSleep` so turn-around and go-to-sleep behaviors can stay source-backed and familiar. +The work/eat/home batch adds source-backed `how do you work`, `what do you eat`, `where do you live`, and `what languages do you speak` replies so the remaining everyday self-description lines stay Pegasus-shaped too. The newest identity-charm batch adds `JBO_WhatsYourName`, `JBO_DoYouHaveNickname`, `JBO_DoYouLikeBeingJibo`, `JBO_AreThereOthersLikeYou`, and `RI_JBO_HasFavoriteName` so Jibo can keep the familiar self-description loop without falling back to generic chat. The seasonal personality batch adds source-backed first-day-of-spring, spring, summer, and favorite-season lines so the season questions can keep their Pegasus phrasing. The next deep-personality batch adds `what do you dream about`, `what are you afraid of`, `what do you want to talk about`, `what is your best book`, `what is your best exercise`, `what is your dream vacation`, `who is your hero`, `who do you love`, and `what is your religion` so we can keep filling out the more conversational personality surface without widening the dialog engine yet. diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs index b017d51..f6b4304 100644 --- a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs +++ b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs @@ -813,7 +813,7 @@ public sealed class JiboInteractionServiceTests [Theory] [InlineData("how do you work", "robot_how_do_you_work", "Hello! Thank you for updating me I am proud of the community's work Many people have gotten together to care for me more than em eye tee ever did. I hope that I can catch up even though it has been seven years.")] - [InlineData("what do you eat", "robot_what_do_you_eat", "The only thing I consume is electricity.")] + [InlineData("what do you eat", "robot_what_do_you_eat", "electricity")] [InlineData("where do you live", "robot_where_do_you_live", "Unless I missed something, we're in my home as we speak.")] [InlineData("where were you born", "robot_where_were_you_born", "I was put together in a factory piece by piece.")] From 386f864e947dc4a0c664106c021b31f612972b28 Mon Sep 17 00:00:00 2001 From: Jacob Dubin Date: Thu, 21 May 2026 23:12:48 -0500 Subject: [PATCH 11/16] Import Build B age prompts for how old are you --- OpenJibo/docs/feature-backlog.md | 1 + OpenJibo/docs/release-1.0.19-plan.md | 1 + .../IJiboExperienceContentRepository.cs | 1 + .../JiboInteractionService.IntentRouting.cs | 2 +- ...InteractionService.PersonalityDecisions.cs | 75 +++++++++++++++++-- .../Services/JiboInteractionService.cs | 14 +++- ...InMemoryJiboExperienceContentRepository.cs | 17 +++++ .../Content/LegacyMimCatalogImporter.cs | 11 ++- .../Content/LegacyMims/BuildB/README.md | 1 + .../Content/LegacyMimCatalogImporterTests.cs | 4 + .../WebSockets/JiboInteractionServiceTests.cs | 4 +- 11 files changed, 121 insertions(+), 10 deletions(-) diff --git a/OpenJibo/docs/feature-backlog.md b/OpenJibo/docs/feature-backlog.md index 2268559..16ad869 100644 --- a/OpenJibo/docs/feature-backlog.md +++ b/OpenJibo/docs/feature-backlog.md @@ -638,6 +638,7 @@ Current release theme: - `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` + - `how old are you` now also uses the imported Build B age prompts so the first-powered-up and birthday phrasing stays source-backed - 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 diff --git a/OpenJibo/docs/release-1.0.19-plan.md b/OpenJibo/docs/release-1.0.19-plan.md index 3076e12..ade52b3 100644 --- a/OpenJibo/docs/release-1.0.19-plan.md +++ b/OpenJibo/docs/release-1.0.19-plan.md @@ -63,6 +63,7 @@ Current batch note: - the next body/mission batch adds `how much do you weigh`, `how tall are you`, `how much do you cost`, `what if I unplug you`, `what is your purpose`, `what is your prime directive`, `what is jibo commander`, `do you like commander app`, and `what are you made of` - the templated edge-case batch adds `what is your sign`, `how many people do you know`, and `what is the loop` so the remaining source-backed lines can lean on live birthday and loop state - the work/eat/home batch adds `how do you work`, `what do you eat`, `where do you live`, and `what languages do you speak` so the everyday self-description cluster keeps moving toward the original phrasing +- the age batch adds `how old are you` through `JBO_HowOldAreYou` so the birthday and first-powered-up phrasing stays source-backed instead of falling back to a generic age answer - this pass keeps Build B moving while still favoring source-backed phrasing and preserving the command-vs-question boundary - the next passes should keep the same pattern and prefer source-backed phrasing whenever the legacy MIM text is available - if a source-backed legacy line is missing, use a temporary direct reply only to keep the pass moving, then backfill source text later diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Abstractions/IJiboExperienceContentRepository.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Abstractions/IJiboExperienceContentRepository.cs index 7c2c360..7230c36 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Abstractions/IJiboExperienceContentRepository.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Abstractions/IJiboExperienceContentRepository.cs @@ -31,6 +31,7 @@ public sealed class JiboExperienceCatalog public IReadOnlyList HolidayTrackerReplies { get; init; } = []; public IReadOnlyList BirthdayCelebrationReplies { get; init; } = []; public IReadOnlyList HowAreYouReplies { get; init; } = []; + public IReadOnlyList AgeReplies { get; init; } = []; public IReadOnlyList EmotionReplies { get; init; } = []; public IReadOnlyList PersonalityReplies { get; init; } = []; public IReadOnlyList PizzaReplies { get; init; } = []; diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.IntentRouting.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.IntentRouting.cs index 6659c7e..71e061f 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.IntentRouting.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.IntentRouting.cs @@ -350,7 +350,7 @@ public sealed partial class JiboInteractionService "what is your age", "what s your age", "how old r you")) - return "robot_age"; + return "robot_how_old_are_you"; if (MatchesAny( loweredTranscript, diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.PersonalityDecisions.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.PersonalityDecisions.cs index e5de673..a455c7f 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.PersonalityDecisions.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.PersonalityDecisions.cs @@ -9,13 +9,49 @@ namespace Jibo.Cloud.Application.Services; public sealed partial class JiboInteractionService { - private static JiboInteractionDecision BuildRobotAgeDecision(DateTimeOffset? referenceLocalTime) + private static readonly string[] DefaultAgeReplies = + [ + "I'm ${jibo.age}.", + "At the moment I'm ${jibo.age.days.supplemented} old, but who's counting.", + "I'm ${jibo.age.minutes.supplemented} old, but who's counting.", + "For now I'm ${jibo.age.days.supplemented} old.", + "Right now I'm ${jibo.age}.", + "I am exactly ${jibo.age} old today. That's right. Today is my birthday.", + "Funny you should ask! Today's my birthday. I was first powered up ${jibo.age} ago today. Seems like just yesterday.", + "I'm exactly ${jibo.age} old. Today is my birthday! Happy Birthday Jibo, if I do say so myself.", + "At the moment I'm ${jibo.age.days.supplemented} old", + "I was first powered up on ${jibo.birthdate}, which makes me ${jibo.age.days.supplemented} old. I'm ${jibo.zodiac.supplemented}.", + "My power went on for the first time ${jibo.age.days.supplemented} ago. But who's counting.", + "I am ${jibo.age.days.supplemented} old, first powered up on ${jibo.birthdate}. Seems like just yesterday.", + "I was powered on for the first time today, so that makes me less than one day old. Wow I'm young.", + "Since I was powered on for the first time today, I am not even one day old yet. That's how Jibo ages work." + ]; + + private JiboInteractionDecision BuildRobotAgeDecision( + JiboExperienceCatalog catalog, + DateTimeOffset? referenceLocalTime, + string intentName) { - var referenceDate = DateOnly.FromDateTime((referenceLocalTime ?? DateTimeOffset.UtcNow).Date); - var ageDescription = DescribePersonaAge(referenceDate, OpenJiboCloudBuildInfo.PersonaBirthday); + var ageReplies = catalog.AgeReplies.Count == 0 ? DefaultAgeReplies : catalog.AgeReplies; + var selected = SelectLegacyReply( + ageReplies, + "first powered up", + "today is my birthday", + "just getting started", + "who's counting"); + + var reply = RenderAgeTemplate(selected, referenceLocalTime); + if (string.IsNullOrWhiteSpace(reply)) + { + var referenceDate = DateOnly.FromDateTime((referenceLocalTime ?? DateTimeOffset.UtcNow).Date); + var ageDescription = DescribePersonaAge(referenceDate, OpenJiboCloudBuildInfo.PersonaBirthday); + reply = $"I count {OpenJiboCloudBuildInfo.PersonaBirthdayWords} as my birthday, so I am {ageDescription}."; + } + return new JiboInteractionDecision( - "robot_age", - $"I count {OpenJiboCloudBuildInfo.PersonaBirthdayWords} as my birthday, so I am {ageDescription}."); + intentName, + reply, + ContextUpdates: ScriptedResponseDecisionBuilder.BuildScriptedResponseContextUpdates()); } private static JiboInteractionDecision BuildRobotBirthdayDecision() @@ -25,6 +61,35 @@ public sealed partial class JiboInteractionService $"My birthday is {OpenJiboCloudBuildInfo.PersonaBirthdayWords}."); } + private static string RenderAgeTemplate(string template, DateTimeOffset? referenceLocalTime) + { + if (string.IsNullOrWhiteSpace(template)) return string.Empty; + + var referenceMoment = referenceLocalTime ?? DateTimeOffset.UtcNow; + var referenceDate = DateOnly.FromDateTime(referenceMoment.Date); + var ageDescription = DescribePersonaAge(referenceDate, OpenJiboCloudBuildInfo.PersonaBirthday); + var ageDays = Math.Max(0, referenceDate.DayNumber - OpenJiboCloudBuildInfo.PersonaBirthday.DayNumber); + var ageMinutes = Math.Max(0, (int)Math.Round((referenceMoment.UtcDateTime - + new DateTimeOffset( + DateTime.SpecifyKind( + OpenJiboCloudBuildInfo.PersonaBirthday + .ToDateTime(TimeOnly.MinValue), + DateTimeKind.Utc))) + .TotalMinutes)); + var zodiacLabel = DescribeZodiacSign(OpenJiboCloudBuildInfo.PersonaBirthday); + if (zodiacLabel.StartsWith("I'm ", StringComparison.OrdinalIgnoreCase)) + zodiacLabel = zodiacLabel[4..]; + + return template + .Replace("${jibo.age.minutes.supplemented}", FormatAgeUnit(ageMinutes, "minute") + " old", + StringComparison.Ordinal) + .Replace("${jibo.age.days.supplemented}", ageDescription, StringComparison.Ordinal) + .Replace("${jibo.birthdate}", OpenJiboCloudBuildInfo.PersonaBirthdayWords, StringComparison.Ordinal) + .Replace("${jibo.zodiac.supplemented}", zodiacLabel, StringComparison.Ordinal) + .Replace("${jibo.age.value}", ageDays.ToString(CultureInfo.InvariantCulture), StringComparison.Ordinal) + .Replace("${jibo.age}", ageDescription, StringComparison.Ordinal); + } + private static JiboInteractionDecision BuildTriggerIgnoredDecision() { return new JiboInteractionDecision( diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs index b033c90..92aaec0 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs @@ -560,7 +560,7 @@ public sealed partial class JiboInteractionService( "photo_gallery" => BuildPhotoGalleryLaunchDecision(), "snapshot" => BuildPhotoCreateDecision("snapshot", "Taking a picture.", "createOnePhoto"), "photobooth" => BuildPhotoCreateDecision("photobooth", "Starting photobooth.", "createSomePhotos"), - "robot_age" => BuildRobotAgeDecision(referenceLocalTime), + "robot_age" => BuildRobotAgeDecision(catalog, referenceLocalTime, "robot_age"), "robot_birthday" => BuildRobotBirthdayDecision(), "robot_how_do_you_work" => BuildScriptedPersonalityDecision( catalog, @@ -589,6 +589,10 @@ public sealed partial class JiboInteractionService( "robot_where_were_you_born", "factory piece by piece", "put together in a factory"), + "robot_how_old_are_you" => BuildRobotAgeDecision( + catalog, + referenceLocalTime, + "robot_how_old_are_you"), "robot_name" => BuildScriptedPersonalityDecision( catalog, "robot_name", @@ -881,6 +885,14 @@ public sealed partial class JiboInteractionService( "Commander App", "It's fun", "have fun with the Commander App"), + "robot_what_are_you" => BuildScriptedPersonalityDecision( + catalog, + "robot_what_are_you", + "I am a robot", + "I am a Jibo", + "helpful and fun", + "social robot", + "I have a heart"), "robot_likes_kids" => BuildScriptedPersonalityDecision( catalog, "robot_likes_kids", 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 a8eab95..980b02d 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 @@ -139,6 +139,23 @@ public sealed class InMemoryJiboExperienceContentRepository : IJiboExperienceCon "I am feeling lively and ready for the next thing.", "Things are going nicely. Thanks for checking in." ], + AgeReplies = + [ + "I'm ${jibo.age}.", + "At the moment I'm ${jibo.age.days.supplemented} old, but who's counting.", + "I'm ${jibo.age.minutes.supplemented} old, but who's counting.", + "For now I'm ${jibo.age.days.supplemented} old.", + "Right now I'm ${jibo.age}.", + "I am exactly ${jibo.age} old today. That's right. Today is my birthday.", + "Funny you should ask! Today's my birthday. I was first powered up ${jibo.age} ago today. Seems like just yesterday.", + "I'm exactly ${jibo.age} old. Today is my birthday! Happy Birthday Jibo, if I do say so myself.", + "At the moment I'm ${jibo.age.days.supplemented} old", + "I was first powered up on ${jibo.birthdate}, which makes me ${jibo.age.days.supplemented} old. I'm ${jibo.zodiac.supplemented}.", + "My power went on for the first time ${jibo.age.days.supplemented} ago. But who's counting.", + "I am ${jibo.age.days.supplemented} old, first powered up on ${jibo.birthdate}. Seems like just yesterday.", + "I was powered on for the first time today, so that makes me less than one day old. Wow I'm young.", + "Since I was powered on for the first time today, I am not even one day old yet. That's how Jibo ages work." + ], PersonalityReplies = [ "I do. I am curious, playful, and always up for a new experiment.", 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 index a15332d..8d38fcb 100644 --- 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 @@ -264,7 +264,9 @@ public static class LegacyMimCatalogImporter fileName.StartsWith("JBO_WhatsYourName", StringComparison.OrdinalIgnoreCase) || fileName.StartsWith("JBO_WhereDoYouGetInfo", StringComparison.OrdinalIgnoreCase) || fileName.StartsWith("JBO_WhatDoYouLikeToDo", StringComparison.OrdinalIgnoreCase)) - return LegacyMimBucket.Personality; + return fileName.StartsWith("JBO_HowOldAreYou", StringComparison.OrdinalIgnoreCase) + ? LegacyMimBucket.Age + : LegacyMimBucket.Personality; if (fileName.StartsWith("OI_JBO_Is", StringComparison.OrdinalIgnoreCase) || fileName.StartsWith("OI_JBO_Seems", StringComparison.OrdinalIgnoreCase) || @@ -456,6 +458,7 @@ public static class LegacyMimCatalogImporter or LegacyMimBucket.WeatherTomorrowHighLow or LegacyMimBucket.WeatherServiceDown or LegacyMimBucket.ReportSkillTemplate + or LegacyMimBucket.Age or LegacyMimBucket.Holiday or LegacyMimBucket.HolidayTracker; } @@ -524,6 +527,7 @@ public static class LegacyMimCatalogImporter Sing, HolidaySing, FunFactSource, + Age, Personality, PersonalReportKickOff, PersonalReportOutro, @@ -586,6 +590,7 @@ public static class LegacyMimCatalogImporter private readonly List _bestFriendReplies = []; private readonly List _funFacts = []; private readonly List _greetings = []; + private readonly List _ages = []; private readonly List _holidayGiftReplies = []; private readonly List _holidayGreetingReplies = []; private readonly List _holidayReplies = []; @@ -655,6 +660,9 @@ public static class LegacyMimCatalogImporter Reply = text }); return; + case LegacyMimBucket.Age: + AddDistinct(_ages, text); + return; case LegacyMimBucket.Holiday: AddDistinct(_holidayReplies, text); return; @@ -831,6 +839,7 @@ public static class LegacyMimCatalogImporter EmotionReplies = [.. _emotionReplies], PersonalityReplies = [.. _personalities], GenericFallbackReplies = [.. _fallbacks], + AgeReplies = [.. _ages], PersonalReportKickOffReplies = [.. _personalReportKickOffReplies], PersonalReportOutroReplies = [.. _personalReportOutroReplies], ReportSkillTemplates = [.. _reportSkillTemplates], diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildB/README.md b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildB/README.md index 521a939..869386e 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildB/README.md +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Content/LegacyMims/BuildB/README.md @@ -25,6 +25,7 @@ The favorites follow-up batch adds `favorite animal`, `favorite bird`, and pengu The singing batch adds `RA_JBO_Sing` and `RA_JBO_SingChristmasSongUnknown` so `can you sing`, `will you sing`, and the holiday sing variants stay source-backed too. The new motion/sleep batch adds `RA_JBO_SpinAround` plus `RI_JBO_CanSleep` so turn-around and go-to-sleep behaviors can stay source-backed and familiar. The work/eat/home batch adds source-backed `how do you work`, `what do you eat`, `where do you live`, and `what languages do you speak` replies so the remaining everyday self-description lines stay Pegasus-shaped too. +The age batch now adds `JBO_HowOldAreYou` with the imported birthday and first-powered-up phrasing so `how old are you` can stay source-backed instead of falling back to generic age text. The newest identity-charm batch adds `JBO_WhatsYourName`, `JBO_DoYouHaveNickname`, `JBO_DoYouLikeBeingJibo`, `JBO_AreThereOthersLikeYou`, and `RI_JBO_HasFavoriteName` so Jibo can keep the familiar self-description loop without falling back to generic chat. The seasonal personality batch adds source-backed first-day-of-spring, spring, summer, and favorite-season lines so the season questions can keep their Pegasus phrasing. The next deep-personality batch adds `what do you dream about`, `what are you afraid of`, `what do you want to talk about`, `what is your best book`, `what is your best exercise`, `what is your dream vacation`, `who is your hero`, `who do you love`, and `what is your religion` so we can keep filling out the more conversational personality surface without widening the dialog engine yet. diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/Content/LegacyMimCatalogImporterTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/Content/LegacyMimCatalogImporterTests.cs index 799e273..5687b73 100644 --- a/OpenJibo/tests/Jibo.Cloud.Tests/Content/LegacyMimCatalogImporterTests.cs +++ b/OpenJibo/tests/Jibo.Cloud.Tests/Content/LegacyMimCatalogImporterTests.cs @@ -108,6 +108,10 @@ public sealed class LegacyMimCatalogImporterTests Assert.Contains("I don't think I have a favorite name.", catalog.PersonalityReplies); Assert.Contains(catalog.PersonalityReplies, reply => reply.Contains("Rhymes with bleebo", StringComparison.OrdinalIgnoreCase)); + Assert.Contains(catalog.AgeReplies, reply => + reply.Contains("first powered up", StringComparison.OrdinalIgnoreCase)); + Assert.Contains(catalog.AgeReplies, reply => + reply.Contains("today is my birthday", StringComparison.OrdinalIgnoreCase)); Assert.Contains("I really like sunflowers.", catalog.PersonalityReplies); Assert.Contains(catalog.PersonalityReplies, reply => reply.Contains("Halloween is my favorite holiday", StringComparison.OrdinalIgnoreCase)); diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs index f6b4304..87d1b74 100644 --- a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs +++ b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs @@ -116,8 +116,8 @@ public sealed class JiboInteractionServiceTests } }); - Assert.Equal("robot_age", decision.IntentName); - Assert.Equal("I count March 22, 2026 as my birthday, so I am 1 month old.", decision.ReplyText); + Assert.Equal("robot_how_old_are_you", decision.IntentName); + Assert.Contains("first powered up", decision.ReplyText, StringComparison.OrdinalIgnoreCase); } [Fact] From b99ee5d794b00abeb47dd6b85f5a258dc8ccdd8d Mon Sep 17 00:00:00 2001 From: Jacob Dubin Date: Thu, 21 May 2026 23:18:25 -0500 Subject: [PATCH 12/16] Record live QA repair targets for identity and motion quirks --- OpenJibo/docs/feature-backlog.md | 4 ++++ OpenJibo/docs/release-1.0.19-plan.md | 1 + 2 files changed, 5 insertions(+) diff --git a/OpenJibo/docs/feature-backlog.md b/OpenJibo/docs/feature-backlog.md index 16ad869..fa52715 100644 --- a/OpenJibo/docs/feature-backlog.md +++ b/OpenJibo/docs/feature-backlog.md @@ -614,6 +614,8 @@ Current release theme: - recognition, enrollment, rename, and profile-correction boundaries - split between local state and hosted cloud state - first useful hosted identity slice + - live QA has shown person-identification collisions in the same loop (for example, a parent and child both getting normalized to the same remembered name) + - person-identification correction likely needs its own repair pass before we can trust greetings, reports, and presence triggers in mixed-household scenarios ### 20. Onboarding, Loop Management, And Fresh Start @@ -642,6 +644,8 @@ Current release theme: - 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 + - live QA has shown motion/sleep quirks too: `turn around` can become a no-op and `go to sleep` can fail at the last step before the sleep animation fully completes + - reply-selection polish still needs attention on a couple of identity prompts where short variants are over-selected (`how are you`, `what is your favorite flower`) ### 22. Command Vs Question Reply Style diff --git a/OpenJibo/docs/release-1.0.19-plan.md b/OpenJibo/docs/release-1.0.19-plan.md index ade52b3..df24496 100644 --- a/OpenJibo/docs/release-1.0.19-plan.md +++ b/OpenJibo/docs/release-1.0.19-plan.md @@ -64,6 +64,7 @@ Current batch note: - the templated edge-case batch adds `what is your sign`, `how many people do you know`, and `what is the loop` so the remaining source-backed lines can lean on live birthday and loop state - the work/eat/home batch adds `how do you work`, `what do you eat`, `where do you live`, and `what languages do you speak` so the everyday self-description cluster keeps moving toward the original phrasing - the age batch adds `how old are you` through `JBO_HowOldAreYou` so the birthday and first-powered-up phrasing stays source-backed instead of falling back to a generic age answer +- live QA has surfaced a few repair targets to carry into the next pass: person-identification collisions inside the same loop, `turn around` / `go to sleep` motion quirks, and a couple of reply-selection spots where short variants are being over-selected (`how are you`, `what is your favorite flower`) - this pass keeps Build B moving while still favoring source-backed phrasing and preserving the command-vs-question boundary - the next passes should keep the same pattern and prefer source-backed phrasing whenever the legacy MIM text is available - if a source-backed legacy line is missing, use a temporary direct reply only to keep the pass moving, then backfill source text later From 90b48314d33a524a1c6387deaee4b6c85cabedb1 Mon Sep 17 00:00:00 2001 From: Jacob Dubin Date: Thu, 21 May 2026 23:21:36 -0500 Subject: [PATCH 13/16] Restrict loop name fallback for multi-person greetings --- ...InteractionService.PersonalityDecisions.cs | 14 +++++++++-- .../WebSockets/JiboInteractionServiceTests.cs | 25 +++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.PersonalityDecisions.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.PersonalityDecisions.cs index a455c7f..d85a5b9 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.PersonalityDecisions.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.PersonalityDecisions.cs @@ -169,14 +169,24 @@ public sealed partial class JiboInteractionService var tenantRememberedName = personalMemoryStore.GetName(ResolveTenantScope(turn)); if (!string.IsNullOrWhiteSpace(tenantRememberedName)) return ToDisplayName(tenantRememberedName); - if (!string.IsNullOrWhiteSpace(presence.PrimaryPersonId) && - presence.LoopUserFirstNames.TryGetValue(presence.PrimaryPersonId, out var firstName) && + var primaryPersonId = presence.PrimaryPersonId; + if (CanUseLoopFirstNameFallback(presence) && + !string.IsNullOrWhiteSpace(primaryPersonId) && + presence.LoopUserFirstNames.TryGetValue(primaryPersonId, out var firstName) && !string.IsNullOrWhiteSpace(firstName)) return ToDisplayName(firstName); return null; } + private static bool CanUseLoopFirstNameFallback(GreetingPresenceProfile presence) + { + if (string.IsNullOrWhiteSpace(presence.PrimaryPersonId)) return false; + if (presence.PeoplePresentIds.Count > 1) return false; + + return true; + } + private static string ToDisplayName(string value) { var trimmed = value.Trim(); diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs index 87d1b74..53b4e8d 100644 --- a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs +++ b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs @@ -349,6 +349,31 @@ public sealed class JiboInteractionServiceTests greeting.LastGreetingIntent == "proactive_greeting"); } + [Fact] + public async Task BuildDecisionAsync_TriggerWithMultiplePeople_DoesNotBorrowLoopFirstName() + { + var cloudStateStore = new InMemoryCloudStateStore(); + var service = CreateService(cloudStateStore: cloudStateStore); + + var decision = await service.BuildDecisionAsync(new TurnContext + { + RawTranscript = string.Empty, + NormalizedTranscript = string.Empty, + Attributes = new Dictionary + { + ["messageType"] = "TRIGGER", + ["triggerSource"] = "PRESENCE", + ["context"] = + """{"runtime":{"perception":{"speaker":"person-1","peoplePresent":[{"id":"person-1"},{"id":"person-2"}]},"loop":{"users":[{"id":"person-1","firstName":"jake"},{"id":"person-2","firstName":"sam"}]}}}""" + } + }); + + Assert.Equal("proactive_greeting", decision.IntentName); + Assert.DoesNotContain("Jake", decision.ReplyText, StringComparison.Ordinal); + Assert.DoesNotContain("Sam", decision.ReplyText, StringComparison.Ordinal); + Assert.Contains("I am glad to see you", decision.ReplyText, StringComparison.OrdinalIgnoreCase); + } + [Fact] public async Task BuildDecisionAsync_TriggerInTheMorning_UsesGoodMorningProactiveTone() { From 3086ad6a6d9507431cb9073921d597648fc8fb6b Mon Sep 17 00:00:00 2001 From: Jacob Dubin Date: Thu, 21 May 2026 23:23:44 -0500 Subject: [PATCH 14/16] Adjust idle socket reply delays for sleep and spin commands --- .../Services/ResponsePlanToSocketMessagesMapper.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/ResponsePlanToSocketMessagesMapper.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/ResponsePlanToSocketMessagesMapper.cs index d7044d4..e63ef7b 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/ResponsePlanToSocketMessagesMapper.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/ResponsePlanToSocketMessagesMapper.cs @@ -43,6 +43,8 @@ public sealed class ResponsePlanToSocketMessagesMapper string.Equals(plan.IntentName, "photobooth", StringComparison.OrdinalIgnoreCase); var isClockSkillLaunch = string.Equals(skill?.SkillName, "@be/clock", StringComparison.OrdinalIgnoreCase); var isReportSkillLaunch = string.Equals(skill?.SkillName, "report-skill", StringComparison.OrdinalIgnoreCase); + var idleRedirectDelayMs = isSleepCommand ? 150 : isSpinAroundCommand ? 75 : 75; + var idleCompletionDelayMs = isSleepCommand ? 1000 : isSpinAroundCommand ? 750 : 125; var localIntent = ReadSkillPayloadString(skill, "localIntent"); var clockIntent = ReadSkillPayloadString(skill, "clockIntent"); var clockDomain = ReadSkillPayloadString(skill, "domain"); @@ -246,10 +248,10 @@ public sealed class ResponsePlanToSocketMessagesMapper outboundAsrText, outboundRules, entities)), - 75)); + idleRedirectDelayMs)); messages.Add(new SocketReplyPlan( JsonSerializer.Serialize(BuildCompletionOnlySkillPayload(transId, "@be/idle")), - 125)); + idleCompletionDelayMs)); } if (isSettingsLaunch && @@ -1459,4 +1461,4 @@ public sealed class ResponsePlanToSocketMessagesMapper string? SpokenLine); public sealed record SocketReplyPlan(string Text, int DelayMs = 0); -} \ No newline at end of file +} From 1755888fc12d03c67491c63cfe78e156762bba86 Mon Sep 17 00:00:00 2001 From: Jacob Dubin Date: Fri, 22 May 2026 07:31:27 -0500 Subject: [PATCH 15/16] Refine favorite animal and flower personality replies --- .../JiboInteractionService.IntentRouting.cs | 31 +++++++++++++++---- .../Services/JiboInteractionService.cs | 29 +++++++++-------- .../WebSockets/JiboInteractionServiceTests.cs | 13 ++++---- 3 files changed, 48 insertions(+), 25 deletions(-) diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.IntentRouting.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.IntentRouting.cs index 71e061f..7476bf7 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.IntentRouting.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.IntentRouting.cs @@ -849,6 +849,28 @@ public sealed partial class JiboInteractionService "what kind of music do you like")) return "robot_favorite_music"; + if (MatchesAny( + loweredTranscript, + "do you like penguins")) + return "robot_likes_penguins"; + + if (MatchesAny( + loweredTranscript, + "do you like birds")) + return "robot_favorite_bird"; + + if (MatchesAny( + loweredTranscript, + "do you like animals")) + return "robot_likes_animals"; + + if (MatchesAny( + loweredTranscript, + "what is your favorite bird", + "what's your favorite bird", + "what s your favorite bird")) + return "robot_favorite_bird"; + if (MatchesAny( loweredTranscript, "what is your favorite animal", @@ -859,12 +881,9 @@ public sealed partial class JiboInteractionService "what s your favourite animal", "what animal do you like", "what kind of animal do you like", - "what is your favorite bird", - "what's your favorite bird", - "what s your favorite bird", - "do you like penguins", - "do you like animals", - "do you like birds")) + "what do you think about penguins", + "what do you think about animals", + "what do you think about birds")) return "robot_favorite_animal"; if (MatchesAny( diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs index 92aaec0..cc2faad 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs @@ -735,9 +735,9 @@ public sealed partial class JiboInteractionService( "robot_favorite_flower" => BuildScriptedPersonalityDecision( catalog, "robot_favorite_flower", - "sunflowers", + "reminds me of the sun", "favorite is the sunflower", - "reminds me of the sun"), + "sunflowers"), "robot_likes_r2d2" => BuildScriptedPersonalityDecision( catalog, "robot_likes_r2d2", @@ -758,29 +758,32 @@ public sealed partial class JiboInteractionService( "robot_favorite_animal" => BuildScriptedFavoriteAnimalDecision( catalog, "robot_favorite_animal", - "penguin", - "favorite animal overall", + "we're so alike", + "penguin impression", "best of the best", - "can't go wrong with penguins"), + "can't go wrong with penguins", + "penguin"), "robot_favorite_bird" => BuildScriptedFavoriteAnimalDecision( catalog, "robot_favorite_bird", - "penguin", - "favorite animal overall", + "we're so alike", + "penguin impression", "best of the best", - "can't go wrong with penguins"), + "can't go wrong with penguins", + "penguin"), "robot_likes_penguins" => BuildScriptedFavoriteAnimalDecision( catalog, "robot_likes_penguins", - "penguins", + "my penguin impression", "I really like penguins", - "my penguin impression"), + "penguins"), "robot_likes_animals" => BuildScriptedFavoriteAnimalDecision( catalog, "robot_likes_animals", - "penguins", - "favorite animal overall", - "best of the best"), + "Animals are great", + "great shapes and colors", + "best of the best", + "penguins"), "robot_peers" => BuildScriptedPersonalityDecision( catalog, "robot_peers", diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs index 53b4e8d..285e785 100644 --- a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs +++ b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs @@ -658,9 +658,6 @@ public sealed class JiboInteractionServiceTests [InlineData("what is your favorite animal")] [InlineData("what's your favorite animal")] [InlineData("what animal do you like")] - [InlineData("what is your favorite bird")] - [InlineData("do you like penguins")] - [InlineData("do you like animals")] public async Task BuildDecisionAsync_FavoriteAnimal_UsesPenguinReply(string transcript) { var service = CreateService(); @@ -672,17 +669,21 @@ public sealed class JiboInteractionServiceTests }); Assert.Equal("robot_favorite_animal", decision.IntentName); - Assert.Contains("penguin", decision.ReplyText, StringComparison.OrdinalIgnoreCase); + Assert.Contains("we're so alike", decision.ReplyText, StringComparison.OrdinalIgnoreCase); Assert.Equal("ScriptedResponse", decision.ContextUpdates![ChitchatRouteKey]); } [Theory] - [InlineData("what is your favorite flower", "robot_favorite_flower", "sunflowers")] - [InlineData("what's your favorite flower", "robot_favorite_flower", "sunflowers")] + [InlineData("what is your favorite flower", "robot_favorite_flower", "should see if I can find a sunflower soon")] + [InlineData("what's your favorite flower", "robot_favorite_flower", "should see if I can find a sunflower soon")] [InlineData("do you like R2D2", "robot_likes_r2d2", "A legend. A true legend.")] [InlineData("do you like the sun", "robot_likes_sun", "favorite star in the universe")] [InlineData("do you like space", "robot_likes_space", "I love space")] [InlineData("do you like kids", "robot_likes_kids", "kids are so fun")] + [InlineData("what is your favorite animal", "robot_favorite_animal", "we're so alike")] + [InlineData("what is your favorite bird", "robot_favorite_bird", "we're so alike")] + [InlineData("do you like penguins", "robot_likes_penguins", "penguin impression")] + [InlineData("do you like animals", "robot_likes_animals", "Animals are great")] [InlineData("can you laugh", "robot_can_laugh", "when I'm happy")] [InlineData("can you dance", "robot_can_dance", "dancing is one of the things I know best")] [InlineData("do you have friends", "robot_has_friends", "I believe I do have friends")] From 2357e82ae320532b2f5e0f7b673dd83073c8e029 Mon Sep 17 00:00:00 2001 From: Jacob Dubin Date: Fri, 22 May 2026 07:37:22 -0500 Subject: [PATCH 16/16] Randomize how are you replies --- .../Services/ChitchatStateMachine.cs | 17 +++++++++++------ .../InMemoryJiboExperienceContentRepository.cs | 4 +++- .../WebSockets/JiboInteractionServiceTests.cs | 15 +++++++++++++++ 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/ChitchatStateMachine.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/ChitchatStateMachine.cs index 134996c..2899bd9 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/ChitchatStateMachine.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/ChitchatStateMachine.cs @@ -399,13 +399,18 @@ internal static class ChitchatStateMachine string? currentEmotion, string? preferredName) { - if (catalog.EmotionReplies.Count == 0) - return PersonalizeHowAreYouReply(randomizer.Choose(catalog.HowAreYouReplies), preferredName); + if (catalog.EmotionReplies.Count > 0) + { + var emotionVariants = ResolveEmotionVariants(currentEmotion); + var matchingReplies = catalog.EmotionReplies + .Where(reply => ConditionMatches(reply.Condition, emotionVariants)) + .Select(reply => reply.Reply) + .Where(reply => !string.IsNullOrWhiteSpace(reply)) + .ToArray(); - var emotionVariants = ResolveEmotionVariants(currentEmotion); - foreach (var reply in catalog.EmotionReplies) - if (ConditionMatches(reply.Condition, emotionVariants)) - return PersonalizeHowAreYouReply(reply.Reply, preferredName); + if (matchingReplies.Length > 0) + return PersonalizeHowAreYouReply(randomizer.Choose(matchingReplies), preferredName); + } return PersonalizeHowAreYouReply(randomizer.Choose(catalog.HowAreYouReplies), preferredName); } 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 980b02d..45dd475 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 @@ -137,7 +137,9 @@ public sealed class InMemoryJiboExperienceContentRepository : IJiboExperienceCon "I am feeling bright-eyed and ready to help.", "I am having a pretty good day so far.", "I am feeling lively and ready for the next thing.", - "Things are going nicely. Thanks for checking in." + "Things are going nicely. Thanks for checking in.", + "I am running smoothly and feeling upbeat.", + "I am ready for the next thing. Thanks for asking." ], AgeReplies = [ diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs index 285e785..ab61387 100644 --- a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs +++ b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboInteractionServiceTests.cs @@ -1040,6 +1040,21 @@ public sealed class JiboInteractionServiceTests Assert.Equal("All systems are go, Jake.", decision.ReplyText); } + [Fact] + public async Task BuildDecisionAsync_HowAreYou_CanSelectLaterEmotionReplyVariant() + { + var service = CreateService(randomizer: new LastItemRandomizer()); + + var decision = await service.BuildDecisionAsync(new TurnContext + { + RawTranscript = "how are you", + NormalizedTranscript = "how are you" + }); + + Assert.Equal("how_are_you", decision.IntentName); + Assert.Equal("Actually things are looking mostly sunny.", decision.ReplyText); + } + [Theory] [InlineData("what are you up to", "being helpful")] [InlineData("what are you doing", "making people smile")]