Beispiel #1
0
            public override string Evaluate(RequestProcess process)
            {
                process.Log(LogLevel.Info, $"In element <test>: running test {this.Name}");
                string text = this.Children?.Evaluate(process) ?? "";

                process.Log(LogLevel.Diagnostic, "In element <test>: processing text '" + text + "'.");
                var newRequest = new Aiml.Request(text, process.User, process.Bot);

                text = process.Bot.ProcessRequest(newRequest, false, false, process.RecursionDepth + 1, out var duration).ToString().Trim();
                process.Log(LogLevel.Diagnostic, "In element <test>: the request returned '" + text + "'.");

                if (process.testResults != null)
                {
                    var        expectedResponse = this.ExpectedResponse.Evaluate(process).Trim();
                    TestResult result;
                    if (process.Bot.Config.CaseSensitiveStringComparer.Equals(text, expectedResponse))
                    {
                        result = TestResult.Pass(duration);
                    }
                    else
                    {
                        result = TestResult.Failure($"Expected response: {expectedResponse}\nActual response: {text}", duration);
                    }
                    process.testResults[this.Name] = result;
                }
                else
                {
                    process.Log(LogLevel.Warning, "In element <test>: Tests are not being used.");
                }

                return(text);
            }
Beispiel #2
0
            public override string Evaluate(RequestProcess process)
            {
                StringBuilder builder = new StringBuilder();

                li item; int loops = 0;

                do
                {
                    ++loops;
                    if (loops > process.Bot.Config.LoopLimit)
                    {
                        process.Log(LogLevel.Warning, "Loop limit exceeded. User: "******"; path: \"" + process.Path + "\"");
                        throw new LoopLimitException();
                    }

                    item = this.Pick(process);
                    if (item == null)
                    {
                        return(string.Empty);
                    }
                    builder.Append(item.Children?.Evaluate(process));
                } while (item.Children != null && item.Children.Loop);

                return(builder.ToString());
            }
Beispiel #3
0
        public Template?Search(RequestSentence sentence, RequestProcess process, string that, bool traceSearch)
        {
            if (process.RecursionDepth > sentence.Bot.Config.RecursionLimit)
            {
                sentence.Bot.Log(LogLevel.Warning, "Recursion limit exceeded. User: "******"; raw input: \"" + sentence.Request.Text + "\"");
                throw new RecursionLimitException();
            }

            // Generate the input path.
            var messageSplit = sentence.Text.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
            var thatSplit    = that.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
            var topicSplit   = sentence.Bot.Normalize(sentence.User.Topic).Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);

            var inputPath = new string[messageSplit.Length + thatSplit.Length + topicSplit.Length + 2];
            int i         = 0;

            messageSplit.CopyTo(inputPath, 0);
            i += messageSplit.Length;
            inputPath[i++] = "<that>";
            thatSplit.CopyTo(inputPath, i);
            i += thatSplit.Length;
            inputPath[i++] = "<topic>";
            topicSplit.CopyTo(inputPath, i);
            if (traceSearch)
            {
                process.Log(LogLevel.Diagnostic, "Normalized path: " + string.Join(" ", inputPath));
            }

            var result = this.Search(sentence, process, inputPath, 0, traceSearch, MatchState.Message);

            return(result);
        }
