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 (https://retrocoders.phatcode.net/index.php?topic=840.0)