1 Commits

30 changed files with 1576 additions and 1466 deletions

View File

@@ -614,8 +614,6 @@ 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
@@ -640,12 +638,9 @@ 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
- 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
@@ -775,7 +770,7 @@ Current release theme:
### 28. Grocery List Capability (Requested Feature)
- Status: `in_progress`
- Status: `discovery`
- Tags: `content`, `docs`, `storage`
- Why now:
- directly requested by Jibo owners and fits memory + household utility roadmap
@@ -784,14 +779,13 @@ 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`
- 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
- Candidate delivery paths:
- native lightweight list skill (fastest user value)
- integration-backed list orchestration (long-term richer ecosystem fit)
- Exit criteria:
- 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
- clear decision on MVP path
- first schema for list items + ownership scope
- initial voice flows and follow-up intent handling defined
### 29. Legacy MIM Personality Import Ladder
@@ -895,10 +889,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
- 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
@@ -984,9 +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
- 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

View File

@@ -77,18 +77,3 @@ 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

View File

@@ -6,8 +6,6 @@ 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`
@@ -58,13 +56,6 @@ 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
- 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
- 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
@@ -93,8 +84,6 @@ 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
- 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

View File

@@ -1,101 +0,0 @@
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
}
}

View File

@@ -16,8 +16,6 @@ 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`

View File

@@ -0,0 +1,317 @@
using Jibo.Cloud.Application.Abstractions;
using Jibo.Cloud.Application.Services;
using Jibo.Cloud.Domain.Models;
using Microsoft.AspNetCore.Mvc;
namespace Jibo.Cloud.Api.Controllers;
[ApiController]
[Route("api/panel")]
public class WebPanelController(
ICloudStateStore stateStore,
IConfiguration configuration) : ControllerBase
{
private static readonly DateTimeOffset _startTime = DateTimeOffset.UtcNow;
[HttpGet("status")]
public ActionResult GetStatus()
{
var persistenceInfo = stateStore.GetPersistenceStateInfo();
var account = stateStore.GetAccount();
var robot = stateStore.GetRobot();
return Ok(new
{
version = OpenJiboCloudBuildInfo.Version,
uptime = (DateTimeOffset.UtcNow - _startTime).ToString(@"hh\:mm\:ss"),
startTime = _startTime.ToString("o"),
persistence = new
{
schemaVersion = persistenceInfo.SchemaVersion,
revision = persistenceInfo.Revision,
lastLoaded = persistenceInfo.LastLoadedUtc?.ToString("o"),
lastSaved = persistenceInfo.LastSavedUtc?.ToString("o")
},
account = new
{
accountId = account.AccountId,
firstName = account.FirstName,
lastName = account.LastName
},
robot = new
{
deviceId = robot.DeviceId,
robotId = robot.RobotId,
friendlyName = robot.FriendlyName,
firmwareVersion = robot.FirmwareVersion,
applicationVersion = robot.ApplicationVersion
},
configuration = new
{
webPanelEnabled = configuration.GetValue<bool>("OpenJibo:WebPanel:Enabled"),
refreshIntervalSeconds = configuration.GetValue<int>("OpenJibo:WebPanel:RefreshIntervalSeconds"),
allowRemoteAccess = configuration.GetValue<bool>("OpenJibo:WebPanel:AllowRemoteAccess")
}
});
}
[HttpGet("sessions")]
public ActionResult GetSessions()
{
// Since ICloudStateStore doesnt have a GetAllSessions method for now ill just return a empty list - TO BE UPGRADED!!
return Ok(new
{
sessions = Array.Empty<object>(),
count = 0
});
}
[HttpGet("robots")]
public ActionResult GetRobots()
{
var robot = stateStore.GetRobot();
var robotProfile = stateStore.GetRobotProfile();
return Ok(new
{
robots = new[]
{
new
{
deviceId = robot.DeviceId,
robotId = robot.RobotId,
friendlyName = robot.FriendlyName,
firmwareVersion = robot.FirmwareVersion,
applicationVersion = robot.ApplicationVersion,
profile = new
{
robotId = robotProfile.RobotId,
connectedAt = robotProfile.UpdatedUtc.ToString("o"),
platform = robotProfile.Payload?.TryGetValue("platform", out var platformValue) == true ? platformValue?.ToString() : null,
serialNumber = robotProfile.Payload?.TryGetValue("serialNumber", out var serialValue) == true ? serialValue?.ToString() : null
}
}
},
count = 1
});
}
[HttpGet("health")]
public ActionResult GetHealth()
{
var persistenceInfo = stateStore.GetPersistenceStateInfo();
return Ok(new
{
status = "healthy",
timestamp = DateTimeOffset.UtcNow.ToString("o"),
checks = new
{
persistence = new
{
status = persistenceInfo.LastSavedUtc.HasValue ? "ok" : "warning",
lastSaved = persistenceInfo.LastSavedUtc?.ToString("o"),
revision = persistenceInfo.Revision
},
stateStore = new
{
status = "ok",
type = "InMemoryCloudStateStore"
}
}
});
}
[HttpPost("state/save")]
public ActionResult SaveState()
{
try
{
stateStore.SavePersistedState();
return Ok(new { success = true, message = "State saved successfully" });
}
catch (Exception ex)
{
return BadRequest(new { success = false, message = ex.Message });
}
}
[HttpPost("state/reload")]
public ActionResult ReloadState()
{
try
{
stateStore.LoadPersistedState();
return Ok(new { success = true, message = "State reloaded successfully" });
}
catch (Exception ex)
{
return BadRequest(new { success = false, message = ex.Message });
}
}
[HttpGet("info")]
public ActionResult GetInfo()
{
var robot = stateStore.GetRobot();
var persistenceInfo = stateStore.GetPersistenceStateInfo();
return Ok(new
{
serverId = Environment.MachineName,
serverName = robot.FriendlyName ?? "OpenJibo Server",
endpoint = Request.Host.Value,
version = OpenJiboCloudBuildInfo.Version,
startTime = _startTime.ToString("o"),
uptime = (DateTimeOffset.UtcNow - _startTime).TotalSeconds,
robotId = robot.RobotId,
deviceId = robot.DeviceId,
stateRevision = persistenceInfo.Revision,
lastStateSave = persistenceInfo.LastSavedUtc?.ToString("o")
});
}
[HttpGet("metrics")]
public ActionResult GetMetrics()
{
var persistenceInfo = stateStore.GetPersistenceStateInfo();
var robot = stateStore.GetRobot();
var loops = stateStore.GetLoops();
var people = stateStore.GetPeople();
var media = stateStore.ListMedia();
var updates = stateStore.ListUpdates();
var backups = stateStore.GetBackups();
return Ok(new
{
timestamp = DateTimeOffset.UtcNow.ToString("o"),
server = new
{
version = OpenJiboCloudBuildInfo.Version,
uptime = (DateTimeOffset.UtcNow - _startTime).TotalSeconds,
startTime = _startTime.ToString("o")
},
state = new
{
revision = persistenceInfo.Revision,
lastLoaded = persistenceInfo.LastLoadedUtc?.ToString("o"),
lastSaved = persistenceInfo.LastSavedUtc?.ToString("o"),
schemaVersion = persistenceInfo.SchemaVersion
},
robot = new
{
robotId = robot.RobotId,
deviceId = robot.DeviceId,
firmwareVersion = robot.FirmwareVersion,
applicationVersion = robot.ApplicationVersion
},
counts = new
{
loops = loops.Count,
people = people.Count,
media = media.Count,
updates = updates.Count,
backups = backups.Count
}
});
}
private static List<object> _serverLogs = new();
private static readonly object _logsLock = new();
[HttpGet("logs")]
public ActionResult GetLogs(long since = 0)
{
lock (_logsLock)
{
// Add some test logs if empty
if (_serverLogs.Count == 0)
{
_serverLogs.Add(new { timestamp = DateTimeOffset.UtcNow.AddSeconds(-10).ToUnixTimeMilliseconds(), level = "info", message = "Server running normally" });
_serverLogs.Add(new { timestamp = DateTimeOffset.UtcNow.AddSeconds(-5).ToUnixTimeMilliseconds(), level = "info", message = "Health check passed" });
_serverLogs.Add(new { timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), level = "info", message = "Web panel accessed" });
}
// Filter logs
var filteredLogs = _serverLogs
.Where(log => (long)((dynamic)log).timestamp > since)
.ToList();
return Ok(new
{
logs = filteredLogs,
count = filteredLogs.Count
});
}
}
[HttpGet("endpoints")]
public ActionResult GetEndpoints()
{
var multiPortEnabled = configuration.GetValue<bool>("OpenJibo:MultiPortMode:Enabled");
if (multiPortEnabled)
{
return Ok(new
{
mode = "multi-port",
enabled = true,
ports = new
{
api = configuration.GetValue<int>("OpenJibo:MultiPortMode:Ports:Api"),
apiSocket = configuration.GetValue<int>("OpenJibo:MultiPortMode:Ports:ApiSocket"),
neoHubListen = configuration.GetValue<int>("OpenJibo:MultiPortMode:Ports:NeoHubListen"),
neoHubProactive = configuration.GetValue<int>("OpenJibo:MultiPortMode:Ports:NeoHubProactive"),
webPanel = configuration.GetValue<int>("OpenJibo:MultiPortMode:Ports:WebPanel")
},
robotConfig = new
{
webCoreServerPort = configuration.GetValue<int>("OpenJibo:MultiPortMode:Ports:Api"),
jetstreamServiceServerPort = configuration.GetValue<int>("OpenJibo:MultiPortMode:Ports:Api"),
jetstreamServiceRegistryPort = configuration.GetValue<int>("OpenJibo:MultiPortMode:Ports:ApiSocket"),
hubClientHubPort = configuration.GetValue<int>("OpenJibo:MultiPortMode:Ports:NeoHubListen"),
hubClientProactivePort = configuration.GetValue<int>("OpenJibo:MultiPortMode:Ports:NeoHubProactive")
}
});
}
else
{
return Ok(new
{
mode = "dns-based",
enabled = false,
description = "Server uses DNS-based routing. Configure robot hostnames to point to this server.",
hosts = new
{
api = "api.jibo.com",
apiSocket = "api-socket.jibo.com",
neoHub = "neo-hub.jibo.com"
}
});
}
}
[HttpPost("endpoints/multi-port/enable")]
public ActionResult EnableMultiPortMode([FromBody] MultiPortConfigRequest request)
{
try
{
// This is a placeholder for future web panel integration
// For now, users need to manually edit appsettings.json
return Ok(new { success = false, message = "Please manually edit appsettings.json to enable multi-port mode. Set OpenJibo:MultiPortMode:Enabled to true and configure the ports." });
}
catch (Exception ex)
{
return BadRequest(new { success = false, message = ex.Message });
}
}
}
public class MultiPortConfigRequest
{
public int? Api { get; set; }
public int? ApiSocket { get; set; }
public int? NeoHubListen { get; set; }
public int? NeoHubProactive { get; set; }
public int? WebPanel { get; set; }
}

View File

@@ -9,12 +9,53 @@ using Jibo.Cloud.Infrastructure.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenJiboCloud(builder.Configuration);
builder.Services.AddControllers();
// Add CORS for multi-server controller support (for future api support so we can hook up azure / aws / firebase / pocketbase) <=====================================================================
builder.Services.AddCors(options =>
{
options.AddPolicy("WebPanelPolicy", policy =>
{
var allowedOrigins = builder.Configuration["OpenJibo:WebPanel:AllowedOrigins"]?.Split(',', StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty<string>();
if (allowedOrigins.Length > 0)
{
policy.WithOrigins(allowedOrigins)
.AllowAnyMethod()
.AllowAnyHeader();
}
else
{
// Default: allow localhost for development
policy.WithOrigins("http://localhost:3380", "http://localhost:3000", "http://localhost:8080")
.AllowAnyMethod()
.AllowAnyHeader();
}
});
});
var app = builder.Build();
app.Logger.LogInformation("Starting Open Jibo Cloud Api version {Version}", OpenJiboCloudBuildInfo.Version);
app.UseWebSockets();
app.UseCors("WebPanelPolicy");
app.UseDefaultFiles();
app.UseStaticFiles();
app.MapControllers();
// Serve web panel index.html for root requests on port 3380 <=====================================================================
app.Use(async (context, next) =>
{
if (context.Request.Path == "/" && (context.Request.Host.Port == 3380 ||
(context.Request.Host.Value != null && context.Request.Host.Value.Contains("3380"))))
{
context.Response.ContentType = "text/html";
await context.Response.SendFileAsync(Path.Combine(app.Environment.WebRootPath, "index.html"));
return;
}
await next();
});
app.Use(async (context, next) =>
{
@@ -24,7 +65,7 @@ app.Use(async (context, next) =>
return;
}
var kind = ResolveSocketKind(context.Request.Host.Host, context.Request.Path);
var kind = ResolveSocketKind(context.Request.Host.Host, context.Request.Path, context.Request.Host.Port, builder.Configuration);
var token = ResolveToken(context.Request);
switch (kind)
{
@@ -127,6 +168,32 @@ app.MapGet("/health", () => Results.Json(new
app.MapMethods("/{**path}", ["GET", "POST", "PUT"], async (HttpContext context, JiboCloudProtocolService service,
IProtocolTelemetrySink telemetrySink, CancellationToken cancellationToken) =>
{
// For web panel port, **try** to serve static files <=====================================================================
if (context.Request.Host.Port == 3380 ||
(context.Request.Host.Value != null && context.Request.Host.Value.Contains("3380")))
{
var path = context.Request.Path.Value ?? "";
var filePath = Path.Combine(app.Environment.WebRootPath, path.TrimStart('/'));
if (File.Exists(filePath))
{
var contentType = Path.GetExtension(filePath) switch
{
".css" => "text/css",
".js" => "application/javascript",
".html" => "text/html",
_ => "application/octet-stream"
};
context.Response.ContentType = contentType;
await context.Response.SendFileAsync(filePath);
return;
}
context.Response.StatusCode = 404;
return;
}
var envelope = await BuildEnvelopeAsync(context, cancellationToken);
var result = await service.DispatchAsync(envelope, cancellationToken);
await telemetrySink.RecordAsync(envelope, result, cancellationToken);
@@ -187,8 +254,21 @@ static async Task<ProtocolEnvelope> BuildEnvelopeAsync(HttpContext context, Canc
};
}
static string ResolveSocketKind(string host, PathString path)
static string ResolveSocketKind(string host, PathString path, int? port, IConfiguration configuration)
{
var multiPortEnabled = configuration.GetValue<bool>("OpenJibo:MultiPortMode:Enabled");
if (multiPortEnabled && port.HasValue)
{
var apiSocketPort = configuration.GetValue<int>("OpenJibo:MultiPortMode:Ports:ApiSocket");
var neoHubListenPort = configuration.GetValue<int>("OpenJibo:MultiPortMode:Ports:NeoHubListen");
var neoHubProactivePort = configuration.GetValue<int>("OpenJibo:MultiPortMode:Ports:NeoHubProactive");
if (port == apiSocketPort) return "api-socket";
if (port == neoHubProactivePort) return "neo-hub-proactive";
if (port == neoHubListenPort) return "neo-hub-listen";
}
if (host.Equals("api-socket.jibo.com", StringComparison.OrdinalIgnoreCase)) return "api-socket";
if (host.Equals("neo-hub.jibo.com", StringComparison.OrdinalIgnoreCase) &&

View File

@@ -1,5 +1,39 @@
{
"Kestrel": {
"Endpoints": {
"Http": {
"Url": "http://localhost:5000"
},
"ApiSocket": {
"Url": "http://localhost:5001"
},
"NeoHubListen": {
"Url": "http://localhost:5002"
},
"NeoHubProactive": {
"Url": "http://localhost:5003"
},
"WebPanel": {
"Url": "http://localhost:3380"
}
}
},
"OpenJibo": {
"MultiPortMode": {
"Enabled": true,
"Ports": {
"Api": 5000,
"ApiSocket": 5001,
"NeoHubListen": 5002,
"NeoHubProactive": 5003,
"WebPanel": 3380
}
},
"WebPanel": {
"Enabled": true,
"RefreshIntervalSeconds": 5,
"AllowRemoteAccess": false
},
"Telemetry": {
"Enabled": true,
"ExportFixtures": true,

View File

@@ -0,0 +1,399 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #363636;
color: #ffffff;
min-height: 100vh;
margin: 0;
}
.app-container {
display: flex;
flex-direction: row;
height: 100vh;
overflow: hidden;
}
/* Custom Sidebar with Material Design styling */
.sidebar {
width: 280px;
height: 100%;
background: #212121;
flex-shrink: 0;
display: flex;
flex-direction: column;
}
.sidebar-header {
padding: 16px;
font-size: 20px;
font-weight: 500;
color: #fbfbfb;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
cursor: pointer;
transition: background-color 0.2s ease;
color: #dfdfdf;
font-size: 20px;
margin: 4px 8px;
}
.nav-item:hover {
background-color: rgba(98, 0, 238, 0.08);
}
.nav-item.active {
background-color: rgba(98, 0, 238, 0.12);
color: #df62ff;
}
.nav-icon {
font-size: 40px;
}
.nav-text {
flex: 1;
}
.header {
background: #6200ee;
color: #ffffff;
padding: 16px 24px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.title {
font-size: 20px;
font-weight: 500;
color: #ffffff;
}
/* Main Content Area */
.main-wrapper {
display: flex;
flex: 1;
flex-direction: column;
overflow: hidden;
}
.main-content {
flex: 1;
padding: 16px;
overflow-y: auto;
display: none;
}
.main-content.active {
display: block;
}
/* Material Web Card */
md-elevated-card {
margin-bottom: 16px;
max-width: 100%;
}
.card-content {
padding: 16px;
}
.status-grid,
.config-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.status-item,
.config-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.status-label,
.config-label,
.detail-label,
.check-label,
.health-label,
.count-label {
font-size: 12px;
color: #666666;
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 500;
}
.status-value,
.config-value,
.detail-value,
.check-value,
.health-value,
.count-value {
font-size: 14px;
color: #111111;
font-weight: 500;
}
.robot-item {
background: #1e1e1e;
border-radius: 4px;
padding: 16px;
}
.robot-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid #e0e0e0;
}
.robot-name {
font-size: 16px;
font-weight: 500;
color: #ffffff;
}
.robot-id {
font-size: 12px;
color: #666666;
font-family: monospace;
}
.robot-details {
display: grid;
gap: 8px;
}
.detail-item {
display: flex;
justify-content: space-between;
align-items: center;
}
.session-count {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 12px;
background: #f5f5f5;
border-radius: 4px;
}
.count-value {
font-size: 24px;
font-weight: 700;
color: #6200ee;
}
.sessions-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.empty-state {
text-align: center;
color: #666666;
font-style: italic;
padding: 20px;
}
.health-status {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 12px;
background: #111111;
border-radius: 4px;
}
.health-value {
font-size: 18px;
font-weight: 500;
color: #4caf50;
}
.health-value.warning {
color: #ff9800;
}
.health-value.error {
color: #f44336;
}
.health-checks {
display: flex;
flex-direction: column;
gap: 12px;
}
.health-check {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: #f5f5f5;
border-radius: 4px;
}
.check-value {
color: #4caf50;
}
.check-value.warning {
color: #ff9800;
}
.check-value.error {
color: #f44336;
}
/* Status indicator */
.status-indicator {
display: flex;
align-items: center;
gap: 8px;
}
.status-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: #9e9e9e;
animation: pulse 2s infinite;
}
.status-dot.connected {
background: #4caf50;
}
.status-dot.disconnected {
background: #f44336;
animation: none;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.status-text {
font-size: 14px;
color: #ffffff;
font-weight: 500;
}
/* Material Web Button warning variant */
md-filled-button.warning {
--md-filled-button-container-color: #f44336;
--md-filled-button-label-text-color: #ffffff;
}
.controls-grid {
display: flex;
gap: 8px;
}
/* Terminal Styles */
.terminal-container {
background: #1e1e1e;
border-radius: 4px;
padding: 16px;
height: calc(100vh - 120px);
overflow: hidden;
display: flex;
flex-direction: column;
}
.terminal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #333;
}
.terminal-title {
font-size: 16px;
font-weight: 500;
color: #d4d4d4;
}
.terminal-controls {
display: flex;
gap: 8px;
}
.terminal-output {
flex: 1;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.4;
color: #d4d4d4;
padding: 8px;
background: #0d0d0d;
border-radius: 4px;
}
.log-entry {
margin-bottom: 4px;
white-space: pre-wrap;
word-break: break-all;
}
.log-entry.info {
color: #5bc0de;
}
.log-entry.warning {
color: #f0ad4e;
}
.log-entry.error {
color: #d9534f;
}
.log-entry.debug {
color: #777;
}
@media (max-width: 768px) {
.app-container {
flex-direction: column;
}
md-navigation-drawer {
width: 100%;
height: auto;
}
.main-content {
grid-template-columns: 1fr;
}
.status-grid,
.config-grid {
grid-template-columns: 1fr;
}
}

View File

@@ -0,0 +1,217 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OpenJibo Cloud Panel</title>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link rel="stylesheet" href="/css/panel.css">
<script type="importmap">
{
"imports": {
"@material/web/": "https://esm.run/@material/web/"
}
}
</script>
<script type="module">
import '@material/web/all.js';
import {styles as typescaleStyles} from '@material/web/typography/md-typescale-styles.js';
document.adoptedStyleSheets.push(typescaleStyles.styleSheet);
</script>
</head>
<body>
<div class="app-container">
<nav class="sidebar">
<div class="sidebar-header">OpenJibo Panel</div>
<div class="nav-item active" data-tab="dashboard" onclick="switchTab('dashboard')">
<span class="material-icons nav-icon">dashboard</span>
<span class="nav-text">Dashboard</span>
</div>
<div class="nav-item" data-tab="robots" onclick="switchTab('robots')">
<span class="material-icons nav-icon">smart_toy</span>
<span class="nav-text">Robots</span>
</div>
<div class="nav-item" data-tab="sessions" onclick="switchTab('sessions')">
<span class="material-icons nav-icon">people</span>
<span class="nav-text">Sessions</span>
</div>
<div class="nav-item" data-tab="health" onclick="switchTab('health')">
<span class="material-icons nav-icon">favorite</span>
<span class="nav-text">Health</span>
</div>
<div class="nav-item" data-tab="config" onclick="switchTab('config')">
<span class="material-icons nav-icon">settings</span>
<span class="nav-text">Config</span>
</div>
<div class="nav-item" data-tab="terminal" onclick="switchTab('terminal')">
<span class="material-icons nav-icon">terminal</span>
<span class="nav-text">Terminal</span>
</div>
</nav>
<div class="main-wrapper">
<header class="header">
<h1 class="title">OpenJibo Cloud Panel Test Thingy</h1>
<div class="status-indicator">
<span class="status-dot" id="connectionStatus"></span>
<span class="status-text" id="connectionText">Connecting...</span>
</div>
</header>
<!-- Dashboard Tab -->
<div id="tab-dashboard" class="main-content active">
<md-elevated-card>
<div class="card-content">
<h2 class="md-typescale-headline-small">Server Status</h2>
<div class="status-grid">
<div class="status-item">
<span class="status-label">Version</span>
<span class="status-value" id="serverVersion">-</span>
</div>
<div class="status-item">
<span class="status-label">Uptime</span>
<span class="status-value" id="serverUptime">-</span>
</div>
<div class="status-item">
<span class="status-label">Started</span>
<span class="status-value" id="serverStartTime">-</span>
</div>
<div class="status-item">
<span class="status-label">Last Saved</span>
<span class="status-value" id="lastSaved">-</span>
</div>
</div>
</div>
</md-elevated-card>
<md-elevated-card>
<div class="card-content">
<h2 class="md-typescale-headline-small">Server Quick Controls</h2>
<div class="controls-grid">
<md-filled-button onclick="saveState()">Save State</md-filled-button>
<md-filled-button class="warning" onclick="reloadState()">Reload State</md-filled-button>
</div>
</div>
</md-elevated-card>
</div>
<!-- Robots Tab -->
<div id="tab-robots" class="main-content">
<md-elevated-card>
<div class="card-content">
<h2 class="md-typescale-headline-small">Will Have Connected Robots</h2>
<div id="robotsList">
<div class="robot-item">
<div class="robot-info">
<span class="robot-name" id="robotName">-</span>
<span class="robot-id" id="robotId">-</span>
</div>
<div class="robot-details">
<div class="detail-item">
<span class="detail-label">Device ID:</span>
<span class="detail-value" id="deviceId">-</span>
</div>
<div class="detail-item">
<span class="detail-label">Firmware:</span>
<span class="detail-value" id="firmwareVersion">-</span>
</div>
<div class="detail-item">
<span class="detail-label">App Version:</span>
<span class="detail-value" id="appVersion">-</span>
</div>
<div class="detail-item">
<span class="detail-label">Platform:</span>
<span class="detail-value" id="platform">-</span>
</div>
</div>
</div>
</div>
</div>
</md-elevated-card>
</div>
<!-- Sessions Tab -->
<div id="tab-sessions" class="main-content">
<md-elevated-card>
<div class="card-content">
<h2 class="md-typescale-headline-small">Active Sessions</h2>
<div class="session-count">
<span class="count-label">Active Sessions:</span>
<span class="count-value" id="sessionCount">0</span>
</div>
<div id="sessionsList" class="sessions-list">
<p class="empty-state">No active sessions</p>
</div>
</div>
</md-elevated-card>
</div>
<!-- Health Tab -->
<div id="tab-health" class="main-content">
<md-elevated-card>
<div class="card-content">
<h2 class="md-typescale-headline-small">Health Check</h2>
<div class="health-status">
<span class="health-label">Overall Status:</span>
<span class="health-value" id="healthStatus">-</span>
</div>
<div class="health-checks">
<div class="health-check">
<span class="check-label">Persistence:</span>
<span class="check-value" id="persistenceStatus">-</span>
</div>
<div class="health-check">
<span class="check-label">State Store:</span>
<span class="check-value" id="stateStoreStatus">-</span>
</div>
</div>
</div>
</md-elevated-card>
</div>
<!-- Config Tab -->
<div id="tab-config" class="main-content">
<md-elevated-card>
<div class="card-content">
<h2 class="md-typescale-headline-small">Will be Configurator</h2>
<div class="config-grid">
<div class="config-item">
<span class="config-label">Web Panel Enabled</span>
<span class="config-value" id="webPanelEnabled">-</span>
</div>
<div class="config-item">
<span class="config-label">Refresh Interval</span>
<span class="config-value" id="refreshInterval">-</span>
</div>
<div class="config-item">
<span class="config-label">Remote Access</span>
<span class="config-value" id="remoteAccess">-</span>
</div>
</div>
</div>
</md-elevated-card>
</div>
<!-- Terminal Tab -->
<div id="tab-terminal" class="main-content">
<div class="terminal-container">
<div class="terminal-header">
<span class="terminal-title">Server Logs</span>
<div class="terminal-controls">
<md-outlined-button onclick="clearTerminal()">Clear</md-outlined-button>
<md-outlined-button onclick="toggleAutoScroll()">Auto Scroll</md-outlined-button>
</div>
</div>
<div class="terminal-output" id="terminalOutput">
<div class="log-entry">Waiting for server logs...</div>
</div>
</div>
</div>
</div>
</div>
<script src="/js/panel.js"></script>
</body>
</html>

View File

@@ -0,0 +1,403 @@
const API_BASE = '/api/panel';
let refreshInterval = 5000; // Default 5 seconds
let refreshTimer = null;
let isConnected = false;
let autoScrollEnabled = true;
let currentTab = 'dashboard';
// Initialize the panel
async function init() {
try {
// Fetch configuration first to get refresh interval
const status = await fetchStatus();
if (status && status.configuration) {
refreshInterval = (status.configuration.refreshIntervalSeconds || 5) * 1000;
}
// Initial data load
await refreshAll();
// Set up auto-refresh
startAutoRefresh();
// Update connection status
setConnectionStatus(true);
// Start terminal if on terminal tab
if (currentTab === 'terminal') {
startTerminal();
}
} catch (error) {
console.error('Failed to initialize panel:', error);
setConnectionStatus(false);
// Retry after 5 seconds
setTimeout(init, 5000);
}
}
// Tab switching
function switchTab(tabName) {
currentTab = tabName;
// Update navigation items
document.querySelectorAll('.nav-item').forEach(item => {
item.classList.remove('active');
if (item.dataset.tab === tabName) {
item.classList.add('active');
}
});
// Update tab content
document.querySelectorAll('.main-content').forEach(content => {
content.classList.remove('active');
});
const targetTab = document.getElementById(`tab-${tabName}`);
if (targetTab) {
targetTab.classList.add('active');
}
// Start terminal if switching to terminal tab
if (tabName === 'terminal') {
startTerminal();
} else {
stopTerminal();
}
}
// Fetch server status
async function fetchStatus() {
try {
const response = await fetch(`${API_BASE}/status`);
if (!response.ok) throw new Error('Failed to fetch status');
return await response.json();
} catch (error) {
console.error('Error fetching status:', error);
return null;
}
}
// Fetch sessions
async function fetchSessions() {
try {
const response = await fetch(`${API_BASE}/sessions`);
if (!response.ok) throw new Error('Failed to fetch sessions');
return await response.json();
} catch (error) {
console.error('Error fetching sessions:', error);
return null;
}
}
// Fetch robots
async function fetchRobots() {
try {
const response = await fetch(`${API_BASE}/robots`);
if (!response.ok) throw new Error('Failed to fetch robots');
return await response.json();
} catch (error) {
console.error('Error fetching robots:', error);
return null;
}
}
// Fetch health
async function fetchHealth() {
try {
const response = await fetch(`${API_BASE}/health`);
if (!response.ok) throw new Error('Failed to fetch health');
return await response.json();
} catch (error) {
console.error('Error fetching health:', error);
return null;
}
}
// Refresh all data
async function refreshAll() {
const [status, sessions, robots, health] = await Promise.all([
fetchStatus(),
fetchSessions(),
fetchRobots(),
fetchHealth()
]);
if (status) updateStatus(status);
if (sessions) updateSessions(sessions);
if (robots) updateRobots(robots);
if (health) updateHealth(health);
updateLastRefresh();
}
// Update server status UI
function updateStatus(data) {
document.getElementById('serverVersion').textContent = data.version || '-';
document.getElementById('serverUptime').textContent = data.uptime || '-';
document.getElementById('serverStartTime').textContent = formatDateTime(data.startTime) || '-';
document.getElementById('lastSaved').textContent = formatDateTime(data.persistence?.lastSaved) || '-';
if (data.configuration) {
document.getElementById('webPanelEnabled').textContent =
data.configuration.webPanelEnabled ? 'Yes' : 'No';
document.getElementById('refreshInterval').textContent =
`${data.configuration.refreshIntervalSeconds}s`;
document.getElementById('remoteAccess').textContent =
data.configuration.allowRemoteAccess ? 'Yes' : 'No';
}
}
// Update sessions UI
function updateSessions(data) {
const count = data.count || 0;
document.getElementById('sessionCount').textContent = count;
const sessionsList = document.getElementById('sessionsList');
if (count === 0 || !data.sessions || data.sessions.length === 0) {
sessionsList.innerHTML = '<p class="empty-state">No active sessions</p>';
} else {
sessionsList.innerHTML = data.sessions.map(session => `
<div class="session-item">
<div class="session-info">
<span class="session-kind">${session.kind || 'Unknown'}</span>
<span class="session-token">${session.token || 'No token'}</span>
</div>
<div class="session-time">
Last seen: ${formatDateTime(session.lastSeenUtc)}
</div>
</div>
`).join('');
}
}
// Update robots UI
function updateRobots(data) {
if (data.robots && data.robots.length > 0) {
const robot = data.robots[0];
document.getElementById('robotName').textContent = robot.friendlyName || 'Unknown Robot';
document.getElementById('robotId').textContent = robot.robotId || '-';
document.getElementById('deviceId').textContent = robot.deviceId || '-';
document.getElementById('firmwareVersion').textContent = robot.firmwareVersion || '-';
document.getElementById('appVersion').textContent = robot.applicationVersion || '-';
document.getElementById('platform').textContent = robot.profile?.platform || '-';
}
}
// Update health UI
function updateHealth(data) {
const healthStatus = document.getElementById('healthStatus');
healthStatus.textContent = data.status || '-';
healthStatus.className = 'health-value';
if (data.status === 'healthy') {
healthStatus.classList.add('success');
} else if (data.status === 'warning') {
healthStatus.classList.add('warning');
} else {
healthStatus.classList.add('error');
}
if (data.checks) {
const persistenceStatus = document.getElementById('persistenceStatus');
persistenceStatus.textContent = data.checks.persistence?.status || '-';
persistenceStatus.className = 'check-value';
if (data.checks.persistence?.status === 'ok') {
persistenceStatus.classList.add('success');
} else if (data.checks.persistence?.status === 'warning') {
persistenceStatus.classList.add('warning');
} else {
persistenceStatus.classList.add('error');
}
const stateStoreStatus = document.getElementById('stateStoreStatus');
stateStoreStatus.textContent = data.checks.stateStore?.status || '-';
stateStoreStatus.className = 'check-value';
if (data.checks.stateStore?.status === 'ok') {
stateStoreStatus.classList.add('success');
} else {
stateStoreStatus.classList.add('error');
}
}
}
// Update connection status indicator
function setConnectionStatus(connected) {
isConnected = connected;
const dot = document.getElementById('connectionStatus');
const text = document.getElementById('connectionText');
dot.className = 'status-dot ' + (connected ? 'connected' : 'disconnected');
text.textContent = connected ? 'Connected' : 'Disconnected';
}
// Update last refresh time
function updateLastRefresh() {
document.getElementById('lastUpdate').textContent = formatDateTime(new Date().toISOString());
updateNextRefresh();
}
// Update next refresh countdown
function updateNextRefresh() {
const nextRefresh = document.getElementById('nextRefresh');
const seconds = Math.ceil(refreshInterval / 1000);
nextRefresh.textContent = `${seconds}s`;
}
// Start auto-refresh
function startAutoRefresh() {
if (refreshTimer) clearInterval(refreshTimer);
refreshTimer = setInterval(() => {
refreshAll();
}, refreshInterval);
}
// Format date/time for display
function formatDateTime(isoString) {
if (!isoString) return '-';
try {
const date = new Date(isoString);
return date.toLocaleString();
} catch (error) {
return '-';
}
}
// Save state
async function saveState() {
if (!confirm('Are you sure you want to save the current state?')) {
return;
}
try {
const response = await fetch(`${API_BASE}/state/save`, {
method: 'POST'
});
const result = await response.json();
if (result.success) {
alert('State saved successfully!');
await refreshAll();
} else {
alert(`Failed to save state: ${result.message}`);
}
} catch (error) {
console.error('Error saving state:', error);
alert('Failed to save state. Check console for details.');
}
}
// Reload state
async function reloadState() {
if (!confirm('Are you sure you want to reload the state? This will discard any unsaved changes.')) {
return;
}
try {
const response = await fetch(`${API_BASE}/state/reload`, {
method: 'POST'
});
const result = await response.json();
if (result.success) {
alert('State reloaded successfully!');
await refreshAll();
} else {
alert(`Failed to reload state: ${result.message}`);
}
} catch (error) {
console.error('Error reloading state:', error);
alert('Failed to reload state. Check console for details.');
}
}
// Terminal functionality
let terminalInterval = null;
let lastLogTimestamp = 0;
async function startTerminal() {
if (terminalInterval) return;
const terminalOutput = document.getElementById('terminalOutput');
terminalOutput.innerHTML = '<div class="log-entry">Connecting to server logs...</div>';
// Fetch logs periodically
await fetchLogs();
terminalInterval = setInterval(fetchLogs, 3000);
}
function stopTerminal() {
if (terminalInterval) {
clearInterval(terminalInterval);
terminalInterval = null;
}
}
async function fetchLogs() {
try {
const response = await fetch(`${API_BASE}/logs?since=${lastLogTimestamp}`);
if (!response.ok) throw new Error('Failed to fetch logs');
const data = await response.json();
if (data.logs && data.logs.length > 0) {
const terminalOutput = document.getElementById('terminalOutput');
if (!terminalOutput) return;
// Clear the "connecting" message if it exists
if (terminalOutput.querySelector('.log-entry')?.textContent === 'Connecting to server logs...') {
terminalOutput.innerHTML = '';
}
// Add new log entries
data.logs.forEach(log => {
addLogEntry(log.level || 'info', `[${new Date(log.timestamp).toISOString()}] ${log.message}`);
// Update last timestamp
if (log.timestamp > lastLogTimestamp) {
lastLogTimestamp = log.timestamp;
}
});
}
} catch (error) {
console.error('Error fetching logs:', error);
addLogEntry('error', 'Failed to fetch logs');
}
}
function addLogEntry(level, message) {
const terminalOutput = document.getElementById('terminalOutput');
if (!terminalOutput) return;
const logEntry = document.createElement('div');
logEntry.className = `log-entry ${level}`;
logEntry.textContent = message;
terminalOutput.appendChild(logEntry);
// Keep only last 100 entries to prevent memory issues
while (terminalOutput.children.length > 100) {
terminalOutput.removeChild(terminalOutput.firstChild);
}
if (autoScrollEnabled) {
terminalOutput.scrollTop = terminalOutput.scrollHeight;
}
}
function clearTerminal() {
const terminalOutput = document.getElementById('terminalOutput');
if (terminalOutput) {
terminalOutput.innerHTML = '<div class="log-entry">Terminal cleared</div>';
}
}
function toggleAutoScroll() {
autoScrollEnabled = !autoScrollEnabled;
const button = event.target;
button.textContent = autoScrollEnabled ? 'Auto Scroll' : 'Scroll Off';
}
// Start the panel when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}

View File

@@ -31,7 +31,6 @@ public sealed class JiboExperienceCatalog
public IReadOnlyList<string> HolidayTrackerReplies { get; init; } = [];
public IReadOnlyList<string> BirthdayCelebrationReplies { get; init; } = [];
public IReadOnlyList<string> HowAreYouReplies { get; init; } = [];
public IReadOnlyList<string> AgeReplies { get; init; } = [];
public IReadOnlyList<JiboConditionedReply> EmotionReplies { get; init; } = [];
public IReadOnlyList<string> PersonalityReplies { get; init; } = [];
public IReadOnlyList<string> PizzaReplies { get; init; } = [];

View File

@@ -199,10 +199,6 @@ 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",
@@ -399,18 +395,13 @@ internal static class ChitchatStateMachine
string? currentEmotion,
string? 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();
if (catalog.EmotionReplies.Count == 0)
return PersonalizeHowAreYouReply(randomizer.Choose(catalog.HowAreYouReplies), preferredName);
if (matchingReplies.Length > 0)
return PersonalizeHowAreYouReply(randomizer.Choose(matchingReplies), preferredName);
}
var emotionVariants = ResolveEmotionVariants(currentEmotion);
foreach (var reply in catalog.EmotionReplies)
if (ConditionMatches(reply.Condition, emotionVariants))
return PersonalizeHowAreYouReply(reply.Reply, preferredName);
return PersonalizeHowAreYouReply(randomizer.Choose(catalog.HowAreYouReplies), preferredName);
}

View File

@@ -7,15 +7,11 @@ 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 =
[
@@ -35,10 +31,6 @@ 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",
@@ -58,7 +50,6 @@ 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);
@@ -67,19 +58,17 @@ internal static class HouseholdListOrchestrator
if (!isActiveState && !isShoppingIntent && !isTodoIntent)
return Task.FromResult<JiboInteractionDecision?>(null);
var resolvedListType = isShoppingIntent ? ShoppingListType : isTodoIntent ? TodoListType : NormalizeListType(listType);
if (string.IsNullOrWhiteSpace(resolvedListType)) resolvedListType = ShoppingListType;
var resolvedDisplayType = ResolveDisplayType(resolvedListType, displayType, isActiveState, loweredTranscript);
var resolvedListType = isShoppingIntent ? "shopping" : isTodoIntent ? "todo" : NormalizeListType(listType);
if (string.IsNullOrWhiteSpace(resolvedListType)) resolvedListType = "shopping";
var tenantScope = tenantScopeResolver(turn);
if (ContainsAny(loweredTranscript, "cancel", "stop", "never mind", "nevermind", "forget it"))
return Task.FromResult<JiboInteractionDecision?>(BuildCancelledDecision(resolvedListType, resolvedDisplayType));
return Task.FromResult<JiboInteractionDecision?>(BuildCancelledDecision(resolvedListType));
if (IsRecallRequest(loweredTranscript))
return Task.FromResult<JiboInteractionDecision?>(BuildRecallDecision(
resolvedListType,
resolvedDisplayType,
personalMemoryStore.GetListItems(tenantScope, resolvedListType)));
var directItem = TryExtractListItem(loweredTranscript);
@@ -87,9 +76,9 @@ internal static class HouseholdListOrchestrator
{
if (IsConversationComplete(loweredTranscript))
return Task.FromResult<JiboInteractionDecision?>(new JiboInteractionDecision(
BuildListIntentName(resolvedListType, "done"),
BuildDoneReply(resolvedDisplayType, personalMemoryStore.GetListItems(tenantScope, resolvedListType)),
ContextUpdates: BuildContextUpdates(resolvedListType, resolvedDisplayType, IdleState)));
resolvedListType == "shopping" ? "shopping_list_done" : "todo_list_done",
BuildDoneReply(resolvedListType, personalMemoryStore.GetListItems(tenantScope, resolvedListType)),
ContextUpdates: BuildContextUpdates(resolvedListType, IdleState)));
directItem = NormalizeItem(transcript);
}
@@ -98,108 +87,104 @@ internal static class HouseholdListOrchestrator
{
personalMemoryStore.AddListItem(tenantScope, resolvedListType, directItem);
return Task.FromResult<JiboInteractionDecision?>(new JiboInteractionDecision(
BuildListIntentName(resolvedListType, "add"),
BuildAddedReply(resolvedDisplayType, directItem,
resolvedListType == "shopping" ? "shopping_list_add" : "todo_list_add",
BuildAddedReply(resolvedListType, directItem,
personalMemoryStore.GetListItems(tenantScope, resolvedListType)),
ContextUpdates: BuildContextUpdates(resolvedListType, resolvedDisplayType, AwaitingItemState)));
ContextUpdates: BuildContextUpdates(resolvedListType, AwaitingItemState)));
}
if (string.IsNullOrWhiteSpace(transcript))
return Task.FromResult<JiboInteractionDecision?>(new JiboInteractionDecision(
BuildListIntentName(resolvedListType, "prompt"),
BuildPromptReply(resolvedDisplayType),
ContextUpdates: BuildContextUpdates(resolvedListType, resolvedDisplayType, AwaitingItemState)));
resolvedListType == "shopping" ? "shopping_list_prompt" : "todo_list_prompt",
BuildPromptReply(resolvedListType),
ContextUpdates: BuildContextUpdates(resolvedListType, AwaitingItemState)));
return Task.FromResult<JiboInteractionDecision?>(new JiboInteractionDecision(
BuildListIntentName(resolvedListType, "prompt"),
BuildPromptReply(resolvedDisplayType),
ContextUpdates: BuildContextUpdates(resolvedListType, resolvedDisplayType, AwaitingItemState)));
resolvedListType == "shopping" ? "shopping_list_prompt" : "todo_list_prompt",
BuildPromptReply(resolvedListType),
ContextUpdates: BuildContextUpdates(resolvedListType, AwaitingItemState)));
}
private static IDictionary<string, object?> BuildContextUpdates(string listType, string displayType, string state)
private static IDictionary<string, object?> BuildContextUpdates(string listType, string state)
{
return new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
[StateMetadataKey] = state,
[TypeMetadataKey] = listType,
[DisplayTypeMetadataKey] = displayType,
[NoMatchCountMetadataKey] = 0,
[NoInputCountMetadataKey] = 0
};
}
private static JiboInteractionDecision BuildCancelledDecision(string listType, string displayType)
private static JiboInteractionDecision BuildCancelledDecision(string listType)
{
return new JiboInteractionDecision(
BuildListIntentName(listType, "cancel"),
$"Okay. I stopped the {BuildListLabel(displayType)}.",
listType == "shopping" ? "shopping_list_cancel" : "todo_list_cancel",
listType == "shopping" ? "Okay. I stopped the shopping list." : "Okay. I stopped the to-do list.",
ContextUpdates: new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
[StateMetadataKey] = IdleState,
[TypeMetadataKey] = listType,
[DisplayTypeMetadataKey] = displayType,
[NoMatchCountMetadataKey] = 0,
[NoInputCountMetadataKey] = 0
});
}
private static JiboInteractionDecision BuildRecallDecision(string listType, string displayType, IReadOnlyList<string> items)
private static JiboInteractionDecision BuildRecallDecision(string listType, IReadOnlyList<string> items)
{
if (items.Count == 0)
return new JiboInteractionDecision(
BuildListIntentName(listType, "recall"),
$"Your {BuildListLabel(displayType)} is empty.",
listType == "shopping" ? "shopping_list_recall" : "todo_list_recall",
listType == "shopping"
? "Your shopping list is empty."
: "Your to-do list is empty.",
ContextUpdates: new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
[StateMetadataKey] = IdleState,
[TypeMetadataKey] = listType,
[DisplayTypeMetadataKey] = displayType,
[NoMatchCountMetadataKey] = 0,
[NoInputCountMetadataKey] = 0
});
return new JiboInteractionDecision(
BuildListIntentName(listType, "recall"),
$"Your {BuildListLabel(displayType)} has {JoinList(items)}.",
listType == "shopping" ? "shopping_list_recall" : "todo_list_recall",
listType == "shopping"
? $"Your shopping list has {JoinList(items)}."
: $"Your to-do list has {JoinList(items)}.",
ContextUpdates: new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
[StateMetadataKey] = IdleState,
[TypeMetadataKey] = listType,
[DisplayTypeMetadataKey] = displayType,
[NoMatchCountMetadataKey] = 0,
[NoInputCountMetadataKey] = 0
});
}
private static string BuildAddedReply(string displayType, string addedItem, IReadOnlyList<string> items)
private static string BuildAddedReply(string listType, string addedItem, IReadOnlyList<string> items)
{
var itemLabel = BuildListLabel(displayType);
var itemLabel = listType == "shopping" ? "shopping list" : "to-do list";
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 displayType)
private static string BuildPromptReply(string listType)
{
return $"What should I add to your {BuildListLabel(displayType)}?";
return listType == "shopping"
? "What should I add to your shopping list?"
: "What should I add to your to-do list?";
}
private static string BuildDoneReply(string displayType, IReadOnlyList<string> items)
private static string BuildDoneReply(string listType, IReadOnlyList<string> items)
{
if (items.Count == 0)
return $"Okay. Your {BuildListLabel(displayType)} is empty.";
return listType == "shopping"
? "Okay. Your shopping list is empty."
: "Okay. Your to-do list is empty.";
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"
};
return listType == "shopping"
? $"Okay. Your shopping list has {JoinList(items)}."
: $"Okay. Your to-do list has {JoinList(items)}.";
}
private static string JoinList(IReadOnlyList<string> items)
@@ -220,13 +205,7 @@ 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);
}
@@ -239,9 +218,6 @@ 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",
@@ -270,96 +246,13 @@ internal static class HouseholdListOrchestrator
var normalized = NormalizeItem(listType ?? string.Empty).ToLowerInvariant();
return normalized.Contains("todo", StringComparison.OrdinalIgnoreCase) ||
normalized.Contains("to do", StringComparison.OrdinalIgnoreCase)
? TodoListType
? "todo"
: normalized.Contains("shopping", StringComparison.OrdinalIgnoreCase) ||
normalized.Contains("grocery", StringComparison.OrdinalIgnoreCase)
? ShoppingListType
? "shopping"
: 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));

View File

@@ -1,5 +1,4 @@
using System.Text;
using System.Security.Cryptography;
using System.Text.Json;
using Jibo.Cloud.Application.Abstractions;
using Jibo.Cloud.Domain.Models;
@@ -344,15 +343,10 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore, IMedia
var meta = ReadObject(body, "meta") ?? new Dictionary<string, object?>(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,
bodyBytes,
string.IsNullOrWhiteSpace(envelope.BodyText) ? [] : Encoding.UTF8.GetBytes(envelope.BodyText),
meta as IReadOnlyDictionary<string, object?>, CancellationToken.None).GetAwaiter().GetResult();
return ProtocolDispatchResult.Ok(

View File

@@ -350,7 +350,7 @@ public sealed partial class JiboInteractionService
"what is your age",
"what s your age",
"how old r you"))
return "robot_how_old_are_you";
return "robot_age";
if (MatchesAny(
loweredTranscript,
@@ -368,47 +368,6 @@ 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 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",
@@ -416,64 +375,6 @@ 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",
@@ -569,81 +470,6 @@ 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 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",
@@ -740,13 +566,6 @@ 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",
@@ -849,28 +668,6 @@ 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",
@@ -881,9 +678,12 @@ public sealed partial class JiboInteractionService
"what s your favourite animal",
"what animal do you like",
"what kind of animal do you like",
"what do you think about penguins",
"what do you think about animals",
"what do you think about birds"))
"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"))
return "robot_favorite_animal";
if (MatchesAny(
@@ -900,23 +700,6 @@ 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",
@@ -997,19 +780,13 @@ 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",

View File

@@ -14,8 +14,7 @@ public sealed partial class JiboInteractionService
string? sourceName,
IReadOnlyList<string>? categories,
int? headlineCount,
IReadOnlyDictionary<string, object?>? providerDiagnostics = null,
IReadOnlyList<NewsHeadline>? headlines = null)
IReadOnlyDictionary<string, object?>? providerDiagnostics = null)
{
var speakableBriefing = NormalizeNewsSpeechText(spokenBriefing);
var payload = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
@@ -26,9 +25,6 @@ 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"] =
$"<speak><anim cat='news' meta='news-stinger' nonBlocking='true' /><break size='0.35'/><es cat='neutral' filter='!ssa-only, !sfx-only' endNeutral='true'>{EscapeForEsml(speakableBriefing)}</es></speak>"
};
@@ -39,18 +35,6 @@ 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<string, object?>(
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;
@@ -93,8 +77,7 @@ public sealed partial class JiboInteractionService
"provider_success",
preferredCategories,
requestedHeadlineCount,
headlines.Length),
headlines);
headlines.Length));
}
private static IReadOnlyDictionary<string, object?> BuildNewsProviderDiagnostics(

View File

@@ -1,6 +1,5 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Jibo.Cloud.Application.Abstractions;
using Jibo.Cloud.Domain.Models;
using Jibo.Runtime.Abstractions;
@@ -9,49 +8,13 @@ namespace Jibo.Cloud.Application.Services;
public sealed partial class JiboInteractionService
{
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 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))
private static JiboInteractionDecision BuildRobotAgeDecision(DateTimeOffset? referenceLocalTime)
{
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(
intentName,
reply,
ContextUpdates: ScriptedResponseDecisionBuilder.BuildScriptedResponseContextUpdates());
"robot_age",
$"I count {OpenJiboCloudBuildInfo.PersonaBirthdayWords} as my birthday, so I am {ageDescription}.");
}
private static JiboInteractionDecision BuildRobotBirthdayDecision()
@@ -61,35 +24,6 @@ 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(
@@ -169,24 +103,14 @@ public sealed partial class JiboInteractionService
var tenantRememberedName = personalMemoryStore.GetName(ResolveTenantScope(turn));
if (!string.IsNullOrWhiteSpace(tenantRememberedName)) return ToDisplayName(tenantRememberedName);
var primaryPersonId = presence.PrimaryPersonId;
if (CanUseLoopFirstNameFallback(presence) &&
!string.IsNullOrWhiteSpace(primaryPersonId) &&
presence.LoopUserFirstNames.TryGetValue(primaryPersonId, out var firstName) &&
if (!string.IsNullOrWhiteSpace(presence.PrimaryPersonId) &&
presence.LoopUserFirstNames.TryGetValue(presence.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();
@@ -505,95 +429,6 @@ 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<PersonRecord> 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<string> 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.";

View File

@@ -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(catalog, referenceLocalTime, "robot_age"),
"robot_age" => BuildRobotAgeDecision(referenceLocalTime),
"robot_birthday" => BuildRobotBirthdayDecision(),
"robot_how_do_you_work" => BuildScriptedPersonalityDecision(
catalog,
@@ -569,14 +569,10 @@ public sealed partial class JiboInteractionService(
"care for me",
"catch up",
"seven years"),
"robot_what_do_you_eat" => BuildScriptedPersonalityDecision(
catalog,
"robot_what_do_you_eat" => new JiboInteractionDecision(
"robot_what_do_you_eat",
"electricity",
"never eaten",
"macaroni",
"non-eating robot",
"I don't eat or drink"),
"The only thing I consume is electricity.",
ContextUpdates: ScriptedResponseDecisionBuilder.BuildScriptedResponseContextUpdates()),
"robot_where_do_you_live" => BuildScriptedPersonalityDecision(
catalog,
"robot_where_do_you_live",
@@ -589,10 +585,6 @@ 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",
@@ -633,56 +625,6 @@ 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_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",
@@ -735,9 +677,9 @@ public sealed partial class JiboInteractionService(
"robot_favorite_flower" => BuildScriptedPersonalityDecision(
catalog,
"robot_favorite_flower",
"reminds me of the sun",
"sunflowers",
"favorite is the sunflower",
"sunflowers"),
"reminds me of the sun"),
"robot_likes_r2d2" => BuildScriptedPersonalityDecision(
catalog,
"robot_likes_r2d2",
@@ -758,144 +700,35 @@ public sealed partial class JiboInteractionService(
"robot_favorite_animal" => BuildScriptedFavoriteAnimalDecision(
catalog,
"robot_favorite_animal",
"we're so alike",
"penguin impression",
"penguin",
"favorite animal overall",
"best of the best",
"can't go wrong with penguins",
"penguin"),
"can't go wrong with penguins"),
"robot_favorite_bird" => BuildScriptedFavoriteAnimalDecision(
catalog,
"robot_favorite_bird",
"we're so alike",
"penguin impression",
"penguin",
"favorite animal overall",
"best of the best",
"can't go wrong with penguins",
"penguin"),
"can't go wrong with penguins"),
"robot_likes_penguins" => BuildScriptedFavoriteAnimalDecision(
catalog,
"robot_likes_penguins",
"my penguin impression",
"penguins",
"I really like penguins",
"penguins"),
"my penguin impression"),
"robot_likes_animals" => BuildScriptedFavoriteAnimalDecision(
catalog,
"robot_likes_animals",
"Animals are great",
"great shapes and colors",
"best of the best",
"penguins"),
"penguins",
"favorite animal overall",
"best of the best"),
"robot_peers" => BuildScriptedPersonalityDecision(
catalog,
"robot_peers",
"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_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_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",
@@ -949,12 +782,10 @@ public sealed partial class JiboInteractionService(
"Jingle Bells",
"Frosty the Snowman",
"holiday songs"),
"robot_what_are_you_made_of" => BuildScriptedPersonalityDecision(
catalog,
"robot_what_are_you_made_of" => new JiboInteractionDecision(
"robot_what_are_you_made_of",
"robot stuff",
"wires, motors, belts, gears, processors, cameras",
"baboon part"),
"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()),
"good_morning" => BuildReactiveGreetingDecision(turn, "good_morning", referenceLocalTime),
"good_afternoon" => BuildReactiveGreetingDecision(turn, "good_afternoon", referenceLocalTime),
"good_evening" => BuildReactiveGreetingDecision(turn, "good_evening", referenceLocalTime),

View File

@@ -43,8 +43,6 @@ 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");
@@ -248,10 +246,10 @@ public sealed class ResponsePlanToSocketMessagesMapper
outboundAsrText,
outboundRules,
entities)),
idleRedirectDelayMs));
75));
messages.Add(new SocketReplyPlan(
JsonSerializer.Serialize(BuildCompletionOnlySkillPayload(transId, "@be/idle")),
idleCompletionDelayMs));
125));
}
if (isSettingsLaunch &&

View File

@@ -106,38 +106,6 @@ public sealed class WebSocketTurnFinalizationService(
"honestly"
];
private static readonly HashSet<string> 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<string> YesNoAffirmativeLeadTokens = new(StringComparer.Ordinal)
{
"yes",
@@ -1149,6 +1117,8 @@ 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) &&
@@ -1158,19 +1128,9 @@ 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")

View File

@@ -137,26 +137,7 @@ 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.",
"I am running smoothly and feeling upbeat.",
"I am ready for the next thing. Thanks for asking."
],
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."
"Things are going nicely. Thanks for checking in."
],
PersonalityReplies =
[

View File

@@ -264,9 +264,7 @@ public static class LegacyMimCatalogImporter
fileName.StartsWith("JBO_WhatsYourName", StringComparison.OrdinalIgnoreCase) ||
fileName.StartsWith("JBO_WhereDoYouGetInfo", StringComparison.OrdinalIgnoreCase) ||
fileName.StartsWith("JBO_WhatDoYouLikeToDo", StringComparison.OrdinalIgnoreCase))
return fileName.StartsWith("JBO_HowOldAreYou", StringComparison.OrdinalIgnoreCase)
? LegacyMimBucket.Age
: LegacyMimBucket.Personality;
return LegacyMimBucket.Personality;
if (fileName.StartsWith("OI_JBO_Is", StringComparison.OrdinalIgnoreCase) ||
fileName.StartsWith("OI_JBO_Seems", StringComparison.OrdinalIgnoreCase) ||
@@ -458,7 +456,6 @@ public static class LegacyMimCatalogImporter
or LegacyMimBucket.WeatherTomorrowHighLow
or LegacyMimBucket.WeatherServiceDown
or LegacyMimBucket.ReportSkillTemplate
or LegacyMimBucket.Age
or LegacyMimBucket.Holiday
or LegacyMimBucket.HolidayTracker;
}
@@ -527,7 +524,6 @@ public static class LegacyMimCatalogImporter
Sing,
HolidaySing,
FunFactSource,
Age,
Personality,
PersonalReportKickOff,
PersonalReportOutro,
@@ -590,7 +586,6 @@ public static class LegacyMimCatalogImporter
private readonly List<string> _bestFriendReplies = [];
private readonly List<string> _funFacts = [];
private readonly List<string> _greetings = [];
private readonly List<string> _ages = [];
private readonly List<string> _holidayGiftReplies = [];
private readonly List<string> _holidayGreetingReplies = [];
private readonly List<string> _holidayReplies = [];
@@ -660,9 +655,6 @@ public static class LegacyMimCatalogImporter
Reply = text
});
return;
case LegacyMimBucket.Age:
AddDistinct(_ages, text);
return;
case LegacyMimBucket.Holiday:
AddDistinct(_holidayReplies, text);
return;
@@ -839,7 +831,6 @@ public static class LegacyMimCatalogImporter
EmotionReplies = [.. _emotionReplies],
PersonalityReplies = [.. _personalities],
GenericFallbackReplies = [.. _fallbacks],
AgeReplies = [.. _ages],
PersonalReportKickOffReplies = [.. _personalReportKickOffReplies],
PersonalReportOutroReplies = [.. _personalReportOutroReplies],
ReportSkillTemplates = [.. _reportSkillTemplates],

View File

@@ -24,12 +24,5 @@ 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 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.
`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.