Beispiel #4
0
            public override string Evaluate(RequestProcess process)
            {
                // Does the triple already exist?
                var clause = new Clause(this.Subject, this.Predicate, this.Object, true);

                clause.Evaluate(process);

                if (string.IsNullOrWhiteSpace(clause.subj) || string.IsNullOrWhiteSpace(clause.pred) || string.IsNullOrWhiteSpace(clause.obj))
                {
                    process.Log(LogLevel.Diagnostic, $"In element <addtriple>: Could not add triple with missing elements.  Subject: {clause.subj}  Predicate: {clause.pred}  Object: {clause.obj}");
                    return(process.Bot.Config.DefaultTriple);
                }

                var triples = process.Bot.Triples.Match(clause);

                if (triples.Count != 0)
                {
                    process.Log(LogLevel.Diagnostic, $"In element <addtriple>: Triple already exists at key {triples.First()}.  Subject: {clause.subj}  Predicate: {clause.pred}  Object: {clause.obj}");
                    return(triples.First().ToString());
                }

                // Add the triple.
                int key = process.Bot.Triples.Add(clause.subj, clause.pred, clause.obj);

                process.Log(LogLevel.Diagnostic, $"In element <addtriple>: Added a new triple with key {key}.  Subject: {clause.subj}  Predicate: {clause.pred}  Object: {clause.obj}");
                return(key.ToString());
            }
Beispiel #5
0
 public override string Evaluate(RequestProcess process)
 {
     try {
         if (process.Bot.SraixServices.TryGetValue(this.ServiceName, out var service))
         {
             var text = this.Children?.Evaluate(process) ?? "";
             process.Log(LogLevel.Diagnostic, "In element <sraix>: querying service '" + this.ServiceName + "' to process text '" + text + "'.");
             text = service.Process(text, this.Attributes, process);
             process.Log(LogLevel.Diagnostic, "In element <sraix>: the request returned '" + text + "'.");
             return(text);
         }
         else
         {
             process.User.Predicates["SraixException"]        = nameof(KeyNotFoundException);
             process.User.Predicates["SraixExceptionMessage"] = "No service named '" + this.ServiceName + "' is known.";
             process.Log(LogLevel.Warning, "In element <sraix>: no service named '" + this.ServiceName + "' is known.");
             return((this.DefaultReply ?? new TemplateElementCollection(new Srai(new TemplateElementCollection("SRAIXFAILED")))).Evaluate(process));
         }
     } catch (Exception ex) {
         process.User.Predicates["SraixException"]        = ex.GetType().Name;
         process.User.Predicates["SraixExceptionMessage"] = ex.Message;
         process.Log(LogLevel.Warning, "In element <sraix>: service '" + this.ServiceName + "' threw " + ex.GetType().Name + ":\n" + ex.ToString());
         return((this.DefaultReply ?? new TemplateElementCollection(new Srai(new TemplateElementCollection("SRAIXFAILED")))).Evaluate(process));
     }
 }
Beispiel #6
0
            public override string Evaluate(RequestProcess process)
            {
                var message = this.Children?.Evaluate(process) ?? "";

                process.Bot.WriteGossip(process, message);
                return(message);
            }
Beispiel #7
0
            public override string Evaluate(RequestProcess process)
            {
                string indices = null; int responseIndex = 1; int sentenceIndex = 1;

                if (this.Index != null)
                {
                    indices = this.Index.Evaluate(process);
                }

                if (!string.IsNullOrWhiteSpace(indices))
                {
                    // Parse the index attribute.
                    string[] fields = indices.Split(',');
                    if (fields.Length > 2)
                    {
                        throw new ArgumentException("index attribute of a that tag evaluated to an invalid value (" + indices + ").");
                    }

                    responseIndex = int.Parse(fields[0].Trim());
                    if (fields.Length == 2)
                    {
                        sentenceIndex = int.Parse(fields[1].Trim());
                    }
                }

                return(process.User.GetThat(responseIndex, sentenceIndex));
            }
