lib.rsannotatedlib.rssource108 lines · 4.5 KB · raw
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}