lmjtfy.git / packages / ask / src / lib.rs
lib.rsannotatedlib.rssource442 lines · 17.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;

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

46pub fn is_sure(p_yes: f64) -> bool {
47    p_yes.max(1.0 - p_yes) >= SURE
48}

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

52pub fn likelihood(p: f64) -> &'static str {
53    match p {
54        p if p < 0.1 => "very unlikely",
55        p if p < 0.4 => "unlikely",
56        p if p <= 0.6 => "a toss-up",
57        p if p <= 0.9 => "likely",
58        _ => "very likely",
59    }
60}

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

63pub fn clean(input: &str) -> String {
64    input.trim().chars().take(MAX_INPUT_CHARS).collect()
65}

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

68const KINDS: [(&str, Kind, &str); 4] = [
69    ("yes_or_no", Kind::Noul, "It asks whether something is so. A yes or a no answers it."),
70    (
71        "pick_one",
72        Kind::Choice,
73        "It asks which, who or what: the best or right one among several possibilities. Naming one answers it.",
74    ),
75    (
76        "how_much",
77        Kind::Score,
78        "It asks for a degree, an amount or a rating. A position on a scale answers it.",
79    ),
80    (
81        "how_likely",
82        Kind::Chance,
83        "It asks for the chance, the odds or the probability of something. A percentage answers it.",
84    ),
85];

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

90pub const ALSO: f64 = 0.3;

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

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

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

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

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

109pub fn question_id(fact: Fact) -> &'static str {
110    match fact {
111        Fact::Reads(_) => "kind",
112        fact => fact.name(),
113    }
114}

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

119pub 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.

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

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.

199pub fn check(draft: &Draft) -> Result<(), ProtocolError> {
200    drafted_question(draft).map(|_| ())
201}

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

204#[derive(Clone, Copy)]
205enum Asked {
206    Noul(Key<NoulAnswer>),
207    Choice(Key<ChoiceAnswer>),
208    Score(Key<ScoreAnswer>),
209}

One question in a request.

212pub struct Part {

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

214    pub id: String,

The Jev type asked: noul, choice or score.

216    pub kind: &'static str,
217    asked: Asked,
218}

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

221pub struct Prepared {

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

223    pub id: &'static str,

The questions, in the order they are asked.

225    pub parts: Vec<Part>,

The request body exactly as it will be sent.

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

231    pub worst_case_dollars: f64,
232    state: Json,
233    questions: Questions,
234}
236fn prepare(
237    model: &ModelId,
238    id: &'static str,
239    input: &str,
240    asked: impl IntoIterator<Item = (String, Question)>,
241) -> Result<Prepared, ProtocolError> {
242    let state = Json::canonical(&serde_json::json!({ "input": input }).to_string())?;
243    let mut questions = Questions::new();
244    let mut parts = Vec::new();
245    for (id, question) in asked {
246        let (kind, asked) = match question {
247            Question::Noul(noul) => ("noul", Asked::Noul(questions.noul(&id, noul)?)),
248            Question::Choice(choice) => ("choice", Asked::Choice(questions.choice(&id, choice)?)),
249            Question::Score(score) => ("score", Asked::Score(questions.score(&id, score)?)),
250        };
251        parts.push(Part { id, kind, asked });
252    }
253    let request = String::from_utf8(request_bytes(model, &state, &questions)?)
254        .map_err(|e| ProtocolError::Invalid(e.to_string()))?;
255    let worst_case_dollars = worst_case_dollars(model, &state, &questions);
256    Ok(Prepared { id, parts, request, worst_case_dollars, state, questions })
257}

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.

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

The questions that teach these facts, in one request.

264    Facts(Vec<Fact>),

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

266    Drafted(Vec<(String, Draft)>),
267}

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

270pub fn wanted(model: &ModelId, input: &str, wanted: &Wanted) -> Result<Prepared, ProtocolError> {
271    match wanted {
272        Wanted::Facts(facts) => {
273            // Several facts can share a question; it is asked once.
274            let mut asked: Vec<(String, Question)> = Vec::new();
275            for fact in facts {
276                let id = question_id(*fact);
277                if asked.iter().all(|(known, _)| known != id) {
278                    asked.push((id.to_owned(), fact_question(*fact)?));
279                }
280            }
281            prepare(model, "facts", input, asked)
282        }
283        Wanted::Drafted(drafts) => {
284            let asked: Result<Vec<_>, _> =
285                drafts.iter().map(|(id, draft)| Ok((id.clone(), drafted_question(draft)?))).collect();
286            prepare(model, "answers", input, asked?)
287        }
288    }
289}

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.