Beispiel #8
0
            public override string Evaluate(RequestProcess process)
            {
                // Evaluate the contents of clauses.
                foreach (var clause in this.Clauses)
                {
                    clause.Evaluate(process);
                }

                string[] visibleVars;
                if (this.Variables == null)
                {
                    visibleVars = new string[0];
                }
                else
                {
                    visibleVars = this.Variables.Evaluate(process).Split((char[])null, StringSplitOptions.RemoveEmptyEntries);
                }

                // Start with an empty tuple.
                Tuple tuple  = new Tuple(new HashSet <string>(visibleVars, process.Bot.Config.StringComparer));
                var   tuples = this.SelectFromRemainingClauses(process, tuple, 0);

                process.Log(LogLevel.Diagnostic, $"In element <select>: Found {tuples.Count} matching {(tuples.Count == 1 ? "tuple" : "tuples")}.");
                if (tuples.Count == 0)
                {
                    return(process.Bot.Config.DefaultTriple);
                }
                return(string.Join(" ", tuples.Select(t => t.Index)));
            }
Beispiel #9
0
            public override string Evaluate(RequestProcess process)
            {
                var value = new StringBuilder(this.Children?.Evaluate(process) ?? "");

                bool firstLetter = true;

                for (int i = 0; i < value.Length; ++i)
                {
                    if (char.IsWhiteSpace(value[i]))
                    {
                        firstLetter = true;
                    }
                    else
                    {
                        if (firstLetter)
                        {
                            if (char.IsLower(value[i]))
                            {
                                value[i] = char.ToUpper(value[i]);
                            }
                            firstLetter = false;
                        }
                        else
                        {
                            if (char.IsUpper(value[i]))
                            {
                                value[i] = char.ToLower(value[i]);
                            }
                        }
                    }
                }

                return(value.ToString());
            }
Beispiel #10
0
            public override string Evaluate(RequestProcess process)
            {
                string value = this.TupleKey?.Evaluate(process);

                if (!string.IsNullOrWhiteSpace(value))
                {
                    // Get a value from a tuple.
                    int index; Tuple tuple;
                    if (int.TryParse(value, out index) && index >= 0 && index < Tuple.Tuples.Count)
                    {
                        tuple = Tuple.Tuples[index];
                        if (tuple.TryGetValue(this.Key.Evaluate(process), out value))
                        {
                            return(value);
                        }
                    }
                    return(process.Bot.Config.DefaultPredicate);
                }

                // Get a user variable or local variable.
                if (this.LocalVar)
                {
                    return(process.GetVariable(this.Key.Evaluate(process)));
                }
                return(process.User.GetPredicate(this.Key.Evaluate(process)));
            }
Beispiel #11
0
            public override string Evaluate(RequestProcess process)
            {
                var value = new StringBuilder(this.Children?.Evaluate(process) ?? "");

                int i;

                for (i = 0; i < value.Length; ++i)
                {
                    if (char.IsLetterOrDigit(value[i]))
                    {
                        if (char.IsLower(value[i]))
                        {
                            value[i] = char.ToUpper(value[i]);
                        }
                        break;
                    }
                }
                for (++i; i < value.Length; ++i)
                {
                    if (char.IsUpper(value[i]))
                    {
                        value[i] = char.ToLower(value[i]);
                    }
                }

                return(value.ToString());
            }
Beispiel #12
0
        /// <summary>Handles a wildcard node by taking words one by one until a template is found.</summary>
        private Template?WildcardSearch(RequestSentence subRequest, RequestProcess process, string[] inputPath, int inputPathIndex, bool traceSearch, MatchState matchState, int minimumWords)
        {
            int inputPathIndex2;
            var star      = process.GetStar(matchState);
            int starIndex = star.Count;

            // Reserve a space in the star list. If a template is found, this slot will be filled with the matched phrase.
            // This function can call other wildcards recursively. The reservation ensures that the star list will be populated correctly.
            star.Add("");

            for (inputPathIndex2 = inputPathIndex + minimumWords; inputPathIndex2 <= inputPath.Length; ++inputPathIndex2)
            {
                var result = this.Search(subRequest, process, inputPath, inputPathIndex2, traceSearch, matchState);
                if (result != null)
                {
                    star[starIndex] = string.Join(" ", inputPath, inputPathIndex, inputPathIndex2 - inputPathIndex);
                    return(result);
                }

                // Wildcards cannot match these tokens.
                if ((matchState == MatchState.Message && inputPath[inputPathIndex2] == "<that>") ||
                    (matchState == MatchState.That && inputPath[inputPathIndex2] == "<topic>"))
                {
                    break;
                }
            }

            // No match; remove the reserved slot.
            star.RemoveAt(starIndex);
            Debug.Assert(star.Count == starIndex);
            return(null);
        }
