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"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"colored",
|
"colored",
|
||||||
|
"dotenv",
|
||||||
"rig-core",
|
"rig-core",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -127,6 +131,12 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dotenv"
|
||||||
|
version = "0.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dyn-clone"
|
name = "dyn-clone"
|
||||||
version = "1.0.20"
|
version = "1.0.20"
|
||||||
|
|||||||
@ -5,5 +5,9 @@ edition = "2024"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
colored = "3.0.0"
|
colored = "3.0.0"
|
||||||
|
dotenv = "0.15.0"
|
||||||
rig-core = "0.24.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"] }
|
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() {
|
use colored::Colorize;
|
||||||
println!("Hello, world!");
|
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