Extract memory and personality decision builders

This commit is contained in:
Jacob Dubin
2026-05-21 09:40:21 -05:00
parent e5e8e72dbf
commit 5fa13a65a2
3 changed files with 676 additions and 656 deletions

View File

@@ -0,0 +1,259 @@
using System.Globalization;
using System.Text.RegularExpressions;
using Jibo.Cloud.Application.Abstractions;
using Jibo.Cloud.Domain.Models;
using Jibo.Runtime.Abstractions;
namespace Jibo.Cloud.Application.Services;
public sealed partial class JiboInteractionService
{
private JiboInteractionDecision BuildRememberNameDecision(TurnContext turn, string transcript)
{
var name = TryExtractNameFact(transcript);
if (string.IsNullOrWhiteSpace(name))
return new JiboInteractionDecision(
"memory_set_name",
"I can remember it if you say, my name is Alex.");
personalMemoryStore.SetName(ResolveTenantScope(turn), name);
return new JiboInteractionDecision(
"memory_set_name",
$"Nice to meet you, {name}. I will remember your name.");
}
private JiboInteractionDecision BuildRecallNameDecision(TurnContext turn, GreetingPresenceProfile? presence = null)
{
var personScope = ResolveTenantScope(turn, presence?.PrimaryPersonId);
var name = personalMemoryStore.GetName(personScope);
if (string.IsNullOrWhiteSpace(name)) name = personalMemoryStore.GetName(ResolveTenantScope(turn));
name = ToDisplayName(name ?? string.Empty);
return string.IsNullOrWhiteSpace(name)
? new JiboInteractionDecision(
"memory_get_name",
"I do not know your name yet. You can say, my name is Alex.")
: new JiboInteractionDecision(
"memory_get_name",
presence is not null && !string.IsNullOrWhiteSpace(presence.PrimaryPersonId)
? $"I think you are {name}."
: $"You told me your name is {name}.");
}
private JiboInteractionDecision BuildRememberBirthdayDecision(TurnContext turn, string transcript)
{
var birthday = TryExtractBirthdayFact(transcript);
if (string.IsNullOrWhiteSpace(birthday))
return new JiboInteractionDecision(
"memory_set_birthday",
"I can remember it if you say, my birthday is March 14.");
var tenantScope = ResolveTenantScope(turn);
personalMemoryStore.SetBirthday(tenantScope, birthday);
var birthdayDate = TryParseBirthdayDate(birthday);
if (birthdayDate is not null)
{
var birthdayLabel = ResolvePreferredBirthdayLabel(turn);
cloudStateStore?.UpsertHoliday(new HolidayRecord
{
EventId = $"birthday-{tenantScope.LoopId}-{tenantScope.PersonId ?? "loop"}",
Name = string.IsNullOrWhiteSpace(birthdayLabel) ? "Birthday" : $"{birthdayLabel}'s Birthday",
Category = "birthday",
Subcategory = "personal",
LoopId = tenantScope.LoopId,
MemberId = tenantScope.PersonId,
IsEnabled = true,
Date = birthdayDate.Value,
Source = "birthday",
CountryCode = "US"
});
}
return new JiboInteractionDecision(
"memory_set_birthday",
$"Got it. I will remember your birthday is {birthday}.");
}
private JiboInteractionDecision BuildRecallBirthdayDecision(TurnContext turn)
{
var birthday = personalMemoryStore.GetBirthday(ResolveTenantScope(turn));
return string.IsNullOrWhiteSpace(birthday)
? new JiboInteractionDecision(
"memory_get_birthday",
"I do not know your birthday yet. You can say, my birthday is March 14.")
: new JiboInteractionDecision(
"memory_get_birthday",
$"You told me your birthday is {birthday}.");
}
private static DateOnly? TryParseBirthdayDate(string birthdayText)
{
if (string.IsNullOrWhiteSpace(birthdayText)) return null;
var normalized = birthdayText.Trim().ToLowerInvariant();
var match = Regex.Match(
normalized,
@"\b(?<month>january|february|march|april|may|june|july|august|september|october|november|december)\s+(?<day>\d{1,2})(?:st|nd|rd|th)?\b",
RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
if (!match.Success) return null;
var month = match.Groups["month"].Value.ToLowerInvariant() switch
{
"january" => 1,
"february" => 2,
"march" => 3,
"april" => 4,
"may" => 5,
"june" => 6,
"july" => 7,
"august" => 8,
"september" => 9,
"october" => 10,
"november" => 11,
"december" => 12,
_ => 0
};
if (month == 0) return null;
if (!int.TryParse(match.Groups["day"].Value, out var day) || day is < 1 or > 31) return null;
var today = DateOnly.FromDateTime(DateTime.UtcNow);
var year = today.Year;
if (day > DateTime.DaysInMonth(year, month)) return null;
DateOnly birthday;
try
{
birthday = new DateOnly(year, month, day);
}
catch
{
return null;
}
if (birthday < today) birthday = birthday.AddYears(1);
return birthday;
}
private static string? ResolvePreferredBirthdayLabel(TurnContext turn)
{
var context = ResolveGreetingPresenceProfile(turn);
return !string.IsNullOrWhiteSpace(context.PrimaryPersonId) &&
context.LoopUserFirstNames.TryGetValue(context.PrimaryPersonId, out var firstName) &&
!string.IsNullOrWhiteSpace(firstName)
? ToDisplayName(firstName)
: null;
}
private JiboInteractionDecision BuildRememberImportantDateDecision(TurnContext turn, string transcript)
{
var importantDate = TryExtractImportantDateSet(transcript);
if (importantDate is null)
return new JiboInteractionDecision(
"memory_set_important_date",
"I can remember it if you say, our anniversary is June 10.");
personalMemoryStore.SetImportantDate(ResolveTenantScope(turn), importantDate.Value.Label,
importantDate.Value.Value);
return new JiboInteractionDecision(
"memory_set_important_date",
$"Got it. I will remember your {importantDate.Value.Label} is {importantDate.Value.Value}.");
}
private JiboInteractionDecision BuildRecallImportantDateDecision(TurnContext turn, string transcript)
{
var label = TryExtractImportantDateLookupLabel(transcript);
if (string.IsNullOrWhiteSpace(label))
return new JiboInteractionDecision(
"memory_get_important_date",
"Ask me like this: when is our anniversary?");
var storedDate = personalMemoryStore.GetImportantDate(ResolveTenantScope(turn), label);
return string.IsNullOrWhiteSpace(storedDate)
? new JiboInteractionDecision(
"memory_get_important_date",
$"I do not know your {label} yet.")
: new JiboInteractionDecision(
"memory_get_important_date",
$"You told me your {label} is {storedDate}.");
}
private JiboInteractionDecision BuildRememberPreferenceDecision(TurnContext turn, string transcript)
{
var preference = TryExtractPreferenceSet(transcript);
if (preference is null)
return new JiboInteractionDecision(
"memory_set_preference",
"I can remember it if you say, my favorite music is jazz.");
personalMemoryStore.SetPreference(ResolveTenantScope(turn), preference.Value.Category, preference.Value.Value);
return new JiboInteractionDecision(
"memory_set_preference",
$"Got it. I will remember your favorite {preference.Value.Category} is {preference.Value.Value}.");
}
private JiboInteractionDecision BuildRecallPreferenceDecision(TurnContext turn, string transcript)
{
var category = TryExtractPreferenceLookupCategory(transcript);
if (string.IsNullOrWhiteSpace(category))
return new JiboInteractionDecision(
"memory_get_preference",
"Ask me like this: what is my favorite music?");
var preference = personalMemoryStore.GetPreference(ResolveTenantScope(turn), category);
return string.IsNullOrWhiteSpace(preference)
? new JiboInteractionDecision(
"memory_get_preference",
$"I do not know your favorite {category} yet.")
: new JiboInteractionDecision(
"memory_get_preference",
$"You told me your favorite {category} is {preference}.");
}
private JiboInteractionDecision BuildRememberAffinityDecision(TurnContext turn, string transcript)
{
var affinitySet = TryExtractAffinitySet(transcript);
if (affinitySet is null)
return new JiboInteractionDecision(
"memory_set_affinity",
"I can remember it if you say, I like pizza or I dislike mushrooms.");
personalMemoryStore.SetAffinity(ResolveTenantScope(turn), affinitySet.Value.Item, affinitySet.Value.Affinity);
return new JiboInteractionDecision(
"memory_set_affinity",
$"Got it. I will remember you {DescribeAffinityAsVerb(affinitySet.Value.Affinity)} {affinitySet.Value.Item}.");
}
private JiboInteractionDecision BuildRecallAffinityDecision(TurnContext turn, string transcript)
{
var lookup = TryExtractAffinityLookup(transcript);
if (lookup is null)
return new JiboInteractionDecision(
"memory_get_affinity",
"Ask me like this: do I like pizza?");
var affinity = personalMemoryStore.GetAffinity(ResolveTenantScope(turn), lookup.Value.Item);
if (affinity is null)
return new JiboInteractionDecision(
"memory_get_affinity",
$"I do not remember how you feel about {lookup.Value.Item} yet.");
if (lookup.Value.ExpectedAffinity is null)
return new JiboInteractionDecision(
"memory_get_affinity",
$"You told me you {DescribeAffinityAsVerb(affinity.Value)} {lookup.Value.Item}.");
var matches = lookup.Value.ExpectedAffinity == PersonalAffinity.Dislike
? affinity == PersonalAffinity.Dislike
: affinity is PersonalAffinity.Like or PersonalAffinity.Love;
return matches
? new JiboInteractionDecision(
"memory_get_affinity",
$"Yes. You told me you {DescribeAffinityAsVerb(affinity.Value)} {lookup.Value.Item}.")
: new JiboInteractionDecision(
"memory_get_affinity",
$"Not exactly. You told me you {DescribeAffinityAsVerb(affinity.Value)} {lookup.Value.Item}.");
}
}

View File

@@ -0,0 +1,417 @@
using System.Collections.Generic;
using System.Globalization;
using Jibo.Cloud.Application.Abstractions;
using Jibo.Cloud.Domain.Models;
using Jibo.Runtime.Abstractions;
namespace Jibo.Cloud.Application.Services;
public sealed partial class JiboInteractionService
{
private static JiboInteractionDecision BuildRobotAgeDecision(DateTimeOffset? referenceLocalTime)
{
var referenceDate = DateOnly.FromDateTime((referenceLocalTime ?? DateTimeOffset.UtcNow).Date);
var ageDescription = DescribePersonaAge(referenceDate, OpenJiboCloudBuildInfo.PersonaBirthday);
return new JiboInteractionDecision(
"robot_age",
$"I count {OpenJiboCloudBuildInfo.PersonaBirthdayWords} as my birthday, so I am {ageDescription}.");
}
private static JiboInteractionDecision BuildRobotBirthdayDecision()
{
return new JiboInteractionDecision(
"robot_birthday",
$"My birthday is {OpenJiboCloudBuildInfo.PersonaBirthdayWords}.");
}
private static JiboInteractionDecision BuildTriggerIgnoredDecision()
{
return new JiboInteractionDecision(
"trigger_ignored",
string.Empty,
"chitchat-skill",
new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
["skillId"] = "chitchat-skill",
["cloudResponseMode"] = "completion_only"
});
}
private JiboInteractionDecision BuildReactiveGreetingDecision(
TurnContext turn,
string greetingIntent,
DateTimeOffset? referenceLocalTime)
{
var presence = ResolveGreetingPresenceProfile(turn);
var displayName = ResolvePreferredGreetingName(turn, presence);
var replyText = BuildReactiveGreetingReply(greetingIntent, displayName, referenceLocalTime);
return new JiboInteractionDecision(
greetingIntent,
replyText,
ContextUpdates: BuildGreetingContextUpdates("ReactiveGreeting", presence.PrimaryPersonId, false));
}
private JiboInteractionDecision BuildProactiveGreetingDecision(
TurnContext turn,
GreetingPresenceProfile presence,
DateTimeOffset? referenceLocalTime)
{
var displayName = ResolvePreferredGreetingName(turn, presence);
var greetingPrefix = ResolveTimeOfDayGreetingPrefix(referenceLocalTime);
var replyText = string.IsNullOrWhiteSpace(displayName)
? $"{greetingPrefix}. I am glad to see you."
: $"{greetingPrefix}, {displayName}. Welcome back.";
return new JiboInteractionDecision(
"proactive_greeting",
replyText,
ContextUpdates: BuildGreetingContextUpdates("ProactiveGreeting", presence.PrimaryPersonId, true));
}
private static string BuildReactiveGreetingReply(
string greetingIntent,
string? displayName,
DateTimeOffset? referenceLocalTime)
{
var namePrefix = string.IsNullOrWhiteSpace(displayName)
? string.Empty
: $", {displayName}";
return greetingIntent switch
{
"good_morning" => $"Good morning{namePrefix}. It is great to see you.",
"good_afternoon" => $"Good afternoon{namePrefix}. I am glad you are here.",
"good_evening" => $"Good evening{namePrefix}. It is nice to have you back.",
"good_night" => $"Good night{namePrefix}. Sleep well.",
"welcome_back" => string.IsNullOrWhiteSpace(displayName)
? $"Welcome back. {ResolveTimeOfDayGreetingPrefix(referenceLocalTime)}."
: $"Welcome back, {displayName}. {ResolveTimeOfDayGreetingPrefix(referenceLocalTime)}.",
_ => $"Hello{namePrefix}. It is nice to see you."
};
}
private string? ResolvePreferredGreetingName(TurnContext turn, GreetingPresenceProfile presence)
{
var rememberedName = personalMemoryStore.GetName(ResolveTenantScope(turn, presence.PrimaryPersonId));
if (!string.IsNullOrWhiteSpace(rememberedName)) return ToDisplayName(rememberedName);
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) &&
!string.IsNullOrWhiteSpace(firstName))
return ToDisplayName(firstName);
return null;
}
private static string ToDisplayName(string value)
{
var trimmed = value.Trim();
return string.IsNullOrWhiteSpace(trimmed)
? string.Empty
: CultureInfo.InvariantCulture.TextInfo.ToTitleCase(trimmed);
}
private static bool ShouldHandleProactiveGreetingTrigger(
TurnContext turn,
string? triggerSource,
GreetingPresenceProfile presence)
{
if (string.Equals(triggerSource, "SURPRISE", StringComparison.OrdinalIgnoreCase)) return false;
if (!presence.HasKnownIdentity) return false;
var lastGreetingUtc = ReadTimestampAttribute(turn, LastProactiveGreetingUtcMetadataKey);
return !lastGreetingUtc.HasValue || DateTimeOffset.UtcNow - lastGreetingUtc.Value >= ProactiveGreetingCooldown;
}
private static DateTimeOffset? ReadTimestampAttribute(TurnContext turn, string key)
{
if (!turn.Attributes.TryGetValue(key, out var value) || value is null) return null;
return DateTimeOffset.TryParse(
value.ToString(),
CultureInfo.InvariantCulture,
DateTimeStyles.RoundtripKind,
out var parsed)
? parsed
: null;
}
private static IDictionary<string, object?> BuildGreetingContextUpdates(string route, string? speakerId,
bool proactive)
{
var updates = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
[ChitchatStateMachine.StateMetadataKey] = "complete",
[ChitchatStateMachine.RouteMetadataKey] = "ScriptedResponse",
[ChitchatStateMachine.EmotionMetadataKey] = string.Empty,
[GreetingRouteMetadataKey] = route,
[GreetingSpeakerMetadataKey] = speakerId ?? string.Empty,
[proactive ? LastProactiveGreetingUtcMetadataKey : LastReactiveGreetingUtcMetadataKey] = DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture)
};
return updates;
}
private static string ResolveTimeOfDayGreetingPrefix(DateTimeOffset? referenceLocalTime)
{
var hour = (referenceLocalTime ?? DateTimeOffset.UtcNow).Hour;
return hour switch
{
>= 5 and < 12 => "Good morning",
>= 12 and < 17 => "Good afternoon",
_ => "Good evening"
};
}
private JiboInteractionDecision BuildPizzaDecision()
{
return BuildPizzaAnimationDecision("pizza", "One pizza, coming right up.");
}
private JiboInteractionDecision BuildPizzaAnimationDecision(string intentName, string replyText)
{
var prompt = randomizer.Choose(PizzaMimPrompts);
return new JiboInteractionDecision(
intentName,
replyText,
"chitchat-skill",
new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
["esml"] = prompt.Esml,
["mim_id"] = "RA_JBO_MakePizza",
["mim_type"] = "announcement",
["prompt_id"] = prompt.PromptId,
["prompt_sub_category"] = "AN"
});
}
private JiboInteractionDecision BuildProactivePizzaDayDecision(DateTimeOffset? referenceLocalTime)
{
var referenceDate = (referenceLocalTime ?? DateTimeOffset.UtcNow).Date;
return BuildPizzaAnimationDecision(
"proactive_pizza_day",
$"Happy National Pizza Day for {referenceDate.ToString("MMMM d", CultureInfo.InvariantCulture)}. One pizza, coming right up.");
}
private JiboInteractionDecision BuildProactivePizzaPreferenceDecision()
{
return BuildPizzaAnimationDecision(
"proactive_pizza_preference",
"You mentioned pizza is a favorite, so I thought we should make one.");
}
private static JiboInteractionDecision BuildProactivePizzaFactOfferDecision()
{
var listenContexts = new[] { "shared/yes_no" };
return new JiboInteractionDecision(
"proactive_offer_pizza_fact",
"Do you want to hear a fun pizza fact?",
"chitchat-skill",
new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
["mim_id"] = "runtime-chat",
["mim_type"] = "question",
["prompt_id"] = "RUNTIME_PROMPT",
["prompt_sub_category"] = "Q",
["listen_contexts"] = listenContexts
});
}
private static JiboInteractionDecision BuildProactivePizzaFactDecision()
{
return new JiboInteractionDecision(
"proactive_pizza_fact",
"Americans consume about 100 acres of pizza every day, roughly 350 slices per second. That's a lot of pizza.");
}
private JiboInteractionDecision BuildProactiveFunFactDecision(JiboExperienceCatalog catalog)
{
var categories = new List<ProactiveFactCategory>();
AddProactiveFactCategory(categories, "fun_fact", catalog.FunFacts);
AddProactiveFactCategory(categories, "robot_fact", catalog.RobotFacts);
AddProactiveFactCategory(categories, "human_fact", catalog.HumanFacts);
if (categories.Count == 0)
return new JiboInteractionDecision("proactive_fun_fact", randomizer.Choose(catalog.SurpriseReplies));
var selectedCategory = randomizer.Choose(categories);
var fact = randomizer.Choose(selectedCategory.Replies);
return new JiboInteractionDecision(
"proactive_fun_fact",
fact,
"chitchat-skill",
new Dictionary<string, object?>
{
["mim_id"] = "runtime-fun-fact",
["mim_type"] = "announcement",
["prompt_id"] = "RUNTIME_FUN_FACT",
["replyType"] = "fun_fact",
["factCategory"] = selectedCategory.CategoryName
});
}
private static void AddProactiveFactCategory(
ICollection<ProactiveFactCategory> categories,
string categoryName,
IReadOnlyList<string> replies)
{
if (replies.Count == 0) return;
categories.Add(new ProactiveFactCategory(categoryName, replies));
}
private JiboInteractionDecision BuildProactiveJokeDecision(JiboExperienceCatalog catalog)
{
return new JiboInteractionDecision(
"proactive_joke",
randomizer.Choose(catalog.Jokes),
"@be/joke",
new Dictionary<string, object?>
{
["replyType"] = "joke"
});
}
private static JiboInteractionDecision BuildProactiveOfferDeclinedDecision()
{
return new JiboInteractionDecision(
"proactive_offer_declined",
"No problem. We can save the pizza fact for another time.");
}
private string BuildGenericReply(JiboExperienceCatalog catalog, string transcript, string lowered)
{
if (string.IsNullOrWhiteSpace(transcript)) return "I am listening.";
if (lowered.Contains("good morning", StringComparison.Ordinal))
return "Good morning! It is nice to hear your voice.";
if (lowered.Contains("good afternoon", StringComparison.Ordinal))
return "Good afternoon. I am happy to be here.";
return lowered.Contains("good night", StringComparison.Ordinal)
? "Good night. Sleep tight."
: randomizer.Choose(catalog.GenericFallbackReplies)
.Replace("{transcript}", transcript, StringComparison.Ordinal);
}
private JiboInteractionDecision BuildScriptedPersonalityDecision(
JiboExperienceCatalog catalog,
string intentName,
params string[] preferredSnippets)
{
return ScriptedResponseDecisionBuilder.BuildScriptedPersonalityDecision(
catalog,
randomizer,
intentName,
preferredSnippets);
}
private JiboInteractionDecision BuildScriptedFavoriteAnimalDecision(
JiboExperienceCatalog catalog,
string intentName,
params string[] preferredSnippets)
{
return ScriptedResponseDecisionBuilder.BuildScriptedFavoriteAnimalDecision(
catalog,
randomizer,
intentName,
preferredSnippets);
}
private JiboInteractionDecision BuildScriptedGreetingDecision(
JiboExperienceCatalog catalog,
string intentName,
params string[] preferredSnippets)
{
return ScriptedResponseDecisionBuilder.BuildScriptedGreetingDecision(
catalog,
randomizer,
intentName,
preferredSnippets);
}
private JiboInteractionDecision BuildScriptedHolidayDecision(
IReadOnlyList<string> replies,
string intentName,
params string[] preferredSnippets)
{
return ScriptedResponseDecisionBuilder.BuildScriptedHolidayDecision(
replies,
randomizer,
intentName,
preferredSnippets);
}
private JiboInteractionDecision BuildScriptedHolidayTrackerDecision(
JiboExperienceCatalog catalog,
string intentName,
params string[] preferredSnippets)
{
return ScriptedResponseDecisionBuilder.BuildScriptedHolidayTrackerDecision(
catalog,
randomizer,
intentName,
preferredSnippets);
}
private JiboInteractionDecision BuildScriptedHolidayGreetingDecision(
JiboExperienceCatalog catalog,
string intentName,
params string[] preferredSnippets)
{
return ScriptedResponseDecisionBuilder.BuildScriptedHolidayGreetingDecision(
catalog,
randomizer,
intentName,
preferredSnippets);
}
private JiboInteractionDecision BuildScriptedHolidayTemplateDecision(
TurnContext turn,
GreetingPresenceProfile presence,
JiboExperienceCatalog catalog,
string intentName,
params string[] preferredSnippets)
{
var selected = ScriptedResponseDecisionBuilder.SelectLegacyReply(
catalog.HolidayReplies,
randomizer,
preferredSnippets);
return new JiboInteractionDecision(
intentName,
RenderHolidayTemplate(selected, turn, presence),
ContextUpdates: ScriptedResponseDecisionBuilder.BuildScriptedResponseContextUpdates());
}
private string SelectLegacyPersonalityReply(JiboExperienceCatalog catalog, params string[] preferredSnippets)
{
return ScriptedResponseDecisionBuilder.SelectLegacyPersonalityReply(catalog, randomizer, preferredSnippets);
}
private string SelectLegacyGreetingReply(JiboExperienceCatalog catalog, params string[] preferredSnippets)
{
return ScriptedResponseDecisionBuilder.SelectLegacyGreetingReply(catalog, randomizer, preferredSnippets);
}
private string SelectLegacyReply(IReadOnlyList<string> replies, params string[] preferredSnippets)
{
return ScriptedResponseDecisionBuilder.SelectLegacyReply(replies, randomizer, preferredSnippets);
}
private string RenderHolidayTemplate(string template, TurnContext turn, GreetingPresenceProfile presence)
{
var ownerName = ResolvePreferredGreetingName(turn, presence);
var speakerName = !string.IsNullOrWhiteSpace(ownerName) ? ownerName : "you";
return template
.Replace("${speaker}'s", $"{speakerName}'s", StringComparison.OrdinalIgnoreCase)
.Replace("${speaker}", speakerName, StringComparison.OrdinalIgnoreCase)
.Replace("${loop.owner}", string.IsNullOrWhiteSpace(ownerName) ? string.Empty : ownerName,
StringComparison.OrdinalIgnoreCase)
.Replace(" ", " ", StringComparison.Ordinal)
.Trim();
}
}

