From 3d2b2342e0d2f60481433737c90d94049ad81528 Mon Sep 17 00:00:00 2001 From: jaakko Date: Fri, 3 Apr 2026 09:56:18 +0300 Subject: [PATCH] FI/SV/EN kielituki about valmis (testaamatta) --- network-poc/REQUIREMENTS.md | 50 +++++++++++++++--- network-poc/hub/src/main.rs | 34 +++++++----- network-poc/node/src/qwen_coder.rs | 6 ++- network-poc/static/index.html | 85 ++++++++++++++++++++++++++++-- 4 files changed, 147 insertions(+), 28 deletions(-) diff --git a/network-poc/REQUIREMENTS.md b/network-poc/REQUIREMENTS.md index 48d3ba1..fe9c2d3 100644 --- a/network-poc/REQUIREMENTS.md +++ b/network-poc/REQUIREMENTS.md @@ -26,9 +26,14 @@ Tässä on kooste projektin vaatimuksista, työtehtävistä ja niiden nykytilant - Sijoittaa Hub-palvelin julkisesti saatavuusosoitteeseen `kipina.studio`. ### Tehtävät -- [ ] Tuotantopalvelimen käyttöönotto Nginxin tai Docker-compose kautta ehtojen täytyttyä -- [ ] Turvamekanismin lisäys: Varmistetaan, ettei kukaan lähetä "falskeja" vastauksia nodeilta -- [ ] Solmuille rekisteröitymismekanismi tai tulostaulukko +- [x] Tuotantopalvelimen käyttöönotto Docker-compose + Caddy TLS kautta (`kipina.studio`) +- [x] Deploy-skripti (`deploy.sh`) + Discord-webhook-notifikaatio julkaisuista +- [x] Admin-dashboard (`/admin`) Basic Auth -suojattuna, live-sessiot ja metriikat +- [x] REST API (`POST /api/v1/chat/completions`) task_id-pohjaisella vastausten reitityksellä +- [x] API timeout (120s) + selkeät virheilmoitukset (504 Gateway Timeout) +- [x] IP-pohjainen rate limiting (max 4 yhteyttä/IP) + origin-validointi +- [ ] Turvamekanismin lisäys: Varmistetaan, ettei kukaan lähetä "falskeja" vastauksia nodeilta (PoW/challenge-response) +- [x] SQLite-sessioseuranta (node_sessions + pair_results) --- @@ -53,7 +58,38 @@ Tässä on kooste projektin vaatimuksista, työtehtävistä ja niiden nykytilant - Kyetä lataamaan selaimen IndexedDB:hen satojen megatavujen painot massivisena fetch-hakuna, kääntää ne WebGPU-puskureihin (Buffers) ja suorittaa tekstigeneraatiota etänä ohjattuna verkosta käsin WebSocketia myöden. ### Tehtävät -- [ ] Refaktoroi Wasm-Noden (Burn.rs) paketti tuomaan Text-Tokenizerit (esim. BPE) ja kielimallin arkkitehtuuri käyttöön -- [ ] Koodaa Nodeen logiikka hakea / kasata mallin painot välimuistista "Chunk"-lohkoina valmiiksi -- [ ] Hub uudistetaan generoimaan pelkkien matikkavaikeuksien sijasta Text Prompts (esim. "Kirjoita haiku Suomesta") ja reitittämään työkuorman vapaalle solmulle -- [ ] Kipinän käyttöliittymään Chat-ikkuna Hubin striimaamien tulossanojen tarkkailuun reaaliajassa +- [x] Refaktoroi Wasm-Noden (Burn.rs) paketti tuomaan Text-Tokenizerit (BPE, Qwen2.5-Coder) ja kielimallin arkkitehtuuri käyttöön +- [x] Koodaa Nodeen logiikka hakea / kasata mallin painot välimuistista IndexedDB:hen (tokenizer.json + model weights) +- [x] Hub uudistetaan generoimaan Text Prompts ja reitittämään työkuorman vapaalle solmulle (broadcast + task_id-matching) +- [x] Kipinän käyttöliittymään Chat-ikkuna Hubin striimaamien tulossanojen tarkkailuun reaaliajassa (llm_chunk streaming) +- [x] SmolLM 135M — täysi transformer (Burn), ~1.2 tok/s CPU +- [x] Qwen2.5 0.5B — Candle-inferenssi, ChatML-muotoilu, ~0.4 tok/s CPU +- [x] Qwen2.5-Coder 0.5B & 3B — koodigeneraatio, streaming-tokenit, task_id-tuki +- [x] Phi-3 Mini — placeholder (liian suuri selaimelle, natiivisolmulle suunnitteilla) +- [x] EN/FI tokenisaatiovertailu overhead-laskennalla +- [x] Natiivisolmu (Rust + CUDA) — Qwen2.5 0.5B, ~50-100 tok/s RTX 4090, NVML GPU-metriikat + +--- + +## 🚀 Vaihe 6: Agent Workspace & CLI (KÄYNNISSÄ) + +### Tavoitteet +- Interaktiivinen terminaalipohjainen käyttöliittymä `kpn`-komennoilla. +- Agenttitiimi (Koodari, Testaaja, Manageri) muokattavilla system prompteilla. +- Agenttien ketjutus: manageri analysoi → koodari toteuttaa → testaaja arvioi. + +### Tehtävät +- [x] KPN-terminaali selaimeen (interaktiivinen komentorivi, komentohistoria) +- [x] `kpn run ""` — tehtävän lähetys REST API:n kautta +- [x] `kpn hello` — tervehdyskomento +- [x] `kpn pipeline ""` — manageri → koodari → testaaja -ketjutus +- [x] `kpn status`, `kpn models`, `kpn clear`, `kpn help` +- [x] Agenttikortit (Koodari/Qwen-Coder, Testaaja/SmolLM, Manageri/KPN CLI) +- [x] Muokattavat system promptit per agentti (localStorage-tallennus) +- [x] Multi-select: yhteinen konteksti useammalle agentille +- [x] Streaming-vastaukset terminaalissa (llm_chunk + vilkkuva kursori) +- [x] URL-hash navigointi (`#agents`, `#codelab`, `#network`) +- [x] SPA fallback (ServeDir + ServeFile) +- [ ] Agenttien välinen keskustelu (manageri ohjaa koodaria ja testaajaa dynaamisesti) +- [ ] Tehtävähistoria ja tulosten tallennus +- [ ] CLI-työkalu (`kpn` binary) lokaaliin käyttöön diff --git a/network-poc/hub/src/main.rs b/network-poc/hub/src/main.rs index e98fe7f..7a330cc 100644 --- a/network-poc/hub/src/main.rs +++ b/network-poc/hub/src/main.rs @@ -851,23 +851,29 @@ async fn api_chat_completions( 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(); + + let timeout = tokio::time::timeout(std::time::Duration::from_secs(120), async move { + 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 { + return Some(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), + }); + } } } } } + None + }).await; + + match timeout { + Ok(Some(res)) => axum::Json(res).into_response(), + Ok(None) => (axum::http::StatusCode::INTERNAL_SERVER_ERROR, "Verkkovirhe: yhteys katkesi").into_response(), + Err(_) => (axum::http::StatusCode::GATEWAY_TIMEOUT, "Aikakatkaisu: yksikään solmu ei vastannut 120s sisällä").into_response(), } - - (axum::http::StatusCode::INTERNAL_SERVER_ERROR, "Network Error").into_response() } diff --git a/network-poc/node/src/qwen_coder.rs b/network-poc/node/src/qwen_coder.rs index 0cf0c13..acbae2c 100644 --- a/network-poc/node/src/qwen_coder.rs +++ b/network-poc/node/src/qwen_coder.rs @@ -225,7 +225,8 @@ pub async fn run_coder_inference(prompt: String, ws: Rc>, use if next_token != eos_token { if let Ok(text) = tokenizer.decode(&[next_token], true) { generated_text.push_str(&text); - let chunk = serde_json::json!({ "type": "llm_chunk", "token": text, "prompt": prompt, "model": "Qwen2.5-Coder" }); + let mut chunk = serde_json::json!({ "type": "llm_chunk", "token": text, "prompt": prompt, "model": "Qwen2.5-Coder" }); + if let Some(ref tid) = task_id { chunk.as_object_mut().unwrap().insert("task_id".to_string(), serde_json::json!(tid)); } let _ = ws.borrow().send_with_str(&chunk.to_string()); } tokens_generated += 1; @@ -258,7 +259,8 @@ pub async fn run_coder_inference(prompt: String, ws: Rc>, use if let Ok(text) = tokenizer.decode(&[next_token], true) { generated_text.push_str(&text); - let chunk = serde_json::json!({ "type": "llm_chunk", "token": text, "prompt": prompt, "model": "Qwen2.5-Coder" }); + let mut chunk = serde_json::json!({ "type": "llm_chunk", "token": text, "prompt": prompt, "model": "Qwen2.5-Coder" }); + if let Some(ref tid) = task_id { chunk.as_object_mut().unwrap().insert("task_id".to_string(), serde_json::json!(tid)); } let _ = ws.borrow().send_with_str(&chunk.to_string()); } tokens_generated += 1; diff --git a/network-poc/static/index.html b/network-poc/static/index.html index 906fdbd..a89499c 100644 --- a/network-poc/static/index.html +++ b/network-poc/static/index.html @@ -120,6 +120,7 @@ .main-panel.active { display: block; } @keyframes spin { to { transform: rotate(360deg); } } + @keyframes blink { 0%,100% { opacity:1; } 50% { opacity:0; } } .code-output { font-family: 'Courier New', Courier, monospace; @@ -1286,36 +1287,87 @@ termPanel.scrollTop = termPanel.scrollHeight; } - async function kpnRun(model, prompt) { + // Aktiiviset streaming-rivit task_id:n mukaan + const activeStreams = {}; + + // Lähettää promptin mallille ja palauttaa vastauksen (tai null virhetilanteessa) + async function kpnRun(model, prompt, silent) { termLog(` → ${model} käsittelee...`, '#8b949e'); try { const taskId = crypto.randomUUID(); - // Liitetään yhteinen konteksti + agentin oma system prompt const agent = Object.values(agentPrompts).find(a => a.model === model); const parts = []; if (sharedPrompt) parts.push(sharedPrompt); if (agent && agent.prompt) parts.push(agent.prompt); parts.push(prompt); const fullPrompt = parts.join('\n\n'); + + // Luodaan streaming-rivi terminaaliin + if (!silent) { + const streamDiv = document.createElement('div'); + streamDiv.className = 'terminal-line'; + streamDiv.style.color = '#c9d1d9'; + streamDiv.innerHTML = ' '; + termPanel.appendChild(streamDiv); + termPanel.scrollTop = termPanel.scrollHeight; + activeStreams[taskId] = streamDiv; + } + const res = await fetch('/api/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model, prompt: fullPrompt, task_id: taskId }), }); + + // Poistetaan streaming-rivi + if (activeStreams[taskId]) { + activeStreams[taskId].remove(); + delete activeStreams[taskId]; + } + if (!res.ok) { - termLog(` ✗ Virhe: ${res.status} ${res.statusText}`, '#f85149'); - return; + const errText = await res.text().catch(() => res.statusText); + termLog(` ✗ ${errText}`, '#f85149'); + return null; } const data = await res.json(); const response = (data.response || '').trim(); const tokGen = data.tokens_generated || 0; termLog(` ${data.model || model} (${tokGen} tok)`); - termLog(` ${response.replace(/━━━ Pipeline käynnistyy ━━━`); + + // Vaihe 1: Manageri analysoi + termLog(`\n[1/3] Manageri — tehtävän analyysi`); + const managerPrompt = `Analysoi seuraava ohjelmistokehitystehtävä ja kirjoita koodarille selkeä tekninen ohje mitä koodata. Vastaa lyhyesti.\n\nTehtävä: ${task}`; + const plan = await kpnRun(agentPrompts.manager.model, managerPrompt); + if (!plan) { termLog(' ✗ Pipeline keskeytyi (manageri)', '#f85149'); return; } + + // Vaihe 2: Koodari toteuttaa + termLog(`\n[2/3] Koodari — toteutus`); + const coderPrompt = `${plan}\n\nKirjoita koodi yllä olevan ohjeen mukaisesti.`; + const code = await kpnRun(agentPrompts.coder.model, coderPrompt); + if (!code) { termLog(' ✗ Pipeline keskeytyi (koodari)', '#f85149'); return; } + + // Vaihe 3: Testaaja arvioi + termLog(`\n[3/3] Testaaja — arviointi`); + const testerPrompt = `Arvioi seuraava koodi lyhyesti. Onko siinä bugeja? Puuttuuko testejä? Anna arvosana 1-5.\n\nTehtävä: ${task}\n\nKoodi:\n${code}`; + await kpnRun(agentPrompts.tester.model, testerPrompt); + + termLog(`\n━━━ Pipeline valmis ━━━`); + } + function termExec(cmd) { termLog(`$ ${cmd.replace(/