use clap::{Parser, Subcommand}; use indicatif::{ProgressBar, ProgressStyle}; use serde::{Deserialize, Serialize}; use std::fs; use std::path::{Path, PathBuf}; use std::time::Duration; #[derive(Parser)] #[command(name = "kpn")] #[command(about = "Kipinä Agent Local CLI", long_about = None)] struct Cli { #[command(subcommand)] command: Commands, } #[derive(Subcommand)] enum Commands { /// Alustaa uuden Kipinä-agenttikansion nykyiseen projektiin Init { #[arg(short, long, default_value = "kipina-tasks")] dir: String, }, /// Ajaa `.md` tiedostossa kuvatun tehtävän Kipinä-verkoston kautta Run { /// Polku `.md` työtiedostoon file: String, }, } #[derive(Debug, Deserialize, Serialize)] struct Frontmatter { agent: Option, status: Option, context: Option>, } #[derive(Serialize)] struct CompletionRequest { model: String, prompt: String, task_id: String, } #[derive(Deserialize)] struct CompletionResponse { response: String, model: String, tokens_generated: u64, } #[tokio::main] async fn main() { let cli = Cli::parse(); match &cli.command { Commands::Init { dir } => { let path = Path::new(dir); if !path.exists() { fs::create_dir_all(path).unwrap(); let example = format!("---\nstatus: open\nagent: qwen-coder-3b\ncontext: []\n---\n\n# Tehtävä\nKirjoita tähän mitä haluat verkon koodaavan."); fs::write(path.join("01-esimerkki.md"), example).unwrap(); println!("✅ Alustettu lokaali agenttikansio: {}", dir); } else { println!("⚠️ Kansio {} on jo olemassa.", dir); } } Commands::Run { file } => { if let Err(e) = run_workflow(file).await { eprintln!("❌ Virhe: {}", e); } } } } async fn run_workflow(filepath: &str) -> Result<(), Box> { let content = fs::read_to_string(filepath)?; // Yksinkertainen frontmatter-parseri let mut frontmatter_str = String::new(); let mut body = String::new(); let mut in_frontmatter = false; let mut fm_found = false; for line in content.lines() { if line.trim() == "---" { if !fm_found { in_frontmatter = true; fm_found = true; continue; } else if in_frontmatter { in_frontmatter = false; continue; } } if in_frontmatter { frontmatter_str.push_str(line); frontmatter_str.push('\n'); } else { body.push_str(line); body.push('\n'); } } let meta: Frontmatter = if fm_found { serde_yaml::from_str(&frontmatter_str).unwrap_or(Frontmatter { agent: None, status: None, context: None }) } else { Frontmatter { agent: None, status: None, context: None } }; let model = meta.agent.unwrap_or_else(|| "qwen-coder-05b".to_string()); // Kerätään kontekstitiedostot let mut mega_prompt = body.trim().to_string(); if let Some(ctx_files) = meta.context { mega_prompt.push_str("\n\n=== KONTEKSTI ===\n"); for ctx in ctx_files { if let Ok(c) = fs::read_to_string(&ctx) { mega_prompt.push_str(&format!("\n--- Tiedosto: {} ---\n{}\n", ctx, c)); } else { println!("⚠️ Varoitus: Kontekstitiedostoa {} ei löytynyt.", ctx); } } } println!("\n🚀 Lähetetään tehtävä Kipinäverkkoon (Malli: {})", model); let pb = ProgressBar::new_spinner(); pb.enable_steady_tick(Duration::from_millis(100)); pb.set_style( ProgressStyle::with_template("{spinner:.green} [{elapsed_precise}] {msg}") .unwrap() .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]), ); pb.set_message("Odotetaan verkon solmua ja laskentaa..."); let task_id = uuid::Uuid::new_v4().to_string(); let client = reqwest::Client::new(); let req = CompletionRequest { model: model.clone(), prompt: mega_prompt.clone(), task_id: task_id.clone(), }; let res = client.post("http://localhost:3000/api/v1/chat/completions") .json(&req) .send() .await?; if res.status().is_success() { let completion: CompletionResponse = res.json().await?; pb.finish_with_message(format!("Tulos saapui verkolta! ({} tokenia)", completion.tokens_generated)); let new_content = format!("{}\n\n## Kipinä Agentin Ratkaisu\n{}\n", content, completion.response); let updated_content = new_content.replace("status: open", "status: done"); fs::write(filepath, updated_content)?; println!("✅ Vastaus tallennettu tiedostoon: {}", filepath); } else { pb.finish_with_message("❌ Verkkopyyntö epäonnistui!"); println!("Virhekoodi: {}", res.status()); } Ok(()) }