diff --git a/crates/tinytools-agent/src/lib.rs b/crates/tinytools-agent/src/lib.rs index 624aee0..18e0989 100644 --- a/crates/tinytools-agent/src/lib.rs +++ b/crates/tinytools-agent/src/lib.rs @@ -44,8 +44,8 @@ pub use tinytools; pub use codecall::{CodeStyle, parse_calls as parse_code_calls, render_code_signature}; pub use parse::{ - extract_json_values, parse_arguments_value, parse_glm_style_tool_calls, parse_text, - parse_tool_call_value, parse_tool_calls, parse_tool_calls_from_json_value, + contains_call_markup, extract_json_values, parse_arguments_value, parse_glm_style_tool_calls, + parse_text, parse_tool_call_value, parse_tool_calls, parse_tool_calls_from_json_value, parse_tool_calls_with_pformat, }; pub use pformat::{ diff --git a/crates/tinytools-agent/src/parse/mod.rs b/crates/tinytools-agent/src/parse/mod.rs index ef5e2fc..05196b0 100644 --- a/crates/tinytools-agent/src/parse/mod.rs +++ b/crates/tinytools-agent/src/parse/mod.rs @@ -274,6 +274,28 @@ pub fn parse_tool_calls_with_pformat( parse_text(response, &options).into_parts() } +/// Whether `text` contains a tool call written as markup in any grammar this +/// crate recognises: a complete call, or a recognised block that did not +/// decode (malformed, or opened and never closed). +/// +/// For a caller that must refuse text which is really a tool call rather +/// than read calls out of it — a summarizer whose request declared no tools, +/// say, where `DeepSeek` V4 can answer with `<|DSML|invoke name="shell">…` +/// instead of a summary. A response that is only a bare JSON object does +/// not count, since an answer may legitimately be one, and markup quoted +/// inside a language-tagged fence is protected as usual. +#[must_use] +pub fn contains_call_markup(text: &str) -> bool { + let outcome = parse_text(text, &ParseOptions::new().without_bare_json()); + !outcome.calls.is_empty() + || outcome.diagnostics.iter().any(|diagnostic| { + matches!( + diagnostic, + ParseDiagnostic::MalformedBlock { .. } | ParseDiagnostic::UnterminatedBlock { .. } + ) + }) +} + /// Normalizes an argument value, decoding stringified JSON when possible. #[must_use] pub fn parse_arguments_value(raw: Option<&serde_json::Value>) -> serde_json::Value { diff --git a/crates/tinytools-agent/src/parse/test/markup.rs b/crates/tinytools-agent/src/parse/test/markup.rs new file mode 100644 index 0000000..dca5d3e --- /dev/null +++ b/crates/tinytools-agent/src/parse/test/markup.rs @@ -0,0 +1,50 @@ +//! `contains_call_markup`: telling text that is really a tool call apart from +//! an answer. + +use crate::parse::contains_call_markup; + +/// What `DeepSeek` V4 returned in place of a summary when its request declared +/// no tools (captured from the `OpenHuman` harness benchmark). +const DSML: &str = "<||DSML|| calls>\n<||DSML|| invoke name=\"shell\">\n\ +<||DSML|| parameter name=\"command\" string=\"true\">cd /app && cat src/lib.rs\n\ +\n"; + +#[test] +fn a_dsml_call_is_markup() { + assert!(contains_call_markup(DSML)); + assert!(contains_call_markup(&format!( + "Let me check first.\n\n{DSML}" + ))); +} + +#[test] +fn tagged_and_invoke_calls_are_markup() { + assert!(contains_call_markup( + "{\"name\":\"shell\",\"arguments\":{\"command\":\"ls\"}}" + )); + assert!(contains_call_markup( + "a.rs" + )); +} + +#[test] +fn a_block_that_does_not_decode_still_counts() { + // Opened and never closed: truncated mid-call. + assert!(contains_call_markup( + "<||DSML|| calls>\n<||DSML|| invoke name=\"shell\">\n<||DSML|| parameter name=\"command\"" + )); +} + +#[test] +fn prose_a_bare_json_answer_and_a_quoted_example_are_not_markup() { + assert!(!contains_call_markup( + "## Goal\nShip the parser.\n\n## Active State\nConfig is {\"retries\": 3}." + )); + assert!(!contains_call_markup( + "{\"name\": \"shell\", \"arguments\": {\"command\": \"ls\"}}" + )); + assert!(!contains_call_markup( + "The format is:\n```xml\nls\n```" + )); + assert!(!contains_call_markup("")); +} diff --git a/crates/tinytools-agent/src/parse/test/mod.rs b/crates/tinytools-agent/src/parse/test/mod.rs index 3705ce3..09bacd2 100644 --- a/crates/tinytools-agent/src/parse/test/mod.rs +++ b/crates/tinytools-agent/src/parse/test/mod.rs @@ -8,6 +8,7 @@ mod function_call; mod glm; mod harmony_mistral; mod invoke_xml; +mod markup; mod regressions; mod sentinel; mod tagged;