Beispiel #13
0
            public override string Evaluate(RequestProcess process)
            {
                var clause = new Clause(this.Subject, this.Predicate, this.Object, true);

                clause.Evaluate(process);

                if (string.IsNullOrWhiteSpace(clause.subj) || string.IsNullOrWhiteSpace(clause.pred) || string.IsNullOrWhiteSpace(clause.obj))
                {
                    process.Log(LogLevel.Diagnostic, $"In element <deletetriple>: Could not delete triple with missing elements.  Subject: {clause.subj}  Predicate: {clause.pred}  Object: {clause.obj}");
                    return(process.Bot.Config.DefaultTriple);
                }

                var triples = process.Bot.Triples.Match(clause);

                if (triples.Count == 0)
                {
                    process.Log(LogLevel.Diagnostic, $"In element <deletetriple>: No such triple exists.  Subject: {clause.subj}  Predicate: {clause.pred}  Object: {clause.obj}");
                    return(process.Bot.Config.DefaultTriple);
                }

                var index = triples.Single();

                process.Bot.Triples.Remove(index);
                process.Log(LogLevel.Diagnostic, $"In element <deletetriple>: Deleted the triple with key {index}.  Subject: {clause.subj}  Predicate: {clause.pred}  Object: {clause.obj}");
                return(index.ToString());
            }
Beispiel #14
0
 public string EvaluateChildren(RequestProcess process)
 {
     if (this.Children == null)
     {
         return("");
     }
     return(this.Children.Evaluate(process));
 }
Beispiel #15
0
            public override string Evaluate(RequestProcess process)
            {
                if (process.Bot.Maps.TryGetValue(this.Name.Evaluate(process), out var map))
                {
                    return(map[this.Children?.Evaluate(process) ?? ""] ?? process.Bot.Config.DefaultMap);
                }

                return(process.Bot.Config.DefaultMap);
            }
Beispiel #16
0
        internal void WriteGossip(RequestProcess process, string message)
        {
            var e = new GossipEventArgs(message);

            this.OnGossip(e);
            if (e.Handled)
            {
                return;
            }
            process.Log(LogLevel.Gossip, "Gossip from " + process.User.ID + ": " + message);
        }
Beispiel #17
0
            public override string Evaluate(RequestProcess process)
            {
                string text = process.star[0];

                process.Log(LogLevel.Diagnostic, "In element <sr>: processing text '" + text + "'.");
                var newRequest = new Aiml.Request(text, process.User, process.Bot);

                text = process.Bot.ProcessRequest(newRequest, false, false, process.RecursionDepth + 1, out _).ToString();
                process.Log(LogLevel.Diagnostic, "In element <sr>: the request returned '" + text + "'.");
                return(text);
            }
Beispiel #18
0
            public override string Evaluate(RequestProcess process)
            {
                int index = int.Parse(this.Index.Evaluate(process));

                if (process.topicstar.Count < index)
                {
                    return(process.Bot.Config.DefaultWildcard);
                }
                var match = process.topicstar[index - 1];

                return(match == "" ? process.Bot.Config.DefaultWildcard : match);
            }
Beispiel #19
0
            public override string Evaluate(RequestProcess process)
            {
                string?indexText = null; int index = 1;

                indexText = this.Index?.Evaluate(process);

                if (!string.IsNullOrWhiteSpace(indexText))
                {
                    index = int.Parse(indexText);
                }

                return(process.User.GetInput(index));
            }
