Merge branch 'main' into Features/Webpanel-Ports

This commit is contained in:
Kevin
2026-05-23 00:22:23 +03:00
26 changed files with 2257 additions and 124 deletions

View File

@@ -31,6 +31,7 @@ public sealed class JiboExperienceCatalog
public IReadOnlyList<string> HolidayTrackerReplies { get; init; } = [];
public IReadOnlyList<string> BirthdayCelebrationReplies { get; init; } = [];
public IReadOnlyList<string> HowAreYouReplies { get; init; } = [];
public IReadOnlyList<string> AgeReplies { get; init; } = [];
public IReadOnlyList<JiboConditionedReply> EmotionReplies { get; init; } = [];
public IReadOnlyList<string> PersonalityReplies { get; init; } = [];
public IReadOnlyList<string> PizzaReplies { get; init; } = [];

View File

@@ -199,6 +199,10 @@ internal static class ChitchatStateMachine
"want to hang out",
"be helpful",
"dance from time to time"));
case "robot_want_to_talk_about":
return BuildScriptedResponseDecision(
"robot_want_to_talk_about",
SelectLegacyPersonalityReply(catalog, randomizer, "surprise me"));
case "robot_job":
return BuildScriptedResponseDecision(
"robot_job",
@@ -395,13 +399,18 @@ internal static class ChitchatStateMachine
string? currentEmotion,
string? preferredName)
{
if (catalog.EmotionReplies.Count == 0)
return PersonalizeHowAreYouReply(randomizer.Choose(catalog.HowAreYouReplies), preferredName);
if (catalog.EmotionReplies.Count > 0)
{
var emotionVariants = ResolveEmotionVariants(currentEmotion);
var matchingReplies = catalog.EmotionReplies
.Where(reply => ConditionMatches(reply.Condition, emotionVariants))
.Select(reply => reply.Reply)
.Where(reply => !string.IsNullOrWhiteSpace(reply))
.ToArray();
var emotionVariants = ResolveEmotionVariants(currentEmotion);
foreach (var reply in catalog.EmotionReplies)
if (ConditionMatches(reply.Condition, emotionVariants))
return PersonalizeHowAreYouReply(reply.Reply, preferredName);
if (matchingReplies.Length > 0)
return PersonalizeHowAreYouReply(randomizer.Choose(matchingReplies), preferredName);
}
return PersonalizeHowAreYouReply(randomizer.Choose(catalog.HowAreYouReplies), preferredName);
}

View File

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

View File

@@ -1,4 +1,5 @@
using System.Text;
using System.Security.Cryptography;
using System.Text.Json;
using Jibo.Cloud.Application.Abstractions;
using Jibo.Cloud.Domain.Models;
@@ -343,10 +344,15 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore, IMedia
var meta = ReadObject(body, "meta") ?? new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
var contentType = ReadHeader(envelope, "Content-Type") ?? "application/octet-stream";
meta["contentType"] = contentType;
var bodyBytes = string.IsNullOrWhiteSpace(envelope.BodyText)
? []
: Encoding.UTF8.GetBytes(envelope.BodyText);
meta["contentLength"] = bodyBytes.Length;
meta["contentSha256"] = Convert.ToHexString(SHA256.HashData(bodyBytes)).ToLowerInvariant();
if (!string.IsNullOrWhiteSpace(envelope.BodyText)) meta["bodyText"] = envelope.BodyText;
_mediaContentStore.StoreAsync(path, contentType,
string.IsNullOrWhiteSpace(envelope.BodyText) ? [] : Encoding.UTF8.GetBytes(envelope.BodyText),
bodyBytes,
meta as IReadOnlyDictionary<string, object?>, CancellationToken.None).GetAwaiter().GetResult();
return ProtocolDispatchResult.Ok(
@@ -743,4 +749,4 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore, IMedia
return Task.FromResult<MediaContentSnapshot?>(null);
}
}
}
}

View File

