From 3c8c20fe474515b5beddf608820d7d4c1cc75619 Mon Sep 17 00:00:00 2001 From: Valeriy Selitskiy <239034+iamwavecut@users.noreply.github.com> Date: Tue, 20 Oct 2026 18:48:06 +0200 Subject: [PATCH] Decode run_python code and send_files from tool arguments The argument decoder filled ToolStep field by field and had no entries for code and send_files, so every native run_python call reached the sandbox with an empty script ("run_python_code_empty" in production). Map both fields, pass send_files arrays through like file_ids, and add a test that every tool schema argument survives JSON decoding. The model also omitted send_files in production, so the argument description now says that only listed files reach the user. --- crates/openplotva-dialog/src/lib.rs | 87 ++++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 2 deletions(-) diff --git a/crates/openplotva-dialog/src/lib.rs b/crates/openplotva-dialog/src/lib.rs index 0ea6f553..a5d8ff42 100644 --- a/crates/openplotva-dialog/src/lib.rs +++ b/crates/openplotva-dialog/src/lib.rs @@ -755,7 +755,7 @@ const RUN_PYTHON_ARGS: &[ToolArgSpec] = &[ ToolArgSpec { name: "send_files", required: false, - description: "Paths in /home to send to the chat when the script succeeds, up to 10. Images are sent as photos, .gif as an animation, .mp4 as a video, .mp3 and .m4a as audio, other files as documents.", + description: "Paths in /home to send to the chat when the script succeeds, up to 10, for example [\"chart.png\"]. Only the listed files reach the user, so list every chart or file they asked for. Images are sent as photos, .gif as an animation, .mp4 as a video, .mp3 and .m4a as audio, other files as documents.", }, ToolArgSpec { name: "caption", @@ -3744,7 +3744,7 @@ fn populate_json_map_args(map: &Map, step: &mut ToolStep) { } populate_tool_args( |key| { - if matches!(key, "file_ids" | "message_ids") + if matches!(key, "file_ids" | "message_ids" | "send_files") && map.get(key).is_some_and(Value::is_array) { return map.get(key).map(Value::to_string); @@ -4116,6 +4116,7 @@ fn primary_tool_arg_key(step: &str) -> Option<&'static str> { STEP_TRANSLATE_TEXT | STEP_SEND_MESSAGE => Some("text"), STEP_REACT_TO_MESSAGE => Some("emoji"), STEP_CURRENCY_RATES => Some("pairs"), + STEP_RUN_PYTHON => Some("code"), _ => None, } } @@ -5049,6 +5050,20 @@ fn populate_tool_args(mut lookup: impl FnMut(&str) -> Option, step: &mut if let Some(value) = lookup("seed") { step.seed = value; } + if let Some(value) = lookup("code") { + step.code = value; + } + if let Some(value) = lookup("send_files") { + step.send_files = serde_json::from_str(&value) + .ok() + .or_else(|| xmlish_reference_list(&value)) + .unwrap_or_else(|| { + value + .split(',') + .map(|path| path.trim().to_owned()) + .collect() + }); + } if let Some(value) = lookup("emoji") { step.emoji = value; } @@ -5229,6 +5244,12 @@ fn normalize_and_validate_step(mut step: ToolStep) -> Result json!(7), + Some("boolean") => json!(true), + Some("array") if arg.name == "message_ids" => json!([7]), + Some("array") => json!(["a.png"]), + // Some string-typed args (chat_id, thread_id) hold numbers. + _ => json!("7"), + }; + let mut map = Map::new(); + map.insert(arg.name.to_owned(), sample.clone()); + let mut step = ToolStep { + step: spec.name.to_owned(), + ..ToolStep::default() + }; + populate_json_map_args(&map, &mut step); + let parsed = serde_json::to_value(&step).expect("serialize tool step"); + let as_text = |value: &Value| match value { + Value::String(text) => text.clone(), + other => other.to_string(), + }; + if parsed.get(arg.name).map(as_text) != Some(as_text(&sample)) { + dropped.push(format!("{}.{}", spec.name, arg.name)); + } + } + } + + assert!( + dropped.is_empty(), + "tool schema args dropped by the argument decoder: {dropped:?}" + ); + } + + #[test] + fn native_run_python_calls_keep_the_script_and_files() -> Result<(), ToolParseError> { + let code = "import matplotlib.pyplot as plt\n\nfor i, v in enumerate([1, 2]):\n plt.text(i, v, str(v))\nplt.savefig('chart.png')"; + let step = parse_native_tool_step(&[NativeToolCall { + id: "call_1".to_owned(), + call_type: "function".to_owned(), + function: NativeToolFunction { + name: STEP_RUN_PYTHON.to_owned(), + arguments: Value::String( + json!({"code": code, "send_files": [" chart.png "], "caption": "Продажи"}) + .to_string(), + ), + }, + }])?; + + assert_eq!(step.step, STEP_RUN_PYTHON); + assert_eq!(step.code, code); + assert_eq!(step.send_files, vec!["chart.png"]); + assert_eq!(step.caption, "Продажи"); + Ok(()) + } + #[test] fn tool_argument_decoders_keep_json_xmlish_and_native_shapes_in_parity() -> Result<(), ToolParseError> {