Beispiel #20
0
            public override string Evaluate(RequestProcess process)
            {
                // Evaluate <eval> tags.
                XmlNode node = this.Node.Clone();

                this.ProcessXml(node, process);

                // Learn the result.
                process.Log(LogLevel.Diagnostic, $"In element <learn>: learning new category for {process.User.ID}: {node.OuterXml}");
                AimlLoader loader = new AimlLoader(process.Bot);

                loader.ProcessCategory(process.User.Graphmaster, node, null);

                return(string.Empty);
            }
Beispiel #21
0
            public override string Evaluate(RequestProcess process)
            {
                string sentence = (this.Children?.Evaluate(process) ?? "").Trim();

                if (sentence == "")
                {
                    return(process.Bot.Config.DefaultListItem);
                }

                int delimiter = sentence.IndexOf(' ');

                if (delimiter == -1)
                {
                    return(process.Bot.Config.DefaultListItem);
                }
                return(sentence.Substring(delimiter + 1).TrimStart());
            }
Beispiel #22
0
            public override string Evaluate(RequestProcess process)
            {
                StringBuilder builder = new StringBuilder();
                li            item;

                do
                {
                    item = this.Pick();
                    if (builder.Length != 0)
                    {
                        builder.Append(" ");
                    }
                    builder.Append(item.Evaluate(process));
                } while (item.Children.Loop);

                return(builder.ToString());
            }
Beispiel #23
0
            public override string Evaluate(RequestProcess process)
            {
                string command = this.Command.Evaluate(process);

                Process process2 = new Process();

                if (Environment.OSVersion.Platform < PlatformID.Unix)
                {
                    // Windows
                    process2.StartInfo = new ProcessStartInfo(Path.Combine(Environment.SystemDirectory, "cmd.exe"), "/Q /D /C \"" +
                                                              Regex.Replace(command, @"[/\\:*?""<>^]", "^$0") + "\"");
                    //    /C string   Carries out the command specified by string and then terminates.
                    //    /Q          Turns echo off.
                    //    /D          Disable execution of AutoRun commands from registry (see 'CMD /?').
                }
                else if (Environment.OSVersion.Platform == PlatformID.Unix)
                {
                    // UNIX
                    process2.StartInfo = new ProcessStartInfo(Path.Combine(Path.GetPathRoot(Environment.SystemDirectory), "bin", "sh"),
                                                              command.Replace(@"\", @"\\").Replace("\"", "\\\""));
                }
                process2.StartInfo.UseShellExecute        = false;
                process2.StartInfo.RedirectStandardOutput = true;
                process2.StartInfo.RedirectStandardError  = true;

                process.Log(LogLevel.Diagnostic, $"In element <system>: executing {process2.StartInfo.FileName} {process2.StartInfo.Arguments}");

                process2.Start();

                string output  = process2.StandardOutput.ReadToEnd();
                string output2 = process2.StandardError.ReadToEnd();

                process2.WaitForExit((int)process.Bot.Config.Timeout);

                if (!process2.HasExited)
                {
                    process.Log(LogLevel.Diagnostic, $"In element <system>: the process timed out.");
                }
                else if (process2.ExitCode != 0)
                {
                    process.Log(LogLevel.Diagnostic, $"In element <system>: the process exited with code {process2.ExitCode}.");
                }

                return(output);
            }
Beispiel #24
0
 private void ProcessXml(XmlNode node, RequestProcess process)
 {
     for (int i = 0; i < node.ChildNodes.Count; ++i)
     {
         XmlNode node2 = node.ChildNodes[i];
         if (node2.NodeType == XmlNodeType.Element)
         {
             if (node2.Name.Equals("eval", StringComparison.InvariantCultureIgnoreCase))
             {
                 TemplateElementCollection tags = TemplateElementCollection.FromXml(node2, process.Bot.AimlLoader);
                 node2.ParentNode.ReplaceChild(node.OwnerDocument.CreateTextNode(tags.Evaluate(process)), node2);
             }
             else
             {
                 this.ProcessXml(node2, process);
             }
         }
     }
 }
