use std::collections::HashSet;
use std::io::{self, IsTerminal, Write};
use std::sync::{Arc, Mutex};
use onetool::runtime::sandbox::policy::{Action, Decision, Policy};
use tracing_subscriber::EnvFilter;
const MODEL: &str = "deepseek-chat";
struct AskUserPolicy {
use_colors: bool,
always_allowed: Mutex<HashSet<String>>,
}
impl AskUserPolicy {
fn new(use_colors: bool) -> Self {
Self {
use_colors,
always_allowed: Mutex::new(HashSet::new()),
}
}
fn prompt_user(&self, label: &str) -> Decision {
{
let allowed = self.always_allowed.lock().unwrap();
if allowed.contains(label) {
return Decision::Allow;
}
}
let yellow = if self.use_colors { "\x1b[33m" } else { "" };
let bold = if self.use_colors { "\x1b[1m" } else { "" };
let reset = if self.use_colors { "\x1b[0m" } else { "" };
eprint!(
"{}{}⚠️ Allow {}?{} [y]es / [n]o / [a]lways: ",
bold, yellow, label, reset
);
io::stderr().flush().ok();
let mut input = String::new();
if io::stdin().read_line(&mut input).is_err() {
return Decision::Deny("Failed to read input".to_string());
}
match input.trim().to_lowercase().as_str() {
"y" | "yes" => Decision::Allow,
"a" | "always" => {
self.always_allowed
.lock()
.unwrap()
.insert(label.to_string());
Decision::Allow
}
_ => Decision::Deny(format!("User denied access to {}", label)),
}
}
}
impl Policy for AskUserPolicy {
fn check_access(&self, action: &Action) -> Decision {
match action {
Action::CallFunction { name, .. } => self.prompt_user(name),
}
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.init();
if std::env::var("DEEPSEEK_API_KEY").is_err() {
eprintln!("Error: DEEPSEEK_API_KEY environment variable is not set.");
eprintln!("Please set it before running this example:");
eprintln!(" export DEEPSEEK_API_KEY=your_key_here");
return Err("Missing DEEPSEEK_API_KEY".into());
}
let use_colors = std::io::stdout().is_terminal();
print_banner(use_colors);
let policy = Arc::new(AskUserPolicy::new(use_colors));
let repl = onetool::Repl::new_with_policy(policy).expect("Failed to create REPL");
let genai_client = genai::Client::default();
let lua_repl = onetool::genai::LuaRepl::new(&repl);
let mut conversation_history: Vec<genai::chat::ChatMessage> = Vec::new();
let dim_cyan = if use_colors { "\x1b[2m\x1b[36m" } else { "" };
let reset = if use_colors { "\x1b[0m" } else { "" };
println!(
"{}Type your prompt and press Enter. Type 'exit' or 'quit' to end the session.{}",
dim_cyan, reset
);
println!();
loop {
match read_user_input(use_colors)? {
UserInput::Empty => continue,
UserInput::Exit => {
let bold_green = if use_colors { "\x1b[1m\x1b[32m" } else { "" };
let reset = if use_colors { "\x1b[0m" } else { "" };
println!("\n{}👋 Goodbye!{}\n", bold_green, reset);
break;
}
UserInput::Command(user_prompt) => {
process_command(
user_prompt,
use_colors,
&mut conversation_history,
&genai_client,
&lua_repl,
)
.await?;
}
}
}
Ok(())
}
async fn process_command(
user_prompt: String,
use_colors: bool,
conversation_history: &mut Vec<genai::chat::ChatMessage>,
genai_client: &genai::Client,
lua_repl: &onetool::genai::LuaRepl<'_>,
) -> Result<(), Box<dyn std::error::Error>> {
conversation_history.push(genai::chat::ChatMessage::user(user_prompt));
loop {
let chat_req = genai::chat::ChatRequest::new(conversation_history.clone())
.with_tools(vec![lua_repl.definition()]);
tracing::debug!("Requesting response from model");
let chat_res = genai_client.exec_chat(MODEL, chat_req, None).await?;
let tool_calls = chat_res.clone().into_tool_calls();
if tool_calls.is_empty() {
let answer = chat_res.first_text().unwrap_or("(no response)");
print_cell(
answer,
&CellRenderer::new(CellType::Answer, use_colors),
"(no answer)",
);
conversation_history.push(genai::chat::ChatMessage::assistant(answer));
break;
}
conversation_history.push(tool_calls.clone().into());
for tool_call in &tool_calls {
let source_code = match tool_call.fn_arguments.get("source_code") {
Some(serde_json::Value::String(code)) => code.as_str(),
_ => {
print_cell(
"Tool call missing 'source_code' parameter",
&CellRenderer::new(CellType::Error, use_colors),
"",
);
continue;
}
};
print_cell(
source_code,
&CellRenderer::new(CellType::Code, use_colors),
"(no code generated)",
);
tracing::debug!("Executing tool call");
let tool_response = lua_repl.call(tool_call);
match serde_json::from_str::<serde_json::Value>(&tool_response.content) {
Ok(response_json) => {
match parse_tool_response(&response_json) {
Ok(output) => {
print_cell(
&output,
&CellRenderer::new(CellType::Output, use_colors),
"(no output)",
);
}
Err(error) => {
print_cell(&error, &CellRenderer::new(CellType::Error, use_colors), "");
}
}
conversation_history.push(tool_response.into());
}
Err(e) => {
print_cell(
&format!("Failed to parse tool response: {}", e),
&CellRenderer::new(CellType::Error, use_colors),
"",
);
conversation_history.push(tool_response.into());
}
}
}
}
Ok(())
}
const CELL_WIDTH: usize = 100;
fn pad_and_truncate_line(line: &str, width: usize) -> String {
if line.chars().count() <= width {
format!("{:width$}", line, width = width)
} else {
let truncated: String = line.chars().take(width - 3).collect();
format!("{}...", truncated)
}
}
#[derive(Clone, Copy)]
enum CellType {
Code,
Output,
Error,
Answer,
}
struct CellRenderer {
icon: &'static str,
label: &'static str,
color: &'static str,
dim_color: &'static str,
reset: &'static str,
bold: &'static str,
}
impl CellRenderer {
fn new(cell_type: CellType, colors_enabled: bool) -> Self {
let (icon, label, color) = match cell_type {
CellType::Code => ("💻", "Generated Code", "\x1b[32m"),
CellType::Output => ("⚡", "Execution Output", "\x1b[33m"),
CellType::Error => ("❌", "Error", "\x1b[31m"),
CellType::Answer => ("✨", "Answer", "\x1b[36m"),
};
if colors_enabled {
Self {
icon,
label,
color,
dim_color: "\x1b[2m",
reset: "\x1b[0m",
bold: "\x1b[1m",
}
} else {
Self {
icon,
label,
color: "",
dim_color: "",
reset: "",
bold: "",
}
}
}
}
fn print_cell(content: &str, renderer: &CellRenderer, empty_msg: &str) {
let width = CELL_WIDTH;
let content_width = width - 4;
println!(
"{}{}{} {}{}",
renderer.bold, renderer.color, renderer.icon, renderer.label, renderer.reset
);
println!(
"{}{}┌{}┐{}",
renderer.dim_color,
renderer.color,
"─".repeat(width - 2),
renderer.reset
);
let content = if content.is_empty() {
empty_msg
} else {
content
};
for line in content.lines() {
let padded = pad_and_truncate_line(line, content_width);
println!(
"{}{}│{}{} {} {}│{}",
renderer.dim_color,
renderer.color,
renderer.reset,
renderer.color,
padded,
renderer.dim_color,
renderer.reset
);
}
println!(
"{}{}└{}┘{}",
renderer.dim_color,
renderer.color,
"─".repeat(width - 2),
renderer.reset
);
println!();
}
fn print_banner(colors_enabled: bool) {
let bold_cyan = if colors_enabled {
"\x1b[1m\x1b[36m"
} else {
""
};
let reset = if colors_enabled { "\x1b[0m" } else { "" };
println!(
"{}╔══════════════════════════════════════════════════════════════════════════════════════════════════╗{}",
bold_cyan, reset
);
println!(
"{}║ ONETOOL NOTEBOOK DEMO • Interactive LLM-Powered Lua REPL ║{}",
bold_cyan, reset
);
println!(
"{}╚══════════════════════════════════════════════════════════════════════════════════════════════════╝{}",
bold_cyan, reset
);
println!();
}
enum UserInput {
Command(String),
Exit,
Empty,
}
fn read_user_input(colors_enabled: bool) -> io::Result<UserInput> {
let bold_blue = if colors_enabled {
"\x1b[1m\x1b[34m"
} else {
""
};
let reset = if colors_enabled { "\x1b[0m" } else { "" };
print!("{}>>> {}", bold_blue, reset);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(UserInput::Empty)
} else if trimmed.eq_ignore_ascii_case("exit") || trimmed.eq_ignore_ascii_case("quit") {
Ok(UserInput::Exit)
} else {
Ok(UserInput::Command(trimmed.to_string()))
}
}
fn parse_tool_response(response_json: &serde_json::Value) -> Result<String, String> {
if let Some(error) = response_json.get("error") {
if let Some(error_msg) = error.as_str() {
return Err(error_msg.to_string());
}
}
let output_text = response_json["output"].as_str().unwrap_or("");
let result_text = response_json["result"].as_str().unwrap_or("");
let combined_output = if !output_text.is_empty() && !result_text.is_empty() {
format!("{}\n\nResult: {}", output_text, result_text)
} else if !output_text.is_empty() {
output_text.to_string()
} else if !result_text.is_empty() {
result_text.to_string()
} else {
"(no output or result)".to_string()
};
Ok(combined_output)
}