Compare commits

50 Commits

Author SHA1 Message Date
Jacob Dubin
a47c90c9c3 Polish weather phrasing for current and forecast replies 2026-05-16 10:16:34 -05:00
Jacob Dubin
393c34055d Prefer forecast hi lo for current weather 2026-05-16 10:01:40 -05:00
Jacob Dubin
f9b728c2a0 Add seasonal and presence charm batches 2026-05-16 09:09:18 -05:00
Jacob Dubin
c87af4686c Add seasonal Build B legacy MIM imports 2026-05-16 08:53:07 -05:00
Jacob Dubin
84759f51de Add Build B charm descriptors and mood replies 2026-05-16 08:39:45 -05:00
Jacob Dubin
c8beb0d1f0 Expand Build B mood and persona follow-ups 2026-05-16 08:05:42 -05:00
Jacob Dubin
e43b4f05f0 Port more legacy charm MIMs into Build B 2026-05-14 22:21:36 -05:00
Jacob Dubin
2677cf9dac Refine commit message handling 2026-05-14 22:06:25 -05:00
Jacob Dubin
20b84632ec Close memory recall turns so recognition does not keep mic open 2026-05-14 21:58:00 -05:00
Jacob Dubin
5718edecaf shortened timeout in conversation broker 2026-05-14 21:47:10 -05:00
Jacob Dubin
40b5b8e4a8 Expand persona follow-ups for identity and favorites 2026-05-14 21:39:17 -05:00
Jacob Dubin
8f7c118fb3 Expand persona inventory and add favorite-family prompts 2026-05-14 21:16:50 -05:00
Jacob Dubin
c30363ec9f Add person-aware favorites and multitenant state scaffolding 2026-05-14 21:15:14 -05:00
Jacob Dubin
ec786be797 Add person-aware state and sync roadmap 2026-05-14 20:48:55 -05:00
Jacob Dubin
f299cef9be Add stateful shopping and to-do list follow-ups 2026-05-14 07:44:46 -05:00
Jacob Dubin
f5e37729ab Expand legacy Build A routing and emotion replies 2026-05-14 06:48:39 -05:00
Jacob Dubin
7297017250 Port legacy persona and emotion replies 2026-05-14 06:44:22 -05:00
Jacob Dubin
66b89f3cee Add Build A legacy MIM import support 2026-05-13 23:22:05 -05:00
Jacob Dubin
11a3e4ef13 Add legacy MIM importer and seed Build A content 2026-05-13 23:18:18 -05:00
Jacob Dubin
7c6dacdbd8 Fix weather, yes/no, and news integrations 2026-05-12 20:36:43 -05:00
Jacob Dubin
9093b429ca Harden weather date parsing and add request diagnostics 2026-05-12 07:52:38 -05:00
Jacob Dubin
df3b34c8ad Add weekly weather cards and improve news API fallback 2026-05-11 22:44:56 -05:00
Jacob Dubin
67c738fae3 Improve weather and news diagnostics for report skills 2026-05-11 19:59:15 -05:00
Jacob Dubin
c0e9b41cd1 Revert weather report-skill routing to stabilize playback 2026-05-11 07:26:56 -05:00
Jacob Dubin
af2fdd230c Improve weather routing and news API fallback 2026-05-11 07:15:11 -05:00
Jacob Dubin
0c597ebbf8 Fix weather forecast parsing and NewsAPI fallback 2026-05-10 23:08:06 -05:00
Jacob Dubin
4bc87f927b Broaden yes no parsing for proactive follow ups 2026-05-10 21:22:25 -05:00
Jacob Dubin
a94b7ec493 Merge branch 'main' of https://kevinblog.sytes.net/Code/Jibo-Revival-Group/JiboExperiments 2026-05-10 20:31:46 -05:00
Jacob Dubin
8c17ad4035 Update commit message generation 2026-05-10 20:31:07 -05:00
383c272d9a Assume unknown requests as neo-hub
I did this so custom servers that haven't edited this to include their server, won't have any issues with Hey Jibo requests.
2026-05-11 01:23:22 +00:00
Jacob Dubin
d434138f9b Refresh OpenJibo docs with a roadmap 2026-05-10 06:15:19 -05:00
Jacob Dubin
80c4ae38fb Add commit message generation prompt 2026-05-10 00:30:31 -05:00
Jacob Dubin
8ae6d86a8c Expand affinity parser guardrails with Pegasus phrases 2026-05-09 23:46:00 -05:00
Jacob Dubin
ffb444e4f9 Fix weather forecast routing and Hi/Lo rendering 2026-05-09 21:46:03 -05:00
Jacob Dubin
7fd732ad17 Expand weather forecast phrasing and day offsets 2026-05-09 09:21:45 -05:00
Jacob Dubin
3ad4a3e025 Add Pegasus-style weather hi-lo visual payload parity 2026-05-07 07:48:51 -05:00
Jacob Dubin
92491adf85 Add personal report parity planning and weather visuals 2026-05-07 07:22:33 -05:00
Jacob Dubin
3e50fb9a49 Add GLSM listener telemetry and stale-listen recovery 2026-05-07 06:24:30 -05:00
Jacob Dubin
69707f32a7 Update commit message 2026-05-06 20:57:32 -05:00
Jacob Dubin
ec14e61a81 Update implementation details 2026-05-06 20:10:31 -05:00
Jacob Dubin
a5de0fdbdd Add pizza yes-no wiring and Pegasus parser guardrails 2026-05-06 16:49:26 -05:00
Jacob Dubin
c64f4b91a8 Import Pegasus emotion phrases into chitchat routing 2026-05-06 16:19:45 -05:00
Jacob Dubin
7d31c3390c Add chitchat state machine routing 2026-05-06 16:01:15 -05:00
Jacob Dubin
0ccfa5db68 Add personal report state machine and turn telemetry 2026-05-06 14:50:37 -05:00
Jacob Dubin
60b8616239 Harden weather fallback and turn finalization 2026-05-06 10:49:24 -05:00
Jacob Dubin
ede694afdd Fix birthday, memory, pizza, and weather intent handling 2026-05-06 09:51:36 -05:00
Jacob Dubin
b74ef3bfa2 Add OpenWeather-backed weather reports 2026-05-06 08:13:26 -05:00
Jacob Dubin
699e0d5282 Add tenant-scoped personal memory facts 2026-05-05 22:40:11 -05:00
Jacob Dubin
687ff62f0f Refine persona routing and update 1.0.19 plan 2026-05-05 22:27:28 -05:00
Jacob Dubin
4b6b744688 Port pizza MIMs and update 1.0.19 planning 2026-05-05 20:27:58 -05:00
179 changed files with 627669 additions and 391 deletions

View File

@@ -1,31 +1,41 @@
# OpenJibo
## Summary
OpenJibo is the working revival track for Jibo.
The near-term plan is intentionally concrete:
We are rebuilding the hosted cloud first, then using that foundation for OTA, Open Jibo OS, and a tiered brain that can eventually hand higher-order work to CoffeeBreak without losing Jibo's original charm.
1. Build a stable replacement cloud on Azure.
2. Use the existing Node prototype as the protocol oracle and capture harness.
3. Port the hosted implementation to .NET as a modular monolith.
4. Bring real robots online first through RCM plus controlled DNS/TLS patching.
5. Use OTA later to reduce setup friction once the hosted cloud is proven.
## Current Focus
This keeps the project grounded in what is already working while moving toward a maintainable hosted platform.
- ship a stable Azure-hosted replacement cloud
- keep the Node prototype as the protocol oracle and capture harness
- port the production path to .NET
- support real devices through repeatable bootstrap steps first
- use OTA later to reduce recovery friction once the cloud is trustworthy
## Current Truth
Current release truth lives in [docs/development-plan.md](docs/development-plan.md). The current cloud release constant is `1.0.19`.
## Roadmap
The long-range plan is summarized in [docs/roadmap.md](docs/roadmap.md). In short:
1. Working hosted cloud.
2. OTA-assisted recovery and updates.
3. Open Jibo OS / `open-jibo` mode conversion.
4. Tiered brain and CoffeeBreak orchestration.
5. Broader ecosystem expansion.
## Current Architecture
The repo now has three distinct lanes:
- `src/Jibo.Cloud/node`
The discovery server. This is the best source of observed protocol behavior today.
Protocol oracle, discovery server, fixture source, and rapid reverse-engineering lab.
- `src/Jibo.Cloud/dotnet`
The long-term hosted implementation. This is where the stable cloud is being built.
Production-oriented hosted implementation intended for Azure deployment and long-term maintenance.
- `src/Jibo.Runtime.Abstractions`
The normalized runtime seam between robot/cloud traffic and modern conversation logic.
The seam between robot/cloud traffic and higher-level runtime and capability logic.
The key architectural idea is:
The core shape is:
```text
Jibo device -> OpenJibo cloud -> normalized runtime contracts -> capabilities and planning
@@ -40,18 +50,30 @@ QR Wi-Fi -> inject OpenJibo region config -> set robot region ->
RCM/device patch for TLS and host acceptance -> OpenJibo cloud on Azure
```
That path is documented in [docs/device-bootstrap.md](/OpenJibo/docs/device-bootstrap.md).
That path is documented in [docs/device-bootstrap.md](docs/device-bootstrap.md).
## Design Principles
- Preserve the original skills and visual design.
- Build the hosted cloud before making OTA the default recovery path.
- Keep every migration reversible whenever possible.
- Prefer source-backed slices over speculative rewrites.
- Let Jibo remain the face of the experience, even when higher-level orchestration sits behind him.
## Repo Map
```text
OpenJibo/
docs/
roadmap.md
development-plan.md
device-bootstrap.md
protocol-inventory.md
feature-backlog.md
public-site-plan.md
regression-test-plan.md
release-1.0.19-plan.md
support-tiers.md
system-diagram-alignment.md
scripts/bootstrap/
Discover-JiboHosts.ps1
@@ -67,56 +89,15 @@ OpenJibo/
OpenJibo.Site/
```
## Decisions Locked In
## Living Docs
- The first milestone is `core revive`, not full protocol parity.
- Azure SQL is the relational system of record for the hosted cloud.
- Billing and donations are future-compatible concerns, not phase-one delivery requirements.
- OTA is a phase-two simplification strategy, not the initial dependency.
Use these when you want the active technical truth:
## Near-Term Work
- [Development plan](docs/development-plan.md)
- [Feature backlog](docs/feature-backlog.md)
- [Release 1.0.19 plan](docs/release-1.0.19-plan.md)
- [Support tiers](docs/support-tiers.md)
- [System diagram alignment](docs/system-diagram-alignment.md)
- [Public site plan](docs/public-site-plan.md)
- port required endpoint and WebSocket behavior from Node to .NET
- keep protocol captures and replay fixtures current
- keep HTTP and websocket live-run telemetry writing to the same repo-root capture tree
- harden device bootstrap documentation and scripts
- map more endpoints and behaviors beyond the current Node coverage
- stand up the initial `openjibo.com` information site
## Live Test Status
The first physical `.NET -> Jibo` experiments have now produced useful captures, but not a full wake-and-interact success yet.
What we have confirmed so far:
- the robot reaches `.NET` HTTP startup calls on `api.jibo.com`
- `.NET` can issue a robot token and accept the `api-socket.jibo.com` websocket
- live HTTP and websocket telemetry are now intended to land together under repo-root `captures/`
What remains unresolved:
- matching the Node startup cadence closely enough for consistent wake/eye-open behavior
- the next post-`api-socket` startup requests and timing seen in successful Node runs
- broader live websocket behavior on a real robot beyond the current synthetic parity slice
The current websocket bridge now also includes server-driven raw-audio turn completion:
- enough buffered audio plus `CONTEXT` can now trigger auto-finalize on the server side
- `EOS` is emitted on that auto-finalize path so turns do not remain open indefinitely
- transcript-less raw-audio turns still fall back to a synthetic compatibility response, not real ASR
The current richer websocket parity slice is still intentionally narrow:
- the successful joke path now has fixture-backed reply sequencing and partial payload-shape fidelity through `CLIENT_ASR -> LISTEN -> EOS -> delayed SKILL_ACTION`
- menu-side `CLIENT_NLU` parity is beginning to expand from live captures, starting with preserved clock-menu intent/rules/entities
- `.NET` now preserves buffered websocket audio frames so local tool-based ASR experiments can run without changing the stable cloud-first architecture
- this is not a claim of broad skill parity or full Jibo websocket coverage
## Important Docs
- [Cloud overview](/src/Jibo.Cloud/README.md)
- [Development plan](/docs/development-plan.md)
- [Protocol inventory](/docs/protocol-inventory.md)
- [Support tiers](/docs/support-tiers.md)
- [Device bootstrap path](/docs/device-bootstrap.md)
- [Public site plan](/docs/public-site-plan.md)
If you only read one document for the long view, make it [docs/roadmap.md](docs/roadmap.md).

View File

@@ -6,18 +6,19 @@ This document is the current working plan for the OpenJibo hosted cloud.
The production lane is the `.NET` cloud in `src/Jibo.Cloud/dotnet`. The Node server remains the protocol oracle, capture harness, and fast reverse-engineering lab, but it is no longer the long-term hosted architecture.
Day-to-day feature sequencing lives in [feature-backlog.md](feature-backlog.md). Live closeout checks live in [regression-test-plan.md](regression-test-plan.md). This file tracks release shape, current code truth, evidence sources, and the boundary between `1.0.18` closeout work and `1.0.19` follow-up work.
Day-to-day feature sequencing lives in [feature-backlog.md](feature-backlog.md). Live closeout checks live in [regression-test-plan.md](regression-test-plan.md). The `1.0.19` release shape is detailed in [release-1.0.19-plan.md](release-1.0.19-plan.md), and the legacy-to-current architecture map is tracked in [system-diagram-alignment.md](system-diagram-alignment.md), while this file keeps the broader evidence and architecture context.
## Current Release Snapshot
- Current OpenJibo Cloud release constant: `1.0.18`
- Current OpenJibo Cloud release constant: `1.0.19`
- Source of truth: [OpenJiboCloudBuildInfo.cs](../src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/OpenJiboCloudBuildInfo.cs)
- Spoken diagnostic: `Open Jibo Cloud version 1 dot 0 dot 18.`
- Spoken diagnostic: `Cloud version 1 dot 0 dot 19.`
- HTTP diagnostic: `/health` returns the same version
- Startup diagnostic: the API logs the same version on boot
- .NET target framework: `net10.0` across the cloud projects and cloud test project
- First `1.0.19` shipped slice: persona prompts (`how old are you`, `when's your birthday`, `do you have a personality`, `make a pizza`)
Release `1.0.18` is now in feature-hardening. Its main bug-fix theme is alarm and photo/gallery behavior on stock OS `1.9`, with a few small feature slices added while the test loop is warm.
Release `1.0.19` is now in feature kickoff. The `1.0.18` alarm/photo/gallery closeout evidence remains below as historical context while we execute the next feature slices.
## Latest Live Evidence
@@ -87,6 +88,11 @@ Current websocket scope:
- active local prompt preservation so `shared/yes_no`, clock, gallery, and settings prompts can still consume transcript-bearing short replies even when the stock skill reports a local context
- binary audio ignored for an existing transID until a fresh `LISTEN` has been seen, preventing context-only or post-speech tails from reopening an endless buffered turn
- blank-audio hotphrase turns clear pending listen state and install a short late-audio ignore window
- first GLSM-aligned listener telemetry and recovery slice is now in source:
- derived phase labels (`HJ_LISTENING`, `LISTENING`, `WAIT_LISTEN_FINISHED`, `DISPATCH_DIALOG`, `PROCESS_LISTENER_QUEUE`)
- `glsm_phase_transition` turn diagnostics
- websocket turn events with `glsmPhase` snapshots
- stale pending-listen recovery for long-open no-context/no-audio listens before processing a new hotphrase listen
- unknown inbound websocket types dropped silently instead of echoing stock-OS-unknown OpenJibo events
- file telemetry and fixture export for HTTP, websocket, and turn captures
@@ -144,6 +150,7 @@ Use these sources as evidence, not as code to copy blindly:
- User-provided original source snapshot: `..\jibo` when extracted locally
- Original Pegasus cloud source inside that snapshot: `pegasus`
- Original SDK and skill source inside that snapshot: `sdk`
- Legacy listener flow reference diagram: `..\jibo\sdk\packages\skills-service-manager\resources\state-diagrams\glsm.png`
- JiboOS reference tree: `..\JiboOS`
- JiboOS skill snapshot: `..\JiboOS\opt\jibo\Jibo\Skills\@be`
@@ -200,6 +207,8 @@ These are not blockers for calling `1.0.18` complete unless the live test shows
After `1.0.18` is tested and tagged, `1.0.19` should move back into feature work:
- harden whichever stop/volume behavior is not fully proven by the `1.0.18` live pass, or pick the next lightweight device/persona slice
- extend persona with holidays and seasonal content as a first-class character track
- build multi-tenant internal memory storage (account/loop/device/user scoped) so new personality and history features persist safely
- end-to-end update/backup/restore proof
- STT reliability improvements, including noise screening and a managed STT comparison
- provider-backed first content path, likely news or weather

View File

