diff --git a/network-poc/Cargo.toml b/network-poc/Cargo.toml index 5809e4a..ff1cd50 100644 --- a/network-poc/Cargo.toml +++ b/network-poc/Cargo.toml @@ -4,4 +4,4 @@ members = [ "hub", "node", "native-node" -] +, "cli"] diff --git a/network-poc/Dockerfile.prod b/network-poc/Dockerfile.prod index f3e4bbe..8bb7ba9 100644 --- a/network-poc/Dockerfile.prod +++ b/network-poc/Dockerfile.prod @@ -15,11 +15,13 @@ COPY Cargo.lock* ./ COPY hub/Cargo.toml hub/Cargo.toml COPY node/Cargo.toml node/Cargo.toml COPY native-node/Cargo.toml native-node/Cargo.toml +COPY cli/Cargo.toml cli/Cargo.toml # Kopioi lähdekoodi COPY hub/src hub/src COPY node/src node/src COPY native-node/src native-node/src +COPY cli/src cli/src COPY static static # Rakenna Wasm — cache mount pitää Cargo-rekisterin ja target-kansion buildien välillä diff --git a/network-poc/cli/Cargo.toml b/network-poc/cli/Cargo.toml new file mode 100644 index 0000000..07b8a51 --- /dev/null +++ b/network-poc/cli/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "cli" +version = "0.1.0" +edition = "2024" + +[dependencies] +clap = { version = "4.6.0", features = ["derive"] } +console = "0.16.3" +indicatif = "0.18.4" +reqwest = { version = "0.13.2", features = ["json"] } +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.149" +serde_yaml = "0.9.34" +tokio = { version = "1.50.0", features = ["rt-multi-thread", "macros"] } +uuid = { version = "1.23.0", features = ["v4"] } diff --git a/network-poc/cli/src/main.rs b/network-poc/cli/src/main.rs new file mode 100644 index 0000000..dc320f1 --- /dev/null +++ b/network-poc/cli/src/main.rs @@ -0,0 +1,165 @@ +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(()) +} diff --git a/network-poc/hub/Cargo.toml b/network-poc/hub/Cargo.toml index a2943f8..24715d2 100644 --- a/network-poc/hub/Cargo.toml +++ b/network-poc/hub/Cargo.toml @@ -5,7 +5,7 @@ edition = "2024" [dependencies] axum = { version = "0.7.4", features = ["ws", "macros"] } -tokio = { version = "1.36.0", features = ["full"] } +tokio = { version = "1.36.0", features = ["full", "sync"] } tower-http = { version = "0.5.2", features = ["fs", "cors", "trace"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/network-poc/hub/src/main.rs b/network-poc/hub/src/main.rs index bbfd1a5..bd5fd9c 100644 --- a/network-poc/hub/src/main.rs +++ b/network-poc/hub/src/main.rs @@ -339,6 +339,7 @@ async fn main() { .route("/api/sessions", get(api_sessions)) .route("/api/pairs", get(api_pairs)) .route("/api/stats", get(api_stats)) + .route("/api/v1/chat/completions", axum::routing::post(api_chat_completions)) .route("/admin", get(admin_page)) .nest_service("/", ServeDir::new(std::env::var("STATIC_DIR").unwrap_or_else(|_| "../static".to_string()))) .with_state(state); @@ -820,3 +821,50 @@ async fn handle_socket(socket: WebSocket, state: Arc, ip: IpAddr) { broadcast_stats(&state).await; sender_task.abort(); } +#[derive(serde::Deserialize)] +struct ChatCompletionRequest { + model: String, + prompt: String, + task_id: String, +} + +#[derive(serde::Serialize)] +struct ChatCompletionResponse { + response: String, + model: String, + tokens_generated: u64, +} + +async fn api_chat_completions( + axum::extract::State(state): axum::extract::State>, + axum::Json(payload): axum::Json, +) -> axum::response::Response { + let msg = serde_json::json!({ + "type": "llm_prompt", + "prompt": payload.prompt, + "model": payload.model, + "task_id": payload.task_id, + }); + + let mut rx = state.stats_tx.subscribe(); + let _ = state.stats_tx.send(msg.to_string()); + + while let Ok(msg_str) = rx.recv().await { + if let Ok(v) = serde_json::from_str::(&msg_str) { + if v["type"].as_str() == Some("llm_done") { + if let Some(tid) = v["task_id"].as_str() { + if tid == payload.task_id { + let res = ChatCompletionResponse { + response: v["response"].as_str().unwrap_or("").to_string(), + model: v["model"].as_str().unwrap_or("").to_string(), + tokens_generated: v["tokens_generated"].as_u64().unwrap_or(0), + }; + return axum::Json(res).into_response(); + } + } + } + } + } + + (axum::http::StatusCode::INTERNAL_SERVER_ERROR, "Network Error").into_response() +} diff --git a/network-poc/node/src/lib.rs b/network-poc/node/src/lib.rs index 542f180..3c05bba 100644 --- a/network-poc/node/src/lib.rs +++ b/network-poc/node/src/lib.rs @@ -298,12 +298,13 @@ pub async fn start_agent_node(hub_url: String, has_webgpu: bool, device_info_jso if LLM_BUSY.load(Ordering::SeqCst) { } else if let Ok(task) = serde_json::from_str::(&msg) { let prompt = task.get("prompt").and_then(|v| v.as_str()).unwrap_or("").to_string(); + let task_id = task.get("task_id").and_then(|v| v.as_str()).map(|s| s.to_string()); if !prompt.is_empty() { let use_3b = current_task == 5; LLM_BUSY.store(true, Ordering::SeqCst); let ws_for_async = ws_clone.clone(); wasm_bindgen_futures::spawn_local(async move { - qwen_coder::run_coder_inference(prompt, ws_for_async, use_3b).await; + qwen_coder::run_coder_inference(prompt, ws_for_async, use_3b, task_id).await; LLM_BUSY.store(false, Ordering::SeqCst); }); } diff --git a/network-poc/node/src/qwen_coder.rs b/network-poc/node/src/qwen_coder.rs index 5f6c0f5..0cf0c13 100644 --- a/network-poc/node/src/qwen_coder.rs +++ b/network-poc/node/src/qwen_coder.rs @@ -76,7 +76,7 @@ async fn ensure_cached(key: &str, url: &str, ws: &Rc>) -> Res } /// use_3b: false = 0.5B (nopea), true = 3B (laadukas) -pub async fn run_coder_inference(prompt: String, ws: Rc>, use_3b: bool) { +pub async fn run_coder_inference(prompt: String, ws: Rc>, use_3b: bool, task_id: Option) { let perf = web_sys::window().unwrap().performance().unwrap(); let size_label = if use_3b { "3B" } else { "0.5B" }; @@ -271,7 +271,7 @@ pub async fn run_coder_inference(prompt: String, ws: Rc>, use let tokens_per_sec = if gen_time > 0.0 { (tokens_generated as f64 / gen_time) * 1000.0 } else { 0.0 }; console_log!("[Coder] {} tokenia | {:.0}ms | {:.1} tok/s", tokens_generated, gen_time, tokens_per_sec); - let done = serde_json::json!({ + let mut done = serde_json::json!({ "type": "llm_done", "prompt": prompt, "model": format!("Qwen2.5-Coder-{}-Instruct", size_label), @@ -281,5 +281,8 @@ pub async fn run_coder_inference(prompt: String, ws: Rc>, use "tokens_per_sec": (tokens_per_sec * 10.0).round() / 10.0, "load_time_ms": (load_time * 100.0).round() / 100.0, }); + if let Some(tid) = task_id { + done.as_object_mut().unwrap().insert("task_id".to_string(), serde_json::json!(tid)); + } let _ = ws.borrow().send_with_str(&done.to_string()); } diff --git a/network-poc/static/avatars/old/forge_hero.png b/network-poc/static/avatars/old/forge_hero.png new file mode 100644 index 0000000..0a6e222 Binary files /dev/null and b/network-poc/static/avatars/old/forge_hero.png differ diff --git a/network-poc/static/avatars/old/gecko_hero.png b/network-poc/static/avatars/old/gecko_hero.png new file mode 100644 index 0000000..8968876 Binary files /dev/null and b/network-poc/static/avatars/old/gecko_hero.png differ diff --git a/network-poc/static/avatars/old/kipina.png b/network-poc/static/avatars/old/kipina.png new file mode 100644 index 0000000..8a56588 Binary files /dev/null and b/network-poc/static/avatars/old/kipina.png differ diff --git a/network-poc/static/avatars/old/serpent_hero.png b/network-poc/static/avatars/old/serpent_hero.png new file mode 100644 index 0000000..a6831d1 Binary files /dev/null and b/network-poc/static/avatars/old/serpent_hero.png differ diff --git a/network-poc/static/index.html b/network-poc/static/index.html index bc080a7..5384df5 100644 --- a/network-poc/static/index.html +++ b/network-poc/static/index.html @@ -368,6 +368,53 @@ color: #8b949e; margin-top: 2px; } + .terminal-panel { + background:#010409; + border:1px solid var(--border-color); + border-radius:6px; + padding:15px; + font-family: 'Courier New', Courier, monospace; + font-size:14px; + color:var(--success-color); + height:500px; + overflow-y:auto; + text-align:left; + } + .terminal-line { margin: 4px 0; } + .terminal-prompt { color: #d29922; } + .avatar-grid { + display:flex; + gap:15px; + justify-content:center; + margin-bottom:20px; + } + .avatar-card { + background:var(--panel-bg); + border:1px solid var(--border-color); + border-radius:8px; + padding:10px; + text-align:center; + width:120px; + opacity: 0.6; + transition: all 0.3s; + } + .avatar-card img { + width:80px; + height:80px; + border-radius:50%; + margin-bottom:10px; + border:2px solid var(--border-color); + } + .avatar-card.active { + opacity: 1; + transform: translateY(-5px); + } + .avatar-card.active img { + border-color:var(--accent-color); + box-shadow: 0 0 15px var(--accent-color); + } + .avatar-name { font-weight: bold; font-size: 13px; color: var(--text-color); } + .avatar-role { font-size: 11px; color: #8b949e; margin-top: 2px; } @@ -379,6 +426,7 @@
Laskentaverkko
Koodilaboratorio
+
Agents & CLI
@@ -663,6 +711,45 @@ + +
+
+
+
+ Kipinä Agent Workspace + Monitoring Active +
+ +
+
+ Forge +
KPN CLI
+
Paikallinen Ohjaus
+
+
+ Gecko +
Qwen-Coder
+
Koodiagentti
+
+
+ Serpent +
SmolLM
+
Logiikka
+
+
+ Discord +
Swarm
+
WebGPU Solmu
+
+
+ +
+
$ kpn hub connect wss://localhost
+
✓ Yhdistetty Kipinä Hubiin
+
+
+
+