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)
|
||||
|
||||
- Status: `discovery`
|
||||
- Status: `in_progress`
|
||||
- Tags: `content`, `docs`, `storage`
|
||||
- Why now:
|
||||
- directly requested by Jibo owners and fits memory + household utility roadmap
|
||||
@@ -779,13 +779,14 @@ Current release theme:
|
||||
- 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)
|
||||
- MVP decision:
|
||||
- use the existing household list engine as the native lightweight grocery MVP
|
||||
- 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:
|
||||
- clear decision on MVP path
|
||||
- first schema for list items + ownership scope
|
||||
- initial voice flows and follow-up intent handling defined
|
||||
- grocery prompts, add/recall/done flows, and list follow-ups consistently speak grocery wording
|
||||
- existing shopping/to-do flows remain unchanged
|
||||
- future integration-backed list work remains a separate backlog item
|
||||
|
||||
### 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.
|
||||
|
||||
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
|
||||
|
||||
- Kickoff date: `2026-05-05`
|
||||
|
||||
@@ -7,11 +7,15 @@ internal static class HouseholdListOrchestrator
|
||||
{
|
||||
internal const string StateMetadataKey = "householdListState";
|
||||
internal const string TypeMetadataKey = "householdListType";
|
||||
internal const string DisplayTypeMetadataKey = "householdListDisplayType";
|
||||
internal const string NoMatchCountMetadataKey = "householdListNoMatchCount";
|
||||
internal const string NoInputCountMetadataKey = "householdListNoInputCount";
|
||||
|
||||
private const string IdleState = "idle";
|
||||
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 =
|
||||
[
|
||||
@@ -31,6 +35,10 @@ internal static class HouseholdListOrchestrator
|
||||
" to my shopping list",
|
||||
" to the 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 the to do list",
|
||||
" on my to do list",
|
||||
@@ -50,6 +58,7 @@ internal static class HouseholdListOrchestrator
|
||||
{
|
||||
var state = ReadString(turn, StateMetadataKey);
|
||||
var listType = ReadString(turn, TypeMetadataKey);
|
||||
var displayType = ReadString(turn, DisplayTypeMetadataKey);
|
||||
var isActiveState = !string.IsNullOrWhiteSpace(state) &&
|
||||
!string.Equals(state, IdleState, StringComparison.OrdinalIgnoreCase);
|
||||
var isShoppingIntent = string.Equals(semanticIntent, "shopping_list", StringComparison.OrdinalIgnoreCase);
|
||||
@@ -58,17 +67,19 @@ internal static class HouseholdListOrchestrator
|
||||
if (!isActiveState && !isShoppingIntent && !isTodoIntent)
|
||||
return Task.FromResult<JiboInteractionDecision?>(null);
|
||||
|
||||
var resolvedListType = isShoppingIntent ? "shopping" : isTodoIntent ? "todo" : NormalizeListType(listType);
|
||||
if (string.IsNullOrWhiteSpace(resolvedListType)) resolvedListType = "shopping";
|
||||
var resolvedListType = isShoppingIntent ? ShoppingListType : isTodoIntent ? TodoListType : NormalizeListType(listType);
|
||||
if (string.IsNullOrWhiteSpace(resolvedListType)) resolvedListType = ShoppingListType;
|
||||
var resolvedDisplayType = ResolveDisplayType(resolvedListType, displayType, isActiveState, loweredTranscript);
|
||||
|
||||
var tenantScope = tenantScopeResolver(turn);
|
||||
|
||||
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))
|
||||
return Task.FromResult<JiboInteractionDecision?>(BuildRecallDecision(
|
||||
resolvedListType,
|
||||
resolvedDisplayType,
|
||||
personalMemoryStore.GetListItems(tenantScope, resolvedListType)));
|
||||
|
||||
var directItem = TryExtractListItem(loweredTranscript);
|
||||
@@ -76,9 +87,9 @@ internal static class HouseholdListOrchestrator
|
||||
{
|
||||
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)));
|
||||
BuildListIntentName(resolvedListType, "done"),
|
||||
BuildDoneReply(resolvedDisplayType, personalMemoryStore.GetListItems(tenantScope, resolvedListType)),
|
||||
ContextUpdates: BuildContextUpdates(resolvedListType, resolvedDisplayType, IdleState)));
|
||||
|
||||
directItem = NormalizeItem(transcript);
|
||||
}
|
||||
@@ -87,104 +98,108 @@ internal static class HouseholdListOrchestrator
|
||||
{
|
||||
personalMemoryStore.AddListItem(tenantScope, resolvedListType, directItem);
|
||||
return Task.FromResult<JiboInteractionDecision?>(new JiboInteractionDecision(
|
||||
resolvedListType == "shopping" ? "shopping_list_add" : "todo_list_add",
|
||||
BuildAddedReply(resolvedListType, directItem,
|
||||
BuildListIntentName(resolvedListType, "add"),
|
||||
BuildAddedReply(resolvedDisplayType, directItem,
|
||||
personalMemoryStore.GetListItems(tenantScope, resolvedListType)),
|
||||
ContextUpdates: BuildContextUpdates(resolvedListType, AwaitingItemState)));
|
||||
ContextUpdates: BuildContextUpdates(resolvedListType, resolvedDisplayType, 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)));
|
||||
BuildListIntentName(resolvedListType, "prompt"),
|
||||
BuildPromptReply(resolvedDisplayType),
|
||||
ContextUpdates: BuildContextUpdates(resolvedListType, resolvedDisplayType, AwaitingItemState)));
|
||||
|
||||
return Task.FromResult<JiboInteractionDecision?>(new JiboInteractionDecision(
|
||||
resolvedListType == "shopping" ? "shopping_list_prompt" : "todo_list_prompt",
|
||||
BuildPromptReply(resolvedListType),
|
||||
ContextUpdates: BuildContextUpdates(resolvedListType, AwaitingItemState)));
|
||||
BuildListIntentName(resolvedListType, "prompt"),
|
||||
BuildPromptReply(resolvedDisplayType),
|
||||
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)
|
||||
{
|
||||
[StateMetadataKey] = state,
|
||||
[TypeMetadataKey] = listType,
|
||||
[DisplayTypeMetadataKey] = displayType,
|
||||
[NoMatchCountMetadataKey] = 0,
|
||||
[NoInputCountMetadataKey] = 0
|
||||
};
|
||||
}
|
||||
|
||||
private static JiboInteractionDecision BuildCancelledDecision(string listType)
|
||||
private static JiboInteractionDecision BuildCancelledDecision(string listType, string displayType)
|
||||
{
|
||||
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.",
|
||||
BuildListIntentName(listType, "cancel"),
|
||||
$"Okay. I stopped the {BuildListLabel(displayType)}.",
|
||||
ContextUpdates: new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
[StateMetadataKey] = IdleState,
|
||||
[TypeMetadataKey] = listType,
|
||||
[DisplayTypeMetadataKey] = displayType,
|
||||
[NoMatchCountMetadataKey] = 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)
|
||||
return new JiboInteractionDecision(
|
||||
listType == "shopping" ? "shopping_list_recall" : "todo_list_recall",
|
||||
listType == "shopping"
|
||||
? "Your shopping list is empty."
|
||||
: "Your to-do list is empty.",
|
||||
BuildListIntentName(listType, "recall"),
|
||||
$"Your {BuildListLabel(displayType)} is empty.",
|
||||
ContextUpdates: new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
[StateMetadataKey] = IdleState,
|
||||
[TypeMetadataKey] = listType,
|
||||
[DisplayTypeMetadataKey] = displayType,
|
||||
[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)}.",
|
||||
BuildListIntentName(listType, "recall"),
|
||||
$"Your {BuildListLabel(displayType)} has {JoinList(items)}.",
|
||||
ContextUpdates: new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
[StateMetadataKey] = IdleState,
|
||||
[TypeMetadataKey] = listType,
|
||||
[DisplayTypeMetadataKey] = displayType,
|
||||
[NoMatchCountMetadataKey] = 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
|
||||
? $"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)
|
||||
private static string BuildPromptReply(string displayType)
|
||||
{
|
||||
return listType == "shopping"
|
||||
? "What should I add to your shopping list?"
|
||||
: "What should I add to your to-do list?";
|
||||
return $"What should I add to your {BuildListLabel(displayType)}?";
|
||||
}
|
||||
|
||||
private static string BuildDoneReply(string listType, IReadOnlyList<string> items)
|
||||
private static string BuildDoneReply(string displayType, IReadOnlyList<string> items)
|
||||
{
|
||||
if (items.Count == 0)
|
||||
return listType == "shopping"
|
||||
? "Okay. Your shopping list is empty."
|
||||
: "Okay. Your to-do list is empty.";
|
||||
return $"Okay. Your {BuildListLabel(displayType)} is empty.";
|
||||
|
||||
return listType == "shopping"
|
||||
? $"Okay. Your shopping list has {JoinList(items)}."
|
||||
: $"Okay. Your to-do list has {JoinList(items)}.";
|
||||
return $"Okay. Your {BuildListLabel(displayType)} 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)
|
||||
@@ -205,7 +220,13 @@ internal static class HouseholdListOrchestrator
|
||||
if (!loweredTranscript.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) continue;
|
||||
|
||||
var remainder = loweredTranscript[prefix.Length..].Trim();
|
||||
if (IsListOnlyRemainder(remainder))
|
||||
return null;
|
||||
|
||||
remainder = TrimTrailingListPhrases(remainder);
|
||||
if (IsListOnlyRemainder(remainder))
|
||||
return null;
|
||||
|
||||
return NormalizeItem(remainder);
|
||||
}
|
||||
|
||||
@@ -218,6 +239,9 @@ internal static class HouseholdListOrchestrator
|
||||
"what is on my shopping list",
|
||||
"what's on 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's on my to do list",
|
||||
"show my to do list",
|
||||
@@ -246,13 +270,96 @@ internal static class HouseholdListOrchestrator
|
||||
var normalized = NormalizeItem(listType ?? string.Empty).ToLowerInvariant();
|
||||
return normalized.Contains("todo", StringComparison.OrdinalIgnoreCase) ||
|
||||
normalized.Contains("to do", StringComparison.OrdinalIgnoreCase)
|
||||
? "todo"
|
||||
? TodoListType
|
||||
: normalized.Contains("shopping", StringComparison.OrdinalIgnoreCase) ||
|
||||
normalized.Contains("grocery", StringComparison.OrdinalIgnoreCase)
|
||||
? "shopping"
|
||||
? ShoppingListType
|
||||
: 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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -780,13 +780,19 @@ public sealed partial class JiboInteractionService
|
||||
loweredTranscript,
|
||||
"shopping list",
|
||||
"grocery list",
|
||||
"my grocery list",
|
||||
"create grocery list",
|
||||
"start grocery list",
|
||||
"to do list",
|
||||
"todo list",
|
||||
"add to my shopping list",
|
||||
"add to my grocery list",
|
||||
"add to my to do list",
|
||||
"add to my todo list",
|
||||
"what's 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 is on my to do list",
|
||||
"what are my tasks",
|
||||
|
||||
@@ -23,6 +23,7 @@ public sealed class JiboInteractionServiceTests
|
||||
private const string PersonalReportNewsEnabledKey = "personalReportNewsEnabled";
|
||||
private const string HouseholdListStateKey = "householdListState";
|
||||
private const string HouseholdListTypeKey = "householdListType";
|
||||
private const string HouseholdListDisplayTypeKey = "householdListDisplayType";
|
||||
private const string ChitchatStateKey = "chitchatState";
|
||||
private const string ChitchatRouteKey = "chitchatRoute";
|
||||
private const string ChitchatEmotionKey = "chitchatEmotion";
|
||||
@@ -2285,13 +2286,17 @@ public sealed class JiboInteractionServiceTests
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("shopping list", "shopping_list_prompt", "What should I add to your shopping list?", "shopping")]
|
||||
[InlineData("to do list", "todo_list_prompt", "What should I add to your to-do list?", "todo")]
|
||||
[InlineData("shopping list", "shopping_list_prompt", "What should I add to your shopping list?", "shopping", "shopping")]
|
||||
[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(
|
||||
string transcript,
|
||||
string expectedIntent,
|
||||
string expectedReply,
|
||||
string expectedListType)
|
||||
string expectedListType,
|
||||
string expectedDisplayType)
|
||||
{
|
||||
var service = CreateService();
|
||||
|
||||
@@ -2306,6 +2311,7 @@ public sealed class JiboInteractionServiceTests
|
||||
Assert.NotNull(decision.ContextUpdates);
|
||||
Assert.Equal("awaiting_item", decision.ContextUpdates![HouseholdListStateKey]);
|
||||
Assert.Equal(expectedListType, decision.ContextUpdates[HouseholdListTypeKey]);
|
||||
Assert.Equal(expectedDisplayType, decision.ContextUpdates[HouseholdListDisplayTypeKey]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -2330,6 +2336,7 @@ public sealed class JiboInteractionServiceTests
|
||||
Assert.Equal("shopping_list_prompt", promptDecision.IntentName);
|
||||
Assert.Equal("awaiting_item", promptDecision.ContextUpdates![HouseholdListStateKey]);
|
||||
Assert.Equal("shopping", promptDecision.ContextUpdates[HouseholdListTypeKey]);
|
||||
Assert.Equal("shopping", promptDecision.ContextUpdates[HouseholdListDisplayTypeKey]);
|
||||
|
||||
var addDecision = await service.BuildDecisionAsync(new TurnContext
|
||||
{
|
||||
@@ -2339,7 +2346,8 @@ public sealed class JiboInteractionServiceTests
|
||||
Attributes = new Dictionary<string, object?>(tenantAttributes)
|
||||
{
|
||||
[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.Equal("awaiting_item", addDecision.ContextUpdates![HouseholdListStateKey]);
|
||||
Assert.Equal("shopping", addDecision.ContextUpdates[HouseholdListTypeKey]);
|
||||
Assert.Equal("shopping", addDecision.ContextUpdates[HouseholdListDisplayTypeKey]);
|
||||
Assert.Equal(["milk"],
|
||||
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)
|
||||
{
|
||||
[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,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Equal("idle", doneDecision.ContextUpdates![HouseholdListStateKey]);
|
||||
Assert.Equal("shopping", doneDecision.ContextUpdates[HouseholdListDisplayTypeKey]);
|
||||
|
||||
var recallDecision = await service.BuildDecisionAsync(new TurnContext
|
||||
{
|
||||
@@ -2378,6 +2389,134 @@ public sealed class JiboInteractionServiceTests
|
||||
|
||||
Assert.Equal("shopping_list_recall", recallDecision.IntentName);
|
||||
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]
|
||||
|
||||
Reference in New Issue
Block a user