lib.rsannotatedlib.rssource184 lines · 7.4 KB · raw
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;