293pub fn learned(fact: Fact, judged: &Judged) -> Option<Value> {
294    match (fact, judged) {
295        (Fact::Answerable | Fact::Several | Fact::Scale | Fact::Fit, Judged::Noul(p_yes)) => {
296            Some(Value::Bool(is_yes(*p_yes)))
297        }
298        (Fact::Yes | Fact::Chance, Judged::Noul(_)) | (Fact::Degree, Judged::Score(_)) => Some(Value::Given),
299        (Fact::Reads(kind), Judged::Choice(answer)) => Some(Value::Bool(readings(answer).contains(&kind))),
300        _ => None,
301    }
302}

Jev's answer, by type.

305#[derive(Clone, Debug, PartialEq)]
306pub enum Judged {

Probability that the answer is yes.

308    Noul(f64),
309    Choice(ChoiceAnswer),
310    Score(ScoreAnswer),
311}

Whether this ask put the request on the wire.

314#[derive(Clone, Copy, Debug, PartialEq)]
315pub enum Sent {

It did, just now.

317    Now,

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

320    Before { at_ms: f64 },
321}

What came back.

324#[derive(Debug)]
325pub enum Outcome {
326    Answered {

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

328        judged: Vec<Judged>,
329        sent: Sent,

The response body exactly as it arrived.

331        body: String,
332        request_id: Option<String>,
333        attempts: u32,
334        usage: Usage,

The whole round trip, retries and waits included.

336        took: Duration,
337    },
338    Failed {
339        error: String,
340        request_id: Option<String>,
341        took: Duration,

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

344        dollars: f64,
345    },
346}
348impl Outcome {

What the call cost, or may have.

350    pub fn dollars(&self) -> f64 {
351        match self {
352            Outcome::Answered { sent: Sent::Before { .. }, .. } => 0.0,
353            Outcome::Answered { usage, .. } => usage.dollars(),
354            Outcome::Failed { dollars, .. } => *dollars,
355        }
356    }
357}
359impl Prepared {

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

362    pub fn asking(&self) -> (&Json, &Questions) {
363        (&self.state, &self.questions)
364    }
365}

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

368pub fn judged(prepared: &Prepared, response: &Response) -> Vec<Judged> {
369    prepared
370        .parts
371        .iter()
372        .map(|part| match part.asked {
373            Asked::Noul(key) => Judged::Noul(response.get(key).noul),
374            Asked::Choice(key) => Judged::Choice(response.get(key).clone()),
375            Asked::Score(key) => Judged::Score(response.get(key).clone()),
376        })
377        .collect()
378}

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

381pub async fn send<T: Transport, R: Runtime, O: Observer>(
382    client: &Client<T, R, O>,
383    runtime: &impl Runtime,
384    prepared: &Prepared,
385) -> Outcome {
386    let started = runtime.now();
387    let asked = client.ask(&prepared.state, &prepared.questions).await;
388    let took = runtime.now() - started;
389    match asked {
390        Ok(answered) => Outcome::Answered {
391            judged: judged(prepared, &answered.response),
392            sent: Sent::Now,
393            body: String::from_utf8_lossy(&answered.body).into_owned(),
394            request_id: answered.request_id,
395            attempts: answered.attempts,
396            usage: answered.response.usage(),
397            took,
398        },
399        Err(error) => Outcome::Failed {
400            request_id: error.request_id().map(str::to_owned),
401            dollars: if error.nothing_billed() { 0.0 } else { prepared.worst_case_dollars },
402            error: error.to_string(),
403            took,
404        },
405    }
406}

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

410pub struct Kept<'a> {
411    pub body: &'a str,
412    pub request_id: Option<String>,
413    pub attempts: u32,
414    pub took: Duration,
415    pub sent: Sent,
416}

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.

421pub fn read(model: &ModelId, prepared: &Prepared, kept: Kept<'_>) -> Outcome {
422    match Response::parse(model, &prepared.questions, kept.body.as_bytes()) {
423        Ok(response) => Outcome::Answered {
424            judged: judged(prepared, &response),
425            sent: kept.sent,
426            body: kept.body.to_owned(),
427            request_id: kept.request_id,
428            attempts: kept.attempts,
429            usage: response.usage(),
430            took: kept.took,
431        },
432        Err(error) => Outcome::Failed {
433            error: format!("the kept response does not answer this request: {error}"),
434            request_id: kept.request_id,
435            took: kept.took,
436            dollars: 0.0,
437        },
438    }
439}
441#[cfg(test)]
442mod tests;