View File

@@ -1,5 +1,4 @@
using System.Text.Json;
using System.Security.Cryptography;
using Azure.Storage.Blobs;
using Jibo.Cloud.Application.Abstractions;
@@ -32,17 +31,11 @@ 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<string, object?>(StringComparer.OrdinalIgnoreCase)
: new Dictionary<string, object?>(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 = manifestMeta
meta
}, JsonOptions);
await metaBlob.UploadAsync(BinaryData.FromString(payload), true, cancellationToken);
}

View File

@@ -1,5 +1,4 @@
using System.Text.Json;
using System.Security.Cryptography;
using Jibo.Cloud.Application.Abstractions;
namespace Jibo.Cloud.Infrastructure.Media;
@@ -30,17 +29,11 @@ internal sealed class FileMediaContentStore : IMediaContentStore
Directory.CreateDirectory(Path.GetDirectoryName(contentPath)!);
await File.WriteAllBytesAsync(contentPath, content, cancellationToken);
var manifestMeta = meta is null
? new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
: new Dictionary<string, object?>(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 = manifestMeta
meta
};
await File.WriteAllTextAsync(metaPath, JsonSerializer.Serialize(payload, JsonOptions), cancellationToken);
}

View File

@@ -108,10 +108,6 @@ 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));
@@ -260,32 +256,6 @@ 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));
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]

