Polish grocery list alias wording and backlog MVP decision
This commit is contained in:
@@ -770,7 +770,7 @@ Current release theme:
|
|||||||
|
|
||||||
### 28. Grocery List Capability (Requested Feature)
|
### 28. Grocery List Capability (Requested Feature)
|
||||||
|
|
||||||
- Status: `discovery`
|
- Status: `in_progress`
|
||||||
- Tags: `content`, `docs`, `storage`
|
- Tags: `content`, `docs`, `storage`
|
||||||
- Why now:
|
- Why now:
|
||||||
- directly requested by Jibo owners and fits memory + household utility roadmap
|
- directly requested by Jibo owners and fits memory + household utility roadmap
|
||||||
@@ -779,13 +779,14 @@ Current release theme:
|
|||||||
- examples:
|
- examples:
|
||||||
- `C:\Projects\jibo\pegasus\packages\chitchat-skill\mims\scripted-responses\RA_JBO_ShoppingList.mim`
|
- `C:\Projects\jibo\pegasus\packages\chitchat-skill\mims\scripted-responses\RA_JBO_ShoppingList.mim`
|
||||||
- `C:\Projects\jibo\pegasus\packages\chitchat-skill\mims\scripted-responses\RA_JBO_ManageToDoList.mim`
|
- `C:\Projects\jibo\pegasus\packages\chitchat-skill\mims\scripted-responses\RA_JBO_ManageToDoList.mim`
|
||||||
- Candidate delivery paths:
|
- MVP decision:
|
||||||
- native lightweight list skill (fastest user value)
|
- use the existing household list engine as the native lightweight grocery MVP
|
||||||
- integration-backed list orchestration (long-term richer ecosystem fit)
|
- keep grocery as a first-class spoken alias over the shopping list storage path
|
||||||
|
- reserve integration-backed list orchestration for a later discovery pass
|
||||||
- Exit criteria:
|
- Exit criteria:
|
||||||
- clear decision on MVP path
|
- grocery prompts, add/recall/done flows, and list follow-ups consistently speak grocery wording
|
||||||
- first schema for list items + ownership scope
|
- existing shopping/to-do flows remain unchanged
|
||||||
- initial voice flows and follow-up intent handling defined
|
- future integration-backed list work remains a separate backlog item
|
||||||
|
|
||||||
### 29. Legacy MIM Personality Import Ladder
|
### 29. Legacy MIM Personality Import Ladder
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ This release starts the shift from `1.0.18` hardening to visible feature growth.
|
|||||||
|
|
||||||
The goal is to keep compatibility work steady while shipping personality and capability slices that make OpenJibo feel less like a placeholder cloud and more like a real assistant platform.
|
The goal is to keep compatibility work steady while shipping personality and capability slices that make OpenJibo feel less like a placeholder cloud and more like a real assistant platform.
|
||||||
|
|
||||||
|
For grocery list capability, the 1.0.19 MVP choice is the existing household list engine with grocery as a first-class spoken alias. That keeps the storage model simple now while leaving integration-backed list orchestration for a later pass.
|
||||||
|
|
||||||
## Snapshot
|
## Snapshot
|
||||||
|
|
||||||
- Kickoff date: `2026-05-05`
|
- Kickoff date: `2026-05-05`
|
||||||
|
|||||||
@@ -7,11 +7,15 @@ internal static class HouseholdListOrchestrator
|
|||||||
{
|
{
|
||||||
internal const string StateMetadataKey = "householdListState";
|
internal const string StateMetadataKey = "householdListState";
|
||||||
internal const string TypeMetadataKey = "householdListType";
|
internal const string TypeMetadataKey = "householdListType";
|
||||||
|
internal const string DisplayTypeMetadataKey = "householdListDisplayType";
|
||||||
internal const string NoMatchCountMetadataKey = "householdListNoMatchCount";
|
internal const string NoMatchCountMetadataKey = "householdListNoMatchCount";
|
||||||
internal const string NoInputCountMetadataKey = "householdListNoInputCount";
|
internal const string NoInputCountMetadataKey = "householdListNoInputCount";
|
||||||
|
|
||||||
private const string IdleState = "idle";
|
private const string IdleState = "idle";
|
||||||
private const string AwaitingItemState = "awaiting_item";
|
private const string AwaitingItemState = "awaiting_item";
|
||||||
|
private const string ShoppingListType = "shopping";
|
||||||
|
private const string GroceryListType = "grocery";
|
||||||
|
private const string TodoListType = "todo";
|
||||||
|
|
||||||
private static readonly string[] ItemPrefixes =
|
private static readonly string[] ItemPrefixes =
|
||||||
[
|
[
|
||||||
@@ -31,6 +35,10 @@ internal static class HouseholdListOrchestrator
|
|||||||
" to my shopping list",
|
" to my shopping list",
|
||||||
" to the shopping list",
|
" to the shopping list",
|
||||||
" on my shopping list",
|
" on my shopping list",
|
||||||
|
" to my grocery list",
|
||||||
|
" to the grocery list",
|
||||||
|
" on my grocery list",
|
||||||
|
" my grocery list",
|
||||||
" to my to do list",
|
" to my to do list",
|
||||||
" to the to do list",
|
" to the to do list",
|
||||||
" on my to do list",
|
" on my to do list",
|
||||||
@@ -50,6 +58,7 @@ internal static class HouseholdListOrchestrator
|
|||||||
{
|
{
|
||||||
var state = ReadString(turn, StateMetadataKey);
|
var state = ReadString(turn, StateMetadataKey);
|
||||||
var listType = ReadString(turn, TypeMetadataKey);
|
var listType = ReadString(turn, TypeMetadataKey);
|
||||||
|
var displayType = ReadString(turn, DisplayTypeMetadataKey);
|
||||||
var isActiveState = !string.IsNullOrWhiteSpace(state) &&
|
var isActiveState = !string.IsNullOrWhiteSpace(state) &&
|
||||||
!string.Equals(state, IdleState, StringComparison.OrdinalIgnoreCase);
|
!string.Equals(state, IdleState, StringComparison.OrdinalIgnoreCase);
|
||||||
var isShoppingIntent = string.Equals(semanticIntent, "shopping_list", StringComparison.OrdinalIgnoreCase);
|
var isShoppingIntent = string.Equals(semanticIntent, "shopping_list", StringComparison.OrdinalIgnoreCase);
|
||||||
@@ -58,17 +67,19 @@ internal static class HouseholdListOrchestrator
|
|||||||
if (!isActiveState && !isShoppingIntent && !isTodoIntent)
|
if (!isActiveState && !isShoppingIntent && !isTodoIntent)
|
||||||
return Task.FromResult<JiboInteractionDecision?>(null);
|
return Task.FromResult<JiboInteractionDecision?>(null);
|
||||||
|
|
||||||
var resolvedListType = isShoppingIntent ? "shopping" : isTodoIntent ? "todo" : NormalizeListType(listType);
|
var resolvedListType = isShoppingIntent ? ShoppingListType : isTodoIntent ? TodoListType : NormalizeListType(listType);
|
||||||
if (string.IsNullOrWhiteSpace(resolvedListType)) resolvedListType = "shopping";
|
if (string.IsNullOrWhiteSpace(resolvedListType)) resolvedListType = ShoppingListType;
|
||||||
|
var resolvedDisplayType = ResolveDisplayType(resolvedListType, displayType, isActiveState, loweredTranscript);
|
||||||
|
|
||||||
var tenantScope = tenantScopeResolver(turn);
|
var tenantScope = tenantScopeResolver(turn);
|
||||||
|
|
||||||
if (ContainsAny(loweredTranscript, "cancel", "stop", "never mind", "nevermind", "forget it"))
|
if (ContainsAny(loweredTranscript, "cancel", "stop", "never mind", "nevermind", "forget it"))
|
||||||
return Task.FromResult<JiboInteractionDecision?>(BuildCancelledDecision(resolvedListType));
|
return Task.FromResult<JiboInteractionDecision?>(BuildCancelledDecision(resolvedListType, resolvedDisplayType));
|
||||||
|
|
||||||
if (IsRecallRequest(loweredTranscript))
|
if (IsRecallRequest(loweredTranscript))
|
||||||
return Task.FromResult<JiboInteractionDecision?>(BuildRecallDecision(
|
return Task.FromResult<JiboInteractionDecision?>(BuildRecallDecision(
|
||||||
resolvedListType,
|
resolvedListType,
|
||||||
|
resolvedDisplayType,
|
||||||
personalMemoryStore.GetListItems(tenantScope, resolvedListType)));
|
personalMemoryStore.GetListItems(tenantScope, resolvedListType)));
|
||||||
|
|
||||||
var directItem = TryExtractListItem(loweredTranscript);
|
var directItem = TryExtractListItem(loweredTranscript);
|
||||||
@@ -76,9 +87,9 @@ internal static class HouseholdListOrchestrator
|
|||||||
{
|
{
|
||||||
if (IsConversationComplete(loweredTranscript))
|
if (IsConversationComplete(loweredTranscript))
|
||||||
return Task.FromResult<JiboInteractionDecision?>(new JiboInteractionDecision(
|
return Task.FromResult<JiboInteractionDecision?>(new JiboInteractionDecision(
|
||||||
resolvedListType == "shopping" ? "shopping_list_done" : "todo_list_done",
|
BuildListIntentName(resolvedListType, "done"),
|
||||||
BuildDoneReply(resolvedListType, personalMemoryStore.GetListItems(tenantScope, resolvedListType)),
|
BuildDoneReply(resolvedDisplayType, personalMemoryStore.GetListItems(tenantScope, resolvedListType)),
|
||||||
ContextUpdates: BuildContextUpdates(resolvedListType, IdleState)));
|
ContextUpdates: BuildContextUpdates(resolvedListType, resolvedDisplayType, IdleState)));
|
||||||
|
|
||||||
directItem = NormalizeItem(transcript);
|
directItem = NormalizeItem(transcript);
|
||||||
}
|
}
|
||||||
@@ -87,104 +98,108 @@ internal static class HouseholdListOrchestrator
|
|||||||
{
|
{
|
||||||
personalMemoryStore.AddListItem(tenantScope, resolvedListType, directItem);
|
personalMemoryStore.AddListItem(tenantScope, resolvedListType, directItem);
|
||||||
return Task.FromResult<JiboInteractionDecision?>(new JiboInteractionDecision(
|
return Task.FromResult<JiboInteractionDecision?>(new JiboInteractionDecision(
|
||||||
resolvedListType == "shopping" ? "shopping_list_add" : "todo_list_add",
|
BuildListIntentName(resolvedListType, "add"),
|
||||||
BuildAddedReply(resolvedListType, directItem,
|
BuildAddedReply(resolvedDisplayType, directItem,
|
||||||
personalMemoryStore.GetListItems(tenantScope, resolvedListType)),
|
personalMemoryStore.GetListItems(tenantScope, resolvedListType)),
|
||||||
ContextUpdates: BuildContextUpdates(resolvedListType, AwaitingItemState)));
|
ContextUpdates: BuildContextUpdates(resolvedListType, resolvedDisplayType, AwaitingItemState)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(transcript))
|
if (string.IsNullOrWhiteSpace(transcript))
|
||||||
return Task.FromResult<JiboInteractionDecision?>(new JiboInteractionDecision(
|
return Task.FromResult<JiboInteractionDecision?>(new JiboInteractionDecision(
|
||||||
resolvedListType == "shopping" ? "shopping_list_prompt" : "todo_list_prompt",
|
BuildListIntentName(resolvedListType, "prompt"),
|
||||||
BuildPromptReply(resolvedListType),
|
BuildPromptReply(resolvedDisplayType),
|
||||||
ContextUpdates: BuildContextUpdates(resolvedListType, AwaitingItemState)));
|
ContextUpdates: BuildContextUpdates(resolvedListType, resolvedDisplayType, AwaitingItemState)));
|
||||||
|
|
||||||
return Task.FromResult<JiboInteractionDecision?>(new JiboInteractionDecision(
|
return Task.FromResult<JiboInteractionDecision?>(new JiboInteractionDecision(
|
||||||
resolvedListType == "shopping" ? "shopping_list_prompt" : "todo_list_prompt",
|
BuildListIntentName(resolvedListType, "prompt"),
|
||||||
BuildPromptReply(resolvedListType),
|
BuildPromptReply(resolvedDisplayType),
|
||||||
ContextUpdates: BuildContextUpdates(resolvedListType, AwaitingItemState)));
|
ContextUpdates: BuildContextUpdates(resolvedListType, resolvedDisplayType, AwaitingItemState)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IDictionary<string, object?> BuildContextUpdates(string listType, string state)
|
private static IDictionary<string, object?> BuildContextUpdates(string listType, string displayType, string state)
|
||||||
{
|
{
|
||||||
return new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
return new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
[StateMetadataKey] = state,
|
[StateMetadataKey] = state,
|
||||||
[TypeMetadataKey] = listType,
|
[TypeMetadataKey] = listType,
|
||||||
|
[DisplayTypeMetadataKey] = displayType,
|
||||||
[NoMatchCountMetadataKey] = 0,
|
[NoMatchCountMetadataKey] = 0,
|
||||||
[NoInputCountMetadataKey] = 0
|
[NoInputCountMetadataKey] = 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JiboInteractionDecision BuildCancelledDecision(string listType)
|
private static JiboInteractionDecision BuildCancelledDecision(string listType, string displayType)
|
||||||
{
|
{
|
||||||
return new JiboInteractionDecision(
|
return new JiboInteractionDecision(
|
||||||
listType == "shopping" ? "shopping_list_cancel" : "todo_list_cancel",
|
BuildListIntentName(listType, "cancel"),
|
||||||
listType == "shopping" ? "Okay. I stopped the shopping list." : "Okay. I stopped the to-do list.",
|
$"Okay. I stopped the {BuildListLabel(displayType)}.",
|
||||||
ContextUpdates: new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
ContextUpdates: new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
[StateMetadataKey] = IdleState,
|
[StateMetadataKey] = IdleState,
|
||||||
[TypeMetadataKey] = listType,
|
[TypeMetadataKey] = listType,
|
||||||
|
[DisplayTypeMetadataKey] = displayType,
|
||||||
[NoMatchCountMetadataKey] = 0,
|
[NoMatchCountMetadataKey] = 0,
|
||||||
[NoInputCountMetadataKey] = 0
|
[NoInputCountMetadataKey] = 0
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JiboInteractionDecision BuildRecallDecision(string listType, IReadOnlyList<string> items)
|
private static JiboInteractionDecision BuildRecallDecision(string listType, string displayType, IReadOnlyList<string> items)
|
||||||
{
|
{
|
||||||
if (items.Count == 0)
|
if (items.Count == 0)
|
||||||
return new JiboInteractionDecision(
|
return new JiboInteractionDecision(
|
||||||
listType == "shopping" ? "shopping_list_recall" : "todo_list_recall",
|
BuildListIntentName(listType, "recall"),
|
||||||
listType == "shopping"
|
$"Your {BuildListLabel(displayType)} is empty.",
|
||||||
? "Your shopping list is empty."
|
|
||||||
: "Your to-do list is empty.",
|
|
||||||
ContextUpdates: new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
ContextUpdates: new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
[StateMetadataKey] = IdleState,
|
[StateMetadataKey] = IdleState,
|
||||||
[TypeMetadataKey] = listType,
|
[TypeMetadataKey] = listType,
|
||||||
|
[DisplayTypeMetadataKey] = displayType,
|
||||||
[NoMatchCountMetadataKey] = 0,
|
[NoMatchCountMetadataKey] = 0,
|
||||||
[NoInputCountMetadataKey] = 0
|
[NoInputCountMetadataKey] = 0
|
||||||
});
|
});
|
||||||
|
|
||||||
return new JiboInteractionDecision(
|
return new JiboInteractionDecision(
|
||||||
listType == "shopping" ? "shopping_list_recall" : "todo_list_recall",
|
BuildListIntentName(listType, "recall"),
|
||||||
listType == "shopping"
|
$"Your {BuildListLabel(displayType)} has {JoinList(items)}.",
|
||||||
? $"Your shopping list has {JoinList(items)}."
|
|
||||||
: $"Your to-do list has {JoinList(items)}.",
|
|
||||||
ContextUpdates: new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
ContextUpdates: new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
[StateMetadataKey] = IdleState,
|
[StateMetadataKey] = IdleState,
|
||||||
[TypeMetadataKey] = listType,
|
[TypeMetadataKey] = listType,
|
||||||
|
[DisplayTypeMetadataKey] = displayType,
|
||||||
[NoMatchCountMetadataKey] = 0,
|
[NoMatchCountMetadataKey] = 0,
|
||||||
[NoInputCountMetadataKey] = 0
|
[NoInputCountMetadataKey] = 0
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string BuildAddedReply(string listType, string addedItem, IReadOnlyList<string> items)
|
private static string BuildAddedReply(string displayType, string addedItem, IReadOnlyList<string> items)
|
||||||
{
|
{
|
||||||
var itemLabel = listType == "shopping" ? "shopping list" : "to-do list";
|
var itemLabel = BuildListLabel(displayType);
|
||||||
return items.Count == 1
|
return items.Count == 1
|
||||||
? $"Added {addedItem} to your {itemLabel}. What else should I add?"
|
? $"Added {addedItem} to your {itemLabel}. What else should I add?"
|
||||||
: $"Added {addedItem} to your {itemLabel}. You now have {JoinList(items)}.";
|
: $"Added {addedItem} to your {itemLabel}. You now have {JoinList(items)}.";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string BuildPromptReply(string listType)
|
private static string BuildPromptReply(string displayType)
|
||||||
{
|
{
|
||||||
return listType == "shopping"
|
return $"What should I add to your {BuildListLabel(displayType)}?";
|
||||||
? "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)
|
private static string BuildDoneReply(string displayType, IReadOnlyList<string> items)
|
||||||
{
|
{
|
||||||
if (items.Count == 0)
|
if (items.Count == 0)
|
||||||
return listType == "shopping"
|
return $"Okay. Your {BuildListLabel(displayType)} is empty.";
|
||||||
? "Okay. Your shopping list is empty."
|
|
||||||
: "Okay. Your to-do list is empty.";
|
|
||||||
|
|
||||||
return listType == "shopping"
|
return $"Okay. Your {BuildListLabel(displayType)} has {JoinList(items)}.";
|
||||||
? $"Okay. Your shopping list has {JoinList(items)}."
|
}
|
||||||
: $"Okay. Your to-do list has {JoinList(items)}.";
|
|
||||||
|
private static string BuildListLabel(string displayType)
|
||||||
|
{
|
||||||
|
return NormalizeDisplayType(displayType) switch
|
||||||
|
{
|
||||||
|
GroceryListType => "grocery list",
|
||||||
|
TodoListType => "to-do list",
|
||||||
|
_ => "shopping list"
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string JoinList(IReadOnlyList<string> items)
|
private static string JoinList(IReadOnlyList<string> items)
|
||||||
@@ -205,7 +220,13 @@ internal static class HouseholdListOrchestrator
|
|||||||
if (!loweredTranscript.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) continue;
|
if (!loweredTranscript.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) continue;
|
||||||
|
|
||||||
var remainder = loweredTranscript[prefix.Length..].Trim();
|
var remainder = loweredTranscript[prefix.Length..].Trim();
|
||||||
|
if (IsListOnlyRemainder(remainder))
|
||||||
|
return null;
|
||||||
|
|
||||||
remainder = TrimTrailingListPhrases(remainder);
|
remainder = TrimTrailingListPhrases(remainder);
|
||||||
|
if (IsListOnlyRemainder(remainder))
|
||||||
|
return null;
|
||||||
|
|
||||||
return NormalizeItem(remainder);
|
return NormalizeItem(remainder);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,6 +239,9 @@ internal static class HouseholdListOrchestrator
|
|||||||
"what is on my shopping list",
|
"what is on my shopping list",
|
||||||
"what's on my shopping list",
|
"what's on my shopping list",
|
||||||
"show my shopping list",
|
"show my shopping list",
|
||||||
|
"what is on my grocery list",
|
||||||
|
"what's on my grocery list",
|
||||||
|
"show my grocery list",
|
||||||
"what is on my to do list",
|
"what is on my to do list",
|
||||||
"what's on my to do list",
|
"what's on my to do list",
|
||||||
"show my to do list",
|
"show my to do list",
|
||||||
@@ -246,13 +270,96 @@ internal static class HouseholdListOrchestrator
|
|||||||
var normalized = NormalizeItem(listType ?? string.Empty).ToLowerInvariant();
|
var normalized = NormalizeItem(listType ?? string.Empty).ToLowerInvariant();
|
||||||
return normalized.Contains("todo", StringComparison.OrdinalIgnoreCase) ||
|
return normalized.Contains("todo", StringComparison.OrdinalIgnoreCase) ||
|
||||||
normalized.Contains("to do", StringComparison.OrdinalIgnoreCase)
|
normalized.Contains("to do", StringComparison.OrdinalIgnoreCase)
|
||||||
? "todo"
|
? TodoListType
|
||||||
: normalized.Contains("shopping", StringComparison.OrdinalIgnoreCase) ||
|
: normalized.Contains("shopping", StringComparison.OrdinalIgnoreCase) ||
|
||||||
normalized.Contains("grocery", StringComparison.OrdinalIgnoreCase)
|
normalized.Contains("grocery", StringComparison.OrdinalIgnoreCase)
|
||||||
? "shopping"
|
? ShoppingListType
|
||||||
: string.Empty;
|
: string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string ResolveDisplayType(string listType, string? storedDisplayType, bool isActiveState, string loweredTranscript)
|
||||||
|
{
|
||||||
|
var transcriptDisplayType = InferDisplayTypeFromTranscript(loweredTranscript);
|
||||||
|
var normalizedStoredDisplayType = NormalizeDisplayType(storedDisplayType);
|
||||||
|
|
||||||
|
if (isActiveState && !string.IsNullOrWhiteSpace(normalizedStoredDisplayType))
|
||||||
|
return normalizedStoredDisplayType;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(transcriptDisplayType))
|
||||||
|
return transcriptDisplayType;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(normalizedStoredDisplayType))
|
||||||
|
return normalizedStoredDisplayType;
|
||||||
|
|
||||||
|
return string.Equals(listType, TodoListType, StringComparison.OrdinalIgnoreCase)
|
||||||
|
? TodoListType
|
||||||
|
: ShoppingListType;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string InferDisplayTypeFromTranscript(string loweredTranscript)
|
||||||
|
{
|
||||||
|
if (loweredTranscript.Contains("grocery", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return GroceryListType;
|
||||||
|
|
||||||
|
if (loweredTranscript.Contains("to do", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
loweredTranscript.Contains("todo", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
loweredTranscript.Contains("task", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return TodoListType;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loweredTranscript.Contains("shopping", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return ShoppingListType;
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeDisplayType(string? displayType)
|
||||||
|
{
|
||||||
|
var normalized = NormalizeItem(displayType ?? string.Empty).ToLowerInvariant();
|
||||||
|
return normalized.Contains("grocery", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? GroceryListType
|
||||||
|
: normalized.Contains("todo", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
normalized.Contains("to do", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? TodoListType
|
||||||
|
: normalized.Contains("shopping", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? ShoppingListType
|
||||||
|
: string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildListIntentName(string listType, string action)
|
||||||
|
{
|
||||||
|
var normalizedListType = string.Equals(listType, TodoListType, StringComparison.OrdinalIgnoreCase)
|
||||||
|
? TodoListType
|
||||||
|
: ShoppingListType;
|
||||||
|
return $"{normalizedListType}_list_{action}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsListOnlyRemainder(string value)
|
||||||
|
{
|
||||||
|
var normalized = NormalizeItem(value).ToLowerInvariant();
|
||||||
|
return normalized is "shopping list" or
|
||||||
|
"grocery list" or
|
||||||
|
"to do list" or
|
||||||
|
"todo list" or
|
||||||
|
"my shopping list" or
|
||||||
|
"my grocery list" or
|
||||||
|
"my to do list" or
|
||||||
|
"my todo list" or
|
||||||
|
"to my shopping list" or
|
||||||
|
"to my grocery list" or
|
||||||
|
"to my to do list" or
|
||||||
|
"to my todo list" or
|
||||||
|
"to the shopping list" or
|
||||||
|
"to the grocery list" or
|
||||||
|
"to the to do list" or
|
||||||
|
"to the todo list" or
|
||||||
|
"on my shopping list" or
|
||||||
|
"on my grocery list" or
|
||||||
|
"on my to do list" or
|
||||||
|
"on my todo list";
|
||||||
|
}
|
||||||
|
|
||||||
private static bool ContainsAny(string loweredTranscript, params string[] phrases)
|
private static bool ContainsAny(string loweredTranscript, params string[] phrases)
|
||||||
{
|
{
|
||||||
return phrases.Any(phrase => loweredTranscript.Contains(phrase, StringComparison.OrdinalIgnoreCase));
|
return phrases.Any(phrase => loweredTranscript.Contains(phrase, StringComparison.OrdinalIgnoreCase));
|
||||||
@@ -274,4 +381,4 @@ internal static class HouseholdListOrchestrator
|
|||||||
{
|
{
|
||||||
return turn.Attributes.TryGetValue(key, out var value) ? value?.ToString() : null;
|
return turn.Attributes.TryGetValue(key, out var value) ? value?.ToString() : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -780,13 +780,19 @@ public sealed partial class JiboInteractionService
|
|||||||
loweredTranscript,
|
loweredTranscript,
|
||||||
"shopping list",
|
"shopping list",
|
||||||
"grocery list",
|
"grocery list",
|
||||||
|
"my grocery list",
|
||||||
|
"create grocery list",
|
||||||
|
"start grocery list",
|
||||||
"to do list",
|
"to do list",
|
||||||
"todo list",
|
"todo list",
|
||||||
"add to my shopping list",
|
"add to my shopping list",
|
||||||
|
"add to my grocery list",
|
||||||
"add to my to do list",
|
"add to my to do list",
|
||||||
"add to my todo list",
|
"add to my todo list",
|
||||||
"what's on my shopping list",
|
"what's on my shopping list",
|
||||||
"what is on my shopping list",
|
"what is on my shopping list",
|
||||||
|
"what's on my grocery list",
|
||||||
|
"what is on my grocery list",
|
||||||
"what's on my to do list",
|
"what's on my to do list",
|
||||||
"what is on my to do list",
|
"what is on my to do list",
|
||||||
"what are my tasks",
|
"what are my tasks",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ public sealed class JiboInteractionServiceTests
|
|||||||
private const string PersonalReportNewsEnabledKey = "personalReportNewsEnabled";
|
private const string PersonalReportNewsEnabledKey = "personalReportNewsEnabled";
|
||||||
private const string HouseholdListStateKey = "householdListState";
|
private const string HouseholdListStateKey = "householdListState";
|
||||||
private const string HouseholdListTypeKey = "householdListType";
|
private const string HouseholdListTypeKey = "householdListType";
|
||||||
|
private const string HouseholdListDisplayTypeKey = "householdListDisplayType";
|
||||||
private const string ChitchatStateKey = "chitchatState";
|
private const string ChitchatStateKey = "chitchatState";
|
||||||
private const string ChitchatRouteKey = "chitchatRoute";
|
private const string ChitchatRouteKey = "chitchatRoute";
|
||||||
private const string ChitchatEmotionKey = "chitchatEmotion";
|
private const string ChitchatEmotionKey = "chitchatEmotion";
|
||||||
@@ -2285,13 +2286,17 @@ public sealed class JiboInteractionServiceTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("shopping list", "shopping_list_prompt", "What should I add to your shopping list?", "shopping")]
|
[InlineData("shopping list", "shopping_list_prompt", "What should I add to your shopping list?", "shopping", "shopping")]
|
||||||
[InlineData("to do list", "todo_list_prompt", "What should I add to your to-do list?", "todo")]
|
[InlineData("grocery list", "shopping_list_prompt", "What should I add to your grocery list?", "shopping", "grocery")]
|
||||||
|
[InlineData("my grocery list", "shopping_list_prompt", "What should I add to your grocery list?", "shopping", "grocery")]
|
||||||
|
[InlineData("create grocery list", "shopping_list_prompt", "What should I add to your grocery list?", "shopping", "grocery")]
|
||||||
|
[InlineData("to do list", "todo_list_prompt", "What should I add to your to-do list?", "todo", "todo")]
|
||||||
public async Task BuildDecisionAsync_ListStart_PromptsForFollowUpItems(
|
public async Task BuildDecisionAsync_ListStart_PromptsForFollowUpItems(
|
||||||
string transcript,
|
string transcript,
|
||||||
string expectedIntent,
|
string expectedIntent,
|
||||||
string expectedReply,
|
string expectedReply,
|
||||||
string expectedListType)
|
string expectedListType,
|
||||||
|
string expectedDisplayType)
|
||||||
{
|
{
|
||||||
var service = CreateService();
|
var service = CreateService();
|
||||||
|
|
||||||
@@ -2306,6 +2311,7 @@ public sealed class JiboInteractionServiceTests
|
|||||||
Assert.NotNull(decision.ContextUpdates);
|
Assert.NotNull(decision.ContextUpdates);
|
||||||
Assert.Equal("awaiting_item", decision.ContextUpdates![HouseholdListStateKey]);
|
Assert.Equal("awaiting_item", decision.ContextUpdates![HouseholdListStateKey]);
|
||||||
Assert.Equal(expectedListType, decision.ContextUpdates[HouseholdListTypeKey]);
|
Assert.Equal(expectedListType, decision.ContextUpdates[HouseholdListTypeKey]);
|
||||||
|
Assert.Equal(expectedDisplayType, decision.ContextUpdates[HouseholdListDisplayTypeKey]);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -2330,6 +2336,7 @@ public sealed class JiboInteractionServiceTests
|
|||||||
Assert.Equal("shopping_list_prompt", promptDecision.IntentName);
|
Assert.Equal("shopping_list_prompt", promptDecision.IntentName);
|
||||||
Assert.Equal("awaiting_item", promptDecision.ContextUpdates![HouseholdListStateKey]);
|
Assert.Equal("awaiting_item", promptDecision.ContextUpdates![HouseholdListStateKey]);
|
||||||
Assert.Equal("shopping", promptDecision.ContextUpdates[HouseholdListTypeKey]);
|
Assert.Equal("shopping", promptDecision.ContextUpdates[HouseholdListTypeKey]);
|
||||||
|
Assert.Equal("shopping", promptDecision.ContextUpdates[HouseholdListDisplayTypeKey]);
|
||||||
|
|
||||||
var addDecision = await service.BuildDecisionAsync(new TurnContext
|
var addDecision = await service.BuildDecisionAsync(new TurnContext
|
||||||
{
|
{
|
||||||
@@ -2339,7 +2346,8 @@ public sealed class JiboInteractionServiceTests
|
|||||||
Attributes = new Dictionary<string, object?>(tenantAttributes)
|
Attributes = new Dictionary<string, object?>(tenantAttributes)
|
||||||
{
|
{
|
||||||
[HouseholdListStateKey] = promptDecision.ContextUpdates[HouseholdListStateKey],
|
[HouseholdListStateKey] = promptDecision.ContextUpdates[HouseholdListStateKey],
|
||||||
[HouseholdListTypeKey] = promptDecision.ContextUpdates[HouseholdListTypeKey]
|
[HouseholdListTypeKey] = promptDecision.ContextUpdates[HouseholdListTypeKey],
|
||||||
|
[HouseholdListDisplayTypeKey] = promptDecision.ContextUpdates[HouseholdListDisplayTypeKey]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2348,6 +2356,7 @@ public sealed class JiboInteractionServiceTests
|
|||||||
Assert.Contains("What else should I add?", addDecision.ReplyText, StringComparison.OrdinalIgnoreCase);
|
Assert.Contains("What else should I add?", addDecision.ReplyText, StringComparison.OrdinalIgnoreCase);
|
||||||
Assert.Equal("awaiting_item", addDecision.ContextUpdates![HouseholdListStateKey]);
|
Assert.Equal("awaiting_item", addDecision.ContextUpdates![HouseholdListStateKey]);
|
||||||
Assert.Equal("shopping", addDecision.ContextUpdates[HouseholdListTypeKey]);
|
Assert.Equal("shopping", addDecision.ContextUpdates[HouseholdListTypeKey]);
|
||||||
|
Assert.Equal("shopping", addDecision.ContextUpdates[HouseholdListDisplayTypeKey]);
|
||||||
Assert.Equal(["milk"],
|
Assert.Equal(["milk"],
|
||||||
memoryStore.GetListItems(new PersonalMemoryTenantScope("acct-a", "loop-a", "device-a"), "shopping"));
|
memoryStore.GetListItems(new PersonalMemoryTenantScope("acct-a", "loop-a", "device-a"), "shopping"));
|
||||||
|
|
||||||
@@ -2359,7 +2368,8 @@ public sealed class JiboInteractionServiceTests
|
|||||||
Attributes = new Dictionary<string, object?>(tenantAttributes)
|
Attributes = new Dictionary<string, object?>(tenantAttributes)
|
||||||
{
|
{
|
||||||
[HouseholdListStateKey] = addDecision.ContextUpdates[HouseholdListStateKey],
|
[HouseholdListStateKey] = addDecision.ContextUpdates[HouseholdListStateKey],
|
||||||
[HouseholdListTypeKey] = addDecision.ContextUpdates[HouseholdListTypeKey]
|
[HouseholdListTypeKey] = addDecision.ContextUpdates[HouseholdListTypeKey],
|
||||||
|
[HouseholdListDisplayTypeKey] = addDecision.ContextUpdates[HouseholdListDisplayTypeKey]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2367,6 +2377,7 @@ public sealed class JiboInteractionServiceTests
|
|||||||
Assert.Contains("Okay. Your shopping list has milk.", doneDecision.ReplyText,
|
Assert.Contains("Okay. Your shopping list has milk.", doneDecision.ReplyText,
|
||||||
StringComparison.OrdinalIgnoreCase);
|
StringComparison.OrdinalIgnoreCase);
|
||||||
Assert.Equal("idle", doneDecision.ContextUpdates![HouseholdListStateKey]);
|
Assert.Equal("idle", doneDecision.ContextUpdates![HouseholdListStateKey]);
|
||||||
|
Assert.Equal("shopping", doneDecision.ContextUpdates[HouseholdListDisplayTypeKey]);
|
||||||
|
|
||||||
var recallDecision = await service.BuildDecisionAsync(new TurnContext
|
var recallDecision = await service.BuildDecisionAsync(new TurnContext
|
||||||
{
|
{
|
||||||
@@ -2378,6 +2389,134 @@ public sealed class JiboInteractionServiceTests
|
|||||||
|
|
||||||
Assert.Equal("shopping_list_recall", recallDecision.IntentName);
|
Assert.Equal("shopping_list_recall", recallDecision.IntentName);
|
||||||
Assert.Contains("milk", recallDecision.ReplyText, StringComparison.OrdinalIgnoreCase);
|
Assert.Contains("milk", recallDecision.ReplyText, StringComparison.OrdinalIgnoreCase);
|
||||||
|
Assert.Contains("shopping list", recallDecision.ReplyText, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BuildDecisionAsync_GroceryList_DirectAddAndRecallVariants_UseGroceryWording()
|
||||||
|
{
|
||||||
|
var memoryStore = new InMemoryPersonalMemoryStore();
|
||||||
|
var service = CreateService(memoryStore);
|
||||||
|
var tenantAttributes = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["accountId"] = "acct-d",
|
||||||
|
["loopId"] = "loop-d"
|
||||||
|
};
|
||||||
|
|
||||||
|
var addStartDecision = await service.BuildDecisionAsync(new TurnContext
|
||||||
|
{
|
||||||
|
RawTranscript = "add to my grocery list",
|
||||||
|
NormalizedTranscript = "add to my grocery list",
|
||||||
|
DeviceId = "device-d",
|
||||||
|
Attributes = new Dictionary<string, object?>(tenantAttributes)
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal("shopping_list_prompt", addStartDecision.IntentName);
|
||||||
|
Assert.Equal("grocery", addStartDecision.ContextUpdates![HouseholdListDisplayTypeKey]);
|
||||||
|
Assert.Equal("What should I add to your grocery list?", addStartDecision.ReplyText);
|
||||||
|
|
||||||
|
var addDecision = await service.BuildDecisionAsync(new TurnContext
|
||||||
|
{
|
||||||
|
RawTranscript = "apples",
|
||||||
|
NormalizedTranscript = "apples",
|
||||||
|
DeviceId = "device-d",
|
||||||
|
Attributes = new Dictionary<string, object?>(tenantAttributes)
|
||||||
|
{
|
||||||
|
[HouseholdListStateKey] = addStartDecision.ContextUpdates[HouseholdListStateKey],
|
||||||
|
[HouseholdListTypeKey] = addStartDecision.ContextUpdates[HouseholdListTypeKey],
|
||||||
|
[HouseholdListDisplayTypeKey] = addStartDecision.ContextUpdates[HouseholdListDisplayTypeKey]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal("shopping_list_add", addDecision.IntentName);
|
||||||
|
Assert.Contains("Added apples to your grocery list.", addDecision.ReplyText, StringComparison.OrdinalIgnoreCase);
|
||||||
|
Assert.Equal(["apples"],
|
||||||
|
memoryStore.GetListItems(new PersonalMemoryTenantScope("acct-d", "loop-d", "device-d"), "shopping"));
|
||||||
|
|
||||||
|
var recallDecision = await service.BuildDecisionAsync(new TurnContext
|
||||||
|
{
|
||||||
|
RawTranscript = "what is on my grocery list",
|
||||||
|
NormalizedTranscript = "what is on my grocery list",
|
||||||
|
DeviceId = "device-d",
|
||||||
|
Attributes = new Dictionary<string, object?>(tenantAttributes)
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal("shopping_list_recall", recallDecision.IntentName);
|
||||||
|
Assert.Contains("apples", recallDecision.ReplyText, StringComparison.OrdinalIgnoreCase);
|
||||||
|
Assert.Contains("grocery list", recallDecision.ReplyText, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BuildDecisionAsync_GroceryList_FollowUpFlow_UsesGroceryWordingAndShoppingStorage()
|
||||||
|
{
|
||||||
|
var memoryStore = new InMemoryPersonalMemoryStore();
|
||||||
|
var service = CreateService(memoryStore);
|
||||||
|
var tenantAttributes = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["accountId"] = "acct-c",
|
||||||
|
["loopId"] = "loop-c"
|
||||||
|
};
|
||||||
|
|
||||||
|
var promptDecision = await service.BuildDecisionAsync(new TurnContext
|
||||||
|
{
|
||||||
|
RawTranscript = "grocery list",
|
||||||
|
NormalizedTranscript = "grocery list",
|
||||||
|
DeviceId = "device-c",
|
||||||
|
Attributes = new Dictionary<string, object?>(tenantAttributes)
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal("shopping_list_prompt", promptDecision.IntentName);
|
||||||
|
Assert.Equal("awaiting_item", promptDecision.ContextUpdates![HouseholdListStateKey]);
|
||||||
|
Assert.Equal("shopping", promptDecision.ContextUpdates[HouseholdListTypeKey]);
|
||||||
|
Assert.Equal("grocery", promptDecision.ContextUpdates[HouseholdListDisplayTypeKey]);
|
||||||
|
Assert.Equal("What should I add to your grocery list?", promptDecision.ReplyText);
|
||||||
|
|
||||||
|
var addDecision = await service.BuildDecisionAsync(new TurnContext
|
||||||
|
{
|
||||||
|
RawTranscript = "milk",
|
||||||
|
NormalizedTranscript = "milk",
|
||||||
|
DeviceId = "device-c",
|
||||||
|
Attributes = new Dictionary<string, object?>(tenantAttributes)
|
||||||
|
{
|
||||||
|
[HouseholdListStateKey] = promptDecision.ContextUpdates[HouseholdListStateKey],
|
||||||
|
[HouseholdListTypeKey] = promptDecision.ContextUpdates[HouseholdListTypeKey],
|
||||||
|
[HouseholdListDisplayTypeKey] = promptDecision.ContextUpdates[HouseholdListDisplayTypeKey]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal("shopping_list_add", addDecision.IntentName);
|
||||||
|
Assert.Contains("Added milk to your grocery list.", addDecision.ReplyText, StringComparison.OrdinalIgnoreCase);
|
||||||
|
Assert.Contains("What else should I add?", addDecision.ReplyText, StringComparison.OrdinalIgnoreCase);
|
||||||
|
Assert.Equal(["milk"],
|
||||||
|
memoryStore.GetListItems(new PersonalMemoryTenantScope("acct-c", "loop-c", "device-c"), "shopping"));
|
||||||
|
|
||||||
|
var doneDecision = await service.BuildDecisionAsync(new TurnContext
|
||||||
|
{
|
||||||
|
RawTranscript = "that's it",
|
||||||
|
NormalizedTranscript = "that's it",
|
||||||
|
DeviceId = "device-c",
|
||||||
|
Attributes = new Dictionary<string, object?>(tenantAttributes)
|
||||||
|
{
|
||||||
|
[HouseholdListStateKey] = addDecision.ContextUpdates![HouseholdListStateKey],
|
||||||
|
[HouseholdListTypeKey] = addDecision.ContextUpdates[HouseholdListTypeKey],
|
||||||
|
[HouseholdListDisplayTypeKey] = addDecision.ContextUpdates[HouseholdListDisplayTypeKey]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal("shopping_list_done", doneDecision.IntentName);
|
||||||
|
Assert.Contains("Okay. Your grocery list has milk.", doneDecision.ReplyText, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var recallDecision = await service.BuildDecisionAsync(new TurnContext
|
||||||
|
{
|
||||||
|
RawTranscript = "what's on my grocery list",
|
||||||
|
NormalizedTranscript = "what's on my grocery list",
|
||||||
|
DeviceId = "device-c",
|
||||||
|
Attributes = new Dictionary<string, object?>(tenantAttributes)
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal("shopping_list_recall", recallDecision.IntentName);
|
||||||
|
Assert.Contains("milk", recallDecision.ReplyText, StringComparison.OrdinalIgnoreCase);
|
||||||
|
Assert.Contains("grocery list", recallDecision.ReplyText, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
Reference in New Issue
Block a user