Beispiel #25
0
            public override string Evaluate(RequestProcess process)
            {
                string key   = this.Key.Evaluate(process);
                string value = (this.Children?.Evaluate(process) ?? "").Trim();

                var dictionary = this.LocalVar ? process.Variables : process.User.Predicates;

                if (process.Bot.Config.UnbindPredicatesWithDefaultValue &&
                    value == (this.LocalVar ? process.Bot.Config.DefaultPredicate : process.Bot.Config.GetDefaultPredicate(key)))
                {
                    dictionary.Remove(key);
                    process.Log(LogLevel.Diagnostic, "In element <set>: Unbound " + (this.LocalVar ? "local variable" : "predicate") + " '" + key + "' with default value '" + value + "'.");
                }
                else
                {
                    dictionary[key] = value;
                    process.Log(LogLevel.Diagnostic, "In element <set>: Set " + (this.LocalVar ? "local variable" : "predicate") + " '" + key + "' to '" + value + "'.");
                }

                return(value);
            }
Beispiel #26
0
        /// <summary>Evaluates the contained tags and returns the result.</summary>
        /// <param name="subRequest">The sub-request for which this tag collection is being evaluated.</param>
        /// <param name="response">The response being built.</param>
        /// <param name="thinking">Indicates whether we are inside a <code>think</code> template element.</param>
        /// <returns>The concatenated results of evaluating all the contained tags.</returns>
        public string Evaluate(RequestProcess process)
        {
            if (this.tags == null || this.tags.Length == 0)
            {
                return(string.Empty);
            }
            StringBuilder builder = new StringBuilder();

            foreach (TemplateNode tag in this.tags)
            {
                var output = tag.Evaluate(process);

                // Condense consecutive spaces.
                if (builder.Length > 0 && char.IsWhiteSpace(builder[builder.Length - 1]))
                {
                    output = output.TrimStart();
                }

                builder.Append(output);
            }
            return(builder.ToString());
        }
Beispiel #27
0
            public override string Evaluate(RequestProcess process)
            {
                // Evaluate <eval> tags.
                XmlNode node = this.Node.Clone();

                this.ProcessXml(node, process);

                // Learn the result.
                process.Log(LogLevel.Diagnostic, "In element <learnf>: learning new category: " + node.OuterXml);
                AimlLoader loader = new AimlLoader(process.Bot);

                loader.ProcessCategory(process.Bot.Graphmaster, node, null);

                // Write it to a file.
                bool         newFile = !File.Exists(process.Bot.Config.LearnfFile) || new FileInfo(process.Bot.Config.LearnfFile).Length < 7;
                StreamWriter writer  = new StreamWriter(File.Open("learnf.aiml", FileMode.OpenOrCreate, FileAccess.Write));

                if (newFile)
                {
                    writer.WriteLine("<!-- This file contains AIML categories the bot has learned via <learnf> elements. -->");
                    writer.WriteLine();
                    writer.WriteLine("<aiml version=\"2.0\">");
                    writer.WriteLine();
                }
                else
                {
                    // Seek to just before the closing </aiml> tag.
                    writer.BaseStream.Seek(-7, SeekOrigin.End);
                }

                writer.WriteLine("<!-- Learned from " + process.User.ID + " via category '" + process.Path + "' on " + DateTime.Now + ". -->");
                writer.Write(node.InnerXml.Trim('\r', '\n'));
                writer.WriteLine();
                writer.WriteLine();
                writer.Write("</aiml>");
                writer.Close();

                return(string.Empty);
            }