View File

@@ -1,5 +1,3 @@
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Jibo.Cloud.Application.Services;
using Jibo.Cloud.Domain.Models;
@@ -422,46 +420,6 @@ 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<string, string>(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()
{

View File

@@ -23,7 +23,6 @@ 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";
@@ -116,8 +115,8 @@ public sealed class JiboInteractionServiceTests
}
});
Assert.Equal("robot_how_old_are_you", decision.IntentName);
Assert.Contains("first powered up", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
Assert.Equal("robot_age", decision.IntentName);
Assert.Equal("I count March 22, 2026 as my birthday, so I am 1 month old.", decision.ReplyText);
}
[Fact]
@@ -349,31 +348,6 @@ 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<string, object?>
{
["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()
{
@@ -658,6 +632,9 @@ 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();
@@ -669,21 +646,17 @@ public sealed class JiboInteractionServiceTests
});
Assert.Equal("robot_favorite_animal", decision.IntentName);
Assert.Contains("we're so alike", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
Assert.Contains("penguin", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
Assert.Equal("ScriptedResponse", decision.ContextUpdates![ChitchatRouteKey]);
}
[Theory]
[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("what is your favorite flower", "robot_favorite_flower", "sunflowers")]
[InlineData("what's your favorite flower", "robot_favorite_flower", "sunflowers")]
[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")]
@@ -712,106 +685,6 @@ 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 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")]
[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")]
@@ -839,7 +712,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", "electricity")]
[InlineData("what do you eat", "robot_what_do_you_eat", "The only thing I consume is 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.")]
@@ -848,7 +721,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",
"robot stuff")]
"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.")]
public async Task BuildDecisionAsync_MoreLegacyPersonaMims_UseImportedReplies(
string transcript,
string expectedIntent,
@@ -867,35 +740,6 @@ 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",
@@ -1040,21 +884,6 @@ 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")]
@@ -2456,17 +2285,13 @@ public sealed class JiboInteractionServiceTests
}
[Theory]
[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")]
[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")]
public async Task BuildDecisionAsync_ListStart_PromptsForFollowUpItems(
string transcript,
string expectedIntent,
string expectedReply,
string expectedListType,
string expectedDisplayType)
string expectedListType)
{
var service = CreateService();
@@ -2481,7 +2306,6 @@ 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]
@@ -2506,7 +2330,6 @@ 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
{
@@ -2516,8 +2339,7 @@ public sealed class JiboInteractionServiceTests
Attributes = new Dictionary<string, object?>(tenantAttributes)
{
[HouseholdListStateKey] = promptDecision.ContextUpdates[HouseholdListStateKey],
[HouseholdListTypeKey] = promptDecision.ContextUpdates[HouseholdListTypeKey],
[HouseholdListDisplayTypeKey] = promptDecision.ContextUpdates[HouseholdListDisplayTypeKey]
[HouseholdListTypeKey] = promptDecision.ContextUpdates[HouseholdListTypeKey]
}
});
@@ -2526,7 +2348,6 @@ 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"));
@@ -2538,8 +2359,7 @@ public sealed class JiboInteractionServiceTests
Attributes = new Dictionary<string, object?>(tenantAttributes)
{
[HouseholdListStateKey] = addDecision.ContextUpdates[HouseholdListStateKey],
[HouseholdListTypeKey] = addDecision.ContextUpdates[HouseholdListTypeKey],
[HouseholdListDisplayTypeKey] = addDecision.ContextUpdates[HouseholdListDisplayTypeKey]
[HouseholdListTypeKey] = addDecision.ContextUpdates[HouseholdListTypeKey]
}
});
@@ -2547,7 +2367,6 @@ 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
{
@@ -2559,134 +2378,6 @@ 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<string, object?>
{
["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<string, object?>(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<string, object?>(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<string, object?>(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<string, object?>
{
["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<string, object?>(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<string, object?>(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<string, object?>(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<string, object?>(tenantAttributes)
});
Assert.Equal("shopping_list_recall", recallDecision.IntentName);
Assert.Contains("milk", recallDecision.ReplyText, StringComparison.OrdinalIgnoreCase);
Assert.Contains("grocery list", recallDecision.ReplyText, StringComparison.OrdinalIgnoreCase);
}
[Fact]
@@ -4501,8 +4192,6 @@ 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<Dictionary<string, object?>[]>(decision.SkillPayload["news_headlines"]);
Assert.Contains("Local robotics team unveils weather-ready helper", decision.ReplyText,
StringComparison.OrdinalIgnoreCase);
Assert.NotNull(provider.LastRequest);

View File

@@ -3801,35 +3801,6 @@ 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()
{