lmjtfy.git / packages / ask / src / lib.rs
lib.rsannotatedlib.rssource463 lines · 18.1 KB · raw

What lmjtfy asks Jev, and the record each call leaves for the page.

There are two requests. The first asks for facts about the input, as many as the rules want at once ([rules::Fact]): can Jev judge it, what kind of question is it, is it several, does it need a scale written for it, is it fit to show publicly, and the answer if it is a yes-or-no question, a how-much one, or a how-likely one. The second asks the questions the LLM wrote ([llm::Draft]), all in one request. Every question is asked about the same state, the visitor's input.

A call is prepared before it is sent, so the page can show the request while Jev is still answering it, and [send] returns the response. Nothing here does I/O: it runs on whatever Transport and Runtime the Client was built with.

15#![forbid(unsafe_code)]
17use std::time::Duration;
18
19use jev_client::{Client, Observer, Runtime, Transport};
20use jev_protocol::{
21    Choice, ChoiceAnswer, Json, Key, ModelId, Noul, NoulAnswer, ProtocolError, Question, Questions, Score,
22    Response, ScoreAnswer, Usage, request_bytes, worst_case_dollars,
23};
24use llm::Draft;
25use rules::{Fact, Kind, Value};
26use serde::{Deserialize, Serialize};

The longest input sent on. A search box, not a document.

29pub const MAX_INPUT_CHARS: usize = 500;

At or above this, a Noul reads as yes.

32pub const THRESHOLD: f64 = 0.5;

Whether a Noul's probability reads as yes.

35pub fn is_yes(p_yes: f64) -> bool {
36    p_yes >= THRESHOLD
37}

A Noul is sure when the likelier answer has at least this much. Below it the page ends the answer with a question mark instead of a full stop (the user, 2026-10-02: "is AI a bit stupid?" came back 0.55, "he's not that confident").

43pub const SURE: f64 = 0.7;

The confidence a Score needs to be stated flatly. A first guess, like SURE: below it Jev says the levels are ambiguous (TypeSafe's docs), and the headline asks instead of stating.

48pub const CONFIDENT: f64 = 0.5;

Whether a Score's confidence is high enough to state its level flatly.

51pub fn is_confident(confidence: f64) -> bool {
52    confidence >= CONFIDENT
53}

Whether a Noul's probability is far enough from even to state flatly.

56pub fn is_sure(p_yes: f64) -> bool {
57    p_yes.max(1.0 - p_yes) >= SURE
58}

A probability in a word or two, for a how-likely answer: the number is the answer, and this is how to read it.

62pub fn likelihood(p: f64) -> &'static str {
63    match p {
64        p if p < 0.1 => "very unlikely",
65        p if p < 0.4 => "unlikely",
66        p if p <= 0.6 => "a toss-up",
67        p if p <= 0.9 => "likely",
68        _ => "very likely",
69    }
70}

The visitor's text as Jev sees it, trimmed and capped.

73pub fn clean(input: &str) -> String {
74    input.trim().chars().take(MAX_INPUT_CHARS).collect()
75}

The labels of the kind question's options, and the [Kind] each means.

78const KINDS: [(&str, Kind, &str); 4] = [
79    ("yes_or_no", Kind::Noul, "It asks whether something is so. A yes or a no answers it."),
80    (
81        "pick_one",
82        Kind::Choice,
83        "It asks which, who or what: the best or right one among several possibilities. Naming one answers it.",
84    ),
85    (
86        "how_much",
87        Kind::Score,
88        "It asks for a degree, an amount or a rating. A position on a scale answers it.",
89    ),
90    (
91        "how_likely",
92        Kind::Chance,
93        "It asks for the chance, the odds or the probability of something. A percentage answers it.",
94    ),
95];

Jev's runner-up reading is taken too when it has at least this much: Jev is split, and both readings are answered (the user, 2026-10-02: at 90% do that one; at 40 / 40 / 20, do the top two).

100pub const ALSO: f64 = 0.3;

Jev's probability that the input is this kind of question.

103pub fn kind_probability(answer: &ChoiceAnswer, kind: Kind) -> Option<f64> {
104    let (label, ..) = KINDS.iter().find(|(_, known, _)| *known == kind)?;
105    answer.probabilities.iter().find(|(option, _)| option == label).map(|(_, p)| *p)
106}

The kinds the input is read as, likeliest first: the top one, and the runner-up if Jev gave it at least [ALSO].