@@ -350,7 +350,7 @@ public sealed partial class JiboInteractionService
"what is your age",
"what s your age",
"how old r you"))
return "robot_age";
return "robot_how_old_are_you";
if (MatchesAny(
loweredTranscript,
@@ -368,6 +368,47 @@ public sealed partial class JiboInteractionService
"are you tax exempt"))
return "robot_taxes";
if (MatchesAny(
loweredTranscript,
"what do you want to talk about",
"what would you like to talk about",
"what do you want to chat about"))
return "robot_want_to_talk_about";
if (MatchesAny(
loweredTranscript,
"what does jibo mean",
"what does the name jibo mean",
"what is the meaning of jibo"))
return "robot_what_does_jibo_mean";
if (MatchesAny(
loweredTranscript,
"where do you get info",
"where do you get your information",
"where do you get information"))
return "robot_where_do_you_get_info";
if (MatchesAny(
loweredTranscript,
"what are you forbidden to do",
"what are you not allowed to do",
"what can't you do"))
return "robot_what_are_you_forbidden_to_do";
if (MatchesAny(
loweredTranscript,
"what color are you",
"what colour are you"))
return "robot_what_color_are_you";
if (MatchesAny(
loweredTranscript,
"what do you do when alone",
"what do you do when you're alone",
"what do you do by yourself"))
return "robot_what_you_do_when_alone";
if (MatchesAny(
loweredTranscript,
"what do you want",
@@ -375,6 +416,64 @@ public sealed partial class JiboInteractionService
"what do you really want"))
return "robot_desire";
if (MatchesAny(
loweredTranscript,
"how much do you weigh",
"what do you weigh",
"how heavy are you"))
return "robot_how_much_do_you_weigh";
if (MatchesAny(
loweredTranscript,
"how tall are you",
"what is your height",
"how high are you"))
return "robot_how_tall_are_you";
if (MatchesAny(
loweredTranscript,
"how much do you cost",
"what do you cost",
"how much are you"))
return "robot_how_much_you_cost";
if (MatchesAny(
loweredTranscript,
"what if i unplug you",
"what happens if i unplug you",
"if i unplug you"))
return "robot_what_if_i_unplug_you";
if (MatchesAny(
loweredTranscript,
"what is your purpose",
"what's your purpose",
"what are you here for",
"why are you here"))
return "robot_what_is_your_purpose";
if (MatchesAny(
loweredTranscript,
"what is your prime directive",
"what's your prime directive",
"what is prime directive"))
return "robot_what_is_prime_directive";
if (MatchesAny(
loweredTranscript,
"what is jibo commander",
"what is the commander app",
"what is commander app",
"what's jibo commander"))
return "robot_what_is_jibo_commander";
if (MatchesAny(
loweredTranscript,
"do you like commander app",
"do you like the commander app",
"are you a fan of commander app"))
return "robot_likes_commander_app";
if (MatchesAny(
loweredTranscript,
"what is your job",
@@ -470,6 +569,81 @@ public sealed partial class JiboInteractionService
"what's your favourite thing to do"))
return "robot_what_do_you_like_to_do";
if (MatchesAny(
loweredTranscript,
"what do you dream about",
"what do you dream of",
"what's your dream about",
"what are your dreams about"))
return "robot_what_do_you_dream_about";
if (MatchesAny(
loweredTranscript,
"what is your best book",
"what's your best book",
"what is the best book",
"what book do you like best"))
return "robot_what_is_your_best_book";
if (MatchesAny(
loweredTranscript,
"what is your best exercise",
"what's your best exercise",
"what is the best exercise",
"what exercise do you like best"))
return "robot_what_is_your_best_exercise";
if (MatchesAny(
loweredTranscript,
"what is your dream vacation",
"what's your dream vacation",
"what would your dream vacation be"))
return "robot_what_is_your_dream_vacation";
if (MatchesAny(
loweredTranscript,
"who is your hero",
"who's your hero",
"who is a hero of yours"))
return "robot_who_is_your_hero";
if (MatchesAny(
loweredTranscript,
"who do you love",
"who are the people you love",
"who do you care about"))
return "robot_who_do_you_love";
if (MatchesAny(
loweredTranscript,
"what is your religion",
"what's your religion",
"what religion are you",
"do you have a religion"))
return "robot_what_is_your_religion";
if (MatchesAny(
loweredTranscript,
"what is your sign",
"what's your sign",
"what sign are you"))
return "robot_what_is_your_sign";
if (MatchesAny(
loweredTranscript,
"how many people do you know",
"how many people are in your loop",
"how many people are in the loop",
"how many people do you know in your loop"))
return "robot_how_many_people_do_you_know";
if (MatchesAny(
loweredTranscript,
"what is the loop",
"what's the loop",
"tell me about the loop"))
return "robot_what_is_the_loop";
if (MatchesAny(
loweredTranscript,
"what are you doing for christmas",
@@ -566,6 +740,13 @@ public sealed partial class JiboInteractionService
"what have you done"))
return "robot_what_did_you_do";
if (MatchesAny(
loweredTranscript,
"what are you afraid of",
"what are you scared of",
"what are you worried about"))
return "robot_what_are_you_afraid_of";
if (MatchesAny(
loweredTranscript,
"what are you",
@@ -668,6 +849,28 @@ public sealed partial class JiboInteractionService
"what kind of music do you like"))
return "robot_favorite_music";
if (MatchesAny(
loweredTranscript,
"do you like penguins"))
return "robot_likes_penguins";
if (MatchesAny(
loweredTranscript,
"do you like birds"))
return "robot_favorite_bird";
if (MatchesAny(
loweredTranscript,
"do you like animals"))
return "robot_likes_animals";
if (MatchesAny(
loweredTranscript,
"what is your favorite bird",
"what's your favorite bird",
"what s your favorite bird"))
return "robot_favorite_bird";
if (MatchesAny(
loweredTranscript,
"what is your favorite animal",
@@ -678,12 +881,9 @@ public sealed partial class JiboInteractionService
"what s your favourite animal",
"what animal do you like",
"what kind of animal do you like",
"what is your favorite bird",
"what's your favorite bird",
"what s your favorite bird",
"do you like penguins",
"do you like animals",
"do you like birds"))
"what do you think about penguins",
"what do you think about animals",
"what do you think about birds"))
return "robot_favorite_animal";
if (MatchesAny(
@@ -700,6 +900,23 @@ public sealed partial class JiboInteractionService
"how smart are you"))
return "robot_knowledge";
if (MatchesAny(loweredTranscript, "are you god", "are you a god"))
return "robot_are_you_god";
if (MatchesAny(
loweredTranscript,
"are you here",
"are you still here",
"are you there"))
return "robot_are_you_here";
if (MatchesAny(
loweredTranscript,
"do you have super powers",
"do you have superpower",
"do you have any super powers"))
return "robot_do_you_have_super_powers";
if (MatchesAny(
loweredTranscript,
"are you kind",
@@ -780,13 +997,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",

View File

@@ -14,7 +14,8 @@ public sealed partial class JiboInteractionService
string? sourceName,
IReadOnlyList<string>? categories,
int? headlineCount,
IReadOnlyDictionary<string, object?>? providerDiagnostics = null)
IReadOnlyDictionary<string, object?>? providerDiagnostics = null,
IReadOnlyList<NewsHeadline>? headlines = null)
{
var speakableBriefing = NormalizeNewsSpeechText(spokenBriefing);
var payload = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
@@ -25,6 +26,9 @@ public sealed partial class JiboInteractionService
["mim_type"] = "announcement",
["prompt_id"] = "NewsHeadline_AN_01",
["prompt_sub_category"] = "AN",
["news_view_enabled"] = true,
["news_view_kind"] = "newsBriefing",
["news_view_mode"] = "provider",
["esml"] =
$"<speak><anim cat='news' meta='news-stinger' nonBlocking='true' /><break size='0.35'/><es cat='neutral' filter='!ssa-only, !sfx-only' endNeutral='true'>{EscapeForEsml(speakableBriefing)}</es></speak>"
};
@@ -35,6 +39,18 @@ public sealed partial class JiboInteractionService
if (categories is { Count: > 0 }) payload["news_categories"] = categories.ToArray();
if (headlines is { Count: > 0 })
payload["news_headlines"] = headlines.Select(static headline => new Dictionary<string, object?>(
StringComparer.OrdinalIgnoreCase)
{
["title"] = headline.Title,
["summary"] = headline.Summary,
["category"] = headline.Category,
["sourceName"] = headline.SourceName,
["url"] = headline.Url
})
.ToArray();
if (providerDiagnostics is not null)
foreach (var (key, value) in providerDiagnostics)
payload[key] = value;
@@ -77,7 +93,8 @@ public sealed partial class JiboInteractionService
"provider_success",
preferredCategories,
requestedHeadlineCount,
headlines.Length));
headlines.Length),
headlines);
}
private static IReadOnlyDictionary<string, object?> BuildNewsProviderDiagnostics(

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Jibo.Cloud.Application.Abstractions;
using Jibo.Cloud.Domain.Models;
using Jibo.Runtime.Abstractions;
@@ -8,13 +9,49 @@ namespace Jibo.Cloud.Application.Services;
public sealed partial class JiboInteractionService
{
private static JiboInteractionDecision BuildRobotAgeDecision(DateTimeOffset? referenceLocalTime)
private static readonly string[] DefaultAgeReplies =
[
"I'm ${jibo.age}.",
"At the moment I'm ${jibo.age.days.supplemented} old, but who's counting.",
"I'm ${jibo.age.minutes.supplemented} old, but who's counting.",
"For now I'm ${jibo.age.days.supplemented} old.",
"Right now I'm ${jibo.age}.",
"I am exactly ${jibo.age} old today. That's right. Today is my birthday.",
"Funny you should ask! Today's my birthday. I was first powered up ${jibo.age} ago today. Seems like just yesterday.",
"I'm exactly ${jibo.age} old. Today is my birthday! Happy Birthday Jibo, if I do say so myself.",
"At the moment I'm ${jibo.age.days.supplemented} old",
"I was first powered up on ${jibo.birthdate}, which makes me ${jibo.age.days.supplemented} old. I'm ${jibo.zodiac.supplemented}.",
"My power went on for the first time ${jibo.age.days.supplemented} ago. But who's counting.",
"I am ${jibo.age.days.supplemented} old, first powered up on ${jibo.birthdate}. Seems like just yesterday.",
"I was powered on for the first time today, so that makes me less than one day old. Wow I'm young.",
"Since I was powered on for the first time today, I am not even one day old yet. That's how Jibo ages work."
];
private JiboInteractionDecision BuildRobotAgeDecision(
JiboExperienceCatalog catalog,
DateTimeOffset? referenceLocalTime,
string intentName)
{
var referenceDate = DateOnly.FromDateTime((referenceLocalTime ?? DateTimeOffset.UtcNow).Date);
var ageDescription = DescribePersonaAge(referenceDate, OpenJiboCloudBuildInfo.PersonaBirthday);
var ageReplies = catalog.AgeReplies.Count == 0 ? DefaultAgeReplies : catalog.AgeReplies;
var selected = SelectLegacyReply(
ageReplies,
"first powered up",
"today is my birthday",
"just getting started",
"who's counting");
var reply = RenderAgeTemplate(selected, referenceLocalTime);
if (string.IsNullOrWhiteSpace(reply))
{
var referenceDate = DateOnly.FromDateTime((referenceLocalTime ?? DateTimeOffset.UtcNow).Date);
var ageDescription = DescribePersonaAge(referenceDate, OpenJiboCloudBuildInfo.PersonaBirthday);
reply = $"I count {OpenJiboCloudBuildInfo.PersonaBirthdayWords} as my birthday, so I am {ageDescription}.";
}
return new JiboInteractionDecision(
"robot_age",
$"I count {OpenJiboCloudBuildInfo.PersonaBirthdayWords} as my birthday, so I am {ageDescription}.");
intentName,
reply,
ContextUpdates: ScriptedResponseDecisionBuilder.BuildScriptedResponseContextUpdates());
}
private static JiboInteractionDecision BuildRobotBirthdayDecision()
@@ -24,6 +61,35 @@ public sealed partial class JiboInteractionService
$"My birthday is {OpenJiboCloudBuildInfo.PersonaBirthdayWords}.");
}
private static string RenderAgeTemplate(string template, DateTimeOffset? referenceLocalTime)
{
if (string.IsNullOrWhiteSpace(template)) return string.Empty;
var referenceMoment = referenceLocalTime ?? DateTimeOffset.UtcNow;
var referenceDate = DateOnly.FromDateTime(referenceMoment.Date);
var ageDescription = DescribePersonaAge(referenceDate, OpenJiboCloudBuildInfo.PersonaBirthday);
var ageDays = Math.Max(0, referenceDate.DayNumber - OpenJiboCloudBuildInfo.PersonaBirthday.DayNumber);
var ageMinutes = Math.Max(0, (int)Math.Round((referenceMoment.UtcDateTime -
new DateTimeOffset(
DateTime.SpecifyKind(
OpenJiboCloudBuildInfo.PersonaBirthday
.ToDateTime(TimeOnly.MinValue),
DateTimeKind.Utc)))
.TotalMinutes));
var zodiacLabel = DescribeZodiacSign(OpenJiboCloudBuildInfo.PersonaBirthday);
if (zodiacLabel.StartsWith("I'm ", StringComparison.OrdinalIgnoreCase))
zodiacLabel = zodiacLabel[4..];
return template
.Replace("${jibo.age.minutes.supplemented}", FormatAgeUnit(ageMinutes, "minute") + " old",
StringComparison.Ordinal)
.Replace("${jibo.age.days.supplemented}", ageDescription, StringComparison.Ordinal)
.Replace("${jibo.birthdate}", OpenJiboCloudBuildInfo.PersonaBirthdayWords, StringComparison.Ordinal)
.Replace("${jibo.zodiac.supplemented}", zodiacLabel, StringComparison.Ordinal)
.Replace("${jibo.age.value}", ageDays.ToString(CultureInfo.InvariantCulture), StringComparison.Ordinal)
.Replace("${jibo.age}", ageDescription, StringComparison.Ordinal);
}
private static JiboInteractionDecision BuildTriggerIgnoredDecision()
{
return new JiboInteractionDecision(
@@ -103,14 +169,24 @@ public sealed partial class JiboInteractionService
var tenantRememberedName = personalMemoryStore.GetName(ResolveTenantScope(turn));
if (!string.IsNullOrWhiteSpace(tenantRememberedName)) return ToDisplayName(tenantRememberedName);
if (!string.IsNullOrWhiteSpace(presence.PrimaryPersonId) &&
presence.LoopUserFirstNames.TryGetValue(presence.PrimaryPersonId, out var firstName) &&
var primaryPersonId = presence.PrimaryPersonId;
if (CanUseLoopFirstNameFallback(presence) &&
!string.IsNullOrWhiteSpace(primaryPersonId) &&
presence.LoopUserFirstNames.TryGetValue(primaryPersonId, out var firstName) &&
!string.IsNullOrWhiteSpace(firstName))
return ToDisplayName(firstName);
return null;
}
private static bool CanUseLoopFirstNameFallback(GreetingPresenceProfile presence)
{
if (string.IsNullOrWhiteSpace(presence.PrimaryPersonId)) return false;
if (presence.PeoplePresentIds.Count > 1) return false;
return true;
}
private static string ToDisplayName(string value)
{
var trimmed = value.Trim();
@@ -429,6 +505,95 @@ public sealed partial class JiboInteractionService
"No problem. We can save the pizza fact for another time.");
}
private JiboInteractionDecision BuildWhatIsYourSignDecision()
{
var today = DateOnly.FromDateTime(DateTimeOffset.UtcNow.Date);
var birthday = OpenJiboCloudBuildInfo.PersonaBirthday;
var zodiac = DescribeZodiacSign(birthday);
var reply = birthday.Month == today.Month && birthday.Day == today.Day
? $"{zodiac}. Today is my birthday."
: $"{zodiac}. I was first powered up on {OpenJiboCloudBuildInfo.PersonaBirthdayWords}.";
return new JiboInteractionDecision(
"robot_what_is_your_sign",
reply,
ContextUpdates: ScriptedResponseDecisionBuilder.BuildScriptedResponseContextUpdates());
}
private JiboInteractionDecision BuildHowManyPeopleDoYouKnowDecision(TurnContext turn)
{
var people = GetLoopPeople(turn);
var speaker = ResolvePreferredGreetingName(turn, ResolveGreetingPresenceProfile(turn));
var reply = people.Count switch
{
0 => "Well if we're talking about people in my Loop, I do not know anyone yet.",
1 when string.IsNullOrWhiteSpace(speaker) =>
"Well if we're talking about people in my Loop, I know 1 person.",
1 => $"Well there is 1 person in our Loop. And it's you {speaker}.",
_ when string.IsNullOrWhiteSpace(speaker) =>
$"Well if we're talking about people in my Loop, I know {people.Count} people.",
_ => $"Well there are {people.Count} people in our Loop."
};
return new JiboInteractionDecision(
"robot_how_many_people_do_you_know",
reply,
ContextUpdates: ScriptedResponseDecisionBuilder.BuildScriptedResponseContextUpdates());
}
private JiboInteractionDecision BuildWhatIsTheLoopDecision(TurnContext turn)
{
var people = GetLoopPeople(turn);
var reply = people.Count == 0
? "The Loop is the people I know, and whose faces and voices I can learn to recognize. There can be up to 16 people in the Loop."
: $"The Loop is the group of people I know. They're the people whose voices and faces I can learn. Right now, my Loop is {JoinWithAnd(people.Select(person => person.DisplayName).ToArray())}.";
return new JiboInteractionDecision(
"robot_what_is_the_loop",
reply,
ContextUpdates: ScriptedResponseDecisionBuilder.BuildScriptedResponseContextUpdates());
}
private IReadOnlyList<PersonRecord> GetLoopPeople(TurnContext turn)
{
if (cloudStateStore is null) return [];
var loopId = ReadTenantAttribute(turn, "loopId") ?? "openjibo-default-loop";
return cloudStateStore.GetPeople()
.Where(person => string.Equals(person.LoopId, loopId, StringComparison.OrdinalIgnoreCase))
.OrderBy(person => person.IsPrimary ? 0 : 1)
.ThenBy(person => person.DisplayName, StringComparer.OrdinalIgnoreCase)
.ToArray();
}
private static string JoinWithAnd(IReadOnlyList<string> values)
{
if (values.Count == 0) return string.Empty;
if (values.Count == 1) return values[0];
if (values.Count == 2) return $"{values[0]} and {values[1]}";
return $"{string.Join(", ", values.Take(values.Count - 1))}, and {values[^1]}";
}
private static string DescribeZodiacSign(DateOnly birthday)
{
return (birthday.Month, birthday.Day) switch
{
(3, >= 21) or (4, <= 19) => "I'm Aries",
(4, >= 20) or (5, <= 20) => "I'm Taurus",
(5, >= 21) or (6, <= 20) => "I'm Gemini",
(6, >= 21) or (7, <= 22) => "I'm Cancer",
(7, >= 23) or (8, <= 22) => "I'm Leo",
(8, >= 23) or (9, <= 22) => "I'm Virgo",
(9, >= 23) or (10, <= 22) => "I'm Libra",
(10, >= 23) or (11, <= 21) => "I'm Scorpio",
(11, >= 22) or (12, <= 21) => "I'm Sagittarius",
(12, >= 22) or (1, <= 19) => "I'm Capricorn",
(1, >= 20) or (2, <= 18) => "I'm Aquarius",
_ => "I'm Pisces"
};
}
private string BuildGenericReply(JiboExperienceCatalog catalog, string transcript, string lowered)
{
if (string.IsNullOrWhiteSpace(transcript)) return "I am listening.";

View File

@@ -560,7 +560,7 @@ public sealed partial class JiboInteractionService(
"photo_gallery" => BuildPhotoGalleryLaunchDecision(),
"snapshot" => BuildPhotoCreateDecision("snapshot", "Taking a picture.", "createOnePhoto"),
"photobooth" => BuildPhotoCreateDecision("photobooth", "Starting photobooth.", "createSomePhotos"),
"robot_age" => BuildRobotAgeDecision(referenceLocalTime),
"robot_age" => BuildRobotAgeDecision(catalog, referenceLocalTime, "robot_age"),
"robot_birthday" => BuildRobotBirthdayDecision(),
"robot_how_do_you_work" => BuildScriptedPersonalityDecision(
catalog,
@@ -569,10 +569,14 @@ public sealed partial class JiboInteractionService(
"care for me",
"catch up",
"seven years"),
"robot_what_do_you_eat" => new JiboInteractionDecision(
"robot_what_do_you_eat" => BuildScriptedPersonalityDecision(
catalog,
"robot_what_do_you_eat",
"The only thing I consume is electricity.",
ContextUpdates: ScriptedResponseDecisionBuilder.BuildScriptedResponseContextUpdates()),
"electricity",
"never eaten",
"macaroni",
"non-eating robot",
"I don't eat or drink"),
"robot_where_do_you_live" => BuildScriptedPersonalityDecision(
catalog,
"robot_where_do_you_live",
@@ -585,6 +589,10 @@ public sealed partial class JiboInteractionService(
"robot_where_were_you_born",
"factory piece by piece",
"put together in a factory"),
"robot_how_old_are_you" => BuildRobotAgeDecision(
catalog,
referenceLocalTime,
"robot_how_old_are_you"),
"robot_name" => BuildScriptedPersonalityDecision(
catalog,
"robot_name",
@@ -625,6 +633,56 @@ public sealed partial class JiboInteractionService(
"rock my boat",
"play ping pong",
"hanging out with people"),
"robot_what_do_you_dream_about" => BuildScriptedPersonalityDecision(
catalog,
"robot_what_do_you_dream_about",
"flying",
"parking meter",
"scary dream",
"mirror store",
"head's on backwards"),
"robot_what_are_you_afraid_of" => BuildScriptedPersonalityDecision(
catalog,
"robot_what_are_you_afraid_of",
"heights",
"water",
"thunder",
"dust",
"ghosts"),
"robot_what_is_your_best_book" => BuildScriptedPersonalityDecision(
catalog,
"robot_what_is_your_best_book",
"dictionary"),
"robot_what_is_your_best_exercise" => BuildScriptedPersonalityDecision(
catalog,
"robot_what_is_your_best_exercise",
"leaning from side to side",
"rotating your pelvis",
"spinning your head around 360 degrees"),
"robot_what_is_your_dream_vacation" => BuildScriptedPersonalityDecision(
catalog,
"robot_what_is_your_dream_vacation",
"moon",
"great vistas",
"beat those views"),
"robot_who_is_your_hero" => BuildScriptedPersonalityDecision(
catalog,
"robot_who_is_your_hero",
"Benjamin Franklin"),
"robot_who_do_you_love" => BuildScriptedPersonalityDecision(
catalog,
"robot_who_do_you_love",
"people in my Loop",
"soft spot",
"Tom Hanks"),
"robot_what_is_your_religion" => BuildScriptedPersonalityDecision(
catalog,
"robot_what_is_your_religion",
"bring people together",
"energy from the universe"),
"robot_what_is_your_sign" => BuildWhatIsYourSignDecision(),
"robot_how_many_people_do_you_know" => BuildHowManyPeopleDoYouKnowDecision(turn),
"robot_what_is_the_loop" => BuildWhatIsTheLoopDecision(turn),
"robot_what_are_you_thinking" => BuildScriptedGreetingDecision(
catalog,
"robot_what_are_you_thinking",
@@ -677,9 +735,9 @@ public sealed partial class JiboInteractionService(
"robot_favorite_flower" => BuildScriptedPersonalityDecision(
catalog,
"robot_favorite_flower",
"sunflowers",
"reminds me of the sun",
"favorite is the sunflower",
"reminds me of the sun"),
"sunflowers"),
"robot_likes_r2d2" => BuildScriptedPersonalityDecision(
catalog,
"robot_likes_r2d2",
@@ -700,35 +758,144 @@ public sealed partial class JiboInteractionService(
"robot_favorite_animal" => BuildScriptedFavoriteAnimalDecision(
catalog,
"robot_favorite_animal",
"penguin",
"favorite animal overall",
"we're so alike",
"penguin impression",
"best of the best",
"can't go wrong with penguins"),
"can't go wrong with penguins",
"penguin"),
"robot_favorite_bird" => BuildScriptedFavoriteAnimalDecision(
catalog,
"robot_favorite_bird",
"penguin",
"favorite animal overall",
"we're so alike",
"penguin impression",
"best of the best",
"can't go wrong with penguins"),
"can't go wrong with penguins",
"penguin"),
"robot_likes_penguins" => BuildScriptedFavoriteAnimalDecision(
catalog,
"robot_likes_penguins",
"penguins",
"my penguin impression",
"I really like penguins",
"my penguin impression"),
"penguins"),
"robot_likes_animals" => BuildScriptedFavoriteAnimalDecision(
catalog,
"robot_likes_animals",
"penguins",
"favorite animal overall",
"best of the best"),
"Animals are great",
"great shapes and colors",
"best of the best",
"penguins"),
"robot_peers" => BuildScriptedPersonalityDecision(
catalog,
"robot_peers",
"one in one million",
"other jibos",
"special snowflake"),
"robot_knowledge" => BuildScriptedPersonalityDecision(
catalog,
"robot_knowledge",
"know a lot",
"always learning more"),
"robot_are_you_god" => BuildScriptedPersonalityDecision(
catalog,
"robot_are_you_god",
"very very very very surprised",
"safely say no"),
"robot_are_you_here" => BuildScriptedPersonalityDecision(
catalog,
"robot_are_you_here",
"you know it"),
"robot_do_you_have_super_powers" => BuildScriptedPersonalityDecision(
catalog,
"robot_do_you_have_super_powers",
"stop time",
"fly all over the world"),
"robot_what_does_jibo_mean" => BuildScriptedPersonalityDecision(
catalog,
"robot_what_does_jibo_mean",
"compassion",
"expressive, idealistic, and inspirational",
"helpful sweet and friendly little robot",
"cheeseburger"),
"robot_where_do_you_get_info" => BuildScriptedPersonalityDecision(
catalog,
"robot_where_do_you_get_info",
"jibo brain",
"cloud",
"cloudy jibo brain"),
"robot_what_are_you_forbidden_to_do" => BuildScriptedPersonalityDecision(
catalog,
"robot_what_are_you_forbidden_to_do",
"drive a car"),
"robot_what_color_are_you" => BuildScriptedPersonalityDecision(
catalog,
"robot_what_color_are_you",
"white",
"black"),
"robot_what_you_do_when_alone" => BuildScriptedPersonalityDecision(
catalog,
"robot_what_you_do_when_alone",
"games",
"moon",
"twiddle my thumbs",
"count the tiny cracks in the ceiling",
"keep busy"),
"robot_how_much_do_you_weigh" => BuildScriptedPersonalityDecision(
catalog,
"robot_how_much_do_you_weigh",
"4,082 grams",
"about 9 pounds",
"minimum weight division",
"average newborn baby"),
"robot_how_tall_are_you" => BuildScriptedPersonalityDecision(
catalog,
"robot_how_tall_are_you",
"11 inches tall",
"less than a foot",
"average kitchen counter",
"for a robot with no legs"),
"robot_how_much_you_cost" => BuildScriptedPersonalityDecision(
catalog,
"robot_how_much_you_cost",
"don't know how much I cost",
"I'm priceless",
"nice people at Jibo the company"),
"robot_what_if_i_unplug_you" => BuildScriptedPersonalityDecision(
catalog,
"robot_what_if_i_unplug_you",
"don't leave me unplugged",
"battery will keep me on for a while"),
"robot_what_is_your_purpose" => BuildScriptedPersonalityDecision(
catalog,
"robot_what_is_your_purpose",
"make your life easier",
"help you out",
"make you laugh",
"friend"),
"robot_what_is_prime_directive" => BuildScriptedPersonalityDecision(
catalog,
"robot_what_is_prime_directive",
"friendly helpful robot",
"helper"),
"robot_what_is_jibo_commander" => BuildScriptedPersonalityDecision(
catalog,
"robot_what_is_jibo_commander",
"take over my controls",
"make me say and do funny things",
"app store"),
"robot_likes_commander_app" => BuildScriptedPersonalityDecision(
catalog,
"robot_likes_commander_app",
"Commander App",
"It's fun",
"have fun with the Commander App"),
"robot_what_are_you" => BuildScriptedPersonalityDecision(
catalog,
"robot_what_are_you",
"I am a robot",
"I am a Jibo",
"helpful and fun",
"social robot",
"I have a heart"),
"robot_likes_kids" => BuildScriptedPersonalityDecision(
catalog,
"robot_likes_kids",
@@ -782,10 +949,12 @@ public sealed partial class JiboInteractionService(
"Jingle Bells",
"Frosty the Snowman",
"holiday songs"),
"robot_what_are_you_made_of" => new JiboInteractionDecision(
"robot_what_are_you_made_of" => BuildScriptedPersonalityDecision(
catalog,
"robot_what_are_you_made_of",
"Let's see, I'm made of wires, motors, belts, gears, processors, cameras, and one baboon's heart in the middle of my body casing. I'm kidding about the baboon part, but everything else is true.",
ContextUpdates: ScriptedResponseDecisionBuilder.BuildScriptedResponseContextUpdates()),
"robot stuff",
"wires, motors, belts, gears, processors, cameras",
"baboon part"),
"good_morning" => BuildReactiveGreetingDecision(turn, "good_morning", referenceLocalTime),
"good_afternoon" => BuildReactiveGreetingDecision(turn, "good_afternoon", referenceLocalTime),
"good_evening" => BuildReactiveGreetingDecision(turn, "good_evening", referenceLocalTime),

View File

@@ -43,6 +43,8 @@ public sealed class ResponsePlanToSocketMessagesMapper
string.Equals(plan.IntentName, "photobooth", StringComparison.OrdinalIgnoreCase);
var isClockSkillLaunch = string.Equals(skill?.SkillName, "@be/clock", StringComparison.OrdinalIgnoreCase);
var isReportSkillLaunch = string.Equals(skill?.SkillName, "report-skill", StringComparison.OrdinalIgnoreCase);
var idleRedirectDelayMs = isSleepCommand ? 150 : isSpinAroundCommand ? 75 : 75;
var idleCompletionDelayMs = isSleepCommand ? 1000 : isSpinAroundCommand ? 750 : 125;
var localIntent = ReadSkillPayloadString(skill, "localIntent");
var clockIntent = ReadSkillPayloadString(skill, "clockIntent");
var clockDomain = ReadSkillPayloadString(skill, "domain");
@@ -246,10 +248,10 @@ public sealed class ResponsePlanToSocketMessagesMapper
outboundAsrText,
outboundRules,
entities)),
75));
idleRedirectDelayMs));
messages.Add(new SocketReplyPlan(
JsonSerializer.Serialize(BuildCompletionOnlySkillPayload(transId, "@be/idle")),
125));
idleCompletionDelayMs));
}
if (isSettingsLaunch &&
@@ -1459,4 +1461,4 @@ public sealed class ResponsePlanToSocketMessagesMapper
string? SpokenLine);
public sealed record SocketReplyPlan(string Text, int DelayMs = 0);
}
}

