Skip to content

Commit d40e018

Browse files
committed
new: added shell action with user confirmation
1 parent d269352 commit d40e018

File tree

9 files changed

+175
-19
lines changed

9 files changed

+175
-19
lines changed

examples/shell/README.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Executes shell commands.
2+
3+
### Example Usage
4+
5+
```sh
6+
nerve -G "openai://gpt-4" -T shell -P 'find the process consuming more ram'
7+
```

examples/shell/task.yml

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using:
2+
- 'shell'
3+
- 'memory'
4+
- 'planning'
5+
- 'goal'
6+
7+
system_prompt: >
8+
You are an useful assistant that executes any task the user provides.
9+
10+

src/agent/mod.rs

+64-16
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ use serialization::xml::serialize;
1111
use state::{SharedState, State};
1212
use task::Task;
1313

14+
use crate::cli;
15+
1416
pub mod events;
1517
pub mod generator;
1618
pub mod namespaces;
@@ -66,6 +68,22 @@ impl Invocation {
6668
payload,
6769
}
6870
}
71+
72+
pub fn as_function_call_string(&self) -> String {
73+
let mut parts = vec![];
74+
75+
if let Some(payload) = &self.payload {
76+
parts.push(payload.to_owned());
77+
}
78+
79+
if let Some(attributes) = &self.attributes {
80+
for (name, value) in attributes {
81+
parts.push(format!("{}={}", name, value))
82+
}
83+
}
84+
85+
return format!("{}({})", &self.action, parts.join(", "));
86+
}
6987
}
7088

7189
pub struct Agent {
@@ -362,22 +380,52 @@ impl Agent {
362380
Duration::from_secs(60 * 60 * 24 * 30)
363381
};
364382

365-
// execute with timeout
366-
let start = std::time::Instant::now();
367-
let ret = tokio::time::timeout(
368-
timeout,
369-
action.run(
370-
self.state.clone(),
371-
inv.attributes.to_owned(),
372-
inv.payload.to_owned(),
373-
),
374-
)
375-
.await;
376-
377-
if ret.is_err() {
378-
self.on_timed_out_action(inv, &start).await;
379-
} else {
380-
self.on_executed_action(inv, ret.unwrap(), &start).await;
383+
let mut execute = true;
384+
385+
if action.requires_user_confirmation() {
386+
log::warn!("user confirmation required");
387+
388+
let start = std::time::Instant::now();
389+
let mut inp = "nope".to_string();
390+
while inp != "" && inp != "n" && inp != "y" {
391+
inp = cli::get_user_input(&format!(
392+
"{} [Yn] ",
393+
inv.as_function_call_string()
394+
))
395+
.to_ascii_lowercase();
396+
}
397+
398+
if inp == "n" {
399+
log::warn!("invocation rejected by user");
400+
self.on_executed_action(
401+
inv.clone(),
402+
Err(anyhow!("rejected by user".to_owned())),
403+
&start,
404+
)
405+
.await;
406+
407+
execute = false;
408+
}
409+
}
410+
411+
if execute {
412+
// execute with timeout
413+
let start = std::time::Instant::now();
414+
let ret = tokio::time::timeout(
415+
timeout,
416+
action.run(
417+
self.state.clone(),
418+
inv.attributes.to_owned(),
419+
inv.payload.to_owned(),
420+
),
421+
)
422+
.await;
423+
424+
if ret.is_err() {
425+
self.on_timed_out_action(inv, &start).await;
426+
} else {
427+
self.on_executed_action(inv, ret.unwrap(), &start).await;
428+
}
381429
}
382430
}
383431
}

src/agent/namespaces/mod.rs

+7
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pub(crate) mod http;
1515
pub(crate) mod memory;
1616
pub(crate) mod planning;
1717
pub(crate) mod rag;
18+
pub(crate) mod shell;
1819
pub(crate) mod task;
1920
pub(crate) mod time;
2021

