Add pizza yes-no wiring and Pegasus parser guardrails
This commit is contained in:
@@ -6,7 +6,7 @@ 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.
|
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). The `1.0.19` release shape is detailed in [release-1.0.19-plan.md](release-1.0.19-plan.md), while this file keeps the broader evidence and architecture context.
|
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 Release Snapshot
|
||||||
|
|
||||||
|
|||||||
@@ -460,6 +460,27 @@ Current release theme:
|
|||||||
- what upload metadata must survive for gallery refresh
|
- what upload metadata must survive for gallery refresh
|
||||||
- how to map this cleanly to Blob Storage
|
- how to map this cleanly to Blob Storage
|
||||||
|
|
||||||
|
### Next Up (`2026-05-06`): Dialog Parsing Expansion And Ambiguity Guardrails
|
||||||
|
|
||||||
|
- Status: `ready`
|
||||||
|
- 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
|
||||||
|
- 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
|
## Discovery Queue
|
||||||
|
|
||||||
### 12. Weather As Cloud Report Plus Local Presentation
|
### 12. Weather As Cloud Report Plus Local Presentation
|
||||||
@@ -661,7 +682,7 @@ For `1.0.19`:
|
|||||||
2. Expand memory-backed personal facts with tenant-scoped storage (beyond the first birthday/preferences foundation) - implemented
|
2. Expand memory-backed personal facts with tenant-scoped storage (beyond the first birthday/preferences foundation) - implemented
|
||||||
3. Proactivity selector baseline with source-backed first offers - implemented
|
3. Proactivity selector baseline with source-backed first offers - implemented
|
||||||
4. Weather report-skill launch compatibility - implemented
|
4. Weather report-skill launch compatibility - implemented
|
||||||
5. Dialog parsing expansion and ambiguity guardrails
|
5. Dialog parsing expansion and ambiguity guardrails - queued next as of `2026-05-06`
|
||||||
6. Holidays and seasonal personality behavior built on the new memory/proactivity foundation
|
6. Holidays and seasonal personality behavior built on the new memory/proactivity foundation
|
||||||
7. Durable memory persistence path (multi-tenant backing store)
|
7. Durable memory persistence path (multi-tenant backing store)
|
||||||
8. Update, backup, and restore proof
|
8. Update, backup, and restore proof
|
||||||
|
|||||||
@@ -105,9 +105,34 @@ The fifth delivered slice adds provider-backed weather content while preserving
|
|||||||
- simple location extraction is supported for phrasing like `what's the weather in Chicago tomorrow`
|
- simple location extraction is supported for phrasing like `what's the weather in Chicago tomorrow`
|
||||||
- provider config supports appsettings and `OPENWEATHER_API_KEY` environment fallback for deployment
|
- provider config supports appsettings and `OPENWEATHER_API_KEY` environment fallback for deployment
|
||||||
|
|
||||||
|
## 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)
|
||||||
|
|
||||||
|
## Next Queued Task (`2026-05-06`)
|
||||||
|
|
||||||
|
Queued next `1.0.19` implementation task:
|
||||||
|
|
||||||
|
- 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
|
||||||
|
|
||||||
## Next Slices
|
## Next Slices
|
||||||
|
|
||||||
1. Dialog parsing expansion (more phrase variants, ambiguity handling, and transcript-to-intent guardrails)
|
1. Dialog parsing expansion (queued next as of `2026-05-06`; more phrase variants, ambiguity handling, and transcript-to-intent guardrails)
|
||||||
2. Holidays and seasonal personality slice beyond pizza day (time-scoped content backed by memory/proactivity path)
|
2. Holidays and seasonal personality slice beyond pizza day (time-scoped content backed by memory/proactivity path)
|
||||||
3. Durable memory persistence path (swap in provider-backed multi-tenant storage while preserving behavior contracts)
|
3. Durable memory persistence path (swap in provider-backed multi-tenant storage while preserving behavior contracts)
|
||||||
4. Update/backup/restore end-to-end proof (operator-run and documented)
|
4. Update/backup/restore end-to-end proof (operator-run and documented)
|
||||||
|
|||||||
104
OpenJibo/docs/system-diagram-alignment.md
Normal file
104
OpenJibo/docs/system-diagram-alignment.md
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
# 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-06`
|
||||||
|
|
||||||
|
## 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`
|
||||||
|
|
||||||
|
## 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 |
|
||||||
|
| `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 |
|
||||||
|
|
||||||
|
## 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.
|
||||||
@@ -2102,18 +2102,7 @@ public sealed class JiboInteractionService(
|
|||||||
{
|
{
|
||||||
var normalized = NormalizeCommandPhrase(transcript);
|
var normalized = NormalizeCommandPhrase(transcript);
|
||||||
|
|
||||||
var directMappings = new (string Prefix, PersonalAffinity Affinity)[]
|
foreach (var (prefix, affinity) in PegasusUserAffinitySetPrefixes)
|
||||||
{
|
|
||||||
("i love ", PersonalAffinity.Love),
|
|
||||||
("i like ", PersonalAffinity.Like),
|
|
||||||
("i dislike ", PersonalAffinity.Dislike),
|
|
||||||
("i hate ", PersonalAffinity.Dislike),
|
|
||||||
("i don t like ", PersonalAffinity.Dislike),
|
|
||||||
("i dont like ", PersonalAffinity.Dislike),
|
|
||||||
("i do not like ", PersonalAffinity.Dislike)
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (var (prefix, affinity) in directMappings)
|
|
||||||
{
|
{
|
||||||
if (!normalized.StartsWith(prefix, StringComparison.Ordinal))
|
if (!normalized.StartsWith(prefix, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
@@ -2133,17 +2122,8 @@ public sealed class JiboInteractionService(
|
|||||||
private static (string Item, PersonalAffinity? ExpectedAffinity)? TryExtractAffinityLookup(string transcript)
|
private static (string Item, PersonalAffinity? ExpectedAffinity)? TryExtractAffinityLookup(string transcript)
|
||||||
{
|
{
|
||||||
var normalized = NormalizeCommandPhrase(transcript);
|
var normalized = NormalizeCommandPhrase(transcript);
|
||||||
var expectationPrefixes = new (string Prefix, PersonalAffinity? ExpectedAffinity)[]
|
|
||||||
{
|
|
||||||
("do i love ", PersonalAffinity.Love),
|
|
||||||
("do i like ", PersonalAffinity.Like),
|
|
||||||
("do i dislike ", PersonalAffinity.Dislike),
|
|
||||||
("do i hate ", PersonalAffinity.Dislike),
|
|
||||||
("how do i feel about ", null),
|
|
||||||
("what do i think about ", null)
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (var (prefix, expectedAffinity) in expectationPrefixes)
|
foreach (var (prefix, expectedAffinity) in PegasusUserAffinityLookupPrefixes)
|
||||||
{
|
{
|
||||||
if (!normalized.StartsWith(prefix, StringComparison.Ordinal))
|
if (!normalized.StartsWith(prefix, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
@@ -2826,7 +2806,47 @@ public sealed class JiboInteractionService(
|
|||||||
private static readonly string[] PreferenceReverseMarkers =
|
private static readonly string[] PreferenceReverseMarkers =
|
||||||
[
|
[
|
||||||
" is my favorite ",
|
" is my favorite ",
|
||||||
" is my favourite "
|
" is my favourite ",
|
||||||
|
" are my favorite ",
|
||||||
|
" are my favourite "
|
||||||
|
];
|
||||||
|
|
||||||
|
// Directly imported from Pegasus parser intent phrase families:
|
||||||
|
// userLikesThing / userDislikesThing / doesUserLikeThing / doesUserDislikeThing.
|
||||||
|
private static readonly (string Prefix, PersonalAffinity Affinity)[] PegasusUserAffinitySetPrefixes =
|
||||||
|
[
|
||||||
|
("i love ", PersonalAffinity.Love),
|
||||||
|
("i like ", PersonalAffinity.Like),
|
||||||
|
("i enjoy ", PersonalAffinity.Like),
|
||||||
|
("i do like ", PersonalAffinity.Like),
|
||||||
|
("i dislike ", PersonalAffinity.Dislike),
|
||||||
|
("i hate ", PersonalAffinity.Dislike),
|
||||||
|
("i don t like ", PersonalAffinity.Dislike),
|
||||||
|
("i dont like ", PersonalAffinity.Dislike),
|
||||||
|
("i do not like ", PersonalAffinity.Dislike),
|
||||||
|
("i don t enjoy ", PersonalAffinity.Dislike),
|
||||||
|
("i dont enjoy ", PersonalAffinity.Dislike),
|
||||||
|
("i do not enjoy ", PersonalAffinity.Dislike),
|
||||||
|
("i don t love ", PersonalAffinity.Dislike),
|
||||||
|
("i dont love ", PersonalAffinity.Dislike),
|
||||||
|
("i do not love ", PersonalAffinity.Dislike),
|
||||||
|
("i can t stand ", PersonalAffinity.Dislike),
|
||||||
|
("i cant stand ", PersonalAffinity.Dislike),
|
||||||
|
("i despise ", PersonalAffinity.Dislike),
|
||||||
|
("i detest ", PersonalAffinity.Dislike)
|
||||||
|
];
|
||||||
|
|
||||||
|
private static readonly (string Prefix, PersonalAffinity? ExpectedAffinity)[] PegasusUserAffinityLookupPrefixes =
|
||||||
|
[
|
||||||
|
("do i love ", PersonalAffinity.Love),
|
||||||
|
("do i like ", PersonalAffinity.Like),
|
||||||
|
("do i enjoy ", PersonalAffinity.Like),
|
||||||
|
("do i dislike ", PersonalAffinity.Dislike),
|
||||||
|
("do i hate ", PersonalAffinity.Dislike),
|
||||||
|
("do i despise ", PersonalAffinity.Dislike),
|
||||||
|
("do i detest ", PersonalAffinity.Dislike),
|
||||||
|
("how do i feel about ", null),
|
||||||
|
("what do i think about ", null)
|
||||||
];
|
];
|
||||||
|
|
||||||
private static readonly string[] PizzaPreferenceCategories =
|
private static readonly string[] PizzaPreferenceCategories =
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
var isVolumeControl = string.Equals(plan.IntentName, "volume_up", StringComparison.OrdinalIgnoreCase) ||
|
var isVolumeControl = string.Equals(plan.IntentName, "volume_up", StringComparison.OrdinalIgnoreCase) ||
|
||||||
string.Equals(plan.IntentName, "volume_down", StringComparison.OrdinalIgnoreCase) ||
|
string.Equals(plan.IntentName, "volume_down", StringComparison.OrdinalIgnoreCase) ||
|
||||||
string.Equals(plan.IntentName, "volume_to_value", StringComparison.OrdinalIgnoreCase);
|
string.Equals(plan.IntentName, "volume_to_value", StringComparison.OrdinalIgnoreCase);
|
||||||
|
var isProactivePizzaFactOffer = string.Equals(plan.IntentName, "proactive_offer_pizza_fact", StringComparison.OrdinalIgnoreCase);
|
||||||
var isSettingsLaunch = string.Equals(skill?.SkillName, "@be/settings", StringComparison.OrdinalIgnoreCase);
|
var isSettingsLaunch = string.Equals(skill?.SkillName, "@be/settings", StringComparison.OrdinalIgnoreCase);
|
||||||
var isGlobalCommand = isStopCommand || isVolumeControl;
|
var isGlobalCommand = isStopCommand || isVolumeControl;
|
||||||
var isPhotoGalleryLaunch = string.Equals(plan.IntentName, "photo_gallery", StringComparison.OrdinalIgnoreCase);
|
var isPhotoGalleryLaunch = string.Equals(plan.IntentName, "photo_gallery", StringComparison.OrdinalIgnoreCase);
|
||||||
@@ -99,7 +100,9 @@ public sealed class ResponsePlanToSocketMessagesMapper
|
|||||||
!string.IsNullOrWhiteSpace(clientIntent)
|
!string.IsNullOrWhiteSpace(clientIntent)
|
||||||
? clientIntent
|
? clientIntent
|
||||||
: transcript;
|
: transcript;
|
||||||
var outboundRules = isWordOfDayLaunch
|
var outboundRules = isProactivePizzaFactOffer
|
||||||
|
? ["shared/yes_no", "$YESNO"]
|
||||||
|
: isWordOfDayLaunch
|
||||||
? ["word-of-the-day/menu"]
|
? ["word-of-the-day/menu"]
|
||||||
: isGlobalCommand
|
: isGlobalCommand
|
||||||
? BuildGlobalCommandRules(rules)
|
? BuildGlobalCommandRules(rules)
|
||||||
|
|||||||
@@ -18,6 +18,28 @@ public sealed partial class WebSocketTurnFinalizationService(
|
|||||||
private static readonly TimeSpan AutoFinalizeMissingTranscriptFallbackAge = TimeSpan.FromMilliseconds(4200);
|
private static readonly TimeSpan AutoFinalizeMissingTranscriptFallbackAge = TimeSpan.FromMilliseconds(4200);
|
||||||
private static readonly TimeSpan AutoFinalizeContinuationDeferralMaxAge = TimeSpan.FromMilliseconds(3600);
|
private static readonly TimeSpan AutoFinalizeContinuationDeferralMaxAge = TimeSpan.FromMilliseconds(3600);
|
||||||
private const int AutoFinalizeContinuationDeferralMaxAttempts = 2;
|
private const int AutoFinalizeContinuationDeferralMaxAttempts = 2;
|
||||||
|
private static readonly HashSet<string> PegasusAffinityContinuationStems = new(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
"i love",
|
||||||
|
"i like",
|
||||||
|
"i enjoy",
|
||||||
|
"i do like",
|
||||||
|
"i dislike",
|
||||||
|
"i hate",
|
||||||
|
"i dont like",
|
||||||
|
"i don t like",
|
||||||
|
"i do not like",
|
||||||
|
"i dont enjoy",
|
||||||
|
"i don t enjoy",
|
||||||
|
"i do not enjoy",
|
||||||
|
"i dont love",
|
||||||
|
"i don t love",
|
||||||
|
"i do not love",
|
||||||
|
"i cant stand",
|
||||||
|
"i can t stand",
|
||||||
|
"i despise",
|
||||||
|
"i detest"
|
||||||
|
};
|
||||||
|
|
||||||
public static void ObserveIncomingMessage(CloudSession session, string? text)
|
public static void ObserveIncomingMessage(CloudSession session, string? text)
|
||||||
{
|
{
|
||||||
@@ -1482,9 +1504,20 @@ public sealed partial class WebSocketTurnFinalizationService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (LooksLikeIncompleteAffinitySet(normalized))
|
||||||
|
{
|
||||||
|
reason = "affinity_set_incomplete";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool LooksLikeIncompleteAffinitySet(string normalized)
|
||||||
|
{
|
||||||
|
return PegasusAffinityContinuationStems.Contains(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
private static Dictionary<string, object?> BuildTurnDiagnosticSnapshot(
|
private static Dictionary<string, object?> BuildTurnDiagnosticSnapshot(
|
||||||
CloudSession session,
|
CloudSession session,
|
||||||
WebSocketMessageEnvelope envelope,
|
WebSocketMessageEnvelope envelope,
|
||||||
|
|||||||
@@ -565,6 +565,43 @@ public sealed class JiboInteractionServiceTests
|
|||||||
Assert.Equal("Yes. You told me you dislike mushrooms.", recallDecision.ReplyText);
|
Assert.Equal("Yes. You told me you dislike mushrooms.", recallDecision.ReplyText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BuildDecisionAsync_AffinityMemory_PegasusEnjoyPhrase_SetThenRecallWithinTenant()
|
||||||
|
{
|
||||||
|
var memoryStore = new InMemoryPersonalMemoryStore();
|
||||||
|
var service = CreateService(memoryStore);
|
||||||
|
|
||||||
|
var setDecision = await service.BuildDecisionAsync(new TurnContext
|
||||||
|
{
|
||||||
|
RawTranscript = "I enjoy country music",
|
||||||
|
NormalizedTranscript = "I enjoy country music",
|
||||||
|
Attributes = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["accountId"] = "acct-a",
|
||||||
|
["loopId"] = "loop-a"
|
||||||
|
},
|
||||||
|
DeviceId = "device-a"
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal("memory_set_affinity", setDecision.IntentName);
|
||||||
|
Assert.Equal("Got it. I will remember you like country music.", setDecision.ReplyText);
|
||||||
|
|
||||||
|
var recallDecision = await service.BuildDecisionAsync(new TurnContext
|
||||||
|
{
|
||||||
|
RawTranscript = "do i enjoy country music",
|
||||||
|
NormalizedTranscript = "do i enjoy country music",
|
||||||
|
Attributes = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["accountId"] = "acct-a",
|
||||||
|
["loopId"] = "loop-a"
|
||||||
|
},
|
||||||
|
DeviceId = "device-a"
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal("memory_get_affinity", recallDecision.IntentName);
|
||||||
|
Assert.Equal("Yes. You told me you like country music.", recallDecision.ReplyText);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task BuildDecisionAsync_PreferenceReversePhrase_ParsesFavoriteVariant()
|
public async Task BuildDecisionAsync_PreferenceReversePhrase_ParsesFavoriteVariant()
|
||||||
{
|
{
|
||||||
@@ -587,6 +624,28 @@ public sealed class JiboInteractionServiceTests
|
|||||||
Assert.Equal("Got it. I will remember your favorite food is pizza.", setDecision.ReplyText);
|
Assert.Equal("Got it. I will remember your favorite food is pizza.", setDecision.ReplyText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BuildDecisionAsync_PreferenceReversePluralPhrase_ParsesFavoriteVariant()
|
||||||
|
{
|
||||||
|
var memoryStore = new InMemoryPersonalMemoryStore();
|
||||||
|
var service = CreateService(memoryStore);
|
||||||
|
|
||||||
|
var setDecision = await service.BuildDecisionAsync(new TurnContext
|
||||||
|
{
|
||||||
|
RawTranscript = "dogs are my favorite animals",
|
||||||
|
NormalizedTranscript = "dogs are my favorite animals",
|
||||||
|
Attributes = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["accountId"] = "acct-a",
|
||||||
|
["loopId"] = "loop-a"
|
||||||
|
},
|
||||||
|
DeviceId = "device-a"
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal("memory_set_preference", setDecision.IntentName);
|
||||||
|
Assert.Equal("Got it. I will remember your favorite animals is dogs.", setDecision.ReplyText);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task BuildDecisionAsync_Surprise_WithPizzaPreference_UsesPizzaProactivity()
|
public async Task BuildDecisionAsync_Surprise_WithPizzaPreference_UsesPizzaProactivity()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -355,6 +355,75 @@ public sealed class JiboWebSocketServiceTests
|
|||||||
Assert.Equal("my favorite sport is football", listenPayload.RootElement.GetProperty("data").GetProperty("asr").GetProperty("text").GetString());
|
Assert.Equal("my favorite sport is football", listenPayload.RootElement.GetProperty("data").GetProperty("asr").GetProperty("text").GetString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BufferedAudio_WithIncompleteAffinityHint_DefersThenFinalizesWhenContinuationArrives()
|
||||||
|
{
|
||||||
|
await _service.HandleMessageAsync(new WebSocketMessageEnvelope
|
||||||
|
{
|
||||||
|
HostName = "neo-hub.jibo.com",
|
||||||
|
Path = "/listen",
|
||||||
|
Kind = "neo-hub-listen",
|
||||||
|
Token = "hub-affinity-continuation-token",
|
||||||
|
Text = """{"type":"LISTEN","transID":"trans-affinity-continuation","data":{"rules":["launch"]}}"""
|
||||||
|
});
|
||||||
|
|
||||||
|
await _service.HandleMessageAsync(new WebSocketMessageEnvelope
|
||||||
|
{
|
||||||
|
HostName = "neo-hub.jibo.com",
|
||||||
|
Path = "/listen",
|
||||||
|
Kind = "neo-hub-listen",
|
||||||
|
Token = "hub-affinity-continuation-token",
|
||||||
|
Text = """{"type":"CONTEXT","transID":"trans-affinity-continuation","data":{"audioTranscriptHint":"i do like"}}"""
|
||||||
|
});
|
||||||
|
|
||||||
|
for (var index = 0; index < 4; index += 1)
|
||||||
|
{
|
||||||
|
var chunkReplies = await _service.HandleMessageAsync(new WebSocketMessageEnvelope
|
||||||
|
{
|
||||||
|
HostName = "neo-hub.jibo.com",
|
||||||
|
Path = "/listen",
|
||||||
|
Kind = "neo-hub-listen",
|
||||||
|
Token = "hub-affinity-continuation-token",
|
||||||
|
Binary = new byte[3000]
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Empty(chunkReplies);
|
||||||
|
}
|
||||||
|
|
||||||
|
var session = _store.FindSessionByToken("hub-affinity-continuation-token");
|
||||||
|
Assert.NotNull(session);
|
||||||
|
session.TurnState.FirstAudioReceivedUtc = DateTimeOffset.UtcNow - TimeSpan.FromSeconds(2);
|
||||||
|
|
||||||
|
var deferredReplies = await _service.HandleMessageAsync(new WebSocketMessageEnvelope
|
||||||
|
{
|
||||||
|
HostName = "neo-hub.jibo.com",
|
||||||
|
Path = "/listen",
|
||||||
|
Kind = "neo-hub-listen",
|
||||||
|
Token = "hub-affinity-continuation-token",
|
||||||
|
Binary = new byte[3000]
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Empty(deferredReplies);
|
||||||
|
|
||||||
|
var finalizedReplies = await _service.HandleMessageAsync(new WebSocketMessageEnvelope
|
||||||
|
{
|
||||||
|
HostName = "neo-hub.jibo.com",
|
||||||
|
Path = "/listen",
|
||||||
|
Kind = "neo-hub-listen",
|
||||||
|
Token = "hub-affinity-continuation-token",
|
||||||
|
Text = """{"type":"CONTEXT","transID":"trans-affinity-continuation","data":{"audioTranscriptHint":"i do like pizza"}}"""
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal(3, finalizedReplies.Count);
|
||||||
|
Assert.Equal("LISTEN", ReadReplyType(finalizedReplies[0]));
|
||||||
|
Assert.Equal("EOS", ReadReplyType(finalizedReplies[1]));
|
||||||
|
Assert.Equal("SKILL_ACTION", ReadReplyType(finalizedReplies[2]));
|
||||||
|
|
||||||
|
using var listenPayload = JsonDocument.Parse(finalizedReplies[0].Text!);
|
||||||
|
Assert.Equal("memory_set_affinity", listenPayload.RootElement.GetProperty("data").GetProperty("nlu").GetProperty("intent").GetString());
|
||||||
|
Assert.Equal("i do like pizza", listenPayload.RootElement.GetProperty("data").GetProperty("asr").GetProperty("text").GetString());
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task MultiChunkAudio_AccumulatesBufferedStateAcrossMessages()
|
public async Task MultiChunkAudio_AccumulatesBufferedStateAcrossMessages()
|
||||||
{
|
{
|
||||||
@@ -3147,6 +3216,9 @@ public sealed class JiboWebSocketServiceTests
|
|||||||
using (var offerListenPayload = JsonDocument.Parse(offerReplies[0].Text!))
|
using (var offerListenPayload = JsonDocument.Parse(offerReplies[0].Text!))
|
||||||
{
|
{
|
||||||
Assert.Equal("proactive_offer_pizza_fact", offerListenPayload.RootElement.GetProperty("data").GetProperty("nlu").GetProperty("intent").GetString());
|
Assert.Equal("proactive_offer_pizza_fact", offerListenPayload.RootElement.GetProperty("data").GetProperty("nlu").GetProperty("intent").GetString());
|
||||||
|
Assert.Equal("shared/yes_no", offerListenPayload.RootElement.GetProperty("data").GetProperty("nlu").GetProperty("rules")[0].GetString());
|
||||||
|
Assert.Equal("$YESNO", offerListenPayload.RootElement.GetProperty("data").GetProperty("nlu").GetProperty("rules")[1].GetString());
|
||||||
|
Assert.Equal("shared/yes_no", offerListenPayload.RootElement.GetProperty("data").GetProperty("match").GetProperty("rule").GetString());
|
||||||
}
|
}
|
||||||
|
|
||||||
var session = _store.FindSessionByToken(token);
|
var session = _store.FindSessionByToken(token);
|
||||||
|
|||||||
Reference in New Issue
Block a user