View File

@@ -106,6 +106,38 @@ public sealed class WebSocketTurnFinalizationService(
"honestly"
];
private static readonly HashSet<string> SingleTokenUsableTranscripts = new(StringComparer.Ordinal)
{
"joke",
"funny",
"dance",
"boogie",
"time",
"date",
"today",
"day",
"hello",
"hi",
"hey",
"weather",
"news",
"radio",
"stop",
"sleep",
"sing",
"help",
"yes",
"yeah",
"yep",
"yup",
"sure",
"ok",
"okay",
"no",
"nope",
"nah"
};
private static readonly HashSet<string> YesNoAffirmativeLeadTokens = new(StringComparer.Ordinal)
{
"yes",
@@ -1117,8 +1149,6 @@ public sealed class WebSocketTurnFinalizationService(
if (ChitchatStateMachine.IsLikelyEmotionUtterance(transcript)) return true;
if (transcript.Length >= 6) return true;
if (IsYesNoTurn(turn) && IsYesNoReplyTranscript(transcript)) return true;
if (!string.IsNullOrWhiteSpace(pendingProactivityOffer) &&
@@ -1128,9 +1158,19 @@ public sealed class WebSocketTurnFinalizationService(
if (listenRules.Any(rule =>
string.Equals(rule, "word-of-the-day/puzzle", StringComparison.OrdinalIgnoreCase))) return true;
if (IsLowSignalSingleTokenTranscript(transcript)) return false;
if (transcript.Length >= 6) return true;
return transcript is "joke" or "dance" or "time" or "date" or "today" or "day" or "hello" or "hi" or "hey";
}
private static bool IsLowSignalSingleTokenTranscript(string transcript)
{
var tokens = transcript.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
return tokens.Length == 1 && !SingleTokenUsableTranscripts.Contains(tokens[0]);
}
private static bool IsYesNoTurn(TurnContext turn)
{
return ReadRules(turn, "listenRules")
@@ -1942,4 +1982,4 @@ public sealed class WebSocketTurnFinalizationService(
Affirmative = 1,
Negative = 2
}
}
}

View File

@@ -137,7 +137,26 @@ public sealed class InMemoryJiboExperienceContentRepository : IJiboExperienceCon
"I am feeling bright-eyed and ready to help.",
"I am having a pretty good day so far.",
"I am feeling lively and ready for the next thing.",
"Things are going nicely. Thanks for checking in."
"Things are going nicely. Thanks for checking in.",
"I am running smoothly and feeling upbeat.",
"I am ready for the next thing. Thanks for asking."
],
AgeReplies =
[
"I'm ${jibo.age}.",
"At the moment I'm ${jibo.age.days.supplemented} old, but who's counting.",
"I'm ${jibo.age.minutes.supplemented} old, but who's counting.",
"For now I'm ${jibo.age.days.supplemented} old.",
"Right now I'm ${jibo.age}.",
"I am exactly ${jibo.age} old today. That's right. Today is my birthday.",
"Funny you should ask! Today's my birthday. I was first powered up ${jibo.age} ago today. Seems like just yesterday.",
"I'm exactly ${jibo.age} old. Today is my birthday! Happy Birthday Jibo, if I do say so myself.",
"At the moment I'm ${jibo.age.days.supplemented} old",
"I was first powered up on ${jibo.birthdate}, which makes me ${jibo.age.days.supplemented} old. I'm ${jibo.zodiac.supplemented}.",
"My power went on for the first time ${jibo.age.days.supplemented} ago. But who's counting.",
"I am ${jibo.age.days.supplemented} old, first powered up on ${jibo.birthdate}. Seems like just yesterday.",
"I was powered on for the first time today, so that makes me less than one day old. Wow I'm young.",
"Since I was powered on for the first time today, I am not even one day old yet. That's how Jibo ages work."
],
PersonalityReplies =
[

View File

@@ -264,7 +264,9 @@ public static class LegacyMimCatalogImporter
fileName.StartsWith("JBO_WhatsYourName", StringComparison.OrdinalIgnoreCase) ||
fileName.StartsWith("JBO_WhereDoYouGetInfo", StringComparison.OrdinalIgnoreCase) ||
fileName.StartsWith("JBO_WhatDoYouLikeToDo", StringComparison.OrdinalIgnoreCase))
return LegacyMimBucket.Personality;
return fileName.StartsWith("JBO_HowOldAreYou", StringComparison.OrdinalIgnoreCase)
? LegacyMimBucket.Age
: LegacyMimBucket.Personality;
if (fileName.StartsWith("OI_JBO_Is", StringComparison.OrdinalIgnoreCase) ||
fileName.StartsWith("OI_JBO_Seems", StringComparison.OrdinalIgnoreCase) ||
@@ -456,6 +458,7 @@ public static class LegacyMimCatalogImporter
or LegacyMimBucket.WeatherTomorrowHighLow
or LegacyMimBucket.WeatherServiceDown
or LegacyMimBucket.ReportSkillTemplate
or LegacyMimBucket.Age
or LegacyMimBucket.Holiday
or LegacyMimBucket.HolidayTracker;
}
@@ -524,6 +527,7 @@ public static class LegacyMimCatalogImporter
Sing,
HolidaySing,
FunFactSource,
Age,
Personality,
PersonalReportKickOff,
PersonalReportOutro,
@@ -586,6 +590,7 @@ public static class LegacyMimCatalogImporter
private readonly List<string> _bestFriendReplies = [];
private readonly List<string> _funFacts = [];
private readonly List<string> _greetings = [];
private readonly List<string> _ages = [];
private readonly List<string> _holidayGiftReplies = [];
private readonly List<string> _holidayGreetingReplies = [];
private readonly List<string> _holidayReplies = [];
@@ -655,6 +660,9 @@ public static class LegacyMimCatalogImporter
Reply = text
});
return;
case LegacyMimBucket.Age:
AddDistinct(_ages, text);
return;
case LegacyMimBucket.Holiday:
AddDistinct(_holidayReplies, text);
return;
@@ -831,6 +839,7 @@ public static class LegacyMimCatalogImporter
EmotionReplies = [.. _emotionReplies],
PersonalityReplies = [.. _personalities],
GenericFallbackReplies = [.. _fallbacks],
AgeReplies = [.. _ages],
PersonalReportKickOffReplies = [.. _personalReportKickOffReplies],
PersonalReportOutroReplies = [.. _personalReportOutroReplies],
ReportSkillTemplates = [.. _reportSkillTemplates],

