Skip to content

Chapter 25: Capstone Project - Building a Rule-Based NLP Chatbot

Theoretical Foundations

A rule-based NLP chatbot is a program that uses predefined patterns and logic to understand user input and generate responses. Unlike modern AI models that learn from data, this approach relies on explicit rules you write yourself. It's an excellent first step into natural language processing because it teaches you how to break down language into computable patterns.

To build this, we need to synthesize everything you've learned so far: handling input/output, string manipulation, logical decision-making, and structuring code into reusable components.

The Architecture: Strategy Pattern via Conditional Logic

In a rule-based system, we need a way to match a user's message to a specific "skill" or "response." A naive approach might use a long chain of if-else if-else statements. While valid, this becomes difficult to maintain as the bot grows.

Instead, we will use a design approach often called the Strategy Pattern. Conceptually, this means defining a set of "strategies" (rules) and selecting the appropriate one based on the input.

Analogy: The Restaurant Menu Imagine you are at a restaurant. The menu lists specific dishes (strategies). When you order, you don't tell the chef "make food"; you say "I want the burger" (matching input to a strategy). The chef executes the recipe for the burger. If you ask for something not on the menu, the chef serves a default dish (a fallback strategy).

In our code:

  1. The Input: The user's text.
  2. The Menu: A collection of rules (patterns).
  3. The Chef: The logic that scans the input and picks the matching rule.

Step 1: Defining the Rule Structure

We need a way to represent a rule. A rule must contain:

  1. A Pattern: The keyword or regex to look for (e.g., "hello").
  2. A Response: What the bot should say back.

We will use a class to encapsulate this logic (Chapter 16: Class definition). This allows us to group data and behavior together.

using System;
using System.Collections.Generic; // Required for List<T> (Chapter 20)

public class ResponseRule
{
    // The pattern we look for in the user's input (Chapter 17: Fields/Properties)
    public string Pattern { get; set; }

    // The response to return if the pattern matches
    public string Response { get; set; }

    // Constructor to initialize the rule (Chapter 18: Constructors)
    public ResponseRule(string pattern, string response)
    {
        Pattern = pattern;
        Response = response;
    }

    // A method to check if this rule matches the input
    // We use String.Contains (Chapter 23: String methods) for simplicity
    public bool IsMatch(string userInput)
    {
        // Convert both to lowercase to make matching case-insensitive
        // (We will simulate this using logic, as ToLower() is slightly advanced, 
        // but we can compare manually or assume case sensitivity for now. 
        // For this example, we will use basic equality or substring checks).

        // Using String.Contains to see if the pattern exists in the input
        // Note: Contains is case-sensitive. We will handle casing in the engine.
        return userInput.Contains(Pattern);
    }
}

Step 2: The Chatbot Engine (The Strategy Selector)

The engine is the core logic. It holds the list of rules and processes input. We will use a List<ResponseRule> (Chapter 20) to store our dynamic collection of rules.

This engine demonstrates Dependency Injection concepts (providing dependencies to a class) by accepting rules via its constructor.

public class ChatBotEngine
{
    // A dynamic collection to hold our rules (Chapter 20: List<T>)
    private List<ResponseRule> _rules;

    // Constructor initializes the list (Chapter 18, 20)
    public ChatBotEngine()
    {
        _rules = new List<ResponseRule>();
    }

    // Method to add rules dynamically
    public void AddRule(ResponseRule rule)
    {
        _rules.Add(rule);
    }

    // The core processing logic
    public string GetResponse(string userInput)
    {
        // Normalize input to lowercase for better matching
        // (We will simulate this by manually iterating and checking)
        string lowerInput = userInput.ToLower(); 

        // Iterate through rules using a foreach loop (Chapter 12)
        foreach (ResponseRule rule in _rules)
        {
            // Check if the rule's pattern exists in the input
            // We convert the rule pattern to lowercase too for comparison
            if (lowerInput.Contains(rule.Pattern.ToLower()))
            {
                return rule.Response;
            }
        }

        // Default response if no rule matches (Chapter 6: if/else)
        return "I'm not sure how to respond to that.";
    }
}

