feat(hardware): enhance tool call parsing and dynamic hardware tool listing

This commit is contained in:
ehushubhamshaw 2026-02-26 20:30:05 -05:00
parent b46efd37f7
commit bfc0c01cd5
4 changed files with 67 additions and 15 deletions

View File

@ -673,7 +673,16 @@ fn parse_tool_calls(response: &str) -> (String, Vec<ParsedToolCall>) {
}
if !parsed_any {
tracing::warn!("Malformed <tool_call> JSON: expected tool-call object in tag body");
// Fallback: try shorthand `toolname{args}` format inside the closed tag.
if let Some(call) = parse_shorthand_tag_call(inner) {
tracing::debug!(
tool = %call.name,
"parsed shorthand tool call (toolname{{args}} format)"
);
calls.push(call);
} else {
tracing::warn!("Malformed <tool_call> JSON: expected tool-call object in tag body");
}
}
remaining = &after_open[close_idx + close_tag.len()..];

View File

@ -554,16 +554,43 @@ pub fn build_system_prompt(
|| *name == "arduino_upload"
});
if has_hardware {
prompt.push_str(
// Build the hardware tool list dynamically so the prompt only mentions
// the tools that are actually present (gpio_read, gpio_write, arduino_upload).
let hw_tools: Vec<&str> = tools
.iter()
.filter_map(|(name, _)| {
if *name == "gpio_read" || *name == "gpio_write" || *name == "arduino_upload" {
Some(*name)
} else {
None
}
})
.collect();
let hw_names = hw_tools.join(", ");
let _ = write!(
prompt,
"## Hardware Access\n\n\
You HAVE direct access to connected hardware. The user owns this system and has configured it.\n\
All hardware tools (gpio_read, gpio_write) are AUTHORIZED and NOT blocked by security.\n\
All hardware tools ({hw_names}) are AUTHORIZED and NOT blocked by security.\n\
When they ask to control LEDs or interact with hardware, USE the tools do NOT refuse or say you cannot access physical devices.\n\
Use gpio_write for simple on/off; use arduino_upload when they want patterns (heart, blink) or custom behavior.\n\n\
Available hardware tools:\n\
- gpio_write: set a GPIO pin HIGH or LOW. Use this to turn on/off LEDs and control output pins.\n\
- gpio_read: read the current state of a GPIO pin.\n\n\
To turn on the Pico onboard LED: gpio_write(device=pico0, pin=25, value=1)\n\
Available hardware tools:\n"
);
if hw_tools.contains(&"gpio_write") {
prompt.push_str(
"- gpio_write: set a GPIO pin HIGH or LOW. Use this to turn on/off LEDs and control output pins.\n",
);
}
if hw_tools.contains(&"gpio_read") {
prompt.push_str("- gpio_read: read the current state of a GPIO pin.\n");
}
if hw_tools.contains(&"arduino_upload") {
prompt.push_str(
"- arduino_upload: upload a sketch or program to an Arduino board.\n",
);
}
prompt.push_str(
"\nTo turn on the Pico onboard LED: gpio_write(device=pico0, pin=25, value=1)\n\
To turn it off: gpio_write(device=pico0, pin=25, value=0)\n\n",
);
}

View File

@ -52,9 +52,13 @@ def handle(msg):
if pin_num == 25:
led.value(value)
else:
# Always (re-)configure as OUT so subsequent reads on this pin
# reflect the driven state rather than clobbering the direction.
pins_cache[pin_num] = Pin(pin_num, Pin.OUT)
# Reuse a cached Pin object when available to avoid repeated
# allocations; re-initialise direction to OUT in case it was
# previously opened as IN by gpio_read.
if pin_num in pins_cache:
pins_cache[pin_num].init(mode=Pin.OUT)
else:
pins_cache[pin_num] = Pin(pin_num, Pin.OUT)
pins_cache[pin_num].value(value)
state = "HIGH" if value == 1 else "LOW"
return {"ok": True, "data": {"pin": pin_num, "value": value, "state": state}}
@ -88,9 +92,11 @@ while True:
msg = json.loads(line)
result = handle(msg)
print(json.dumps(result))
except (ValueError, KeyError, TypeError) as e:
# ValueError — json.loads() on malformed input
# KeyError — unexpected missing key in a message dict
# TypeError — wrong type in an operation
except (ValueError, KeyError, TypeError, OSError, AttributeError) as e:
# ValueError — json.loads() on malformed input
# KeyError — unexpected missing key in a message dict
# TypeError — wrong type in an operation
# OSError — GPIO/hardware errors from Pin()/Pin.value()
# AttributeError — msg.get(...) called on non-dict JSON value
# Any other exception propagates so bugs surface during development.
print(json.dumps({"ok": False, "error": str(e)}))

View File

@ -28,6 +28,10 @@ use tokio::time::{timeout, Duration};
/// Subprocess timeout — kill the child process after this many seconds.
const SUBPROCESS_TIMEOUT_SECS: u64 = 10;
/// Timeout for waiting on child process exit after stdout has been read.
/// Prevents a hung cleanup phase from blocking indefinitely.
const PROCESS_EXIT_TIMEOUT_SECS: u64 = 5;
/// A tool backed by an external subprocess.
///
/// The binary receives the LLM-supplied JSON arguments on stdin (one line,
@ -226,7 +230,13 @@ impl Tool for SubprocessTool {
// Let the process finish naturally — plugins that write their
// result and then do cleanup should not be interrupted.
Ok(Ok(line)) => {
let child_status = child.wait().await.ok();
let child_status = timeout(
Duration::from_secs(PROCESS_EXIT_TIMEOUT_SECS),
child.wait(),
)
.await
.ok()
.and_then(|r| r.ok());
let stderr_msg = collect_stderr(stderr_handle).await;
let line = line.trim();