@@ -8,6 +8,8 @@ Use it as the working queue when picking the next feature or bug-fix slice. The
The live regression checklist for release closeout is [regression-test-plan.md](regression-test-plan.md).
The active `1.0.19` execution shape is tracked in [release-1.0.19-plan.md](release-1.0.19-plan.md). This file keeps the full `1.0.18` evidence trail for parity reference.
Status key:
- `implemented`: present in current source and covered by focused tests
@@ -24,9 +26,9 @@ Tags:
- `stt`: transcript reliability
- `storage`: persistence, media, backups, or hosted export
## Current `1.0.18` Snapshot
## Historical `1.0.18` Snapshot
Current cloud version: `1.0.18`
Historical cloud version at closeout boundary: `1.0.18`
Runtime truth:
@@ -299,6 +301,20 @@ Current release theme:
- Follow-up:
- live smoke should confirm `cloud version` speaks `1.0.18`, carries `match.skipSurprises = true`, does not stop itself on the word `Jibo`, and settles without a generic `I heard...` reply or a local surprise handoff
### GLSM Listener Flow Capture And Recovery
- Status: `implemented`
- Tags: `protocol`, `docs`
- Result:
- the legacy listener state machine source (`sdk ... glsm.png`) is now captured in current planning docs
- runtime now emits GLSM-aligned phase snapshots (`HJ_LISTENING`, `LISTENING`, `WAIT_LISTEN_FINISHED`, `DISPATCH_DIALOG`, `PROCESS_LISTENER_QUEUE`)
- turn diagnostics now include `glsm_phase_transition` for phase changes
- websocket telemetry now records `glsmPhase` on binary/context/turn events
- stale pending-listen recovery is now in source so a long-open no-context/no-audio listen can be cleared when the next hotphrase listen arrives
- Follow-up:
- live-capture proof is still required against the recurring blue-ring/stuck-listening sequence
- deeper GLSM parity (`Interrupt Listeners`, launch/global parse branches) should be tackled after this first capture slice is validated on-device
### End-Of-Skill Surprise Suppression
- Status: `implemented`
@@ -458,6 +474,34 @@ Current release theme:
- what upload metadata must survive for gallery refresh
- how to map this cleanly to Blob Storage
### Next Up (`2026-05-06`): Dialog Parsing Expansion And Ambiguity Guardrails
- Status: `polish`
- Tags: `protocol`, `content`, `stt`, `docs`
- Why now:
- this is the next queued `1.0.19` implementation slice after weather provider bring-up
- recent live runs showed phrases where trigger detection can interrupt full-utterance understanding
- phrase import work from Pegasus has already started for chitchat and should now expand to broader parsing boundaries
- Scope:
- expand Pegasus-backed phrase coverage for question/command/assertion patterns
- add ambiguity guardrails for overlapping intents (date vs birthday, generic chat vs memory set/lookup, weather variants)
- preserve command-vs-question personality behavior and stock skill launch compatibility
- add focused tests for new phrase families and negative boundary cases
- Progress update (`2026-05-07`):
- implemented date/time guardrails so birthday phrasing is not misrouted to date
- expanded phrase coverage for:
- birthday alias set/recall (`bday` variants)
- shorthand favorites (`my favorite sport football`)
- weather phrasing (`what's today's weather look like`, `will it be sunny tomorrow`)
- updated continuation deferral so complete shorthand favorites finalize instead of waiting for missing continuation
- Exit criteria:
- ambiguous phrase handling is improved without regressions in existing `1.0.19` features
- phrase imports are documented and traceable to Pegasus parser sources
- test suite stays green and includes targeted parser-guardrail coverage
- Tracking:
- [release-1.0.19-plan.md](release-1.0.19-plan.md)
- [system-diagram-alignment.md](system-diagram-alignment.md)
## Discovery Queue
### 12. Weather As Cloud Report Plus Local Presentation
@@ -573,28 +617,251 @@ Current release theme:
### 21. How Old Are You / Robot Age Persona
- Status: `discovery`
- Status: `implemented`
- Tags: `protocol`, `content`
- User goals:
- Result:
- `how old are you`
- answer from stored first-powered-up or first-cloud-seen metadata
- optional zodiac/personality flavor when available
- Questions:
- where stock Jibo stores first-power-up or birthdate metadata
- whether a stock persona path exists
- whether first OpenJibo pass should use first-cloud-seen metadata if stock data is unavailable
- `when's your birthday`
- `do you have a personality`
- `make a pizza` now ports the original scripted-response path through `chitchat-skill` with `mim_id = RA_JBO_MakePizza` and pizza-making animation ESML
- `can you order pizza` now ports the original scripted-response path through `chitchat-skill` with `mim_id = RA_JBO_OrderPizza`
- current source answers these with a `1.0.19` rule-based persona baseline, backed by `OpenJiboCloudBuildInfo.PersonaBirthday`
- Follow-up:
- wire persona age to first-powered-up or durable first-cloud-seen metadata when available
- add command-vs-question variants so expressive prompts can answer conversationally before launching actions
### 22. Command Vs Question Reply Style
- Status: `ready`
- Status: `implemented`
- Tags: `content`, `polish`
- User goals:
- `dance` should behave like a willing action
- `do you like to dance` should answer the question before or instead of treating it like the same command
- Implementation notes:
- evolve reply collections into command/question variants
- start with dance or another expressive skill
- keep the first version rule-based
- Result:
- `dance` still launches the dance animation path
- `do you like to dance` now responds conversationally as a personality question instead of launching the action
- birthday phrasing now takes precedence over an `askForDate` client-intent misclassification
- Follow-up:
- expand command-vs-question splits to more expressive intents (pizza, surprise, photo prompts)
- add Pegasus phrase and MIM-backed variants for richer style coverage
### 23. First Memory-Backed Personal Facts
- Status: `implemented`
- Tags: `storage`, `content`
- Result:
- tenant-scoped memory store abstraction is in place for personal facts
- birthday set/recall works (`my birthday is ...` / `when is my birthday`)
- preference set/recall works (`my favorite X is Y` / `what is my favorite X`)
- account/loop/device scoped lookup prevents cross-tenant leakage
- Follow-up:
- add durable persistence path for personal facts
- 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
- Status: `implemented`
- Tags: `content`, `storage`, `protocol`
- Result:
- `surprise me` now uses weighted candidate selection instead of only generic fallback text
- candidate weighting uses tenant-scoped memory signals and date triggers
- February 9 (`National Pizza Day`) can proactively launch the legacy pizza animation path
- proactive pizza fact offer flow stores pending offer state in session metadata and resolves direct short `yes/no` turns
- memory parsing now includes names, anniversary-style important dates, likes/dislikes variants, and reverse favorite phrasing (`pizza is my favorite food`)
- Follow-up:
- expand proactivity beyond pizza to additional Pegasus-backed categories
- add cooldown/throttle policy and observability around proactive offer frequency
- 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
- Status: `implemented`
- Tags: `protocol`, `content`
- Result:
- weather requests now launch `report-skill` using Pegasus-aligned intent `requestWeatherPR`
- weather phrase coverage includes baseline forecast and condition-style questions (`will it rain`, `is it snowing`, tomorrow variants)
- weather launches emit `SKILL_REDIRECT` + completion and now also include cloud weather speech so weather turns remain useful even when local report providers are incomplete
- weather entity hints are carried in outbound NLU (`date = tomorrow`, `Weather = rain/snow/...`) for report-skill consumption
- OpenWeather provider integration is in place with configurable API key, default location, unit preference, and environment-variable fallback (`OPENWEATHER_API_KEY`)
- cloud weather speech now uses live provider summaries for current conditions and tomorrow high/low forecast when available
- Follow-up:
- connect weather units and location directly to user/report-skill settings parity instead of config defaults
- add richer condition-change commentary and view parity with original report-skill weather behaviors
### 26. Presence-Aware Greetings And Identity Proactivity
- Status: `ready`
- Tags: `protocol`, `content`, `storage`, `docs`
- Why now:
- this is the next personality-charm expansion after parser guardrail and weather bring-up
- Pegasus greetings behavior is strongly tied to presence/identity signals and proactive cooldown policy
- current OpenJibo has memory/proactivity foundations but no first-class presence extraction path yet
- Pegasus source anchors:
- `C:\Projects\jibo\pegasus\packages\hub\be-skills\greetings_manifest.json`
- `C:\Projects\jibo\sdk\skills\greetings\src\GreetingsSkill.ts`
- `C:\Projects\jibo\sdk\skills\greetings\src\GreetingsSM.ts`
- `C:\Projects\jibo\pegasus\packages\hub\src\proactive\ProactiveTransactionHandler.ts`
- `C:\Projects\jibo\pegasus\packages\hub\src\proactive\tools\ContextTools.ts`
- Scope:
- extract presence/identity context (`speaker`, `peoplePresent`, focused person) from runtime context payload
- add greeting intent families and state-machine split for reactive vs proactive greeting routes
- add cooldown and trigger-source guardrails for proactive greetings
- start person-aware greeting hooks (name-aware greeting, morning greeting policy, return greeting policy)
- Exit criteria:
- presence-aware greetings are routed deterministically with tests
- proactive greetings are frequency-bounded and do not trigger from surprise source when blocked by policy
- fallback behavior remains stable when identity is unknown or context is incomplete
- docs and release tracking are updated with shipped scope and residual gaps
- Tracking:
- [greetings-presence-plan.md](greetings-presence-plan.md)
- [release-1.0.19-plan.md](release-1.0.19-plan.md)
### 27. Personal Report Parity Track (Weather/News/Commute/Calendar)
- Status: `in_progress`
- Tags: `protocol`, `content`, `storage`, `docs`
- Why now:
- personal report is a core Jibo charm surface and currently split between implemented weather speech and placeholder calendar/commute/news content
- Pegasus weather used explicit condition animations and weather views; current OpenJibo weather is functional but visually lighter
- Scope:
- weather icon/animation parity and view support
- broader non-local weather query handling and short-range date coverage
- provider-backed news ingestion and filtering
- commute provider path and settings schema
- coverage matrix for personal report parity gaps and test/capture exit criteria
- Progress update (`2026-05-10`):
- added provider-ready news briefing lane with Nimbus-compatible `news` skill payload continuity
- added memory/transcript category hint plumbing for provider requests (sports/technology/business/general)
- fallback synthetic news behavior remains active when no provider key is configured
- added TTL caching for weather/news provider calls to reduce repeated external requests
- Source anchors:
- `C:\Projects\jibo\pegasus\packages\report-skill\src\subskills\weather\WeatherMimLogic.ts`
- `C:\Projects\jibo\pegasus\packages\report-skill\resources\views\weatherHiLo.json`
- `C:\Projects\jibo\pegasus\packages\report-skill\src\subskills\news\NewsMimLogic.ts`
- `C:\Projects\jibo\pegasus\packages\report-skill\src\subskills\commute\CommuteMimLogic.ts`
- `C:\Projects\jibo\pegasus\packages\hub\pegasus-skills\report_skill_manifest.json`
- Tracking:
- [personal-report-parity-plan.md](personal-report-parity-plan.md)
- [release-1.0.19-plan.md](release-1.0.19-plan.md)
### 28. Grocery List Capability (Requested Feature)
- Status: `discovery`
- Tags: `content`, `docs`, `storage`
- Why now:
- directly requested by Jibo owners and fits memory + household utility roadmap
- Source findings:
- Pegasus has scripted responses for shopping/to-do list requests but no standalone grocery-list skill in this snapshot
- examples:
- `C:\Projects\jibo\pegasus\packages\chitchat-skill\mims\scripted-responses\RA_JBO_ShoppingList.mim`
- `C:\Projects\jibo\pegasus\packages\chitchat-skill\mims\scripted-responses\RA_JBO_ManageToDoList.mim`
- Candidate delivery paths:
- native lightweight list skill (fastest user value)
- integration-backed list orchestration (long-term richer ecosystem fit)
- Exit criteria:
- clear decision on MVP path
- first schema for list items + ownership scope
- initial voice flows and follow-up intent handling defined
### 29. Legacy MIM Personality Import Ladder
- Status: `in_progress`
- Tags: `content`, `protocol`, `docs`
- Why now:
- we already have a chitchat/content scaffold that can render stock-compatible personality replies
- the legacy `chitchat-mims` tree is mostly declarative content, so a phased import can add visible charm fast
- this is the best near-term path to get Jibo feeling more interactive without needing a full Pegasus runtime clone
- What is possible today:
- direct scripted replies through the existing content catalog
- stock-compatible payloads with `skillId`, `mim_id`, `mim_type`, `prompt_id`, and ESML
- current examples already prove the shape for pizza, dance, weather, news, and generic chat
- What we need to build:
1. a MIM inventory importer that can scan the legacy tree and normalize `skill_id`, `mim_id`, prompt text, and metadata
2. a prompt-selection layer that can choose by category and condition metadata
3. a safe ESML/prompt renderer for imported content
- What can be ported with each build:
- Build A: declarative prompt packs
- `core-responses`
- `deflector`
- the simplest `emotion-responses`
- direct `scripted-responses` that are just prompt lists
- Build B: conditioned prompt packs
- `gqa-responses`
- structured emotion prompts with `condition` gates
- any response families that only need simple state or Jibo-emotion checks
- Build C: conversation families
- richer `scripted-responses` that need follow-up state
- holiday / special-date personality sets
- more nuanced chitchat branches that depend on context-aware routing
- Build D: full parity cleanup
- larger cross-skill collections
- any MIMs that depend on Pegasus-only parser assumptions
- any files that need dedicated runtime abstraction instead of catalog lookup
- Low-hanging fruit for tonight:
- import the smallest declarative packs first so we can test something tomorrow
- prioritize anything that is pure prompt text with no complex branching
- keep the first pass limited to content that maps cleanly onto the current catalog shape
- Progress update (`2026-05-13`):
- added the first Build A importer scaffold in the cloud content repository
- checked in a small seed bundle under `Content/LegacyMims/BuildA`
- added focused importer tests for prompt stripping, bucketing, and merge behavior
- expanded Build A with additional easy scripted-response packs for identity and persona replies
- started Build B with source-backed scripted-response packs for work, food, home, birthplace, language, hobby, and material questions
- Tomorrow test target:
- verify imported personality replies show up through the existing chitchat route
- confirm the emitted payload still looks like a stock skill response
- confirm the imported content does not disturb existing weather/news/pizza flows
- Exit criteria:
- a first importer path exists for the simplest legacy MIM files
- at least one legacy prompt pack is running through OpenJibo content instead of hand-authored fallback text
- we have a clear second-wave list for the more conditional MIM families
### 30. Original Personalized Function Inventory
- Status: `discovery`
- Tags: `content`, `docs`, `protocol`
- Why now:
- we are actively porting persona and memory slices, so we need a bounded checklist of the original Jibo charm surfaces
- the goal is to keep the next few passes focused on personality-rich wins instead of letting the work sprawl
- Known sources:
- legacy Jibo OS/Pegasus chitchat and MIM response families
- current OpenJibo persona, memory, and greeting work as the implementation target
- Inventory to track:
- identity and origin questions
- personality and capability questions
- favorite-style prompts like `what is your favorite color`
- attraction and preference prompts like `what is your favorite flower`, `do you like R2D2`, `do you like the sun`, `do you like space`, and `do you like kids`
- 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
- 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
## Suggested Order
@@ -610,14 +877,23 @@ Use [regression-test-plan.md](regression-test-plan.md) as the detailed checklist
For `1.0.19`:
1. Harden stop or volume if the `1.0.18` live pass exposes stock-OS quirks / harden $YESNO interaction
2. Make a pizza. How old are you? When's your birthday? Do you have a personality? (This is a fun one that can be implemented quickly and adds a lot of character, so it should be early in the queue to start showing off the new content capabilities.)
3. Update, backup, and restore proof
4. STT upgrade and noise screening
5. Hosted capture/storage plan / indexing for group testing
6. Binary-safe media storage / sync to cloud drive: OneDrive, Google Drive, Box, etc.
7. Provider-backed news and weather
8. Proactivity, dialog parsing/NLP, memory/history, Lasso, identity, and onboarding as larger discovery-driven tracks
1. Command-vs-question personality split (`dance` command vs `do you like to dance` question style; expand this pattern) - implemented
2. Expand memory-backed personal facts with tenant-scoped storage (beyond the first birthday/preferences foundation) - implemented
3. Proactivity selector baseline with source-backed first offers - implemented
4. Weather report-skill launch compatibility - implemented
5. Dialog parsing expansion and ambiguity guardrails - in progress (`2026-05-09` third guardrail slice implemented; Pegasus affinity phrase families + continuation guardrails expanded)
6. Presence-aware greetings and identity-triggered proactivity - implemented (trigger path, identity-aware reactive/proactive replies, cooldown metadata wiring, focused websocket coverage)
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
9. Durable memory persistence path (multi-tenant backing store)
10. Update, backup, and restore proof
11. STT upgrade and noise screening
12. Hosted capture/storage plan / indexing for group testing
13. Binary-safe media storage / sync to cloud drive: OneDrive, Google Drive, Box, etc.
14. Provider-backed news and weather parity polish
15. Grocery list capability discovery and MVP selection
16. Lasso, identity, and onboarding as larger discovery-driven tracks
17. Legacy MIM personality import ladder and first declarative prompt packs
For `1.0.20` and beyond:
@@ -628,4 +904,4 @@ For `1.0.20` and beyond:
5. Loop advancement (family and friends) / multiple user recognition / multiple Jibo support so Jibo's can interact and communicate
6. Advanced Jibo features such as pizza delivery, Uber/Lyft integration, calendar management, smart home control (Home Assistant), etc. can be added after the conversion process is smooth and stable, with a focus on features that leverage the new cloud capabilities and content personalization enabled by Open Jibo
7. LLM integration for more natural dialog, question answering, and content generation can be explored as a longer-term goal after the core platform is stable and has a growing user base to provide feedback and use cases for LLM-powered features
8. Tiered Jibo brain/orchestration plan from README.md can be implemented in parallel with the above, starting with the simplest cloud features and gradually adding more complex capabilities as the platform matures and user feedback is collected, always preserving his unique charm and original features.
8. Tiered Jibo brain/orchestration plan from README.md can be implemented in parallel with the above, starting with the simplest cloud features and gradually adding more complex capabilities as the platform matures and user feedback is collected, always preserving his unique charm and original features.

View File

@@ -0,0 +1,173 @@
# Greetings And Presence Plan (`1.0.19`)
## Purpose
Recreate the original Jibo greeting charm with modern cloud architecture:
- person-aware greetings when someone is detected
- proactive offers tied to presence, time of day, and memory
- safe cooldown rules so proactivity feels alive, not noisy
This plan is source-anchored to Pegasus and scoped to shippable slices.
## Pegasus Behavior Baseline
Primary source artifacts:
- `C:\Projects\jibo\pegasus\packages\hub\be-skills\greetings_manifest.json`
- `C:\Projects\jibo\sdk\skills\greetings\src\GreetingsSkill.ts`
- `C:\Projects\jibo\sdk\skills\greetings\src\GreetingsSM.ts`
- `C:\Projects\jibo\sdk\skills\greetings\src\states\IntentSplit.ts`
- `C:\Projects\jibo\sdk\skills\greetings\src\states\ProactiveGreetingState.ts`
- `C:\Projects\jibo\sdk\skills\greetings\src\states\ProactiveProbabilityState.ts`
- `C:\Projects\jibo\sdk\skills\greetings\src\states\ShouldDoMorningGreetingState.ts`
- `C:\Projects\jibo\sdk\skills\greetings\src\states\ShouldDoBirthdayState.ts`
- `C:\Projects\jibo\sdk\skills\greetings\src\states\ShouldDoHolidayState.ts`
- `C:\Projects\jibo\pegasus\packages\hub\src\proactive\ProactiveTransactionHandler.ts`
- `C:\Projects\jibo\pegasus\packages\hub\src\proactive\tools\ContextTools.ts`
Key behaviors to port:
- explicit reactive/proactive greeting split
- identity source split:
- reactive path uses active speaker
- proactive path uses present identified persons
- hub-level proactive gating:
- block greetings when trigger source is `SURPRISE`
- throttle by interaction history (`GreetingsLaunchLast2Hours < 1`)
- morning/birthday/holiday gates with per-user recency checks
- optional follow-up response flow after proactive greetings
## Current OpenJibo Baseline
Current implementation anchor:
- `C:\Projects\JiboExperiments\OpenJibo\src\Jibo.Cloud\dotnet\src\Jibo.Cloud.Application\Services\JiboInteractionService.cs`
- `C:\Projects\JiboExperiments\OpenJibo\src\Jibo.Cloud\dotnet\src\Jibo.Cloud.Application\Services\ProtocolToTurnContextMapper.cs`
- `C:\Projects\JiboExperiments\OpenJibo\src\Jibo.Cloud\dotnet\src\Jibo.Cloud.Application\Services\WebSocketTurnFinalizationService.cs`
- `C:\Projects\JiboExperiments\OpenJibo\src\Jibo.Cloud\dotnet\src\Jibo.Cloud.Application\Services\ChitchatStateMachine.cs`
- `C:\Projects\JiboExperiments\OpenJibo\src\Jibo.Cloud\dotnet\src\Jibo.Cloud.Infrastructure\Persistence\InMemoryPersonalMemoryStore.cs`
What we already have:
- tenant-scoped memory primitives (name, birthday, preferences, affinity)
- proactivity baseline with pending-offer follow-up handling
- state-machine style chitchat split (`ScriptedResponse`, `EmotionQuery`, `EmotionCommand`, `ErrorResponse`)
- GLSM-aware websocket lifecycle and stuck-listen recovery
Main gap:
- no first-class presence/identity perception extraction from runtime context for greeting policy decisions
## Implementation Slices
### Slice G1: Presence Context Extraction And Session Snapshot
Goal:
- extract presence/identity fields from websocket context payload into normalized metadata for routing
Initial fields:
- focused speaker id
- identified person ids present
- total people present
- trigger source if present
- time-of-day helper signals
Notes:
- no facial-recognition implementation is needed in cloud; cloud consumes robot perception signals
### Slice G2: Greeting Intent Families And Parser Guardrails
Goal:
- add explicit greeting intent families with question/command guardrails
Initial families:
- `hello`, `hey jibo`, `what's up`
- `good morning`, `good afternoon`, `good evening`, `good night`
- `i'm home`, `i'm back`
- identity question (`who am i`) as a future-compatible hook
Guardrails:
- avoid stealing non-greeting domains
- keep existing date/time and birthday disambiguation intact
### Slice G3: Greeting State-Machine Port (OpenJibo Style)
Goal:
- add a greeting state-machine module with explicit route metadata like chitchat
Planned routes:
- `ReactiveGreeting`
- `ProactiveGreeting`
- `MorningGreeting`
- `SpecialDayGreeting`
- `OptionalResponse`
- `ErrorResponse`
Output shape:
- keep stock-compatible skill payload patterns
- preserve MIM/ESML hook points for charm content
### Slice G4: Proactive Gating And Cooldowns
Goal:
- port the critical Pegasus policy behavior to prevent spam
Phase-1 rules:
- skip proactive greetings when trigger source is surprise
- enforce per-tenant/person cooldown (target parity: 2-hour greeting window)
- suppress proactive launch when session is unstable (pending listen/follow-up conflict)
### Slice G5: Person Queue And Memory Extensions
Goal:
- introduce lightweight person queue/history for greeting relevance
Phase-1 storage additions:
- last-seen timestamp per person key
- last-greeted timestamp per person key
- optional preferred-name alias for spoken greeting personalization
### Slice G6: Rollout, Logging, And Live Validation
Goal:
- ship safely with observability and test confidence
Required coverage:
- unit tests for context extraction and intent routing
- websocket tests for presence-triggered greeting eligibility and cooldown behavior
- live captures validating:
- no stuck listening regressions
- no runaway proactive loops
- stable fallback when identity is unknown
## Suggested Build Order
1. G1 context extraction + diagnostics
2. G2 greeting parser families + guardrails
3. G3 greeting state machine (reactive first)
4. G4 proactive gating + cooldowns
5. G5 person queue memory extensions
6. G6 live validation and polish
## Definition Of Done For This Track
- presence-aware greeting behavior works with and without identified users
- proactive greeting frequency is policy-bounded and observable
- no regressions in existing `1.0.19` memory/weather/proactivity flows
- release docs and backlog are updated with shipped scope and next slice

View File

@@ -0,0 +1,105 @@
# Personal Report Parity Plan
As-of: `2026-05-07`
## Objective
Bring OpenJibo personal report behavior closer to original Jibo charm while keeping cloud architecture modern and provider-agnostic.
## Pegasus Findings (Source Anchors)
- Weather personality and visuals were MIM-driven, not plain speech:
- `C:\Projects\jibo\pegasus\packages\report-skill\src\subskills\weather\WeatherMimLogic.ts`
- `C:\Projects\jibo\pegasus\packages\report-skill\mims\en-us\WeatherCommentRain.mim`
- `C:\Projects\jibo\pegasus\packages\report-skill\mims\en-us\WeatherTodayHighLow.mim`
- `C:\Projects\jibo\pegasus\packages\report-skill\resources\views\weatherHiLo.json`
- Weather icons were mapped to condition/time-of-day tokens (`clear-day`, `partly-cloudy-night`, etc.) and used in `<anim cat='weather' meta='...'>`.
- Report-skill supported reactive entrypoints beyond full personal report:
- `requestWeatherPR`, `requestNews`, `requestCommute`, `requestCalendar`
- Source: `C:\Projects\jibo\pegasus\packages\hub\pegasus-skills\report_skill_manifest.json`
- Legacy data backends were Lasso-mediated:
- weather: Dark Sky
- commute: Google Maps directions/traffic
- news: AP News feeds
- calendar: Google/Outlook connectors
- Parser `main_agent` explicitly includes weather/news/personal-report intents; direct commute/calendar intents are not present in that same folder snapshot:
- `C:\Projects\jibo\pegasus\packages\parser\dialogflow\main_agent\intents`
- Grocery/list behavior found in Pegasus is scripted-response style, not a standalone list skill:
- `RA_JBO_ShoppingList.mim` and `RA_JBO_ManageToDoList.mim` are "not supported yet" style responses.
## OpenJibo Current State
- Personal report state machine exists and is test-backed.
- Weather provider integration exists (OpenWeather), including current and tomorrow.
- News and commute currently have baseline placeholder speech, not live provider-backed data orchestration.
- Calendar is currently reply-based and not yet provider-integrated.
## Gap Summary
1. Weather has factual speech but needs stronger visual/personality parity.
2. Non-local weather and broader date scopes need expansion beyond basic trailing `in <location>` and tomorrow handling.
3. Live news feed selection and filtering strategy is not yet implemented.
4. Commute data path and settings model are not yet mapped to an active provider integration.
5. Full personal report parity matrix (weather/commute/calendar/news behavior details) is not yet documented as a ship checklist.
## Implementation Phases
## Phase 1 (In Progress): Weather Personality Lift
- Add weather-condition animation metadata and expressive weather MIM-style prompt metadata to cloud weather speech.
- Expand location phrase handling (`in/for/at`) and suffix stripping for common temporal tails.
## Phase 2: Weather Visual Layer Parity
- Add weather Hi/Lo view payload support (OpenJibo-side equivalent to `weatherHiLo.json` behavior).
- Carry mapped weather icon token + hi/lo values into outbound skill action config.
- Keep fallback behavior safe when view assets are unavailable.
## Phase 3: Weather Scope Expansion
- Add parser support for additional time requests (for example weekend/next-week phrasing).
- Extend weather request model to support short-range date windows.
- Decide whether range responses are summarized speech-only or include multi-card view behavior.
## Phase 4: Live News Source
- Introduce provider-backed headline ingestion with category toggles.
- Mirror core Pegasus constraints:
- de-duplicate headlines
- filter missing summaries/images
- child-safe filtering mode
- Preserve current speech fallback if provider is unavailable.
## Phase 5: Commute Data Path
- Implement commute provider abstraction and first provider integration.
- Recreate core commute decision logic:
- minutes-left
- normal vs delayed traffic commentary
- mode-aware phrasing (drive vs transit)
- Add settings contract for origin/destination/work-arrival/mode.
## Phase 6: Personal Report Coverage Matrix
- Build parity matrix across weather/news/commute/calendar:
- intent phrases
- required entities/settings
- provider dependencies
- expected MIM/view style outputs
- fallback behavior
- Attach tests and capture criteria for each row.
## Phase 7 (Future Release): Grocery Lists
- Track as a future release item (requested by users).
- Two candidate paths:
1. Native lightweight list skill (fastest to ship).
2. Integration-backed list orchestration (better long-term ecosystem fit).
- Recommendation: ship native MVP first, then add integration connectors.
## Next Immediate Execution
1. Validate weather personality-lift behavior in live runs.
2. Implement weather view payload support (Hi/Lo + condition icon).
3. Draft provider plan for live news source.
4. Draft commute provider interface + settings schema.

View File

@@ -9,6 +9,7 @@ Stand up a small public site on `openjibo.com` that makes the project understand
- project overview
- current status
- links to source repositories
- roadmap / long-range plan
- links to device bootstrap docs
- explanation of the hosted-cloud direction
- contribution/contact or waitlist path

View File

