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:
parent
43896a0028
commit
bf4e66ca36
@ -0,0 +1 @@
|
||||
DEEPSEEK_API_KEY=
|
||||
3
CLAUDE.md
Normal file
3
CLAUDE.md
Normal file
@ -0,0 +1,3 @@
|
||||
这是一个基于 rust 和 rig 的 命令行 agent 工具项目。
|
||||
|
||||
只做最小化修改,不要做任何多余动作,代码保持量最小,最简洁,最有函数式风格。用中文回复我,但是在代码中只能用英文做注释。在做完之后考虑使用 cargo build 来编译看看是否通过。不允许创造任何 markdown 文档,你直接梗概你所做事情即可。
|
||||
10
Cargo.lock
generated
10
Cargo.lock
generated
@ -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"
|
||||
|
||||
@ -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
199
src/agent.rs
Normal 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
0
src/error.rs
Normal file
52
src/main.rs
52
src/main.rs
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user