10_file_saving/
10_file_saving.rs

1//! Agent with File Saving Tool Example
2//!
3//! This example demonstrates how to create a custom tool that can save
4//! content to files, allowing the agent to persist generated output.
5//! Files are organized in a dedicated output directory.
6//!
7//! Run with: cargo run --example 10_file_saving --manifest-path ceylon/Cargo.toml
8
9use ceylon_next::agent::Agent;
10use ceylon_next::tasks::{OutputData, TaskRequest};
11use ceylon_next::tools::ToolTrait;
12use serde_json::{json, Value};
13use std::fs;
14use std::path::Path;
15
16// Define a custom tool for saving content to files
17struct FileSaverTool;
18
19impl ToolTrait for FileSaverTool {
20    fn name(&self) -> String {
21        "file_saver".to_string()
22    }
23
24    fn description(&self) -> String {
25        "A tool that saves content to a file. \
26         Accepts 'filename' (path to save the file) and 'content' (text content to save). \
27         Returns success status and the file path."
28            .to_string()
29    }
30
31    fn input_schema(&self) -> Value {
32        json!({
33            "type": "object",
34            "properties": {
35                "filename": {
36                    "type": "string",
37                    "description": "The path where the file should be saved (e.g., 'output.txt', 'data/results.json')"
38                },
39                "content": {
40                    "type": "string",
41                    "description": "The text content to write to the file"
42                }
43            },
44            "required": ["filename", "content"]
45        })
46    }
47
48    fn execute(&self, input: Value) -> Value {
49        let filename = input["filename"].as_str().unwrap_or("output.txt");
50        let content = input["content"].as_str().unwrap_or("");
51
52        // Create parent directories if they don't exist
53        if let Some(parent) = Path::new(filename).parent() {
54            if !parent.as_os_str().is_empty() {
55                if let Err(e) = fs::create_dir_all(parent) {
56                    return json!({
57                        "success": false,
58                        "filename": filename,
59                        "error": format!("Failed to create directory: {}", e)
60                    });
61                }
62            }
63        }
64
65        // Write content to file
66        match fs::write(filename, content) {
67            Ok(_) => json!({
68                "success": true,
69                "filename": filename,
70                "bytes_written": content.len(),
71                "message": format!("Successfully saved {} bytes to {}", content.len(), filename)
72            }),
73            Err(e) => json!({
74                "success": false,
75                "filename": filename,
76                "error": format!("Failed to write file: {}", e)
77            }),
78        }
79    }
80}
81
82#[tokio::main]
83async fn main() {
84    println!("šŸ¤– Ceylon Agent - File Saving Example\n");
85
86    // Define the output directory
87    let output_dir = "test/web_project_output";
88
89    // Create the output directory if it doesn't exist
90    if let Err(e) = fs::create_dir_all(output_dir) {
91        eprintln!("āŒ Failed to create output directory: {}", e);
92        return;
93    }
94    println!("šŸ“ Output directory: {}/\n", output_dir);
95
96    // Step 1: Create an agent
97    let mut agent = Agent::new("ContentGenerator", "ollama::gemma3:latest");
98
99    agent.with_system_prompt(
100        &format!(
101            "You are a helpful assistant that can generate content and save it to files. \
102             When asked to create content, generate it and use the file_saver tool to save it. \
103             \
104             IMPORTANT: All files MUST be saved inside the '{}' directory. \
105             For example, to save 'server.js', use '{}/server.js' as the filename. \
106             For files in subdirectories, use '{}/src/app.js' format. \
107             \
108             Be clear about what you're creating and where you're saving it.",
109            output_dir, output_dir, output_dir
110        )
111    );
112
113    // Step 2: Register the file saver tool
114    println!("šŸ”§ Registering file saver tool...");
115    agent.add_tool(FileSaverTool);
116    println!("āœ“ Tool registered\n");
117
118    // Step 3: Create a task that generates content and saves it
119    let mut task = TaskRequest::new(
120        &format!(
121            "Create a simple web project with a file upload API. Save all files in the '{}' directory:\
122             \n1. server.js - A Node.js Express server with POST /upload endpoint\
123             \n2. package.json - Package configuration with dependencies\
124             \n3. test.http - HTTP test file to test the upload endpoint\
125             \n4. README.md - Instructions on how to install and run the server\
126             \n\nMake sure ALL files are saved in the '{}' directory!",
127            output_dir, output_dir
128        )
129    );
130    task.with_name("Generate and Save Web Project")
131        .with_description("Generate a complete web project with file upload API")
132        .with_priority(7);
133
134    // Step 4: Run the agent
135    println!("šŸ“‹ Task: {}\n", task.id());
136    println!("šŸ”„ Running agent...\n");
137
138    let response = agent.run(task).await;
139
140    // Step 5: Display the result
141    match response.result() {
142        OutputData::Text(answer) => {
143            println!("\nšŸ“ Agent Response:\n{}\n", answer);
144        }
145        _ => {
146            println!("āŒ Unexpected response type");
147        }
148    }
149
150    // Step 6: Verify files were created
151    println!("šŸ” Checking created files in '{}/'\n", output_dir);
152
153    let expected_files = vec![
154        "server.js",
155        "package.json",
156        "test.http",
157        "README.md"
158    ];
159
160    let mut created_count = 0;
161
162    for file in &expected_files {
163        let file_path = format!("{}/{}", output_dir, file);
164        if Path::new(&file_path).exists() {
165            created_count += 1;
166            println!("āœ“ {} created successfully", file);
167
168            if let Ok(content) = fs::read_to_string(&file_path) {
169                let preview: String = content.chars().take(100).collect();
170                println!("  Preview: {}", preview.replace('\n', " "));
171            }
172        } else {
173            println!("⚠ {} not found", file);
174        }
175    }
176
177    println!("\nšŸ“Š Summary: {}/{} expected files created", created_count, expected_files.len());
178
179    // Show directory structure
180    println!("\nšŸ“‚ Directory structure:");
181    if let Ok(entries) = fs::read_dir(output_dir) {
182        for entry in entries.flatten() {
183            let path = entry.path();
184            if path.is_file() {
185                if let Some(filename) = path.file_name() {
186                    if let Ok(metadata) = entry.metadata() {
187                        println!("  - {} ({} bytes)",
188                                 filename.to_string_lossy(),
189                                 metadata.len());
190                    }
191                }
192            }
193        }
194    }
195
196    println!("\nāœ… Example completed successfully!");
197    println!("šŸ“ All files are in: {}/", output_dir);
198}