1//! What Jev is asked, and the rules that turn its answers into what happens. 2//! 3//! Three judgments, each a module: a shell command before it runs 4//! ([`command`]), a turn before it ends ([`stop`]), and a prompt before its 5//! turn starts ([`model`]). Each is the same three things: 6//! 7//! - **Facts**: what can be known. Most are Jev's to give, and a threshold 8//! is applied where an answer is read, so a fact is a plain yes or no (or 9//! one of a few named values) and a rule can test it. 10//! - **Questions**: how Jev is asked for those facts (`jev_facts::Source`). 11//! Every word that matters is defined in the option or level it belongs 12//! to, as concrete acts, never as an adjective. 13//! - **Rules**: tests on facts and what to do when they hold, compiled into 14//! a `rete` network. Order is priority. The network is what decides, and 15//! it is what the website draws, so the picture cannot show other rules 16//! than the ones that run. 17//! 18//! Nothing here does I/O or is async. The daemon asks a network what to do 19//! next, does it (asks Jev, measures memory), records what it learned, and 20//! asks again. 21#![forbid(unsafe_code)] 22 23use jev_protocol::{ModelId, ProtocolError}; 24 25pub mod command; 26pub mod model; 27pub mod stop; 28 29/// The Jev model every question goes to. Pinned, and pinned here, because 30/// the thresholds in this crate were set against this model's answers. 31pub const MODEL: &str = "jev-1.13.0"; 32 33/// [`MODEL`] as the protocol's type. 34pub fn jev_model() -> Result<ModelId, ProtocolError> { 35 ModelId::pinned(MODEL) 36} 37 38/// How much of a command, a prompt or a final message Jev is shown. 39pub(crate) const SHOWN_CHARS: usize = 6000; 40 41/// A domain with no effect, or no note, says so with this: it has no value. 42#[derive(Clone, Copy, Debug, PartialEq, Eq)] 43pub enum Never {} 44 45/// The first `count` characters of `text`. 46pub fn head(text: &str, count: usize) -> &str { 47 text.char_indices().nth(count).map_or(text, |(at, _)| &text[..at]) 48} 49 50/// The last `count` characters of `text`. 51pub fn tail(text: &str, count: usize) -> &str { 52 let skip = text.chars().count().saturating_sub(count); 53 text.char_indices().nth(skip).map_or("", |(at, _)| &text[at..]) 54} 55 56pub(crate) fn percent(probability: f64) -> f64 { 57 (probability * 100.0).round() 58} 59 60/// A Score's answer as the response body carries it, for answers made up in 61/// a test: `score` spread over the two levels either side of it. 62#[cfg(feature = "made-up")] 63pub(crate) fn made_up_score(score: f64, levels: usize) -> String { 64 let score = score.clamp(0.0, (levels - 1) as f64); 65 let below = (score.floor() as usize).min(levels - 1); 66 let share = score - below as f64; 67 let probability = |level: usize| match level { 68 l if l == below => 1.0 - share, 69 l if l == below + 1 => share, 70 _ => 0.0, 71 }; 72 let each = |value: &dyn Fn(usize) -> String| (0..levels).map(|level| format!(r#""{level}":{}"#, value(level))).collect::<Vec<_>>().join(","); 73 format!( 74 r#"{{"type":"score","score":{score},"confidence":1.0,"legend":{{{}}},"probabilities":{{{}}}}}"#, 75 each(&|level| format!(r#""level {level}""#)), 76 each(&|level| probability(level).to_string()), 77 ) 78} 79 80/// A Choice's answer as the response body carries it, for answers made up in 81/// a test: the likeliest option is the choice. 82#[cfg(feature = "made-up")] 83pub(crate) fn made_up_choice(probabilities: &[(&str, f64)]) -> String { 84 let (choice, confidence) = probabilities.iter().copied().max_by(|a, b| a.1.total_cmp(&b.1)).unwrap_or(("", 0.0)); 85 let each = probabilities.iter().map(|(label, p)| format!(r#""{label}":{p}"#)).collect::<Vec<_>>().join(","); 86 format!(r#"{{"type":"choice","choice":"{choice}","confidence":{confidence},"probabilities":{{{each}}}}}"#) 87} 88 89/// A whole response body with these answers, for answers made up in a test. 90#[cfg(feature = "made-up")] 91pub(crate) fn made_up_body(answers: &[(&str, String)]) -> String { 92 let each = answers.iter().map(|(id, answer)| format!(r#""{id}":{answer}"#)).collect::<Vec<_>>().join(","); 93 format!(r#"{{"model":"{MODEL}","answers":{{{each}}},"usage":{{"input_tokens":0,"output_tokens":0}}}}"#) 94} 95 96#[cfg(test)] 97mod tests { 98 use super::*; 99 100 /// Commands and messages are clipped by characters, never bytes: a cut inside a multi-byte 101 /// character would panic on real text. 102 #[test] 103 fn clipping_keeps_whole_characters() { 104 assert_eq!(head("héllo", 2), "hé"); 105 assert_eq!(tail("héllo — ✓", 3), "— ✓"); 106 assert_eq!(tail("ab", 5), "ab"); 107 } 108}