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