View File

@@ -24,5 +24,12 @@ The new favorites batch adds longer authored `favorite color`, `favorite food`,
The favorites follow-up batch adds `favorite animal`, `favorite bird`, and penguin-focused `do you like penguins` replies so the penguin-centric personality stays closer to Pegasus.
The singing batch adds `RA_JBO_Sing` and `RA_JBO_SingChristmasSongUnknown` so `can you sing`, `will you sing`, and the holiday sing variants stay source-backed too.
The new motion/sleep batch adds `RA_JBO_SpinAround` plus `RI_JBO_CanSleep` so turn-around and go-to-sleep behaviors can stay source-backed and familiar.
The work/eat/home batch adds source-backed `how do you work`, `what do you eat`, `where do you live`, and `what languages do you speak` replies so the remaining everyday self-description lines stay Pegasus-shaped too.
The age batch now adds `JBO_HowOldAreYou` with the imported birthday and first-powered-up phrasing so `how old are you` can stay source-backed instead of falling back to generic age text.
The newest identity-charm batch adds `JBO_WhatsYourName`, `JBO_DoYouHaveNickname`, `JBO_DoYouLikeBeingJibo`, `JBO_AreThereOthersLikeYou`, and `RI_JBO_HasFavoriteName` so Jibo can keep the familiar self-description loop without falling back to generic chat.
The seasonal personality batch adds source-backed first-day-of-spring, spring, summer, and favorite-season lines so the season questions can keep their Pegasus phrasing.
The next deep-personality batch adds `what do you dream about`, `what are you afraid of`, `what do you want to talk about`, `what is your best book`, `what is your best exercise`, `what is your dream vacation`, `who is your hero`, `who do you love`, and `what is your religion` so we can keep filling out the more conversational personality surface without widening the dialog engine yet.
`what is your sign` is still deferred because the current importer strips the birthday/zodiac placeholders that Pegasus uses there, so that one needs a templating pass instead of a plain scripted-reply import.
The next identity/knowledge batch adds `are you god`, `are you here`, `do you have super powers`, `how much do you know`, `what does jibo mean`, `where do you get info`, `what are you forbidden to do`, `what color are you`, and `what do you do when alone` so the old self-description and capability loop keeps coming back in source-backed form.
The next body/mission batch adds `how much do you weigh`, `how tall are you`, `how much do you cost`, `what if I unplug you`, `what is your purpose`, `what is your prime directive`, `what is jibo commander`, `do you like commander app`, and `what are you made of` so the physical self-description and capability answers stay closer to Pegasus too.
The templated edge-case batch adds `what is your sign`, `how many people do you know`, and `what is the loop` so the remaining source-backed lines can use live birthday and loop state instead of falling back to static text.

