Import Pegasus emotion phrases into chitchat routing

This commit is contained in:
Jacob Dubin
2026-05-06 16:19:45 -05:00
parent 7d31c3390c
commit c64f4b91a8
2 changed files with 311 additions and 19 deletions

View File

@@ -1,4 +1,5 @@
using Jibo.Cloud.Application.Abstractions;
using System.Text.RegularExpressions;
namespace Jibo.Cloud.Application.Services;
@@ -22,21 +23,110 @@ internal static class ChitchatStateMachine
[
"how are you feeling",
"how do you feel",
"what are you feeling",
"what mood are you in",
"what is your mood",
"what's your mood",
"are you happy",
"are you sad",
"are you excited",
"do you have emotions"
"do you have emotions",
"how angry are you",
"how jealous are you",
"how sad are you",
"how upset do you feel",
"how bored are you right now"
];
private static readonly (string Emotion, string[] Phrases)[] EmotionCommandPhrases =
// Pegasus parser-derived query anchors from descriptor/emotion intent families.
private static readonly string[] EmotionQueryPrefixes =
[
("happy", ["smile", "be happy", "look happy", "cheer up"]),
("sad", ["be sad", "look sad"]),
("excited", ["be excited", "get excited", "act excited"]),
("calm", ["be calm", "relax"])
"are you ",
"are you feeling ",
"are you able to feel ",
"are you able to get ",
"are you ever ",
"can you be ",
"do you feel ",
"do you ever feel ",
"do you ever get ",
"do you get ",
"does ",
"would ",
"how ",
"describe how "
];
// Pegasus parser-derived specific-emotion assertion forms.
private static readonly string[] EmotionAssertionPrefixes =
[
"you are ",
"you re ",
"you are acting ",
"you seem ",
"you look ",
"i think you are ",
"i think you re ",
"i feel like you are ",
"i feel like you re ",
"in my opinion you are ",
"in my opinion you re "
];
private static readonly string[] EmotionCommandPositivePrefixes =
[
"be ",
"be a little ",
"be a bit ",
"be very ",
"be more ",
"you should be ",
"you should try to be ",
"try to be ",
"look ",
"act "
];
private static readonly string[] EmotionCommandNegativePrefixes =
[
"do not be ",
"don t be ",
"dont be ",
"try not to be ",
"you should not be ",
"you shouldn t be "
];
private static readonly (string Phrase, string Emotion)[] DirectEmotionCommandPhrases =
[
("smile", "happy"),
("look happy", "happy"),
("cheer up", "happy"),
("be happy", "happy"),
("be excited", "excited"),
("get excited", "excited"),
("act excited", "excited"),
("be sad", "sad"),
("look sad", "sad"),
("be calm", "calm"),
("calm down", "calm"),
("relax", "calm")
];
// Derived from Pegasus parser Emotion entity and utterance sets.
private static readonly (string Emotion, string[] Synonyms)[] PegasusEmotionSynonyms =
[
("afraid", ["afraid", "fearful", "frightened", "scared", "terrified", "spooked", "freak out", "freaked out"]),
("amused", ["amused", "entertained", "tickled", "tickled pink"]),
("angry", ["angry", "mad", "furious", "enraged", "irate", "incensed", "cross"]),
("annoyed", ["annoyed", "aggravated", "bothered", "irritated", "grumpy", "nettled", "vexed", "bored"]),
("anxious", ["anxious", "nervous", "worried", "tense", "on edge", "jittery", "restless", "concerned"]),
("confident", ["confident", "assured", "secure", "self assured", "self confident"]),
("confused", ["confused", "at a loss", "perplexed", "puzzled", "stumped", "uncertain", "unsure"]),
("embarrassed", ["embarrassed", "ashamed", "flustered", "self conscious", "sheepish"]),
("excited", ["excited", "jazzed", "psyched", "pumped"]),
("happy", ["happy", "cheerful", "jovial", "pleased", "joyful", "content", "thrilled"]),
("jealous", ["jealous", "envious", "covetous"]),
("lonely", ["lonely", "alone", "lonesome"]),
("proud", ["proud", "honored"]),
("sad", ["sad", "upset", "unhappy", "depressed", "somber", "downcast", "gloomy", "miserable", "bummed", "heartbroken", "troubled"])
];
private static readonly string[] EmotionCommandReplies =
@@ -46,6 +136,16 @@ internal static class ChitchatStateMachine
"Okay, mood change activated."
];
private static readonly Regex PhrasePunctuationPattern = new(
@"[^\w\s]",
RegexOptions.CultureInvariant | RegexOptions.Compiled);
private static readonly Regex PhraseWhitespacePattern = new(
@"\s+",
RegexOptions.CultureInvariant | RegexOptions.Compiled);
private static readonly (string Phrase, string Emotion)[] EmotionSynonymMappings = BuildEmotionSynonymMappings();
public static JiboInteractionDecision? TryBuildDecision(
string semanticIntent,
string transcript,
@@ -54,6 +154,7 @@ internal static class ChitchatStateMachine
IJiboRandomizer randomizer,
Func<string> buildErrorResponse)
{
var normalizedLoweredTranscript = NormalizeForPhraseMatching(loweredTranscript);
switch (semanticIntent)
{
case "hello":
@@ -69,14 +170,14 @@ internal static class ChitchatStateMachine
"how_are_you",
randomizer.Choose(catalog.HowAreYouReplies));
case "chat":
if (IsEmotionQuery(loweredTranscript))
if (IsEmotionQuery(normalizedLoweredTranscript))
{
return BuildEmotionQueryDecision(
"emotion_query",
randomizer.Choose(catalog.HowAreYouReplies));
}
if (TryResolveEmotionCommand(loweredTranscript, out var emotion))
if (TryResolveEmotionCommand(normalizedLoweredTranscript, out var emotion))
{
return BuildEmotionCommandDecision(randomizer, emotion!);
}
@@ -90,8 +191,9 @@ internal static class ChitchatStateMachine
}
}
public static bool IsLikelyEmotionUtterance(string normalizedLoweredTranscript)
public static bool IsLikelyEmotionUtterance(string transcript)
{
var normalizedLoweredTranscript = NormalizeForPhraseMatching(transcript);
return IsEmotionQuery(normalizedLoweredTranscript) ||
TryResolveEmotionCommand(normalizedLoweredTranscript, out _);
}
@@ -176,15 +278,71 @@ internal static class ChitchatStateMachine
private static bool IsEmotionQuery(string loweredTranscript)
{
return ContainsAnyPhrase(loweredTranscript, EmotionQueryPhrases);
if (ContainsAnyPhrase(loweredTranscript, EmotionQueryPhrases))
{
return true;
}
if (!TryResolveEmotionFromText(loweredTranscript, out _))
{
return false;
}
return StartsWithAnyPhrase(loweredTranscript, EmotionQueryPrefixes) ||
StartsWithAnyPhrase(loweredTranscript, EmotionAssertionPrefixes);
}
private static bool TryResolveEmotionCommand(string loweredTranscript, out string? emotion)
{
emotion = null;
foreach (var mapping in EmotionCommandPhrases)
foreach (var mapping in DirectEmotionCommandPhrases)
{
if (!ContainsAnyPhrase(loweredTranscript, mapping.Phrases))
if (!ContainsPhrase(loweredTranscript, mapping.Phrase))
{
continue;
}
emotion = mapping.Emotion;
return true;
}
var isNegativeCommand = StartsWithAnyPhrase(loweredTranscript, EmotionCommandNegativePrefixes);
var isPositiveCommand = !isNegativeCommand && StartsWithAnyPhrase(loweredTranscript, EmotionCommandPositivePrefixes);
if (!isNegativeCommand && !isPositiveCommand)
{
return false;
}
if (!TryResolveEmotionFromText(loweredTranscript, out var canonicalEmotion) ||
string.IsNullOrWhiteSpace(canonicalEmotion))
{
return false;
}
emotion = isNegativeCommand
? "calm"
: MapCanonicalEmotionToRuntimeEmotion(canonicalEmotion);
return true;
}
private static string MapCanonicalEmotionToRuntimeEmotion(string canonicalEmotion)
{
return canonicalEmotion switch
{
"happy" or "amused" or "excited" or "confident" or "proud" => "happy",
"sad" or "lonely" or "afraid" or "anxious" or "embarrassed" or "confused" => "sad",
"angry" or "annoyed" or "jealous" => "calm",
_ => "calm"
};
}
private static bool TryResolveEmotionFromText(string loweredTranscript, out string? emotion)
{
emotion = null;
foreach (var mapping in EmotionSynonymMappings)
{
if (!ContainsPhrase(loweredTranscript, mapping.Phrase))
{
continue;
}
@@ -200,10 +358,7 @@ internal static class ChitchatStateMachine
{
foreach (var phrase in phrases)
{
if (string.Equals(loweredTranscript, phrase, StringComparison.Ordinal) ||
loweredTranscript.StartsWith($"{phrase} ", StringComparison.Ordinal) ||
loweredTranscript.Contains($" {phrase} ", StringComparison.Ordinal) ||
loweredTranscript.EndsWith($" {phrase}", StringComparison.Ordinal))
if (ContainsPhrase(loweredTranscript, phrase))
{
return true;
}
@@ -211,4 +366,75 @@ internal static class ChitchatStateMachine
return false;
}
private static bool StartsWithAnyPhrase(string loweredTranscript, IEnumerable<string> phrases)
{
foreach (var phrase in phrases)
{
var normalizedPhrase = NormalizeForPhraseMatching(phrase);
if (string.IsNullOrWhiteSpace(normalizedPhrase))
{
continue;
}
if (string.Equals(loweredTranscript, normalizedPhrase, StringComparison.Ordinal) ||
loweredTranscript.StartsWith($"{normalizedPhrase} ", StringComparison.Ordinal))
{
return true;
}
}
return false;
}
private static bool ContainsPhrase(string loweredTranscript, string phrase)
{
var normalizedPhrase = NormalizeForPhraseMatching(phrase);
if (string.IsNullOrWhiteSpace(normalizedPhrase) ||
string.IsNullOrWhiteSpace(loweredTranscript))
{
return false;
}
return string.Equals(loweredTranscript, normalizedPhrase, StringComparison.Ordinal) ||
loweredTranscript.StartsWith($"{normalizedPhrase} ", StringComparison.Ordinal) ||
loweredTranscript.Contains($" {normalizedPhrase} ", StringComparison.Ordinal) ||
loweredTranscript.EndsWith($" {normalizedPhrase}", StringComparison.Ordinal);
}
private static string NormalizeForPhraseMatching(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return string.Empty;
}
var lowered = value.ToLowerInvariant();
var withoutPunctuation = PhrasePunctuationPattern.Replace(lowered, " ");
return PhraseWhitespacePattern.Replace(withoutPunctuation, " ").Trim();
}
private static (string Phrase, string Emotion)[] BuildEmotionSynonymMappings()
{
var seen = new HashSet<string>(StringComparer.Ordinal);
var mappings = new List<(string Phrase, string Emotion)>();
foreach (var emotionMapping in PegasusEmotionSynonyms)
{
foreach (var synonym in emotionMapping.Synonyms)
{
var normalizedSynonym = NormalizeForPhraseMatching(synonym);
if (string.IsNullOrWhiteSpace(normalizedSynonym) ||
!seen.Add(normalizedSynonym))
{
continue;
}
mappings.Add((normalizedSynonym, emotionMapping.Emotion));
}
}
mappings.Sort(static (left, right) => right.Phrase.Length.CompareTo(left.Phrase.Length));
return [.. mappings];
}
}

View File

@@ -220,6 +220,72 @@ public sealed class JiboInteractionServiceTests
Assert.Equal("ErrorResponse", decision.ContextUpdates![ChitchatRouteKey]);
}
[Fact]
public async Task BuildDecisionAsync_HowAngryAreYou_RoutesThroughPegasusEmotionQueryPhrase()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "how angry are you",
NormalizedTranscript = "how angry are you"
});
Assert.Equal("emotion_query", decision.IntentName);
Assert.NotNull(decision.ContextUpdates);
Assert.Equal("EmotionQuery", decision.ContextUpdates![ChitchatRouteKey]);
}
[Fact]
public async Task BuildDecisionAsync_YouSeemSad_RoutesThroughPegasusEmotionAssertionPhrase()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "you seem sad",
NormalizedTranscript = "you seem sad"
});
Assert.Equal("emotion_query", decision.IntentName);
Assert.NotNull(decision.ContextUpdates);
Assert.Equal("EmotionQuery", decision.ContextUpdates![ChitchatRouteKey]);
}
[Fact]
public async Task BuildDecisionAsync_YouShouldTryToBeHappy_RoutesThroughPegasusEmotionCommandPhrase()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "you should try to be happy",
NormalizedTranscript = "you should try to be happy"
});
Assert.Equal("emotion_command", decision.IntentName);
Assert.NotNull(decision.ContextUpdates);
Assert.Equal("EmotionCommand", decision.ContextUpdates![ChitchatRouteKey]);
Assert.Equal("happy", decision.ContextUpdates[ChitchatEmotionKey]);
}
[Fact]
public async Task BuildDecisionAsync_DontBeAngry_RoutesThroughPegasusNegativeEmotionCommandPhrase()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "don't be angry",
NormalizedTranscript = "don't be angry"
});
Assert.Equal("emotion_command", decision.IntentName);
Assert.NotNull(decision.ContextUpdates);
Assert.Equal("EmotionCommand", decision.ContextUpdates![ChitchatRouteKey]);
Assert.Equal("calm", decision.ContextUpdates[ChitchatEmotionKey]);
}
[Fact]
public async Task BuildDecisionAsync_BirthdayMemory_SetThenRecallWithinTenant()
{