Skip to main content

jev_repl/
codegen.rs

1//! Turn the current session into a program written against this SDK.
2
3use serde_json::Value;
4
5use crate::session::Session;
6
7pub fn rust(session: &Session, model: &str, threshold: f64) -> String {
8    let questions: Vec<(String, Value)> = session
9        .questions
10        .iter()
11        .filter_map(|(name, q)| Some((name.clone(), serde_json::to_value(q).ok()?)))
12        .collect();
13
14    let mut out = String::new();
15    out.push_str("#[tokio::main]\nasync fn main() -> typesafe::Result<()> {\n");
16    out.push_str("    let client = Client::from_env()?; // TYPESAFE_API_KEY\n\n");
17    out.push_str("    let res = client\n        .system_one(\n");
18    out.push_str(&format!("            {},\n", state_literal(&session.state)));
19    out.push_str("            Questions::new()\n");
20    for (name, q) in &questions {
21        out.push_str(&format!(
22            "                .with({:?}, {})\n",
23            name,
24            builder(q, 20)
25        ));
26    }
27    out.push_str("        )\n");
28    out.push_str(&format!("        .model({model:?})\n"));
29    out.push_str("        .await?;\n\n");
30
31    if questions.is_empty() {
32        out.push_str("    // add questions in the REPL and run :rust again\n");
33    }
34    for (name, q) in &questions {
35        out.push_str(&reader(name, q, threshold));
36    }
37    out.push_str("\n    Ok(())\n}\n");
38
39    // Imports are worked out from what the body actually used, `json!` included.
40    let mut imports = vec!["Client", "Questions"];
41    for (_, q) in &questions {
42        match kind(q) {
43            "noul" => imports.push("Noul"),
44            "choice" => imports.push("Choice"),
45            "score" => imports.push("Score"),
46            _ => {}
47        }
48    }
49    if out.contains("json!(") {
50        imports.push("json");
51    }
52    imports.sort_unstable();
53    imports.dedup();
54
55    format!(
56        "// Cargo.toml: typesafe-ai-sdk = \"0.1\"\nuse typesafe::{{{}}};\n\n{out}",
57        imports.join(", ")
58    )
59}
60
61fn kind(q: &Value) -> &str {
62    q.get("type").and_then(Value::as_str).unwrap_or("raw")
63}
64
65fn builder(q: &Value, indent: usize) -> String {
66    let pad = " ".repeat(indent + 4);
67    let instructions = q.get("instructions").map(literal).unwrap_or_default();
68    match kind(q) {
69        "noul" => {
70            let mut s = format!("Noul::new({instructions})");
71            let criteria = q.get("criteria");
72            if let Some(v) = criteria.and_then(|c| c.get("true")) {
73                s.push_str(&format!("\n{pad}.when_true({})", literal(v)));
74            }
75            if let Some(v) = criteria.and_then(|c| c.get("false")) {
76                s.push_str(&format!("\n{pad}.when_false({})", literal(v)));
77            }
78            s
79        }
80        "choice" => {
81            let mut s = format!("Choice::new({instructions})");
82            if let Some(criteria) = q.get("criteria").and_then(Value::as_object) {
83                for (label, desc) in criteria {
84                    s.push_str(&match desc {
85                        Value::Null => format!("\n{pad}.label({label:?})"),
86                        v => format!("\n{pad}.option({label:?}, {})", literal(v)),
87                    });
88                }
89            }
90            s
91        }
92        "score" => {
93            let levels: Vec<String> = q
94                .get("criteria")
95                .and_then(Value::as_array)
96                .map(|a| a.iter().map(literal).collect())
97                .unwrap_or_default();
98            format!(
99                "Score::new(\n{pad}{instructions},\n{pad}[{}],\n{})",
100                levels.join(", "),
101                " ".repeat(indent)
102            )
103        }
104        _ => format!("json!({q})"),
105    }
106}
107
108fn reader(name: &str, q: &Value, threshold: f64) -> String {
109    match kind(q) {
110        "noul" => format!(
111            "    let {name} = res.noul({name:?}).expect(\"asked\");\n\
112             \x20   println!(\"{name}: {{:.2}} → {{}}\", {name}.noul, {name}.is_yes({threshold:.2}));\n"
113        ),
114        "choice" => format!(
115            "    let {name} = res.choice({name:?}).expect(\"asked\");\n\
116             \x20   if {name}.confidence >= 0.6 {{\n\
117             \x20       println!(\"{name}: {{}}\", {name}.choice);\n\
118             \x20   }} else {{\n\
119             \x20       println!(\"{name}: unsure ({{:.2}}), send to a human\", {name}.confidence);\n\
120             \x20   }}\n"
121        ),
122        "score" => format!(
123            "    let {name} = res.score({name:?}).expect(\"asked\");\n\
124             \x20   println!(\"{name}: {{:.2}} of {{}} (confidence {{:.2}})\", {name}.score, {name}.legend.len() - 1, {name}.confidence);\n"
125        ),
126        _ => format!("    // {name}: a raw question — read it from res.raw\n"),
127    }
128}
129
130fn state_literal(state: &Value) -> String {
131    match state {
132        Value::String(s) => format!("{s:?}"),
133        other => format!("json!({other})"),
134    }
135}
136
137fn literal(v: &Value) -> String {
138    match v {
139        Value::String(s) => format!("{s:?}"),
140        other => format!("json!({other})"),
141    }
142}