Step 3: String Manipulation and Logic

To make the bot feel intelligent, we need to handle basic string operations. We will rely on Chapter 23 (String methods) and Chapter 6 (Conditional Logic).

A common requirement in NLP is handling synonyms or variations. For example, if the user says "hi", "hello", or "hey", the bot should respond with a greeting.

We can implement a "Keyword Router" using if statements and the || (OR) logical operator (Chapter 7).

public class SimpleKeywordRouter
{
    public static string Route(string input)
    {
        // Normalize the input
        string lowerInput = input.ToLower();

        // Check for greetings using logical OR (Chapter 7)
        if (lowerInput.Contains("hello") || lowerInput.Contains("hi") || lowerInput.Contains("hey"))
        {
            return "Hello there! How can I help you today?";
        }

        // Check for farewells
        if (lowerInput.Contains("bye") || lowerInput.Contains("exit") || lowerInput.Contains("quit"))
        {
            return "Goodbye! Have a great day.";
        }

        // Check for a specific command using String.Substring (Chapter 23)
        // We check if the input starts with "calculate"
        if (lowerInput.Length >= 9 && lowerInput.Substring(0, 9) == "calculate")
        {
            return "I can perform basic math if you ask me to add numbers.";
        }

        return "I didn't understand that.";
    }
}

Step 4: The Conversation Loop

To keep the chatbot running, we need a loop. A while loop (Chapter 8) is perfect for this. We will run the loop until the user explicitly types "exit".

We also need to capture user input using Console.ReadLine() (Chapter 5).

public class Program
{
    public static void Main()
    {
        // Initialize the engine
        ChatBotEngine bot = new ChatBotEngine();

        // Add rules using the class we defined earlier
        bot.AddRule(new ResponseRule("hello", "Hi there! Ready to chat?"));
        bot.AddRule(new ResponseRule("help", "I can answer basic questions. Try asking about the weather."));
        bot.AddRule(new ResponseRule("weather", "It's always sunny in the console!"));
        bot.AddRule(new ResponseRule("name", "I am a C# Rule-Based Bot."));

        Console.WriteLine("Chatbot initialized. Type 'exit' to stop.");

        // The main conversation loop (Chapter 8: while loop)
        while (true)
        {
            Console.Write("> "); // Prompt (Chapter 2)
            string input = Console.ReadLine(); // Read input (Chapter 5)

            // Check for exit condition
            if (input.ToLower() == "exit")
            {
                Console.WriteLine("Goodbye!");
                break; // Exit the loop (Chapter 10: Break)
            }

            // Get response from the engine
            string response = bot.GetResponse(input);

            // Output the response
            Console.WriteLine($"Bot: {response}"); // String Interpolation (Chapter 3)
        }
    }
}

Step 5: Advanced Pattern Matching with Arrays

While Contains is useful, sometimes we want to match exact words. We can split the user's sentence into an array of words and check if our keyword exists in that array.

This uses Chapter 11 (Arrays) and Chapter 23 (String.Split).

public class WordMatcher
{
    public static bool ContainsWord(string sentence, string targetWord)
    {
        // Split the sentence by spaces into an array of words
        string[] words = sentence.Split(' '); // Chapter 23: Split

        // Iterate through the array (Chapter 12: foreach)
        foreach (string word in words)
        {
            // Compare the current word to the target (Chapter 6: ==)
            if (word == targetWord)
            {
                return true;
            }
        }

        return false;
    }
}

Step 6: Handling Dynamic Data with Lists

A static bot is boring. Let's allow the user to teach the bot new responses. This requires a dynamic collection that can grow at runtime—perfect for List<T> (Chapter 20).

We will modify our ChatBotEngine to allow adding new rules via a specific command.

public class LearningBotEngine
{
    private List<ResponseRule> _rules = new List<ResponseRule>();

    public void Learn(string pattern, string response)
    {
        // Create a new rule and add it to the list
        ResponseRule newRule = new ResponseRule(pattern, response);
        _rules.Add(newRule); // Chapter 20: List.Add
        Console.WriteLine($"Learned new response for '{pattern}'.");
    }

