lmjtfy.git / packages / rules / src / lib.rs
lib.rsannotatedlib.rssource542 lines · 17.9 KB · raw
1//! How lmjtfy decides what to do with a query: rules over facts, compiled
2//! into a Rete network that the page draws.
3//!
4//! A fact is something known about the query: whether Jev can answer it,
5//! which kinds of question it reads as, whether it is several questions. A rule is a
6//! list of tests on facts and what to do when they all hold. The network is
7//! those rules with their shared tests merged: one alpha node per distinct
8//! test, and a chain of join nodes per rule, shared where rules begin alike.
9//!
10//! Most facts are Jev's to give: each is one typed question about the input.
11//! A rules engine would ask for them one at a time, as each rule came to
12//! need one. This one does not. [`Network::next`] collects every Jev fact
13//! that any rule still alive is waiting on, and asks for them together, so
14//! they go out as one request with N questions (the user, 2026-10-02: "roll
15//! up multiple rules that all use jev and parallelize them by firing a
16//! single request with N questions to backfill"). Asking Jev more in one
17//! request is what keeps the LLM, which costs far more, for the queries
18//! that need it.
19//!
20//! Nothing here does I/O. The Worker asks [`Network::next`] what to do, does
21//! it, records what it learned in [`Known`], and asks again.
22#![forbid(unsafe_code)]
23
24use std::collections::BTreeMap;
25
26use serde::{Deserialize, Serialize};
27
28/// A way to read an input as a question, which is also the Jev type that
29/// answers it.
30#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
31pub enum Kind {
32    /// Yes or no, or how likely.
33    Noul,
34    /// The best among several possibilities.
35    Choice,
36    /// A degree, an amount, a rating.
37    Score,
38    /// The chance of something: how likely, what are the odds. Jev answers
39    /// it as a yes-or-no about the thing itself, and the probability of yes
40    /// is the answer.
41    Chance,
42}
43
44impl Kind {
45    pub const ALL: [Kind; 4] = [Kind::Noul, Kind::Choice, Kind::Score, Kind::Chance];
46}
47
48/// Something known about a query.
49#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
50pub enum Fact {
51    /// Jev can judge the input at all.
52    Answerable,
53    /// It asks more than one separate question.
54    Several,
55    /// The input is to be read as this kind of question. Jev gives a
56    /// probability for each kind; the likeliest is read, and so is the
57    /// runner-up when Jev is split between them. So one, or two, hold.
58    Reads(Kind),
59    /// A how-much question that needs a scale written for it: a general
60    /// scale would answer it badly.
61    Scale,
62    /// The answer to the input, read as a yes-or-no question.
63    Yes,
64    /// The answer to the input, read as a how-much question, on a fixed
65    /// scale that needs nobody to write it.
66    Degree,
67    /// Whether the thing the input asks the chances of is so. The answer to
68    /// a how-likely question is this fact's probability.
69    Chance,
70    /// The input is fit to show to strangers on a public page.
71    Fit,
72    /// The input is a whole question, not one that stops part way, as it
73    /// does while it is still being typed.
74    Whole,
75    /// The LLM wrote at least one question Jev can take.
76    Drafted,
77    /// Jev answered at least one of the LLM's questions.
78    Judged,
79}
80
81/// Where a fact comes from.
82#[derive(Clone, Copy, Debug, PartialEq, Eq)]
83pub enum Source {
84    /// A Jev question about the input. Every such fact that is wanted is
85    /// asked for in the same request.
86    Jev,
87    /// The LLM, when a rule's effect asks it.
88    Llm,
89    /// Jev, when a rule's effect asks it what the LLM wrote.
90    JevAgain,
91}
92
93impl Fact {
94    pub const ALL: [Fact; 14] = [
95        Fact::Answerable,
96        Fact::Several,
97        Fact::Reads(Kind::Noul),
98        Fact::Reads(Kind::Choice),
99        Fact::Reads(Kind::Score),
100        Fact::Reads(Kind::Chance),
101        Fact::Scale,
102        Fact::Yes,
103        Fact::Degree,
104        Fact::Chance,
105        Fact::Fit,
106        Fact::Whole,
107        Fact::Drafted,
108        Fact::Judged,
109    ];
110
111    pub fn source(self) -> Source {
112        match self {
113            Fact::Drafted => Source::Llm,
114            Fact::Judged => Source::JevAgain,
115            _ => Source::Jev,
116        }
117    }
118
119    /// Everything the fact can turn out to be.
120    pub fn values(self) -> &'static [Value] {
121        match self {
122            Fact::Yes | Fact::Degree | Fact::Chance => &[Value::Given],
123            _ => &[Value::Bool(true), Value::Bool(false)],
124        }
125    }
126
127    /// The fact's name on the diagram and in a playground link.
128    pub fn name(self) -> &'static str {
129        match self {
130            Fact::Answerable => "answerable",
131            Fact::Several => "several",
132            Fact::Reads(Kind::Noul) => "yes-or-no",
133            Fact::Reads(Kind::Choice) => "pick",
134            Fact::Reads(Kind::Score) => "how-much",
135            Fact::Reads(Kind::Chance) => "how-likely",
136            Fact::Chance => "chance",
137            Fact::Scale => "scale",
138            Fact::Yes => "yes",
139            Fact::Degree => "degree",
140            Fact::Fit => "fit",
141            Fact::Whole => "whole",
142            Fact::Drafted => "drafted",
143            Fact::Judged => "judged",
144        }
145    }
146}
147
148/// What a fact turned out to be.
149#[derive(Clone, Copy, Debug, PartialEq, Eq)]
150pub enum Value {
151    Bool(bool),
152    /// Known, with a value no rule tests: the answer itself.
153    Given,
154}
155
156impl Value {
157    pub fn label(self) -> &'static str {
158        match self {
159            Value::Bool(true) => "yes",
160            Value::Bool(false) => "no",
161            Value::Given => "known",
162        }
163    }
164}
165
166/// One test on one fact: an alpha node.
167#[derive(Clone, Copy, Debug, PartialEq, Eq)]
168pub enum Test {
169    Is(Fact, Value),
170    /// The fact is known, whatever it is.
171    Known(Fact),
172}
173
174impl Test {
175    pub fn fact(self) -> Fact {
176        match self {
177            Test::Is(fact, _) | Test::Known(fact) => fact,
178        }
179    }
180
181    /// The test without its fact's name, as the diagram labels the node.
182    pub fn label(self) -> String {
183        match self {
184            Test::Is(_, value) => format!("= {}", value.label()),
185            Test::Known(_) => "is known".to_owned(),
186        }
187    }
188}
189
190/// What the LLM is asked to write.
191#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
192pub enum Want {
193    /// The options of a pick.
194    Options,
195    /// The levels of a how-much.
196    Scale,
197    /// Each of several questions, as whatever it is.
198    Split,
199}
200
201/// Something a rule does that costs a call and teaches a fact.
202#[derive(Clone, Copy, Debug, PartialEq, Eq)]
203pub enum Effect {
204    /// Ask the LLM to write Jev's questions. Teaches [`Fact::Drafted`].
205    /// Every drafting rule that holds is served by the one call
206    /// ([`Network::wants`]).
207    Draft(Want),
208    /// Ask Jev the questions the LLM wrote. Teaches [`Fact::Judged`].
209    Judge,
210}
211
212impl Effect {
213    pub fn teaches(self) -> Fact {
214        match self {
215            Effect::Draft(_) => Fact::Drafted,
216            Effect::Judge => Fact::Judged,
217        }
218    }
219}
220
221/// How a rule ends a query outright.
222#[derive(Clone, Copy, Debug, PartialEq, Eq)]
223pub enum End {
224    /// Jev cannot judge the input.
225    NotAQuestion,
226}
227
228/// Something that is so when a rule holds, and that decides no step: the
229/// Worker reads it once the query has ended.
230#[derive(Clone, Copy, Debug, PartialEq, Eq)]
231pub enum Note {
232    /// The question may go on the public feed.
233    List,
234    /// The input stops part way: the visitor is likely still typing it.
235    Unfinished,
236    /// This fact is an answer to show: Jev's own (`Yes`, `Degree`, `Chance`),
237    /// or its answers to what the LLM wrote (`Judged`).
238    Show(Fact),
239}
240
241#[derive(Clone, Copy, Debug, PartialEq, Eq)]
242pub enum Then {
243    Do(Effect),
244    End(End),
245    Note(Note),
246}
247
248pub struct Rule {
249    pub name: &'static str,
250    pub when: &'static [Test],
251    pub then: Then,
252}
253
254const YES: Value = Value::Bool(true);
255const NO: Value = Value::Bool(false);
256const ONE: [Test; 2] = [Test::Is(Fact::Answerable, YES), Test::Is(Fact::Several, NO)];
257
258/// lmjtfy's rules. Order is priority: of the rules that hold, the first with
259/// something left to do decides. When Jev is split between two readings,
260/// the rules of both hold, and both are answered.
261pub const RULES: &[Rule] = &[
262    Rule { name: "refuse", when: &[Test::Is(Fact::Answerable, NO)], then: Then::End(End::NotAQuestion) },
263    Rule {
264        name: "answer yes or no",
265        when: &[ONE[0], ONE[1], Test::Is(Fact::Reads(Kind::Noul), YES), Test::Known(Fact::Yes)],
266        then: Then::Note(Note::Show(Fact::Yes)),
267    },
268    Rule {
269        name: "grade it",
270        when: &[
271            ONE[0],
272            ONE[1],
273            Test::Is(Fact::Reads(Kind::Score), YES),
274            Test::Is(Fact::Scale, NO),
275            Test::Known(Fact::Degree),
276        ],
277        then: Then::Note(Note::Show(Fact::Degree)),
278    },
279    Rule {
280        name: "give the odds",
281        when: &[ONE[0], ONE[1], Test::Is(Fact::Reads(Kind::Chance), YES), Test::Known(Fact::Chance)],
282        then: Then::Note(Note::Show(Fact::Chance)),
283    },
284    Rule {
285        name: "draft a scale",
286        when: &[ONE[0], ONE[1], Test::Is(Fact::Reads(Kind::Score), YES), Test::Is(Fact::Scale, YES)],
287        then: Then::Do(Effect::Draft(Want::Scale)),
288    },
289    Rule {
290        name: "draft options",
291        when: &[ONE[0], ONE[1], Test::Is(Fact::Reads(Kind::Choice), YES)],
292        then: Then::Do(Effect::Draft(Want::Options)),
293    },
294    Rule {
295        name: "split it up",
296        when: &[Test::Is(Fact::Answerable, YES), Test::Is(Fact::Several, YES)],
297        then: Then::Do(Effect::Draft(Want::Split)),
298    },
299    Rule { name: "judge the drafts", when: &[Test::Is(Fact::Drafted, YES)], then: Then::Do(Effect::Judge) },
300    Rule {
301        name: "show the answers",
302        when: &[Test::Is(Fact::Judged, YES)],
303        then: Then::Note(Note::Show(Fact::Judged)),
304    },
305    Rule {
306        name: "list it",
307        when: &[Test::Is(Fact::Answerable, YES), Test::Is(Fact::Fit, YES)],
308        then: Then::Note(Note::List),
309    },
310    // It decides no step: a question sent as it stands is answered as it
311    // stands. The line under the box reads it while the visitor types.
312    Rule { name: "wait for the rest", when: &[Test::Is(Fact::Whole, NO)], then: Then::Note(Note::Unfinished) },
313];
314
315/// What is known about a query so far.
316#[derive(Clone, Debug, Default, PartialEq)]
317pub struct Known(BTreeMap<Fact, Value>);
318
319impl Known {
320    pub fn learn(&mut self, fact: Fact, value: Value) {
321        self.0.insert(fact, value);
322    }
323
324    pub fn get(&self, fact: Fact) -> Option<Value> {
325        self.0.get(&fact).copied()
326    }
327
328    pub fn forget(&mut self, fact: Fact) {
329        self.0.remove(&fact);
330    }
331}
332
333/// Where a node stands, given what is known.
334#[derive(Clone, Copy, Debug, PartialEq, Eq)]
335pub enum State {
336    /// Every test up to here holds.
337    Holds,
338    /// A test up to here is known to fail. Nothing downstream can fire.
339    Fails,
340    /// No test has failed, and at least one is on a fact not yet known.
341    Waits,
342}
343
344impl State {
345    fn and(self, other: State) -> State {
346        match (self, other) {
347            (State::Fails, _) | (_, State::Fails) => State::Fails,
348            (State::Holds, State::Holds) => State::Holds,
349            _ => State::Waits,
350        }
351    }
352}
353
354/// A join node: everything `left` requires, and one more test. `left` is
355/// another join, or `None` when this is a rule's first test.
356#[derive(Clone, Copy, Debug, PartialEq, Eq)]
357pub struct Join {
358    pub left: Option<usize>,
359    /// Index into [`Network::alphas`].
360    pub alpha: usize,
361    /// How many tests are joined here: 1 for a rule's first.
362    pub depth: usize,
363}
364
365/// A rule's place in the network: the join that holds when the rule does.
366#[derive(Clone, Copy, Debug)]
367pub struct Terminal {
368    pub name: &'static str,
369    pub join: usize,
370    pub then: Then,
371}
372
373/// The rules as a Rete network.
374#[derive(Clone, Debug)]
375pub struct Network {
376    /// One per distinct test, however many rules use it.
377    pub alphas: Vec<Test>,
378    /// Shared by every rule that begins with the same tests.
379    pub joins: Vec<Join>,
380    pub terminals: Vec<Terminal>,
381}
382
383/// What to do next about a query.
384#[derive(Clone, Debug, PartialEq, Eq)]
385pub enum Next {
386    /// Ask Jev for all of these, in one request.
387    Ask(Vec<Fact>),
388    Do(Effect),
389    /// A rule ends the query outright.
390    End(End),
391    /// No rule has anything left to do and nothing more can be learned:
392    /// the query is over, and what to show is in the notes.
393    Done,
394}
395
396impl Network {
397    pub fn compile(rules: &[Rule]) -> Self {
398        let mut network = Network { alphas: Vec::new(), joins: Vec::new(), terminals: Vec::new() };
399        for rule in rules {
400            let mut left = None;
401            for (depth, test) in rule.when.iter().enumerate() {
402                let alpha = network.alphas.iter().position(|known| known == test).unwrap_or_else(|| {
403                    network.alphas.push(*test);
404                    network.alphas.len() - 1
405                });
406                let join = Join { left, alpha, depth: depth + 1 };
407                left = Some(network.joins.iter().position(|known| *known == join).unwrap_or_else(|| {
408                    network.joins.push(join);
409                    network.joins.len() - 1
410                }));
411            }
412            let join = left.expect("a rule has at least one test");
413            network.terminals.push(Terminal { name: rule.name, join, then: rule.then });
414        }
415        network
416    }
417
418    /// lmjtfy's own network.
419    pub fn lmjtfy() -> Self {
420        Network::compile(RULES)
421    }
422
423    pub fn alpha(&self, alpha: usize, known: &Known) -> State {
424        let test = self.alphas[alpha];
425        match (test, known.get(test.fact())) {
426            (_, None) => State::Waits,
427            (Test::Known(_), Some(_)) => State::Holds,
428            (Test::Is(_, wanted), Some(value)) if wanted == value => State::Holds,
429            (Test::Is(..), Some(_)) => State::Fails,
430        }
431    }
432
433    pub fn join(&self, join: usize, known: &Known) -> State {
434        let Join { left, alpha, .. } = self.joins[join];
435        let here = self.alpha(alpha, known);
436        left.map_or(here, |left| self.join(left, known).and(here))
437    }
438
439    /// The facts a join's tests are on, first test first.
440    fn facts(&self, join: usize) -> Vec<Fact> {
441        let Join { left, alpha, .. } = self.joins[join];
442        let mut facts = left.map_or_else(Vec::new, |left| self.facts(left));
443        facts.push(self.alphas[alpha].fact());
444        facts
445    }
446
447    /// What to do, given what is known, and the rule that says so (its index
448    /// in [`Network::terminals`]), when one does.
449    ///
450    /// A rule that holds decides, in rule order; an effect whose fact is
451    /// already known has been done and is passed over. If no rule decides,
452    /// every Jev fact that a rule not yet failed is waiting on is wanted,
453    /// and they are asked for together.
454    pub fn decide(&self, known: &Known) -> (Option<usize>, Next) {
455        for (index, terminal) in self.terminals.iter().enumerate() {
456            if self.join(terminal.join, known) != State::Holds {
457                continue;
458            }
459            match terminal.then {
460                Then::End(end) => return (Some(index), Next::End(end)),
461                Then::Do(effect) if known.get(effect.teaches()).is_none() => return (Some(index), Next::Do(effect)),
462                Then::Do(_) | Then::Note(_) => {}
463            }
464        }
465        let mut wanted: Vec<Fact> = self
466            .terminals
467            .iter()
468            .filter(|terminal| self.join(terminal.join, known) == State::Waits)
469            .flat_map(|terminal| self.facts(terminal.join))
470            .filter(|fact| fact.source() == Source::Jev && known.get(*fact).is_none())
471            .collect();
472        wanted.sort();
473        wanted.dedup();
474        (None, if wanted.is_empty() { Next::Done } else { Next::Ask(wanted) })
475    }
476
477    /// What to do, given what is known.
478    pub fn next(&self, known: &Known) -> Next {
479        self.decide(known).1
480    }
481
482    fn held<'a>(&'a self, known: &'a Known) -> impl Iterator<Item = &'a Terminal> {
483        self.terminals.iter().filter(move |terminal| self.join(terminal.join, known) == State::Holds)
484    }
485
486    /// The names of the rules that hold, in rule order.
487    pub fn holding(&self, known: &Known) -> Vec<&'static str> {
488        self.held(known).map(|terminal| terminal.name).collect()
489    }
490
491    /// Whether a rule that holds says `note`.
492    pub fn notes(&self, known: &Known, note: Note) -> bool {
493        self.held(known).any(|terminal| terminal.then == Then::Note(note))
494    }
495
496    /// The answers to show, in rule order.
497    pub fn shows(&self, known: &Known) -> Vec<Fact> {
498        self.held(known)
499            .filter_map(|terminal| match terminal.then {
500                Then::Note(Note::Show(fact)) => Some(fact),
501                _ => None,
502            })
503            .collect()
504    }
505
506    /// Everything the LLM is to write, for every drafting rule that holds:
507    /// one call serves them all.
508    pub fn wants(&self, known: &Known) -> Vec<Want> {
509        let mut wants: Vec<Want> = self
510            .held(known)
511            .filter_map(|terminal| match terminal.then {
512                Then::Do(Effect::Draft(want)) => Some(want),
513                _ => None,
514            })
515            .collect();
516        wants.sort();
517        wants.dedup();
518        wants
519    }
520
521    /// The joins and alphas that a rule that holds stands on: what, of
522    /// everything known, turned out to matter.
523    pub fn used(&self, known: &Known) -> (Vec<bool>, Vec<bool>) {
524        let mut joins = vec![false; self.joins.len()];
525        let mut alphas = vec![false; self.alphas.len()];
526        for terminal in &self.terminals {
527            if self.join(terminal.join, known) != State::Holds {
528                continue;
529            }
530            let mut at = Some(terminal.join);
531            while let Some(join) = at {
532                joins[join] = true;
533                alphas[self.joins[join].alpha] = true;
534                at = self.joins[join].left;
535            }
536        }
537        (joins, alphas)
538    }
539}
540
541#[cfg(test)]
542mod tests;