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