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;