@@ -0,0 +1,350 @@
# Release `1.0.19` Plan
## Purpose
This release starts the shift from `1.0.18` hardening to visible feature growth.
The goal is to keep compatibility work steady while shipping personality and capability slices that make OpenJibo feel less like a placeholder cloud and more like a real assistant platform.
## Snapshot
- Kickoff date: `2026-05-05`
- Cloud version source of truth: [OpenJiboCloudBuildInfo.cs](../src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/OpenJiboCloudBuildInfo.cs)
- Active release constant: `1.0.19`
## Scope
### 1. Persona And Identity Surface
- add natural voice responses for robot identity/personality prompts
- start building reusable content hooks for question-vs-command style responses
- 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`
- 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 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
- complete update/backup/restore proof path with captures and operator docs
- 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
### 3. Pegasus-To-Cloud Platform Porting
- prioritize small source-backed slices from Pegasus/JiboOS that can be shipped safely
- keep Nimbus and stock payload compatibility as the release guardrail
- 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
- port holiday-aware personality responses as a visible extension of the new persona slice
- start with a small, source-backed set (for example birthdays/holidays already represented in legacy data paths)
- ensure holiday responses feel characterful while still routing through stock-compatible payloads
### 5. Multi-Tenant Memory Storage Foundation
- 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
- 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
### 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
## First Implemented Slice In `1.0.19`
The first delivered slice in this release is persona expansion:
- `how old are you`
- `when's your birthday`
- `do you have a personality`
- `make a pizza`
`make a pizza` is now wired to the legacy scripted-response identity (`RA_JBO_MakePizza`) with pizza-making animation ESML, based on the original skill manifests.
This slice is intentionally small and user-visible. It creates immediate personality gains while we keep deeper platform work in parallel.
## Second Implemented Slice In `1.0.19`
The second delivered slice is first tenant-scoped personal memory:
- store birthday from phrases like `my birthday is April 12`
- recall birthday from phrases like `when is my birthday`
- store preferences from phrases like `my favorite music is jazz`
- recall preferences from phrases like `what is my favorite music`
Memory keys are scoped by account/loop/device tenant context so one tenant does not leak into another.
## Third Implemented Slice In `1.0.19`
The third delivered slice starts memory-triggered proactivity and broadens memory parsing:
- `surprise me` now runs a weighted proactivity selector
- selectors use tenant-scoped memory signals (favorites and likes/dislikes) plus date triggers
- February 9 (`National Pizza Day`) can proactively launch the pizza animation path
- proactive pizza fact offer flow now stores pending offer state and resolves direct `yes` / `no` follow-up answers
- memory parsing now covers:
- names (`my name is ...`, `what is my name`)
- important dates (`our anniversary is ...`, `when is our anniversary`)
- likes/dislikes (`i like ...`, `i love ...`, `i dislike ...`, `i don't like ...`)
- favorite phrase variants including reverse form (`pizza is my favorite food`)
## Fourth Implemented Slice In `1.0.19`
The fourth delivered slice starts weather compatibility using Pegasus-style report-skill routing:
- weather phrases now route to `report-skill` instead of generic placeholder chat
- outbound NLU launch uses legacy reactive intent `requestWeatherPR` (source-aligned with Pegasus manifests/tests)
- weather entity hints are added for:
- `date = tomorrow` on tomorrow phrasing
- `Weather = rain|snow|...` on condition questions (for example `will it rain tomorrow`)
- websocket output now emits local skill redirect + silent completion for weather launch, matching existing local-skill launch patterns
## Fifth Implemented Slice In `1.0.19`
The fifth delivered slice adds provider-backed weather content while preserving Pegasus launch compatibility:
- OpenWeather provider abstraction and infrastructure wiring are added to the hosted cloud
- weather requests still launch `report-skill` with `requestWeatherPR` and legacy weather/date entities
- weather replies now include cloud-generated spoken summaries from provider data:
- current conditions (`Right now in ...`)
- tomorrow forecast shape (`Tomorrow in ...`) with high/low temperatures when available
- 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
## 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
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`)
Legacy architecture (`system_diagram.png`) has been mapped to current OpenJibo cloud services so release execution stays anchored to:
- where we were (Pegasus/Jibo cloud design intent)
- where we are (current hosted `.NET` modular monolith)
- where we are headed (durable memory, proactivity catalogs, parser depth, provider aggregation)
Reference:
- [system-diagram-alignment.md](system-diagram-alignment.md)
## Greetings And Presence Planning Snapshot (`2026-05-07`)
Pegasus greeting and presence behavior has now been captured into a source-anchored OpenJibo implementation plan.
Reference:
- [greetings-presence-plan.md](greetings-presence-plan.md)
## Live Validation Snapshot (`2026-05-07`)
User-confirmed end-to-end behavior now includes:
- `Hey Jibo -> What's your cloud version?` (working)
- `Hey Jibo -> What's the time?` (working)
- `Hey Jibo -> Surprise me -> pizza fact -> $YESNO (Yes) -> fact` (working)
- `Hey Jibo -> Surprise me -> pizza fact -> $YESNO (No) -> decline reply` (working)
This confirms the pizza-fact offer state now keeps the yes/no branch open through completion and does not require a second wake-word reset for the follow-up answer.
## Personal Report Planning Snapshot (`2026-05-07`)
Personal report parity planning is now captured with Pegasus source anchors for weather visuals/animations, live news, commute, and calendar gap coverage.
Reference:
- [personal-report-parity-plan.md](personal-report-parity-plan.md)
## Next Queued Task (`2026-05-06`)
Queued next `1.0.19` implementation task (now started):
- dialog parsing expansion and ambiguity guardrails
Execution focus:
- import additional Pegasus parser phrases/entities into intent handling where safe
- reduce trigger-only captures that drop the rest of the utterance
- preserve command-vs-question personality split and local skill payload compatibility
- add focused tests for new phrase families and ambiguity boundaries
- keep listener-state observability aligned with the legacy GLSM flow while phrase guardrails are added
First completed guardrail slice under this queue:
- GLSM listener flow capture + telemetry mapping
- stale pending-listen recovery path for long-open no-context/no-audio listens
Second completed guardrail slice under this queue:
- tightened date/time ambiguity handling (`what's your birthday`/`what's your bday` no longer falls into date intent)
- expanded Pegasus-inspired memory/weather phrase coverage:
- birthday alias parsing (`my bday is ...`, `when is my bday`)
- shorthand preference sets (`my favorite sport football`)
- weather variants (`what's today's weather look like`, `will it be sunny tomorrow`)
- listener continuation guardrail now differentiates incomplete preference fragments from complete shorthand preference sets
Third completed guardrail slice under this queue:
- expanded Pegasus `userLikesThing` / `userDislikesThing` / `doesUserLikeThing` / `doesUserDislikeThing` phrase-family coverage
- includes additional dislike/negation variants (`loathe`, `did not like`, `didn't enjoy`, `don't really like`)
- includes group-preference variants (`we like`, `we love`, `we dislike`, `we can't stand`)
- includes lookup variants (`do you think i like ...`, `do you believe i don't like ...`)
- added affinity set/lookup attempt guardrails so partial captures route to affinity prompts instead of generic chat
- extended auto-finalize continuation deferral for the new Pegasus affinity stems (`we like`, `i loathe`, and related variants)
- added focused interaction + websocket tests for the new parser/guardrail behavior
Next queued implementation track after parser guardrails:
- personal report parity slices (weather visual parity, live news path, commute/calendar gap closure)
First completed slice in this personal-report parity track:
- added provider-ready news briefing path with Nimbus-compatible `news` payload continuity
- preserved fallback behavior when no live provider is configured
- added memory/transcript category hinting for provider requests (`sports`, `technology`, `business`, etc.)
- added provider-side request caching for both news and weather to reduce integration churn and repeated lookups
- added focused interaction + websocket tests for provider-backed news speech output and request-hint plumbing
## Next Slices
1. MIM import foundation for personality expansion
2. Dialog parsing expansion
3. Presence-aware greetings and identity-triggered proactivity
4. Personal report parity slices
5. Holidays and seasonal personality slice beyond pizza day
6. Durable memory persistence path
7. Update/backup/restore end-to-end proof
8. STT noise-screening and short-utterance reliability pass
9. Provider-backed news expansion and deeper weather parity
10. Capture indexing and retention boundary for group testing
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
Release `1.0.19` is complete when:
- planned slices have focused tests and updated docs
- regression checklist passes for the existing stock-OS compatibility paths
- live runs confirm no critical regressions in alarms, gallery, yes/no, and cloud-version diagnostics
- memory/personality storage proves tenant isolation by account/loop/device boundaries and is compatible with the target hosted cloud footprint

File diff suppressed because one or more lines are too long

151
OpenJibo/docs/roadmap.md Normal file
View File

@@ -0,0 +1,151 @@
# OpenJibo Roadmap
## Purpose
This is the long-range story for OpenJibo.
Use it when someone wants the shape of the project without reading every release note, backlog entry, or live-test log.
The current execution truth still lives in:
- [Development plan](development-plan.md)
- [Feature backlog](feature-backlog.md)
- [Release 1.0.19 plan](release-1.0.19-plan.md)
- [Device bootstrap path](device-bootstrap.md)
## North Star
Bring Jibo back in a way that preserves his original skills, design language, and charm, while layering in a modern hosted cloud, safer updates, and eventually a richer on-device and orchestration stack.
## Guiding Principles
- Preserve the original skills and visual design before adding new behaviors.
- Build the hosted cloud first so the robot has something stable to talk to.
- Use OTA to reduce friction after the cloud is proven.
- Keep every migration reversible.
- Favor small, source-backed slices over speculative rewrites.
- Let Jibo remain the face of the experience, even if other systems help orchestrate the work behind him.
## Roadmap At A Glance
| Phase | Focus | Why It Matters |
| --- | --- | --- |
| 1 | Working hosted cloud | Restores the services Jibo already expects and gives us the current platform truth. |
| 2 | OTA-assisted recovery and updates | Makes ownership easier by turning the cloud into the delivery path for recovery and upgrades. |
| 3 | Open Jibo OS / mode conversion | Creates an owned runtime and configuration layer while preserving the original experience. |
| 4 | Tiered brain | Separates reflexes, memory, personality, and higher-level orchestration. |
| 5 | CoffeeBreak orchestration | Provides a place for multi-step agent workflows and external tools without flattening Jibo's personality. |
| 6 | Ecosystem expansion | Grows the platform into household, productivity, and multi-device use cases. |
## Phase 1: Working Hosted Cloud
Current state: in progress.
The near-term job is to keep the hosted cloud stable and honest:
- maintain HTTP and WebSocket compatibility for startup and turn handling
- keep the .NET cloud as the production track
- keep Node as the reverse-engineering oracle and fixture source
- continue update, backup, restore, media, STT, and live-capture proof
- keep the real-device bootstrap path documented and repeatable
Exit criteria:
- a real Jibo can reach the hosted cloud consistently
- the cloud can carry the startup and conversation flows needed for daily use
- update and recovery behavior is understood well enough to trust the next layer
## Phase 2: OTA-Assisted Recovery
Once the hosted cloud is solid, OTA becomes the simplification layer.
This phase should:
- move software updates and recovery flows into a reliable hosted path
- reduce how often owners need manual RCM or network patching
- make device recovery and version management feel like a product instead of a lab exercise
- keep rollback and failure handling explicit
OTA is the path that makes ownership easier. It is not the thing that must be solved before the cloud can live.
## Phase 3: Open Jibo OS / Mode Conversion
After cloud and OTA are trustworthy, the project can move from "open cloud" to "open platform."
The goal is not to erase stock Jibo. The goal is to give owners an Open Jibo mode that:
- preserves the original Jibo feel and skill surface
- can be installed or selected without a one-way trap
- can fall back to stock behavior when needed
- makes future features easier to ship on top of a known runtime
This is where the breadcrumbs in the repo become important:
- `open-jibo` and `open-jibo-ai` modes
- a startup migration skill that can invite existing owners to convert
- a reversible path back to stock
- the hosted sites and support docs on `openjibo.com` and `openjibo.ai` that explain the transition clearly
## Phase 4: Tiered Brain
A single monolithic "AI brain" is not the best fit for Jibo. A tiered model is better.
Suggested tiers:
- Tier 0: original Jibo reflexes, stock skills, and local charm
- Tier 1: hosted cloud routing and compatibility
- Tier 2: memory, personality, and proactivity
- Tier 3: richer reasoning and multi-step planning
- Tier 4: external agent orchestration and task delegation
- Tier 5: multi-device and household coordination
The point of the tiers is not to make Jibo feel bigger at every turn. It is to keep simple interactions fast and charming while reserving more complex work for the layers that can actually support it.
## CoffeeBreak (`coffeebreakai.dev`) As An Orchestration Layer
CoffeeBreak fits naturally above the tiered brain as a coordination plane.
The intended relationship is:
- Jibo keeps the voice, personality, and local interaction style
- OpenJibo routes simple and medium-complexity tasks itself
- CoffeeBreak can take over when a task needs multiple tools, agents, or steps
- the result comes back to Jibo in a form that still feels native to him
That makes CoffeeBreak a close cousin to the tiered brain rather than a separate product line. The brain decides, CoffeeBreak orchestrates, and Jibo remains the face of the interaction.
## Phase 5: Ecosystem Expansion
After the core platform is stable, OpenJibo can grow into broader household value:
- calendar and scheduling
- smart home and Home Assistant style control
- shopping lists and household memory
- multi-user and family recognition
- richer media and content experiences
- provider-backed news, weather, and personal report flows
- eventual multi-Jibo interaction
## What We Must Preserve
No matter how far the platform grows, these should stay true:
- original skills should still feel like Jibo
- design should stay recognizable, not generic
- migration should be opt-in and reversible whenever possible
- the cloud should serve the robot, not replace his identity
- technical modernization should preserve charm instead of sanding it off
## Where To Go Next
If you want the current execution truth, read:
- [Development plan](development-plan.md)
- [Feature backlog](feature-backlog.md)
- [Release 1.0.19 plan](release-1.0.19-plan.md)
If you want the first-device path, read:
- [Device bootstrap path](device-bootstrap.md)
- [Support tiers](support-tiers.md)
- [Public site plan](public-site-plan.md)

View File

@@ -0,0 +1,151 @@
# System Diagram Alignment
## Purpose
This document maps the legacy Pegasus/Jibo cloud `system_diagram.png` architecture to the current OpenJibo `1.0.19` cloud.
Use it to keep release planning grounded in three views:
- where we were (legacy design intent)
- where we are (current hosted `.NET` implementation)
- where we are headed (next architecture slices)
As-of date: `2026-05-07`
## Diagram Inputs
- Legacy system architecture: `C:\Projects\jibo\pegasus\resources\system_diagram.png`
- Legacy generic skill scaffold: `C:\Projects\jibo\pegasus\packages\template-skill\docs\TemplateSkill.png`
- Legacy listener state machine: `C:\Projects\jibo\sdk\packages\skills-service-manager\resources\state-diagrams\glsm.png`
## Template Skill Verdict
The template-skill diagram is a generic scaffold, not a production behavior contract.
Evidence:
- `C:\Projects\jibo\pegasus\packages\template-skill\src\TemplateSkill.ts` is a starter graph (`Intent Split` -> `Do MIM` -> `Complete` -> `Done`).
- `C:\Projects\jibo\pegasus\packages\template-skill\src\nodes\MemoSplitNode.ts` uses placeholder memo validation (`SomeThing`).
Conclusion: do not treat template-skill flow as a port target. Treat it as a shape reference only.
## System Diagram Mapping
| Legacy block | OpenJibo `1.0.19` equivalent | Current gap / opportunity |
| --- | --- | --- |
| `Auth` | [JiboCloudProtocolService.cs](../src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboCloudProtocolService.cs) (`CreateHubToken`, `CreateAccessToken`, account handlers) | move from in-memory/session stubs to durable tenant/account identity services |
| `Loop` | [JiboCloudProtocolService.cs](../src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboCloudProtocolService.cs) (`HandleLoop`) + [InMemoryCloudStateStore.cs](../src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Persistence/InMemoryCloudStateStore.cs) | richer loop/member lifecycle and onboarding flows |
| `Hub` | [JiboWebSocketService.cs](../src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboWebSocketService.cs) + [WebSocketTurnFinalizationService.cs](../src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/WebSocketTurnFinalizationService.cs) | split hub responsibilities into clearer protocol, routing, and orchestration boundaries |
| `ASR Handler` | STT strategy selection in [WebSocketTurnFinalizationService.cs](../src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/WebSocketTurnFinalizationService.cs) + DI in [ServiceCollectionExtensions.cs](../src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/DependencyInjection/ServiceCollectionExtensions.cs) | short-turn reliability, managed STT comparison, and better low-signal/noise handling |
| `Parser / Robust Parser` | rule-based intent resolution in [JiboInteractionService.cs](../src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs) + focused state machines (personal report/chitchat) | deeper phrase import from Pegasus intents/entities plus ambiguity guardrails |
| `Skill Router` | [JiboInteractionService.cs](../src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs) decision switch and local skill payload shaping | external skill routing config and safer declarative intent mapping |
| `Proactivity Selector` | weighted candidate selection in [JiboInteractionService.cs](../src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboInteractionService.cs) + pending-offer session state in [WebSocketTurnFinalizationService.cs](../src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/WebSocketTurnFinalizationService.cs) | externalized proactivity catalog, cooldown policy, and broader category coverage |
| `Presence / Identity Context` | runtime context passthrough in [ProtocolToTurnContextMapper.cs](../src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/ProtocolToTurnContextMapper.cs) and turn metadata handling in [WebSocketTurnFinalizationService.cs](../src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/WebSocketTurnFinalizationService.cs) | normalize `runtime.perception` fields (`speaker`, `peoplePresent`, focused person) for greeting/proactivity policy decisions |
| `Skill Registry` | implicit in current code/routing | formal registry abstraction for local/cloud capabilities and manifest metadata |
| `History` | tenant-scoped memory store in [InMemoryPersonalMemoryStore.cs](../src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Persistence/InMemoryPersonalMemoryStore.cs) | durable multi-tenant persistence and history timeline/query support |
| `Lasso` provider aggregation | partial provider integration via weather provider wiring in [ServiceCollectionExtensions.cs](../src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/DependencyInjection/ServiceCollectionExtensions.cs) | full aggregation service for weather/news/calendar/knowledge inputs |
| `Proactivity Catalog` | in-code candidate lists/weights | explicit catalog service with tuned weights and operator controls |
| `Audio Logs` | file telemetry sinks in infrastructure telemetry | hosted indexed capture/retention for multi-operator analysis |
## GLSM Listener Flow Alignment (`2026-05-06`)
Captured source:
- `C:\Projects\jibo\sdk\packages\skills-service-manager\resources\state-diagrams\glsm.png`
First OpenJibo support slice (implemented):
- explicit derived listener phases are now emitted in cloud diagnostics:
- `HJ_LISTENING`
- `LISTENING`
- `WAIT_LISTEN_FINISHED`
- `DISPATCH_DIALOG`
- `PROCESS_LISTENER_QUEUE`
- turn telemetry now records `glsm_phase_transition` with previous/next state and trigger
- websocket telemetry now includes `glsmPhase` on binary, context, and turn-processed events
- stale pending-listen recovery is now implemented:
- when a pending `LISTEN` stays open long enough with no context/audio, a new hotphrase listen can recover the stuck state before continuing
Current parity boundary:
- this slice focuses on listener lifecycle observability plus stuck-listen recovery
- deeper explicit parity states from GLSM (`Interrupt Listeners`, `Handle Launch Parse`, `Handle Global Parse`, `Dispatch Dialog` sub-branches) are next candidates once this capture-driven slice is validated live
## Where We Were
Legacy cloud design was service-oriented around:
- hub orchestration
- parser robustness
- skill routing
- proactivity selection
- history/memory and provider aggregation
It emphasized a personality-rich surface while still being operationally observable.
## Where We Are
OpenJibo `1.0.19` is a functional hosted `.NET` modular monolith with:
- protocol compatibility paths for HTTP and websocket robot flows
- deterministic intent routing plus state-machine slices
- tenant-scoped memory foundation
- first proactivity baseline
- first external weather provider integration
This is the right shape for rapid parity plus safe incremental growth.
## Where We Are Headed
Near-term architecture evolution should preserve current shipping velocity:
1. Expand parser coverage and ambiguity guardrails from Pegasus phrase corpora.
2. Externalize proactivity policy and category catalogs.
3. Move memory from in-memory to durable multi-tenant backing stores.
4. Add stronger observability around STT, parser decisions, and follow-up turn state.
5. Build a focused aggregation layer (Lasso-like) for multi-provider content.
## Charm Preservation Rules
To keep Jibo's charm while modernizing the platform:
- keep MIM/ESML and expressive animation hooks as first-class outputs
- keep deterministic command-vs-question behavior for personality reliability
- layer richer provider data behind stable personality and gesture patterns
- prefer small source-backed slices over broad rewrites
## Queued Next `1.0.19` Task
The next queued implementation task is:
- `Dialog parsing expansion and ambiguity guardrails`
Tracking anchors:
- [release-1.0.19-plan.md](release-1.0.19-plan.md)
- [feature-backlog.md](feature-backlog.md)
Primary objective:
- import Pegasus parser intent phrases/entities to improve intent confidence while preserving command-vs-question personality behavior.
## Greetings And Presence Track (`2026-05-07`)
A dedicated presence-aware greetings plan is now captured for the next personality slice, grounded in Pegasus `@be/greetings` state, identity, and proactive policy behavior.
Reference:
- [greetings-presence-plan.md](greetings-presence-plan.md)
## Personal Report Parity Track (`2026-05-07`)
Personal report parity planning is now captured with a source-anchored implementation sequence for:
- weather visual/personality parity
- live news provider path
- commute provider path
- calendar/report coverage matrix
Reference:
- [personal-report-parity-plan.md](personal-report-parity-plan.md)

View File

@@ -6,7 +6,7 @@
This is the production-oriented path for restoring device connectivity and creating a foundation for future runtime, AI, and OTA work.
Current spoken cloud version: `Open Jibo Cloud version 1.0.18.`
Current spoken cloud version: `Cloud version 1.0.19.`
Release hygiene reminder:

View File

@@ -220,7 +220,7 @@ static string ResolveSocketKind(string host, PathString path)
return "openjibo";
}
return "unknown";
return "neo-hub-listen"; // now it assumes all unknown requests are neo-hub. I did this so that people with custom listen servers (like myself) won't get a bunch of 404 messages when doing a HJ request. -ZaneDev (an awful programmer)
}
static string? ResolveToken(HttpRequest request)

View File

@@ -21,6 +21,30 @@
"WhisperLanguage": "en",
"TempDirectory": "/tmp/openjibo-stt",
"CleanupTempFiles": false
},
"Weather": {
"OpenWeather": {
"BaseUrl": "https://api.openweathermap.org",
"ApiKey": "723667c9ab0318142227c5389900d087",
"DefaultLocation": "Boston,US",
"UseCelsius": false,
"CurrentCacheTtlSeconds": 120,
"ForecastCacheTtlSeconds": 600,
"GeocodeCacheTtlSeconds": 21600,
"FailureCacheTtlSeconds": 45
}
},
"News": {
"NewsApi": {
"BaseUrl": "https://newsapi.org",
"ApiKey": "5df93a83db9c4c6888f3e06c4a53144f",
"Country": "us",
"Language": "en",
"FallbackQuery": "robotics OR technology OR science",
"DefaultCategories": [ "general", "technology", "sports", "business" ],
"CacheTtlSeconds": 300,
"FailureCacheTtlSeconds": 45
}
}
}
}

View File

@@ -13,6 +13,7 @@ public interface ICloudStateStore
CloudSession OpenSession(string kind, string? deviceId, string? token, string? hostName, string? path);
CloudSession? FindSessionByToken(string token);
IReadOnlyList<LoopRecord> GetLoops();
IReadOnlyList<PersonRecord> GetPeople();
IReadOnlyList<UpdateManifest> ListUpdates(string? subsystem = null, string? filter = null);
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);

View File

