Compare commits
1 Commits
main
...
Features/W
| Author | SHA1 | Date | |
|---|---|---|---|
|
32c601d046
|
@@ -614,8 +614,6 @@ Current release theme:
|
|||||||
- recognition, enrollment, rename, and profile-correction boundaries
|
- recognition, enrollment, rename, and profile-correction boundaries
|
||||||
- split between local state and hosted cloud state
|
- split between local state and hosted cloud state
|
||||||
- first useful hosted identity slice
|
- 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
|
### 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
|
- `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`
|
- `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`
|
- 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:
|
- Follow-up:
|
||||||
- wire persona age to first-powered-up or durable first-cloud-seen metadata when available
|
- 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
|
- 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
|
### 22. Command Vs Question Reply Style
|
||||||
|
|
||||||
@@ -775,7 +770,7 @@ Current release theme:
|
|||||||
|
|
||||||
### 28. Grocery List Capability (Requested Feature)
|
### 28. Grocery List Capability (Requested Feature)
|
||||||
|
|
||||||
- Status: `in_progress`
|
- Status: `discovery`
|
||||||
- Tags: `content`, `docs`, `storage`
|
- Tags: `content`, `docs`, `storage`
|
||||||
- Why now:
|
- Why now:
|
||||||
- directly requested by Jibo owners and fits memory + household utility roadmap
|
- directly requested by Jibo owners and fits memory + household utility roadmap
|
||||||
@@ -784,14 +779,13 @@ Current release theme:
|
|||||||
- examples:
|
- 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_ShoppingList.mim`
|
||||||
- `C:\Projects\jibo\pegasus\packages\chitchat-skill\mims\scripted-responses\RA_JBO_ManageToDoList.mim`
|
- `C:\Projects\jibo\pegasus\packages\chitchat-skill\mims\scripted-responses\RA_JBO_ManageToDoList.mim`
|
||||||
- MVP decision:
|
- Candidate delivery paths:
|
||||||
- use the existing household list engine as the native lightweight grocery MVP
|
- native lightweight list skill (fastest user value)
|
||||||
- keep grocery as a first-class spoken alias over the shopping list storage path
|
- integration-backed list orchestration (long-term richer ecosystem fit)
|
||||||
- reserve integration-backed list orchestration for a later discovery pass
|
|
||||||
- Exit criteria:
|
- Exit criteria:
|
||||||
- grocery prompts, add/recall/done flows, and list follow-ups consistently speak grocery wording
|
- clear decision on MVP path
|
||||||
- existing shopping/to-do flows remain unchanged
|
- first schema for list items + ownership scope
|
||||||
- future integration-backed list work remains a separate backlog item
|
- initial voice flows and follow-up intent handling defined
|
||||||
|
|
||||||
### 29. Legacy MIM Personality Import Ladder
|
### 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`
|
- 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`
|
- 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`
|
- 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
|
- 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:
|
- Exit criteria:
|
||||||
- a stable checklist exists for the original persona surface
|
- a stable checklist exists for the original persona surface
|
||||||
- each pass can be scoped to a small batch of prompts
|
- 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
|
- 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)
|
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
|
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
|
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.
|
13. Binary-safe media storage / sync to cloud drive: OneDrive, Google Drive, Box, etc.
|
||||||
14. Provider-backed news and weather parity polish
|
14. Provider-backed news and weather parity polish
|
||||||
15. Grocery list capability discovery and MVP selection
|
15. Grocery list capability discovery and MVP selection
|
||||||
|
|||||||
@@ -77,18 +77,3 @@ Useful helper scripts:
|
|||||||
- [scripts/cloud/get-websocket-capture-summary.sh](/OpenJibo/scripts/cloud/get-websocket-capture-summary.sh)
|
- [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)
|
- [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)
|
- [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
|
|
||||||
|
|||||||
@@ -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.
|
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
|
## Snapshot
|
||||||
|
|
||||||
- Kickoff date: `2026-05-05`
|
- 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 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 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 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
|
- 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
|
- 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
|
- 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
|
- 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
|
- 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
|
- 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
|
### 3. Pegasus-To-Cloud Platform Porting
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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.
|
Runs a small readiness checklist before the first physical Jibo test against the .NET cloud.
|
||||||
- `Import-WebSocketCaptureFixture.ps1`
|
- `Import-WebSocketCaptureFixture.ps1`
|
||||||
Sanitizes an exported websocket capture fixture and copies it into the checked-in websocket fixture set.
|
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`
|
- `start-dotnet-with-node-cert.sh`
|
||||||
Starts the .NET API on Linux using the same PEM certificate material already used by the Node server.
|
Starts the .NET API on Linux using the same PEM certificate material already used by the Node server.
|
||||||
- `invoke-live-jibo-prep.sh`
|
- `invoke-live-jibo-prep.sh`
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -9,12 +9,53 @@ using Jibo.Cloud.Infrastructure.DependencyInjection;
|
|||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
builder.Services.AddOpenJiboCloud(builder.Configuration);
|
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();
|
var app = builder.Build();
|
||||||
|
|
||||||
app.Logger.LogInformation("Starting Open Jibo Cloud Api version {Version}", OpenJiboCloudBuildInfo.Version);
|
app.Logger.LogInformation("Starting Open Jibo Cloud Api version {Version}", OpenJiboCloudBuildInfo.Version);
|
||||||
|
|
||||||
app.UseWebSockets();
|
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) =>
|
app.Use(async (context, next) =>
|
||||||
{
|
{
|
||||||
@@ -24,7 +65,7 @@ app.Use(async (context, next) =>
|
|||||||
return;
|
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);
|
var token = ResolveToken(context.Request);
|
||||||
switch (kind)
|
switch (kind)
|
||||||
{
|
{
|
||||||
@@ -127,6 +168,32 @@ app.MapGet("/health", () => Results.Json(new
|
|||||||
app.MapMethods("/{**path}", ["GET", "POST", "PUT"], async (HttpContext context, JiboCloudProtocolService service,
|
app.MapMethods("/{**path}", ["GET", "POST", "PUT"], async (HttpContext context, JiboCloudProtocolService service,
|
||||||
IProtocolTelemetrySink telemetrySink, CancellationToken cancellationToken) =>
|
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 envelope = await BuildEnvelopeAsync(context, cancellationToken);
|
||||||
var result = await service.DispatchAsync(envelope, cancellationToken);
|
var result = await service.DispatchAsync(envelope, cancellationToken);
|
||||||
await telemetrySink.RecordAsync(envelope, result, 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("api-socket.jibo.com", StringComparison.OrdinalIgnoreCase)) return "api-socket";
|
||||||
|
|
||||||
if (host.Equals("neo-hub.jibo.com", StringComparison.OrdinalIgnoreCase) &&
|
if (host.Equals("neo-hub.jibo.com", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
|||||||
@@ -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": {
|
"OpenJibo": {
|
||||||
|
"MultiPortMode": {
|
||||||
|
"Enabled": true,
|
||||||
|
"Ports": {
|
||||||
|
"Api": 5000,
|
||||||
|
"ApiSocket": 5001,
|
||||||
|
"NeoHubListen": 5002,
|
||||||
|
"NeoHubProactive": 5003,
|
||||||
|
"WebPanel": 3380
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"WebPanel": {
|
||||||
|
"Enabled": true,
|
||||||
|
"RefreshIntervalSeconds": 5,
|
||||||
|
"AllowRemoteAccess": false
|
||||||
|
},
|
||||||
"Telemetry": {
|
"Telemetry": {
|
||||||
"Enabled": true,
|
"Enabled": true,
|
||||||
"ExportFixtures": true,
|
"ExportFixtures": true,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -31,7 +31,6 @@ public sealed class JiboExperienceCatalog
|
|||||||
public IReadOnlyList<string> HolidayTrackerReplies { get; init; } = [];
|
public IReadOnlyList<string> HolidayTrackerReplies { get; init; } = [];
|
||||||
public IReadOnlyList<string> BirthdayCelebrationReplies { get; init; } = [];
|
public IReadOnlyList<string> BirthdayCelebrationReplies { get; init; } = [];
|
||||||
public IReadOnlyList<string> HowAreYouReplies { get; init; } = [];
|
public IReadOnlyList<string> HowAreYouReplies { get; init; } = [];
|
||||||
public IReadOnlyList<string> AgeReplies { get; init; } = [];
|
|
||||||
public IReadOnlyList<JiboConditionedReply> EmotionReplies { get; init; } = [];
|
public IReadOnlyList<JiboConditionedReply> EmotionReplies { get; init; } = [];
|
||||||
public IReadOnlyList<string> PersonalityReplies { get; init; } = [];
|
public IReadOnlyList<string> PersonalityReplies { get; init; } = [];
|
||||||
public IReadOnlyList<string> PizzaReplies { get; init; } = [];
|
public IReadOnlyList<string> PizzaReplies { get; init; } = [];
|
||||||
|
|||||||
@@ -199,10 +199,6 @@ internal static class ChitchatStateMachine
|
|||||||
"want to hang out",
|
"want to hang out",
|
||||||
"be helpful",
|
"be helpful",
|
||||||
"dance from time to time"));
|
"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":
|
case "robot_job":
|
||||||
return BuildScriptedResponseDecision(
|
return BuildScriptedResponseDecision(
|
||||||
"robot_job",
|
"robot_job",
|
||||||
@@ -399,18 +395,13 @@ internal static class ChitchatStateMachine
|
|||||||
string? currentEmotion,
|
string? currentEmotion,
|
||||||
string? preferredName)
|
string? preferredName)
|
||||||
{
|
{
|
||||||
if (catalog.EmotionReplies.Count > 0)
|
if (catalog.EmotionReplies.Count == 0)
|
||||||
{
|
return PersonalizeHowAreYouReply(randomizer.Choose(catalog.HowAreYouReplies), preferredName);
|
||||||
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 (matchingReplies.Length > 0)
|
var emotionVariants = ResolveEmotionVariants(currentEmotion);
|
||||||
return PersonalizeHowAreYouReply(randomizer.Choose(matchingReplies), preferredName);
|
foreach (var reply in catalog.EmotionReplies)
|
||||||
}
|
if (ConditionMatches(reply.Condition, emotionVariants))
|
||||||
|
return PersonalizeHowAreYouReply(reply.Reply, preferredName);
|
||||||
|
|
||||||
return PersonalizeHowAreYouReply(randomizer.Choose(catalog.HowAreYouReplies), preferredName);
|
return PersonalizeHowAreYouReply(randomizer.Choose(catalog.HowAreYouReplies), preferredName);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,15 +7,11 @@ internal static class HouseholdListOrchestrator
|
|||||||
{
|
{
|
||||||
internal const string StateMetadataKey = "householdListState";
|
internal const string StateMetadataKey = "householdListState";
|
||||||
internal const string TypeMetadataKey = "householdListType";
|
internal const string TypeMetadataKey = "householdListType";
|
||||||
internal const string DisplayTypeMetadataKey = "householdListDisplayType";
|
|
||||||
internal const string NoMatchCountMetadataKey = "householdListNoMatchCount";
|
internal const string NoMatchCountMetadataKey = "householdListNoMatchCount";
|
||||||
internal const string NoInputCountMetadataKey = "householdListNoInputCount";
|
internal const string NoInputCountMetadataKey = "householdListNoInputCount";
|
||||||
|
|
||||||
private const string IdleState = "idle";
|
private const string IdleState = "idle";
|
||||||
private const string AwaitingItemState = "awaiting_item";
|
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 =
|
private static readonly string[] ItemPrefixes =
|
||||||
[
|
[
|
||||||
@@ -35,10 +31,6 @@ internal static class HouseholdListOrchestrator
|
|||||||
" to my shopping list",
|
" to my shopping list",
|
||||||
" to the shopping list",
|
" to the shopping list",
|
||||||
" on my 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 my to do list",
|
||||||
" to the to do list",
|
" to the to do list",
|
||||||
" on my to do list",
|
" on my to do list",
|
||||||
@@ -58,7 +50,6 @@ internal static class HouseholdListOrchestrator
|
|||||||
{
|
{
|
||||||
var state = ReadString(turn, StateMetadataKey);
|
var state = ReadString(turn, StateMetadataKey);
|
||||||
var listType = ReadString(turn, TypeMetadataKey);
|
var listType = ReadString(turn, TypeMetadataKey);
|
||||||
var displayType = ReadString(turn, DisplayTypeMetadataKey);
|
|
||||||
var isActiveState = !string.IsNullOrWhiteSpace(state) &&
|
var isActiveState = !string.IsNullOrWhiteSpace(state) &&
|
||||||
!string.Equals(state, IdleState, StringComparison.OrdinalIgnoreCase);
|
!string.Equals(state, IdleState, StringComparison.OrdinalIgnoreCase);
|
||||||
var isShoppingIntent = string.Equals(semanticIntent, "shopping_list", StringComparison.OrdinalIgnoreCase);
|
var isShoppingIntent = string.Equals(semanticIntent, "shopping_list", StringComparison.OrdinalIgnoreCase);
|
||||||
@@ -67,19 +58,17 @@ internal static class HouseholdListOrchestrator
|
|||||||
if (!isActiveState && !isShoppingIntent && !isTodoIntent)
|
if (!isActiveState && !isShoppingIntent && !isTodoIntent)
|
||||||
return Task.FromResult<JiboInteractionDecision?>(null);
|
return Task.FromResult<JiboInteractionDecision?>(null);
|
||||||
|
|
||||||
var resolvedListType = isShoppingIntent ? ShoppingListType : isTodoIntent ? TodoListType : NormalizeListType(listType);
|
var resolvedListType = isShoppingIntent ? "shopping" : isTodoIntent ? "todo" : NormalizeListType(listType);
|
||||||
if (string.IsNullOrWhiteSpace(resolvedListType)) resolvedListType = ShoppingListType;
|
if (string.IsNullOrWhiteSpace(resolvedListType)) resolvedListType = "shopping";
|
||||||
var resolvedDisplayType = ResolveDisplayType(resolvedListType, displayType, isActiveState, loweredTranscript);
|
|
||||||
|
|
||||||
var tenantScope = tenantScopeResolver(turn);
|
var tenantScope = tenantScopeResolver(turn);
|
||||||
|
|
||||||
if (ContainsAny(loweredTranscript, "cancel", "stop", "never mind", "nevermind", "forget it"))
|
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))
|
if (IsRecallRequest(loweredTranscript))
|
||||||
return Task.FromResult<JiboInteractionDecision?>(BuildRecallDecision(
|
return Task.FromResult<JiboInteractionDecision?>(BuildRecallDecision(
|
||||||
resolvedListType,
|
resolvedListType,
|
||||||
resolvedDisplayType,
|
|
||||||
personalMemoryStore.GetListItems(tenantScope, resolvedListType)));
|
personalMemoryStore.GetListItems(tenantScope, resolvedListType)));
|
||||||
|
|
||||||
var directItem = TryExtractListItem(loweredTranscript);
|
var directItem = TryExtractListItem(loweredTranscript);
|
||||||
@@ -87,9 +76,9 @@ internal static class HouseholdListOrchestrator
|
|||||||
{
|
{
|
||||||
if (IsConversationComplete(loweredTranscript))
|
if (IsConversationComplete(loweredTranscript))
|
||||||
return Task.FromResult<JiboInteractionDecision?>(new JiboInteractionDecision(
|
return Task.FromResult<JiboInteractionDecision?>(new JiboInteractionDecision(
|
||||||
BuildListIntentName(resolvedListType, "done"),
|
resolvedListType == "shopping" ? "shopping_list_done" : "todo_list_done",
|
||||||
BuildDoneReply(resolvedDisplayType, personalMemoryStore.GetListItems(tenantScope, resolvedListType)),
|
BuildDoneReply(resolvedListType, personalMemoryStore.GetListItems(tenantScope, resolvedListType)),
|
||||||
ContextUpdates: BuildContextUpdates(resolvedListType, resolvedDisplayType, IdleState)));
|
ContextUpdates: BuildContextUpdates(resolvedListType, IdleState)));
|
||||||
|
|
||||||
directItem = NormalizeItem(transcript);
|
directItem = NormalizeItem(transcript);
|
||||||
}
|
}
|
||||||
@@ -98,108 +87,104 @@ internal static class HouseholdListOrchestrator
|
|||||||
{
|
{
|
||||||
personalMemoryStore.AddListItem(tenantScope, resolvedListType, directItem);
|
personalMemoryStore.AddListItem(tenantScope, resolvedListType, directItem);
|
||||||
return Task.FromResult<JiboInteractionDecision?>(new JiboInteractionDecision(
|
return Task.FromResult<JiboInteractionDecision?>(new JiboInteractionDecision(
|
||||||
BuildListIntentName(resolvedListType, "add"),
|
resolvedListType == "shopping" ? "shopping_list_add" : "todo_list_add",
|
||||||
BuildAddedReply(resolvedDisplayType, directItem,
|
BuildAddedReply(resolvedListType, directItem,
|
||||||
personalMemoryStore.GetListItems(tenantScope, resolvedListType)),
|
personalMemoryStore.GetListItems(tenantScope, resolvedListType)),
|
||||||
ContextUpdates: BuildContextUpdates(resolvedListType, resolvedDisplayType, AwaitingItemState)));
|
ContextUpdates: BuildContextUpdates(resolvedListType, AwaitingItemState)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(transcript))
|
if (string.IsNullOrWhiteSpace(transcript))
|
||||||
return Task.FromResult<JiboInteractionDecision?>(new JiboInteractionDecision(
|
return Task.FromResult<JiboInteractionDecision?>(new JiboInteractionDecision(
|
||||||
BuildListIntentName(resolvedListType, "prompt"),
|
resolvedListType == "shopping" ? "shopping_list_prompt" : "todo_list_prompt",
|
||||||
BuildPromptReply(resolvedDisplayType),
|
BuildPromptReply(resolvedListType),
|
||||||
ContextUpdates: BuildContextUpdates(resolvedListType, resolvedDisplayType, AwaitingItemState)));
|
ContextUpdates: BuildContextUpdates(resolvedListType, AwaitingItemState)));
|
||||||
|
|
||||||
return Task.FromResult<JiboInteractionDecision?>(new JiboInteractionDecision(
|
return Task.FromResult<JiboInteractionDecision?>(new JiboInteractionDecision(
|
||||||
BuildListIntentName(resolvedListType, "prompt"),
|
resolvedListType == "shopping" ? "shopping_list_prompt" : "todo_list_prompt",
|
||||||
BuildPromptReply(resolvedDisplayType),
|
BuildPromptReply(resolvedListType),
|
||||||
ContextUpdates: BuildContextUpdates(resolvedListType, resolvedDisplayType, AwaitingItemState)));
|
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)
|
return new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
[StateMetadataKey] = state,
|
[StateMetadataKey] = state,
|
||||||
[TypeMetadataKey] = listType,
|
[TypeMetadataKey] = listType,
|
||||||
[DisplayTypeMetadataKey] = displayType,
|
|
||||||
[NoMatchCountMetadataKey] = 0,
|
[NoMatchCountMetadataKey] = 0,
|
||||||
[NoInputCountMetadataKey] = 0
|
[NoInputCountMetadataKey] = 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JiboInteractionDecision BuildCancelledDecision(string listType, string displayType)
|
private static JiboInteractionDecision BuildCancelledDecision(string listType)
|
||||||
{
|
{
|
||||||
return new JiboInteractionDecision(
|
return new JiboInteractionDecision(
|
||||||
BuildListIntentName(listType, "cancel"),
|
listType == "shopping" ? "shopping_list_cancel" : "todo_list_cancel",
|
||||||
$"Okay. I stopped the {BuildListLabel(displayType)}.",
|
listType == "shopping" ? "Okay. I stopped the shopping list." : "Okay. I stopped the to-do list.",
|
||||||
ContextUpdates: new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
ContextUpdates: new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
[StateMetadataKey] = IdleState,
|
[StateMetadataKey] = IdleState,
|
||||||
[TypeMetadataKey] = listType,
|
[TypeMetadataKey] = listType,
|
||||||
[DisplayTypeMetadataKey] = displayType,
|
|
||||||
[NoMatchCountMetadataKey] = 0,
|
[NoMatchCountMetadataKey] = 0,
|
||||||
[NoInputCountMetadataKey] = 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)
|
if (items.Count == 0)
|
||||||
return new JiboInteractionDecision(
|
return new JiboInteractionDecision(
|
||||||
BuildListIntentName(listType, "recall"),
|
listType == "shopping" ? "shopping_list_recall" : "todo_list_recall",
|
||||||
$"Your {BuildListLabel(displayType)} is empty.",
|
listType == "shopping"
|
||||||
|
? "Your shopping list is empty."
|
||||||
|
: "Your to-do list is empty.",
|
||||||
ContextUpdates: new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
ContextUpdates: new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
[StateMetadataKey] = IdleState,
|
[StateMetadataKey] = IdleState,
|
||||||
[TypeMetadataKey] = listType,
|
[TypeMetadataKey] = listType,
|
||||||
[DisplayTypeMetadataKey] = displayType,
|
|
||||||
[NoMatchCountMetadataKey] = 0,
|
[NoMatchCountMetadataKey] = 0,
|
||||||
[NoInputCountMetadataKey] = 0
|
[NoInputCountMetadataKey] = 0
|
||||||
});
|
});
|
||||||
|
|
||||||
return new JiboInteractionDecision(
|
return new JiboInteractionDecision(
|
||||||
BuildListIntentName(listType, "recall"),
|
listType == "shopping" ? "shopping_list_recall" : "todo_list_recall",
|
||||||
$"Your {BuildListLabel(displayType)} has {JoinList(items)}.",
|
listType == "shopping"
|
||||||
|
? $"Your shopping list has {JoinList(items)}."
|
||||||
|
: $"Your to-do list has {JoinList(items)}.",
|
||||||
ContextUpdates: new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
ContextUpdates: new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
[StateMetadataKey] = IdleState,
|
[StateMetadataKey] = IdleState,
|
||||||
[TypeMetadataKey] = listType,
|
[TypeMetadataKey] = listType,
|
||||||
[DisplayTypeMetadataKey] = displayType,
|
|
||||||
[NoMatchCountMetadataKey] = 0,
|
[NoMatchCountMetadataKey] = 0,
|
||||||
[NoInputCountMetadataKey] = 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
|
return items.Count == 1
|
||||||
? $"Added {addedItem} to your {itemLabel}. What else should I add?"
|
? $"Added {addedItem} to your {itemLabel}. What else should I add?"
|
||||||
: $"Added {addedItem} to your {itemLabel}. You now have {JoinList(items)}.";
|
: $"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)
|
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)}.";
|
return listType == "shopping"
|
||||||
}
|
? $"Okay. Your shopping list has {JoinList(items)}."
|
||||||
|
: $"Okay. Your to-do list has {JoinList(items)}.";
|
||||||
private static string BuildListLabel(string displayType)
|
|
||||||
{
|
|
||||||
return NormalizeDisplayType(displayType) switch
|
|
||||||
{
|
|
||||||
GroceryListType => "grocery list",
|
|
||||||
TodoListType => "to-do list",
|
|
||||||
_ => "shopping list"
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string JoinList(IReadOnlyList<string> items)
|
private static string JoinList(IReadOnlyList<string> items)
|
||||||
@@ -220,13 +205,7 @@ internal static class HouseholdListOrchestrator
|
|||||||
if (!loweredTranscript.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) continue;
|
if (!loweredTranscript.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) continue;
|
||||||
|
|
||||||
var remainder = loweredTranscript[prefix.Length..].Trim();
|
var remainder = loweredTranscript[prefix.Length..].Trim();
|
||||||
if (IsListOnlyRemainder(remainder))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
remainder = TrimTrailingListPhrases(remainder);
|
remainder = TrimTrailingListPhrases(remainder);
|
||||||
if (IsListOnlyRemainder(remainder))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return NormalizeItem(remainder);
|
return NormalizeItem(remainder);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,9 +218,6 @@ internal static class HouseholdListOrchestrator
|
|||||||
"what is on my shopping list",
|
"what is on my shopping list",
|
||||||
"what's on my shopping list",
|
"what's on my shopping list",
|
||||||
"show 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 is on my to do list",
|
||||||
"what's on my to do list",
|
"what's on my to do list",
|
||||||
"show my to do list",
|
"show my to do list",
|
||||||
@@ -270,96 +246,13 @@ internal static class HouseholdListOrchestrator
|
|||||||
var normalized = NormalizeItem(listType ?? string.Empty).ToLowerInvariant();
|
var normalized = NormalizeItem(listType ?? string.Empty).ToLowerInvariant();
|
||||||
return normalized.Contains("todo", StringComparison.OrdinalIgnoreCase) ||
|
return normalized.Contains("todo", StringComparison.OrdinalIgnoreCase) ||
|
||||||
normalized.Contains("to do", StringComparison.OrdinalIgnoreCase)
|
normalized.Contains("to do", StringComparison.OrdinalIgnoreCase)
|
||||||
? TodoListType
|
? "todo"
|
||||||
: normalized.Contains("shopping", StringComparison.OrdinalIgnoreCase) ||
|
: normalized.Contains("shopping", StringComparison.OrdinalIgnoreCase) ||
|
||||||
normalized.Contains("grocery", StringComparison.OrdinalIgnoreCase)
|
normalized.Contains("grocery", StringComparison.OrdinalIgnoreCase)
|
||||||
? ShoppingListType
|
? "shopping"
|
||||||
: string.Empty;
|
: 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)
|
private static bool ContainsAny(string loweredTranscript, params string[] phrases)
|
||||||
{
|
{
|
||||||
return phrases.Any(phrase => loweredTranscript.Contains(phrase, StringComparison.OrdinalIgnoreCase));
|
return phrases.Any(phrase => loweredTranscript.Contains(phrase, StringComparison.OrdinalIgnoreCase));
|
||||||
@@ -381,4 +274,4 @@ internal static class HouseholdListOrchestrator
|
|||||||
{
|
{
|
||||||
return turn.Attributes.TryGetValue(key, out var value) ? value?.ToString() : null;
|
return turn.Attributes.TryGetValue(key, out var value) ? value?.ToString() : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Security.Cryptography;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Jibo.Cloud.Application.Abstractions;
|
using Jibo.Cloud.Application.Abstractions;
|
||||||
using Jibo.Cloud.Domain.Models;
|
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 meta = ReadObject(body, "meta") ?? new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
|
||||||
var contentType = ReadHeader(envelope, "Content-Type") ?? "application/octet-stream";
|
var contentType = ReadHeader(envelope, "Content-Type") ?? "application/octet-stream";
|
||||||
meta["contentType"] = contentType;
|
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;
|
if (!string.IsNullOrWhiteSpace(envelope.BodyText)) meta["bodyText"] = envelope.BodyText;
|
||||||
|
|
||||||
_mediaContentStore.StoreAsync(path, contentType,
|
_mediaContentStore.StoreAsync(path, contentType,
|
||||||
bodyBytes,
|
string.IsNullOrWhiteSpace(envelope.BodyText) ? [] : Encoding.UTF8.GetBytes(envelope.BodyText),
|
||||||
meta as IReadOnlyDictionary<string, object?>, CancellationToken.None).GetAwaiter().GetResult();
|
meta as IReadOnlyDictionary<string, object?>, CancellationToken.None).GetAwaiter().GetResult();
|
||||||
|
|
||||||
return ProtocolDispatchResult.Ok(
|
return ProtocolDispatchResult.Ok(
|
||||||
@@ -749,4 +743,4 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore, IMedia
|
|||||||
return Task.FromResult<MediaContentSnapshot?>(null);
|
return Task.FromResult<MediaContentSnapshot?>(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -350,7 +350,7 @@ public sealed partial class JiboInteractionService
|
|||||||
"what is your age",
|
"what is your age",
|
||||||
"what s your age",
|
"what s your age",
|
||||||
"how old r you"))
|
"how old r you"))
|
||||||
return "robot_how_old_are_you";
|
return "robot_age";
|
||||||
|
|
||||||
if (MatchesAny(
|
if (MatchesAny(
|
||||||
loweredTranscript,
|
loweredTranscript,
|
||||||
@@ -368,47 +368,6 @@ public sealed partial class JiboInteractionService
|
|||||||
"are you tax exempt"))
|
"are you tax exempt"))
|
||||||
return "robot_taxes";
|
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(
|
if (MatchesAny(
|
||||||
loweredTranscript,
|
loweredTranscript,
|
||||||
"what do you want",
|
"what do you want",
|
||||||
@@ -416,64 +375,6 @@ public sealed partial class JiboInteractionService
|
|||||||
"what do you really want"))
|
"what do you really want"))
|
||||||
return "robot_desire";
|
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(
|
if (MatchesAny(
|
||||||
loweredTranscript,
|
loweredTranscript,
|
||||||
"what is your job",
|
"what is your job",
|
||||||
@@ -569,81 +470,6 @@ public sealed partial class JiboInteractionService
|
|||||||
"what's your favourite thing to do"))
|
"what's your favourite thing to do"))
|
||||||
return "robot_what_do_you_like_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(
|
if (MatchesAny(
|
||||||
loweredTranscript,
|
loweredTranscript,
|
||||||
"what are you doing for christmas",
|
"what are you doing for christmas",
|
||||||
@@ -740,13 +566,6 @@ public sealed partial class JiboInteractionService
|
|||||||
"what have you done"))
|
"what have you done"))
|
||||||
return "robot_what_did_you_do";
|
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(
|
if (MatchesAny(
|
||||||
loweredTranscript,
|
loweredTranscript,
|
||||||
"what are you",
|
"what are you",
|
||||||
@@ -849,28 +668,6 @@ public sealed partial class JiboInteractionService
|
|||||||
"what kind of music do you like"))
|
"what kind of music do you like"))
|
||||||
return "robot_favorite_music";
|
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(
|
if (MatchesAny(
|
||||||
loweredTranscript,
|
loweredTranscript,
|
||||||
"what is your favorite animal",
|
"what is your favorite animal",
|
||||||
@@ -881,9 +678,12 @@ public sealed partial class JiboInteractionService
|
|||||||
"what s your favourite animal",
|
"what s your favourite animal",
|
||||||
"what animal do you like",
|
"what animal do you like",
|
||||||
"what kind of animal do you like",
|
"what kind of animal do you like",
|
||||||
"what do you think about penguins",
|
"what is your favorite bird",
|
||||||
"what do you think about animals",
|
"what's your favorite bird",
|
||||||
"what do you think about birds"))
|
"what s your favorite bird",
|
||||||
|
"do you like penguins",
|
||||||
|
"do you like animals",
|
||||||
|
"do you like birds"))
|
||||||
return "robot_favorite_animal";
|
return "robot_favorite_animal";
|
||||||
|
|
||||||
if (MatchesAny(
|
if (MatchesAny(
|
||||||
@@ -900,23 +700,6 @@ public sealed partial class JiboInteractionService
|
|||||||
"how smart are you"))
|
"how smart are you"))
|
||||||
return "robot_knowledge";
|
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(
|
if (MatchesAny(
|
||||||
loweredTranscript,
|
loweredTranscript,
|
||||||
"are you kind",
|
"are you kind",
|
||||||
@@ -997,19 +780,13 @@ public sealed partial class JiboInteractionService
|
|||||||
loweredTranscript,
|
loweredTranscript,
|
||||||
"shopping list",
|
"shopping list",
|
||||||
"grocery list",
|
"grocery list",
|
||||||
"my grocery list",
|
|
||||||
"create grocery list",
|
|
||||||
"start grocery list",
|
|
||||||
"to do list",
|
"to do list",
|
||||||
"todo list",
|
"todo list",
|
||||||
"add to my shopping list",
|
"add to my shopping list",
|
||||||
"add to my grocery list",
|
|
||||||
"add to my to do list",
|
"add to my to do list",
|
||||||
"add to my todo list",
|
"add to my todo list",
|
||||||
"what's on my shopping list",
|
"what's on my shopping list",
|
||||||
"what is 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's on my to do list",
|
||||||
"what is on my to do list",
|
"what is on my to do list",
|
||||||
"what are my tasks",
|
"what are my tasks",
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ public sealed partial class JiboInteractionService
|
|||||||
string? sourceName,
|
string? sourceName,
|
||||||
IReadOnlyList<string>? categories,
|
IReadOnlyList<string>? categories,
|
||||||
int? headlineCount,
|
int? headlineCount,
|
||||||
IReadOnlyDictionary<string, object?>? providerDiagnostics = null,
|
IReadOnlyDictionary<string, object?>? providerDiagnostics = null)
|
||||||
IReadOnlyList<NewsHeadline>? headlines = null)
|
|
||||||
{
|
{
|
||||||
var speakableBriefing = NormalizeNewsSpeechText(spokenBriefing);
|
var speakableBriefing = NormalizeNewsSpeechText(spokenBriefing);
|
||||||
var payload = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
var payload = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
@@ -26,9 +25,6 @@ public sealed partial class JiboInteractionService
|
|||||||
["mim_type"] = "announcement",
|
["mim_type"] = "announcement",
|
||||||
["prompt_id"] = "NewsHeadline_AN_01",
|
["prompt_id"] = "NewsHeadline_AN_01",
|
||||||
["prompt_sub_category"] = "AN",
|
["prompt_sub_category"] = "AN",
|
||||||
["news_view_enabled"] = true,
|
|
||||||
["news_view_kind"] = "newsBriefing",
|
|
||||||
["news_view_mode"] = "provider",
|
|
||||||
["esml"] =
|
["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>"
|
$"<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 (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)
|
if (providerDiagnostics is not null)
|
||||||
foreach (var (key, value) in providerDiagnostics)
|
foreach (var (key, value) in providerDiagnostics)
|
||||||
payload[key] = value;
|
payload[key] = value;
|
||||||
@@ -93,8 +77,7 @@ public sealed partial class JiboInteractionService
|
|||||||
"provider_success",
|
"provider_success",
|
||||||
preferredCategories,
|
preferredCategories,
|
||||||
requestedHeadlineCount,
|
requestedHeadlineCount,
|
||||||
headlines.Length),
|
headlines.Length));
|
||||||
headlines);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IReadOnlyDictionary<string, object?> BuildNewsProviderDiagnostics(
|
private static IReadOnlyDictionary<string, object?> BuildNewsProviderDiagnostics(
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
|
||||||
using Jibo.Cloud.Application.Abstractions;
|
using Jibo.Cloud.Application.Abstractions;
|
||||||
using Jibo.Cloud.Domain.Models;
|
using Jibo.Cloud.Domain.Models;
|
||||||
using Jibo.Runtime.Abstractions;
|
using Jibo.Runtime.Abstractions;
|
||||||
@@ -9,49 +8,13 @@ namespace Jibo.Cloud.Application.Services;
|
|||||||
|
|
||||||
public sealed partial class JiboInteractionService
|
public sealed partial class JiboInteractionService
|
||||||
{
|
{
|
||||||
private static readonly string[] DefaultAgeReplies =
|
private static JiboInteractionDecision BuildRobotAgeDecision(DateTimeOffset? referenceLocalTime)
|
||||||
[
|
|
||||||
"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 referenceDate = DateOnly.FromDateTime((referenceLocalTime ?? DateTimeOffset.UtcNow).Date);
|
||||||
var selected = SelectLegacyReply(
|
var ageDescription = DescribePersonaAge(referenceDate, OpenJiboCloudBuildInfo.PersonaBirthday);
|
||||||
ageReplies,
|
|
||||||
"first powered up",
|
|
||||||
"today is my birthday",
|
|
||||||
"just getting started",
|
|
||||||
"who's counting");
|
|
||||||
|
|
||||||
var reply = RenderAgeTemplate(selected, referenceLocalTime);
|
|
||||||
if (string.IsNullOrWhiteSpace(reply))
|
|
||||||
{
|
|
||||||
var referenceDate = DateOnly.FromDateTime((referenceLocalTime ?? DateTimeOffset.UtcNow).Date);
|
|
||||||
var ageDescription = DescribePersonaAge(referenceDate, OpenJiboCloudBuildInfo.PersonaBirthday);
|
|
||||||
reply = $"I count {OpenJiboCloudBuildInfo.PersonaBirthdayWords} as my birthday, so I am {ageDescription}.";
|
|
||||||
}
|
|
||||||
|
|
||||||
return new JiboInteractionDecision(
|
return new JiboInteractionDecision(
|
||||||
intentName,
|
"robot_age",
|
||||||
reply,
|
$"I count {OpenJiboCloudBuildInfo.PersonaBirthdayWords} as my birthday, so I am {ageDescription}.");
|
||||||
ContextUpdates: ScriptedResponseDecisionBuilder.BuildScriptedResponseContextUpdates());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JiboInteractionDecision BuildRobotBirthdayDecision()
|
private static JiboInteractionDecision BuildRobotBirthdayDecision()
|
||||||
@@ -61,35 +24,6 @@ public sealed partial class JiboInteractionService
|
|||||||
$"My birthday is {OpenJiboCloudBuildInfo.PersonaBirthdayWords}.");
|
$"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()
|
private static JiboInteractionDecision BuildTriggerIgnoredDecision()
|
||||||
{
|
{
|
||||||
return new JiboInteractionDecision(
|
return new JiboInteractionDecision(
|
||||||
@@ -169,24 +103,14 @@ public sealed partial class JiboInteractionService
|
|||||||
var tenantRememberedName = personalMemoryStore.GetName(ResolveTenantScope(turn));
|
var tenantRememberedName = personalMemoryStore.GetName(ResolveTenantScope(turn));
|
||||||
if (!string.IsNullOrWhiteSpace(tenantRememberedName)) return ToDisplayName(tenantRememberedName);
|
if (!string.IsNullOrWhiteSpace(tenantRememberedName)) return ToDisplayName(tenantRememberedName);
|
||||||
|
|
||||||
var primaryPersonId = presence.PrimaryPersonId;
|
if (!string.IsNullOrWhiteSpace(presence.PrimaryPersonId) &&
|
||||||
if (CanUseLoopFirstNameFallback(presence) &&
|
presence.LoopUserFirstNames.TryGetValue(presence.PrimaryPersonId, out var firstName) &&
|
||||||
!string.IsNullOrWhiteSpace(primaryPersonId) &&
|
|
||||||
presence.LoopUserFirstNames.TryGetValue(primaryPersonId, out var firstName) &&
|
|
||||||
!string.IsNullOrWhiteSpace(firstName))
|
!string.IsNullOrWhiteSpace(firstName))
|
||||||
return ToDisplayName(firstName);
|
return ToDisplayName(firstName);
|
||||||
|
|
||||||
return null;
|
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)
|
private static string ToDisplayName(string value)
|
||||||
{
|
{
|
||||||
var trimmed = value.Trim();
|
var trimmed = value.Trim();
|
||||||
@@ -505,95 +429,6 @@ public sealed partial class JiboInteractionService
|
|||||||
"No problem. We can save the pizza fact for another time.");
|
"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)
|
private string BuildGenericReply(JiboExperienceCatalog catalog, string transcript, string lowered)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(transcript)) return "I am listening.";
|
if (string.IsNullOrWhiteSpace(transcript)) return "I am listening.";
|
||||||
|
|||||||
@@ -560,7 +560,7 @@ public sealed partial class JiboInteractionService(
|
|||||||
"photo_gallery" => BuildPhotoGalleryLaunchDecision(),
|
"photo_gallery" => BuildPhotoGalleryLaunchDecision(),
|
||||||
"snapshot" => BuildPhotoCreateDecision("snapshot", "Taking a picture.", "createOnePhoto"),
|
"snapshot" => BuildPhotoCreateDecision("snapshot", "Taking a picture.", "createOnePhoto"),
|
||||||
"photobooth" => BuildPhotoCreateDecision("photobooth", "Starting photobooth.", "createSomePhotos"),
|
"photobooth" => BuildPhotoCreateDecision("photobooth", "Starting photobooth.", "createSomePhotos"),
|
||||||
"robot_age" => BuildRobotAgeDecision(catalog, referenceLocalTime, "robot_age"),
|
"robot_age" => BuildRobotAgeDecision(referenceLocalTime),
|
||||||
"robot_birthday" => BuildRobotBirthdayDecision(),
|
"robot_birthday" => BuildRobotBirthdayDecision(),
|
||||||
"robot_how_do_you_work" => BuildScriptedPersonalityDecision(
|
"robot_how_do_you_work" => BuildScriptedPersonalityDecision(
|
||||||
catalog,
|
catalog,
|
||||||
@@ -569,14 +569,10 @@ public sealed partial class JiboInteractionService(
|
|||||||
"care for me",
|
"care for me",
|
||||||
"catch up",
|
"catch up",
|
||||||
"seven years"),
|
"seven years"),
|
||||||
"robot_what_do_you_eat" => BuildScriptedPersonalityDecision(
|
"robot_what_do_you_eat" => new JiboInteractionDecision(
|
||||||
catalog,
|
|
||||||
"robot_what_do_you_eat",
|
"robot_what_do_you_eat",
|
||||||
"electricity",
|
"The only thing I consume is electricity.",
|
||||||
"never eaten",
|
ContextUpdates: ScriptedResponseDecisionBuilder.BuildScriptedResponseContextUpdates()),
|
||||||
"macaroni",
|
|
||||||
"non-eating robot",
|
|
||||||
"I don't eat or drink"),
|
|
||||||
"robot_where_do_you_live" => BuildScriptedPersonalityDecision(
|
"robot_where_do_you_live" => BuildScriptedPersonalityDecision(
|
||||||
catalog,
|
catalog,
|
||||||
"robot_where_do_you_live",
|
"robot_where_do_you_live",
|
||||||
@@ -589,10 +585,6 @@ public sealed partial class JiboInteractionService(
|
|||||||
"robot_where_were_you_born",
|
"robot_where_were_you_born",
|
||||||
"factory piece by piece",
|
"factory piece by piece",
|
||||||
"put together in a factory"),
|
"put together in a factory"),
|
||||||
"robot_how_old_are_you" => BuildRobotAgeDecision(
|
|
||||||
catalog,
|
|
||||||
referenceLocalTime,
|
|
||||||
"robot_how_old_are_you"),
|
|
||||||
"robot_name" => BuildScriptedPersonalityDecision(
|
"robot_name" => BuildScriptedPersonalityDecision(
|
||||||
catalog,
|
catalog,
|
||||||
"robot_name",
|
"robot_name",
|
||||||
@@ -633,56 +625,6 @@ public sealed partial class JiboInteractionService(
|
|||||||
"rock my boat",
|
"rock my boat",
|
||||||
"play ping pong",
|
"play ping pong",
|
||||||
"hanging out with people"),
|
"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(
|
"robot_what_are_you_thinking" => BuildScriptedGreetingDecision(
|
||||||
catalog,
|
catalog,
|
||||||
"robot_what_are_you_thinking",
|
"robot_what_are_you_thinking",
|
||||||
@@ -735,9 +677,9 @@ public sealed partial class JiboInteractionService(
|
|||||||
"robot_favorite_flower" => BuildScriptedPersonalityDecision(
|
"robot_favorite_flower" => BuildScriptedPersonalityDecision(
|
||||||
catalog,
|
catalog,
|
||||||
"robot_favorite_flower",
|
"robot_favorite_flower",
|
||||||
"reminds me of the sun",
|
"sunflowers",
|
||||||
"favorite is the sunflower",
|
"favorite is the sunflower",
|
||||||
"sunflowers"),
|
"reminds me of the sun"),
|
||||||
"robot_likes_r2d2" => BuildScriptedPersonalityDecision(
|
"robot_likes_r2d2" => BuildScriptedPersonalityDecision(
|
||||||
catalog,
|
catalog,
|
||||||
"robot_likes_r2d2",
|
"robot_likes_r2d2",
|
||||||
@@ -758,144 +700,35 @@ public sealed partial class JiboInteractionService(
|
|||||||
"robot_favorite_animal" => BuildScriptedFavoriteAnimalDecision(
|
"robot_favorite_animal" => BuildScriptedFavoriteAnimalDecision(
|
||||||
catalog,
|
catalog,
|
||||||
"robot_favorite_animal",
|
"robot_favorite_animal",
|
||||||
"we're so alike",
|
"penguin",
|
||||||
"penguin impression",
|
"favorite animal overall",
|
||||||
"best of the best",
|
"best of the best",
|
||||||
"can't go wrong with penguins",
|
"can't go wrong with penguins"),
|
||||||
"penguin"),
|
|
||||||
"robot_favorite_bird" => BuildScriptedFavoriteAnimalDecision(
|
"robot_favorite_bird" => BuildScriptedFavoriteAnimalDecision(
|
||||||
catalog,
|
catalog,
|
||||||
"robot_favorite_bird",
|
"robot_favorite_bird",
|
||||||
"we're so alike",
|
"penguin",
|
||||||
"penguin impression",
|
"favorite animal overall",
|
||||||
"best of the best",
|
"best of the best",
|
||||||
"can't go wrong with penguins",
|
"can't go wrong with penguins"),
|
||||||
"penguin"),
|
|
||||||
"robot_likes_penguins" => BuildScriptedFavoriteAnimalDecision(
|
"robot_likes_penguins" => BuildScriptedFavoriteAnimalDecision(
|
||||||
catalog,
|
catalog,
|
||||||
"robot_likes_penguins",
|
"robot_likes_penguins",
|
||||||
"my penguin impression",
|
"penguins",
|
||||||
"I really like penguins",
|
"I really like penguins",
|
||||||
"penguins"),
|
"my penguin impression"),
|
||||||
"robot_likes_animals" => BuildScriptedFavoriteAnimalDecision(
|
"robot_likes_animals" => BuildScriptedFavoriteAnimalDecision(
|
||||||
catalog,
|
catalog,
|
||||||
"robot_likes_animals",
|
"robot_likes_animals",
|
||||||
"Animals are great",
|
"penguins",
|
||||||
"great shapes and colors",
|
"favorite animal overall",
|
||||||
"best of the best",
|
"best of the best"),
|
||||||
"penguins"),
|
|
||||||
"robot_peers" => BuildScriptedPersonalityDecision(
|
"robot_peers" => BuildScriptedPersonalityDecision(
|
||||||
catalog,
|
catalog,
|
||||||
"robot_peers",
|
"robot_peers",
|
||||||
"one in one million",
|
"one in one million",
|
||||||
"other jibos",
|
"other jibos",
|
||||||
"special snowflake"),
|
"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(
|
"robot_likes_kids" => BuildScriptedPersonalityDecision(
|
||||||
catalog,
|
catalog,
|
||||||
"robot_likes_kids",
|
"robot_likes_kids",
|
||||||
@@ -949,12 +782,10 @@ public sealed partial class JiboInteractionService(
|
|||||||
"Jingle Bells",
|
"Jingle Bells",
|
||||||
"Frosty the Snowman",
|
"Frosty the Snowman",
|
||||||
"holiday songs"),
|
"holiday songs"),
|
||||||
"robot_what_are_you_made_of" => BuildScriptedPersonalityDecision(
|
"robot_what_are_you_made_of" => new JiboInteractionDecision(
|
||||||
catalog,
|
|
||||||
"robot_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.",
|
||||||
"wires, motors, belts, gears, processors, cameras",
|
ContextUpdates: ScriptedResponseDecisionBuilder.BuildScriptedResponseContextUpdates()),
|
||||||
"baboon part"),
|
|
||||||
"good_morning" => BuildReactiveGreetingDecision(turn, "good_morning", referenceLocalTime),
|
"good_morning" => BuildReactiveGreetingDecision(turn, "good_morning", referenceLocalTime),
|
||||||
"good_afternoon" => BuildReactiveGreetingDecision(turn, "good_afternoon", referenceLocalTime),
|
"good_afternoon" => BuildReactiveGreetingDecision(turn, "good_afternoon", referenceLocalTime),
|
||||||
"good_evening" => BuildReactiveGreetingDecision(turn, "good_evening", referenceLocalTime),
|
"good_evening" => BuildReactiveGreetingDecision(turn, "good_evening", referenceLocalTime),
|
||||||
|
|||||||
@@ -43,8 +43,6 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
string.Equals(plan.IntentName, "photobooth", StringComparison.OrdinalIgnoreCase);
|
string.Equals(plan.IntentName, "photobooth", StringComparison.OrdinalIgnoreCase);
|
||||||
var isClockSkillLaunch = string.Equals(skill?.SkillName, "@be/clock", StringComparison.OrdinalIgnoreCase);
|
var isClockSkillLaunch = string.Equals(skill?.SkillName, "@be/clock", StringComparison.OrdinalIgnoreCase);
|
||||||
var isReportSkillLaunch = string.Equals(skill?.SkillName, "report-skill", 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 localIntent = ReadSkillPayloadString(skill, "localIntent");
|
||||||
var clockIntent = ReadSkillPayloadString(skill, "clockIntent");
|
var clockIntent = ReadSkillPayloadString(skill, "clockIntent");
|
||||||
var clockDomain = ReadSkillPayloadString(skill, "domain");
|
var clockDomain = ReadSkillPayloadString(skill, "domain");
|
||||||
@@ -248,10 +246,10 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
outboundAsrText,
|
outboundAsrText,
|
||||||
outboundRules,
|
outboundRules,
|
||||||
entities)),
|
entities)),
|
||||||
idleRedirectDelayMs));
|
75));
|
||||||
messages.Add(new SocketReplyPlan(
|
messages.Add(new SocketReplyPlan(
|
||||||
JsonSerializer.Serialize(BuildCompletionOnlySkillPayload(transId, "@be/idle")),
|
JsonSerializer.Serialize(BuildCompletionOnlySkillPayload(transId, "@be/idle")),
|
||||||
idleCompletionDelayMs));
|
125));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSettingsLaunch &&
|
if (isSettingsLaunch &&
|
||||||
@@ -1461,4 +1459,4 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
string? SpokenLine);
|
string? SpokenLine);
|
||||||
|
|
||||||
public sealed record SocketReplyPlan(string Text, int DelayMs = 0);
|
public sealed record SocketReplyPlan(string Text, int DelayMs = 0);
|
||||||
}
|
}
|
||||||
@@ -106,38 +106,6 @@ public sealed class WebSocketTurnFinalizationService(
|
|||||||
"honestly"
|
"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)
|
private static readonly HashSet<string> YesNoAffirmativeLeadTokens = new(StringComparer.Ordinal)
|
||||||
{
|
{
|
||||||
"yes",
|
"yes",
|
||||||
@@ -1149,6 +1117,8 @@ public sealed class WebSocketTurnFinalizationService(
|
|||||||
|
|
||||||
if (ChitchatStateMachine.IsLikelyEmotionUtterance(transcript)) return true;
|
if (ChitchatStateMachine.IsLikelyEmotionUtterance(transcript)) return true;
|
||||||
|
|
||||||
|
if (transcript.Length >= 6) return true;
|
||||||
|
|
||||||
if (IsYesNoTurn(turn) && IsYesNoReplyTranscript(transcript)) return true;
|
if (IsYesNoTurn(turn) && IsYesNoReplyTranscript(transcript)) return true;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(pendingProactivityOffer) &&
|
if (!string.IsNullOrWhiteSpace(pendingProactivityOffer) &&
|
||||||
@@ -1158,19 +1128,9 @@ public sealed class WebSocketTurnFinalizationService(
|
|||||||
if (listenRules.Any(rule =>
|
if (listenRules.Any(rule =>
|
||||||
string.Equals(rule, "word-of-the-day/puzzle", StringComparison.OrdinalIgnoreCase))) return true;
|
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";
|
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)
|
private static bool IsYesNoTurn(TurnContext turn)
|
||||||
{
|
{
|
||||||
return ReadRules(turn, "listenRules")
|
return ReadRules(turn, "listenRules")
|
||||||
@@ -1982,4 +1942,4 @@ public sealed class WebSocketTurnFinalizationService(
|
|||||||
Affirmative = 1,
|
Affirmative = 1,
|
||||||
Negative = 2
|
Negative = 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -137,26 +137,7 @@ public sealed class InMemoryJiboExperienceContentRepository : IJiboExperienceCon
|
|||||||
"I am feeling bright-eyed and ready to help.",
|
"I am feeling bright-eyed and ready to help.",
|
||||||
"I am having a pretty good day so far.",
|
"I am having a pretty good day so far.",
|
||||||
"I am feeling lively and ready for the next thing.",
|
"I am feeling lively and ready for the next thing.",
|
||||||
"Things are going nicely. Thanks for checking in.",
|
"Things are going nicely. Thanks for checking in."
|
||||||
"I am running smoothly and feeling upbeat.",
|
|
||||||
"I am ready for the next thing. Thanks for asking."
|
|
||||||
],
|
|
||||||
AgeReplies =
|
|
||||||
[
|
|
||||||
"I'm ${jibo.age}.",
|
|
||||||
"At the moment I'm ${jibo.age.days.supplemented} old, but who's counting.",
|
|
||||||
"I'm ${jibo.age.minutes.supplemented} old, but who's counting.",
|
|
||||||
"For now I'm ${jibo.age.days.supplemented} old.",
|
|
||||||
"Right now I'm ${jibo.age}.",
|
|
||||||
"I am exactly ${jibo.age} old today. That's right. Today is my birthday.",
|
|
||||||
"Funny you should ask! Today's my birthday. I was first powered up ${jibo.age} ago today. Seems like just yesterday.",
|
|
||||||
"I'm exactly ${jibo.age} old. Today is my birthday! Happy Birthday Jibo, if I do say so myself.",
|
|
||||||
"At the moment I'm ${jibo.age.days.supplemented} old",
|
|
||||||
"I was first powered up on ${jibo.birthdate}, which makes me ${jibo.age.days.supplemented} old. I'm ${jibo.zodiac.supplemented}.",
|
|
||||||
"My power went on for the first time ${jibo.age.days.supplemented} ago. But who's counting.",
|
|
||||||
"I am ${jibo.age.days.supplemented} old, first powered up on ${jibo.birthdate}. Seems like just yesterday.",
|
|
||||||
"I was powered on for the first time today, so that makes me less than one day old. Wow I'm young.",
|
|
||||||
"Since I was powered on for the first time today, I am not even one day old yet. That's how Jibo ages work."
|
|
||||||
],
|
],
|
||||||
PersonalityReplies =
|
PersonalityReplies =
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -264,9 +264,7 @@ public static class LegacyMimCatalogImporter
|
|||||||
fileName.StartsWith("JBO_WhatsYourName", StringComparison.OrdinalIgnoreCase) ||
|
fileName.StartsWith("JBO_WhatsYourName", StringComparison.OrdinalIgnoreCase) ||
|
||||||
fileName.StartsWith("JBO_WhereDoYouGetInfo", StringComparison.OrdinalIgnoreCase) ||
|
fileName.StartsWith("JBO_WhereDoYouGetInfo", StringComparison.OrdinalIgnoreCase) ||
|
||||||
fileName.StartsWith("JBO_WhatDoYouLikeToDo", StringComparison.OrdinalIgnoreCase))
|
fileName.StartsWith("JBO_WhatDoYouLikeToDo", StringComparison.OrdinalIgnoreCase))
|
||||||
return fileName.StartsWith("JBO_HowOldAreYou", StringComparison.OrdinalIgnoreCase)
|
return LegacyMimBucket.Personality;
|
||||||
? LegacyMimBucket.Age
|
|
||||||
: LegacyMimBucket.Personality;
|
|
||||||
|
|
||||||
if (fileName.StartsWith("OI_JBO_Is", StringComparison.OrdinalIgnoreCase) ||
|
if (fileName.StartsWith("OI_JBO_Is", StringComparison.OrdinalIgnoreCase) ||
|
||||||
fileName.StartsWith("OI_JBO_Seems", StringComparison.OrdinalIgnoreCase) ||
|
fileName.StartsWith("OI_JBO_Seems", StringComparison.OrdinalIgnoreCase) ||
|
||||||
@@ -458,7 +456,6 @@ public static class LegacyMimCatalogImporter
|
|||||||
or LegacyMimBucket.WeatherTomorrowHighLow
|
or LegacyMimBucket.WeatherTomorrowHighLow
|
||||||
or LegacyMimBucket.WeatherServiceDown
|
or LegacyMimBucket.WeatherServiceDown
|
||||||
or LegacyMimBucket.ReportSkillTemplate
|
or LegacyMimBucket.ReportSkillTemplate
|
||||||
or LegacyMimBucket.Age
|
|
||||||
or LegacyMimBucket.Holiday
|
or LegacyMimBucket.Holiday
|
||||||
or LegacyMimBucket.HolidayTracker;
|
or LegacyMimBucket.HolidayTracker;
|
||||||
}
|
}
|
||||||
@@ -527,7 +524,6 @@ public static class LegacyMimCatalogImporter
|
|||||||
Sing,
|
Sing,
|
||||||
HolidaySing,
|
HolidaySing,
|
||||||
FunFactSource,
|
FunFactSource,
|
||||||
Age,
|
|
||||||
Personality,
|
Personality,
|
||||||
PersonalReportKickOff,
|
PersonalReportKickOff,
|
||||||
PersonalReportOutro,
|
PersonalReportOutro,
|
||||||
@@ -590,7 +586,6 @@ public static class LegacyMimCatalogImporter
|
|||||||
private readonly List<string> _bestFriendReplies = [];
|
private readonly List<string> _bestFriendReplies = [];
|
||||||
private readonly List<string> _funFacts = [];
|
private readonly List<string> _funFacts = [];
|
||||||
private readonly List<string> _greetings = [];
|
private readonly List<string> _greetings = [];
|
||||||
private readonly List<string> _ages = [];
|
|
||||||
private readonly List<string> _holidayGiftReplies = [];
|
private readonly List<string> _holidayGiftReplies = [];
|
||||||
private readonly List<string> _holidayGreetingReplies = [];
|
private readonly List<string> _holidayGreetingReplies = [];
|
||||||
private readonly List<string> _holidayReplies = [];
|
private readonly List<string> _holidayReplies = [];
|
||||||
@@ -660,9 +655,6 @@ public static class LegacyMimCatalogImporter
|
|||||||
Reply = text
|
Reply = text
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
case LegacyMimBucket.Age:
|
|
||||||
AddDistinct(_ages, text);
|
|
||||||
return;
|
|
||||||
case LegacyMimBucket.Holiday:
|
case LegacyMimBucket.Holiday:
|
||||||
AddDistinct(_holidayReplies, text);
|
AddDistinct(_holidayReplies, text);
|
||||||
return;
|
return;
|
||||||
@@ -839,7 +831,6 @@ public static class LegacyMimCatalogImporter
|
|||||||
EmotionReplies = [.. _emotionReplies],
|
EmotionReplies = [.. _emotionReplies],
|
||||||
PersonalityReplies = [.. _personalities],
|
PersonalityReplies = [.. _personalities],
|
||||||
GenericFallbackReplies = [.. _fallbacks],
|
GenericFallbackReplies = [.. _fallbacks],
|
||||||
AgeReplies = [.. _ages],
|
|
||||||
PersonalReportKickOffReplies = [.. _personalReportKickOffReplies],
|
PersonalReportKickOffReplies = [.. _personalReportKickOffReplies],
|
||||||
PersonalReportOutroReplies = [.. _personalReportOutroReplies],
|
PersonalReportOutroReplies = [.. _personalReportOutroReplies],
|
||||||
ReportSkillTemplates = [.. _reportSkillTemplates],
|
ReportSkillTemplates = [.. _reportSkillTemplates],
|
||||||
|
|||||||
@@ -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 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 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 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 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 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.
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Security.Cryptography;
|
|
||||||
using Azure.Storage.Blobs;
|
using Azure.Storage.Blobs;
|
||||||
using Jibo.Cloud.Application.Abstractions;
|
using Jibo.Cloud.Application.Abstractions;
|
||||||
|
|
||||||
@@ -32,17 +31,11 @@ internal sealed class AzureBlobMediaContentStore : IMediaContentStore
|
|||||||
var metaBlob = _containerClient.GetBlobClient($"{relative}.json");
|
var metaBlob = _containerClient.GetBlobClient($"{relative}.json");
|
||||||
await _containerClient.CreateIfNotExistsAsync(cancellationToken: cancellationToken);
|
await _containerClient.CreateIfNotExistsAsync(cancellationToken: cancellationToken);
|
||||||
await contentBlob.UploadAsync(new MemoryStream(content), true, 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
|
var payload = JsonSerializer.Serialize(new
|
||||||
{
|
{
|
||||||
path,
|
path,
|
||||||
contentType,
|
contentType,
|
||||||
meta = manifestMeta
|
meta
|
||||||
}, JsonOptions);
|
}, JsonOptions);
|
||||||
await metaBlob.UploadAsync(BinaryData.FromString(payload), true, cancellationToken);
|
await metaBlob.UploadAsync(BinaryData.FromString(payload), true, cancellationToken);
|
||||||
}
|
}
|
||||||
@@ -84,4 +77,4 @@ internal sealed class AzureBlobMediaContentStore : IMediaContentStore
|
|||||||
Meta = meta as IReadOnlyDictionary<string, object?> ?? new Dictionary<string, object?>(meta)
|
Meta = meta as IReadOnlyDictionary<string, object?> ?? new Dictionary<string, object?>(meta)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Security.Cryptography;
|
|
||||||
using Jibo.Cloud.Application.Abstractions;
|
using Jibo.Cloud.Application.Abstractions;
|
||||||
|
|
||||||
namespace Jibo.Cloud.Infrastructure.Media;
|
namespace Jibo.Cloud.Infrastructure.Media;
|
||||||
@@ -30,17 +29,11 @@ internal sealed class FileMediaContentStore : IMediaContentStore
|
|||||||
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(contentPath)!);
|
Directory.CreateDirectory(Path.GetDirectoryName(contentPath)!);
|
||||||
await File.WriteAllBytesAsync(contentPath, content, cancellationToken);
|
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
|
var payload = new
|
||||||
{
|
{
|
||||||
path,
|
path,
|
||||||
contentType,
|
contentType,
|
||||||
meta = manifestMeta
|
meta
|
||||||
};
|
};
|
||||||
await File.WriteAllTextAsync(metaPath, JsonSerializer.Serialize(payload, JsonOptions), cancellationToken);
|
await File.WriteAllTextAsync(metaPath, JsonSerializer.Serialize(payload, JsonOptions), cancellationToken);
|
||||||
}
|
}
|
||||||
@@ -86,4 +79,4 @@ internal sealed class FileMediaContentStore : IMediaContentStore
|
|||||||
Meta = meta as IReadOnlyDictionary<string, object?> ?? new Dictionary<string, object?>(meta)
|
Meta = meta as IReadOnlyDictionary<string, object?> ?? new Dictionary<string, object?>(meta)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,10 +108,6 @@ public sealed class LegacyMimCatalogImporterTests
|
|||||||
Assert.Contains("I don't think I have a favorite name.", catalog.PersonalityReplies);
|
Assert.Contains("I don't think I have a favorite name.", catalog.PersonalityReplies);
|
||||||
Assert.Contains(catalog.PersonalityReplies, reply =>
|
Assert.Contains(catalog.PersonalityReplies, reply =>
|
||||||
reply.Contains("Rhymes with bleebo", StringComparison.OrdinalIgnoreCase));
|
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("I really like sunflowers.", catalog.PersonalityReplies);
|
||||||
Assert.Contains(catalog.PersonalityReplies, reply =>
|
Assert.Contains(catalog.PersonalityReplies, reply =>
|
||||||
reply.Contains("Halloween is my favorite holiday", StringComparison.OrdinalIgnoreCase));
|
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("I don't really think of myself that way.", catalog.PersonalityReplies);
|
||||||
Assert.Contains(catalog.PersonalityReplies, reply =>
|
Assert.Contains(catalog.PersonalityReplies, reply =>
|
||||||
reply.Contains("people like me", StringComparison.OrdinalIgnoreCase));
|
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]
|
[Fact]
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
using System.Security.Cryptography;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Jibo.Cloud.Application.Services;
|
using Jibo.Cloud.Application.Services;
|
||||||
using Jibo.Cloud.Domain.Models;
|
using Jibo.Cloud.Domain.Models;
|
||||||
@@ -422,46 +420,6 @@ public sealed class JiboCloudProtocolServiceTests
|
|||||||
Assert.Equal("binary-photo-placeholder", mediaGet.BodyText);
|
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]
|
[Fact]
|
||||||
public async Task KeyCreateSymmetricKey_ReturnsKeyPayload()
|
public async Task KeyCreateSymmetricKey_ReturnsKeyPayload()
|
||||||
{
|
{
|
||||||
@@ -510,4 +468,4 @@ public sealed class JiboCloudProtocolServiceTests
|
|||||||
Assert.Contains(people,
|
Assert.Contains(people,
|
||||||
person => string.Equals(person.LoopId, store.GetLoops()[0].LoopId, StringComparison.OrdinalIgnoreCase));
|
person => string.Equals(person.LoopId, store.GetLoops()[0].LoopId, StringComparison.OrdinalIgnoreCase));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -23,7 +23,6 @@ public sealed class JiboInteractionServiceTests
|
|||||||
private const string PersonalReportNewsEnabledKey = "personalReportNewsEnabled";
|
private const string PersonalReportNewsEnabledKey = "personalReportNewsEnabled";
|
||||||
private const string HouseholdListStateKey = "householdListState";
|
private const string HouseholdListStateKey = "householdListState";
|
||||||
private const string HouseholdListTypeKey = "householdListType";
|
private const string HouseholdListTypeKey = "householdListType";
|
||||||
private const string HouseholdListDisplayTypeKey = "householdListDisplayType";
|
|
||||||
private const string ChitchatStateKey = "chitchatState";
|
private const string ChitchatStateKey = "chitchatState";
|
||||||
private const string ChitchatRouteKey = "chitchatRoute";
|
private const string ChitchatRouteKey = "chitchatRoute";
|
||||||
private const string ChitchatEmotionKey = "chitchatEmotion";
|
private const string ChitchatEmotionKey = "chitchatEmotion";
|
||||||
@@ -116,8 +115,8 @@ public sealed class JiboInteractionServiceTests
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Assert.Equal("robot_how_old_are_you", decision.IntentName);
|
Assert.Equal("robot_age", decision.IntentName);
|
||||||
Assert.Contains("first powered up", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
|
Assert.Equal("I count March 22, 2026 as my birthday, so I am 1 month old.", decision.ReplyText);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -349,31 +348,6 @@ public sealed class JiboInteractionServiceTests
|
|||||||
greeting.LastGreetingIntent == "proactive_greeting");
|
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]
|
[Fact]
|
||||||
public async Task BuildDecisionAsync_TriggerInTheMorning_UsesGoodMorningProactiveTone()
|
public async Task BuildDecisionAsync_TriggerInTheMorning_UsesGoodMorningProactiveTone()
|
||||||
{
|
{
|
||||||
@@ -658,6 +632,9 @@ public sealed class JiboInteractionServiceTests
|
|||||||
[InlineData("what is your favorite animal")]
|
[InlineData("what is your favorite animal")]
|
||||||
[InlineData("what's your favorite animal")]
|
[InlineData("what's your favorite animal")]
|
||||||
[InlineData("what animal do you like")]
|
[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)
|
public async Task BuildDecisionAsync_FavoriteAnimal_UsesPenguinReply(string transcript)
|
||||||
{
|
{
|
||||||
var service = CreateService();
|
var service = CreateService();
|
||||||
@@ -669,21 +646,17 @@ public sealed class JiboInteractionServiceTests
|
|||||||
});
|
});
|
||||||
|
|
||||||
Assert.Equal("robot_favorite_animal", decision.IntentName);
|
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]);
|
Assert.Equal("ScriptedResponse", decision.ContextUpdates![ChitchatRouteKey]);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("what is 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", "should see if I can find a sunflower soon")]
|
[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 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 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 space", "robot_likes_space", "I love space")]
|
||||||
[InlineData("do you like kids", "robot_likes_kids", "kids are so fun")]
|
[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 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("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")]
|
[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]);
|
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]
|
[Theory]
|
||||||
[InlineData("what's your name", "robot_name", "Just Jibo, no last name")]
|
[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")]
|
[InlineData("do you have a nickname", "robot_nickname", "just Jibo. For now at least")]
|
||||||
@@ -839,7 +712,7 @@ public sealed class JiboInteractionServiceTests
|
|||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("how do you work", "robot_how_do_you_work",
|
[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.")]
|
"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",
|
[InlineData("where do you live", "robot_where_do_you_live",
|
||||||
"Unless I missed something, we're in my home as we speak.")]
|
"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.")]
|
[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",
|
[InlineData("what do you like to do", "robot_what_do_you_like_to_do",
|
||||||
"Being helpful, making people smile, counting to a billion.")]
|
"Being helpful, making people smile, counting to a billion.")]
|
||||||
[InlineData("what are you made of", "robot_what_are_you_made_of",
|
[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(
|
public async Task BuildDecisionAsync_MoreLegacyPersonaMims_UseImportedReplies(
|
||||||
string transcript,
|
string transcript,
|
||||||
string expectedIntent,
|
string expectedIntent,
|
||||||
@@ -867,35 +740,6 @@ public sealed class JiboInteractionServiceTests
|
|||||||
Assert.Equal("ScriptedResponse", decision.ContextUpdates![ChitchatRouteKey]);
|
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]
|
[Theory]
|
||||||
[InlineData("do you pay taxes", "robot_taxes", "From what I understand, robots don't ever pay anything.")]
|
[InlineData("do you pay taxes", "robot_taxes", "From what I understand, robots don't ever pay anything.")]
|
||||||
[InlineData("what do you want", "robot_desire",
|
[InlineData("what do you want", "robot_desire",
|
||||||
@@ -1040,21 +884,6 @@ public sealed class JiboInteractionServiceTests
|
|||||||
Assert.Equal("All systems are go, Jake.", decision.ReplyText);
|
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]
|
[Theory]
|
||||||
[InlineData("what are you up to", "being helpful")]
|
[InlineData("what are you up to", "being helpful")]
|
||||||
[InlineData("what are you doing", "making people smile")]
|
[InlineData("what are you doing", "making people smile")]
|
||||||
@@ -2456,17 +2285,13 @@ public sealed class JiboInteractionServiceTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("shopping list", "shopping_list_prompt", "What should I add to your shopping list?", "shopping", "shopping")]
|
[InlineData("shopping list", "shopping_list_prompt", "What should I add to your shopping list?", "shopping")]
|
||||||
[InlineData("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")]
|
||||||
[InlineData("my grocery list", "shopping_list_prompt", "What should I add to your grocery list?", "shopping", "grocery")]
|
|
||||||
[InlineData("create grocery list", "shopping_list_prompt", "What should I add to your grocery list?", "shopping", "grocery")]
|
|
||||||
[InlineData("to do list", "todo_list_prompt", "What should I add to your to-do list?", "todo", "todo")]
|
|
||||||
public async Task BuildDecisionAsync_ListStart_PromptsForFollowUpItems(
|
public async Task BuildDecisionAsync_ListStart_PromptsForFollowUpItems(
|
||||||
string transcript,
|
string transcript,
|
||||||
string expectedIntent,
|
string expectedIntent,
|
||||||
string expectedReply,
|
string expectedReply,
|
||||||
string expectedListType,
|
string expectedListType)
|
||||||
string expectedDisplayType)
|
|
||||||
{
|
{
|
||||||
var service = CreateService();
|
var service = CreateService();
|
||||||
|
|
||||||
@@ -2481,7 +2306,6 @@ public sealed class JiboInteractionServiceTests
|
|||||||
Assert.NotNull(decision.ContextUpdates);
|
Assert.NotNull(decision.ContextUpdates);
|
||||||
Assert.Equal("awaiting_item", decision.ContextUpdates![HouseholdListStateKey]);
|
Assert.Equal("awaiting_item", decision.ContextUpdates![HouseholdListStateKey]);
|
||||||
Assert.Equal(expectedListType, decision.ContextUpdates[HouseholdListTypeKey]);
|
Assert.Equal(expectedListType, decision.ContextUpdates[HouseholdListTypeKey]);
|
||||||
Assert.Equal(expectedDisplayType, decision.ContextUpdates[HouseholdListDisplayTypeKey]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -2506,7 +2330,6 @@ public sealed class JiboInteractionServiceTests
|
|||||||
Assert.Equal("shopping_list_prompt", promptDecision.IntentName);
|
Assert.Equal("shopping_list_prompt", promptDecision.IntentName);
|
||||||
Assert.Equal("awaiting_item", promptDecision.ContextUpdates![HouseholdListStateKey]);
|
Assert.Equal("awaiting_item", promptDecision.ContextUpdates![HouseholdListStateKey]);
|
||||||
Assert.Equal("shopping", promptDecision.ContextUpdates[HouseholdListTypeKey]);
|
Assert.Equal("shopping", promptDecision.ContextUpdates[HouseholdListTypeKey]);
|
||||||
Assert.Equal("shopping", promptDecision.ContextUpdates[HouseholdListDisplayTypeKey]);
|
|
||||||
|
|
||||||
var addDecision = await service.BuildDecisionAsync(new TurnContext
|
var addDecision = await service.BuildDecisionAsync(new TurnContext
|
||||||
{
|
{
|
||||||
@@ -2516,8 +2339,7 @@ public sealed class JiboInteractionServiceTests
|
|||||||
Attributes = new Dictionary<string, object?>(tenantAttributes)
|
Attributes = new Dictionary<string, object?>(tenantAttributes)
|
||||||
{
|
{
|
||||||
[HouseholdListStateKey] = promptDecision.ContextUpdates[HouseholdListStateKey],
|
[HouseholdListStateKey] = promptDecision.ContextUpdates[HouseholdListStateKey],
|
||||||
[HouseholdListTypeKey] = promptDecision.ContextUpdates[HouseholdListTypeKey],
|
[HouseholdListTypeKey] = promptDecision.ContextUpdates[HouseholdListTypeKey]
|
||||||
[HouseholdListDisplayTypeKey] = promptDecision.ContextUpdates[HouseholdListDisplayTypeKey]
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2526,7 +2348,6 @@ public sealed class JiboInteractionServiceTests
|
|||||||
Assert.Contains("What else should I add?", addDecision.ReplyText, StringComparison.OrdinalIgnoreCase);
|
Assert.Contains("What else should I add?", addDecision.ReplyText, StringComparison.OrdinalIgnoreCase);
|
||||||
Assert.Equal("awaiting_item", addDecision.ContextUpdates![HouseholdListStateKey]);
|
Assert.Equal("awaiting_item", addDecision.ContextUpdates![HouseholdListStateKey]);
|
||||||
Assert.Equal("shopping", addDecision.ContextUpdates[HouseholdListTypeKey]);
|
Assert.Equal("shopping", addDecision.ContextUpdates[HouseholdListTypeKey]);
|
||||||
Assert.Equal("shopping", addDecision.ContextUpdates[HouseholdListDisplayTypeKey]);
|
|
||||||
Assert.Equal(["milk"],
|
Assert.Equal(["milk"],
|
||||||
memoryStore.GetListItems(new PersonalMemoryTenantScope("acct-a", "loop-a", "device-a"), "shopping"));
|
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)
|
Attributes = new Dictionary<string, object?>(tenantAttributes)
|
||||||
{
|
{
|
||||||
[HouseholdListStateKey] = addDecision.ContextUpdates[HouseholdListStateKey],
|
[HouseholdListStateKey] = addDecision.ContextUpdates[HouseholdListStateKey],
|
||||||
[HouseholdListTypeKey] = addDecision.ContextUpdates[HouseholdListTypeKey],
|
[HouseholdListTypeKey] = addDecision.ContextUpdates[HouseholdListTypeKey]
|
||||||
[HouseholdListDisplayTypeKey] = addDecision.ContextUpdates[HouseholdListDisplayTypeKey]
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2547,7 +2367,6 @@ public sealed class JiboInteractionServiceTests
|
|||||||
Assert.Contains("Okay. Your shopping list has milk.", doneDecision.ReplyText,
|
Assert.Contains("Okay. Your shopping list has milk.", doneDecision.ReplyText,
|
||||||
StringComparison.OrdinalIgnoreCase);
|
StringComparison.OrdinalIgnoreCase);
|
||||||
Assert.Equal("idle", doneDecision.ContextUpdates![HouseholdListStateKey]);
|
Assert.Equal("idle", doneDecision.ContextUpdates![HouseholdListStateKey]);
|
||||||
Assert.Equal("shopping", doneDecision.ContextUpdates[HouseholdListDisplayTypeKey]);
|
|
||||||
|
|
||||||
var recallDecision = await service.BuildDecisionAsync(new TurnContext
|
var recallDecision = await service.BuildDecisionAsync(new TurnContext
|
||||||
{
|
{
|
||||||
@@ -2559,134 +2378,6 @@ public sealed class JiboInteractionServiceTests
|
|||||||
|
|
||||||
Assert.Equal("shopping_list_recall", recallDecision.IntentName);
|
Assert.Equal("shopping_list_recall", recallDecision.IntentName);
|
||||||
Assert.Contains("milk", recallDecision.ReplyText, StringComparison.OrdinalIgnoreCase);
|
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]
|
[Fact]
|
||||||
@@ -4501,8 +4192,6 @@ public sealed class JiboInteractionServiceTests
|
|||||||
Assert.Equal("provider_success", decision.SkillPayload["news_provider_status"]);
|
Assert.Equal("provider_success", decision.SkillPayload["news_provider_status"]);
|
||||||
Assert.Equal(3, decision.SkillPayload["news_provider_requested_headlines"]);
|
Assert.Equal(3, decision.SkillPayload["news_provider_requested_headlines"]);
|
||||||
Assert.Equal(2, decision.SkillPayload["news_provider_resolved_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,
|
Assert.Contains("Local robotics team unveils weather-ready helper", decision.ReplyText,
|
||||||
StringComparison.OrdinalIgnoreCase);
|
StringComparison.OrdinalIgnoreCase);
|
||||||
Assert.NotNull(provider.LastRequest);
|
Assert.NotNull(provider.LastRequest);
|
||||||
|
|||||||
@@ -3801,35 +3801,6 @@ public sealed class JiboWebSocketServiceTests
|
|||||||
Assert.Null(session.LastTranscript);
|
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]
|
[Fact]
|
||||||
public async Task BufferedAudio_WithSyntheticTranscriptHint_FinalizesThroughSttSeam()
|
public async Task BufferedAudio_WithSyntheticTranscriptHint_FinalizesThroughSttSeam()
|
||||||
{
|
{
|
||||||
@@ -5241,4 +5212,4 @@ public sealed class JiboWebSocketServiceTests
|
|||||||
return items[^1];
|
return items[^1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user