first commit
a tool that automatically organize your messy folders
This commit is contained in:
2
.env.example
Normal file
2
.env.example
Normal file
@@ -0,0 +1,2 @@
|
||||
GEMINI_API_KEY=
|
||||
DOWNLOAD_FOLDER=
|
||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
.env
|
||||
1596
Cargo.lock
generated
Normal file
1596
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
11
Cargo.toml
Normal file
11
Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "noentropy"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
dotenv = "0.15.0"
|
||||
reqwest = { version = "0.12.26", features = ["json"] }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.145"
|
||||
tokio = { version = "1.48.0", features = ["full"] }
|
||||
56
HACKATHON_REVIEW.md
Normal file
56
HACKATHON_REVIEW.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# Hackathon Review: noentropy
|
||||
|
||||
## Overall Assessment
|
||||
|
||||
**Score: 8.5/10**
|
||||
|
||||
`noentropy` is a highly effective and impressive hackathon project. Its core concept of using a Large Language Model (LLM) to automate the tedious task of file organization is both innovative and genuinely useful. The project is well-scoped for a hackathon, demonstrating a complete and functional loop from analyzing files to executing a plan.
|
||||
|
||||
### Strengths
|
||||
|
||||
* **High "Wow" Factor:** Demonstrates a practical and intelligent use of AI that solves a common problem. It's the kind of project that gets people excited.
|
||||
* **Practical Usefulness:** This isn't just a technical demo; it's a tool that people would actually want to use to manage their cluttered "Downloads" folders.
|
||||
* **Solid Technical Foundation:** The choice of Rust with `tokio` for asynchronous API calls is a good one, showing technical competence. The interaction with the Gemini API is direct and effective.
|
||||
* **Complete End-to-End Loop:** The program successfully scans files, communicates with an external API, parses the response, and acts on it.
|
||||
|
||||
## Suggested Improvements for a Winning Edge
|
||||
|
||||
This project is already strong, but the following improvements could elevate it from a great project to a potential winner.
|
||||
|
||||
### High-Impact Improvements
|
||||
|
||||
1. **Configuration File for Categories:**
|
||||
* **Problem:** The file categories (`Images`, `Documents`, etc.) are currently hardcoded in the prompt. This is inflexible.
|
||||
* **Solution:** Create a `config.toml` file where users can define their own categories and maybe even provide rules (e.g., "all `.jpg` files go to `Photos`"). This would make the tool dramatically more powerful and personalizable.
|
||||
|
||||
2. **Dry-Run Mode:**
|
||||
* **Problem:** Users, especially first-time users, will be hesitant to run a tool that automatically moves their files without knowing what it's going to do.
|
||||
* **Solution:** Add a `--dry-run` command-line flag. In this mode, the tool should print out the proposed file movements without actually touching any files. For example: `[DRY RUN] Would move 'report.pdf' to 'Documents/'`.
|
||||
|
||||
3. **Interactive Mode:**
|
||||
* **Problem:** The current process is fully automated. What if the AI makes a mistake?
|
||||
* **Solution:** Add an `--interactive` flag. After getting the plan from Gemini, the tool could present the plan to the user and ask for confirmation for each move or for categories of moves. `Move 5 files to 'Images'? [Y/n]`.
|
||||
|
||||
### Technical & Robustness Improvements
|
||||
|
||||
4. **Correct the Model Name:**
|
||||
* In `src/gemini.rs`, the model `gemini-3-flash-preview` is likely a typo. It should probably be `gemini-1.5-flash-preview` or another valid, available model.
|
||||
|
||||
5. **Robust API Response Parsing:**
|
||||
* **Problem:** The code manually traverses the JSON response from Gemini. If the API response structure changes even slightly, the program will crash.
|
||||
* **Solution:** Define Rust structs that mirror the *entire* Gemini API response and use `serde` to deserialize into them. This is far more resilient to API changes.
|
||||
|
||||
6. **Eliminate `.expect()`:**
|
||||
* **Problem:** The code uses `.expect()` in several places (e.g., for environment variables and creating directories). This can cause the program to panic unexpectedly.
|
||||
* **Solution:** Replace `.expect()` calls with proper `Result` handling and provide more user-friendly error messages. For example, if the `DOWNLOAD_FOLDER` isn't set, print a clear message telling the user how to set it.
|
||||
|
||||
7. **More Context for the LLM:**
|
||||
* **Problem:** Sending only filenames might not be enough for accurate categorization. Is `resume.pdf` a document or something else?
|
||||
* **Solution:** To improve accuracy, consider sending more metadata to Gemini. The prompt could include file size, creation date, or even the first few lines of text for file types like `.txt` or `.md`. (This would require more complex file handling but would make the AI's job easier).
|
||||
|
||||
### Feature Expansion
|
||||
|
||||
8. **Recursive Folder Processing:**
|
||||
* Add a `--recursive` or `-r` flag to allow the tool to organize files in subdirectories as well, not just the top-level directory.
|
||||
|
||||
By implementing a few of these suggestions, particularly the high-impact ones, `noentropy` could be a truly standout project. Great work!
|
||||
76
src/files.rs
Normal file
76
src/files.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{fs, path::Path, path::PathBuf};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct FileCategory {
|
||||
pub filename: String,
|
||||
pub category: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct OrganizationPlan {
|
||||
pub files: Vec<FileCategory>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FileBatch {
|
||||
pub filenames: Vec<String>,
|
||||
pub paths: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
impl FileBatch {
|
||||
/// Reads a directory path and populates lists of all files inside it.
|
||||
/// It skips sub-directories (does not read recursively).
|
||||
pub fn from_path(root_path: PathBuf) -> Self {
|
||||
let mut filenames = Vec::new();
|
||||
let mut paths = Vec::new();
|
||||
|
||||
// Check if the path exists and is a directory
|
||||
if root_path.is_dir() {
|
||||
// fs::read_dir returns a Result, so we must handle it
|
||||
if let Ok(read_dir) = fs::read_dir(&root_path) {
|
||||
for child in read_dir {
|
||||
if let Ok(child) = child {
|
||||
// We only want to list FILES, not sub-folders,
|
||||
// otherwise we might try to move a folder into a folder
|
||||
if child.path().is_file() {
|
||||
filenames.push(child.file_name().to_string_lossy().into_owned());
|
||||
paths.push(child.path());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileBatch { filenames, paths }
|
||||
}
|
||||
|
||||
/// Helper to get the number of files found
|
||||
pub fn count(&self) -> usize {
|
||||
self.filenames.len()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn execute_move(base_path: &Path, plan: OrganizationPlan) {
|
||||
for item in plan.files {
|
||||
let source = base_path.join(&item.filename);
|
||||
let target_dir = base_path.join(&item.category);
|
||||
let target = target_dir.join(&item.filename);
|
||||
|
||||
// 1. Create the category folder if it doesn't exist (e.g., "Downloads/Images")
|
||||
if !target_dir.exists() {
|
||||
fs::create_dir_all(&target_dir).expect("Failed to create folder");
|
||||
println!("Created folder: {:?}", item.category);
|
||||
}
|
||||
|
||||
// 2. Move the file
|
||||
if source.exists() {
|
||||
match fs::rename(&source, &target) {
|
||||
Ok(_) => println!("Moved: {} -> {}/", item.filename, item.category),
|
||||
Err(e) => println!("Failed to move {}: {}", item.filename, e),
|
||||
}
|
||||
} else {
|
||||
println!("Skipping: {} (File not found)", item.filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
65
src/gemini.rs
Normal file
65
src/gemini.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use crate::files::OrganizationPlan;
|
||||
use reqwest::Client;
|
||||
use serde_json::json;
|
||||
|
||||
pub struct GeminiClient {
|
||||
api_key: String,
|
||||
client: Client,
|
||||
base_url: String,
|
||||
}
|
||||
impl GeminiClient {
|
||||
pub fn new(api_key: String) -> Self {
|
||||
Self {
|
||||
api_key,
|
||||
client: Client::new(),
|
||||
base_url: "https://generativelanguage.googleapis.com/v1beta/models/gemini-3-flash-preview:generateContent".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Takes a list of filenames and asks Gemini to categorize them
|
||||
pub async fn organize_files(
|
||||
&self,
|
||||
filenames: Vec<String>,
|
||||
) -> Result<OrganizationPlan, Box<dyn std::error::Error>> {
|
||||
let url = format!("{}?key={}", self.base_url, self.api_key);
|
||||
|
||||
// 1. Construct the Prompt
|
||||
let file_list_str = filenames.join(", ");
|
||||
let prompt = format!(
|
||||
"I have these files in my Downloads folder: [{}]. \
|
||||
Categorize them into these folders: 'Images', 'Documents', 'Installers', 'Music', 'Archives', 'Code', 'Misc'. \
|
||||
Return ONLY a JSON object with this structure: {{ 'files': [ {{ 'filename': 'name', 'category': 'folder' }} ] }}",
|
||||
file_list_str
|
||||
);
|
||||
|
||||
// 2. Build Request with JSON Mode enforced
|
||||
let request_body = json!({
|
||||
"contents": [{
|
||||
"parts": [{ "text": prompt }]
|
||||
}],
|
||||
"generationConfig": {
|
||||
"response_mime_type": "application/json"
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Send
|
||||
let res = self.client.post(&url).json(&request_body).send().await?;
|
||||
|
||||
// 4. Parse
|
||||
if res.status().is_success() {
|
||||
let resp_json: serde_json::Value = res.json().await?;
|
||||
|
||||
// Extract the raw JSON string from Gemini
|
||||
let raw_text = resp_json["candidates"][0]["content"]["parts"][0]["text"]
|
||||
.as_str()
|
||||
.ok_or("Failed to get text from Gemini")?;
|
||||
|
||||
// Deserialize into our Rust Struct
|
||||
let plan: OrganizationPlan = serde_json::from_str(raw_text)?;
|
||||
Ok(plan)
|
||||
} else {
|
||||
let err = res.text().await?;
|
||||
Err(format!("API Error: {}", err).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
2
src/lib.rs
Normal file
2
src/lib.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod files;
|
||||
pub mod gemini;
|
||||
42
src/main.rs
Normal file
42
src/main.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use noentropy::files::FileBatch;
|
||||
use noentropy::files::OrganizationPlan;
|
||||
use noentropy::files::execute_move;
|
||||
use noentropy::gemini::GeminiClient;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
dotenv::dotenv().ok();
|
||||
let api_key = std::env::var("GEMINI_API_KEY").expect("KEY not set");
|
||||
let download_path_var = std::env::var("DOWNLOAD_FOLDER").expect("Set DOWNLOAD_FOLDER={path}");
|
||||
|
||||
// 1. Setup
|
||||
let download_path: PathBuf = PathBuf::from(download_path_var.to_string());
|
||||
let client: GeminiClient = GeminiClient::new(api_key);
|
||||
|
||||
// 2. Get Files (Using your previous FileBatch logic)
|
||||
// Assuming FileBatch::from_path returns a struct with .filenames
|
||||
let batch = FileBatch::from_path(download_path.clone());
|
||||
|
||||
if batch.filenames.is_empty() {
|
||||
println!("No files found to organize!");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!(
|
||||
"Found {} files. Asking Gemini to organize...",
|
||||
batch.filenames.len()
|
||||
);
|
||||
|
||||
// 3. Call Gemini
|
||||
let plan: OrganizationPlan = client.organize_files(batch.filenames).await?;
|
||||
|
||||
println!("Gemini Plan received! Moving files...");
|
||||
|
||||
// 4. Execute
|
||||
execute_move(&download_path, plan);
|
||||
|
||||
println!("Done!");
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user