feat: add CLI agent with file and command tools

- Add agent module with file operations and command execution tools
- Implement create_file, delete_file, read_file, and execute_command tools
- Add colored terminal output and user confirmation for commands
- Update dependencies: dotenv, serde, serde_json, thiserror
- Add .env.example with DEEPSEEK_API_KEY template
- Add CLAUDE.md project documentation
- Refactor main.rs to use agent system with interactive prompt
This commit is contained in:
YinMo19 2025-11-20 17:21:06 +08:00
parent 43896a0028
commit bf4e66ca36
7 changed files with 267 additions and 2 deletions

View File

@ -0,0 +1 @@
DEEPSEEK_API_KEY=

3
CLAUDE.md Normal file
View File

@ -0,0 +1,3 @@
这是一个基于 rust 和 rig 的 命令行 agent 工具项目。
只做最小化修改,不要做任何多余动作,代码保持量最小,最简洁,最有函数式风格。用中文回复我,但是在代码中只能用英文做注释。在做完之后考虑使用 cargo build 来编译看看是否通过。不允许创造任何 markdown 文档,你直接梗概你所做事情即可。

10
Cargo.lock generated
View File

@ -112,7 +112,11 @@ name = "course_4"
version = "0.1.0"
dependencies = [
"colored",
"dotenv",
"rig-core",
"serde",
"serde_json",
"thiserror",
"tokio",
]
@ -127,6 +131,12 @@ dependencies = [
"syn",
]
[[package]]
name = "dotenv"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
[[package]]
name = "dyn-clone"
version = "1.0.20"

View File

@ -5,5 +5,9 @@ edition = "2024"
[dependencies]
colored = "3.0.0"
dotenv = "0.15.0"
rig-core = "0.24.0"
serde = "1.0.228"
serde_json = "1.0.145"
thiserror = "2.0.17"
tokio = { version = "1.48.0", features = ["full"] }

199
src/agent.rs Normal file
View File

@ -0,0 +1,199 @@
use colored::Colorize;
use rig::completion::ToolDefinition;
use rig::tool::Tool;
use serde::{Deserialize, Serialize};
use std::io::Write;
use serde_json::json;
use std::fs;
use std::process::Command;
pub const ROBOT_SYSTEM_PROMPT: &'static str = "You are a helpful file and command assistant. You can create, read, delete files, and execute shell commands. Always respond in Chinese.";
// Error types
#[derive(Debug, thiserror::Error)]
pub enum ToolError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Command execution failed: {0}")]
CommandFailed(String),
}
// Create file tool
#[derive(Deserialize)]
pub struct CreateFileArgs {
path: String,
content: String,
}
#[derive(Serialize, Deserialize)]
pub struct CreateFileTool;
impl Tool for CreateFileTool {
const NAME: &'static str = "create_file";
type Error = ToolError;
type Args = CreateFileArgs;
type Output = String;
async fn definition(&self, _prompt: String) -> ToolDefinition {
ToolDefinition {
name: Self::NAME.to_string(),
description: "Create a new file with specified content".to_string(),
parameters: json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path of the file to create"
},
"content": {
"type": "string",
"description": "Content to write to the file"
}
},
"required": ["path", "content"]
}),
}
}
async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
fs::write(&args.path, &args.content)?;
println!("file created: {}", args.path);
Ok(format!("file created: {}", args.path))
}
}
// Delete file tool
#[derive(Deserialize)]
pub struct DeleteFileArgs {
path: String,
}
#[derive(Serialize, Deserialize)]
pub struct DeleteFileTool;
impl Tool for DeleteFileTool {
const NAME: &'static str = "delete_file";
type Error = ToolError;
type Args = DeleteFileArgs;
type Output = String;
async fn definition(&self, _prompt: String) -> ToolDefinition {
ToolDefinition {
name: Self::NAME.to_string(),
description: "Delete a file".to_string(),
parameters: json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path of the file to delete"
}
},
"required": ["path"]
}),
}
}
async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
fs::remove_file(&args.path)?;
println!("deleted: {}", args.path);
Ok(format!("deleted: {}", args.path))
}
}
// Read file tool
#[derive(Deserialize)]
pub struct ReadFileArgs {
path: String,
}
#[derive(Serialize, Deserialize)]
pub struct ReadFileTool;
impl Tool for ReadFileTool {
const NAME: &'static str = "read_file";
type Error = ToolError;
type Args = ReadFileArgs;
type Output = String;
async fn definition(&self, _prompt: String) -> ToolDefinition {
ToolDefinition {
name: Self::NAME.to_string(),
description: "Read the content of a file".to_string(),
parameters: json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path of the file to read"
}
},
"required": ["path"]
}),
}
}
async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
let content = fs::read_to_string(&args.path)?;
println!("file content:\n{}", content);
Ok(format!("file content:\n{}", content))
}
}
// Execute command tool
#[derive(Deserialize)]
pub struct ExecuteCommandArgs {
command: String,
}
#[derive(Serialize, Deserialize)]
pub struct ExecuteCommandTool;
impl Tool for ExecuteCommandTool {
const NAME: &'static str = "execute_command";
type Error = ToolError;
type Args = ExecuteCommandArgs;
type Output = String;
async fn definition(&self, _prompt: String) -> ToolDefinition {
ToolDefinition {
name: Self::NAME.to_string(),
description: "Execute a shell command. Use this only if file operations cannot complete the task.".to_string(),
parameters: json!({
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "Shell command to execute"
}
},
"required": ["command"]
}),
}
}
async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
let output = Command::new("sh").arg("-c").arg(&args.command).output()?;
println!("{}: {}", "agent will exec command".blue(), args.command);
print!("{}", "execute it? ok just press enter, or other key to abort:");
std::io::stdout().flush().expect("Failed to flush stdout");
let mut input = String::new();
std::io::stdin().read_line(&mut input).expect("Failed to read input");
if !input.trim().is_empty() {
return Err(ToolError::CommandFailed(format!("User canceled.")));
}
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
println!("{}: {}", "exec result".blue(), stdout);
if output.status.success() {
Ok(format!("exec success:\n{}", stdout))
} else {
Err(ToolError::CommandFailed(format!("{}\n{}", stdout, stderr)))
}
}
}

0
src/error.rs Normal file
View File

View File

@ -1,3 +1,51 @@
fn main() {
println!("Hello, world!");
use colored::Colorize;
use std::io::Write;
use rig::{completion::Prompt, providers::deepseek, client::CompletionClient};
mod agent;
fn get_prompt() -> String {
print!("{}", " > ".green());
std::io::stdout().flush().expect("Failed to flush stdout");
let mut input = String::new();
std::io::stdin()
.read_line(&mut input)
.expect("Failed to read line");
input.trim().to_string()
}
#[tokio::main]
async fn main() {
dotenv::dotenv().ok();
let client = deepseek::Client::from_env();
let deepseek_agent = client
.agent("deepseek-chat")
.preamble(agent::ROBOT_SYSTEM_PROMPT)
.tool(agent::CreateFileTool)
.tool(agent::DeleteFileTool)
.tool(agent::ReadFileTool)
.tool(agent::ExecuteCommandTool)
.build();
println!("{}", "Agent started (Ctrl+C to abort)".cyan());
loop {
print!("{}", "user prompt".magenta());
let user_prompt = get_prompt();
if user_prompt.is_empty() {
continue;
}
match deepseek_agent.prompt(&user_prompt).multi_turn(100).await {
Ok(response) => {
println!("{} {}", "agent >".green(), response);
}
Err(e) => {
eprintln!("{} {}", "error >".red(), e);
}
}
}
}