Expand affinity parser guardrails with Pegasus phrases

This commit is contained in:
Jacob Dubin
2026-05-09 23:46:00 -05:00
parent ffb444e4f9
commit 8ae6d86a8c
9 changed files with 931 additions and 6 deletions

View File

@@ -100,6 +100,8 @@ public sealed class DemoConversationBroker(JiboInteractionService interactionSer
"snapshot" => false,
"photobooth" => false,
"news" => false,
"trigger_ignored" => false,
"proactive_greeting" => false,
_ => true
};
}

View File

@@ -18,6 +18,12 @@ public sealed class JiboInteractionService(
var transcript = (turn.NormalizedTranscript ?? turn.RawTranscript ?? string.Empty).Trim();
var lowered = transcript.ToLowerInvariant();
var referenceLocalTime = TryResolveReferenceLocalTime(turn);
var messageType = turn.Attributes.TryGetValue("messageType", out var rawMessageType)
? rawMessageType?.ToString()
: null;
var triggerSource = turn.Attributes.TryGetValue("triggerSource", out var rawTriggerSource)
? rawTriggerSource?.ToString()
: null;
var clientIntent = turn.Attributes.TryGetValue("clientIntent", out var rawClientIntent)
? rawClientIntent?.ToString()
: null;
@@ -32,6 +38,17 @@ public sealed class JiboInteractionService(
? rawPendingProactivityOffer?.ToString()
: null;
var isYesNoTurn = IsYesNoTurn(turn);
var greetingPresence = ResolveGreetingPresenceProfile(turn);
if (string.Equals(messageType, "TRIGGER", StringComparison.OrdinalIgnoreCase))
{
if (ShouldHandleProactiveGreetingTrigger(turn, triggerSource, greetingPresence))
{
return BuildProactiveGreetingDecision(turn, greetingPresence, referenceLocalTime);
}
return BuildTriggerIgnoredDecision();
}
var isTimerValueTurn = IsClockTimerValueTurn(clientRules, listenRules);
var isAlarmValueTurn = IsClockAlarmValueTurn(clientRules, listenRules);
@@ -110,6 +127,11 @@ public sealed class JiboInteractionService(
"photobooth" => BuildPhotoCreateDecision("photobooth", "Starting photobooth.", "createSomePhotos"),
"robot_age" => BuildRobotAgeDecision(referenceLocalTime),
"robot_birthday" => BuildRobotBirthdayDecision(),
"good_morning" => BuildReactiveGreetingDecision(turn, "good_morning", referenceLocalTime),
"good_afternoon" => BuildReactiveGreetingDecision(turn, "good_afternoon", referenceLocalTime),
"good_evening" => BuildReactiveGreetingDecision(turn, "good_evening", referenceLocalTime),
"good_night" => BuildReactiveGreetingDecision(turn, "good_night", referenceLocalTime),
"welcome_back" => BuildReactiveGreetingDecision(turn, "welcome_back", referenceLocalTime),
"memory_set_name" => BuildRememberNameDecision(turn, transcript),
"memory_get_name" => BuildRecallNameDecision(turn),
"memory_set_birthday" => BuildRememberBirthdayDecision(turn, transcript),
@@ -163,6 +185,159 @@ public sealed class JiboInteractionService(
$"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, proactive: 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, proactive: 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));
if (!string.IsNullOrWhiteSpace(rememberedName))
{
return ToDisplayName(rememberedName);
}
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
};
updates[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);
@@ -1084,12 +1259,12 @@ public sealed class JiboInteractionService(
return "memory_get_important_date";
}
if (IsAffinitySetStatement(loweredTranscript))
if (IsAffinitySetStatement(loweredTranscript) || IsAffinitySetAttempt(loweredTranscript))
{
return "memory_set_affinity";
}
if (IsAffinityRecallQuestion(loweredTranscript))
if (IsAffinityRecallQuestion(loweredTranscript) || IsAffinityRecallAttempt(loweredTranscript))
{
return "memory_get_affinity";
}
@@ -1315,6 +1490,31 @@ public sealed class JiboInteractionService(
return "news";
}
if (IsWelcomeBackGreeting(loweredTranscript))
{
return "welcome_back";
}
if (IsGoodMorningGreeting(loweredTranscript))
{
return "good_morning";
}
if (IsGoodAfternoonGreeting(loweredTranscript))
{
return "good_afternoon";
}
if (IsGoodEveningGreeting(loweredTranscript))
{
return "good_evening";
}
if (IsGoodNightGreeting(loweredTranscript))
{
return "good_night";
}
if (MatchesAny(loweredTranscript, "how are you", "what's up", "what s up", "what up"))
{
return "how_are_you";
@@ -2004,6 +2204,115 @@ public sealed class JiboInteractionService(
}
}
private static GreetingPresenceProfile ResolveGreetingPresenceProfile(TurnContext turn)
{
if (!turn.Attributes.TryGetValue("context", out var contextValue) ||
contextValue is null ||
string.IsNullOrWhiteSpace(contextValue.ToString()))
{
return GreetingPresenceProfile.Empty;
}
try
{
using var document = JsonDocument.Parse(contextValue.ToString()!);
if (!document.RootElement.TryGetProperty("runtime", out var runtime) ||
runtime.ValueKind != JsonValueKind.Object)
{
return GreetingPresenceProfile.Empty;
}
var loopUsers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (runtime.TryGetProperty("loop", out var loop) &&
loop.ValueKind == JsonValueKind.Object &&
loop.TryGetProperty("users", out var users) &&
users.ValueKind == JsonValueKind.Array)
{
foreach (var user in users.EnumerateArray())
{
var id = TryReadStringProperty(user, "id");
var firstName = TryReadStringProperty(user, "firstName");
if (!string.IsNullOrWhiteSpace(id) && !string.IsNullOrWhiteSpace(firstName))
{
loopUsers[id] = firstName;
}
}
}
var speakerId = string.Empty;
var peoplePresentIds = new List<string>();
if (runtime.TryGetProperty("perception", out var perception) &&
perception.ValueKind == JsonValueKind.Object)
{
if (perception.TryGetProperty("speaker", out var speaker))
{
if (speaker.ValueKind == JsonValueKind.String)
{
speakerId = speaker.GetString() ?? string.Empty;
}
else if (speaker.ValueKind == JsonValueKind.Object)
{
speakerId = TryReadStringProperty(speaker, "id", "looperID", "looperId") ?? string.Empty;
}
}
if (perception.TryGetProperty("peoplePresent", out var peoplePresent) &&
peoplePresent.ValueKind == JsonValueKind.Array)
{
foreach (var person in peoplePresent.EnumerateArray())
{
var personId = person.ValueKind switch
{
JsonValueKind.String => person.GetString(),
JsonValueKind.Object => TryReadStringProperty(person, "id", "looperID", "looperId"),
_ => null
};
if (!string.IsNullOrWhiteSpace(personId) &&
!string.Equals(personId, "NOT_TRAINED", StringComparison.OrdinalIgnoreCase))
{
peoplePresentIds.Add(personId);
}
}
}
}
var triggerLooperId = turn.Attributes.TryGetValue("triggerLooperId", out var rawTriggerLooperId)
? rawTriggerLooperId?.ToString()
: null;
var primaryPersonId = !string.IsNullOrWhiteSpace(speakerId)
? speakerId
: !string.IsNullOrWhiteSpace(triggerLooperId)
? triggerLooperId
: peoplePresentIds.FirstOrDefault();
return new GreetingPresenceProfile(
primaryPersonId,
string.IsNullOrWhiteSpace(speakerId) ? null : speakerId,
peoplePresentIds,
loopUsers);
}
catch
{
return GreetingPresenceProfile.Empty;
}
}
private static string? TryReadStringProperty(JsonElement source, params string[] propertyNames)
{
foreach (var propertyName in propertyNames)
{
if (source.TryGetProperty(propertyName, out var value) &&
value.ValueKind == JsonValueKind.String &&
!string.IsNullOrWhiteSpace(value.GetString()))
{
return value.GetString();
}
}
return null;
}
private static double? TryReadDoubleProperty(JsonElement source, params string[] propertyNames)
{
foreach (var propertyName in propertyNames)
@@ -2315,6 +2624,57 @@ public sealed class JiboInteractionService(
};
}
private static bool IsWelcomeBackGreeting(string loweredTranscript)
{
return MatchesAny(
loweredTranscript,
"i am back",
"i m back",
"im back",
"i am home",
"i m home",
"im home",
"i'm back",
"i'm home",
"welcome back");
}
private static bool IsGoodMorningGreeting(string loweredTranscript)
{
return MatchesAny(
loweredTranscript,
"good morning",
"morning jibo",
"morning, jibo");
}
private static bool IsGoodAfternoonGreeting(string loweredTranscript)
{
return MatchesAny(
loweredTranscript,
"good afternoon",
"afternoon jibo",
"afternoon, jibo");
}
private static bool IsGoodEveningGreeting(string loweredTranscript)
{
return MatchesAny(
loweredTranscript,
"good evening",
"evening jibo",
"evening, jibo");
}
private static bool IsGoodNightGreeting(string loweredTranscript)
{
return MatchesAny(
loweredTranscript,
"good night",
"night jibo",
"night, jibo");
}
private static bool IsDanceQuestion(string loweredTranscript)
{
return MatchesAny(
@@ -2670,11 +3030,29 @@ public sealed class JiboInteractionService(
return TryExtractAffinitySet(loweredTranscript) is not null;
}
private static bool IsAffinitySetAttempt(string loweredTranscript)
{
var normalized = NormalizeCommandPhrase(loweredTranscript);
return PegasusUserAffinitySetPrefixes.Any(prefix => MatchesPrefixOrStem(normalized, prefix.Prefix));
}
private static bool IsAffinityRecallQuestion(string loweredTranscript)
{
return TryExtractAffinityLookup(loweredTranscript) is not null;
}
private static bool IsAffinityRecallAttempt(string loweredTranscript)
{
var normalized = NormalizeCommandPhrase(loweredTranscript);
return PegasusUserAffinityLookupPrefixes.Any(prefix => MatchesPrefixOrStem(normalized, prefix.Prefix));
}
private static bool MatchesPrefixOrStem(string normalized, string prefix)
{
return normalized.StartsWith(prefix, StringComparison.Ordinal) ||
string.Equals(normalized, prefix.TrimEnd(), StringComparison.Ordinal);
}
private static (string Item, PersonalAffinity Affinity)? TryExtractAffinitySet(string transcript)
{
var normalized = NormalizeCommandPhrase(transcript);
@@ -3331,6 +3709,21 @@ public sealed class JiboInteractionService(
private sealed record PizzaSignal(PersonalAffinity? Affinity);
private sealed record GreetingPresenceProfile(
string? PrimaryPersonId,
string? SpeakerId,
IReadOnlyList<string> PeoplePresentIds,
IReadOnlyDictionary<string, string> LoopUserFirstNames)
{
public static GreetingPresenceProfile Empty { get; } = new(
null,
null,
Array.Empty<string>(),
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase));
public bool HasKnownIdentity => !string.IsNullOrWhiteSpace(PrimaryPersonId);
}
private sealed record WeatherDateEntity(string? DateEntity, int ForecastDayOffset, string? ForecastLeadIn)
{
public static WeatherDateEntity None { get; } = new(null, 0, null);
@@ -3421,21 +3814,55 @@ public sealed class JiboInteractionService(
[
("i love ", PersonalAffinity.Love),
("i like ", PersonalAffinity.Like),
("i like the ", PersonalAffinity.Like),
("i enjoy ", PersonalAffinity.Like),
("i do like ", PersonalAffinity.Like),
("we love ", PersonalAffinity.Love),
("we like ", PersonalAffinity.Like),
("we enjoy ", PersonalAffinity.Like),
("i dislike ", PersonalAffinity.Dislike),
("i hate ", PersonalAffinity.Dislike),
("i hate the ", PersonalAffinity.Dislike),
("i loathe ", PersonalAffinity.Dislike),
("i don t like ", PersonalAffinity.Dislike),
("i dont like ", PersonalAffinity.Dislike),
("i not like ", PersonalAffinity.Dislike),
("i do not like ", PersonalAffinity.Dislike),
("i did not like ", PersonalAffinity.Dislike),
("i did not like the ", PersonalAffinity.Dislike),
("i didn t like ", PersonalAffinity.Dislike),
("i didnt like ", PersonalAffinity.Dislike),
("i didn t like the ", PersonalAffinity.Dislike),
("i didnt like the ", PersonalAffinity.Dislike),
("i didn t really like ", PersonalAffinity.Dislike),
("i didnt really like ", PersonalAffinity.Dislike),
("i don t really like ", PersonalAffinity.Dislike),
("i dont really like ", PersonalAffinity.Dislike),
("i don t enjoy ", PersonalAffinity.Dislike),
("i dont enjoy ", PersonalAffinity.Dislike),
("i do not enjoy ", PersonalAffinity.Dislike),
("i did not enjoy ", PersonalAffinity.Dislike),
("i didn t enjoy ", PersonalAffinity.Dislike),
("i didnt enjoy ", PersonalAffinity.Dislike),
("i didn t really enjoy ", PersonalAffinity.Dislike),
("i didnt really enjoy ", PersonalAffinity.Dislike),
("i don t love ", PersonalAffinity.Dislike),
("i dont love ", PersonalAffinity.Dislike),
("i do not love ", PersonalAffinity.Dislike),
("i don t love to ", PersonalAffinity.Dislike),
("i dont love to ", PersonalAffinity.Dislike),
("i do not love to ", PersonalAffinity.Dislike),
("i can t stand ", PersonalAffinity.Dislike),
("i cant stand ", PersonalAffinity.Dislike),
("i can t stand the ", PersonalAffinity.Dislike),
("i cant stand the ", PersonalAffinity.Dislike),
("we dislike ", PersonalAffinity.Dislike),
("we hate ", PersonalAffinity.Dislike),
("we despise ", PersonalAffinity.Dislike),
("we detest ", PersonalAffinity.Dislike),
("we loathe ", PersonalAffinity.Dislike),
("we can t stand ", PersonalAffinity.Dislike),
("we cant stand ", PersonalAffinity.Dislike),
("i despise ", PersonalAffinity.Dislike),
("i detest ", PersonalAffinity.Dislike)
];
@@ -3447,8 +3874,14 @@ public sealed class JiboInteractionService(
("do i enjoy ", PersonalAffinity.Like),
("do i dislike ", PersonalAffinity.Dislike),
("do i hate ", PersonalAffinity.Dislike),
("do i loathe ", PersonalAffinity.Dislike),
("do i not like ", PersonalAffinity.Dislike),
("do i despise ", PersonalAffinity.Dislike),
("do i detest ", PersonalAffinity.Dislike),
("do you think i like ", PersonalAffinity.Like),
("do you believe i like ", PersonalAffinity.Like),
("do you think i don t like ", PersonalAffinity.Dislike),
("do you believe i don t like ", PersonalAffinity.Dislike),
("how do i feel about ", null),
("what do i think about ", null)
];
@@ -3488,6 +3921,12 @@ public sealed class JiboInteractionService(
"our neighbourhood"
};
private const string GreetingRouteMetadataKey = "greetingsRoute";
private const string GreetingSpeakerMetadataKey = "greetingsSpeaker";
private const string LastProactiveGreetingUtcMetadataKey = "greetingsLastProactiveUtc";
private const string LastReactiveGreetingUtcMetadataKey = "greetingsLastReactiveUtc";
private static readonly TimeSpan ProactiveGreetingCooldown = TimeSpan.FromMinutes(20);
private const int MaxWeatherForecastDayOffset = 5;
private static readonly (string Phrase, string Station)[] RadioGenreAliases =

View File

@@ -104,7 +104,7 @@ public sealed class JiboWebSocketService(
}, cancellationToken);
return replies;
}
case "CLIENT_NLU" or "CLIENT_ASR":
case "CLIENT_NLU" or "CLIENT_ASR" or "TRIGGER":
{
var replies = await turnFinalizationService.HandleTurnAsync(session, envelope, parsedType, cancellationToken);
await telemetrySink.RecordTurnEventAsync(envelope, session, "turn_processed", new Dictionary<string, object?>

View File

@@ -60,7 +60,8 @@ public sealed class ProtocolToTurnContextMapper
foreach (var pair in session.Metadata)
{
if ((!pair.Key.StartsWith("personalReport", StringComparison.OrdinalIgnoreCase) &&
!pair.Key.StartsWith("chitchat", StringComparison.OrdinalIgnoreCase)) ||
!pair.Key.StartsWith("chitchat", StringComparison.OrdinalIgnoreCase) &&
!pair.Key.StartsWith("greetings", StringComparison.OrdinalIgnoreCase)) ||
pair.Value is null)
{
continue;
@@ -154,6 +155,22 @@ public sealed class ProtocolToTurnContextMapper
attributes["clientIntent"] = intent.GetString();
}
if (data.TryGetProperty("triggerSource", out var triggerSource) &&
triggerSource.ValueKind == JsonValueKind.String &&
!string.IsNullOrWhiteSpace(triggerSource.GetString()))
{
attributes["triggerSource"] = triggerSource.GetString();
}
if (data.TryGetProperty("triggerData", out var triggerData) &&
triggerData.ValueKind == JsonValueKind.Object &&
triggerData.TryGetProperty("looperID", out var triggerLooperId) &&
triggerLooperId.ValueKind == JsonValueKind.String &&
!string.IsNullOrWhiteSpace(triggerLooperId.GetString()))
{
attributes["triggerLooperId"] = triggerLooperId.GetString();
}
if (data.TryGetProperty("rules", out var rules) && rules.ValueKind == JsonValueKind.Array)
{
attributes["clientRules"] = rules.EnumerateArray()

View File

@@ -24,21 +24,52 @@ public sealed partial class WebSocketTurnFinalizationService(
{
"i love",
"i like",
"i like the",
"i enjoy",
"i do like",
"we love",
"we like",
"we enjoy",
"i dislike",
"i hate",
"i hate the",
"i loathe",
"i not like",
"i dont like",
"i don t like",
"i do not like",
"i did not like",
"i didn t like",
"i didnt like",
"i didn t really like",
"i didnt really like",
"i don t really like",
"i dont really like",
"i dont enjoy",
"i don t enjoy",
"i do not enjoy",
"i did not enjoy",
"i didn t enjoy",
"i didnt enjoy",
"i didn t really enjoy",
"i didnt really enjoy",
"i dont love",
"i don t love",
"i do not love",
"i don t love to",
"i dont love to",
"i do not love to",
"i cant stand",
"i can t stand",
"i cant stand the",
"i can t stand the",
"we dislike",
"we hate",
"we despise",
"we detest",
"we loathe",
"we cant stand",
"we can t stand",
"i despise",
"i detest"
};
@@ -526,7 +557,9 @@ public sealed partial class WebSocketTurnFinalizationService(
}
var allowEmptyTranscriptForPersonalReport = IsActivePersonalReportTurn(finalizedTurn);
var allowEmptyTranscriptForTrigger = string.Equals(ReadMessageType(finalizedTurn), "TRIGGER", StringComparison.OrdinalIgnoreCase);
if (!allowEmptyTranscriptForPersonalReport &&
!allowEmptyTranscriptForTrigger &&
string.IsNullOrWhiteSpace(finalizedTurn.NormalizedTranscript) &&
string.IsNullOrWhiteSpace(finalizedTurn.RawTranscript))
{