Beispiel #28
0
            public override string Evaluate(RequestProcess process)
            {
                this.Clause.Evaluate(process);

                // Find triples that match.
                var triples = process.Bot.Triples.Match(this.Clause);

                if (triples.Count == 0)
                {
                    process.Log(LogLevel.Diagnostic, $"In element <uniq>: No matching triple exists.  Subject: {this.Clause.subj}  Predicate: {this.Clause.pred}  Object: {this.Clause.obj}");
                    return(process.Bot.Config.DefaultTriple);
                }
                else if (triples.Count > 1)
                {
                    process.Log(LogLevel.Diagnostic, $"In element <uniq>: Found {triples.Count} matching triples.  Subject: {this.Clause.subj}  Predicate: {this.Clause.pred}  Object: {this.Clause.obj}");
                }

                var tripleIndex = triples.First();
                var triple      = process.Bot.Triples[tripleIndex];

                process.Log(LogLevel.Diagnostic, $"In element <uniq>: Found triple {tripleIndex}.  Subject: {triple.Subject}  Predicate: {triple.Predicate}  Object: {triple.Object}");

                // Get the result.
                if (this.Clause.obj.StartsWith("?"))
                {
                    return(triple.Object);
                }
                if (this.Clause.pred.StartsWith("?"))
                {
                    return(triple.Predicate);
                }
                if (this.Clause.subj.StartsWith("?"))
                {
                    return(triple.Subject);
                }
                process.Log(LogLevel.Warning, $"In element <uniq>: The clause contains no variables.  Subject: {this.Clause.subj}  Predicate: {this.Clause.pred}  Object: {this.Clause.obj}");
                return(process.Bot.Config.DefaultTriple);
            }
Beispiel #29
0
 public override string Evaluate(RequestProcess process)
 {
     return(process.User.ID);
 }
