lib.rsannotatedlib.rssource184 lines · 7.4 KB · raw

Facts a rules engine wants, asked of Jev in one request.

A rules engine that learns its facts from Jev does not ask one question per rule. At each step it knows every fact that some rule still alive is waiting on, and all of them go to Jev in one request: N questions about the same state, one round trip. This crate is that step, on Jev's side of it. It turns "these facts are wanted" into one request, and the response back into "these facts are now known".

It knows nothing of the engine. A host says what its facts are with a [Source]: which question teaches a fact, and what an answer makes it. Whatever decides which facts are wanted (the rete crate's Next::Ask(facts), or a host's own three lines) hands them to [wanted], sends what comes back however it sends things, and gives the response to [Prepared::judged] and [learn].

Nothing here does I/O, and nothing is async: preparing a request and reading a response are pure, so a page can show the request while Jev is still answering it, and a test needs no network.

20#![forbid(unsafe_code)]
22use jev_protocol::{
23    ChoiceAnswer, Json, Key, ModelId, NoulAnswer, ProtocolError, Question, Questions, Response, ScoreAnswer, request_bytes, worst_case_dollars,
24};

What a host's facts are to Jev.

Two facts may be taught by one question (three readings of one Choice, each a fact): they give the same [Source::question_id], the question is asked once, and each reads its own value out of the one answer.

31pub trait Source {

Something that can be known. Small and copied freely, as a rules engine's facts are.

34    type Fact: Copy;

What a fact can turn out to be.

36    type Value;

The id of the question that teaches fact: its name in the request and in the response. Facts that share a question share this.

40    fn question_id(&self, fact: Self::Fact) -> String;

The question itself. Called once per distinct id in a request.

43    fn question(&self, fact: Self::Fact) -> Result<Question, ProtocolError>;

What Jev's answer to that question makes fact. None when the answer is not of the type the question was, which [learn] reports and never guesses past.

48    fn learned(&self, fact: Self::Fact, judged: &Judged) -> Option<Self::Value>;
49}

Jev's answer to one question, by the type that was asked.

52#[derive(Clone, Debug, PartialEq)]
53pub enum Judged {

The probability that the answer is yes.

55    Noul(f64),
56    Choice(ChoiceAnswer),
57    Score(ScoreAnswer),
58}

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}

One question in a request.

69pub struct Part {

The question's id in the request, and wherever a host shows it.

71    pub id: String,

The Jev type asked: noul, choice or score.

73    pub kind: &'static str,
74    asked: Asked,
75}

A call that is ready to send: one request, one or more questions.

78pub struct Prepared {

The questions, in the order they are asked.

80    pub parts: Vec<Part>,

The request body exactly as it will be sent.

82    pub request: String,

The most this call can cost, every retry billed (jev_protocol::worst_case_dollars): what a budget holds before it is sent.

86    pub worst_case_dollars: f64,
87    state: Json,
88    questions: Questions,
89}

One request that asks every question in asked about state. Each item is a question's id and the question; an id used twice is the protocol's 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}

One request that teaches every fact in facts about state: each fact's question, a question that several facts share asked once, in the 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}
124impl Prepared {

The state and the questions, for a client that takes them and not the body (jev-http's ask).

127    pub fn asking(&self) -> (&Json, &Questions) {
128        (&self.state, &self.questions)
129    }

The answers in a response to this request, one per question, in the 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}

A fact that a response did not teach.

146#[derive(Clone, Debug, PartialEq, Eq)]
147pub enum Unlearned {

The request had no question with the fact's id: the facts given to [learn] are not the ones the request was prepared for.

150    NotAsked { id: String },

The answer to the fact's question is not of the type the question was.

152    WrongType { id: String },
153}
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 {}

What judged (from [Prepared::judged]) makes each of facts: every fact with its value, in the order given, or the first fact that was not 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}
183#[cfg(test)]
184mod tests;