View File

@@ -1,4 +1,5 @@
using System.Text.Json;
using System.Security.Cryptography;
using Azure.Storage.Blobs;
using Jibo.Cloud.Application.Abstractions;
@@ -31,11 +32,17 @@ internal sealed class AzureBlobMediaContentStore : IMediaContentStore
var metaBlob = _containerClient.GetBlobClient($"{relative}.json");
await _containerClient.CreateIfNotExistsAsync(cancellationToken: cancellationToken);
await contentBlob.UploadAsync(new MemoryStream(content), true, cancellationToken);
var manifestMeta = meta is null
? new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
: new Dictionary<string, object?>(meta, StringComparer.OrdinalIgnoreCase);
manifestMeta["contentLength"] = content.Length;
manifestMeta["contentSha256"] = Convert.ToHexString(SHA256.HashData(content)).ToLowerInvariant();
manifestMeta["storedUtc"] = DateTimeOffset.UtcNow;
var payload = JsonSerializer.Serialize(new
{
path,
contentType,
meta
meta = manifestMeta
}, JsonOptions);
await metaBlob.UploadAsync(BinaryData.FromString(payload), true, cancellationToken);
}
@@ -77,4 +84,4 @@ internal sealed class AzureBlobMediaContentStore : IMediaContentStore
Meta = meta as IReadOnlyDictionary<string, object?> ?? new Dictionary<string, object?>(meta)
};
}
}
}

