use serde_json::{Map, Value, json};
use super::Rail;
use super::call::{Outcome, request};
use crate::mcp::tools::ToolResult;
const PREV: &str = "$prev";
struct Step<'a> {
subject: &'a str,
input: &'a Value,
reply: bool,
pass_for: Option<&'a str>,
}
fn step(value: &Value, index: usize) -> Result<Step<'_>, String> {
let subject = value
.get("subject")
.and_then(Value::as_str)
.ok_or_else(|| format!("step {index} is missing subject"))?;
let Some(input @ Value::Object(_)) = value.get("input") else {
return Err(format!("step {index}'s input is missing or not an object"));
};
let reply = value.get("reply").and_then(Value::as_bool).unwrap_or(true);
let pass_for = match value.get("pass_for") {
None => None,
Some(Value::String(audience)) => Some(audience.as_str()),
Some(_) => return Err(format!("step {index}'s pass_for is not a string")),
};
Ok(Step {
subject,
input,
reply,
pass_for,
})
}
pub fn fill(input: &Value, prev: Option<&Value>) -> Result<Value, String> {
match input {
Value::Object(object) => {
if let (1, Some(Value::String(pointer))) = (object.len(), object.get(PREV)) {
let prev = prev.ok_or_else(|| format!("{PREV} {pointer:?} in the first step"))?;
return prev.pointer(pointer).cloned().ok_or_else(|| {
format!("{PREV} {pointer:?} names nothing in the previous reply")
});
}
let mut filled = Map::with_capacity(object.len());
for (key, value) in object {
filled.insert(key.clone(), fill(value, prev)?);
}
Ok(Value::Object(filled))
}
Value::Array(items) => items
.iter()
.map(|item| fill(item, prev))
.collect::<Result<_, _>>()
.map(Value::Array),
other => Ok(other.clone()),
}
}
pub fn flow<R: Rail>(rail: &R, steps: &[Value]) -> ToolResult {
let limit = rail.flow_steps();
if steps.is_empty() {
return ToolResult::failed("the flow has no steps", json!({ "steps": [] }));
}
if steps.len() > limit {
return ToolResult::failed(
&format!(
"the flow has {} steps, over the limit of {limit}",
steps.len()
),
json!({ "steps": [] }),
);
}
let mut answered: Vec<Value> = Vec::with_capacity(steps.len());
let mut prev: Option<Value> = None;
for (index, value) in steps.iter().enumerate() {
let failed = |words: String, answered: &[Value]| {
ToolResult::failed(
&format!("step {index} stopped the flow: {words}"),
json!({ "failed_step": index, "steps": answered }),
)
};
let step = match step(value, index) {
Ok(step) => step,
Err(words) => return failed(words, &answered),
};
let input = match fill(step.input, prev.as_ref()) {
Ok(input) => input,
Err(words) => return failed(words, &answered),
};
match request(rail, step.subject, &input, step.reply, step.pass_for) {
Outcome::Reply(reply) => {
answered.push(json!({ "subject": step.subject, "reply": reply }));
prev = Some(reply);
}
Outcome::Sent => {
answered.push(json!({ "subject": step.subject, "sent": true }));
prev = Some(Value::Null);
}
Outcome::Refused(words) => {
return failed(format!("{}: {words}", step.subject), &answered);
}
}
}
ToolResult::ok(json!({ "steps": answered }))
}
#[cfg(test)]
#[path = "flow_tests.rs"]
mod tests;