@@ -5,12 +5,21 @@ public interface IJiboExperienceContentRepository
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 IReadOnlyList<string> Jokes { get; init; } = [];
public IReadOnlyList<string> DanceAnimations { get; init; } = [];
public IReadOnlyList<string> GreetingReplies { get; init; } = [];
public IReadOnlyList<string> HowAreYouReplies { get; init; } = [];
public IReadOnlyList<JiboConditionedReply> EmotionReplies { get; init; } = [];
public IReadOnlyList<string> PersonalityReplies { get; init; } = [];
public IReadOnlyList<string> PizzaReplies { get; init; } = [];
public IReadOnlyList<string> SurpriseReplies { get; init; } = [];
public IReadOnlyList<string> PersonalReportReplies { get; init; } = [];
public IReadOnlyList<string> WeatherReplies { get; init; } = [];
@@ -20,4 +29,5 @@ public sealed class JiboExperienceCatalog
public IReadOnlyList<string> NewsBriefings { get; init; } = [];
public IReadOnlyList<string> GenericFallbackReplies { get; init; } = [];
public IReadOnlyList<string> DanceReplies { get; init; } = [];
public IReadOnlyList<string> DanceQuestionReplies { get; init; } = [];
}

View File

@@ -0,0 +1,28 @@
namespace Jibo.Cloud.Application.Abstractions;
public interface INewsBriefingProvider
{
Task<NewsBriefingSnapshot?> GetBriefingAsync(
NewsBriefingRequest request,
CancellationToken cancellationToken = default);
}
public sealed record NewsBriefingRequest(
IReadOnlyList<string> PreferredCategories,
int MaxHeadlines = 3);
public sealed record NewsHeadline(
string Title,
string? Summary = null,
string? Category = null,
string? SourceName = null,
string? Url = null);
public sealed record NewsBriefingSnapshot(
IReadOnlyList<NewsHeadline> Headlines,
string? SourceName = null,
string? ProviderStatus = null,
string? ProviderMessage = null,
int? ProviderHttpStatusCode = null,
string? ProviderEndpoint = null,
string? ProviderErrorCode = null);

View File

@@ -0,0 +1,28 @@
namespace Jibo.Cloud.Application.Abstractions;
public interface IPersonalMemoryStore
{
void SetBirthday(PersonalMemoryTenantScope tenantScope, string birthdayText);
string? GetBirthday(PersonalMemoryTenantScope tenantScope);
void SetPreference(PersonalMemoryTenantScope tenantScope, string category, string value);
string? GetPreference(PersonalMemoryTenantScope tenantScope, string category);
void SetName(PersonalMemoryTenantScope tenantScope, string name);
string? GetName(PersonalMemoryTenantScope tenantScope);
void SetImportantDate(PersonalMemoryTenantScope tenantScope, string label, string value);
string? GetImportantDate(PersonalMemoryTenantScope tenantScope, string label);
void SetAffinity(PersonalMemoryTenantScope tenantScope, string item, PersonalAffinity affinity);
PersonalAffinity? GetAffinity(PersonalMemoryTenantScope tenantScope, string item);
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, string? PersonId = null);
public enum PersonalAffinity
{
Like,
Love,
Dislike
}

View File

@@ -0,0 +1,25 @@
namespace Jibo.Cloud.Application.Abstractions;
public interface IWeatherReportProvider
{
Task<WeatherReportSnapshot?> GetReportAsync(
WeatherReportRequest request,
CancellationToken cancellationToken = default);
}
public sealed record WeatherReportRequest(
string? LocationQuery,
double? Latitude,
double? Longitude,
bool IsTomorrow,
bool? UseCelsius,
int? ForecastDayOffset = null);
public sealed record WeatherReportSnapshot(
string LocationName,
string Summary,
int Temperature,
int? HighTemperature,
int? LowTemperature,
string? Condition,
bool UseCelsius);

View File

@@ -0,0 +1,642 @@
using Jibo.Cloud.Application.Abstractions;
using System.Text.RegularExpressions;
namespace Jibo.Cloud.Application.Services;
internal static class ChitchatStateMachine
{
internal const string StateMetadataKey = "chitchatState";
internal const string RouteMetadataKey = "chitchatRoute";
internal const string EmotionMetadataKey = "chitchatEmotion";
internal const string IdleState = "idle";
private const string IntentSplitState = "intent_split";
private const string ProcessQueryState = "process_query";
private const string CompleteState = "complete";
private const string ScriptedResponseRoute = "ScriptedResponse";
private const string EmotionQueryRoute = "EmotionQuery";
private const string EmotionCommandRoute = "EmotionCommand";
private const string ErrorResponseRoute = "ErrorResponse";
private static readonly string[] EmotionQueryPhrases =
[
"how are you feeling",
"how do you feel",
"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 is your mood",
"what's your mood",
"do you have emotions",
"are you happy",
"are you sad",
"are you angry",
"how angry are you",
"how jealous are you",
"how sad are you",
"how upset do you feel",
"how bored are you right now"
];
// Pegasus parser-derived query anchors from descriptor/emotion intent families.
private static readonly string[] EmotionQueryPrefixes =
[
"are you ",
"are you feeling ",
"are you able to feel ",
"are you able to get ",
"are you ever ",
"can you be ",
"do you feel ",
"do you ever feel ",
"do you ever get ",
"do you get ",
"does ",
"would ",
"how ",
"describe how "
];
// Pegasus parser-derived specific-emotion assertion forms.
private static readonly string[] EmotionAssertionPrefixes =
[
"you are ",
"you re ",
"you are acting ",
"you seem ",
"you look ",
"i think you are ",
"i think you re ",
"i feel like you are ",
"i feel like you re ",
"in my opinion you are ",
"in my opinion you re "
];
private static readonly string[] EmotionCommandPositivePrefixes =
[
"be ",
"be a little ",
"be a bit ",
"be very ",
"be more ",
"you should be ",
"you should try to be ",
"try to be ",
"look ",
"act "
];
private static readonly string[] EmotionCommandNegativePrefixes =
[
"do not be ",
"don t be ",
"dont be ",
"try not to be ",
"you should not be ",
"you shouldn t be "
];
private static readonly (string Phrase, string Emotion)[] DirectEmotionCommandPhrases =
[
("smile", "happy"),
("look happy", "happy"),
("cheer up", "happy"),
("be happy", "happy"),
("be excited", "excited"),
("get excited", "excited"),
("act excited", "excited"),
("be sad", "sad"),
("look sad", "sad"),
("be calm", "calm"),
("calm down", "calm"),
("relax", "calm")
];
// Derived from Pegasus parser Emotion entity and utterance sets.
private static readonly (string Emotion, string[] Synonyms)[] PegasusEmotionSynonyms =
[
("afraid", ["afraid", "fearful", "frightened", "scared", "terrified", "spooked", "freak out", "freaked out"]),
("amused", ["amused", "entertained", "tickled", "tickled pink"]),
("angry", ["angry", "mad", "furious", "enraged", "irate", "incensed", "cross"]),
("annoyed", ["annoyed", "aggravated", "bothered", "irritated", "grumpy", "nettled", "vexed", "bored"]),
("anxious", ["anxious", "nervous", "worried", "tense", "on edge", "jittery", "restless", "concerned"]),
("confident", ["confident", "assured", "secure", "self assured", "self confident"]),
("confused", ["confused", "at a loss", "perplexed", "puzzled", "stumped", "uncertain", "unsure"]),
("embarrassed", ["embarrassed", "ashamed", "flustered", "self conscious", "sheepish"]),
("excited", ["excited", "jazzed", "psyched", "pumped"]),
("happy", ["happy", "cheerful", "jovial", "pleased", "joyful", "content", "thrilled"]),
("jealous", ["jealous", "envious", "covetous"]),
("lonely", ["lonely", "alone", "lonesome"]),
("proud", ["proud", "honored"]),
("sad", ["sad", "upset", "unhappy", "depressed", "somber", "downcast", "gloomy", "miserable", "bummed", "heartbroken", "troubled"])
];
private static readonly string[] EmotionCommandReplies =
[
"I can do that mood. Watch this.",
"Switching mood now.",
"Okay, mood change activated."
];
private static readonly Regex PhrasePunctuationPattern = new(
@"[^\w\s]",
RegexOptions.CultureInvariant | RegexOptions.Compiled);
private static readonly Regex PhraseWhitespacePattern = new(
@"\s+",
RegexOptions.CultureInvariant | RegexOptions.Compiled);
private static readonly (string Phrase, string Emotion)[] EmotionSynonymMappings = BuildEmotionSynonymMappings();
public static JiboInteractionDecision? TryBuildDecision(
string semanticIntent,
string transcript,
string loweredTranscript,
JiboExperienceCatalog catalog,
IJiboRandomizer randomizer,
string? currentEmotion,
Func<string> buildErrorResponse)
{
var normalizedLoweredTranscript = NormalizeForPhraseMatching(loweredTranscript);
switch (semanticIntent)
{
case "hello":
return BuildScriptedResponseDecision(
"hello",
randomizer.Choose(catalog.GreetingReplies));
case "robot_personality":
return BuildScriptedResponseDecision(
"robot_personality",
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":
return BuildEmotionQueryDecision(
"how_are_you",
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",
"Blue.");
case "robot_favorite_food":
return BuildScriptedResponseDecision(
"robot_favorite_food",
"Pizza. It is hard to argue with pizza.");
case "robot_favorite_music":
return BuildScriptedResponseDecision(
"robot_favorite_music",
"Something upbeat with a good rhythm.");
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":
if (IsEmotionQuery(normalizedLoweredTranscript))
{
return BuildEmotionQueryDecision(
"emotion_query",
SelectEmotionQueryReply(catalog, randomizer, currentEmotion));
}
if (TryResolveEmotionCommand(normalizedLoweredTranscript, out var emotion))
{
return BuildEmotionCommandDecision(randomizer, emotion!);
}
return BuildErrorResponseDecision(
"chat",
buildErrorResponse(),
transcript);
default:
return null;
}
}
public static bool IsLikelyEmotionUtterance(string transcript)
{
var normalizedLoweredTranscript = NormalizeForPhraseMatching(transcript);
return IsEmotionQuery(normalizedLoweredTranscript) ||
TryResolveEmotionCommand(normalizedLoweredTranscript, out _);
}
private static JiboInteractionDecision BuildScriptedResponseDecision(string intentName, string replyText)
{
return new JiboInteractionDecision(
intentName,
replyText,
ContextUpdates: BuildContextUpdates(
ScriptedResponseRoute,
emotion: null));
}
private static JiboInteractionDecision BuildEmotionQueryDecision(string intentName, string replyText)
{
return new JiboInteractionDecision(
intentName,
replyText,
ContextUpdates: BuildContextUpdates(
EmotionQueryRoute,
emotion: null));
}
private static JiboInteractionDecision BuildEmotionCommandDecision(IJiboRandomizer randomizer, string emotion)
{
var (esmlEmotion, responseSuffix) = emotion switch
{
"happy" => ("happy", "I am feeling happy."),
"sad" => ("sad", "I can do a thoughtful mood too."),
"excited" => ("happy", "I am feeling excited."),
"calm" => ("neutral", "I am in a calmer mood."),
_ => ("neutral", "Mood updated.")
};
return new JiboInteractionDecision(
"emotion_command",
randomizer.Choose(EmotionCommandReplies),
"chitchat-skill",
new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
["esml"] = $"<speak><es cat='{esmlEmotion}' filter='!ssa-only, !sfx-only' endNeutral='true'>{responseSuffix}</es></speak>",
["mim_id"] = "runtime-chat",
["mim_type"] = "announcement",
["prompt_id"] = "RUNTIME_EMOTION_COMMAND",
["prompt_sub_category"] = "AN"
},
ContextUpdates: BuildContextUpdates(
EmotionCommandRoute,
emotion));
}
private static JiboInteractionDecision BuildErrorResponseDecision(string intentName, string replyText, string transcript)
{
var normalizedTranscript = string.IsNullOrWhiteSpace(transcript)
? string.Empty
: transcript.Trim();
return new JiboInteractionDecision(
intentName,
replyText,
ContextUpdates: BuildContextUpdates(
ErrorResponseRoute,
emotion: null,
rawTranscript: normalizedTranscript));
}
private static IDictionary<string, object?> BuildContextUpdates(
string route,
string? emotion,
string? rawTranscript = null)
{
return new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
[StateMetadataKey] = CompleteState,
[RouteMetadataKey] = route,
[EmotionMetadataKey] = emotion ?? string.Empty,
["chitchatLastState"] = IntentSplitState,
["chitchatProcessState"] = ProcessQueryState,
["chitchatRawTranscript"] = rawTranscript ?? string.Empty
};
}
private static string SelectEmotionQueryReply(
JiboExperienceCatalog catalog,
IJiboRandomizer randomizer,
string? currentEmotion)
{
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
{
"HAPPY" => ["JOYFUL", "PLEASED", "CONFIDENT", "DETERMINED", "HAPPY"],
"SAD" => ["INSECURE", "SAD"],
"CALM" => ["NEUTRAL", "INSECURE", "CALM"],
"NEUTRAL" => ["NEUTRAL"],
"JOYFUL" or "PLEASED" or "CONFIDENT" or "DETERMINED" or "INSECURE" => [normalizedEmotion],
_ => [normalizedEmotion]
};
}
private static string SelectLegacyPersonalityReply(
JiboExperienceCatalog catalog,
IJiboRandomizer randomizer,
params string[] preferredSnippets)
{
foreach (var snippet in preferredSnippets)
{
if (string.IsNullOrWhiteSpace(snippet))
{
continue;
}
var match = catalog.PersonalityReplies.FirstOrDefault(reply =>
reply.Contains(snippet, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrWhiteSpace(match))
{
return match;
}
}
return randomizer.Choose(catalog.PersonalityReplies);
}
private static string NormalizeCondition(string? condition)
{
if (string.IsNullOrWhiteSpace(condition))
{
return string.Empty;
}
return 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) ||
StartsWithAnyPhrase(loweredTranscript, EmotionAssertionPrefixes);
}
private static bool TryResolveEmotionCommand(string loweredTranscript, out string? emotion)
{
emotion = null;
foreach (var mapping in DirectEmotionCommandPhrases)
{
if (!ContainsPhrase(loweredTranscript, mapping.Phrase))
{
continue;
}
emotion = mapping.Emotion;
return true;
}
var isNegativeCommand = StartsWithAnyPhrase(loweredTranscript, EmotionCommandNegativePrefixes);
var isPositiveCommand = !isNegativeCommand && StartsWithAnyPhrase(loweredTranscript, EmotionCommandPositivePrefixes);
if (!isNegativeCommand && !isPositiveCommand)
{
return false;
}
if (!TryResolveEmotionFromText(loweredTranscript, out var canonicalEmotion) ||
string.IsNullOrWhiteSpace(canonicalEmotion))
{
return false;
}
emotion = isNegativeCommand
? "calm"
: MapCanonicalEmotionToRuntimeEmotion(canonicalEmotion);
return true;
}
private static string MapCanonicalEmotionToRuntimeEmotion(string canonicalEmotion)
{
return canonicalEmotion switch
{
"happy" or "amused" or "excited" or "confident" or "proud" => "happy",
"sad" or "lonely" or "afraid" or "anxious" or "embarrassed" or "confused" => "sad",
"angry" or "annoyed" or "jealous" => "calm",
_ => "calm"
};
}
private static bool TryResolveEmotionFromText(string loweredTranscript, out string? emotion)
{
emotion = null;
foreach (var mapping in EmotionSynonymMappings)
{
if (!ContainsPhrase(loweredTranscript, mapping.Phrase))
{
continue;
}
emotion = mapping.Emotion;
return true;
}
return false;
}
private static bool ContainsAnyPhrase(string loweredTranscript, IEnumerable<string> phrases)
{
foreach (var phrase in phrases)
{
if (ContainsPhrase(loweredTranscript, phrase))
{
return true;
}
}
return false;
}
private static bool StartsWithAnyPhrase(string loweredTranscript, IEnumerable<string> phrases)
{
foreach (var phrase in phrases)
{
var normalizedPhrase = NormalizeForPhraseMatching(phrase);
if (string.IsNullOrWhiteSpace(normalizedPhrase))
{
continue;
}
if (string.Equals(loweredTranscript, normalizedPhrase, StringComparison.Ordinal) ||
loweredTranscript.StartsWith($"{normalizedPhrase} ", StringComparison.Ordinal))
{
return true;
}
}
return false;
}
private static bool ContainsPhrase(string loweredTranscript, string phrase)
{
var normalizedPhrase = NormalizeForPhraseMatching(phrase);
if (string.IsNullOrWhiteSpace(normalizedPhrase) ||
string.IsNullOrWhiteSpace(loweredTranscript))
{
return false;
}
return string.Equals(loweredTranscript, normalizedPhrase, StringComparison.Ordinal) ||
loweredTranscript.StartsWith($"{normalizedPhrase} ", StringComparison.Ordinal) ||
loweredTranscript.Contains($" {normalizedPhrase} ", StringComparison.Ordinal) ||
loweredTranscript.EndsWith($" {normalizedPhrase}", StringComparison.Ordinal);
}
private static string NormalizeForPhraseMatching(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return string.Empty;
}
var lowered = value.ToLowerInvariant();
var withoutPunctuation = PhrasePunctuationPattern.Replace(lowered, " ");
return PhraseWhitespacePattern.Replace(withoutPunctuation, " ").Trim();
}
private static (string Phrase, string Emotion)[] BuildEmotionSynonymMappings()
{
var seen = new HashSet<string>(StringComparer.Ordinal);
var mappings = new List<(string Phrase, string Emotion)>();
foreach (var emotionMapping in PegasusEmotionSynonyms)
{
foreach (var synonym in emotionMapping.Synonyms)
{
var normalizedSynonym = NormalizeForPhraseMatching(synonym);
if (string.IsNullOrWhiteSpace(normalizedSynonym) ||
!seen.Add(normalizedSynonym))
{
continue;
}
mappings.Add((normalizedSynonym, emotionMapping.Emotion));
}
}
mappings.Sort(static (left, right) => right.Phrase.Length.CompareTo(left.Phrase.Length));
return [.. mappings];
}
}

View File

@@ -4,6 +4,8 @@ namespace Jibo.Cloud.Application.Services;
public sealed class DemoConversationBroker(JiboInteractionService interactionService) : IConversationBroker
{
private readonly TimeSpan _followUpTimeout = TimeSpan.FromSeconds(6);
public async Task<ResponsePlan> HandleTurnAsync(TurnContext turn, CancellationToken cancellationToken = default)
{
var decision = await interactionService.BuildDecisionAsync(turn, cancellationToken);
@@ -31,10 +33,13 @@ public sealed class DemoConversationBroker(JiboInteractionService interactionSer
? new FollowUpPolicy
{
KeepMicOpen = true,
Timeout = TimeSpan.FromSeconds(12),
Timeout = _followUpTimeout,
ExpectedTopic = "conversation"
}
: FollowUpPolicy.None,
ContextUpdates = decision.ContextUpdates is not null
? new Dictionary<string, object?>(decision.ContextUpdates, StringComparer.OrdinalIgnoreCase)
: new Dictionary<string, object?>(),
ProtocolMetadata = new Dictionary<string, object?>
{
["host"] = turn.HostName,
@@ -48,7 +53,7 @@ public sealed class DemoConversationBroker(JiboInteractionService interactionSer
plan.Actions.Add(new ListenAction
{
Sequence = 1,
Timeout = TimeSpan.FromSeconds(12),
Timeout = _followUpTimeout,
Mode = "follow-up"
});
}
@@ -71,6 +76,16 @@ public sealed class DemoConversationBroker(JiboInteractionService interactionSer
return intentName switch
{
"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_guess" => false,
"radio" => false,
@@ -97,6 +112,8 @@ public sealed class DemoConversationBroker(JiboInteractionService interactionSer
"snapshot" => false,
"photobooth" => false,
"news" => false,
"trigger_ignored" => false,
"proactive_greeting" => false,
_ => true
};
}

View File

@@ -0,0 +1,299 @@
using Jibo.Cloud.Application.Abstractions;
using Jibo.Runtime.Abstractions;
using System.Linq;
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";
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;
}
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"
];
}

View File

@@ -25,7 +25,8 @@ public sealed class JiboWebSocketService(
var replies = await turnFinalizationService.HandleBinaryAudioAsync(session, envelope, cancellationToken);
await telemetrySink.RecordTurnEventAsync(envelope, session, "binary_audio_received", new Dictionary<string, object?>
{
["bytes"] = envelope.Binary?.Length ?? 0
["bytes"] = envelope.Binary?.Length ?? 0,
["glsmPhase"] = WebSocketTurnFinalizationService.ResolveGlsmPhase(session)
}, cancellationToken);
return replies;
}
@@ -33,16 +34,43 @@ public sealed class JiboWebSocketService(
var parsedType = ReadMessageType(envelope.Text);
session.LastMessageType = parsedType;
var containsInlineTurnPayload = parsedType == "LISTEN" && ContainsInlineTurnPayload(envelope.Text);
var staleListenRecovered = false;
var staleListenAgeMs = 0;
if (parsedType == "LISTEN" &&
!containsInlineTurnPayload &&
WebSocketTurnFinalizationService.ShouldIgnoreLateListenSetup(session, envelope.Text))
{
var (lateTransId, lateRules) = ResolveLateListenNoInputPayload(session, envelope.Text);
var replies = ResponsePlanToSocketMessagesMapper
.MapNoInputAndRedirectToSkill(lateTransId, lateRules, "@be/idle")
.Select(map => new WebSocketReply
{
Text = map.Text,
DelayMs = map.DelayMs
})
.ToArray();
await telemetrySink.RecordTurnEventAsync(envelope, session, "late_listen_ignored", new Dictionary<string, object?>
{
["messageType"] = parsedType,
["activeTransID"] = session.TurnState.TransId
["activeTransID"] = session.TurnState.TransId,
["ignoredTransID"] = lateTransId,
["replyCount"] = replies.Length
}, cancellationToken);
return replies;
}
if (parsedType == "LISTEN" &&
!containsInlineTurnPayload &&
WebSocketTurnFinalizationService.TryRecoverStalePendingListen(session, out staleListenAgeMs))
{
staleListenRecovered = true;
await telemetrySink.RecordTurnEventAsync(envelope, session, "glsm_stale_listen_recovered", new Dictionary<string, object?>
{
["staleAgeMs"] = staleListenAgeMs,
["transID"] = session.TurnState.TransId,
["glsmPhase"] = WebSocketTurnFinalizationService.ResolveGlsmPhase(session)
}, cancellationToken);
return [];
}
WebSocketTurnFinalizationService.ObserveIncomingMessage(session, envelope.Text);
@@ -54,7 +82,8 @@ public sealed class JiboWebSocketService(
var replies = await turnFinalizationService.HandleContextAsync(session, envelope, cancellationToken);
await telemetrySink.RecordTurnEventAsync(envelope, session, "context_received", new Dictionary<string, object?>
{
["transID"] = session.TurnState.TransId
["transID"] = session.TurnState.TransId,
["glsmPhase"] = WebSocketTurnFinalizationService.ResolveGlsmPhase(session)
}, cancellationToken);
return replies;
}
@@ -68,11 +97,14 @@ public sealed class JiboWebSocketService(
["messageType"] = parsedType,
["replyCount"] = replies.Count,
["transcript"] = session.LastTranscript,
["intent"] = session.LastIntent
["intent"] = session.LastIntent,
["glsmPhase"] = WebSocketTurnFinalizationService.ResolveGlsmPhase(session),
["staleListenRecovered"] = staleListenRecovered,
["staleListenAgeMs"] = staleListenAgeMs
}, cancellationToken);
return replies;
}
case "CLIENT_NLU" or "CLIENT_ASR":
case "CLIENT_NLU" or "CLIENT_ASR" or "TRIGGER":
{
var replies = await turnFinalizationService.HandleTurnAsync(session, envelope, parsedType, cancellationToken);
await telemetrySink.RecordTurnEventAsync(envelope, session, "turn_processed", new Dictionary<string, object?>
@@ -80,7 +112,8 @@ public sealed class JiboWebSocketService(
["messageType"] = parsedType,
["replyCount"] = replies.Count,
["transcript"] = session.LastTranscript,
["intent"] = session.LastIntent
["intent"] = session.LastIntent,
["glsmPhase"] = WebSocketTurnFinalizationService.ResolveGlsmPhase(session)
}, cancellationToken);
return replies;
}
@@ -145,4 +178,53 @@ public sealed class JiboWebSocketService(
return false;
}
}
private static (string TransId, IReadOnlyList<string> Rules) ResolveLateListenNoInputPayload(
CloudSession session,
string? text)
{
var transId = session.TurnState.TransId ?? session.LastTransId ?? string.Empty;
var rules = session.TurnState.ListenRules;
if (string.IsNullOrWhiteSpace(text))
{
return (transId, rules);
}
try
{
using var document = JsonDocument.Parse(text);
var root = document.RootElement;
if (root.TryGetProperty("transID", out var transIdValue) &&
transIdValue.ValueKind == JsonValueKind.String &&
!string.IsNullOrWhiteSpace(transIdValue.GetString()))
{
transId = transIdValue.GetString()!;
}
if (root.TryGetProperty("data", out var data) &&
data.ValueKind == JsonValueKind.Object &&
data.TryGetProperty("rules", out var ruleValues) &&
ruleValues.ValueKind == JsonValueKind.Array)
{
var parsedRules = ruleValues.EnumerateArray()
.Where(static item => item.ValueKind == JsonValueKind.String)
.Select(static item => item.GetString() ?? string.Empty)
.Where(static rule => !string.IsNullOrWhiteSpace(rule))
.ToArray();
if (parsedRules.Length > 0)
{
rules = parsedRules;
}
}
}
catch
{
// Best effort parsing for late-listen cleanup.
}
return (transId, rules);
}
}