View File

@@ -803,542 +803,6 @@ public sealed partial class JiboInteractionService(
SkillPayload: new Dictionary<string, object?> { ["esml"] = OpenJiboCloudBuildInfo.EsmlVersion });
}
private static JiboInteractionDecision BuildRobotAgeDecision(DateTimeOffset? referenceLocalTime)
{
var referenceDate = DateOnly.FromDateTime((referenceLocalTime ?? DateTimeOffset.UtcNow).Date);
var ageDescription = DescribePersonaAge(referenceDate, OpenJiboCloudBuildInfo.PersonaBirthday);
return new JiboInteractionDecision(
"robot_age",
$"I count {OpenJiboCloudBuildInfo.PersonaBirthdayWords} as my birthday, so I am {ageDescription}.");
}
private static JiboInteractionDecision BuildRobotBirthdayDecision()
{
return new JiboInteractionDecision(
"robot_birthday",
$"My birthday is {OpenJiboCloudBuildInfo.PersonaBirthdayWords}.");
}
private static JiboInteractionDecision BuildTriggerIgnoredDecision()
{
return new JiboInteractionDecision(
"trigger_ignored",
string.Empty,
"chitchat-skill",
new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
["skillId"] = "chitchat-skill",
["cloudResponseMode"] = "completion_only"
});
}
private JiboInteractionDecision BuildReactiveGreetingDecision(
TurnContext turn,
string greetingIntent,
DateTimeOffset? referenceLocalTime)
{
var presence = ResolveGreetingPresenceProfile(turn);
var displayName = ResolvePreferredGreetingName(turn, presence);
var replyText = BuildReactiveGreetingReply(greetingIntent, displayName, referenceLocalTime);
return new JiboInteractionDecision(
greetingIntent,
replyText,
ContextUpdates: BuildGreetingContextUpdates("ReactiveGreeting", presence.PrimaryPersonId, false));
}
private JiboInteractionDecision BuildProactiveGreetingDecision(
TurnContext turn,
GreetingPresenceProfile presence,
DateTimeOffset? referenceLocalTime)
{
var displayName = ResolvePreferredGreetingName(turn, presence);
var greetingPrefix = ResolveTimeOfDayGreetingPrefix(referenceLocalTime);
var replyText = string.IsNullOrWhiteSpace(displayName)
? $"{greetingPrefix}. I am glad to see you."
: $"{greetingPrefix}, {displayName}. Welcome back.";
return new JiboInteractionDecision(
"proactive_greeting",
replyText,
ContextUpdates: BuildGreetingContextUpdates("ProactiveGreeting", presence.PrimaryPersonId, true));
}
private static string BuildReactiveGreetingReply(
string greetingIntent,
string? displayName,
DateTimeOffset? referenceLocalTime)
{
var namePrefix = string.IsNullOrWhiteSpace(displayName)
? string.Empty
: $", {displayName}";
return greetingIntent switch
{
"good_morning" => $"Good morning{namePrefix}. It is great to see you.",
"good_afternoon" => $"Good afternoon{namePrefix}. I am glad you are here.",
"good_evening" => $"Good evening{namePrefix}. It is nice to have you back.",
"good_night" => $"Good night{namePrefix}. Sleep well.",
"welcome_back" => string.IsNullOrWhiteSpace(displayName)
? $"Welcome back. {ResolveTimeOfDayGreetingPrefix(referenceLocalTime)}."
: $"Welcome back, {displayName}. {ResolveTimeOfDayGreetingPrefix(referenceLocalTime)}.",
_ => $"Hello{namePrefix}. It is nice to see you."
};
}
private string? ResolvePreferredGreetingName(TurnContext turn, GreetingPresenceProfile presence)
{
var rememberedName = personalMemoryStore.GetName(ResolveTenantScope(turn, presence.PrimaryPersonId));
if (!string.IsNullOrWhiteSpace(rememberedName)) return ToDisplayName(rememberedName);
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) &&
!string.IsNullOrWhiteSpace(firstName))
return ToDisplayName(firstName);
return null;
}
private static string ToDisplayName(string value)
{
var trimmed = value.Trim();
return string.IsNullOrWhiteSpace(trimmed)
? string.Empty
: CultureInfo.InvariantCulture.TextInfo.ToTitleCase(trimmed);
}
private static bool ShouldHandleProactiveGreetingTrigger(
TurnContext turn,
string? triggerSource,
GreetingPresenceProfile presence)
{
if (string.Equals(triggerSource, "SURPRISE", StringComparison.OrdinalIgnoreCase)) return false;
if (!presence.HasKnownIdentity) return false;
var lastGreetingUtc = ReadTimestampAttribute(turn, LastProactiveGreetingUtcMetadataKey);
return !lastGreetingUtc.HasValue || DateTimeOffset.UtcNow - lastGreetingUtc.Value >= ProactiveGreetingCooldown;
}
private static DateTimeOffset? ReadTimestampAttribute(TurnContext turn, string key)
{
if (!turn.Attributes.TryGetValue(key, out var value) || value is null) return null;
return DateTimeOffset.TryParse(
value.ToString(),
CultureInfo.InvariantCulture,
DateTimeStyles.RoundtripKind,
out var parsed)
? parsed
: null;
}
private static IDictionary<string, object?> BuildGreetingContextUpdates(string route, string? speakerId,
bool proactive)
{
var updates = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
[ChitchatStateMachine.StateMetadataKey] = "complete",
[ChitchatStateMachine.RouteMetadataKey] = "ScriptedResponse",
[ChitchatStateMachine.EmotionMetadataKey] = string.Empty,
[GreetingRouteMetadataKey] = route,
[GreetingSpeakerMetadataKey] = speakerId ?? string.Empty,
[proactive ? LastProactiveGreetingUtcMetadataKey : LastReactiveGreetingUtcMetadataKey] = DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture)
};
return updates;
}
private static string ResolveTimeOfDayGreetingPrefix(DateTimeOffset? referenceLocalTime)
{
var hour = (referenceLocalTime ?? DateTimeOffset.UtcNow).Hour;
return hour switch
{
>= 5 and < 12 => "Good morning",
>= 12 and < 17 => "Good afternoon",
_ => "Good evening"
};
}
private JiboInteractionDecision BuildRememberNameDecision(TurnContext turn, string transcript)
{
var name = TryExtractNameFact(transcript);
if (string.IsNullOrWhiteSpace(name))
return new JiboInteractionDecision(
"memory_set_name",
"I can remember it if you say, my name is Alex.");
personalMemoryStore.SetName(ResolveTenantScope(turn), name);
return new JiboInteractionDecision(
"memory_set_name",
$"Nice to meet you, {name}. I will remember your name.");
}
private JiboInteractionDecision BuildRecallNameDecision(TurnContext turn, GreetingPresenceProfile? presence = null)
{
var personScope = ResolveTenantScope(turn, presence?.PrimaryPersonId);
var name = personalMemoryStore.GetName(personScope);
if (string.IsNullOrWhiteSpace(name)) name = personalMemoryStore.GetName(ResolveTenantScope(turn));
name = ToDisplayName(name ?? string.Empty);
return string.IsNullOrWhiteSpace(name)
? new JiboInteractionDecision(
"memory_get_name",
"I do not know your name yet. You can say, my name is Alex.")
: new JiboInteractionDecision(
"memory_get_name",
presence is not null && !string.IsNullOrWhiteSpace(presence.PrimaryPersonId)
? $"I think you are {name}."
: $"You told me your name is {name}.");
}
private JiboInteractionDecision BuildRememberBirthdayDecision(TurnContext turn, string transcript)
{
var birthday = TryExtractBirthdayFact(transcript);
if (string.IsNullOrWhiteSpace(birthday))
return new JiboInteractionDecision(
"memory_set_birthday",
"I can remember it if you say, my birthday is March 14.");
var tenantScope = ResolveTenantScope(turn);
personalMemoryStore.SetBirthday(tenantScope, birthday);
var birthdayDate = TryParseBirthdayDate(birthday);
if (birthdayDate is not null)
{
var birthdayLabel = ResolvePreferredBirthdayLabel(turn);
cloudStateStore?.UpsertHoliday(new HolidayRecord
{
EventId = $"birthday-{tenantScope.LoopId}-{tenantScope.PersonId ?? "loop"}",
Name = string.IsNullOrWhiteSpace(birthdayLabel) ? "Birthday" : $"{birthdayLabel}'s Birthday",
Category = "birthday",
Subcategory = "personal",
LoopId = tenantScope.LoopId,
MemberId = tenantScope.PersonId,
IsEnabled = true,
Date = birthdayDate.Value,
Source = "birthday",
CountryCode = "US"
});
}
return new JiboInteractionDecision(
"memory_set_birthday",
$"Got it. I will remember your birthday is {birthday}.");
}
private JiboInteractionDecision BuildRecallBirthdayDecision(TurnContext turn)
{
var birthday = personalMemoryStore.GetBirthday(ResolveTenantScope(turn));
return string.IsNullOrWhiteSpace(birthday)
? new JiboInteractionDecision(
"memory_get_birthday",
"I do not know your birthday yet. You can say, my birthday is March 14.")
: new JiboInteractionDecision(
"memory_get_birthday",
$"You told me your birthday is {birthday}.");
}
private static DateOnly? TryParseBirthdayDate(string birthdayText)
{
if (string.IsNullOrWhiteSpace(birthdayText)) return null;
var normalized = birthdayText.Trim().ToLowerInvariant();
var match = Regex.Match(
normalized,
@"\b(?<month>january|february|march|april|may|june|july|august|september|october|november|december)\s+(?<day>\d{1,2})(?:st|nd|rd|th)?\b",
RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
if (!match.Success) return null;
var month = match.Groups["month"].Value.ToLowerInvariant() switch
{
"january" => 1,
"february" => 2,
"march" => 3,
"april" => 4,
"may" => 5,
"june" => 6,
"july" => 7,
"august" => 8,
"september" => 9,
"october" => 10,
"november" => 11,
"december" => 12,
_ => 0
};
if (month == 0) return null;
if (!int.TryParse(match.Groups["day"].Value, out var day) || day is < 1 or > 31) return null;
var today = DateOnly.FromDateTime(DateTime.UtcNow);
var year = today.Year;
if (day > DateTime.DaysInMonth(year, month)) return null;
DateOnly birthday;
try
{
birthday = new DateOnly(year, month, day);
}
catch
{
return null;
}
if (birthday < today) birthday = birthday.AddYears(1);
return birthday;
}
private static string? ResolvePreferredBirthdayLabel(TurnContext turn)
{
var context = ResolveGreetingPresenceProfile(turn);
return !string.IsNullOrWhiteSpace(context.PrimaryPersonId) &&
context.LoopUserFirstNames.TryGetValue(context.PrimaryPersonId, out var firstName) &&
!string.IsNullOrWhiteSpace(firstName)
? ToDisplayName(firstName)
: null;
}
private string RenderHolidayTemplate(string template, TurnContext turn, GreetingPresenceProfile presence)
{
var ownerName = ResolvePreferredGreetingName(turn, presence);
var speakerName = !string.IsNullOrWhiteSpace(ownerName) ? ownerName : "you";
return template
.Replace("${speaker}'s", $"{speakerName}'s", StringComparison.OrdinalIgnoreCase)
.Replace("${speaker}", speakerName, StringComparison.OrdinalIgnoreCase)
.Replace("${loop.owner}", string.IsNullOrWhiteSpace(ownerName) ? string.Empty : ownerName,
StringComparison.OrdinalIgnoreCase)
.Replace(" ", " ", StringComparison.Ordinal)
.Trim();
}
private JiboInteractionDecision BuildRememberImportantDateDecision(TurnContext turn, string transcript)
{
var importantDate = TryExtractImportantDateSet(transcript);
if (importantDate is null)
return new JiboInteractionDecision(
"memory_set_important_date",
"I can remember it if you say, our anniversary is June 10.");
personalMemoryStore.SetImportantDate(ResolveTenantScope(turn), importantDate.Value.Label,
importantDate.Value.Value);
return new JiboInteractionDecision(
"memory_set_important_date",
$"Got it. I will remember your {importantDate.Value.Label} is {importantDate.Value.Value}.");
}
private JiboInteractionDecision BuildRecallImportantDateDecision(TurnContext turn, string transcript)
{
var label = TryExtractImportantDateLookupLabel(transcript);
if (string.IsNullOrWhiteSpace(label))
return new JiboInteractionDecision(
"memory_get_important_date",
"Ask me like this: when is our anniversary?");
var storedDate = personalMemoryStore.GetImportantDate(ResolveTenantScope(turn), label);
return string.IsNullOrWhiteSpace(storedDate)
? new JiboInteractionDecision(
"memory_get_important_date",
$"I do not know your {label} yet.")
: new JiboInteractionDecision(
"memory_get_important_date",
$"You told me your {label} is {storedDate}.");
}
private JiboInteractionDecision BuildRememberPreferenceDecision(TurnContext turn, string transcript)
{
var preference = TryExtractPreferenceSet(transcript);
if (preference is null)
return new JiboInteractionDecision(
"memory_set_preference",
"I can remember it if you say, my favorite music is jazz.");
personalMemoryStore.SetPreference(ResolveTenantScope(turn), preference.Value.Category, preference.Value.Value);
return new JiboInteractionDecision(
"memory_set_preference",
$"Got it. I will remember your favorite {preference.Value.Category} is {preference.Value.Value}.");
}
private JiboInteractionDecision BuildRecallPreferenceDecision(TurnContext turn, string transcript)
{
var category = TryExtractPreferenceLookupCategory(transcript);
if (string.IsNullOrWhiteSpace(category))
return new JiboInteractionDecision(
"memory_get_preference",
"Ask me like this: what is my favorite music?");
var preference = personalMemoryStore.GetPreference(ResolveTenantScope(turn), category);
return string.IsNullOrWhiteSpace(preference)
? new JiboInteractionDecision(
"memory_get_preference",
$"I do not know your favorite {category} yet.")
: new JiboInteractionDecision(
"memory_get_preference",
$"You told me your favorite {category} is {preference}.");
}
private JiboInteractionDecision BuildRememberAffinityDecision(TurnContext turn, string transcript)
{
var affinitySet = TryExtractAffinitySet(transcript);
if (affinitySet is null)
return new JiboInteractionDecision(
"memory_set_affinity",
"I can remember it if you say, I like pizza or I dislike mushrooms.");
personalMemoryStore.SetAffinity(ResolveTenantScope(turn), affinitySet.Value.Item, affinitySet.Value.Affinity);
return new JiboInteractionDecision(
"memory_set_affinity",
$"Got it. I will remember you {DescribeAffinityAsVerb(affinitySet.Value.Affinity)} {affinitySet.Value.Item}.");
}
private JiboInteractionDecision BuildRecallAffinityDecision(TurnContext turn, string transcript)
{
var lookup = TryExtractAffinityLookup(transcript);
if (lookup is null)
return new JiboInteractionDecision(
"memory_get_affinity",
"Ask me like this: do I like pizza?");
var affinity = personalMemoryStore.GetAffinity(ResolveTenantScope(turn), lookup.Value.Item);
if (affinity is null)
return new JiboInteractionDecision(
"memory_get_affinity",
$"I do not remember how you feel about {lookup.Value.Item} yet.");
if (lookup.Value.ExpectedAffinity is null)
return new JiboInteractionDecision(
"memory_get_affinity",
$"You told me you {DescribeAffinityAsVerb(affinity.Value)} {lookup.Value.Item}.");
var matches = lookup.Value.ExpectedAffinity == PersonalAffinity.Dislike
? affinity == PersonalAffinity.Dislike
: affinity is PersonalAffinity.Like or PersonalAffinity.Love;
return matches
? new JiboInteractionDecision(
"memory_get_affinity",
$"Yes. You told me you {DescribeAffinityAsVerb(affinity.Value)} {lookup.Value.Item}.")
: new JiboInteractionDecision(
"memory_get_affinity",
$"Not exactly. You told me you {DescribeAffinityAsVerb(affinity.Value)} {lookup.Value.Item}.");
}
private JiboInteractionDecision BuildPizzaDecision()
{
return BuildPizzaAnimationDecision("pizza", "One pizza, coming right up.");
}
private JiboInteractionDecision BuildPizzaAnimationDecision(string intentName, string replyText)
{
var prompt = randomizer.Choose(PizzaMimPrompts);
return new JiboInteractionDecision(
intentName,
replyText,
"chitchat-skill",
new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
["esml"] = prompt.Esml,
["mim_id"] = "RA_JBO_MakePizza",
["mim_type"] = "announcement",
["prompt_id"] = prompt.PromptId,
["prompt_sub_category"] = "AN"
});
}
private JiboInteractionDecision BuildProactivePizzaDayDecision(DateTimeOffset? referenceLocalTime)
{
var referenceDate = (referenceLocalTime ?? DateTimeOffset.UtcNow).Date;
return BuildPizzaAnimationDecision(
"proactive_pizza_day",
$"Happy National Pizza Day for {referenceDate.ToString("MMMM d", CultureInfo.InvariantCulture)}. One pizza, coming right up.");
}
private JiboInteractionDecision BuildProactivePizzaPreferenceDecision()
{
return BuildPizzaAnimationDecision(
"proactive_pizza_preference",
"You mentioned pizza is a favorite, so I thought we should make one.");
}
private static JiboInteractionDecision BuildProactivePizzaFactOfferDecision()
{
var listenContexts = new[] { "shared/yes_no" };
return new JiboInteractionDecision(
"proactive_offer_pizza_fact",
"Do you want to hear a fun pizza fact?",
"chitchat-skill",
new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
["mim_id"] = "runtime-chat",
["mim_type"] = "question",
["prompt_id"] = "RUNTIME_PROMPT",
["prompt_sub_category"] = "Q",
["listen_contexts"] = listenContexts
});
}
private static JiboInteractionDecision BuildProactivePizzaFactDecision()
{
return new JiboInteractionDecision(
"proactive_pizza_fact",
"Americans consume about 100 acres of pizza every day, roughly 350 slices per second. That's a lot of pizza.");
}
private JiboInteractionDecision BuildProactiveFunFactDecision(JiboExperienceCatalog catalog)
{
var categories = new List<ProactiveFactCategory>();
AddProactiveFactCategory(categories, "fun_fact", catalog.FunFacts);
AddProactiveFactCategory(categories, "robot_fact", catalog.RobotFacts);
AddProactiveFactCategory(categories, "human_fact", catalog.HumanFacts);
if (categories.Count == 0)
return new JiboInteractionDecision("proactive_fun_fact", randomizer.Choose(catalog.SurpriseReplies));
var selectedCategory = randomizer.Choose(categories);
var fact = randomizer.Choose(selectedCategory.Replies);
return new JiboInteractionDecision(
"proactive_fun_fact",
fact,
"chitchat-skill",
new Dictionary<string, object?>
{
["mim_id"] = "runtime-fun-fact",
["mim_type"] = "announcement",
["prompt_id"] = "RUNTIME_FUN_FACT",
["replyType"] = "fun_fact",
["factCategory"] = selectedCategory.CategoryName
});
}
private static void AddProactiveFactCategory(
ICollection<ProactiveFactCategory> categories,
string categoryName,
IReadOnlyList<string> replies)
{
if (replies.Count == 0) return;
categories.Add(new ProactiveFactCategory(categoryName, replies));
}
private JiboInteractionDecision BuildProactiveJokeDecision(JiboExperienceCatalog catalog)
{
return new JiboInteractionDecision(
"proactive_joke",
randomizer.Choose(catalog.Jokes),
"@be/joke",
new Dictionary<string, object?>
{
["replyType"] = "joke"
});
}
private static JiboInteractionDecision BuildProactiveOfferDeclinedDecision()
{
return new JiboInteractionDecision(
"proactive_offer_declined",
"No problem. We can save the pizza fact for another time.");
}
private static JiboInteractionDecision BuildCurrentLocationDecision(TurnContext turn)
{
var locationName = TryResolveCurrentLocationName(turn);
@@ -1508,126 +972,6 @@ public sealed partial class JiboInteractionService(
return new PizzaSignal(null);
}
private string BuildGenericReply(JiboExperienceCatalog catalog, string transcript, string lowered)
{
if (string.IsNullOrWhiteSpace(transcript)) return "I am listening.";
if (lowered.Contains("good morning", StringComparison.Ordinal))
return "Good morning! It is nice to hear your voice.";
if (lowered.Contains("good afternoon", StringComparison.Ordinal))
return "Good afternoon. I am happy to be here.";
return lowered.Contains("good night", StringComparison.Ordinal)
? "Good night. Sleep tight."
: randomizer.Choose(catalog.GenericFallbackReplies)
.Replace("{transcript}", transcript, StringComparison.Ordinal);
}
private JiboInteractionDecision BuildScriptedPersonalityDecision(
JiboExperienceCatalog catalog,
string intentName,
params string[] preferredSnippets)
{
return ScriptedResponseDecisionBuilder.BuildScriptedPersonalityDecision(
catalog,
randomizer,
intentName,
preferredSnippets);
}
private JiboInteractionDecision BuildScriptedFavoriteAnimalDecision(
JiboExperienceCatalog catalog,
string intentName,
params string[] preferredSnippets)
{
return ScriptedResponseDecisionBuilder.BuildScriptedFavoriteAnimalDecision(
catalog,
randomizer,
intentName,
preferredSnippets);
}
private JiboInteractionDecision BuildScriptedGreetingDecision(
JiboExperienceCatalog catalog,
string intentName,
params string[] preferredSnippets)
{
return ScriptedResponseDecisionBuilder.BuildScriptedGreetingDecision(
catalog,
randomizer,
intentName,
preferredSnippets);
}
private JiboInteractionDecision BuildScriptedHolidayDecision(
IReadOnlyList<string> replies,
string intentName,
params string[] preferredSnippets)
{
return ScriptedResponseDecisionBuilder.BuildScriptedHolidayDecision(
replies,
randomizer,
intentName,
preferredSnippets);
}
private JiboInteractionDecision BuildScriptedHolidayTrackerDecision(
JiboExperienceCatalog catalog,
string intentName,
params string[] preferredSnippets)
{
return ScriptedResponseDecisionBuilder.BuildScriptedHolidayTrackerDecision(
catalog,
randomizer,
intentName,
preferredSnippets);
}
private JiboInteractionDecision BuildScriptedHolidayGreetingDecision(
JiboExperienceCatalog catalog,
string intentName,
params string[] preferredSnippets)
{
return ScriptedResponseDecisionBuilder.BuildScriptedHolidayGreetingDecision(
catalog,
randomizer,
intentName,
preferredSnippets);
}
private JiboInteractionDecision BuildScriptedHolidayTemplateDecision(
TurnContext turn,
GreetingPresenceProfile presence,
JiboExperienceCatalog catalog,
string intentName,
params string[] preferredSnippets)
{
var selected = ScriptedResponseDecisionBuilder.SelectLegacyReply(
catalog.HolidayReplies,
randomizer,
preferredSnippets);
return new JiboInteractionDecision(
intentName,
RenderHolidayTemplate(selected, turn, presence),
ContextUpdates: ScriptedResponseDecisionBuilder.BuildScriptedResponseContextUpdates());
}
private string SelectLegacyPersonalityReply(JiboExperienceCatalog catalog, params string[] preferredSnippets)
{
return ScriptedResponseDecisionBuilder.SelectLegacyPersonalityReply(catalog, randomizer, preferredSnippets);
}
private string SelectLegacyGreetingReply(JiboExperienceCatalog catalog, params string[] preferredSnippets)
{
return ScriptedResponseDecisionBuilder.SelectLegacyGreetingReply(catalog, randomizer, preferredSnippets);
}
private string SelectLegacyReply(IReadOnlyList<string> replies, params string[] preferredSnippets)
{
return ScriptedResponseDecisionBuilder.SelectLegacyReply(replies, randomizer, preferredSnippets);
}
private static string ResolveSemanticIntent(
string loweredTranscript,
DateTimeOffset? referenceLocalTime,