# Kipinä Agentic Studio — Rakennuspalaset Tämä dokumentti kuvaa projektin UI-komponentit, arkkitehtuuripatternit ja työnkulut niin, että vastaavan hajautetun AI-laskentaverkon ja agenttipohjaisen käyttöliittymän voi rakentaa alusta asti. ## Yleiskuva ``` ┌─────────────────────────────────────────────────────┐ │ Selain (käyttäjä) │ │ ┌──────────┐ ┌──────────┐ ┌───────────────────┐ │ │ │ Verkko- │ │ Koodi- │ │ Agents-näkymä │ │ │ │ näkymä │ │ labra │ │ ┌───────────────┐ │ │ │ │ │ │ │ │ │ Terminaali │ │ │ │ │ Stats │ │ Editor │ │ │ Tab-complete │ │ │ │ │ Chat │ │ Pipeline │ │ │ Dropdown │ │ │ │ │ Tokenit │ │ Tulokset │ │ │ Historia │ │ │ │ └────┬─────┘ └────┬─────┘ │ └───────────────┘ │ │ │ │ │ └────────┬──────────┘ │ │ └──────────┬───┘ │ │ │ UI WebSocket HTTP API │ │ │ /api/v1/chat │ │ ┌───────────────┴──────────────┐ │ │ │ │ Wasm Compute Node │ │ │ │ │ (Candle + Burn) │ │ │ │ │ ┌─────────┐ ┌────────────┐ │ │ │ │ │ │ RAM │ │ IndexedDB │ │ │ │ │ │ │ Cache │ │ Cache │ │ │ │ │ │ └─────────┘ └────────────┘ │ │ │ │ │ ┌─────────────────────────┐ │ │ │ │ │ │ Model Cache (QwenModel) │ │ │ │ │ │ └─────────────────────────┘ │ │ │ │ └──────────────┬───────────────┘ │ │ │ │ WS │ │ └─────────────────┼──────────────────────┼─────────────┘ │ │ ┌────────┴──────────────────────┴──┐ │ Hub (Axum + Tokio) │ │ ┌────────────┐ ┌─────────────┐ │ │ │ Broadcast │ │ Node │ │ │ │ Channel │ │ Registry │ │ │ └────────────┘ └─────────────┘ │ │ ┌────────────┐ ┌─────────────┐ │ │ │ Busy-State │ │ Rate Limit │ │ │ │ Tracker │ │ + Auth │ │ │ └────────────┘ └─────────────┘ │ │ ┌─────────────────────────────┐ │ │ │ SQLite (sessiot, tulokset) │ │ │ └─────────────────────────────┘ │ └──────────────────────────────────┘ ``` --- ## 1. WebSocket-reaaliaikakommunikaatio ### 1.1 Hub ↔ Node broadcast-kanava **Tarkoitus:** Jakaa tehtäviä ja vastaanottaa tuloksia kaikilta laskentasolmuilta. **Työnkulku:** 1. Hub luo `tokio::sync::broadcast::channel(100)` 2. Jokainen solmu saa oman `rx = stats_tx.subscribe()` 3. Hub broadcastaa tehtävät: `stats_tx.send(json)` 4. Solmut suodattavat viestin tyypin ja `selected_task`:n perusteella **Viestityupit:** | Tyyppi | Suunta | Sisältö | |--------|--------|---------| | `stats` | Hub → kaikki | nodes, vram_gb, tasks | | `pair_task` | Hub → tokenize-solmut | en, fi tekstiparit | | `llm_prompt` | Hub → valittu solmu | prompt, model, task_id | | `llm_chunk` | Solmu → Hub → UI | token (1 kerrallaan) | | `llm_done` | Solmu → Hub → UI | response, tokens_generated, duration_ms | | `llm_error` | Solmu → Hub → UI | error, task_id | | `task_routed` | Hub → UI | status (routed/queued), node_id, message | **Lagged-viestien käsittely:** ```rust match rx.recv().await { Ok(msg) => { /* käsittele */ } Err(broadcast::error::RecvError::Lagged(n)) => { // Ohitetaan vanhat viestit, ei katkaista yhteyttä continue; } Err(_) => break, // Kanava suljettu } ``` ### 1.2 Kohdennettu reititys (Direct Channel) **Tarkoitus:** Lähetä tehtävä yhdelle tietylle solmulle broadcastin sijaan. **Työnkulku:** 1. Jokainen solmu saa `mpsc::unbounded_channel` yhdistyessään 2. Hub tallentaa `node_channels: HashMap` 3. API-pyyntö → valitaan vapaa solmu → lähetetään suoraan kanavaan 4. Broadcast-kanavaa käytetään vain tuloksen välittämiseen UI:lle ```rust let channels = state.node_channels.read().await; if let Some(tx) = channels.get(&target_node_id) { tx.send(msg.to_string()); } ``` ### 1.3 Busy-state ja työjono **Tarkoitus:** Estä tehtävien reititys varatuille solmuille. **Rakenne:** - `node_busy: HashSet` — solmut joilla on aktiivinen tehtävä - Asetetaan kun tehtävä reititetään, vapautetaan `llm_done`/`llm_error`:ssa - Jos kaikki solmut varattuja → pollaa 500ms välein, max 30s **UI-palaute:** ```json {"type": "task_routed", "status": "queued", "message": "Kaikki 2 solmua varattuja — odotetaan..."} {"type": "task_routed", "status": "routed", "node_id": 3, "message": "Solmu #3 vapautui (2.5s jonossa)"} ``` --- ## 2. Wasm-laskentasolmu ### 2.1 Elinkaari ``` init() → start_agent_node(ws_url, has_webgpu, device_info, task_id) │ ├─ Avaa WebSocket hubiin ├─ Lähettää auth-viestin (laitetiedot, selected_task) ├─ Rekisteröityy onmessage-käsittelijä │ ├─ pair_task → tokenize │ ├─ llm_prompt → inference │ └─ ai_task → tensor matmul └─ Odottaa tehtäviä loopissa ``` **Globaali tila (atominen, lukitsematon):** ```rust static GPU_LOAD_PERCENT: AtomicU32 = AtomicU32::new(50); static LLM_BUSY: AtomicBool = AtomicBool::new(false); static SELECTED_TASK: AtomicU32 = AtomicU32::new(0); ``` ### 2.2 Kolmitasoinen cache ``` Pyyntö → [1] RAM-cache (thread_local HashMap) │ miss ▼ [2] IndexedDB (selaimen pysyvä tallennus) │ miss ▼ [3] Verkko (HuggingFace CDN, streaming + 5% progressi) │ ▼ Tallenna → IndexedDB → RAM-cache ``` | Taso | Nopeus | Koko | Pysyvyys | |------|--------|------|----------| | RAM | ~0ms | Rajaton | Sivulataus | | IndexedDB | ~50ms | ~50GB | Pysyvä | | Verkko | ~10s/100MB | ∞ | — | **Malliinstanssin cache (neljäs taso):** ```rust thread_local! { static MODEL_CACHE: RefCell> = RefCell::new(None); } // clear_kv_cache() promptien välillä — ei tarvitse rakentaa mallia uusiksi ``` ### 2.3 Warmup-esilataus **Tarkoitus:** Lataa malli valmiiksi ennen ensimmäistä oikeaa promptia. ```javascript // Lähetetään 1 tokenin warmup heti kun WS on auki uiSocket.send(JSON.stringify({ type: 'user_text', text: '{"prompt":"warmup","max_tokens":1}', task_type: 'qwen-coder' })); ``` --- ## 3. LLM-inferenssipipeline ### 3.1 Prompt-formaatti (ChatML + prefill) ``` <|im_start|>system You are a coding assistant. Respond with ONLY code.<|im_end|> <|im_start|>user hello world in python<|im_end|> <|im_start|>assistant ``` ← PREFILL: pakottaa mallin aloittamaan koodilla ``` **Prefill-tekniikka:** Lisäämällä ` ``` ` assistantin vastauksen alkuun malli jatkaa suoraan koodilla eikä tuota "Sure! Here is..." -johdantoa. Säästää 10-20 tokenia per vastaus. ### 3.2 Sampling-parametrit | Parametri | Arvo | Tarkoitus | |-----------|------|-----------| | `temperature` | 0.7 | Pehmentää jakaumaa, vähentää toistoa | | `top_k` | 40 | Rajaa valinnan 40 todennäköisimpään tokeniin | | `repetition_penalty` | 1.15 | Rankaisee jo generoitujen tokenien uudelleenvalintaa | | `max_tokens` | 128 | Oletusraja, JSON-promptilla konfiguroitavissa | **Sampling-funktio (top-k + temperature + repetition penalty):** ```rust fn sample_top_k_with_penalty(logits, k, temperature, generated_tokens, penalty) -> u32 { // 1. Repetition penalty: vähennä aiempien tokenien logitteja // 2. Temperature scaling: jaa logitit temperaturella // 3. Top-k: ota k suurinta // 4. Softmax top-k:lle // 5. Satunnaisvalinta kumulatiivisella todennäköisyydellä (XorShift RNG) } ``` ### 3.3 Stop-sekvenssit Generointi katkaistaan ja teksti trimmataan kun malli alkaa selittää: ```rust let stop_patterns = ["\n###", "\nExplanation", "\nNote:", "\nOutput:", "\n```\n\n"]; ``` ### 3.4 Vastauksen siivous ``` Raakavastaus: "Sure! Here is...\n```python\n# This is a simple program\nprint('hi')\n```" │ strip_markdown: "# This is a simple program\nprint('hi')" │ strip_preamble: "print('hi')" ``` **Tunnistettavat selityskommentit:** `# This is`, `# simple`, `# program that`, `# here is`, `# the following`, `# below` ### 3.5 Streaming Jokainen generoitu token lähetetään heti `llm_chunk`-viestinä: ```json {"type": "llm_chunk", "token": "print", "prompt": "...", "model": "Qwen2.5-Coder", "task_id": "uuid"} ``` UI päivittää streaming-korttia reaaliaikaisesti appendaamalla tokeneita. --- ## 4. Terminaaliemulaattori ### 4.1 Rakenne ```html
$
``` ### 4.2 Komentojen käsittely ```javascript function termExec(cmd) { // Parsitaan: "kpn" + alikomento + argumentit // Tuetut: help, run, pipeline, load, status, models, hello, clear // Agenttinimi → malli-mapping: "coder" → "qwen-coder" } ``` ### 4.3 Tab-completion (kolmitasoinen) ```javascript const kpnCommands = { 'kpn': ['help', 'run', 'pipeline', 'load', ...], 'kpn run': ['coder', 'manager', 'qwen-coder', ...], }; const kpnExamples = { 'kpn run coder': ['"hello world in python"', ...], }; ``` **Käyttö:** | Näppäin | Toiminto | |---------|----------| | TAB | Täydennä seuraava sana tai avaa dropdown | | Shift-TAB | Poista viimeinen sana (lainausmerkit kokonaisuutena) | | ↑ / ↓ | Navigoi dropdownissa (tai komentohistoriassa) | | Enter | Valitse dropdownista tai suorita komento | | Esc | Sulje dropdown | ### 4.4 Dropdown-valikko ```javascript function showDropdown(items, prefix) { // Luo div.term-dd-item per vaihtoehto // Positio: absolute, bottom: 100% (inputin yläpuolella) // Mouseenter → highlight, click → valinta } ``` ### 4.5 Komentohistoria ```javascript const termHistory = []; // Kaikki ajetut komennot (viimeisin ensin) let termHistIdx = -1; // Nykyinen positio historiassa // ArrowUp: termHistIdx++, ArrowDown: termHistIdx-- ``` --- ## 5. Status-palkit ja tilaindikaattorit ### 5.1 Hub-yhteyden tila | Tila | Väri | Teksti | Tooltip | |------|------|--------|---------| | Yhdistetään | 🟡 | "Yhdistetään..." | WebSocket-yhteys Kipinä Hubiin | | Yhdistetty | 🟢 | "Yhdistetty" | Tehtävien jakelu aktiivinen | | Katkennut | 🔴 | "Yhteys katkennut" | Tarkista verkko, lataa uudelleen | ### 5.2 Laskentasolmun tila | Tila | Väri | Teksti | Nappi | |------|------|--------|-------| | Ei käynnissä | ⚫ | "—" | `[Alusta laskentasolmu]` sininen | | Lataa | 🟡 | "Ladataan..." | `[Peruuta]` punainen | | Valmis | 🟢 | "Qwen2.5-Coder" | `[✓ Valmis]` vihreä | ### 5.3 Pipeline-tilakone (Codelab) ``` Step 1: WebAssembly-ytimen lataus [◯ → ◷ → ✓] Step 2: Tokenizer (7 MB) [◯ → ◷ → ✓] Step 3: Mallipainot (990 MB) [◯ → ◷ 45% → ✓ cache] Step 4: Mallin rakentaminen [◯ → ◷ → ✓] Step 5: Valmis generoimaan [◯ → ✓] ``` **Seuranta console.log-viesteistä:** ```javascript if (msg.includes('[Coder]') && msg.includes('Malli ladattu')) { // Merkkaa kaikki vaiheet valmiiksi (myös cache-hitillä) setStep('step-wasm', 'done'); setStep('step-tokenizer', 'done'); setStep('step-model', 'done', 'cache'); setStep('step-build', 'done'); setStep('step-ready', 'done'); } ``` --- ## 6. Tietoturva ### 6.1 XSS-suojaus ```javascript function esc(str) { return String(str).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } ``` **Käyttöpaikat:** Kaikki `innerHTML`-insertoinnit joissa on käyttäjä- tai backend-dataa. ### 6.2 System prompt -piilotus ```javascript function stripSystemPrompt(prompt) { const parts = prompt.split('\n\n'); return parts[parts.length - 1] || prompt; } ``` ### 6.3 Viestityyppivalidointi (backend) ```rust const ALLOWED_MSG_TYPES: &[&str] = &[ "auth", "result", "pair_done", "llm_chunk", "llm_done", "llm_error", "download_progress", "user_text", "single_tokenize_done" ]; fn validate_message(text: &str) -> Result { // 1. JSON-parsinta // 2. "type"-kenttä pakollinen // 3. Tyyppi sallittujen listalla // 4. Tyyppikohtainen validointi (esim. pair_done: token_count <= 10000) } ``` ### 6.4 Rate limiting ```rust // Per-IP liukuva ikkuna: max 10 pyyntöä per 60s let entry = limits.entry(addr.ip()).or_insert((now, 0)); if now.duration_since(entry.0).as_secs() >= 60 { *entry = (now, 1); } else { entry.1 += 1; if entry.1 > 10 { return 429 Too Many Requests; } } ``` ### 6.5 Gamification-huijauksen esto ```rust // Hub jakaa task_id:n → tallentaa pending_task_ids:hen // Merkkejä jaetaan VAIN jos llm_done sisältää validin task_id:n let valid_task = state.pending_task_ids.lock().unwrap().remove(tid); if active_incentives && valid_task { *balance += 20; } ``` --- ## 7. Syntaksikorostus ### 7.1 Highlight.js-integraatio ```html ``` ```javascript function highlightCode(code) { if (typeof hljs !== 'undefined') { return hljs.highlightAuto(code).value; // Automaattinen kielentunnistus } return esc(code); // Fallback } ``` **Käyttöpaikat:** Codelab-tulokset, agents-terminaalin vastaukset, network-chat. --- ## 8. Agenttien orkestrointi ### 8.1 Multi-agent pipeline ``` ┌──────────┐ ┌──────────┐ ┌──────────┐ │ Manageri │ ──→ │ Koodari │ ──→ │ Testaaja │ │ Analysoi │ │ Koodaa │ │ Arvioi │ │ tehtävä │ │ ratkaisu │ │ koodi │ └──────────┘ └──────────┘ └──────────┘ ``` ```javascript async function kpnPipeline(task) { const plan = await kpnRun('qwen-coder', `Analysoi: ${task}`); if (!plan) return; const code = await kpnRun('qwen-coder', `Koodaa: ${plan}`); if (!code) return; await kpnRun('smollm-135m', `Arvioi: ${code}`); } ``` ### 8.2 Agenttien promptien hallinta ```javascript const agentPrompts = { manager: { model: 'qwen-coder', prompt: 'Olet projektipäällikkö...' }, coder: { model: 'qwen-coder', prompt: 'Olet ohjelmistokehittäjä...' }, // ... }; // Tallennetaan localStorage:en per agentti localStorage.setItem('kpn-agent-prompt-coder', customPrompt); ``` ### 8.3 Yhteinen promptikonteksti ```javascript async function kpnRun(model, prompt) { const parts = []; if (sharedPrompt) parts.push(sharedPrompt); // Kaikille yhteinen if (agent.prompt) parts.push(agent.prompt); // Agenttikohtainen parts.push(prompt); // Käyttäjän pyyntö const fullPrompt = parts.join('\n\n'); // → HTTP POST /api/v1/chat/completions } ``` --- ## 9. Teknologiapino | Kerros | Teknologia | Tarkoitus | |--------|------------|-----------| | Frontend | Vanilla JS + HTML + CSS | Ei build-steppiä, toimii suoraan | | Wasm | Rust + wasm-bindgen | Inferenssi selaimessa | | LLM | Candle (Rust) | Transformer-inferenssi CPU:lla | | Tensorit | Burn (Rust) | GPU-tensorilaskenta (WebGPU/NdArray) | | Backend | Axum + Tokio (Rust) | Async WebSocket + HTTP -palvelin | | Tietokanta | SQLite (rusqlite) | Sessiot ja tulokset | | Cache | IndexedDB | Mallipainot selaimen pysyvässä muistissa | | Korostus | Highlight.js (CDN) | Syntaksikorostus, automaattinen kielentunnistus | | Tokenizer | HuggingFace tokenizers | BPE-tokenisaatio Wasmissa | --- ## 10. Jatkokehitysideoita Näiden rakennuspalasten pohjalta voi rakentaa: - **Oma chat-UI:** WebSocket + streaming + syntaksikorostus - **Hajautettu laskentaverkko:** Hub + node-rekisteri + busy-state + työjono - **Selain-LLM:** Wasm + Candle + IndexedDB-cache + warmup - **Agenttipohjainen työnkulku:** Pipeline + prompt-orkestrointi + reititys - **Terminaaliemulasttori:** Input + historia + tab-completion + dropdown - **Reaaliaikadashboard:** WebSocket broadcast + tilaindikaattorit + metriikat