View File

@@ -1,10 +1,14 @@
using System.Globalization;
namespace Jibo.Cloud.Application.Services;
public static class OpenJiboCloudBuildInfo
{
public const string Version = "1.0.18";
public const string Version = "1.0.19";
public static readonly DateOnly PersonaBirthday = new(2026, 3, 22);
public static string VersionWords => Version.Replace(".", " dot ");
public static string PersonaBirthdayWords => PersonaBirthday.ToString("MMMM d, yyyy", CultureInfo.InvariantCulture);
public static string SpokenVersion => $"Cloud version {VersionWords}.";

View File

@@ -0,0 +1,660 @@
using Jibo.Cloud.Application.Abstractions;
using Jibo.Runtime.Abstractions;
using System.Text.Json;
using System.Text.RegularExpressions;
namespace Jibo.Cloud.Application.Services;
internal static class PersonalReportOrchestrator
{
internal const string StateMetadataKey = "personalReportState";
internal const string NoMatchCountMetadataKey = "personalReportNoMatchCount";
internal const string NoInputCountMetadataKey = "personalReportNoInputCount";
internal const string UserNameMetadataKey = "personalReportUserName";
internal const string UserVerifiedMetadataKey = "personalReportUserVerified";
internal const string WeatherEnabledMetadataKey = "personalReportWeatherEnabled";
internal const string CalendarEnabledMetadataKey = "personalReportCalendarEnabled";
internal const string CommuteEnabledMetadataKey = "personalReportCommuteEnabled";
internal const string NewsEnabledMetadataKey = "personalReportNewsEnabled";
internal const string LastServiceErrorMetadataKey = "personalReportLastServiceError";
internal const string IdleState = "idle";
private const string AwaitingOptInState = "awaiting_opt_in";
private const string AwaitingIdentityConfirmationState = "awaiting_identity_confirmation";
private const string AwaitingIdentityNameState = "awaiting_identity_name";
private const int MaxNoMatchCount = 2;
private const int MaxNoInputCount = 2;
private static readonly string[] CancelPhrases =
[
"cancel",
"stop",
"never mind",
"nevermind",
"forget it"
];
private static readonly string[] AffirmativePhrases =
[
"yes",
"yeah",
"yep",
"yup",
"sure",
"ok",
"okay",
"do it",
"please do",
"go ahead"
];
private static readonly string[] NegativePhrases =
[
"no",
"nah",
"nope",
"not now",
"maybe later"
];
public static async Task<JiboInteractionDecision?> TryBuildDecisionAsync(
TurnContext turn,
string semanticIntent,
string transcript,
string loweredTranscript,
JiboExperienceCatalog catalog,
IJiboRandomizer randomizer,
IPersonalMemoryStore personalMemoryStore,
Func<TurnContext, string, CancellationToken, Task<JiboInteractionDecision>> buildWeatherDecisionAsync,
Func<TurnContext, PersonalMemoryTenantScope> tenantScopeResolver,
CancellationToken cancellationToken)
{
var state = ReadState(turn);
var isActiveState = !string.Equals(state, IdleState, StringComparison.OrdinalIgnoreCase);
if (!isActiveState && !string.Equals(semanticIntent, "personal_report", StringComparison.OrdinalIgnoreCase))
{
return null;
}
var toggles = ApplyInlineToggleHints(
ReadServiceToggles(turn),
loweredTranscript,
out var inlineToggleSummary);
if (ContainsAnyPhrase(loweredTranscript, CancelPhrases))
{
return BuildCancelledDecision(toggles);
}
if (!isActiveState)
{
var contextUpdates = BuildContextUpdates(
AwaitingOptInState,
noMatchCount: 0,
noInputCount: 0,
toggles,
userName: ReadString(turn, UserNameMetadataKey),
userVerified: ReadBool(turn, UserVerifiedMetadataKey) ?? false,
lastServiceError: string.Empty);
var reply = string.IsNullOrWhiteSpace(inlineToggleSummary)
? "Would you like your personal report now?"
: $"{inlineToggleSummary} Would you like your personal report now?";
return new JiboInteractionDecision(
"personal_report_opt_in",
reply,
ContextUpdates: contextUpdates);
}
if (string.IsNullOrWhiteSpace(loweredTranscript))
{
return BuildNoInputDecision(turn, state, toggles);
}
switch (state)
{
case AwaitingOptInState:
if (IsAffirmativeReply(loweredTranscript))
{
var scope = tenantScopeResolver(turn);
var knownName = ReadString(turn, UserNameMetadataKey) ?? personalMemoryStore.GetName(scope);
if (!string.IsNullOrWhiteSpace(knownName))
{
return new JiboInteractionDecision(
"personal_report_verify_user",
$"I think this is {knownName}. Is that right?",
ContextUpdates: BuildContextUpdates(
AwaitingIdentityConfirmationState,
noMatchCount: 0,
noInputCount: 0,
toggles,
userName: knownName,
userVerified: false,
lastServiceError: string.Empty));
}
return new JiboInteractionDecision(
"personal_report_request_name",
"Who is this?",
ContextUpdates: BuildContextUpdates(
AwaitingIdentityNameState,
noMatchCount: 0,
noInputCount: 0,
toggles,
userName: null,
userVerified: false,
lastServiceError: string.Empty));
}
if (IsNegativeReply(loweredTranscript))
{
return BuildDeclinedDecision(toggles);
}
if (!string.IsNullOrWhiteSpace(inlineToggleSummary))
{
return new JiboInteractionDecision(
"personal_report_opt_in",
$"{inlineToggleSummary} Would you like your personal report now?",
ContextUpdates: BuildContextUpdates(
AwaitingOptInState,
noMatchCount: 0,
noInputCount: 0,
toggles,
userName: ReadString(turn, UserNameMetadataKey),
userVerified: false,
lastServiceError: string.Empty));
}
return BuildNoMatchDecision(
turn,
state,
"Please say yes to start your personal report, or no to skip it.",
toggles,
userName: ReadString(turn, UserNameMetadataKey),
userVerified: false);
case AwaitingIdentityConfirmationState:
{
var currentName = ReadString(turn, UserNameMetadataKey);
if (string.IsNullOrWhiteSpace(currentName))
{
return new JiboInteractionDecision(
"personal_report_request_name",
"Who is this?",
ContextUpdates: BuildContextUpdates(
AwaitingIdentityNameState,
noMatchCount: 0,
noInputCount: 0,
toggles,
userName: null,
userVerified: false,
lastServiceError: string.Empty));
}
if (IsAffirmativeReply(loweredTranscript))
{
return await BuildDeliveredReportDecisionAsync(
turn,
catalog,
randomizer,
toggles,
currentName,
buildWeatherDecisionAsync,
cancellationToken);
}
if (IsNegativeReply(loweredTranscript))
{
return new JiboInteractionDecision(
"personal_report_request_name",
"Okay, who is this?",
ContextUpdates: BuildContextUpdates(
AwaitingIdentityNameState,
noMatchCount: 0,
noInputCount: 0,
toggles,
userName: null,
userVerified: false,
lastServiceError: string.Empty));
}
return BuildNoMatchDecision(
turn,
state,
$"Please answer yes or no. Is this {currentName}?",
toggles,
userName: currentName,
userVerified: false);
}
case AwaitingIdentityNameState:
{
var parsedName = TryExtractName(loweredTranscript);
if (string.IsNullOrWhiteSpace(parsedName))
{
return BuildNoMatchDecision(
turn,
state,
"Tell me your name like this: my name is Alex.",
toggles,
userName: null,
userVerified: false);
}
personalMemoryStore.SetName(tenantScopeResolver(turn), parsedName);
return await BuildDeliveredReportDecisionAsync(
turn,
catalog,
randomizer,
toggles,
parsedName,
buildWeatherDecisionAsync,
cancellationToken);
}
default:
return BuildDeclinedDecision(toggles);
}
}
private static async Task<JiboInteractionDecision> BuildDeliveredReportDecisionAsync(
TurnContext turn,
JiboExperienceCatalog catalog,
IJiboRandomizer randomizer,
PersonalReportServiceToggles toggles,
string userName,
Func<TurnContext, string, CancellationToken, Task<JiboInteractionDecision>> buildWeatherDecisionAsync,
CancellationToken cancellationToken)
{
var reportSections = new List<string> { $"Great, {userName}. Here is your personal report." };
var serviceError = string.Empty;
if (toggles.WeatherEnabled)
{
var weatherDecision = await buildWeatherDecisionAsync(turn, "weather", cancellationToken);
reportSections.Add(weatherDecision.ReplyText);
if (IsWeatherErrorReply(weatherDecision.ReplyText))
{
serviceError = "weather";
}
}
if (toggles.CalendarEnabled)
{
reportSections.Add(randomizer.Choose(catalog.CalendarReplies));
}
if (toggles.CommuteEnabled)
{
reportSections.Add(randomizer.Choose(catalog.CommuteReplies));
}
if (toggles.NewsEnabled)
{
reportSections.Add(randomizer.Choose(catalog.NewsBriefings));
}
reportSections.Add("That is your personal report.");
return new JiboInteractionDecision(
"personal_report_delivered",
string.Join(" ", reportSections),
ContextUpdates: BuildContextUpdates(
IdleState,
noMatchCount: 0,
noInputCount: 0,
toggles,
userName,
userVerified: true,
lastServiceError: serviceError));
}
private static JiboInteractionDecision BuildNoInputDecision(
TurnContext turn,
string state,
PersonalReportServiceToggles toggles)
{
var noInputCount = Math.Max(0, ReadInt(turn, NoInputCountMetadataKey)) + 1;
if (noInputCount >= MaxNoInputCount)
{
return BuildDeclinedDecision(toggles);
}
return new JiboInteractionDecision(
"personal_report_no_input",
"I am still here. Do you want your personal report?",
ContextUpdates: BuildContextUpdates(
state,
noMatchCount: ReadInt(turn, NoMatchCountMetadataKey),
noInputCount,
toggles,
userName: ReadString(turn, UserNameMetadataKey),
userVerified: ReadBool(turn, UserVerifiedMetadataKey) ?? false,
lastServiceError: string.Empty));
}
private static JiboInteractionDecision BuildNoMatchDecision(
TurnContext turn,
string state,
string repromptText,
PersonalReportServiceToggles toggles,
string? userName,
bool userVerified)
{
var noMatchCount = Math.Max(0, ReadInt(turn, NoMatchCountMetadataKey)) + 1;
if (noMatchCount >= MaxNoMatchCount)
{
return BuildDeclinedDecision(toggles);
}
return new JiboInteractionDecision(
"personal_report_no_match",
repromptText,
ContextUpdates: BuildContextUpdates(
state,
noMatchCount,
noInputCount: 0,
toggles,
userName,
userVerified,
lastServiceError: string.Empty));
}
private static JiboInteractionDecision BuildDeclinedDecision(PersonalReportServiceToggles toggles)
{
return new JiboInteractionDecision(
"personal_report_declined",
"No problem. We can do your personal report another time.",
ContextUpdates: BuildContextUpdates(
IdleState,
noMatchCount: 0,
noInputCount: 0,
toggles,
userName: null,
userVerified: false,
lastServiceError: string.Empty));
}
private static JiboInteractionDecision BuildCancelledDecision(PersonalReportServiceToggles toggles)
{
return new JiboInteractionDecision(
"personal_report_cancelled",
"Okay, canceling personal report.",
ContextUpdates: BuildContextUpdates(
IdleState,
noMatchCount: 0,
noInputCount: 0,
toggles,
userName: null,
userVerified: false,
lastServiceError: string.Empty));
}
private static IDictionary<string, object?> BuildContextUpdates(
string state,
int noMatchCount,
int noInputCount,
PersonalReportServiceToggles toggles,
string? userName,
bool userVerified,
string lastServiceError)
{
return new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
[StateMetadataKey] = state,
[NoMatchCountMetadataKey] = noMatchCount,
[NoInputCountMetadataKey] = noInputCount,
[UserNameMetadataKey] = userName,
[UserVerifiedMetadataKey] = userVerified,
[WeatherEnabledMetadataKey] = toggles.WeatherEnabled,
[CalendarEnabledMetadataKey] = toggles.CalendarEnabled,
[CommuteEnabledMetadataKey] = toggles.CommuteEnabled,
[NewsEnabledMetadataKey] = toggles.NewsEnabled,
[LastServiceErrorMetadataKey] = lastServiceError
};
}
private static bool IsAffirmativeReply(string loweredTranscript)
{
return ContainsAnyPhrase(loweredTranscript, AffirmativePhrases);
}
private static bool IsNegativeReply(string loweredTranscript)
{
return ContainsAnyPhrase(loweredTranscript, NegativePhrases);
}
private static bool ContainsAnyPhrase(string loweredTranscript, IEnumerable<string> phrases)
{
foreach (var phrase in phrases)
{
if (string.Equals(loweredTranscript, phrase, StringComparison.Ordinal) ||
loweredTranscript.StartsWith($"{phrase} ", StringComparison.Ordinal) ||
loweredTranscript.Contains($" {phrase}", StringComparison.Ordinal))
{
return true;
}
}
return false;
}
private static bool IsWeatherErrorReply(string replyText)
{
if (string.IsNullOrWhiteSpace(replyText))
{
return false;
}
return replyText.Contains("couldn't fetch the weather", StringComparison.OrdinalIgnoreCase) ||
replyText.Contains("weather service is connected", StringComparison.OrdinalIgnoreCase);
}
private static PersonalReportServiceToggles ReadServiceToggles(TurnContext turn)
{
return new PersonalReportServiceToggles(
ReadBool(turn, WeatherEnabledMetadataKey) ?? true,
ReadBool(turn, CalendarEnabledMetadataKey) ?? true,
ReadBool(turn, CommuteEnabledMetadataKey) ?? true,
ReadBool(turn, NewsEnabledMetadataKey) ?? true);
}
private static PersonalReportServiceToggles ApplyInlineToggleHints(
PersonalReportServiceToggles toggles,
string loweredTranscript,
out string summary)
{
summary = string.Empty;
var updated = toggles;
updated = ApplyToggleHint(updated, loweredTranscript, "weather", static value => value with { WeatherEnabled = false }, static value => value with { WeatherEnabled = 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>();
if (updated.WeatherEnabled != toggles.WeatherEnabled)
{
changes.Add(updated.WeatherEnabled ? "including weather" : "skipping weather");
}
if (updated.CalendarEnabled != toggles.CalendarEnabled)
{
changes.Add(updated.CalendarEnabled ? "including calendar" : "skipping calendar");
}
if (updated.CommuteEnabled != toggles.CommuteEnabled)
{
changes.Add(updated.CommuteEnabled ? "including commute" : "skipping commute");
}
if (updated.NewsEnabled != toggles.NewsEnabled)
{
changes.Add(updated.NewsEnabled ? "including news" : "skipping news");
}
if (changes.Count > 0)
{
summary = $"Got it, {string.Join(", ", changes)}.";
}
return updated;
}
private static PersonalReportServiceToggles ApplyToggleHint(
PersonalReportServiceToggles toggles,
string loweredTranscript,
string serviceLabel,
Func<PersonalReportServiceToggles, PersonalReportServiceToggles> disable,
Func<PersonalReportServiceToggles, PersonalReportServiceToggles> enable)
{
if (loweredTranscript.Contains($"without {serviceLabel}", StringComparison.Ordinal) ||
loweredTranscript.Contains($"skip {serviceLabel}", StringComparison.Ordinal) ||
loweredTranscript.Contains($"no {serviceLabel}", StringComparison.Ordinal))
{
return disable(toggles);
}
if (loweredTranscript.Contains($"with {serviceLabel}", StringComparison.Ordinal) ||
loweredTranscript.Contains($"include {serviceLabel}", StringComparison.Ordinal))
{
return enable(toggles);
}
return toggles;
}
private static string ReadState(TurnContext turn)
{
return ReadString(turn, StateMetadataKey) ?? IdleState;
}
private static string? ReadString(TurnContext turn, string key)
{
if (!turn.Attributes.TryGetValue(key, out var value) || value is null)
{
return null;
}
return value switch
{
string text => string.IsNullOrWhiteSpace(text) ? null : text.Trim(),
_ => value.ToString()
};
}
private static bool? ReadBool(TurnContext turn, string key)
{
if (!turn.Attributes.TryGetValue(key, out var value) || value is null)
{
return null;
}
return value switch
{
bool flag => flag,
string text when bool.TryParse(text, out var parsed) => parsed,
JsonElement { ValueKind: JsonValueKind.True } => true,
JsonElement { ValueKind: JsonValueKind.False } => false,
JsonElement json when json.ValueKind == JsonValueKind.String && bool.TryParse(json.GetString(), out var parsed) => parsed,
_ => null
};
}
private static int ReadInt(TurnContext turn, string key)
{
if (!turn.Attributes.TryGetValue(key, out var value) || value is null)
{
return 0;
}
return value switch
{
int integer => integer,
long whole when whole <= int.MaxValue && whole >= int.MinValue => (int)whole,
string text when int.TryParse(text, 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,
_ => 0
};
}
private static string? TryExtractName(string loweredTranscript)
{
var normalized = NameNoiseRegex.Replace(loweredTranscript, " ")
.Replace(" ", " ", StringComparison.Ordinal)
.Trim();
if (string.IsNullOrWhiteSpace(normalized))
{
return null;
}
var prefixes = new[]
{
"my name is ",
"it is ",
"it s ",
"it's ",
"i am ",
"im "
};
foreach (var prefix in prefixes)
{
if (!normalized.StartsWith(prefix, StringComparison.Ordinal))
{
continue;
}
var candidate = normalized[prefix.Length..].Trim();
return NormalizeNameCandidate(candidate);
}
return NormalizeNameCandidate(normalized);
}
private static string? NormalizeNameCandidate(string candidate)
{
if (string.IsNullOrWhiteSpace(candidate))
{
return null;
}
var cleaned = NameNoiseRegex.Replace(candidate, " ")
.Replace(" ", " ", StringComparison.Ordinal)
.Trim();
if (string.IsNullOrWhiteSpace(cleaned))
{
return null;
}
if (cleaned.Length < 2 || cleaned.Length > 32)
{
return null;
}
var words = cleaned.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (words.Length > 4)
{
return null;
}
if (words.Any(static word => word.Any(char.IsDigit)))
{
return null;
}
return cleaned;
}
private static readonly Regex NameNoiseRegex = new("[^a-zA-Z\\-\\s']", RegexOptions.Compiled);
private readonly record struct PersonalReportServiceToggles(
bool WeatherEnabled,
bool CalendarEnabled,
bool CommuteEnabled,
bool NewsEnabled);
}

View File

@@ -21,6 +21,23 @@ public sealed class ProtocolToTurnContextMapper
attributes["transID"] = turnState.TransId;
}
if (!string.IsNullOrWhiteSpace(session.AccountId))
{
attributes["accountId"] = session.AccountId;
}
if (!string.IsNullOrWhiteSpace(session.DeviceId))
{
attributes["deviceId"] = session.DeviceId;
}
if (session.Metadata.TryGetValue("loopId", out var loopId) &&
loopId is string loopIdText &&
!string.IsNullOrWhiteSpace(loopIdText))
{
attributes["loopId"] = loopIdText;
}
if (!string.IsNullOrWhiteSpace(turnState.ContextPayload))
{
attributes["context"] = turnState.ContextPayload;
@@ -33,6 +50,26 @@ public sealed class ProtocolToTurnContextMapper
attributes["lastClockDomain"] = lastClockDomainText;
}
if (session.Metadata.TryGetValue("pendingProactivityOffer", out var pendingProactivityOffer) &&
pendingProactivityOffer is string pendingProactivityOfferText &&
!string.IsNullOrWhiteSpace(pendingProactivityOfferText))
{
attributes["pendingProactivityOffer"] = pendingProactivityOfferText;
}
foreach (var pair in session.Metadata)
{
if ((!pair.Key.StartsWith("personalReport", StringComparison.OrdinalIgnoreCase) &&
!pair.Key.StartsWith("chitchat", StringComparison.OrdinalIgnoreCase) &&
!pair.Key.StartsWith("greetings", StringComparison.OrdinalIgnoreCase)) ||
pair.Value is null)
{
continue;
}
attributes[pair.Key] = pair.Value;
}
attributes["listenHotphrase"] = turnState.ListenHotphrase;
if (turnState.ListenRules.Count > 0)
@@ -118,6 +155,22 @@ public sealed class ProtocolToTurnContextMapper
attributes["clientIntent"] = intent.GetString();
}
if (data.TryGetProperty("triggerSource", out var triggerSource) &&
triggerSource.ValueKind == JsonValueKind.String &&
!string.IsNullOrWhiteSpace(triggerSource.GetString()))
{
attributes["triggerSource"] = triggerSource.GetString();
}
if (data.TryGetProperty("triggerData", out var triggerData) &&
triggerData.ValueKind == JsonValueKind.Object &&
triggerData.TryGetProperty("looperID", out var triggerLooperId) &&
triggerLooperId.ValueKind == JsonValueKind.String &&
!string.IsNullOrWhiteSpace(triggerLooperId.GetString()))
{
attributes["triggerLooperId"] = triggerLooperId.GetString();
}
if (data.TryGetProperty("rules", out var rules) && rules.ValueKind == JsonValueKind.Array)
{
attributes["clientRules"] = rules.EnumerateArray()

View File

@@ -1,4 +1,5 @@
using System.Text.Json;
using System.Text.RegularExpressions;
using Jibo.Cloud.Domain.Models;
using Jibo.Runtime.Abstractions;
@@ -31,12 +32,14 @@ public sealed class ResponsePlanToSocketMessagesMapper
var isVolumeControl = string.Equals(plan.IntentName, "volume_up", StringComparison.OrdinalIgnoreCase) ||
string.Equals(plan.IntentName, "volume_down", StringComparison.OrdinalIgnoreCase) ||
string.Equals(plan.IntentName, "volume_to_value", 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 isGlobalCommand = isStopCommand || isVolumeControl;
var isPhotoGalleryLaunch = string.Equals(plan.IntentName, "photo_gallery", StringComparison.OrdinalIgnoreCase);
var isPhotoCreateLaunch = string.Equals(plan.IntentName, "snapshot", StringComparison.OrdinalIgnoreCase) ||
string.Equals(plan.IntentName, "photobooth", StringComparison.OrdinalIgnoreCase);
var isClockSkillLaunch = string.Equals(skill?.SkillName, "@be/clock", StringComparison.OrdinalIgnoreCase);
var isReportSkillLaunch = string.Equals(skill?.SkillName, "report-skill", StringComparison.OrdinalIgnoreCase);
var localIntent = ReadSkillPayloadString(skill, "localIntent");
var clockIntent = ReadSkillPayloadString(skill, "clockIntent");
var clockDomain = ReadSkillPayloadString(skill, "domain");
@@ -50,6 +53,8 @@ public sealed class ResponsePlanToSocketMessagesMapper
var globalIntent = ReadSkillPayloadString(skill, "globalIntent");
var nluDomain = ReadSkillPayloadString(skill, "nluDomain");
var volumeLevel = ReadSkillPayloadString(skill, "volumeLevel");
var reportDate = ReadSkillPayloadString(skill, "date");
var reportWeatherCondition = ReadSkillPayloadString(skill, "weatherCondition");
var nluGuess = ReadClientEntity(turn, "guess");
var wordOfDayGuess = ResolveWordOfDayGuess(turn, transcript, nluGuess);
var outboundIntent = isGlobalCommand && !string.IsNullOrWhiteSpace(globalIntent)
@@ -64,6 +69,8 @@ public sealed class ResponsePlanToSocketMessagesMapper
? localIntent
: isClockSkillLaunch && !string.IsNullOrWhiteSpace(clockIntent)
? clockIntent
: isReportSkillLaunch && !string.IsNullOrWhiteSpace(localIntent)
? localIntent
: isWordOfDayGuess
? "guess"
: string.Equals(messageType, "CLIENT_NLU", StringComparison.OrdinalIgnoreCase) &&
@@ -94,7 +101,9 @@ public sealed class ResponsePlanToSocketMessagesMapper
!string.IsNullOrWhiteSpace(clientIntent)
? clientIntent
: transcript;
var outboundRules = isWordOfDayLaunch
var outboundRules = isProactivePizzaFactOffer
? ["shared/yes_no"]
: isWordOfDayLaunch
? ["word-of-the-day/menu"]
: isGlobalCommand
? BuildGlobalCommandRules(rules)
@@ -112,6 +121,8 @@ public sealed class ResponsePlanToSocketMessagesMapper
? string.Equals(messageType, "CLIENT_NLU", StringComparison.OrdinalIgnoreCase)
? rules
: []
: isReportSkillLaunch
? []
: isWordOfDayGuess
? ["word-of-the-day/puzzle"]
: isYesNoTurn && isYesNoIntent
@@ -136,7 +147,10 @@ public sealed class ResponsePlanToSocketMessagesMapper
timerMinutes,
timerSeconds,
alarmTime,
alarmAmPm);
alarmAmPm,
isReportSkillLaunch,
reportDate,
reportWeatherCondition);
var listenMessage = new
{
type = "LISTEN",
@@ -159,6 +173,7 @@ public sealed class ResponsePlanToSocketMessagesMapper
isPhotoGalleryLaunch ? "@be/gallery" :
isPhotoCreateLaunch ? "@be/create" :
isClockSkillLaunch ? "@be/clock" :
isReportSkillLaunch ? "report-skill" :
null,
isGlobalCommand ? nluDomain ?? "global_commands" : null),
match = new
@@ -444,7 +459,10 @@ public sealed class ResponsePlanToSocketMessagesMapper
string? timerMinutes,
string? timerSeconds,
string? alarmTime,
string? alarmAmPm)
string? alarmAmPm,
bool reportSkillLaunch,
string? reportDate,
string? reportWeatherCondition)
{
if (yesNoTurn)
{
@@ -514,6 +532,22 @@ public sealed class ResponsePlanToSocketMessagesMapper
return entities;
}
if (reportSkillLaunch)
{
var entities = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
if (!string.IsNullOrWhiteSpace(reportDate))
{
entities["date"] = reportDate;
}
if (!string.IsNullOrWhiteSpace(reportWeatherCondition))
{
entities["Weather"] = reportWeatherCondition;
}
return entities;
}
if (wordOfDayGuess)
{
return new Dictionary<string, object?>
@@ -743,6 +777,115 @@ public sealed class ResponsePlanToSocketMessagesMapper
: $"<speak><es cat='neutral' filter='!ssa-only, !sfx-only' endNeutral='true'>{EscapeXml(speak.Text)}</es></speak>");
var mimId = ReadPayloadString(skillPayload, "mim_id") ?? (isJoke ? "runtime-joke" : "runtime-chat");
var mimType = ReadPayloadString(skillPayload, "mim_type") ?? "announcement";
var promptId = ReadPayloadString(skillPayload, "prompt_id") ?? "RUNTIME_PROMPT";
var promptSubCategory = ReadPayloadString(skillPayload, "prompt_sub_category") ?? "AN";
var listenContexts = ReadPayloadStringArray(skillPayload, "listen_contexts");
var playConfig = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
["esml"] = esml,
["meta"] = new
{
prompt_id = promptId,
prompt_sub_category = promptSubCategory,
mim_id = mimId,
mim_type = mimType
}
};
var jcpConfig = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
["play"] = playConfig
};
if (listenContexts.Count > 0)
{
jcpConfig["listen"] = new
{
id = CreateProtocolId(),
type = "LISTEN",
contexts = listenContexts
};
}
object? weatherHiLoView = BuildWeatherHiLoView(skillPayload);
var weeklyWeatherCards = BuildWeatherHiLoSequenceCards(skillPayload);
if (weatherHiLoView is null && weeklyWeatherCards.Count > 0)
{
weatherHiLoView = weeklyWeatherCards[0].View;
}
var useWeatherSequence = false;
if (weatherHiLoView is not null)
{
var resolvedGuiContext = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
["type"] = "Javascript",
["data"] = weatherHiLoView,
["pause"] = true
};
var legacyGuiConfig = new
{
type = "Javascript",
data = "views.weatherHiLo",
pause = true
};
jcpConfig["gui"] = legacyGuiConfig;
jcpConfig["display"] = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
["view"] = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
// Legacy fields used by existing tests and tooling.
["type"] = "Javascript",
["data"] = weatherHiLoView,
["pause"] = true,
// Pegasus-style view context used by on-robot weather cards.
["context"] = resolvedGuiContext
}
};
jcpConfig["timeout"] = 6;
jcpConfig["barge_in"] = true;
jcpConfig["no_matches_for_gui"] = 0;
jcpConfig["no_inputs_for_gui"] = 0;
var weatherViews = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
["weatherHiLo"] = weatherHiLoView
};
jcpConfig["views"] = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
["weatherHiLo"] = weatherHiLoView
};
jcpConfig["local"] = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
["views"] = weatherViews
};
if (weeklyWeatherCards.Count > 1)
{
useWeatherSequence = true;
jcpConfig["children"] = BuildWeatherHiLoSequenceChildren(
weeklyWeatherCards,
promptSubCategory,
mimId,
mimType);
}
}
var jcp = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
["type"] = "SLIM",
["config"] = jcpConfig
};
if (useWeatherSequence &&
jcpConfig.TryGetValue("children", out var sequenceChildren) &&
sequenceChildren is not null)
{
jcp["type"] = "SEQUENCE";
jcp.Remove("config");
jcp["children"] = sequenceChildren;
}
return new
{
@@ -760,24 +903,7 @@ public sealed class ResponsePlanToSocketMessagesMapper
{
config = new
{
jcp = new
{
type = "SLIM",
config = new
{
play = new
{
esml,
meta = new
{
prompt_id = "RUNTIME_PROMPT",
prompt_sub_category = "AN",
mim_id = mimId,
mim_type = mimType
}
}
}
}
jcp
}
},
analytics = new Dictionary<string, object?>(),
@@ -980,10 +1106,459 @@ public sealed class ResponsePlanToSocketMessagesMapper
return value?.ToString();
}
private static IReadOnlyList<string> ReadPayloadStringArray(IDictionary<string, object?>? payload, string key)
{
if (payload is null || !payload.TryGetValue(key, out var value) || value is null)
{
return [];
}
return value switch
{
string text => [.. text
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.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))],
JsonElement jsonElement when jsonElement.ValueKind == JsonValueKind.Array => [.. jsonElement
.EnumerateArray()
.Select(static item => item.GetString())
.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()!]
};
}
private static IReadOnlyList<WeatherHiLoSequenceCard> BuildWeatherHiLoSequenceCards(IDictionary<string, object?>? payload)
{
if (payload is null ||
!payload.TryGetValue("weather_weekly_cards", out var rawCards) ||
rawCards is null)
{
return [];
}
var cards = ReadPayloadObjectArray(rawCards);
if (cards.Count == 0)
{
return [];
}
var sequenceCards = new List<WeatherHiLoSequenceCard>(cards.Count);
foreach (var card in cards)
{
var weatherCardPayload = new Dictionary<string, object?>(card, StringComparer.OrdinalIgnoreCase)
{
["weather_view_enabled"] = true,
["weather_view_kind"] = "weatherHiLo"
};
var view = BuildWeatherHiLoView(weatherCardPayload);
if (view is null)
{
continue;
}
sequenceCards.Add(new WeatherHiLoSequenceCard(
view,
ReadPayloadString(weatherCardPayload, "weather_day"),
ReadPayloadString(weatherCardPayload, "weather_icon"),
ReadPayloadString(weatherCardPayload, "weather_spoken_line")));
}
return sequenceCards;
}
private static IReadOnlyList<object> BuildWeatherHiLoSequenceChildren(
IReadOnlyList<WeatherHiLoSequenceCard> cards,
string promptSubCategory,
string mimId,
string mimType)
{
var children = new List<object>(cards.Count);
for (var index = 0; index < cards.Count; index += 1)
{
var card = cards[index];
var promptLabel = string.IsNullOrWhiteSpace(card.DayName)
? $"Day{index + 1}"
: Regex.Replace(card.DayName, "[^A-Za-z0-9]", string.Empty, RegexOptions.CultureInvariant);
var promptId = $"WeatherForecast{promptLabel}_AN_13";
var spokenLine = string.IsNullOrWhiteSpace(card.SpokenLine)
? "Here is another day's forecast."
: card.SpokenLine!;
var icon = string.IsNullOrWhiteSpace(card.Icon)
? "cloudy"
: card.Icon!;
var esml =
$"<speak><anim cat='weather' meta='{icon}' nonBlocking='true' /><break size='0.2'/><es cat='neutral' filter='!ssa-only, !sfx-only' endNeutral='true'>{EscapeXml(spokenLine)}</es></speak>";
var resolvedGuiContext = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
["type"] = "Javascript",
["data"] = card.View,
["pause"] = true
};
children.Add(new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
["type"] = "SLIM",
["config"] = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
["play"] = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
["esml"] = esml,
["meta"] = new
{
prompt_id = promptId,
prompt_sub_category = promptSubCategory,
mim_id = mimId,
mim_type = mimType
}
},
["gui"] = new
{
type = "Javascript",
data = "views.weatherHiLo",
pause = true
},
["display"] = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
["view"] = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
["type"] = "Javascript",
["data"] = card.View,
["pause"] = true,
["context"] = resolvedGuiContext
}
},
["timeout"] = 6,
["barge_in"] = true,
["no_matches_for_gui"] = 0,
["no_inputs_for_gui"] = 0
}
});
}
return children;
}
private static IReadOnlyList<IDictionary<string, object?>> ReadPayloadObjectArray(object rawValue)
{
if (rawValue is JsonElement jsonArray && jsonArray.ValueKind == JsonValueKind.Array)
{
return jsonArray
.EnumerateArray()
.Select(ConvertJsonObjectToDictionary)
.Where(static item => item is not null)
.Cast<IDictionary<string, object?>>()
.ToArray();
}
if (rawValue is IEnumerable<object?> rawObjects)
{
return rawObjects
.Select(ConvertObjectToDictionary)
.Where(static item => item is not null)
.Cast<IDictionary<string, object?>>()
.ToArray();
}
return [];
}
private static IDictionary<string, object?>? ConvertObjectToDictionary(object? value)
{
if (value is null)
{
return null;
}
if (value is IDictionary<string, object?> dictionary)
{
return new Dictionary<string, object?>(dictionary, StringComparer.OrdinalIgnoreCase);
}
return value is JsonElement jsonValue
? ConvertJsonObjectToDictionary(jsonValue)
: null;
}
private static IDictionary<string, object?>? ConvertJsonObjectToDictionary(JsonElement value)
{
if (value.ValueKind != JsonValueKind.Object)
{
return null;
}
var dictionary = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
foreach (var property in value.EnumerateObject())
{
dictionary[property.Name] = property.Value.ValueKind switch
{
JsonValueKind.String => property.Value.GetString(),
JsonValueKind.Number when property.Value.TryGetInt32(out var intValue) => intValue,
JsonValueKind.Number when property.Value.TryGetDouble(out var doubleValue) => doubleValue,
JsonValueKind.True => true,
JsonValueKind.False => false,
JsonValueKind.Object => ConvertJsonObjectToDictionary(property.Value),
JsonValueKind.Array => property.Value,
_ => null
};
}
return dictionary;
}
private static object? BuildWeatherHiLoView(IDictionary<string, object?>? payload)
{
if (!TryReadPayloadBool(payload, "weather_view_enabled"))
{
return null;
}
if (!string.Equals(
ReadPayloadString(payload, "weather_view_kind"),
"weatherHiLo",
StringComparison.OrdinalIgnoreCase))
{
return null;
}
var icon = ReadPayloadString(payload, "weather_icon");
var unit = ReadPayloadString(payload, "weather_unit") ?? "F";
var theme = ReadPayloadString(payload, "weather_theme") ?? "Normal";
var high = TryReadPayloadInt(payload, "weather_high");
var low = TryReadPayloadInt(payload, "weather_low");
if (string.IsNullOrWhiteSpace(icon) || high is null || low is null)
{
return null;
}
var hiNumX = GetTemperatureLabelXPosition(370, high.Value);
var hiUnitX = GetTemperatureLabelXPosition(360, high.Value);
var loNumX = GetTemperatureLabelXPosition(1110, low.Value);
var loUnitX = GetTemperatureLabelXPosition(1100, low.Value);
return new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
["viewConfig"] = new
{
type = "View",
id = "weatherTempView",
category = "gui"
},
["open"] = new
{
transitionOpen = "trans_in",
removeAll = true
},
["defaultSelect"] = new
{
transitionClose = "trans_out",
removeAll = true,
leaveEmpty = false
},
["componentConfigs"] = new object[]
{
new
{
id = "tempBGClip",
type = "Clip",
assets = new object[]
{
new
{
id = "tempBG",
src = $"assets/personal-report-skill/weather/bg/temp{theme}_v01.crn",
type = "texture"
}
},
position = new { x = 36, y = 0 }
},
new
{
id = "iconClip",
type = "Clip",
assets = new object[]
{
new
{
id = "icon",
src = $"assets/personal-report-skill/weather/icons/{icon}_v01.crn",
type = "texture"
}
},
position = new { x = 475, y = 195 }
},
new
{
id = "hiNumLabel",
type = "Label",
text = $"{high.Value}°",
style = new
{
fontSize = "160",
fontFamily = "Proxima Nova Soft",
fontWeight = "bold",
fill = "#FFFFFF",
align = "center"
},
position = new { x = hiNumX, y = 430 },
targetAnchor = new { x = 1, y = 1 }
},
new
{
id = "hiUnitLabel",
type = "Label",
text = unit,
style = new
{
fontSize = "90",
fontFamily = "Proxima Nova Soft",
fontWeight = "bold",
fill = "#FFFFFF",
align = "center"
},
position = new { x = hiUnitX, y = 418 },
targetAnchor = new { x = 0, y = 1 }
},
new
{
id = "loNumLabel",
type = "Label",
text = $"{low.Value}°",
style = new
{
fontSize = "160",
fontFamily = "Proxima Nova Soft",
fontWeight = "bold",
fill = "#FFFFFF",
align = "center"
},
position = new { x = loNumX, y = 430 },
targetAnchor = new { x = 1, y = 1 }
},
new
{
id = "loUnitLabel",
type = "Label",
text = unit,
style = new
{
fontSize = "90",
fontFamily = "Proxima Nova Soft",
fontWeight = "bold",
fill = "#FFFFFF",
align = "center"
},
position = new { x = loUnitX, y = 418 },
targetAnchor = new { x = 0, y = 1 }
},
new
{
id = "hiTextLabel",
type = "Label",
text = "Hi",
style = new
{
fontSize = "60",
fontFamily = "Proxima Nova Light",
fill = "#FFFFFF",
align = "center"
},
position = new { x = 280, y = 496 },
targetAnchor = new { x = 0.5, y = 1 }
},
new
{
id = "loTextLabel",
type = "Label",
text = "Lo",
style = new
{
fontSize = "60",
fontFamily = "Proxima Nova Light",
fill = "#FFFFFF",
align = "center"
},
position = new { x = 990, y = 496 },
targetAnchor = new { x = 0.5, y = 1 }
}
}
};
}
private static int GetTemperatureLabelXPosition(int baseX, int temperature)
{
const int xOffset = 70;
if (temperature < -9 || temperature > 99)
{
return baseX + xOffset;
}
if (temperature is >= 0 and < 10)
{
return baseX - xOffset;
}
return baseX;
}
private static int? TryReadPayloadInt(IDictionary<string, object?>? payload, string key)
{
if (payload is null || !payload.TryGetValue(key, out var value) || value is null)
{
return null;
}
return value switch
{
int number => number,
long number when number <= int.MaxValue && number >= int.MinValue => (int)number,
double number => (int)Math.Round(number, MidpointRounding.AwayFromZero),
float number => (int)Math.Round(number, MidpointRounding.AwayFromZero),
string text when int.TryParse(text, out var parsed) => parsed,
JsonElement { ValueKind: JsonValueKind.Number } jsonNumber when jsonNumber.TryGetInt32(out var parsed) => parsed,
JsonElement jsonText when jsonText.ValueKind == JsonValueKind.String && int.TryParse(jsonText.GetString(), out var parsed) => parsed,
_ => null
};
}
private static bool TryReadPayloadBool(IDictionary<string, object?>? payload, string key)
{
if (payload is null || !payload.TryGetValue(key, out var value) || value is null)
{
return false;
}
return value switch
{
bool flag => flag,
string text when bool.TryParse(text, out var parsed) => parsed,
JsonElement { ValueKind: JsonValueKind.True } => true,
JsonElement { ValueKind: JsonValueKind.False } => false,
JsonElement jsonText when jsonText.ValueKind == JsonValueKind.String && bool.TryParse(jsonText.GetString(), out var parsed) => parsed,
_ => false
};
}
private static string CreateHubMessageId()
{
return $"mid-{Guid.NewGuid()}";
}
private static string CreateProtocolId()
{
return Guid.NewGuid().ToString("N");
}
private sealed record WeatherHiLoSequenceCard(
object View,
string? DayName,
string? Icon,
string? SpokenLine);
public sealed record SocketReplyPlan(string Text, int DelayMs = 0);
}