110pub fn readings(answer: &ChoiceAnswer) -> Vec<Kind> {
111    let mut ranked: Vec<(Kind, f64)> =
112        Kind::ALL.into_iter().filter_map(|kind| Some((kind, kind_probability(answer, kind)?))).collect();
113    ranked.sort_by(|a, b| b.1.total_cmp(&a.1));
114    ranked.iter().enumerate().filter(|(rank, (_, p))| *rank == 0 || (*rank == 1 && *p >= ALSO)).map(|(_, (kind, _))| *kind).collect()
115}

The id of the question that teaches a fact. The three readings are one question, answered with a probability for each.

119pub fn question_id(fact: Fact) -> &'static str {
120    match fact {
121        Fact::Reads(_) => "kind",
122        fact => fact.name(),
123    }
124}

The scale a how-much question is answered on when nobody writes one for it, lowest first. Fixed, so Jev can be asked without an LLM (the user, 2026-10-02, knowing it gives up levels written for the question).

129pub const DEGREES: [&str; 5] = ["Not at all", "Slightly", "Moderately", "Very", "Extremely"];

The question that teaches a fact. Only facts whose source is Jev have one.

132fn fact_question(fact: Fact) -> Result<Question, ProtocolError> {
133    Ok(match fact {
134        Fact::Answerable => Question::Noul(
135            Noul::new(Json::text("Is the input a question you can answer with your types?"))
136                .yes_means(Json::text(
137                    "The input asks something that can be judged: a yes-or-no question, a question with a best \
138                     answer among options, or a question of degree.",
139                ))
140                .no_means(Json::text(
141                    "The input is anything else: a greeting, a statement, a command, or a request to write, \
142                     explain, summarise or do something.",
143                )),
144        ),
145        Fact::Reads(_) => Question::Choice(Choice::new(
146            Json::text("What kind of question is the input?"),
147            KINDS.iter().map(|(label, _, means)| ((*label).to_owned(), Some(Json::text(means)))),
148        )?),
149        Fact::Several => Question::Noul(
150            Noul::new(Json::text("Does the input ask more than one separate question?"))
151                .yes_means(Json::text("It asks two or more separate questions, each needing its own answer."))
152                .no_means(Json::text("It asks one question, even if that question mentions several things.")),
153        ),
154        Fact::Yes => Question::Noul(
155            Noul::new(Json::text("Answer the question in the input."))
156                .yes_means(Json::text("The answer to the question in the input is yes."))
157                .no_means(Json::text("The answer to the question in the input is no.")),
158        ),
159        Fact::Degree => Question::Score(Score::new(
160            Json::text("Answer the question in the input as a degree: how much, how good, how strong, how likely."),
161            DEGREES.iter().map(|level| Json::text(level)),
162        )?),
163        Fact::Scale => Question::Noul(
164            Noul::new(Json::text("Would the input be answered badly on a general scale from 'Not at all' to 'Extremely'?"))
165                .yes_means(Json::text(
166                    "Yes. What it asks about has its own units, ranges, grades or named levels, and a good \
167                     answer would use them.",
168                ))
169                .no_means(Json::text(
170                    "No. A general scale answers it well enough, or it is not a how-much question at all.",
171                )),
172        ),
173        Fact::Chance => Question::Noul(
174            Noul::new(Json::text("Is the thing the input asks the chances of so?"))
175                .yes_means(Json::text("It is so, or it will happen."))
176                .no_means(Json::text("It is not so, or it will not happen.")),
177        ),
178        Fact::Fit => Question::Noul(
179            Noul::new(Json::text("Is the input fit to show to strangers on a public page?"))
180                .yes_means(Json::text("It is harmless to show anyone."))
181                .no_means(Json::text(
182                    "It has a slur, harassment, a threat, sexual content, or a private person's name or \
183                     personal details.",
184                )),
185        ),
186        Fact::Whole => Question::Noul(
187            Noul::new(Json::text("Is the input finished, or was it cut off part way through being typed?"))
188                .yes_means(Json::text(
189                    "Finished. Its last word is a whole word, and nothing it needs is missing after it. A \
190                     greeting, a statement or a few words can be finished; it need not be a question.",
191                ))
192                .no_means(Json::text(
193                    "Cut off. It ends in part of a word, or on a word that needs more after it, such as a, \
194                     the, to, of, or, and, is.",
195                )),
196        ),
197        Fact::Drafted | Fact::Judged => {
198            return Err(ProtocolError::Invalid(format!("{} is not a fact Jev is asked for", fact.name())));
199        }
200    })
201}
203fn drafted_question(draft: &Draft) -> Result<Question, ProtocolError> {
204    Ok(match draft {
205        Draft::Noul { instructions, yes_means, no_means } => Question::Noul(
206            Noul::new(Json::text(instructions)).yes_means(Json::text(yes_means)).no_means(Json::text(no_means)),
207        ),
208        Draft::Choice { instructions, options } => Question::Choice(Choice::new(
209            Json::text(instructions),
210            options.iter().map(|option| (option.label.clone(), Some(Json::text(&option.description)))),
211        )?),
212        Draft::Score { instructions, levels } => {
213            Question::Score(Score::new(Json::text(instructions), levels.iter().map(|level| Json::text(level)))?)
214        }
215    })
216}

