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}