use serde_json::{json, Value};
pub const QUESTIONS: &str = "<escouade>\nYou have no tool to ask the user a question here. When you need the user to choose or answer before going on, end your turn with one block, and nothing after it:\n<escouade_question>{\"questions\": [{\"question\": \"…\", \"options\": [\"…\", \"…\"], \"multiSelect\": false}]}</escouade_question>\nEscouade shows it to the user and sends the answers as the next message.\n</escouade>";
pub const OPEN: &str = "<escouade_question>";
pub const CLOSE: &str = "</escouade_question>";
pub fn split(text: &str) -> (String, Option<String>) {
match text.find(OPEN) {
Some(at) => (
text[..at].trim_end().to_string(),
Some(text[at..].to_string()),
),
None => (text.to_string(), None),
}
}
pub fn question_params(call: &str, block: &str) -> Option<Value> {
let inner = block.trim().strip_prefix(OPEN)?.split(CLOSE).next()?.trim();
let inner = inner
.trim_start_matches("```json")
.trim_start_matches("```")
.trim_end_matches("```")
.trim();
let parsed: Value = serde_json::from_str(inner).ok()?;
let list = match &parsed {
Value::Array(_) => &parsed,
_ => &parsed["questions"],
};
let questions: Vec<Value> = list
.as_array()?
.iter()
.enumerate()
.filter_map(|(i, q)| {
let text = q["question"].as_str()?.trim();
if text.is_empty() {
return None;
}
let options: Vec<Value> = q["options"]
.as_array()
.into_iter()
.flatten()
.filter_map(|o| o.as_str().or(o["label"].as_str()))
.map(|l| json!({ "id": l, "label": l }))
.collect();
Some(json!({
"id": format!("q{i}"), "prompt": text, "options": options,
"allowMultiple": q["multiSelect"] == true,
}))
})
.collect();
(!questions.is_empty()).then(|| json!({ "toolCallId": call, "questions": questions }))
}
pub fn answers_message(params: &Value, answers: &Value) -> String {
let lines: Vec<String> = params["questions"]
.as_array()
.into_iter()
.flatten()
.map(|q| {
let text = q["prompt"].as_str().unwrap_or_default();
let a = answers[text].as_str().unwrap_or_default().trim();
format!("- {text} → {a}")
})
.collect();
format!("The user answered your questions:\n{}", lines.join("\n"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_block_ends_the_text_and_reads_as_questions() {
let (before, block) = split("Il me faut ton choix.\n<escouade_question>{\"questions\": [{\"question\": \"Rouge ou bleu ?\", \"options\": [\"rouge\", \"bleu\"]}]}</escouade_question>");
assert_eq!(before, "Il me faut ton choix.");
let p = question_params("c", &block.unwrap()).unwrap();
assert_eq!(p["questions"][0]["prompt"], "Rouge ou bleu ?");
assert_eq!(
answers_message(&p, &json!({ "Rouge ou bleu ?": "rouge" })),
"The user answered your questions:\n- Rouge ou bleu ? → rouge"
);
assert!(
question_params("c", "<escouade_question>pas du json</escouade_question>").is_none()
);
}
}