1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227
//! The script defines a set of rules which enable ELIZA to engage in discourse with a user. //! //! The beauty of ELIZA's design methodology means that the role of the programmer and playwright //! are separated. An important property of ELIZA is that a script is data - it is not part of the //! program itself. Hence, ELIZA is not restricted to a particular set of recognition patterns or //! responses, indeed not even to any specific language. //! //! ## Script Structure //! //! The script is written in `json` and is composed of the following. //! //! ```json,no_run //! { //! "greetings" : ["", ...], //! "farewells" : ["", ...], //! "fallbacks" : ["", ...], //! "transforms" : [ //! {"word": "", "equivalents": ["", ...]}, //! ... //! ], //! "synonyms" : [ //! {"word": "", "equivalents": ["", ...]}, //! ... //! ], //! "reflections" : [ //! {"word": "", "inverse": ["", ...], "twoway": bool}, //! ... //! ], //! "keywords" : [ //! { //! "key": "", "rank": number, //! "rules": [ //! { //! "memorise": bool, "decomposition_rule": rust_regex, //! "reassembly_rules": ["", ...] //! }, //! ... //! ] //! }, //! ... //! ] //! } //! ``` //! //! See struct documentation for more information on each element. //! use rand; use serde; use serde_json; use rand::seq::SliceRandom; use self::serde::de::Deserialize; use std::error::Error; use std::fs::File; use std::path::Path; /// A rule to transform a user's input prior to processing. /// /// # Example /// For example, if we had the `Transform` rule: /// /// ```json,no_run /// { "word" : "remember", "equivalents" : ["recollect", "recall"]} /// ``` /// Then the text `"I can't recollect, or even recall nowdays"` would be transformed to /// `"I can't remember, or even remember nowdays"` before performing a keyword search. /// #[derive(Serialize, Deserialize, Debug)] pub struct Transform { pub word: String, pub equivalents: Vec<String>, } /// A rule to aid the playwright in constructing simple decomposition rules. /// /// # Example /// For example, if we had the `Synonym` rule: /// /// ```json,no_run /// { "word" : "family", "equivalents" : ["mother","father","sister","brother"]} /// ``` /// Then the decomposition rule `"(.*)my (.*@family)(.*)"`, would be tried with the following /// perumtations: /// /// * `"(.*)my (.*family)(.*)"` /// * `"(.*)my (.*mother)(.*)"` /// * `"(.*)my (.*father)(.*)"` /// * `"(.*)my (.*sister)(.*)"` /// * `"(.*)my (.*brother)(.*)"` /// /// Note the special `@` symbol denotes that the word should be permutated. /// #[derive(Serialize, Deserialize, Debug)] pub struct Synonym { pub word: String, pub equivalents: Vec<String>, } /// A set of string pairs, used to post process any contextual information in an ELIZA /// response. /// /// # Example /// For example, if we had the `Reflection` rules: /// /// ```json,no_run /// { "word" : "your", "inverse" : "my", "twoway" : true}, /// { "word" : "i", "inverse" : "you", "twoway" : true} /// ``` /// * The reassembly rule: `"Really, $2?"` /// * The contextual information: `$2 = I think about my life` /// /// Then the assembled response would look like `"Really, you think about your life?"` /// #[derive(Serialize, Deserialize, Debug)] pub struct Reflection { pub word: String, pub inverse: String, pub twoway: bool, } /// A rule to decompose a user's input then assemble a response based on that input. /// /// * **memorise**: Used to indicate whether the response should be used now, or saved to /// internal memory for later use (true). /// * **decomposition_rule**: A rust regex used to match and extract contextual information from /// user input. /// * **reassembly_rules**: A list of strings that are to be used for ELIZA's reponse if the /// associated `decomposition_rule` matched. /// /// # Example /// For example, if we had the `Rule`: /// /// ```json,no_run /// { "memorise" : false, "decomposition_rule": "(.*)my(.+)", /// "reassembly_rules" : ["Really, $2?"]} /// ``` /// Then the input `"I think about my life"` would match and the assembled response would look like /// `"Really, life?"`. /// /// Note the special `$[num]` symbol denotes that a replacement with a regex capture group should /// occur. /// #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Rule { pub memorise: bool, pub decomposition_rule: String, pub reassembly_rules: Vec<String>, } /// A keyword and it's associated decompositon and reassembly rules. /// /// * **key**: The keyword to look for in the input text. /// * **rank**: Denotes it's importance over other keywords. Higher rank = Higher priority. /// * **rules**: The associated decompositon and reassembly rules /// #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Keyword { pub key: String, pub rank: u8, pub rules: Vec<Rule>, } /// A collection of ELIZA directives. /// /// * **greetings**: A set of strings that are used to greet the user upon program start /// * **farewells**: A set of strings that are used to farewell the user upon program termination /// * **fallbacks**: A set of strings that are used when ELIZA can't match any /// keywords/decompositon rules against user input /// * **transforms**: A set of rules to transform a user's input prior to processing. /// * **synonyms**: A set of synonyms to aid the playwright in constructing simple decomposition /// rules /// * **reflections**: A set of string pairs, that are used to post process any contextual /// information in an ELIZA response. /// * **keywords**: A set of keywords and their associated decompositon and reassembly rules. /// #[derive(Default, Serialize, Deserialize)] pub struct Script { pub greetings: Vec<String>, pub farewells: Vec<String>, pub fallbacks: Vec<String>, pub transforms: Vec<Transform>, pub synonyms: Vec<Synonym>, pub reflections: Vec<Reflection>, pub keywords: Vec<Keyword>, } impl Script { /// Will load an ELIZA json script from the file system. /// /// Will return `Err` if the script at the specified location is invalid or non-existant. pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Script, Box<dyn Error>> where for<'de> Script: Deserialize<'de>, { //Attempt to open file and parse the script let file = File::open(path)?; let script: Script = serde_json::from_reader(file)?; Ok(script) } pub fn from_str(val: &str) -> Result<Script, Box<dyn Error>> { let script: Script = serde_json::from_str(val)?; Ok(script) } /// Returns a random string from the `greetings` vector. /// /// Will return None if the vector is empty. pub fn rand_greet(&self) -> Option<&String> { self.greetings.choose(&mut rand::thread_rng()) } /// Returns a random string from the `farewell` vector. /// /// Will return None if the vector is empty. pub fn rand_farewell(&self) -> Option<&String> { self.farewells.choose(&mut rand::thread_rng()) // rand::thread_rng().choose(&self.farewells) } /// Returns a random string from the `fallback` vector. /// /// Will return None if the vector is empty. pub fn rand_fallback(&self) -> Option<&String> { self.fallbacks.choose(&mut rand::thread_rng()) } }