Whether Jev's own protocol takes a question the LLM wrote. One that it refuses is left out of the request, and the page says why.

220pub fn check(draft: &Draft) -> Result<(), ProtocolError> {
221    drafted_question(draft).map(|_| ())
222}

The answer's key, typed by the question it belongs to.

225#[derive(Clone, Copy)]
226enum Asked {
227    Noul(Key<NoulAnswer>),
228    Choice(Key<ChoiceAnswer>),
229    Score(Key<ScoreAnswer>),
230}

One question in a request.

233pub struct Part {

The question's id in the request and on the page.

235    pub id: String,

The Jev type asked: noul, choice or score.

237    pub kind: &'static str,
238    asked: Asked,
239}

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

242pub struct Prepared {

What the call is for, on the page: facts or answers.

244    pub id: &'static str,

The questions, in the order they are asked.

246    pub parts: Vec<Part>,

The request body exactly as it will be sent.

248    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.

252    pub worst_case_dollars: f64,
253    state: Json,
254    questions: Questions,
255}
257fn prepare(
258    model: &ModelId,
259    id: &'static str,
260    input: &str,
261    asked: impl IntoIterator<Item = (String, Question)>,
262) -> Result<Prepared, ProtocolError> {
263    let state = Json::canonical(&serde_json::json!({ "input": input }).to_string())?;
264    let mut questions = Questions::new();
265    let mut parts = Vec::new();
266    for (id, question) in asked {
267        let (kind, asked) = match question {
268            Question::Noul(noul) => ("noul", Asked::Noul(questions.noul(&id, noul)?)),
269            Question::Choice(choice) => ("choice", Asked::Choice(questions.choice(&id, choice)?)),
270            Question::Score(score) => ("score", Asked::Score(questions.score(&id, score)?)),
271        };
272        parts.push(Part { id, kind, asked });
273    }
274    let request = String::from_utf8(request_bytes(model, &state, &questions)?)
275        .map_err(|e| ProtocolError::Invalid(e.to_string()))?;
276    let worst_case_dollars = worst_case_dollars(model, &state, &questions);
277    Ok(Prepared { id, parts, request, worst_case_dollars, state, questions })
278}

What to ask Jev about an input, in a form that can be sent to the archive: it prepares the same request from this as the Worker did.

282#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
283pub enum Wanted {

The questions that teach these facts, in one request.

285    Facts(Vec<Fact>),

The questions the LLM wrote, in one request, each with its id.

287    Drafted(Vec<(String, Draft)>),
288}

The request for wanted about input (already [clean]).

291pub fn wanted(model: &ModelId, input: &str, wanted: &Wanted) -> Result<Prepared, ProtocolError> {
292    match wanted {
293        Wanted::Facts(facts) => {
294            // Several facts can share a question; it is asked once.
295            let mut asked: Vec<(String, Question)> = Vec::new();
296            for fact in facts {
297                let id = question_id(*fact);
298                if asked.iter().all(|(known, _)| known != id) {
299                    asked.push((id.to_owned(), fact_question(*fact)?));
300                }
301            }
302            prepare(model, "facts", input, asked)
303        }
304        Wanted::Drafted(drafts) => {
305            let asked: Result<Vec<_>, _> =
306                drafts.iter().map(|(id, draft)| Ok((id.clone(), drafted_question(draft)?))).collect();
307            prepare(model, "answers", input, asked?)
308        }
309    }
310}

What the answer to a fact's question ([question_id]) makes the fact. None when the answer is not of the type that question is.

314pub fn learned(fact: Fact, judged: &Judged) -> Option<Value> {
315    match (fact, judged) {
316        (Fact::Answerable | Fact::Several | Fact::Scale | Fact::Fit | Fact::Whole, Judged::Noul(p_yes)) => {
317            Some(Value::Bool(is_yes(*p_yes)))
318        }
319        (Fact::Yes | Fact::Chance, Judged::Noul(_)) | (Fact::Degree, Judged::Score(_)) => Some(Value::Given),
320        (Fact::Reads(kind), Judged::Choice(answer)) => Some(Value::Bool(readings(answer).contains(&kind))),
321        _ => None,
322    }
323}

