Tweak how mims work to get better responses
This commit is contained in:
@@ -49,4 +49,12 @@ public sealed class JiboExperienceCatalog
|
|||||||
public IReadOnlyList<string> GenericFallbackReplies { get; init; } = [];
|
public IReadOnlyList<string> GenericFallbackReplies { get; init; } = [];
|
||||||
public IReadOnlyList<string> DanceReplies { get; init; } = [];
|
public IReadOnlyList<string> DanceReplies { get; init; } = [];
|
||||||
public IReadOnlyList<string> DanceQuestionReplies { get; init; } = [];
|
public IReadOnlyList<string> DanceQuestionReplies { get; init; } = [];
|
||||||
|
|
||||||
|
// Key = MIM stem (e.g. "RI_JBO_CanMakeCoffee"), Value = list of stripped reply texts
|
||||||
|
public IReadOnlyDictionary<string, IReadOnlyList<string>> NamedScriptedReplies { get; init; }
|
||||||
|
= new Dictionary<string, IReadOnlyList<string>>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
// Key = lowercased trigger phrase, Value = MIM stem it maps to
|
||||||
|
public IReadOnlyDictionary<string, string> NamedScriptedTriggers { get; init; }
|
||||||
|
= new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -753,7 +753,8 @@ public sealed class JiboInteractionService(
|
|||||||
"calendar" => new JiboInteractionDecision("calendar", randomizer.Choose(catalog.CalendarReplies)),
|
"calendar" => new JiboInteractionDecision("calendar", randomizer.Choose(catalog.CalendarReplies)),
|
||||||
"commute" => new JiboInteractionDecision("commute", randomizer.Choose(catalog.CommuteReplies)),
|
"commute" => new JiboInteractionDecision("commute", randomizer.Choose(catalog.CommuteReplies)),
|
||||||
"news" => await BuildNewsDecisionAsync(turn, transcript, catalog, cancellationToken),
|
"news" => await BuildNewsDecisionAsync(turn, transcript, catalog, cancellationToken),
|
||||||
_ => new JiboInteractionDecision("chat", BuildGenericReply(catalog, transcript, lowered))
|
_ => TryBuildNamedMimDecision(catalog, lowered)
|
||||||
|
?? new JiboInteractionDecision("chat", BuildGenericReply(catalog, transcript, lowered))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2233,10 +2234,16 @@ public sealed class JiboInteractionService(
|
|||||||
if (lowered.Contains("good afternoon", StringComparison.Ordinal))
|
if (lowered.Contains("good afternoon", StringComparison.Ordinal))
|
||||||
return "Good afternoon. I am happy to be here.";
|
return "Good afternoon. I am happy to be here.";
|
||||||
|
|
||||||
return lowered.Contains("good night", StringComparison.Ordinal)
|
if (lowered.Contains("good night", StringComparison.Ordinal))
|
||||||
? "Good night. Sleep tight."
|
return "Good night. Sleep tight.";
|
||||||
: randomizer.Choose(catalog.GenericFallbackReplies)
|
|
||||||
.Replace("{transcript}", transcript, StringComparison.Ordinal);
|
// For unrecognized chitchat, use personality replies rather than the
|
||||||
|
// CC_Error "sources unavailable" content (which is reserved for service failures).
|
||||||
|
var chitchatPool = catalog.PersonalityReplies.Count > 0
|
||||||
|
? catalog.PersonalityReplies
|
||||||
|
: catalog.GenericFallbackReplies;
|
||||||
|
return randomizer.Choose(chitchatPool)
|
||||||
|
.Replace("{transcript}", transcript, StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
private JiboInteractionDecision BuildScriptedPersonalityDecision(
|
private JiboInteractionDecision BuildScriptedPersonalityDecision(
|
||||||
@@ -2250,6 +2257,31 @@ public sealed class JiboInteractionService(
|
|||||||
ContextUpdates: BuildScriptedResponseContextUpdates());
|
ContextUpdates: BuildScriptedResponseContextUpdates());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private JiboInteractionDecision? TryBuildNamedMimDecision(
|
||||||
|
JiboExperienceCatalog catalog, string loweredTranscript)
|
||||||
|
{
|
||||||
|
// Exact trigger match first
|
||||||
|
if (!catalog.NamedScriptedTriggers.TryGetValue(loweredTranscript, out var stem))
|
||||||
|
{
|
||||||
|
// Partial contains match: find the longest trigger phrase contained in the transcript
|
||||||
|
stem = catalog.NamedScriptedTriggers
|
||||||
|
.Where(kv => loweredTranscript.Contains(kv.Key, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.OrderByDescending(kv => kv.Key.Length)
|
||||||
|
.Select(static kv => kv.Value)
|
||||||
|
.FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stem is null) return null;
|
||||||
|
|
||||||
|
if (!catalog.NamedScriptedReplies.TryGetValue(stem, out var replies) || replies.Count == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return new JiboInteractionDecision(
|
||||||
|
stem,
|
||||||
|
randomizer.Choose(replies),
|
||||||
|
ContextUpdates: BuildScriptedResponseContextUpdates());
|
||||||
|
}
|
||||||
|
|
||||||
private JiboInteractionDecision BuildScriptedGreetingDecision(
|
private JiboInteractionDecision BuildScriptedGreetingDecision(
|
||||||
JiboExperienceCatalog catalog,
|
JiboExperienceCatalog catalog,
|
||||||
string intentName,
|
string intentName,
|
||||||
|
|||||||
@@ -30,6 +30,20 @@ public static class LegacyMimCatalogImporter
|
|||||||
@"\s+([,.;:!?])",
|
@"\s+([,.;:!?])",
|
||||||
RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
||||||
|
|
||||||
|
// Splits CamelCase words, e.g. "CanMakeCoffee" → ["Can", "Make", "Coffee"]
|
||||||
|
private static readonly Regex CamelCaseSplitPattern = new(
|
||||||
|
@"(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])",
|
||||||
|
RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
||||||
|
|
||||||
|
// Known file prefixes to strip when deriving trigger phrases
|
||||||
|
private static readonly string[] KnownPrefixes =
|
||||||
|
[
|
||||||
|
"RI_JBO_", "OI_JBO_", "JBO_", "RA_JBO_", "RN_JBO_", "RN_",
|
||||||
|
"KU_JBO_", "KU_", "JF_JBO_", "JF_", "SUP_JBO_", "SUP_",
|
||||||
|
"SRS_JBO_", "SRS_", "USR_JBO_", "USR_", "PR_JBO_", "PR_",
|
||||||
|
"CC_", "RA_", "OI_", "RI_"
|
||||||
|
];
|
||||||
|
|
||||||
public static JiboExperienceCatalog MergeInto(
|
public static JiboExperienceCatalog MergeInto(
|
||||||
JiboExperienceCatalog baseCatalog,
|
JiboExperienceCatalog baseCatalog,
|
||||||
string? rootDirectory)
|
string? rootDirectory)
|
||||||
@@ -56,18 +70,35 @@ public static class LegacyMimCatalogImporter
|
|||||||
var bucket = ResolveBucket(filePath);
|
var bucket = ResolveBucket(filePath);
|
||||||
if (bucket is null) continue;
|
if (bucket is null) continue;
|
||||||
|
|
||||||
|
var fileName = Path.GetFileNameWithoutExtension(filePath);
|
||||||
|
var isScriptedResponse = IsScriptedResponsePath(filePath);
|
||||||
|
|
||||||
|
var texts = new List<string>();
|
||||||
foreach (var prompt in definition.Prompts)
|
foreach (var prompt in definition.Prompts)
|
||||||
{
|
{
|
||||||
var text = NormalizePrompt(prompt.Prompt, IsTemplateBucket(bucket.Value));
|
var text = NormalizePrompt(prompt.Prompt, IsTemplateBucket(bucket.Value));
|
||||||
if (string.IsNullOrWhiteSpace(text)) continue;
|
if (string.IsNullOrWhiteSpace(text)) continue;
|
||||||
|
|
||||||
builder.Add(bucket.Value, prompt.Condition, text, prompt.Prompt);
|
builder.Add(bucket.Value, prompt.Condition, text, prompt.Prompt);
|
||||||
|
texts.Add(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build named lookup for all scripted-response files
|
||||||
|
if (isScriptedResponse && texts.Count > 0)
|
||||||
|
builder.AddNamed(fileName, texts);
|
||||||
}
|
}
|
||||||
|
|
||||||
return builder.Build();
|
return builder.Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsScriptedResponsePath(string filePath)
|
||||||
|
{
|
||||||
|
var normalizedPath = filePath.Replace('\\', '/');
|
||||||
|
return normalizedPath.Contains("/scripted-responses/", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| normalizedPath.Contains("/emotion-responses/", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| normalizedPath.Contains("/gqa-responses/", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
private static bool TryLoadDefinition(string filePath, out LegacyMimDefinition definition)
|
private static bool TryLoadDefinition(string filePath, out LegacyMimDefinition definition)
|
||||||
{
|
{
|
||||||
definition = new LegacyMimDefinition();
|
definition = new LegacyMimDefinition();
|
||||||
@@ -197,6 +228,128 @@ public static class LegacyMimCatalogImporter
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Derives natural-language trigger phrases from a MIM filename stem.
|
||||||
|
/// E.g. "RI_JBO_CanMakeCoffee" → ["can make coffee", "can you make coffee", "are you able to make coffee"]
|
||||||
|
/// </summary>
|
||||||
|
internal static IReadOnlyList<string> DeriveTriggerPhrases(string fileName)
|
||||||
|
{
|
||||||
|
var name = fileName;
|
||||||
|
|
||||||
|
// Strip known prefix
|
||||||
|
foreach (var prefix in KnownPrefixes)
|
||||||
|
{
|
||||||
|
if (name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
name = name[prefix.Length..];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(name)) return [];
|
||||||
|
|
||||||
|
// Split CamelCase and lowercase
|
||||||
|
var parts = CamelCaseSplitPattern.Split(name);
|
||||||
|
var lowered = parts.Select(static p => p.ToLowerInvariant()).Where(static p => !string.IsNullOrEmpty(p)).ToArray();
|
||||||
|
if (lowered.Length == 0) return [];
|
||||||
|
|
||||||
|
var joined = string.Join(" ", lowered);
|
||||||
|
var rest = lowered.Length > 1 ? string.Join(" ", lowered.Skip(1)) : string.Empty;
|
||||||
|
|
||||||
|
var triggers = new List<string> { joined };
|
||||||
|
|
||||||
|
var first = lowered[0];
|
||||||
|
|
||||||
|
switch (first)
|
||||||
|
{
|
||||||
|
case "can":
|
||||||
|
if (!string.IsNullOrEmpty(rest))
|
||||||
|
{
|
||||||
|
triggers.Add($"can you {rest}");
|
||||||
|
triggers.Add($"are you able to {rest}");
|
||||||
|
triggers.Add($"could you {rest}");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "is":
|
||||||
|
if (!string.IsNullOrEmpty(rest))
|
||||||
|
{
|
||||||
|
triggers.Add($"are you {rest}");
|
||||||
|
triggers.Add($"is jibo {rest}");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "are":
|
||||||
|
if (!string.IsNullOrEmpty(rest))
|
||||||
|
triggers.Add($"are you {rest}");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "likes" or "like":
|
||||||
|
if (!string.IsNullOrEmpty(rest))
|
||||||
|
{
|
||||||
|
triggers.Add($"do you like {rest}");
|
||||||
|
triggers.Add($"do you enjoy {rest}");
|
||||||
|
triggers.Add($"does jibo like {rest}");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "loves" or "love":
|
||||||
|
if (!string.IsNullOrEmpty(rest))
|
||||||
|
{
|
||||||
|
triggers.Add($"do you love {rest}");
|
||||||
|
triggers.Add($"do you like {rest}");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "believes" or "believe":
|
||||||
|
if (!string.IsNullOrEmpty(rest))
|
||||||
|
{
|
||||||
|
triggers.Add($"do you believe {rest}");
|
||||||
|
// "BelievesInSanta" → rest = "in santa" → already covered, but also add without "in"
|
||||||
|
if (rest.StartsWith("in ", StringComparison.Ordinal))
|
||||||
|
triggers.Add($"do you believe {rest["in ".Length..]}");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "knows" or "know":
|
||||||
|
if (!string.IsNullOrEmpty(rest))
|
||||||
|
{
|
||||||
|
triggers.Add($"do you know {rest}");
|
||||||
|
triggers.Add($"do you know about {rest}");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "has" or "have":
|
||||||
|
if (!string.IsNullOrEmpty(rest))
|
||||||
|
{
|
||||||
|
triggers.Add($"do you have {rest}");
|
||||||
|
triggers.Add($"have you {rest}");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "wants" or "want":
|
||||||
|
if (!string.IsNullOrEmpty(rest))
|
||||||
|
triggers.Add($"do you want {rest}");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "what":
|
||||||
|
case "who":
|
||||||
|
case "where":
|
||||||
|
case "when":
|
||||||
|
case "why":
|
||||||
|
case "how":
|
||||||
|
// Already in question form — keep as-is, no extra variants needed
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Generic: emit "do you [all words]" as a fallback variant
|
||||||
|
triggers.Add($"do you {joined}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return triggers.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
private static string NormalizePrompt(string? prompt)
|
private static string NormalizePrompt(string? prompt)
|
||||||
{
|
{
|
||||||
return NormalizePrompt(prompt, false);
|
return NormalizePrompt(prompt, false);
|
||||||
@@ -266,7 +419,9 @@ public static class LegacyMimCatalogImporter
|
|||||||
NewsBriefings = Merge(baseCatalog.NewsBriefings, importedCatalog.NewsBriefings),
|
NewsBriefings = Merge(baseCatalog.NewsBriefings, importedCatalog.NewsBriefings),
|
||||||
GenericFallbackReplies = Merge(baseCatalog.GenericFallbackReplies, importedCatalog.GenericFallbackReplies),
|
GenericFallbackReplies = Merge(baseCatalog.GenericFallbackReplies, importedCatalog.GenericFallbackReplies),
|
||||||
DanceReplies = Merge(baseCatalog.DanceReplies, importedCatalog.DanceReplies),
|
DanceReplies = Merge(baseCatalog.DanceReplies, importedCatalog.DanceReplies),
|
||||||
DanceQuestionReplies = Merge(baseCatalog.DanceQuestionReplies, importedCatalog.DanceQuestionReplies)
|
DanceQuestionReplies = Merge(baseCatalog.DanceQuestionReplies, importedCatalog.DanceQuestionReplies),
|
||||||
|
NamedScriptedReplies = MergeNamed(baseCatalog.NamedScriptedReplies, importedCatalog.NamedScriptedReplies),
|
||||||
|
NamedScriptedTriggers = MergeTriggers(baseCatalog.NamedScriptedTriggers, importedCatalog.NamedScriptedTriggers)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,6 +469,50 @@ public static class LegacyMimCatalogImporter
|
|||||||
return merged;
|
return merged;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyDictionary<string, IReadOnlyList<string>> MergeNamed(
|
||||||
|
IReadOnlyDictionary<string, IReadOnlyList<string>> baseDict,
|
||||||
|
IReadOnlyDictionary<string, IReadOnlyList<string>> importedDict)
|
||||||
|
{
|
||||||
|
var result = new Dictionary<string, IReadOnlyList<string>>(
|
||||||
|
baseDict, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var (key, importedReplies) in importedDict)
|
||||||
|
{
|
||||||
|
if (result.TryGetValue(key, out var existing))
|
||||||
|
{
|
||||||
|
// Merge reply lists, deduplicating
|
||||||
|
var seen = new HashSet<string>(existing, StringComparer.OrdinalIgnoreCase);
|
||||||
|
var merged = new List<string>(existing);
|
||||||
|
foreach (var reply in importedReplies)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(reply) && seen.Add(reply.Trim()))
|
||||||
|
merged.Add(reply.Trim());
|
||||||
|
}
|
||||||
|
result[key] = merged;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result[key] = importedReplies;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyDictionary<string, string> MergeTriggers(
|
||||||
|
IReadOnlyDictionary<string, string> baseDict,
|
||||||
|
IReadOnlyDictionary<string, string> importedDict)
|
||||||
|
{
|
||||||
|
// Base catalog's explicit triggers win; imported fills gaps
|
||||||
|
var result = new Dictionary<string, string>(baseDict, StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var (trigger, stem) in importedDict)
|
||||||
|
{
|
||||||
|
if (!result.ContainsKey(trigger))
|
||||||
|
result[trigger] = stem;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
private static string NormalizeCondition(string? condition)
|
private static string NormalizeCondition(string? condition)
|
||||||
{
|
{
|
||||||
return string.IsNullOrWhiteSpace(condition) ? string.Empty : WhitespacePattern.Replace(condition.Trim(), " ");
|
return string.IsNullOrWhiteSpace(condition) ? string.Empty : WhitespacePattern.Replace(condition.Trim(), " ");
|
||||||
@@ -389,6 +588,12 @@ public static class LegacyMimCatalogImporter
|
|||||||
private readonly List<string> _weatherTomorrowHighLowReplies = [];
|
private readonly List<string> _weatherTomorrowHighLowReplies = [];
|
||||||
private readonly List<string> _weatherTomorrowIntroReplies = [];
|
private readonly List<string> _weatherTomorrowIntroReplies = [];
|
||||||
|
|
||||||
|
// Named MIM dictionaries
|
||||||
|
private readonly Dictionary<string, List<string>> _namedReplies =
|
||||||
|
new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly Dictionary<string, string> _namedTriggers =
|
||||||
|
new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
public void Add(LegacyMimBucket bucket, string? condition, string text, string? sourcePrompt = null)
|
public void Add(LegacyMimBucket bucket, string? condition, string text, string? sourcePrompt = null)
|
||||||
{
|
{
|
||||||
switch (bucket)
|
switch (bucket)
|
||||||
@@ -511,8 +716,37 @@ public static class LegacyMimCatalogImporter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void AddNamed(string fileName, IReadOnlyList<string> replies)
|
||||||
|
{
|
||||||
|
if (!_namedReplies.TryGetValue(fileName, out var list))
|
||||||
|
{
|
||||||
|
list = [];
|
||||||
|
_namedReplies[fileName] = list;
|
||||||
|
}
|
||||||
|
|
||||||
|
var seen = new HashSet<string>(list, StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var reply in replies)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(reply) && seen.Add(reply.Trim()))
|
||||||
|
list.Add(reply.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Derive and register trigger phrases
|
||||||
|
var triggers = DeriveTriggerPhrases(fileName);
|
||||||
|
foreach (var trigger in triggers)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(trigger) && !_namedTriggers.ContainsKey(trigger))
|
||||||
|
_namedTriggers[trigger] = fileName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public JiboExperienceCatalog Build()
|
public JiboExperienceCatalog Build()
|
||||||
{
|
{
|
||||||
|
var namedReplies = _namedReplies.ToDictionary(
|
||||||
|
static kv => kv.Key,
|
||||||
|
static kv => (IReadOnlyList<string>)kv.Value,
|
||||||
|
StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
return new JiboExperienceCatalog
|
return new JiboExperienceCatalog
|
||||||
{
|
{
|
||||||
Jokes = [.. _jokes],
|
Jokes = [.. _jokes],
|
||||||
@@ -539,7 +773,9 @@ public static class LegacyMimCatalogImporter
|
|||||||
CommuteServiceDownReplies = [.. _commuteServiceDownReplies],
|
CommuteServiceDownReplies = [.. _commuteServiceDownReplies],
|
||||||
NewsIntroReplies = [.. _newsIntroReplies],
|
NewsIntroReplies = [.. _newsIntroReplies],
|
||||||
NewsCategoryIntroReplies = [.. _newsCategoryIntroReplies],
|
NewsCategoryIntroReplies = [.. _newsCategoryIntroReplies],
|
||||||
NewsOutroReplies = [.. _newsOutroReplies]
|
NewsOutroReplies = [.. _newsOutroReplies],
|
||||||
|
NamedScriptedReplies = namedReplies,
|
||||||
|
NamedScriptedTriggers = new Dictionary<string, string>(_namedTriggers, StringComparer.OrdinalIgnoreCase)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user