News:

Welcome to RetroCoders Community

Main Menu

Danny chatbot in Rust

Started by ron77, Dec 27, 2024, 01:54 PM

Previous topic - Next topic

ron77

use std::fs::File;
use std::io::{self, BufRead, BufReader, Write};
use std::process::Command;
use std::collections::HashMap;
use rand::prelude::*;
use anyhow::{Result, Context as _};

// const MAX_STRING_LENGTH: usize = 1000;
// const MAX_KEYWORDS: usize = 20;
// const MAX_REPLIES: usize = 50;
const MAX_MEMORY: usize = 15;

#[derive(Default)]
struct ArraySet {
    keywords: Vec<String>,
    replies: Vec<String>,
}

#[derive(Default)]
struct ChatBot {
    key_reply: Vec<ArraySet>,
    default_replies: Vec<String>,
    memory: Vec<String>,
    word_swaps: HashMap<String, String>,
    rng: ThreadRng,
    single_line_mode: bool,
}

impl ChatBot {
    fn new() -> Self {
        Self {
            key_reply: Vec::new(),
            default_replies: Vec::new(),
            memory: Vec::with_capacity(MAX_MEMORY),
            word_swaps: HashMap::new(),
            rng: thread_rng(),
            single_line_mode: true,
        }
    }

    fn load_arrays(&mut self, filename: &str) -> Result<()> {
        let file = File::open(filename)
            .with_context(|| format!("Failed to open file: {}", filename))?;
        let reader = BufReader::new(file);
        let mut current_set: Option<ArraySet> = None;

        for line in reader.lines() {
            let line = line?;
            if line.starts_with("d1:") {
                self.default_replies.push(line[3..].to_string());
            } else if line.starts_with("k:") {
                if current_set.is_some() && !current_set.as_ref().unwrap().replies.is_empty() {
                    if let Some(set) = current_set.take() {
                        self.key_reply.push(set);
                    }
                }
                
                if current_set.is_none() {
                    current_set = Some(ArraySet::default());
                }
                
                if let Some(set) = &mut current_set {
                    set.keywords.push(line[2..].to_string());
                }
            } else if line.starts_with("r:") {
                if let Some(set) = &mut current_set {
                    set.replies.push(line[2..].to_string());
                }
            }
        }

        if let Some(set) = current_set {
            self.key_reply.push(set);
        }

        println!("Number of keywords-replies pair groups: {}", self.key_reply.len());
        Ok(())
    }

    fn load_swap_words(&mut self, filename: &str) -> Result<()> {
        let file = File::open(filename)
            .with_context(|| format!("Failed to open swap words file: {}", filename))?;
        let reader = BufReader::new(file);

        for line in reader.lines() {
            let line = line?;
            if line.starts_with("s:") {
                if let Some(pos) = line[2..].find('>') {
                    let (word_in, word_out) = line[2..].split_at(pos);
                    self.word_swaps.insert(
                        word_in.to_string(),
                        word_out[1..].to_string()
                    );
                }
            }
        }
        Ok(())
    }

    fn swap_words(&self, input: &str) -> String {
        input
            .split_whitespace()
            .map(|word| {
                self.word_swaps
                    .get(word)
                    .map_or(word.to_string(), |w| w.clone())
            })
            .collect::<Vec<_>>()
            .join(" ")
    }

    fn isolate_punctuation(s: &str) -> String {
        s.chars()
            .map(|c| {
                if "?!,.:;<>(){}[]".contains(c) {
                    format!(" {} ", c)
                } else {
                    c.to_string()
                }
            })
            .collect()
    }

    fn process_reply(&mut self, reply: &str, user_input: &str) -> String {
        if !reply.ends_with('*') {
            return reply.to_string();
        }

        let reply = &reply[..reply.len() - 1];
        
        if self.memory.len() < MAX_MEMORY {
            self.memory.push(user_input.to_string());
        }
        
        for set in &self.key_reply {
            for keyword in &set.keywords {
                if user_input.contains(keyword.as_str()) {
                    if let Some(rest) = user_input.split(keyword.as_str()).nth(1) {
                        let swapped = self.swap_words(rest);
                        return format!("{} {}", reply, swapped);
                    }
                }
            }
        }
        
        reply.to_string()
    }

    fn user_question(&mut self, txt: &str) -> String {
        let mut matched_replies = Vec::new();
        let mut found = false;

        for set in &self.key_reply {
            for keyword in &set.keywords {
                if txt.contains(keyword.as_str()) {
                    if let Some(reply) = set.replies.choose(&mut self.rng).cloned() {
                        matched_replies.push(reply);
                        found = true;
                        break;
                    }
                }
            }
        }

        let processed_replies: Vec<String> = matched_replies.into_iter()
            .map(|reply| self.process_reply(&reply, txt))
            .collect();

        if !processed_replies.is_empty() {
            if self.single_line_mode {
                processed_replies.join(" | ")
            } else {
                processed_replies.join("\n")
            }
        } else if !found && !self.default_replies.is_empty() {
            self.default_replies
                .choose(&mut self.rng)
                .cloned()
                .unwrap_or_else(|| "I don't understand. Can you rephrase that?".to_string())
        } else {
            "I don't understand. Can you rephrase that?".to_string()
        }
    }

    fn commands(&mut self, txt: &str) -> String {
        if txt.starts_with("/") {
            match txt {
                "/single" => {
                    self.single_line_mode = true;
                    return "Switched to single-line mode.".to_string();
                }
                "/multi" => {
                    self.single_line_mode = false;
                    return "Switched to multi-line mode.".to_string();
                }
                _ => {}
            }
        }

        let isolated = Self::isolate_punctuation(txt);
        let lowercase = isolated.to_lowercase();
        self.user_question(&lowercase)
    }

    fn speak(&self, text: &str) -> Result<()> {
        Command::new("voice")
            .args(["-r", "-1", "-n", "Microsoft David Desktop", text])
            .spawn()
            .with_context(|| "Failed to start TTS process")?
            .wait()
            .with_context(|| "Failed to wait for TTS process")?;
        Ok(())
    }
}

fn main() -> Result<()> {
    println!("CHATBOT DANNY IN RUST VERSION 1.0.0");
    println!("Commands:");
    println!("  /single - Switch to single-line mode");
    println!("  /multi  - Switch to multi-line mode");
    println!("  quit    - Exit the program");
    
    let mut chatbot = ChatBot::new();
    chatbot.load_arrays("database.txt")?;
    chatbot.load_swap_words("swapwords.txt")?;

    let stdin = io::stdin();
    let mut stdout = io::stdout();
    
    loop {
        print!("> ");
        stdout.flush()?;
        
        let mut input = String::new();
        stdin.lock().read_line(&mut input)?;
        let input = input.trim();
        
        if input == "quit" {
            break;
        }
        
        let response = chatbot.commands(input);
        
        if chatbot.single_line_mode {
            println!("{}", response);
            chatbot.speak(&response)?;
        } else {
            for line in response.lines() {
                println!("{}", line);
                chatbot.speak(line)?;
            }
        }
    }

    Ok(())
}

https://retrocoders.phatcode.net/index.php?topic=840.0