lmjtfy.git / apps / lmjtfy / src / playground.rs
1//! The rules with facts you set by hand: `/rules?answerable=yes&kind=choice`.
2//! Pure, and it asks nobody anything: the facts come from the link.
3//!
4//! Every test on the diagram is a link that sets its fact to the value it
5//! tests, or clears it if it is already that. Every fact is a link that
6//! steps through what it can be: not known, then each value in turn. So the
7//! page holds no state; the link is the state.
8
9use rules::{Effect, End, Fact, Known, Network, Next, Note, Source, Test, Value, Want};
10
11use crate::diagram::Clicked;
12
13pub struct Play {
14    /// The facts the link set.
15    pub known: Known,
16    /// The rules that would have decided a step, in order.
17    pub fired: Vec<usize>,
18    /// What the rules say to do now.
19    pub next: Next,
20}
21
22/// The value a test makes its fact, when it holds.
23fn sets(test: Test) -> (Fact, Value) {
24    match test {
25        Test::Is(fact, value) => (fact, value),
26        Test::Known(fact) => (fact, Value::Given),
27    }
28}
29
30/// Reads the link's parameters. Only a value its fact can have is taken; the
31/// rest of the link is ignored.
32pub fn play(network: &Network, params: &[(String, String)]) -> Play {
33    let mut known = Known::default();
34    for (name, label) in params {
35        let set = Fact::ALL
36            .into_iter()
37            .filter(|fact| fact.name() == name)
38            .flat_map(|fact| fact.values().iter().map(move |value| (fact, *value)))
39            .find(|(_, value)| value.label() == label);
40        if let Some((fact, value)) = set {
41            known.learn(fact, value);
42        }
43    }
44    // The order the rules would have decided in: from what Jev says, each
45    // effect teaching its fact as the link says it went.
46    let mut so_far = Known::default();
47    for fact in Fact::ALL.into_iter().filter(|fact| fact.source() == Source::Jev) {
48        if let Some(value) = known.get(fact) {
49            so_far.learn(fact, value);
50        }
51    }
52    let mut fired = Vec::new();
53    loop {
54        let (by, next) = network.decide(&so_far);
55        fired.extend(by);
56        match next {
57            Next::Do(effect) => match known.get(effect.teaches()) {
58                Some(value) => so_far.learn(effect.teaches(), value),
59                None => break,
60            },
61            _ => break,
62        }
63    }
64    let next = network.next(&known);
65    Play { known, fired, next }
66}
67
68fn href(known: &Known) -> String {
69    let query: Vec<String> = Fact::ALL
70        .into_iter()
71        .filter_map(|fact| Some(format!("{}={}", fact.name(), known.get(fact)?.label())))
72        .collect();
73    if query.is_empty() { "/rules".to_owned() } else { format!("/rules?{}", query.join("&")) }
74}
75
76/// Where a click leads. A test sets its fact to what it tests, or clears the
77/// fact if it is that already. A fact steps to the next thing it can be, and
78/// from the last back to not known.
79pub fn link(known: &Known, clicked: Clicked) -> String {
80    let mut after = known.clone();
81    match clicked {
82        Clicked::Test(test) => {
83            let (fact, value) = sets(test);
84            if known.get(fact) == Some(value) {
85                after.forget(fact);
86            } else {
87                after.learn(fact, value);
88            }
89        }
90        Clicked::Fact(fact) => {
91            let values = fact.values();
92            let next = match known.get(fact) {
93                None => values.first(),
94                Some(now) => values.iter().skip_while(|value| **value != now).nth(1),
95            };
96            match next {
97                Some(value) => after.learn(fact, *value),
98                None => after.forget(fact),
99            }
100        }
101    }
102    href(&after)
103}
104
105/// What the rules say to do now, in words.
106pub fn says(network: &Network, play: &Play) -> String {
107    let said = match &play.next {
108        Next::Ask(facts) => {
109            let names: Vec<&str> = facts.iter().map(|fact| fact.name()).collect();
110            format!("Ask Jev for {}, in one request.", names.join(", "))
111        }
112        Next::Do(Effect::Draft(_)) => {
113            let wants: Vec<&str> = network
114                .wants(&play.known)
115                .into_iter()
116                .map(|want| match want {
117                    Want::Options => "the options",
118                    Want::Scale => "a scale",
119                    Want::Split => "each of the questions",
120                })
121                .collect();
122            format!("Ask the LLM to write {}, in one call. Set drafted to say how that went.", wants.join(" and "))
123        }
124        Next::Do(Effect::Judge) => "Ask Jev what the LLM wrote, in one request. Set judged to say how that went.".to_owned(),
125        Next::End(End::NotAQuestion) => "Done: Jev says no, it can't answer that.".to_owned(),
126        Next::Done => {
127            let shows: Vec<&str> = network
128                .shows(&play.known)
129                .into_iter()
130                .map(|fact| match fact {
131                    Fact::Yes => "Jev's yes or no",
132                    Fact::Degree => "Jev's degree",
133                    Fact::Chance => "Jev's probability",
134                    _ => "Jev's answers to what the LLM wrote",
135                })
136                .collect();
137            if shows.is_empty() { "Done: there is nothing to show.".to_owned() } else { format!("Done: show {}.", shows.join(" and ")) }
138        }
139    };
140    if network.notes(&play.known, Note::List) { format!("{said} If it is answered, it goes on the feed.") } else { said }
141}
142
143#[cfg(test)]
144mod tests {
145    use rules::Kind;
146
147    use super::*;
148
149    fn params(pairs: &[(&str, &str)]) -> Vec<(String, String)> {
150        pairs.iter().map(|(name, value)| ((*name).to_owned(), (*value).to_owned())).collect()
151    }
152
153    #[test]
154    fn an_empty_link_knows_nothing_and_asks_jev_for_everything() {
155        let network = Network::lmjtfy();
156        let play = play(&network, &[]);
157        assert_eq!(play.known, Known::default());
158        assert!(play.fired.is_empty());
159        assert!(says(&network, &play).starts_with("Ask Jev for answerable, several, yes-or-no, pick, how-much, how-likely, scale"));
160    }
161
162    #[test]
163    fn a_link_sets_only_what_a_rule_tests() {
164        let network = Network::lmjtfy();
165        let play = play(&network, &params(&[("pick", "yes"), ("fit", "purple"), ("nonsense", "yes"), ("yes", "known"), ("degree", "yes")]));
166        assert_eq!(play.known.get(Fact::Fit), None);
167        assert_eq!(play.known.get(Fact::Degree), None);
168        assert_eq!(play.known.get(Fact::Reads(Kind::Choice)), Some(Value::Bool(true)));
169        assert_eq!(play.known.get(Fact::Yes), Some(Value::Given));
170        assert_eq!(play.known.get(Fact::Answerable), None);
171    }
172
173    #[test]
174    fn the_whole_run_is_replayed_in_order() {
175        let network = Network::lmjtfy();
176        let all = params(&[
177            ("answerable", "yes"),
178            ("several", "no"),
179            ("yes-or-no", "no"),
180            ("pick", "yes"),
181            ("how-much", "no"),
182            ("how-likely", "no"),
183            ("scale", "no"),
184            ("yes", "known"),
185            ("degree", "known"),
186            ("chance", "known"),
187            ("fit", "no"),
188            ("drafted", "yes"),
189            ("judged", "yes"),
190        ]);
191        let play = play(&network, &all);
192        let names: Vec<&str> = play.fired.iter().map(|rule| network.terminals[*rule].name).collect();
193        assert_eq!(names, ["draft options", "judge the drafts"]);
194        assert_eq!(play.next, Next::Done);
195        assert_eq!(says(&network, &play), "Done: show Jev's answers to what the LLM wrote.");
196        // The same, fit to show: it would be listed.
197        let listed: Vec<(String, String)> = all.iter().filter(|(name, _)| name != "fit").cloned().chain(params(&[("fit", "yes")])).collect();
198        assert!(says(&network, &super::play(&network, &listed)).ends_with("it goes on the feed."));
199    }
200
201    #[test]
202    fn a_split_reading_shows_both_answers_with_no_llm() {
203        let network = Network::lmjtfy();
204        let both = params(&[
205            ("answerable", "yes"),
206            ("several", "no"),
207            ("yes-or-no", "yes"),
208            ("how-much", "yes"),
209            ("scale", "no"),
210            ("yes", "known"),
211            ("degree", "known"),
212        ]);
213        let play = play(&network, &both);
214        assert!(says(&network, &play).starts_with("Ask Jev for pick, how-likely, chance, fit"), "{}", says(&network, &play));
215        assert_eq!(network.shows(&play.known), [Fact::Yes, Fact::Degree]);
216    }
217
218    #[test]
219    fn a_test_links_to_its_fact_set_and_clears_it_when_it_is_set() {
220        let mut known = Known::default();
221        known.learn(Fact::Answerable, Value::Bool(true));
222        let pick = Test::Is(Fact::Reads(Kind::Choice), Value::Bool(true));
223        assert_eq!(link(&known, Clicked::Test(pick)), "/rules?answerable=yes&pick=yes");
224        assert_eq!(link(&known, Clicked::Test(Test::Is(Fact::Answerable, Value::Bool(false)))), "/rules?answerable=no");
225        assert_eq!(link(&known, Clicked::Test(Test::Is(Fact::Answerable, Value::Bool(true)))), "/rules");
226        assert_eq!(link(&known, Clicked::Test(Test::Known(Fact::Yes))), "/rules?answerable=yes&yes=known");
227    }
228
229    #[test]
230    fn a_fact_steps_through_what_it_can_be_and_back_to_not_known() {
231        let mut known = Known::default();
232        assert_eq!(link(&known, Clicked::Fact(Fact::Fit)), "/rules?fit=yes");
233        known.learn(Fact::Fit, Value::Bool(true));
234        assert_eq!(link(&known, Clicked::Fact(Fact::Fit)), "/rules?fit=no");
235        known.learn(Fact::Fit, Value::Bool(false));
236        assert_eq!(link(&known, Clicked::Fact(Fact::Fit)), "/rules");
237        assert_eq!(link(&Known::default(), Clicked::Fact(Fact::Yes)), "/rules?yes=known");
238    }
239}