1use anyhow::{Result, anyhow};
2use handlebars::Handlebars;
3use rand::{Rng, rng};
4use serde_json::{Value, json};
5use tokio::time::{Duration, sleep};
6
7use gsm_core::MessageEnvelope;
8
9pub async fn run_tool(
10 cfg: &crate::model::ToolNode,
11 env: &MessageEnvelope,
12 state: &Value,
13 endpoint: &str,
14) -> Result<Value> {
15 let input = render_tool_input(cfg, env, state)?;
16 run_tool_with_input(cfg, input, endpoint).await
17}
18
19pub fn render_tool_input(
20 cfg: &crate::model::ToolNode,
21 env: &MessageEnvelope,
22 state: &Value,
23) -> Result<Value> {
24 let mut input = cfg.input.clone();
25 render_json_strings(&mut input, &json!({"state":state, "envelope":env}))?;
26 Ok(input)
27}
28
29pub async fn run_tool_with_input(
30 cfg: &crate::model::ToolNode,
31 input: Value,
32 endpoint: &str,
33) -> Result<Value> {
34 let url = format!(
35 "{}/{}/{}",
36 endpoint.trim_end_matches('/'),
37 cfg.tool,
38 cfg.action
39 );
40
41 let retries = cfg.retry.unwrap_or(1);
42 let timeout = cfg.timeout_secs.unwrap_or(10);
43 let base = cfg.delay_secs.unwrap_or(1);
44
45 for attempt in 0..=retries {
46 let resp = tokio::time::timeout(Duration::from_secs(timeout), async {
47 reqwest::Client::new().post(&url).json(&input).send().await
48 })
49 .await;
50
51 match resp {
52 Ok(Ok(r)) if r.status().is_success() => {
53 let v: Value = r.json().await.unwrap_or_else(|_| json!({}));
54 return Ok(v);
55 }
56 _ => {
57 if attempt == retries {
58 return Err(anyhow!("tool call failed after {} attempts", retries + 1));
59 }
60 let jitter: f64 = rng().random_range(0.5..1.5);
61 let delay = (base as f64 * 2f64.powi(attempt as i32) * jitter).round() as u64;
62 sleep(Duration::from_secs(delay)).await;
63 }
64 }
65 }
66 Err(anyhow!("unreachable"))
67}
68
69#[allow(dead_code)]
70pub fn run_tool_stub_with_input(input: Value) -> Result<Value> {
71 let _ = input;
72 Ok(json!({"ok": true}))
73}
74
75fn render_json_strings(value: &mut Value, ctx: &Value) -> Result<()> {
76 let h = Handlebars::new();
77 match value {
78 Value::String(s) => {
79 *s = h.render_template(s, ctx)?;
80 }
81 Value::Array(arr) => {
82 for v in arr {
83 render_json_strings(v, ctx)?;
84 }
85 }
86 Value::Object(map) => {
87 for (_, v) in map.iter_mut() {
88 render_json_strings(v, ctx)?;
89 }
90 }
91 _ => {}
92 }
93 Ok(())
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99
100 #[test]
101 fn render_json_strings_substitutes_nested_templates() {
102 let mut value = json!({
103 "greeting": "Hello {{state.user}}",
104 "items": ["{{envelope.chat_id}}", "{{state.extra}}"]
105 });
106 let ctx = json!({
107 "state": { "user": "Alice", "extra": "item-2" },
108 "envelope": { "chat_id": "chat-1" }
109 });
110
111 render_json_strings(&mut value, &ctx).unwrap();
112 assert_eq!(value["greeting"], "Hello Alice");
113 assert_eq!(value["items"][0], "chat-1");
114 assert_eq!(value["items"][1], "item-2");
115 }
116
117 #[test]
118 fn render_json_strings_leaves_non_strings() {
119 let mut value = json!({
120 "count": 3,
121 "flags": [true, false],
122 "note": "Hi {{state.name}}"
123 });
124 let ctx = json!({ "state": { "name": "Bob" } });
125
126 render_json_strings(&mut value, &ctx).unwrap();
127 assert_eq!(value["count"], 3);
128 assert_eq!(value["flags"][0], true);
129 assert_eq!(value["note"], "Hi Bob");
130 }
131}