use crate::domain::agent_output::{ActionInvocation, AgentOutput};
use ras_tools::domain::registry::ActionRegistry;
pub fn salvage_into(output: &mut AgentOutput, registry: &ActionRegistry) {
if !output.action.is_empty() {
return;
}
let salvaged: Vec<ActionInvocation> = salvage_actions(&output.current_state.next_goal)
.into_iter()
.filter(|a| registry.get(&a.name).is_some())
.collect();
if !salvaged.is_empty() {
tracing::warn!(
count = salvaged.len(),
"salvaged schema-valid action(s) from next_goal (model misplaced the action)"
);
output.action = salvaged;
}
}
#[must_use]
pub fn salvage_actions(text: &str) -> Vec<ActionInvocation> {
let Some(json) = json_slice(text) else {
return Vec::new();
};
if let Ok(out) = serde_json::from_str::<AgentOutput>(json)
&& !out.action.is_empty()
{
return out.action;
}
if let Ok(actions) = serde_json::from_str::<Vec<ActionInvocation>>(json)
&& !actions.is_empty()
{
return actions;
}
if let Ok(action) = serde_json::from_str::<ActionInvocation>(json) {
return vec![action];
}
Vec::new()
}
fn json_slice(text: &str) -> Option<&str> {
let start = text.find(['{', '['])?;
let end = text.rfind(['}', ']'])?;
(end > start).then(|| &text[start..=end])
}
#[cfg(test)]
mod tests {
use super::salvage_actions;
#[test]
fn salvages_action_from_a_nested_agent_output() {
let ng = r#"{"current_state":{"next_goal":""},"action":[{"name":"type_text","parameters":{"index":83}}]}"#;
let out = salvage_actions(ng);
assert_eq!(out.len(), 1);
assert_eq!(out[0].name.0.as_str(), "type_text");
}
#[test]
fn salvages_a_bare_action_object_wrapped_in_prose() {
let out =
salvage_actions("I will: {\"name\":\"click_element\",\"parameters\":{\"index\":5}}");
assert_eq!(out.len(), 1);
assert_eq!(out[0].name.0.as_str(), "click_element");
}
#[test]
fn no_actions_from_plain_prose() {
assert!(salvage_actions("I will now click the search button").is_empty());
}
#[test]
fn salvage_into_keeps_only_registry_valid_actions() {
use super::salvage_into;
use ras_tools::{ActionRegistry, register_default_actions};
let mut registry = ActionRegistry::new();
register_default_actions(&mut registry).expect("register default actions");
let mut output =
output_with_next_goal(r#"{"name":"type_text","parameters":{"index":1,"text":"x"}}"#);
salvage_into(&mut output, ®istry);
assert_eq!(output.action.len(), 1, "a registered action is salvaged");
assert_eq!(output.action[0].name.0.as_str(), "type_text");
let mut unknown =
output_with_next_goal(r#"{"name":"definitely_not_a_tool","parameters":{}}"#);
salvage_into(&mut unknown, ®istry);
assert!(
unknown.action.is_empty(),
"an unregistered action is dropped"
);
}
fn output_with_next_goal(next_goal: &str) -> super::AgentOutput {
use crate::domain::agent_output::AgentBrain;
super::AgentOutput {
current_state: AgentBrain {
evaluation_previous_goal: String::new(),
memory: String::new(),
next_goal: next_goal.into(),
},
action: vec![],
plan: None,
current_plan_item: None,
}
}
}