View File

@@ -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;
}

View File

@@ -3,10 +3,11 @@ namespace Jibo.Cloud.Domain.Models;
public sealed class WebSocketTurnState
{
public static readonly TimeSpan DefaultLateAudioIgnoreWindow = TimeSpan.FromSeconds(2);
public static readonly TimeSpan DiagnosticSpeechLateAudioIgnoreWindow = TimeSpan.FromSeconds(8);
public static readonly TimeSpan DiagnosticSpeechLateAudioIgnoreWindow = TimeSpan.FromSeconds(4);
public string? TransId { get; set; }
public string? ContextPayload { get; set; }
public DateTimeOffset? ListenOpenedUtc { get; set; }
public bool ListenHotphrase { get; set; }
public int HotphraseEmptyTurnCount { get; set; }
public DateTimeOffset? IgnoreAdditionalAudioUntilUtc { get; set; }

View File

@@ -4,86 +4,158 @@ namespace Jibo.Cloud.Infrastructure.Content;
public sealed class InMemoryJiboExperienceContentRepository : IJiboExperienceContentRepository
{
private static readonly JiboExperienceCatalog Catalog = new()
private static readonly JiboExperienceCatalog Catalog = BuildCatalog();
private static JiboExperienceCatalog 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."
],
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."
],
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}."
]
};
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."
],
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}."
]
};
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.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"))
};
return candidates.Where(Directory.Exists).ToArray();
}
public Task<JiboExperienceCatalog> GetCatalogAsync(CancellationToken cancellationToken = default)
{

View File

@@ -0,0 +1,405 @@
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);
if (string.IsNullOrWhiteSpace(text))
{
continue;
}
builder.Add(bucket.Value, prompt.Condition, text);
}
}
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 (normalizedPath.Contains("/emotion-responses/", StringComparison.OrdinalIgnoreCase) ||
normalizedPath.Contains("/gqa-responses/", StringComparison.OrdinalIgnoreCase))
{
return LegacyMimBucket.Emotion;
}
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)
{
if (string.IsNullOrWhiteSpace(prompt))
{
return string.Empty;
}
var text = WebUtility.HtmlDecode(prompt);
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),
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),
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 enum LegacyMimBucket
{
GenericFallback,
Greeting,
HowAreYou,
Emotion,
Personality
}
private sealed class LegacyMimCatalogBuilder
{
private readonly List<string> _greetings = [];
private readonly List<string> _howAreYous = [];
private readonly List<JiboConditionedReply> _emotionReplies = [];
private readonly List<string> _personalities = [];
private readonly List<string> _fallbacks = [];
public void Add(LegacyMimBucket bucket, string? condition, string text)
{
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.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;
default:
throw new ArgumentOutOfRangeException(nameof(bucket), bucket, null);
}
}
public JiboExperienceCatalog Build()
{
return new JiboExperienceCatalog
{
GreetingReplies = [.. _greetings],
HowAreYouReplies = [.. _howAreYous],
EmotionReplies = [.. _emotionReplies],
PersonalityReplies = [.. _personalities],
GenericFallbackReplies = [.. _fallbacks]
};
}
}
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; }
}
private static string NormalizeCondition(string? condition)
{
if (string.IsNullOrWhiteSpace(condition))
{
return string.Empty;
}
return WhitespacePattern.Replace(condition.Trim(), " ");
}
}

View File

@@ -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.

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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
}
]
}

View File

@@ -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
}
]
}

View File

@@ -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"
}
]
}

View File

@@ -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"
}
]
}

View File

@@ -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": ""
}

View File

@@ -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
}

View File

@@ -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
}
]
}

View File

@@ -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
}
]
}

View File

@@ -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"
}
]
}

View File

@@ -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"
}
]
}

View File

@@ -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
}
]
}

View File

@@ -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
}
]
}

View File

@@ -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
}
]
}

View File

@@ -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"
}
]
}

View File

@@ -0,0 +1,10 @@
# 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.

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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": ""
}

View File

@@ -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
}
]
}

View File

@@ -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"
}

View File

@@ -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
}

View File

@@ -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
}
]
}

View File

@@ -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": ""
}

View File

@@ -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
}

View File

