1//! Facts a rules engine wants, asked of Jev in one request. 2//! 3//! A rules engine that learns its facts from Jev does not ask one question 4//! per rule. At each step it knows every fact that some rule still alive is 5//! waiting on, and all of them go to Jev in one request: N questions about 6//! the same state, one round trip. This crate is that step, on Jev's side of 7//! it. It turns "these facts are wanted" into one request, and the response 8//! back into "these facts are now known". 9//! 10//! It knows nothing of the engine. A host says what its facts are with a 11//! [`Source`]: which question teaches a fact, and what an answer makes it. 12//! Whatever decides which facts are wanted (the `rete` crate's 13//! `Next::Ask(facts)`, or a host's own three lines) hands them to [`wanted`], 14//! sends what comes back however it sends things, and gives the response to 15//! [`Prepared::judged`] and [`learn`]. 16//! 17//! Nothing here does I/O, and nothing is async: preparing a request and 18//! reading a response are pure, so a page can show the request while Jev is 19//! still answering it, and a test needs no network. 20#![forbid(unsafe_code)] 21 22use jev_protocol::{ 23 ChoiceAnswer, Json, Key, ModelId, NoulAnswer, ProtocolError, Question, Questions, Response, ScoreAnswer, request_bytes, worst_case_dollars, 24}; 25 26/// What a host's facts are to Jev. 27/// 28/// Two facts may be taught by one question (three readings of one Choice, 29/// each a fact): they give the same [`Source::question_id`], the question is 30/// asked once, and each reads its own value out of the one answer. 31pub trait Source { 32 /// Something that can be known. Small and copied freely, as a rules 33 /// engine's facts are. 34 type Fact: Copy; 35 /// What a fact can turn out to be. 36 type Value; 37 38 /// The id of the question that teaches `fact`: its name in the request 39 /// and in the response. Facts that share a question share this. 40 fn question_id(&self, fact: Self::Fact) -> String; 41 42 /// The question itself. Called once per distinct id in a request. 43 fn question(&self, fact: Self::Fact) -> Result<Question, ProtocolError>; 44 45 /// What Jev's answer to that question makes `fact`. `None` when the 46 /// answer is not of the type the question was, which [`learn`] reports 47 /// and never guesses past. 48 fn learned(&self, fact: Self::Fact, judged: &Judged) -> Option<Self::Value>; 49} 50 51/// Jev's answer to one question, by the type that was asked. 52#[derive(Clone, Debug, PartialEq)] 53pub enum Judged { 54 /// The probability that the answer is yes. 55 Noul(f64), 56 Choice(ChoiceAnswer), 57 Score(ScoreAnswer), 58} 59 60/// The answer's key, typed by the question it belongs to. 61#[derive(Clone, Copy)] 62enum Asked { 63 Noul(Key<NoulAnswer>), 64 Choice(Key<ChoiceAnswer>), 65 Score(Key<ScoreAnswer>), 66} 67 68/// One question in a request. 69pub struct Part { 70 /// The question's id in the request, and wherever a host shows it. 71 pub id: String, 72 /// The Jev type asked: `noul`, `choice` or `score`. 73 pub kind: &'static str, 74 asked: Asked, 75} 76 77/// A call that is ready to send: one request, one or more questions. 78pub struct Prepared { 79 /// The questions, in the order they are asked. 80 pub parts: Vec<Part>, 81 /// The request body exactly as it will be sent. 82 pub request: String, 83 /// The most this call can cost, every retry billed 84 /// (`jev_protocol::worst_case_dollars`): what a budget holds before it 85 /// is sent. 86 pub worst_case_dollars: f64, 87 state: Json, 88 questions: Questions, 89} 90 91/// One request that asks every question in `asked` about `state`. Each item 92/// is a question's id and the question; an id used twice is the protocol's 93/// error, not a silent overwrite. 94pub fn prepare(model: &ModelId, state: Json, asked: impl IntoIterator<Item = (String, Question)>) -> Result<Prepared, ProtocolError> { 95 let mut questions = Questions::new(); 96 let mut parts = Vec::new(); 97 for (id, question) in asked { 98 let (kind, asked) = match question { 99 Question::Noul(noul) => ("noul", Asked::Noul(questions.noul(&id, noul)?)), 100 Question::Choice(choice) => ("choice", Asked::Choice(questions.choice(&id, choice)?)), 101 Question::Score(score) => ("score", Asked::Score(questions.score(&id, score)?)), 102 }; 103 parts.push(Part { id, kind, asked }); 104 } 105 let request = String::from_utf8(request_bytes(model, &state, &questions)?).map_err(|e| ProtocolError::Invalid(e.to_string()))?; 106 let worst_case_dollars = worst_case_dollars(model, &state, &questions); 107 Ok(Prepared { parts, request, worst_case_dollars, state, questions }) 108} 109 110/// One request that teaches every fact in `facts` about `state`: each 111/// fact's question, a question that several facts share asked once, in the 112/// order the facts first name it. 113pub fn wanted<S: Source>(source: &S, model: &ModelId, state: Json, facts: &[S::Fact]) -> Result<Prepared, ProtocolError> { 114 let mut asked: Vec<(String, Question)> = Vec::new(); 115 for &fact in facts { 116 let id = source.question_id(fact); 117 if asked.iter().all(|(known, _)| *known != id) { 118 asked.push((id, source.question(fact)?)); 119 } 120 } 121 prepare(model, state, asked) 122} 123 124impl Prepared { 125 /// The state and the questions, for a client that takes them and not the 126 /// body (`jev-http`'s `ask`). 127 pub fn asking(&self) -> (&Json, &Questions) { 128 (&self.state, &self.questions) 129 } 130 131 /// The answers in a response to this request, one per question, in the 132 /// order of [`Prepared::parts`]. 133 pub fn judged(&self, response: &Response) -> Vec<Judged> { 134 self.parts 135 .iter() 136 .map(|part| match part.asked { 137 Asked::Noul(key) => Judged::Noul(response.get(key).noul), 138 Asked::Choice(key) => Judged::Choice(response.get(key).clone()), 139 Asked::Score(key) => Judged::Score(response.get(key).clone()), 140 }) 141 .collect() 142 } 143} 144 145/// A fact that a response did not teach. 146#[derive(Clone, Debug, PartialEq, Eq)] 147pub enum Unlearned { 148 /// The request had no question with the fact's id: the facts given to 149 /// [`learn`] are not the ones the request was prepared for. 150 NotAsked { id: String }, 151 /// The answer to the fact's question is not of the type the question was. 152 WrongType { id: String }, 153} 154 155impl std::fmt::Display for Unlearned { 156 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 157 match self { 158 Unlearned::NotAsked { id } => write!(f, "the request did not ask {id}"), 159 Unlearned::WrongType { id } => write!(f, "Jev's answer for {id} is not the type that was asked"), 160 } 161 } 162} 163 164impl std::error::Error for Unlearned {} 165 166/// What `judged` (from [`Prepared::judged`]) makes each of `facts`: every 167/// fact with its value, in the order given, or the first fact that was not 168/// taught. All or nothing, so a host never records half an answer. 169pub fn learn<S: Source>(source: &S, prepared: &Prepared, judged: &[Judged], facts: &[S::Fact]) -> Result<Vec<(S::Fact, S::Value)>, Unlearned> { 170 facts 171 .iter() 172 .map(|&fact| { 173 let id = source.question_id(fact); 174 let answer = prepared.parts.iter().position(|part| part.id == id).and_then(|index| judged.get(index)); 175 match answer { 176 None => Err(Unlearned::NotAsked { id }), 177 Some(answer) => source.learned(fact, answer).map(|value| (fact, value)).ok_or(Unlearned::WrongType { id }), 178 } 179 }) 180 .collect() 181} 182 183#[cfg(test)] 184mod tests;