Beispiel #30
0
        private Template?Search(RequestSentence sentence, RequestProcess process, string[] inputPath, int inputPathIndex, bool traceSearch, MatchState matchState)
        {
            if (traceSearch)
            {
                sentence.Bot.Log(LogLevel.Diagnostic, "Search: " + process.Path);
            }

            int pathDepth = process.patternPathTokens.Count;

            if (process.CheckTimeout())
            {
                sentence.Bot.Log(LogLevel.Warning, "Request timeout. User: "******"; raw input: \"" + sentence.Request.Text + "\"");
                throw new TimeoutException();
            }

            bool tokensRemaining;

            if (inputPathIndex >= inputPath.Length)
            {
                // No tokens remaining in the input path. If this node has a template, return success.
                if (this.Template != null)
                {
                    return(this.Template);
                }
                // Otherwise, look for zero+ wildcards.
                tokensRemaining = false;
            }
            else
            {
                tokensRemaining = true;

                switch (matchState)
                {
                case MatchState.Message:
                    if (inputPath[inputPathIndex] == "<that>")
                    {
                        matchState = MatchState.That;
                    }
                    break;

                case MatchState.That:
                    if (inputPath[inputPathIndex] == "<topic>")
                    {
                        matchState = MatchState.Topic;
                    }
                    break;
                }
            }

            // Reserve a space in the pattern path list here. This is so that further recursive calls will leave it alone.
            // If we find a template, we replace the empty string with the correct token.
            process.patternPathTokens.Add("?");

            //var star = matchState == MatchState.That ? subRequest.thatstar :
            //	matchState == MatchState.Topic ? subRequest.topicstar :
            //	subRequest.star;

            // Search for child nodes that match the input in priority order.

            // Priority exact match.
            if (tokensRemaining && this.children.TryGetValue("$" + inputPath[inputPathIndex], out var node))
            {
                process.patternPathTokens[pathDepth] = "$" + inputPath[inputPathIndex];
                var result = node.Search(sentence, process, inputPath, inputPathIndex + 1, traceSearch, matchState);
                if (result != null)
                {
                    return(result);
                }
            }

            // Priority zero+ wildcard.
            if (this.children.TryGetValue("#", out node))
            {
                process.patternPathTokens[pathDepth] = "#";
                var result = node.WildcardSearch(sentence, process, inputPath, inputPathIndex, traceSearch, matchState, 0);
                if (result != null)
                {
                    return(result);
                }
            }

            // Priority one+ wildcard.
            if (this.children.TryGetValue("_", out node))
            {
                process.patternPathTokens[pathDepth] = "_";
                var result = node.WildcardSearch(sentence, process, inputPath, inputPathIndex, traceSearch, matchState, 1);
                if (result != null)
                {
                    return(result);
                }
            }

            // Exact match.
            if (tokensRemaining && this.children.TryGetValue(inputPath[inputPathIndex], out node))
            {
                process.patternPathTokens[pathDepth] = inputPath[inputPathIndex];
                var result = node.Search(sentence, process, inputPath, inputPathIndex + 1, traceSearch, matchState);
                if (result != null)
                {
                    return(result);
                }
            }

            // Sets. (The empty string cannot be matched by a set token.)
            if (tokensRemaining)
            {
                foreach (var child in this.setChildren)
                {
                    process.patternPathTokens[pathDepth] = $"<set>{child.SetName}</set>";
                    if (sentence.Bot.Sets.TryGetValue(child.SetName, out var set))
                    {
                        var star      = process.GetStar(matchState);
                        int starIndex = star.Count;
                        star.Add("");                          // Reserving a space; see above.

                        // Similarly to a wildcard search, we take words one by one until either a template is found, or no words remain.
                        // This time, each time we take a word, we must check that the phrase is in the set.
                        var phrase = new StringBuilder(); int wordCount = 0;
                        for (int inputPathIndex2 = inputPathIndex; inputPathIndex2 < inputPath.Length; ++inputPathIndex2)
                        {
                            if ((matchState == MatchState.Message && inputPath[inputPathIndex2] == "<that>") ||
                                (matchState == MatchState.That && inputPath[inputPathIndex2] == "<topic>"))
                            {
                                break;
                            }

                            if (phrase.Length > 0)
                            {
                                phrase.Append(' ');
                            }
                            phrase.Append(inputPath[inputPathIndex2]);
                            ++wordCount;

                            if (set.Contains(phrase.ToString()))
                            {
                                // Phrase found in the set. Now continue with the tree search.
                                var result = child.Node.Search(sentence, process, inputPath, inputPathIndex + wordCount, traceSearch, matchState);
                                if (result != null)
                                {
                                    star[starIndex] = phrase.ToString();
                                    return(result);
                                }
                            }

                            // Each set keeps track of the greatest number of words any element in the set has.
                            // After reaching that number, we can stop searching.
                            if (wordCount >= set.MaxWords)
                            {
                                break;
                            }
                        }

                        // No match; release the reserved space.
                        star.RemoveAt(starIndex);
                        Debug.Assert(star.Count == starIndex);
                    }
                    else
                    {
                        sentence.Request.Bot.Log(LogLevel.Warning, $"Reference to a missing set in pattern path '{string.Join(" ", process.patternPathTokens)}'.");
                    }
                }
            }

            // Zero+ wildcard.
            if (this.children.TryGetValue("^", out node))
            {
                process.patternPathTokens[pathDepth] = "^";
                var result = node.WildcardSearch(sentence, process, inputPath, inputPathIndex, traceSearch, matchState, 0);
                if (result != null)
                {
                    return(result);
                }
            }

            // One+ wildcard.
            if (this.children.TryGetValue("*", out node))
            {
                process.patternPathTokens[pathDepth] = "*";
                var result = node.WildcardSearch(sentence, process, inputPath, inputPathIndex, traceSearch, matchState, 1);
                if (result != null)
                {
                    return(result);
                }
            }

            // No match.
            process.patternPathTokens.RemoveAt(pathDepth);
            Debug.Assert(process.patternPathTokens.Count == pathDepth);
            return(null);
        }