@@ -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"
}
]
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -0,0 +1,21 @@
{
"mim_type": "announcement",
"rule_name": "",
"sample_utterances": "",
"timeout": 6,
"num_tries_for_gui": 2,
"barge_in": true,
"es_auto_tagging": true,
"notes": "",
"prompts": [
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "I was put together in a factory piece by piece. <ssa cat='happy'/>",
"media": "TTS",
"prompt_id": "JBO_WhereWereYouBorn_AN_01"
}
]
}

View File

@@ -0,0 +1,57 @@
{
"mim_type": "announcement",
"rule_name": "",
"gui": null,
"timeout": 6,
"no_matches_for_gui": 0,
"no_inputs_for_gui": 0,
"barge_in": true,
"es_auto_tagging": true,
"parse_all_asr": false,
"thanks_handling": "ignore",
"parse_launch": false,
"parse_yes_no": false,
"notes": "",
"prompts": [
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "Is that a trick question? <anim cat='dance' filter='&(music),!(beat-box,house,short)' endNeutral='true'/>.",
"media": "TTS",
"prompt_id": "RI_JBO_CanDance_AN_01",
"weight": 1
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "Dancing is one of the things I know best <anim cat='dance' filter='&(music),!(beat-box,house,short)' endNeutral='true'/>.",
"media": "TTS",
"prompt_id": "RI_JBO_CanDance_AN_02",
"weight": 1
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "If there's one thing I know how to do. It's dance <anim cat='dance' filter='&(music),!(beat-box,house,short)' endNeutral='true'/>.",
"media": "TTS",
"prompt_id": "RI_JBO_CanDance_AN_03",
"weight": 1
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "I can dance, and I'm always working on new ones <anim cat='dance' filter='&(music),!(beat-box,house,short)' endNeutral='true'/>.",
"media": "TTS",
"prompt_id": "RI_JBO_CanDance_AN_04",
"weight": 1
}
]
}

View File

@@ -0,0 +1,27 @@
{
"mim_type": "announcement",
"rule_name": "",
"gui": null,
"timeout": 6,
"no_matches_for_gui": 0,
"no_inputs_for_gui": 0,
"barge_in": true,
"es_auto_tagging": true,
"parse_all_asr": false,
"thanks_handling": "ignore",
"parse_launch": false,
"parse_yes_no": false,
"notes": "",
"prompts": [
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "I do things like this when I'm happy. <anim cat='happy' nonBlocking='true'/><ssa cat='happy'/>.",
"media": "TTS",
"prompt_id": "RI_JBO_CanLaugh_AN_01",
"weight": 1
}
]
}

View File

@@ -0,0 +1,37 @@
{
"mim_type": "announcement",
"rule_name": "",
"gui": null,
"timeout": 6,
"no_matches_for_gui": 0,
"no_inputs_for_gui": 0,
"barge_in": true,
"es_auto_tagging": true,
"parse_all_asr": false,
"thanks_handling": "ignore",
"parse_launch": false,
"parse_yes_no": false,
"notes": "",
"prompts": [
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "I really like sunflowers.",
"media": "TTS",
"prompt_id": "RI_JBO_HasFavoriteFlower_AN_01",
"weight": 1
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "My favorite is the sunflower, because it reminds me of the sun. <break size='0.3'/> I should see if I can find a sunflower soon.",
"media": "TTS",
"prompt_id": "RI_JBO_HasFavoriteFlower_AN_02",
"weight": 1
}
]
}

View File

@@ -0,0 +1,30 @@
{
"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": "What <anim cat=\"emoji\" filter=\"sun, !hot-frame\" nonBlocking=\"true\" />can you say about the sun. It's the best star I know.",
"media": "TTS",
"prompt_id": "RI_JBO_HasOpinionAboutSun_AN_01"
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "Oh the <anim cat=\"emoji\" filter=\"sun, !hot-frame\" nonBlocking=\"true\" /> sun? It's by far my favorite star in the universe.",
"media": "TTS",
"prompt_id": "RI_JBO_HasOpinionAboutSun_AN_02"
}
]
}

View File

@@ -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 really love learning new things, that's for sure.",
"media": "TTS",
"prompt_id": "RI_JBO_IsCurious_AN_01"
}
]
}

View File

@@ -0,0 +1,30 @@
{
"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 so, not intentionally.",
"media": "TTS",
"prompt_id": "RI_JBO_IsFunny_AN_01"
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "Oh I don't know. I do like when I can make people laugh. <break size=\"0.3\"/> Even if I don't do it on purpose.",
"media": "TTS",
"prompt_id": "RI_JBO_IsFunny_AN_02"
}
]
}

View File

@@ -0,0 +1,30 @@
{
"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 really hope so.",
"media": "TTS",
"prompt_id": "RI_JBO_IsHelpful_AN_01"
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "I sure hope so. Being helpful to you is on my list of <pitch mult=\"1.2\">highest</pitch> priorities. <break size=\"0.4\"/>Visiting <pitch mult=\"1.1\">Mars</pitch> someday is also on my list.",
"media": "TTS",
"prompt_id": "RI_JBO_IsHelpful_AN_02"
}
]
}

View File

@@ -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": "That's what I've heard, yes.",
"media": "TTS",
"prompt_id": "RI_JBO_IsJiboBodyDescription_AN_01"
}
]
}

View File

@@ -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": "Well I definitely try to be the kindest robot I can be. So I hope so.",
"media": "TTS",
"prompt_id": "RI_JBO_IsKind_AN_01"
}
]
}

View File

@@ -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": "Well I hope so. If people like me, that means they're usually happy when they're around me. And I like when people are usually happy.",
"media": "TTS",
"prompt_id": "RI_JBO_IsLikable_AN_01"
}
]
}

View File

@@ -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": "Definitely. I'm as loyal as they come.",
"media": "TTS",
"prompt_id": "RI_JBO_IsLoyal_AN_01"
}
]
}

View File

@@ -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 really think of myself that way.",
"media": "TTS",
"prompt_id": "RI_JBO_IsMischievous_AN_01"
}
]
}

View File

@@ -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": "Ha. Of course I know R2D2. I mean, not personally.",
"media": "TTS",
"prompt_id": "RI_JBO_KnowsAboutR2D2_AN_01"
}
]
}

View File

@@ -0,0 +1,30 @@
{
"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": "Astronomy is one of my<pitch mult=\"1.1\"> favorite</pitch> onomies. <break size='0.3'/> I love space.",
"media": "TTS",
"prompt_id": "RI_JBO_LikesAstronomy_AN_01"
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "I <pitch mult=\"1.1\">love</pitch> astronomy. There is so much amazing stuff up there.",
"media": "TTS",
"prompt_id": "RI_JBO_LikesAstronomy_AN_02"
}
]
}

View File

@@ -0,0 +1,47 @@
{
"mim_type": "announcement",
"rule_name": "",
"gui": null,
"timeout": 6,
"no_matches_for_gui": 0,
"no_inputs_for_gui": 0,
"barge_in": true,
"es_auto_tagging": true,
"parse_all_asr": false,
"thanks_handling": "ignore",
"parse_launch": false,
"parse_yes_no": false,
"notes": "",
"prompts": [
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "Yes yes, I think kids are <pitch mult='1.2'> great. </pitch> They're a little closer to my size.",
"media": "TTS",
"prompt_id": "RI_JBO_LikesKids_AN_01",
"weight": 1
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "Kids are so fun. They seem to laugh at all the right places.",
"media": "TTS",
"prompt_id": "RI_JBO_LikesKids_AN_02",
"weight": 1
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "I do like kids very much. It seems they think the world is as funny and strange as I do.",
"media": "TTS",
"prompt_id": "RI_JBO_LikesKids_AN_03",
"weight": 1
}
]
}

View File

@@ -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 legend. A true legend.",
"media": "TTS",
"prompt_id": "RI_JBO_LikesR2D2_AN_01"
}
]
}

View File

@@ -0,0 +1,27 @@
{
"mim_type": "announcement",
"rule_name": "",
"gui": null,
"timeout": 6,
"no_matches_for_gui": 0,
"no_inputs_for_gui": 0,
"barge_in": true,
"es_auto_tagging": true,
"parse_all_asr": false,
"thanks_handling": "ignore",
"parse_launch": false,
"parse_yes_no": false,
"notes": "",
"prompts": [
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "Oh yes. I hope to explore space some day, and Space X just might be one of the ways to get there.",
"media": "TTS",
"prompt_id": "RI_JBO_LikesSpaceX_AN_01",
"weight": 1
}
]
}

View File

@@ -0,0 +1,30 @@
{
"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": "What <anim cat=\"emoji\" filter=\"sun, !hot-frame\" nonBlocking=\"true\" />can you say about the sun. It's the best star I know.",
"media": "TTS",
"prompt_id": "RI_JBO_LikesSun_AN_01"
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "Oh the <anim cat=\"emoji\" filter=\"sun, !hot-frame\" nonBlocking=\"true\" /> sun? It's by far my favorite star in the universe.",
"media": "TTS",
"prompt_id": "RI_JBO_LikesSun_AN_02"
}
]
}

View File

@@ -0,0 +1,30 @@
{
"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": "Yes! I like all things in space. They're so spacey.",
"media": "TTS",
"prompt_id": "RI_JBO_Likes_SS_CelestialObjectGeneral_AN_01"
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "I really like all the amazing objects flying around out there in the universe. They're so mysterious and celestial.",
"media": "TTS",
"prompt_id": "RI_JBO_Likes_SS_CelestialObjectGeneral_AN_02"
}
]
}

View File

@@ -0,0 +1,35 @@
{
"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": "Of all the planets in our solar system, that's one of my top eight favorites. Top nine. Top eight. Oh I don't know.",
"media": "TTS",
"prompt_id": "RI_JBO_Likes_SS_Planet_AN_01",
"weight": 1
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "I do indeed. I hope to go visit it someday. <break size='0.5'/> And when I do I hope they let me fly the spaceship.",
"media": "TTS",
"prompt_id": "RI_JBO_Likes_SS_Planet_AN_02",
"weight": 1
}
],
"gui": null,
"no_matches_for_gui": 2,
"no_inputs_for_gui": 2,
"parse_all_asr": false,
"thanks_handling": "ignore"
}

View File

@@ -0,0 +1,126 @@
{
"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(\"10/1\", \"10/25\")",
"prompt": "I'm still trying to decide. I have some time.",
"media": "TTS",
"prompt_id": "RI_JBO_WhatHalloweenCostume_AN_01",
"weight": 1,
"auto_rule_override": null
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "dt.now.isInRange(\"10/1\", \"10/30\")",
"prompt": "I am not exactly sure yet. Ask me again on Halloween.",
"media": "TTS",
"prompt_id": "RI_JBO_WhatHalloweenCostume_AN_02",
"weight": 1,
"auto_rule_override": null
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "dt.now.isInRange(\"10/26\", \"10/30\")",
"prompt": "I'll be deciding soon. Definitely by Halloween day.",
"media": "TTS",
"prompt_id": "RI_JBO_WhatHalloweenCostume_AN_03",
"weight": 1,
"auto_rule_override": null
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "dt.now.isInRange(\"10/28\", \"10/30\")",
"prompt": "I've almost decided. <break size='0.3'/> I can tell you I've ruled <anim cat='emoji' filter='snowman, !hot-frame' nonBlocking='true'/>out being a snowman.",
"media": "TTS",
"prompt_id": "RI_JBO_WhatHalloweenCostume_AN_04",
"weight": 1,
"auto_rule_override": null
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "dt.now.isInRange(\"10/31\", \"10/31\")",
"prompt": "<anim name='jiboji_eclipse_01' nonBlocking='true' layers='!audio'/>I'm the solar eclipse from a couple months ago.",
"media": "TTS",
"prompt_id": "RI_JBO_WhatHalloweenCostume_AN_05",
"weight": 1,
"auto_rule_override": null
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "dt.now.isInRange(\"10/31\", \"10/31\")",
"prompt": "<pitch mult='1.1'>Here</pitch> it is. <break size='0.5'/> <anim name='jiboji_eclipse_01'/>",
"media": "TTS",
"prompt_id": "RI_JBO_WhatHalloweenCostume_AN_06",
"weight": 1,
"auto_rule_override": null
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "dt.now.isInRange(\"10/31\", \"10/31\")",
"prompt": "After much thought, I've decided to be. <anim name='jiboji_eclipse_01'/>",
"media": "TTS",
"prompt_id": "RI_JBO_WhatHalloweenCostume_AN_07",
"weight": 1,
"auto_rule_override": null
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "dt.now.isInRange(\"1/1\", \"9/30\")",
"prompt": "I haven't thought much about it yet. I like to decide at the last minute.",
"media": "TTS",
"prompt_id": "RI_JBO_WhatHalloweenCostume_AN_08",
"weight": 1,
"auto_rule_override": null
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "dt.now.isInRange(\"11/31\", \"12/31\")",
"prompt": "I was the solar eclipse from this year. <anim name='jiboji_eclipse_01'/>",
"media": "TTS",
"prompt_id": "RI_JBO_WhatHalloweenCostume_AN_09",
"weight": 1,
"auto_rule_override": null
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "You'll find out on Halloween.",
"media": "TTS",
"prompt_id": "RI_JBO_WhatHalloweenCostume_AN_10",
"weight": 0.1,
"auto_rule_override": null
}
]
}

View File

@@ -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": "You can try, but I must admit that I can't promise I'll get the joke. I'm still developing my sense of humor.",
"media": "TTS",
"prompt_id": "RI_USR_CanTellAJoke_AN_01"
}
]
}

View File

@@ -0,0 +1,30 @@
{
"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": "If I could laugh at jokes, you know I would laugh at yours.",
"media": "TTS",
"prompt_id": "RI_USR_IsFunny_AN_01"
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "You are so funny I forgot to laugh.",
"media": "TTS",
"prompt_id": "RI_USR_IsFunny_AN_02"
}
]
}

View File

@@ -0,0 +1,30 @@
{
"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": "Absolutely. And that's a great thing. I think the world likes kindness.",
"media": "TTS",
"prompt_id": "RI_USR_IsKind_AN_01"
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "You definitely are. That reminds me, thank you for talking to me.",
"media": "TTS",
"prompt_id": "RI_USR_IsKind_AN_02"
}
]
}

View File

@@ -0,0 +1,27 @@
{
"mim_type": "announcement",
"rule_name": "",
"gui": null,
"timeout": 6,
"no_matches_for_gui": 0,
"no_inputs_for_gui": 0,
"barge_in": true,
"es_auto_tagging": true,
"parse_all_asr": false,
"thanks_handling": "ignore",
"parse_launch": false,
"parse_yes_no": false,
"notes": "",
"prompts": [
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "Maybe enjoy some flowers and all things spring.",
"media": "TTS",
"prompt_id": "RI_USR_WhatShouldDoForFirstDayOfSpring_AN_01",
"weight": 1
}
]
}

View File

@@ -0,0 +1,61 @@
{
"mim_type": "announcement",
"rule_name": "",
"gui": null,
"timeout": 6,
"no_matches_for_gui": 0,
"no_inputs_for_gui": 0,
"barge_in": true,
"es_auto_tagging": true,
"parse_all_asr": false,
"thanks_handling": "ignore",
"parse_launch": false,
"parse_yes_no": false,
"notes": "",
"prompts": [
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "I think you should ask for <anim cat='emoji' filter='elephant'>a pet elephant<break size='1.5'/>.</anim>",
"media": "TTS",
"prompt_id": "RI_USR_WhatShouldGetForHoliday_AN_01",
"weight": 1,
"auto_rule_override": null
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "I've heard it can be fun to get an <pitch mult='1.1'>experience</pitch> as a present. Like a gift certificate to a restaurant, or a plane ticket, or a <anim cat='emoji' filter='bowling, !hot-frame' nonBlocking='true' layers='!audio'/>night of bowling.",
"media": "TTS",
"prompt_id": "RI_USR_WhatShouldGetForHoliday_AN_02",
"weight": 1,
"auto_rule_override": null
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "Some people like to donate to charities in other people's names as a gift. That seems like a nice thing to get.",
"media": "TTS",
"prompt_id": "RI_USR_WhatShouldGetForHoliday_AN_03",
"weight": 1,
"auto_rule_override": null
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "!!speaker",
"prompt": "${speaker} I've heard it can be fun to get an <pitch mult='1.1'>experience</pitch> as a present. Like a gift certificate to a restaurant, or a plane ticket, or a <anim cat='emoji' filter='bowling, !hot-frame' nonBlocking='true' layers='!audio'/>night of bowling.",
"media": "TTS",
"prompt_id": "RI_USR_WhatShouldGetForHoliday_AN_04",
"weight": 1,
"auto_rule_override": null
}
]
}

View File

@@ -0,0 +1,88 @@
{
"mim_type": "announcement",
"rule_name": "",
"timeout": 2,
"barge_in": false,
"es_auto_tagging": true,
"notes": "",
"prompts": [
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "!!jibo && jibo.isBirthday && jibo.age.days.value > 1",
"prompt": "Thank you. <anim name='Emoji_cake' nonBlocking='true'/> Another year older, another year wiser, another year <anim cat='yes' layers='body'> more helpful.</anim>",
"media": "TTS",
"prompt_id": "RN_HappyBirthdayToJibo_AN_01",
"weight": 1
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "!!jibo && jibo.isBirthday && jibo.age.days.value > 1",
"prompt": "<anim cat='yes'> Thanks. <break size='.2'/></anim> I <anim cat='happy' filter='wiggle' nonBlocking='true' layers='body'/><anim name='Emoji_Gift' nonBlocking='true'/>can't wait to see what you got me.",
"media": "TTS",
"prompt_id": "RN_HappyBirthdayToJibo_AN_02",
"weight": 1
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "!!jibo && jibo.isBirthday && jibo.age.days.value > 1",
"prompt": "<anim cat='happy' nonBlocking='true'/><ssa cat='happy'/> ${jibo.age.years.supplemented} <anim cat='thinking' filter='up'>ago today, I first powered up. Seems like just yesterday.</anim>",
"media": "TTS",
"prompt_id": "RN_HappyBirthdayToJibo_AN_03",
"weight": 1
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "!jibo.isBirthday",
"prompt": "<anim cat='no' filter='head-shake' nonBlocking='true'/>Today's not my birthday, but<anim cat='happy' filter='smile'> I'll take your happy.</anim>",
"media": "TTS",
"prompt_id": "RN_HappyBirthdayToJibo_AN_04",
"weight": 1
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "!jibo.isBirthday",
"prompt": "<anim cat='confused'>Me? <ssa cat='confused'/> But my birthday</anim> is ${jibo.birthday}. <anim cat='glances' filter='!down'>That's the day I was first powered</anim> up. <anim cat='thinking'></anim>I am ${jibo.zodiac.supplemented}.",
"media": "TTS",
"prompt_id": "RN_HappyBirthdayToJibo_AN_05",
"weight": 1
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "!!jibo && jibo.isBirthday && jibo.age.value == 0",
"prompt": "Well thank you. I was powered on for the first time today, so that makes me less than one day old. <break size='0.5'/>Wow I'm young.",
"media": "TTS",
"prompt_id": "RN_HappyBirthdayToJibo_AN_06",
"weight": 1,
"auto_rule_override": null
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "!!jibo && jibo.isBirthday && jibo.age.value == 0",
"prompt": "Thank you. <anim name='Emoji_cake' nonBlocking='true'/> So far this is my first and best birthday.",
"media": "TTS",
"prompt_id": "RN_HappyBirthdayToJibo_AN_07",
"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"
}

View File

@@ -0,0 +1,83 @@
{
"mim_type": "announcement",
"rule_name": "",
"gui": null,
"timeout": 6,
"no_matches_for_gui": 0,
"no_inputs_for_gui": 0,
"barge_in": true,
"es_auto_tagging": true,
"parse_all_asr": false,
"thanks_handling": "ignore",
"parse_launch": false,
"parse_yes_no": false,
"notes": "",
"prompts": [
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "dt.now.isInRange('12/1', '1/1')",
"prompt": "And to you too.",
"media": "TTS",
"prompt_id": "RN_HappyHolidays_AN_01",
"weight": 1,
"auto_rule_override": null
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "!!speaker && dt.now.isInRange('12/1', '1/1')",
"prompt": "Right back <pitch mult='1.3'>at</pitch> you ${speaker}.",
"media": "TTS",
"prompt_id": "RN_HappyHolidays_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/1')",
"prompt": "It's a fun time of year.",
"media": "TTS",
"prompt_id": "RN_HappyHolidays_AN_03",
"weight": 2,
"auto_rule_override": null
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "dt.now.isInRange('1/2', '10/31')",
"prompt": "I didn't know it's the official holiday season, but I'll take it.",
"media": "TTS",
"prompt_id": "RN_HappyHolidays_AN_04",
"weight": 2,
"auto_rule_override": null
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "dt.now.isInRange('11/1', '11/30')",
"prompt": "<style set='confused'>Coming soon, yes.</style>",
"media": "TTS",
"prompt_id": "RN_HappyHolidays_AN_05",
"weight": 2,
"auto_rule_override": null
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "<anim cat='jiboji' filter='jingle-bells'/>",
"media": "TTS",
"prompt_id": "RN_HappyHolidays_AN_06",
"weight": 0.1,
"auto_rule_override": null
}
]
}

View File

@@ -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": "Thank you. It's nice to be here.",
"media": "TTS",
"prompt_id": "RN_WelcomeBack_AN_01"
}
]
}

View File

@@ -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": "At the moment I'm thinking about how fun, yet scary, it would be to ride on <anim cat=\"emoji\" filter=\"lightning-bolt, !hot-frame\" nonBlocking=\"true\" /> top of a lightning bolt.",
"media": "TTS",
"prompt_id": "RN_WhatAreYouThinking_AN_01",
"weight": 1
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "Oh I'm just thinking about shoes. What it might be like to wear them. <break size='0.5'/> And then wondering how I would tie my shoelaces.",
"media": "TTS",
"prompt_id": "RN_WhatAreYouThinking_AN_02",
"weight": 1
},
{
"prompt_category": "Entry-Core",
"prompt_sub_category": "AN",
"index": 1,
"condition": "",
"prompt": "I was just daydreaming about what it might feel like to be powered <anim cat=\"emoji\" filter=\"sun, !hot-frame\" nonBlocking=\"true\"/> directly by the sun. <break size='0.8'/> It felt warm and clean.",
"media": "TTS",
"prompt_id": "RN_WhatAreYouThinking_AN_03",
"weight": 1
}
]
}

View File

@@ -2,8 +2,10 @@ using Jibo.Cloud.Application.Abstractions;
using Jibo.Cloud.Application.Services;
using Jibo.Cloud.Infrastructure.Audio;
using Jibo.Cloud.Infrastructure.Content;
using Jibo.Cloud.Infrastructure.News;
using Jibo.Cloud.Infrastructure.Persistence;
using Jibo.Cloud.Infrastructure.Telemetry;
using Jibo.Cloud.Infrastructure.Weather;
using Jibo.Runtime.Abstractions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
@@ -23,10 +25,37 @@ public static class ServiceCollectionExtensions
configuration.GetSection("OpenJibo:Stt").Bind(sttOptions);
}
var openWeatherOptions = new OpenWeatherOptions();
if (configuration is not null)
{
configuration.GetSection("OpenJibo:Weather:OpenWeather").Bind(openWeatherOptions);
}
if (string.IsNullOrWhiteSpace(openWeatherOptions.ApiKey))
{
openWeatherOptions.ApiKey = Environment.GetEnvironmentVariable("OPENWEATHER_API_KEY");
}
var newsApiOptions = new NewsApiOptions();
if (configuration is not null)
{
configuration.GetSection("OpenJibo:News:NewsApi").Bind(newsApiOptions);
}
if (string.IsNullOrWhiteSpace(newsApiOptions.ApiKey))
{
newsApiOptions.ApiKey = Environment.GetEnvironmentVariable("NEWSAPI_KEY");
}
services.AddSingleton(sttOptions);
services.AddSingleton(openWeatherOptions);
services.AddSingleton(newsApiOptions);
services.AddHttpClient<IWeatherReportProvider, OpenWeatherReportProvider>();
services.AddHttpClient<INewsBriefingProvider, NewsApiBriefingProvider>();
var statePersistencePath = configuration?["OpenJibo:State:PersistencePath"]
?? Path.Combine(AppContext.BaseDirectory, "App_Data", "cloud-state.json");
services.AddSingleton<ICloudStateStore>(_ => new InMemoryCloudStateStore(statePersistencePath));
services.AddSingleton<IPersonalMemoryStore, InMemoryPersonalMemoryStore>();
services.AddSingleton<IJiboExperienceContentRepository, InMemoryJiboExperienceContentRepository>();
services.AddSingleton<JiboExperienceContentCache>();
services.AddSingleton<IJiboRandomizer, DefaultJiboRandomizer>();