@@ -31,6 +32,7 @@ lazy_static! {
3132
map.insert("filesystem".to_string(), filesystem::get_namespace as fn() -> Namespace);
3233
map.insert("rag".to_string(), rag::get_namespace as fn() -> Namespace);
3334
map.insert("http".to_string(), http::get_namespace as fn() -> Namespace);
35+
map.insert("shell".to_string(), shell::get_namespace as fn() -> Namespace);
3436

3537
map
3638
};
@@ -181,6 +183,11 @@ pub(crate) trait Action: std::fmt::Debug + Sync + Send + ActionClone {
181183
fn required_variables(&self) -> Option<Vec<String>> {
182184
None
183185
}
186+
187+
// optional method to indicate if this action requires user confirmation before execution
188+
fn requires_user_confirmation(&self) -> bool {
189+
false
190+
}
184191
}
185192

186193
// https://stackoverflow.com/questions/30353462/how-to-clone-a-struct-storing-a-boxed-trait-object

src/agent/namespaces/shell/mod.rs

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
use std::collections::HashMap;
2+
3+
use anyhow::Result;
4+
use async_trait::async_trait;
5+
use tokio::process::Command;
6+
7+
use crate::agent::state::SharedState;
8+
9+
use super::{Action, Namespace};
10+
11+
#[derive(Debug, Default, Clone)]
12+
struct Shell {}
13+
14+
#[async_trait]
15+
impl Action for Shell {
16+
fn name(&self) -> &str {
17+
"shell"
18+
}
19+
20+
fn description(&self) -> &str {
21+
include_str!("shell.prompt")
22+
}
23+
24+
fn example_payload(&self) -> Option<&str> {
25+
Some("ls -la")
26+
}
27+
28+
fn requires_user_confirmation(&self) -> bool {
29+
// this one definitely does
30+
true
31+
}
32+
33+
async fn run(
34+
&self,
35+
_: SharedState,
36+
_: Option<HashMap<String, String>>,
37+
payload: Option<String>,
38+
) -> Result<Option<String>> {
39+
let command = payload.unwrap();
40+
// TODO: make the shell configurable
41+
let output = Command::new("/bin/sh")
42+
.arg("-c")
43+
.arg(&command)
44+
.output()
45+
.await?;
46+
47+
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
48+
println!("{}", &stdout);
49+
50+
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
51+
if !stderr.is_empty() {
52+
eprintln!("{}", stderr);
53+
}
54+
55+
let result = format!(
56+
"Exit Code: {}\n\nStdout:\n{}\n\nStderr:\n{}",
57+
output.status.code().unwrap_or(-1),
58+
stdout,
59+
stderr
60+
);
61+
62+
log::debug!("{}", &result);
63+
64+
Ok(Some(result))
65+
}
66+
}
67+
68+
pub(crate) fn get_namespace() -> Namespace {
69+
Namespace::new_non_default(
70+
"Shell".to_string(),
71+
include_str!("ns.prompt").to_string(),
72+
vec![Box::<Shell>::default()],
73+
None,
74+
)
75+
}

src/agent/namespaces/shell/ns.prompt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Use this action to execute shell commands and get their output.

src/agent/namespaces/shell/shell.prompt

Whitespace-only changes.

src/cli.rs

-2
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,6 @@ impl Args {
140140
}
141141

142142
pub(crate) fn get_user_input(prompt: &str) -> String {
143-
log::warn!("user prompt input required");
144-
145143
print!("\n{}", prompt);
146144
let _ = io::stdout().flush();
147145

src/main.rs

+11-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,17 @@ const APP_VERSION: &str = env!("CARGO_PKG_VERSION");
1515
#[tokio::main]
1616
async fn main() -> Result<()> {
1717
// TODO: save/restore session
18-
let args = cli::Args::parse();
18+
let mut args = cli::Args::parse();
19+
20+
// set generator url if env variable is set
21+
if let Ok(env_generator) = std::env::var("NERVE_GENERATOR") {
22+
args.generator = env_generator;
23+
}
24+
25+
// set tasklet if env variable is set
26+
if let Ok(env_tasklet) = std::env::var("NERVE_TASKLET") {
27+
args.tasklet = Some(env_tasklet);
28+
}
1929

2030
// TODO: handle max tokens
2131

0 commit comments

Comments
 (0)