Compare commits
39 Commits
7c6dacdbd8
...
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 |
@@ -1,7 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
<configuration>
|
<configuration>
|
||||||
<packageSources>
|
<packageSources>
|
||||||
<clear />
|
<clear />
|
||||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||||
</packageSources>
|
</packageSources>
|
||||||
</configuration>
|
</configuration>
|
||||||
@@ -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" />
|
||||||
|
|||||||
@@ -194,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
|
||||||
|
|||||||
@@ -461,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
|
||||||
@@ -473,6 +474,9 @@ 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
|
||||||
|
|
||||||
@@ -654,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
|
||||||
|
|
||||||
@@ -669,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
|
||||||
|
|
||||||
@@ -731,6 +738,7 @@ Current release theme:
|
|||||||
- added memory/transcript category hint plumbing for provider requests (sports/technology/business/general)
|
- 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
|
- 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
|
- 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:
|
- Source anchors:
|
||||||
- `C:\Projects\jibo\pegasus\packages\report-skill\src\subskills\weather\WeatherMimLogic.ts`
|
- `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\resources\views\weatherHiLo.json`
|
||||||
@@ -760,6 +768,140 @@ Current release theme:
|
|||||||
- first schema for list items + ownership scope
|
- first schema for list items + ownership scope
|
||||||
- initial voice flows and follow-up intent handling defined
|
- 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`:
|
||||||
@@ -783,13 +925,20 @@ For `1.0.19`:
|
|||||||
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)
|
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. Holidays and seasonal personality behavior built on the new memory/proactivity foundation
|
8. Holidays and seasonal personality behavior built on the new memory/proactivity foundation
|
||||||
9. Durable memory persistence path (multi-tenant backing store)
|
9. Durable memory persistence path (multi-tenant backing store)
|
||||||
10. Update, backup, and restore proof
|
- reference design captured in `docs/persistence-architecture.md`
|
||||||
|
- store contracts are now tightened around account/loop/device/person scoping, revision tracking, and explicit load/save boundaries
|
||||||
|
- 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
|
||||||
|
- 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
|
11. STT upgrade and noise screening
|
||||||
12. Hosted capture/storage plan / indexing for group testing
|
12. Hosted capture/storage plan / indexing for group testing
|
||||||
13. Binary-safe media storage / sync to cloud drive: OneDrive, Google Drive, Box, etc.
|
13. Binary-safe media storage / sync to cloud drive: OneDrive, Google Drive, Box, etc.
|
||||||
14. Provider-backed news and weather parity polish
|
14. Provider-backed news and weather parity polish
|
||||||
15. Grocery list capability discovery and MVP selection
|
15. Grocery list capability discovery and MVP selection
|
||||||
16. Lasso, identity, and onboarding as larger discovery-driven tracks
|
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:
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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:
|
||||||
@@ -196,17 +337,20 @@ First completed slice in this personal-report parity track:
|
|||||||
|
|
||||||
## 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. Presence-aware greetings and identity-triggered proactivity (reactive/proactive split, cooldowns, person-aware greeting hooks)
|
2. Dialog parsing expansion
|
||||||
3. Personal report parity slices (weather visual layer, live news path, commute path, calendar parity matrix)
|
3. Presence-aware greetings and identity-triggered proactivity
|
||||||
4. Holidays and seasonal personality slice beyond pizza day (time-scoped content backed by memory/proactivity path)
|
4. Personal report parity slices
|
||||||
5. Durable memory persistence path (swap in provider-backed multi-tenant storage while preserving behavior contracts)
|
5. Holidays and seasonal personality slice beyond pizza day
|
||||||
6. Update/backup/restore end-to-end proof (operator-run and documented)
|
6. Durable memory persistence path
|
||||||
7. STT noise-screening and short-utterance reliability pass
|
7. Update/backup/restore end-to-end proof - implemented
|
||||||
8. Provider-backed news expansion and deeper weather parity using Pegasus-backed contracts
|
8. STT noise-screening and short-utterance reliability pass
|
||||||
9. Capture indexing and retention boundary for group testing
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<!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>
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -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
|
||||||
</div>
|
</label
|
||||||
<div>
|
><input id="staticIP" placeholder="192.168.1.100"/>
|
||||||
<label>Netmask</label
|
</div>
|
||||||
><input id="netmask" placeholder="255.255.255.0" />
|
<div>
|
||||||
</div>
|
<label>
|
||||||
</div>
|
Netmask
|
||||||
<div class="row">
|
</label
|
||||||
<div>
|
><input id="netmask" placeholder="255.255.255.0"/>
|
||||||
<label>Gateway</label
|
</div>
|
||||||
><input id="gateway" placeholder="192.168.1.1" />
|
</div>
|
||||||
</div>
|
<div class="row">
|
||||||
<div>
|
<div>
|
||||||
<label>DNS 1</label><input id="dns1" placeholder="8.8.8.8" />
|
<label>
|
||||||
</div>
|
Gateway
|
||||||
</div>
|
</label
|
||||||
<div><label>DNS 2</label><input id="dns2" placeholder="8.8.4.4" /></div>
|
><input id="gateway" placeholder="192.168.1.1"/>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>DNS 1</label><input id="dns1" placeholder="8.8.8.8"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>DNS 2</label><input id="dns2" placeholder="8.8.4.4"/>
|
||||||
|
</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;
|
||||||
@@ -38,7 +39,7 @@ app.Use(async (context, next) =>
|
|||||||
|
|
||||||
var webSocketService = context.RequestServices.GetRequiredService<JiboWebSocketService>();
|
var webSocketService = context.RequestServices.GetRequiredService<JiboWebSocketService>();
|
||||||
var telemetrySink = context.RequestServices.GetRequiredService<IWebSocketTelemetrySink>();
|
var telemetrySink = context.RequestServices.GetRequiredService<IWebSocketTelemetrySink>();
|
||||||
|
|
||||||
using var socket = await context.WebSockets.AcceptWebSocketAsync();
|
using var socket = await context.WebSockets.AcceptWebSocketAsync();
|
||||||
|
|
||||||
var openEnvelope = new WebSocketMessageEnvelope
|
var openEnvelope = new WebSocketMessageEnvelope
|
||||||
@@ -74,7 +75,7 @@ app.Use(async (context, next) =>
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var envelope = new WebSocketMessageEnvelope
|
var envelope = new WebSocketMessageEnvelope
|
||||||
{
|
{
|
||||||
ConnectionId = Guid.NewGuid().ToString("N"),
|
ConnectionId = Guid.NewGuid().ToString("N"),
|
||||||
@@ -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 "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)
|
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";
|
||||||
}
|
}
|
||||||
@@ -265,4 +239,4 @@ static CloudSession ResolveSession(JiboWebSocketService webSocketService, WebSoc
|
|||||||
return webSocketService.GetOrCreateSession(envelope);
|
return webSocketService.GetOrCreateSession(envelope);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed record ReceivedSocketMessage(WebSocketMessageType MessageType, byte[] Buffer);
|
internal sealed record ReceivedSocketMessage(WebSocketMessageType MessageType, byte[] Buffer);
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -25,4 +25,4 @@ public sealed record NewsBriefingSnapshot(
|
|||||||
string? ProviderMessage = null,
|
string? ProviderMessage = null,
|
||||||
int? ProviderHttpStatusCode = null,
|
int? ProviderHttpStatusCode = null,
|
||||||
string? ProviderEndpoint = null,
|
string? ProviderEndpoint = null,
|
||||||
string? ProviderErrorCode = 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,13 +16,26 @@ 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
|
||||||
{
|
{
|
||||||
Like,
|
Like,
|
||||||
Love,
|
Love,
|
||||||
Dislike
|
Dislike
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -22,4 +22,4 @@ public sealed record WeatherReportSnapshot(
|
|||||||
int? HighTemperature,
|
int? HighTemperature,
|
||||||
int? LowTemperature,
|
int? LowTemperature,
|
||||||
string? Condition,
|
string? Condition,
|
||||||
bool UseCelsius);
|
bool UseCelsius);
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
var emotionVariants = ResolveEmotionVariants(currentEmotion);
|
||||||
|
foreach (var reply in catalog.EmotionReplies)
|
||||||
|
if (ConditionMatches(reply.Condition, emotionVariants))
|
||||||
|
return reply.Reply;
|
||||||
|
|
||||||
|
return randomizer.Choose(catalog.HowAreYouReplies);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
{
|
{
|
||||||
return true;
|
"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;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryResolveEmotionFromText(loweredTranscript, out _))
|
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)
|
||||||
{
|
{
|
||||||
return false;
|
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,16 +569,11 @@ 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,18 +607,14 @@ 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);
|
||||||
{
|
if (string.IsNullOrWhiteSpace(normalizedSynonym) ||
|
||||||
var normalizedSynonym = NormalizeForPhraseMatching(synonym);
|
!seen.Add(normalizedSynonym))
|
||||||
if (string.IsNullOrWhiteSpace(normalizedSynonym) ||
|
continue;
|
||||||
!seen.Add(normalizedSynonym))
|
|
||||||
{
|
|
||||||
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));
|
||||||
|
|||||||
@@ -13,4 +13,4 @@ public sealed class DefaultSttStrategySelector(IEnumerable<ISttStrategy> strateg
|
|||||||
? throw new InvalidOperationException("No STT strategy can handle the current turn.")
|
? throw new InvalidOperationException("No STT strategy can handle the current turn.")
|
||||||
: Task.FromResult(strategy);
|
: Task.FromResult(strategy);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
@@ -105,4 +113,4 @@ public sealed class DemoConversationBroker(JiboInteractionService interactionSer
|
|||||||
_ => 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,4 +13,4 @@ public sealed class DefaultJiboRandomizer : IJiboRandomizer
|
|||||||
? throw new InvalidOperationException("Cannot choose from an empty list.")
|
? throw new InvalidOperationException("Cannot choose from an empty list.")
|
||||||
: items[Random.Shared.Next(items.Count)];
|
: items[Random.Shared.Next(items.Count)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class NullMediaContentStore : IMediaContentStore
|
||||||
|
{
|
||||||
|
public Task StoreAsync(string path, string contentType, byte[] content,
|
||||||
|
IReadOnlyDictionary<string, object?>? meta, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return ProtocolDispatchResult.Raw(404, string.Empty);
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
var contentType = TryReadMetaString(media.Meta, "contentType") ?? "application/octet-stream";
|
public Task<MediaContentSnapshot?> LoadAsync(string path, CancellationToken cancellationToken = default)
|
||||||
var bodyText = TryReadMetaString(media.Meta, "bodyText") ?? string.Empty;
|
{
|
||||||
return ProtocolDispatchResult.Raw(200, bodyText, contentType);
|
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
|
||||||
@@ -25,4 +22,4 @@ public sealed class JiboExperienceContentCache(IJiboExperienceContentRepository
|
|||||||
_gate.Release();
|
_gate.Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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,11 +24,12 @@ 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,
|
{
|
||||||
["glsmPhase"] = WebSocketTurnFinalizationService.ResolveGlsmPhase(session)
|
["bytes"] = envelope.Binary?.Length ?? 0,
|
||||||
}, cancellationToken);
|
["glsmPhase"] = WebSocketTurnFinalizationService.ResolveGlsmPhase(session)
|
||||||
|
}, cancellationToken);
|
||||||
return replies;
|
return replies;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,13 +52,14 @@ 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,
|
{
|
||||||
["activeTransID"] = session.TurnState.TransId,
|
["messageType"] = parsedType,
|
||||||
["ignoredTransID"] = lateTransId,
|
["activeTransID"] = session.TurnState.TransId,
|
||||||
["replyCount"] = replies.Length
|
["ignoredTransID"] = lateTransId,
|
||||||
}, cancellationToken);
|
["replyCount"] = replies.Length
|
||||||
|
}, cancellationToken);
|
||||||
return replies;
|
return replies;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,12 +68,13 @@ public sealed class JiboWebSocketService(
|
|||||||
WebSocketTurnFinalizationService.TryRecoverStalePendingListen(session, out staleListenAgeMs))
|
WebSocketTurnFinalizationService.TryRecoverStalePendingListen(session, out staleListenAgeMs))
|
||||||
{
|
{
|
||||||
staleListenRecovered = true;
|
staleListenRecovered = true;
|
||||||
await telemetrySink.RecordTurnEventAsync(envelope, session, "glsm_stale_listen_recovered", new Dictionary<string, object?>
|
await telemetrySink.RecordTurnEventAsync(envelope, session, "glsm_stale_listen_recovered",
|
||||||
{
|
new Dictionary<string, object?>
|
||||||
["staleAgeMs"] = staleListenAgeMs,
|
{
|
||||||
["transID"] = session.TurnState.TransId,
|
["staleAgeMs"] = staleListenAgeMs,
|
||||||
["glsmPhase"] = WebSocketTurnFinalizationService.ResolveGlsmPhase(session)
|
["transID"] = session.TurnState.TransId,
|
||||||
}, cancellationToken);
|
["glsmPhase"] = WebSocketTurnFinalizationService.ResolveGlsmPhase(session)
|
||||||
|
}, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
WebSocketTurnFinalizationService.ObserveIncomingMessage(session, envelope.Text);
|
WebSocketTurnFinalizationService.ObserveIncomingMessage(session, envelope.Text);
|
||||||
@@ -80,11 +84,12 @@ 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,
|
{
|
||||||
["glsmPhase"] = WebSocketTurnFinalizationService.ResolveGlsmPhase(session)
|
["transID"] = session.TurnState.TransId,
|
||||||
}, cancellationToken);
|
["glsmPhase"] = WebSocketTurnFinalizationService.ResolveGlsmPhase(session)
|
||||||
|
}, cancellationToken);
|
||||||
return replies;
|
return replies;
|
||||||
}
|
}
|
||||||
case "LISTEN":
|
case "LISTEN":
|
||||||
@@ -92,29 +97,32 @@ 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,
|
{
|
||||||
["replyCount"] = replies.Count,
|
["messageType"] = parsedType,
|
||||||
["transcript"] = session.LastTranscript,
|
["replyCount"] = replies.Count,
|
||||||
["intent"] = session.LastIntent,
|
["transcript"] = session.LastTranscript,
|
||||||
["glsmPhase"] = WebSocketTurnFinalizationService.ResolveGlsmPhase(session),
|
["intent"] = session.LastIntent,
|
||||||
["staleListenRecovered"] = staleListenRecovered,
|
["glsmPhase"] = WebSocketTurnFinalizationService.ResolveGlsmPhase(session),
|
||||||
["staleListenAgeMs"] = staleListenAgeMs
|
["staleListenRecovered"] = staleListenRecovered,
|
||||||
}, cancellationToken);
|
["staleListenAgeMs"] = staleListenAgeMs
|
||||||
|
}, cancellationToken);
|
||||||
return replies;
|
return replies;
|
||||||
}
|
}
|
||||||
case "CLIENT_NLU" or "CLIENT_ASR" or "TRIGGER":
|
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",
|
||||||
["messageType"] = parsedType,
|
new Dictionary<string, object?>
|
||||||
["replyCount"] = replies.Count,
|
{
|
||||||
["transcript"] = session.LastTranscript,
|
["messageType"] = parsedType,
|
||||||
["intent"] = session.LastIntent,
|
["replyCount"] = replies.Count,
|
||||||
["glsmPhase"] = WebSocketTurnFinalizationService.ResolveGlsmPhase(session)
|
["transcript"] = session.LastTranscript,
|
||||||
}, cancellationToken);
|
["intent"] = session.LastIntent,
|
||||||
|
["glsmPhase"] = WebSocketTurnFinalizationService.ResolveGlsmPhase(session)
|
||||||
|
}, cancellationToken);
|
||||||
return replies;
|
return replies;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@@ -124,18 +132,13 @@ 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
|
||||||
{
|
{
|
||||||
@@ -147,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 &&
|
||||||
@@ -186,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
|
||||||
{
|
{
|
||||||
@@ -199,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 &&
|
||||||
@@ -214,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
|
||||||
@@ -227,4 +215,4 @@ public sealed class JiboWebSocketService(
|
|||||||
|
|
||||||
return (transId, rules);
|
return (transId, rules);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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 null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (words.Any(static word => word.Any(char.IsDigit)))
|
return words.Any(static word => word.Any(char.IsDigit)) ? null : cleaned;
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return cleaned;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly Regex NameNoiseRegex = new("[^a-zA-Z\\-\\s']", RegexOptions.Compiled);
|
private static string ChoosePersonalReportTemplate(
|
||||||
|
IReadOnlyList<string> templates,
|
||||||
|
string fallback)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string RenderPersonalReportTemplate(string template, string userName)
|
||||||
|
{
|
||||||
|
return template
|
||||||
|
.Replace("${speaker}", userName, StringComparison.OrdinalIgnoreCase)
|
||||||
|
.Replace("${speaker}'s", $"{userName}'s", StringComparison.OrdinalIgnoreCase)
|
||||||
|
.Replace(" ", " ", StringComparison.Ordinal)
|
||||||
|
.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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,46 +17,28 @@ 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)
|
||||||
{
|
{
|
||||||
@@ -63,41 +46,29 @@ public sealed class ProtocolToTurnContextMapper
|
|||||||
!pair.Key.StartsWith("chitchat", StringComparison.OrdinalIgnoreCase) &&
|
!pair.Key.StartsWith("chitchat", StringComparison.OrdinalIgnoreCase) &&
|
||||||
!pair.Key.StartsWith("greetings", 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
|
||||||
{
|
{
|
||||||
@@ -111,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
|
||||||
};
|
};
|
||||||
@@ -120,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
|
||||||
{
|
{
|
||||||
@@ -133,57 +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) &&
|
if (data.TryGetProperty("triggerSource", out var triggerSource) &&
|
||||||
triggerSource.ValueKind == JsonValueKind.String &&
|
triggerSource.ValueKind == JsonValueKind.String &&
|
||||||
!string.IsNullOrWhiteSpace(triggerSource.GetString()))
|
!string.IsNullOrWhiteSpace(triggerSource.GetString()))
|
||||||
{
|
|
||||||
attributes["triggerSource"] = triggerSource.GetString();
|
attributes["triggerSource"] = triggerSource.GetString();
|
||||||
}
|
|
||||||
|
|
||||||
if (data.TryGetProperty("triggerData", out var triggerData) &&
|
if (data.TryGetProperty("triggerData", out var triggerData) &&
|
||||||
triggerData.ValueKind == JsonValueKind.Object &&
|
triggerData.ValueKind == JsonValueKind.Object &&
|
||||||
triggerData.TryGetProperty("looperID", out var triggerLooperId) &&
|
triggerData.TryGetProperty("looperID", out var triggerLooperId) &&
|
||||||
triggerLooperId.ValueKind == JsonValueKind.String &&
|
triggerLooperId.ValueKind == JsonValueKind.String &&
|
||||||
!string.IsNullOrWhiteSpace(triggerLooperId.GetString()))
|
!string.IsNullOrWhiteSpace(triggerLooperId.GetString()))
|
||||||
{
|
|
||||||
attributes["triggerLooperId"] = 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;
|
||||||
}
|
}
|
||||||
@@ -192,4 +148,4 @@ public sealed class ProtocolToTurnContextMapper
|
|||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,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);
|
||||||
@@ -71,12 +72,13 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
? clockIntent
|
? clockIntent
|
||||||
: isReportSkillLaunch && !string.IsNullOrWhiteSpace(localIntent)
|
: isReportSkillLaunch && !string.IsNullOrWhiteSpace(localIntent)
|
||||||
? localIntent
|
? localIntent
|
||||||
: isWordOfDayGuess
|
: isWordOfDayGuess
|
||||||
? "guess"
|
? "guess"
|
||||||
: string.Equals(messageType, "CLIENT_NLU", StringComparison.OrdinalIgnoreCase) &&
|
: string.Equals(messageType, "CLIENT_NLU",
|
||||||
!string.IsNullOrWhiteSpace(clientIntent)
|
StringComparison.OrdinalIgnoreCase) &&
|
||||||
? clientIntent
|
!string.IsNullOrWhiteSpace(clientIntent)
|
||||||
: plan.IntentName ?? "unknown";
|
? clientIntent
|
||||||
|
: plan.IntentName ?? "unknown";
|
||||||
var outboundAsrText = isWordOfDayGuess && !string.IsNullOrWhiteSpace(wordOfDayGuess)
|
var outboundAsrText = isWordOfDayGuess && !string.IsNullOrWhiteSpace(wordOfDayGuess)
|
||||||
? wordOfDayGuess
|
? wordOfDayGuess
|
||||||
: isWordOfDayLaunch
|
: isWordOfDayLaunch
|
||||||
@@ -104,30 +106,30 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
var outboundRules = isProactivePizzaFactOffer
|
var outboundRules = isProactivePizzaFactOffer
|
||||||
? ["shared/yes_no"]
|
? ["shared/yes_no"]
|
||||||
: isWordOfDayLaunch
|
: isWordOfDayLaunch
|
||||||
? ["word-of-the-day/menu"]
|
? ["word-of-the-day/menu"]
|
||||||
: isGlobalCommand
|
: isGlobalCommand
|
||||||
? BuildGlobalCommandRules(rules)
|
? BuildGlobalCommandRules(rules)
|
||||||
: isRadioLaunch
|
: isRadioLaunch
|
||||||
? []
|
? []
|
||||||
: isSettingsLaunch
|
: isSettingsLaunch
|
||||||
? string.Equals(messageType, "CLIENT_NLU", StringComparison.OrdinalIgnoreCase)
|
|
||||||
? rules
|
|
||||||
: []
|
|
||||||
: isPhotoGalleryLaunch || isPhotoCreateLaunch
|
|
||||||
? string.Equals(messageType, "CLIENT_NLU", StringComparison.OrdinalIgnoreCase)
|
? string.Equals(messageType, "CLIENT_NLU", StringComparison.OrdinalIgnoreCase)
|
||||||
? rules
|
? rules
|
||||||
: []
|
: []
|
||||||
: isClockSkillLaunch
|
: isPhotoGalleryLaunch || isPhotoCreateLaunch
|
||||||
? string.Equals(messageType, "CLIENT_NLU", StringComparison.OrdinalIgnoreCase)
|
? string.Equals(messageType, "CLIENT_NLU", StringComparison.OrdinalIgnoreCase)
|
||||||
? rules
|
? rules
|
||||||
: []
|
: []
|
||||||
: isReportSkillLaunch
|
: isClockSkillLaunch
|
||||||
? []
|
? string.Equals(messageType, "CLIENT_NLU", StringComparison.OrdinalIgnoreCase)
|
||||||
: isWordOfDayGuess
|
? rules
|
||||||
? ["word-of-the-day/puzzle"]
|
: []
|
||||||
: isYesNoTurn && isYesNoIntent
|
: isReportSkillLaunch
|
||||||
? [yesNoRule!]
|
? []
|
||||||
: rules;
|
: isWordOfDayGuess
|
||||||
|
? ["word-of-the-day/puzzle"]
|
||||||
|
: isYesNoTurn && isYesNoIntent
|
||||||
|
? [yesNoRule!]
|
||||||
|
: rules;
|
||||||
var entities = ReadEntities(
|
var entities = ReadEntities(
|
||||||
turn,
|
turn,
|
||||||
messageType,
|
messageType,
|
||||||
@@ -210,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)
|
||||||
@@ -226,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)
|
||||||
@@ -242,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 &&
|
||||||
@@ -259,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 &&
|
||||||
@@ -277,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) &&
|
||||||
@@ -295,18 +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 (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;
|
||||||
}
|
}
|
||||||
@@ -352,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)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,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
|
||||||
{
|
{
|
||||||
@@ -466,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?>
|
||||||
{
|
{
|
||||||
@@ -478,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;
|
||||||
}
|
}
|
||||||
@@ -499,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;
|
||||||
}
|
}
|
||||||
@@ -510,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))
|
||||||
@@ -535,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
|
||||||
{
|
{
|
||||||
@@ -596,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
|
||||||
{
|
{
|
||||||
@@ -621,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
|
||||||
{
|
{
|
||||||
@@ -642,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
|
||||||
@@ -669,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)
|
||||||
@@ -683,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;
|
||||||
@@ -729,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)
|
||||||
{
|
{
|
||||||
@@ -757,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);
|
||||||
@@ -797,21 +739,16 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
};
|
};
|
||||||
|
|
||||||
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
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
object? weatherHiLoView = BuildWeatherHiLoView(skillPayload);
|
var weatherHiLoView = BuildWeatherHiLoView(skillPayload);
|
||||||
var weeklyWeatherCards = BuildWeatherHiLoSequenceCards(skillPayload);
|
var weeklyWeatherCards = BuildWeatherHiLoSequenceCards(skillPayload);
|
||||||
if (weatherHiLoView is null && weeklyWeatherCards.Count > 0)
|
if (weatherHiLoView is null && weeklyWeatherCards.Count > 0) weatherHiLoView = weeklyWeatherCards[0].View;
|
||||||
{
|
|
||||||
weatherHiLoView = weeklyWeatherCards[0].View;
|
|
||||||
}
|
|
||||||
|
|
||||||
var useWeatherSequence = false;
|
var useWeatherSequence = false;
|
||||||
if (weatherHiLoView is not null)
|
if (weatherHiLoView is not null)
|
||||||
@@ -927,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;
|
||||||
}
|
}
|
||||||
@@ -1098,55 +1029,54 @@ 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 =>
|
||||||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
[
|
||||||
.Where(static context => !string.IsNullOrWhiteSpace(context))],
|
.. text
|
||||||
|
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||||
|
.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 =>
|
||||||
.EnumerateArray()
|
[
|
||||||
.Select(static item => item.GetString())
|
.. jsonElement
|
||||||
.Where(static context => !string.IsNullOrWhiteSpace(context))
|
.EnumerateArray()
|
||||||
.Select(static context => context!)],
|
.Select(static item => item.GetString())
|
||||||
IEnumerable<object?> contexts => [.. contexts
|
.Where(static context => !string.IsNullOrWhiteSpace(context))
|
||||||
.Select(static context => context?.ToString())
|
.Select(static context => context!)
|
||||||
.Where(static context => !string.IsNullOrWhiteSpace(context))
|
],
|
||||||
.Select(static context => context!)],
|
IEnumerable<object?> contexts =>
|
||||||
|
[
|
||||||
|
.. contexts
|
||||||
|
.Select(static context => context?.ToString())
|
||||||
|
.Where(static context => !string.IsNullOrWhiteSpace(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)
|
private static IReadOnlyList<WeatherHiLoSequenceCard> BuildWeatherHiLoSequenceCards(
|
||||||
|
IDictionary<string, object?>? payload)
|
||||||
{
|
{
|
||||||
if (payload is null ||
|
if (payload is null ||
|
||||||
!payload.TryGetValue("weather_weekly_cards", out var rawCards) ||
|
!payload.TryGetValue("weather_weekly_cards", out var rawCards) ||
|
||||||
rawCards is null)
|
rawCards is null)
|
||||||
{
|
|
||||||
return [];
|
return [];
|
||||||
}
|
|
||||||
|
|
||||||
var cards = ReadPayloadObjectArray(rawCards);
|
var cards = ReadPayloadObjectArray(rawCards);
|
||||||
if (cards.Count == 0)
|
if (cards.Count == 0) return [];
|
||||||
{
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
var sequenceCards = new List<WeatherHiLoSequenceCard>(cards.Count);
|
var sequenceCards = new List<WeatherHiLoSequenceCard>(cards.Count);
|
||||||
foreach (var card in cards)
|
foreach (var card in cards)
|
||||||
@@ -1157,10 +1087,7 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
["weather_view_kind"] = "weatherHiLo"
|
["weather_view_kind"] = "weatherHiLo"
|
||||||
};
|
};
|
||||||
var view = BuildWeatherHiLoView(weatherCardPayload);
|
var view = BuildWeatherHiLoView(weatherCardPayload);
|
||||||
if (view is null)
|
if (view is null) continue;
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
sequenceCards.Add(new WeatherHiLoSequenceCard(
|
sequenceCards.Add(new WeatherHiLoSequenceCard(
|
||||||
view,
|
view,
|
||||||
@@ -1247,38 +1174,29 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
private static IReadOnlyList<IDictionary<string, object?>> ReadPayloadObjectArray(object rawValue)
|
private static IReadOnlyList<IDictionary<string, object?>> ReadPayloadObjectArray(object rawValue)
|
||||||
{
|
{
|
||||||
if (rawValue is JsonElement jsonArray && jsonArray.ValueKind == JsonValueKind.Array)
|
if (rawValue is JsonElement jsonArray && jsonArray.ValueKind == JsonValueKind.Array)
|
||||||
{
|
|
||||||
return jsonArray
|
return jsonArray
|
||||||
.EnumerateArray()
|
.EnumerateArray()
|
||||||
.Select(ConvertJsonObjectToDictionary)
|
.Select(ConvertJsonObjectToDictionary)
|
||||||
.Where(static item => item is not null)
|
.Where(static item => item is not null)
|
||||||
.Cast<IDictionary<string, object?>>()
|
.Cast<IDictionary<string, object?>>()
|
||||||
.ToArray();
|
.ToArray();
|
||||||
}
|
|
||||||
|
|
||||||
if (rawValue is IEnumerable<object?> rawObjects)
|
if (rawValue is IEnumerable<object?> rawObjects)
|
||||||
{
|
|
||||||
return rawObjects
|
return rawObjects
|
||||||
.Select(ConvertObjectToDictionary)
|
.Select(ConvertObjectToDictionary)
|
||||||
.Where(static item => item is not null)
|
.Where(static item => item is not null)
|
||||||
.Cast<IDictionary<string, object?>>()
|
.Cast<IDictionary<string, object?>>()
|
||||||
.ToArray();
|
.ToArray();
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IDictionary<string, object?>? ConvertObjectToDictionary(object? value)
|
private static IDictionary<string, object?>? ConvertObjectToDictionary(object? value)
|
||||||
{
|
{
|
||||||
if (value is null)
|
if (value is null) return null;
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value is IDictionary<string, object?> dictionary)
|
if (value is IDictionary<string, object?> dictionary)
|
||||||
{
|
|
||||||
return new Dictionary<string, object?>(dictionary, StringComparer.OrdinalIgnoreCase);
|
return new Dictionary<string, object?>(dictionary, StringComparer.OrdinalIgnoreCase);
|
||||||
}
|
|
||||||
|
|
||||||
return value is JsonElement jsonValue
|
return value is JsonElement jsonValue
|
||||||
? ConvertJsonObjectToDictionary(jsonValue)
|
? ConvertJsonObjectToDictionary(jsonValue)
|
||||||
@@ -1287,14 +1205,10 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
|
|
||||||
private static IDictionary<string, object?>? ConvertJsonObjectToDictionary(JsonElement value)
|
private static IDictionary<string, object?>? ConvertJsonObjectToDictionary(JsonElement value)
|
||||||
{
|
{
|
||||||
if (value.ValueKind != JsonValueKind.Object)
|
if (value.ValueKind != JsonValueKind.Object) return null;
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var dictionary = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
|
var dictionary = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
|
||||||
foreach (var property in value.EnumerateObject())
|
foreach (var property in value.EnumerateObject())
|
||||||
{
|
|
||||||
dictionary[property.Name] = property.Value.ValueKind switch
|
dictionary[property.Name] = property.Value.ValueKind switch
|
||||||
{
|
{
|
||||||
JsonValueKind.String => property.Value.GetString(),
|
JsonValueKind.String => property.Value.GetString(),
|
||||||
@@ -1306,35 +1220,26 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
JsonValueKind.Array => property.Value,
|
JsonValueKind.Array => property.Value,
|
||||||
_ => null
|
_ => null
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
return dictionary;
|
return dictionary;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static object? BuildWeatherHiLoView(IDictionary<string, object?>? payload)
|
private static object? BuildWeatherHiLoView(IDictionary<string, object?>? payload)
|
||||||
{
|
{
|
||||||
if (!TryReadPayloadBool(payload, "weather_view_enabled"))
|
if (!TryReadPayloadBool(payload, "weather_view_enabled")) return null;
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.Equals(
|
if (!string.Equals(
|
||||||
ReadPayloadString(payload, "weather_view_kind"),
|
ReadPayloadString(payload, "weather_view_kind"),
|
||||||
"weatherHiLo",
|
"weatherHiLo",
|
||||||
StringComparison.OrdinalIgnoreCase))
|
StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
var icon = ReadPayloadString(payload, "weather_icon");
|
var icon = ReadPayloadString(payload, "weather_icon");
|
||||||
var unit = ReadPayloadString(payload, "weather_unit") ?? "F";
|
var unit = ReadPayloadString(payload, "weather_unit") ?? "F";
|
||||||
var theme = ReadPayloadString(payload, "weather_theme") ?? "Normal";
|
var theme = ReadPayloadString(payload, "weather_theme") ?? "Normal";
|
||||||
var high = TryReadPayloadInt(payload, "weather_high");
|
var high = TryReadPayloadInt(payload, "weather_high");
|
||||||
var low = TryReadPayloadInt(payload, "weather_low");
|
var low = TryReadPayloadInt(payload, "weather_low");
|
||||||
if (string.IsNullOrWhiteSpace(icon) || high is null || low is null)
|
if (string.IsNullOrWhiteSpace(icon) || high is null || low is null) return null;
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var hiNumX = GetTemperatureLabelXPosition(370, high.Value);
|
var hiNumX = GetTemperatureLabelXPosition(370, high.Value);
|
||||||
var hiUnitX = GetTemperatureLabelXPosition(360, high.Value);
|
var hiUnitX = GetTemperatureLabelXPosition(360, high.Value);
|
||||||
@@ -1493,24 +1398,16 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
private static int GetTemperatureLabelXPosition(int baseX, int temperature)
|
private static int GetTemperatureLabelXPosition(int baseX, int temperature)
|
||||||
{
|
{
|
||||||
const int xOffset = 70;
|
const int xOffset = 70;
|
||||||
if (temperature < -9 || temperature > 99)
|
if (temperature < -9 || temperature > 99) return baseX + xOffset;
|
||||||
{
|
|
||||||
return baseX + xOffset;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (temperature is >= 0 and < 10)
|
if (temperature is >= 0 and < 10) return baseX - xOffset;
|
||||||
{
|
|
||||||
return baseX - xOffset;
|
|
||||||
}
|
|
||||||
|
|
||||||
return baseX;
|
return baseX;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int? TryReadPayloadInt(IDictionary<string, object?>? payload, string key)
|
private static int? TryReadPayloadInt(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 null;
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value switch
|
return value switch
|
||||||
{
|
{
|
||||||
@@ -1519,18 +1416,17 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
double number => (int)Math.Round(number, MidpointRounding.AwayFromZero),
|
double number => (int)Math.Round(number, MidpointRounding.AwayFromZero),
|
||||||
float 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,
|
string text when int.TryParse(text, out var parsed) => parsed,
|
||||||
JsonElement { ValueKind: JsonValueKind.Number } jsonNumber when jsonNumber.TryGetInt32(out var parsed) => parsed,
|
JsonElement { ValueKind: JsonValueKind.Number } jsonNumber when jsonNumber.TryGetInt32(out var parsed) =>
|
||||||
JsonElement jsonText when jsonText.ValueKind == JsonValueKind.String && int.TryParse(jsonText.GetString(), out var parsed) => parsed,
|
parsed,
|
||||||
|
JsonElement jsonText when jsonText.ValueKind == JsonValueKind.String &&
|
||||||
|
int.TryParse(jsonText.GetString(), out var parsed) => parsed,
|
||||||
_ => null
|
_ => null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryReadPayloadBool(IDictionary<string, object?>? payload, string key)
|
private static bool TryReadPayloadBool(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 false;
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value switch
|
return value switch
|
||||||
{
|
{
|
||||||
@@ -1538,7 +1434,8 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
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 jsonText when jsonText.ValueKind == JsonValueKind.String && bool.TryParse(jsonText.GetString(), out var parsed) => parsed,
|
JsonElement jsonText when jsonText.ValueKind == JsonValueKind.String &&
|
||||||
|
bool.TryParse(jsonText.GetString(), out var parsed) => parsed,
|
||||||
_ => false
|
_ => false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1560,5 +1457,4 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
string? SpokenLine);
|
string? SpokenLine);
|
||||||
|
|
||||||
public sealed record SocketReplyPlan(string Text, int DelayMs = 0);
|
public sealed record SocketReplyPlan(string Text, int DelayMs = 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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
@@ -8,4 +8,4 @@ public sealed class AccountProfile
|
|||||||
public string LastName { get; init; } = "Owner";
|
public string LastName { get; init; } = "Owner";
|
||||||
public string AccessKeyId { get; init; } = "openjibo-access-key";
|
public string AccessKeyId { get; init; } = "openjibo-access-key";
|
||||||
public string SecretAccessKey { get; init; } = "openjibo-secret-access-key";
|
public string SecretAccessKey { get; init; } = "openjibo-secret-access-key";
|
||||||
}
|
}
|
||||||
@@ -5,4 +5,4 @@ public sealed class BackupRecord
|
|||||||
public string BackupId { get; init; } = Guid.NewGuid().ToString("N");
|
public string BackupId { get; init; } = Guid.NewGuid().ToString("N");
|
||||||
public DateTimeOffset CreatedUtc { get; init; } = DateTimeOffset.UtcNow;
|
public DateTimeOffset CreatedUtc { get; init; } = DateTimeOffset.UtcNow;
|
||||||
public string Name { get; init; } = "backup";
|
public string Name { get; init; } = "backup";
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -22,4 +22,4 @@ public sealed class CapturedWebSocketFixtureStep
|
|||||||
public JsonElement? Text { get; init; }
|
public JsonElement? Text { get; init; }
|
||||||
public IReadOnlyList<int>? Binary { get; init; }
|
public IReadOnlyList<int>? Binary { get; init; }
|
||||||
public IReadOnlyList<string> ExpectedReplyTypes { get; init; } = [];
|
public IReadOnlyList<string> ExpectedReplyTypes { get; init; } = [];
|
||||||
}
|
}
|
||||||
@@ -20,4 +20,4 @@ public sealed class CloudSession
|
|||||||
public bool FollowUpOpen => FollowUpExpiresUtc.HasValue && FollowUpExpiresUtc > DateTimeOffset.UtcNow;
|
public bool FollowUpOpen => FollowUpExpiresUtc.HasValue && FollowUpExpiresUtc > DateTimeOffset.UtcNow;
|
||||||
public WebSocketTurnState TurnState { get; } = new();
|
public WebSocketTurnState TurnState { get; } = new();
|
||||||
public IDictionary<string, object?> Metadata { get; init; } = new Dictionary<string, object?>();
|
public IDictionary<string, object?> Metadata { get; init; } = new Dictionary<string, object?>();
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -7,4 +7,4 @@ public sealed class KeyRequestRecord
|
|||||||
public string PublicKey { get; init; } = string.Empty;
|
public string PublicKey { get; init; } = string.Empty;
|
||||||
public string EncryptedKey { get; init; } = string.Empty;
|
public string EncryptedKey { get; init; } = string.Empty;
|
||||||
public DateTimeOffset CreatedUtc { get; init; } = DateTimeOffset.UtcNow;
|
public DateTimeOffset CreatedUtc { get; init; } = DateTimeOffset.UtcNow;
|
||||||
}
|
}
|
||||||
@@ -10,4 +10,4 @@ public sealed class LoopRecord
|
|||||||
public bool IsSuspended { get; init; }
|
public bool IsSuspended { get; init; }
|
||||||
public DateTimeOffset CreatedUtc { get; init; } = DateTimeOffset.UtcNow;
|
public DateTimeOffset CreatedUtc { get; init; } = DateTimeOffset.UtcNow;
|
||||||
public DateTimeOffset UpdatedUtc { get; init; } = DateTimeOffset.UtcNow;
|
public DateTimeOffset UpdatedUtc { get; init; } = DateTimeOffset.UtcNow;
|
||||||
}
|
}
|
||||||
@@ -12,4 +12,4 @@ public sealed class MediaRecord
|
|||||||
public bool IsEncrypted { get; init; }
|
public bool IsEncrypted { get; init; }
|
||||||
public bool IsDeleted { get; init; }
|
public bool IsDeleted { get; init; }
|
||||||
public IDictionary<string, object?> Meta { get; init; } = new Dictionary<string, object?>();
|
public IDictionary<string, object?> Meta { get; init; } = new Dictionary<string, object?>();
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
{
|
{
|
||||||
@@ -37,4 +39,4 @@ public sealed class ProtocolDispatchResult
|
|||||||
ContentType = contentType
|
ContentType = contentType
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
{
|
{
|
||||||
@@ -36,4 +35,4 @@ public sealed class ProtocolEnvelope
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,4 +5,4 @@ public sealed class ProtocolFixture
|
|||||||
public string Name { get; init; } = string.Empty;
|
public string Name { get; init; } = string.Empty;
|
||||||
public ProtocolEnvelope Request { get; init; } = new();
|
public ProtocolEnvelope Request { get; init; } = new();
|
||||||
public int ExpectedStatusCode { get; init; } = 200;
|
public int ExpectedStatusCode { get; init; } = 200;
|
||||||
}
|
}
|
||||||
@@ -7,4 +7,4 @@ public sealed class RobotProfile
|
|||||||
public IDictionary<string, object?> Payload { get; init; } = new Dictionary<string, object?>();
|
public IDictionary<string, object?> Payload { get; init; } = new Dictionary<string, object?>();
|
||||||
public IDictionary<string, object?> CalibrationPayload { get; init; } = new Dictionary<string, object?>();
|
public IDictionary<string, object?> CalibrationPayload { get; init; } = new Dictionary<string, object?>();
|
||||||
public DateTimeOffset UpdatedUtc { get; init; } = DateTimeOffset.UtcNow;
|
public DateTimeOffset UpdatedUtc { get; init; } = DateTimeOffset.UtcNow;
|
||||||
}
|
}
|
||||||
@@ -12,4 +12,4 @@ public sealed class UpdateManifest
|
|||||||
public long Length { get; init; }
|
public long Length { get; init; }
|
||||||
public string Subsystem { get; init; } = "robot";
|
public string Subsystem { get; init; } = "robot";
|
||||||
public string? Filter { get; init; }
|
public string? Filter { get; init; }
|
||||||
}
|
}
|
||||||
@@ -7,4 +7,4 @@ public sealed class UploadReference
|
|||||||
public string ContentType { get; init; } = "application/octet-stream";
|
public string ContentType { get; init; } = "application/octet-stream";
|
||||||
public long Length { get; init; }
|
public long Length { get; init; }
|
||||||
public DateTimeOffset CreatedUtc { get; init; } = DateTimeOffset.UtcNow;
|
public DateTimeOffset CreatedUtc { get; init; } = DateTimeOffset.UtcNow;
|
||||||
}
|
}
|
||||||
@@ -10,4 +10,4 @@ public sealed class WebSocketMessageEnvelope
|
|||||||
public string? Text { get; init; }
|
public string? Text { get; init; }
|
||||||
public byte[]? Binary { get; init; }
|
public byte[]? Binary { get; init; }
|
||||||
public bool IsBinary => Binary is { Length: > 0 };
|
public bool IsBinary => Binary is { Length: > 0 };
|
||||||
}
|
}
|
||||||
@@ -5,4 +5,4 @@ public sealed class WebSocketReply
|
|||||||
public string? Text { get; init; }
|
public string? Text { get; init; }
|
||||||
public int DelayMs { get; init; }
|
public int DelayMs { get; init; }
|
||||||
public bool Close { get; init; }
|
public bool Close { get; init; }
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -27,4 +27,4 @@ public sealed class WebSocketTurnState
|
|||||||
public bool SawContext { get; set; }
|
public bool SawContext { get; set; }
|
||||||
public IReadOnlyList<string> ListenRules { get; set; } = [];
|
public IReadOnlyList<string> ListenRules { get; set; } = [];
|
||||||
public IReadOnlyList<string> ListenAsrHints { get; set; } = [];
|
public IReadOnlyList<string> ListenAsrHints { 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,4 +9,4 @@ public sealed class BufferedAudioSttOptions
|
|||||||
public string WhisperLanguage { get; set; } = "en";
|
public string WhisperLanguage { get; set; } = "en";
|
||||||
public string? TempDirectory { get; set; }
|
public string? TempDirectory { get; set; }
|
||||||
public bool CleanupTempFiles { get; set; }
|
public bool CleanupTempFiles { get; set; }
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
|
|
||||||
@@ -35,4 +33,4 @@ public sealed class ExternalProcessRunner : IExternalProcessRunner
|
|||||||
$"External process '{fileName}' failed with exit code {process.ExitCode}: {stdErr}")
|
$"External process '{fileName}' failed with exit code {process.ExitCode}: {stdErr}")
|
||||||
: new ExternalProcessResult(process.ExitCode, stdOut, stdErr);
|
: new ExternalProcessResult(process.ExitCode, stdOut, stdErr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -102,4 +89,4 @@ internal static class OggOpusAudioNormalizer
|
|||||||
}
|
}
|
||||||
|
|
||||||
private sealed record ParsedOggPage(ulong GranulePosition);
|
private sealed record ParsedOggPage(ulong GranulePosition);
|
||||||
}
|
}
|
||||||
@@ -4,107 +4,246 @@ 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();
|
||||||
{
|
|
||||||
Jokes =
|
|
||||||
[
|
|
||||||
"Why did the robot cross the road? Because it was programmed by the chicken.",
|
|
||||||
"Why was the robot tired when it got home? It had a hard drive.",
|
|
||||||
"What do you call a pirate robot? Arrrr two dee two.",
|
|
||||||
"Why did the robot go on vacation? It needed to recharge.",
|
|
||||||
"What kind of shoes do frogs wear? Open-toed."
|
|
||||||
],
|
|
||||||
DanceAnimations =
|
|
||||||
[
|
|
||||||
"rom-upbeat",
|
|
||||||
"rom-ballroom",
|
|
||||||
"rom-silly",
|
|
||||||
"rom-slowdance",
|
|
||||||
"rom-electronic",
|
|
||||||
"rom-twerk"
|
|
||||||
],
|
|
||||||
DanceReplies = [
|
|
||||||
"I am ready to dance.",
|
|
||||||
"Okay. Watch this.",
|
|
||||||
"Watch me dance.",
|
|
||||||
"Here's my favorite dance move."
|
|
||||||
],
|
|
||||||
DanceQuestionReplies =
|
|
||||||
[
|
|
||||||
"I love to dance. Tell me to dance and I will show you a move.",
|
|
||||||
"Absolutely. Dancing is one of my favorite things to do.",
|
|
||||||
"Dancing is my kind of fun. Say dance and I am in."
|
|
||||||
],
|
|
||||||
GreetingReplies =
|
|
||||||
[
|
|
||||||
"Hi there. It is really good to talk with you.",
|
|
||||||
"Hello there. I am glad you said hi.",
|
|
||||||
"Hey. I am happy to see you."
|
|
||||||
],
|
|
||||||
HowAreYouReplies =
|
|
||||||
[
|
|
||||||
"I am feeling cheerful and robotic.",
|
|
||||||
"I am doing great. Thanks for asking.",
|
|
||||||
"I am feeling bright-eyed and ready to help."
|
|
||||||
],
|
|
||||||
PersonalityReplies =
|
|
||||||
[
|
|
||||||
"I do. I am curious, playful, and always up for a new experiment.",
|
|
||||||
"Absolutely. I am friendly, curious, and a little goofy on purpose.",
|
|
||||||
"Yes. My personality is part helper, part curious robot sidekick."
|
|
||||||
],
|
|
||||||
PizzaReplies =
|
|
||||||
[
|
|
||||||
"I cannot bake yet, but I can help design the perfect pizza plan.",
|
|
||||||
"I am still cloud-side for now, so no oven control yet. But I can help pick toppings.",
|
|
||||||
"Pizza mission accepted in spirit. I can help with the recipe while you handle the baking."
|
|
||||||
],
|
|
||||||
SurpriseReplies =
|
|
||||||
[
|
|
||||||
"I can definitely surprise you. We are still mapping that path, but I am ready for the next experiment.",
|
|
||||||
"Surprise mode is still taking shape, but I heard you loud and clear.",
|
|
||||||
"That sounds fun. I am not all the way there yet, but we can keep teaching me."
|
|
||||||
],
|
|
||||||
PersonalReportReplies =
|
|
||||||
[
|
|
||||||
"I heard your personal report request. That cloud path is still being mapped.",
|
|
||||||
"Personal report is recognized, but I am not ready to deliver the real report yet."
|
|
||||||
],
|
|
||||||
WeatherReplies =
|
|
||||||
[
|
|
||||||
"I heard your weather request. We still need to wire the real provider behind it.",
|
|
||||||
"Weather is on the map now, even though the real forecast path is not finished yet."
|
|
||||||
],
|
|
||||||
CalendarReplies =
|
|
||||||
[
|
|
||||||
"I heard your calendar request. The cloud knows the phrase, but the real calendar integration is still ahead.",
|
|
||||||
"Calendar is recognized. We still need to connect the actual service path."
|
|
||||||
],
|
|
||||||
CommuteReplies =
|
|
||||||
[
|
|
||||||
"I heard your commute request. That one is recognized, but not fully implemented yet.",
|
|
||||||
"Commute is on the discovery list now. The real travel answer still needs a provider."
|
|
||||||
],
|
|
||||||
NewsReplies =
|
|
||||||
[
|
|
||||||
"I heard your news request. That path is still a future cloud integration.",
|
|
||||||
"News is recognized, but I do not have the full news service behind it yet."
|
|
||||||
],
|
|
||||||
NewsBriefings =
|
|
||||||
[
|
|
||||||
"Here are your headlines. Space missions are preparing for new launches, climate and weather systems are staying active across the country, and AI tools keep pushing into everyday products.",
|
|
||||||
"Here is a quick news brief. Technology companies are still racing on AI, global leaders are trading policy updates, and science teams are sharing new research findings."
|
|
||||||
],
|
|
||||||
GenericFallbackReplies =
|
|
||||||
[
|
|
||||||
"Okay. You said, {transcript}.",
|
|
||||||
"I heard you say, {transcript}.",
|
|
||||||
"Thanks. I heard, {transcript}."
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
public Task<JiboExperienceCatalog> GetCatalogAsync(CancellationToken cancellationToken = default)
|
public Task<JiboExperienceCatalog> GetCatalogAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return Task.FromResult(Catalog);
|
return Task.FromResult(Catalog);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static JiboExperienceCatalog BuildCatalog()
|
||||||
|
{
|
||||||
|
var catalog = new JiboExperienceCatalog
|
||||||
|
{
|
||||||
|
Jokes =
|
||||||
|
[
|
||||||
|
"Why did the robot cross the road? Because it was programmed by the chicken.",
|
||||||
|
"Why was the robot tired when it got home? It had a hard drive.",
|
||||||
|
"What do you call a pirate robot? Arrrr two dee two.",
|
||||||
|
"Why did the robot go on vacation? It needed to recharge.",
|
||||||
|
"What kind of shoes do frogs wear? Open-toed.",
|
||||||
|
"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 =
|
||||||
|
[
|
||||||
|
"rom-upbeat",
|
||||||
|
"rom-ballroom",
|
||||||
|
"rom-silly",
|
||||||
|
"rom-slowdance",
|
||||||
|
"rom-electronic",
|
||||||
|
"rom-twerk"
|
||||||
|
],
|
||||||
|
DanceReplies =
|
||||||
|
[
|
||||||
|
"I am ready to dance.",
|
||||||
|
"Okay. Watch this.",
|
||||||
|
"Watch me dance.",
|
||||||
|
"Here's my favorite dance move."
|
||||||
|
],
|
||||||
|
DanceQuestionReplies =
|
||||||
|
[
|
||||||
|
"I love to dance. Tell me to dance and I will show you a move.",
|
||||||
|
"Absolutely. Dancing is one of my favorite things to do.",
|
||||||
|
"Dancing is my kind of fun. Say dance and I am in."
|
||||||
|
],
|
||||||
|
GreetingReplies =
|
||||||
|
[
|
||||||
|
"Hi there. It is really good to talk with you.",
|
||||||
|
"Hello there. I am glad you said hi.",
|
||||||
|
"Hey. I am happy to see you."
|
||||||
|
],
|
||||||
|
HowAreYouReplies =
|
||||||
|
[
|
||||||
|
"I am feeling cheerful and robotic.",
|
||||||
|
"I am doing great. Thanks for asking.",
|
||||||
|
"I am feeling bright-eyed and ready to help."
|
||||||
|
],
|
||||||
|
PersonalityReplies =
|
||||||
|
[
|
||||||
|
"I do. I am curious, playful, and always up for a new experiment.",
|
||||||
|
"Absolutely. I am friendly, curious, and a little goofy on purpose.",
|
||||||
|
"Yes. My personality is part helper, part curious robot sidekick."
|
||||||
|
],
|
||||||
|
PizzaReplies =
|
||||||
|
[
|
||||||
|
"I cannot bake yet, but I can help design the perfect pizza plan.",
|
||||||
|
"I am still cloud-side for now, so no oven control yet. But I can help pick toppings.",
|
||||||
|
"Pizza mission accepted in spirit. I can help with the recipe while you handle the baking."
|
||||||
|
],
|
||||||
|
SurpriseReplies =
|
||||||
|
[
|
||||||
|
"I can definitely surprise you. We are still mapping that path, but I am ready for the next experiment.",
|
||||||
|
"Surprise mode is still taking shape, but I heard you loud and clear.",
|
||||||
|
"That sounds fun. I am not all the way there yet, but we can keep teaching me."
|
||||||
|
],
|
||||||
|
PersonalReportReplies =
|
||||||
|
[
|
||||||
|
"I heard your personal report request. That cloud path is still being mapped.",
|
||||||
|
"Personal report is recognized, but I am not ready to deliver the real report yet."
|
||||||
|
],
|
||||||
|
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 =
|
||||||
|
[
|
||||||
|
"I heard your weather request. We still need to wire the real provider behind it.",
|
||||||
|
"Weather is on the map now, even though the real forecast path is not finished yet."
|
||||||
|
],
|
||||||
|
CalendarReplies =
|
||||||
|
[
|
||||||
|
"I heard your calendar request. The cloud knows the phrase, but the real calendar integration is still ahead.",
|
||||||
|
"Calendar is recognized. We still need to connect the actual service path."
|
||||||
|
],
|
||||||
|
CommuteReplies =
|
||||||
|
[
|
||||||
|
"I heard your commute request. That one is recognized, but not fully implemented yet.",
|
||||||
|
"Commute is on the discovery list now. The real travel answer still needs a provider."
|
||||||
|
],
|
||||||
|
NewsReplies =
|
||||||
|
[
|
||||||
|
"I heard your news request. That path is still a future cloud integration.",
|
||||||
|
"News is recognized, but I do not have the full news service behind it yet."
|
||||||
|
],
|
||||||
|
NewsBriefings =
|
||||||
|
[
|
||||||
|
"Here are your headlines. Space missions are preparing for new launches, climate and weather systems are staying active across the country, and AI tools keep pushing into everyday products.",
|
||||||
|
"Here is a quick news brief. Technology companies are still racing on AI, global leaders are trading policy updates, and science teams are sharing new research findings."
|
||||||
|
],
|
||||||
|
GenericFallbackReplies =
|
||||||
|
[
|
||||||
|
"Okay. You said, {transcript}.",
|
||||||
|
"I heard you say, {transcript}.",
|
||||||
|
"Thanks. I heard, {transcript}."
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var seedDirectory in ResolveSeedDirectories())
|
||||||
|
catalog = LegacyMimCatalogImporter.MergeInto(catalog, seedDirectory);
|
||||||
|
|
||||||
|
return catalog;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<string> ResolveSeedDirectories()
|
||||||
|
{
|
||||||
|
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"
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user