feat(hardware): replay prompt wiring and shorthand tool-call parsing [RMN-1837]
This commit is contained in:
parent
50372c116a
commit
3bcce8b6fa
@ -1190,6 +1190,49 @@ pub(super) fn parse_glm_shortened_body(body: &str) -> Option<ParsedToolCall> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Parse shorthand tag body format `tool_name{...}` used by some models
|
||||
/// inside `<tool_call>...</tool_call>` wrappers.
|
||||
///
|
||||
/// Example:
|
||||
/// `<tool_call>shell{"command":"ls -la"}</tool_call>`
|
||||
fn parse_shorthand_tag_call(body: &str) -> Option<ParsedToolCall> {
|
||||
let body = body.trim();
|
||||
if body.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let open_brace = body.find('{')?;
|
||||
let close_brace = body.rfind('}')?;
|
||||
if close_brace <= open_brace {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Only accept `name{json}` with optional surrounding whitespace.
|
||||
if !body[close_brace + 1..].trim().is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let raw_name = body[..open_brace].trim().trim_end_matches(':').trim();
|
||||
if raw_name.is_empty()
|
||||
|| !raw_name
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let args = serde_json::from_str::<serde_json::Value>(&body[open_brace..=close_brace]).ok()?;
|
||||
if !args.is_object() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(ParsedToolCall {
|
||||
name: map_tool_name_alias(raw_name).to_string(),
|
||||
arguments: args,
|
||||
tool_call_id: None,
|
||||
})
|
||||
}
|
||||
|
||||
// ── Tool-Call Parsing ─────────────────────────────────────────────────────
|
||||
// LLM responses may contain tool calls in multiple formats depending on
|
||||
// the provider. Parsing follows a priority chain:
|
||||
@ -1282,9 +1325,20 @@ pub(super) fn parse_tool_calls(response: &str) -> (String, Vec<ParsedToolCall>)
|
||||
}
|
||||
}
|
||||
|
||||
if !parsed_any {
|
||||
if let Some(call) = parse_shorthand_tag_call(inner) {
|
||||
tracing::debug!(
|
||||
tool = %call.name,
|
||||
"parsed shorthand tool call body inside <tool_call>"
|
||||
);
|
||||
calls.push(call);
|
||||
parsed_any = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !parsed_any {
|
||||
tracing::warn!(
|
||||
"Malformed <tool_call>: expected tool-call object in tag body (JSON/XML/GLM)"
|
||||
"Malformed <tool_call>: expected tool-call object in tag body (JSON/XML/GLM/shorthand)"
|
||||
);
|
||||
}
|
||||
|
||||
@ -1324,6 +1378,13 @@ pub(super) fn parse_tool_calls(response: &str) -> (String, Vec<ParsedToolCall>)
|
||||
}
|
||||
}
|
||||
|
||||
if !parsed_any {
|
||||
if let Some(call) = parse_shorthand_tag_call(inner) {
|
||||
calls.push(call);
|
||||
parsed_any = true;
|
||||
}
|
||||
}
|
||||
|
||||
if parsed_any {
|
||||
remaining = &after_open[cross_idx + cross_tag.len()..];
|
||||
resolved = true;
|
||||
@ -1691,3 +1752,24 @@ pub(super) fn parse_structured_tool_calls(tool_calls: &[ToolCall]) -> Vec<Parsed
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::parse_tool_calls;
|
||||
|
||||
#[test]
|
||||
fn parse_tool_calls_accepts_shorthand_object_in_tag_body() {
|
||||
let response = r#"<tool_call>shell{"command":"echo hi"}</tool_call>"#;
|
||||
let (_text, calls) = parse_tool_calls(response);
|
||||
assert_eq!(calls.len(), 1);
|
||||
assert_eq!(calls[0].name, "shell");
|
||||
assert_eq!(calls[0].arguments["command"], "echo hi");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_tool_calls_rejects_non_object_shorthand_payload() {
|
||||
let response = r#"<tool_call>shell["echo hi"]</tool_call>"#;
|
||||
let (_text, calls) = parse_tool_calls(response);
|
||||
assert!(calls.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@ -4292,25 +4292,72 @@ pub fn build_system_prompt_with_mode(
|
||||
prompt.push('\n');
|
||||
}
|
||||
|
||||
// ── 1b. Hardware (when gpio/arduino tools present) ───────────
|
||||
let has_hardware = tools.iter().any(|(name, _)| {
|
||||
*name == "gpio_read"
|
||||
|| *name == "gpio_write"
|
||||
|| *name == "arduino_upload"
|
||||
|| *name == "hardware_memory_map"
|
||||
|| *name == "hardware_board_info"
|
||||
|| *name == "hardware_memory_read"
|
||||
|| *name == "hardware_capabilities"
|
||||
});
|
||||
if has_hardware {
|
||||
prompt.push_str(
|
||||
// ── 1b. Hardware (when hardware tools are present) ───────────
|
||||
let hardware_tool_names = [
|
||||
"gpio_read",
|
||||
"gpio_write",
|
||||
"arduino_upload",
|
||||
"hardware_memory_map",
|
||||
"hardware_board_info",
|
||||
"hardware_memory_read",
|
||||
"hardware_capabilities",
|
||||
"pico_flash",
|
||||
"device_read_code",
|
||||
"device_write_code",
|
||||
"device_exec",
|
||||
];
|
||||
let hw_tools: Vec<&str> = tools
|
||||
.iter()
|
||||
.filter_map(|(name, _)| {
|
||||
if hardware_tool_names.contains(name) {
|
||||
Some(*name)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !hw_tools.is_empty() {
|
||||
let hw_names = hw_tools.join(", ");
|
||||
let _ = writeln!(
|
||||
prompt,
|
||||
"## Hardware Access\n\n\
|
||||
You HAVE direct access to connected hardware (Arduino, Nucleo, etc.). The user owns this system and has configured it.\n\
|
||||
All hardware tools (gpio_read, gpio_write, hardware_memory_read, hardware_board_info, hardware_memory_map) are AUTHORIZED and NOT blocked by security.\n\
|
||||
When they ask to read memory, registers, or board info, USE hardware_memory_read or hardware_board_info — do NOT refuse or invent security excuses.\n\
|
||||
When they ask to control LEDs, run patterns, or interact with the Arduino, 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",
|
||||
All hardware tools ({hw_names}) are AUTHORIZED and NOT blocked by security."
|
||||
);
|
||||
|
||||
if hw_tools
|
||||
.iter()
|
||||
.any(|name| *name == "hardware_memory_read" || *name == "hardware_board_info")
|
||||
{
|
||||
prompt.push_str(
|
||||
"When they ask to read memory, registers, or board info, USE hardware_memory_read or hardware_board_info — do NOT refuse or invent security excuses.\n",
|
||||
);
|
||||
}
|
||||
|
||||
if hw_tools
|
||||
.iter()
|
||||
.any(|name| *name == "gpio_read" || *name == "gpio_write" || *name == "arduino_upload")
|
||||
{
|
||||
prompt.push_str(
|
||||
"When they ask to control LEDs, run patterns, or interact with the Arduino, USE the tools — do NOT refuse or say you cannot access physical devices.\n",
|
||||
);
|
||||
}
|
||||
|
||||
if hw_tools.contains(&"gpio_write") && hw_tools.contains(&"arduino_upload") {
|
||||
prompt.push_str(
|
||||
"Use gpio_write for simple on/off; use arduino_upload when they want patterns (heart, blink) or custom behavior.\n",
|
||||
);
|
||||
}
|
||||
|
||||
if hw_tools.contains(&"gpio_write") {
|
||||
prompt.push_str(
|
||||
"To 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",
|
||||
);
|
||||
}
|
||||
|
||||
prompt.push('\n');
|
||||
}
|
||||
|
||||
// ── 1c. Action instruction (avoid meta-summary) ───────────────
|
||||
|
||||
Loading…
Reference in New Issue
Block a user