    public string GetResponse(string userInput)
    {
        // Iterate using a for loop for index access (Chapter 9)
        // This allows us to potentially remove rules later if needed
        for (int i = 0; i < _rules.Count; i++)
        {
            if (userInput.Contains(_rules[i].Pattern))
            {
                return _rules[i].Response;
            }
        }
        return "I don't know that yet. Teach me?";
    }
}

Step 7: Structuring for Extensibility (File I/O)

To make the bot persistent (so it remembers what it learned even after closing), we need to save the rules to a file. We will use Chapter 24 (File I/O).

We will save rules as simple text lines: pattern|response.

Saving Rules:

using System.IO; // Required for File class

public static void SaveRulesToFile(List<ResponseRule> rules, string filePath)
{
    // Create a list of strings to write
    List<string> lines = new List<string>();

    foreach (ResponseRule rule in rules)
    {
        // Format: pattern|response
        string line = $"{rule.Pattern}|{rule.Response}";
        lines.Add(line);
    }

    // Write all lines to the file (Chapter 24)
    File.WriteAllLines(filePath, lines);
}

Loading Rules:

public static List<ResponseRule> LoadRulesFromFile(string filePath)
{
    List<ResponseRule> loadedRules = new List<ResponseRule>();

    // Check if file exists first to avoid errors
    if (File.Exists(filePath))
    {
        string[] lines = File.ReadAllLines(filePath);

        foreach (string line in lines)
        {
            // Split the line by the pipe character
            string[] parts = line.Split('|');

            // Ensure we have exactly 2 parts (pattern and response)
            if (parts.Length == 2)
            {
                loadedRules.Add(new ResponseRule(parts[0], parts[1]));
            }
        }
    }

    return loadedRules;
}

Step 8: Unit Testing the Logic

Testing ensures our bot behaves as expected. Since we cannot use advanced testing frameworks yet, we will write a simple testing method using Console.WriteLine to verify our logic.

We will test the WordMatcher and the ChatBotEngine.

public class BotTests
{
    public static void RunTests()
    {
        Console.WriteLine("--- Running Bot Tests ---");

        // Test 1: Word Matching
        bool test1 = WordMatcher.ContainsWord("Hello world", "world");
        Console.WriteLine($"Test 'world' in 'Hello world': {(test1 ? "PASS" : "FAIL")}");

        // Test 2: Case Sensitivity (Edge Case)
        bool test2 = WordMatcher.ContainsWord("Hello World", "world");
        Console.WriteLine($"Test 'world' in 'Hello World' (Case sensitive): {(test2 ? "FAIL (Expected)" : "PASS (Expected Failure)")}");

        // Test 3: Engine Response
        LearningBotEngine bot = new LearningBotEngine();
        bot.Learn("test", "This is a test response.");
        string response = bot.GetResponse("This is a test");
        Console.WriteLine($"Test Bot Response: {(response == "This is a test response." ? "PASS" : "FAIL")}");

        Console.WriteLine("--- Tests Complete ---");
    }
}

Summary of Concepts Used

  1. Classes (Ch 16): We defined ResponseRule, ChatBotEngine, and LearningBotEngine to encapsulate data and behavior.
  2. Lists (Ch 20): We used List<ResponseRule> to store a dynamic number of rules, allowing the bot to expand its knowledge.
  3. Loops (Ch 8, 9, 12): while for the conversation loop, for and foreach for iterating through rules and arrays.
  4. String Methods (Ch 23): We used Split to break sentences into words and Contains to find keywords.
  5. File I/O (Ch 24): We used File.WriteAllLines and File.ReadAllLines to persist bot knowledge.
  6. Conditional Logic (Ch 6, 7): We used if, else, and logical operators (||) to route user input to the correct response.

This architecture creates a modular chatbot. By separating the rule definition from the processing logic, you can easily extend the bot—perhaps by adding a MathRule that parses numbers and performs arithmetic, or a DateRule that returns the current time. The Strategy pattern approach ensures that adding a new capability is as simple as adding a new rule to the list.

Basic Code Example

