Import Pegasus emotion phrases into chitchat routing
This commit is contained in:
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user