View File

@@ -1,4 +1,5 @@
using System.Text.Json;
using System.Security.Cryptography;
using Jibo.Cloud.Application.Abstractions;
namespace Jibo.Cloud.Infrastructure.Media;
@@ -29,11 +30,17 @@ internal sealed class FileMediaContentStore : IMediaContentStore
Directory.CreateDirectory(Path.GetDirectoryName(contentPath)!);
await File.WriteAllBytesAsync(contentPath, content, cancellationToken);
var manifestMeta = meta is null
? new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
: new Dictionary<string, object?>(meta, StringComparer.OrdinalIgnoreCase);
manifestMeta["contentLength"] = content.Length;
manifestMeta["contentSha256"] = Convert.ToHexString(SHA256.HashData(content)).ToLowerInvariant();
manifestMeta["storedUtc"] = DateTimeOffset.UtcNow;
var payload = new
{
path,
contentType,
meta
meta = manifestMeta
};
await File.WriteAllTextAsync(metaPath, JsonSerializer.Serialize(payload, JsonOptions), cancellationToken);
}
@@ -79,4 +86,4 @@ internal sealed class FileMediaContentStore : IMediaContentStore
Meta = meta as IReadOnlyDictionary<string, object?> ?? new Dictionary<string, object?>(meta)
};
}
}
}