Let's build the simplest possible foundation for our chatbot. Before we introduce complex patterns, we must understand the core engine: a loop that listens for user input and responds based on a simple rule.

We will simulate a "Customer Support Assistant" for a generic store. The goal is to handle one specific request: asking for the store's opening hours.

The Core Logic Example

Here is the complete, runnable C# code for a single-rule chatbot.

using System;

namespace SimpleChatbot
{
    class Program
    {
        // The Main method is the entry point of our application
        static void Main(string[] args)
        {
            // 1. Define the rule: A keyword and the intended response.
            // We use 'string' variables to hold text data.
            string targetKeyword = "hours";
            string response = "We are open from 9 AM to 5 PM, Monday to Friday.";

            // 2. Start an infinite loop to keep the chatbot running.
            // We use a 'while' loop that checks if 'true' (always runs).
            while (true)
            {
                // 3. Prompt the user and capture their input.
                Console.Write("User: ");
                string userInput = Console.ReadLine();

                // 4. Check if the user wants to exit.
                // We use the '!' (not) operator and '==' (equality).
                if (userInput == "exit")
                {
                    break; // Exit the loop
                }

                // 5. Analyze the input.
                // We use the 'Contains' method (allowed from String methods) to find keywords.
                // To be safe, we convert both to lowercase so "Hours" matches "hours".
                if (userInput.ToLower().Contains(targetKeyword))
                {
                    // 6. Output the response if the rule matches.
                    Console.WriteLine("Bot: " + response);
                }
                else
                {
                    // 7. Fallback response if no rule matches.
                    Console.WriteLine("Bot: I'm sorry, I don't understand. Ask about our hours.");
                }
            }
        }
    }
}

How It Works (Step-by-Step)

  1. Variable Initialization: We define targetKeyword and response. These act as the "brain" of our bot. By storing the response in a variable, we can easily change it later without rewriting the logic.
  2. The Infinite Loop: while (true) creates a cycle that repeats forever. This allows the user to ask multiple questions in a single session without restarting the program.
  3. Input Capture: Console.ReadLine() pauses the program and waits for the user to type something and press Enter. That text is stored in userInput.
  4. The Exit Condition: A loop that never stops is dangerous (you'd have to force-close the window). We check if (userInput == "exit"). If true, the break keyword immediately terminates the loop, ending the program gracefully.
  5. String Analysis: This is the "NLP" (Natural Language Processing) part. We use userInput.ToLower() to normalize the text (removing case sensitivity) and .Contains(targetKeyword) to scan for the word "hours". If the user types "What are your hours?", this check returns true.
  6. Conditional Output: Based on the if check, the bot prints a specific line of text to the console.

Visualizing the Logic Flow

This diagram shows how the program moves through the decision-making process.

Diagram: G
Hold "Ctrl" to enable pan & zoom

Common Pitfalls

1. Case Sensitivity A beginner might write: if (userInput == "hours").

  • The Problem: If the user types "Hours" (capital H) or "HOURS", the check fails. Computers are literal.
  • The Solution: Always normalize your inputs using .ToLower() or .ToUpper() before comparing, as shown in the example.

2. The Missing Break A beginner might forget the break statement inside the if (userInput == "exit") block.

  • The Problem: The user types "exit", but the program keeps running and asking for more input.
  • The Solution: Ensure every while (true) loop has a clearly defined "escape hatch" using break.

3. Variable Scope A beginner might try to define string response inside the while loop.

  • The Problem: While this works, it is inefficient. The variable is created and destroyed every single time the loop runs.
  • The Solution: Define variables that don't change (like the bot's response text) outside the loop, at the top of the Main method.

The chapter continues with advanced code, exercises and solutions with analysis, you can find them on the ebook on Leanpub.com or Amazon



Code License: All code examples are released under the MIT License. Github repo.

Content Copyright: Copyright © 2026 Edgar Milvus | Privacy & Cookie Policy. All rights reserved.

All textual explanations, original diagrams, and illustrations are the intellectual property of the author. To support the maintenance of this site via AdSense, please read this content exclusively online. Copying, redistribution, or reproduction is strictly prohibited.