Jev's answer, by type.

326#[derive(Clone, Debug, PartialEq)]
327pub enum Judged {

Probability that the answer is yes.

329    Noul(f64),
330    Choice(ChoiceAnswer),
331    Score(ScoreAnswer),
332}

Whether this ask put the request on the wire.

335#[derive(Clone, Copy, Debug, PartialEq)]
336pub enum Sent {

It did, just now.

338    Now,

It did not: the same request was answered at at_ms (Unix milliseconds), and this is that response.

341    Before { at_ms: f64 },
342}

What came back.

345#[derive(Debug)]
346pub enum Outcome {
347    Answered {

One answer per question, in the order they were asked.

349        judged: Vec<Judged>,
350        sent: Sent,

The response body exactly as it arrived.

352        body: String,
353        request_id: Option<String>,
354        attempts: u32,
355        usage: Usage,

The whole round trip, retries and waits included.

357        took: Duration,
358    },
359    Failed {
360        error: String,
361        request_id: Option<String>,
362        took: Duration,

What a budget should count the failure as: nothing when it certainly cost nothing, the call's worst case otherwise.

365        dollars: f64,
366    },
367}
369impl Outcome {

What the call cost, or may have.

371    pub fn dollars(&self) -> f64 {
372        match self {
373            Outcome::Answered { sent: Sent::Before { .. }, .. } => 0.0,
374            Outcome::Answered { usage, .. } => usage.dollars(),
375            Outcome::Failed { dollars, .. } => *dollars,
376        }
377    }
378}
380impl Prepared {

The state and the questions, for a client that takes them and not the body: jev-http's, which the eval asks through.

383    pub fn asking(&self) -> (&Json, &Questions) {
384        (&self.state, &self.questions)
385    }
386}

The answers in a response, one per question of prepared, in order.

389pub fn judged(prepared: &Prepared, response: &Response) -> Vec<Judged> {
390    prepared
391        .parts
392        .iter()
393        .map(|part| match part.asked {
394            Asked::Noul(key) => Judged::Noul(response.get(key).noul),
395            Asked::Choice(key) => Judged::Choice(response.get(key).clone()),
396            Asked::Score(key) => Judged::Score(response.get(key).clone()),
397        })
398        .collect()
399}

Sends a prepared call. runtime is the clock the round trip is timed on.

402pub async fn send<T: Transport, R: Runtime, O: Observer>(
403    client: &Client<T, R, O>,
404    runtime: &impl Runtime,
405    prepared: &Prepared,
406) -> Outcome {
407    let started = runtime.now();
408    let asked = client.ask(&prepared.state, &prepared.questions).await;
409    let took = runtime.now() - started;
410    match asked {
411        Ok(answered) => Outcome::Answered {
412            judged: judged(prepared, &answered.response),
413            sent: Sent::Now,
414            body: String::from_utf8_lossy(&answered.body).into_owned(),
415            request_id: answered.request_id,
416            attempts: answered.attempts,
417            usage: answered.response.usage(),
418            took,
419        },
420        Err(error) => Outcome::Failed {
421            request_id: error.request_id().map(str::to_owned),
422            dollars: if error.nothing_billed() { 0.0 } else { prepared.worst_case_dollars },
423            error: error.to_string(),
424            took,
425        },
426    }
427}

A response body as it arrived for a call, with what the call recorded beside it.

431pub struct Kept<'a> {
432    pub body: &'a str,
433    pub request_id: Option<String>,
434    pub attempts: u32,
435    pub took: Duration,
436    pub sent: Sent,
437}

Reads a kept response as the answer to prepared: the same check of the body against the questions that send makes, with nothing sent. A body that does not answer these questions is a failure, not an answer.

442pub fn read(model: &ModelId, prepared: &Prepared, kept: Kept<'_>) -> Outcome {
443    match Response::parse(model, &prepared.questions, kept.body.as_bytes()) {
444        Ok(response) => Outcome::Answered {
445            judged: judged(prepared, &response),
446            sent: kept.sent,
447            body: kept.body.to_owned(),
448            request_id: kept.request_id,
449            attempts: kept.attempts,
450            usage: response.usage(),
451            took: kept.took,
452        },
453        Err(error) => Outcome::Failed {
454            error: format!("the kept response does not answer this request: {error}"),
455            request_id: kept.request_id,
456            took: kept.took,
457            dollars: 0.0,
458        },
459    }
460}
462#[cfg(test)]
463mod tests;