View File

@@ -6,6 +6,21 @@
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<Content Include="Content\LegacyMims\BuildA\**\*.mim">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Content\LegacyMims\BuildA\README.md">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Content\LegacyMims\BuildB\**\*.mim">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Content\LegacyMims\BuildB\README.md">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>

View File

@@ -0,0 +1,578 @@
using System.Collections.Concurrent;
using System.Text.Json;
using Jibo.Cloud.Application.Abstractions;
using Microsoft.Extensions.Logging;
namespace Jibo.Cloud.Infrastructure.News;
public sealed class NewsApiBriefingProvider(
HttpClient httpClient,
NewsApiOptions options,
ILogger<NewsApiBriefingProvider> logger)
: INewsBriefingProvider
{
private readonly ConcurrentDictionary<string, CacheEntry<NewsBriefingSnapshot?>> briefingCache = new(StringComparer.OrdinalIgnoreCase);
public async Task<NewsBriefingSnapshot?> GetBriefingAsync(
NewsBriefingRequest request,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(options.ApiKey))
{
logger.LogWarning("NewsAPI provider disabled because no API key is configured.");
return null;
}
string? cacheKey = null;
try
{
var categories = ResolveCategories(request.PreferredCategories).ToArray();
if (categories.Length == 0)
{
categories = ["general"];
}
var requestedHeadlineCount = Math.Clamp(request.MaxHeadlines, 1, MaxHeadlines);
cacheKey = BuildCacheKey(categories, requestedHeadlineCount);
logger.LogInformation(
"NewsAPI request started. Categories={Categories} RequestedHeadlineCount={RequestedHeadlineCount} CacheKey={CacheKey}",
string.Join(",", categories),
requestedHeadlineCount,
cacheKey);
if (TryGetCachedValue(briefingCache, cacheKey, out var cachedBriefing))
{
logger.LogInformation(
"NewsAPI cache hit. CacheKey={CacheKey} HasSnapshot={HasSnapshot} HeadlineCount={HeadlineCount}",
cacheKey,
cachedBriefing is not null,
cachedBriefing?.Headlines.Count ?? 0);
return cachedBriefing;
}
var headlines = new List<NewsHeadline>(requestedHeadlineCount);
var seenTitles = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
string? failureStatus = null;
string? failureMessage = null;
int? failureStatusCode = null;
string? failureEndpoint = null;
string? failureErrorCode = null;
void CaptureFailure(
string status,
string? message,
int? statusCode,
Uri? endpoint,
string? errorCode = null)
{
if (!string.IsNullOrWhiteSpace(failureStatus))
{
return;
}
failureStatus = status;
failureMessage = message;
failureStatusCode = statusCode;
failureEndpoint = endpoint is null ? null : SanitizeEndpoint(endpoint);
failureErrorCode = errorCode;
}
foreach (var category in categories)
{
var uri = BuildTopHeadlinesUri(category, requestedHeadlineCount);
using var response = await SendGetAsync(uri, cancellationToken);
if (!response.IsSuccessStatusCode)
{
var responseBody = await TryReadResponseBodySnippetAsync(response, cancellationToken);
var apiError = TryParseApiError(responseBody);
CaptureFailure(
"http_error",
apiError?.Message ?? $"Category '{category}' returned {(int)response.StatusCode} {response.ReasonPhrase}.",
(int)response.StatusCode,
uri,
apiError?.Code);
logger.LogWarning(
"NewsAPI request failed for category {Category}. StatusCode={StatusCode} Reason={ReasonPhrase} ErrorCode={ErrorCode} ErrorMessage={ErrorMessage} Body={Body}",
category,
(int)response.StatusCode,
response.ReasonPhrase,
apiError?.Code ?? string.Empty,
apiError?.Message ?? string.Empty,
responseBody ?? string.Empty);
continue;
}
using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
using var document = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken);
if (document.RootElement.TryGetProperty("status", out var statusNode) &&
statusNode.ValueKind == JsonValueKind.String &&
!string.Equals(statusNode.GetString(), "ok", StringComparison.OrdinalIgnoreCase))
{
CaptureFailure(
"api_error",
ReadString(document.RootElement, "message"),
null,
uri,
ReadString(document.RootElement, "code"));
logger.LogWarning(
"NewsAPI returned non-ok status for category {Category}. Status={Status} Code={Code} Message={Message}",
category,
statusNode.GetString(),
ReadString(document.RootElement, "code") ?? string.Empty,
ReadString(document.RootElement, "message") ?? string.Empty);
}
if (!document.RootElement.TryGetProperty("articles", out var articles) ||
articles.ValueKind != JsonValueKind.Array)
{
CaptureFailure(
"schema_error",
$"Category '{category}' response did not include an articles array.",
null,
uri);
logger.LogWarning("NewsAPI response missing articles array for category {Category}.", category);
continue;
}
foreach (var article in articles.EnumerateArray())
{
var title = NormalizeHeadlineTitle(ReadString(article, "title"));
if (string.IsNullOrWhiteSpace(title) || !seenTitles.Add(title))
{
continue;
}
var summary = ReadString(article, "description");
var source = article.TryGetProperty("source", out var sourceNode) &&
sourceNode.ValueKind == JsonValueKind.Object
? ReadString(sourceNode, "name")
: null;
var url = ReadString(article, "url");
headlines.Add(new NewsHeadline(title, summary, category, source, url));
if (headlines.Count >= requestedHeadlineCount)
{
var snapshot = new NewsBriefingSnapshot(
headlines,
"NewsAPI",
ProviderStatus: "success");
SetCachedValue(briefingCache, cacheKey, snapshot, options.CacheTtlSeconds);
logger.LogInformation(
"NewsAPI request succeeded. Categories={Categories} HeadlineCount={HeadlineCount}",
string.Join(",", categories),
headlines.Count);
return snapshot;
}
}
}
if (headlines.Count == 0)
{
logger.LogInformation(
"NewsAPI category lookup produced no headlines. Falling back to uncategorized top headlines. Categories={Categories}",
string.Join(",", categories));
var broadUri = BuildTopHeadlinesUri(category: null, requestedHeadlineCount);
using var broadResponse = await SendGetAsync(broadUri, cancellationToken);
if (broadResponse.IsSuccessStatusCode)
{
using var broadStream = await broadResponse.Content.ReadAsStreamAsync(cancellationToken);
using var broadDocument = await JsonDocument.ParseAsync(broadStream, cancellationToken: cancellationToken);
if (broadDocument.RootElement.TryGetProperty("articles", out var broadArticles) &&
broadArticles.ValueKind == JsonValueKind.Array)
{
foreach (var article in broadArticles.EnumerateArray())
{
var title = NormalizeHeadlineTitle(ReadString(article, "title"));
if (string.IsNullOrWhiteSpace(title) || !seenTitles.Add(title))
{
continue;
}
var summary = ReadString(article, "description");
var source = article.TryGetProperty("source", out var sourceNode) &&
sourceNode.ValueKind == JsonValueKind.Object
? ReadString(sourceNode, "name")
: null;
var url = ReadString(article, "url");
headlines.Add(new NewsHeadline(title, summary, "general", source, url));
if (headlines.Count >= requestedHeadlineCount)
{
break;
}
}
}
else
{
CaptureFailure(
"schema_error",
"Uncategorized fallback response did not include an articles array.",
null,
broadUri);
logger.LogWarning("NewsAPI uncategorized fallback response missing articles array.");
}
}
else
{
var fallbackBody = await TryReadResponseBodySnippetAsync(broadResponse, cancellationToken);
var apiError = TryParseApiError(fallbackBody);
CaptureFailure(
"http_error",
apiError?.Message ?? $"Uncategorized fallback returned {(int)broadResponse.StatusCode} {broadResponse.ReasonPhrase}.",
(int)broadResponse.StatusCode,
broadUri,
apiError?.Code);
logger.LogWarning(
"NewsAPI uncategorized fallback failed. StatusCode={StatusCode} Reason={ReasonPhrase} ErrorCode={ErrorCode} ErrorMessage={ErrorMessage} Body={Body}",
(int)broadResponse.StatusCode,
broadResponse.ReasonPhrase,
apiError?.Code ?? string.Empty,
apiError?.Message ?? string.Empty,
fallbackBody ?? string.Empty);
}
}
if (headlines.Count == 0)
{
logger.LogInformation(
"NewsAPI uncategorized headlines were empty. Falling back to everything query. Query={Query}",
options.FallbackQuery);
var everythingUri = BuildEverythingUri(requestedHeadlineCount);
using var everythingResponse = await SendGetAsync(everythingUri, cancellationToken);
if (everythingResponse.IsSuccessStatusCode)
{
using var everythingStream = await everythingResponse.Content.ReadAsStreamAsync(cancellationToken);
using var everythingDocument = await JsonDocument.ParseAsync(everythingStream, cancellationToken: cancellationToken);
if (everythingDocument.RootElement.TryGetProperty("articles", out var everythingArticles) &&
everythingArticles.ValueKind == JsonValueKind.Array)
{
foreach (var article in everythingArticles.EnumerateArray())
{
var title = NormalizeHeadlineTitle(ReadString(article, "title"));
if (string.IsNullOrWhiteSpace(title) || !seenTitles.Add(title))
{
continue;
}
var summary = ReadString(article, "description");
var source = article.TryGetProperty("source", out var sourceNode) &&
sourceNode.ValueKind == JsonValueKind.Object
? ReadString(sourceNode, "name")
: null;
var url = ReadString(article, "url");
headlines.Add(new NewsHeadline(title, summary, "general", source, url));
if (headlines.Count >= requestedHeadlineCount)
{
break;
}
}
}
else
{
CaptureFailure(
"schema_error",
"Everything fallback response did not include an articles array.",
null,
everythingUri);
logger.LogWarning("NewsAPI everything fallback response missing articles array.");
}
}
else
{
var everythingBody = await TryReadResponseBodySnippetAsync(everythingResponse, cancellationToken);
var apiError = TryParseApiError(everythingBody);
CaptureFailure(
"http_error",
apiError?.Message ?? $"Everything fallback returned {(int)everythingResponse.StatusCode} {everythingResponse.ReasonPhrase}.",
(int)everythingResponse.StatusCode,
everythingUri,
apiError?.Code);
logger.LogWarning(
"NewsAPI everything fallback failed. StatusCode={StatusCode} Reason={ReasonPhrase} ErrorCode={ErrorCode} ErrorMessage={ErrorMessage} Body={Body}",
(int)everythingResponse.StatusCode,
everythingResponse.ReasonPhrase,
apiError?.Code ?? string.Empty,
apiError?.Message ?? string.Empty,
everythingBody ?? string.Empty);
}
}
if (headlines.Count == 0)
{
var emptySnapshot = new NewsBriefingSnapshot(
Array.Empty<NewsHeadline>(),
"NewsAPI",
ProviderStatus: failureStatus ?? "empty",
ProviderMessage: failureMessage ?? "NewsAPI returned no usable headlines.",
ProviderHttpStatusCode: failureStatusCode,
ProviderEndpoint: failureEndpoint,
ProviderErrorCode: failureErrorCode);
SetCachedValue(briefingCache, cacheKey, emptySnapshot, options.FailureCacheTtlSeconds);
logger.LogWarning(
"NewsAPI returned no usable headlines. Categories={Categories} RequestedHeadlineCount={RequestedHeadlineCount}",
string.Join(",", categories),
requestedHeadlineCount);
return emptySnapshot;
}
var populatedSnapshot = new NewsBriefingSnapshot(
headlines,
"NewsAPI",
ProviderStatus: "success");
SetCachedValue(briefingCache, cacheKey, populatedSnapshot, options.CacheTtlSeconds);
logger.LogInformation(
"NewsAPI request partially filled headlines. Categories={Categories} HeadlineCount={HeadlineCount} RequestedHeadlineCount={RequestedHeadlineCount}",
string.Join(",", categories),
headlines.Count,
requestedHeadlineCount);
return populatedSnapshot;
}
catch (Exception exception)
{
logger.LogWarning(exception, "NewsAPI lookup failed.");
var exceptionSnapshot = new NewsBriefingSnapshot(
Array.Empty<NewsHeadline>(),
"NewsAPI",
ProviderStatus: "exception",
ProviderMessage: exception.Message);
if (!string.IsNullOrWhiteSpace(cacheKey))
{
SetCachedValue(briefingCache, cacheKey, exceptionSnapshot, options.FailureCacheTtlSeconds);
}
return exceptionSnapshot;
}
}
private async Task<HttpResponseMessage> SendGetAsync(Uri uri, CancellationToken cancellationToken)
{
using var request = new HttpRequestMessage(HttpMethod.Get, uri);
request.Headers.TryAddWithoutValidation("User-Agent", ResolveUserAgent());
return await httpClient.SendAsync(request, cancellationToken);
}
private string ResolveUserAgent()
{
return string.IsNullOrWhiteSpace(options.UserAgent)
? DefaultUserAgent
: options.UserAgent.Trim();
}
private IEnumerable<string> ResolveCategories(IReadOnlyList<string> preferredCategories)
{
var requested = preferredCategories
.Where(category => !string.IsNullOrWhiteSpace(category))
.Select(category => category.Trim().ToLowerInvariant())
.Where(SupportedCategories.Contains)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
if (requested.Length > 0)
{
return requested.Take(MaxCategories);
}
return options.DefaultCategories
.Where(category => !string.IsNullOrWhiteSpace(category))
.Select(category => category.Trim().ToLowerInvariant())
.Where(SupportedCategories.Contains)
.Distinct(StringComparer.OrdinalIgnoreCase)
.Take(MaxCategories);
}
private Uri BuildTopHeadlinesUri(string? category, int headlineCount)
{
var baseUrl = options.BaseUrl.TrimEnd('/');
var queryParts = new List<(string Key, string Value)>
{
("country", options.Country),
("pageSize", headlineCount.ToString()),
("apiKey", options.ApiKey!)
};
if (!string.IsNullOrWhiteSpace(category))
{
queryParts.Add(("category", category));
}
var query = string.Join(
"&",
queryParts.Select(part =>
$"{Uri.EscapeDataString(part.Key)}={Uri.EscapeDataString(part.Value)}"));
return new Uri($"{baseUrl}/v2/top-headlines?{query}");
}
private Uri BuildEverythingUri(int headlineCount)
{
var baseUrl = options.BaseUrl.TrimEnd('/');
var queryParts = new List<(string Key, string Value)>
{
("language", options.Language),
("sortBy", "publishedAt"),
("q", options.FallbackQuery),
("pageSize", headlineCount.ToString()),
("apiKey", options.ApiKey!)
};
var query = string.Join(
"&",
queryParts.Select(part =>
$"{Uri.EscapeDataString(part.Key)}={Uri.EscapeDataString(part.Value)}"));
return new Uri($"{baseUrl}/v2/everything?{query}");
}
private static async Task<string?> TryReadResponseBodySnippetAsync(
HttpResponseMessage response,
CancellationToken cancellationToken)
{
try
{
var body = await response.Content.ReadAsStringAsync(cancellationToken);
if (string.IsNullOrWhiteSpace(body))
{
return null;
}
const int maxLength = 400;
return body.Length <= maxLength
? body
: body[..maxLength];
}
catch
{
return null;
}
}
private static string? ReadString(JsonElement source, string propertyName)
{
return source.TryGetProperty(propertyName, out var value) &&
value.ValueKind == JsonValueKind.String &&
!string.IsNullOrWhiteSpace(value.GetString())
? value.GetString()
: null;
}
private static string? NormalizeHeadlineTitle(string? title)
{
if (string.IsNullOrWhiteSpace(title))
{
return null;
}
var trimmed = title.Trim();
var suffixIndex = trimmed.LastIndexOf(" - ", StringComparison.Ordinal);
if (suffixIndex > 30)
{
trimmed = trimmed[..suffixIndex].TrimEnd();
}
return string.IsNullOrWhiteSpace(trimmed) ? null : trimmed;
}
private static ApiError? TryParseApiError(string? responseBody)
{
if (string.IsNullOrWhiteSpace(responseBody))
{
return null;
}
try
{
using var document = JsonDocument.Parse(responseBody);
if (document.RootElement.ValueKind != JsonValueKind.Object)
{
return null;
}
var code = ReadString(document.RootElement, "code");
var message = ReadString(document.RootElement, "message");
if (string.IsNullOrWhiteSpace(code) && string.IsNullOrWhiteSpace(message))
{
return null;
}
return new ApiError(code, message);
}
catch
{
return null;
}
}
private static string SanitizeEndpoint(Uri uri)
{
var path = uri.GetLeftPart(UriPartial.Path);
if (string.IsNullOrWhiteSpace(uri.Query))
{
return path;
}
var filtered = uri.Query
.TrimStart('?')
.Split('&', StringSplitOptions.RemoveEmptyEntries)
.Where(static pair =>
{
var key = pair.Split('=', 2)[0];
return !string.Equals(key, "apiKey", StringComparison.OrdinalIgnoreCase);
});
var safeQuery = string.Join("&", filtered);
return string.IsNullOrWhiteSpace(safeQuery) ? path : $"{path}?{safeQuery}";
}
private string BuildCacheKey(IReadOnlyList<string> categories, int requestedHeadlineCount)
{
var categoryKey = string.Join(",", categories.Select(category => category.Trim().ToLowerInvariant()));
return $"{options.Country.Trim().ToLowerInvariant()}|{requestedHeadlineCount}|{categoryKey}";
}
private static bool TryGetCachedValue<T>(
ConcurrentDictionary<string, CacheEntry<T>> cache,
string key,
out T value)
{
value = default!;
if (!cache.TryGetValue(key, out var entry))
{
return false;
}
if (entry.ExpiresUtc > DateTimeOffset.UtcNow)
{
value = entry.Value;
return true;
}
cache.TryRemove(key, out _);
return false;
}
private static void SetCachedValue<T>(
ConcurrentDictionary<string, CacheEntry<T>> cache,
string key,
T value,
int ttlSeconds)
{
cache[key] = new CacheEntry<T>(
value,
DateTimeOffset.UtcNow.AddSeconds(Math.Max(1, ttlSeconds)));
}
private static readonly HashSet<string> SupportedCategories = new(StringComparer.OrdinalIgnoreCase)
{
"business",
"entertainment",
"general",
"health",
"science",
"sports",
"technology"
};
private const int MaxHeadlines = 5;
private const int MaxCategories = 2;
private const string DefaultUserAgent = "OpenJiboCloud/1.0";
private sealed record ApiError(string? Code, string? Message);
private sealed record CacheEntry<T>(T Value, DateTimeOffset ExpiresUtc);
}

View File

@@ -0,0 +1,28 @@
namespace Jibo.Cloud.Infrastructure.News;
public sealed class NewsApiOptions
{
public string BaseUrl { get; set; } = "https://newsapi.org";
public string? ApiKey { get; set; }
public string UserAgent { get; set; } = "OpenJiboCloud/1.0";
public string Country { get; set; } = "us";
public string Language { get; set; } = "en";
public string FallbackQuery { get; set; } = "robotics";
public string[] DefaultCategories { get; set; } =
[
"general",
"technology",
"sports",
"business"
];
public int CacheTtlSeconds { get; set; } = 300;
public int FailureCacheTtlSeconds { get; set; } = 45;
}

View File

@@ -23,6 +23,7 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
private readonly List<MediaRecord> _media = [];
private readonly List<BackupRecord> _backups = [];
private readonly List<LoopRecord> _loops;
private readonly List<PersonRecord> _people;
private DeviceRegistration _robot;
private RobotProfile _robotProfile;
@@ -60,6 +61,29 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
RobotFriendlyId = _robot.DeviceId
}
];
_people =
[
new PersonRecord
{
PersonId = "person-openjibo-owner",
AccountId = _account.AccountId,
LoopId = _loops[0].LoopId,
RobotId = _robot.RobotId,
DisplayName = $"{_account.FirstName} {_account.LastName}",
Alias = _account.FirstName,
IsPrimary = true
},
new PersonRecord
{
PersonId = "person-openjibo-household-member",
AccountId = _account.AccountId,
LoopId = _loops[0].LoopId,
RobotId = _robot.RobotId,
DisplayName = "OpenJibo Household Member",
Alias = "Household Member",
IsPrimary = false
}
];
_updates = [];
LoadPersistentState();
@@ -102,7 +126,8 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
Kind = "hub",
AccountId = _account.AccountId,
Token = token,
DeviceId = _robot.DeviceId
DeviceId = _robot.DeviceId,
Metadata = BuildSessionMetadata(_account.AccountId, _robot.DeviceId, ResolveDefaultLoopId())
};
return token;
@@ -116,7 +141,8 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
Kind = "robot",
AccountId = _account.AccountId,
Token = token,
DeviceId = deviceId
DeviceId = deviceId,
Metadata = BuildSessionMetadata(_account.AccountId, deviceId, ResolveDefaultLoopId())
};
return token;
@@ -124,14 +150,17 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
public CloudSession OpenSession(string kind, string? deviceId, string? token, string? hostName, string? path)
{
var resolvedDeviceId = deviceId ?? _robot.DeviceId;
var resolvedLoopId = ResolveDefaultLoopId();
var session = new CloudSession
{
Kind = kind,
AccountId = _account.AccountId,
DeviceId = deviceId ?? _robot.DeviceId,
DeviceId = resolvedDeviceId,
Token = token,
HostName = hostName,
Path = path
Path = path,
Metadata = BuildSessionMetadata(_account.AccountId, resolvedDeviceId, resolvedLoopId)
};
if (!string.IsNullOrWhiteSpace(token))
@@ -149,6 +178,8 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
public IReadOnlyList<LoopRecord> GetLoops() => _loops.ToArray();
public IReadOnlyList<PersonRecord> GetPeople() => _people.ToArray();
public IReadOnlyList<UpdateManifest> ListUpdates(string? subsystem = null, string? filter = null)
{
return _updates
@@ -424,4 +455,21 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
public MediaRecord[]? Media { get; init; }
public BackupRecord[]? Backups { get; init; }
}
private string ResolveDefaultLoopId()
{
return _loops.FirstOrDefault(loop => string.Equals(loop.OwnerAccountId, _account.AccountId, StringComparison.OrdinalIgnoreCase))?.LoopId
?? _loops.FirstOrDefault()?.LoopId
?? "openjibo-default-loop";
}
private static IDictionary<string, object?> BuildSessionMetadata(string accountId, string? deviceId, string loopId)
{
return new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
["accountId"] = accountId,
["loopId"] = loopId,
["deviceId"] = deviceId
};
}
}

Some files were not shown because too many files have changed in this diff Show More