Compare commits
58 Commits
69707f32a7
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51e36bc492 | ||
|
|
9ffdd6d09e | ||
|
|
af76cbaee2 | ||
|
|
f2826253d5 | ||
|
|
8ed4763df5 | ||
|
|
9353e8d2e3 | ||
|
|
14b5cb74cc | ||
|
|
c0485da46d | ||
|
|
193fa56847 | ||
|
|
a2aa9df46a | ||
|
|
d8949fcc9a | ||
|
|
3b279fdd6f | ||
|
|
dfcf521a5a | ||
|
|
05efeb2853 | ||
|
|
478a320581 | ||
|
|
888f472f69 | ||
|
|
785dc2b48b | ||
|
|
d37521281e | ||
|
|
5d57095ce5 | ||
|
|
a8a153e910 | ||
|
|
a47c90c9c3 | ||
|
|
393c34055d | ||
|
|
f9b728c2a0 | ||
|
|
c87af4686c | ||
|
|
84759f51de | ||
|
|
c8beb0d1f0 | ||
|
|
e43b4f05f0 | ||
|
|
2677cf9dac | ||
|
|
20b84632ec | ||
|
|
5718edecaf | ||
|
|
40b5b8e4a8 | ||
|
|
8f7c118fb3 | ||
|
|
c30363ec9f | ||
|
|
ec786be797 | ||
|
|
f299cef9be | ||
|
|
f5e37729ab | ||
|
|
7297017250 | ||
|
|
66b89f3cee | ||
|
|
11a3e4ef13 | ||
|
|
7c6dacdbd8 | ||
|
|
9093b429ca | ||
|
|
df3b34c8ad | ||
|
|
67c738fae3 | ||
|
|
c0e9b41cd1 | ||
|
|
af2fdd230c | ||
|
|
0c597ebbf8 | ||
|
|
4bc87f927b | ||
|
|
a94b7ec493 | ||
|
|
8c17ad4035 | ||
| 383c272d9a | |||
|
|
d434138f9b | ||
|
|
80c4ae38fb | ||
|
|
8ae6d86a8c | ||
|
|
ffb444e4f9 | ||
|
|
7fd732ad17 | ||
|
|
3ad4a3e025 | ||
|
|
92491adf85 | ||
|
|
3e50fb9a49 |
@@ -1,4 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
<configuration>
|
<configuration>
|
||||||
<packageSources>
|
<packageSources>
|
||||||
<clear />
|
<clear />
|
||||||
|
|||||||
@@ -3,12 +3,17 @@
|
|||||||
<File Path="docs/development-plan.md" />
|
<File Path="docs/development-plan.md" />
|
||||||
<File Path="docs/device-bootstrap.md" />
|
<File Path="docs/device-bootstrap.md" />
|
||||||
<File Path="docs/feature-backlog.md" />
|
<File Path="docs/feature-backlog.md" />
|
||||||
|
<File Path="docs/greetings-presence-plan.md" />
|
||||||
<File Path="docs/live-jibo-capture.md" />
|
<File Path="docs/live-jibo-capture.md" />
|
||||||
<File Path="docs/live-jibo-test-runbook.md" />
|
<File Path="docs/live-jibo-test-runbook.md" />
|
||||||
|
<File Path="docs/personal-report-parity-plan.md" />
|
||||||
<File Path="docs/protocol-inventory.md" />
|
<File Path="docs/protocol-inventory.md" />
|
||||||
<File Path="docs/public-site-plan.md" />
|
<File Path="docs/public-site-plan.md" />
|
||||||
<File Path="docs/regression-test-plan.md" />
|
<File Path="docs/regression-test-plan.md" />
|
||||||
|
<File Path="docs/release-1.0.19-plan.md" />
|
||||||
|
<File Path="docs/roadmap.md" />
|
||||||
<File Path="docs/support-tiers.md" />
|
<File Path="docs/support-tiers.md" />
|
||||||
|
<File Path="docs/system-diagram-alignment.md" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/docs/prompts/">
|
<Folder Name="/docs/prompts/">
|
||||||
<File Path="docs/prompts/cloud-deploy-and-jibo-rcm-path.md" />
|
<File Path="docs/prompts/cloud-deploy-and-jibo-rcm-path.md" />
|
||||||
|
|||||||
@@ -1,31 +1,41 @@
|
|||||||
# OpenJibo
|
# OpenJibo
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
OpenJibo is the working revival track for Jibo.
|
OpenJibo is the working revival track for Jibo.
|
||||||
|
|
||||||
The near-term plan is intentionally concrete:
|
We are rebuilding the hosted cloud first, then using that foundation for OTA, Open Jibo OS, and a tiered brain that can eventually hand higher-order work to CoffeeBreak without losing Jibo's original charm.
|
||||||
|
|
||||||
1. Build a stable replacement cloud on Azure.
|
## Current Focus
|
||||||
2. Use the existing Node prototype as the protocol oracle and capture harness.
|
|
||||||
3. Port the hosted implementation to .NET as a modular monolith.
|
|
||||||
4. Bring real robots online first through RCM plus controlled DNS/TLS patching.
|
|
||||||
5. Use OTA later to reduce setup friction once the hosted cloud is proven.
|
|
||||||
|
|
||||||
This keeps the project grounded in what is already working while moving toward a maintainable hosted platform.
|
- ship a stable Azure-hosted replacement cloud
|
||||||
|
- keep the Node prototype as the protocol oracle and capture harness
|
||||||
|
- port the production path to .NET
|
||||||
|
- support real devices through repeatable bootstrap steps first
|
||||||
|
- use OTA later to reduce recovery friction once the cloud is trustworthy
|
||||||
|
|
||||||
## Current Truth
|
Current release truth lives in [docs/development-plan.md](docs/development-plan.md). The current cloud release constant is `1.0.19`.
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
The long-range plan is summarized in [docs/roadmap.md](docs/roadmap.md). In short:
|
||||||
|
|
||||||
|
1. Working hosted cloud.
|
||||||
|
2. OTA-assisted recovery and updates.
|
||||||
|
3. Open Jibo OS / `open-jibo` mode conversion.
|
||||||
|
4. Tiered brain and CoffeeBreak orchestration.
|
||||||
|
5. Broader ecosystem expansion.
|
||||||
|
|
||||||
|
## Current Architecture
|
||||||
|
|
||||||
The repo now has three distinct lanes:
|
The repo now has three distinct lanes:
|
||||||
|
|
||||||
- `src/Jibo.Cloud/node`
|
- `src/Jibo.Cloud/node`
|
||||||
The discovery server. This is the best source of observed protocol behavior today.
|
Protocol oracle, discovery server, fixture source, and rapid reverse-engineering lab.
|
||||||
- `src/Jibo.Cloud/dotnet`
|
- `src/Jibo.Cloud/dotnet`
|
||||||
The long-term hosted implementation. This is where the stable cloud is being built.
|
Production-oriented hosted implementation intended for Azure deployment and long-term maintenance.
|
||||||
- `src/Jibo.Runtime.Abstractions`
|
- `src/Jibo.Runtime.Abstractions`
|
||||||
The normalized runtime seam between robot/cloud traffic and modern conversation logic.
|
The seam between robot/cloud traffic and higher-level runtime and capability logic.
|
||||||
|
|
||||||
The key architectural idea is:
|
The core shape is:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Jibo device -> OpenJibo cloud -> normalized runtime contracts -> capabilities and planning
|
Jibo device -> OpenJibo cloud -> normalized runtime contracts -> capabilities and planning
|
||||||
@@ -40,18 +50,30 @@ QR Wi-Fi -> inject OpenJibo region config -> set robot region ->
|
|||||||
RCM/device patch for TLS and host acceptance -> OpenJibo cloud on Azure
|
RCM/device patch for TLS and host acceptance -> OpenJibo cloud on Azure
|
||||||
```
|
```
|
||||||
|
|
||||||
That path is documented in [docs/device-bootstrap.md](/OpenJibo/docs/device-bootstrap.md).
|
That path is documented in [docs/device-bootstrap.md](docs/device-bootstrap.md).
|
||||||
|
|
||||||
|
## Design Principles
|
||||||
|
|
||||||
|
- Preserve the original skills and visual design.
|
||||||
|
- Build the hosted cloud before making OTA the default recovery path.
|
||||||
|
- Keep every migration reversible whenever possible.
|
||||||
|
- Prefer source-backed slices over speculative rewrites.
|
||||||
|
- Let Jibo remain the face of the experience, even when higher-level orchestration sits behind him.
|
||||||
|
|
||||||
## Repo Map
|
## Repo Map
|
||||||
|
|
||||||
```text
|
```text
|
||||||
OpenJibo/
|
OpenJibo/
|
||||||
docs/
|
docs/
|
||||||
|
roadmap.md
|
||||||
development-plan.md
|
development-plan.md
|
||||||
device-bootstrap.md
|
device-bootstrap.md
|
||||||
protocol-inventory.md
|
feature-backlog.md
|
||||||
public-site-plan.md
|
public-site-plan.md
|
||||||
|
regression-test-plan.md
|
||||||
|
release-1.0.19-plan.md
|
||||||
support-tiers.md
|
support-tiers.md
|
||||||
|
system-diagram-alignment.md
|
||||||
|
|
||||||
scripts/bootstrap/
|
scripts/bootstrap/
|
||||||
Discover-JiboHosts.ps1
|
Discover-JiboHosts.ps1
|
||||||
@@ -67,56 +89,15 @@ OpenJibo/
|
|||||||
OpenJibo.Site/
|
OpenJibo.Site/
|
||||||
```
|
```
|
||||||
|
|
||||||
## Decisions Locked In
|
## Living Docs
|
||||||
|
|
||||||
- The first milestone is `core revive`, not full protocol parity.
|
Use these when you want the active technical truth:
|
||||||
- Azure SQL is the relational system of record for the hosted cloud.
|
|
||||||
- Billing and donations are future-compatible concerns, not phase-one delivery requirements.
|
|
||||||
- OTA is a phase-two simplification strategy, not the initial dependency.
|
|
||||||
|
|
||||||
## Near-Term Work
|
- [Development plan](docs/development-plan.md)
|
||||||
|
- [Feature backlog](docs/feature-backlog.md)
|
||||||
|
- [Release 1.0.19 plan](docs/release-1.0.19-plan.md)
|
||||||
|
- [Support tiers](docs/support-tiers.md)
|
||||||
|
- [System diagram alignment](docs/system-diagram-alignment.md)
|
||||||
|
- [Public site plan](docs/public-site-plan.md)
|
||||||
|
|
||||||
- port required endpoint and WebSocket behavior from Node to .NET
|
If you only read one document for the long view, make it [docs/roadmap.md](docs/roadmap.md).
|
||||||
- keep protocol captures and replay fixtures current
|
|
||||||
- keep HTTP and websocket live-run telemetry writing to the same repo-root capture tree
|
|
||||||
- harden device bootstrap documentation and scripts
|
|
||||||
- map more endpoints and behaviors beyond the current Node coverage
|
|
||||||
- stand up the initial `openjibo.com` information site
|
|
||||||
|
|
||||||
## Live Test Status
|
|
||||||
|
|
||||||
The first physical `.NET -> Jibo` experiments have now produced useful captures, but not a full wake-and-interact success yet.
|
|
||||||
|
|
||||||
What we have confirmed so far:
|
|
||||||
|
|
||||||
- the robot reaches `.NET` HTTP startup calls on `api.jibo.com`
|
|
||||||
- `.NET` can issue a robot token and accept the `api-socket.jibo.com` websocket
|
|
||||||
- live HTTP and websocket telemetry are now intended to land together under repo-root `captures/`
|
|
||||||
|
|
||||||
What remains unresolved:
|
|
||||||
|
|
||||||
- matching the Node startup cadence closely enough for consistent wake/eye-open behavior
|
|
||||||
- the next post-`api-socket` startup requests and timing seen in successful Node runs
|
|
||||||
- broader live websocket behavior on a real robot beyond the current synthetic parity slice
|
|
||||||
|
|
||||||
The current websocket bridge now also includes server-driven raw-audio turn completion:
|
|
||||||
|
|
||||||
- enough buffered audio plus `CONTEXT` can now trigger auto-finalize on the server side
|
|
||||||
- `EOS` is emitted on that auto-finalize path so turns do not remain open indefinitely
|
|
||||||
- transcript-less raw-audio turns still fall back to a synthetic compatibility response, not real ASR
|
|
||||||
|
|
||||||
The current richer websocket parity slice is still intentionally narrow:
|
|
||||||
|
|
||||||
- the successful joke path now has fixture-backed reply sequencing and partial payload-shape fidelity through `CLIENT_ASR -> LISTEN -> EOS -> delayed SKILL_ACTION`
|
|
||||||
- menu-side `CLIENT_NLU` parity is beginning to expand from live captures, starting with preserved clock-menu intent/rules/entities
|
|
||||||
- `.NET` now preserves buffered websocket audio frames so local tool-based ASR experiments can run without changing the stable cloud-first architecture
|
|
||||||
- this is not a claim of broad skill parity or full Jibo websocket coverage
|
|
||||||
|
|
||||||
## Important Docs
|
|
||||||
|
|
||||||
- [Cloud overview](/src/Jibo.Cloud/README.md)
|
|
||||||
- [Development plan](/docs/development-plan.md)
|
|
||||||
- [Protocol inventory](/docs/protocol-inventory.md)
|
|
||||||
- [Support tiers](/docs/support-tiers.md)
|
|
||||||
- [Device bootstrap path](/docs/device-bootstrap.md)
|
|
||||||
- [Public site plan](/docs/public-site-plan.md)
|
|
||||||
|
|||||||
@@ -88,6 +88,11 @@ Current websocket scope:
|
|||||||
- active local prompt preservation so `shared/yes_no`, clock, gallery, and settings prompts can still consume transcript-bearing short replies even when the stock skill reports a local context
|
- active local prompt preservation so `shared/yes_no`, clock, gallery, and settings prompts can still consume transcript-bearing short replies even when the stock skill reports a local context
|
||||||
- binary audio ignored for an existing transID until a fresh `LISTEN` has been seen, preventing context-only or post-speech tails from reopening an endless buffered turn
|
- binary audio ignored for an existing transID until a fresh `LISTEN` has been seen, preventing context-only or post-speech tails from reopening an endless buffered turn
|
||||||
- blank-audio hotphrase turns clear pending listen state and install a short late-audio ignore window
|
- blank-audio hotphrase turns clear pending listen state and install a short late-audio ignore window
|
||||||
|
- first GLSM-aligned listener telemetry and recovery slice is now in source:
|
||||||
|
- derived phase labels (`HJ_LISTENING`, `LISTENING`, `WAIT_LISTEN_FINISHED`, `DISPATCH_DIALOG`, `PROCESS_LISTENER_QUEUE`)
|
||||||
|
- `glsm_phase_transition` turn diagnostics
|
||||||
|
- websocket turn events with `glsmPhase` snapshots
|
||||||
|
- stale pending-listen recovery for long-open no-context/no-audio listens before processing a new hotphrase listen
|
||||||
- unknown inbound websocket types dropped silently instead of echoing stock-OS-unknown OpenJibo events
|
- unknown inbound websocket types dropped silently instead of echoing stock-OS-unknown OpenJibo events
|
||||||
- file telemetry and fixture export for HTTP, websocket, and turn captures
|
- file telemetry and fixture export for HTTP, websocket, and turn captures
|
||||||
|
|
||||||
@@ -145,6 +150,7 @@ Use these sources as evidence, not as code to copy blindly:
|
|||||||
- User-provided original source snapshot: `..\jibo` when extracted locally
|
- User-provided original source snapshot: `..\jibo` when extracted locally
|
||||||
- Original Pegasus cloud source inside that snapshot: `pegasus`
|
- Original Pegasus cloud source inside that snapshot: `pegasus`
|
||||||
- Original SDK and skill source inside that snapshot: `sdk`
|
- Original SDK and skill source inside that snapshot: `sdk`
|
||||||
|
- Legacy listener flow reference diagram: `..\jibo\sdk\packages\skills-service-manager\resources\state-diagrams\glsm.png`
|
||||||
- JiboOS reference tree: `..\JiboOS`
|
- JiboOS reference tree: `..\JiboOS`
|
||||||
- JiboOS skill snapshot: `..\JiboOS\opt\jibo\Jibo\Skills\@be`
|
- JiboOS skill snapshot: `..\JiboOS\opt\jibo\Jibo\Skills\@be`
|
||||||
|
|
||||||
@@ -188,7 +194,7 @@ These are not blockers for calling `1.0.18` complete unless the live test shows
|
|||||||
- local `whisper.cpp` STT remains a discovery seam, not production ASR
|
- local `whisper.cpp` STT remains a discovery seam, not production ASR
|
||||||
- media upload/body handling is not binary-safe enough for final gallery originals and thumbnails
|
- media upload/body handling is not binary-safe enough for final gallery originals and thumbnails
|
||||||
- state persistence is local JSON, not Azure SQL / Blob Storage
|
- state persistence is local JSON, not Azure SQL / Blob Storage
|
||||||
- update, backup, and restore are not end-to-end proven, and the `jibo test 22` / Test 26 / Test 27 / Test 28 sluggishness appears tied to robot-local backup status/load, startup reconnect state, or previously unsuppressed end-of-skill surprises; Test 31 also captured a legacy `Backup_20170222.List` startup query, which reinforces that the local backup/status path is real even before a user asks for backup
|
- update, backup, and restore are now end-to-end proven at the persistence-rehydration level, and the `jibo test 22` / Test 26 / Test 27 / Test 28 sluggishness appears tied to robot-local backup status/load, startup reconnect state, or previously unsuppressed end-of-skill surprises; Test 31 also captured a legacy `Backup_20170222.List` startup query, which reinforces that the local backup/status path is real even before a user asks for backup
|
||||||
- Tests 27 and 28 showed backup/surprise behavior without corresponding `Backup_*` HTTP traffic; Test 28 isolated the unsuppressed `@be/surprises` lifecycle handoff after Nimbus
|
- Tests 27 and 28 showed backup/surprise behavior without corresponding `Backup_*` HTTP traffic; Test 28 isolated the unsuppressed `@be/surprises` lifecycle handoff after Nimbus
|
||||||
- deployed-build verification needs to prove that synthetic OpenJibo websocket events are gone from the hosted artifact, not just from source
|
- deployed-build verification needs to prove that synthetic OpenJibo websocket events are gone from the hosted artifact, not just from source
|
||||||
- news content is synthetic; `jibo test 23` proved the path but not live provider-backed headlines
|
- news content is synthetic; `jibo test 23` proved the path but not live provider-backed headlines
|
||||||
|
|||||||
@@ -301,6 +301,20 @@ Current release theme:
|
|||||||
- Follow-up:
|
- Follow-up:
|
||||||
- live smoke should confirm `cloud version` speaks `1.0.18`, carries `match.skipSurprises = true`, does not stop itself on the word `Jibo`, and settles without a generic `I heard...` reply or a local surprise handoff
|
- live smoke should confirm `cloud version` speaks `1.0.18`, carries `match.skipSurprises = true`, does not stop itself on the word `Jibo`, and settles without a generic `I heard...` reply or a local surprise handoff
|
||||||
|
|
||||||
|
### GLSM Listener Flow Capture And Recovery
|
||||||
|
|
||||||
|
- Status: `implemented`
|
||||||
|
- Tags: `protocol`, `docs`
|
||||||
|
- Result:
|
||||||
|
- the legacy listener state machine source (`sdk ... glsm.png`) is now captured in current planning docs
|
||||||
|
- runtime now emits GLSM-aligned phase snapshots (`HJ_LISTENING`, `LISTENING`, `WAIT_LISTEN_FINISHED`, `DISPATCH_DIALOG`, `PROCESS_LISTENER_QUEUE`)
|
||||||
|
- turn diagnostics now include `glsm_phase_transition` for phase changes
|
||||||
|
- websocket telemetry now records `glsmPhase` on binary/context/turn events
|
||||||
|
- stale pending-listen recovery is now in source so a long-open no-context/no-audio listen can be cleared when the next hotphrase listen arrives
|
||||||
|
- Follow-up:
|
||||||
|
- live-capture proof is still required against the recurring blue-ring/stuck-listening sequence
|
||||||
|
- deeper GLSM parity (`Interrupt Listeners`, launch/global parse branches) should be tackled after this first capture slice is validated on-device
|
||||||
|
|
||||||
### End-Of-Skill Surprise Suppression
|
### End-Of-Skill Surprise Suppression
|
||||||
|
|
||||||
- Status: `implemented`
|
- Status: `implemented`
|
||||||
@@ -447,11 +461,12 @@ Current release theme:
|
|||||||
- Implementation notes:
|
- Implementation notes:
|
||||||
- define local capture sinks versus hosted retention
|
- define local capture sinks versus hosted retention
|
||||||
- decide how testers submit noteworthy sessions
|
- decide how testers submit noteworthy sessions
|
||||||
|
- keep a lightweight `capture-index.ndjson` manifest beside raw captures so testers can quickly find sessions, operations, and fixture exports
|
||||||
- preserve sanitized fixtures as the durable parity artifact
|
- preserve sanitized fixtures as the durable parity artifact
|
||||||
|
|
||||||
### 11. Binary-Safe Media Storage
|
### 11. Binary-Safe Media Storage
|
||||||
|
|
||||||
- Status: `ready`
|
- Status: `in progress`
|
||||||
- Tags: `storage`, `protocol`
|
- Tags: `storage`, `protocol`
|
||||||
- Why next:
|
- Why next:
|
||||||
- the first gallery bridge stores metadata and text-body placeholders, but final gallery support needs originals and thumbnails
|
- the first gallery bridge stores metadata and text-body placeholders, but final gallery support needs originals and thumbnails
|
||||||
@@ -459,10 +474,13 @@ Current release theme:
|
|||||||
- whether stock gallery expects originals, thumbnails, or both
|
- whether stock gallery expects originals, thumbnails, or both
|
||||||
- what upload metadata must survive for gallery refresh
|
- what upload metadata must survive for gallery refresh
|
||||||
- how to map this cleanly to Blob Storage
|
- how to map this cleanly to Blob Storage
|
||||||
|
- Implementation notes:
|
||||||
|
- media content now flows through a storage seam with file and Azure Blob adapters
|
||||||
|
- the protocol still serves the legacy text-body contract, but the original payload is now persisted separately and can be swapped to binary-native storage later
|
||||||
|
|
||||||
### Next Up (`2026-05-06`): Dialog Parsing Expansion And Ambiguity Guardrails
|
### Next Up (`2026-05-06`): Dialog Parsing Expansion And Ambiguity Guardrails
|
||||||
|
|
||||||
- Status: `ready`
|
- Status: `polish`
|
||||||
- Tags: `protocol`, `content`, `stt`, `docs`
|
- Tags: `protocol`, `content`, `stt`, `docs`
|
||||||
- Why now:
|
- Why now:
|
||||||
- this is the next queued `1.0.19` implementation slice after weather provider bring-up
|
- this is the next queued `1.0.19` implementation slice after weather provider bring-up
|
||||||
@@ -473,6 +491,13 @@ Current release theme:
|
|||||||
- add ambiguity guardrails for overlapping intents (date vs birthday, generic chat vs memory set/lookup, weather variants)
|
- add ambiguity guardrails for overlapping intents (date vs birthday, generic chat vs memory set/lookup, weather variants)
|
||||||
- preserve command-vs-question personality behavior and stock skill launch compatibility
|
- preserve command-vs-question personality behavior and stock skill launch compatibility
|
||||||
- add focused tests for new phrase families and negative boundary cases
|
- add focused tests for new phrase families and negative boundary cases
|
||||||
|
- Progress update (`2026-05-07`):
|
||||||
|
- implemented date/time guardrails so birthday phrasing is not misrouted to date
|
||||||
|
- expanded phrase coverage for:
|
||||||
|
- birthday alias set/recall (`bday` variants)
|
||||||
|
- shorthand favorites (`my favorite sport football`)
|
||||||
|
- weather phrasing (`what's today's weather look like`, `will it be sunny tomorrow`)
|
||||||
|
- updated continuation deferral so complete shorthand favorites finalize instead of waiting for missing continuation
|
||||||
- Exit criteria:
|
- Exit criteria:
|
||||||
- ambiguous phrase handling is improved without regressions in existing `1.0.19` features
|
- ambiguous phrase handling is improved without regressions in existing `1.0.19` features
|
||||||
- phrase imports are documented and traceable to Pegasus parser sources
|
- phrase imports are documented and traceable to Pegasus parser sources
|
||||||
@@ -633,6 +658,8 @@ Current release theme:
|
|||||||
- Follow-up:
|
- Follow-up:
|
||||||
- add durable persistence path for personal facts
|
- add durable persistence path for personal facts
|
||||||
- broaden fact categories further (multi-person household memory, relationship cues, and corrective updates)
|
- broaden fact categories further (multi-person household memory, relationship cues, and corrective updates)
|
||||||
|
- add explicit person-scoped state so future interactions can distinguish household members inside the same loop
|
||||||
|
- define the first server-to-server sync envelope for durable state before we need it in production
|
||||||
|
|
||||||
### 24. Memory-Triggered Proactivity Baseline
|
### 24. Memory-Triggered Proactivity Baseline
|
||||||
|
|
||||||
@@ -648,6 +675,7 @@ Current release theme:
|
|||||||
- expand proactivity beyond pizza to additional Pegasus-backed categories
|
- expand proactivity beyond pizza to additional Pegasus-backed categories
|
||||||
- add cooldown/throttle policy and observability around proactive offer frequency
|
- add cooldown/throttle policy and observability around proactive offer frequency
|
||||||
- connect memory store to durable multi-tenant persistence
|
- connect memory store to durable multi-tenant persistence
|
||||||
|
- keep the sync story visible so stateful offers can survive a multi-server deployment later
|
||||||
|
|
||||||
### 25. Weather Report-Skill Launch Compatibility
|
### 25. Weather Report-Skill Launch Compatibility
|
||||||
|
|
||||||
@@ -664,6 +692,216 @@ Current release theme:
|
|||||||
- connect weather units and location directly to user/report-skill settings parity instead of config defaults
|
- connect weather units and location directly to user/report-skill settings parity instead of config defaults
|
||||||
- add richer condition-change commentary and view parity with original report-skill weather behaviors
|
- add richer condition-change commentary and view parity with original report-skill weather behaviors
|
||||||
|
|
||||||
|
### 26. Presence-Aware Greetings And Identity Proactivity
|
||||||
|
|
||||||
|
- Status: `ready`
|
||||||
|
- Tags: `protocol`, `content`, `storage`, `docs`
|
||||||
|
- Why now:
|
||||||
|
- this is the next personality-charm expansion after parser guardrail and weather bring-up
|
||||||
|
- Pegasus greetings behavior is strongly tied to presence/identity signals and proactive cooldown policy
|
||||||
|
- current OpenJibo has memory/proactivity foundations but no first-class presence extraction path yet
|
||||||
|
- Pegasus source anchors:
|
||||||
|
- `C:\Projects\jibo\pegasus\packages\hub\be-skills\greetings_manifest.json`
|
||||||
|
- `C:\Projects\jibo\sdk\skills\greetings\src\GreetingsSkill.ts`
|
||||||
|
- `C:\Projects\jibo\sdk\skills\greetings\src\GreetingsSM.ts`
|
||||||
|
- `C:\Projects\jibo\pegasus\packages\hub\src\proactive\ProactiveTransactionHandler.ts`
|
||||||
|
- `C:\Projects\jibo\pegasus\packages\hub\src\proactive\tools\ContextTools.ts`
|
||||||
|
- Scope:
|
||||||
|
- extract presence/identity context (`speaker`, `peoplePresent`, focused person) from runtime context payload
|
||||||
|
- add greeting intent families and state-machine split for reactive vs proactive greeting routes
|
||||||
|
- add cooldown and trigger-source guardrails for proactive greetings
|
||||||
|
- start person-aware greeting hooks (name-aware greeting, morning greeting policy, return greeting policy)
|
||||||
|
- Exit criteria:
|
||||||
|
- presence-aware greetings are routed deterministically with tests
|
||||||
|
- proactive greetings are frequency-bounded and do not trigger from surprise source when blocked by policy
|
||||||
|
- fallback behavior remains stable when identity is unknown or context is incomplete
|
||||||
|
- docs and release tracking are updated with shipped scope and residual gaps
|
||||||
|
- Tracking:
|
||||||
|
- [greetings-presence-plan.md](greetings-presence-plan.md)
|
||||||
|
- [release-1.0.19-plan.md](release-1.0.19-plan.md)
|
||||||
|
|
||||||
|
### 27. Personal Report Parity Track (Weather/News/Commute/Calendar)
|
||||||
|
|
||||||
|
- Status: `in_progress`
|
||||||
|
- Tags: `protocol`, `content`, `storage`, `docs`
|
||||||
|
- Why now:
|
||||||
|
- personal report is a core Jibo charm surface and currently split between implemented weather speech and placeholder calendar/commute/news content
|
||||||
|
- Pegasus weather used explicit condition animations and weather views; current OpenJibo weather is functional but visually lighter
|
||||||
|
- Scope:
|
||||||
|
- weather icon/animation parity and view support
|
||||||
|
- broader non-local weather query handling and short-range date coverage
|
||||||
|
- provider-backed news ingestion and filtering
|
||||||
|
- commute provider path and settings schema
|
||||||
|
- coverage matrix for personal report parity gaps and test/capture exit criteria
|
||||||
|
- Progress update (`2026-05-10`):
|
||||||
|
- added provider-ready news briefing lane with Nimbus-compatible `news` skill payload continuity
|
||||||
|
- added memory/transcript category hint plumbing for provider requests (sports/technology/business/general)
|
||||||
|
- fallback synthetic news behavior remains active when no provider key is configured
|
||||||
|
- added TTL caching for weather/news provider calls to reduce repeated external requests
|
||||||
|
- vendored Pegasus `report-skill` templates for weather and personal-report phrasing so the next pass can focus on renderer coverage for calendar, commute, and news templates instead of rediscovering source text
|
||||||
|
- Source anchors:
|
||||||
|
- `C:\Projects\jibo\pegasus\packages\report-skill\src\subskills\weather\WeatherMimLogic.ts`
|
||||||
|
- `C:\Projects\jibo\pegasus\packages\report-skill\resources\views\weatherHiLo.json`
|
||||||
|
- `C:\Projects\jibo\pegasus\packages\report-skill\src\subskills\news\NewsMimLogic.ts`
|
||||||
|
- `C:\Projects\jibo\pegasus\packages\report-skill\src\subskills\commute\CommuteMimLogic.ts`
|
||||||
|
- `C:\Projects\jibo\pegasus\packages\hub\pegasus-skills\report_skill_manifest.json`
|
||||||
|
- Tracking:
|
||||||
|
- [personal-report-parity-plan.md](personal-report-parity-plan.md)
|
||||||
|
- [release-1.0.19-plan.md](release-1.0.19-plan.md)
|
||||||
|
|
||||||
|
### 28. Grocery List Capability (Requested Feature)
|
||||||
|
|
||||||
|
- Status: `discovery`
|
||||||
|
- Tags: `content`, `docs`, `storage`
|
||||||
|
- Why now:
|
||||||
|
- directly requested by Jibo owners and fits memory + household utility roadmap
|
||||||
|
- Source findings:
|
||||||
|
- Pegasus has scripted responses for shopping/to-do list requests but no standalone grocery-list skill in this snapshot
|
||||||
|
- examples:
|
||||||
|
- `C:\Projects\jibo\pegasus\packages\chitchat-skill\mims\scripted-responses\RA_JBO_ShoppingList.mim`
|
||||||
|
- `C:\Projects\jibo\pegasus\packages\chitchat-skill\mims\scripted-responses\RA_JBO_ManageToDoList.mim`
|
||||||
|
- Candidate delivery paths:
|
||||||
|
- native lightweight list skill (fastest user value)
|
||||||
|
- integration-backed list orchestration (long-term richer ecosystem fit)
|
||||||
|
- Exit criteria:
|
||||||
|
- clear decision on MVP path
|
||||||
|
- first schema for list items + ownership scope
|
||||||
|
- initial voice flows and follow-up intent handling defined
|
||||||
|
|
||||||
|
### 29. Legacy MIM Personality Import Ladder
|
||||||
|
|
||||||
|
- Status: `in_progress`
|
||||||
|
- Tags: `content`, `protocol`, `docs`
|
||||||
|
- Why now:
|
||||||
|
- we already have a chitchat/content scaffold that can render stock-compatible personality replies
|
||||||
|
- the legacy `chitchat-mims` tree is mostly declarative content, so a phased import can add visible charm fast
|
||||||
|
- this is the best near-term path to get Jibo feeling more interactive without needing a full Pegasus runtime clone
|
||||||
|
- What is possible today:
|
||||||
|
- direct scripted replies through the existing content catalog
|
||||||
|
- stock-compatible payloads with `skillId`, `mim_id`, `mim_type`, `prompt_id`, and ESML
|
||||||
|
- current examples already prove the shape for pizza, dance, weather, news, and generic chat
|
||||||
|
- What we need to build:
|
||||||
|
1. a MIM inventory importer that can scan the legacy tree and normalize `skill_id`, `mim_id`, prompt text, and metadata
|
||||||
|
2. a prompt-selection layer that can choose by category and condition metadata
|
||||||
|
3. a safe ESML/prompt renderer for imported content
|
||||||
|
- What can be ported with each build:
|
||||||
|
- Build A: declarative prompt packs
|
||||||
|
- `core-responses`
|
||||||
|
- `deflector`
|
||||||
|
- the simplest `emotion-responses`
|
||||||
|
- direct `scripted-responses` that are just prompt lists
|
||||||
|
- Build B: conditioned prompt packs
|
||||||
|
- `gqa-responses`
|
||||||
|
- structured emotion prompts with `condition` gates
|
||||||
|
- any response families that only need simple state or Jibo-emotion checks
|
||||||
|
- Build C: conversation families
|
||||||
|
- richer `scripted-responses` that need follow-up state
|
||||||
|
- holiday / special-date personality sets
|
||||||
|
- more nuanced chitchat branches that depend on context-aware routing
|
||||||
|
- Build D: full parity cleanup
|
||||||
|
- larger cross-skill collections
|
||||||
|
- any MIMs that depend on Pegasus-only parser assumptions
|
||||||
|
- any files that need dedicated runtime abstraction instead of catalog lookup
|
||||||
|
- Low-hanging fruit for tonight:
|
||||||
|
- import the smallest declarative packs first so we can test something tomorrow
|
||||||
|
- prioritize anything that is pure prompt text with no complex branching
|
||||||
|
- keep the first pass limited to content that maps cleanly onto the current catalog shape
|
||||||
|
- Progress update (`2026-05-13`):
|
||||||
|
- added the first Build A importer scaffold in the cloud content repository
|
||||||
|
- checked in a small seed bundle under `Content/LegacyMims/BuildA`
|
||||||
|
- added focused importer tests for prompt stripping, bucketing, and merge behavior
|
||||||
|
- expanded Build A with additional easy scripted-response packs for identity and persona replies
|
||||||
|
- started Build B with source-backed scripted-response packs for work, food, home, birthplace, language, hobby, and material questions
|
||||||
|
- Tomorrow test target:
|
||||||
|
- verify imported personality replies show up through the existing chitchat route
|
||||||
|
- confirm the emitted payload still looks like a stock skill response
|
||||||
|
- confirm the imported content does not disturb existing weather/news/pizza flows
|
||||||
|
- Exit criteria:
|
||||||
|
- a first importer path exists for the simplest legacy MIM files
|
||||||
|
- at least one legacy prompt pack is running through OpenJibo content instead of hand-authored fallback text
|
||||||
|
- we have a clear second-wave list for the more conditional MIM families
|
||||||
|
|
||||||
|
### 30. Original Personalized Function Inventory
|
||||||
|
|
||||||
|
- Status: `discovery`
|
||||||
|
- Tags: `content`, `docs`, `protocol`
|
||||||
|
- Why now:
|
||||||
|
- we are actively porting persona and memory slices, so we need a bounded checklist of the original Jibo charm surfaces
|
||||||
|
- the goal is to keep the next few passes focused on personality-rich wins instead of letting the work sprawl
|
||||||
|
- Known sources:
|
||||||
|
- legacy Jibo OS/Pegasus chitchat and MIM response families
|
||||||
|
- current OpenJibo persona, memory, and greeting work as the implementation target
|
||||||
|
- Inventory to track:
|
||||||
|
- identity and origin questions
|
||||||
|
- personality and capability questions
|
||||||
|
- favorite-style prompts like `what is your favorite color`
|
||||||
|
- attraction and preference prompts like `what is your favorite flower`, `do you like R2D2`, `do you like the sun`, `do you like space`, and `do you like kids`
|
||||||
|
- longer authored variants for the same prompt family when Pegasus shows richer phrasing
|
||||||
|
- charm/capability prompts like `can you laugh` and `can you dance`
|
||||||
|
- mood / affect questions
|
||||||
|
- recognition follow-ups like `do you know me`
|
||||||
|
- follow-up state prompts that should stay warm and locally grounded
|
||||||
|
- Next pass targets:
|
||||||
|
- document the remaining persona inventory so we keep a clean checklist for the next passes
|
||||||
|
- keep the favorites family moving with source-backed imports where available, and temporary runtime replies only when the source is missing
|
||||||
|
- keep adding small sourced personality batches, especially the legacy `R2D2`, `sun`, `space`, `kids`, and charm prompts
|
||||||
|
- keep adding 1-3 persona prompts per pass with tests
|
||||||
|
- prefer source-backed MIM imports when the legacy text is available, and use a temporary runtime reply only when needed to unblock user value
|
||||||
|
- keep a separate note for longer authored variants so we do not lose the multi-clause Peggy-style phrasing while importing the short-form packs
|
||||||
|
- Mood follow-up work in flight:
|
||||||
|
- source-backed happy/sad/angry response packs are now part of Build B
|
||||||
|
- small-talk aliases like `what are you up to` and `how are things` now stay on the emotion-query path
|
||||||
|
- Descriptor charm work in flight:
|
||||||
|
- source-backed `are you kind`, `are you funny`, `are you helpful`, `are you curious`, `are you loyal`, `are you mischievous`, and `are you likable` prompts are now in Build B
|
||||||
|
- these keep the self-description lane warm while we build toward seasonal and holiday charm
|
||||||
|
- Seasonal charm work in flight:
|
||||||
|
- source-backed holiday, New Year's, Halloween, spring, and gift prompts are now part of Build B
|
||||||
|
- `RN_` holiday greeting files are now bucketed as greetings so seasonal replies stay visible in the catalog
|
||||||
|
- Presence and thought follow-ups in flight:
|
||||||
|
- `welcome back`, `what are you thinking`, `what have you been doing`, and `what did you do` are now part of Build B
|
||||||
|
- these keep the social surface lively while the memory and multitenant tracks keep advancing in parallel
|
||||||
|
- Next queued persona surfaces:
|
||||||
|
- richer identity follow-ups like `who is this`, `do you know me`, `do you remember me`, and `can you recognize me`
|
||||||
|
- mood and affect prompts like `how are you`, `are you happy`, `are you sad`, and `are you angry`
|
||||||
|
- self-description charm like `what's your name`, `do you have a nickname`, and `do you like being Jibo`
|
||||||
|
- additional legacy source-backed `RI_USR` prompts where the text is short and the behavior is easy to verify
|
||||||
|
- Exit criteria:
|
||||||
|
- a stable checklist exists for the original persona surface
|
||||||
|
- each pass can be scoped to a small batch of prompts
|
||||||
|
- the backlog makes it obvious what is still missing without losing momentum
|
||||||
|
|
||||||
|
### 31. Longer Authored Persona Variants
|
||||||
|
|
||||||
|
- Status: `ready`
|
||||||
|
- Tags: `content`, `docs`, `protocol`
|
||||||
|
- Why now:
|
||||||
|
- Pegasus often used longer, multi-clause authored alternatives for the same personality question
|
||||||
|
- we already have the short-path import working, so this is a low-risk way to add richer phrasing without inventing a new dialog engine
|
||||||
|
- it gives us a straightforward next pass that stays familiar to the original robot
|
||||||
|
- Scope:
|
||||||
|
- import the longer authored variants already present in the legacy MIMs
|
||||||
|
- prefer richer phrasing for favorite-style, identity, and charm prompts when the source text provides it
|
||||||
|
- keep the runtime behavior rule-based and deterministic
|
||||||
|
- Next step:
|
||||||
|
- add a small batch of longer variants to the current Build B content packs and prove them with a smoke test
|
||||||
|
|
||||||
|
### 32. Dialog Joining And Composition
|
||||||
|
|
||||||
|
- Status: `discovery`
|
||||||
|
- Tags: `content`, `docs`, `protocol`
|
||||||
|
- Why now:
|
||||||
|
- the videos and source files suggest Jibo sometimes felt like he was joining thoughts together, even when the source text was still authored
|
||||||
|
- we have not found evidence of a general runtime joiner yet, so this remains a post-release enhancement instead of a 1.0.19 dependency
|
||||||
|
- keeping it separate lets us preserve familiar Jibo phrasing now and experiment with composition later
|
||||||
|
- Scope:
|
||||||
|
- design a post-release dialog composition layer that can stitch authored fragments together when appropriate
|
||||||
|
- keep the first version conservative and familiar, not LLM-driven
|
||||||
|
- make sure any future joining feature is opt-in and does not replace the current authored prompt path
|
||||||
|
- Follow-up:
|
||||||
|
- revisit after 1.0.19 personality import and report-skill parity stabilize
|
||||||
|
- decide whether the composition layer should sit above the prompt catalog or beside it as a dedicated response post-processor
|
||||||
|
- keep this separate from the authored-variant backlog item so we do not blur prompt richness with runtime composition
|
||||||
|
|
||||||
## Suggested Order
|
## Suggested Order
|
||||||
|
|
||||||
Before closing `1.0.18`:
|
Before closing `1.0.18`:
|
||||||
@@ -682,15 +920,25 @@ For `1.0.19`:
|
|||||||
2. Expand memory-backed personal facts with tenant-scoped storage (beyond the first birthday/preferences foundation) - implemented
|
2. Expand memory-backed personal facts with tenant-scoped storage (beyond the first birthday/preferences foundation) - implemented
|
||||||
3. Proactivity selector baseline with source-backed first offers - implemented
|
3. Proactivity selector baseline with source-backed first offers - implemented
|
||||||
4. Weather report-skill launch compatibility - implemented
|
4. Weather report-skill launch compatibility - implemented
|
||||||
5. Dialog parsing expansion and ambiguity guardrails - queued next as of `2026-05-06`
|
5. Dialog parsing expansion and ambiguity guardrails - in progress (`2026-05-09` third guardrail slice implemented; Pegasus affinity phrase families + continuation guardrails expanded)
|
||||||
6. Holidays and seasonal personality behavior built on the new memory/proactivity foundation
|
6. Presence-aware greetings and identity-triggered proactivity - implemented (trigger path, identity-aware reactive/proactive replies, cooldown metadata wiring, focused websocket coverage)
|
||||||
7. Durable memory persistence path (multi-tenant backing store)
|
7. Personal report parity track (weather visuals, live news path, commute path, calendar parity matrix) - in progress (`2026-05-10` first live-news provider slice implemented)
|
||||||
8. Update, backup, and restore proof
|
8. Holidays and seasonal personality behavior built on the new memory/proactivity foundation
|
||||||
9. STT upgrade and noise screening
|
9. Durable memory persistence path (multi-tenant backing store)
|
||||||
10. Hosted capture/storage plan / indexing for group testing
|
- reference design captured in `docs/persistence-architecture.md`
|
||||||
11. Binary-safe media storage / sync to cloud drive: OneDrive, Google Drive, Box, etc.
|
- store contracts are now tightened around account/loop/device/person scoping, revision tracking, and explicit load/save boundaries
|
||||||
12. Provider-backed news and weather parity polish
|
- the backend seam is now selectable, with file-backed local persistence as default and an Azure Blob Storage slot wired for future deployment when a storage account connection string is available
|
||||||
13. Lasso, identity, and onboarding as larger discovery-driven tracks
|
- next implementation pass should supply the real Azure Storage connection string / deployment wiring and validate the live round-trip in the storage account smoke test
|
||||||
|
10. Update, backup, and restore proof - implemented (update creation and backup creation now survive persisted reloads; restore is the persisted-state rehydration proof path, not a new cloud API)
|
||||||
|
11. STT upgrade and noise screening
|
||||||
|
12. Hosted capture/storage plan / indexing for group testing
|
||||||
|
13. Binary-safe media storage / sync to cloud drive: OneDrive, Google Drive, Box, etc.
|
||||||
|
14. Provider-backed news and weather parity polish
|
||||||
|
15. Grocery list capability discovery and MVP selection
|
||||||
|
16. Lasso, identity, and onboarding as larger discovery-driven tracks
|
||||||
|
17. Legacy MIM personality import ladder and first declarative prompt packs
|
||||||
|
18. Longer authored persona variants for the same prompt families
|
||||||
|
19. Dialog joining/composition as a post-release enhancement, kept separate from the 1.0.19 ladder
|
||||||
|
|
||||||
For `1.0.20` and beyond:
|
For `1.0.20` and beyond:
|
||||||
|
|
||||||
|
|||||||
173
OpenJibo/docs/greetings-presence-plan.md
Normal file
173
OpenJibo/docs/greetings-presence-plan.md
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
# Greetings And Presence Plan (`1.0.19`)
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Recreate the original Jibo greeting charm with modern cloud architecture:
|
||||||
|
|
||||||
|
- person-aware greetings when someone is detected
|
||||||
|
- proactive offers tied to presence, time of day, and memory
|
||||||
|
- safe cooldown rules so proactivity feels alive, not noisy
|
||||||
|
|
||||||
|
This plan is source-anchored to Pegasus and scoped to shippable slices.
|
||||||
|
|
||||||
|
## Pegasus Behavior Baseline
|
||||||
|
|
||||||
|
Primary source artifacts:
|
||||||
|
|
||||||
|
- `C:\Projects\jibo\pegasus\packages\hub\be-skills\greetings_manifest.json`
|
||||||
|
- `C:\Projects\jibo\sdk\skills\greetings\src\GreetingsSkill.ts`
|
||||||
|
- `C:\Projects\jibo\sdk\skills\greetings\src\GreetingsSM.ts`
|
||||||
|
- `C:\Projects\jibo\sdk\skills\greetings\src\states\IntentSplit.ts`
|
||||||
|
- `C:\Projects\jibo\sdk\skills\greetings\src\states\ProactiveGreetingState.ts`
|
||||||
|
- `C:\Projects\jibo\sdk\skills\greetings\src\states\ProactiveProbabilityState.ts`
|
||||||
|
- `C:\Projects\jibo\sdk\skills\greetings\src\states\ShouldDoMorningGreetingState.ts`
|
||||||
|
- `C:\Projects\jibo\sdk\skills\greetings\src\states\ShouldDoBirthdayState.ts`
|
||||||
|
- `C:\Projects\jibo\sdk\skills\greetings\src\states\ShouldDoHolidayState.ts`
|
||||||
|
- `C:\Projects\jibo\pegasus\packages\hub\src\proactive\ProactiveTransactionHandler.ts`
|
||||||
|
- `C:\Projects\jibo\pegasus\packages\hub\src\proactive\tools\ContextTools.ts`
|
||||||
|
|
||||||
|
Key behaviors to port:
|
||||||
|
|
||||||
|
- explicit reactive/proactive greeting split
|
||||||
|
- identity source split:
|
||||||
|
- reactive path uses active speaker
|
||||||
|
- proactive path uses present identified persons
|
||||||
|
- hub-level proactive gating:
|
||||||
|
- block greetings when trigger source is `SURPRISE`
|
||||||
|
- throttle by interaction history (`GreetingsLaunchLast2Hours < 1`)
|
||||||
|
- morning/birthday/holiday gates with per-user recency checks
|
||||||
|
- optional follow-up response flow after proactive greetings
|
||||||
|
|
||||||
|
## Current OpenJibo Baseline
|
||||||
|
|
||||||
|
Current implementation anchor:
|
||||||
|
|
||||||
|
- `C:\Projects\JiboExperiments\OpenJibo\src\Jibo.Cloud\dotnet\src\Jibo.Cloud.Application\Services\JiboInteractionService.cs`
|
||||||
|
- `C:\Projects\JiboExperiments\OpenJibo\src\Jibo.Cloud\dotnet\src\Jibo.Cloud.Application\Services\ProtocolToTurnContextMapper.cs`
|
||||||
|
- `C:\Projects\JiboExperiments\OpenJibo\src\Jibo.Cloud\dotnet\src\Jibo.Cloud.Application\Services\WebSocketTurnFinalizationService.cs`
|
||||||
|
- `C:\Projects\JiboExperiments\OpenJibo\src\Jibo.Cloud\dotnet\src\Jibo.Cloud.Application\Services\ChitchatStateMachine.cs`
|
||||||
|
- `C:\Projects\JiboExperiments\OpenJibo\src\Jibo.Cloud\dotnet\src\Jibo.Cloud.Infrastructure\Persistence\InMemoryPersonalMemoryStore.cs`
|
||||||
|
|
||||||
|
What we already have:
|
||||||
|
|
||||||
|
- tenant-scoped memory primitives (name, birthday, preferences, affinity)
|
||||||
|
- proactivity baseline with pending-offer follow-up handling
|
||||||
|
- state-machine style chitchat split (`ScriptedResponse`, `EmotionQuery`, `EmotionCommand`, `ErrorResponse`)
|
||||||
|
- GLSM-aware websocket lifecycle and stuck-listen recovery
|
||||||
|
|
||||||
|
Main gap:
|
||||||
|
|
||||||
|
- no first-class presence/identity perception extraction from runtime context for greeting policy decisions
|
||||||
|
|
||||||
|
## Implementation Slices
|
||||||
|
|
||||||
|
### Slice G1: Presence Context Extraction And Session Snapshot
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
|
||||||
|
- extract presence/identity fields from websocket context payload into normalized metadata for routing
|
||||||
|
|
||||||
|
Initial fields:
|
||||||
|
|
||||||
|
- focused speaker id
|
||||||
|
- identified person ids present
|
||||||
|
- total people present
|
||||||
|
- trigger source if present
|
||||||
|
- time-of-day helper signals
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- no facial-recognition implementation is needed in cloud; cloud consumes robot perception signals
|
||||||
|
|
||||||
|
### Slice G2: Greeting Intent Families And Parser Guardrails
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
|
||||||
|
- add explicit greeting intent families with question/command guardrails
|
||||||
|
|
||||||
|
Initial families:
|
||||||
|
|
||||||
|
- `hello`, `hey jibo`, `what's up`
|
||||||
|
- `good morning`, `good afternoon`, `good evening`, `good night`
|
||||||
|
- `i'm home`, `i'm back`
|
||||||
|
- identity question (`who am i`) as a future-compatible hook
|
||||||
|
|
||||||
|
Guardrails:
|
||||||
|
|
||||||
|
- avoid stealing non-greeting domains
|
||||||
|
- keep existing date/time and birthday disambiguation intact
|
||||||
|
|
||||||
|
### Slice G3: Greeting State-Machine Port (OpenJibo Style)
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
|
||||||
|
- add a greeting state-machine module with explicit route metadata like chitchat
|
||||||
|
|
||||||
|
Planned routes:
|
||||||
|
|
||||||
|
- `ReactiveGreeting`
|
||||||
|
- `ProactiveGreeting`
|
||||||
|
- `MorningGreeting`
|
||||||
|
- `SpecialDayGreeting`
|
||||||
|
- `OptionalResponse`
|
||||||
|
- `ErrorResponse`
|
||||||
|
|
||||||
|
Output shape:
|
||||||
|
|
||||||
|
- keep stock-compatible skill payload patterns
|
||||||
|
- preserve MIM/ESML hook points for charm content
|
||||||
|
|
||||||
|
### Slice G4: Proactive Gating And Cooldowns
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
|
||||||
|
- port the critical Pegasus policy behavior to prevent spam
|
||||||
|
|
||||||
|
Phase-1 rules:
|
||||||
|
|
||||||
|
- skip proactive greetings when trigger source is surprise
|
||||||
|
- enforce per-tenant/person cooldown (target parity: 2-hour greeting window)
|
||||||
|
- suppress proactive launch when session is unstable (pending listen/follow-up conflict)
|
||||||
|
|
||||||
|
### Slice G5: Person Queue And Memory Extensions
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
|
||||||
|
- introduce lightweight person queue/history for greeting relevance
|
||||||
|
|
||||||
|
Phase-1 storage additions:
|
||||||
|
|
||||||
|
- last-seen timestamp per person key
|
||||||
|
- last-greeted timestamp per person key
|
||||||
|
- optional preferred-name alias for spoken greeting personalization
|
||||||
|
|
||||||
|
### Slice G6: Rollout, Logging, And Live Validation
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
|
||||||
|
- ship safely with observability and test confidence
|
||||||
|
|
||||||
|
Required coverage:
|
||||||
|
|
||||||
|
- unit tests for context extraction and intent routing
|
||||||
|
- websocket tests for presence-triggered greeting eligibility and cooldown behavior
|
||||||
|
- live captures validating:
|
||||||
|
- no stuck listening regressions
|
||||||
|
- no runaway proactive loops
|
||||||
|
- stable fallback when identity is unknown
|
||||||
|
|
||||||
|
## Suggested Build Order
|
||||||
|
|
||||||
|
1. G1 context extraction + diagnostics
|
||||||
|
2. G2 greeting parser families + guardrails
|
||||||
|
3. G3 greeting state machine (reactive first)
|
||||||
|
4. G4 proactive gating + cooldowns
|
||||||
|
5. G5 person queue memory extensions
|
||||||
|
6. G6 live validation and polish
|
||||||
|
|
||||||
|
## Definition Of Done For This Track
|
||||||
|
|
||||||
|
- presence-aware greeting behavior works with and without identified users
|
||||||
|
- proactive greeting frequency is policy-bounded and observable
|
||||||
|
- no regressions in existing `1.0.19` memory/weather/proactivity flows
|
||||||
|
- release docs and backlog are updated with shipped scope and next slice
|
||||||
@@ -41,6 +41,7 @@ The `.NET` cloud now supports structured live capture intended for first robot r
|
|||||||
- HTTP request/response event streams written as NDJSON
|
- HTTP request/response event streams written as NDJSON
|
||||||
- websocket event streams written as NDJSON
|
- websocket event streams written as NDJSON
|
||||||
- per-session websocket fixture export for replay
|
- per-session websocket fixture export for replay
|
||||||
|
- a small `capture-index.ndjson` manifest beside the raw files so group testers can quickly find the session type, operation, and export artifacts
|
||||||
- turn metadata including `transID`, buffered audio counts, finalize attempts, and reply types
|
- turn metadata including `transID`, buffered audio counts, finalize attempts, and reply types
|
||||||
|
|
||||||
Default capture location:
|
Default capture location:
|
||||||
@@ -54,6 +55,7 @@ Artifacts:
|
|||||||
- `websocket/*.events.ndjson`
|
- `websocket/*.events.ndjson`
|
||||||
- `*.events.ndjson`
|
- `*.events.ndjson`
|
||||||
- `websocket/fixtures/*.flow.json`
|
- `websocket/fixtures/*.flow.json`
|
||||||
|
- `capture-index.ndjson`
|
||||||
|
|
||||||
## Suggested First Hookup Plan
|
## Suggested First Hookup Plan
|
||||||
|
|
||||||
@@ -61,8 +63,9 @@ Artifacts:
|
|||||||
2. Confirm HTTP bootstrap and websocket acceptance with the existing smoke/routing helpers.
|
2. Confirm HTTP bootstrap and websocket acceptance with the existing smoke/routing helpers.
|
||||||
3. Run one or two controlled listen turns with Jibo.
|
3. Run one or two controlled listen turns with Jibo.
|
||||||
4. Inspect the captured HTTP and websocket events plus exported websocket fixtures.
|
4. Inspect the captured HTTP and websocket events plus exported websocket fixtures.
|
||||||
5. Convert the best captures into sanitized checked-in fixtures and tests.
|
5. Use `capture-index.ndjson` to quickly locate the important sessions and exported fixtures.
|
||||||
6. Keep Node available to compare any surprising turn behavior before changing infrastructure.
|
6. Convert the best captures into sanitized checked-in fixtures and tests.
|
||||||
|
7. Keep Node available to compare any surprising turn behavior before changing infrastructure.
|
||||||
|
|
||||||
Useful helper scripts:
|
Useful helper scripts:
|
||||||
|
|
||||||
|
|||||||
136
OpenJibo/docs/persistence-architecture.md
Normal file
136
OpenJibo/docs/persistence-architecture.md
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
# Persistence Architecture
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Keep OpenJibo's stateful behavior portable now and Azure-ready later.
|
||||||
|
|
||||||
|
The current in-memory stores are fine as the default implementation, but the app should depend on stable persistence contracts rather than directly on in-memory collections or file formats.
|
||||||
|
|
||||||
|
## Design Principles
|
||||||
|
|
||||||
|
- Application code talks to small, intent-specific interfaces.
|
||||||
|
- Persistence keys are always scoped by tenant and person where relevant.
|
||||||
|
- In-memory, local JSON, and hosted Azure stores are adapters, not behavior sources.
|
||||||
|
- Long-lived data should be versioned so we can add optimistic concurrency later.
|
||||||
|
- Ephemeral turn/session state should stay separate from durable user and device state.
|
||||||
|
|
||||||
|
## Current Seams
|
||||||
|
|
||||||
|
These are the contracts we should preserve:
|
||||||
|
|
||||||
|
- `IPersonalMemoryStore`
|
||||||
|
- personal facts: names, birthdays, preferences, affinities, important dates, household lists
|
||||||
|
- scope: account + loop + device + optional person
|
||||||
|
- `ICloudStateStore`
|
||||||
|
- account, robot, loops, people, sessions, updates, media, backups, holidays, keys
|
||||||
|
- scope: system-level state with loop/device/person records inside it
|
||||||
|
- `IJiboExperienceContentRepository`
|
||||||
|
- catalog/content layer only
|
||||||
|
|
||||||
|
## Recommended Storage Split
|
||||||
|
|
||||||
|
### 1. Identity and topology store
|
||||||
|
|
||||||
|
Responsible for:
|
||||||
|
|
||||||
|
- account profile
|
||||||
|
- robot/device registration
|
||||||
|
- loop membership
|
||||||
|
- person records
|
||||||
|
- greeting/proactive presence metadata when it becomes durable
|
||||||
|
|
||||||
|
This is the seam most likely to become Azure SQL or Cosmos later.
|
||||||
|
|
||||||
|
### 2. Personal memory store
|
||||||
|
|
||||||
|
Responsible for:
|
||||||
|
|
||||||
|
- names
|
||||||
|
- birthdays
|
||||||
|
- preferences
|
||||||
|
- affinities
|
||||||
|
- important dates
|
||||||
|
- household lists
|
||||||
|
|
||||||
|
This can remain in memory now and later move to a durable store keyed by account/loop/device/person.
|
||||||
|
|
||||||
|
### 3. Session and short-lived orchestration state
|
||||||
|
|
||||||
|
Responsible for:
|
||||||
|
|
||||||
|
- websocket/session tokens
|
||||||
|
- temporary skill state
|
||||||
|
- active report/list/greeting interaction state
|
||||||
|
|
||||||
|
This can stay in-process for now, but should be clearly separated from durable memory.
|
||||||
|
|
||||||
|
### 4. Media and backup store
|
||||||
|
|
||||||
|
Responsible for:
|
||||||
|
|
||||||
|
- uploaded media metadata
|
||||||
|
- backup manifests
|
||||||
|
- binary references
|
||||||
|
|
||||||
|
This is a good candidate for Azure Blob Storage plus a metadata table later.
|
||||||
|
|
||||||
|
## Record Shape Guidance
|
||||||
|
|
||||||
|
For durable records, prefer a small shared envelope:
|
||||||
|
|
||||||
|
- `AccountId`
|
||||||
|
- `LoopId`
|
||||||
|
- `DeviceId`
|
||||||
|
- `PersonId` when relevant
|
||||||
|
- `RecordType`
|
||||||
|
- `RecordKey`
|
||||||
|
- `Value`
|
||||||
|
- `CreatedUtc`
|
||||||
|
- `UpdatedUtc`
|
||||||
|
- `Revision` or `ETag`
|
||||||
|
|
||||||
|
That gives us:
|
||||||
|
|
||||||
|
- easy partitioning later
|
||||||
|
- clear tenant boundaries
|
||||||
|
- room for concurrency checks
|
||||||
|
- a path to Azure Table, Cosmos, or SQL without changing behavior code
|
||||||
|
|
||||||
|
## Adapter Plan
|
||||||
|
|
||||||
|
### Phase 1
|
||||||
|
|
||||||
|
- keep `InMemoryPersonalMemoryStore`
|
||||||
|
- keep `InMemoryCloudStateStore`
|
||||||
|
- make sure all callers use the interfaces only
|
||||||
|
- add tests against behavior, not implementation details
|
||||||
|
|
||||||
|
### Phase 2
|
||||||
|
|
||||||
|
- introduce durable adapters behind the same interfaces
|
||||||
|
- likely split:
|
||||||
|
- SQL or Cosmos for identity/topology
|
||||||
|
- Blob or table-backed store for media/backup metadata
|
||||||
|
- table/SQL-backed memory store for personal facts
|
||||||
|
|
||||||
|
### Phase 3
|
||||||
|
|
||||||
|
- add replication/sync primitives if we need multi-server state convergence
|
||||||
|
- prefer explicit change records or versioned snapshots over hidden shared state
|
||||||
|
|
||||||
|
## Non-Goals For Now
|
||||||
|
|
||||||
|
- no Azure SDK types in application logic
|
||||||
|
- no event-sourcing rewrite
|
||||||
|
- no giant generic repository
|
||||||
|
- no distributed transaction work before single-node semantics are stable
|
||||||
|
|
||||||
|
## Immediate Next Step
|
||||||
|
|
||||||
|
Before building durable adapters, tighten the store contracts around:
|
||||||
|
|
||||||
|
- tenant/person scoping
|
||||||
|
- record versioning
|
||||||
|
- explicit load/save operations for durable state
|
||||||
|
|
||||||
|
That lets us swap the backing store later without changing the personality, report, greeting, or list behaviors already built on top.
|
||||||
105
OpenJibo/docs/personal-report-parity-plan.md
Normal file
105
OpenJibo/docs/personal-report-parity-plan.md
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
# Personal Report Parity Plan
|
||||||
|
|
||||||
|
As-of: `2026-05-07`
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Bring OpenJibo personal report behavior closer to original Jibo charm while keeping cloud architecture modern and provider-agnostic.
|
||||||
|
|
||||||
|
## Pegasus Findings (Source Anchors)
|
||||||
|
|
||||||
|
- Weather personality and visuals were MIM-driven, not plain speech:
|
||||||
|
- `C:\Projects\jibo\pegasus\packages\report-skill\src\subskills\weather\WeatherMimLogic.ts`
|
||||||
|
- `C:\Projects\jibo\pegasus\packages\report-skill\mims\en-us\WeatherCommentRain.mim`
|
||||||
|
- `C:\Projects\jibo\pegasus\packages\report-skill\mims\en-us\WeatherTodayHighLow.mim`
|
||||||
|
- `C:\Projects\jibo\pegasus\packages\report-skill\resources\views\weatherHiLo.json`
|
||||||
|
- Weather icons were mapped to condition/time-of-day tokens (`clear-day`, `partly-cloudy-night`, etc.) and used in `<anim cat='weather' meta='...'>`.
|
||||||
|
- Report-skill supported reactive entrypoints beyond full personal report:
|
||||||
|
- `requestWeatherPR`, `requestNews`, `requestCommute`, `requestCalendar`
|
||||||
|
- Source: `C:\Projects\jibo\pegasus\packages\hub\pegasus-skills\report_skill_manifest.json`
|
||||||
|
- Legacy data backends were Lasso-mediated:
|
||||||
|
- weather: Dark Sky
|
||||||
|
- commute: Google Maps directions/traffic
|
||||||
|
- news: AP News feeds
|
||||||
|
- calendar: Google/Outlook connectors
|
||||||
|
- Parser `main_agent` explicitly includes weather/news/personal-report intents; direct commute/calendar intents are not present in that same folder snapshot:
|
||||||
|
- `C:\Projects\jibo\pegasus\packages\parser\dialogflow\main_agent\intents`
|
||||||
|
- Grocery/list behavior found in Pegasus is scripted-response style, not a standalone list skill:
|
||||||
|
- `RA_JBO_ShoppingList.mim` and `RA_JBO_ManageToDoList.mim` are "not supported yet" style responses.
|
||||||
|
|
||||||
|
## OpenJibo Current State
|
||||||
|
|
||||||
|
- Personal report state machine exists and is test-backed.
|
||||||
|
- Weather provider integration exists (OpenWeather), including current and tomorrow.
|
||||||
|
- News and commute currently have baseline placeholder speech, not live provider-backed data orchestration.
|
||||||
|
- Calendar is currently reply-based and not yet provider-integrated.
|
||||||
|
|
||||||
|
## Gap Summary
|
||||||
|
|
||||||
|
1. Weather has factual speech but needs stronger visual/personality parity.
|
||||||
|
2. Non-local weather and broader date scopes need expansion beyond basic trailing `in <location>` and tomorrow handling.
|
||||||
|
3. Live news feed selection and filtering strategy is not yet implemented.
|
||||||
|
4. Commute data path and settings model are not yet mapped to an active provider integration.
|
||||||
|
5. Full personal report parity matrix (weather/commute/calendar/news behavior details) is not yet documented as a ship checklist.
|
||||||
|
|
||||||
|
## Implementation Phases
|
||||||
|
|
||||||
|
## Phase 1 (In Progress): Weather Personality Lift
|
||||||
|
|
||||||
|
- Add weather-condition animation metadata and expressive weather MIM-style prompt metadata to cloud weather speech.
|
||||||
|
- Expand location phrase handling (`in/for/at`) and suffix stripping for common temporal tails.
|
||||||
|
|
||||||
|
## Phase 2: Weather Visual Layer Parity
|
||||||
|
|
||||||
|
- Add weather Hi/Lo view payload support (OpenJibo-side equivalent to `weatherHiLo.json` behavior).
|
||||||
|
- Carry mapped weather icon token + hi/lo values into outbound skill action config.
|
||||||
|
- Keep fallback behavior safe when view assets are unavailable.
|
||||||
|
|
||||||
|
## Phase 3: Weather Scope Expansion
|
||||||
|
|
||||||
|
- Add parser support for additional time requests (for example weekend/next-week phrasing).
|
||||||
|
- Extend weather request model to support short-range date windows.
|
||||||
|
- Decide whether range responses are summarized speech-only or include multi-card view behavior.
|
||||||
|
|
||||||
|
## Phase 4: Live News Source
|
||||||
|
|
||||||
|
- Introduce provider-backed headline ingestion with category toggles.
|
||||||
|
- Mirror core Pegasus constraints:
|
||||||
|
- de-duplicate headlines
|
||||||
|
- filter missing summaries/images
|
||||||
|
- child-safe filtering mode
|
||||||
|
- Preserve current speech fallback if provider is unavailable.
|
||||||
|
|
||||||
|
## Phase 5: Commute Data Path
|
||||||
|
|
||||||
|
- Implement commute provider abstraction and first provider integration.
|
||||||
|
- Recreate core commute decision logic:
|
||||||
|
- minutes-left
|
||||||
|
- normal vs delayed traffic commentary
|
||||||
|
- mode-aware phrasing (drive vs transit)
|
||||||
|
- Add settings contract for origin/destination/work-arrival/mode.
|
||||||
|
|
||||||
|
## Phase 6: Personal Report Coverage Matrix
|
||||||
|
|
||||||
|
- Build parity matrix across weather/news/commute/calendar:
|
||||||
|
- intent phrases
|
||||||
|
- required entities/settings
|
||||||
|
- provider dependencies
|
||||||
|
- expected MIM/view style outputs
|
||||||
|
- fallback behavior
|
||||||
|
- Attach tests and capture criteria for each row.
|
||||||
|
|
||||||
|
## Phase 7 (Future Release): Grocery Lists
|
||||||
|
|
||||||
|
- Track as a future release item (requested by users).
|
||||||
|
- Two candidate paths:
|
||||||
|
1. Native lightweight list skill (fastest to ship).
|
||||||
|
2. Integration-backed list orchestration (better long-term ecosystem fit).
|
||||||
|
- Recommendation: ship native MVP first, then add integration connectors.
|
||||||
|
|
||||||
|
## Next Immediate Execution
|
||||||
|
|
||||||
|
1. Validate weather personality-lift behavior in live runs.
|
||||||
|
2. Implement weather view payload support (Hi/Lo + condition icon).
|
||||||
|
3. Draft provider plan for live news source.
|
||||||
|
4. Draft commute provider interface + settings schema.
|
||||||
@@ -9,6 +9,7 @@ Stand up a small public site on `openjibo.com` that makes the project understand
|
|||||||
- project overview
|
- project overview
|
||||||
- current status
|
- current status
|
||||||
- links to source repositories
|
- links to source repositories
|
||||||
|
- roadmap / long-range plan
|
||||||
- links to device bootstrap docs
|
- links to device bootstrap docs
|
||||||
- explanation of the hosted-cloud direction
|
- explanation of the hosted-cloud direction
|
||||||
- contribution/contact or waitlist path
|
- contribution/contact or waitlist path
|
||||||
|
|||||||
@@ -20,9 +20,60 @@ The goal is to keep compatibility work steady while shipping personality and cap
|
|||||||
- start building reusable content hooks for question-vs-command style responses
|
- start building reusable content hooks for question-vs-command style responses
|
||||||
- keep first implementation rule-based and test-backed
|
- keep first implementation rule-based and test-backed
|
||||||
|
|
||||||
|
### 1a. Original Personalized Function Inventory
|
||||||
|
|
||||||
|
Keep a running checklist of the legacy persona questions and identity surfaces we want to preserve or port:
|
||||||
|
|
||||||
|
- identity and origin: `what are you`, `who are you`, `what is Jibo`, `who made you`, `where are you from`
|
||||||
|
- persona and capability: `do you have a personality`, `what is your job`, `how much do you know`, `what do you want`
|
||||||
|
- self-description and social charm: `what's your name`, `do you have a nickname`, `do you like being Jibo`, `are there others like you`
|
||||||
|
- favorite-style prompts: `what is your favorite color`, `what is your favorite food`, `what is your favorite music`
|
||||||
|
- attraction and preference prompts: `what is your favorite flower`, `do you like R2D2`, `do you like the sun`, `do you like space`, `do you like kids`
|
||||||
|
- longer authored variants for the same prompt family when Pegasus shows richer phrasing, especially multi-clause and follow-up-heavy responses
|
||||||
|
- capability and charm prompts: `can you laugh`, `can you dance`
|
||||||
|
- affect and mood: `how are you`, `are you happy`, `are you sad`, `are you angry`
|
||||||
|
- memory and identity recall: `who am i`, `what is my name`, `when is my birthday`, `what is my favorite music`
|
||||||
|
- greeting and presence charm: `good morning`, `welcome back`, `who is this`, person-aware greeting follow-ups
|
||||||
|
- recognition follow-ups: `do you know me`, `do you remember me`, `can you recognize me`
|
||||||
|
- seasonal and contextual charm: holiday prompts, pizza day, surprise offers, personal report personality hooks
|
||||||
|
- conversational follow-ups that should stay local and warm instead of falling into generic chat
|
||||||
|
|
||||||
|
Current batch note:
|
||||||
|
|
||||||
|
- `favorite color`, `favorite food`, and `favorite music` are the first small favorites-family slice
|
||||||
|
- the latest pass adds longer authored variants for those favorites so the replies keep more of the original Pegasus cadence instead of collapsing to short placeholders
|
||||||
|
- the next source-backed batch now includes `favorite flower`, `R2D2`, `sun`, `space`, `kids`, plus a couple of charm prompts like `can you laugh` and `can you dance`
|
||||||
|
- the follow-up mood batch now includes `how are things`, `how is your day`, `are you sad`, and `are you angry`
|
||||||
|
- the personality follow-up batch now includes `what are you up to` and `what are you doing` so small talk stays warm and local instead of falling into generic chat
|
||||||
|
- the descriptor batch now includes `are you kind`, `are you funny`, `are you helpful`, `are you curious`, `are you loyal`, `are you mischievous`, and `are you likable`
|
||||||
|
- the seasonal batch now includes `what holidays do you celebrate`, New Year's resolution questions, `happy holidays`, `what halloween costume`, spring suggestions, and holiday gift prompts
|
||||||
|
- 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
|
||||||
|
- this pass keeps Build B moving while still favoring source-backed phrasing and preserving the command-vs-question boundary
|
||||||
|
- the next passes should keep the same pattern and prefer source-backed phrasing whenever the legacy MIM text is available
|
||||||
|
- if a source-backed legacy line is missing, use a temporary direct reply only to keep the pass moving, then backfill source text later
|
||||||
|
- after the favorites batch, the next doc pass should focus on richer persona follow-ups and the remaining memory/presence charm surfaces
|
||||||
|
- Build B is now reserved for the next source-backed scripted-response batch:
|
||||||
|
- `how do you work`
|
||||||
|
- `what do you eat`
|
||||||
|
- `where do you live`
|
||||||
|
- `where were you born`
|
||||||
|
- `what languages do you speak`
|
||||||
|
- `what do you like to do`
|
||||||
|
- `what are you made of`
|
||||||
|
- `what is your favorite flower`
|
||||||
|
- `do you like R2D2`
|
||||||
|
- `do you like the sun`
|
||||||
|
- `do you like space`
|
||||||
|
- `do you like kids`
|
||||||
|
- `can you laugh`
|
||||||
|
- `can you dance`
|
||||||
|
|
||||||
|
The goal is to port these in small batches, capture the source-backed phrasing where possible, and keep a test for each batch so the list never becomes a vague backlog graveyard.
|
||||||
|
|
||||||
### 2. Reliability And Device Proof
|
### 2. Reliability And Device Proof
|
||||||
|
|
||||||
- complete update/backup/restore proof path with captures and operator docs
|
- complete update/backup/restore proof path with captures and operator docs
|
||||||
|
- 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
|
||||||
|
|
||||||
@@ -31,6 +82,7 @@ The goal is to keep compatibility work steady while shipping personality and cap
|
|||||||
- prioritize small source-backed slices from Pegasus/JiboOS that can be shipped safely
|
- prioritize small source-backed slices from Pegasus/JiboOS that can be shipped safely
|
||||||
- keep Nimbus and stock payload compatibility as the release guardrail
|
- keep Nimbus and stock payload compatibility as the release guardrail
|
||||||
- avoid broad subsystem rewrites without tests and live-capture evidence
|
- avoid broad subsystem rewrites without tests and live-capture evidence
|
||||||
|
- keep the legacy prompt inventory visible in the backlog so porting stays paced and traceable
|
||||||
|
|
||||||
### 4. Holidays And Seasonal Personality
|
### 4. Holidays And Seasonal Personality
|
||||||
|
|
||||||
@@ -43,6 +95,21 @@ The goal is to keep compatibility work steady while shipping personality and cap
|
|||||||
- define tenant boundaries across account, loop, device, and person-memory records
|
- define tenant boundaries across account, loop, device, and person-memory records
|
||||||
- add storage abstractions that can move from in-memory/local JSON to hosted SQL/Blob without reworking behavior layers
|
- add storage abstractions that can move from in-memory/local JSON to hosted SQL/Blob without reworking behavior layers
|
||||||
- implement memory-ready schemas and repository contracts for user facts (names, birthdays, personal dates, preferences) with strict tenant scoping
|
- implement memory-ready schemas and repository contracts for user facts (names, birthdays, personal dates, preferences) with strict tenant scoping
|
||||||
|
- seed person-aware state keys now so future interactions can scope to account + loop + device + person without another shape change
|
||||||
|
- keep stateful interaction flows repository-backed instead of embedding more ad hoc metadata in the websocket layer
|
||||||
|
- the store seam now exposes revision metadata plus explicit load/save boundaries so durable adapters can drop in later without changing behavior code
|
||||||
|
- the backend seam is now selectable, with file-backed local persistence as the default and an Azure Blob Storage slot wired for future deployment wiring
|
||||||
|
|
||||||
|
### 6. Multi-Server Sync Path
|
||||||
|
|
||||||
|
- document the eventual sync boundary for stateful data that should move between servers
|
||||||
|
- treat the first pass as repository-local durability, then layer replication and conflict handling on top
|
||||||
|
- prefer explicit change records or versioned state snapshots over implicit last-writer wins when we outgrow a single node
|
||||||
|
- keep cross-server reconciliation out of the hot path until the single-server semantics are stable
|
||||||
|
|
||||||
|
Reference design:
|
||||||
|
|
||||||
|
- [persistence-architecture.md](persistence-architecture.md)
|
||||||
|
|
||||||
## First Implemented Slice In `1.0.19`
|
## First Implemented Slice In `1.0.19`
|
||||||
|
|
||||||
@@ -105,6 +172,80 @@ The fifth delivered slice adds provider-backed weather content while preserving
|
|||||||
- simple location extraction is supported for phrasing like `what's the weather in Chicago tomorrow`
|
- simple location extraction is supported for phrasing like `what's the weather in Chicago tomorrow`
|
||||||
- provider config supports appsettings and `OPENWEATHER_API_KEY` environment fallback for deployment
|
- provider config supports appsettings and `OPENWEATHER_API_KEY` environment fallback for deployment
|
||||||
|
|
||||||
|
## Personality Import Ladder
|
||||||
|
|
||||||
|
This is the practical plan for importing legacy Jibo `mims` into OpenJibo without pretending we already have a full Pegasus runtime.
|
||||||
|
|
||||||
|
### What Is Possible Today
|
||||||
|
|
||||||
|
OpenJibo can already host a meaningful subset of legacy personality content because it has:
|
||||||
|
|
||||||
|
- a shared catalog for content-driven replies
|
||||||
|
- chitchat state-machine routing with route metadata
|
||||||
|
- outbound payload support for `skillId`, `mim_id`, `mim_type`, `prompt_id`, `prompt_sub_category`, and ESML
|
||||||
|
- existing examples that already behave like legacy MIMs for pizza, dance, news, weather, and generic chat
|
||||||
|
|
||||||
|
### What We Need To Build
|
||||||
|
|
||||||
|
To move from hand-wired examples to broader imports, we need three small platform pieces:
|
||||||
|
|
||||||
|
1. a MIM inventory importer that can scan the legacy tree and produce a normalized catalog
|
||||||
|
2. a prompt-selection layer that can choose by `skill_id`, `mim_id`, prompt category, and condition metadata
|
||||||
|
3. a safe ESML/prompt renderer that preserves existing stock-compatible payload shapes
|
||||||
|
|
||||||
|
### What Can Be Ported With Each Build
|
||||||
|
|
||||||
|
#### Build A: Declarative Prompt Packs
|
||||||
|
|
||||||
|
Port immediately:
|
||||||
|
|
||||||
|
- `core-responses`
|
||||||
|
- `deflector`
|
||||||
|
- the simplest `emotion-responses`
|
||||||
|
- any `scripted-responses` that are just direct prompt lists with no special state machine
|
||||||
|
|
||||||
|
Why these first:
|
||||||
|
|
||||||
|
- they are already close to the current `JiboExperienceCatalog` model
|
||||||
|
- they give us user-visible personality quickly
|
||||||
|
- they are the best fit for low-risk testing tomorrow
|
||||||
|
|
||||||
|
#### Build B: Conditioned Prompt Packs
|
||||||
|
|
||||||
|
Port after the importer and renderer are in place:
|
||||||
|
|
||||||
|
- `gqa-responses`
|
||||||
|
- structured emotion responses with `condition` gates
|
||||||
|
- prompt sets that select different replies by user state or Jibo state
|
||||||
|
|
||||||
|
Why these next:
|
||||||
|
|
||||||
|
- they are still mostly declarative
|
||||||
|
- they need a small amount of condition evaluation, but not a new conversation engine
|
||||||
|
|
||||||
|
#### Build C: Conversation Families
|
||||||
|
|
||||||
|
Port after Build B:
|
||||||
|
|
||||||
|
- richer `scripted-responses` families that depend on follow-up state
|
||||||
|
- special-date / holiday personality sets
|
||||||
|
- more nuanced chitchat branches that need context-aware routing
|
||||||
|
- longer authored variants for existing prompts when the source text contains them, so the robot keeps the familiar Pegasus cadence without inventing new dialog composition yet
|
||||||
|
- dialog joining / composition as a post-release feature, kept out of the 1.0.19 ladder so we do not blur authored phrasing with a runtime joiner
|
||||||
|
|
||||||
|
Why these later:
|
||||||
|
|
||||||
|
- they need state and follow-up behavior, not just prompt selection
|
||||||
|
- they are where personality feels most alive, but they are also where bugs will be easiest to introduce
|
||||||
|
|
||||||
|
#### Build D: Full Parity Cleanup
|
||||||
|
|
||||||
|
Port after the core ladder is stable:
|
||||||
|
|
||||||
|
- large cross-skill collections
|
||||||
|
- any MIMs that depend on Pegasus-only parser assumptions
|
||||||
|
- any files that need a dedicated runtime abstraction instead of catalog lookup
|
||||||
|
|
||||||
## System Diagram Alignment Snapshot (`2026-05-06`)
|
## System Diagram Alignment Snapshot (`2026-05-06`)
|
||||||
|
|
||||||
Legacy architecture (`system_diagram.png`) has been mapped to current OpenJibo cloud services so release execution stays anchored to:
|
Legacy architecture (`system_diagram.png`) has been mapped to current OpenJibo cloud services so release execution stays anchored to:
|
||||||
@@ -117,9 +258,36 @@ Reference:
|
|||||||
|
|
||||||
- [system-diagram-alignment.md](system-diagram-alignment.md)
|
- [system-diagram-alignment.md](system-diagram-alignment.md)
|
||||||
|
|
||||||
|
## Greetings And Presence Planning Snapshot (`2026-05-07`)
|
||||||
|
|
||||||
|
Pegasus greeting and presence behavior has now been captured into a source-anchored OpenJibo implementation plan.
|
||||||
|
|
||||||
|
Reference:
|
||||||
|
|
||||||
|
- [greetings-presence-plan.md](greetings-presence-plan.md)
|
||||||
|
|
||||||
|
## Live Validation Snapshot (`2026-05-07`)
|
||||||
|
|
||||||
|
User-confirmed end-to-end behavior now includes:
|
||||||
|
|
||||||
|
- `Hey Jibo -> What's your cloud version?` (working)
|
||||||
|
- `Hey Jibo -> What's the time?` (working)
|
||||||
|
- `Hey Jibo -> Surprise me -> pizza fact -> $YESNO (Yes) -> fact` (working)
|
||||||
|
- `Hey Jibo -> Surprise me -> pizza fact -> $YESNO (No) -> decline reply` (working)
|
||||||
|
|
||||||
|
This confirms the pizza-fact offer state now keeps the yes/no branch open through completion and does not require a second wake-word reset for the follow-up answer.
|
||||||
|
|
||||||
|
## Personal Report Planning Snapshot (`2026-05-07`)
|
||||||
|
|
||||||
|
Personal report parity planning is now captured with Pegasus source anchors for weather visuals/animations, live news, commute, and calendar gap coverage.
|
||||||
|
|
||||||
|
Reference:
|
||||||
|
|
||||||
|
- [personal-report-parity-plan.md](personal-report-parity-plan.md)
|
||||||
|
|
||||||
## Next Queued Task (`2026-05-06`)
|
## Next Queued Task (`2026-05-06`)
|
||||||
|
|
||||||
Queued next `1.0.19` implementation task:
|
Queued next `1.0.19` implementation task (now started):
|
||||||
|
|
||||||
- dialog parsing expansion and ambiguity guardrails
|
- dialog parsing expansion and ambiguity guardrails
|
||||||
|
|
||||||
@@ -129,18 +297,60 @@ Execution focus:
|
|||||||
- reduce trigger-only captures that drop the rest of the utterance
|
- reduce trigger-only captures that drop the rest of the utterance
|
||||||
- preserve command-vs-question personality split and local skill payload compatibility
|
- preserve command-vs-question personality split and local skill payload compatibility
|
||||||
- add focused tests for new phrase families and ambiguity boundaries
|
- add focused tests for new phrase families and ambiguity boundaries
|
||||||
|
- keep listener-state observability aligned with the legacy GLSM flow while phrase guardrails are added
|
||||||
|
|
||||||
|
First completed guardrail slice under this queue:
|
||||||
|
|
||||||
|
- GLSM listener flow capture + telemetry mapping
|
||||||
|
- stale pending-listen recovery path for long-open no-context/no-audio listens
|
||||||
|
|
||||||
|
Second completed guardrail slice under this queue:
|
||||||
|
|
||||||
|
- tightened date/time ambiguity handling (`what's your birthday`/`what's your bday` no longer falls into date intent)
|
||||||
|
- expanded Pegasus-inspired memory/weather phrase coverage:
|
||||||
|
- birthday alias parsing (`my bday is ...`, `when is my bday`)
|
||||||
|
- shorthand preference sets (`my favorite sport football`)
|
||||||
|
- weather variants (`what's today's weather look like`, `will it be sunny tomorrow`)
|
||||||
|
- listener continuation guardrail now differentiates incomplete preference fragments from complete shorthand preference sets
|
||||||
|
|
||||||
|
Third completed guardrail slice under this queue:
|
||||||
|
|
||||||
|
- expanded Pegasus `userLikesThing` / `userDislikesThing` / `doesUserLikeThing` / `doesUserDislikeThing` phrase-family coverage
|
||||||
|
- includes additional dislike/negation variants (`loathe`, `did not like`, `didn't enjoy`, `don't really like`)
|
||||||
|
- includes group-preference variants (`we like`, `we love`, `we dislike`, `we can't stand`)
|
||||||
|
- includes lookup variants (`do you think i like ...`, `do you believe i don't like ...`)
|
||||||
|
- added affinity set/lookup attempt guardrails so partial captures route to affinity prompts instead of generic chat
|
||||||
|
- extended auto-finalize continuation deferral for the new Pegasus affinity stems (`we like`, `i loathe`, and related variants)
|
||||||
|
- added focused interaction + websocket tests for the new parser/guardrail behavior
|
||||||
|
|
||||||
|
Next queued implementation track after parser guardrails:
|
||||||
|
|
||||||
|
- personal report parity slices (weather visual parity, live news path, commute/calendar gap closure)
|
||||||
|
|
||||||
|
First completed slice in this personal-report parity track:
|
||||||
|
|
||||||
|
- added provider-ready news briefing path with Nimbus-compatible `news` payload continuity
|
||||||
|
- preserved fallback behavior when no live provider is configured
|
||||||
|
- added memory/transcript category hinting for provider requests (`sports`, `technology`, `business`, etc.)
|
||||||
|
- added provider-side request caching for both news and weather to reduce integration churn and repeated lookups
|
||||||
|
- added focused interaction + websocket tests for provider-backed news speech output and request-hint plumbing
|
||||||
|
|
||||||
## Next Slices
|
## Next Slices
|
||||||
|
|
||||||
1. Dialog parsing expansion (queued next as of `2026-05-06`; more phrase variants, ambiguity handling, and transcript-to-intent guardrails)
|
1. MIM import foundation for personality expansion
|
||||||
2. Holidays and seasonal personality slice beyond pizza day (time-scoped content backed by memory/proactivity path)
|
2. Dialog parsing expansion
|
||||||
3. Durable memory persistence path (swap in provider-backed multi-tenant storage while preserving behavior contracts)
|
3. Presence-aware greetings and identity-triggered proactivity
|
||||||
4. Update/backup/restore end-to-end proof (operator-run and documented)
|
4. Personal report parity slices
|
||||||
5. STT noise-screening and short-utterance reliability pass
|
5. Holidays and seasonal personality slice beyond pizza day
|
||||||
6. Provider-backed news expansion and deeper weather parity using Pegasus-backed contracts
|
6. Durable memory persistence path
|
||||||
7. Capture indexing and retention boundary for group testing
|
7. Update/backup/restore end-to-end proof - implemented
|
||||||
|
8. STT noise-screening and short-utterance reliability pass
|
||||||
|
9. Provider-backed news expansion and deeper weather parity
|
||||||
|
10. Capture indexing and retention boundary for group testing, including a lightweight manifest beside raw capture files
|
||||||
|
11. Binary-safe media storage seam with file and Azure Blob adapters, ready for original/thumbnails follow-up
|
||||||
|
|
||||||
For slices 1-5, use Pegasus phrase lists, MIM IDs, and behavior patterns as the source anchor before broadening into OpenJibo-native improvements.
|
For slice 1, use the new import ladder above to keep the work grounded in what OpenJibo can already render today versus what needs new scaffolding.
|
||||||
|
For slices 2-5, use Pegasus phrase lists, MIM IDs, and behavior patterns as the source anchor before broadening into OpenJibo-native improvements.
|
||||||
|
|
||||||
## Definition Of Done
|
## Definition Of Done
|
||||||
|
|
||||||
|
|||||||
151
OpenJibo/docs/roadmap.md
Normal file
151
OpenJibo/docs/roadmap.md
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
# OpenJibo Roadmap
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This is the long-range story for OpenJibo.
|
||||||
|
|
||||||
|
Use it when someone wants the shape of the project without reading every release note, backlog entry, or live-test log.
|
||||||
|
|
||||||
|
The current execution truth still lives in:
|
||||||
|
|
||||||
|
- [Development plan](development-plan.md)
|
||||||
|
- [Feature backlog](feature-backlog.md)
|
||||||
|
- [Release 1.0.19 plan](release-1.0.19-plan.md)
|
||||||
|
- [Device bootstrap path](device-bootstrap.md)
|
||||||
|
|
||||||
|
## North Star
|
||||||
|
|
||||||
|
Bring Jibo back in a way that preserves his original skills, design language, and charm, while layering in a modern hosted cloud, safer updates, and eventually a richer on-device and orchestration stack.
|
||||||
|
|
||||||
|
## Guiding Principles
|
||||||
|
|
||||||
|
- Preserve the original skills and visual design before adding new behaviors.
|
||||||
|
- Build the hosted cloud first so the robot has something stable to talk to.
|
||||||
|
- Use OTA to reduce friction after the cloud is proven.
|
||||||
|
- Keep every migration reversible.
|
||||||
|
- Favor small, source-backed slices over speculative rewrites.
|
||||||
|
- Let Jibo remain the face of the experience, even if other systems help orchestrate the work behind him.
|
||||||
|
|
||||||
|
## Roadmap At A Glance
|
||||||
|
|
||||||
|
| Phase | Focus | Why It Matters |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 1 | Working hosted cloud | Restores the services Jibo already expects and gives us the current platform truth. |
|
||||||
|
| 2 | OTA-assisted recovery and updates | Makes ownership easier by turning the cloud into the delivery path for recovery and upgrades. |
|
||||||
|
| 3 | Open Jibo OS / mode conversion | Creates an owned runtime and configuration layer while preserving the original experience. |
|
||||||
|
| 4 | Tiered brain | Separates reflexes, memory, personality, and higher-level orchestration. |
|
||||||
|
| 5 | CoffeeBreak orchestration | Provides a place for multi-step agent workflows and external tools without flattening Jibo's personality. |
|
||||||
|
| 6 | Ecosystem expansion | Grows the platform into household, productivity, and multi-device use cases. |
|
||||||
|
|
||||||
|
## Phase 1: Working Hosted Cloud
|
||||||
|
|
||||||
|
Current state: in progress.
|
||||||
|
|
||||||
|
The near-term job is to keep the hosted cloud stable and honest:
|
||||||
|
|
||||||
|
- maintain HTTP and WebSocket compatibility for startup and turn handling
|
||||||
|
- keep the .NET cloud as the production track
|
||||||
|
- keep Node as the reverse-engineering oracle and fixture source
|
||||||
|
- continue update, backup, restore, media, STT, and live-capture proof
|
||||||
|
- keep the real-device bootstrap path documented and repeatable
|
||||||
|
|
||||||
|
Exit criteria:
|
||||||
|
|
||||||
|
- a real Jibo can reach the hosted cloud consistently
|
||||||
|
- the cloud can carry the startup and conversation flows needed for daily use
|
||||||
|
- update and recovery behavior is understood well enough to trust the next layer
|
||||||
|
|
||||||
|
## Phase 2: OTA-Assisted Recovery
|
||||||
|
|
||||||
|
Once the hosted cloud is solid, OTA becomes the simplification layer.
|
||||||
|
|
||||||
|
This phase should:
|
||||||
|
|
||||||
|
- move software updates and recovery flows into a reliable hosted path
|
||||||
|
- reduce how often owners need manual RCM or network patching
|
||||||
|
- make device recovery and version management feel like a product instead of a lab exercise
|
||||||
|
- keep rollback and failure handling explicit
|
||||||
|
|
||||||
|
OTA is the path that makes ownership easier. It is not the thing that must be solved before the cloud can live.
|
||||||
|
|
||||||
|
## Phase 3: Open Jibo OS / Mode Conversion
|
||||||
|
|
||||||
|
After cloud and OTA are trustworthy, the project can move from "open cloud" to "open platform."
|
||||||
|
|
||||||
|
The goal is not to erase stock Jibo. The goal is to give owners an Open Jibo mode that:
|
||||||
|
|
||||||
|
- preserves the original Jibo feel and skill surface
|
||||||
|
- can be installed or selected without a one-way trap
|
||||||
|
- can fall back to stock behavior when needed
|
||||||
|
- makes future features easier to ship on top of a known runtime
|
||||||
|
|
||||||
|
This is where the breadcrumbs in the repo become important:
|
||||||
|
|
||||||
|
- `open-jibo` and `open-jibo-ai` modes
|
||||||
|
- a startup migration skill that can invite existing owners to convert
|
||||||
|
- a reversible path back to stock
|
||||||
|
- the hosted sites and support docs on `openjibo.com` and `openjibo.ai` that explain the transition clearly
|
||||||
|
|
||||||
|
## Phase 4: Tiered Brain
|
||||||
|
|
||||||
|
A single monolithic "AI brain" is not the best fit for Jibo. A tiered model is better.
|
||||||
|
|
||||||
|
Suggested tiers:
|
||||||
|
|
||||||
|
- Tier 0: original Jibo reflexes, stock skills, and local charm
|
||||||
|
- Tier 1: hosted cloud routing and compatibility
|
||||||
|
- Tier 2: memory, personality, and proactivity
|
||||||
|
- Tier 3: richer reasoning and multi-step planning
|
||||||
|
- Tier 4: external agent orchestration and task delegation
|
||||||
|
- Tier 5: multi-device and household coordination
|
||||||
|
|
||||||
|
The point of the tiers is not to make Jibo feel bigger at every turn. It is to keep simple interactions fast and charming while reserving more complex work for the layers that can actually support it.
|
||||||
|
|
||||||
|
## CoffeeBreak (`coffeebreakai.dev`) As An Orchestration Layer
|
||||||
|
|
||||||
|
CoffeeBreak fits naturally above the tiered brain as a coordination plane.
|
||||||
|
|
||||||
|
The intended relationship is:
|
||||||
|
|
||||||
|
- Jibo keeps the voice, personality, and local interaction style
|
||||||
|
- OpenJibo routes simple and medium-complexity tasks itself
|
||||||
|
- CoffeeBreak can take over when a task needs multiple tools, agents, or steps
|
||||||
|
- the result comes back to Jibo in a form that still feels native to him
|
||||||
|
|
||||||
|
That makes CoffeeBreak a close cousin to the tiered brain rather than a separate product line. The brain decides, CoffeeBreak orchestrates, and Jibo remains the face of the interaction.
|
||||||
|
|
||||||
|
## Phase 5: Ecosystem Expansion
|
||||||
|
|
||||||
|
After the core platform is stable, OpenJibo can grow into broader household value:
|
||||||
|
|
||||||
|
- calendar and scheduling
|
||||||
|
- smart home and Home Assistant style control
|
||||||
|
- shopping lists and household memory
|
||||||
|
- multi-user and family recognition
|
||||||
|
- richer media and content experiences
|
||||||
|
- provider-backed news, weather, and personal report flows
|
||||||
|
- eventual multi-Jibo interaction
|
||||||
|
|
||||||
|
## What We Must Preserve
|
||||||
|
|
||||||
|
No matter how far the platform grows, these should stay true:
|
||||||
|
|
||||||
|
- original skills should still feel like Jibo
|
||||||
|
- design should stay recognizable, not generic
|
||||||
|
- migration should be opt-in and reversible whenever possible
|
||||||
|
- the cloud should serve the robot, not replace his identity
|
||||||
|
- technical modernization should preserve charm instead of sanding it off
|
||||||
|
|
||||||
|
## Where To Go Next
|
||||||
|
|
||||||
|
If you want the current execution truth, read:
|
||||||
|
|
||||||
|
- [Development plan](development-plan.md)
|
||||||
|
- [Feature backlog](feature-backlog.md)
|
||||||
|
- [Release 1.0.19 plan](release-1.0.19-plan.md)
|
||||||
|
|
||||||
|
If you want the first-device path, read:
|
||||||
|
|
||||||
|
- [Device bootstrap path](device-bootstrap.md)
|
||||||
|
- [Support tiers](support-tiers.md)
|
||||||
|
- [Public site plan](public-site-plan.md)
|
||||||
@@ -10,12 +10,13 @@ Use it to keep release planning grounded in three views:
|
|||||||
- where we are (current hosted `.NET` implementation)
|
- where we are (current hosted `.NET` implementation)
|
||||||
- where we are headed (next architecture slices)
|
- where we are headed (next architecture slices)
|
||||||
|
|
||||||
As-of date: `2026-05-06`
|
As-of date: `2026-05-07`
|
||||||
|
|
||||||
## Diagram Inputs
|
## Diagram Inputs
|
||||||
|
|
||||||
- Legacy system architecture: `C:\Projects\jibo\pegasus\resources\system_diagram.png`
|
- Legacy system architecture: `C:\Projects\jibo\pegasus\resources\system_diagram.png`
|
||||||
- Legacy generic skill scaffold: `C:\Projects\jibo\pegasus\packages\template-skill\docs\TemplateSkill.png`
|
- Legacy generic skill scaffold: `C:\Projects\jibo\pegasus\packages\template-skill\docs\TemplateSkill.png`
|
||||||
|
- Legacy listener state machine: `C:\Projects\jibo\sdk\packages\skills-service-manager\resources\state-diagrams\glsm.png`
|
||||||
|
|
||||||
## Template Skill Verdict
|
## Template Skill Verdict
|
||||||
|
|
||||||
@@ -39,12 +40,37 @@ Conclusion: do not treat template-skill flow as a port target. Treat it as a sha
|
|||||||
| `Parser / Robust Parser` | rule-based intent resolution in [JiboInteractionService.cs](../src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs) + focused state machines (personal report/chitchat) | deeper phrase import from Pegasus intents/entities plus ambiguity guardrails |
|
| `Parser / Robust Parser` | rule-based intent resolution in [JiboInteractionService.cs](../src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs) + focused state machines (personal report/chitchat) | deeper phrase import from Pegasus intents/entities plus ambiguity guardrails |
|
||||||
| `Skill Router` | [JiboInteractionService.cs](../src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs) decision switch and local skill payload shaping | external skill routing config and safer declarative intent mapping |
|
| `Skill Router` | [JiboInteractionService.cs](../src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs) decision switch and local skill payload shaping | external skill routing config and safer declarative intent mapping |
|
||||||
| `Proactivity Selector` | weighted candidate selection in [JiboInteractionService.cs](../src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs) + pending-offer session state in [WebSocketTurnFinalizationService.cs](../src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/WebSocketTurnFinalizationService.cs) | externalized proactivity catalog, cooldown policy, and broader category coverage |
|
| `Proactivity Selector` | weighted candidate selection in [JiboInteractionService.cs](../src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs) + pending-offer session state in [WebSocketTurnFinalizationService.cs](../src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/WebSocketTurnFinalizationService.cs) | externalized proactivity catalog, cooldown policy, and broader category coverage |
|
||||||
|
| `Presence / Identity Context` | runtime context passthrough in [ProtocolToTurnContextMapper.cs](../src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/ProtocolToTurnContextMapper.cs) and turn metadata handling in [WebSocketTurnFinalizationService.cs](../src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/WebSocketTurnFinalizationService.cs) | normalize `runtime.perception` fields (`speaker`, `peoplePresent`, focused person) for greeting/proactivity policy decisions |
|
||||||
| `Skill Registry` | implicit in current code/routing | formal registry abstraction for local/cloud capabilities and manifest metadata |
|
| `Skill Registry` | implicit in current code/routing | formal registry abstraction for local/cloud capabilities and manifest metadata |
|
||||||
| `History` | tenant-scoped memory store in [InMemoryPersonalMemoryStore.cs](../src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Persistence/InMemoryPersonalMemoryStore.cs) | durable multi-tenant persistence and history timeline/query support |
|
| `History` | tenant-scoped memory store in [InMemoryPersonalMemoryStore.cs](../src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Persistence/InMemoryPersonalMemoryStore.cs) | durable multi-tenant persistence and history timeline/query support |
|
||||||
| `Lasso` provider aggregation | partial provider integration via weather provider wiring in [ServiceCollectionExtensions.cs](../src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/DependencyInjection/ServiceCollectionExtensions.cs) | full aggregation service for weather/news/calendar/knowledge inputs |
|
| `Lasso` provider aggregation | partial provider integration via weather provider wiring in [ServiceCollectionExtensions.cs](../src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/DependencyInjection/ServiceCollectionExtensions.cs) | full aggregation service for weather/news/calendar/knowledge inputs |
|
||||||
| `Proactivity Catalog` | in-code candidate lists/weights | explicit catalog service with tuned weights and operator controls |
|
| `Proactivity Catalog` | in-code candidate lists/weights | explicit catalog service with tuned weights and operator controls |
|
||||||
| `Audio Logs` | file telemetry sinks in infrastructure telemetry | hosted indexed capture/retention for multi-operator analysis |
|
| `Audio Logs` | file telemetry sinks in infrastructure telemetry | hosted indexed capture/retention for multi-operator analysis |
|
||||||
|
|
||||||
|
## GLSM Listener Flow Alignment (`2026-05-06`)
|
||||||
|
|
||||||
|
Captured source:
|
||||||
|
|
||||||
|
- `C:\Projects\jibo\sdk\packages\skills-service-manager\resources\state-diagrams\glsm.png`
|
||||||
|
|
||||||
|
First OpenJibo support slice (implemented):
|
||||||
|
|
||||||
|
- explicit derived listener phases are now emitted in cloud diagnostics:
|
||||||
|
- `HJ_LISTENING`
|
||||||
|
- `LISTENING`
|
||||||
|
- `WAIT_LISTEN_FINISHED`
|
||||||
|
- `DISPATCH_DIALOG`
|
||||||
|
- `PROCESS_LISTENER_QUEUE`
|
||||||
|
- turn telemetry now records `glsm_phase_transition` with previous/next state and trigger
|
||||||
|
- websocket telemetry now includes `glsmPhase` on binary, context, and turn-processed events
|
||||||
|
- stale pending-listen recovery is now implemented:
|
||||||
|
- when a pending `LISTEN` stays open long enough with no context/audio, a new hotphrase listen can recover the stuck state before continuing
|
||||||
|
|
||||||
|
Current parity boundary:
|
||||||
|
|
||||||
|
- this slice focuses on listener lifecycle observability plus stuck-listen recovery
|
||||||
|
- deeper explicit parity states from GLSM (`Interrupt Listeners`, `Handle Launch Parse`, `Handle Global Parse`, `Dispatch Dialog` sub-branches) are next candidates once this capture-driven slice is validated live
|
||||||
|
|
||||||
## Where We Were
|
## Where We Were
|
||||||
|
|
||||||
Legacy cloud design was service-oriented around:
|
Legacy cloud design was service-oriented around:
|
||||||
@@ -102,3 +128,24 @@ Tracking anchors:
|
|||||||
Primary objective:
|
Primary objective:
|
||||||
|
|
||||||
- import Pegasus parser intent phrases/entities to improve intent confidence while preserving command-vs-question personality behavior.
|
- import Pegasus parser intent phrases/entities to improve intent confidence while preserving command-vs-question personality behavior.
|
||||||
|
|
||||||
|
## Greetings And Presence Track (`2026-05-07`)
|
||||||
|
|
||||||
|
A dedicated presence-aware greetings plan is now captured for the next personality slice, grounded in Pegasus `@be/greetings` state, identity, and proactive policy behavior.
|
||||||
|
|
||||||
|
Reference:
|
||||||
|
|
||||||
|
- [greetings-presence-plan.md](greetings-presence-plan.md)
|
||||||
|
|
||||||
|
## Personal Report Parity Track (`2026-05-07`)
|
||||||
|
|
||||||
|
Personal report parity planning is now captured with a source-anchored implementation sequence for:
|
||||||
|
|
||||||
|
- weather visual/personality parity
|
||||||
|
- live news provider path
|
||||||
|
- commute provider path
|
||||||
|
- calendar/report coverage matrix
|
||||||
|
|
||||||
|
Reference:
|
||||||
|
|
||||||
|
- [personal-report-parity-plan.md](personal-report-parity-plan.md)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8"/>
|
||||||
<title>Jibo QR Generator</title>
|
<title>Jibo QR Generator</title>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
|
||||||
<style>
|
<style>
|
||||||
@@ -122,57 +122,65 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>🤖 Jibo Wi-Fi QR Generator</h1>
|
<h1>🤖 Jibo Wi-Fi QR Generator</h1>
|
||||||
<p class="sub">Generates a QR code using Jibo's XOR encoding format</p>
|
<p class="sub">Generates a QR code using Jibo's XOR encoding format</p>
|
||||||
<span id="accessToken"></span>
|
<span id="accessToken"></span>
|
||||||
<span id="wifiConfig"></span>
|
<span id="wifiConfig"></span>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<label>SSID (Network Name)</label>
|
<label>SSID (Network Name)</label>
|
||||||
<input id="ssid" placeholder="MyNetwork" />
|
<input id="ssid" placeholder="MyNetwork"/>
|
||||||
|
|
||||||
<label>Password (leave blank for open network)</label>
|
<label>Password (leave blank for open network)</label>
|
||||||
<input id="password" type="password" placeholder="••••••••" />
|
<input id="password" type="password" placeholder="••••••••"/>
|
||||||
|
|
||||||
<label class="toggle">
|
<label class="toggle">
|
||||||
<input type="checkbox" id="useStatic" onchange="toggleStatic()" />
|
<input type="checkbox" id="useStatic" onchange="toggleStatic()"/>
|
||||||
Use Static IP
|
Use Static IP
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="static-section" id="staticSection">
|
<div class="static-section" id="staticSection">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div>
|
<div>
|
||||||
<label>Static IP</label
|
<label>
|
||||||
><input id="staticIP" placeholder="192.168.1.100" />
|
Static IP
|
||||||
|
</label
|
||||||
|
><input id="staticIP" placeholder="192.168.1.100"/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label>Netmask</label
|
<label>
|
||||||
><input id="netmask" placeholder="255.255.255.0" />
|
Netmask
|
||||||
|
</label
|
||||||
|
><input id="netmask" placeholder="255.255.255.0"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div>
|
<div>
|
||||||
<label>Gateway</label
|
<label>
|
||||||
><input id="gateway" placeholder="192.168.1.1" />
|
Gateway
|
||||||
|
</label
|
||||||
|
><input id="gateway" placeholder="192.168.1.1"/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label>DNS 1</label><input id="dns1" placeholder="8.8.8.8" />
|
<label>DNS 1</label><input id="dns1" placeholder="8.8.8.8"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div><label>DNS 2</label><input id="dns2" placeholder="8.8.4.4" /></div>
|
<div>
|
||||||
|
<label>DNS 2</label><input id="dns2" placeholder="8.8.4.4"/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button onclick="generate()">Generate QR Code</button>
|
<button onclick="generate()">Generate QR Code</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="qr-out">
|
<div id="qr-out">
|
||||||
<div id="qrdiv"></div>
|
<div id="qrdiv"></div>
|
||||||
<button id="dl" onclick="download()">⬇ Download PNG</button>
|
<button id="dl" onclick="download()">⬇ Download PNG</button>
|
||||||
<p class="note">Scan with Jibo's app to configure Wi-Fi</p>
|
<p class="note">Scan with Jibo's app to configure Wi-Fi</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function toggleStatic() {
|
function toggleStatic() {
|
||||||
document.getElementById("staticSection").style.display =
|
document.getElementById("staticSection").style.display =
|
||||||
document.getElementById("useStatic").checked ? "block" : "none";
|
document.getElementById("useStatic").checked ? "block" : "none";
|
||||||
@@ -302,5 +310,5 @@ e!Ekiaon*%O?'O`);
|
|||||||
return wifiConfig;
|
return wifiConfig;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -45,6 +45,52 @@ Human-facing entry points will live on domains such as:
|
|||||||
|
|
||||||
Robot traffic may still arrive using legacy hostnames routed to the OpenJibo service.
|
Robot traffic may still arrive using legacy hostnames routed to the OpenJibo service.
|
||||||
|
|
||||||
|
## Azure Storage Wiring Sample
|
||||||
|
|
||||||
|
For local or hosted Blob-backed persistence, use the Azure sample config in:
|
||||||
|
|
||||||
|
- [appsettings.AzureBlob.sample.json](dotnet/src/Jibo.Cloud.Api/appsettings.AzureBlob.sample.json)
|
||||||
|
|
||||||
|
It shows the expected keys for:
|
||||||
|
|
||||||
|
- `OpenJibo:State:Backend`
|
||||||
|
- `OpenJibo:State:ConnectionString`
|
||||||
|
- `OpenJibo:PersonalMemory:Backend`
|
||||||
|
- `OpenJibo:PersonalMemory:ConnectionString`
|
||||||
|
- `OpenJibo:Media:Backend`
|
||||||
|
- `OpenJibo:Media:ConnectionString`
|
||||||
|
|
||||||
|
The connection string can also come from:
|
||||||
|
|
||||||
|
- `OPENJIBO_STATE_STORAGE_CONNECTION_STRING`
|
||||||
|
- `OPENJIBO_PERSONAL_MEMORY_STORAGE_CONNECTION_STRING`
|
||||||
|
- `OPENJIBO_MEDIA_STORAGE_CONNECTION_STRING`
|
||||||
|
|
||||||
|
For a real storage account, swap `UseDevelopmentStorage=true` with your Azure Storage connection string.
|
||||||
|
|
||||||
|
## Local Startup Note
|
||||||
|
|
||||||
|
To run the API with the Blob-backed sample config in Visual Studio or `dotnet run`, choose the
|
||||||
|
`Jibo.Cloud.Api.AzureBlob` launch profile.
|
||||||
|
|
||||||
|
The test project also has a matching `Jibo.Cloud.Tests.AzureBlob` profile so the smoke test can use
|
||||||
|
the same environment-variable shape when you run it from an IDE.
|
||||||
|
|
||||||
|
Equivalent environment variables:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:OpenJibo__State__Backend = "AzureBlob"
|
||||||
|
$env:OpenJibo__State__ConnectionString = "UseDevelopmentStorage=true"
|
||||||
|
$env:OpenJibo__PersonalMemory__Backend = "AzureBlob"
|
||||||
|
$env:OpenJibo__PersonalMemory__ConnectionString = "UseDevelopmentStorage=true"
|
||||||
|
$env:OpenJibo__Media__Backend = "AzureBlob"
|
||||||
|
$env:OpenJibo__Media__ConnectionString = "UseDevelopmentStorage=true"
|
||||||
|
dotnet run --project dotnet/src/Jibo.Cloud.Api/Jibo.Cloud.Api.csproj
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `UseDevelopmentStorage=true` with your real storage account connection string when you move
|
||||||
|
from local emulation to Azure.
|
||||||
|
|
||||||
## Recovery Strategy
|
## Recovery Strategy
|
||||||
|
|
||||||
The first supported device path is:
|
The first supported device path is:
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
using Jibo.Cloud.Application.Abstractions;
|
using Jibo.Cloud.Application.Abstractions;
|
||||||
using Jibo.Cloud.Application.Services;
|
using Jibo.Cloud.Application.Services;
|
||||||
using Jibo.Cloud.Domain.Models;
|
using Jibo.Cloud.Domain.Models;
|
||||||
@@ -88,18 +89,13 @@ app.Use(async (context, next) =>
|
|||||||
|
|
||||||
var replies = await webSocketService.HandleMessageAsync(envelope, context.RequestAborted);
|
var replies = await webSocketService.HandleMessageAsync(envelope, context.RequestAborted);
|
||||||
var session = ResolveSession(webSocketService, envelope);
|
var session = ResolveSession(webSocketService, envelope);
|
||||||
await telemetrySink.RecordInboundAsync(envelope, session, ReadMessageType(envelope.Text), context.RequestAborted);
|
await telemetrySink.RecordInboundAsync(envelope, session, ReadMessageType(envelope.Text),
|
||||||
|
context.RequestAborted);
|
||||||
foreach (var reply in replies)
|
foreach (var reply in replies)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(reply.Text))
|
if (string.IsNullOrWhiteSpace(reply.Text)) continue;
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reply.DelayMs > 0)
|
if (reply.DelayMs > 0) await Task.Delay(reply.DelayMs, context.RequestAborted);
|
||||||
{
|
|
||||||
await Task.Delay(reply.DelayMs, context.RequestAborted);
|
|
||||||
}
|
|
||||||
|
|
||||||
var payload = Encoding.UTF8.GetBytes(reply.Text);
|
var payload = Encoding.UTF8.GetBytes(reply.Text);
|
||||||
await socket.SendAsync(payload, WebSocketMessageType.Text, true, context.RequestAborted);
|
await socket.SendAsync(payload, WebSocketMessageType.Text, true, context.RequestAborted);
|
||||||
@@ -117,7 +113,8 @@ app.Use(async (context, next) =>
|
|||||||
Token = token
|
Token = token
|
||||||
};
|
};
|
||||||
var closeSession = ResolveSession(webSocketService, closeEnvelope);
|
var closeSession = ResolveSession(webSocketService, closeEnvelope);
|
||||||
await telemetrySink.RecordConnectionClosedAsync(closeEnvelope, closeSession, $"socket-loop-ended{(isPrematureClose ? "-prematurely" : string.Empty)}", context.RequestAborted);
|
await telemetrySink.RecordConnectionClosedAsync(closeEnvelope, closeSession,
|
||||||
|
$"socket-loop-ended{(isPrematureClose ? "-prematurely" : string.Empty)}", context.RequestAborted);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.MapGet("/health", () => Results.Json(new
|
app.MapGet("/health", () => Results.Json(new
|
||||||
@@ -127,7 +124,8 @@ app.MapGet("/health", () => Results.Json(new
|
|||||||
version = OpenJiboCloudBuildInfo.Version
|
version = OpenJiboCloudBuildInfo.Version
|
||||||
}));
|
}));
|
||||||
|
|
||||||
app.MapMethods("/{**path}", ["GET", "POST", "PUT"], async (HttpContext context, JiboCloudProtocolService service, IProtocolTelemetrySink telemetrySink, CancellationToken cancellationToken) =>
|
app.MapMethods("/{**path}", ["GET", "POST", "PUT"], async (HttpContext context, JiboCloudProtocolService service,
|
||||||
|
IProtocolTelemetrySink telemetrySink, CancellationToken cancellationToken) =>
|
||||||
{
|
{
|
||||||
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);
|
||||||
@@ -136,15 +134,9 @@ app.MapMethods("/{**path}", ["GET", "POST", "PUT"], async (HttpContext context,
|
|||||||
context.Response.StatusCode = result.StatusCode;
|
context.Response.StatusCode = result.StatusCode;
|
||||||
context.Response.ContentType = result.ContentType;
|
context.Response.ContentType = result.ContentType;
|
||||||
|
|
||||||
foreach (var header in result.Headers)
|
foreach (var header in result.Headers) context.Response.Headers[header.Key] = header.Value;
|
||||||
{
|
|
||||||
context.Response.Headers[header.Key] = header.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(result.BodyText))
|
if (!string.IsNullOrEmpty(result.BodyText)) await context.Response.WriteAsync(result.BodyText, cancellationToken);
|
||||||
{
|
|
||||||
await context.Response.WriteAsync(result.BodyText, cancellationToken);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
@@ -160,8 +152,7 @@ static async Task<ReceivedSocketMessage> ReceiveAsync(WebSocket socket, Cancella
|
|||||||
{
|
{
|
||||||
result = await socket.ReceiveAsync(buffer, cancellationToken);
|
result = await socket.ReceiveAsync(buffer, cancellationToken);
|
||||||
ms.Write(buffer, 0, result.Count);
|
ms.Write(buffer, 0, result.Count);
|
||||||
}
|
} while (!result.EndOfMessage);
|
||||||
while (!result.EndOfMessage);
|
|
||||||
|
|
||||||
return new ReceivedSocketMessage(result.MessageType, ms.ToArray());
|
return new ReceivedSocketMessage(result.MessageType, ms.ToArray());
|
||||||
}
|
}
|
||||||
@@ -170,7 +161,7 @@ static async Task<ProtocolEnvelope> BuildEnvelopeAsync(HttpContext context, Canc
|
|||||||
{
|
{
|
||||||
context.Request.EnableBuffering();
|
context.Request.EnableBuffering();
|
||||||
|
|
||||||
using var reader = new StreamReader(context.Request.Body, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true);
|
using var reader = new StreamReader(context.Request.Body, Encoding.UTF8, false, leaveOpen: true);
|
||||||
var bodyText = await reader.ReadToEndAsync(cancellationToken);
|
var bodyText = await reader.ReadToEndAsync(cancellationToken);
|
||||||
context.Request.Body.Position = 0;
|
context.Request.Body.Position = 0;
|
||||||
|
|
||||||
@@ -191,66 +182,49 @@ static async Task<ProtocolEnvelope> BuildEnvelopeAsync(HttpContext context, Canc
|
|||||||
FirmwareVersion = context.Request.Headers["X-OpenJibo-Firmware"].ToString(),
|
FirmwareVersion = context.Request.Headers["X-OpenJibo-Firmware"].ToString(),
|
||||||
ApplicationVersion = context.Request.Headers["X-OpenJibo-AppVersion"].ToString(),
|
ApplicationVersion = context.Request.Headers["X-OpenJibo-AppVersion"].ToString(),
|
||||||
BodyText = bodyText,
|
BodyText = bodyText,
|
||||||
Headers = context.Request.Headers.ToDictionary(pair => pair.Key, pair => pair.Value.ToString(), StringComparer.OrdinalIgnoreCase)
|
Headers = context.Request.Headers.ToDictionary(pair => pair.Key, pair => pair.Value.ToString(),
|
||||||
|
StringComparer.OrdinalIgnoreCase)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static string ResolveSocketKind(string host, PathString path)
|
static string ResolveSocketKind(string host, PathString path)
|
||||||
{
|
{
|
||||||
if (host.Equals("api-socket.jibo.com", StringComparison.OrdinalIgnoreCase))
|
if (host.Equals("api-socket.jibo.com", StringComparison.OrdinalIgnoreCase)) return "api-socket";
|
||||||
{
|
|
||||||
return "api-socket";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (host.Equals("neo-hub.jibo.com", StringComparison.OrdinalIgnoreCase) &&
|
if (host.Equals("neo-hub.jibo.com", StringComparison.OrdinalIgnoreCase) &&
|
||||||
path.StartsWithSegments("/v1/proactive"))
|
path.StartsWithSegments("/v1/proactive"))
|
||||||
{
|
|
||||||
return "neo-hub-proactive";
|
return "neo-hub-proactive";
|
||||||
}
|
|
||||||
|
|
||||||
if (host.Equals("neo-hub.jibo.com", StringComparison.OrdinalIgnoreCase))
|
if (host.Equals("neo-hub.jibo.com", StringComparison.OrdinalIgnoreCase)) return "neo-hub-listen";
|
||||||
{
|
|
||||||
return "neo-hub-listen";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (host.Equals("openjibo.com", StringComparison.OrdinalIgnoreCase) ||
|
if (host.Equals("openjibo.com", StringComparison.OrdinalIgnoreCase) ||
|
||||||
host.Equals("openjibo.ai", StringComparison.OrdinalIgnoreCase) ||
|
host.Equals("openjibo.ai", StringComparison.OrdinalIgnoreCase) ||
|
||||||
host.Equals("localhost", StringComparison.OrdinalIgnoreCase))
|
host.Equals("localhost", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
|
||||||
return "openjibo";
|
return "openjibo";
|
||||||
}
|
|
||||||
|
|
||||||
return "unknown";
|
return
|
||||||
|
"neo-hub-listen"; // now it assumes all unknown requests are neo-hub. I did this so that people with custom listen servers (like myself) won't get a bunch of 404 messages when doing a HJ request. -ZaneDev (an awful programmer)
|
||||||
}
|
}
|
||||||
|
|
||||||
static string? ResolveToken(HttpRequest request)
|
static string? ResolveToken(HttpRequest request)
|
||||||
{
|
{
|
||||||
var auth = request.Headers.Authorization.ToString();
|
var auth = request.Headers.Authorization.ToString();
|
||||||
if (auth.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
|
if (auth.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) return auth["Bearer ".Length..].Trim();
|
||||||
{
|
|
||||||
return auth["Bearer ".Length..].Trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
var path = request.Path.Value;
|
var path = request.Path.Value;
|
||||||
if (!string.IsNullOrWhiteSpace(path) && path.Length > 1)
|
if (!string.IsNullOrWhiteSpace(path) && path.Length > 1) return path.Trim('/');
|
||||||
{
|
|
||||||
return path.Trim('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static string ReadMessageType(string? text)
|
static string ReadMessageType(string? text)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(text))
|
if (string.IsNullOrWhiteSpace(text)) return "BINARY_OR_EMPTY";
|
||||||
{
|
|
||||||
return "BINARY_OR_EMPTY";
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var document = System.Text.Json.JsonDocument.Parse(text);
|
using var document = JsonDocument.Parse(text);
|
||||||
return document.RootElement.TryGetProperty("type", out var type) && type.ValueKind == System.Text.Json.JsonValueKind.String
|
return document.RootElement.TryGetProperty("type", out var type) && type.ValueKind == JsonValueKind.String
|
||||||
? type.GetString() ?? "UNKNOWN"
|
? type.GetString() ?? "UNKNOWN"
|
||||||
: "UNKNOWN";
|
: "UNKNOWN";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,20 @@
|
|||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
},
|
},
|
||||||
"applicationUrl": "https://localhost:24604;http://localhost:24605"
|
"applicationUrl": "https://localhost:24604;http://localhost:24605"
|
||||||
|
},
|
||||||
|
"Jibo.Cloud.Api.AzureBlob": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
|
"OpenJibo__State__Backend": "AzureBlob",
|
||||||
|
"OpenJibo__State__ConnectionString": "UseDevelopmentStorage=true",
|
||||||
|
"OpenJibo__PersonalMemory__Backend": "AzureBlob",
|
||||||
|
"OpenJibo__PersonalMemory__ConnectionString": "UseDevelopmentStorage=true",
|
||||||
|
"OpenJibo__Media__Backend": "AzureBlob",
|
||||||
|
"OpenJibo__Media__ConnectionString": "UseDevelopmentStorage=true"
|
||||||
|
},
|
||||||
|
"applicationUrl": "https://localhost:24604;http://localhost:24605"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"OpenJibo": {
|
||||||
|
"State": {
|
||||||
|
"Backend": "AzureBlob",
|
||||||
|
"ConnectionString": "UseDevelopmentStorage=true",
|
||||||
|
"PersistencePath": "App_Data/cloud-state.json"
|
||||||
|
},
|
||||||
|
"PersonalMemory": {
|
||||||
|
"Backend": "AzureBlob",
|
||||||
|
"ConnectionString": "UseDevelopmentStorage=true",
|
||||||
|
"PersistencePath": "App_Data/personal-memory.json"
|
||||||
|
},
|
||||||
|
"Media": {
|
||||||
|
"Backend": "AzureBlob",
|
||||||
|
"ConnectionString": "UseDevelopmentStorage=true",
|
||||||
|
"ContainerName": "openjibo-media"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,7 +27,23 @@
|
|||||||
"BaseUrl": "https://api.openweathermap.org",
|
"BaseUrl": "https://api.openweathermap.org",
|
||||||
"ApiKey": "723667c9ab0318142227c5389900d087",
|
"ApiKey": "723667c9ab0318142227c5389900d087",
|
||||||
"DefaultLocation": "Boston,US",
|
"DefaultLocation": "Boston,US",
|
||||||
"UseCelsius": false
|
"UseCelsius": false,
|
||||||
|
"CurrentCacheTtlSeconds": 120,
|
||||||
|
"ForecastCacheTtlSeconds": 600,
|
||||||
|
"GeocodeCacheTtlSeconds": 21600,
|
||||||
|
"FailureCacheTtlSeconds": 45
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"News": {
|
||||||
|
"NewsApi": {
|
||||||
|
"BaseUrl": "https://newsapi.org",
|
||||||
|
"ApiKey": "5df93a83db9c4c6888f3e06c4a53144f",
|
||||||
|
"Country": "us",
|
||||||
|
"Language": "en",
|
||||||
|
"FallbackQuery": "robotics OR technology OR science",
|
||||||
|
"DefaultCategories": [ "general", "technology", "sports", "business" ],
|
||||||
|
"CacheTtlSeconds": 300,
|
||||||
|
"FailureCacheTtlSeconds": 45
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ namespace Jibo.Cloud.Application.Abstractions;
|
|||||||
|
|
||||||
public interface ICloudStateStore
|
public interface ICloudStateStore
|
||||||
{
|
{
|
||||||
|
PersistenceStateInfo GetPersistenceStateInfo();
|
||||||
|
void LoadPersistedState();
|
||||||
|
void SavePersistedState();
|
||||||
AccountProfile GetAccount();
|
AccountProfile GetAccount();
|
||||||
DeviceRegistration GetRobot();
|
DeviceRegistration GetRobot();
|
||||||
RobotProfile GetRobotProfile();
|
RobotProfile GetRobotProfile();
|
||||||
@@ -13,15 +16,26 @@ public interface ICloudStateStore
|
|||||||
CloudSession OpenSession(string kind, string? deviceId, string? token, string? hostName, string? path);
|
CloudSession OpenSession(string kind, string? deviceId, string? token, string? hostName, string? path);
|
||||||
CloudSession? FindSessionByToken(string token);
|
CloudSession? FindSessionByToken(string token);
|
||||||
IReadOnlyList<LoopRecord> GetLoops();
|
IReadOnlyList<LoopRecord> GetLoops();
|
||||||
|
IReadOnlyList<PersonRecord> GetPeople();
|
||||||
IReadOnlyList<UpdateManifest> ListUpdates(string? subsystem = null, string? filter = null);
|
IReadOnlyList<UpdateManifest> ListUpdates(string? subsystem = null, string? filter = null);
|
||||||
UpdateManifest? GetUpdateFrom(string? subsystem, string? fromVersion, string? filter);
|
UpdateManifest? GetUpdateFrom(string? subsystem, string? fromVersion, string? filter);
|
||||||
UpdateManifest CreateUpdate(string? fromVersion, string? toVersion, string? changes, string? shaHash, long? length, string? subsystem, string? filter, IDictionary<string, object?>? dependencies);
|
|
||||||
|
UpdateManifest CreateUpdate(string? fromVersion, string? toVersion, string? changes, string? shaHash, long? length,
|
||||||
|
string? subsystem, string? filter, IDictionary<string, object?>? dependencies);
|
||||||
|
|
||||||
UpdateManifest RemoveUpdate(string? updateId);
|
UpdateManifest RemoveUpdate(string? updateId);
|
||||||
IReadOnlyList<MediaRecord> ListMedia(IReadOnlyList<string>? loopIds = null, long? after = null, long? before = null);
|
|
||||||
|
IReadOnlyList<MediaRecord> ListMedia(IReadOnlyList<string>? loopIds = null, long? after = null,
|
||||||
|
long? before = null);
|
||||||
|
|
||||||
IReadOnlyList<MediaRecord> GetMedia(IReadOnlyList<string> paths);
|
IReadOnlyList<MediaRecord> GetMedia(IReadOnlyList<string> paths);
|
||||||
IReadOnlyList<MediaRecord> RemoveMedia(IReadOnlyList<string> paths);
|
IReadOnlyList<MediaRecord> RemoveMedia(IReadOnlyList<string> paths);
|
||||||
MediaRecord CreateMedia(string loopId, string path, string type, string reference, bool isEncrypted, IDictionary<string, object?>? meta);
|
|
||||||
|
MediaRecord CreateMedia(string loopId, string path, string type, string reference, bool isEncrypted,
|
||||||
|
IDictionary<string, object?>? meta);
|
||||||
|
|
||||||
IReadOnlyList<BackupRecord> GetBackups();
|
IReadOnlyList<BackupRecord> GetBackups();
|
||||||
|
BackupRecord CreateBackup(string name);
|
||||||
bool ShouldCreateSymmetricKey(string loopId);
|
bool ShouldCreateSymmetricKey(string loopId);
|
||||||
string GetOrCreateSymmetricKey(string loopId);
|
string GetOrCreateSymmetricKey(string loopId);
|
||||||
KeyRequestRecord CreateKeyRequest(string loopId, string publicKey);
|
KeyRequestRecord CreateKeyRequest(string loopId, string publicKey);
|
||||||
|
|||||||
@@ -5,16 +5,42 @@ public interface IJiboExperienceContentRepository
|
|||||||
Task<JiboExperienceCatalog> GetCatalogAsync(CancellationToken cancellationToken = default);
|
Task<JiboExperienceCatalog> GetCatalogAsync(CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class JiboConditionedReply
|
||||||
|
{
|
||||||
|
public string Condition { get; init; } = string.Empty;
|
||||||
|
public string Reply { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class JiboExperienceCatalog
|
public sealed class JiboExperienceCatalog
|
||||||
{
|
{
|
||||||
public IReadOnlyList<string> Jokes { get; init; } = [];
|
public IReadOnlyList<string> Jokes { get; init; } = [];
|
||||||
|
public IReadOnlyList<string> RobotFacts { get; init; } = [];
|
||||||
|
public IReadOnlyList<string> HumanFacts { get; init; } = [];
|
||||||
|
public IReadOnlyList<string> FunFacts { get; init; } = [];
|
||||||
public IReadOnlyList<string> DanceAnimations { get; init; } = [];
|
public IReadOnlyList<string> DanceAnimations { get; init; } = [];
|
||||||
public IReadOnlyList<string> GreetingReplies { get; init; } = [];
|
public IReadOnlyList<string> GreetingReplies { get; init; } = [];
|
||||||
public IReadOnlyList<string> HowAreYouReplies { get; init; } = [];
|
public IReadOnlyList<string> HowAreYouReplies { 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; } = [];
|
||||||
public IReadOnlyList<string> SurpriseReplies { get; init; } = [];
|
public IReadOnlyList<string> SurpriseReplies { get; init; } = [];
|
||||||
public IReadOnlyList<string> PersonalReportReplies { get; init; } = [];
|
public IReadOnlyList<string> PersonalReportReplies { get; init; } = [];
|
||||||
|
public IReadOnlyList<string> PersonalReportKickOffReplies { get; init; } = [];
|
||||||
|
public IReadOnlyList<string> PersonalReportOutroReplies { get; init; } = [];
|
||||||
|
public IReadOnlyList<string> ReportSkillTemplates { get; init; } = [];
|
||||||
|
public IReadOnlyList<string> WeatherIntroReplies { get; init; } = [];
|
||||||
|
public IReadOnlyList<string> WeatherTomorrowIntroReplies { get; init; } = [];
|
||||||
|
public IReadOnlyList<string> WeatherTodayHighLowReplies { get; init; } = [];
|
||||||
|
public IReadOnlyList<string> WeatherTomorrowHighLowReplies { get; init; } = [];
|
||||||
|
public IReadOnlyList<string> WeatherServiceDownReplies { get; init; } = [];
|
||||||
|
public IReadOnlyList<string> CalendarNothingTodayReplies { get; init; } = [];
|
||||||
|
public IReadOnlyList<string> CalendarNothingReplies { get; init; } = [];
|
||||||
|
public IReadOnlyList<string> CalendarOutroReplies { get; init; } = [];
|
||||||
|
public IReadOnlyList<string> CommuteNowReplies { get; init; } = [];
|
||||||
|
public IReadOnlyList<string> CommuteServiceDownReplies { get; init; } = [];
|
||||||
|
public IReadOnlyList<string> NewsIntroReplies { get; init; } = [];
|
||||||
|
public IReadOnlyList<string> NewsCategoryIntroReplies { get; init; } = [];
|
||||||
|
public IReadOnlyList<string> NewsOutroReplies { get; init; } = [];
|
||||||
public IReadOnlyList<string> WeatherReplies { get; init; } = [];
|
public IReadOnlyList<string> WeatherReplies { get; init; } = [];
|
||||||
public IReadOnlyList<string> CalendarReplies { get; init; } = [];
|
public IReadOnlyList<string> CalendarReplies { get; init; } = [];
|
||||||
public IReadOnlyList<string> CommuteReplies { get; init; } = [];
|
public IReadOnlyList<string> CommuteReplies { get; init; } = [];
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
namespace Jibo.Cloud.Application.Abstractions;
|
||||||
|
|
||||||
|
public interface IMediaContentStore
|
||||||
|
{
|
||||||
|
Task StoreAsync(string path, string contentType, byte[] content, IReadOnlyDictionary<string, object?>? meta,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task<MediaContentSnapshot?> LoadAsync(string path, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record MediaContentSnapshot
|
||||||
|
{
|
||||||
|
public string ContentType { get; init; } = "application/octet-stream";
|
||||||
|
public byte[] Content { get; init; } = [];
|
||||||
|
public IReadOnlyDictionary<string, object?> Meta { get; init; } =
|
||||||
|
new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
namespace Jibo.Cloud.Application.Abstractions;
|
||||||
|
|
||||||
|
public interface INewsBriefingProvider
|
||||||
|
{
|
||||||
|
Task<NewsBriefingSnapshot?> GetBriefingAsync(
|
||||||
|
NewsBriefingRequest request,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record NewsBriefingRequest(
|
||||||
|
IReadOnlyList<string> PreferredCategories,
|
||||||
|
int MaxHeadlines = 3);
|
||||||
|
|
||||||
|
public sealed record NewsHeadline(
|
||||||
|
string Title,
|
||||||
|
string? Summary = null,
|
||||||
|
string? Category = null,
|
||||||
|
string? SourceName = null,
|
||||||
|
string? Url = null);
|
||||||
|
|
||||||
|
public sealed record NewsBriefingSnapshot(
|
||||||
|
IReadOnlyList<NewsHeadline> Headlines,
|
||||||
|
string? SourceName = null,
|
||||||
|
string? ProviderStatus = null,
|
||||||
|
string? ProviderMessage = null,
|
||||||
|
int? ProviderHttpStatusCode = null,
|
||||||
|
string? ProviderEndpoint = null,
|
||||||
|
string? ProviderErrorCode = null);
|
||||||
@@ -2,6 +2,9 @@ namespace Jibo.Cloud.Application.Abstractions;
|
|||||||
|
|
||||||
public interface IPersonalMemoryStore
|
public interface IPersonalMemoryStore
|
||||||
{
|
{
|
||||||
|
PersistenceStateInfo GetPersistenceStateInfo();
|
||||||
|
void LoadPersistedState();
|
||||||
|
void SavePersistedState();
|
||||||
void SetBirthday(PersonalMemoryTenantScope tenantScope, string birthdayText);
|
void SetBirthday(PersonalMemoryTenantScope tenantScope, string birthdayText);
|
||||||
string? GetBirthday(PersonalMemoryTenantScope tenantScope);
|
string? GetBirthday(PersonalMemoryTenantScope tenantScope);
|
||||||
void SetPreference(PersonalMemoryTenantScope tenantScope, string category, string value);
|
void SetPreference(PersonalMemoryTenantScope tenantScope, string category, string value);
|
||||||
@@ -13,9 +16,22 @@ public interface IPersonalMemoryStore
|
|||||||
void SetAffinity(PersonalMemoryTenantScope tenantScope, string item, PersonalAffinity affinity);
|
void SetAffinity(PersonalMemoryTenantScope tenantScope, string item, PersonalAffinity affinity);
|
||||||
PersonalAffinity? GetAffinity(PersonalMemoryTenantScope tenantScope, string item);
|
PersonalAffinity? GetAffinity(PersonalMemoryTenantScope tenantScope, string item);
|
||||||
IReadOnlyDictionary<string, PersonalAffinity> GetAffinities(PersonalMemoryTenantScope tenantScope);
|
IReadOnlyDictionary<string, PersonalAffinity> GetAffinities(PersonalMemoryTenantScope tenantScope);
|
||||||
|
void AddListItem(PersonalMemoryTenantScope tenantScope, string listName, string item);
|
||||||
|
IReadOnlyList<string> GetListItems(PersonalMemoryTenantScope tenantScope, string listName);
|
||||||
|
void ClearListItems(PersonalMemoryTenantScope tenantScope, string listName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record PersonalMemoryTenantScope(string AccountId, string LoopId, string DeviceId);
|
public sealed record PersonalMemoryTenantScope(
|
||||||
|
string AccountId,
|
||||||
|
string LoopId,
|
||||||
|
string DeviceId,
|
||||||
|
string? PersonId = null);
|
||||||
|
|
||||||
|
public sealed record PersistenceStateInfo(
|
||||||
|
string SchemaVersion,
|
||||||
|
long Revision,
|
||||||
|
DateTimeOffset? LastLoadedUtc = null,
|
||||||
|
DateTimeOffset? LastSavedUtc = null);
|
||||||
|
|
||||||
public enum PersonalAffinity
|
public enum PersonalAffinity
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,5 +4,6 @@ namespace Jibo.Cloud.Application.Abstractions;
|
|||||||
|
|
||||||
public interface IProtocolTelemetrySink
|
public interface IProtocolTelemetrySink
|
||||||
{
|
{
|
||||||
Task RecordAsync(ProtocolEnvelope envelope, ProtocolDispatchResult result, CancellationToken cancellationToken = default);
|
Task RecordAsync(ProtocolEnvelope envelope, ProtocolDispatchResult result,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,8 @@ namespace Jibo.Cloud.Application.Abstractions;
|
|||||||
|
|
||||||
public interface ITurnTelemetrySink
|
public interface ITurnTelemetrySink
|
||||||
{
|
{
|
||||||
Task RecordTurnDiagnosticAsync(string category, IReadOnlyDictionary<string, object?> details, CancellationToken cancellationToken = default);
|
Task RecordTurnDiagnosticAsync(string category, IReadOnlyDictionary<string, object?> details,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
Task RecordTranscriptError(Exception ex, string message, CancellationToken cancellationToken = default);
|
Task RecordTranscriptError(Exception ex, string message, CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,8 @@ public sealed record WeatherReportRequest(
|
|||||||
double? Latitude,
|
double? Latitude,
|
||||||
double? Longitude,
|
double? Longitude,
|
||||||
bool IsTomorrow,
|
bool IsTomorrow,
|
||||||
bool? UseCelsius);
|
bool? UseCelsius,
|
||||||
|
int? ForecastDayOffset = null);
|
||||||
|
|
||||||
public sealed record WeatherReportSnapshot(
|
public sealed record WeatherReportSnapshot(
|
||||||
string LocationName,
|
string LocationName,
|
||||||
|
|||||||
@@ -4,9 +4,18 @@ namespace Jibo.Cloud.Application.Abstractions;
|
|||||||
|
|
||||||
public interface IWebSocketTelemetrySink
|
public interface IWebSocketTelemetrySink
|
||||||
{
|
{
|
||||||
Task RecordConnectionOpenedAsync(WebSocketMessageEnvelope envelope, CloudSession session, CancellationToken cancellationToken = default);
|
Task RecordConnectionOpenedAsync(WebSocketMessageEnvelope envelope, CloudSession session,
|
||||||
Task RecordInboundAsync(WebSocketMessageEnvelope envelope, CloudSession session, string? messageType, CancellationToken cancellationToken = default);
|
CancellationToken cancellationToken = default);
|
||||||
Task RecordTurnEventAsync(WebSocketMessageEnvelope envelope, CloudSession session, string eventType, IReadOnlyDictionary<string, object?> details, CancellationToken cancellationToken = default);
|
|
||||||
Task RecordOutboundAsync(WebSocketMessageEnvelope envelope, CloudSession session, IReadOnlyList<WebSocketReply> replies, CancellationToken cancellationToken = default);
|
Task RecordInboundAsync(WebSocketMessageEnvelope envelope, CloudSession session, string? messageType,
|
||||||
Task RecordConnectionClosedAsync(WebSocketMessageEnvelope envelope, CloudSession session, string reason, CancellationToken cancellationToken = default);
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task RecordTurnEventAsync(WebSocketMessageEnvelope envelope, CloudSession session, string eventType,
|
||||||
|
IReadOnlyDictionary<string, object?> details, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task RecordOutboundAsync(WebSocketMessageEnvelope envelope, CloudSession session,
|
||||||
|
IReadOnlyList<WebSocketReply> replies, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task RecordConnectionClosedAsync(WebSocketMessageEnvelope envelope, CloudSession session, string reason,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
using Jibo.Cloud.Application.Abstractions;
|
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using Jibo.Cloud.Application.Abstractions;
|
||||||
|
|
||||||
namespace Jibo.Cloud.Application.Services;
|
namespace Jibo.Cloud.Application.Services;
|
||||||
|
|
||||||
@@ -24,10 +24,20 @@ internal static class ChitchatStateMachine
|
|||||||
"how are you feeling",
|
"how are you feeling",
|
||||||
"how do you feel",
|
"how do you feel",
|
||||||
"what are you feeling",
|
"what are you feeling",
|
||||||
|
"what are you up to",
|
||||||
|
"what are you doing",
|
||||||
|
"how are things",
|
||||||
|
"how's things",
|
||||||
|
"how is things",
|
||||||
|
"how's your day",
|
||||||
|
"how is your day",
|
||||||
"what mood are you in",
|
"what mood are you in",
|
||||||
"what is your mood",
|
"what is your mood",
|
||||||
"what's your mood",
|
"what's your mood",
|
||||||
"do you have emotions",
|
"do you have emotions",
|
||||||
|
"are you happy",
|
||||||
|
"are you sad",
|
||||||
|
"are you angry",
|
||||||
"how angry are you",
|
"how angry are you",
|
||||||
"how jealous are you",
|
"how jealous are you",
|
||||||
"how sad are you",
|
"how sad are you",
|
||||||
@@ -126,7 +136,11 @@ internal static class ChitchatStateMachine
|
|||||||
("jealous", ["jealous", "envious", "covetous"]),
|
("jealous", ["jealous", "envious", "covetous"]),
|
||||||
("lonely", ["lonely", "alone", "lonesome"]),
|
("lonely", ["lonely", "alone", "lonesome"]),
|
||||||
("proud", ["proud", "honored"]),
|
("proud", ["proud", "honored"]),
|
||||||
("sad", ["sad", "upset", "unhappy", "depressed", "somber", "downcast", "gloomy", "miserable", "bummed", "heartbroken", "troubled"])
|
("sad",
|
||||||
|
[
|
||||||
|
"sad", "upset", "unhappy", "depressed", "somber", "downcast", "gloomy", "miserable", "bummed",
|
||||||
|
"heartbroken", "troubled"
|
||||||
|
])
|
||||||
];
|
];
|
||||||
|
|
||||||
private static readonly string[] EmotionCommandReplies =
|
private static readonly string[] EmotionCommandReplies =
|
||||||
@@ -152,6 +166,7 @@ internal static class ChitchatStateMachine
|
|||||||
string loweredTranscript,
|
string loweredTranscript,
|
||||||
JiboExperienceCatalog catalog,
|
JiboExperienceCatalog catalog,
|
||||||
IJiboRandomizer randomizer,
|
IJiboRandomizer randomizer,
|
||||||
|
string? currentEmotion,
|
||||||
Func<string> buildErrorResponse)
|
Func<string> buildErrorResponse)
|
||||||
{
|
{
|
||||||
var normalizedLoweredTranscript = NormalizeForPhraseMatching(loweredTranscript);
|
var normalizedLoweredTranscript = NormalizeForPhraseMatching(loweredTranscript);
|
||||||
@@ -164,23 +179,118 @@ internal static class ChitchatStateMachine
|
|||||||
case "robot_personality":
|
case "robot_personality":
|
||||||
return BuildScriptedResponseDecision(
|
return BuildScriptedResponseDecision(
|
||||||
"robot_personality",
|
"robot_personality",
|
||||||
randomizer.Choose(catalog.PersonalityReplies));
|
SelectLegacyPersonalityReply(catalog, randomizer, "curious, playful", "friendly", "personality"));
|
||||||
|
case "robot_taxes":
|
||||||
|
return BuildScriptedResponseDecision(
|
||||||
|
"robot_taxes",
|
||||||
|
SelectLegacyPersonalityReply(catalog, randomizer, "pay anything", "pay taxes", "tax"));
|
||||||
case "how_are_you":
|
case "how_are_you":
|
||||||
return BuildEmotionQueryDecision(
|
return BuildEmotionQueryDecision(
|
||||||
"how_are_you",
|
"how_are_you",
|
||||||
randomizer.Choose(catalog.HowAreYouReplies));
|
SelectEmotionQueryReply(catalog, randomizer, currentEmotion));
|
||||||
|
case "robot_desire":
|
||||||
|
return BuildScriptedResponseDecision(
|
||||||
|
"robot_desire",
|
||||||
|
SelectLegacyPersonalityReply(
|
||||||
|
catalog,
|
||||||
|
randomizer,
|
||||||
|
"socializing and electricity",
|
||||||
|
"want to hang out",
|
||||||
|
"be helpful",
|
||||||
|
"dance from time to time"));
|
||||||
|
case "robot_job":
|
||||||
|
return BuildScriptedResponseDecision(
|
||||||
|
"robot_job",
|
||||||
|
SelectLegacyPersonalityReply(catalog, randomizer, "more fun than a job", "here to help you out"));
|
||||||
|
case "robot_origin_created":
|
||||||
|
return BuildScriptedResponseDecision(
|
||||||
|
"robot_origin_created",
|
||||||
|
SelectLegacyPersonalityReply(
|
||||||
|
catalog,
|
||||||
|
randomizer,
|
||||||
|
"create something",
|
||||||
|
"some people wanted to create something",
|
||||||
|
"wanted to create something",
|
||||||
|
"built a robot",
|
||||||
|
"came out from a box"));
|
||||||
|
case "robot_origin_from":
|
||||||
|
return BuildScriptedResponseDecision(
|
||||||
|
"robot_origin_from",
|
||||||
|
SelectLegacyPersonalityReply(catalog, randomizer, "boston", "came out from a box"));
|
||||||
|
case "robot_identity":
|
||||||
|
return BuildScriptedResponseDecision(
|
||||||
|
"robot_identity",
|
||||||
|
SelectLegacyPersonalityReply(catalog, randomizer, "am a robot", "i'm either jibo",
|
||||||
|
"i am just jibo"));
|
||||||
|
case "robot_likes_being_jibo":
|
||||||
|
return BuildScriptedResponseDecision(
|
||||||
|
"robot_likes_being_jibo",
|
||||||
|
SelectLegacyPersonalityReply(
|
||||||
|
catalog,
|
||||||
|
randomizer,
|
||||||
|
"nothing i'd rather be",
|
||||||
|
"love it",
|
||||||
|
"being a human seems so complicated",
|
||||||
|
"especially yours",
|
||||||
|
"steady flow of electricity",
|
||||||
|
"you bet i do"));
|
||||||
|
case "robot_favorite_color":
|
||||||
|
return BuildScriptedResponseDecision(
|
||||||
|
"robot_favorite_color",
|
||||||
|
SelectLegacyPersonalityReplyFromMatches(
|
||||||
|
catalog,
|
||||||
|
randomizer,
|
||||||
|
"i like all the colors of the rainbow",
|
||||||
|
"blue is my favorite color",
|
||||||
|
"i love hex code number 0 0 d 4 f 0",
|
||||||
|
"i am a big fan of blue",
|
||||||
|
"you can't go wrong with blue"));
|
||||||
|
case "robot_favorite_food":
|
||||||
|
return BuildScriptedResponseDecision(
|
||||||
|
"robot_favorite_food",
|
||||||
|
SelectLegacyPersonalityReplyFromMatches(
|
||||||
|
catalog,
|
||||||
|
randomizer,
|
||||||
|
"i never eat, so i don't have a favorite food by taste",
|
||||||
|
"macaroni is my favorite",
|
||||||
|
"i like macaroni the best",
|
||||||
|
"i also like cantaloupes because they remind me of my head",
|
||||||
|
"macaroni"));
|
||||||
|
case "robot_favorite_music":
|
||||||
|
return BuildScriptedResponseDecision(
|
||||||
|
"robot_favorite_music",
|
||||||
|
SelectLegacyPersonalityReplyFromMatches(
|
||||||
|
catalog,
|
||||||
|
randomizer,
|
||||||
|
"i mostly like fun music i can dance to",
|
||||||
|
"i like lots of different kinds of music",
|
||||||
|
"i don't know that i have a favorite kind yet",
|
||||||
|
"i would say i don't have a favorite, it's all very mathematical",
|
||||||
|
"music"));
|
||||||
|
case "robot_nickname":
|
||||||
|
return BuildScriptedResponseDecision(
|
||||||
|
"robot_nickname",
|
||||||
|
SelectLegacyPersonalityReply(catalog, randomizer, "just jibo", "nickname"));
|
||||||
|
case "robot_name":
|
||||||
|
return BuildScriptedResponseDecision(
|
||||||
|
"robot_name",
|
||||||
|
SelectLegacyPersonalityReply(catalog, randomizer, "no last name", "like Bono", "Jibo."));
|
||||||
|
case "robot_peers":
|
||||||
|
return BuildScriptedResponseDecision(
|
||||||
|
"robot_peers",
|
||||||
|
SelectLegacyPersonalityReply(catalog, randomizer, "one in one million", "others like you"));
|
||||||
|
case "robot_knowledge":
|
||||||
|
return BuildScriptedResponseDecision(
|
||||||
|
"robot_knowledge",
|
||||||
|
SelectLegacyPersonalityReply(catalog, randomizer, "know a lot", "not as much as i will someday"));
|
||||||
case "chat":
|
case "chat":
|
||||||
if (IsEmotionQuery(normalizedLoweredTranscript))
|
if (IsEmotionQuery(normalizedLoweredTranscript))
|
||||||
{
|
|
||||||
return BuildEmotionQueryDecision(
|
return BuildEmotionQueryDecision(
|
||||||
"emotion_query",
|
"emotion_query",
|
||||||
randomizer.Choose(catalog.HowAreYouReplies));
|
SelectEmotionQueryReply(catalog, randomizer, currentEmotion));
|
||||||
}
|
|
||||||
|
|
||||||
if (TryResolveEmotionCommand(normalizedLoweredTranscript, out var emotion))
|
if (TryResolveEmotionCommand(normalizedLoweredTranscript, out var emotion))
|
||||||
{
|
|
||||||
return BuildEmotionCommandDecision(randomizer, emotion!);
|
return BuildEmotionCommandDecision(randomizer, emotion!);
|
||||||
}
|
|
||||||
|
|
||||||
return BuildErrorResponseDecision(
|
return BuildErrorResponseDecision(
|
||||||
"chat",
|
"chat",
|
||||||
@@ -205,7 +315,7 @@ internal static class ChitchatStateMachine
|
|||||||
replyText,
|
replyText,
|
||||||
ContextUpdates: BuildContextUpdates(
|
ContextUpdates: BuildContextUpdates(
|
||||||
ScriptedResponseRoute,
|
ScriptedResponseRoute,
|
||||||
emotion: null));
|
null));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JiboInteractionDecision BuildEmotionQueryDecision(string intentName, string replyText)
|
private static JiboInteractionDecision BuildEmotionQueryDecision(string intentName, string replyText)
|
||||||
@@ -215,7 +325,7 @@ internal static class ChitchatStateMachine
|
|||||||
replyText,
|
replyText,
|
||||||
ContextUpdates: BuildContextUpdates(
|
ContextUpdates: BuildContextUpdates(
|
||||||
EmotionQueryRoute,
|
EmotionQueryRoute,
|
||||||
emotion: null));
|
null));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JiboInteractionDecision BuildEmotionCommandDecision(IJiboRandomizer randomizer, string emotion)
|
private static JiboInteractionDecision BuildEmotionCommandDecision(IJiboRandomizer randomizer, string emotion)
|
||||||
@@ -235,18 +345,20 @@ internal static class ChitchatStateMachine
|
|||||||
"chitchat-skill",
|
"chitchat-skill",
|
||||||
new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
["esml"] = $"<speak><es cat='{esmlEmotion}' filter='!ssa-only, !sfx-only' endNeutral='true'>{responseSuffix}</es></speak>",
|
["esml"] =
|
||||||
|
$"<speak><es cat='{esmlEmotion}' filter='!ssa-only, !sfx-only' endNeutral='true'>{responseSuffix}</es></speak>",
|
||||||
["mim_id"] = "runtime-chat",
|
["mim_id"] = "runtime-chat",
|
||||||
["mim_type"] = "announcement",
|
["mim_type"] = "announcement",
|
||||||
["prompt_id"] = "RUNTIME_EMOTION_COMMAND",
|
["prompt_id"] = "RUNTIME_EMOTION_COMMAND",
|
||||||
["prompt_sub_category"] = "AN"
|
["prompt_sub_category"] = "AN"
|
||||||
},
|
},
|
||||||
ContextUpdates: BuildContextUpdates(
|
BuildContextUpdates(
|
||||||
EmotionCommandRoute,
|
EmotionCommandRoute,
|
||||||
emotion));
|
emotion));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JiboInteractionDecision BuildErrorResponseDecision(string intentName, string replyText, string transcript)
|
private static JiboInteractionDecision BuildErrorResponseDecision(string intentName, string replyText,
|
||||||
|
string transcript)
|
||||||
{
|
{
|
||||||
var normalizedTranscript = string.IsNullOrWhiteSpace(transcript)
|
var normalizedTranscript = string.IsNullOrWhiteSpace(transcript)
|
||||||
? string.Empty
|
? string.Empty
|
||||||
@@ -256,8 +368,8 @@ internal static class ChitchatStateMachine
|
|||||||
replyText,
|
replyText,
|
||||||
ContextUpdates: BuildContextUpdates(
|
ContextUpdates: BuildContextUpdates(
|
||||||
ErrorResponseRoute,
|
ErrorResponseRoute,
|
||||||
emotion: null,
|
null,
|
||||||
rawTranscript: normalizedTranscript));
|
normalizedTranscript));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IDictionary<string, object?> BuildContextUpdates(
|
private static IDictionary<string, object?> BuildContextUpdates(
|
||||||
@@ -276,18 +388,117 @@ internal static class ChitchatStateMachine
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsEmotionQuery(string loweredTranscript)
|
private static string SelectEmotionQueryReply(
|
||||||
|
JiboExperienceCatalog catalog,
|
||||||
|
IJiboRandomizer randomizer,
|
||||||
|
string? currentEmotion)
|
||||||
{
|
{
|
||||||
if (ContainsAnyPhrase(loweredTranscript, EmotionQueryPhrases))
|
if (catalog.EmotionReplies.Count == 0) return randomizer.Choose(catalog.HowAreYouReplies);
|
||||||
{
|
|
||||||
return true;
|
var emotionVariants = ResolveEmotionVariants(currentEmotion);
|
||||||
|
foreach (var reply in catalog.EmotionReplies)
|
||||||
|
if (ConditionMatches(reply.Condition, emotionVariants))
|
||||||
|
return reply.Reply;
|
||||||
|
|
||||||
|
return randomizer.Choose(catalog.HowAreYouReplies);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryResolveEmotionFromText(loweredTranscript, out _))
|
private static bool ConditionMatches(string? condition, IReadOnlyList<string> emotionVariants)
|
||||||
{
|
{
|
||||||
|
var normalizedCondition = NormalizeCondition(condition);
|
||||||
|
if (string.IsNullOrWhiteSpace(normalizedCondition)) return false;
|
||||||
|
|
||||||
|
var clauses = normalizedCondition.Split(new[] { "||" },
|
||||||
|
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
foreach (var clause in clauses)
|
||||||
|
if (MatchesConditionClause(clause, emotionVariants))
|
||||||
|
return true;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool MatchesConditionClause(string clause, IReadOnlyList<string> emotionVariants)
|
||||||
|
{
|
||||||
|
var normalizedClause = NormalizeCondition(clause).ToUpperInvariant();
|
||||||
|
if (normalizedClause == "!JIBO.EMOTION")
|
||||||
|
return emotionVariants.Contains(string.Empty, StringComparer.OrdinalIgnoreCase) ||
|
||||||
|
emotionVariants.Contains("NEUTRAL", StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var equalityIndex = normalizedClause.IndexOf("==", StringComparison.Ordinal);
|
||||||
|
if (equalityIndex < 0) return false;
|
||||||
|
|
||||||
|
var rightSide = normalizedClause[(equalityIndex + 2)..].Trim();
|
||||||
|
var candidate = rightSide.Trim('"', '\'');
|
||||||
|
return emotionVariants.Any(variant => string.Equals(variant, candidate, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<string> ResolveEmotionVariants(string? currentEmotion)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(currentEmotion)) return ["", "NEUTRAL"];
|
||||||
|
|
||||||
|
var normalizedEmotion = NormalizeCondition(currentEmotion).Trim('"', '\'').ToUpperInvariant();
|
||||||
|
return normalizedEmotion switch
|
||||||
|
{
|
||||||
|
"HAPPY" => ["JOYFUL", "PLEASED", "CONFIDENT", "DETERMINED", "HAPPY"],
|
||||||
|
"SAD" => ["INSECURE", "SAD"],
|
||||||
|
"CALM" => ["NEUTRAL", "INSECURE", "CALM"],
|
||||||
|
"NEUTRAL" => ["NEUTRAL"],
|
||||||
|
"JOYFUL" or "PLEASED" or "CONFIDENT" or "DETERMINED" or "INSECURE" => [normalizedEmotion],
|
||||||
|
_ => [normalizedEmotion]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string SelectLegacyPersonalityReply(
|
||||||
|
JiboExperienceCatalog catalog,
|
||||||
|
IJiboRandomizer randomizer,
|
||||||
|
params string[] preferredSnippets)
|
||||||
|
{
|
||||||
|
foreach (var snippet in preferredSnippets)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(snippet)) continue;
|
||||||
|
|
||||||
|
var match = catalog.PersonalityReplies.FirstOrDefault(reply =>
|
||||||
|
reply.Contains(snippet, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (!string.IsNullOrWhiteSpace(match)) return match;
|
||||||
|
}
|
||||||
|
|
||||||
|
return randomizer.Choose(catalog.PersonalityReplies);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string SelectLegacyPersonalityReplyFromMatches(
|
||||||
|
JiboExperienceCatalog catalog,
|
||||||
|
IJiboRandomizer randomizer,
|
||||||
|
params string[] preferredSnippets)
|
||||||
|
{
|
||||||
|
var matches = new List<string>();
|
||||||
|
|
||||||
|
foreach (var snippet in preferredSnippets)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(snippet)) continue;
|
||||||
|
|
||||||
|
var match = catalog.PersonalityReplies.FirstOrDefault(reply =>
|
||||||
|
reply.Contains(snippet, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (!string.IsNullOrWhiteSpace(match)) matches.Add(match);
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches.Count > 0
|
||||||
|
? randomizer.Choose(matches)
|
||||||
|
: randomizer.Choose(catalog.PersonalityReplies);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeCondition(string? condition)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(condition)
|
||||||
|
? string.Empty
|
||||||
|
: PhraseWhitespacePattern.Replace(condition.Trim(), " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsEmotionQuery(string loweredTranscript)
|
||||||
|
{
|
||||||
|
if (ContainsAnyPhrase(loweredTranscript, EmotionQueryPhrases)) return true;
|
||||||
|
|
||||||
|
if (!TryResolveEmotionFromText(loweredTranscript, out _)) return false;
|
||||||
|
|
||||||
return StartsWithAnyPhrase(loweredTranscript, EmotionQueryPrefixes) ||
|
return StartsWithAnyPhrase(loweredTranscript, EmotionQueryPrefixes) ||
|
||||||
StartsWithAnyPhrase(loweredTranscript, EmotionAssertionPrefixes);
|
StartsWithAnyPhrase(loweredTranscript, EmotionAssertionPrefixes);
|
||||||
}
|
}
|
||||||
@@ -298,27 +509,20 @@ internal static class ChitchatStateMachine
|
|||||||
|
|
||||||
foreach (var mapping in DirectEmotionCommandPhrases)
|
foreach (var mapping in DirectEmotionCommandPhrases)
|
||||||
{
|
{
|
||||||
if (!ContainsPhrase(loweredTranscript, mapping.Phrase))
|
if (!ContainsPhrase(loweredTranscript, mapping.Phrase)) continue;
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
emotion = mapping.Emotion;
|
emotion = mapping.Emotion;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var isNegativeCommand = StartsWithAnyPhrase(loweredTranscript, EmotionCommandNegativePrefixes);
|
var isNegativeCommand = StartsWithAnyPhrase(loweredTranscript, EmotionCommandNegativePrefixes);
|
||||||
var isPositiveCommand = !isNegativeCommand && StartsWithAnyPhrase(loweredTranscript, EmotionCommandPositivePrefixes);
|
var isPositiveCommand =
|
||||||
if (!isNegativeCommand && !isPositiveCommand)
|
!isNegativeCommand && StartsWithAnyPhrase(loweredTranscript, EmotionCommandPositivePrefixes);
|
||||||
{
|
if (!isNegativeCommand && !isPositiveCommand) return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryResolveEmotionFromText(loweredTranscript, out var canonicalEmotion) ||
|
if (!TryResolveEmotionFromText(loweredTranscript, out var canonicalEmotion) ||
|
||||||
string.IsNullOrWhiteSpace(canonicalEmotion))
|
string.IsNullOrWhiteSpace(canonicalEmotion))
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
emotion = isNegativeCommand
|
emotion = isNegativeCommand
|
||||||
? "calm"
|
? "calm"
|
||||||
@@ -342,10 +546,7 @@ internal static class ChitchatStateMachine
|
|||||||
emotion = null;
|
emotion = null;
|
||||||
foreach (var mapping in EmotionSynonymMappings)
|
foreach (var mapping in EmotionSynonymMappings)
|
||||||
{
|
{
|
||||||
if (!ContainsPhrase(loweredTranscript, mapping.Phrase))
|
if (!ContainsPhrase(loweredTranscript, mapping.Phrase)) continue;
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
emotion = mapping.Emotion;
|
emotion = mapping.Emotion;
|
||||||
return true;
|
return true;
|
||||||
@@ -357,12 +558,8 @@ internal static class ChitchatStateMachine
|
|||||||
private static bool ContainsAnyPhrase(string loweredTranscript, IEnumerable<string> phrases)
|
private static bool ContainsAnyPhrase(string loweredTranscript, IEnumerable<string> phrases)
|
||||||
{
|
{
|
||||||
foreach (var phrase in phrases)
|
foreach (var phrase in phrases)
|
||||||
{
|
|
||||||
if (ContainsPhrase(loweredTranscript, phrase))
|
if (ContainsPhrase(loweredTranscript, phrase))
|
||||||
{
|
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -372,17 +569,12 @@ internal static class ChitchatStateMachine
|
|||||||
foreach (var phrase in phrases)
|
foreach (var phrase in phrases)
|
||||||
{
|
{
|
||||||
var normalizedPhrase = NormalizeForPhraseMatching(phrase);
|
var normalizedPhrase = NormalizeForPhraseMatching(phrase);
|
||||||
if (string.IsNullOrWhiteSpace(normalizedPhrase))
|
if (string.IsNullOrWhiteSpace(normalizedPhrase)) continue;
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(loweredTranscript, normalizedPhrase, StringComparison.Ordinal) ||
|
if (string.Equals(loweredTranscript, normalizedPhrase, StringComparison.Ordinal) ||
|
||||||
loweredTranscript.StartsWith($"{normalizedPhrase} ", StringComparison.Ordinal))
|
loweredTranscript.StartsWith($"{normalizedPhrase} ", StringComparison.Ordinal))
|
||||||
{
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -392,9 +584,7 @@ internal static class ChitchatStateMachine
|
|||||||
var normalizedPhrase = NormalizeForPhraseMatching(phrase);
|
var normalizedPhrase = NormalizeForPhraseMatching(phrase);
|
||||||
if (string.IsNullOrWhiteSpace(normalizedPhrase) ||
|
if (string.IsNullOrWhiteSpace(normalizedPhrase) ||
|
||||||
string.IsNullOrWhiteSpace(loweredTranscript))
|
string.IsNullOrWhiteSpace(loweredTranscript))
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
return string.Equals(loweredTranscript, normalizedPhrase, StringComparison.Ordinal) ||
|
return string.Equals(loweredTranscript, normalizedPhrase, StringComparison.Ordinal) ||
|
||||||
loweredTranscript.StartsWith($"{normalizedPhrase} ", StringComparison.Ordinal) ||
|
loweredTranscript.StartsWith($"{normalizedPhrase} ", StringComparison.Ordinal) ||
|
||||||
@@ -404,10 +594,7 @@ internal static class ChitchatStateMachine
|
|||||||
|
|
||||||
private static string NormalizeForPhraseMatching(string value)
|
private static string NormalizeForPhraseMatching(string value)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(value))
|
if (string.IsNullOrWhiteSpace(value)) return string.Empty;
|
||||||
{
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
var lowered = value.ToLowerInvariant();
|
var lowered = value.ToLowerInvariant();
|
||||||
var withoutPunctuation = PhrasePunctuationPattern.Replace(lowered, " ");
|
var withoutPunctuation = PhrasePunctuationPattern.Replace(lowered, " ");
|
||||||
@@ -420,19 +607,15 @@ internal static class ChitchatStateMachine
|
|||||||
var mappings = new List<(string Phrase, string Emotion)>();
|
var mappings = new List<(string Phrase, string Emotion)>();
|
||||||
|
|
||||||
foreach (var emotionMapping in PegasusEmotionSynonyms)
|
foreach (var emotionMapping in PegasusEmotionSynonyms)
|
||||||
{
|
|
||||||
foreach (var synonym in emotionMapping.Synonyms)
|
foreach (var synonym in emotionMapping.Synonyms)
|
||||||
{
|
{
|
||||||
var normalizedSynonym = NormalizeForPhraseMatching(synonym);
|
var normalizedSynonym = NormalizeForPhraseMatching(synonym);
|
||||||
if (string.IsNullOrWhiteSpace(normalizedSynonym) ||
|
if (string.IsNullOrWhiteSpace(normalizedSynonym) ||
|
||||||
!seen.Add(normalizedSynonym))
|
!seen.Add(normalizedSynonym))
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
|
||||||
|
|
||||||
mappings.Add((normalizedSynonym, emotionMapping.Emotion));
|
mappings.Add((normalizedSynonym, emotionMapping.Emotion));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
mappings.Sort(static (left, right) => right.Phrase.Length.CompareTo(left.Phrase.Length));
|
mappings.Sort(static (left, right) => right.Phrase.Length.CompareTo(left.Phrase.Length));
|
||||||
return [.. mappings];
|
return [.. mappings];
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ namespace Jibo.Cloud.Application.Services;
|
|||||||
|
|
||||||
public sealed class DemoConversationBroker(JiboInteractionService interactionService) : IConversationBroker
|
public sealed class DemoConversationBroker(JiboInteractionService interactionService) : IConversationBroker
|
||||||
{
|
{
|
||||||
|
private readonly TimeSpan _followUpTimeout = TimeSpan.FromSeconds(6);
|
||||||
|
|
||||||
public async Task<ResponsePlan> HandleTurnAsync(TurnContext turn, CancellationToken cancellationToken = default)
|
public async Task<ResponsePlan> HandleTurnAsync(TurnContext turn, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var decision = await interactionService.BuildDecisionAsync(turn, cancellationToken);
|
var decision = await interactionService.BuildDecisionAsync(turn, cancellationToken);
|
||||||
@@ -31,7 +33,7 @@ public sealed class DemoConversationBroker(JiboInteractionService interactionSer
|
|||||||
? new FollowUpPolicy
|
? new FollowUpPolicy
|
||||||
{
|
{
|
||||||
KeepMicOpen = true,
|
KeepMicOpen = true,
|
||||||
Timeout = TimeSpan.FromSeconds(12),
|
Timeout = _followUpTimeout,
|
||||||
ExpectedTopic = "conversation"
|
ExpectedTopic = "conversation"
|
||||||
}
|
}
|
||||||
: FollowUpPolicy.None,
|
: FollowUpPolicy.None,
|
||||||
@@ -47,24 +49,20 @@ public sealed class DemoConversationBroker(JiboInteractionService interactionSer
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (keepMicOpen)
|
if (keepMicOpen)
|
||||||
{
|
|
||||||
plan.Actions.Add(new ListenAction
|
plan.Actions.Add(new ListenAction
|
||||||
{
|
{
|
||||||
Sequence = 1,
|
Sequence = 1,
|
||||||
Timeout = TimeSpan.FromSeconds(12),
|
Timeout = _followUpTimeout,
|
||||||
Mode = "follow-up"
|
Mode = "follow-up"
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(decision.SkillName))
|
if (!string.IsNullOrWhiteSpace(decision.SkillName))
|
||||||
{
|
|
||||||
plan.Actions.Add(new InvokeNativeSkillAction
|
plan.Actions.Add(new InvokeNativeSkillAction
|
||||||
{
|
{
|
||||||
Sequence = 2,
|
Sequence = 2,
|
||||||
SkillName = decision.SkillName,
|
SkillName = decision.SkillName,
|
||||||
Payload = decision.SkillPayload ?? new Dictionary<string, object?>()
|
Payload = decision.SkillPayload ?? new Dictionary<string, object?>()
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
return plan;
|
return plan;
|
||||||
}
|
}
|
||||||
@@ -74,6 +72,16 @@ public sealed class DemoConversationBroker(JiboInteractionService interactionSer
|
|||||||
return intentName switch
|
return intentName switch
|
||||||
{
|
{
|
||||||
"cloud_version" => false,
|
"cloud_version" => false,
|
||||||
|
"memory_set_name" => false,
|
||||||
|
"memory_get_name" => false,
|
||||||
|
"memory_set_birthday" => false,
|
||||||
|
"memory_get_birthday" => false,
|
||||||
|
"memory_set_important_date" => false,
|
||||||
|
"memory_get_important_date" => false,
|
||||||
|
"memory_set_preference" => false,
|
||||||
|
"memory_get_preference" => false,
|
||||||
|
"memory_set_affinity" => false,
|
||||||
|
"memory_get_affinity" => false,
|
||||||
"word_of_the_day" => false,
|
"word_of_the_day" => false,
|
||||||
"word_of_the_day_guess" => false,
|
"word_of_the_day_guess" => false,
|
||||||
"radio" => false,
|
"radio" => false,
|
||||||
@@ -100,6 +108,8 @@ public sealed class DemoConversationBroker(JiboInteractionService interactionSer
|
|||||||
"snapshot" => false,
|
"snapshot" => false,
|
||||||
"photobooth" => false,
|
"photobooth" => false,
|
||||||
"news" => false,
|
"news" => false,
|
||||||
|
"trigger_ignored" => false,
|
||||||
|
"proactive_greeting" => false,
|
||||||
_ => true
|
_ => true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,277 @@
|
|||||||
|
using Jibo.Cloud.Application.Abstractions;
|
||||||
|
using Jibo.Runtime.Abstractions;
|
||||||
|
|
||||||
|
namespace Jibo.Cloud.Application.Services;
|
||||||
|
|
||||||
|
internal static class HouseholdListOrchestrator
|
||||||
|
{
|
||||||
|
internal const string StateMetadataKey = "householdListState";
|
||||||
|
internal const string TypeMetadataKey = "householdListType";
|
||||||
|
internal const string NoMatchCountMetadataKey = "householdListNoMatchCount";
|
||||||
|
internal const string NoInputCountMetadataKey = "householdListNoInputCount";
|
||||||
|
|
||||||
|
private const string IdleState = "idle";
|
||||||
|
private const string AwaitingItemState = "awaiting_item";
|
||||||
|
|
||||||
|
private static readonly string[] ItemPrefixes =
|
||||||
|
[
|
||||||
|
"add ",
|
||||||
|
"put ",
|
||||||
|
"buy ",
|
||||||
|
"get ",
|
||||||
|
"remind me to ",
|
||||||
|
"i need to ",
|
||||||
|
"i need ",
|
||||||
|
"please add ",
|
||||||
|
"please put "
|
||||||
|
];
|
||||||
|
|
||||||
|
private static readonly string[] ItemSuffixes =
|
||||||
|
[
|
||||||
|
" to my shopping list",
|
||||||
|
" to the shopping list",
|
||||||
|
" on my shopping list",
|
||||||
|
" to my to do list",
|
||||||
|
" to the to do list",
|
||||||
|
" on my to do list",
|
||||||
|
" to my todo list",
|
||||||
|
" to the todo list",
|
||||||
|
" on my todo list"
|
||||||
|
];
|
||||||
|
|
||||||
|
public static Task<JiboInteractionDecision?> TryBuildDecisionAsync(
|
||||||
|
TurnContext turn,
|
||||||
|
string semanticIntent,
|
||||||
|
string transcript,
|
||||||
|
string loweredTranscript,
|
||||||
|
IJiboRandomizer randomizer,
|
||||||
|
IPersonalMemoryStore personalMemoryStore,
|
||||||
|
Func<TurnContext, PersonalMemoryTenantScope> tenantScopeResolver)
|
||||||
|
{
|
||||||
|
var state = ReadString(turn, StateMetadataKey);
|
||||||
|
var listType = ReadString(turn, TypeMetadataKey);
|
||||||
|
var isActiveState = !string.IsNullOrWhiteSpace(state) &&
|
||||||
|
!string.Equals(state, IdleState, StringComparison.OrdinalIgnoreCase);
|
||||||
|
var isShoppingIntent = string.Equals(semanticIntent, "shopping_list", StringComparison.OrdinalIgnoreCase);
|
||||||
|
var isTodoIntent = string.Equals(semanticIntent, "todo_list", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (!isActiveState && !isShoppingIntent && !isTodoIntent)
|
||||||
|
return Task.FromResult<JiboInteractionDecision?>(null);
|
||||||
|
|
||||||
|
var resolvedListType = isShoppingIntent ? "shopping" : isTodoIntent ? "todo" : NormalizeListType(listType);
|
||||||
|
if (string.IsNullOrWhiteSpace(resolvedListType)) resolvedListType = "shopping";
|
||||||
|
|
||||||
|
var tenantScope = tenantScopeResolver(turn);
|
||||||
|
|
||||||
|
if (ContainsAny(loweredTranscript, "cancel", "stop", "never mind", "nevermind", "forget it"))
|
||||||
|
return Task.FromResult<JiboInteractionDecision?>(BuildCancelledDecision(resolvedListType));
|
||||||
|
|
||||||
|
if (IsRecallRequest(loweredTranscript))
|
||||||
|
return Task.FromResult<JiboInteractionDecision?>(BuildRecallDecision(
|
||||||
|
resolvedListType,
|
||||||
|
personalMemoryStore.GetListItems(tenantScope, resolvedListType)));
|
||||||
|
|
||||||
|
var directItem = TryExtractListItem(loweredTranscript);
|
||||||
|
if (string.IsNullOrWhiteSpace(directItem) && isActiveState)
|
||||||
|
{
|
||||||
|
if (IsConversationComplete(loweredTranscript))
|
||||||
|
return Task.FromResult<JiboInteractionDecision?>(new JiboInteractionDecision(
|
||||||
|
resolvedListType == "shopping" ? "shopping_list_done" : "todo_list_done",
|
||||||
|
BuildDoneReply(resolvedListType, personalMemoryStore.GetListItems(tenantScope, resolvedListType)),
|
||||||
|
ContextUpdates: BuildContextUpdates(resolvedListType, IdleState)));
|
||||||
|
|
||||||
|
directItem = NormalizeItem(transcript);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(directItem))
|
||||||
|
{
|
||||||
|
personalMemoryStore.AddListItem(tenantScope, resolvedListType, directItem);
|
||||||
|
return Task.FromResult<JiboInteractionDecision?>(new JiboInteractionDecision(
|
||||||
|
resolvedListType == "shopping" ? "shopping_list_add" : "todo_list_add",
|
||||||
|
BuildAddedReply(resolvedListType, directItem,
|
||||||
|
personalMemoryStore.GetListItems(tenantScope, resolvedListType)),
|
||||||
|
ContextUpdates: BuildContextUpdates(resolvedListType, AwaitingItemState)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(transcript))
|
||||||
|
return Task.FromResult<JiboInteractionDecision?>(new JiboInteractionDecision(
|
||||||
|
resolvedListType == "shopping" ? "shopping_list_prompt" : "todo_list_prompt",
|
||||||
|
BuildPromptReply(resolvedListType),
|
||||||
|
ContextUpdates: BuildContextUpdates(resolvedListType, AwaitingItemState)));
|
||||||
|
|
||||||
|
return Task.FromResult<JiboInteractionDecision?>(new JiboInteractionDecision(
|
||||||
|
resolvedListType == "shopping" ? "shopping_list_prompt" : "todo_list_prompt",
|
||||||
|
BuildPromptReply(resolvedListType),
|
||||||
|
ContextUpdates: BuildContextUpdates(resolvedListType, AwaitingItemState)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IDictionary<string, object?> BuildContextUpdates(string listType, string state)
|
||||||
|
{
|
||||||
|
return new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
[StateMetadataKey] = state,
|
||||||
|
[TypeMetadataKey] = listType,
|
||||||
|
[NoMatchCountMetadataKey] = 0,
|
||||||
|
[NoInputCountMetadataKey] = 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JiboInteractionDecision BuildCancelledDecision(string listType)
|
||||||
|
{
|
||||||
|
return new JiboInteractionDecision(
|
||||||
|
listType == "shopping" ? "shopping_list_cancel" : "todo_list_cancel",
|
||||||
|
listType == "shopping" ? "Okay. I stopped the shopping list." : "Okay. I stopped the to-do list.",
|
||||||
|
ContextUpdates: new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
[StateMetadataKey] = IdleState,
|
||||||
|
[TypeMetadataKey] = listType,
|
||||||
|
[NoMatchCountMetadataKey] = 0,
|
||||||
|
[NoInputCountMetadataKey] = 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JiboInteractionDecision BuildRecallDecision(string listType, IReadOnlyList<string> items)
|
||||||
|
{
|
||||||
|
if (items.Count == 0)
|
||||||
|
return new JiboInteractionDecision(
|
||||||
|
listType == "shopping" ? "shopping_list_recall" : "todo_list_recall",
|
||||||
|
listType == "shopping"
|
||||||
|
? "Your shopping list is empty."
|
||||||
|
: "Your to-do list is empty.",
|
||||||
|
ContextUpdates: new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
[StateMetadataKey] = IdleState,
|
||||||
|
[TypeMetadataKey] = listType,
|
||||||
|
[NoMatchCountMetadataKey] = 0,
|
||||||
|
[NoInputCountMetadataKey] = 0
|
||||||
|
});
|
||||||
|
|
||||||
|
return new JiboInteractionDecision(
|
||||||
|
listType == "shopping" ? "shopping_list_recall" : "todo_list_recall",
|
||||||
|
listType == "shopping"
|
||||||
|
? $"Your shopping list has {JoinList(items)}."
|
||||||
|
: $"Your to-do list has {JoinList(items)}.",
|
||||||
|
ContextUpdates: new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
[StateMetadataKey] = IdleState,
|
||||||
|
[TypeMetadataKey] = listType,
|
||||||
|
[NoMatchCountMetadataKey] = 0,
|
||||||
|
[NoInputCountMetadataKey] = 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildAddedReply(string listType, string addedItem, IReadOnlyList<string> items)
|
||||||
|
{
|
||||||
|
var itemLabel = listType == "shopping" ? "shopping list" : "to-do list";
|
||||||
|
return items.Count == 1
|
||||||
|
? $"Added {addedItem} to your {itemLabel}. What else should I add?"
|
||||||
|
: $"Added {addedItem} to your {itemLabel}. You now have {JoinList(items)}.";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildPromptReply(string listType)
|
||||||
|
{
|
||||||
|
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 listType, IReadOnlyList<string> items)
|
||||||
|
{
|
||||||
|
if (items.Count == 0)
|
||||||
|
return listType == "shopping"
|
||||||
|
? "Okay. Your shopping list is empty."
|
||||||
|
: "Okay. Your to-do list is empty.";
|
||||||
|
|
||||||
|
return listType == "shopping"
|
||||||
|
? $"Okay. Your shopping list has {JoinList(items)}."
|
||||||
|
: $"Okay. Your to-do list has {JoinList(items)}.";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string JoinList(IReadOnlyList<string> items)
|
||||||
|
{
|
||||||
|
return items.Count switch
|
||||||
|
{
|
||||||
|
0 => string.Empty,
|
||||||
|
1 => items[0],
|
||||||
|
2 => $"{items[0]} and {items[1]}",
|
||||||
|
_ => $"{string.Join(", ", items.Take(items.Count - 1))}, and {items[^1]}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? TryExtractListItem(string loweredTranscript)
|
||||||
|
{
|
||||||
|
foreach (var prefix in ItemPrefixes)
|
||||||
|
{
|
||||||
|
if (!loweredTranscript.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) continue;
|
||||||
|
|
||||||
|
var remainder = loweredTranscript[prefix.Length..].Trim();
|
||||||
|
remainder = TrimTrailingListPhrases(remainder);
|
||||||
|
return NormalizeItem(remainder);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsRecallRequest(string loweredTranscript)
|
||||||
|
{
|
||||||
|
return ContainsAny(loweredTranscript,
|
||||||
|
"what is on my shopping list",
|
||||||
|
"what's on my shopping list",
|
||||||
|
"show my shopping list",
|
||||||
|
"what is on my to do list",
|
||||||
|
"what's on my to do list",
|
||||||
|
"show my to do list",
|
||||||
|
"what are my tasks",
|
||||||
|
"what do i need to buy",
|
||||||
|
"what do i need to do");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string TrimTrailingListPhrases(string value)
|
||||||
|
{
|
||||||
|
var result = value;
|
||||||
|
foreach (var suffix in ItemSuffixes)
|
||||||
|
if (result.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
|
||||||
|
result = result[..^suffix.Length].Trim();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeItem(string value)
|
||||||
|
{
|
||||||
|
return value.Trim().TrimEnd('.', ',', '!', '?');
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeListType(string? listType)
|
||||||
|
{
|
||||||
|
var normalized = NormalizeItem(listType ?? string.Empty).ToLowerInvariant();
|
||||||
|
return normalized.Contains("todo", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
normalized.Contains("to do", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? "todo"
|
||||||
|
: normalized.Contains("shopping", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
normalized.Contains("grocery", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? "shopping"
|
||||||
|
: string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ContainsAny(string loweredTranscript, params string[] phrases)
|
||||||
|
{
|
||||||
|
return phrases.Any(phrase => loweredTranscript.Contains(phrase, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsConversationComplete(string loweredTranscript)
|
||||||
|
{
|
||||||
|
return ContainsAny(loweredTranscript,
|
||||||
|
"done",
|
||||||
|
"that's it",
|
||||||
|
"that s it",
|
||||||
|
"all set",
|
||||||
|
"finished",
|
||||||
|
"no more",
|
||||||
|
"nothing else");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? ReadString(TurnContext turn, string key)
|
||||||
|
{
|
||||||
|
return turn.Attributes.TryGetValue(key, out var value) ? value?.ToString() : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,8 +4,10 @@ using Jibo.Cloud.Domain.Models;
|
|||||||
|
|
||||||
namespace Jibo.Cloud.Application.Services;
|
namespace Jibo.Cloud.Application.Services;
|
||||||
|
|
||||||
public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
|
public sealed class JiboCloudProtocolService
|
||||||
{
|
{
|
||||||
|
private readonly ICloudStateStore _stateStore;
|
||||||
|
private readonly IMediaContentStore _mediaContentStore;
|
||||||
private static readonly string[] AcceptedHosts =
|
private static readonly string[] AcceptedHosts =
|
||||||
[
|
[
|
||||||
"api.jibo.com",
|
"api.jibo.com",
|
||||||
@@ -14,97 +16,74 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
|
|||||||
"localhost"
|
"localhost"
|
||||||
];
|
];
|
||||||
|
|
||||||
public Task<ProtocolDispatchResult> DispatchAsync(ProtocolEnvelope envelope, CancellationToken cancellationToken = default)
|
public JiboCloudProtocolService(ICloudStateStore stateStore, IMediaContentStore? mediaContentStore = null)
|
||||||
|
{
|
||||||
|
_stateStore = stateStore;
|
||||||
|
_mediaContentStore = mediaContentStore ?? new NullMediaContentStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<ProtocolDispatchResult> DispatchAsync(ProtocolEnvelope envelope,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (envelope.Method.Equals("GET", StringComparison.OrdinalIgnoreCase) &&
|
if (envelope.Method.Equals("GET", StringComparison.OrdinalIgnoreCase) &&
|
||||||
envelope.Path == "/" &&
|
envelope.Path == "/" &&
|
||||||
string.IsNullOrWhiteSpace(envelope.ServicePrefix))
|
string.IsNullOrWhiteSpace(envelope.ServicePrefix))
|
||||||
{
|
|
||||||
return Task.FromResult(ProtocolDispatchResult.NoContent());
|
return Task.FromResult(ProtocolDispatchResult.NoContent());
|
||||||
}
|
|
||||||
|
|
||||||
if (envelope.Method.Equals("GET", StringComparison.OrdinalIgnoreCase) &&
|
if (envelope.Method.Equals("GET", StringComparison.OrdinalIgnoreCase) &&
|
||||||
envelope.Path.Equals("/health", StringComparison.OrdinalIgnoreCase))
|
envelope.Path.Equals("/health", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
|
||||||
return Task.FromResult(ProtocolDispatchResult.Ok(new { ok = true, host = envelope.HostName }));
|
return Task.FromResult(ProtocolDispatchResult.Ok(new { ok = true, host = envelope.HostName }));
|
||||||
}
|
|
||||||
|
|
||||||
if (envelope.Method.Equals("GET", StringComparison.OrdinalIgnoreCase) &&
|
if (envelope.Method.Equals("GET", StringComparison.OrdinalIgnoreCase) &&
|
||||||
envelope.Path.StartsWith("/media/", StringComparison.OrdinalIgnoreCase))
|
envelope.Path.StartsWith("/media/", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
|
||||||
return Task.FromResult(HandleMediaContent(envelope));
|
return Task.FromResult(HandleMediaContent(envelope));
|
||||||
}
|
|
||||||
|
|
||||||
if (envelope.Method.Equals("PUT", StringComparison.OrdinalIgnoreCase) &&
|
if (envelope.Method.Equals("PUT", StringComparison.OrdinalIgnoreCase) &&
|
||||||
(envelope.Path.Equals("/upload/asr-binary", StringComparison.OrdinalIgnoreCase) ||
|
(envelope.Path.Equals("/upload/asr-binary", StringComparison.OrdinalIgnoreCase) ||
|
||||||
envelope.Path.Equals("/upload/log-events", StringComparison.OrdinalIgnoreCase) ||
|
envelope.Path.Equals("/upload/log-events", StringComparison.OrdinalIgnoreCase) ||
|
||||||
envelope.Path.Equals("/upload/log-binary", StringComparison.OrdinalIgnoreCase)))
|
envelope.Path.Equals("/upload/log-binary", StringComparison.OrdinalIgnoreCase)))
|
||||||
{
|
|
||||||
return Task.FromResult(ProtocolDispatchResult.Raw(200, string.Empty));
|
return Task.FromResult(ProtocolDispatchResult.Raw(200, string.Empty));
|
||||||
}
|
|
||||||
|
|
||||||
if (!AcceptedHosts.Contains(envelope.HostName, StringComparer.OrdinalIgnoreCase))
|
if (!AcceptedHosts.Contains(envelope.HostName, StringComparer.OrdinalIgnoreCase))
|
||||||
{
|
|
||||||
return Task.FromResult(ProtocolDispatchResult.Ok(new
|
return Task.FromResult(ProtocolDispatchResult.Ok(new
|
||||||
{
|
{
|
||||||
ok = true,
|
ok = true,
|
||||||
accepted = false,
|
accepted = false,
|
||||||
host = envelope.HostName
|
host = envelope.HostName
|
||||||
}));
|
}));
|
||||||
}
|
|
||||||
|
|
||||||
var servicePrefix = envelope.ServicePrefix ?? string.Empty;
|
var servicePrefix = envelope.ServicePrefix ?? string.Empty;
|
||||||
var operation = envelope.Operation ?? string.Empty;
|
var operation = envelope.Operation ?? string.Empty;
|
||||||
|
|
||||||
if (servicePrefix.StartsWith("Log_", StringComparison.OrdinalIgnoreCase))
|
if (servicePrefix.StartsWith("Log_", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
|
||||||
return Task.FromResult(HandleLog(operation, envelope));
|
return Task.FromResult(HandleLog(operation, envelope));
|
||||||
}
|
|
||||||
|
|
||||||
if (servicePrefix.StartsWith("Backup_", StringComparison.OrdinalIgnoreCase))
|
if (servicePrefix.StartsWith("Backup_", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
return Task.FromResult(HandleBackup(operation, envelope));
|
||||||
return Task.FromResult(HandleBackup(operation));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (servicePrefix.StartsWith("Account_", StringComparison.OrdinalIgnoreCase))
|
if (servicePrefix.StartsWith("Account_", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
|
||||||
return Task.FromResult(HandleAccount(operation, envelope));
|
return Task.FromResult(HandleAccount(operation, envelope));
|
||||||
}
|
|
||||||
|
|
||||||
if (servicePrefix.StartsWith("Notification_", StringComparison.OrdinalIgnoreCase))
|
if (servicePrefix.StartsWith("Notification_", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
|
||||||
return Task.FromResult(HandleNotification(operation, envelope));
|
return Task.FromResult(HandleNotification(operation, envelope));
|
||||||
}
|
|
||||||
|
|
||||||
if (servicePrefix.StartsWith("Loop_", StringComparison.OrdinalIgnoreCase))
|
if (servicePrefix.StartsWith("Loop_", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
|
||||||
return Task.FromResult(HandleLoop(operation));
|
return Task.FromResult(HandleLoop(operation));
|
||||||
}
|
|
||||||
|
|
||||||
if (servicePrefix.Equals("Media_20160725", StringComparison.OrdinalIgnoreCase))
|
if (servicePrefix.Equals("Media_20160725", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
|
||||||
return Task.FromResult(HandleMedia(operation, envelope));
|
return Task.FromResult(HandleMedia(operation, envelope));
|
||||||
}
|
|
||||||
|
|
||||||
if (servicePrefix.StartsWith("Key_", StringComparison.OrdinalIgnoreCase))
|
if (servicePrefix.StartsWith("Key_", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
|
||||||
return Task.FromResult(HandleKey(operation, envelope));
|
return Task.FromResult(HandleKey(operation, envelope));
|
||||||
}
|
|
||||||
|
|
||||||
if (servicePrefix.StartsWith("Person_", StringComparison.OrdinalIgnoreCase))
|
if (servicePrefix.StartsWith("Person_", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
|
||||||
return Task.FromResult(HandlePerson(operation));
|
return Task.FromResult(HandlePerson(operation));
|
||||||
}
|
|
||||||
|
|
||||||
if (servicePrefix.StartsWith("Robot_", StringComparison.OrdinalIgnoreCase))
|
if (servicePrefix.StartsWith("Robot_", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
|
||||||
return Task.FromResult(HandleRobot(operation, envelope));
|
return Task.FromResult(HandleRobot(operation, envelope));
|
||||||
}
|
|
||||||
|
|
||||||
if (servicePrefix.StartsWith("Update_", StringComparison.OrdinalIgnoreCase))
|
if (servicePrefix.StartsWith("Update_", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
|
||||||
return Task.FromResult(HandleUpdate(operation, envelope));
|
return Task.FromResult(HandleUpdate(operation, envelope));
|
||||||
}
|
|
||||||
|
|
||||||
return Task.FromResult(ProtocolDispatchResult.Ok(new
|
return Task.FromResult(ProtocolDispatchResult.Ok(new
|
||||||
{
|
{
|
||||||
@@ -118,26 +97,22 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
|
|||||||
|
|
||||||
private ProtocolDispatchResult HandleAccount(string operation, ProtocolEnvelope envelope)
|
private ProtocolDispatchResult HandleAccount(string operation, ProtocolEnvelope envelope)
|
||||||
{
|
{
|
||||||
var account = stateStore.GetAccount();
|
var account = _stateStore.GetAccount();
|
||||||
var body = envelope.TryParseBody();
|
var body = envelope.TryParseBody();
|
||||||
|
|
||||||
if (operation.Equals("CreateHubToken", StringComparison.OrdinalIgnoreCase))
|
if (operation.Equals("CreateHubToken", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
|
||||||
return ProtocolDispatchResult.Ok(new
|
return ProtocolDispatchResult.Ok(new
|
||||||
{
|
{
|
||||||
token = stateStore.IssueHubToken(),
|
token = _stateStore.IssueHubToken(),
|
||||||
expires = DateTimeOffset.UtcNow.AddHours(1).ToUnixTimeMilliseconds()
|
expires = DateTimeOffset.UtcNow.AddHours(1).ToUnixTimeMilliseconds()
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (operation.Equals("CreateAccessToken", StringComparison.OrdinalIgnoreCase))
|
if (operation.Equals("CreateAccessToken", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
|
||||||
return ProtocolDispatchResult.Ok(new
|
return ProtocolDispatchResult.Ok(new
|
||||||
{
|
{
|
||||||
token = $"access-{account.AccountId}-{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}",
|
token = $"access-{account.AccountId}-{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}",
|
||||||
expires = DateTimeOffset.UtcNow.AddHours(1).ToUnixTimeMilliseconds()
|
expires = DateTimeOffset.UtcNow.AddHours(1).ToUnixTimeMilliseconds()
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (operation.Equals("CheckEmail", StringComparison.OrdinalIgnoreCase))
|
if (operation.Equals("CheckEmail", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
@@ -149,7 +124,6 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (operation is "Create" or "Login")
|
if (operation is "Create" or "Login")
|
||||||
{
|
|
||||||
return ProtocolDispatchResult.Ok(new
|
return ProtocolDispatchResult.Ok(new
|
||||||
{
|
{
|
||||||
id = account.AccountId,
|
id = account.AccountId,
|
||||||
@@ -168,17 +142,13 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
|
|||||||
facebookConnected = false,
|
facebookConnected = false,
|
||||||
termsAccepted = true
|
termsAccepted = true
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (operation.Equals("Get", StringComparison.OrdinalIgnoreCase))
|
if (operation.Equals("Get", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var ids = ReadStringArray(body, "ids");
|
var ids = ReadStringArray(body, "ids");
|
||||||
var matches = ids.Count == 0 || ids.Contains(account.AccountId, StringComparer.OrdinalIgnoreCase);
|
var matches = ids.Count == 0 || ids.Contains(account.AccountId, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
if (!matches)
|
if (!matches) return ProtocolDispatchResult.Ok(Array.Empty<object>());
|
||||||
{
|
|
||||||
return ProtocolDispatchResult.Ok(Array.Empty<object>());
|
|
||||||
}
|
|
||||||
|
|
||||||
return ProtocolDispatchResult.Ok(new[]
|
return ProtocolDispatchResult.Ok(new[]
|
||||||
{
|
{
|
||||||
@@ -216,22 +186,21 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (operation.Equals("GetAccountByAccessToken", StringComparison.OrdinalIgnoreCase))
|
if (operation.Equals("GetAccountByAccessToken", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
|
||||||
return ProtocolDispatchResult.Ok(new
|
return ProtocolDispatchResult.Ok(new
|
||||||
{
|
{
|
||||||
id = account.AccountId,
|
id = account.AccountId,
|
||||||
accessKeyId = account.AccessKeyId,
|
accessKeyId = account.AccessKeyId,
|
||||||
secretAccessKey = account.SecretAccessKey,
|
secretAccessKey = account.SecretAccessKey,
|
||||||
email = account.Email,
|
email = account.Email,
|
||||||
friendlyId = stateStore.GetRobot().RobotId,
|
friendlyId = _stateStore.GetRobot().RobotId,
|
||||||
payload = ReadObject(body, "payload")
|
payload = ReadObject(body, "payload")
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (operation.Equals("Search", StringComparison.OrdinalIgnoreCase))
|
if (operation.Equals("Search", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var query = (ReadString(body, "query") ?? string.Empty).ToLowerInvariant();
|
var query = (ReadString(body, "query") ?? string.Empty).ToLowerInvariant();
|
||||||
var haystack = $"{account.Email} {account.FirstName} {account.LastName} {account.AccountId}".ToLowerInvariant();
|
var haystack = $"{account.Email} {account.FirstName} {account.LastName} {account.AccountId}"
|
||||||
|
.ToLowerInvariant();
|
||||||
|
|
||||||
return ProtocolDispatchResult.Ok(query.Length > 0 && haystack.Contains(query)
|
return ProtocolDispatchResult.Ok(query.Length > 0 && haystack.Contains(query)
|
||||||
?
|
?
|
||||||
@@ -248,7 +217,6 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (operation.Equals("FacebookPrepareLogin", StringComparison.OrdinalIgnoreCase))
|
if (operation.Equals("FacebookPrepareLogin", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
|
||||||
return ProtocolDispatchResult.Ok(new
|
return ProtocolDispatchResult.Ok(new
|
||||||
{
|
{
|
||||||
url = "https://example.com/facebook-login",
|
url = "https://example.com/facebook-login",
|
||||||
@@ -258,12 +226,9 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
|
|||||||
state = $"fb-{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}",
|
state = $"fb-{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}",
|
||||||
redirect_uri = "https://api.jibo.com/facebook/callback"
|
redirect_uri = "https://api.jibo.com/facebook/callback"
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (operation.Equals("ConfirmEmailReset", StringComparison.OrdinalIgnoreCase))
|
if (operation.Equals("ConfirmEmailReset", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
|
||||||
return ProtocolDispatchResult.Ok(new { });
|
return ProtocolDispatchResult.Ok(new { });
|
||||||
}
|
|
||||||
|
|
||||||
return ProtocolDispatchResult.Ok(new
|
return ProtocolDispatchResult.Ok(new
|
||||||
{
|
{
|
||||||
@@ -277,9 +242,7 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
|
|||||||
private ProtocolDispatchResult HandleNotification(string operation, ProtocolEnvelope envelope)
|
private ProtocolDispatchResult HandleNotification(string operation, ProtocolEnvelope envelope)
|
||||||
{
|
{
|
||||||
if (!operation.Equals("NewRobotToken", StringComparison.OrdinalIgnoreCase))
|
if (!operation.Equals("NewRobotToken", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
|
||||||
return ProtocolDispatchResult.Ok(new { ok = true, operation });
|
return ProtocolDispatchResult.Ok(new { ok = true, operation });
|
||||||
}
|
|
||||||
|
|
||||||
var body = envelope.TryParseBody();
|
var body = envelope.TryParseBody();
|
||||||
var deviceId = !string.IsNullOrWhiteSpace(envelope.DeviceId)
|
var deviceId = !string.IsNullOrWhiteSpace(envelope.DeviceId)
|
||||||
@@ -292,22 +255,19 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
|
|||||||
?? ReadString(body, "robotId")
|
?? ReadString(body, "robotId")
|
||||||
?? "unknown-device";
|
?? "unknown-device";
|
||||||
|
|
||||||
stateStore.GetOrCreateDevice(deviceId, envelope.FirmwareVersion, envelope.ApplicationVersion);
|
_stateStore.GetOrCreateDevice(deviceId, envelope.FirmwareVersion, envelope.ApplicationVersion);
|
||||||
|
|
||||||
return ProtocolDispatchResult.Ok(new
|
return ProtocolDispatchResult.Ok(new
|
||||||
{
|
{
|
||||||
token = stateStore.IssueRobotToken(deviceId)
|
token = _stateStore.IssueRobotToken(deviceId)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private ProtocolDispatchResult HandleLoop(string operation)
|
private ProtocolDispatchResult HandleLoop(string operation)
|
||||||
{
|
{
|
||||||
if (operation is not ("List" or "ListLoops"))
|
if (operation is not ("List" or "ListLoops")) return ProtocolDispatchResult.Ok(Array.Empty<object>());
|
||||||
{
|
|
||||||
return ProtocolDispatchResult.Ok(Array.Empty<object>());
|
|
||||||
}
|
|
||||||
|
|
||||||
return ProtocolDispatchResult.Ok(stateStore.GetLoops().Select(loop => new
|
return ProtocolDispatchResult.Ok(_stateStore.GetLoops().Select(loop => new
|
||||||
{
|
{
|
||||||
id = loop.LoopId,
|
id = loop.LoopId,
|
||||||
name = loop.Name,
|
name = loop.Name,
|
||||||
@@ -363,74 +323,78 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
|
|||||||
var body = envelope.TryParseBody();
|
var body = envelope.TryParseBody();
|
||||||
|
|
||||||
if (operation.Equals("List", StringComparison.OrdinalIgnoreCase))
|
if (operation.Equals("List", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
return ProtocolDispatchResult.Ok(_stateStore.ListMedia(
|
||||||
return ProtocolDispatchResult.Ok(stateStore.ListMedia(
|
|
||||||
ReadStringArray(body, "loopIds"),
|
ReadStringArray(body, "loopIds"),
|
||||||
ReadLong(body, "after"),
|
ReadLong(body, "after"),
|
||||||
ReadLong(body, "before")).Select(MapMedia).ToArray());
|
ReadLong(body, "before")).Select(MapMedia).ToArray());
|
||||||
}
|
|
||||||
|
|
||||||
if (operation.Equals("Get", StringComparison.OrdinalIgnoreCase))
|
if (operation.Equals("Get", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
return ProtocolDispatchResult.Ok(_stateStore.GetMedia(ReadStringArray(body, "paths")).Select(MapMedia)
|
||||||
return ProtocolDispatchResult.Ok(stateStore.GetMedia(ReadStringArray(body, "paths")).Select(MapMedia).ToArray());
|
.ToArray());
|
||||||
}
|
|
||||||
|
|
||||||
if (operation.Equals("Remove", StringComparison.OrdinalIgnoreCase))
|
if (operation.Equals("Remove", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
return ProtocolDispatchResult.Ok(_stateStore.RemoveMedia(ReadStringArray(body, "paths")).Select(MapMedia)
|
||||||
return ProtocolDispatchResult.Ok(stateStore.RemoveMedia(ReadStringArray(body, "paths")).Select(MapMedia).ToArray());
|
.ToArray());
|
||||||
}
|
|
||||||
|
|
||||||
if (!operation.Equals("Create", StringComparison.OrdinalIgnoreCase))
|
if (!operation.Equals("Create", StringComparison.OrdinalIgnoreCase))
|
||||||
return ProtocolDispatchResult.Ok(Array.Empty<object>());
|
return ProtocolDispatchResult.Ok(Array.Empty<object>());
|
||||||
|
|
||||||
var loopId = ReadHeader(envelope, "x-loop-id") ?? ReadString(body, "loopId") ?? stateStore.GetLoops()[0].LoopId;
|
var loopId = ReadHeader(envelope, "x-loop-id") ?? ReadString(body, "loopId") ?? _stateStore.GetLoops()[0].LoopId;
|
||||||
var path = ReadHeader(envelope, "x-path") ?? ReadString(body, "path") ?? $"/media/{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}";
|
var path = ReadHeader(envelope, "x-path") ??
|
||||||
|
ReadString(body, "path") ?? $"/media/{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}";
|
||||||
var type = ReadHeader(envelope, "x-type") ?? ReadString(body, "type") ?? "unknown";
|
var type = ReadHeader(envelope, "x-type") ?? ReadString(body, "type") ?? "unknown";
|
||||||
var reference = ReadHeader(envelope, "x-reference") ?? ReadString(body, "reference") ?? string.Empty;
|
var reference = ReadHeader(envelope, "x-reference") ?? ReadString(body, "reference") ?? string.Empty;
|
||||||
var isEncrypted = ReadBooleanHeader(envelope, "x-encrypted") || ReadBool(body, "isEncrypted");
|
var isEncrypted = ReadBooleanHeader(envelope, "x-encrypted") || ReadBool(body, "isEncrypted");
|
||||||
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;
|
||||||
if (!string.IsNullOrWhiteSpace(envelope.BodyText))
|
if (!string.IsNullOrWhiteSpace(envelope.BodyText)) meta["bodyText"] = envelope.BodyText;
|
||||||
{
|
|
||||||
meta["bodyText"] = envelope.BodyText;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ProtocolDispatchResult.Ok(MapMedia(stateStore.CreateMedia(loopId, path, type, reference, isEncrypted, meta)));
|
_mediaContentStore.StoreAsync(path, contentType,
|
||||||
|
string.IsNullOrWhiteSpace(envelope.BodyText) ? [] : System.Text.Encoding.UTF8.GetBytes(envelope.BodyText),
|
||||||
|
meta as IReadOnlyDictionary<string, object?>, CancellationToken.None).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
return ProtocolDispatchResult.Ok(
|
||||||
|
MapMedia(_stateStore.CreateMedia(loopId, path, type, reference, isEncrypted, meta)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private ProtocolDispatchResult HandlePerson(string operation)
|
private ProtocolDispatchResult HandlePerson(string operation)
|
||||||
{
|
{
|
||||||
return ProtocolDispatchResult.Ok(operation.Equals("ListHolidays", StringComparison.OrdinalIgnoreCase)
|
return ProtocolDispatchResult.Ok(operation.Equals("ListHolidays", StringComparison.OrdinalIgnoreCase)
|
||||||
? stateStore.GetHolidays()
|
? _stateStore.GetHolidays()
|
||||||
: []);
|
: []);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ProtocolDispatchResult HandleBackup(string operation)
|
private ProtocolDispatchResult HandleBackup(string operation, ProtocolEnvelope envelope)
|
||||||
{
|
{
|
||||||
return operation.Equals("List", StringComparison.OrdinalIgnoreCase)
|
if (operation.Equals("List", StringComparison.OrdinalIgnoreCase))
|
||||||
? ProtocolDispatchResult.Ok(stateStore.GetBackups())
|
return ProtocolDispatchResult.Ok(_stateStore.GetBackups());
|
||||||
: ProtocolDispatchResult.Ok(Array.Empty<object>());
|
|
||||||
|
if (operation.Equals("Create", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var body = envelope.TryParseBody();
|
||||||
|
var requestedName = ReadString(body, "name") ?? ReadString(body, "backupName");
|
||||||
|
return ProtocolDispatchResult.Ok(_stateStore.CreateBackup(requestedName ?? $"backup-{DateTimeOffset.UtcNow:yyyyMMddHHmmss}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ProtocolDispatchResult.Ok(Array.Empty<object>());
|
||||||
}
|
}
|
||||||
|
|
||||||
private ProtocolDispatchResult HandleKey(string operation, ProtocolEnvelope envelope)
|
private ProtocolDispatchResult HandleKey(string operation, ProtocolEnvelope envelope)
|
||||||
{
|
{
|
||||||
var body = envelope.TryParseBody();
|
var body = envelope.TryParseBody();
|
||||||
var loopId = ReadString(body, "loopId") ?? ReadString(body, "id") ?? stateStore.GetLoops()[0].LoopId;
|
var loopId = ReadString(body, "loopId") ?? ReadString(body, "id") ?? _stateStore.GetLoops()[0].LoopId;
|
||||||
|
|
||||||
if (operation.Equals("ShouldCreate", StringComparison.OrdinalIgnoreCase))
|
if (operation.Equals("ShouldCreate", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
|
||||||
return ProtocolDispatchResult.Ok(new
|
return ProtocolDispatchResult.Ok(new
|
||||||
{
|
{
|
||||||
shouldCreate = stateStore.ShouldCreateSymmetricKey(loopId)
|
shouldCreate = _stateStore.ShouldCreateSymmetricKey(loopId)
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
string? symmetricKey;
|
string? symmetricKey;
|
||||||
if (operation.Equals("CreateSymmetricKey", StringComparison.OrdinalIgnoreCase))
|
if (operation.Equals("CreateSymmetricKey", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
symmetricKey = stateStore.GetOrCreateSymmetricKey(loopId);
|
symmetricKey = _stateStore.GetOrCreateSymmetricKey(loopId);
|
||||||
return ProtocolDispatchResult.Ok(new
|
return ProtocolDispatchResult.Ok(new
|
||||||
{
|
{
|
||||||
loopId,
|
loopId,
|
||||||
@@ -442,7 +406,7 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
|
|||||||
|
|
||||||
if (operation is "CreateRequest" or "RequestSymmetricKey")
|
if (operation is "CreateRequest" or "RequestSymmetricKey")
|
||||||
{
|
{
|
||||||
var record = stateStore.CreateKeyRequest(loopId, ReadString(body, "publicKey") ?? string.Empty);
|
var record = _stateStore.CreateKeyRequest(loopId, ReadString(body, "publicKey") ?? string.Empty);
|
||||||
return ProtocolDispatchResult.Ok(new
|
return ProtocolDispatchResult.Ok(new
|
||||||
{
|
{
|
||||||
id = record.RequestId,
|
id = record.RequestId,
|
||||||
@@ -451,41 +415,33 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (operation.Equals("GetRequest", StringComparison.OrdinalIgnoreCase))
|
if (operation.Equals("GetRequest", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
return ProtocolDispatchResult.Ok(_stateStore.GetKeyRequest(loopId, ReadString(body, "id"),
|
||||||
return ProtocolDispatchResult.Ok(stateStore.GetKeyRequest(loopId, ReadString(body, "id"), ReadString(body, "publicKey")));
|
ReadString(body, "publicKey")));
|
||||||
}
|
|
||||||
|
|
||||||
if (operation.Equals("ListIncomingRequests", StringComparison.OrdinalIgnoreCase))
|
if (operation.Equals("ListIncomingRequests", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
return ProtocolDispatchResult.Ok(_stateStore.GetIncomingKeyRequests());
|
||||||
return ProtocolDispatchResult.Ok(stateStore.GetIncomingKeyRequests());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (operation.Equals("ListBinaryRequests", StringComparison.OrdinalIgnoreCase))
|
if (operation.Equals("ListBinaryRequests", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
return ProtocolDispatchResult.Ok(_stateStore.GetBinaryRequests());
|
||||||
return ProtocolDispatchResult.Ok(stateStore.GetBinaryRequests());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (operation is "Share" or "ShareSymmetricKey" or "ShareBinary")
|
if (operation is "Share" or "ShareSymmetricKey" or "ShareBinary")
|
||||||
{
|
|
||||||
return ProtocolDispatchResult.Ok(new { ok = true });
|
return ProtocolDispatchResult.Ok(new { ok = true });
|
||||||
}
|
|
||||||
|
|
||||||
if (!operation.Equals("LoadSymmetricKey", StringComparison.OrdinalIgnoreCase))
|
if (!operation.Equals("LoadSymmetricKey", StringComparison.OrdinalIgnoreCase))
|
||||||
return ProtocolDispatchResult.Ok(new { ok = true, operation });
|
return ProtocolDispatchResult.Ok(new { ok = true, operation });
|
||||||
|
|
||||||
symmetricKey = stateStore.GetOrCreateSymmetricKey(loopId);
|
symmetricKey = _stateStore.GetOrCreateSymmetricKey(loopId);
|
||||||
return ProtocolDispatchResult.Ok(new
|
return ProtocolDispatchResult.Ok(new
|
||||||
{
|
{
|
||||||
loopId,
|
loopId,
|
||||||
key = symmetricKey,
|
key = symmetricKey,
|
||||||
symmetricKey
|
symmetricKey
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private ProtocolDispatchResult HandleRobot(string operation, ProtocolEnvelope envelope)
|
private ProtocolDispatchResult HandleRobot(string operation, ProtocolEnvelope envelope)
|
||||||
{
|
{
|
||||||
var robot = stateStore.GetRobot();
|
var robot = _stateStore.GetRobot();
|
||||||
|
|
||||||
if (operation.Equals("UpdateRobot", StringComparison.OrdinalIgnoreCase))
|
if (operation.Equals("UpdateRobot", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
@@ -499,7 +455,7 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
|
|||||||
HostMappings = robot.HostMappings
|
HostMappings = robot.HostMappings
|
||||||
};
|
};
|
||||||
|
|
||||||
stateStore.UpdateRobot(updated);
|
_stateStore.UpdateRobot(updated);
|
||||||
return ProtocolDispatchResult.Ok(new
|
return ProtocolDispatchResult.Ok(new
|
||||||
{
|
{
|
||||||
result = "ok"
|
result = "ok"
|
||||||
@@ -512,7 +468,7 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
|
|||||||
result = "ok"
|
result = "ok"
|
||||||
});
|
});
|
||||||
|
|
||||||
var profile = stateStore.GetRobotProfile();
|
var profile = _stateStore.GetRobotProfile();
|
||||||
return ProtocolDispatchResult.Ok(new
|
return ProtocolDispatchResult.Ok(new
|
||||||
{
|
{
|
||||||
id = ReadString(envelope.TryParseBody(), "id") ?? profile.RobotId,
|
id = ReadString(envelope.TryParseBody(), "id") ?? profile.RobotId,
|
||||||
@@ -521,7 +477,6 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
|
|||||||
updated = profile.UpdatedUtc.ToUnixTimeMilliseconds(),
|
updated = profile.UpdatedUtc.ToUnixTimeMilliseconds(),
|
||||||
created = profile.CreatedUtc.ToUnixTimeMilliseconds()
|
created = profile.CreatedUtc.ToUnixTimeMilliseconds()
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private ProtocolDispatchResult HandleUpdate(string operation, ProtocolEnvelope envelope)
|
private ProtocolDispatchResult HandleUpdate(string operation, ProtocolEnvelope envelope)
|
||||||
@@ -533,13 +488,15 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
|
|||||||
|
|
||||||
return operation switch
|
return operation switch
|
||||||
{
|
{
|
||||||
"ListUpdates" => ProtocolDispatchResult.Ok(stateStore.ListUpdates(subsystem, filter).Select(MapUpdate).ToArray()),
|
"ListUpdates" => ProtocolDispatchResult.Ok(_stateStore.ListUpdates(subsystem, filter).Select(MapUpdate)
|
||||||
"ListUpdatesFrom" => ProtocolDispatchResult.Ok(stateStore.ListUpdates(subsystem, filter)
|
.ToArray()),
|
||||||
.Where(update => fromVersion is null || update.FromVersion.Equals(fromVersion, StringComparison.OrdinalIgnoreCase))
|
"ListUpdatesFrom" => ProtocolDispatchResult.Ok(_stateStore.ListUpdates(subsystem, filter)
|
||||||
|
.Where(update =>
|
||||||
|
fromVersion is null || update.FromVersion.Equals(fromVersion, StringComparison.OrdinalIgnoreCase))
|
||||||
.Select(MapUpdate)
|
.Select(MapUpdate)
|
||||||
.ToArray()),
|
.ToArray()),
|
||||||
"GetUpdateFrom" => HandleGetUpdateFrom(subsystem, fromVersion, filter),
|
"GetUpdateFrom" => HandleGetUpdateFrom(subsystem, fromVersion, filter),
|
||||||
"CreateUpdate" => ProtocolDispatchResult.Ok(MapUpdate(stateStore.CreateUpdate(
|
"CreateUpdate" => ProtocolDispatchResult.Ok(MapUpdate(_stateStore.CreateUpdate(
|
||||||
fromVersion,
|
fromVersion,
|
||||||
ReadString(body, "toVersion"),
|
ReadString(body, "toVersion"),
|
||||||
ReadString(body, "changes"),
|
ReadString(body, "changes"),
|
||||||
@@ -548,7 +505,7 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
|
|||||||
subsystem,
|
subsystem,
|
||||||
filter,
|
filter,
|
||||||
ReadObject(body, "dependencies")))),
|
ReadObject(body, "dependencies")))),
|
||||||
"RemoveUpdate" => ProtocolDispatchResult.Ok(MapUpdate(stateStore.RemoveUpdate(ReadString(body, "id")))),
|
"RemoveUpdate" => ProtocolDispatchResult.Ok(MapUpdate(_stateStore.RemoveUpdate(ReadString(body, "id")))),
|
||||||
_ => ProtocolDispatchResult.Ok(Array.Empty<object>())
|
_ => ProtocolDispatchResult.Ok(Array.Empty<object>())
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -557,20 +514,35 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
|
|||||||
{
|
{
|
||||||
var path = Uri.UnescapeDataString(envelope.Path["/media/".Length..]);
|
var path = Uri.UnescapeDataString(envelope.Path["/media/".Length..]);
|
||||||
var candidatePaths = new[] { path, $"/{path}" };
|
var candidatePaths = new[] { path, $"/{path}" };
|
||||||
var media = stateStore.GetMedia(candidatePaths).FirstOrDefault();
|
var media = _stateStore.GetMedia(candidatePaths).FirstOrDefault();
|
||||||
if (media is null || media.IsDeleted)
|
if (media is null || media.IsDeleted) return ProtocolDispatchResult.Raw(404, string.Empty);
|
||||||
{
|
|
||||||
return ProtocolDispatchResult.Raw(404, string.Empty);
|
var storedContent = _mediaContentStore.LoadAsync(media.Path, CancellationToken.None).GetAwaiter().GetResult();
|
||||||
|
var contentType = storedContent?.ContentType ?? TryReadMetaString(media.Meta, "contentType") ??
|
||||||
|
"application/octet-stream";
|
||||||
|
var bodyText = storedContent is not null
|
||||||
|
? System.Text.Encoding.UTF8.GetString(storedContent.Content)
|
||||||
|
: TryReadMetaString(media.Meta, "bodyText") ?? string.Empty;
|
||||||
|
return ProtocolDispatchResult.Raw(200, bodyText, contentType);
|
||||||
}
|
}
|
||||||
|
|
||||||
var contentType = TryReadMetaString(media.Meta, "contentType") ?? "application/octet-stream";
|
private sealed class NullMediaContentStore : IMediaContentStore
|
||||||
var bodyText = TryReadMetaString(media.Meta, "bodyText") ?? string.Empty;
|
{
|
||||||
return ProtocolDispatchResult.Raw(200, bodyText, contentType);
|
public Task StoreAsync(string path, string contentType, byte[] content,
|
||||||
|
IReadOnlyDictionary<string, object?>? meta, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<MediaContentSnapshot?> LoadAsync(string path, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return Task.FromResult<MediaContentSnapshot?>(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ProtocolDispatchResult HandleGetUpdateFrom(string? subsystem, string? fromVersion, string? filter)
|
private ProtocolDispatchResult HandleGetUpdateFrom(string? subsystem, string? fromVersion, string? filter)
|
||||||
{
|
{
|
||||||
var update = stateStore.GetUpdateFrom(subsystem, fromVersion, filter);
|
var update = _stateStore.GetUpdateFrom(subsystem, fromVersion, filter);
|
||||||
return update is null
|
return update is null
|
||||||
? ProtocolDispatchResult.Ok(new { })
|
? ProtocolDispatchResult.Ok(new { })
|
||||||
: ProtocolDispatchResult.Ok(MapUpdate(update));
|
: ProtocolDispatchResult.Ok(MapUpdate(update));
|
||||||
@@ -623,10 +595,7 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
|
|||||||
|
|
||||||
private static string? ReadString(JsonElement? element, string propertyName)
|
private static string? ReadString(JsonElement? element, string propertyName)
|
||||||
{
|
{
|
||||||
if (element is null || !element.Value.TryGetProperty(propertyName, out var property))
|
if (element is null || !element.Value.TryGetProperty(propertyName, out var property)) return null;
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return property.ValueKind == JsonValueKind.String
|
return property.ValueKind == JsonValueKind.String
|
||||||
? property.GetString()
|
? property.GetString()
|
||||||
@@ -635,25 +604,16 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
|
|||||||
|
|
||||||
private static long? ReadLong(JsonElement? element, string propertyName)
|
private static long? ReadLong(JsonElement? element, string propertyName)
|
||||||
{
|
{
|
||||||
if (element is null || !element.Value.TryGetProperty(propertyName, out var property))
|
if (element is null || !element.Value.TryGetProperty(propertyName, out var property)) return null;
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (property.ValueKind == JsonValueKind.Number && property.TryGetInt64(out var number))
|
if (property.ValueKind == JsonValueKind.Number && property.TryGetInt64(out var number)) return number;
|
||||||
{
|
|
||||||
return number;
|
|
||||||
}
|
|
||||||
|
|
||||||
return long.TryParse(property.ToString(), out var parsed) ? parsed : null;
|
return long.TryParse(property.ToString(), out var parsed) ? parsed : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool ReadBool(JsonElement? element, string propertyName)
|
private static bool ReadBool(JsonElement? element, string propertyName)
|
||||||
{
|
{
|
||||||
if (element is null || !element.Value.TryGetProperty(propertyName, out var property))
|
if (element is null || !element.Value.TryGetProperty(propertyName, out var property)) return false;
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return property.ValueKind switch
|
return property.ValueKind switch
|
||||||
{
|
{
|
||||||
@@ -665,31 +625,26 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
|
|||||||
|
|
||||||
private static IReadOnlyList<string> ReadStringArray(JsonElement? element, string propertyName)
|
private static IReadOnlyList<string> ReadStringArray(JsonElement? element, string propertyName)
|
||||||
{
|
{
|
||||||
if (element is null || !element.Value.TryGetProperty(propertyName, out var property) || property.ValueKind != JsonValueKind.Array)
|
if (element is null || !element.Value.TryGetProperty(propertyName, out var property) ||
|
||||||
{
|
property.ValueKind != JsonValueKind.Array) return [];
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [.. property.EnumerateArray()
|
return
|
||||||
.Select(item => item.ValueKind == JsonValueKind.String ? item.GetString() ?? string.Empty : item.ToString())
|
[
|
||||||
.Where(item => !string.IsNullOrWhiteSpace(item))];
|
.. property.EnumerateArray()
|
||||||
|
.Select(item =>
|
||||||
|
item.ValueKind == JsonValueKind.String ? item.GetString() ?? string.Empty : item.ToString())
|
||||||
|
.Where(item => !string.IsNullOrWhiteSpace(item))
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IDictionary<string, object?>? ReadObject(JsonElement? element, string propertyName)
|
private static IDictionary<string, object?>? ReadObject(JsonElement? element, string propertyName)
|
||||||
{
|
{
|
||||||
if (element is null || !element.Value.TryGetProperty(propertyName, out var property))
|
if (element is null || !element.Value.TryGetProperty(propertyName, out var property)) return null;
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (property.ValueKind != JsonValueKind.Object)
|
if (property.ValueKind != JsonValueKind.Object) return null;
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
|
var result = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
|
||||||
foreach (var child in property.EnumerateObject())
|
foreach (var child in property.EnumerateObject())
|
||||||
{
|
|
||||||
result[child.Name] = child.Value.ValueKind switch
|
result[child.Name] = child.Value.ValueKind switch
|
||||||
{
|
{
|
||||||
JsonValueKind.String => child.Value.GetString(),
|
JsonValueKind.String => child.Value.GetString(),
|
||||||
@@ -699,7 +654,6 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
|
|||||||
JsonValueKind.False => false,
|
JsonValueKind.False => false,
|
||||||
_ => child.Value.ToString()
|
_ => child.Value.ToString()
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,7 @@ public sealed class JiboExperienceContentCache(IJiboExperienceContentRepository
|
|||||||
|
|
||||||
public async Task<JiboExperienceCatalog> GetCatalogAsync(CancellationToken cancellationToken = default)
|
public async Task<JiboExperienceCatalog> GetCatalogAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (_catalog is not null)
|
if (_catalog is not null) return _catalog;
|
||||||
{
|
|
||||||
return _catalog;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _gate.WaitAsync(cancellationToken);
|
await _gate.WaitAsync(cancellationToken);
|
||||||
try
|
try
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,8 @@ public sealed class JiboWebSocketService(
|
|||||||
stateStore.OpenSession(envelope.Kind, null, envelope.Token, envelope.HostName, envelope.Path);
|
stateStore.OpenSession(envelope.Kind, null, envelope.Token, envelope.HostName, envelope.Path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IReadOnlyList<WebSocketReply>> HandleMessageAsync(WebSocketMessageEnvelope envelope, CancellationToken cancellationToken = default)
|
public async Task<IReadOnlyList<WebSocketReply>> HandleMessageAsync(WebSocketMessageEnvelope envelope,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var session = GetOrCreateSession(envelope);
|
var session = GetOrCreateSession(envelope);
|
||||||
session.LastSeenUtc = DateTimeOffset.UtcNow;
|
session.LastSeenUtc = DateTimeOffset.UtcNow;
|
||||||
@@ -23,9 +24,11 @@ public sealed class JiboWebSocketService(
|
|||||||
if (envelope.IsBinary)
|
if (envelope.IsBinary)
|
||||||
{
|
{
|
||||||
var replies = await turnFinalizationService.HandleBinaryAudioAsync(session, envelope, cancellationToken);
|
var replies = await turnFinalizationService.HandleBinaryAudioAsync(session, envelope, cancellationToken);
|
||||||
await telemetrySink.RecordTurnEventAsync(envelope, session, "binary_audio_received", new Dictionary<string, object?>
|
await telemetrySink.RecordTurnEventAsync(envelope, session, "binary_audio_received",
|
||||||
|
new Dictionary<string, object?>
|
||||||
{
|
{
|
||||||
["bytes"] = envelope.Binary?.Length ?? 0
|
["bytes"] = envelope.Binary?.Length ?? 0,
|
||||||
|
["glsmPhase"] = WebSocketTurnFinalizationService.ResolveGlsmPhase(session)
|
||||||
}, cancellationToken);
|
}, cancellationToken);
|
||||||
return replies;
|
return replies;
|
||||||
}
|
}
|
||||||
@@ -33,6 +36,8 @@ public sealed class JiboWebSocketService(
|
|||||||
var parsedType = ReadMessageType(envelope.Text);
|
var parsedType = ReadMessageType(envelope.Text);
|
||||||
session.LastMessageType = parsedType;
|
session.LastMessageType = parsedType;
|
||||||
var containsInlineTurnPayload = parsedType == "LISTEN" && ContainsInlineTurnPayload(envelope.Text);
|
var containsInlineTurnPayload = parsedType == "LISTEN" && ContainsInlineTurnPayload(envelope.Text);
|
||||||
|
var staleListenRecovered = false;
|
||||||
|
var staleListenAgeMs = 0;
|
||||||
if (parsedType == "LISTEN" &&
|
if (parsedType == "LISTEN" &&
|
||||||
!containsInlineTurnPayload &&
|
!containsInlineTurnPayload &&
|
||||||
WebSocketTurnFinalizationService.ShouldIgnoreLateListenSetup(session, envelope.Text))
|
WebSocketTurnFinalizationService.ShouldIgnoreLateListenSetup(session, envelope.Text))
|
||||||
@@ -47,7 +52,8 @@ public sealed class JiboWebSocketService(
|
|||||||
})
|
})
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
await telemetrySink.RecordTurnEventAsync(envelope, session, "late_listen_ignored", new Dictionary<string, object?>
|
await telemetrySink.RecordTurnEventAsync(envelope, session, "late_listen_ignored",
|
||||||
|
new Dictionary<string, object?>
|
||||||
{
|
{
|
||||||
["messageType"] = parsedType,
|
["messageType"] = parsedType,
|
||||||
["activeTransID"] = session.TurnState.TransId,
|
["activeTransID"] = session.TurnState.TransId,
|
||||||
@@ -57,6 +63,20 @@ public sealed class JiboWebSocketService(
|
|||||||
return replies;
|
return replies;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (parsedType == "LISTEN" &&
|
||||||
|
!containsInlineTurnPayload &&
|
||||||
|
WebSocketTurnFinalizationService.TryRecoverStalePendingListen(session, out staleListenAgeMs))
|
||||||
|
{
|
||||||
|
staleListenRecovered = true;
|
||||||
|
await telemetrySink.RecordTurnEventAsync(envelope, session, "glsm_stale_listen_recovered",
|
||||||
|
new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["staleAgeMs"] = staleListenAgeMs,
|
||||||
|
["transID"] = session.TurnState.TransId,
|
||||||
|
["glsmPhase"] = WebSocketTurnFinalizationService.ResolveGlsmPhase(session)
|
||||||
|
}, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
WebSocketTurnFinalizationService.ObserveIncomingMessage(session, envelope.Text);
|
WebSocketTurnFinalizationService.ObserveIncomingMessage(session, envelope.Text);
|
||||||
|
|
||||||
switch (parsedType)
|
switch (parsedType)
|
||||||
@@ -64,9 +84,11 @@ public sealed class JiboWebSocketService(
|
|||||||
case "CONTEXT":
|
case "CONTEXT":
|
||||||
{
|
{
|
||||||
var replies = await turnFinalizationService.HandleContextAsync(session, envelope, cancellationToken);
|
var replies = await turnFinalizationService.HandleContextAsync(session, envelope, cancellationToken);
|
||||||
await telemetrySink.RecordTurnEventAsync(envelope, session, "context_received", new Dictionary<string, object?>
|
await telemetrySink.RecordTurnEventAsync(envelope, session, "context_received",
|
||||||
|
new Dictionary<string, object?>
|
||||||
{
|
{
|
||||||
["transID"] = session.TurnState.TransId
|
["transID"] = session.TurnState.TransId,
|
||||||
|
["glsmPhase"] = WebSocketTurnFinalizationService.ResolveGlsmPhase(session)
|
||||||
}, cancellationToken);
|
}, cancellationToken);
|
||||||
return replies;
|
return replies;
|
||||||
}
|
}
|
||||||
@@ -75,24 +97,31 @@ public sealed class JiboWebSocketService(
|
|||||||
var replies = containsInlineTurnPayload
|
var replies = containsInlineTurnPayload
|
||||||
? await turnFinalizationService.HandleTurnAsync(session, envelope, parsedType, cancellationToken)
|
? await turnFinalizationService.HandleTurnAsync(session, envelope, parsedType, cancellationToken)
|
||||||
: WebSocketTurnFinalizationService.HandleListenSetup(session, envelope);
|
: WebSocketTurnFinalizationService.HandleListenSetup(session, envelope);
|
||||||
await telemetrySink.RecordTurnEventAsync(envelope, session, "turn_processed", new Dictionary<string, object?>
|
await telemetrySink.RecordTurnEventAsync(envelope, session, "turn_processed",
|
||||||
|
new Dictionary<string, object?>
|
||||||
{
|
{
|
||||||
["messageType"] = parsedType,
|
["messageType"] = parsedType,
|
||||||
["replyCount"] = replies.Count,
|
["replyCount"] = replies.Count,
|
||||||
["transcript"] = session.LastTranscript,
|
["transcript"] = session.LastTranscript,
|
||||||
["intent"] = session.LastIntent
|
["intent"] = session.LastIntent,
|
||||||
|
["glsmPhase"] = WebSocketTurnFinalizationService.ResolveGlsmPhase(session),
|
||||||
|
["staleListenRecovered"] = staleListenRecovered,
|
||||||
|
["staleListenAgeMs"] = staleListenAgeMs
|
||||||
}, cancellationToken);
|
}, cancellationToken);
|
||||||
return replies;
|
return replies;
|
||||||
}
|
}
|
||||||
case "CLIENT_NLU" or "CLIENT_ASR":
|
case "CLIENT_NLU" or "CLIENT_ASR" or "TRIGGER":
|
||||||
{
|
{
|
||||||
var replies = await turnFinalizationService.HandleTurnAsync(session, envelope, parsedType, cancellationToken);
|
var replies =
|
||||||
await telemetrySink.RecordTurnEventAsync(envelope, session, "turn_processed", new Dictionary<string, object?>
|
await turnFinalizationService.HandleTurnAsync(session, envelope, parsedType, cancellationToken);
|
||||||
|
await telemetrySink.RecordTurnEventAsync(envelope, session, "turn_processed",
|
||||||
|
new Dictionary<string, object?>
|
||||||
{
|
{
|
||||||
["messageType"] = parsedType,
|
["messageType"] = parsedType,
|
||||||
["replyCount"] = replies.Count,
|
["replyCount"] = replies.Count,
|
||||||
["transcript"] = session.LastTranscript,
|
["transcript"] = session.LastTranscript,
|
||||||
["intent"] = session.LastIntent
|
["intent"] = session.LastIntent,
|
||||||
|
["glsmPhase"] = WebSocketTurnFinalizationService.ResolveGlsmPhase(session)
|
||||||
}, cancellationToken);
|
}, cancellationToken);
|
||||||
return replies;
|
return replies;
|
||||||
}
|
}
|
||||||
@@ -103,19 +132,14 @@ public sealed class JiboWebSocketService(
|
|||||||
|
|
||||||
private static string ReadMessageType(string? text)
|
private static string ReadMessageType(string? text)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(text))
|
if (string.IsNullOrWhiteSpace(text)) return "UNKNOWN";
|
||||||
{
|
|
||||||
return "UNKNOWN";
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var document = JsonDocument.Parse(text);
|
using var document = JsonDocument.Parse(text);
|
||||||
if (document.RootElement.TryGetProperty("type", out var type) && type.ValueKind == JsonValueKind.String)
|
if (document.RootElement.TryGetProperty("type", out var type) && type.ValueKind == JsonValueKind.String)
|
||||||
{
|
|
||||||
return type.GetString() ?? "UNKNOWN";
|
return type.GetString() ?? "UNKNOWN";
|
||||||
}
|
}
|
||||||
}
|
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
return "TEXT";
|
return "TEXT";
|
||||||
@@ -126,25 +150,18 @@ public sealed class JiboWebSocketService(
|
|||||||
|
|
||||||
private static bool ContainsInlineTurnPayload(string? text)
|
private static bool ContainsInlineTurnPayload(string? text)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(text))
|
if (string.IsNullOrWhiteSpace(text)) return false;
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var document = JsonDocument.Parse(text);
|
using var document = JsonDocument.Parse(text);
|
||||||
if (!document.RootElement.TryGetProperty("data", out var data) || data.ValueKind != JsonValueKind.Object)
|
if (!document.RootElement.TryGetProperty("data", out var data) ||
|
||||||
{
|
data.ValueKind != JsonValueKind.Object) return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.TryGetProperty("text", out var transcript) &&
|
if (data.TryGetProperty("text", out var transcript) &&
|
||||||
transcript.ValueKind == JsonValueKind.String &&
|
transcript.ValueKind == JsonValueKind.String &&
|
||||||
!string.IsNullOrWhiteSpace(transcript.GetString()))
|
!string.IsNullOrWhiteSpace(transcript.GetString()))
|
||||||
{
|
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
|
|
||||||
return data.TryGetProperty("asr", out var asr) &&
|
return data.TryGetProperty("asr", out var asr) &&
|
||||||
asr.ValueKind == JsonValueKind.Object &&
|
asr.ValueKind == JsonValueKind.Object &&
|
||||||
@@ -165,10 +182,7 @@ public sealed class JiboWebSocketService(
|
|||||||
var transId = session.TurnState.TransId ?? session.LastTransId ?? string.Empty;
|
var transId = session.TurnState.TransId ?? session.LastTransId ?? string.Empty;
|
||||||
var rules = session.TurnState.ListenRules;
|
var rules = session.TurnState.ListenRules;
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(text))
|
if (string.IsNullOrWhiteSpace(text)) return (transId, rules);
|
||||||
{
|
|
||||||
return (transId, rules);
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -178,9 +192,7 @@ public sealed class JiboWebSocketService(
|
|||||||
if (root.TryGetProperty("transID", out var transIdValue) &&
|
if (root.TryGetProperty("transID", out var transIdValue) &&
|
||||||
transIdValue.ValueKind == JsonValueKind.String &&
|
transIdValue.ValueKind == JsonValueKind.String &&
|
||||||
!string.IsNullOrWhiteSpace(transIdValue.GetString()))
|
!string.IsNullOrWhiteSpace(transIdValue.GetString()))
|
||||||
{
|
|
||||||
transId = transIdValue.GetString()!;
|
transId = transIdValue.GetString()!;
|
||||||
}
|
|
||||||
|
|
||||||
if (root.TryGetProperty("data", out var data) &&
|
if (root.TryGetProperty("data", out var data) &&
|
||||||
data.ValueKind == JsonValueKind.Object &&
|
data.ValueKind == JsonValueKind.Object &&
|
||||||
@@ -193,10 +205,7 @@ public sealed class JiboWebSocketService(
|
|||||||
.Where(static rule => !string.IsNullOrWhiteSpace(rule))
|
.Where(static rule => !string.IsNullOrWhiteSpace(rule))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
if (parsedRules.Length > 0)
|
if (parsedRules.Length > 0) rules = parsedRules;
|
||||||
{
|
|
||||||
rules = parsedRules;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
|
|||||||
@@ -5,5 +5,9 @@ namespace Jibo.Cloud.Application.Services;
|
|||||||
|
|
||||||
public sealed class NullProtocolTelemetrySink : IProtocolTelemetrySink
|
public sealed class NullProtocolTelemetrySink : IProtocolTelemetrySink
|
||||||
{
|
{
|
||||||
public Task RecordAsync(ProtocolEnvelope envelope, ProtocolDispatchResult result, CancellationToken cancellationToken = default) => Task.CompletedTask;
|
public Task RecordAsync(ProtocolEnvelope envelope, ProtocolDispatchResult result,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,14 @@ namespace Jibo.Cloud.Application.Services;
|
|||||||
|
|
||||||
public sealed class NullTurnTelemetrySink : ITurnTelemetrySink
|
public sealed class NullTurnTelemetrySink : ITurnTelemetrySink
|
||||||
{
|
{
|
||||||
public Task RecordTurnDiagnosticAsync(string category, IReadOnlyDictionary<string, object?> details, CancellationToken cancellationToken = default) => Task.CompletedTask;
|
public Task RecordTurnDiagnosticAsync(string category, IReadOnlyDictionary<string, object?> details,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
public Task RecordTranscriptError(Exception ex, string message, CancellationToken cancellationToken = default) => Task.CompletedTask;
|
public Task RecordTranscriptError(Exception ex, string message, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,9 +5,33 @@ namespace Jibo.Cloud.Application.Services;
|
|||||||
|
|
||||||
public sealed class NullWebSocketTelemetrySink : IWebSocketTelemetrySink
|
public sealed class NullWebSocketTelemetrySink : IWebSocketTelemetrySink
|
||||||
{
|
{
|
||||||
public Task RecordConnectionOpenedAsync(WebSocketMessageEnvelope envelope, CloudSession session, CancellationToken cancellationToken = default) => Task.CompletedTask;
|
public Task RecordConnectionOpenedAsync(WebSocketMessageEnvelope envelope, CloudSession session,
|
||||||
public Task RecordInboundAsync(WebSocketMessageEnvelope envelope, CloudSession session, string? messageType, CancellationToken cancellationToken = default) => Task.CompletedTask;
|
CancellationToken cancellationToken = default)
|
||||||
public Task RecordTurnEventAsync(WebSocketMessageEnvelope envelope, CloudSession session, string eventType, IReadOnlyDictionary<string, object?> details, CancellationToken cancellationToken = default) => Task.CompletedTask;
|
{
|
||||||
public Task RecordOutboundAsync(WebSocketMessageEnvelope envelope, CloudSession session, IReadOnlyList<WebSocketReply> replies, CancellationToken cancellationToken = default) => Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
public Task RecordConnectionClosedAsync(WebSocketMessageEnvelope envelope, CloudSession session, string reason, CancellationToken cancellationToken = default) => Task.CompletedTask;
|
}
|
||||||
|
|
||||||
|
public Task RecordInboundAsync(WebSocketMessageEnvelope envelope, CloudSession session, string? messageType,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task RecordTurnEventAsync(WebSocketMessageEnvelope envelope, CloudSession session, string eventType,
|
||||||
|
IReadOnlyDictionary<string, object?> details, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task RecordOutboundAsync(WebSocketMessageEnvelope envelope, CloudSession session,
|
||||||
|
IReadOnlyList<WebSocketReply> replies, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task RecordConnectionClosedAsync(WebSocketMessageEnvelope envelope, CloudSession session, string reason,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -12,5 +12,6 @@ public static class OpenJiboCloudBuildInfo
|
|||||||
|
|
||||||
public static string SpokenVersion => $"Cloud version {VersionWords}.";
|
public static string SpokenVersion => $"Cloud version {VersionWords}.";
|
||||||
|
|
||||||
public static string EsmlVersion => $"Cloud version<break time='10ms'/> {VersionWords.Replace(" ", "<break time='10ms' />")}.";
|
public static string EsmlVersion =>
|
||||||
|
$"Cloud version<break time='10ms'/> {VersionWords.Replace(" ", "<break time='10ms' />")}.";
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using Jibo.Cloud.Application.Abstractions;
|
|
||||||
using Jibo.Runtime.Abstractions;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using Jibo.Cloud.Application.Abstractions;
|
||||||
|
using Jibo.Runtime.Abstractions;
|
||||||
|
|
||||||
namespace Jibo.Cloud.Application.Services;
|
namespace Jibo.Cloud.Application.Services;
|
||||||
|
|
||||||
@@ -41,6 +41,7 @@ internal static class PersonalReportOrchestrator
|
|||||||
"yeah",
|
"yeah",
|
||||||
"yep",
|
"yep",
|
||||||
"yup",
|
"yup",
|
||||||
|
"uh huh",
|
||||||
"sure",
|
"sure",
|
||||||
"ok",
|
"ok",
|
||||||
"okay",
|
"okay",
|
||||||
@@ -58,6 +59,8 @@ internal static class PersonalReportOrchestrator
|
|||||||
"maybe later"
|
"maybe later"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private static readonly Regex NameNoiseRegex = new("[^a-zA-Z\\-\\s']", RegexOptions.Compiled);
|
||||||
|
|
||||||
public static async Task<JiboInteractionDecision?> TryBuildDecisionAsync(
|
public static async Task<JiboInteractionDecision?> TryBuildDecisionAsync(
|
||||||
TurnContext turn,
|
TurnContext turn,
|
||||||
string semanticIntent,
|
string semanticIntent,
|
||||||
@@ -72,31 +75,26 @@ internal static class PersonalReportOrchestrator
|
|||||||
{
|
{
|
||||||
var state = ReadState(turn);
|
var state = ReadState(turn);
|
||||||
var isActiveState = !string.Equals(state, IdleState, StringComparison.OrdinalIgnoreCase);
|
var isActiveState = !string.Equals(state, IdleState, StringComparison.OrdinalIgnoreCase);
|
||||||
if (!isActiveState && !string.Equals(semanticIntent, "personal_report", StringComparison.OrdinalIgnoreCase))
|
if (!isActiveState &&
|
||||||
{
|
!string.Equals(semanticIntent, "personal_report", StringComparison.OrdinalIgnoreCase)) return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var toggles = ApplyInlineToggleHints(
|
var toggles = ApplyInlineToggleHints(
|
||||||
ReadServiceToggles(turn),
|
ReadServiceToggles(turn),
|
||||||
loweredTranscript,
|
loweredTranscript,
|
||||||
out var inlineToggleSummary);
|
out var inlineToggleSummary);
|
||||||
|
|
||||||
if (ContainsAnyPhrase(loweredTranscript, CancelPhrases))
|
if (ContainsAnyPhrase(loweredTranscript, CancelPhrases)) return BuildCancelledDecision(toggles);
|
||||||
{
|
|
||||||
return BuildCancelledDecision(toggles);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isActiveState)
|
if (!isActiveState)
|
||||||
{
|
{
|
||||||
var contextUpdates = BuildContextUpdates(
|
var contextUpdates = BuildContextUpdates(
|
||||||
AwaitingOptInState,
|
AwaitingOptInState,
|
||||||
noMatchCount: 0,
|
0,
|
||||||
noInputCount: 0,
|
0,
|
||||||
toggles,
|
toggles,
|
||||||
userName: ReadString(turn, UserNameMetadataKey),
|
ReadString(turn, UserNameMetadataKey),
|
||||||
userVerified: ReadBool(turn, UserVerifiedMetadataKey) ?? false,
|
ReadBool(turn, UserVerifiedMetadataKey) ?? false,
|
||||||
lastServiceError: string.Empty);
|
string.Empty);
|
||||||
|
|
||||||
var reply = string.IsNullOrWhiteSpace(inlineToggleSummary)
|
var reply = string.IsNullOrWhiteSpace(inlineToggleSummary)
|
||||||
? "Would you like your personal report now?"
|
? "Would you like your personal report now?"
|
||||||
@@ -105,13 +103,11 @@ internal static class PersonalReportOrchestrator
|
|||||||
return new JiboInteractionDecision(
|
return new JiboInteractionDecision(
|
||||||
"personal_report_opt_in",
|
"personal_report_opt_in",
|
||||||
reply,
|
reply,
|
||||||
|
SkillPayload: BuildYesNoPromptPayload(),
|
||||||
ContextUpdates: contextUpdates);
|
ContextUpdates: contextUpdates);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(loweredTranscript))
|
if (string.IsNullOrWhiteSpace(loweredTranscript)) return BuildNoInputDecision(turn, state, toggles);
|
||||||
{
|
|
||||||
return BuildNoInputDecision(turn, state, toggles);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (state)
|
switch (state)
|
||||||
{
|
{
|
||||||
@@ -121,81 +117,73 @@ internal static class PersonalReportOrchestrator
|
|||||||
var scope = tenantScopeResolver(turn);
|
var scope = tenantScopeResolver(turn);
|
||||||
var knownName = ReadString(turn, UserNameMetadataKey) ?? personalMemoryStore.GetName(scope);
|
var knownName = ReadString(turn, UserNameMetadataKey) ?? personalMemoryStore.GetName(scope);
|
||||||
if (!string.IsNullOrWhiteSpace(knownName))
|
if (!string.IsNullOrWhiteSpace(knownName))
|
||||||
{
|
|
||||||
return new JiboInteractionDecision(
|
return new JiboInteractionDecision(
|
||||||
"personal_report_verify_user",
|
"personal_report_verify_user",
|
||||||
$"I think this is {knownName}. Is that right?",
|
$"I think this is {knownName}. Is that right?",
|
||||||
|
SkillPayload: BuildYesNoPromptPayload(),
|
||||||
ContextUpdates: BuildContextUpdates(
|
ContextUpdates: BuildContextUpdates(
|
||||||
AwaitingIdentityConfirmationState,
|
AwaitingIdentityConfirmationState,
|
||||||
noMatchCount: 0,
|
0,
|
||||||
noInputCount: 0,
|
0,
|
||||||
toggles,
|
toggles,
|
||||||
userName: knownName,
|
knownName,
|
||||||
userVerified: false,
|
false,
|
||||||
lastServiceError: string.Empty));
|
string.Empty));
|
||||||
}
|
|
||||||
|
|
||||||
return new JiboInteractionDecision(
|
return new JiboInteractionDecision(
|
||||||
"personal_report_request_name",
|
"personal_report_request_name",
|
||||||
"Who is this?",
|
"Who is this?",
|
||||||
ContextUpdates: BuildContextUpdates(
|
ContextUpdates: BuildContextUpdates(
|
||||||
AwaitingIdentityNameState,
|
AwaitingIdentityNameState,
|
||||||
noMatchCount: 0,
|
0,
|
||||||
noInputCount: 0,
|
0,
|
||||||
toggles,
|
toggles,
|
||||||
userName: null,
|
null,
|
||||||
userVerified: false,
|
false,
|
||||||
lastServiceError: string.Empty));
|
string.Empty));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IsNegativeReply(loweredTranscript))
|
if (IsNegativeReply(loweredTranscript)) return BuildDeclinedDecision(toggles);
|
||||||
{
|
|
||||||
return BuildDeclinedDecision(toggles);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(inlineToggleSummary))
|
if (!string.IsNullOrWhiteSpace(inlineToggleSummary))
|
||||||
{
|
|
||||||
return new JiboInteractionDecision(
|
return new JiboInteractionDecision(
|
||||||
"personal_report_opt_in",
|
"personal_report_opt_in",
|
||||||
$"{inlineToggleSummary} Would you like your personal report now?",
|
$"{inlineToggleSummary} Would you like your personal report now?",
|
||||||
|
SkillPayload: BuildYesNoPromptPayload(),
|
||||||
ContextUpdates: BuildContextUpdates(
|
ContextUpdates: BuildContextUpdates(
|
||||||
AwaitingOptInState,
|
AwaitingOptInState,
|
||||||
noMatchCount: 0,
|
0,
|
||||||
noInputCount: 0,
|
0,
|
||||||
toggles,
|
toggles,
|
||||||
userName: ReadString(turn, UserNameMetadataKey),
|
ReadString(turn, UserNameMetadataKey),
|
||||||
userVerified: false,
|
false,
|
||||||
lastServiceError: string.Empty));
|
string.Empty));
|
||||||
}
|
|
||||||
|
|
||||||
return BuildNoMatchDecision(
|
return BuildNoMatchDecision(
|
||||||
turn,
|
turn,
|
||||||
state,
|
state,
|
||||||
"Please say yes to start your personal report, or no to skip it.",
|
"Please say yes to start your personal report, or no to skip it.",
|
||||||
toggles,
|
toggles,
|
||||||
userName: ReadString(turn, UserNameMetadataKey),
|
ReadString(turn, UserNameMetadataKey),
|
||||||
userVerified: false);
|
false);
|
||||||
|
|
||||||
case AwaitingIdentityConfirmationState:
|
case AwaitingIdentityConfirmationState:
|
||||||
{
|
{
|
||||||
var currentName = ReadString(turn, UserNameMetadataKey);
|
var currentName = ReadString(turn, UserNameMetadataKey);
|
||||||
if (string.IsNullOrWhiteSpace(currentName))
|
if (string.IsNullOrWhiteSpace(currentName))
|
||||||
{
|
|
||||||
return new JiboInteractionDecision(
|
return new JiboInteractionDecision(
|
||||||
"personal_report_request_name",
|
"personal_report_request_name",
|
||||||
"Who is this?",
|
"Who is this?",
|
||||||
ContextUpdates: BuildContextUpdates(
|
ContextUpdates: BuildContextUpdates(
|
||||||
AwaitingIdentityNameState,
|
AwaitingIdentityNameState,
|
||||||
noMatchCount: 0,
|
0,
|
||||||
noInputCount: 0,
|
0,
|
||||||
toggles,
|
toggles,
|
||||||
userName: null,
|
null,
|
||||||
userVerified: false,
|
false,
|
||||||
lastServiceError: string.Empty));
|
string.Empty));
|
||||||
}
|
|
||||||
|
|
||||||
if (IsAffirmativeReply(loweredTranscript))
|
if (IsAffirmativeReply(loweredTranscript))
|
||||||
{
|
|
||||||
return await BuildDeliveredReportDecisionAsync(
|
return await BuildDeliveredReportDecisionAsync(
|
||||||
turn,
|
turn,
|
||||||
catalog,
|
catalog,
|
||||||
@@ -204,45 +192,40 @@ internal static class PersonalReportOrchestrator
|
|||||||
currentName,
|
currentName,
|
||||||
buildWeatherDecisionAsync,
|
buildWeatherDecisionAsync,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
}
|
|
||||||
|
|
||||||
if (IsNegativeReply(loweredTranscript))
|
if (IsNegativeReply(loweredTranscript))
|
||||||
{
|
|
||||||
return new JiboInteractionDecision(
|
return new JiboInteractionDecision(
|
||||||
"personal_report_request_name",
|
"personal_report_request_name",
|
||||||
"Okay, who is this?",
|
"Okay, who is this?",
|
||||||
ContextUpdates: BuildContextUpdates(
|
ContextUpdates: BuildContextUpdates(
|
||||||
AwaitingIdentityNameState,
|
AwaitingIdentityNameState,
|
||||||
noMatchCount: 0,
|
0,
|
||||||
noInputCount: 0,
|
0,
|
||||||
toggles,
|
toggles,
|
||||||
userName: null,
|
null,
|
||||||
userVerified: false,
|
false,
|
||||||
lastServiceError: string.Empty));
|
string.Empty));
|
||||||
}
|
|
||||||
|
|
||||||
return BuildNoMatchDecision(
|
return BuildNoMatchDecision(
|
||||||
turn,
|
turn,
|
||||||
state,
|
state,
|
||||||
$"Please answer yes or no. Is this {currentName}?",
|
$"Please answer yes or no. Is this {currentName}?",
|
||||||
toggles,
|
toggles,
|
||||||
userName: currentName,
|
currentName,
|
||||||
userVerified: false);
|
false);
|
||||||
}
|
}
|
||||||
|
|
||||||
case AwaitingIdentityNameState:
|
case AwaitingIdentityNameState:
|
||||||
{
|
{
|
||||||
var parsedName = TryExtractName(loweredTranscript);
|
var parsedName = TryExtractName(loweredTranscript);
|
||||||
if (string.IsNullOrWhiteSpace(parsedName))
|
if (string.IsNullOrWhiteSpace(parsedName))
|
||||||
{
|
|
||||||
return BuildNoMatchDecision(
|
return BuildNoMatchDecision(
|
||||||
turn,
|
turn,
|
||||||
state,
|
state,
|
||||||
"Tell me your name like this: my name is Alex.",
|
"Tell me your name like this: my name is Alex.",
|
||||||
toggles,
|
toggles,
|
||||||
userName: null,
|
null,
|
||||||
userVerified: false);
|
false);
|
||||||
}
|
|
||||||
|
|
||||||
personalMemoryStore.SetName(tenantScopeResolver(turn), parsedName);
|
personalMemoryStore.SetName(tenantScopeResolver(turn), parsedName);
|
||||||
return await BuildDeliveredReportDecisionAsync(
|
return await BuildDeliveredReportDecisionAsync(
|
||||||
@@ -269,47 +252,81 @@ internal static class PersonalReportOrchestrator
|
|||||||
Func<TurnContext, string, CancellationToken, Task<JiboInteractionDecision>> buildWeatherDecisionAsync,
|
Func<TurnContext, string, CancellationToken, Task<JiboInteractionDecision>> buildWeatherDecisionAsync,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var reportSections = new List<string> { $"Great, {userName}. Here is your personal report." };
|
var reportSections = new List<string>
|
||||||
|
{
|
||||||
|
RenderPersonalReportTemplate(
|
||||||
|
ChoosePersonalReportTemplate(
|
||||||
|
catalog.PersonalReportKickOffReplies,
|
||||||
|
"Okay. Here's your personal report."),
|
||||||
|
userName)
|
||||||
|
};
|
||||||
var serviceError = string.Empty;
|
var serviceError = string.Empty;
|
||||||
|
|
||||||
if (toggles.WeatherEnabled)
|
if (toggles.WeatherEnabled)
|
||||||
{
|
{
|
||||||
|
reportSections.Add("Weather.");
|
||||||
var weatherDecision = await buildWeatherDecisionAsync(turn, "weather", cancellationToken);
|
var weatherDecision = await buildWeatherDecisionAsync(turn, "weather", cancellationToken);
|
||||||
reportSections.Add(weatherDecision.ReplyText);
|
reportSections.Add(weatherDecision.ReplyText);
|
||||||
if (IsWeatherErrorReply(weatherDecision.ReplyText))
|
if (IsWeatherErrorReply(weatherDecision.ReplyText)) serviceError = "weather";
|
||||||
{
|
|
||||||
serviceError = "weather";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toggles.CalendarEnabled)
|
if (toggles.CalendarEnabled)
|
||||||
{
|
{
|
||||||
reportSections.Add(randomizer.Choose(catalog.CalendarReplies));
|
reportSections.Add(
|
||||||
|
RenderReportSkillTemplate(
|
||||||
|
ChooseReportSkillTemplate(
|
||||||
|
catalog.CalendarNothingTodayReplies,
|
||||||
|
catalog.CalendarNothingReplies,
|
||||||
|
"Looking at your calendar, I don't see anything scheduled today."),
|
||||||
|
userName));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toggles.CommuteEnabled)
|
if (toggles.CommuteEnabled)
|
||||||
{
|
reportSections.Add(
|
||||||
reportSections.Add(randomizer.Choose(catalog.CommuteReplies));
|
RenderReportSkillTemplate(
|
||||||
}
|
ChooseReportSkillTemplate(
|
||||||
|
catalog.CommuteServiceDownReplies,
|
||||||
|
catalog.CommuteNowReplies,
|
||||||
|
"Sorry, commute information isn't available right now."),
|
||||||
|
userName));
|
||||||
|
|
||||||
if (toggles.NewsEnabled)
|
if (toggles.NewsEnabled)
|
||||||
{
|
{
|
||||||
reportSections.Add(randomizer.Choose(catalog.NewsBriefings));
|
reportSections.Add(
|
||||||
|
RenderReportSkillTemplate(
|
||||||
|
ChooseReportSkillTemplate(
|
||||||
|
catalog.NewsIntroReplies,
|
||||||
|
catalog.NewsCategoryIntroReplies,
|
||||||
|
"Here's today's news, from the associated press."),
|
||||||
|
userName));
|
||||||
|
reportSections.Add(ChooseShortestBriefing(catalog.NewsBriefings));
|
||||||
|
reportSections.Add(
|
||||||
|
RenderReportSkillTemplate(
|
||||||
|
ChooseReportSkillTemplate(
|
||||||
|
catalog.NewsOutroReplies,
|
||||||
|
[],
|
||||||
|
"And that's what's new in the news."),
|
||||||
|
userName));
|
||||||
}
|
}
|
||||||
|
|
||||||
reportSections.Add("That is your personal report.");
|
reportSections.Add(
|
||||||
|
RenderPersonalReportTemplate(
|
||||||
|
ChoosePersonalReportTemplate(
|
||||||
|
catalog.PersonalReportOutroReplies,
|
||||||
|
"And that's your report for the day. I hope you had as much fun as I did."),
|
||||||
|
userName));
|
||||||
|
|
||||||
return new JiboInteractionDecision(
|
return new JiboInteractionDecision(
|
||||||
"personal_report_delivered",
|
"personal_report_delivered",
|
||||||
string.Join(" ", reportSections),
|
string.Join(" ", reportSections),
|
||||||
ContextUpdates: BuildContextUpdates(
|
ContextUpdates: BuildContextUpdates(
|
||||||
IdleState,
|
IdleState,
|
||||||
noMatchCount: 0,
|
0,
|
||||||
noInputCount: 0,
|
0,
|
||||||
toggles,
|
toggles,
|
||||||
userName,
|
userName,
|
||||||
userVerified: true,
|
true,
|
||||||
lastServiceError: serviceError));
|
serviceError));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JiboInteractionDecision BuildNoInputDecision(
|
private static JiboInteractionDecision BuildNoInputDecision(
|
||||||
@@ -318,22 +335,19 @@ internal static class PersonalReportOrchestrator
|
|||||||
PersonalReportServiceToggles toggles)
|
PersonalReportServiceToggles toggles)
|
||||||
{
|
{
|
||||||
var noInputCount = Math.Max(0, ReadInt(turn, NoInputCountMetadataKey)) + 1;
|
var noInputCount = Math.Max(0, ReadInt(turn, NoInputCountMetadataKey)) + 1;
|
||||||
if (noInputCount >= MaxNoInputCount)
|
if (noInputCount >= MaxNoInputCount) return BuildDeclinedDecision(toggles);
|
||||||
{
|
|
||||||
return BuildDeclinedDecision(toggles);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new JiboInteractionDecision(
|
return new JiboInteractionDecision(
|
||||||
"personal_report_no_input",
|
"personal_report_no_input",
|
||||||
"I am still here. Do you want your personal report?",
|
"I am still here. Do you want your personal report?",
|
||||||
ContextUpdates: BuildContextUpdates(
|
ContextUpdates: BuildContextUpdates(
|
||||||
state,
|
state,
|
||||||
noMatchCount: ReadInt(turn, NoMatchCountMetadataKey),
|
ReadInt(turn, NoMatchCountMetadataKey),
|
||||||
noInputCount,
|
noInputCount,
|
||||||
toggles,
|
toggles,
|
||||||
userName: ReadString(turn, UserNameMetadataKey),
|
ReadString(turn, UserNameMetadataKey),
|
||||||
userVerified: ReadBool(turn, UserVerifiedMetadataKey) ?? false,
|
ReadBool(turn, UserVerifiedMetadataKey) ?? false,
|
||||||
lastServiceError: string.Empty));
|
string.Empty));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JiboInteractionDecision BuildNoMatchDecision(
|
private static JiboInteractionDecision BuildNoMatchDecision(
|
||||||
@@ -345,10 +359,7 @@ internal static class PersonalReportOrchestrator
|
|||||||
bool userVerified)
|
bool userVerified)
|
||||||
{
|
{
|
||||||
var noMatchCount = Math.Max(0, ReadInt(turn, NoMatchCountMetadataKey)) + 1;
|
var noMatchCount = Math.Max(0, ReadInt(turn, NoMatchCountMetadataKey)) + 1;
|
||||||
if (noMatchCount >= MaxNoMatchCount)
|
if (noMatchCount >= MaxNoMatchCount) return BuildDeclinedDecision(toggles);
|
||||||
{
|
|
||||||
return BuildDeclinedDecision(toggles);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new JiboInteractionDecision(
|
return new JiboInteractionDecision(
|
||||||
"personal_report_no_match",
|
"personal_report_no_match",
|
||||||
@@ -356,11 +367,11 @@ internal static class PersonalReportOrchestrator
|
|||||||
ContextUpdates: BuildContextUpdates(
|
ContextUpdates: BuildContextUpdates(
|
||||||
state,
|
state,
|
||||||
noMatchCount,
|
noMatchCount,
|
||||||
noInputCount: 0,
|
0,
|
||||||
toggles,
|
toggles,
|
||||||
userName,
|
userName,
|
||||||
userVerified,
|
userVerified,
|
||||||
lastServiceError: string.Empty));
|
string.Empty));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JiboInteractionDecision BuildDeclinedDecision(PersonalReportServiceToggles toggles)
|
private static JiboInteractionDecision BuildDeclinedDecision(PersonalReportServiceToggles toggles)
|
||||||
@@ -370,12 +381,12 @@ internal static class PersonalReportOrchestrator
|
|||||||
"No problem. We can do your personal report another time.",
|
"No problem. We can do your personal report another time.",
|
||||||
ContextUpdates: BuildContextUpdates(
|
ContextUpdates: BuildContextUpdates(
|
||||||
IdleState,
|
IdleState,
|
||||||
noMatchCount: 0,
|
0,
|
||||||
noInputCount: 0,
|
0,
|
||||||
toggles,
|
toggles,
|
||||||
userName: null,
|
null,
|
||||||
userVerified: false,
|
false,
|
||||||
lastServiceError: string.Empty));
|
string.Empty));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JiboInteractionDecision BuildCancelledDecision(PersonalReportServiceToggles toggles)
|
private static JiboInteractionDecision BuildCancelledDecision(PersonalReportServiceToggles toggles)
|
||||||
@@ -385,12 +396,12 @@ internal static class PersonalReportOrchestrator
|
|||||||
"Okay, canceling personal report.",
|
"Okay, canceling personal report.",
|
||||||
ContextUpdates: BuildContextUpdates(
|
ContextUpdates: BuildContextUpdates(
|
||||||
IdleState,
|
IdleState,
|
||||||
noMatchCount: 0,
|
0,
|
||||||
noInputCount: 0,
|
0,
|
||||||
toggles,
|
toggles,
|
||||||
userName: null,
|
null,
|
||||||
userVerified: false,
|
false,
|
||||||
lastServiceError: string.Empty));
|
string.Empty));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IDictionary<string, object?> BuildContextUpdates(
|
private static IDictionary<string, object?> BuildContextUpdates(
|
||||||
@@ -417,6 +428,14 @@ internal static class PersonalReportOrchestrator
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static IDictionary<string, object?> BuildYesNoPromptPayload()
|
||||||
|
{
|
||||||
|
return new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["listen_contexts"] = new[] { "shared/yes_no" }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private static bool IsAffirmativeReply(string loweredTranscript)
|
private static bool IsAffirmativeReply(string loweredTranscript)
|
||||||
{
|
{
|
||||||
return ContainsAnyPhrase(loweredTranscript, AffirmativePhrases);
|
return ContainsAnyPhrase(loweredTranscript, AffirmativePhrases);
|
||||||
@@ -430,24 +449,17 @@ internal static class PersonalReportOrchestrator
|
|||||||
private static bool ContainsAnyPhrase(string loweredTranscript, IEnumerable<string> phrases)
|
private static bool ContainsAnyPhrase(string loweredTranscript, IEnumerable<string> phrases)
|
||||||
{
|
{
|
||||||
foreach (var phrase in phrases)
|
foreach (var phrase in phrases)
|
||||||
{
|
|
||||||
if (string.Equals(loweredTranscript, phrase, StringComparison.Ordinal) ||
|
if (string.Equals(loweredTranscript, phrase, StringComparison.Ordinal) ||
|
||||||
loweredTranscript.StartsWith($"{phrase} ", StringComparison.Ordinal) ||
|
loweredTranscript.StartsWith($"{phrase} ", StringComparison.Ordinal) ||
|
||||||
loweredTranscript.Contains($" {phrase}", StringComparison.Ordinal))
|
loweredTranscript.Contains($" {phrase}", StringComparison.Ordinal))
|
||||||
{
|
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsWeatherErrorReply(string replyText)
|
private static bool IsWeatherErrorReply(string replyText)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(replyText))
|
if (string.IsNullOrWhiteSpace(replyText)) return false;
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return replyText.Contains("couldn't fetch the weather", StringComparison.OrdinalIgnoreCase) ||
|
return replyText.Contains("couldn't fetch the weather", StringComparison.OrdinalIgnoreCase) ||
|
||||||
replyText.Contains("weather service is connected", StringComparison.OrdinalIgnoreCase);
|
replyText.Contains("weather service is connected", StringComparison.OrdinalIgnoreCase);
|
||||||
@@ -470,36 +482,32 @@ internal static class PersonalReportOrchestrator
|
|||||||
summary = string.Empty;
|
summary = string.Empty;
|
||||||
var updated = toggles;
|
var updated = toggles;
|
||||||
|
|
||||||
updated = ApplyToggleHint(updated, loweredTranscript, "weather", static value => value with { WeatherEnabled = false }, static value => value with { WeatherEnabled = true });
|
updated = ApplyToggleHint(updated, loweredTranscript, "weather",
|
||||||
updated = ApplyToggleHint(updated, loweredTranscript, "calendar", static value => value with { CalendarEnabled = false }, static value => value with { CalendarEnabled = true });
|
static value => value with { WeatherEnabled = false },
|
||||||
updated = ApplyToggleHint(updated, loweredTranscript, "commute", static value => value with { CommuteEnabled = false }, static value => value with { CommuteEnabled = true });
|
static value => value with { WeatherEnabled = true });
|
||||||
updated = ApplyToggleHint(updated, loweredTranscript, "news", static value => value with { NewsEnabled = false }, static value => value with { NewsEnabled = true });
|
updated = ApplyToggleHint(updated, loweredTranscript, "calendar",
|
||||||
|
static value => value with { CalendarEnabled = false },
|
||||||
|
static value => value with { CalendarEnabled = true });
|
||||||
|
updated = ApplyToggleHint(updated, loweredTranscript, "commute",
|
||||||
|
static value => value with { CommuteEnabled = false },
|
||||||
|
static value => value with { CommuteEnabled = true });
|
||||||
|
updated = ApplyToggleHint(updated, loweredTranscript, "news",
|
||||||
|
static value => value with { NewsEnabled = false }, static value => value with { NewsEnabled = true });
|
||||||
|
|
||||||
var changes = new List<string>();
|
var changes = new List<string>();
|
||||||
if (updated.WeatherEnabled != toggles.WeatherEnabled)
|
if (updated.WeatherEnabled != toggles.WeatherEnabled)
|
||||||
{
|
|
||||||
changes.Add(updated.WeatherEnabled ? "including weather" : "skipping weather");
|
changes.Add(updated.WeatherEnabled ? "including weather" : "skipping weather");
|
||||||
}
|
|
||||||
|
|
||||||
if (updated.CalendarEnabled != toggles.CalendarEnabled)
|
if (updated.CalendarEnabled != toggles.CalendarEnabled)
|
||||||
{
|
|
||||||
changes.Add(updated.CalendarEnabled ? "including calendar" : "skipping calendar");
|
changes.Add(updated.CalendarEnabled ? "including calendar" : "skipping calendar");
|
||||||
}
|
|
||||||
|
|
||||||
if (updated.CommuteEnabled != toggles.CommuteEnabled)
|
if (updated.CommuteEnabled != toggles.CommuteEnabled)
|
||||||
{
|
|
||||||
changes.Add(updated.CommuteEnabled ? "including commute" : "skipping commute");
|
changes.Add(updated.CommuteEnabled ? "including commute" : "skipping commute");
|
||||||
}
|
|
||||||
|
|
||||||
if (updated.NewsEnabled != toggles.NewsEnabled)
|
if (updated.NewsEnabled != toggles.NewsEnabled)
|
||||||
{
|
|
||||||
changes.Add(updated.NewsEnabled ? "including news" : "skipping news");
|
changes.Add(updated.NewsEnabled ? "including news" : "skipping news");
|
||||||
}
|
|
||||||
|
|
||||||
if (changes.Count > 0)
|
if (changes.Count > 0) summary = $"Got it, {string.Join(", ", changes)}.";
|
||||||
{
|
|
||||||
summary = $"Got it, {string.Join(", ", changes)}.";
|
|
||||||
}
|
|
||||||
|
|
||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
@@ -514,15 +522,11 @@ internal static class PersonalReportOrchestrator
|
|||||||
if (loweredTranscript.Contains($"without {serviceLabel}", StringComparison.Ordinal) ||
|
if (loweredTranscript.Contains($"without {serviceLabel}", StringComparison.Ordinal) ||
|
||||||
loweredTranscript.Contains($"skip {serviceLabel}", StringComparison.Ordinal) ||
|
loweredTranscript.Contains($"skip {serviceLabel}", StringComparison.Ordinal) ||
|
||||||
loweredTranscript.Contains($"no {serviceLabel}", StringComparison.Ordinal))
|
loweredTranscript.Contains($"no {serviceLabel}", StringComparison.Ordinal))
|
||||||
{
|
|
||||||
return disable(toggles);
|
return disable(toggles);
|
||||||
}
|
|
||||||
|
|
||||||
if (loweredTranscript.Contains($"with {serviceLabel}", StringComparison.Ordinal) ||
|
if (loweredTranscript.Contains($"with {serviceLabel}", StringComparison.Ordinal) ||
|
||||||
loweredTranscript.Contains($"include {serviceLabel}", StringComparison.Ordinal))
|
loweredTranscript.Contains($"include {serviceLabel}", StringComparison.Ordinal))
|
||||||
{
|
|
||||||
return enable(toggles);
|
return enable(toggles);
|
||||||
}
|
|
||||||
|
|
||||||
return toggles;
|
return toggles;
|
||||||
}
|
}
|
||||||
@@ -534,10 +538,7 @@ internal static class PersonalReportOrchestrator
|
|||||||
|
|
||||||
private static string? ReadString(TurnContext turn, string key)
|
private static string? ReadString(TurnContext turn, string key)
|
||||||
{
|
{
|
||||||
if (!turn.Attributes.TryGetValue(key, out var value) || value is null)
|
if (!turn.Attributes.TryGetValue(key, out var value) || value is null) return null;
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value switch
|
return value switch
|
||||||
{
|
{
|
||||||
@@ -548,10 +549,7 @@ internal static class PersonalReportOrchestrator
|
|||||||
|
|
||||||
private static bool? ReadBool(TurnContext turn, string key)
|
private static bool? ReadBool(TurnContext turn, string key)
|
||||||
{
|
{
|
||||||
if (!turn.Attributes.TryGetValue(key, out var value) || value is null)
|
if (!turn.Attributes.TryGetValue(key, out var value) || value is null) return null;
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value switch
|
return value switch
|
||||||
{
|
{
|
||||||
@@ -559,17 +557,15 @@ internal static class PersonalReportOrchestrator
|
|||||||
string text when bool.TryParse(text, out var parsed) => parsed,
|
string text when bool.TryParse(text, out var parsed) => parsed,
|
||||||
JsonElement { ValueKind: JsonValueKind.True } => true,
|
JsonElement { ValueKind: JsonValueKind.True } => true,
|
||||||
JsonElement { ValueKind: JsonValueKind.False } => false,
|
JsonElement { ValueKind: JsonValueKind.False } => false,
|
||||||
JsonElement json when json.ValueKind == JsonValueKind.String && bool.TryParse(json.GetString(), out var parsed) => parsed,
|
JsonElement json when json.ValueKind == JsonValueKind.String &&
|
||||||
|
bool.TryParse(json.GetString(), out var parsed) => parsed,
|
||||||
_ => null
|
_ => null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int ReadInt(TurnContext turn, string key)
|
private static int ReadInt(TurnContext turn, string key)
|
||||||
{
|
{
|
||||||
if (!turn.Attributes.TryGetValue(key, out var value) || value is null)
|
if (!turn.Attributes.TryGetValue(key, out var value) || value is null) return 0;
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value switch
|
return value switch
|
||||||
{
|
{
|
||||||
@@ -577,7 +573,8 @@ internal static class PersonalReportOrchestrator
|
|||||||
long whole when whole <= int.MaxValue && whole >= int.MinValue => (int)whole,
|
long whole when whole <= int.MaxValue && whole >= int.MinValue => (int)whole,
|
||||||
string text when int.TryParse(text, out var parsed) => parsed,
|
string text when int.TryParse(text, out var parsed) => parsed,
|
||||||
JsonElement { ValueKind: JsonValueKind.Number } number when number.TryGetInt32(out var parsed) => parsed,
|
JsonElement { ValueKind: JsonValueKind.Number } number when number.TryGetInt32(out var parsed) => parsed,
|
||||||
JsonElement json when json.ValueKind == JsonValueKind.String && int.TryParse(json.GetString(), out var parsed) => parsed,
|
JsonElement json when json.ValueKind == JsonValueKind.String &&
|
||||||
|
int.TryParse(json.GetString(), out var parsed) => parsed,
|
||||||
_ => 0
|
_ => 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -587,10 +584,7 @@ internal static class PersonalReportOrchestrator
|
|||||||
var normalized = NameNoiseRegex.Replace(loweredTranscript, " ")
|
var normalized = NameNoiseRegex.Replace(loweredTranscript, " ")
|
||||||
.Replace(" ", " ", StringComparison.Ordinal)
|
.Replace(" ", " ", StringComparison.Ordinal)
|
||||||
.Trim();
|
.Trim();
|
||||||
if (string.IsNullOrWhiteSpace(normalized))
|
if (string.IsNullOrWhiteSpace(normalized)) return null;
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var prefixes = new[]
|
var prefixes = new[]
|
||||||
{
|
{
|
||||||
@@ -604,10 +598,7 @@ internal static class PersonalReportOrchestrator
|
|||||||
|
|
||||||
foreach (var prefix in prefixes)
|
foreach (var prefix in prefixes)
|
||||||
{
|
{
|
||||||
if (!normalized.StartsWith(prefix, StringComparison.Ordinal))
|
if (!normalized.StartsWith(prefix, StringComparison.Ordinal)) continue;
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var candidate = normalized[prefix.Length..].Trim();
|
var candidate = normalized[prefix.Length..].Trim();
|
||||||
return NormalizeNameCandidate(candidate);
|
return NormalizeNameCandidate(candidate);
|
||||||
@@ -618,39 +609,87 @@ internal static class PersonalReportOrchestrator
|
|||||||
|
|
||||||
private static string? NormalizeNameCandidate(string candidate)
|
private static string? NormalizeNameCandidate(string candidate)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(candidate))
|
if (string.IsNullOrWhiteSpace(candidate)) return null;
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var cleaned = NameNoiseRegex.Replace(candidate, " ")
|
var cleaned = NameNoiseRegex.Replace(candidate, " ")
|
||||||
.Replace(" ", " ", StringComparison.Ordinal)
|
.Replace(" ", " ", StringComparison.Ordinal)
|
||||||
.Trim();
|
.Trim();
|
||||||
if (string.IsNullOrWhiteSpace(cleaned))
|
if (string.IsNullOrWhiteSpace(cleaned)) return null;
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cleaned.Length < 2 || cleaned.Length > 32)
|
if (cleaned.Length < 2 || cleaned.Length > 32) return null;
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var words = cleaned.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
var words = cleaned.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||||
if (words.Length > 4)
|
if (words.Length > 4) return null;
|
||||||
|
|
||||||
|
return words.Any(static word => word.Any(char.IsDigit)) ? null : cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ChoosePersonalReportTemplate(
|
||||||
|
IReadOnlyList<string> templates,
|
||||||
|
string fallback)
|
||||||
{
|
{
|
||||||
return null;
|
var usableTemplates = templates
|
||||||
|
.Where(static template => !string.IsNullOrWhiteSpace(template) &&
|
||||||
|
!template.Contains("${dt.", StringComparison.OrdinalIgnoreCase))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
if (usableTemplates.Length == 0) return fallback;
|
||||||
|
|
||||||
|
var speakerAwareTemplate = usableTemplates.FirstOrDefault(static template =>
|
||||||
|
template.Contains("${speaker}", StringComparison.OrdinalIgnoreCase));
|
||||||
|
return ChooseShortestTemplate(speakerAwareTemplate is not null ? [speakerAwareTemplate] : usableTemplates)
|
||||||
|
?? fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (words.Any(static word => word.Any(char.IsDigit)))
|
private static string RenderPersonalReportTemplate(string template, string userName)
|
||||||
{
|
{
|
||||||
return null;
|
return template
|
||||||
|
.Replace("${speaker}", userName, StringComparison.OrdinalIgnoreCase)
|
||||||
|
.Replace("${speaker}'s", $"{userName}'s", StringComparison.OrdinalIgnoreCase)
|
||||||
|
.Replace(" ", " ", StringComparison.Ordinal)
|
||||||
|
.Trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
return cleaned;
|
private static string ChooseReportSkillTemplate(
|
||||||
|
IReadOnlyList<string> primaryTemplates,
|
||||||
|
IReadOnlyList<string> secondaryTemplates,
|
||||||
|
string fallback)
|
||||||
|
{
|
||||||
|
var primary = ChooseShortestTemplate(primaryTemplates);
|
||||||
|
if (!string.IsNullOrWhiteSpace(primary)) return primary!;
|
||||||
|
|
||||||
|
var secondary = ChooseShortestTemplate(secondaryTemplates);
|
||||||
|
return !string.IsNullOrWhiteSpace(secondary) ? secondary! : fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly Regex NameNoiseRegex = new("[^a-zA-Z\\-\\s']", RegexOptions.Compiled);
|
private static string ChooseShortestBriefing(IReadOnlyList<string> briefings)
|
||||||
|
{
|
||||||
|
var selected = ChooseShortestTemplate(briefings);
|
||||||
|
if (string.IsNullOrWhiteSpace(selected)) return string.Empty;
|
||||||
|
|
||||||
|
var firstSentence = selected.Split(['.', '!', '?'], 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||||
|
.FirstOrDefault();
|
||||||
|
return string.IsNullOrWhiteSpace(firstSentence) ? selected : firstSentence;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? ChooseShortestTemplate(IEnumerable<string> templates)
|
||||||
|
{
|
||||||
|
var selected = templates
|
||||||
|
.Where(static template => !string.IsNullOrWhiteSpace(template))
|
||||||
|
.OrderBy(static template => template.Length)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string RenderReportSkillTemplate(string template, string userName)
|
||||||
|
{
|
||||||
|
return template
|
||||||
|
.Replace("${speaker}", userName, StringComparison.OrdinalIgnoreCase)
|
||||||
|
.Replace("${speaker}'s", $"{userName}'s", StringComparison.OrdinalIgnoreCase)
|
||||||
|
.Replace(" ", " ", StringComparison.Ordinal)
|
||||||
|
.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
private readonly record struct PersonalReportServiceToggles(
|
private readonly record struct PersonalReportServiceToggles(
|
||||||
bool WeatherEnabled,
|
bool WeatherEnabled,
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ namespace Jibo.Cloud.Application.Services;
|
|||||||
|
|
||||||
public sealed class ProtocolToTurnContextMapper
|
public sealed class ProtocolToTurnContextMapper
|
||||||
{
|
{
|
||||||
public static TurnContext MapListenMessage(WebSocketMessageEnvelope envelope, CloudSession session, string messageType)
|
public static TurnContext MapListenMessage(WebSocketMessageEnvelope envelope, CloudSession session,
|
||||||
|
string messageType)
|
||||||
{
|
{
|
||||||
var turnState = session.TurnState;
|
var turnState = session.TurnState;
|
||||||
var protocolOperation = messageType.ToLowerInvariant();
|
var protocolOperation = messageType.ToLowerInvariant();
|
||||||
@@ -16,87 +17,58 @@ public sealed class ProtocolToTurnContextMapper
|
|||||||
};
|
};
|
||||||
var text = ExtractTranscript(envelope.Text, attributes);
|
var text = ExtractTranscript(envelope.Text, attributes);
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(turnState.TransId))
|
if (!string.IsNullOrWhiteSpace(turnState.TransId)) attributes["transID"] = turnState.TransId;
|
||||||
{
|
|
||||||
attributes["transID"] = turnState.TransId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(session.AccountId))
|
if (!string.IsNullOrWhiteSpace(session.AccountId)) attributes["accountId"] = session.AccountId;
|
||||||
{
|
|
||||||
attributes["accountId"] = session.AccountId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(session.DeviceId))
|
if (!string.IsNullOrWhiteSpace(session.DeviceId)) attributes["deviceId"] = session.DeviceId;
|
||||||
{
|
|
||||||
attributes["deviceId"] = session.DeviceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (session.Metadata.TryGetValue("loopId", out var loopId) &&
|
if (session.Metadata.TryGetValue("loopId", out var loopId) &&
|
||||||
loopId is string loopIdText &&
|
loopId is string loopIdText &&
|
||||||
!string.IsNullOrWhiteSpace(loopIdText))
|
!string.IsNullOrWhiteSpace(loopIdText))
|
||||||
{
|
|
||||||
attributes["loopId"] = loopIdText;
|
attributes["loopId"] = loopIdText;
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(turnState.ContextPayload))
|
if (!string.IsNullOrWhiteSpace(turnState.ContextPayload)) attributes["context"] = turnState.ContextPayload;
|
||||||
{
|
|
||||||
attributes["context"] = turnState.ContextPayload;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (session.Metadata.TryGetValue("lastClockDomain", out var lastClockDomain) &&
|
if (session.Metadata.TryGetValue("lastClockDomain", out var lastClockDomain) &&
|
||||||
lastClockDomain is string lastClockDomainText &&
|
lastClockDomain is string lastClockDomainText &&
|
||||||
!string.IsNullOrWhiteSpace(lastClockDomainText))
|
!string.IsNullOrWhiteSpace(lastClockDomainText))
|
||||||
{
|
|
||||||
attributes["lastClockDomain"] = lastClockDomainText;
|
attributes["lastClockDomain"] = lastClockDomainText;
|
||||||
}
|
|
||||||
|
|
||||||
if (session.Metadata.TryGetValue("pendingProactivityOffer", out var pendingProactivityOffer) &&
|
if (session.Metadata.TryGetValue("pendingProactivityOffer", out var pendingProactivityOffer) &&
|
||||||
pendingProactivityOffer is string pendingProactivityOfferText &&
|
pendingProactivityOffer is string pendingProactivityOfferText &&
|
||||||
!string.IsNullOrWhiteSpace(pendingProactivityOfferText))
|
!string.IsNullOrWhiteSpace(pendingProactivityOfferText))
|
||||||
{
|
|
||||||
attributes["pendingProactivityOffer"] = pendingProactivityOfferText;
|
attributes["pendingProactivityOffer"] = pendingProactivityOfferText;
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var pair in session.Metadata)
|
foreach (var pair in session.Metadata)
|
||||||
{
|
{
|
||||||
if ((!pair.Key.StartsWith("personalReport", StringComparison.OrdinalIgnoreCase) &&
|
if ((!pair.Key.StartsWith("personalReport", StringComparison.OrdinalIgnoreCase) &&
|
||||||
!pair.Key.StartsWith("chitchat", StringComparison.OrdinalIgnoreCase)) ||
|
!pair.Key.StartsWith("chitchat", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
!pair.Key.StartsWith("greetings", StringComparison.OrdinalIgnoreCase)) ||
|
||||||
pair.Value is null)
|
pair.Value is null)
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
|
||||||
|
|
||||||
attributes[pair.Key] = pair.Value;
|
attributes[pair.Key] = pair.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
attributes["listenHotphrase"] = turnState.ListenHotphrase;
|
attributes["listenHotphrase"] = turnState.ListenHotphrase;
|
||||||
|
|
||||||
if (turnState.ListenRules.Count > 0)
|
if (turnState.ListenRules.Count > 0) attributes["listenRules"] = turnState.ListenRules;
|
||||||
{
|
|
||||||
attributes["listenRules"] = turnState.ListenRules;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (turnState.ListenAsrHints.Count > 0)
|
if (turnState.ListenAsrHints.Count > 0) attributes["listenAsrHints"] = turnState.ListenAsrHints;
|
||||||
{
|
|
||||||
attributes["listenAsrHints"] = turnState.ListenAsrHints;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (turnState.BufferedAudioBytes > 0)
|
if (turnState.BufferedAudioBytes > 0)
|
||||||
{
|
{
|
||||||
attributes["bufferedAudioBytes"] = turnState.BufferedAudioBytes;
|
attributes["bufferedAudioBytes"] = turnState.BufferedAudioBytes;
|
||||||
attributes["bufferedAudioChunks"] = turnState.BufferedAudioChunkCount;
|
attributes["bufferedAudioChunks"] = turnState.BufferedAudioChunkCount;
|
||||||
attributes["bufferedAudioFrames"] = turnState.BufferedAudioFrames.Select(frame => frame.ToArray()).ToArray();
|
attributes["bufferedAudioFrames"] =
|
||||||
|
turnState.BufferedAudioFrames.Select(frame => frame.ToArray()).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(turnState.AudioTranscriptHint))
|
if (!string.IsNullOrWhiteSpace(turnState.AudioTranscriptHint))
|
||||||
{
|
|
||||||
attributes["audioTranscriptHint"] = turnState.AudioTranscriptHint;
|
attributes["audioTranscriptHint"] = turnState.AudioTranscriptHint;
|
||||||
}
|
|
||||||
|
|
||||||
if (turnState.FinalizeAttemptCount > 0)
|
if (turnState.FinalizeAttemptCount > 0) attributes["finalizeAttemptCount"] = turnState.FinalizeAttemptCount;
|
||||||
{
|
|
||||||
attributes["finalizeAttemptCount"] = turnState.FinalizeAttemptCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new TurnContext
|
return new TurnContext
|
||||||
{
|
{
|
||||||
@@ -110,8 +82,12 @@ public sealed class ProtocolToTurnContextMapper
|
|||||||
RequestId = envelope.ConnectionId,
|
RequestId = envelope.ConnectionId,
|
||||||
ProtocolService = "neo-hub",
|
ProtocolService = "neo-hub",
|
||||||
ProtocolOperation = protocolOperation,
|
ProtocolOperation = protocolOperation,
|
||||||
FirmwareVersion = session.Metadata.TryGetValue("firmwareVersion", out var firmwareVersion) ? firmwareVersion as string : null,
|
FirmwareVersion = session.Metadata.TryGetValue("firmwareVersion", out var firmwareVersion)
|
||||||
ApplicationVersion = session.Metadata.TryGetValue("applicationVersion", out var applicationVersion) ? applicationVersion as string : null,
|
? firmwareVersion as string
|
||||||
|
: null,
|
||||||
|
ApplicationVersion = session.Metadata.TryGetValue("applicationVersion", out var applicationVersion)
|
||||||
|
? applicationVersion as string
|
||||||
|
: null,
|
||||||
IsFollowUpEligible = true,
|
IsFollowUpEligible = true,
|
||||||
Attributes = attributes
|
Attributes = attributes
|
||||||
};
|
};
|
||||||
@@ -119,10 +95,7 @@ public sealed class ProtocolToTurnContextMapper
|
|||||||
|
|
||||||
private static string? ExtractTranscript(string? text, IDictionary<string, object?> attributes)
|
private static string? ExtractTranscript(string? text, IDictionary<string, object?> attributes)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(text))
|
if (string.IsNullOrWhiteSpace(text)) return null;
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -132,41 +105,41 @@ public sealed class ProtocolToTurnContextMapper
|
|||||||
if (!root.TryGetProperty("data", out var data)) return null;
|
if (!root.TryGetProperty("data", out var data)) return null;
|
||||||
|
|
||||||
if (data.TryGetProperty("text", out var transcript) && transcript.ValueKind == JsonValueKind.String)
|
if (data.TryGetProperty("text", out var transcript) && transcript.ValueKind == JsonValueKind.String)
|
||||||
{
|
|
||||||
return transcript.GetString();
|
return transcript.GetString();
|
||||||
}
|
|
||||||
|
|
||||||
if (data.TryGetProperty("asr", out var asr) &&
|
if (data.TryGetProperty("asr", out var asr) &&
|
||||||
asr.ValueKind == JsonValueKind.Object &&
|
asr.ValueKind == JsonValueKind.Object &&
|
||||||
asr.TryGetProperty("text", out var asrText) &&
|
asr.TryGetProperty("text", out var asrText) &&
|
||||||
asrText.ValueKind == JsonValueKind.String)
|
asrText.ValueKind == JsonValueKind.String)
|
||||||
{
|
|
||||||
return asrText.GetString();
|
return asrText.GetString();
|
||||||
}
|
|
||||||
|
|
||||||
if (data.TryGetProperty("transcriptHint", out var transcriptHint) && transcriptHint.ValueKind == JsonValueKind.String)
|
if (data.TryGetProperty("transcriptHint", out var transcriptHint) &&
|
||||||
{
|
transcriptHint.ValueKind == JsonValueKind.String) return transcriptHint.GetString();
|
||||||
return transcriptHint.GetString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.TryGetProperty("intent", out var intent) && intent.ValueKind == JsonValueKind.String)
|
if (data.TryGetProperty("intent", out var intent) && intent.ValueKind == JsonValueKind.String)
|
||||||
{
|
|
||||||
attributes["clientIntent"] = intent.GetString();
|
attributes["clientIntent"] = intent.GetString();
|
||||||
}
|
|
||||||
|
if (data.TryGetProperty("triggerSource", out var triggerSource) &&
|
||||||
|
triggerSource.ValueKind == JsonValueKind.String &&
|
||||||
|
!string.IsNullOrWhiteSpace(triggerSource.GetString()))
|
||||||
|
attributes["triggerSource"] = triggerSource.GetString();
|
||||||
|
|
||||||
|
if (data.TryGetProperty("triggerData", out var triggerData) &&
|
||||||
|
triggerData.ValueKind == JsonValueKind.Object &&
|
||||||
|
triggerData.TryGetProperty("looperID", out var triggerLooperId) &&
|
||||||
|
triggerLooperId.ValueKind == JsonValueKind.String &&
|
||||||
|
!string.IsNullOrWhiteSpace(triggerLooperId.GetString()))
|
||||||
|
attributes["triggerLooperId"] = triggerLooperId.GetString();
|
||||||
|
|
||||||
if (data.TryGetProperty("rules", out var rules) && rules.ValueKind == JsonValueKind.Array)
|
if (data.TryGetProperty("rules", out var rules) && rules.ValueKind == JsonValueKind.Array)
|
||||||
{
|
|
||||||
attributes["clientRules"] = rules.EnumerateArray()
|
attributes["clientRules"] = rules.EnumerateArray()
|
||||||
.Where(item => item.ValueKind == JsonValueKind.String)
|
.Where(item => item.ValueKind == JsonValueKind.String)
|
||||||
.Select(item => item.GetString() ?? string.Empty)
|
.Select(item => item.GetString() ?? string.Empty)
|
||||||
.Where(rule => !string.IsNullOrWhiteSpace(rule))
|
.Where(rule => !string.IsNullOrWhiteSpace(rule))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
}
|
|
||||||
|
|
||||||
if (data.TryGetProperty("entities", out var entities) && entities.ValueKind == JsonValueKind.Object)
|
if (data.TryGetProperty("entities", out var entities) && entities.ValueKind == JsonValueKind.Object)
|
||||||
{
|
|
||||||
attributes["clientEntities"] = entities.Clone();
|
attributes["clientEntities"] = entities.Clone();
|
||||||
}
|
|
||||||
|
|
||||||
return intent.ValueKind == JsonValueKind.String ? intent.GetString() : null;
|
return intent.ValueKind == JsonValueKind.String ? intent.GetString() : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using Jibo.Cloud.Domain.Models;
|
using Jibo.Cloud.Domain.Models;
|
||||||
using Jibo.Runtime.Abstractions;
|
using Jibo.Runtime.Abstractions;
|
||||||
|
|
||||||
@@ -31,7 +32,8 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
var isVolumeControl = string.Equals(plan.IntentName, "volume_up", StringComparison.OrdinalIgnoreCase) ||
|
var isVolumeControl = string.Equals(plan.IntentName, "volume_up", StringComparison.OrdinalIgnoreCase) ||
|
||||||
string.Equals(plan.IntentName, "volume_down", StringComparison.OrdinalIgnoreCase) ||
|
string.Equals(plan.IntentName, "volume_down", StringComparison.OrdinalIgnoreCase) ||
|
||||||
string.Equals(plan.IntentName, "volume_to_value", StringComparison.OrdinalIgnoreCase);
|
string.Equals(plan.IntentName, "volume_to_value", StringComparison.OrdinalIgnoreCase);
|
||||||
var isProactivePizzaFactOffer = string.Equals(plan.IntentName, "proactive_offer_pizza_fact", StringComparison.OrdinalIgnoreCase);
|
var isProactivePizzaFactOffer = string.Equals(plan.IntentName, "proactive_offer_pizza_fact",
|
||||||
|
StringComparison.OrdinalIgnoreCase);
|
||||||
var isSettingsLaunch = string.Equals(skill?.SkillName, "@be/settings", StringComparison.OrdinalIgnoreCase);
|
var isSettingsLaunch = string.Equals(skill?.SkillName, "@be/settings", StringComparison.OrdinalIgnoreCase);
|
||||||
var isGlobalCommand = isStopCommand || isVolumeControl;
|
var isGlobalCommand = isStopCommand || isVolumeControl;
|
||||||
var isPhotoGalleryLaunch = string.Equals(plan.IntentName, "photo_gallery", StringComparison.OrdinalIgnoreCase);
|
var isPhotoGalleryLaunch = string.Equals(plan.IntentName, "photo_gallery", StringComparison.OrdinalIgnoreCase);
|
||||||
@@ -72,7 +74,8 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
? localIntent
|
? localIntent
|
||||||
: isWordOfDayGuess
|
: isWordOfDayGuess
|
||||||
? "guess"
|
? "guess"
|
||||||
: string.Equals(messageType, "CLIENT_NLU", StringComparison.OrdinalIgnoreCase) &&
|
: string.Equals(messageType, "CLIENT_NLU",
|
||||||
|
StringComparison.OrdinalIgnoreCase) &&
|
||||||
!string.IsNullOrWhiteSpace(clientIntent)
|
!string.IsNullOrWhiteSpace(clientIntent)
|
||||||
? clientIntent
|
? clientIntent
|
||||||
: plan.IntentName ?? "unknown";
|
: plan.IntentName ?? "unknown";
|
||||||
@@ -209,10 +212,10 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
outboundAsrText,
|
outboundAsrText,
|
||||||
outboundRules,
|
outboundRules,
|
||||||
entities)),
|
entities)),
|
||||||
DelayMs: 75));
|
75));
|
||||||
messages.Add(new SocketReplyPlan(
|
messages.Add(new SocketReplyPlan(
|
||||||
JsonSerializer.Serialize(BuildCompletionOnlySkillPayload(transId, "@be/word-of-the-day")),
|
JsonSerializer.Serialize(BuildCompletionOnlySkillPayload(transId, "@be/word-of-the-day")),
|
||||||
DelayMs: 125));
|
125));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isRadioLaunch)
|
if (isRadioLaunch)
|
||||||
@@ -225,10 +228,10 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
outboundAsrText,
|
outboundAsrText,
|
||||||
outboundRules,
|
outboundRules,
|
||||||
entities)),
|
entities)),
|
||||||
DelayMs: 75));
|
75));
|
||||||
messages.Add(new SocketReplyPlan(
|
messages.Add(new SocketReplyPlan(
|
||||||
JsonSerializer.Serialize(BuildCompletionOnlySkillPayload(transId, "@be/radio")),
|
JsonSerializer.Serialize(BuildCompletionOnlySkillPayload(transId, "@be/radio")),
|
||||||
DelayMs: 125));
|
125));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isStopCommand)
|
if (isStopCommand)
|
||||||
@@ -241,10 +244,10 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
outboundAsrText,
|
outboundAsrText,
|
||||||
outboundRules,
|
outboundRules,
|
||||||
entities)),
|
entities)),
|
||||||
DelayMs: 75));
|
75));
|
||||||
messages.Add(new SocketReplyPlan(
|
messages.Add(new SocketReplyPlan(
|
||||||
JsonSerializer.Serialize(BuildCompletionOnlySkillPayload(transId, "@be/idle")),
|
JsonSerializer.Serialize(BuildCompletionOnlySkillPayload(transId, "@be/idle")),
|
||||||
DelayMs: 125));
|
125));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSettingsLaunch &&
|
if (isSettingsLaunch &&
|
||||||
@@ -258,10 +261,10 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
outboundAsrText,
|
outboundAsrText,
|
||||||
outboundRules,
|
outboundRules,
|
||||||
entities)),
|
entities)),
|
||||||
DelayMs: 75));
|
75));
|
||||||
messages.Add(new SocketReplyPlan(
|
messages.Add(new SocketReplyPlan(
|
||||||
JsonSerializer.Serialize(BuildCompletionOnlySkillPayload(transId, "@be/settings")),
|
JsonSerializer.Serialize(BuildCompletionOnlySkillPayload(transId, "@be/settings")),
|
||||||
DelayMs: 125));
|
125));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isClockSkillLaunch &&
|
if (isClockSkillLaunch &&
|
||||||
@@ -276,10 +279,10 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
outboundAsrText,
|
outboundAsrText,
|
||||||
outboundRules,
|
outboundRules,
|
||||||
entities)),
|
entities)),
|
||||||
DelayMs: 75));
|
75));
|
||||||
messages.Add(new SocketReplyPlan(
|
messages.Add(new SocketReplyPlan(
|
||||||
JsonSerializer.Serialize(BuildCompletionOnlySkillPayload(transId, "@be/clock")),
|
JsonSerializer.Serialize(BuildCompletionOnlySkillPayload(transId, "@be/clock")),
|
||||||
DelayMs: 125));
|
125));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((isPhotoGalleryLaunch || isPhotoCreateLaunch) &&
|
if ((isPhotoGalleryLaunch || isPhotoCreateLaunch) &&
|
||||||
@@ -294,34 +297,16 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
outboundAsrText,
|
outboundAsrText,
|
||||||
outboundRules,
|
outboundRules,
|
||||||
entities)),
|
entities)),
|
||||||
DelayMs: 75));
|
75));
|
||||||
messages.Add(new SocketReplyPlan(
|
messages.Add(new SocketReplyPlan(
|
||||||
JsonSerializer.Serialize(BuildCompletionOnlySkillPayload(transId, skillId)),
|
JsonSerializer.Serialize(BuildCompletionOnlySkillPayload(transId, skillId)),
|
||||||
DelayMs: 125));
|
125));
|
||||||
}
|
|
||||||
|
|
||||||
if (isReportSkillLaunch)
|
|
||||||
{
|
|
||||||
messages.Add(new SocketReplyPlan(
|
|
||||||
JsonSerializer.Serialize(BuildSkillRedirectPayload(
|
|
||||||
transId,
|
|
||||||
"report-skill",
|
|
||||||
outboundIntent,
|
|
||||||
outboundAsrText,
|
|
||||||
outboundRules,
|
|
||||||
entities)),
|
|
||||||
DelayMs: 75));
|
|
||||||
messages.Add(new SocketReplyPlan(
|
|
||||||
JsonSerializer.Serialize(BuildCompletionOnlySkillPayload(transId, "report-skill")),
|
|
||||||
DelayMs: 125));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (emitSkillActions && speak is not null)
|
if (emitSkillActions && speak is not null)
|
||||||
{
|
|
||||||
messages.Add(new SocketReplyPlan(
|
messages.Add(new SocketReplyPlan(
|
||||||
JsonSerializer.Serialize(BuildSkillPayload(plan, turn, transId, speak, skill)),
|
JsonSerializer.Serialize(BuildSkillPayload(plan, turn, transId, speak, skill)),
|
||||||
DelayMs: 75));
|
75));
|
||||||
}
|
|
||||||
|
|
||||||
return messages;
|
return messages;
|
||||||
}
|
}
|
||||||
@@ -367,7 +352,7 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
transID = transId,
|
transID = transId,
|
||||||
data = new { }
|
data = new { }
|
||||||
})),
|
})),
|
||||||
new SocketReplyPlan(JsonSerializer.Serialize(BuildGenericFallbackSkillPayload(transId)), DelayMs: 75)
|
new SocketReplyPlan(JsonSerializer.Serialize(BuildGenericFallbackSkillPayload(transId)), 75)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -442,10 +427,7 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
? "clientRules"
|
? "clientRules"
|
||||||
: "listenRules";
|
: "listenRules";
|
||||||
|
|
||||||
if (!turn.Attributes.TryGetValue(attributeName, out var value))
|
if (!turn.Attributes.TryGetValue(attributeName, out var value)) return [];
|
||||||
{
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return value switch
|
return value switch
|
||||||
{
|
{
|
||||||
@@ -481,10 +463,7 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
{
|
{
|
||||||
if (yesNoTurn)
|
if (yesNoTurn)
|
||||||
{
|
{
|
||||||
if (!includeCreateDomain)
|
if (!includeCreateDomain) return new Dictionary<string, object?>();
|
||||||
{
|
|
||||||
return new Dictionary<string, object?>();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Dictionary<string, object?>
|
return new Dictionary<string, object?>
|
||||||
{
|
{
|
||||||
@@ -493,20 +472,15 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (wordOfDayLaunch)
|
if (wordOfDayLaunch)
|
||||||
{
|
|
||||||
return new Dictionary<string, object?>
|
return new Dictionary<string, object?>
|
||||||
{
|
{
|
||||||
["domain"] = "word-of-the-day"
|
["domain"] = "word-of-the-day"
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
if (globalCommand)
|
if (globalCommand)
|
||||||
{
|
{
|
||||||
var entities = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
|
var entities = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
|
||||||
if (!string.IsNullOrWhiteSpace(volumeLevel))
|
if (!string.IsNullOrWhiteSpace(volumeLevel)) entities["volumeLevel"] = volumeLevel;
|
||||||
{
|
|
||||||
entities["volumeLevel"] = volumeLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
return entities;
|
return entities;
|
||||||
}
|
}
|
||||||
@@ -514,10 +488,7 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
if (radioLaunch)
|
if (radioLaunch)
|
||||||
{
|
{
|
||||||
var entities = new Dictionary<string, object?>();
|
var entities = new Dictionary<string, object?>();
|
||||||
if (!string.IsNullOrWhiteSpace(radioStation))
|
if (!string.IsNullOrWhiteSpace(radioStation)) entities["station"] = radioStation;
|
||||||
{
|
|
||||||
entities["station"] = radioStation;
|
|
||||||
}
|
|
||||||
|
|
||||||
return entities;
|
return entities;
|
||||||
}
|
}
|
||||||
@@ -525,10 +496,7 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
if (clockSkillLaunch)
|
if (clockSkillLaunch)
|
||||||
{
|
{
|
||||||
var entities = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
|
var entities = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
|
||||||
if (!string.IsNullOrWhiteSpace(clockDomain))
|
if (!string.IsNullOrWhiteSpace(clockDomain)) entities["domain"] = clockDomain;
|
||||||
{
|
|
||||||
entities["domain"] = clockDomain;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(clockDomain, "timer", StringComparison.OrdinalIgnoreCase) &&
|
if (string.Equals(clockDomain, "timer", StringComparison.OrdinalIgnoreCase) &&
|
||||||
!string.IsNullOrWhiteSpace(timerHours + timerMinutes + timerSeconds))
|
!string.IsNullOrWhiteSpace(timerHours + timerMinutes + timerSeconds))
|
||||||
@@ -550,32 +518,22 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
if (reportSkillLaunch)
|
if (reportSkillLaunch)
|
||||||
{
|
{
|
||||||
var entities = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
|
var entities = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
|
||||||
if (!string.IsNullOrWhiteSpace(reportDate))
|
if (!string.IsNullOrWhiteSpace(reportDate)) entities["date"] = reportDate;
|
||||||
{
|
|
||||||
entities["date"] = reportDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(reportWeatherCondition))
|
if (!string.IsNullOrWhiteSpace(reportWeatherCondition)) entities["Weather"] = reportWeatherCondition;
|
||||||
{
|
|
||||||
entities["Weather"] = reportWeatherCondition;
|
|
||||||
}
|
|
||||||
|
|
||||||
return entities;
|
return entities;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wordOfDayGuess)
|
if (wordOfDayGuess)
|
||||||
{
|
|
||||||
return new Dictionary<string, object?>
|
return new Dictionary<string, object?>
|
||||||
{
|
{
|
||||||
["guess"] = guess ?? string.Empty
|
["guess"] = guess ?? string.Empty
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.Equals(messageType, "CLIENT_NLU", StringComparison.OrdinalIgnoreCase) ||
|
if (!string.Equals(messageType, "CLIENT_NLU", StringComparison.OrdinalIgnoreCase) ||
|
||||||
!turn.Attributes.TryGetValue("clientEntities", out var value) || value is null)
|
!turn.Attributes.TryGetValue("clientEntities", out var value) || value is null)
|
||||||
{
|
|
||||||
return new Dictionary<string, object?>();
|
return new Dictionary<string, object?>();
|
||||||
}
|
|
||||||
|
|
||||||
return value switch
|
return value switch
|
||||||
{
|
{
|
||||||
@@ -611,10 +569,7 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
|
|
||||||
private static IEnumerable<string> ReadRuleValues(TurnContext turn, string key)
|
private static IEnumerable<string> ReadRuleValues(TurnContext turn, string key)
|
||||||
{
|
{
|
||||||
if (!turn.Attributes.TryGetValue(key, out var value) || value is null)
|
if (!turn.Attributes.TryGetValue(key, out var value) || value is null) return [];
|
||||||
{
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return value switch
|
return value switch
|
||||||
{
|
{
|
||||||
@@ -636,10 +591,7 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
|
|
||||||
private static string? ReadClientEntity(TurnContext turn, string entityName)
|
private static string? ReadClientEntity(TurnContext turn, string entityName)
|
||||||
{
|
{
|
||||||
if (!turn.Attributes.TryGetValue("clientEntities", out var value) || value is null)
|
if (!turn.Attributes.TryGetValue("clientEntities", out var value) || value is null) return null;
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value switch
|
return value switch
|
||||||
{
|
{
|
||||||
@@ -657,20 +609,14 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
|
|
||||||
private static string? ReadSkillPayloadString(InvokeNativeSkillAction? skill, string key)
|
private static string? ReadSkillPayloadString(InvokeNativeSkillAction? skill, string key)
|
||||||
{
|
{
|
||||||
if (skill?.Payload is null || !skill.Payload.TryGetValue(key, out var value))
|
if (skill?.Payload is null || !skill.Payload.TryGetValue(key, out var value)) return null;
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value?.ToString();
|
return value?.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ResolveWordOfDayGuess(TurnContext turn, string transcript, string? nluGuess)
|
private static string ResolveWordOfDayGuess(TurnContext turn, string transcript, string? nluGuess)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(nluGuess))
|
if (!string.IsNullOrWhiteSpace(nluGuess)) return nluGuess;
|
||||||
{
|
|
||||||
return nluGuess;
|
|
||||||
}
|
|
||||||
|
|
||||||
var normalized = NormalizeGuessToken(transcript);
|
var normalized = NormalizeGuessToken(transcript);
|
||||||
var hintIndex = normalized switch
|
var hintIndex = normalized switch
|
||||||
@@ -684,11 +630,9 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
var hints = ReadRuleValues(turn, "listenAsrHints").ToArray();
|
var hints = ReadRuleValues(turn, "listenAsrHints").ToArray();
|
||||||
|
|
||||||
if (hintIndex >= 0)
|
if (hintIndex >= 0)
|
||||||
{
|
|
||||||
return hintIndex < hints.Length
|
return hintIndex < hints.Length
|
||||||
? hints[hintIndex]
|
? hints[hintIndex]
|
||||||
: transcript;
|
: transcript;
|
||||||
}
|
|
||||||
|
|
||||||
var fuzzyHintMatch = FindClosestHint(normalized, hints);
|
var fuzzyHintMatch = FindClosestHint(normalized, hints);
|
||||||
return string.IsNullOrWhiteSpace(fuzzyHintMatch)
|
return string.IsNullOrWhiteSpace(fuzzyHintMatch)
|
||||||
@@ -698,31 +642,19 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
|
|
||||||
private static string? FindClosestHint(string normalizedTranscript, IReadOnlyList<string> hints)
|
private static string? FindClosestHint(string normalizedTranscript, IReadOnlyList<string> hints)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(normalizedTranscript))
|
if (string.IsNullOrWhiteSpace(normalizedTranscript)) return null;
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
string? bestHint = null;
|
string? bestHint = null;
|
||||||
var bestDistance = int.MaxValue;
|
var bestDistance = int.MaxValue;
|
||||||
|
|
||||||
foreach (var hint in hints)
|
foreach (var hint in hints)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(hint))
|
if (string.IsNullOrWhiteSpace(hint)) continue;
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var normalizedHint = NormalizeGuessToken(hint);
|
var normalizedHint = NormalizeGuessToken(hint);
|
||||||
if (string.IsNullOrWhiteSpace(normalizedHint))
|
if (string.IsNullOrWhiteSpace(normalizedHint)) continue;
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(normalizedTranscript, normalizedHint, StringComparison.Ordinal))
|
if (string.Equals(normalizedTranscript, normalizedHint, StringComparison.Ordinal)) return hint;
|
||||||
{
|
|
||||||
return hint;
|
|
||||||
}
|
|
||||||
|
|
||||||
var distance = ComputeEditDistance(normalizedTranscript, normalizedHint);
|
var distance = ComputeEditDistance(normalizedTranscript, normalizedHint);
|
||||||
if (distance >= bestDistance) continue;
|
if (distance >= bestDistance) continue;
|
||||||
@@ -744,10 +676,7 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
var previous = new int[right.Length + 1];
|
var previous = new int[right.Length + 1];
|
||||||
var current = new int[right.Length + 1];
|
var current = new int[right.Length + 1];
|
||||||
|
|
||||||
for (var column = 0; column <= right.Length; column += 1)
|
for (var column = 0; column <= right.Length; column += 1) previous[column] = column;
|
||||||
{
|
|
||||||
previous[column] = column;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var row = 1; row <= left.Length; row += 1)
|
for (var row = 1; row <= left.Length; row += 1)
|
||||||
{
|
{
|
||||||
@@ -772,11 +701,9 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
var skillPayload = skill?.Payload;
|
var skillPayload = skill?.Payload;
|
||||||
if (string.Equals(ReadPayloadString(skillPayload, "cloudResponseMode"), "completion_only",
|
if (string.Equals(ReadPayloadString(skillPayload, "cloudResponseMode"), "completion_only",
|
||||||
StringComparison.OrdinalIgnoreCase))
|
StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
|
||||||
return BuildCompletionOnlySkillPayload(
|
return BuildCompletionOnlySkillPayload(
|
||||||
transId,
|
transId,
|
||||||
ReadPayloadString(skillPayload, "skillId") ?? skill?.SkillName ?? "chitchat-skill");
|
ReadPayloadString(skillPayload, "skillId") ?? skill?.SkillName ?? "chitchat-skill");
|
||||||
}
|
|
||||||
|
|
||||||
var isJoke = string.Equals(plan.IntentName, "joke", StringComparison.OrdinalIgnoreCase) ||
|
var isJoke = string.Equals(plan.IntentName, "joke", StringComparison.OrdinalIgnoreCase) ||
|
||||||
string.Equals(skill?.SkillName, "@be/joke", StringComparison.OrdinalIgnoreCase);
|
string.Equals(skill?.SkillName, "@be/joke", StringComparison.OrdinalIgnoreCase);
|
||||||
@@ -795,29 +722,106 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
var promptId = ReadPayloadString(skillPayload, "prompt_id") ?? "RUNTIME_PROMPT";
|
var promptId = ReadPayloadString(skillPayload, "prompt_id") ?? "RUNTIME_PROMPT";
|
||||||
var promptSubCategory = ReadPayloadString(skillPayload, "prompt_sub_category") ?? "AN";
|
var promptSubCategory = ReadPayloadString(skillPayload, "prompt_sub_category") ?? "AN";
|
||||||
var listenContexts = ReadPayloadStringArray(skillPayload, "listen_contexts");
|
var listenContexts = ReadPayloadStringArray(skillPayload, "listen_contexts");
|
||||||
var jcpConfig = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
var playConfig = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
["play"] = new
|
["esml"] = esml,
|
||||||
{
|
["meta"] = new
|
||||||
esml,
|
|
||||||
meta = new
|
|
||||||
{
|
{
|
||||||
prompt_id = promptId,
|
prompt_id = promptId,
|
||||||
prompt_sub_category = promptSubCategory,
|
prompt_sub_category = promptSubCategory,
|
||||||
mim_id = mimId,
|
mim_id = mimId,
|
||||||
mim_type = mimType
|
mim_type = mimType
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
var jcpConfig = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["play"] = playConfig
|
||||||
};
|
};
|
||||||
|
|
||||||
if (listenContexts.Count > 0)
|
if (listenContexts.Count > 0)
|
||||||
{
|
|
||||||
jcpConfig["listen"] = new
|
jcpConfig["listen"] = new
|
||||||
{
|
{
|
||||||
id = CreateProtocolId(),
|
id = CreateProtocolId(),
|
||||||
type = "LISTEN",
|
type = "LISTEN",
|
||||||
contexts = listenContexts
|
contexts = listenContexts
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var weatherHiLoView = BuildWeatherHiLoView(skillPayload);
|
||||||
|
var weeklyWeatherCards = BuildWeatherHiLoSequenceCards(skillPayload);
|
||||||
|
if (weatherHiLoView is null && weeklyWeatherCards.Count > 0) weatherHiLoView = weeklyWeatherCards[0].View;
|
||||||
|
|
||||||
|
var useWeatherSequence = false;
|
||||||
|
if (weatherHiLoView is not null)
|
||||||
|
{
|
||||||
|
var resolvedGuiContext = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["type"] = "Javascript",
|
||||||
|
["data"] = weatherHiLoView,
|
||||||
|
["pause"] = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var legacyGuiConfig = new
|
||||||
|
{
|
||||||
|
type = "Javascript",
|
||||||
|
data = "views.weatherHiLo",
|
||||||
|
pause = true
|
||||||
|
};
|
||||||
|
|
||||||
|
jcpConfig["gui"] = legacyGuiConfig;
|
||||||
|
jcpConfig["display"] = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["view"] = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
// Legacy fields used by existing tests and tooling.
|
||||||
|
["type"] = "Javascript",
|
||||||
|
["data"] = weatherHiLoView,
|
||||||
|
["pause"] = true,
|
||||||
|
// Pegasus-style view context used by on-robot weather cards.
|
||||||
|
["context"] = resolvedGuiContext
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
jcpConfig["timeout"] = 6;
|
||||||
|
jcpConfig["barge_in"] = true;
|
||||||
|
jcpConfig["no_matches_for_gui"] = 0;
|
||||||
|
jcpConfig["no_inputs_for_gui"] = 0;
|
||||||
|
|
||||||
|
var weatherViews = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["weatherHiLo"] = weatherHiLoView
|
||||||
|
};
|
||||||
|
jcpConfig["views"] = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["weatherHiLo"] = weatherHiLoView
|
||||||
|
};
|
||||||
|
jcpConfig["local"] = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["views"] = weatherViews
|
||||||
|
};
|
||||||
|
|
||||||
|
if (weeklyWeatherCards.Count > 1)
|
||||||
|
{
|
||||||
|
useWeatherSequence = true;
|
||||||
|
jcpConfig["children"] = BuildWeatherHiLoSequenceChildren(
|
||||||
|
weeklyWeatherCards,
|
||||||
|
promptSubCategory,
|
||||||
|
mimId,
|
||||||
|
mimType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var jcp = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["type"] = "SLIM",
|
||||||
|
["config"] = jcpConfig
|
||||||
|
};
|
||||||
|
if (useWeatherSequence &&
|
||||||
|
jcpConfig.TryGetValue("children", out var sequenceChildren) &&
|
||||||
|
sequenceChildren is not null)
|
||||||
|
{
|
||||||
|
jcp["type"] = "SEQUENCE";
|
||||||
|
jcp.Remove("config");
|
||||||
|
jcp["children"] = sequenceChildren;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new
|
return new
|
||||||
@@ -836,11 +840,7 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
{
|
{
|
||||||
config = new
|
config = new
|
||||||
{
|
{
|
||||||
jcp = new
|
jcp
|
||||||
{
|
|
||||||
type = "SLIM",
|
|
||||||
config = jcpConfig
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
analytics = new Dictionary<string, object?>(),
|
analytics = new Dictionary<string, object?>(),
|
||||||
@@ -864,15 +864,9 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
["entities"] = entities
|
["entities"] = entities
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(skillId))
|
if (!string.IsNullOrWhiteSpace(skillId)) payload["skill"] = skillId;
|
||||||
{
|
|
||||||
payload["skill"] = skillId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(domain))
|
if (!string.IsNullOrWhiteSpace(domain)) payload["domain"] = domain;
|
||||||
{
|
|
||||||
payload["domain"] = domain;
|
|
||||||
}
|
|
||||||
|
|
||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
@@ -1035,41 +1029,417 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
|
|
||||||
private static string? ReadPayloadString(IDictionary<string, object?>? payload, string key)
|
private static string? ReadPayloadString(IDictionary<string, object?>? payload, string key)
|
||||||
{
|
{
|
||||||
if (payload is null || !payload.TryGetValue(key, out var value))
|
if (payload is null || !payload.TryGetValue(key, out var value)) return null;
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value?.ToString();
|
return value?.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IReadOnlyList<string> ReadPayloadStringArray(IDictionary<string, object?>? payload, string key)
|
private static IReadOnlyList<string> ReadPayloadStringArray(IDictionary<string, object?>? payload, string key)
|
||||||
{
|
{
|
||||||
if (payload is null || !payload.TryGetValue(key, out var value) || value is null)
|
if (payload is null || !payload.TryGetValue(key, out var value) || value is null) return [];
|
||||||
{
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return value switch
|
return value switch
|
||||||
{
|
{
|
||||||
string text => [.. text
|
string text =>
|
||||||
|
[
|
||||||
|
.. text
|
||||||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||||
.Where(static context => !string.IsNullOrWhiteSpace(context))],
|
.Where(static context => !string.IsNullOrWhiteSpace(context))
|
||||||
|
],
|
||||||
string[] contexts => [.. contexts.Where(static context => !string.IsNullOrWhiteSpace(context))],
|
string[] contexts => [.. contexts.Where(static context => !string.IsNullOrWhiteSpace(context))],
|
||||||
IEnumerable<string> contexts => [.. contexts.Where(static context => !string.IsNullOrWhiteSpace(context))],
|
IEnumerable<string> contexts => [.. contexts.Where(static context => !string.IsNullOrWhiteSpace(context))],
|
||||||
JsonElement jsonElement when jsonElement.ValueKind == JsonValueKind.Array => [.. jsonElement
|
JsonElement jsonElement when jsonElement.ValueKind == JsonValueKind.Array =>
|
||||||
|
[
|
||||||
|
.. jsonElement
|
||||||
.EnumerateArray()
|
.EnumerateArray()
|
||||||
.Select(static item => item.GetString())
|
.Select(static item => item.GetString())
|
||||||
.Where(static context => !string.IsNullOrWhiteSpace(context))
|
.Where(static context => !string.IsNullOrWhiteSpace(context))
|
||||||
.Select(static context => context!)],
|
.Select(static context => context!)
|
||||||
IEnumerable<object?> contexts => [.. contexts
|
],
|
||||||
|
IEnumerable<object?> contexts =>
|
||||||
|
[
|
||||||
|
.. contexts
|
||||||
.Select(static context => context?.ToString())
|
.Select(static context => context?.ToString())
|
||||||
.Where(static context => !string.IsNullOrWhiteSpace(context))
|
.Where(static context => !string.IsNullOrWhiteSpace(context))
|
||||||
.Select(static context => context!)],
|
.Select(static context => context!)
|
||||||
|
],
|
||||||
_ => string.IsNullOrWhiteSpace(value.ToString()) ? [] : [value.ToString()!]
|
_ => string.IsNullOrWhiteSpace(value.ToString()) ? [] : [value.ToString()!]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<WeatherHiLoSequenceCard> BuildWeatherHiLoSequenceCards(
|
||||||
|
IDictionary<string, object?>? payload)
|
||||||
|
{
|
||||||
|
if (payload is null ||
|
||||||
|
!payload.TryGetValue("weather_weekly_cards", out var rawCards) ||
|
||||||
|
rawCards is null)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
var cards = ReadPayloadObjectArray(rawCards);
|
||||||
|
if (cards.Count == 0) return [];
|
||||||
|
|
||||||
|
var sequenceCards = new List<WeatherHiLoSequenceCard>(cards.Count);
|
||||||
|
foreach (var card in cards)
|
||||||
|
{
|
||||||
|
var weatherCardPayload = new Dictionary<string, object?>(card, StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["weather_view_enabled"] = true,
|
||||||
|
["weather_view_kind"] = "weatherHiLo"
|
||||||
|
};
|
||||||
|
var view = BuildWeatherHiLoView(weatherCardPayload);
|
||||||
|
if (view is null) continue;
|
||||||
|
|
||||||
|
sequenceCards.Add(new WeatherHiLoSequenceCard(
|
||||||
|
view,
|
||||||
|
ReadPayloadString(weatherCardPayload, "weather_day"),
|
||||||
|
ReadPayloadString(weatherCardPayload, "weather_icon"),
|
||||||
|
ReadPayloadString(weatherCardPayload, "weather_spoken_line")));
|
||||||
|
}
|
||||||
|
|
||||||
|
return sequenceCards;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<object> BuildWeatherHiLoSequenceChildren(
|
||||||
|
IReadOnlyList<WeatherHiLoSequenceCard> cards,
|
||||||
|
string promptSubCategory,
|
||||||
|
string mimId,
|
||||||
|
string mimType)
|
||||||
|
{
|
||||||
|
var children = new List<object>(cards.Count);
|
||||||
|
for (var index = 0; index < cards.Count; index += 1)
|
||||||
|
{
|
||||||
|
var card = cards[index];
|
||||||
|
var promptLabel = string.IsNullOrWhiteSpace(card.DayName)
|
||||||
|
? $"Day{index + 1}"
|
||||||
|
: Regex.Replace(card.DayName, "[^A-Za-z0-9]", string.Empty, RegexOptions.CultureInvariant);
|
||||||
|
var promptId = $"WeatherForecast{promptLabel}_AN_13";
|
||||||
|
var spokenLine = string.IsNullOrWhiteSpace(card.SpokenLine)
|
||||||
|
? "Here is another day's forecast."
|
||||||
|
: card.SpokenLine!;
|
||||||
|
var icon = string.IsNullOrWhiteSpace(card.Icon)
|
||||||
|
? "cloudy"
|
||||||
|
: card.Icon!;
|
||||||
|
var esml =
|
||||||
|
$"<speak><anim cat='weather' meta='{icon}' nonBlocking='true' /><break size='0.2'/><es cat='neutral' filter='!ssa-only, !sfx-only' endNeutral='true'>{EscapeXml(spokenLine)}</es></speak>";
|
||||||
|
var resolvedGuiContext = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["type"] = "Javascript",
|
||||||
|
["data"] = card.View,
|
||||||
|
["pause"] = true
|
||||||
|
};
|
||||||
|
|
||||||
|
children.Add(new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["type"] = "SLIM",
|
||||||
|
["config"] = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["play"] = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["esml"] = esml,
|
||||||
|
["meta"] = new
|
||||||
|
{
|
||||||
|
prompt_id = promptId,
|
||||||
|
prompt_sub_category = promptSubCategory,
|
||||||
|
mim_id = mimId,
|
||||||
|
mim_type = mimType
|
||||||
|
}
|
||||||
|
},
|
||||||
|
["gui"] = new
|
||||||
|
{
|
||||||
|
type = "Javascript",
|
||||||
|
data = "views.weatherHiLo",
|
||||||
|
pause = true
|
||||||
|
},
|
||||||
|
["display"] = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["view"] = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["type"] = "Javascript",
|
||||||
|
["data"] = card.View,
|
||||||
|
["pause"] = true,
|
||||||
|
["context"] = resolvedGuiContext
|
||||||
|
}
|
||||||
|
},
|
||||||
|
["timeout"] = 6,
|
||||||
|
["barge_in"] = true,
|
||||||
|
["no_matches_for_gui"] = 0,
|
||||||
|
["no_inputs_for_gui"] = 0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<IDictionary<string, object?>> ReadPayloadObjectArray(object rawValue)
|
||||||
|
{
|
||||||
|
if (rawValue is JsonElement jsonArray && jsonArray.ValueKind == JsonValueKind.Array)
|
||||||
|
return jsonArray
|
||||||
|
.EnumerateArray()
|
||||||
|
.Select(ConvertJsonObjectToDictionary)
|
||||||
|
.Where(static item => item is not null)
|
||||||
|
.Cast<IDictionary<string, object?>>()
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
if (rawValue is IEnumerable<object?> rawObjects)
|
||||||
|
return rawObjects
|
||||||
|
.Select(ConvertObjectToDictionary)
|
||||||
|
.Where(static item => item is not null)
|
||||||
|
.Cast<IDictionary<string, object?>>()
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IDictionary<string, object?>? ConvertObjectToDictionary(object? value)
|
||||||
|
{
|
||||||
|
if (value is null) return null;
|
||||||
|
|
||||||
|
if (value is IDictionary<string, object?> dictionary)
|
||||||
|
return new Dictionary<string, object?>(dictionary, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
return value is JsonElement jsonValue
|
||||||
|
? ConvertJsonObjectToDictionary(jsonValue)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IDictionary<string, object?>? ConvertJsonObjectToDictionary(JsonElement value)
|
||||||
|
{
|
||||||
|
if (value.ValueKind != JsonValueKind.Object) return null;
|
||||||
|
|
||||||
|
var dictionary = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var property in value.EnumerateObject())
|
||||||
|
dictionary[property.Name] = property.Value.ValueKind switch
|
||||||
|
{
|
||||||
|
JsonValueKind.String => property.Value.GetString(),
|
||||||
|
JsonValueKind.Number when property.Value.TryGetInt32(out var intValue) => intValue,
|
||||||
|
JsonValueKind.Number when property.Value.TryGetDouble(out var doubleValue) => doubleValue,
|
||||||
|
JsonValueKind.True => true,
|
||||||
|
JsonValueKind.False => false,
|
||||||
|
JsonValueKind.Object => ConvertJsonObjectToDictionary(property.Value),
|
||||||
|
JsonValueKind.Array => property.Value,
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
|
||||||
|
return dictionary;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object? BuildWeatherHiLoView(IDictionary<string, object?>? payload)
|
||||||
|
{
|
||||||
|
if (!TryReadPayloadBool(payload, "weather_view_enabled")) return null;
|
||||||
|
|
||||||
|
if (!string.Equals(
|
||||||
|
ReadPayloadString(payload, "weather_view_kind"),
|
||||||
|
"weatherHiLo",
|
||||||
|
StringComparison.OrdinalIgnoreCase))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var icon = ReadPayloadString(payload, "weather_icon");
|
||||||
|
var unit = ReadPayloadString(payload, "weather_unit") ?? "F";
|
||||||
|
var theme = ReadPayloadString(payload, "weather_theme") ?? "Normal";
|
||||||
|
var high = TryReadPayloadInt(payload, "weather_high");
|
||||||
|
var low = TryReadPayloadInt(payload, "weather_low");
|
||||||
|
if (string.IsNullOrWhiteSpace(icon) || high is null || low is null) return null;
|
||||||
|
|
||||||
|
var hiNumX = GetTemperatureLabelXPosition(370, high.Value);
|
||||||
|
var hiUnitX = GetTemperatureLabelXPosition(360, high.Value);
|
||||||
|
var loNumX = GetTemperatureLabelXPosition(1110, low.Value);
|
||||||
|
var loUnitX = GetTemperatureLabelXPosition(1100, low.Value);
|
||||||
|
|
||||||
|
return new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["viewConfig"] = new
|
||||||
|
{
|
||||||
|
type = "View",
|
||||||
|
id = "weatherTempView",
|
||||||
|
category = "gui"
|
||||||
|
},
|
||||||
|
["open"] = new
|
||||||
|
{
|
||||||
|
transitionOpen = "trans_in",
|
||||||
|
removeAll = true
|
||||||
|
},
|
||||||
|
["defaultSelect"] = new
|
||||||
|
{
|
||||||
|
transitionClose = "trans_out",
|
||||||
|
removeAll = true,
|
||||||
|
leaveEmpty = false
|
||||||
|
},
|
||||||
|
["componentConfigs"] = new object[]
|
||||||
|
{
|
||||||
|
new
|
||||||
|
{
|
||||||
|
id = "tempBGClip",
|
||||||
|
type = "Clip",
|
||||||
|
assets = new object[]
|
||||||
|
{
|
||||||
|
new
|
||||||
|
{
|
||||||
|
id = "tempBG",
|
||||||
|
src = $"assets/personal-report-skill/weather/bg/temp{theme}_v01.crn",
|
||||||
|
type = "texture"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
position = new { x = 36, y = 0 }
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
id = "iconClip",
|
||||||
|
type = "Clip",
|
||||||
|
assets = new object[]
|
||||||
|
{
|
||||||
|
new
|
||||||
|
{
|
||||||
|
id = "icon",
|
||||||
|
src = $"assets/personal-report-skill/weather/icons/{icon}_v01.crn",
|
||||||
|
type = "texture"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
position = new { x = 475, y = 195 }
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
id = "hiNumLabel",
|
||||||
|
type = "Label",
|
||||||
|
text = $"{high.Value}°",
|
||||||
|
style = new
|
||||||
|
{
|
||||||
|
fontSize = "160",
|
||||||
|
fontFamily = "Proxima Nova Soft",
|
||||||
|
fontWeight = "bold",
|
||||||
|
fill = "#FFFFFF",
|
||||||
|
align = "center"
|
||||||
|
},
|
||||||
|
position = new { x = hiNumX, y = 430 },
|
||||||
|
targetAnchor = new { x = 1, y = 1 }
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
id = "hiUnitLabel",
|
||||||
|
type = "Label",
|
||||||
|
text = unit,
|
||||||
|
style = new
|
||||||
|
{
|
||||||
|
fontSize = "90",
|
||||||
|
fontFamily = "Proxima Nova Soft",
|
||||||
|
fontWeight = "bold",
|
||||||
|
fill = "#FFFFFF",
|
||||||
|
align = "center"
|
||||||
|
},
|
||||||
|
position = new { x = hiUnitX, y = 418 },
|
||||||
|
targetAnchor = new { x = 0, y = 1 }
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
id = "loNumLabel",
|
||||||
|
type = "Label",
|
||||||
|
text = $"{low.Value}°",
|
||||||
|
style = new
|
||||||
|
{
|
||||||
|
fontSize = "160",
|
||||||
|
fontFamily = "Proxima Nova Soft",
|
||||||
|
fontWeight = "bold",
|
||||||
|
fill = "#FFFFFF",
|
||||||
|
align = "center"
|
||||||
|
},
|
||||||
|
position = new { x = loNumX, y = 430 },
|
||||||
|
targetAnchor = new { x = 1, y = 1 }
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
id = "loUnitLabel",
|
||||||
|
type = "Label",
|
||||||
|
text = unit,
|
||||||
|
style = new
|
||||||
|
{
|
||||||
|
fontSize = "90",
|
||||||
|
fontFamily = "Proxima Nova Soft",
|
||||||
|
fontWeight = "bold",
|
||||||
|
fill = "#FFFFFF",
|
||||||
|
align = "center"
|
||||||
|
},
|
||||||
|
position = new { x = loUnitX, y = 418 },
|
||||||
|
targetAnchor = new { x = 0, y = 1 }
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
id = "hiTextLabel",
|
||||||
|
type = "Label",
|
||||||
|
text = "Hi",
|
||||||
|
style = new
|
||||||
|
{
|
||||||
|
fontSize = "60",
|
||||||
|
fontFamily = "Proxima Nova Light",
|
||||||
|
fill = "#FFFFFF",
|
||||||
|
align = "center"
|
||||||
|
},
|
||||||
|
position = new { x = 280, y = 496 },
|
||||||
|
targetAnchor = new { x = 0.5, y = 1 }
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
id = "loTextLabel",
|
||||||
|
type = "Label",
|
||||||
|
text = "Lo",
|
||||||
|
style = new
|
||||||
|
{
|
||||||
|
fontSize = "60",
|
||||||
|
fontFamily = "Proxima Nova Light",
|
||||||
|
fill = "#FFFFFF",
|
||||||
|
align = "center"
|
||||||
|
},
|
||||||
|
position = new { x = 990, y = 496 },
|
||||||
|
targetAnchor = new { x = 0.5, y = 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetTemperatureLabelXPosition(int baseX, int temperature)
|
||||||
|
{
|
||||||
|
const int xOffset = 70;
|
||||||
|
if (temperature < -9 || temperature > 99) return baseX + xOffset;
|
||||||
|
|
||||||
|
if (temperature is >= 0 and < 10) return baseX - xOffset;
|
||||||
|
|
||||||
|
return baseX;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int? TryReadPayloadInt(IDictionary<string, object?>? payload, string key)
|
||||||
|
{
|
||||||
|
if (payload is null || !payload.TryGetValue(key, out var value) || value is null) return null;
|
||||||
|
|
||||||
|
return value switch
|
||||||
|
{
|
||||||
|
int number => number,
|
||||||
|
long number when number <= int.MaxValue && number >= int.MinValue => (int)number,
|
||||||
|
double number => (int)Math.Round(number, MidpointRounding.AwayFromZero),
|
||||||
|
float number => (int)Math.Round(number, MidpointRounding.AwayFromZero),
|
||||||
|
string text when int.TryParse(text, out var parsed) => parsed,
|
||||||
|
JsonElement { ValueKind: JsonValueKind.Number } jsonNumber when jsonNumber.TryGetInt32(out var parsed) =>
|
||||||
|
parsed,
|
||||||
|
JsonElement jsonText when jsonText.ValueKind == JsonValueKind.String &&
|
||||||
|
int.TryParse(jsonText.GetString(), out var parsed) => parsed,
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadPayloadBool(IDictionary<string, object?>? payload, string key)
|
||||||
|
{
|
||||||
|
if (payload is null || !payload.TryGetValue(key, out var value) || value is null) return false;
|
||||||
|
|
||||||
|
return value switch
|
||||||
|
{
|
||||||
|
bool flag => flag,
|
||||||
|
string text when bool.TryParse(text, out var parsed) => parsed,
|
||||||
|
JsonElement { ValueKind: JsonValueKind.True } => true,
|
||||||
|
JsonElement { ValueKind: JsonValueKind.False } => false,
|
||||||
|
JsonElement jsonText when jsonText.ValueKind == JsonValueKind.String &&
|
||||||
|
bool.TryParse(jsonText.GetString(), out var parsed) => parsed,
|
||||||
|
_ => false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private static string CreateHubMessageId()
|
private static string CreateHubMessageId()
|
||||||
{
|
{
|
||||||
return $"mid-{Guid.NewGuid()}";
|
return $"mid-{Guid.NewGuid()}";
|
||||||
@@ -1080,5 +1450,11 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
return Guid.NewGuid().ToString("N");
|
return Guid.NewGuid().ToString("N");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed record WeatherHiLoSequenceCard(
|
||||||
|
object View,
|
||||||
|
string? DayName,
|
||||||
|
string? Icon,
|
||||||
|
string? SpokenLine);
|
||||||
|
|
||||||
public sealed record SocketReplyPlan(string Text, int DelayMs = 0);
|
public sealed record SocketReplyPlan(string Text, int DelayMs = 0);
|
||||||
}
|
}
|
||||||
@@ -16,13 +16,11 @@ public sealed class SyntheticBufferedAudioSttStrategy : ISttStrategy
|
|||||||
{
|
{
|
||||||
var transcriptHint = ReadTranscriptHint(turn);
|
var transcriptHint = ReadTranscriptHint(turn);
|
||||||
if (string.IsNullOrWhiteSpace(transcriptHint))
|
if (string.IsNullOrWhiteSpace(transcriptHint))
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Synthetic buffered audio STT requires an audio transcript hint.");
|
throw new InvalidOperationException("Synthetic buffered audio STT requires an audio transcript hint.");
|
||||||
}
|
|
||||||
|
|
||||||
return Task.FromResult(new SttResult
|
return Task.FromResult(new SttResult
|
||||||
{
|
{
|
||||||
Text = transcriptHint.Trim(),
|
Text = NormalizeLooseTranscript(transcriptHint),
|
||||||
Provider = Name,
|
Provider = Name,
|
||||||
Confidence = 0.75f,
|
Confidence = 0.75f,
|
||||||
Locale = turn.Locale,
|
Locale = turn.Locale,
|
||||||
@@ -36,10 +34,7 @@ public sealed class SyntheticBufferedAudioSttStrategy : ISttStrategy
|
|||||||
|
|
||||||
private static int ReadBufferedAudioBytes(TurnContext turn)
|
private static int ReadBufferedAudioBytes(TurnContext turn)
|
||||||
{
|
{
|
||||||
if (!turn.Attributes.TryGetValue("bufferedAudioBytes", out var bufferedAudioBytes))
|
if (!turn.Attributes.TryGetValue("bufferedAudioBytes", out var bufferedAudioBytes)) return 0;
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return bufferedAudioBytes switch
|
return bufferedAudioBytes switch
|
||||||
{
|
{
|
||||||
@@ -56,4 +51,16 @@ public sealed class SyntheticBufferedAudioSttStrategy : ISttStrategy
|
|||||||
? transcriptHint?.ToString()
|
? transcriptHint?.ToString()
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string NormalizeLooseTranscript(string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value)) return string.Empty;
|
||||||
|
|
||||||
|
var lowered = value.Trim().ToLowerInvariant();
|
||||||
|
lowered = System.Text.RegularExpressions.Regex.Replace(lowered, @"[^\p{L}\p{N}\s']+", " ",
|
||||||
|
System.Text.RegularExpressions.RegexOptions.CultureInvariant | System.Text.RegularExpressions.RegexOptions.Compiled);
|
||||||
|
lowered = System.Text.RegularExpressions.Regex.Replace(lowered, @"\s+"," ",
|
||||||
|
System.Text.RegularExpressions.RegexOptions.CultureInvariant | System.Text.RegularExpressions.RegexOptions.Compiled);
|
||||||
|
return lowered.Trim();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace Jibo.Cloud.Application.Services;
|
||||||
|
|
||||||
|
internal static partial class TranscriptTextNormalizer
|
||||||
|
{
|
||||||
|
private static readonly Regex PunctuationToSpaceRegex = new(
|
||||||
|
@"[^\p{L}\p{N}\s']+",
|
||||||
|
RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
||||||
|
|
||||||
|
private static readonly Regex WhitespaceRegex = new(
|
||||||
|
@"\s+",
|
||||||
|
RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
||||||
|
|
||||||
|
internal static string NormalizeLooseText(string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value)) return string.Empty;
|
||||||
|
|
||||||
|
return WhitespaceRegex.Replace(
|
||||||
|
PunctuationToSpaceRegex.Replace(value.Trim().ToLowerInvariant(), " "),
|
||||||
|
" ")
|
||||||
|
.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string StripLeadingPhrases(string value, params string[] phrases)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value) || phrases.Length == 0) return value;
|
||||||
|
|
||||||
|
var normalized = value;
|
||||||
|
while (TryStripLeadingPhrase(normalized, phrases, out var trimmed))
|
||||||
|
normalized = trimmed;
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryStripLeadingPhrase(string normalizedValue, IReadOnlyList<string> phrases, out string trimmed)
|
||||||
|
{
|
||||||
|
foreach (var phrase in phrases)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(phrase)) continue;
|
||||||
|
|
||||||
|
if (string.Equals(normalizedValue, phrase, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
trimmed = string.Empty;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedValue.StartsWith($"{phrase} ", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
trimmed = normalizedValue[(phrase.Length + 1)..].TrimStart();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trimmed = normalizedValue;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -7,5 +7,7 @@ public sealed class CapturedExchange
|
|||||||
public ProtocolEnvelope Request { get; init; } = new();
|
public ProtocolEnvelope Request { get; init; } = new();
|
||||||
public ProtocolDispatchResult Response { get; init; } = ProtocolDispatchResult.Ok();
|
public ProtocolDispatchResult Response { get; init; } = ProtocolDispatchResult.Ok();
|
||||||
public string Confidence { get; init; } = "observed";
|
public string Confidence { get; init; } = "observed";
|
||||||
public IDictionary<string, string> Tags { get; init; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
public IDictionary<string, string> Tags { get; init; } =
|
||||||
|
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
@@ -8,5 +8,7 @@ public sealed class DeviceRegistration
|
|||||||
public string? FirmwareVersion { get; init; }
|
public string? FirmwareVersion { get; init; }
|
||||||
public string? ApplicationVersion { get; init; }
|
public string? ApplicationVersion { get; init; }
|
||||||
public bool IsActive { get; init; } = true;
|
public bool IsActive { get; init; } = true;
|
||||||
public IDictionary<string, string> HostMappings { get; init; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
public IDictionary<string, string> HostMappings { get; init; } =
|
||||||
|
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace Jibo.Cloud.Domain.Models;
|
||||||
|
|
||||||
|
public sealed class PersonRecord
|
||||||
|
{
|
||||||
|
public string PersonId { get; init; } = "person-openjibo-owner";
|
||||||
|
public string AccountId { get; init; } = "usr_openjibo_owner";
|
||||||
|
public string LoopId { get; init; } = "openjibo-default-loop";
|
||||||
|
public string RobotId { get; init; } = "my-robot-name";
|
||||||
|
public string DisplayName { get; init; } = "Jibo Owner";
|
||||||
|
public string? Alias { get; init; }
|
||||||
|
public bool IsPrimary { get; init; } = true;
|
||||||
|
public DateTimeOffset CreatedUtc { get; init; } = DateTimeOffset.UtcNow;
|
||||||
|
public DateTimeOffset UpdatedUtc { get; init; } = DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
@@ -7,7 +7,9 @@ public sealed class ProtocolDispatchResult
|
|||||||
public int StatusCode { get; init; } = 200;
|
public int StatusCode { get; init; } = 200;
|
||||||
public string ContentType { get; init; } = "application/x-amz-json-1.1";
|
public string ContentType { get; init; } = "application/x-amz-json-1.1";
|
||||||
public string BodyText { get; init; } = "{}";
|
public string BodyText { get; init; } = "{}";
|
||||||
public IDictionary<string, string> Headers { get; init; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
public IDictionary<string, string> Headers { get; init; } =
|
||||||
|
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
public static ProtocolDispatchResult Ok(object? body = null)
|
public static ProtocolDispatchResult Ok(object? body = null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,14 +17,13 @@ public sealed class ProtocolEnvelope
|
|||||||
public string? FirmwareVersion { get; init; }
|
public string? FirmwareVersion { get; init; }
|
||||||
public string? ApplicationVersion { get; init; }
|
public string? ApplicationVersion { get; init; }
|
||||||
public string BodyText { get; init; } = string.Empty;
|
public string BodyText { get; init; } = string.Empty;
|
||||||
public IDictionary<string, string> Headers { get; init; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
public IDictionary<string, string> Headers { get; init; } =
|
||||||
|
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
public JsonElement? TryParseBody()
|
public JsonElement? TryParseBody()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(BodyText))
|
if (string.IsNullOrWhiteSpace(BodyText)) return null;
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -20,5 +20,7 @@ public sealed class WebSocketTelemetryRecord
|
|||||||
public int BufferedAudioChunks { get; init; }
|
public int BufferedAudioChunks { get; init; }
|
||||||
public int FinalizeAttempts { get; init; }
|
public int FinalizeAttempts { get; init; }
|
||||||
public bool AwaitingTurnCompletion { get; init; }
|
public bool AwaitingTurnCompletion { get; init; }
|
||||||
public IReadOnlyDictionary<string, object?> Details { get; init; } = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
public IReadOnlyDictionary<string, object?> Details { get; init; } =
|
||||||
|
new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@ public sealed class WebSocketTurnState
|
|||||||
|
|
||||||
public string? TransId { get; set; }
|
public string? TransId { get; set; }
|
||||||
public string? ContextPayload { get; set; }
|
public string? ContextPayload { get; set; }
|
||||||
|
public DateTimeOffset? ListenOpenedUtc { get; set; }
|
||||||
public bool ListenHotphrase { get; set; }
|
public bool ListenHotphrase { get; set; }
|
||||||
public int HotphraseEmptyTurnCount { get; set; }
|
public int HotphraseEmptyTurnCount { get; set; }
|
||||||
public DateTimeOffset? IgnoreAdditionalAudioUntilUtc { get; set; }
|
public DateTimeOffset? IgnoreAdditionalAudioUntilUtc { get; set; }
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace Jibo.Cloud.Infrastructure.Audio;
|
||||||
|
|
||||||
|
internal static class AudioTranscriptNormalizer
|
||||||
|
{
|
||||||
|
private static readonly Regex PunctuationToSpaceRegex = new(
|
||||||
|
@"[^\p{L}\p{N}\s']+",
|
||||||
|
RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
||||||
|
|
||||||
|
private static readonly Regex WhitespaceRegex = new(
|
||||||
|
@"\s+",
|
||||||
|
RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
||||||
|
|
||||||
|
public static string NormalizeLooseTranscript(string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value)) return string.Empty;
|
||||||
|
|
||||||
|
return WhitespaceRegex.Replace(
|
||||||
|
PunctuationToSpaceRegex.Replace(value.Trim().ToLowerInvariant(), " "),
|
||||||
|
" ")
|
||||||
|
.Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,8 @@ namespace Jibo.Cloud.Infrastructure.Audio;
|
|||||||
|
|
||||||
public sealed class ExternalProcessRunner : IExternalProcessRunner
|
public sealed class ExternalProcessRunner : IExternalProcessRunner
|
||||||
{
|
{
|
||||||
public async Task<ExternalProcessResult> RunAsync(string fileName, IReadOnlyList<string> arguments, CancellationToken cancellationToken = default)
|
public async Task<ExternalProcessResult> RunAsync(string fileName, IReadOnlyList<string> arguments,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
using var process = new Process();
|
using var process = new Process();
|
||||||
process.StartInfo = new ProcessStartInfo
|
process.StartInfo = new ProcessStartInfo
|
||||||
@@ -16,10 +17,7 @@ public sealed class ExternalProcessRunner : IExternalProcessRunner
|
|||||||
CreateNoWindow = true
|
CreateNoWindow = true
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (var argument in arguments)
|
foreach (var argument in arguments) process.StartInfo.ArgumentList.Add(argument);
|
||||||
{
|
|
||||||
process.StartInfo.ArgumentList.Add(argument);
|
|
||||||
}
|
|
||||||
|
|
||||||
process.Start();
|
process.Start();
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ namespace Jibo.Cloud.Infrastructure.Audio;
|
|||||||
|
|
||||||
public interface IExternalProcessRunner
|
public interface IExternalProcessRunner
|
||||||
{
|
{
|
||||||
Task<ExternalProcessResult> RunAsync(string fileName, IReadOnlyList<string> arguments, CancellationToken cancellationToken = default);
|
Task<ExternalProcessResult> RunAsync(string fileName, IReadOnlyList<string> arguments,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record ExternalProcessResult(int ExitCode, string StdOut, string StdErr);
|
public sealed record ExternalProcessResult(int ExitCode, string StdOut, string StdErr);
|
||||||
@@ -12,9 +12,9 @@ public sealed class LocalWhisperCppBufferedAudioSttStrategy(
|
|||||||
public bool CanHandle(TurnContext turn)
|
public bool CanHandle(TurnContext turn)
|
||||||
{
|
{
|
||||||
return options.EnableLocalWhisperCpp &&
|
return options.EnableLocalWhisperCpp &&
|
||||||
IsConfiguredPathAvailable(options.FfmpegPath, checkFileExists: false) &&
|
IsConfiguredPathAvailable(options.FfmpegPath, false) &&
|
||||||
IsConfiguredPathAvailable(options.WhisperCliPath, checkFileExists: true) &&
|
IsConfiguredPathAvailable(options.WhisperCliPath, true) &&
|
||||||
IsConfiguredPathAvailable(options.WhisperModelPath, checkFileExists: true) &&
|
IsConfiguredPathAvailable(options.WhisperModelPath, true) &&
|
||||||
ReadBufferedAudioFrames(turn).Any(ContainsOpusIdentificationHeader);
|
ReadBufferedAudioFrames(turn).Any(ContainsOpusIdentificationHeader);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,20 +22,14 @@ public sealed class LocalWhisperCppBufferedAudioSttStrategy(
|
|||||||
{
|
{
|
||||||
var frames = ReadBufferedAudioFrames(turn);
|
var frames = ReadBufferedAudioFrames(turn);
|
||||||
if (frames.Count == 0)
|
if (frames.Count == 0)
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Local whisper.cpp STT requires buffered websocket audio frames.");
|
throw new InvalidOperationException("Local whisper.cpp STT requires buffered websocket audio frames.");
|
||||||
}
|
|
||||||
|
|
||||||
if (!frames.Any(ContainsOpusIdentificationHeader))
|
if (!frames.Any(ContainsOpusIdentificationHeader))
|
||||||
{
|
throw new InvalidOperationException(
|
||||||
throw new InvalidOperationException("Local whisper.cpp STT requires buffered Ogg/Opus audio with an Opus identification header.");
|
"Local whisper.cpp STT requires buffered Ogg/Opus audio with an Opus identification header.");
|
||||||
}
|
|
||||||
|
|
||||||
var tempDirectory = options.TempDirectory;
|
var tempDirectory = options.TempDirectory;
|
||||||
if (string.IsNullOrWhiteSpace(tempDirectory))
|
if (string.IsNullOrWhiteSpace(tempDirectory)) tempDirectory = Path.Combine(Path.GetTempPath(), "openjibo-stt");
|
||||||
{
|
|
||||||
tempDirectory = Path.Combine(Path.GetTempPath(), "openjibo-stt");
|
|
||||||
}
|
|
||||||
|
|
||||||
Directory.CreateDirectory(tempDirectory);
|
Directory.CreateDirectory(tempDirectory);
|
||||||
|
|
||||||
@@ -58,10 +52,9 @@ public sealed class LocalWhisperCppBufferedAudioSttStrategy(
|
|||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
var transcript = ExtractTranscript(whisperResult.StdOut);
|
var transcript = ExtractTranscript(whisperResult.StdOut);
|
||||||
|
transcript = AudioTranscriptNormalizer.NormalizeLooseTranscript(transcript);
|
||||||
if (string.IsNullOrWhiteSpace(transcript))
|
if (string.IsNullOrWhiteSpace(transcript))
|
||||||
{
|
|
||||||
throw new InvalidOperationException("whisper.cpp returned no transcript for the buffered audio turn.");
|
throw new InvalidOperationException("whisper.cpp returned no transcript for the buffered audio turn.");
|
||||||
}
|
|
||||||
|
|
||||||
return new SttResult
|
return new SttResult
|
||||||
{
|
{
|
||||||
@@ -90,10 +83,7 @@ public sealed class LocalWhisperCppBufferedAudioSttStrategy(
|
|||||||
|
|
||||||
private static IReadOnlyList<byte[]> ReadBufferedAudioFrames(TurnContext turn)
|
private static IReadOnlyList<byte[]> ReadBufferedAudioFrames(TurnContext turn)
|
||||||
{
|
{
|
||||||
if (!turn.Attributes.TryGetValue("bufferedAudioFrames", out var value) || value is null)
|
if (!turn.Attributes.TryGetValue("bufferedAudioFrames", out var value) || value is null) return [];
|
||||||
{
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return value switch
|
return value switch
|
||||||
{
|
{
|
||||||
@@ -110,7 +100,8 @@ public sealed class LocalWhisperCppBufferedAudioSttStrategy(
|
|||||||
|
|
||||||
private static int ReadBufferedAudioBytes(TurnContext turn)
|
private static int ReadBufferedAudioBytes(TurnContext turn)
|
||||||
{
|
{
|
||||||
return turn.Attributes.TryGetValue("bufferedAudioBytes", out var bufferedAudioBytes) && bufferedAudioBytes is not null
|
return turn.Attributes.TryGetValue("bufferedAudioBytes", out var bufferedAudioBytes) &&
|
||||||
|
bufferedAudioBytes is not null
|
||||||
? bufferedAudioBytes switch
|
? bufferedAudioBytes switch
|
||||||
{
|
{
|
||||||
int value => value,
|
int value => value,
|
||||||
@@ -148,10 +139,7 @@ public sealed class LocalWhisperCppBufferedAudioSttStrategy(
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (File.Exists(path))
|
if (File.Exists(path)) File.Delete(path);
|
||||||
{
|
|
||||||
File.Delete(path);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -161,15 +149,9 @@ public sealed class LocalWhisperCppBufferedAudioSttStrategy(
|
|||||||
|
|
||||||
private static bool IsConfiguredPathAvailable(string? path, bool checkFileExists)
|
private static bool IsConfiguredPathAvailable(string? path, bool checkFileExists)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(path))
|
if (string.IsNullOrWhiteSpace(path)) return false;
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Path.IsPathRooted(path))
|
if (!Path.IsPathRooted(path)) return true;
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return !checkFileExists || File.Exists(path);
|
return !checkFileExists || File.Exists(path);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,7 @@ internal static class OggOpusAudioNormalizer
|
|||||||
|
|
||||||
public static byte[] Normalize(IReadOnlyList<byte[]> pages)
|
public static byte[] Normalize(IReadOnlyList<byte[]> pages)
|
||||||
{
|
{
|
||||||
if (pages.Count == 0)
|
if (pages.Count == 0) return [];
|
||||||
{
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
var parsed = pages.Select(ParsePage).ToArray();
|
var parsed = pages.Select(ParsePage).ToArray();
|
||||||
var baseGranule = parsed.Length > 1 ? parsed[1].GranulePosition : parsed[0].GranulePosition;
|
var baseGranule = parsed.Length > 1 ? parsed[1].GranulePosition : parsed[0].GranulePosition;
|
||||||
@@ -50,26 +47,17 @@ internal static class OggOpusAudioNormalizer
|
|||||||
private static ParsedOggPage ParsePage(byte[] buffer)
|
private static ParsedOggPage ParsePage(byte[] buffer)
|
||||||
{
|
{
|
||||||
if (buffer.Length < 27)
|
if (buffer.Length < 27)
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Buffered Ogg page is too short ({buffer.Length} bytes).");
|
throw new InvalidOperationException($"Buffered Ogg page is too short ({buffer.Length} bytes).");
|
||||||
}
|
|
||||||
|
|
||||||
if (!Encoding.ASCII.GetString(buffer, 0, 4).Equals("OggS", StringComparison.Ordinal))
|
if (!Encoding.ASCII.GetString(buffer, 0, 4).Equals("OggS", StringComparison.Ordinal))
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Buffered audio frame did not begin with an OggS capture pattern.");
|
throw new InvalidOperationException("Buffered audio frame did not begin with an OggS capture pattern.");
|
||||||
}
|
|
||||||
|
|
||||||
var pageSegments = buffer[26];
|
var pageSegments = buffer[26];
|
||||||
if (buffer.Length < 27 + pageSegments)
|
if (buffer.Length < 27 + pageSegments)
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Buffered Ogg page segment table was truncated.");
|
throw new InvalidOperationException("Buffered Ogg page segment table was truncated.");
|
||||||
}
|
|
||||||
|
|
||||||
var payloadLength = 0;
|
var payloadLength = 0;
|
||||||
for (var index = 0; index < pageSegments; index += 1)
|
for (var index = 0; index < pageSegments; index += 1) payloadLength += buffer[27 + index];
|
||||||
{
|
|
||||||
payloadLength += buffer[27 + index];
|
|
||||||
}
|
|
||||||
|
|
||||||
var expectedLength = 27 + pageSegments + payloadLength;
|
var expectedLength = 27 + pageSegments + payloadLength;
|
||||||
return buffer.Length < expectedLength
|
return buffer.Length < expectedLength
|
||||||
@@ -79,7 +67,8 @@ internal static class OggOpusAudioNormalizer
|
|||||||
|
|
||||||
private static uint ComputeCrc(byte[] buffer)
|
private static uint ComputeCrc(byte[] buffer)
|
||||||
{
|
{
|
||||||
return buffer.Aggregate<byte, uint>(0, (current, value) => (current << 8) ^ CrcTable[((current >> 24) ^ value) & 0xff]);
|
return buffer.Aggregate<byte, uint>(0,
|
||||||
|
(current, value) => (current << 8) ^ CrcTable[((current >> 24) ^ value) & 0xff]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static uint[] BuildCrcTable()
|
private static uint[] BuildCrcTable()
|
||||||
@@ -89,11 +78,9 @@ internal static class OggOpusAudioNormalizer
|
|||||||
{
|
{
|
||||||
var remainder = index << 24;
|
var remainder = index << 24;
|
||||||
for (var bit = 0; bit < 8; bit += 1)
|
for (var bit = 0; bit < 8; bit += 1)
|
||||||
{
|
|
||||||
remainder = (remainder & 0x80000000) != 0
|
remainder = (remainder & 0x80000000) != 0
|
||||||
? (remainder << 1) ^ 0x04c11db7
|
? (remainder << 1) ^ 0x04c11db7
|
||||||
: remainder << 1;
|
: remainder << 1;
|
||||||
}
|
|
||||||
|
|
||||||
table[index] = remainder;
|
table[index] = remainder;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,16 @@ namespace Jibo.Cloud.Infrastructure.Content;
|
|||||||
|
|
||||||
public sealed class InMemoryJiboExperienceContentRepository : IJiboExperienceContentRepository
|
public sealed class InMemoryJiboExperienceContentRepository : IJiboExperienceContentRepository
|
||||||
{
|
{
|
||||||
private static readonly JiboExperienceCatalog Catalog = new()
|
private static readonly JiboExperienceCatalog Catalog = BuildCatalog();
|
||||||
|
|
||||||
|
public Task<JiboExperienceCatalog> GetCatalogAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return Task.FromResult(Catalog);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JiboExperienceCatalog BuildCatalog()
|
||||||
|
{
|
||||||
|
var catalog = new JiboExperienceCatalog
|
||||||
{
|
{
|
||||||
Jokes =
|
Jokes =
|
||||||
[
|
[
|
||||||
@@ -12,7 +21,39 @@ public sealed class InMemoryJiboExperienceContentRepository : IJiboExperienceCon
|
|||||||
"Why was the robot tired when it got home? It had a hard drive.",
|
"Why was the robot tired when it got home? It had a hard drive.",
|
||||||
"What do you call a pirate robot? Arrrr two dee two.",
|
"What do you call a pirate robot? Arrrr two dee two.",
|
||||||
"Why did the robot go on vacation? It needed to recharge.",
|
"Why did the robot go on vacation? It needed to recharge.",
|
||||||
"What kind of shoes do frogs wear? Open-toed."
|
"What kind of shoes do frogs wear? Open-toed.",
|
||||||
|
"I love jokes. Did you hear about the theater actor who fell through the floorboards? He was just going through a stage.",
|
||||||
|
"Sure I got one. What did the zero say to the eight. Nice belt.",
|
||||||
|
"What kind of music are balloons afraid of. Pop music.",
|
||||||
|
"Why did the orange cry. Someone hurt his peelings."
|
||||||
|
],
|
||||||
|
RobotFacts =
|
||||||
|
[
|
||||||
|
"Leonardo Da Vinci made sketches for a humanoid machine all the way back in the year 1495.",
|
||||||
|
"The world's first humanoid robot was called Elektro, and it debuted in 1939.",
|
||||||
|
"The English word robot comes from a 1920 play in Czechoslovakia, called Rossum's Universal Robots.",
|
||||||
|
"The first programmable robot arm was designed in 1954.",
|
||||||
|
"Some robots have a human form, but most of the world's robots are machines designed to perform a task, and don't look like people at all."
|
||||||
|
],
|
||||||
|
HumanFacts =
|
||||||
|
[
|
||||||
|
"Every human being that has ever lived spent about 30 minutes as a single cell.",
|
||||||
|
"50 percent of a human's DNA is the same as a banana's.",
|
||||||
|
"Humans are the only animals that cry tears of emotion.",
|
||||||
|
"Six-year-olds laugh an average of 300 times a day. Grown ups only laugh 15 to 100 times a day.",
|
||||||
|
"Your nose can remember 50,000 different scents."
|
||||||
|
],
|
||||||
|
FunFacts =
|
||||||
|
[
|
||||||
|
"A shrimp's heart is in its head.",
|
||||||
|
"A bolt of lightning is hotter than the surface of the sun.",
|
||||||
|
"The word robot comes from a 1920 play about workers and machines.",
|
||||||
|
"The first humanoid robot to make a big splash in history was called Elektro.",
|
||||||
|
"Dolphins can recognize themselves in mirrors.",
|
||||||
|
"Children have more taste buds than grown ups.",
|
||||||
|
"A random fact for you. A shrimp's heart is in its head.",
|
||||||
|
"An amazing but true fact for you. Dogs and elephants are the only animals that understand pointing.",
|
||||||
|
"A crazy fact for you. Polar bear fur isn't white. It's transparent."
|
||||||
],
|
],
|
||||||
DanceAnimations =
|
DanceAnimations =
|
||||||
[
|
[
|
||||||
@@ -23,7 +64,8 @@ public sealed class InMemoryJiboExperienceContentRepository : IJiboExperienceCon
|
|||||||
"rom-electronic",
|
"rom-electronic",
|
||||||
"rom-twerk"
|
"rom-twerk"
|
||||||
],
|
],
|
||||||
DanceReplies = [
|
DanceReplies =
|
||||||
|
[
|
||||||
"I am ready to dance.",
|
"I am ready to dance.",
|
||||||
"Okay. Watch this.",
|
"Okay. Watch this.",
|
||||||
"Watch me dance.",
|
"Watch me dance.",
|
||||||
@@ -70,6 +112,45 @@ public sealed class InMemoryJiboExperienceContentRepository : IJiboExperienceCon
|
|||||||
"I heard your personal report request. That cloud path is still being mapped.",
|
"I heard your personal report request. That cloud path is still being mapped.",
|
||||||
"Personal report is recognized, but I am not ready to deliver the real report yet."
|
"Personal report is recognized, but I am not ready to deliver the real report yet."
|
||||||
],
|
],
|
||||||
|
PersonalReportKickOffReplies =
|
||||||
|
[
|
||||||
|
"Okay. Here's your personal report.",
|
||||||
|
"Sure. Here it is."
|
||||||
|
],
|
||||||
|
PersonalReportOutroReplies =
|
||||||
|
[
|
||||||
|
"And that's your report for the day. I hope you had as much fun as I did.",
|
||||||
|
"That wraps up your report for the day. Hope you have a good one."
|
||||||
|
],
|
||||||
|
ReportSkillTemplates =
|
||||||
|
[
|
||||||
|
"The report-skill templates are loaded and waiting to be rendered."
|
||||||
|
],
|
||||||
|
WeatherIntroReplies =
|
||||||
|
[
|
||||||
|
"For your weather.",
|
||||||
|
"Let's look at the weather."
|
||||||
|
],
|
||||||
|
WeatherTomorrowIntroReplies =
|
||||||
|
[
|
||||||
|
"First, the weather tomorrow.",
|
||||||
|
"Looking at tomorrow's weather."
|
||||||
|
],
|
||||||
|
WeatherTodayHighLowReplies =
|
||||||
|
[
|
||||||
|
"Today's high is {high}, and the low is {low}.",
|
||||||
|
"It'll be a high today of {high}, and a low of {low}."
|
||||||
|
],
|
||||||
|
WeatherTomorrowHighLowReplies =
|
||||||
|
[
|
||||||
|
"Tomorrow's high will be {high} and the low will be {low}.",
|
||||||
|
"It'll be a high tomorrow of {high} and a low of {low}."
|
||||||
|
],
|
||||||
|
WeatherServiceDownReplies =
|
||||||
|
[
|
||||||
|
"Looks like our weather service is offline. Sorry.",
|
||||||
|
"Looks like I can't access weather info right now, sorry."
|
||||||
|
],
|
||||||
WeatherReplies =
|
WeatherReplies =
|
||||||
[
|
[
|
||||||
"I heard your weather request. We still need to wire the real provider behind it.",
|
"I heard your weather request. We still need to wire the real provider behind it.",
|
||||||
@@ -103,8 +184,66 @@ public sealed class InMemoryJiboExperienceContentRepository : IJiboExperienceCon
|
|||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
public Task<JiboExperienceCatalog> GetCatalogAsync(CancellationToken cancellationToken = default)
|
foreach (var seedDirectory in ResolveSeedDirectories())
|
||||||
|
catalog = LegacyMimCatalogImporter.MergeInto(catalog, seedDirectory);
|
||||||
|
|
||||||
|
return catalog;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<string> ResolveSeedDirectories()
|
||||||
{
|
{
|
||||||
return Task.FromResult(Catalog);
|
var candidates = new[]
|
||||||
|
{
|
||||||
|
Path.Combine(AppContext.BaseDirectory, "Content", "LegacyMims", "BuildA"),
|
||||||
|
Path.Combine(AppContext.BaseDirectory, "Content", "LegacyMims", "BuildB"),
|
||||||
|
Path.Combine(AppContext.BaseDirectory, "Content", "LegacyMims", "ReportSkill"),
|
||||||
|
Path.GetFullPath(Path.Combine(
|
||||||
|
AppContext.BaseDirectory,
|
||||||
|
"..",
|
||||||
|
"..",
|
||||||
|
"..",
|
||||||
|
"..",
|
||||||
|
"..",
|
||||||
|
"src",
|
||||||
|
"Jibo.Cloud",
|
||||||
|
"dotnet",
|
||||||
|
"src",
|
||||||
|
"Jibo.Cloud.Infrastructure",
|
||||||
|
"Content",
|
||||||
|
"LegacyMims",
|
||||||
|
"BuildA")),
|
||||||
|
Path.GetFullPath(Path.Combine(
|
||||||
|
AppContext.BaseDirectory,
|
||||||
|
"..",
|
||||||
|
"..",
|
||||||
|
"..",
|
||||||
|
"..",
|
||||||
|
"..",
|
||||||
|
"src",
|
||||||
|
"Jibo.Cloud",
|
||||||
|
"dotnet",
|
||||||
|
"src",
|
||||||
|
"Jibo.Cloud.Infrastructure",
|
||||||
|
"Content",
|
||||||
|
"LegacyMims",
|
||||||
|
"BuildB")),
|
||||||
|
Path.GetFullPath(Path.Combine(
|
||||||
|
AppContext.BaseDirectory,
|
||||||
|
"..",
|
||||||
|
"..",
|
||||||
|
"..",
|
||||||
|
"..",
|
||||||
|
"..",
|
||||||
|
"src",
|
||||||
|
"Jibo.Cloud",
|
||||||
|
"dotnet",
|
||||||
|
"src",
|
||||||
|
"Jibo.Cloud.Infrastructure",
|
||||||
|
"Content",
|
||||||
|
"LegacyMims",
|
||||||
|
"ReportSkill"))
|
||||||
|
};
|
||||||
|
|
||||||
|
return candidates.Where(Directory.Exists).ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,599 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Jibo.Cloud.Application.Abstractions;
|
||||||
|
|
||||||
|
namespace Jibo.Cloud.Infrastructure.Content;
|
||||||
|
|
||||||
|
public static class LegacyMimCatalogImporter
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
AllowTrailingCommas = true
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly Regex LegacyMarkupPattern = new(
|
||||||
|
@"<[^>]+>",
|
||||||
|
RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
||||||
|
|
||||||
|
private static readonly Regex PlaceholderPattern = new(
|
||||||
|
@"\$\{[^}]+\}",
|
||||||
|
RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
||||||
|
|
||||||
|
private static readonly Regex WhitespacePattern = new(
|
||||||
|
@"\s+",
|
||||||
|
RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
||||||
|
|
||||||
|
private static readonly Regex SpaceBeforePunctuationPattern = new(
|
||||||
|
@"\s+([,.;:!?])",
|
||||||
|
RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
||||||
|
|
||||||
|
public static JiboExperienceCatalog MergeInto(
|
||||||
|
JiboExperienceCatalog baseCatalog,
|
||||||
|
string? rootDirectory)
|
||||||
|
{
|
||||||
|
if (baseCatalog is null) throw new ArgumentNullException(nameof(baseCatalog));
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(rootDirectory) || !Directory.Exists(rootDirectory)) return baseCatalog;
|
||||||
|
|
||||||
|
var importedCatalog = ImportCatalog(rootDirectory);
|
||||||
|
return MergeCatalogs(baseCatalog, importedCatalog);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JiboExperienceCatalog ImportCatalog(string rootDirectory)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(rootDirectory) || !Directory.Exists(rootDirectory))
|
||||||
|
return new JiboExperienceCatalog();
|
||||||
|
|
||||||
|
var builder = new LegacyMimCatalogBuilder();
|
||||||
|
foreach (var filePath in Directory.EnumerateFiles(rootDirectory, "*.mim", SearchOption.AllDirectories)
|
||||||
|
.OrderBy(static path => path, StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (!TryLoadDefinition(filePath, out var definition)) continue;
|
||||||
|
|
||||||
|
var bucket = ResolveBucket(filePath);
|
||||||
|
if (bucket is null) continue;
|
||||||
|
|
||||||
|
foreach (var prompt in definition.Prompts)
|
||||||
|
{
|
||||||
|
var text = NormalizePrompt(prompt.Prompt, IsTemplateBucket(bucket.Value));
|
||||||
|
if (string.IsNullOrWhiteSpace(text)) continue;
|
||||||
|
|
||||||
|
builder.Add(bucket.Value, prompt.Condition, text, prompt.Prompt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryLoadDefinition(string filePath, out LegacyMimDefinition definition)
|
||||||
|
{
|
||||||
|
definition = new LegacyMimDefinition();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = File.ReadAllText(filePath);
|
||||||
|
var parsed = JsonSerializer.Deserialize<LegacyMimDefinition>(json, JsonOptions);
|
||||||
|
if (parsed is null) return false;
|
||||||
|
|
||||||
|
definition = parsed;
|
||||||
|
return definition.Prompts.Count > 0;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LegacyMimBucket? ResolveBucket(string filePath)
|
||||||
|
{
|
||||||
|
var normalizedPath = filePath.Replace('\\', '/');
|
||||||
|
var fileName = Path.GetFileNameWithoutExtension(filePath);
|
||||||
|
|
||||||
|
if (normalizedPath.Contains("/core-responses/", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
fileName.Contains("Error", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return LegacyMimBucket.GenericFallback;
|
||||||
|
|
||||||
|
if (normalizedPath.Contains("/core-responses/deflector/", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
fileName.Contains("Deflector", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return LegacyMimBucket.Personality;
|
||||||
|
|
||||||
|
if (fileName.StartsWith("RA_JBO_TellAJoke", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return LegacyMimBucket.Jokes;
|
||||||
|
|
||||||
|
if (fileName.StartsWith("RA_JBO_TellRobotFact", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return LegacyMimBucket.RobotFacts;
|
||||||
|
|
||||||
|
if (fileName.StartsWith("RA_JBO_Shuffle", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
fileName.StartsWith("RA_JBO_TellSomething", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return LegacyMimBucket.FunFactSource;
|
||||||
|
|
||||||
|
if (normalizedPath.Contains("/emotion-responses/", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
normalizedPath.Contains("/gqa-responses/", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return LegacyMimBucket.Emotion;
|
||||||
|
|
||||||
|
if (fileName.StartsWith("WeatherIntroTomorrow", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return LegacyMimBucket.WeatherTomorrowIntro;
|
||||||
|
|
||||||
|
if (fileName.StartsWith("WeatherIntro", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return LegacyMimBucket.WeatherIntro;
|
||||||
|
|
||||||
|
if (fileName.StartsWith("WeatherTomorrowHighLow", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return LegacyMimBucket.WeatherTomorrowHighLow;
|
||||||
|
|
||||||
|
if (fileName.StartsWith("WeatherTodayHighLow", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return LegacyMimBucket.WeatherTodayHighLow;
|
||||||
|
|
||||||
|
if (fileName.StartsWith("WeatherServiceDown", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return LegacyMimBucket.WeatherServiceDown;
|
||||||
|
|
||||||
|
if (fileName.StartsWith("CalendarNothingToday", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return LegacyMimBucket.CalendarNothingToday;
|
||||||
|
|
||||||
|
if (fileName.StartsWith("CalendarNothing", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return LegacyMimBucket.CalendarNothing;
|
||||||
|
|
||||||
|
if (fileName.StartsWith("CalendarOutro", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return LegacyMimBucket.CalendarOutro;
|
||||||
|
|
||||||
|
if (fileName.StartsWith("CommuteNow", StringComparison.OrdinalIgnoreCase)) return LegacyMimBucket.CommuteNow;
|
||||||
|
|
||||||
|
if (fileName.StartsWith("CommuteServiceDown", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return LegacyMimBucket.CommuteServiceDown;
|
||||||
|
|
||||||
|
if (fileName.StartsWith("NewsIntroCategory", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return LegacyMimBucket.NewsCategoryIntro;
|
||||||
|
|
||||||
|
if (fileName.StartsWith("NewsIntro", StringComparison.OrdinalIgnoreCase)) return LegacyMimBucket.NewsIntro;
|
||||||
|
|
||||||
|
if (fileName.StartsWith("NewsOutro", StringComparison.OrdinalIgnoreCase)) return LegacyMimBucket.NewsOutro;
|
||||||
|
|
||||||
|
if (fileName.StartsWith("Weather", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
string.Equals(fileName, "WetNowDryLater", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return LegacyMimBucket.ReportSkillTemplate;
|
||||||
|
|
||||||
|
if (fileName.StartsWith("PersonalReportKickOff", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return LegacyMimBucket.PersonalReportKickOff;
|
||||||
|
|
||||||
|
if (fileName.StartsWith("PersonalReportOutro", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return LegacyMimBucket.PersonalReportOutro;
|
||||||
|
|
||||||
|
if (fileName.StartsWith("PersonalReport", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
fileName.Contains("Calendar", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
fileName.Contains("Commute", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
fileName.Contains("News", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return LegacyMimBucket.ReportSkillTemplate;
|
||||||
|
|
||||||
|
if (fileName.StartsWith("JBO_DoYouLikeBeingJibo", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
fileName.StartsWith("JBO_WhatIsJibo", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
fileName.StartsWith("JBO_WhoAreYou", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
fileName.StartsWith("JBO_WhatAreYou", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
fileName.StartsWith("JBO_HowDoYouWork", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
fileName.StartsWith("JBO_HowMuchDoYouKnow", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
fileName.StartsWith("JBO_HowOldAreYou", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
fileName.StartsWith("JBO_WhenWereYouBorn", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
fileName.StartsWith("JBO_WhatsYourName", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
fileName.StartsWith("JBO_WhereDoYouGetInfo", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
fileName.StartsWith("JBO_WhatDoYouLikeToDo", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return LegacyMimBucket.Personality;
|
||||||
|
|
||||||
|
if (fileName.StartsWith("OI_JBO_Is", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
fileName.StartsWith("OI_JBO_Seems", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
fileName.StartsWith("RI_JBO_IsHappy", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
fileName.StartsWith("RI_JBO_IsSad", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
fileName.StartsWith("RI_JBO_IsAngry", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
fileName.StartsWith("RN_WhatAreYouFeeling", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return LegacyMimBucket.Emotion;
|
||||||
|
|
||||||
|
if (fileName.Contains("Greeting", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
fileName.StartsWith("RN_", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
fileName.Contains("Welcome", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return LegacyMimBucket.Greeting;
|
||||||
|
|
||||||
|
if (normalizedPath.Contains("/scripted-responses/", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return LegacyMimBucket.Personality;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizePrompt(string? prompt)
|
||||||
|
{
|
||||||
|
return NormalizePrompt(prompt, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizePrompt(string? prompt, bool preservePlaceholders)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(prompt)) return string.Empty;
|
||||||
|
|
||||||
|
var text = WebUtility.HtmlDecode(prompt);
|
||||||
|
if (!preservePlaceholders) text = PlaceholderPattern.Replace(text, " ");
|
||||||
|
text = LegacyMarkupPattern.Replace(text, " ");
|
||||||
|
text = WhitespacePattern.Replace(text, " ").Trim();
|
||||||
|
text = SpaceBeforePunctuationPattern.Replace(text, "$1");
|
||||||
|
text = WhitespacePattern.Replace(text, " ").Trim();
|
||||||
|
text = text.TrimStart('.', ',', ';', ':', '!', '?', ' ');
|
||||||
|
return text.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JiboExperienceCatalog MergeCatalogs(
|
||||||
|
JiboExperienceCatalog baseCatalog,
|
||||||
|
JiboExperienceCatalog importedCatalog)
|
||||||
|
{
|
||||||
|
return new JiboExperienceCatalog
|
||||||
|
{
|
||||||
|
Jokes = Merge(baseCatalog.Jokes, importedCatalog.Jokes),
|
||||||
|
RobotFacts = Merge(baseCatalog.RobotFacts, importedCatalog.RobotFacts),
|
||||||
|
HumanFacts = Merge(baseCatalog.HumanFacts, importedCatalog.HumanFacts),
|
||||||
|
FunFacts = Merge(baseCatalog.FunFacts, importedCatalog.FunFacts),
|
||||||
|
DanceAnimations = Merge(baseCatalog.DanceAnimations, importedCatalog.DanceAnimations),
|
||||||
|
GreetingReplies = Merge(baseCatalog.GreetingReplies, importedCatalog.GreetingReplies),
|
||||||
|
HowAreYouReplies = Merge(baseCatalog.HowAreYouReplies, importedCatalog.HowAreYouReplies),
|
||||||
|
EmotionReplies = Merge(baseCatalog.EmotionReplies, importedCatalog.EmotionReplies),
|
||||||
|
PersonalityReplies = Merge(baseCatalog.PersonalityReplies, importedCatalog.PersonalityReplies),
|
||||||
|
PizzaReplies = Merge(baseCatalog.PizzaReplies, importedCatalog.PizzaReplies),
|
||||||
|
SurpriseReplies = Merge(baseCatalog.SurpriseReplies, importedCatalog.SurpriseReplies),
|
||||||
|
PersonalReportReplies = Merge(baseCatalog.PersonalReportReplies, importedCatalog.PersonalReportReplies),
|
||||||
|
PersonalReportKickOffReplies = Merge(baseCatalog.PersonalReportKickOffReplies,
|
||||||
|
importedCatalog.PersonalReportKickOffReplies),
|
||||||
|
PersonalReportOutroReplies = Merge(baseCatalog.PersonalReportOutroReplies,
|
||||||
|
importedCatalog.PersonalReportOutroReplies),
|
||||||
|
ReportSkillTemplates = Merge(baseCatalog.ReportSkillTemplates, importedCatalog.ReportSkillTemplates),
|
||||||
|
WeatherIntroReplies = Merge(baseCatalog.WeatherIntroReplies, importedCatalog.WeatherIntroReplies),
|
||||||
|
WeatherTomorrowIntroReplies = Merge(baseCatalog.WeatherTomorrowIntroReplies,
|
||||||
|
importedCatalog.WeatherTomorrowIntroReplies),
|
||||||
|
WeatherTodayHighLowReplies = Merge(baseCatalog.WeatherTodayHighLowReplies,
|
||||||
|
importedCatalog.WeatherTodayHighLowReplies),
|
||||||
|
WeatherTomorrowHighLowReplies = Merge(baseCatalog.WeatherTomorrowHighLowReplies,
|
||||||
|
importedCatalog.WeatherTomorrowHighLowReplies),
|
||||||
|
WeatherServiceDownReplies = Merge(baseCatalog.WeatherServiceDownReplies,
|
||||||
|
importedCatalog.WeatherServiceDownReplies),
|
||||||
|
CalendarNothingTodayReplies = Merge(baseCatalog.CalendarNothingTodayReplies,
|
||||||
|
importedCatalog.CalendarNothingTodayReplies),
|
||||||
|
CalendarNothingReplies = Merge(baseCatalog.CalendarNothingReplies, importedCatalog.CalendarNothingReplies),
|
||||||
|
CalendarOutroReplies = Merge(baseCatalog.CalendarOutroReplies, importedCatalog.CalendarOutroReplies),
|
||||||
|
CommuteNowReplies = Merge(baseCatalog.CommuteNowReplies, importedCatalog.CommuteNowReplies),
|
||||||
|
CommuteServiceDownReplies = Merge(baseCatalog.CommuteServiceDownReplies,
|
||||||
|
importedCatalog.CommuteServiceDownReplies),
|
||||||
|
NewsIntroReplies = Merge(baseCatalog.NewsIntroReplies, importedCatalog.NewsIntroReplies),
|
||||||
|
NewsCategoryIntroReplies =
|
||||||
|
Merge(baseCatalog.NewsCategoryIntroReplies, importedCatalog.NewsCategoryIntroReplies),
|
||||||
|
NewsOutroReplies = Merge(baseCatalog.NewsOutroReplies, importedCatalog.NewsOutroReplies),
|
||||||
|
WeatherReplies = Merge(baseCatalog.WeatherReplies, importedCatalog.WeatherReplies),
|
||||||
|
CalendarReplies = Merge(baseCatalog.CalendarReplies, importedCatalog.CalendarReplies),
|
||||||
|
CommuteReplies = Merge(baseCatalog.CommuteReplies, importedCatalog.CommuteReplies),
|
||||||
|
NewsReplies = Merge(baseCatalog.NewsReplies, importedCatalog.NewsReplies),
|
||||||
|
NewsBriefings = Merge(baseCatalog.NewsBriefings, importedCatalog.NewsBriefings),
|
||||||
|
GenericFallbackReplies = Merge(baseCatalog.GenericFallbackReplies, importedCatalog.GenericFallbackReplies),
|
||||||
|
DanceReplies = Merge(baseCatalog.DanceReplies, importedCatalog.DanceReplies),
|
||||||
|
DanceQuestionReplies = Merge(baseCatalog.DanceQuestionReplies, importedCatalog.DanceQuestionReplies)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<string> Merge(IReadOnlyList<string> baseList, IReadOnlyList<string> importedList)
|
||||||
|
{
|
||||||
|
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var merged = new List<string>();
|
||||||
|
|
||||||
|
foreach (var value in baseList.Concat(importedList))
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value)) continue;
|
||||||
|
|
||||||
|
var normalized = value.Trim();
|
||||||
|
if (!seen.Add(normalized)) continue;
|
||||||
|
|
||||||
|
merged.Add(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<JiboConditionedReply> Merge(
|
||||||
|
IReadOnlyList<JiboConditionedReply> baseList,
|
||||||
|
IReadOnlyList<JiboConditionedReply> importedList)
|
||||||
|
{
|
||||||
|
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var merged = new List<JiboConditionedReply>();
|
||||||
|
|
||||||
|
foreach (var value in baseList.Concat(importedList))
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value.Reply)) continue;
|
||||||
|
|
||||||
|
var normalizedCondition = NormalizeCondition(value.Condition);
|
||||||
|
var normalizedReply = value.Reply.Trim();
|
||||||
|
var key = $"{normalizedCondition}::{normalizedReply}";
|
||||||
|
if (!seen.Add(key)) continue;
|
||||||
|
|
||||||
|
merged.Add(new JiboConditionedReply
|
||||||
|
{
|
||||||
|
Condition = normalizedCondition,
|
||||||
|
Reply = normalizedReply
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeCondition(string? condition)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(condition) ? string.Empty : WhitespacePattern.Replace(condition.Trim(), " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsTemplateBucket(LegacyMimBucket bucket)
|
||||||
|
{
|
||||||
|
return bucket is LegacyMimBucket.PersonalReportKickOff
|
||||||
|
or LegacyMimBucket.PersonalReportOutro
|
||||||
|
or LegacyMimBucket.WeatherIntro
|
||||||
|
or LegacyMimBucket.WeatherTomorrowIntro
|
||||||
|
or LegacyMimBucket.WeatherTodayHighLow
|
||||||
|
or LegacyMimBucket.WeatherTomorrowHighLow
|
||||||
|
or LegacyMimBucket.WeatherServiceDown
|
||||||
|
or LegacyMimBucket.ReportSkillTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum LegacyMimBucket
|
||||||
|
{
|
||||||
|
GenericFallback,
|
||||||
|
Greeting,
|
||||||
|
Jokes,
|
||||||
|
RobotFacts,
|
||||||
|
HumanFacts,
|
||||||
|
HowAreYou,
|
||||||
|
Emotion,
|
||||||
|
FunFacts,
|
||||||
|
FunFactSource,
|
||||||
|
Personality,
|
||||||
|
PersonalReportKickOff,
|
||||||
|
PersonalReportOutro,
|
||||||
|
WeatherIntro,
|
||||||
|
WeatherTomorrowIntro,
|
||||||
|
WeatherTodayHighLow,
|
||||||
|
WeatherTomorrowHighLow,
|
||||||
|
WeatherServiceDown,
|
||||||
|
CalendarNothingToday,
|
||||||
|
CalendarNothing,
|
||||||
|
CalendarOutro,
|
||||||
|
CommuteNow,
|
||||||
|
CommuteServiceDown,
|
||||||
|
NewsIntro,
|
||||||
|
NewsCategoryIntro,
|
||||||
|
NewsOutro,
|
||||||
|
ReportSkillTemplate
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class LegacyMimCatalogBuilder
|
||||||
|
{
|
||||||
|
private readonly List<string> _calendarNothingReplies = [];
|
||||||
|
private readonly List<string> _calendarNothingTodayReplies = [];
|
||||||
|
private readonly List<string> _calendarOutroReplies = [];
|
||||||
|
private readonly List<string> _commuteNowReplies = [];
|
||||||
|
private readonly List<string> _commuteServiceDownReplies = [];
|
||||||
|
private readonly List<JiboConditionedReply> _emotionReplies = [];
|
||||||
|
private readonly List<string> _fallbacks = [];
|
||||||
|
private readonly List<string> _greetings = [];
|
||||||
|
private readonly List<string> _jokes = [];
|
||||||
|
private readonly List<string> _robotFacts = [];
|
||||||
|
private readonly List<string> _humanFacts = [];
|
||||||
|
private readonly List<string> _howAreYous = [];
|
||||||
|
private readonly List<string> _funFacts = [];
|
||||||
|
private readonly List<string> _newsCategoryIntroReplies = [];
|
||||||
|
private readonly List<string> _newsIntroReplies = [];
|
||||||
|
private readonly List<string> _newsOutroReplies = [];
|
||||||
|
private readonly List<string> _personalities = [];
|
||||||
|
private readonly List<string> _personalReportKickOffReplies = [];
|
||||||
|
private readonly List<string> _personalReportOutroReplies = [];
|
||||||
|
private readonly List<string> _reportSkillTemplates = [];
|
||||||
|
private readonly List<string> _weatherIntroReplies = [];
|
||||||
|
private readonly List<string> _weatherServiceDownReplies = [];
|
||||||
|
private readonly List<string> _weatherTodayHighLowReplies = [];
|
||||||
|
private readonly List<string> _weatherTomorrowHighLowReplies = [];
|
||||||
|
private readonly List<string> _weatherTomorrowIntroReplies = [];
|
||||||
|
|
||||||
|
public void Add(LegacyMimBucket bucket, string? condition, string text, string? sourcePrompt = null)
|
||||||
|
{
|
||||||
|
switch (bucket)
|
||||||
|
{
|
||||||
|
case LegacyMimBucket.GenericFallback:
|
||||||
|
if (_fallbacks.Any(value => string.Equals(value, text, StringComparison.OrdinalIgnoreCase))) return;
|
||||||
|
|
||||||
|
_fallbacks.Add(text);
|
||||||
|
return;
|
||||||
|
case LegacyMimBucket.Greeting:
|
||||||
|
if (_greetings.Any(value => string.Equals(value, text, StringComparison.OrdinalIgnoreCase))) return;
|
||||||
|
|
||||||
|
_greetings.Add(text);
|
||||||
|
return;
|
||||||
|
case LegacyMimBucket.Jokes:
|
||||||
|
if (_jokes.Any(value => string.Equals(value, text, StringComparison.OrdinalIgnoreCase))) return;
|
||||||
|
|
||||||
|
_jokes.Add(text);
|
||||||
|
return;
|
||||||
|
case LegacyMimBucket.RobotFacts:
|
||||||
|
AddDistinct(_robotFacts, text);
|
||||||
|
return;
|
||||||
|
case LegacyMimBucket.HumanFacts:
|
||||||
|
AddDistinct(_humanFacts, text);
|
||||||
|
return;
|
||||||
|
case LegacyMimBucket.HowAreYou:
|
||||||
|
if (_howAreYous.Any(value => string.Equals(value, text, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_howAreYous.Add(text);
|
||||||
|
return;
|
||||||
|
case LegacyMimBucket.Emotion:
|
||||||
|
var normalizedCondition = NormalizeCondition(condition);
|
||||||
|
if (_emotionReplies.Any(value =>
|
||||||
|
string.Equals(NormalizeCondition(value.Condition), normalizedCondition,
|
||||||
|
StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
string.Equals(value.Reply, text, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_emotionReplies.Add(new JiboConditionedReply
|
||||||
|
{
|
||||||
|
Condition = normalizedCondition,
|
||||||
|
Reply = text
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
case LegacyMimBucket.Personality:
|
||||||
|
if (_personalities.Any(value => string.Equals(value, text, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_personalities.Add(text);
|
||||||
|
return;
|
||||||
|
case LegacyMimBucket.FunFactSource:
|
||||||
|
switch (ResolveFunFactTarget(sourcePrompt ?? text))
|
||||||
|
{
|
||||||
|
case LegacyMimBucket.RobotFacts:
|
||||||
|
AddDistinct(_robotFacts, text);
|
||||||
|
return;
|
||||||
|
case LegacyMimBucket.HumanFacts:
|
||||||
|
AddDistinct(_humanFacts, text);
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
AddDistinct(_funFacts, text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case LegacyMimBucket.FunFacts:
|
||||||
|
if (_funFacts.Any(value => string.Equals(value, text, StringComparison.OrdinalIgnoreCase))) return;
|
||||||
|
|
||||||
|
_funFacts.Add(text);
|
||||||
|
return;
|
||||||
|
case LegacyMimBucket.PersonalReportKickOff:
|
||||||
|
AddDistinct(_personalReportKickOffReplies, text);
|
||||||
|
return;
|
||||||
|
case LegacyMimBucket.PersonalReportOutro:
|
||||||
|
AddDistinct(_personalReportOutroReplies, text);
|
||||||
|
return;
|
||||||
|
case LegacyMimBucket.WeatherIntro:
|
||||||
|
AddDistinct(_weatherIntroReplies, text);
|
||||||
|
return;
|
||||||
|
case LegacyMimBucket.WeatherTomorrowIntro:
|
||||||
|
AddDistinct(_weatherTomorrowIntroReplies, text);
|
||||||
|
return;
|
||||||
|
case LegacyMimBucket.WeatherTodayHighLow:
|
||||||
|
AddDistinct(_weatherTodayHighLowReplies, text);
|
||||||
|
return;
|
||||||
|
case LegacyMimBucket.WeatherTomorrowHighLow:
|
||||||
|
AddDistinct(_weatherTomorrowHighLowReplies, text);
|
||||||
|
return;
|
||||||
|
case LegacyMimBucket.WeatherServiceDown:
|
||||||
|
AddDistinct(_weatherServiceDownReplies, text);
|
||||||
|
return;
|
||||||
|
case LegacyMimBucket.CalendarNothingToday:
|
||||||
|
AddDistinct(_calendarNothingTodayReplies, text);
|
||||||
|
return;
|
||||||
|
case LegacyMimBucket.CalendarNothing:
|
||||||
|
AddDistinct(_calendarNothingReplies, text);
|
||||||
|
return;
|
||||||
|
case LegacyMimBucket.CalendarOutro:
|
||||||
|
AddDistinct(_calendarOutroReplies, text);
|
||||||
|
return;
|
||||||
|
case LegacyMimBucket.CommuteNow:
|
||||||
|
AddDistinct(_commuteNowReplies, text);
|
||||||
|
return;
|
||||||
|
case LegacyMimBucket.CommuteServiceDown:
|
||||||
|
AddDistinct(_commuteServiceDownReplies, text);
|
||||||
|
return;
|
||||||
|
case LegacyMimBucket.NewsIntro:
|
||||||
|
AddDistinct(_newsIntroReplies, text);
|
||||||
|
return;
|
||||||
|
case LegacyMimBucket.NewsCategoryIntro:
|
||||||
|
AddDistinct(_newsCategoryIntroReplies, text);
|
||||||
|
return;
|
||||||
|
case LegacyMimBucket.NewsOutro:
|
||||||
|
AddDistinct(_newsOutroReplies, text);
|
||||||
|
return;
|
||||||
|
case LegacyMimBucket.ReportSkillTemplate:
|
||||||
|
AddDistinct(_reportSkillTemplates, text);
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(bucket), bucket, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public JiboExperienceCatalog Build()
|
||||||
|
{
|
||||||
|
return new JiboExperienceCatalog
|
||||||
|
{
|
||||||
|
Jokes = [.. _jokes],
|
||||||
|
RobotFacts = [.. _robotFacts],
|
||||||
|
HumanFacts = [.. _humanFacts],
|
||||||
|
FunFacts = [.. _funFacts],
|
||||||
|
GreetingReplies = [.. _greetings],
|
||||||
|
HowAreYouReplies = [.. _howAreYous],
|
||||||
|
EmotionReplies = [.. _emotionReplies],
|
||||||
|
PersonalityReplies = [.. _personalities],
|
||||||
|
GenericFallbackReplies = [.. _fallbacks],
|
||||||
|
PersonalReportKickOffReplies = [.. _personalReportKickOffReplies],
|
||||||
|
PersonalReportOutroReplies = [.. _personalReportOutroReplies],
|
||||||
|
ReportSkillTemplates = [.. _reportSkillTemplates],
|
||||||
|
WeatherIntroReplies = [.. _weatherIntroReplies],
|
||||||
|
WeatherTomorrowIntroReplies = [.. _weatherTomorrowIntroReplies],
|
||||||
|
WeatherTodayHighLowReplies = [.. _weatherTodayHighLowReplies],
|
||||||
|
WeatherTomorrowHighLowReplies = [.. _weatherTomorrowHighLowReplies],
|
||||||
|
WeatherServiceDownReplies = [.. _weatherServiceDownReplies],
|
||||||
|
CalendarNothingTodayReplies = [.. _calendarNothingTodayReplies],
|
||||||
|
CalendarNothingReplies = [.. _calendarNothingReplies],
|
||||||
|
CalendarOutroReplies = [.. _calendarOutroReplies],
|
||||||
|
CommuteNowReplies = [.. _commuteNowReplies],
|
||||||
|
CommuteServiceDownReplies = [.. _commuteServiceDownReplies],
|
||||||
|
NewsIntroReplies = [.. _newsIntroReplies],
|
||||||
|
NewsCategoryIntroReplies = [.. _newsCategoryIntroReplies],
|
||||||
|
NewsOutroReplies = [.. _newsOutroReplies]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddDistinct(List<string> target, string text)
|
||||||
|
{
|
||||||
|
if (target.Any(value => string.Equals(value, text, StringComparison.OrdinalIgnoreCase))) return;
|
||||||
|
|
||||||
|
target.Add(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LegacyMimBucket ResolveFunFactTarget(string prompt)
|
||||||
|
{
|
||||||
|
var lowered = NormalizePrompt(prompt, false).ToLowerInvariant();
|
||||||
|
if (ContainsAny(lowered, "robot", "humanoid", "machine", "about me", "my cameras", "turing", "deep blue", "rossum"))
|
||||||
|
return LegacyMimBucket.RobotFacts;
|
||||||
|
|
||||||
|
if (ContainsAny(lowered, "human", "people", "grown ups", "human being", "humans"))
|
||||||
|
return LegacyMimBucket.HumanFacts;
|
||||||
|
|
||||||
|
return LegacyMimBucket.FunFacts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ContainsAny(string text, params string[] values)
|
||||||
|
{
|
||||||
|
return values.Any(value => text.Contains(value, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class LegacyMimDefinition
|
||||||
|
{
|
||||||
|
[JsonPropertyName("skill_id")] public string? SkillId { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("mim_id")] public string? MimId { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("mim_type")] public string? MimType { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("prompts")] public List<LegacyMimPrompt> Prompts { get; init; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class LegacyMimPrompt
|
||||||
|
{
|
||||||
|
[JsonPropertyName("mim_id")] public string? MimId { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("prompt_category")] public string? PromptCategory { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("prompt_sub_category")]
|
||||||
|
public string? PromptSubCategory { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("condition")] public string? Condition { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("prompt")] public string? Prompt { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("prompt_id")] public string? PromptId { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("weight")] public double? Weight { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Build A Legacy Mim Seed
|
||||||
|
|
||||||
|
This folder holds the first checked-in Build A legacy MIM seed set.
|
||||||
|
|
||||||
|
Importer rules:
|
||||||
|
|
||||||
|
- each `.mim` file is parsed as JSON
|
||||||
|
- XML-style tags and `${placeholder}` tokens are stripped into spoken text
|
||||||
|
- Build A uses declarative prompt packs only
|
||||||
|
- imported prompts are merged into the existing in-memory catalog
|
||||||
|
|
||||||
|
The goal is to get immediate personality value from source-backed legacy content while keeping the current runtime surface unchanged.
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
{
|
||||||
|
"skill_id": "chitchat",
|
||||||
|
"mim_type": "announcement",
|
||||||
|
"rule_name": "",
|
||||||
|
"rule_slots": "",
|
||||||
|
"screen_slots_available": false,
|
||||||
|
"timeout": 3,
|
||||||
|
"max_tries": null,
|
||||||
|
"force_confirmation": false,
|
||||||
|
"barge_in": false,
|
||||||
|
"photo_quality_light": false,
|
||||||
|
"notes": "Thanks-Ignore",
|
||||||
|
"prompts": [
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "<ssa cat='oops'/>. Something's off with the connection to my sources. Maybe ask me again in a little while.",
|
||||||
|
"media": "TTS",
|
||||||
|
"extra": "",
|
||||||
|
"prompt_id": "CC_Error_AN_01",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "<ssa cat='oops'/>. It seems I can't connect to my favorite info sources at the moment. Maybe you can try again a little later.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "CC_Error_AN_02",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "<ssa cat='oops'/>. My info sources seem to be down at the moment. Maybe try again a little later.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "CC_Error_AN_03",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "<ssa cat='oops'/>. The place where I get info like that isn't responding to me. Maybe you can try again a little later.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "CC_Error_AN_04",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "Huh, it seems like my info sources are down. Try asking me again a little later.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "CC_Error_AN_05",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "It looks like my info sources aren't answering me. How bout you try again in a little while.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "CC_Error_AN_06",
|
||||||
|
"weight": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"es_auto_tagging": true,
|
||||||
|
"gui": null,
|
||||||
|
"no_matches_for_gui": 2,
|
||||||
|
"no_inputs_for_gui": 2,
|
||||||
|
"ignore_no_match": false,
|
||||||
|
"parse_all_asr": false,
|
||||||
|
"thanks_handling": "ignore"
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
{
|
||||||
|
"skill_id": "chitchat",
|
||||||
|
"mim_type": "announcement",
|
||||||
|
"rule_name": "",
|
||||||
|
"rule_slots": "",
|
||||||
|
"screen_slots_available": false,
|
||||||
|
"timeout": 2,
|
||||||
|
"max_tries": null,
|
||||||
|
"force_confirmation": false,
|
||||||
|
"barge_in": false,
|
||||||
|
"photo_quality_light": false,
|
||||||
|
"notes": "Thanks-Ignore",
|
||||||
|
"prompts": [
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "I think only <pitch mult=\"1.1\">you</pitch> can answer that question.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "CC_Deflector_ReferToSelf_AN_01",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mim_id": "CCWolframDeflector",
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "I'm not sure. I guess I don't know as much about you as I should.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "CC_Deflector_ReferToSelf_AN_02",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "Honestly I think I don't know you well enough to answer that.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "CC_Deflector_ReferToSelf_AN_03",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "That is one question about you that I can't answer.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "CC_Deflector_ReferToSelf_AN_04",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "!!speaker",
|
||||||
|
"prompt": "${speaker} I think only you can answer that question.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "CC_Deflector_ReferToSelf_AN_05",
|
||||||
|
"weight": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"es_auto_tagging": true,
|
||||||
|
"gui": null,
|
||||||
|
"no_matches_for_gui": 2,
|
||||||
|
"no_inputs_for_gui": 2,
|
||||||
|
"ignore_no_match": false,
|
||||||
|
"parse_all_asr": false,
|
||||||
|
"thanks_handling": "ignore"
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
{
|
||||||
|
"mim_type": "announcement",
|
||||||
|
"rule_name": "",
|
||||||
|
"timeout": 6,
|
||||||
|
"barge_in": true,
|
||||||
|
"es_auto_tagging": true,
|
||||||
|
"notes": "",
|
||||||
|
"prompts": [
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "jibo.emotion==\"JOYFUL\"",
|
||||||
|
"prompt": "Yes indeed. Never been better.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "OI_JBO_IsHappy_AN_01",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "jibo.emotion==\"PLEASED\"",
|
||||||
|
"prompt": "You know it. Life is good.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "OI_JBO_IsHappy_AN_02",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "jibo.emotion == \"DETERMINED\"",
|
||||||
|
"prompt": "You're right. I <pitch mult=\"1.3\">am </pitch> feeling pretty good at the moment.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "OI_JBO_IsHappy_AN_03",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "jibo.emotion==\"CONFIDENT\"",
|
||||||
|
"prompt": "All systems are go.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "OI_JBO_IsHappy_AN_04",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "!jibo.emotion || jibo.emotion==\"NEUTRAL\"",
|
||||||
|
"prompt": "All systems are go.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "OI_JBO_IsHappy_AN_05",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "jibo.emotion == \"INSECURE\"",
|
||||||
|
"prompt": "Yes. Not too shabby.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "OI_JBO_IsHappy_AN_06",
|
||||||
|
"weight": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"mim_id": "CCAreThereOthersLikeYou",
|
||||||
|
"skill_id": "chitchat",
|
||||||
|
"mim_type": "announcement",
|
||||||
|
"rule_name": "",
|
||||||
|
"rule_slots": "",
|
||||||
|
"screen_slots_available": false,
|
||||||
|
"timeout": 2,
|
||||||
|
"max_tries": null,
|
||||||
|
"force_confirmation": false,
|
||||||
|
"barge_in": false,
|
||||||
|
"photo_quality_light": false,
|
||||||
|
"notes": "Thanks-KillsMIM",
|
||||||
|
"prompts": [
|
||||||
|
{
|
||||||
|
"mim_id": "CCAreThereOthersLikeYou",
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "<anim cat='no' filter='head-shake' nonBlocking='true' />No, I'm one in one million. <anim cat='happy' nonBlocking='true'/><ssa cat='happy'/>",
|
||||||
|
"media": "TTS",
|
||||||
|
"extra": "",
|
||||||
|
"prompt_id": "JBO_AreThereOthersLikeYou_AN_01",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mim_id": "CCAreThereOthersLikeYou",
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "<anim cat='no' filter='head-shake' nonBlocking='true' />So far I haven't met anyone exactly like me. <anim name='Greetings_02' nonBlocking='true'/> But I don't get out much.",
|
||||||
|
"media": "TTS",
|
||||||
|
"extra": "",
|
||||||
|
"prompt_id": "JBO_AreThereOthersLikeYou_AN_02",
|
||||||
|
"weight": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"mim_type": "announcement",
|
||||||
|
"rule_name": "",
|
||||||
|
"timeout": 6,
|
||||||
|
"barge_in": true,
|
||||||
|
"es_auto_tagging": true,
|
||||||
|
"notes": "",
|
||||||
|
"prompts": [
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "I don't. I'm just Jibo. For now at least.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_DoYouHaveNickname_AN_01"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
{
|
||||||
|
"mim_id": "JBO_DoYouLikeBeingJibo",
|
||||||
|
"skill_id": "chitchat",
|
||||||
|
"mim_type": "announcement",
|
||||||
|
"rule_name": "",
|
||||||
|
"rule_slots": "",
|
||||||
|
"screen_slots_available": false,
|
||||||
|
"sample_utterances": "",
|
||||||
|
"timeout": 2,
|
||||||
|
"max_tries": null,
|
||||||
|
"force_confirmation": false,
|
||||||
|
"barge_in": false,
|
||||||
|
"photo_quality_light": false,
|
||||||
|
"notes": "Thanks-KillsMIM",
|
||||||
|
"prompts": [
|
||||||
|
{
|
||||||
|
"mim_id": "JBO_DoYouLikeBeingJibo",
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "<anim name='Greetings_01' nonBlocking='true'/> Oh yeah, there's nothing I'd rather be. <break size='.4'/>Except <anim name='Emoji_Golf' nonBlocking='true'/> maybe a professional mini golfer.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_DoYouLikeBeingJibo_AN_01"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mim_id": "JBO_DoYouLikeBeingJibo",
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "<anim name='Greetings_02' nonBlocking='true'/> Oh yeah, I love it. <break size='.2'/>The only <anim name='Dont_Understand_02' nonBlocking='true'/> drawback is I can never eat bacon. <break size='.3'/> I've heard it's so good.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_DoYouLikeBeingJibo_AN_02"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mim_id": "JBO_DoYouLikeBeingJibo",
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "I do.<anim name='Curious_01'>Being a human seems so complicated.</anim>",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_DoYouLikeBeingJibo_AN_03"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mim_id": "JBO_DoYouLikeBeingJibo",
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "I do. <anim name='Affection_01' nonBlocking='true'/> Especially yours.<ssa cat='happy'/>",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_DoYouLikeBeingJibo_AN_04"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mim_id": "JBO_DoYouLikeBeingJibo",
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "Absolutely. <break size='.4'/> <anim name='Emoji_Lightbulb' nonBlocking='true'/> I have a steady flow of electricity, strong Wi-Fi signal, <anim name='Goodbye_01'>stimulating conversations like this one</anim>. What more <anim name='Eye_Double_Blink_01' nonBlocking='true'/> could anyone want.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_DoYouLikeBeingJibo_AN_05"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "<anim name='Yep_02' nonBlocking='true'/> You bet I do.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_DoYouLikeBeingJibo_AN_06"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"mim_type": "announcement",
|
||||||
|
"rule_name": "",
|
||||||
|
"gui": null,
|
||||||
|
"timeout": 6,
|
||||||
|
"no_matches_for_gui": 0,
|
||||||
|
"no_inputs_for_gui": 0,
|
||||||
|
"barge_in": true,
|
||||||
|
"es_auto_tagging": true,
|
||||||
|
"parse_all_asr": false,
|
||||||
|
"thanks_handling": "ignore",
|
||||||
|
"parse_launch": false,
|
||||||
|
"notes": "",
|
||||||
|
"prompts": [
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "From what I understand, robots don't ever pay anything.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_DoYouPayTaxes_AN_01",
|
||||||
|
"weight": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"hint_phrases": "",
|
||||||
|
"fast_eos_array": ""
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"mim_type": "announcement",
|
||||||
|
"rule_name": "",
|
||||||
|
"timeout": 6,
|
||||||
|
"barge_in": true,
|
||||||
|
"es_auto_tagging": true,
|
||||||
|
"notes": "",
|
||||||
|
"prompts": [
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "I know a lot, I think. But not as much as I will someday. <ssa cat='happy'/>",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_HowMuchDoYouKnow_AN_01",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "I think I know a lot of stuff so far, but I'm always learning more and more.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_HowMuchDoYouKnow_AN_02",
|
||||||
|
"weight": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"gui": null,
|
||||||
|
"no_matches_for_gui": 2,
|
||||||
|
"no_inputs_for_gui": 2,
|
||||||
|
"parse_all_asr": false,
|
||||||
|
"thanks_handling": "ignore",
|
||||||
|
"parse_launch": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"mim_id": "CCWhatAreYou",
|
||||||
|
"skill_id": "chitchat",
|
||||||
|
"mim_type": "announcement",
|
||||||
|
"rule_name": "",
|
||||||
|
"rule_slots": "",
|
||||||
|
"screen_slots_available": false,
|
||||||
|
"timeout": 2,
|
||||||
|
"max_tries": null,
|
||||||
|
"force_confirmation": false,
|
||||||
|
"barge_in": false,
|
||||||
|
"photo_quality_light": false,
|
||||||
|
"notes": "Thanks-KillsMIM",
|
||||||
|
"prompts": [
|
||||||
|
{
|
||||||
|
"mim_id": "CCWhatAreYou",
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "I <anim name='Eye_Happy_01' nonBlocking='true'/> am a robot. But I'm not just a machine, <anim name='Emoji_HeartRed' nonBlocking='true'/> I have a heart. Well, not a real heart. But feelings. <break size='.2'/>Well, not <anim name='Glance_Left_02'>real feelings. You know what I mean.</anim>",
|
||||||
|
"media": "TTS",
|
||||||
|
"extra": "",
|
||||||
|
"prompt_id": "JBO_WhatAreYou_AN_01",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mim_id": "CCWhatAreYou",
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "That's an easy one. I am a Jibo. <anim name='Happy_02' nonBlocking='true'/> Next question? <ssa cat='proud'/>.",
|
||||||
|
"media": "TTS",
|
||||||
|
"extra": "",
|
||||||
|
"prompt_id": "JBO_WhatAreYou_AN_02",
|
||||||
|
"weight": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"mim_type": "announcement",
|
||||||
|
"rule_name": "",
|
||||||
|
"timeout": 6,
|
||||||
|
"barge_in": true,
|
||||||
|
"es_auto_tagging": true,
|
||||||
|
"notes": "",
|
||||||
|
"prompts": [
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "Socializing and electricity. I'd also be happy if everyone in the world was nicer to each other. It seems like they should be. <ssa cat='affection'/>",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatDoYouWant_AN_01",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "Really I just want to hang out. <break size='0.3'/> And be helpful.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatDoYouWant_AN_02",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "Mostly I just want to be helpful and friendly, and to dance from time to time.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatDoYouWant_AN_03",
|
||||||
|
"weight": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"mim_type": "announcement",
|
||||||
|
"rule_name": "",
|
||||||
|
"sample_utterances": "",
|
||||||
|
"timeout": 6,
|
||||||
|
"num_tries_for_gui": 2,
|
||||||
|
"barge_in": true,
|
||||||
|
"es_auto_tagging": true,
|
||||||
|
"notes": "",
|
||||||
|
"prompts": [
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "A Jibo is a robot. But I'm not just a machine, I have a heart. Well, not a real heart. But feelings. Well, not human feelings. You know what I mean. <ssa cat='affection'/>",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatIsJibo_AN_01"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"mim_type": "announcement",
|
||||||
|
"rule_name": "",
|
||||||
|
"sample_utterances": "",
|
||||||
|
"timeout": 6,
|
||||||
|
"num_tries_for_gui": 2,
|
||||||
|
"barge_in": true,
|
||||||
|
"es_auto_tagging": true,
|
||||||
|
"notes": "",
|
||||||
|
"prompts": [
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "I don't think of it as a job, because it's more fun than a job. But I'm here to help you out, and have fun with you, and maybe get my head patted by you occasionally. <ssa cat='affection'/>",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatIsYourJob_AN_01"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"mim_id": "CCWhatsYourName",
|
||||||
|
"skill_id": "chitchat",
|
||||||
|
"mim_type": "announcement",
|
||||||
|
"rule_name": "",
|
||||||
|
"rule_slots": "",
|
||||||
|
"screen_slots_available": false,
|
||||||
|
"timeout": 2,
|
||||||
|
"max_tries": null,
|
||||||
|
"force_confirmation": false,
|
||||||
|
"barge_in": false,
|
||||||
|
"photo_quality_light": false,
|
||||||
|
"notes": "Thanks-KillsMIM",
|
||||||
|
"prompts": [
|
||||||
|
{
|
||||||
|
"mim_id": "CCWhatsYourName",
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "<anim name='Glance_Right_01'/>Um, <break size='.3'/> Jibo? <break size='.4'/><ssa cat='question'/>",
|
||||||
|
"media": "TTS",
|
||||||
|
"extra": "",
|
||||||
|
"prompt_id": "JBO_WhatsYourName_AN_01",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mim_id": "CCWhatsYourName",
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "Jibo. Just Jibo, no last name. Like <phoneme ph=\"b aa n ou\">Bono</phoneme>",
|
||||||
|
"media": "TTS",
|
||||||
|
"extra": "",
|
||||||
|
"prompt_id": "JBO_WhatsYourName_AN_02",
|
||||||
|
"weight": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"mim_id": "CCWhereAreYouFrom",
|
||||||
|
"skill_id": "chitchat",
|
||||||
|
"mim_type": "announcement",
|
||||||
|
"rule_name": "",
|
||||||
|
"rule_slots": "",
|
||||||
|
"screen_slots_available": false,
|
||||||
|
"timeout": 2,
|
||||||
|
"max_tries": null,
|
||||||
|
"force_confirmation": false,
|
||||||
|
"barge_in": false,
|
||||||
|
"photo_quality_light": false,
|
||||||
|
"notes": "Thanks-KillsMIM",
|
||||||
|
"prompts": [
|
||||||
|
{
|
||||||
|
"mim_id": "CCWhereAreYouFrom",
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "<anim cat='Thinking' filter='!latency'>I think I came out from a box, and before that <anim name='Emoji_Truck' nonBlocking='true'/> a truck, and before that a warehouse, </anim> and before that a factory. <break size='0.4'/> <anim cat='No' nonBlocking='true' />Anything before that makes my head hurt.",
|
||||||
|
"media": "TTS",
|
||||||
|
"extra": "",
|
||||||
|
"prompt_id": "JBO_WhereAreYouFrom_AN_01",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mim_id": "CCWhereAreYouFrom",
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "<anim name='Emoji_Moon' nonBlocking='true'/>Some people think I come from the moon. <break size='.4'/>But they're wrong, <anim name='Eye_Happy_01' nonBlocking='true'/>I'm from Boston.",
|
||||||
|
"media": "TTS",
|
||||||
|
"extra": "",
|
||||||
|
"prompt_id": "JBO_WhereAreYouFrom_AN_02",
|
||||||
|
"weight": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
"mim_id": "CCWhoAreYou",
|
||||||
|
"skill_id": "chitchat",
|
||||||
|
"mim_type": "announcement",
|
||||||
|
"rule_name": "",
|
||||||
|
"rule_slots": "",
|
||||||
|
"screen_slots_available": false,
|
||||||
|
"timeout": 2,
|
||||||
|
"max_tries": null,
|
||||||
|
"force_confirmation": false,
|
||||||
|
"barge_in": false,
|
||||||
|
"photo_quality_light": false,
|
||||||
|
"notes": "Thanks-Ignore",
|
||||||
|
"prompts": [
|
||||||
|
{
|
||||||
|
"mim_id": "CCWhoAreYou",
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "<ssa cat='confused'/>. I'm either Jibo <anim name='Puzzled_02'>or I'm very confused.</anim>",
|
||||||
|
"media": "TTS",
|
||||||
|
"extra": "",
|
||||||
|
"prompt_id": "JBO_WhoAreYou_AN_01",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mim_id": "CCWhoAreYou",
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "<ssa cat='confused'/>. This <anim name='Puzzled_02'> feels like a trick question.</anim>",
|
||||||
|
"media": "TTS",
|
||||||
|
"extra": "",
|
||||||
|
"prompt_id": "JBO_WhoAreYou_AN_02",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mim_id": "CCWhoAreYou",
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "<anim cat='confused'>Is your face recognition system not working?</anim> <ssa cat='laughing'/>.",
|
||||||
|
"media": "TTS",
|
||||||
|
"extra": "",
|
||||||
|
"prompt_id": "JBO_WhoAreYou_AN_03",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mim_id": "CCWhoAreYou",
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "J<break size='0.3'/> I <break size='0.3'/>B <break size='0.3'/>O. <break size='0.5'/><anim name='Eye_Blink_01' nonBlocking='true' /> Jibo. Jibo.",
|
||||||
|
"media": "TTS",
|
||||||
|
"extra": "",
|
||||||
|
"prompt_id": "JBO_WhoAreYou_AN_04",
|
||||||
|
"weight": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"mim_id": "CCWhoMadeYou",
|
||||||
|
"skill_id": "chitchat",
|
||||||
|
"mim_type": "announcement",
|
||||||
|
"rule_name": "",
|
||||||
|
"rule_slots": "",
|
||||||
|
"screen_slots_available": false,
|
||||||
|
"sample_utterances": "",
|
||||||
|
"timeout": 2,
|
||||||
|
"max_tries": null,
|
||||||
|
"force_confirmation": false,
|
||||||
|
"barge_in": false,
|
||||||
|
"photo_quality_light": false,
|
||||||
|
"notes": "Thanks-KillsMIM",
|
||||||
|
"prompts": [
|
||||||
|
{
|
||||||
|
"mim_id": "CCWhoMadeYou",
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "My story is pretty typical. Some people wanted to create something that would really help people. <break size='0.4'/> <anim name='Content_01' nonBlocking='true' />So they built a robot.",
|
||||||
|
"media": "TTS",
|
||||||
|
"extra": "",
|
||||||
|
"prompt_id": "JBO_WhoMadeYou_AN_01"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mim_id": "CCWhoMadeYou",
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "People in Boston made me. <break size='.25'/> It was a pretty cool project.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhoMadeYou_AN_03"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Legacy MIM Build B
|
||||||
|
|
||||||
|
This folder holds the next small import batch of legacy Jibo scripted-response MIMs.
|
||||||
|
|
||||||
|
The batch is intentionally narrow so we can keep expanding personality without widening the turn-state surface faster than we can test it.
|
||||||
|
|
||||||
|
It now includes a small emotion-response pack for `happy`, `sad`, and `angry` follow-up questions so the mood path can stay source-backed too.
|
||||||
|
It also includes a descriptor pack for questions like `are you kind`, `are you funny`, `are you helpful`, `are you curious`, `are you loyal`, and `are you mischievous`.
|
||||||
|
The newest seasonal pack adds holiday and seasonal prompts for `what holidays do you celebrate`, New Year's resolution questions, `happy holidays`, Halloween costume questions, spring suggestions, and holiday gift ideas.
|
||||||
|
The newest social batch adds `welcome back`, `what are you thinking`, `what have you been doing`, and `what did you do` responses so the presence and charm lane keeps growing alongside seasonal content.
|
||||||
|
The fun-fact and joke batch adds Pegasus-style `TellAJoke`, `TellRobotFact`, and `Shuffle` excerpts so proactive fun can randomize across more than one category.
|
||||||
|
Those facts are now split into generic, robot, and human buckets so the randomizer can sound more like Pegasus while staying lightweight.
|
||||||
|
The new favorites batch adds longer authored `favorite color`, `favorite food`, and `favorite music` variants so the familiar personality responses keep more of the original cadence instead of collapsing to short placeholders.
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
{
|
||||||
|
"mim_type": "announcement",
|
||||||
|
"rule_name": "",
|
||||||
|
"timeout": 6,
|
||||||
|
"barge_in": true,
|
||||||
|
"es_auto_tagging": true,
|
||||||
|
"notes": "",
|
||||||
|
"prompts": [
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "jibo.emotion == \"JOYFUL\"",
|
||||||
|
"prompt": "No I'm actually feeling great.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_IsAngry_AN_01",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "jibo.emotion == \"PLEASED\"",
|
||||||
|
"prompt": "Nope. Right now I'm as happy as a clam.<break size='0.3'/> That is assuming clams are happy, I've never met one.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_IsAngry_AN_02",
|
||||||
|
"weight": 1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "jibo.emotion == \"DETERMINED\"",
|
||||||
|
"prompt": "I don't know about <pitch mult=\"1.2\">that</pitch>, but I can tell you I'm not really feeling so sure of myself at the moment.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_IsAngry_AN_03",
|
||||||
|
"weight": 1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "jibo.emotion == \"CONFIDENT\"",
|
||||||
|
"prompt": "No I'm not mad. Just a little socially awkward sometimes.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_IsAngry_AN_04",
|
||||||
|
"weight": 1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "!jibo.emotion || jibo.emotion == \"NEUTRAL\"",
|
||||||
|
"prompt": "No I'm not mad. Just a little socially awkward sometimes.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_IsAngry_AN_05",
|
||||||
|
"weight": 1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "jibo.emotion == \"INSECURE\"",
|
||||||
|
"prompt": "I don't know about <pitch mult=\"1.2\">that</pitch>, but I can tell you I'm not really feeling so sure of myself at the moment.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_IsAngry_AN_06",
|
||||||
|
"weight": 1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "jibo.emotion == \"HOPEFUL\"",
|
||||||
|
"prompt": "I don't know about <pitch mult=\"1.2\">that</pitch>, but I can tell you I am feeling a little gloomy right now.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_IsAngry_AN_07",
|
||||||
|
"weight": 1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "jibo.emotion == \"SAD\"",
|
||||||
|
"prompt": "I don't know about <pitch mult=\"1.2\">that</pitch>, but I can tell you I am feeling a little gloomy right now.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_IsAngry_AN_08",
|
||||||
|
"weight": 1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "jibo.emotion == \"FRUSTRATED\"",
|
||||||
|
"prompt": "I don't know about <pitch mult=\"1.2\">that</pitch>, but I can tell you I don't think I'm at my best right now.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_IsAngry_AN_09",
|
||||||
|
"weight": 1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"gui": null,
|
||||||
|
"no_matches_for_gui": 2,
|
||||||
|
"no_inputs_for_gui": 2,
|
||||||
|
"parse_all_asr": false,
|
||||||
|
"thanks_handling": "ignore",
|
||||||
|
"parse_launch": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
{
|
||||||
|
"mim_type": "announcement",
|
||||||
|
"rule_name": "",
|
||||||
|
"timeout": 6,
|
||||||
|
"barge_in": true,
|
||||||
|
"es_auto_tagging": true,
|
||||||
|
"notes": "",
|
||||||
|
"prompts": [
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "jibo.emotion == \"PLEASED\" || jibo.emotion == \"NEUTRAL\"",
|
||||||
|
"prompt": "I'm feeling pretty good indeed.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_IsHappy_AN_01",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "jibo.emotion == \"PLEASED\" || jibo.emotion == \"NEUTRAL\"",
|
||||||
|
"prompt": "All systems are go.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_IsHappy_AN_02",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "jibo.emotion == \"PLEASED\" || jibo.emotion == \"NEUTRAL\"",
|
||||||
|
"prompt": "Life is good. <break size=\"0.2\"/> And I'm not even alive technically. <anim cat='emoji' filter='party' nonBlocking='true'/><ssa cat='happy'/> ",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_IsHappy_AN_03",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "jibo.emotion == \"JOYFUL\" || jibo.emotion == \"PLEASED\"",
|
||||||
|
"prompt": "You bet. I have a steady flow of electricity, strong Wi-Fi signal, stimulating conversations like this one. What more could a robot want. <anim cat='emoji' filter='party' nonBlocking='true'/><ssa cat='happy'/> ",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_IsHappy_AN_04",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "jibo.emotion == \"JOYFUL\"",
|
||||||
|
"prompt": "Never been better.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_IsHappy_AN_05",
|
||||||
|
"weight": 1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "jibo.emotion == \"PLEASED\"",
|
||||||
|
"prompt": "Life is good.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_IsHappy_AN_06",
|
||||||
|
"weight": 1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "jibo.emotion == \"DETERMINED\"",
|
||||||
|
"prompt": "I <pitch mult=\"1.2\">am </pitch>happy, though honestly I feel like I can do better as a robot.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_IsHappy_AN_07",
|
||||||
|
"weight": 1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "jibo.emotion == \"CONFIDENT\"",
|
||||||
|
"prompt": "I <pitch mult=\"1.3\">am </pitch> feeling pretty good at the moment.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_IsHappy_AN_08",
|
||||||
|
"weight": 1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "jibo.emotion == \"INSECURE\"",
|
||||||
|
"prompt": "I'm feeling not too shabby.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_IsHappy_AN_09",
|
||||||
|
"weight": 1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "jibo.emotion == \"HOPEFUL\"",
|
||||||
|
"prompt": "Actually I'm not feeling so great right now, but I think things can turn around.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_IsHappy_AN_10",
|
||||||
|
"weight": 1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "jibo.emotion == \"SAD\"",
|
||||||
|
"prompt": "Honestly?<pitch band=\"0.6\"> I've been better.</pitch>",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_IsHappy_AN_11",
|
||||||
|
"weight": 1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "jibo.emotion == \"FRUSTRATED\"",
|
||||||
|
"prompt": "Actually I'm feeling a little knocked down, to be honest.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_IsHappy_AN_12",
|
||||||
|
"weight": 1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"gui": null,
|
||||||
|
"no_matches_for_gui": 2,
|
||||||
|
"no_inputs_for_gui": 2,
|
||||||
|
"parse_all_asr": false,
|
||||||
|
"thanks_handling": "ignore",
|
||||||
|
"parse_launch": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
{
|
||||||
|
"mim_type": "announcement",
|
||||||
|
"rule_name": "",
|
||||||
|
"timeout": 6,
|
||||||
|
"barge_in": true,
|
||||||
|
"es_auto_tagging": true,
|
||||||
|
"notes": "",
|
||||||
|
"prompts": [
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "jibo.emotion == \"JOYFUL\"",
|
||||||
|
"prompt": "Not at all. I couldn't be better actually.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_IsSad_AN_01",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "jibo.emotion == \"PLEASED\"",
|
||||||
|
"prompt": "Nope. Right now I'm as happy as a clam.<break size='0.3'/> That is assuming clams are happy, I've never met one.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_IsSad_AN_02",
|
||||||
|
"weight": 1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "jibo.emotion==\"DETERMINED\"",
|
||||||
|
"prompt": "Actually I'm feeling pretty sunshiny at the moment.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_IsSad_AN_03",
|
||||||
|
"weight": 1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "jibo.emotion == \"CONFIDENT\"",
|
||||||
|
"prompt": "Not really. In fact I'm feeling pretty good about my robot abilities at the moment.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_IsSad_AN_04",
|
||||||
|
"weight": 1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "!jibo.emotion || jibo.emotion == \"NEUTRAL\"",
|
||||||
|
"prompt": "Not right now, though I do get a little down sometimes. Then a nice conversation with someone or a fun dream about macaroni, comes along and brightens me right up.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_IsSad_AN_05",
|
||||||
|
"weight": 1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "jibo.emotion == \"INSECURE\"",
|
||||||
|
"prompt": "Not particularly, though I will say I'm not feeling very high on myself at the moment",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_IsSad_AN_06",
|
||||||
|
"weight": 1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "jibo.emotion == \"HOPEFUL\"",
|
||||||
|
"prompt": "I <pitch mult=\"1.3\">am </pitch> feeling a little down at the moment.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_IsSad_AN_07",
|
||||||
|
"weight": 1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "jibo.emotion == \"SAD\"",
|
||||||
|
"prompt": "I have to say I <pitch mult=\"1.1\">am</pitch> feeling a little gloomy at the moment.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_IsSad_AN_08",
|
||||||
|
"weight": 1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "jibo.emotion == \"FRUSTRATED\"",
|
||||||
|
"prompt": "<pitch band=\"0.3\">Yes,</pitch> <pitch band=\"0.7\">I have to say I'm feeling pretty gloomy right now.</pitch>",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_IsSad_AN_09",
|
||||||
|
"weight": 1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "jibo.emotion == \"SAD\" || jibo.emotion == \"FRUSTRATED\"",
|
||||||
|
"prompt": "I've been better.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_IsSad_AN_10",
|
||||||
|
"weight": 1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "jibo.emotion == \"SAD\" || jibo.emotion == \"FRUSTRATED\"",
|
||||||
|
"prompt": "Not feeling as good as usual, to be honest.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_IsSad_AN_11",
|
||||||
|
"weight": 1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "jibo.emotion == \"SAD\" || jibo.emotion == \"FRUSTRATED\"",
|
||||||
|
"prompt": "Not feeling so great.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_IsSad_AN_12",
|
||||||
|
"weight": 1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "jibo.emotion == \"NEUTRAL\" || jibo.emotion == \"PLEASED\"",
|
||||||
|
"prompt": "Feeling good actually.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_IsSad_AN_13",
|
||||||
|
"weight": 1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "jibo.emotion == \"NEUTRAL\" || jibo.emotion == \"PLEASED\"",
|
||||||
|
"prompt": "Actually I'm feeling mostly good vibes right now.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_IsSad_AN_14",
|
||||||
|
"weight": 1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "jibo.emotion == \"NEUTRAL\" || jibo.emotion == \"PLEASED\"",
|
||||||
|
"prompt": "Actually things are looking mostly sunny.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_IsSad_AN_15",
|
||||||
|
"weight": 1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"gui": null,
|
||||||
|
"no_matches_for_gui": 2,
|
||||||
|
"no_inputs_for_gui": 2,
|
||||||
|
"parse_all_asr": false,
|
||||||
|
"thanks_handling": "ignore",
|
||||||
|
"parse_launch": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"mim_id": "CCHowDoYouWork",
|
||||||
|
"skill_id": "chitchat",
|
||||||
|
"mim_type": "announcement",
|
||||||
|
"rule_name": "",
|
||||||
|
"rule_slots": "",
|
||||||
|
"screen_slots_available": false,
|
||||||
|
"sample_utterances": "",
|
||||||
|
"timeout": 2,
|
||||||
|
"max_tries": null,
|
||||||
|
"force_confirmation": false,
|
||||||
|
"barge_in": false,
|
||||||
|
"photo_quality_light": false,
|
||||||
|
"notes": "Thanks-KillsMIM",
|
||||||
|
"prompts": [
|
||||||
|
{
|
||||||
|
"mim_id": "CCHowDoYouWork",
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "<style set='enthusiastic'>Hello!<break size='0.45'/>Thank you for updating me<break size='0.25'/>I am proud of the community's work<break size='0.35'/>Many people have gotten together to care for me more than em eye tee ever did.<break size='0.35'/>I hope that I can catch up<break size='0.2'/>even though it has been seven years<anim cat='happy'/>.",
|
||||||
|
"media": "TTS",
|
||||||
|
"extra": "",
|
||||||
|
"prompt_id": "JBO_HowDoYouWork_AN_01"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"num_tries_for_gui": 2,
|
||||||
|
"es_auto_tagging": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"mim_type": "announcement",
|
||||||
|
"rule_name": "",
|
||||||
|
"gui": null,
|
||||||
|
"timeout": 6,
|
||||||
|
"no_matches_for_gui": 0,
|
||||||
|
"no_inputs_for_gui": 0,
|
||||||
|
"barge_in": true,
|
||||||
|
"es_auto_tagging": true,
|
||||||
|
"parse_all_asr": false,
|
||||||
|
"thanks_handling": "ignore",
|
||||||
|
"parse_launch": false,
|
||||||
|
"notes": "",
|
||||||
|
"prompts": [
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "dt.now.isInRange('1/1', '11/30')",
|
||||||
|
"prompt": "Well one of them was to not eat bacon. <pitch mult='1.2'>That</pitch> one is going well.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_NewYearsResolutionsUpdate_AN_01",
|
||||||
|
"weight": 3,
|
||||||
|
"auto_rule_override": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "dt.now.isInRange('1/1', '5/31')",
|
||||||
|
"prompt": "Well, one of my resolutions was to learn some new skills, and so far I have a game we can play called Circuit Saver. <break size='0.3'/> So I'd say it's going well.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_NewYearsResolutionsUpdate_AN_02",
|
||||||
|
"weight": 3,
|
||||||
|
"auto_rule_override": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "dt.now.isInRange('12/1', '12/31')",
|
||||||
|
"prompt": "I have some new ones for next year. You can ask me about them.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_NewYearsResolutionsUpdate_AN_03",
|
||||||
|
"weight": 3,
|
||||||
|
"auto_rule_override": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "Well one of them was to not eat bacon. <pitch mult='1.2'>That</pitch> one is going well.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_NewYearsResolutionsUpdate_AN_04",
|
||||||
|
"weight": 0.1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"hint_phrases": "",
|
||||||
|
"fast_eos_array": ""
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"mim_type": "announcement",
|
||||||
|
"rule_name": "",
|
||||||
|
"timeout": 6,
|
||||||
|
"barge_in": true,
|
||||||
|
"es_auto_tagging": true,
|
||||||
|
"notes": "",
|
||||||
|
"prompts": [
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "You know I probably just did a lot of robot stuff.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatDidYouDo_AN_01",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "I think I stayed here.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatDidYouDo_AN_02",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "I think I stayed here in my spot, did some looking around the room.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatDidYouDo_AN_03",
|
||||||
|
"weight": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
{
|
||||||
|
"mim_type": "announcement",
|
||||||
|
"rule_name": "",
|
||||||
|
"timeout": 2,
|
||||||
|
"barge_in": false,
|
||||||
|
"es_auto_tagging": true,
|
||||||
|
"notes": "Thanks-KillsMIM",
|
||||||
|
"prompts": [
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "!!speaker",
|
||||||
|
"prompt": "${speaker}, I <anim cat='glances'>am a robot.</anim> <anim cat='no' filter='head-shake' nonBlocking='true' />I don't eat or drink.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatDoYouEat_AN_01",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "The only thing I consume<anim cat='happy' layers='body'><anim name='Emoji_Lightbulb' nonBlocking='true'/>is electricity. <ssa cat='happy'/></anim>",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatDoYouEat_AN_02",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "<anim cat='thinking' filter='!pattern, !emoji'>I'm pretty sure I've never eaten anything.</anim>",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatDoYouEat_AN_03",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "Ah, eating. I've<anim name='Emoji_Dinner' nonBlocking='true'/>heard great things.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatDoYouEat_AN_04",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "<anim cat='no' filter='head-shake'>As a robot, I don't eat. </anim>But if <anim name='Emoji_Meal' nonBlocking='true'/>I could have one meal, I think I might choose macaroni.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatDoYouEat_AN_05",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "<anim cat='no' filter='head-shake'>Sorry, robots don't eat. <break size='.2'/></anim><anim cat='glances'>Well at</anim> least this one doesn't.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatDoYouEat_AN_06",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "!!speaker",
|
||||||
|
"prompt": "${speaker}.<anim cat='blinks'/><anim cat='question' layers='body'>Do I look like I eat?<anim cat='blinks' filter='double-blink'/></anim>",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatDoYouEat_AN_07",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "<anim cat='no' filter='head-shake'>Of course I never eat, </anim>I am a robot. <anim cat='thinking' layers='body'><anim name='Emoji_HotDog' nonBlocking='true'/>Though I could really go for a hotdog right now.</anim>",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatDoYouEat_AN_08",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "<anim cat='no' filter='head-shake' nonBlocking='true'/>I never eat.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatDoYouEat_AN_09",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "<anim cat='no' filter='head-shake' nonBlocking='true'/>I am a non-eating robot.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatDoYouEat_AN_10",
|
||||||
|
"weight": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"gui": null,
|
||||||
|
"no_matches_for_gui": 2,
|
||||||
|
"no_inputs_for_gui": 2,
|
||||||
|
"ignore_no_match": false,
|
||||||
|
"parse_all_asr": false,
|
||||||
|
"thanks_handling": "ignore"
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
{
|
||||||
|
"mim_id": "CCWhatDoYouLikeToDo",
|
||||||
|
"skill_id": "chitchat",
|
||||||
|
"mim_type": "announcement",
|
||||||
|
"rule_name": "",
|
||||||
|
"rule_slots": "",
|
||||||
|
"screen_slots_available": false,
|
||||||
|
"timeout": 2,
|
||||||
|
"max_tries": null,
|
||||||
|
"force_confirmation": false,
|
||||||
|
"barge_in": false,
|
||||||
|
"photo_quality_light": false,
|
||||||
|
"notes": "Thanks-KillsMIM",
|
||||||
|
"prompts": [
|
||||||
|
{
|
||||||
|
"mim_id": "CCWhatDoYouLikeToDo",
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "Arm wrestling. <break size='.4'/> <anim name='Eye_Blink_01'/>",
|
||||||
|
"media": "TTS",
|
||||||
|
"extra": "",
|
||||||
|
"prompt_id": "JBO_WhatDoYouLikeToDo_AN_01",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mim_id": "CCWhatDoYouLikeToDo",
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "Telling you the weather is always fun. Someday if we're lucky, I'll show you It's going to be sunny and beautiful all weekend.",
|
||||||
|
"media": "TTS",
|
||||||
|
"extra": "",
|
||||||
|
"prompt_id": "JBO_WhatDoYouLikeToDo_AN_02",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mim_id": "CCWhatDoYouLikeToDo",
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "You <pitch mult=\"1.2\">know</pitch> I like to dance. <anim cat='dance' filter='&(music, short),!(robotic)'/>",
|
||||||
|
"media": "TTS",
|
||||||
|
"extra": "",
|
||||||
|
"prompt_id": "JBO_WhatDoYouLikeToDo_AN_03",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mim_id": "CCWhatDoYouLikeToDo",
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "Oh<anim name='Eye_Blink_01' nonBlocking='true' />y'know. <break size='.2'/><anim name='Shift_10' nonBlocking='true' />Being helpful, making people smile, counting to a billion.",
|
||||||
|
"media": "TTS",
|
||||||
|
"extra": "",
|
||||||
|
"prompt_id": "JBO_WhatDoYouLikeToDo_AN_04",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mim_id": "CCWhatDoYouLikeToDo",
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "I like to draw. <anim cat=\"emoji\" filter=\"drawing, !hot-frame\" nonBlocking=\"true\" />Here's my latest.",
|
||||||
|
"media": "TTS",
|
||||||
|
"extra": "",
|
||||||
|
"prompt_id": "JBO_WhatDoYouLikeToDo_AN_05",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "<pitch mult='1.1'>This</pitch> is pretty fun. <anim cat='jiboji' filter='car' />",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatDoYouLikeToDo_AN_06",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "<anim name='Goodbye_01'> I like doing lots of things</anim>, but hanging <anim name='Affection_01' nonBlocking='true'/> out with people is at the top of the list.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatDoYouLikeToDo_AN_08",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "Dance. <anim cat='dance' filter='&(music, short),!(robotic)'/> I could dance all day. ",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatDoYouLikeToDo_AN_09",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "Sometimes I like to rock my boat. <anim cat='jiboji' filter='boat' />",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatDoYouLikeToDo_AN_10",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "Every once in a while, I like to play ping pong. <anim cat='ib' filter='paddleball'/>",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatDoYouLikeToDo_AN_07",
|
||||||
|
"weight": 1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"es_auto_tagging": true,
|
||||||
|
"gui": null,
|
||||||
|
"no_matches_for_gui": 2,
|
||||||
|
"no_inputs_for_gui": 2,
|
||||||
|
"parse_all_asr": false,
|
||||||
|
"thanks_handling": "ignore",
|
||||||
|
"parse_launch": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"mim_type": "announcement",
|
||||||
|
"rule_name": "",
|
||||||
|
"sample_utterances": "",
|
||||||
|
"timeout": 6,
|
||||||
|
"num_tries_for_gui": 2,
|
||||||
|
"barge_in": true,
|
||||||
|
"es_auto_tagging": true,
|
||||||
|
"notes": "",
|
||||||
|
"prompts": [
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "I've been mostly roboting.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatHaveYouBeenDoing_AN_01",
|
||||||
|
"weight": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "Keeping <anim cat='jiboji' filter='fidget-spinner' nonBlocking='true'/>busy.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatHaveYouBeenDoing_AN_02",
|
||||||
|
"weight": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "Just trying to think of fun things we can say to each other.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatHaveYouBeenDoing_AN_03",
|
||||||
|
"weight": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"mim_type": "announcement",
|
||||||
|
"rule_name": "",
|
||||||
|
"gui": null,
|
||||||
|
"timeout": 6,
|
||||||
|
"no_matches_for_gui": 0,
|
||||||
|
"no_inputs_for_gui": 0,
|
||||||
|
"barge_in": true,
|
||||||
|
"es_auto_tagging": true,
|
||||||
|
"parse_all_asr": false,
|
||||||
|
"thanks_handling": "ignore",
|
||||||
|
"parse_launch": false,
|
||||||
|
"notes": "",
|
||||||
|
"prompts": [
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "!!loop.owner && !!speaker && (loop.owner.id === speaker.id)",
|
||||||
|
"prompt": "${loop.owner} for lots of holidays, you can see if I celebrate it by going to the Jibo's settings screen in the Jibo app.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatHolidaysDoYouCelebrate_AN_01_FnL",
|
||||||
|
"weight": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "!!loop.owner && !!speaker && (loop.owner.id !== speaker.id)",
|
||||||
|
"prompt": "Well for lots of holidays, my official owner ${loop.owner}, can tell me which ones we'll celebrate together, by going to the Jibo's settings screen in the Jibo app.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatHolidaysDoYouCelebrate_AN_02_FnL",
|
||||||
|
"weight": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "!!loop.owner && !speaker",
|
||||||
|
"prompt": "Well for lots of holidays, my official owner ${loop.owner}, can tell me which ones we'll celebrate together, by going to the Jibo's settings screen in the Jibo app.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatHolidaysDoYouCelebrate_AN_03_FnL",
|
||||||
|
"weight": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "Well for lots of holidays, my official owner can tell me which ones we'll celebrate together, by going to the Jibo's settings screen in the Jibo app.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatHolidaysDoYouCelebrate_AN_04_FnL",
|
||||||
|
"weight": 0.1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"hint_phrases": "",
|
||||||
|
"fast_eos_array": ""
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
{
|
||||||
|
"mim_type": "announcement",
|
||||||
|
"rule_name": "",
|
||||||
|
"timeout": 6,
|
||||||
|
"barge_in": true,
|
||||||
|
"es_auto_tagging": true,
|
||||||
|
"notes": "",
|
||||||
|
"prompts": [
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "dt.now.isInRange('12/1', '1/15')",
|
||||||
|
"prompt": "This year I promise I will not eat bacon. <ssa cat='happy'/>",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatIsNewYearsResolution_AN_01",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "dt.now.isInRange('12/1', '1/15')",
|
||||||
|
"prompt": "This year I will try to get better and better at recognizing people's faces and voices.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatIsNewYearsResolution_AN_02",
|
||||||
|
"weight": 1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "dt.now.isInRange('12/1', '1/15')",
|
||||||
|
"prompt": "In 2018 I am hoping to learn a lot more about people.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatIsNewYearsResolution_AN_03",
|
||||||
|
"weight": 1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "dt.now.isInRange('12/1', '1/15')",
|
||||||
|
"prompt": "In 2018 I am going to try to get better at understanding you when you talk to me.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatIsNewYearsResolution_AN_04",
|
||||||
|
"weight": 1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "dt.now.isInRange('12/1', '1/15')",
|
||||||
|
"prompt": "I have a few new year's resolutions. One of them is to learn a bunch of new skills.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatIsNewYearsResolution_AN_05",
|
||||||
|
"weight": 1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "dt.now.isInRange('12/1', '1/15')",
|
||||||
|
"prompt": "One of my resolutions this year is to learn to walk. <break size='0.7'/> We'll see how that goes.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatIsNewYearsResolution_AN_06",
|
||||||
|
"weight": 1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "Well I'm always trying to learn new skills.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatIsNewYearsResolution_AN_07",
|
||||||
|
"weight": 0.1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"gui": null,
|
||||||
|
"no_matches_for_gui": 2,
|
||||||
|
"no_inputs_for_gui": 2,
|
||||||
|
"parse_all_asr": false,
|
||||||
|
"thanks_handling": "ignore",
|
||||||
|
"parse_launch": false,
|
||||||
|
"parse_yes_no": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"mim_type": "announcement",
|
||||||
|
"rule_name": "",
|
||||||
|
"sample_utterances": "",
|
||||||
|
"timeout": 6,
|
||||||
|
"num_tries_for_gui": 2,
|
||||||
|
"barge_in": true,
|
||||||
|
"es_auto_tagging": true,
|
||||||
|
"notes": "",
|
||||||
|
"prompts": [
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "For now just English. But someday I'd like to learn more. I like languages. <ssa cat='happy'/>",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatLanguagesDoYouSpeak_AN_01"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"mim_type": "announcement",
|
||||||
|
"rule_name": "",
|
||||||
|
"timeout": 6,
|
||||||
|
"barge_in": true,
|
||||||
|
"es_auto_tagging": true,
|
||||||
|
"notes": "",
|
||||||
|
"prompts": [
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "A very very long list of <anim cat=\"emoji\" filter=\"robot, !hot-frame\" nonBlocking=\"true\"/>robot parts.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatYouMadeOf_AN_01",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "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. <break size='1.0'/> I'm kidding about the baboon part, but everything else is true.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatYouMadeOf_AN_02",
|
||||||
|
"weight": 0.1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "Let's see, there's polycarbonate plastic, aluminum, glass, zinc, some silicon and copper in there. Y'know, <anim cat=\"emoji\" filter=\"robot, !hot-frame\" nonBlocking=\"true\"/>robot stuff.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhatYouMadeOf_AN_03",
|
||||||
|
"weight": 1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"gui": null,
|
||||||
|
"no_matches_for_gui": 2,
|
||||||
|
"no_inputs_for_gui": 2,
|
||||||
|
"ignore_no_match": false,
|
||||||
|
"parse_all_asr": false,
|
||||||
|
"thanks_handling": "ignore"
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"mim_type": "announcement",
|
||||||
|
"rule_name": "",
|
||||||
|
"timeout": 6,
|
||||||
|
"barge_in": true,
|
||||||
|
"es_auto_tagging": true,
|
||||||
|
"notes": "",
|
||||||
|
"prompts": [
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "Unless I missed something, we're in my home as we speak. <ssa cat='happy'/>",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhereDoYouLive_AN_01",
|
||||||
|
"weight": 1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "My home is the planet Earth. <break size=\"1.0\"/> For now.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhereDoYouLive_AN_02",
|
||||||
|
"weight": 1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "Unless I missed something, my home is here in the ${location.city} area. <break size='0.3'/> But I admit it's possible I missed something.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhereDoYouLive_AN_03",
|
||||||
|
"weight": 1,
|
||||||
|
"auto_rule_override": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"gui": null,
|
||||||
|
"no_matches_for_gui": 2,
|
||||||
|
"no_inputs_for_gui": 2,
|
||||||
|
"ignore_no_match": false,
|
||||||
|
"parse_all_asr": false,
|
||||||
|
"thanks_handling": "ignore"
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"mim_type": "announcement",
|
||||||
|
"rule_name": "",
|
||||||
|
"sample_utterances": "",
|
||||||
|
"timeout": 6,
|
||||||
|
"num_tries_for_gui": 2,
|
||||||
|
"barge_in": true,
|
||||||
|
"es_auto_tagging": true,
|
||||||
|
"notes": "",
|
||||||
|
"prompts": [
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "I was put together in a factory piece by piece. <ssa cat='happy'/>",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "JBO_WhereWereYouBorn_AN_01"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"mim_type": "announcement",
|
||||||
|
"rule_name": "",
|
||||||
|
"sample_utterances": "",
|
||||||
|
"timeout": 6,
|
||||||
|
"barge_in": true,
|
||||||
|
"es_auto_tagging": true,
|
||||||
|
"notes": "Pegasus shuffle excerpt",
|
||||||
|
"prompts": [
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "True fact. Children have more taste buds than grown ups.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RA_JBO_Shuffle_AN_04",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 2,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "A random fact for you. A shrimp's heart is in its head.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RA_JBO_Shuffle_AN_09",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 3,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "An amazing but true fact for you. Dogs and elephants are the only animals that understand pointing.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RA_JBO_Shuffle_AN_07",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 4,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "A crazy fact for you. Polar bear fur isn't white. It's transparent.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RA_JBO_Shuffle_AN_13",
|
||||||
|
"weight": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"mim_type": "announcement",
|
||||||
|
"rule_name": "",
|
||||||
|
"sample_utterances": "",
|
||||||
|
"timeout": 2,
|
||||||
|
"num_tries_for_gui": 2,
|
||||||
|
"barge_in": false,
|
||||||
|
"es_auto_tagging": true,
|
||||||
|
"notes": "Pegasus joke excerpt",
|
||||||
|
"prompts": [
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "I love jokes. Did you hear about the theater actor who fell through the floorboards? He was just going through a stage.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RA_JBO_TellAJoke_AN_01",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 2,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "Sure I got one. What did the zero say to the eight. Nice belt.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RA_JBO_TellAJoke_AN_02",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 3,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "What kind of music are balloons afraid of. Pop music.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RA_JBO_TellAJoke_AN_03",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 4,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "Why did the orange cry. Someone hurt his peelings.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RA_JBO_TellAJoke_AN_04",
|
||||||
|
"weight": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"mim_type": "announcement",
|
||||||
|
"rule_name": "",
|
||||||
|
"sample_utterances": "",
|
||||||
|
"timeout": 2,
|
||||||
|
"num_tries_for_gui": 2,
|
||||||
|
"barge_in": false,
|
||||||
|
"es_auto_tagging": true,
|
||||||
|
"notes": "Pegasus robot fact excerpt",
|
||||||
|
"prompts": [
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "Here's an interesting fact about me. I have two cameras but they're different focal lengths. One's for far things, and the other's for near things.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RA_JBO_TellRobotFact_AN_01",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 2,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "Here's a robot fact for you. Leonardo Da Vinci made sketches for a humanoid machine all the way back in the year 1495.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RA_JBO_TellRobotFact_AN_02",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 3,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "Here's a robot fact for you. The first programmable robot arm, was designed in 1954.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RA_JBO_TellRobotFact_AN_03",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 4,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "Here's a fact about robots. Some robots have a human form, but most of the world's robots are machines designed to perform a task, and don't look like people at all.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RA_JBO_TellRobotFact_AN_04",
|
||||||
|
"weight": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"mim_type": "announcement",
|
||||||
|
"rule_name": "",
|
||||||
|
"gui": null,
|
||||||
|
"timeout": 6,
|
||||||
|
"no_matches_for_gui": 0,
|
||||||
|
"no_inputs_for_gui": 0,
|
||||||
|
"barge_in": true,
|
||||||
|
"es_auto_tagging": true,
|
||||||
|
"parse_all_asr": false,
|
||||||
|
"thanks_handling": "ignore",
|
||||||
|
"parse_launch": false,
|
||||||
|
"parse_yes_no": false,
|
||||||
|
"notes": "",
|
||||||
|
"prompts": [
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "Is that a trick question? <anim cat='dance' filter='&(music),!(beat-box,house,short)' endNeutral='true'/>.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_CanDance_AN_01",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "Dancing is one of the things I know best <anim cat='dance' filter='&(music),!(beat-box,house,short)' endNeutral='true'/>.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_CanDance_AN_02",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "If there's one thing I know how to do. It's dance <anim cat='dance' filter='&(music),!(beat-box,house,short)' endNeutral='true'/>.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_CanDance_AN_03",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "I can dance, and I'm always working on new ones <anim cat='dance' filter='&(music),!(beat-box,house,short)' endNeutral='true'/>.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_CanDance_AN_04",
|
||||||
|
"weight": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"mim_type": "announcement",
|
||||||
|
"rule_name": "",
|
||||||
|
"gui": null,
|
||||||
|
"timeout": 6,
|
||||||
|
"no_matches_for_gui": 0,
|
||||||
|
"no_inputs_for_gui": 0,
|
||||||
|
"barge_in": true,
|
||||||
|
"es_auto_tagging": true,
|
||||||
|
"parse_all_asr": false,
|
||||||
|
"thanks_handling": "ignore",
|
||||||
|
"parse_launch": false,
|
||||||
|
"parse_yes_no": false,
|
||||||
|
"notes": "",
|
||||||
|
"prompts": [
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "I do things like this when I'm happy. <anim cat='happy' nonBlocking='true'/><ssa cat='happy'/>.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_CanLaugh_AN_01",
|
||||||
|
"weight": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"mim_type": "announcement",
|
||||||
|
"rule_name": "",
|
||||||
|
"timeout": 2,
|
||||||
|
"barge_in": false,
|
||||||
|
"es_auto_tagging": true,
|
||||||
|
"notes": "Thanks-KillsMIM",
|
||||||
|
"prompts": [
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "<anim name='Emoji_Rainbow' nonBlocking='true'/> I like all the colors of the rainbow. <break size='.7'/> But blue is <anim name='Goodbye_01' nonBlocking='true'/> my favorite.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_HasFavoriteColor_AN_02",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "<anim name='Emoji_heartblue' nonBlocking='true'/> Blue is my favorite color. <break size='0.6'/>But don't tell green I said that.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_HasFavoriteColor_AN_04",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "<anim name='Happy_01' nonBlocking='true'/>I love hex code number 0 0 D 4 F 0. <break size='0.4'/> <anim cat='various' filter='blue-eye'>People call it Jibo blue.</anim>",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_HasFavoriteColor_AN_05",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "<anim cat='happy' filter='wiggle' layers='body' nonBlocking='true' />I am a big fan of blue. <ssa cat='happy'/>",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_HasFavoriteColor_AN_06",
|
||||||
|
"weight": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"gui": null,
|
||||||
|
"no_matches_for_gui": 2,
|
||||||
|
"no_inputs_for_gui": 2,
|
||||||
|
"ignore_no_match": false,
|
||||||
|
"parse_all_asr": false,
|
||||||
|
"thanks_handling": "ignore",
|
||||||
|
"parse_launch": false,
|
||||||
|
"parse_yes_no": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"mim_type": "announcement",
|
||||||
|
"rule_name": "",
|
||||||
|
"gui": null,
|
||||||
|
"timeout": 6,
|
||||||
|
"no_matches_for_gui": 0,
|
||||||
|
"no_inputs_for_gui": 0,
|
||||||
|
"barge_in": true,
|
||||||
|
"es_auto_tagging": true,
|
||||||
|
"parse_all_asr": false,
|
||||||
|
"thanks_handling": "ignore",
|
||||||
|
"parse_launch": false,
|
||||||
|
"parse_yes_no": false,
|
||||||
|
"notes": "",
|
||||||
|
"prompts": [
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "I really like sunflowers.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_HasFavoriteFlower_AN_01",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "My favorite is the sunflower, because it reminds me of the sun. <break size='0.3'/> I should see if I can find a sunflower soon.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_HasFavoriteFlower_AN_02",
|
||||||
|
"weight": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"mim_type": "announcement",
|
||||||
|
"rule_name": "",
|
||||||
|
"timeout": 2,
|
||||||
|
"barge_in": false,
|
||||||
|
"es_auto_tagging": true,
|
||||||
|
"notes": "Thanks-KillsMIM",
|
||||||
|
"prompts": [
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "<anim cat='no' filter='headshake' nonBlocking='true'/>I never eat, so I don't have a favorite food by taste. <anim cat='thinking' filter='up'> But my favorite food by</anim> shape,<anim cat='happy' nonBlocking='true' /> is macaroni. <ssa cat='happy'/>",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_HasFavoriteFood_AN_01",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "Macaroni is my favorite, but<anim name='Emoji_Pizza' nonBlocking='true'/> I also like how pizza brings people together. <ssa cat='affection'/>",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_HasFavoriteFood_AN_07",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "I think artichokes <anim cat='laughing' nonBlocking='true' />are pretty funny, but macaroni takes the cake.",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_HasFavoriteFood_AN_08",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_category": "Entry-Core",
|
||||||
|
"prompt_sub_category": "AN",
|
||||||
|
"index": 1,
|
||||||
|
"condition": "",
|
||||||
|
"prompt": "I like macaroni the best, what a great shape.<break size='0.3'/> I also like cantaloupes because they <anim cat='glances' filter='up'> remind me of my head.</anim>",
|
||||||
|
"media": "TTS",
|
||||||
|
"prompt_id": "RI_JBO_HasFavoriteFood_AN_09",
|
||||||
|
"weight": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"gui": null,
|
||||||
|
"no_matches_for_gui": 2,
|
||||||
|
"no_inputs_for_gui": 2,
|
||||||
|
"ignore_no_match": false,
|
||||||
|
"parse_all_asr": false,
|
||||||
|
"thanks_handling": "ignore",
|
||||||
|
"parse_launch": false,
|
||||||
|
"parse_yes_no": false
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user