use adk_core::Tool;
use serde_json::{Map, Value};
pub(crate) const MONTY_PROMPT: &str = "\
You write Python that runs on Monty — a fast, sandboxed, Rust-native Python
interpreter. Emit exactly ONE fenced code block per turn (```python ... ```).
Monty runs a SUBSET of Python:
- No `class` definitions and no `match` statements.
- No third-party libraries and no imports beyond a small standard library
(`sys` , `os`, `typing`, `asyncio`, `re`, `datetime`, `json`, `pathlib`).
- Filesystem, environment, and clock access are restricted by the host policy
described in the OS access section below.
- Functions, loops, comprehensions, conditionals, f-strings, and the common
builtins (`len`, `sum`, `min`, `max`, `sorted`, `range`, `enumerate`,
`int`, `float`, `str`, `dict`, `list`, ...) all work.
Monty has no top-level `return`: the result of your script is the value of its
LAST EXPRESSION. Make that final expression the tagged result dict described
above.
Invoke a tool ONLY with the built-in `call_tool` function, passing the tool name
and a single dict of arguments — this is the single way to call a tool, and tool
names are never in scope as bare callables:
result = call_tool(\"tool_name\", {\"arg\": value, ...})
Put every tool argument inside that dict (never as a keyword argument to
`call_tool` itself), and compose the results with ordinary Python. Call tools
synchronously — do NOT use `await`.";
pub(crate) const TOOL_DISPATCH_FN: &str = "call_tool";
pub(crate) fn tool_entry(tool: &dyn Tool) -> String {
let decl = tool.declaration();
let params = decl.get("parameters");
let properties = params.and_then(|p| p.get("properties")).and_then(Value::as_object);
let required: Vec<String> = params
.and_then(|p| p.get("required"))
.and_then(Value::as_array)
.map(|arr| arr.iter().filter_map(|v| v.as_str().map(str::to_string)).collect())
.unwrap_or_default();
let mut ordered: Vec<String> = required.clone();
if let Some(props) = properties {
for key in props.keys() {
if !ordered.contains(key) {
ordered.push(key.clone());
}
}
}
let rendered: Vec<String> = ordered
.iter()
.map(|p| {
let ty = py_type(properties, p);
let placeholder =
if required.contains(p) { format!("<{ty}>") } else { format!("<{ty}, optional>") };
format!("{p:?}: {placeholder}")
})
.collect();
let name = tool.name();
let call = if rendered.is_empty() {
format!("{TOOL_DISPATCH_FN}({name:?})")
} else {
format!("{TOOL_DISPATCH_FN}({name:?}, {{{}}})", rendered.join(", "))
};
let description = one_line(tool.description());
if description.is_empty() {
format!("{call}\n")
} else {
format!("# {}\n{call}\n", escape_comment(&description))
}
}
fn escape_comment(text: &str) -> String {
text.replace(['\n', '\r'], " ")
}
fn py_type(properties: Option<&Map<String, Value>>, name: &str) -> &'static str {
let ty = properties
.and_then(|props| props.get(name))
.and_then(|schema| schema.get("type"))
.and_then(Value::as_str);
match ty {
Some("string") => "str",
Some("integer") => "int",
Some("number") => "float",
Some("boolean") => "bool",
Some("array") => "list",
Some("object") => "dict",
_ => "Any",
}
}
fn one_line(text: &str) -> String {
text.split_whitespace().collect::<Vec<_>>().join(" ")
}
#[cfg(test)]
mod tests {
use super::*;
use async_trait::async_trait;
use std::sync::Arc;
struct DemoTool;
#[async_trait]
impl Tool for DemoTool {
fn name(&self) -> &str {
"search"
}
fn description(&self) -> &str {
"Search the web\nfor a query."
}
fn parameters_schema(&self) -> Option<Value> {
Some(serde_json::json!({
"type": "object",
"properties": {
"query": {"type": "string"},
"limit": {"type": "integer"}
},
"required": ["query"]
}))
}
async fn execute(
&self,
_ctx: Arc<dyn adk_core::ToolContext>,
_args: Value,
) -> adk_core::Result<Value> {
Ok(Value::Null)
}
}
#[test]
fn every_tool_is_rendered_as_a_call_tool_args_dict_with_collapsed_description() {
let entry = tool_entry(&DemoTool);
assert!(
entry.contains("call_tool(\"search\", {\"query\": <str>, \"limit\": <int, optional>})"),
"unexpected entry: {entry}"
);
assert!(!entry.contains("def "), "tools must not be rendered as `def`: {entry}");
assert!(entry.contains("# Search the web for a query."), "unexpected entry: {entry}");
}
struct HyphenTool;
#[async_trait]
impl Tool for HyphenTool {
fn name(&self) -> &str {
"fetch-cart"
}
fn description(&self) -> &str {
"Fetch a cart."
}
fn parameters_schema(&self) -> Option<Value> {
Some(serde_json::json!({
"type": "object",
"properties": { "user_id": {"type": "string"} },
"required": ["user_id"]
}))
}
async fn execute(
&self,
_ctx: Arc<dyn adk_core::ToolContext>,
_args: Value,
) -> adk_core::Result<Value> {
Ok(Value::Null)
}
}
#[test]
fn a_non_identifier_name_uses_the_same_call_tool_form() {
let entry = tool_entry(&HyphenTool);
assert!(
entry.contains("call_tool(\"fetch-cart\", {\"user_id\": <str>})"),
"unexpected entry: {entry}"
);
assert!(!entry.contains("def fetch-cart"), "no bare def for a hyphenated name: {entry}");
}
struct NoArgTool;
#[async_trait]
impl Tool for NoArgTool {
fn name(&self) -> &str {
"ping"
}
fn description(&self) -> &str {
""
}
async fn execute(
&self,
_ctx: Arc<dyn adk_core::ToolContext>,
_args: Value,
) -> adk_core::Result<Value> {
Ok(Value::Null)
}
}
#[test]
fn a_zero_parameter_tool_takes_no_args_dict() {
let entry = tool_entry(&NoArgTool);
assert!(entry.contains("call_tool(\"ping\")\n"), "unexpected entry: {entry}");
assert!(!entry.contains("call_tool(\"ping\", {"), "zero-arg tool needs no dict: {entry}");
}
struct KeywordParamTool;
#[async_trait]
impl Tool for KeywordParamTool {
fn name(&self) -> &str {
"classify"
}
fn description(&self) -> &str {
"Classify input."
}
fn parameters_schema(&self) -> Option<Value> {
Some(serde_json::json!({
"type": "object",
"properties": { "class": {"type": "string"} },
"required": ["class"]
}))
}
async fn execute(
&self,
_ctx: Arc<dyn adk_core::ToolContext>,
_args: Value,
) -> adk_core::Result<Value> {
Ok(Value::Null)
}
}
#[test]
fn a_keyword_named_parameter_is_just_a_dict_key() {
let entry = tool_entry(&KeywordParamTool);
assert!(
entry.contains("call_tool(\"classify\", {\"class\": <str>})"),
"keyword-named param should be a plain string key: {entry}"
);
}
}