34 Commits

Author SHA1 Message Date
fc95cf8c1b Terminaaliin varoitus inferenssin aikana + yield ennen blokkia
Käyttäjälle näytetään '(selain voi hidastua)' kun inferenssi alkaa.
setTimeout yield varmistaa statusrivin piirtämisen ennen WASM-blokkia.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 19:31:25 +03:00
1ae1bf98e2 API timeout nostettu 120s → 600s: WASM-inferenssi on hidasta
Kvantisoidun 1.5B-mallin inferenssi on ~0.2 tok/s WASM:ssa.
Pipeline-tehtävät vaativat pidemmän odotusajan.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 19:29:15 +03:00
f567fd3f8a Mallin automaattinen lataus poistettu — käyttäjä käynnistää kpn load:lla
Aiemmin localStorage muisti edellisen latauksen ja käynnisti mallin
automaattisesti sivulle tullessa. Nyt käyttäjä päättää itse milloin lataa.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 18:03:17 +03:00
38367eac97 Terminaaliin latauksen tilaindikaattori (spinner + vaihe)
Mallin latauksen aikana terminaalissa näkyy animoitu spinner
ja nykyinen vaihe: WASM → tokenizer → malli (%) → rakennus → valmis.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 16:29:33 +03:00
20716186bc Hub: qwen-coder reititys tunnistaa kaikki coder-solmut (05b, 3b, 1.5b)
API etsi vain 'qwen-coder-05b' tai 'qwen-coder', ei 'qwen-coder-3b'.
Nyt task.starts_with('qwen-coder') matchaa kaikki variantit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 16:27:49 +03:00
4e810ed4a2 Kaikki agentit käyttävät qwen-coder -mallia + valmis-viesti deduplikoitu
QA ja DevOps käyttivät smollm-135m:ää jota ei ole selaimessa ladattuna.
Nyt kaikki agentit käyttävät ladattua qwen-coder-mallia.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 16:23:59 +03:00
91ff9e00f9 kvantisointia 2026-04-06 16:15:56 +03:00
e652bf7ab6 1.5B Q4_K_M: vaihdettu 3B→1.5B koska 3B ei mahdu WASM:iin (~1 GB vs ~2 GB)
3B GGUF vaati ~5 GB muistia parsinnassa → SIGILL WASM:n 4 GB rajalla.
1.5B Q4_K_M on ~1 GB ja mahtuu turvallisesti selaimeen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 16:14:41 +03:00
eb69893124 WASM release-build: GGUF dequantize vaatii optimointeja
Debug-moodi aiheutti SIGILL (Illegal Instruction) GGUF-tensorien
dequantisoinnissa. Release-build ratkaisee ongelman.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 14:07:02 +03:00
d18314bfc8 GGUF Q4_K_M -tuki 3B-mallille: kvantisoidtu versio (~1.9 GB) mahtuu selaimeen
Safetensors-muotoinen 3B (~6.2 GB) aiheutti WASM capacity overflow.
Nyt käytetään candle quantized_qwen2 -moduulia GGUF-tiedoston lataamiseen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 13:54:23 +03:00
99b011e399 Isomman qwen-mallin lataus 2026-04-06 13:40:19 +03:00
Jaakko Vanhala
3976bb6251 IP-yhteysraja nostettu 4→10: mahdollistaa useamman laitteen samasta IP:stä
Jokainen selain tarvitsee 2 WebSocket-yhteyttä (UI + coder-node).
Vanha raja 4 esti toisen koneen yhdistämisen samasta IP:stä (esim. kotiverkko).
Uusi raja 10 riittää 5 samanaikaiselle selaimelle / laitteelle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 08:56:36 +03:00
Jaakko Vanhala
0c32fecdc4 GUIDE.md: laajennettu tokenisaatio-osio suomi/englanti-vertailulla
Lisätty konkreettiset esimerkit Qwen2.5-Coder -tokenisaattorilla:
- Koodi-esimerkki: print vs. tulosta
- Kolme lauseparia taulukossa (The cat sat / Kissa istui jne.)
- Merkkejä/token -sarake näyttää tehokkuuseron
- Selitys miksi englanti on 30-50% tehokkaampaa
- Miksi tämä merkitsee: nopeus, konteksti, ymmärrys

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 08:41:12 +03:00
Jaakko Vanhala
801cc0371d Yhtenäinen kirjoitusasu: Qwen2.5-Coder:0.5B ja Qwen2.5-Coder:3B (kaksoispiste)
Korjattu agents-sivun status-palkki, codelab-loading ja GUIDE.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 08:27:22 +03:00
Jaakko Vanhala
176f2d6915 Mermaid-kaaviot oppaaseen + mallitiedot agents-sivun status-palkkiin
GUIDE.md:n ASCII-kaaviot korvattu Mermaid-kaavioilla:
- Projekti-pipeline: flowchart TD värikoodatuilla vaiheilla
- Prompttirakenne: system → agent → user → prefill ketju

Mermaid ladataan CDN:stä ja renderöidään automaattisesti dark-teemalla.
Fallback: kaavion lähdekoodi näkyy tekstinä jos Mermaid ei lataudu.

Agents-sivun compute-status näyttää nyt tarkan mallitiedon:
- "Qwen2.5-Coder-0.5B" tai "Qwen2.5-Coder-3B"
- Tooltip: parametrimäärä, runtime, max tokenit

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 08:23:19 +03:00
Jaakko Vanhala
dd1945ab28 Opas-välilehti: GUIDE.md renderöidään sivustolle omana näkymänä
Uusi "Opas"-välilehti (panel-guide) lataa GUIDE.md:n fetchillä ja
renderöi sen inline markdown→HTML -parserilla:
- Otsikot (h1-h3) GitHub-tyylisesti
- Koodiblokit highlight.js-korostuksella
- Taulukot (header + body, border-collapse)
- Listat (bullet + numeroitu)
- Inline-muotoilu: **bold**, *italic*, `code`
- Horisontaaliviivat

GUIDE.md siirretty static/-hakemistoon jotta hub servaa sen suoraan.
Navigointi: #guide hash tai klikkaa "Opas"-välilehteä.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 08:20:54 +03:00
Jaakko Vanhala
262fee3b49 GUIDE.md: opettavainen yhteenveto kielimalleista, tokeneista ja laadun parantamisesta
Kattaa:
- Kielimallit ja parametrimäärät (135M → 1800B vertailu)
- Tokenit: mitä ne ovat, miksi kieli vaikuttaa, token-budjetti
- Prompttirakenne: system/agent/user/prefill + miksi englanniksi
- Prefill-tekniikka: miten se toimii ja miksi se säästää tokeneita
- Sampling: temperature, top-k, repetition penalty selitettyinä
- Stop-sekvenssit: milloin generointi loppuu
- Projekti-pipeline: agenttitiimin työnkulku kaaviona
- Laadun parantaminen 10 eri keinolla:
  1. Isompi malli
  2. Paremmat promptit
  3. Kontekstin hallinta
  4. Iterointi (review-luuppi)
  5. Erikoistetut system promptit
  6. Few-shot esimerkit
  7. Temperature-säätö tehtävän mukaan
  8. Ensemble (sama prompti usealle mallille)
  9. Post-processing
  10. Fine-tuning (LoRA)
- Välimuistiarkkitehtuuri: miksi toinen lataus on nopea
- Käytännön lukuja: token-määrät, ajat, kustannukset

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 08:16:36 +03:00
Jaakko Vanhala
aa7540a6bf Prompt Inspector: [>]-nappi status-rivillä näyttää mitä mallille lähetettiin
Jokaisen kpnRun-tuloksen status-rivillä on [>]-nappi joka avaa inspektor-paneelin:
- system: inferenssin system prompt
- shared: kaikille agenteille yhteinen prompti (jos asetettu)
- agent: valitun agentin system prompt
- user: käyttäjän/pipelinen prompti (kokonaisuudessaan, scrollattava)
- prefill: ``` (ChatML prefill-tekniikka)
- Token-estimaatti: ~N tok in → M tok out

Paneeli avautuu/sulkeutuu klikkaamalla. Näyttää eksaktisti saman
mitä malli saa syötteeksi — hyödyllinen debuggaukseen ja promptien
kehittämiseen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 08:00:11 +03:00
Jaakko Vanhala
762066102a PROMPTS.md: kaikki järjestelmän promptit dokumentoitu eksaktisti
Kattaa kaikki 9 osa-aluetta:
1. Inferenssin system prompt (ChatML)
2. Agenttikohtaiset system promptit (7 agenttia)
3. Projekti-pipeline promptit (5 vaihetta + erikoistapaukset)
4. Yksinkertaisen pipelinen promptit
5. Yksittäiset komennot (run, hello, warmup)
6. Stop-sekvenssit (10 kpl)
7. Vastauksen siivous (4 vaihetta)
8. ChatML-promptin koostaminen (prefill-tekniikka)
9. Sampling-parametrit

Jokainen prompti on eksaktissa muodossaan muuttujamerkinnöillä.
Parsintasäännöt ja erikoistapaukset (pyproject.toml, requirements.txt)
dokumentoitu yksityiskohtaisesti.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 07:53:01 +03:00
Jaakko Vanhala
bef5b6fc3c uv/pyproject.toml tuki projektipipelineen, requirements.txt fallbackina
Managerin prompti ohjaa käyttämään pyproject.toml:ia (.toml sallittu).
Koodari saa pyproject.toml-tiedostolle eksplisiittisen esimerkkiformaatin
jossa [project] + dependencies + [project.scripts] start-komennolla.
requirements.txt toimii edelleen fallbackina jos malli tuottaa sen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 07:43:47 +03:00
Jaakko Vanhala
095b72d2d6 Managerin prompti: riippuvuusjärjestys (models.py ennen main.py)
Lisätty sääntö: "List dependencies first, then main app" jotta
koodari saa kirjoitettua riippuvuudet (models, schemas) ensin
ja pääsovelluksen (main.py) saa kontekstiksi oikeat importit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 20:52:25 +03:00
Jaakko Vanhala
4cb6128a27 Tiedostoparsinta: hyväksyy myös pelkät tiedostonimet ilman kuvausta
Manageri tuottaa toisinaan pelkän listan (app.py, requirements.txt)
ilman "filename: description" -formaattia. Parsija hyväksyy nyt molemmat.
Koodarin prompti vahvistettu: "Use the exact libraries mentioned in the
project description" estää Flaskiin vaihtamisen kun tehtävä sanoo FastAPI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 20:44:16 +03:00
Jaakko Vanhala
4dff534fbf Projektikortti: tiedostovälilehdet, kopioi per tiedosto, lataa ZIP
Pipeline-tulokset renderöidään interaktiivisena projektikorttina terminaaliin:

- Tiedostovälilehdet (klikkaa vaihtaaksesi: main.py | models.py | ...)
- Syntaksikorostus (highlight.js) jokaisessa tiedostossa
- "Kopioi"-nappi per tiedosto (leikepöydälle)
- "Kopioi kaikki" -nappi (kaikki tiedostot yhtenä tekstinä)
- "Lataa ZIP" -nappi (selaimessa generoitu ZIP ilman ulkoisia kirjastoja)

ZIP-generointi on toteutettu puhtaalla JavaScriptillä (uncompressed store)
ilman JSZip- tai muita riippuvuuksia.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 20:37:10 +03:00
Jaakko Vanhala
d5ab6272d3 Paranneltu project-pipelinen promptit ja tiedostoparsinta
Managerin prompti:
- Selkeämpi formaatti: "filename.py: what this file contains"
- Eksplisiittiset säännöt: max 4 tiedostoa, ei polkuja, vain tiedostonimet
- Sallitut tiedostopäätteet: .py, .txt, .json, .html

Tiedostoparsinta tiukennettu:
- Hylkää polut (chucknorris/fastapi/...) — vaatii ettei sisällä /
- Vaatii tiedostopäätteen (.xyz)
- Ei välilyöntejä nimessä

Koodarin prompti:
- "Project:" konteksti ensin, sitten tarkka tiedostokohtainen ohje
- "Write correct, working code. No explanations."

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 20:34:23 +03:00
Jaakko Vanhala
2e7b86deeb Pipeline-vaiheiden visuaalinen seuranta agenttinäkymässä
Terminaalin yläpuolelle ilmestyy pipeline-progress-palkki:
  ✓ Suunnittelu → ✓ models.py → ◷ main.py → ◯ Review

Jokainen vaihe on hover-tooltip joka näyttää:
- Vaiheen nimi ja agentti (värikoodattu)
- Input: mitä agentti sai syötteeksi
- Output: mitä agentti tuotti (esikatselu 150 merkkiä)

Myös agenttien avatar-korttien tooltip päivittyy reaaliaikaisesti
näyttämään viimeisimmän vaiheen input/output.

Palkki tyhjenee automaattisesti uuden pipelinen alkaessa.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 20:32:59 +03:00
Jaakko Vanhala
a6e49870d6 Monivaiheinen projektipipeline: kpn project -komento
Uusi kpn project -komento rakentaa ohjelmistoprojektin tiedosto kerrallaan:

1. Manageri pilkkoo projektin tiedostoiksi (max 5)
   → parsii "FILENAME: description" -rivit
2. Koodari generoi jokaisen tiedoston erikseen
   → saa kontekstina aiemmin generoidut tiedostot
3. Testaaja arvioi koko projektin
   → etsii bugeja ja puutteita
4. Korjausluuppi: jos testaaja löytää ongelmia
   → koodari saa review-palautteen ja korjaa
   → testaaja arvioi uudelleen

Fallback: jos manageri ei tuota tiedostolistaa, generoidaan yhtenä kokonaisuutena.

kpn pipeline säilyy yksinkertaisena 3-vaiheisena (manageri → koodari → testaaja).

Esimerkkejä:
  kpn project "FastAPI + SQLite REST API for users"
  kpn project "Flask todo app with database"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 19:30:38 +03:00
Jaakko Vanhala
d68882249e Token-raja 256→512: mahdollistaa pidemmät kooditiedostot
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 19:11:09 +03:00
Jaakko Vanhala
6a587cd080 Terminaalin status-rivit tiivistetty: yksi rivi per tehtävä jota päivitetään
Ennen (3 riviä):
  → qwen-coder käsittelee...
  → Reititetty solmulle #2
  ✓ Qwen2.5-Coder-0.5B-Instruct (75 tok)

Jälkeen (1 rivi jota päivitetään):
  → qwen-coder käsittelee...  →  → Reititetty solmulle #2 ▌  →  ✓ Qwen2.5-Coder 75 tok · 12.0s · 6.3 tok/s

Sama div päivitetään pyynnön elinkaaren läpi: käsittelee → reititys → valmis/virhe.
task_routed-viestit päivittävät samaa riviä uusien rivien sijaan.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 18:34:07 +03:00
Jaakko Vanhala
f17fcf0f9d Terminaalin koodivastaus tiivistetty yhdelle riville, klikkaus laajentaa
Tuloste näyttää ensimmäisen koodirivin + "(+N riviä)":
  ✓ Qwen2.5-Coder (75 tok)
  ▶ fn fibonacci(n: usize) -> usize { (+8 riviä)

Klikkaus laajentaa/sulkee koko koodin highlight.js-korostuksella
ja vasemman reunan indikaattoriviivalla.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 18:29:18 +03:00
Jaakko Vanhala
ac15336c9f Stop-sekvenssit: katkaistaan myös "// Example usage" ja "# Example" kommentit
Malli tuottaa toisinaan esimerkkikoodia funktioiden jälkeen joka ei ole osa
varsinaista vastausta. Nyt generointi katkeaa ennen näitä.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 18:21:40 +03:00
Jaakko Vanhala
7a15cacebf Malli säilyy refreshin yli: automaattinen uudelleenlataus IndexedDB-cachesta
- coderSize tallennetaan localStorageen (valinta säilyy)
- Kun malli on kerran ladattu, 'kpn-coder-loaded' lippu asetetaan
- Sivulatauksessa: jos lippu on asetettu, ensureCoderNode() käynnistyy
  automaattisesti — painot tulevat IndexedDB-cachesta, ei verkosta
- Radio-napit asetetaan oikeaan tilaan localStoragesta

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 18:13:33 +03:00
Jaakko Vanhala
27135a8f14 Numeroidut mallilistat: kpn models ja kpn load tukevat numerovalintaa
kpn models näyttää:
  1  qwen-coder     Qwen2.5-Coder:0.5B  ~990 MB
  2  qwen-coder-3b  Qwen2.5-Coder:3B    ~6.2 GB
  3  smollm-135m    SmolLM 135M         ~270 MB
  ...

kpn load näyttää ladattavat mallit ja hyväksyy numeron:
  kpn load     → näytä lista
  kpn load 1   → lataa 0.5B
  kpn load 2   → lataa 3B
  kpn load 3b  → toimii myös nimellä

Jo ladattu malli merkitään ✓-merkillä.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 17:58:50 +03:00
Jaakko Vanhala
e28a715f32 Max tokens 128→256 + coder-3b malli agents-terminaaliin
- Oletustokenimäärä nostettu 256:een (monimutkaisemmat vastaukset mahtuvat)
- kpn run coder-3b "..." käynnistää 3B-mallin (parempi koodinlaatu)
- kpn load 3b lataa 3B-mallin (~6.2 GB)
- Tab-completion tukee coder-3b + esimerkkipromptit

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 17:56:56 +03:00
Jaakko Vanhala
24d29d9ba9 Avatar-aktivointi vain omille agents-tehtäville, ei broadcast-viesteille
Agenttiavatarit vilkkuivat itsestään koska llm_prompt-handler reagoi kaikkiin
broadcastattuihin viesteihin (hubin automaattiset 10s-tehtävät, warmup jne.).
Nyt avatar-logiikka laukeaa VAIN jos viestissä on task_id joka löytyy
activeStreams:stä — eli kyseessä on käyttäjän oma agents-pipelinen tehtävä.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 13:55:58 +03:00
9 changed files with 1990 additions and 128 deletions

475
docker-errors.log Normal file
View File

@@ -0,0 +1,475 @@
[INFO]: Checking for the Wasm target...
info: downloading component rust-std
[INFO]: Compiling to Wasm...
Compiling node v0.1.0 (/app/node)
warning: unused imports: `DType`, `Device`, and `Tensor`
--> node/src/smollm.rs:1:19
|
1 | use candle_core::{Device, Tensor, DType};
| ^^^^^^ ^^^^^^ ^^^^^
|
= note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default
warning: unused import: `candle_nn::VarBuilder`
--> node/src/smollm.rs:2:5
|
2 | use candle_nn::VarBuilder;
| ^^^^^^^^^^^^^^^^^^^^^
warning: unused imports: `Cache`, `LlamaConfig`, `LlamaEosToks`, and `Llama`
--> node/src/smollm.rs:3:42
|
3 | use candle_transformers::models::llama::{Llama, LlamaConfig, LlamaEosToks, Cache};
| ^^^^^ ^^^^^^^^^^^ ^^^^^^^^^^^^ ^^^^^
warning: unused imports: `DType`, `Device`, and `Tensor`
--> node/src/phi3.rs:1:19
|
1 | use candle_core::{Device, Tensor, DType};
| ^^^^^^ ^^^^^^ ^^^^^
warning: unused import: `candle_nn::VarBuilder`
--> node/src/phi3.rs:2:5
|
2 | use candle_nn::VarBuilder;
| ^^^^^^^^^^^^^^^^^^^^^
warning: unused imports: `Config as Phi3Config` and `Model as Phi3Model`
--> node/src/phi3.rs:3:41
|
3 | use candle_transformers::models::phi3::{Config as Phi3Config, Model as Phi3Model};
| ^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^
warning: unused import: `wasm_bindgen::JsCast`
--> node/src/phi3.rs:4:5
|
4 | use wasm_bindgen::JsCast;
| ^^^^^^^^^^^^^^^^^^^^
warning: unused import: `crate::storage`
--> node/src/phi3.rs:9:5
|
9 | use crate::storage;
| ^^^^^^^^^^^^^^
warning: unused import: `Int`
--> node/src/burn_smollm/attention.rs:2:46
|
2 | use burn::tensor::{backend::Backend, Tensor, Int};
| ^^^
warning: unused imports: `Mlp` and `RmsNorm`
--> node/src/burn_smollm/attention.rs:4:22
|
4 | use super::modules::{RmsNorm, Mlp};
| ^^^^^^^ ^^^
warning: use of deprecated struct `burn::tensor::Data`: the internal data format has changed, please use `TensorData` instead
--> node/src/smollm.rs:174:23
|
174 | burn::tensor::Data::new(input_ids.iter().map(|&x| x as i32).collect::<Vec<_>>(), [input_len].into()),
| ^^^^
|
= note: `#[warn(deprecated)]` on by default
warning: use of deprecated struct `burn::tensor::Data`: the internal data format has changed, please use `TensorData` instead
--> node/src/smollm.rs:200:27
|
200 | burn::tensor::Data::new(vec![next_token as i32], [1].into()),
| ^^^^
warning: use of deprecated struct `burn::tensor::Data`: the internal data format has changed, please use `TensorData` instead
--> node/src/burn_smollm/loader.rs:1:46
|
1 | use burn::tensor::{backend::Backend, Tensor, Data};
| ^^^^
warning: use of deprecated struct `burn::tensor::Data`: the internal data format has changed, please use `TensorData` instead
--> node/src/burn_smollm/loader.rs:17:16
|
17 | let data = Data::new(vec, shape_out_in.into());
| ^^^^
warning: use of deprecated struct `burn::tensor::Data`: the internal data format has changed, please use `TensorData` instead
--> node/src/burn_smollm/loader.rs:32:16
|
32 | let data = Data::new(vec, shape.into());
| ^^^^
warning: use of deprecated struct `burn::tensor::Data`: the internal data format has changed, please use `TensorData` instead
--> node/src/burn_smollm/loader.rs:45:16
|
45 | let data = Data::new(vec, shape.into());
| ^^^^
error[E0061]: this function takes 2 arguments but 1 argument was supplied
--> node/src/smollm.rs:124:9
|
124 | burn_wgpu::init_async::<burn_wgpu::AutoGraphicsApi>(&Default::default()).await;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^--------------------- argument #2 of type `RuntimeOptions` is missing
|
note: function defined here
--> /usr/local/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cubecl-wgpu-0.2.0/src/runtime.rs:116:14
|
116 | pub async fn init_async<G: GraphicsApi>(device: &WgpuDevice, options: RuntimeOptions) {
| ^^^^^^^^^^
help: provide the argument
|
124 | burn_wgpu::init_async::<burn_wgpu::AutoGraphicsApi>(&Default::default(), /* RuntimeOptions */).await;
| ++++++++++++++++++++++
error[E0277]: the trait bound `TensorData: From<burn::tensor::Data<i32, 1>>` is not satisfied
--> node/src/smollm.rs:174:9
|
173 | let mut input_tensor = burn::tensor::Tensor::<B, 1, burn::tensor::Int>::from_data(
| ---------------------------------------------------------- required by a bound introduced by this call
174 | burn::tensor::Data::new(input_ids.iter().map(|&x| x as i32).collect::<Vec<_>>(), [input_len].into()),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `From<burn::tensor::Data<i32, 1>>` is not implemented for `TensorData`
|
= help: the following other types implement trait `From<T>`:
`TensorData` implements `From<&[E]>`
`TensorData` implements `From<&[usize]>`
`TensorData` implements `From<[E; A]>`
`TensorData` implements `From<[[E; B]; A]>`
`TensorData` implements `From<[[[E; C]; B]; A]>`
`TensorData` implements `From<[[[[E; D]; C]; B]; A]>`
`TensorData` implements `From<[[[[[Elem; E]; D]; C]; B]; A]>`
`TensorData` implements `From<[usize; A]>`
= note: required for `burn::tensor::Data<i32, 1>` to implement `Into<TensorData>`
note: required by a bound in `burn::tensor::Tensor::<B, D, K>::from_data`
--> /usr/local/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/burn-tensor-0.14.0/src/tensor/api/base.rs:719:12
|
717 | pub fn from_data<T>(data: T, device: &B::Device) -> Self
| --------- required by a bound in this associated function
718 | where
719 | T: Into<TensorData>,
| ^^^^^^^^^^^^^^^^ required by this bound in `Tensor::<B, D, K>::from_data`
error[E0061]: this method takes 2 arguments but 0 arguments were supplied
--> node/src/smollm.rs:183:51
|
183 | let next_token_tensor = last_logits.argmax(2).flatten::<1>();
| ^^^^^^^^^^^^-- two arguments of type `usize` and `usize` are missing
|
note: method defined here
--> /usr/local/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/burn-tensor-0.14.0/src/tensor/api/base.rs:292:12
|
292 | pub fn flatten<const D2: usize>(self, start_dim: usize, end_dim: usize) -> Tensor<B, D2, K> {
| ^^^^^^^
help: provide the arguments
|
183 | let next_token_tensor = last_logits.argmax(2).flatten::<1>(/* usize */, /* usize */);
| ++++++++++++++++++++++++
error[E0277]: the trait bound `TensorData: From<burn::tensor::Data<i32, 1>>` is not satisfied
--> node/src/smollm.rs:200:13
|
199 | let mut input_tensor = burn::tensor::Tensor::<B, 1, burn::tensor::Int>::from_data(
| ---------------------------------------------------------- required by a bound introduced by this call
200 | burn::tensor::Data::new(vec![next_token as i32], [1].into()),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `From<burn::tensor::Data<i32, 1>>` is not implemented for `TensorData`
|
= help: the following other types implement trait `From<T>`:
`TensorData` implements `From<&[E]>`
`TensorData` implements `From<&[usize]>`
`TensorData` implements `From<[E; A]>`
`TensorData` implements `From<[[E; B]; A]>`
`TensorData` implements `From<[[[E; C]; B]; A]>`
`TensorData` implements `From<[[[[E; D]; C]; B]; A]>`
`TensorData` implements `From<[[[[[Elem; E]; D]; C]; B]; A]>`
`TensorData` implements `From<[usize; A]>`
= note: required for `burn::tensor::Data<i32, 1>` to implement `Into<TensorData>`
note: required by a bound in `burn::tensor::Tensor::<B, D, K>::from_data`
--> /usr/local/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/burn-tensor-0.14.0/src/tensor/api/base.rs:719:12
|
717 | pub fn from_data<T>(data: T, device: &B::Device) -> Self
| --------- required by a bound in this associated function
718 | where
719 | T: Into<TensorData>,
| ^^^^^^^^^^^^^^^^ required by this bound in `Tensor::<B, D, K>::from_data`
error[E0061]: this method takes 2 arguments but 0 arguments were supplied
--> node/src/smollm.rs:207:50
|
207 | let next_token_tensor = logits.argmax(2).flatten::<1>();
| ^^^^^^^^^^^^-- two arguments of type `usize` and `usize` are missing
|
note: method defined here
--> /usr/local/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/burn-tensor-0.14.0/src/tensor/api/base.rs:292:12
|
292 | pub fn flatten<const D2: usize>(self, start_dim: usize, end_dim: usize) -> Tensor<B, D2, K> {
| ^^^^^^^
help: provide the arguments
|
207 | let next_token_tensor = logits.argmax(2).flatten::<1>(/* usize */, /* usize */);
| ++++++++++++++++++++++++
error[E0308]: mismatched types
--> node/src/burn_smollm/attention.rs:58:13
|
58 | q = q.reshape([batch, seq_len, self.num_heads, self.head_dim]).swap_dims(1, 2);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `3`, found `4`
|
= note: expected struct `burn::tensor::Tensor<_, 3>`
found struct `burn::tensor::Tensor<_, 4>`
error[E0308]: mismatched types
--> node/src/burn_smollm/attention.rs:59:13
|
59 | k = k.reshape([batch, seq_len, self.num_kv_heads, self.head_dim]).swap_dims(1, 2);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `3`, found `4`
|
= note: expected struct `burn::tensor::Tensor<_, 3>`
found struct `burn::tensor::Tensor<_, 4>`
error[E0308]: mismatched types
--> node/src/burn_smollm/attention.rs:60:13
|
60 | v = v.reshape([batch, seq_len, self.num_kv_heads, self.head_dim]).swap_dims(1, 2);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `3`, found `4`
|
= note: expected struct `burn::tensor::Tensor<_, 3>`
found struct `burn::tensor::Tensor<_, 4>`
error[E0308]: mismatched types
--> node/src/burn_smollm/attention.rs:63:31
|
63 | q = self.rope.forward(q, offset);
| ------- ^ expected `4`, found `3`
| |
| arguments to this method are incorrect
|
= note: expected struct `burn::tensor::Tensor<_, 4>`
found struct `burn::tensor::Tensor<_, 3>`
note: method defined here
--> node/src/burn_smollm/rope.rs:35:12
|
35 | pub fn forward(&self, x: Tensor<B, 4>, offset: usize) -> Tensor<B, 4> {
| ^^^^^^^ ---------------
error[E0308]: mismatched types
--> node/src/burn_smollm/attention.rs:63:13
|
63 | q = self.rope.forward(q, offset);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `3`, found `4`
|
= note: expected struct `burn::tensor::Tensor<_, 3>`
found struct `burn::tensor::Tensor<_, 4>`
error[E0308]: mismatched types
--> node/src/burn_smollm/attention.rs:64:31
|
64 | k = self.rope.forward(k, offset);
| ------- ^ expected `4`, found `3`
| |
| arguments to this method are incorrect
|
= note: expected struct `burn::tensor::Tensor<_, 4>`
found struct `burn::tensor::Tensor<_, 3>`
note: method defined here
--> node/src/burn_smollm/rope.rs:35:12
|
35 | pub fn forward(&self, x: Tensor<B, 4>, offset: usize) -> Tensor<B, 4> {
| ^^^^^^^ ---------------
error[E0308]: mismatched types
--> node/src/burn_smollm/attention.rs:64:13
|
64 | k = self.rope.forward(k, offset);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `3`, found `4`
|
= note: expected struct `burn::tensor::Tensor<_, 3>`
found struct `burn::tensor::Tensor<_, 4>`
error[E0308]: mismatched types
--> node/src/burn_smollm/attention.rs:68:41
|
68 | c.k = Tensor::cat(vec![c.k, k], 2);
| ^ expected `4`, found `3`
|
= note: expected struct `burn::tensor::Tensor<_, 4>`
found struct `burn::tensor::Tensor<_, 3>`
error[E0308]: mismatched types
--> node/src/burn_smollm/attention.rs:69:41
|
69 | c.v = Tensor::cat(vec![c.v, v], 2);
| ^ expected `4`, found `3`
|
= note: expected struct `burn::tensor::Tensor<_, 4>`
found struct `burn::tensor::Tensor<_, 3>`
error[E0308]: `if` and `else` have incompatible types
--> node/src/burn_smollm/attention.rs:72:13
|
67 | let (k, v) = if let Some(mut c) = cache {
| ______________________-
68 | | c.k = Tensor::cat(vec![c.k, k], 2);
69 | | c.v = Tensor::cat(vec![c.v, v], 2);
70 | | (c.k.clone(), c.v.clone())
| | -------------------------- expected because of this
71 | | } else {
72 | | (k.clone(), v.clone())
| | ^^^^^^^^^^^^^^^^^^^^^^ expected `4`, found `3`
73 | | };
| |_________- `if` and `else` have incompatible types
|
= note: expected tuple `(burn::tensor::Tensor<_, 4>, burn::tensor::Tensor<_, 4>)`
found tuple `(burn::tensor::Tensor<_, 3>, burn::tensor::Tensor<_, 3>)`
error[E0282]: type annotations needed
--> node/src/burn_smollm/attention.rs:75:38
|
75 | let new_cache = KVCache { k: k.clone(), v: v.clone() };
| ^ cannot infer type
error[E0282]: type annotations needed
--> node/src/burn_smollm/attention.rs:75:52
|
75 | let new_cache = KVCache { k: k.clone(), v: v.clone() };
| ^ cannot infer type
error[E0277]: the trait bound `TensorData: From<burn::tensor::Data<f32, 2>>` is not satisfied
--> node/src/burn_smollm/loader.rs:18:44
|
18 | let t_burn = Tensor::<B, 2>::from_data(data, device);
| ------------------------- ^^^^ the trait `From<burn::tensor::Data<f32, 2>>` is not implemented for `TensorData`
| |
| required by a bound introduced by this call
|
= help: the following other types implement trait `From<T>`:
`TensorData` implements `From<&[E]>`
`TensorData` implements `From<&[usize]>`
`TensorData` implements `From<[E; A]>`
`TensorData` implements `From<[[E; B]; A]>`
`TensorData` implements `From<[[[E; C]; B]; A]>`
`TensorData` implements `From<[[[[E; D]; C]; B]; A]>`
`TensorData` implements `From<[[[[[Elem; E]; D]; C]; B]; A]>`
`TensorData` implements `From<[usize; A]>`
= note: required for `burn::tensor::Data<f32, 2>` to implement `Into<TensorData>`
note: required by a bound in `burn::tensor::Tensor::<B, D, K>::from_data`
--> /usr/local/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/burn-tensor-0.14.0/src/tensor/api/base.rs:719:12
|
717 | pub fn from_data<T>(data: T, device: &B::Device) -> Self
| --------- required by a bound in this associated function
718 | where
719 | T: Into<TensorData>,
| ^^^^^^^^^^^^^^^^ required by this bound in `Tensor::<B, D, K>::from_data`
error[E0277]: the trait bound `TensorData: From<burn::tensor::Data<f32, 1>>` is not satisfied
--> node/src/burn_smollm/loader.rs:33:53
|
33 | Ok(Param::from_tensor(Tensor::<B, 1>::from_data(data, device)))
| ------------------------- ^^^^ the trait `From<burn::tensor::Data<f32, 1>>` is not implemented for `TensorData`
| |
| required by a bound introduced by this call
|
= help: the following other types implement trait `From<T>`:
`TensorData` implements `From<&[E]>`
`TensorData` implements `From<&[usize]>`
`TensorData` implements `From<[E; A]>`
`TensorData` implements `From<[[E; B]; A]>`
`TensorData` implements `From<[[[E; C]; B]; A]>`
`TensorData` implements `From<[[[[E; D]; C]; B]; A]>`
`TensorData` implements `From<[[[[[Elem; E]; D]; C]; B]; A]>`
`TensorData` implements `From<[usize; A]>`
= note: required for `burn::tensor::Data<f32, 1>` to implement `Into<TensorData>`
note: required by a bound in `burn::tensor::Tensor::<B, D, K>::from_data`
--> /usr/local/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/burn-tensor-0.14.0/src/tensor/api/base.rs:719:12
|
717 | pub fn from_data<T>(data: T, device: &B::Device) -> Self
| --------- required by a bound in this associated function
718 | where
719 | T: Into<TensorData>,
| ^^^^^^^^^^^^^^^^ required by this bound in `Tensor::<B, D, K>::from_data`
error[E0277]: the trait bound `TensorData: From<burn::tensor::Data<f32, 2>>` is not satisfied
--> node/src/burn_smollm/loader.rs:47:53
|
47 | Ok(Param::from_tensor(Tensor::<B, 2>::from_data(data, device)))
| ------------------------- ^^^^ the trait `From<burn::tensor::Data<f32, 2>>` is not implemented for `TensorData`
| |
| required by a bound introduced by this call
|
= help: the following other types implement trait `From<T>`:
`TensorData` implements `From<&[E]>`
`TensorData` implements `From<&[usize]>`
`TensorData` implements `From<[E; A]>`
`TensorData` implements `From<[[E; B]; A]>`
`TensorData` implements `From<[[[E; C]; B]; A]>`
`TensorData` implements `From<[[[[E; D]; C]; B]; A]>`
`TensorData` implements `From<[[[[[Elem; E]; D]; C]; B]; A]>`
`TensorData` implements `From<[usize; A]>`
= note: required for `burn::tensor::Data<f32, 2>` to implement `Into<TensorData>`
note: required by a bound in `burn::tensor::Tensor::<B, D, K>::from_data`
--> /usr/local/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/burn-tensor-0.14.0/src/tensor/api/base.rs:719:12
|
717 | pub fn from_data<T>(data: T, device: &B::Device) -> Self
| --------- required by a bound in this associated function
718 | where
719 | T: Into<TensorData>,
| ^^^^^^^^^^^^^^^^ required by this bound in `Tensor::<B, D, K>::from_data`
error[E0599]: no function or associated item named `arange` found for struct `burn::tensor::Tensor<B, 1>` in the current scope
--> node/src/burn_smollm/rope.rs:19:33
|
19 | let t = Tensor::<B, 1>::arange(0..max_seq_len as i64, device).float().unsqueeze::<2>().transpose();
| ^^^^^^ function or associated item not found in `burn::tensor::Tensor<B, 1>`
|
note: if you're trying to build a new `burn::tensor::Tensor<B, 1>` consider using one of the following associated functions:
burn::tensor::Tensor::<B, D, K>::new
burn::tensor::Tensor::<B, D, K>::from_primitive
burn::tensor::Tensor::<B, D, K>::empty
burn::tensor::Tensor::<B, D, K>::from_data
and 9 others
--> /usr/local/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/burn-tensor-0.14.0/src/tensor/api/base.rs:24:10
|
24 | #[derive(new, Clone, Debug)]
| ^^^
...
55 | pub fn from_primitive(tensor: K::Primitive<D>) -> Self {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...
60 | pub fn empty<S: Into<Shape<D>>>(shape: S, device: &B::Device) -> Self {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...
717 | / pub fn from_data<T>(data: T, device: &B::Device) -> Self
718 | | where
719 | | T: Into<TensorData>,
| |____________________________^
= note: the function or associated item was found for
- `burn::tensor::Tensor<B, 1, burn::tensor::Int>`
= note: this error originates in the derive macro `new` (in Nightly builds, run with -Z macro-backtrace for more info)
warning: variable does not need to be mutable
--> node/src/burn_smollm/loader.rs:70:13
|
70 | let mut layer = &mut model.layers[i];
| ----^^^^^
| |
| help: remove this `mut`
|
= note: `#[warn(unused_mut)]` (part of `#[warn(unused)]`) on by default
warning: unused variable: `batch`
--> node/src/burn_smollm/model.rs:79:14
|
79 | let [batch, seq_len] = input_ids.dims();
| ^^^^^ help: if this is intentional, prefix it with an underscore: `_batch`
|
= note: `#[warn(unused_variables)]` (part of `#[warn(unused)]`) on by default
warning: unused variable: `seq_len`
--> node/src/burn_smollm/model.rs:79:21
|
79 | let [batch, seq_len] = input_ids.dims();
| ^^^^^^^ help: if this is intentional, prefix it with an underscore: `_seq_len`
Some errors have detailed explanations: E0061, E0277, E0282, E0308, E0599.
For more information about an error, try `rustc --explain E0061`.
warning: `node` (lib) generated 19 warnings
error: could not compile `node` (lib) due to 21 previous errors; 19 warnings emitted
Error: Compiling your crate to WebAssembly failed
Caused by: Compiling your crate to WebAssembly failed
Caused by: failed to execute `cargo build`: exited with exit status: 101
full command: cd "/app/node" && "cargo" "build" "--lib" "--release" "--target" "wasm32-unknown-unknown"

348
network-poc/PROMPTS.md Normal file
View File

@@ -0,0 +1,348 @@
# Kipinä Agentic Studio — Promptit
Kaikki järjestelmässä käytetyt promptit. Jokainen on dokumentoitu eksaktisti
niin kuin se lähetetään mallille, muuttujat merkitty `${...}`-syntaksilla.
---
## 1. Inferenssin system prompt (Wasm + natiivi)
**Sijainti:** `node/src/qwen_coder.rs` rivi 256, `native-node/src/inference.rs` rivi 127
**Malli:** Qwen2.5-Coder-0.5B/3B
**ChatML-rooli:** `<|im_start|>system`
```
You are a coding assistant. Respond with ONLY code. No explanations, no markdown, no comments unless asked.
```
**Tarkoitus:** Pakottaa malli tuottamaan pelkkää koodia ilman selityksiä.
**Prefill:** Assistantin vastaus alkaa ` ``` ` joka ohjaa mallin koodiblokkiin.
---
## 2. Agenttikohtaiset system promptit (frontend)
**Sijainti:** `static/index.html` rivit 1136-1144
**Tallennus:** localStorage (`kpn-agent-prompt-{key}`)
**ChatML-rooli:** Liitetään `<|im_start|>user` -blokkiin osaksi promptia
### 2.1 Manageri (manager)
```
Olet projektipäällikkö. Jaa tehtävät osiin, priorisoi ja koordinoi tiimin työtä.
```
**Malli:** qwen-coder
### 2.2 Koodari (coder)
```
Olet kokenut ohjelmistokehittäjä. Kirjoita selkeää, testattavaa koodia ja vastaa aina koodilla.
```
**Malli:** qwen-coder
### 2.3 Data-agentti (data)
```
Olet tietokanta-asiantuntija. Vastaat skeemojen suunnittelusta, SQL-kyselyiden optimoinnista ja datamalleista.
```
**Malli:** qwen-coder
### 2.4 QA (qa)
```
Olet laadunvarmistaja (QA). Kirjoitat testejä, etsit virheitä ja varmistat, että kaikki reunatapaukset on huomioitu.
```
**Malli:** smollm-135m
### 2.5 DevOps / Testaaja (tester)
```
Olet DevOps-insinööri. Vastaat koodin julkaisuputkista, serveri-infrastruktuurista ja ympäristön suorituskyvystä.
```
**Malli:** smollm-135m
### 2.6 Tarkkailija (observer)
```
Olet ohjelmistoprojektin riippumaton valvoja. Sinulla on täysi pääsy kaikkiin projektin tietoihin ja muiden agenttien keskusteluihin. Valvo tiimin (Manageri, Koodari, Data, QA, DevOps) toimintaa asiantuntijana kokonaisuutena ja huomauta välittömästi visio- tai turvallisuusriskeistä.
```
**Malli:** deepseek-r1
### 2.7 Asiakas (client)
```
Kirjoita tähän asiakkaan toiveet ja projektin vaatimukset. Orkestraattori (Manageri) purkaa ja delegoi nämä työt asiantuntijoille.
```
**Malli:** user-input (ei LLM:ää, käyttäjän teksti)
---
## 3. Projekti-pipeline (`kpn project`)
### 3.1 Vaihe 1: Managerin tiedostojako
**Konteksti:** Käyttäjä on antanut projektin kuvauksen.
**Tavoite:** Pilkotaan projekti yksittäisiksi tiedostoiksi oikeassa riippuvuusjärjestyksessä.
```
List the source files needed for this project. One file per line, format:
filename.py: what this file contains
Rules:
- Max 4 files
- Only .py, .toml, .json, .html files
- No directories, no paths, just filenames
- List dependencies first, then main app (e.g. models.py before main.py)
- Use pyproject.toml for dependencies (not requirements.txt)
Project: ${task}
```
**Odotettu vastausformaatti:**
```
models.py: SQLAlchemy User model and database setup
main.py: FastAPI app with CRUD endpoints
pyproject.toml: project dependencies
```
**Parsintasäännöt:**
- Rivi voi olla `filename.ext: kuvaus` tai pelkkä `filename.ext`
- Tiedostonimessä ei saa olla `/`, välilyöntejä tai polkuja
- Päättyy tiedostopäätteeseen (`/\.\w{1,5}$/`)
- Numerot, `-`, `*` ja `` ` `` strippataan rivin alusta
- Max 40 merkin tiedostonimi
### 3.2 Vaihe 2: Koodarin tiedostogenerointi (per tiedosto)
**Konteksti:** Managerin tiedostolista on parsittu. Jokaiselle tiedostolle generoidaan koodi erikseen. Aiemmin generoidut tiedostot annetaan kontekstina.
**Perusmuoto:**
```
${context}Project: ${task}
Write ONLY the file "${filename}"${description ? ': ' + description : ''}.
Use the exact libraries mentioned in the project description. Write correct, working code.
```
**`${context}` (kun aiempia tiedostoja on generoitu):**
```
Already written files:
--- models.py ---
from sqlalchemy import ...
...
--- main.py ---
from fastapi import ...
...
```
**Erikoistapaus: pyproject.toml**
Koska 0.5B-malli ei tunne uv/pyproject.toml-formaattia, annetaan eksplisiittinen esimerkki:
```
${context}Project: ${task}
Write ONLY the file "pyproject.toml": ${description}.
Use this exact format:
[project]
name = "projectname"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = ["fastapi", "uvicorn"]
[project.scripts]
start = "uvicorn main:app --reload"
Use the exact libraries mentioned in the project description. Write correct, working code.
```
**Erikoistapaus: requirements.txt (fallback)**
```
...
List one dependency per line. No version pins unless necessary.
...
```
### 3.3 Vaihe 2 (fallback): Yhtenä kokonaisuutena
Jos managerin vastaus ei tuota parsittavaa tiedostolistaa:
```
Project: ${task}
Files: ${managerin_vastaus}
Write all the code for this project. Use the exact libraries mentioned in the project description. Use pyproject.toml for dependencies (not requirements.txt).
```
### 3.4 Vaihe 3: Testerin arviointi
**Konteksti:** Kaikki generoidut tiedostot yhdistettynä.
```
Review this project. List bugs or issues. Be brief.
If the code is correct, say "LGTM".
--- models.py ---
from sqlalchemy import ...
--- main.py ---
from fastapi import ...
```
**Odotettu vastaus:** Bugilista tai `LGTM`.
**Trigger korjausluuppiin:** Jos vastaus EI sisällä "lgtm" tai "looks good" (case-insensitive).
### 3.5 Vaihe 4: Koodarin korjaukset (ehdollinen)
Ajetaan vain jos testeri löysi ongelmia.
```
Fix the issues found in the review.
Review feedback: ${review}
Current code:
--- models.py ---
...
--- main.py ---
...
Write the corrected code.
```
### 3.6 Vaihe 5: Testerin uudelleenarviointi (ehdollinen)
```
Review the corrected code briefly:
${fixedCode}
```
---
## 4. Yksinkertainen pipeline (`kpn pipeline`)
### 4.1 Manageri
```
Analyse this task briefly and write a technical spec for a coder:
${task}
```
### 4.2 Koodari
```
${managerin_vastaus}
Write the code.
```
### 4.3 Testaaja
```
Review briefly:
${koodarin_vastaus}
```
---
## 5. Yksittäiset komennot
### 5.1 `kpn run <malli> "<prompti>"`
Promptin koostaminen `kpnRun`-funktiossa:
```
${sharedPrompt} ← Kaikille agenteille yhteinen (jos asetettu)
${agentPrompt} ← Valitun agentin system prompt (jos löytyy)
${käyttäjän_prompti} ← Käyttäjän kirjoittama teksti
```
Osat yhdistetään `\n\n`-erottimella ja lähetetään `<|im_start|>user`-blokkiin.
### 5.2 `kpn hello`
Kiinteä prompti SmolLM-135M -mallille:
```
Tervehdi käyttäjää iloisesti ja lyhyesti suomeksi. Ole innostunut ja energinen! Vastaa yhdellä lauseella.
```
### 5.3 Warmup (automaattinen)
Lähetetään automaattisesti kun laskentasolmu käynnistyy. Triggeröi mallin latauksen ilman näkyvää tulosta.
```json
{"prompt": "warmup", "max_tokens": 1}
```
---
## 6. Stop-sekvenssit (inferenssi)
**Sijainti:** `node/src/qwen_coder.rs` rivi 345, `native-node/src/inference.rs` rivi 210
Generointi katkaistaan ja teksti trimmataan kun malli tuottaa minkä tahansa näistä:
| Sekvenssi | Tarkoitus |
|-----------|-----------|
| `\n###` | Markdown-otsikko (selitysosio alkaa) |
| `\nExplanation` | Selitysosio |
| `\nNote:` | Huomautus |
| `\nOutput:` | Esimerkkitulostus |
| `` \n```\n\n `` | Koodiblokin loppu + tyhjä rivi |
| `\n// Example` | Esimerkkikoodi (C/Rust/JS) |
| `\n// example` | Sama pienellä |
| `\n# Example` | Esimerkkikoodi (Python/Ruby) |
| `\n# example` | Sama pienellä |
---
## 7. Vastauksen siivous (post-processing)
**Sijainti:** `strip_markdown_wrapper()` molemmissa inferenssimoduuleissa
### 7.1 Kielitunnisteen poisto
Jos ensimmäinen rivi on tunnettu kielitunniste, se poistetaan.
Tunnistetut: `python`, `py`, `rust`, `rs`, `javascript`, `js`, `typescript`, `ts`,
`java`, `kotlin`, `scala`, `go`, `ruby`, `rb`, `php`, `swift`,
`c`, `cpp`, `c++`, `c#`, `csharp`, `r`, `sql`, `bash`, `sh`, `zsh`,
`html`, `css`, `json`, `yaml`, `yml`, `toml`, `xml`, `markdown`, `md`,
`lua`, `perl`, `dart`, `elixir`, `haskell`, `hs`, `ocaml`, `zig`,
`plaintext`, `text`, `txt`
### 7.2 Sulkevan ` ``` ` poisto
Poistetaan VAIN jos ` ``` ` on omalla rivillään tiedoston lopussa
(edeltävä merkki on rivinvaihto tai alku).
### 7.3 Johdantolauseiden poisto
Ensimmäinen rivi poistetaan jos se alkaa (case-insensitive):
`Sure!`, `Here is`, `Here's`, `Certainly!`, `Below is`
### 7.4 Selityskommenttien poisto
Alun `# `-alkuiset rivit poistetaan jos ne sisältävät (case-insensitive):
`this is`, `simple`, `program that`, `here is`, `the following`, `below`
Shebang (`#!`) säilytetään.
---
## 8. Promptin kulku mallille (ChatML)
Lopullinen viesti mallille koostetaan näin:
```
<|im_start|>system
You are a coding assistant. Respond with ONLY code. No explanations, no markdown, no comments unless asked.<|im_end|>
<|im_start|>user
${sharedPrompt}
${agentPrompt}
${käyttäjän/pipelinen prompti}<|im_end|>
<|im_start|>assistant
```
```
**Huomio:** ` ``` ` assistantin alussa on prefill — se on osa syötettä eikä mallin tuottamaa. Malli jatkaa suoraan koodilla.
---
## 9. Sampling-parametrit
| Parametri | Arvo | Kuvaus |
|-----------|------|--------|
| `temperature` | 0.7 | Jakaumaa pehmentävä kerroin |
| `top_k` | 40 | Valinnan rajoitus 40 todennäköisimpään tokeniin |
| `repetition_penalty` | 1.15 | Aiemmin generoitujen tokenien rankaisu |
| `max_tokens` | 512 (oletus) | Konfiguroitavissa JSON-promptilla |
| `eos_token` | 151645 | Qwen2.5:n päätöstokeni |

View File

@@ -9,7 +9,7 @@ services:
volumes:
- .:/app
# Käännetään aina käynnistyksen yhteydessä varmuuden vuoksi Wasm uusimmista koodeista, ja päälle pyöräytetään Hub!
command: bash -c "cd node && wasm-pack build --target web --out-dir ../static/pkg && cd ../hub && cargo run"
command: bash -c "cd node && wasm-pack build --release --target web --out-dir ../static/pkg && cd ../hub && cargo run"
# Valinnainen natiivi-solmu — kerää oikeat laitteistotiedot (nvidia-smi-taso)
native-node:

Binary file not shown.

View File

@@ -486,15 +486,15 @@ async fn ws_handler(
.and_then(|s| s.trim().parse::<IpAddr>().ok())
.unwrap_or_else(|| addr.ip());
// Max 2 yhteyttä per IP (dashboard-UI + selainsolmu)
// Max yhteyttä per IP: jokainen selain tarvitsee 2 (UI + coder-node)
{
let conns = state.ip_connections.lock().unwrap();
let count = conns.get(&ip).copied().unwrap_or(0);
if count >= 4 {
tracing::warn!("IP {} ylitti yhteysrajan ({}/4) — estetty", ip, count);
if count >= 10 {
tracing::warn!("IP {} ylitti yhteysrajan ({}/10) — estetty", ip, count);
return (
axum::http::StatusCode::TOO_MANY_REQUESTS,
"Max 4 yhteyttä per IP",
"Max 10 yhteyttä per IP",
).into_response();
}
}
@@ -978,7 +978,7 @@ async fn api_chat_completions(
let busy = state.node_busy.lock().unwrap();
let matching: Vec<u64> = tasks.iter().filter(|(_, task)| {
if payload.model == "qwen-coder" {
*task == "qwen-coder-05b" || *task == "qwen-coder"
task.starts_with("qwen-coder")
} else {
**task == payload.model
}
@@ -1080,7 +1080,7 @@ async fn api_chat_completions(
}
}
let timeout = tokio::time::timeout(std::time::Duration::from_secs(120), async move {
let timeout = tokio::time::timeout(std::time::Duration::from_secs(600), async move {
loop {
let msg_str = match rx.recv().await {
Ok(msg) => msg,

View File

@@ -207,8 +207,8 @@ impl LlmEngine {
// Stop-sekvenssit: katkaistaan kun malli alkaa selittää
let lower = generated_text.to_lowercase();
if lower.contains("\n###") || lower.contains("\nexplanation") || lower.contains("\nnote:") || lower.contains("\noutput:") || lower.contains("\n```\n\n") {
for stop in &["\n###", "\nExplanation", "\nNote:", "\nOutput:", "\n```\n\n"] {
if lower.contains("\n###") || lower.contains("\nexplanation") || lower.contains("\nnote:") || lower.contains("\noutput:") || lower.contains("\n```\n\n") || lower.contains("\n// example") || lower.contains("\n# example") {
for stop in &["\n###", "\nExplanation", "\nNote:", "\nOutput:", "\n```\n\n", "\n// Example", "\n// example", "\n# Example", "\n# example"] {
if let Some(pos) = generated_text.find(stop) {
generated_text.truncate(pos);
}

View File

@@ -1,6 +1,8 @@
use candle_core::{Device, Tensor, DType};
use candle_core::quantized::gguf_file;
use candle_nn::VarBuilder;
use candle_transformers::models::qwen2::{Config as QwenConfig, ModelForCausalLM as QwenModel};
use candle_transformers::models::quantized_qwen2::ModelWeights as QwenQuantizedModel;
use wasm_bindgen::JsCast;
use std::cell::RefCell;
use std::rc::Rc;
@@ -16,13 +18,36 @@ macro_rules! console_log {
const MODEL_05B_URL: &str = "https://huggingface.co/Qwen/Qwen2.5-Coder-0.5B-Instruct/resolve/main/model.safetensors";
const TOKENIZER_05B_URL: &str = "https://huggingface.co/Qwen/Qwen2.5-Coder-0.5B-Instruct/resolve/main/tokenizer.json";
// 3B — parempi laatu, vaatii enemmän muistia (~6 GB lataus, ~12 GB RAM)
const MODEL_3B_PART1_URL: &str = "https://huggingface.co/Qwen/Qwen2.5-Coder-3B-Instruct/resolve/main/model-00001-of-00002.safetensors";
const MODEL_3B_PART2_URL: &str = "https://huggingface.co/Qwen/Qwen2.5-Coder-3B-Instruct/resolve/main/model-00002-of-00002.safetensors";
const TOKENIZER_3B_URL: &str = "https://huggingface.co/Qwen/Qwen2.5-Coder-3B-Instruct/resolve/main/tokenizer.json";
// 1.5B GGUF Q4_K_M — kvantisoidtu, mahtuu selaimeen (~1 GB)
const MODEL_GGUF_URL: &str = "https://huggingface.co/Qwen/Qwen2.5-Coder-1.5B-Instruct-GGUF/resolve/main/qwen2.5-coder-1.5b-instruct-q4_k_m.gguf";
const TOKENIZER_GGUF_URL: &str = "https://huggingface.co/Qwen/Qwen2.5-Coder-1.5B-Instruct/resolve/main/tokenizer.json";
enum CoderModel {
Full(QwenModel),
Quantized(QwenQuantizedModel),
}
impl CoderModel {
fn forward(&mut self, x: &Tensor, pos: usize) -> candle_core::Result<Tensor> {
match self {
CoderModel::Full(m) => m.forward(x, pos),
CoderModel::Quantized(m) => m.forward(x, pos),
}
}
fn clear_kv_cache(&mut self) {
match self {
CoderModel::Full(m) => m.clear_kv_cache(),
CoderModel::Quantized(_) => {
// Quantized model nollaa KV-cachen automaattisesti kun forward kutsutaan pos=0:lla
// (ks. quantized_qwen2.rs rivi 118: if index_pos == 0)
}
}
}
}
struct CachedModel {
model: QwenModel,
model: CoderModel,
tokenizer: tokenizers::Tokenizer,
is_3b: bool,
}
@@ -182,50 +207,39 @@ async fn get_or_build_model(use_3b: bool, ws: &Rc<RefCell<WebSocket>>) -> Result
let dtype = DType::F32;
// Tokenizer
let tok_url = if use_3b { TOKENIZER_3B_URL } else { TOKENIZER_05B_URL };
let tok_key = if use_3b { "coder3b-tokenizer.json" } else { "coder05b-tokenizer.json" };
let tok_url = if use_3b { TOKENIZER_GGUF_URL } else { TOKENIZER_05B_URL };
let tok_key = if use_3b { "coder15b-tokenizer.json" } else { "coder05b-tokenizer.json" };
let tok_bytes = ensure_cached(tok_key, tok_url, ws).await?;
let tokenizer = tokenizers::Tokenizer::from_bytes(&tok_bytes[..])
.map_err(|e| format!("Tokenizer: {}", e))?;
// Painot
let tensors = if use_3b {
let part1 = ensure_cached("coder3b-model-part1.safetensors", MODEL_3B_PART1_URL, ws).await?;
let part2 = ensure_cached("coder3b-model-part2.safetensors", MODEL_3B_PART2_URL, ws).await?;
console_log!("[Coder] Rakennetaan 3B-mallia...");
let mut all_tensors = candle_core::safetensors::load_buffer(&part1[..], &device)
.map_err(|e| format!("Part1: {}", e))?;
let tensors2 = candle_core::safetensors::load_buffer(&part2[..], &device)
.map_err(|e| format!("Part2: {}", e))?;
all_tensors.extend(tensors2);
all_tensors
let model = if use_3b {
// GGUF Q4_K_M — kvantisoidtu 3B-malli (~1.9 GB)
let gguf_bytes = ensure_cached("coder15b-q4km.gguf", MODEL_GGUF_URL, ws).await?;
console_log!("[Coder] Rakennetaan kvantisoidun 1.5B-mallia (Q4_K_M)...");
let mut cursor = std::io::Cursor::new(&gguf_bytes[..]);
let content = gguf_file::Content::read(&mut cursor)
.map_err(|e| format!("GGUF parse: {}", e))?;
let qmodel = QwenQuantizedModel::from_gguf(content, &mut cursor, &device)
.map_err(|e| format!("GGUF model: {}", e))?;
CoderModel::Quantized(qmodel)
} else {
let model_bytes = ensure_cached("coder05b-model.safetensors", MODEL_05B_URL, ws).await?;
console_log!("[Coder] Rakennetaan 0.5B-mallia...");
candle_core::safetensors::load_buffer(&model_bytes[..], &device)
.map_err(|e| format!("Safetensors: {}", e))?
};
let vb = VarBuilder::from_tensors(tensors, dtype, &device);
let config = if use_3b {
QwenConfig {
vocab_size: 151936, hidden_size: 2048, intermediate_size: 11008,
num_hidden_layers: 36, num_attention_heads: 16, num_key_value_heads: 2,
max_position_embeddings: 32768, sliding_window: 32768, max_window_layers: 36,
tie_word_embeddings: true, rope_theta: 1000000.0, rms_norm_eps: 1e-6,
use_sliding_window: false, hidden_act: candle_nn::Activation::Silu,
}
} else {
QwenConfig {
let tensors = candle_core::safetensors::load_buffer(&model_bytes[..], &device)
.map_err(|e| format!("Safetensors: {}", e))?;
let config = QwenConfig {
vocab_size: 151936, hidden_size: 896, intermediate_size: 4864,
num_hidden_layers: 24, num_attention_heads: 14, num_key_value_heads: 2,
max_position_embeddings: 32768, sliding_window: 32768, max_window_layers: 21,
tie_word_embeddings: true, rope_theta: 1000000.0, rms_norm_eps: 1e-6,
use_sliding_window: false, hidden_act: candle_nn::Activation::Silu,
}
};
let vb = VarBuilder::from_tensors(tensors, dtype, &device);
let qwen = QwenModel::new(&config, vb).map_err(|e| format!("Malli: {}", e))?;
CoderModel::Full(qwen)
};
let model = QwenModel::new(&config, vb).map_err(|e| format!("Malli: {}", e))?;
console_log!("[Coder] Malli ladattu ja välimuistitettu");
MODEL_CACHE.with(|c| {
@@ -258,13 +272,13 @@ pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&prompt) {
let p = json.get("prompt").and_then(|v| v.as_str()).unwrap_or(&prompt).to_string();
let s = json.get("system").and_then(|v| v.as_str()).unwrap_or(default_system).to_string();
let m = json.get("max_tokens").and_then(|v| v.as_u64()).unwrap_or(128) as usize;
let m = json.get("max_tokens").and_then(|v| v.as_u64()).unwrap_or(512) as usize;
(p, s, m)
} else {
(prompt.clone(), default_system.to_string(), 128)
(prompt.clone(), default_system.to_string(), 512)
}
} else {
(prompt.clone(), default_system.to_string(), 128)
(prompt.clone(), default_system.to_string(), 512)
};
// Prefill: aloitetaan vastaus ```-koodiblokkilla, jolloin malli jatkaa suoraan koodilla
@@ -342,8 +356,8 @@ pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use
// Stop-sekvenssit: katkaistaan kun malli alkaa selittää
let lower = generated_text.to_lowercase();
if lower.contains("\n###") || lower.contains("\nexplanation") || lower.contains("\nnote:") || lower.contains("\noutput:") || lower.contains("\n```\n\n") {
for stop in &["\n###", "\nExplanation", "\nNote:", "\nOutput:", "\n```\n\n"] {
if lower.contains("\n###") || lower.contains("\nexplanation") || lower.contains("\nnote:") || lower.contains("\noutput:") || lower.contains("\n```\n\n") || lower.contains("\n// example") || lower.contains("\n# example") {
for stop in &["\n###", "\nExplanation", "\nNote:", "\nOutput:", "\n```\n\n", "\n// Example", "\n// example", "\n# Example", "\n# example"] {
if let Some(pos) = generated_text.find(stop) {
generated_text.truncate(pos);
}

429
network-poc/static/GUIDE.md Normal file
View File

@@ -0,0 +1,429 @@
# Kipinä Agentic Studio — Opas
Hajautettu AI-laskentaverkko jossa kielimallit ajavat koodia suoraan selaimessa.
Tämä opas selittää miten kielimallit toimivat, miten niitä ohjataan, ja miten
tuloksia voi parantaa.
---
## Kielimallit ja niiden koot
Kielimalli on neuroverkko joka ennustaa seuraavan sanan (tokenin) edellisten
perusteella. Mallin "koko" tarkoittaa parametrien (painojen) määrää:
| Malli | Parametrit | Koko levyllä | Nopeus selaimessa | Koodinlaatu |
|-------|-----------|-------------|-------------------|-------------|
| SmolLM 135M | 135 miljoonaa | ~270 MB | ~5 tok/s | Yksinkertainen teksti |
| Qwen2.5-Coder:0.5B | 500 miljoonaa | ~990 MB | ~3-6 tok/s | Pienet funktiot |
| Qwen2.5-Coder:3B | 3 miljardia | ~6.2 GB | ~0.4 tok/s | Kokonaiset tiedostot |
| GPT-4 (vertailu) | ~1800 miljardia | ~3.6 TB | pilvipalvelu | Kokonaiset projektit |
**Parametrien vaikutus:** Jokainen parametri on yksi liukuluku (float16 = 2 tavua)
joka tallentaa opittua tietoa. 0.5B-malli tietää perusrakenteet mutta tekee
loogisia virheitä. 3B-malli ymmärtää kontekstin paremmin. Ero on kuin sanakirjan
ja oppikirjan välillä.
**Miksi selaimessa?** Malli ajetaan käyttäjän omalla laitteella WebAssemblyn
kautta. Data ei lähde koneelta, eikä tarvita pilvipalvelua. Haittapuoli on
hitaus — GPU-palvelimella sama 0.5B-malli tuottaa ~100 tok/s.
---
## Tokenit — kielimallin "sanat"
Malli ei näe tekstiä kirjaimina vaan **tokeneina**. Tokeni on yleensä
sanan osa, kokonainen sana tai välilyönti. Tokenisaatio tehdään
BPE-algoritmilla (Byte Pair Encoding) joka oppii yleisimmät
merkkijonot harjoitusdatasta.
### Esimerkki: koodi
```
"print('Hello')" → [print] [(' ] [Hello] [')] = 4 tokenia
"tulosta('Hei')" → [tul] [osta] [(' ] [He] [i] [')] = 6 tokenia
```
Koodi tokenisoidaan tehokkaasti koska `print`, `def`, `return` yms.
ovat kokonaisia tokeneita. Suomenkielinen `tulosta` joudutaan pilkkomaan
osiin koska se ei esiinny harjoitusdatassa kokonaisena.
### Esimerkki: suomi vs. englanti
Sama lause kahdella kielellä Qwen2.5-Coder -tokenisaattorilla:
| | Teksti | Tokenit | Määrä | Merkkejä/token |
|---|---|---|---|---|
| EN | The cat sat on the mat | [The] [ cat] [ sat] [ on] [ the] [ mat] | **6** | 3.7 |
| FI | Kissa istui matolla | [K] [issa] [ ist] [ui] [ mat] [olla] | **6** | 3.2 |
| EN | Distributed computing in the browser | [Dist] [ributed] [ computing] [ in] [ the] [ browser] | **6** | 6.0 |
| FI | Hajautettu laskenta selaimessa | [H] [aj] [au] [tettu] [ las] [kenta] [ sel] [aim] [essa] | **9** | 3.3 |
| EN | Write a function that sorts a list | [Write] [ a] [ function] [ that] [ sorts] [ a] [ list] | **7** | 5.0 |
| FI | Kirjoita funktio joka lajittelee listan | [K] [irj] [oita] [ funkt] [io] [ joka] [ laj] [ittel] [ee] [ listan] | **10** | 4.0 |
**Huomaa miten:**
- Englannin yleiset sanat (`the`, `in`, `a`, `function`) ovat kokonaisia tokeneita
- Suomen sanat pilkotaan pienempiin osiin (`Hajautettu` → 4 tokenia, `Distributed` → 2)
- Suomi vaatii **30-50% enemmän tokeneita** saman merkityksen välittämiseen
- Koodiavainsanat (`function`, `list`, `sort`) ovat tehokkaita molemmilla kielillä
### Miksi tämä merkitsee?
**Jokainen tokeni = yksi laskentakierros.** Jos suomi vaatii 50% enemmän tokeneita:
1. **Hitaampi vastaus:** 100 tokenin englanninkielinen vastaus ≈ 150 tokenia suomeksi
→ 50% pidempi odotusaika
2. **Pienempi konteksti:** Sama merkityssisältö vie enemmän tilaa konteksti-ikkunasta
3. **Huonompi ymmärrys:** Pitkät sanat pilkotaan osiin jotka malli ei välttämättä
tunnista → hallusinaatiot lisääntyvät
**Siksi tekniset promptit ovat englanniksi** — malli saa enemmän informaatiota
samassa token-budjetissa ja ymmärtää ohjeet paremmin.
**Token-budjetti tässä järjestelmässä:**
| Osa | Tokeneita | Osuus |
|-----|-----------|-------|
| System prompt | ~30 | kiinteä |
| Agent prompt | ~25 | kiinteä |
| Konteksti (aiemmat tiedostot) | 0-300 | kasvaa |
| Käyttäjän prompti | ~20-50 | vaihtelee |
| **Syöte yhteensä** | **~75-400** | |
| Generoitu vastaus (max) | 512 | raja |
| **Yhteensä** | **~600-900** | /32 768 |
Konteksti-ikkuna on reilusti riittävä. Pullonkaula ei ole ikkunan koko
vaan **mallin kyky ymmärtää pitkää kontekstia** — 0.5B-malli alkaa
"unohtaa" ohjeet kun konteksti kasvaa yli ~200 tokenin.
---
## Promptit — miten mallia ohjataan
### Kolmitasoinen prompttirakenne
```mermaid
flowchart TD
S["System prompt<br/><i>You are a coding assistant. Respond with ONLY code.</i><br/>🔒 Kiinteä, kovakoodattu — malli priorisoi tämän"]
A["Agent prompt<br/><i>Olet kokenut ohjelmistokehittäjä...</i><br/>✏️ Käyttäjän muokattavissa UI:ssa"]
U["User prompt<br/><i>Write ONLY the file main.py...</i><br/>📋 Vaihtelee joka kutsussa, sisältää kontekstin"]
P["Prefill: ``` <br/>🎯 Pakottaa mallin aloittamaan koodilla"]
S --> A --> U --> P
P -->|malli jatkaa| R["Generoitu koodi"]
style S fill:#1a1e2e,stroke:#f85149,color:#c9d1d9
style A fill:#1a1e2e,stroke:#d29922,color:#c9d1d9
style U fill:#1a1e2e,stroke:#3fb950,color:#c9d1d9
style P fill:#1a1e2e,stroke:#a371f7,color:#c9d1d9
style R fill:#0d1117,stroke:#58a6ff,color:#58a6ff
```
### Miksi promptit ovat englanniksi?
Qwen2.5-Coder on harjoitettu pääosin englanninkielisellä koodilla ja
dokumentaatiolla. Suomenkielinen ohje kuluttaa enemmän tokeneita JA
malli ymmärtää sen huonommin. Agenttien nimet ja käyttöliittymä ovat
suomeksi, mutta tekniset ohjeet mallille englanniksi.
Poikkeus: agenttipromptit ovat suomeksi koska ne menevät user-blokkiin
(ei system-blokkiin) ja niiden tarkoitus on enemmän "persoonallisuus"
kuin tekninen ohje.
---
## Prefill-tekniikka
Normaalisti malli päättää vapaasti miten vastaa:
```
Ilman prefilliä:
Malli: "Sure! Here is a Python program that prints Hello World:\n```python\nprint('Hello')\n```"
→ 25 tokenia, joista 15 turhia
Prefillin kanssa:
Me syötämme: ```
Malli jatkaa: python\nprint('Hello')\n```
→ 5 tokenia, kaikki hyödyllisiä
```
Prefill on kuin aloittaisit lauseen toisen puolesta — malli jatkaa
siitä mihin jäit sen sijaan, että aloittaisi kohteliaalla johdannolla.
**Sivuvaikutus:** Malli tuottaa kielitunnisteen (`python`, `rust`) ja
sulkevan ` ``` `:n. Nämä siivotaan jälkikäteen `strip_markdown_wrapper`-funktiolla.
---
## Sampling — miten malli valitsee seuraavan tokenin
Malli ei "tiedä" oikeaa vastausta. Se laskee jokaiselle mahdolliselle
seuraavalle tokenille todennäköisyyden ja valitsee yhden. Valintaa
ohjataan kolmella parametrilla:
### Temperature (0.7)
Kontrolloi "luovuutta" vs. "varmuutta":
```
Temperature 0.0 (greedy): Aina todennäköisin tokeni → "def fibonacci(n):"
Temperature 0.7 (oletus): Painottaa todennäköisiä mutta sallii vaihtelua
Temperature 1.5 (luova): Lähes satunnainen → "async lambda fib = ..."
```
0.7 on kompromissi: tarpeeksi determinististä tuottamaan toimivaa koodia,
mutta tarpeeksi vaihtelevaa välttämään toistoa.
### Top-k (40)
Rajaa valinnan 40 todennäköisimpään tokeniin. Estää mallia valitsemasta
täysin absurdeja vaihtoehtoja:
```
Ilman top-k: 150 936 vaihtoehtoa → voi valita minkä tahansa
Top-k 40: 40 vaihtoehtoa → järkevät vaihtoehdot
Top-k 1: 1 vaihtoehto → greedy (aina sama vastaus)
```
### Repetition penalty (1.15)
Vähentää jo tuotettujen tokenien todennäköisyyttä. Estää mallia
juuttumasta luuppiin:
```
Ilman rangaistusta: "print print print print print..."
Penalty 1.15: "print('Hello')\nprint('World')"
```
1.15 on lievä rangaistus — estää pahimman toiston mutta sallii
saman avainsanan (esim. `return`) esiintymisen useasti.
---
## Stop-sekvenssit — milloin generointi loppuu
Malli generoi tokeneita kunnes jokin näistä tapahtuu:
1. **EOS-tokeni** (151645): Mallin oma "loppu"-merkki
2. **Max tokens** (512): Kovakoodattu raja
3. **Stop-sekvenssi**: Malli alkaa tuottaa selitystä
```
fn fibonacci(n: usize) -> usize {
if n <= 1 { return n; }
fibonacci(n-1) + fibonacci(n-2)
}
← Tähän asti koodia, ok
// Example usage: ← Stop! Tämä ei ole enää vastausta
let result = fibonacci(10); ← Ei generoida
```
Tunnistetut stop-sekvenssit: `### `, `Explanation`, `Note:`, `Output:`,
`// Example`, `# Example`. Generointi katkaistaan ja teksti trimmataan
stop-kohtaan.
---
## Projekti-pipeline — miten agenttitiimi toimii
```mermaid
flowchart TD
U["Käyttäjä: FastAPI + SQLite REST API for users"] --> M
M["🟡 Manageri: Pilko tiedostoiksi"] -->|tiedostolista| C1
C1["🟢 Koodari: models.py"] -->|"konteksti: models.py"| C2
C2["🟢 Koodari: main.py"] -->|"konteksti: models + main"| C3
C3["🟢 Koodari: pyproject.toml"] -->|kaikki tiedostot| T1
T1["🔵 Testaaja: Review"] -->|bugeja löytyi| C4
T1 -->|LGTM| Done["✅ Projekti valmis"]
C4["🟡 Koodari: Korjaukset"] --> T2
T2["🔵 Testaaja: Uudelleenarviointi"] --> Done
```
**Kontekstin ketjutus** on kriittistä: kun koodari kirjoittaa `main.py`:tä,
se saa `models.py`:n sisällön promptissa. Ilman tätä se ei tietäisi
mitä luokkia importata.
**Riippuvuusjärjestys:** Manageria pyydetään listaamaan riippuvuudet ensin
(models.py ennen main.py) jotta kontekstiketju toimii oikeaan suuntaan.
---
## Laadun parantaminen
### 1. Isompi malli (suurin vaikutus)
| | 0.5B | 3B | Pilvi-API |
|---|---|---|---|
| Fibonacci | Joskus virheitä | Yleensä oikein | Aina oikein |
| FastAPI CRUD | Voi käyttää Flaskia | Oikea kirjasto | Täydellinen |
| Monimutkainen logiikka | Hallusinoi | Osaa perusasiat | Syvä ymmärrys |
| Nopeus (selain) | ~5 tok/s | ~0.4 tok/s | — |
| Latauksen koko | 990 MB | 6.2 GB | 0 (API) |
**Käytännössä:** `kpn load 2` lataa 3B-mallin. Hitaampi mutta huomattavasti
parempi koodinlaatu. Suositus monimutkaisiin projekteihin.
### 2. Paremmat promptit (ilmaista)
**Huono:** `"tee fibonacci"`
- Malli ei tiedä kieltä, formaattia tai kontekstia
**Hyvä:** `"Write a fibonacci function in Rust that returns Vec<u64>"`
- Kieli, palautustyyppi ja rakenne määritelty
**Promptin säännöt:**
- Englanniksi (tehokkaampi tokenisointi, parempi ymmärrys)
- Konkreettinen (mainitse kieli, kirjastot, palautustyyppi)
- Lyhyt (jokainen sana kuluttaa tokenin konteksti-ikkunasta)
- Positiivinen ("Write X" ei "Don't write Y")
### 3. Kontekstin hallinta (pipeline-taso)
**Ongelma:** 0.5B-malli "unohtaa" promptin alun kun konteksti kasvaa.
**Ratkaisu:** Pienet, kohdennetut promptit:
- Yksi tiedosto kerrallaan (ei "kirjoita koko projekti")
- Vain relevantit aiemmat tiedostot kontekstina
- Max 4 tiedostoa per projekti
### 4. Iterointi (review-luuppi)
Yksi generointikierros tuottaa harvoin virheetöntä koodia.
Pipeline-arkkitehtuuri mahdollistaa:
1. **Generointi** — ensimmäinen versio
2. **Review** — testaaja löytää ongelmat
3. **Korjaus** — koodari saa palautteen ja korjaa
4. **Uusi review** — tarkistetaan korjaukset
Nykyinen järjestelmä tekee max 1 korjauskierroksen. Useampi
iteraatio parantaisi laatua mutta kasvattaisi laskenta-aikaa.
### 5. Erikoistetut system promptit
Oletuspromptit ovat yleiskäyttöisiä. Projektikohtaiset promptit
parantavat laatua merkittävästi:
```
Oletus: "Olet kokenut ohjelmistokehittäjä."
Parempi: "You are a Python backend developer specializing in FastAPI.
Always use Pydantic models for request/response schemas.
Always use dependency injection for database sessions.
Follow the repository pattern."
```
Agenttikohtaiset promptit voi muokata suoraan UI:ssa.
### 6. Few-shot esimerkit
Malli oppii parhaiten esimerkeistä. Sen sijaan, että sanot "kirjoita
FastAPI endpoint", näytä miltä haluat tuloksen näyttävän:
```
Write a GET endpoint like this example:
@app.get("/items")
def list_items():
db = SessionLocal()
return db.query(Item).all()
Now write a similar endpoint for /users.
```
0.5B-malli jäljittelee rakennetta tehokkaasti — se on parempi kopioimaan
kuin keksimään. Nykyinen pyproject.toml-esimerkki promptissa on tätä tekniikkaa.
### 7. Temperature-säätö tehtävän mukaan
Nykyinen temperature 0.7 on kompromissi. Eri tehtävät hyötyisivät eri arvoista:
| Tehtävä | Paras temperature | Miksi |
|---------|-------------------|-------|
| Tarkka koodi (CRUD, boilerplate) | 0.2-0.4 | Determinismi tärkeää |
| Luova koodi (algoritmit, arkkitehtuuri) | 0.6-0.8 | Vaihtelu löytää ratkaisuja |
| Vapaa teksti (kommentit, dokumentaatio) | 0.8-1.0 | Luonnollisempi kieli |
Järjestelmä voisi valita temperaturen automaattisesti tehtävätyypin perusteella.
### 8. Ensemble — sama prompti usealle mallille
Lähetetään sama tehtävä kahdelle solmulle ja valitaan parempi vastaus.
Nykyinen Proof of Compute -arkkitehtuuri tukee tätä periaatteessa:
hub voisi reitittää saman task_id:n kahdelle solmulle ja verrata tuloksia.
Käytännössä tämä kaksinkertaistaa laskenta-ajan mutta parantaa laatua
merkittävästi — virheellinen vastaus harvoin on sama kahdella ajolla
koska sampling on stokastinen.
### 9. Post-processing (nykyinen)
Mallin raakavastaus siivotaan:
1. Kielitunniste poistetaan (`python`, `rust`, ...)
2. Sulkeva ` ``` ` poistetaan
3. Johdantolauseet poistetaan ("Sure!", "Here is...")
4. Selityskommentit poistetaan ("# This is a simple...")
5. Stop-sekvenssit katkaisevat generoinnin
Tämä ei paranna mallin ajattelua mutta poistaa turhan roskan.
### 10. Mallin hienosäätö (fine-tuning)
Qwen2.5-Coder on yleiskäyttöinen koodimalli. Jos sitä hienosäätäisi
omalla koodiaineistolla (esim. yrityksen koodikanta, tietty framework),
se tuottaisi huomattavasti parempaa koodia juuri siihen kontekstiin.
LoRA-hienosäätö 0.5B-mallille vaatii ~4 GB GPU-muistia ja muutaman
tunnin harjoittelua. Tulos on erikoistunut malli joka osaa tuottaa
esimerkiksi juuri FastAPI + SQLAlchemy -koodia luotettavasti.
---
## Välimuistiarkkitehtuuri — miksi toinen lataus on nopea
```
Ensimmäinen lataus (hidas):
Verkko (HuggingFace CDN) → IndexedDB → RAM → Mallin rakennus
~990 MB lataus, ~30-60s
Toinen lataus samalla sivulatauksella (nopea):
RAM-cache → Mallia ei rakenneta uusiksi, vain KV-cache nollataan
~0ms
Refresh jälkeen (keskitaso):
IndexedDB → RAM → Mallin rakennus
~0 MB lataus, ~2-5s rakennus
Uusi selain/laite (hidas):
Verkko → IndexedDB → RAM → Mallin rakennus
Kuten ensimmäinen lataus
```
**KV-cache:** Mallin sisäinen muisti joka tallentaa aiempien tokenien
laskenta tulokset. Nollataan (`clear_kv_cache()`) jokaisen promptin
välillä jotta edellinen vastaus ei vuoda seuraavaan.
---
## Lukuja käytännöstä
**Yksittäinen funktio** (esim. fibonacci):
- Input: ~80 tokenia
- Output: ~50-100 tokenia
- Aika: ~10-20s (0.5B, selain)
- Laatu: Yleensä toimiva, joskus loogisia virheitä
**3 tiedoston projekti** (esim. FastAPI CRUD):
- Manageri: ~30 tok out
- Koodari (3x): ~100-150 tok out per tiedosto
- Testeri: ~50 tok out
- Korjaukset: ~100 tok out (jos tarpeen)
- **Yhteensä: ~500-700 tokenia, ~3-5 min**
- Laatu: Rakenne oikein, yksittäisiä bugeja
**Token-kustannus vs. pilvipalvelu:**
- Tässä järjestelmässä: 0 euroa (laskenta omalla koneella)
- GPT-4 API: ~700 tokenia x $0.03/1K = ~$0.02 per projekti
- Claude API: ~700 tokenia x $0.015/1K = ~$0.01 per projekti
Selaimessa ajettava malli on ilmainen mutta huomattavasti hitaampi
ja heikompilaatuinen kuin pilvi-API. Sopii oppimiseen, prototypointiin
ja tilanteisiin joissa data ei saa lähteä omalta koneelta.

View File

@@ -6,6 +6,7 @@
<title>Kipinä Agentic Playground</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
<style>
:root {
--bg-color: #0d1117;
@@ -143,6 +144,13 @@
}
.code-output .hljs { background: transparent; padding: 0; }
#guide-content { scrollbar-color: #30363d transparent; }
#guide-content h1 { color: #e6edf3; }
#guide-content h2 { color: #e6edf3; }
#guide-content a { color: #58a6ff; }
#guide-content table { border: 1px solid #30363d; border-radius: 6px; overflow: hidden; }
#guide-content pre { scrollbar-color: #30363d transparent; }
.code-task-card {
background: #0d1117;
border: 1px solid var(--border-color);
@@ -701,6 +709,7 @@
<div class="main-tab active" onclick="switchMainTab('network')" data-i18n="tab_network">Laskentaverkko</div>
<div class="main-tab" onclick="switchMainTab('codelab')" data-i18n="tab_codelab">Koodilaboratorio</div>
<div class="main-tab" onclick="switchMainTab('agents')" data-i18n="tab_agents">Kipinä Agentic Playground</div>
<div class="main-tab" onclick="switchMainTab('guide')" data-i18n="tab_guide">Opas</div>
</div>
<!-- PANEELI 1: Laskentaverkko -->
@@ -1103,6 +1112,7 @@
<button id="agent-compute-btn" style="margin-left:4px;padding:2px 10px;border-radius:4px;border:1px solid #30363d;background:#161b22;color:#58a6ff;font-size:12px;font-family:inherit;cursor:pointer" title="Käynnistä kielimalli omalla koneellasi laskentaa varten">Alusta laskentasolmu</button>
</span>
</div>
<div id="pipeline-steps" style="display:none;background:#0d1117;border:1px solid var(--border-color);border-top:none;padding:8px 14px;font-family:'Courier New',monospace;font-size:12px;overflow-x:auto;white-space:nowrap"></div>
<div class="terminal-panel" id="agent-terminal" style="margin-top:0;border-top:none;border-radius:0">
</div>
<div style="position:relative;display:flex;align-items:center;background:#010409;border:1px solid var(--border-color);border-top:none;border-radius:0 0 6px 6px;padding:8px 12px;font-family:'Courier New',monospace;font-size:14px">
@@ -1114,6 +1124,13 @@
</div>
</div><!-- /panel-agents -->
<!-- PANEELI 4: Opas -->
<div id="panel-guide" class="main-panel">
<div id="guide-content" style="max-width:800px;margin:0 auto;padding:20px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;color:var(--text-color);line-height:1.7;font-size:15px">
<p style="color:#8b949e">Ladataan opasta...</p>
</div>
</div>
</div>
<script type="module">
@@ -1139,8 +1156,8 @@
manager: { name: 'Manageri — System Prompt', model: 'qwen-coder', default: 'Olet projektipäällikkö. Jaa tehtävät osiin, priorisoi ja koordinoi tiimin työtä.' },
coder: { name: 'Koodari — System Prompt', model: 'qwen-coder', default: 'Olet kokenut ohjelmistokehittäjä. Kirjoita selkeää, testattavaa koodia ja vastaa aina koodilla.' },
data: { name: 'Data-Agentti — System Prompt', model: 'qwen-coder', default: 'Olet tietokanta-asiantuntija. Vastaat skeemojen suunnittelusta, SQL-kyselyiden optimoinnista ja datamalleista.' },
qa: { name: 'QA — System Prompt', model: 'smollm-135m', default: 'Olet laadunvarmistaja (QA). Kirjoitat testejä, etsit virheitä ja varmistat, että kaikki reunatapaukset on huomioitu.' },
tester: { name: 'DevOps — System Prompt', model: 'smollm-135m', default: 'Olet DevOps-insinööri. Vastaat koodin julkaisuputkista, serveri-infrastruktuurista ja ympäristön suorituskyvystä.' },
qa: { name: 'QA — System Prompt', model: 'qwen-coder', default: 'Olet laadunvarmistaja (QA). Kirjoitat testejä, etsit virheitä ja varmistat, että kaikki reunatapaukset on huomioitu.' },
tester: { name: 'DevOps — System Prompt', model: 'qwen-coder', default: 'Olet DevOps-insinööri. Vastaat koodin julkaisuputkista, serveri-infrastruktuurista ja ympäristön suorituskyvystä.' },
};
const selectedAgents = new Set();
let sharedPrompt = localStorage.getItem('kpn-shared-prompt') || '';
@@ -1468,7 +1485,7 @@
// URL-hash navigointi
const initHash = window.location.hash.replace('#', '');
if (['codelab', 'agents'].includes(initHash)) {
if (['codelab', 'agents', 'guide'].includes(initHash)) {
switchMainTab(initHash);
}
@@ -1491,12 +1508,19 @@
let detectedWebGPU = false;
let detectedGpuInfo = null;
let wasmInitialized = false;
let coderSize = '05b'; // '05b' tai '3b'
let coderSize = localStorage.getItem('kpn-coder-size') || '05b';
// Mallivalinnan radio-napit
// Mallivalinnan radio-napit — asetetaan oikea valinta localStoragesta
const savedRadio = document.querySelector(`input[name="coder-size"][value="${coderSize}"]`);
if (savedRadio) savedRadio.checked = true;
if (coderSize === '3b') {
document.getElementById('coder-opt-05b')?.style && (document.getElementById('coder-opt-05b').style.borderColor = 'var(--border-color)');
document.getElementById('coder-opt-3b')?.style && (document.getElementById('coder-opt-3b').style.borderColor = 'var(--accent-color)');
}
document.querySelectorAll('input[name="coder-size"]').forEach(radio => {
radio.addEventListener('change', (e) => {
coderSize = e.target.value;
localStorage.setItem('kpn-coder-size', coderSize);
// Visuaalinen korostus
document.getElementById('coder-opt-05b').style.borderColor = coderSize === '05b' ? 'var(--accent-color)' : 'var(--border-color)';
document.getElementById('coder-opt-3b').style.borderColor = coderSize === '3b' ? 'var(--accent-color)' : 'var(--border-color)';
@@ -1744,8 +1768,17 @@
// Lähettää promptin mallille ja palauttaa vastauksen (tai null virhetilanteessa)
async function kpnRun(model, prompt, silent) {
termLog(` → <span style="color:#58a6ff">${model}</span> käsittelee...`, '#8b949e');
const taskId = crypto.randomUUID();
// Yksittäinen status-rivi jota päivitetään läpi pyynnön elinkaaren
const statusDiv = document.createElement('div');
statusDiv.className = 'terminal-line';
statusDiv.id = 'status-' + taskId;
statusDiv.innerHTML = ` <span style="color:#8b949e">→ <span style="color:#58a6ff">${model}</span> käsittelee... <span style="color:#d29922">(selain voi hidastua)</span></span>`;
termPanel.appendChild(statusDiv);
termPanel.scrollTop = termPanel.scrollHeight;
// Yield jotta status-rivi ehditään piirtää ennen mahdollista blokkia
await new Promise(r => setTimeout(r, 50));
try {
const agent = Object.values(agentPrompts).find(a => a.model === model);
const parts = [];
@@ -1773,20 +1806,45 @@
if (!res.ok) {
const errText = await res.text().catch(() => res.statusText);
termLog(` ${errText}`, '#f85149');
statusDiv.innerHTML = ` <span style="color:#f85149">${esc(errText)}</span>`;
return null;
}
const data = await res.json();
const response = (data.response || '').trim();
const tokGen = data.tokens_generated || 0;
termLog(` <span style="color:#3fb950">✓</span> <span style="color:#58a6ff">${data.model || model}</span> <span style="color:#8b949e">(${tokGen} tok)</span>`);
const durS = data.duration_ms ? (data.duration_ms / 1000).toFixed(1) + 's' : '';
const tokS = data.tokens_per_sec ? data.tokens_per_sec.toFixed(1) + ' tok/s' : '';
const inspectId = 'inspect-' + taskId;
// Prompt-inspektor: tallennetaan promptin osat
const systemPrompt = 'You are a coding assistant. Respond with ONLY code. No explanations, no markdown, no comments unless asked.';
const agentPromptText = agent?.prompt || '';
const inputTokensEst = Math.round(fullPrompt.length / 3.5);
statusDiv.innerHTML = ` <span style="color:#3fb950">✓</span> <span style="color:#58a6ff">${esc(data.model || model)}</span> <span style="color:#8b949e">${tokGen} tok · ${durS} · ${tokS}</span>`
+ ` <span style="color:#30363d;cursor:pointer;font-size:11px" onclick="document.getElementById('${inspectId}').style.display=document.getElementById('${inspectId}').style.display==='none'?'block':'none'" title="Prompt Inspector">[&gt;]</span>`
+ `<div id="${inspectId}" style="display:none;margin:6px 0 4px 16px;padding:8px 12px;background:#0d1117;border:1px solid #30363d;border-radius:4px;font-size:12px;line-height:1.6">`
+ `<div style="color:#8b949e;margin-bottom:6px">Prompt Inspector · <span style="color:#58a6ff">~${inputTokensEst} tok in</span> → <span style="color:#3fb950">${tokGen} tok out</span></div>`
+ `<div style="margin-bottom:4px"><span style="color:#f85149">system:</span> <span style="color:#8b949e">${esc(systemPrompt)}</span></div>`
+ (sharedPrompt ? `<div style="margin-bottom:4px"><span style="color:#d2a8ff">shared:</span> <span style="color:#8b949e">${esc(sharedPrompt).substring(0, 150)}${sharedPrompt.length > 150 ? '...' : ''}</span></div>` : '')
+ (agentPromptText ? `<div style="margin-bottom:4px"><span style="color:#d29922">agent:</span> <span style="color:#8b949e">${esc(agentPromptText)}</span></div>` : '')
+ `<div style="margin-bottom:4px"><span style="color:#3fb950">user:</span> <pre style="margin:2px 0 0 0;padding:6px;background:#161b22;border-radius:3px;white-space:pre-wrap;color:#c9d1d9;font:inherit;max-height:150px;overflow-y:auto">${esc(prompt)}</pre></div>`
+ `<div><span style="color:#58a6ff">prefill:</span> <span style="color:#8b949e">\`\`\`</span></div>`
+ `</div>`;
if (!silent) {
const highlighted = highlightCode(response).replace(/\n/g, '\n ');
termLog(` <pre style="margin:0;font:inherit;white-space:pre-wrap">${highlighted}</pre>`);
// Kompakti yksirivinen esikatselu — klikkaa/hover laajentaa
const firstLine = response.split('\n').find(l => l.trim()) || response;
const lineCount = response.split('\n').filter(l => l.trim()).length;
const preview = esc(firstLine.trim());
const fullHighlighted = highlightCode(response).replace(/\n/g, '\n ');
const uid = 'code-' + Date.now();
termLog(` <span style="color:#3fb950;cursor:pointer" onclick="document.getElementById('${uid}').style.display=document.getElementById('${uid}').style.display==='none'?'block':'none'" title="Klikkaa nähdäksesi koko koodi">`
+ `<span style="color:#8b949e">▶</span> ${preview} <span style="color:#8b949e">${lineCount > 1 ? `(+${lineCount - 1} riviä)` : ''}</span></span>`
+ `<pre id="${uid}" style="display:none;margin:4px 0 0 16px;font:inherit;white-space:pre-wrap;border-left:2px solid #30363d;padding-left:10px">${fullHighlighted}</pre>`);
}
return response;
} catch (e) {
termLog(` ${e.message}`, '#f85149');
statusDiv.innerHTML = ` <span style="color:#f85149">${esc(e.message)}</span>`;
return null;
} finally {
if (activeStreams[taskId]) {
@@ -1796,27 +1854,345 @@
}
}
// Pipeline: manageri → koodari → testaaja
// Pipeline-vaiheiden seuranta ja visualisointi
const pipelineSteps = [];
function pipelineStep(agent, label, status, input, output) {
const step = { agent, label, status, input: input || '', output: output || '' };
// Päivitetään olemassaoleva tai lisätään uusi
const existing = pipelineSteps.find(s => s.label === label && s.status !== 'done');
if (existing && status !== 'done') {
Object.assign(existing, step);
} else if (status === 'done' && existing) {
existing.status = 'done';
existing.output = output || existing.output;
} else {
pipelineSteps.push(step);
}
renderPipelineSteps();
// Päivitetään agentin avatar tooltip
const avatarMap = { manager: 'avatar-kpn', coder: 'avatar-coder', tester: 'avatar-tester', qa: 'avatar-qa', data: 'avatar-data' };
const avatarId = avatarMap[agent];
if (avatarId) {
const el = document.getElementById(avatarId);
if (el) {
const truncOut = (output || '').substring(0, 200).replace(/\n/g, ' ');
el.title = `${label}\n${status === 'active' ? '⏳ Käsittelee...' : '✓ Valmis'}\n\nInput: ${(input || '').substring(0, 100)}...\nOutput: ${truncOut}...`;
}
}
}
function renderPipelineSteps() {
const container = document.getElementById('pipeline-steps');
if (!container) return;
if (pipelineSteps.length === 0) { container.style.display = 'none'; return; }
container.style.display = 'block';
container.innerHTML = pipelineSteps.map((s, i) => {
const colors = { manager: '#d29922', coder: '#3fb950', tester: '#58a6ff', qa: '#a371f7', data: '#d2a8ff' };
const color = colors[s.agent] || '#8b949e';
const icon = s.status === 'done' ? '✓' : s.status === 'active' ? '◷' : '◯';
const iconColor = s.status === 'done' ? '#3fb950' : s.status === 'active' ? '#d29922' : '#8b949e';
const arrow = i < pipelineSteps.length - 1 ? ' <span style="color:#30363d">→</span> ' : '';
// Tooltip: input/output esikatselu
const tip = esc(`${s.label}\nInput: ${(s.input || '').substring(0, 150)}\nOutput: ${(s.output || '').substring(0, 150)}`).replace(/\n/g, '&#10;');
return `<span title="${tip}" style="cursor:help"><span style="color:${iconColor}">${icon}</span> <span style="color:${color}">${esc(s.label)}</span></span>${arrow}`;
}).join('');
}
function pipelineClear() {
pipelineSteps.length = 0;
const container = document.getElementById('pipeline-steps');
if (container) container.style.display = 'none';
}
// Projektikortti: tiedostovälilehdet + kopioi + lataa ZIP
function renderProjectCard(files, projectName) {
const fileEntries = Object.entries(files);
if (fileEntries.length === 0) return;
const cardId = 'proj-' + Date.now();
const tabsHtml = fileEntries.map(([name], i) =>
`<span class="proj-tab" data-card="${cardId}" data-idx="${i}" style="padding:4px 10px;cursor:pointer;border-radius:4px 4px 0 0;font-size:12px;${i === 0 ? 'background:#161b22;color:#58a6ff;border:1px solid #30363d;border-bottom:none' : 'color:#8b949e'}" onclick="switchProjectTab('${cardId}',${i})">${esc(name)}</span>`
).join('');
const panelsHtml = fileEntries.map(([name, code], i) =>
`<div class="proj-panel" data-card="${cardId}" data-idx="${i}" style="${i > 0 ? 'display:none' : ''}">
<div style="display:flex;justify-content:flex-end;padding:4px 8px;background:#0d1117;border-bottom:1px solid #21262d">
<button onclick="copyFileContent('${cardId}',${i})" style="background:none;border:1px solid #30363d;color:#8b949e;font-size:11px;padding:2px 8px;border-radius:3px;cursor:pointer" title="Kopioi ${esc(name)} leikepöydälle">Kopioi</button>
</div>
<pre style="margin:0;padding:10px;font-size:12px;line-height:1.5;overflow-x:auto;white-space:pre-wrap">${highlightCode(code)}</pre>
</div>`
).join('');
const allText = fileEntries.map(([name, code]) => `# --- ${name} ---\n${code}`).join('\n\n');
const cardHtml = `
<div id="${cardId}" style="margin:8px 0;border:1px solid #30363d;border-radius:6px;background:#161b22;overflow:hidden" data-files='${esc(JSON.stringify(files))}'>
<div style="display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background:#0d1117;border-bottom:1px solid #30363d">
<span style="color:#a371f7;font-weight:600;font-size:13px">${esc(projectName || 'Projekti')} <span style="color:#8b949e;font-weight:normal">(${fileEntries.length} tiedostoa)</span></span>
<span style="display:flex;gap:6px">
<button onclick="copyAllFiles('${cardId}')" style="background:none;border:1px solid #30363d;color:#8b949e;font-size:11px;padding:2px 8px;border-radius:3px;cursor:pointer" title="Kopioi kaikki tiedostot leikepöydälle">Kopioi kaikki</button>
<button onclick="downloadZip('${cardId}')" style="background:none;border:1px solid #30363d;color:#58a6ff;font-size:11px;padding:2px 8px;border-radius:3px;cursor:pointer" title="Lataa projekti ZIP-tiedostona">Lataa ZIP</button>
</span>
</div>
<div style="display:flex;gap:2px;padding:6px 8px 0;background:#0d1117">${tabsHtml}</div>
<div style="background:#161b22">${panelsHtml}</div>
</div>`;
const div = document.createElement('div');
div.innerHTML = cardHtml;
termPanel.appendChild(div.firstElementChild);
termPanel.scrollTop = termPanel.scrollHeight;
}
// Globaalit funktiot projektikortin interaktioille
window.switchProjectTab = function(cardId, idx) {
document.querySelectorAll(`.proj-tab[data-card="${cardId}"]`).forEach((tab, i) => {
tab.style.background = i === idx ? '#161b22' : 'transparent';
tab.style.color = i === idx ? '#58a6ff' : '#8b949e';
tab.style.border = i === idx ? '1px solid #30363d' : 'none';
tab.style.borderBottom = i === idx ? 'none' : '';
});
document.querySelectorAll(`.proj-panel[data-card="${cardId}"]`).forEach((panel, i) => {
panel.style.display = i === idx ? '' : 'none';
});
};
window.copyFileContent = function(cardId, idx) {
const card = document.getElementById(cardId);
if (!card) return;
const files = JSON.parse(card.dataset.files);
const entries = Object.entries(files);
if (entries[idx]) {
navigator.clipboard.writeText(entries[idx][1]);
// Visuaalinen palaute
const btn = card.querySelectorAll(`.proj-panel[data-idx="${idx}"] button`)[0];
if (btn) { const orig = btn.textContent; btn.textContent = '✓ Kopioitu'; setTimeout(() => btn.textContent = orig, 1500); }
}
};
window.copyAllFiles = function(cardId) {
const card = document.getElementById(cardId);
if (!card) return;
const files = JSON.parse(card.dataset.files);
const text = Object.entries(files).map(([name, code]) => `# --- ${name} ---\n${code}`).join('\n\n');
navigator.clipboard.writeText(text);
const btn = card.querySelector('[onclick*="copyAllFiles"]');
if (btn) { const orig = btn.textContent; btn.textContent = '✓ Kopioitu'; setTimeout(() => btn.textContent = orig, 1500); }
};
window.downloadZip = async function(cardId) {
const card = document.getElementById(cardId);
if (!card) return;
const files = JSON.parse(card.dataset.files);
// Luodaan ZIP ilman ulkoisia kirjastoja (yksinkertainen uncompressed ZIP)
const entries = Object.entries(files);
const parts = [];
const centralDir = [];
let offset = 0;
for (const [name, content] of entries) {
const nameBytes = new TextEncoder().encode(name);
const contentBytes = new TextEncoder().encode(content);
// Local file header
const header = new Uint8Array(30 + nameBytes.length);
const view = new DataView(header.buffer);
view.setUint32(0, 0x04034b50, true); // Signature
view.setUint16(4, 20, true); // Version needed
view.setUint16(8, 0, true); // Method: store
view.setUint32(18, contentBytes.length, true); // Compressed size
view.setUint32(22, contentBytes.length, true); // Uncompressed size
view.setUint16(26, nameBytes.length, true);
header.set(nameBytes, 30);
// Central directory entry
const cdEntry = new Uint8Array(46 + nameBytes.length);
const cdView = new DataView(cdEntry.buffer);
cdView.setUint32(0, 0x02014b50, true);
cdView.setUint16(4, 20, true);
cdView.setUint16(6, 20, true);
cdView.setUint32(20, contentBytes.length, true);
cdView.setUint32(24, contentBytes.length, true);
cdView.setUint16(28, nameBytes.length, true);
cdView.setUint32(42, offset, true);
cdEntry.set(nameBytes, 46);
parts.push(header, contentBytes);
centralDir.push(cdEntry);
offset += header.length + contentBytes.length;
}
const cdOffset = offset;
let cdSize = 0;
for (const cd of centralDir) { parts.push(cd); cdSize += cd.length; }
// End of central directory
const eocd = new Uint8Array(22);
const eocdView = new DataView(eocd.buffer);
eocdView.setUint32(0, 0x06054b50, true);
eocdView.setUint16(8, entries.length, true);
eocdView.setUint16(10, entries.length, true);
eocdView.setUint32(12, cdSize, true);
eocdView.setUint32(16, cdOffset, true);
parts.push(eocd);
const blob = new Blob(parts, { type: 'application/zip' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'project.zip';
a.click();
URL.revokeObjectURL(url);
};
// Pipeline: manageri → koodari (per tiedosto) → testaaja → korjausluuppi
async function kpnPipeline(task) {
pipelineClear();
termLog(`<span style="color:#a371f7;font-weight:bold">━━━ Pipeline käynnistyy ━━━</span>`);
// Vaihe 1: Manageri analysoi
termLog(`\n<span style="color:#d29922;font-weight:bold">[1/3] Manageri</span> — tehtävän analyysi`);
const managerPrompt = `Analysoi seuraava ohjelmistokehitystehtävä ja kirjoita koodarille selkeä tekninen ohje mitä koodata. Vastaa lyhyesti.\n\nTehtävä: ${task}`;
// Vaihe 1: Manageri pilkkoo projektin tiedostoiksi
termLog(`\n<span style="color:#d29922;font-weight:bold">[1] Manageri</span> — projektin suunnittelu`);
pipelineStep('manager', 'Suunnittelu', 'active', task);
const managerPrompt = `List the source files needed for this project. One file per line, format:
filename.py: what this file contains
Rules:
- Max 4 files
- Only .py, .toml, .json, .html files
- No directories, no paths, just filenames
- List dependencies first, then main app (e.g. models.py before main.py)
- Use pyproject.toml for dependencies (not requirements.txt)
Project: ${task}`;
const plan = await kpnRun(agentPrompts.manager.model, managerPrompt);
if (!plan) { termLog(' ✗ Pipeline keskeytyi (manageri)', '#f85149'); return; }
pipelineStep('manager', 'Suunnittelu', 'done', task, plan);
// Vaihe 2: Koodari toteuttaa
termLog(`\n<span style="color:#3fb950;font-weight:bold">[2/3] Koodari</span> — 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; }
// Parsitaan tiedostolista: "filename.py: description" TAI pelkkä "filename.py"
const fileList = plan.split('\n')
.map(line => line.trim().replace(/^[\d\.\-\*\s]+/, '').replace(/\*+/g, '').replace(/`/g, ''))
.map(line => {
if (line.includes(':')) {
const [name, ...desc] = line.split(':');
return { name: name.trim(), desc: desc.join(':').trim() };
}
// Pelkkä tiedostonimi ilman kuvausta
return { name: line.trim(), desc: '' };
})
.filter(f => {
const n = f.name;
return n.length > 0 && n.length < 40 && !n.includes('/') && !n.includes(' ')
&& /\.\w{1,5}$/.test(n);
});
// Vaihe 3: Testaaja arvioi
termLog(`\n<span style="color:#58a6ff;font-weight:bold">[3/3] Testaaja</span> — 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);
if (fileList.length === 0) {
// Fallback: manageri ei tuottanut tiedostolistaa, käytetään koko vastausta ohjeena
termLog(' <span style="color:#8b949e">Ei tiedostojakoa — generoidaan yhtenä kokonaisuutena</span>');
termLog(`\n<span style="color:#3fb950;font-weight:bold">[2] Koodari</span> — toteutus`);
const code = await kpnRun(agentPrompts.coder.model, `Project: ${task}\nFiles: ${plan}\n\nWrite all the code for this project. Use the exact libraries mentioned in the project description. Use pyproject.toml for dependencies (not requirements.txt).`);
if (code) {
termLog(`\n<span style="color:#a371f7;font-weight:bold">━━━ Pipeline valmis ━━━</span>`);
}
return;
}
termLog(` <span style="color:#8b949e">${fileList.length} tiedostoa: ${fileList.map(f => f.name).join(', ')}</span>`);
// Vaihe 2: Koodari generoi tiedosto kerrallaan, konteksti ketjutetaan
const generatedFiles = {};
for (let i = 0; i < fileList.length; i++) {
const file = fileList[i];
termLog(`\n<span style="color:#3fb950;font-weight:bold">[${i + 2}] Koodari</span> — ${esc(file.name)}`);
pipelineStep('coder', file.name, 'active', file.desc);
// Rakennetaan konteksti: aiemmin generoidut tiedostot
let context = '';
const prevFiles = Object.entries(generatedFiles);
if (prevFiles.length > 0) {
context = 'Already written files:\n' + prevFiles.map(([name, code]) =>
`--- ${name} ---\n${code}`
).join('\n\n') + '\n\n';
}
// Erityisohjeet pyproject.toml / requirements.txt -tiedostoille
let extraInstructions = '';
if (file.name === 'pyproject.toml') {
extraInstructions = `\nUse this exact format:
[project]
name = "projectname"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = ["fastapi", "uvicorn"]
[project.scripts]
start = "uvicorn main:app --reload"`;
} else if (file.name === 'requirements.txt') {
extraInstructions = '\nList one dependency per line. No version pins unless necessary.';
}
const coderPrompt = `${context}Project: ${task}
Write ONLY the file "${file.name}"${file.desc ? ': ' + file.desc : ''}.${extraInstructions}
Use the exact libraries mentioned in the project description. Write correct, working code.`;
const code = await kpnRun(agentPrompts.coder.model, coderPrompt);
if (!code) {
termLog(` ✗ Pipeline keskeytyi (${file.name})`, '#f85149');
return;
}
generatedFiles[file.name] = code;
pipelineStep('coder', file.name, 'done', file.desc, code);
}
// Vaihe 3: Testaaja arvioi koko projektin
const allCode = Object.entries(generatedFiles)
.map(([name, code]) => `--- ${name} ---\n${code}`)
.join('\n\n');
termLog(`\n<span style="color:#58a6ff;font-weight:bold">[${fileList.length + 2}] Testaaja</span> — arviointi`);
pipelineStep('tester', 'Review', 'active', `${Object.keys(generatedFiles).length} tiedostoa`);
const reviewPrompt = `Review this project. List bugs or issues. Be brief.
If the code is correct, say "LGTM".
${allCode}`;
const review = await kpnRun(agentPrompts.tester.model, reviewPrompt);
pipelineStep('tester', 'Review', 'done', `${Object.keys(generatedFiles).length} tiedostoa`, review);
// Vaihe 4: Korjausluuppi — jos testaaja löysi ongelmia
if (review && !review.toLowerCase().includes('lgtm') && !review.toLowerCase().includes('looks good')) {
termLog(`\n<span style="color:#d29922;font-weight:bold">[${fileList.length + 3}] Koodari</span> — korjaukset`);
pipelineStep('coder', 'Korjaukset', 'active', review);
const fixPrompt = `Fix the issues found in the review.
Review feedback: ${review}
Current code:
${allCode}
Write the corrected code.`;
const fixedCode = await kpnRun(agentPrompts.coder.model, fixPrompt);
pipelineStep('coder', 'Korjaukset', 'done', review, fixedCode);
if (fixedCode) {
termLog(`\n<span style="color:#58a6ff;font-weight:bold">[${fileList.length + 4}] Testaaja</span> — uudelleenarviointi`);
pipelineStep('tester', 'Re-review', 'active', fixedCode);
const reReview = await kpnRun(agentPrompts.tester.model, `Review the corrected code briefly:\n${fixedCode}`);
pipelineStep('tester', 'Re-review', 'done', fixedCode, reReview);
}
}
termLog(`\n<span style="color:#a371f7;font-weight:bold">━━━ Pipeline valmis (${Object.keys(generatedFiles).length} tiedostoa) ━━━</span>`);
renderProjectCard(generatedFiles, task);
}
// Yksinkertainen pipeline (vanha: manageri → koodari → testaaja)
async function kpnPipelineSimple(task) {
termLog(`<span style="color:#a371f7;font-weight:bold">━━━ Pipeline käynnistyy ━━━</span>`);
termLog(`\n<span style="color:#d29922;font-weight:bold">[1/3] Manageri</span>`);
const plan = await kpnRun(agentPrompts.manager.model, `Analyse this task briefly and write a technical spec for a coder:\n${task}`);
if (!plan) return;
termLog(`\n<span style="color:#3fb950;font-weight:bold">[2/3] Koodari</span>`);
const code = await kpnRun(agentPrompts.coder.model, `${plan}\n\nWrite the code.`);
if (!code) return;
termLog(`\n<span style="color:#58a6ff;font-weight:bold">[3/3] Testaaja</span>`);
await kpnRun(agentPrompts.tester.model, `Review briefly:\n${code}`);
termLog(`\n<span style="color:#a371f7;font-weight:bold">━━━ Pipeline valmis ━━━</span>`);
}
@@ -1835,7 +2211,8 @@
if (sub === 'help' || !sub) {
termLog(' kpn hello — iloinen tervehdys verkosta', '#a5d6ff');
termLog(' kpn run &lt;malli&gt; "&lt;prompti&gt;" — aja tehtävä verkossa', '#a5d6ff');
termLog(' kpn pipeline "&lt;tehtävä&gt;" — manageri → koodari → testaaja', '#a5d6ff');
termLog(' kpn pipeline "&lt;tehtävä&gt;" — nopea: manageri → koodari → testaaja', '#a5d6ff');
termLog(' kpn project "&lt;kuvaus&gt;" — projekti: tiedostojako + generointi + review', '#a5d6ff');
termLog(' kpn load — lataa kielimalli omalle koneelle', '#a5d6ff');
termLog(' kpn status — verkon tila', '#a5d6ff');
termLog(' kpn models — käytettävissä olevat mallit', '#a5d6ff');
@@ -1849,14 +2226,37 @@
}
if (sub === 'load') {
const arg = parts[2];
const btn = document.getElementById('agent-compute-btn');
if (btn && btn.dataset.state === 'ready') {
termLog(' ✓ Kielimalli on jo ladattu ja valmis', '#3fb950');
} else {
termLog(' Alustetaan laskentasolmua...', '#d29922');
if (btn) btn.click(); // Käytetään samaa logiikkaa kuin napissa
else ensureCoderNode();
// Mallikatalogista valinta numerolla tai nimellä
const loadModels = [
{ id: '1', key: '05b', name: 'Qwen2.5-Coder:0.5B', size: '~990 MB', coderSize: '05b' },
{ id: '2', key: '3b', name: 'Qwen2.5-Coder:1.5B Q4', size: '~1 GB', coderSize: '3b' },
];
if (!arg) {
// Näytetään lista
termLog(' Ladattavat mallit:', '#c9d1d9');
for (const m of loadModels) {
const active = (btn?.dataset.state === 'ready' && coderSize === m.coderSize) ? ' <span style="color:#3fb950">✓ ladattu</span>' : '';
termLog(` <span style="color:#58a6ff">${m.id}</span> ${m.name} <span style="color:#8b949e">(${m.size})</span>${active}`);
}
termLog(' Käyttö: kpn load &lt;numero&gt;', '#8b949e');
return;
}
const selected = loadModels.find(m => m.id === arg || m.key === arg || m.coderSize === arg);
if (!selected) {
termLog(` Tuntematon malli "${esc(arg)}". Kokeile: kpn load`, '#f85149');
return;
}
if (btn?.dataset.state === 'ready' && coderSize === selected.coderSize) {
termLog(`${selected.name} on jo ladattu ja valmis`, '#3fb950');
return;
}
coderSize = selected.coderSize;
localStorage.setItem('kpn-coder-size', coderSize);
termLog(` Alustetaan ${selected.name} (${selected.size})...`, '#d29922');
if (btn) btn.click();
else ensureCoderNode();
return;
}
@@ -1868,22 +2268,38 @@
}
if (sub === 'models') {
termLog(' smollm-135m — SmolLM 135M (kevyt)', '#a5d6ff');
termLog(' qwen-05b Qwen2.5 0.5B', '#a5d6ff');
termLog(' phi3-mini — Phi-3 Mini', '#a5d6ff');
termLog(' qwen-coder — Qwen2.5-Coder 0.5B', '#a5d6ff');
termLog(' qwen-coder-3b — Qwen2.5-Coder 3B', '#a5d6ff');
termLog(' Käytettävissä olevat mallit:', '#c9d1d9');
termLog(' <span style="color:#58a6ff">1</span> qwen-coder Qwen2.5-Coder:0.5B <span style="color:#8b949e">~990 MB | koodin generointi</span>');
termLog(' <span style="color:#58a6ff">2</span> qwen-coder-3b Qwen2.5-Coder:1.5B Q4 <span style="color:#8b949e">~1 GB | kvantisoidtu, parempi laatu</span>');
termLog(' <span style="color:#58a6ff">3</span> smollm-135m SmolLM 135M <span style="color:#8b949e">~270 MB | kevyt, nopea</span>');
termLog(' <span style="color:#58a6ff">4</span> qwen-05b Qwen2.5:0.5B <span style="color:#8b949e">~990 MB | yleismalli</span>');
termLog(' <span style="color:#58a6ff">5</span> phi3-mini Phi-3 Mini <span style="color:#8b949e">~2.2 GB | Microsoftin malli</span>');
termLog(' Käyttö: kpn run &lt;malli&gt; "&lt;prompti&gt;"', '#8b949e');
termLog(' Lataus: kpn load &lt;numero&gt;', '#8b949e');
return;
}
if (sub === 'pipeline') {
const afterPipeline = cmd.replace(/^kpn\s+pipeline\s*/, '');
const pMatch = afterPipeline.match(/^"(.+)"$|^'(.+)'$|^(.+)$/);
const afterCmd = cmd.replace(/^kpn\s+pipeline\s*/, '');
const pMatch = afterCmd.match(/^"(.+)"$|^'(.+)'$|^(.+)$/);
const pTask = (pMatch && (pMatch[1] || pMatch[2] || pMatch[3] || '')).trim();
if (!pTask) {
termLog(' Käyttö: kpn pipeline "&lt;tehtävä&gt;"', '#f85149');
return;
}
kpnPipelineSimple(pTask);
return;
}
if (sub === 'project') {
const afterCmd = cmd.replace(/^kpn\s+project\s*/, '');
const pMatch = afterCmd.match(/^"(.+)"$|^'(.+)'$|^(.+)$/);
const pTask = (pMatch && (pMatch[1] || pMatch[2] || pMatch[3] || '')).trim();
if (!pTask) {
termLog(' Käyttö: kpn project "&lt;projektin kuvaus&gt;"', '#f85149');
termLog(' Esim: kpn project "FastAPI + SQLite REST API for users"', '#8b949e');
return;
}
kpnPipeline(pTask);
return;
}
@@ -1905,7 +2321,9 @@
}
// Jos käyttäjä syötti agentin nimen (esim. "coder"), vaihdetaan se oikeaksi tekoälymalliksi ("qwen-coder")
if (agentPrompts[model]) {
if (model === 'coder-3b') {
model = 'qwen-coder-3b';
} else if (agentPrompts[model]) {
model = agentPrompts[model].model;
}
@@ -1918,15 +2336,18 @@
// Tab-completion: ennustava komennonsyöttö sana kerrallaan
const kpnCommands = {
'kpn': ['help', 'run', 'pipeline', 'load', 'status', 'models', 'hello', 'clear'],
'kpn run': ['coder', 'manager', 'tester', 'qa', 'data', 'observer', 'qwen-coder', 'smollm-135m', 'qwen-05b', 'phi3-mini'],
'kpn': ['help', 'run', 'project', 'pipeline', 'load', 'status', 'models', 'hello', 'clear'],
'kpn run': ['coder', 'coder-3b', 'manager', 'tester', 'qa', 'data', 'observer', 'qwen-coder', 'qwen-coder-3b', 'smollm-135m', 'qwen-05b', 'phi3-mini'],
'kpn load': ['1', '2'],
'kpn pipeline': ['"'],
};
// Esimerkkipromptit malleittain
const kpnExamples = {
'kpn run coder': ['"hello world in python"', '"fibonacci in rust"', '"quicksort in javascript"'],
'kpn run coder-3b': ['"binary search tree in rust"', '"REST API with Flask"', '"async web scraper in python"'],
'kpn run manager': ['"suunnittele REST API"', '"priorisoi tiimin tehtävät"'],
'kpn run tester': ['"testaa login-toiminto"'],
'kpn project': ['"FastAPI + SQLite REST API for users"', '"Flask todo app with database"', '"CLI tool for CSV processing in Python"'],
'kpn pipeline': ['"rakenna todo-sovellus"', '"tee laskin pythonilla"'],
};
@@ -2133,7 +2554,7 @@
uiSocket.onmessage = (event) => {
try {
const raw = event.data;
if (raw.includes('"single_tokenize"') || raw.includes('"download_progress"')) return;
if (raw.includes('"single_tokenize"')) return;
const data = JSON.parse(raw);
if (data.type === "stats") {
@@ -2158,6 +2579,24 @@
} else {
dlBar.style.display = 'none';
}
// Terminaaliin latauksen edistyminen
const term = document.getElementById('agent-terminal');
if (term) {
let dlLine = term.querySelector('.term-download');
if (data.pct >= 100) {
if (dlLine) dlLine.remove();
termLog(` <span style="color:#3fb950">✓</span> ${data.file} ladattu`, '#a5d6ff');
} else {
if (!dlLine) {
dlLine = document.createElement('div');
dlLine.className = 'terminal-line term-download';
term.appendChild(dlLine);
}
const bar = '█'.repeat(Math.floor(data.pct / 5)) + '░'.repeat(20 - Math.floor(data.pct / 5));
dlLine.innerHTML = ` <span style="color:#d29922">${data.file}</span> <span style="color:#8b949e">${bar}</span> <span style="color:#58a6ff">${data.pct}%</span> <span style="color:#8b949e">${data.loaded_mb}/${data.total_mb} MB</span>`;
term.scrollTop = term.scrollHeight;
}
}
} else if (data.type === "single_tokenize_done") {
chatBox.classList.remove('hidden');
const r = data.result || {};
@@ -2418,24 +2857,16 @@
}
}
} else if (data.type === "task_routed") {
const term = document.getElementById('agent-terminal');
const isQueued = data.status === 'queued';
const color = isQueued ? '#d29922' : '#58a6ff';
const color = isQueued ? '#d29922' : '#8b949e';
const icon = isQueued ? '⏳' : '→';
const msg = esc(data.message || '');
// Agents-terminaali
if (term && data.task_id && activeStreams[data.task_id]) {
const div = document.createElement('div');
div.className = 'terminal-line';
div.style.color = color;
div.innerHTML = ` ${icon} ${msg}`;
if (isQueued) div.id = 'routing-' + data.task_id;
// Päivitetään olemassaoleva jonorivi jos löytyy
const existing = document.getElementById('routing-' + data.task_id);
if (existing) { existing.innerHTML = ` ${icon} ${msg}`; existing.style.color = color; }
else term.appendChild(div);
term.scrollTop = term.scrollHeight;
// Päivitetään olemassaoleva status-rivi (kpnRun luo sen)
const statusDiv = document.getElementById('status-' + data.task_id);
if (statusDiv) {
statusDiv.innerHTML = ` <span style="color:${color}">${icon} ${msg}${isQueued ? '' : ' <span style="animation:blink 1s infinite">▌</span>'}</span>`;
termPanel.scrollTop = termPanel.scrollHeight;
}
// Codelab-loading-teksti
@@ -2446,7 +2877,8 @@
: `${msg} — generoidaan...`;
}
} else if (data.type === "llm_prompt") {
if (data.task_id) {
// Reagoidaan VAIN agents-pipelinen tehtäviin (task_id + activeStreams)
if (data.task_id && activeStreams[data.task_id]) {
const term = document.getElementById('agent-terminal');
if (term) {
const model = data.model || 'llm';
@@ -2458,26 +2890,26 @@
while (term.children.length > 50 && !term.firstChild.querySelector('.stream-content')) term.removeChild(term.firstChild);
term.scrollTop = term.scrollHeight;
}
// Avatar-aktivointi vain omille tehtäville
document.querySelectorAll('.avatar-card').forEach(c => c.classList.remove('active'));
const model = data.model || '';
const p = data.prompt ? data.prompt.toLowerCase() : '';
if (p.includes('tiiminvetäjä') || p.includes('pilko')) {
document.getElementById('avatar-kpn')?.classList.add('active');
} else if (p.includes('arvioi seuraava koodi') || p.includes('ohjelmiston julkaisu')) {
document.getElementById('avatar-tester')?.classList.add('active');
} else if (p.includes('tervehdi')) {
document.getElementById('avatar-client')?.classList.add('active');
} else if (p.includes('test')) {
document.getElementById('avatar-qa')?.classList.add('active');
} else if (model.includes('coder') || model.includes('Coder')) {
document.getElementById('avatar-coder')?.classList.add('active');
} else if (model.includes('deepseek') || model.includes('r1')) {
document.getElementById('avatar-observer')?.classList.add('active');
}
}
document.querySelectorAll('.avatar-card').forEach(c => c.classList.remove('active'));
const model = data.model || '';
const p = data.prompt ? data.prompt.toLowerCase() : '';
if (p.includes('tiiminvetäjä') || p.includes('pilko')) {
document.getElementById('avatar-kpn')?.classList.add('active');
} else if (p.includes('arvioi seuraava koodi') || p.includes('ohjelmiston julkaisu')) {
document.getElementById('avatar-tester')?.classList.add('active');
} else if (p.includes('tervehdi')) {
document.getElementById('avatar-client')?.classList.add('active');
} else if (p.includes('test')) {
document.getElementById('avatar-qa')?.classList.add('active');
} else if (model.includes('coder') || model.includes('Coder')) {
// Koodari aktivoituu, jos kyse on suoraan koodarille osoitetusta mallitehtävästä (esim. network task)
document.getElementById('avatar-coder')?.classList.add('active');
} else if (model.includes('deepseek') || model.includes('r1')) {
document.getElementById('avatar-observer')?.classList.add('active');
}
// Emme enää aseta oletusagenttia, jottei tuntemattomissa verkkopyynnöissä mikään turhaan hypi silmille.
}
} catch(e) {}
};
@@ -2692,10 +3124,43 @@
}
// Kuuntele console.log-viestejä pipeline-vaiheiden seuraamiseksi
// Terminaalin lataustilarivi — päivittyy dynaamisesti
function termLoadStatus(phase, detail) {
const term = document.getElementById('agent-terminal');
if (!term) return;
let statusLine = term.querySelector('.term-load-status');
if (!statusLine) {
statusLine = document.createElement('div');
statusLine.className = 'terminal-line term-load-status';
term.appendChild(statusLine);
}
const spinner = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
const frame = spinner[Math.floor(Date.now() / 100) % spinner.length];
statusLine.innerHTML = ` <span style="color:#d29922">${frame}</span> <span style="color:#8b949e">${phase}</span>${detail ? ` <span style="color:#58a6ff">${detail}</span>` : ''}`;
term.scrollTop = term.scrollHeight;
}
function termLoadDone() {
const term = document.getElementById('agent-terminal');
if (!term) return;
const statusLine = term.querySelector('.term-load-status');
if (statusLine) statusLine.remove();
}
const origCodeLog = console.log;
const codeLogListener = (...args) => {
const msg = args.join(' ');
if (msg.includes('[Coder]') || msg.includes('[Storage]') || msg.includes('Burn Wasm') || msg.includes('Kipinä Agent Node')) {
// Terminaalin lataustilapäivitys
if (msg.includes('Agent Node käynnistyy')) termLoadStatus('WASM alustettu');
if (msg.includes('Ladataan') && msg.includes('tokenizer')) termLoadStatus('Ladataan tokenizer...');
if (msg.includes('tokenizer') && (msg.includes('löytyi') || msg.includes('tallennettu'))) termLoadStatus('Tokenizer ✓');
if (msg.includes('Ladataan') && msg.includes('gguf')) termLoadStatus('Ladataan mallia...');
const dlMatch = msg.match(/lataus: (\d+)%/);
if (dlMatch) termLoadStatus('Ladataan mallia...', dlMatch[1] + '%');
if (msg.includes('tallennettu') && msg.includes('gguf')) termLoadStatus('Malli tallennettu');
if (msg.includes('Rakennetaan')) termLoadStatus('Rakennetaan mallia...');
if (msg.includes('Malli ladattu')) termLoadDone();
if (msg.includes('Burn Wasm')) setStep('step-wasm', 'active');
if (msg.includes('Agent Node käynnistyy')) { setStep('step-wasm', 'done'); }
// Tokenizer: [Coder] tai [Storage] -prefiksi
@@ -2744,8 +3209,19 @@
const cl = document.getElementById('agent-compute-label');
const btn = document.getElementById('agent-compute-btn');
if (cd) cd.style.background = '#3fb950';
if (cl) { cl.textContent = 'Qwen2.5-Coder'; cl.style.color = '#3fb950'; }
const sizeLabel = coderSize === '3b' ? '3B (3 miljardia parametria)' : '0.5B (500 miljoonaa parametria)';
if (cl) { cl.textContent = 'Qwen2.5-Coder:' + (coderSize === '3b' ? '3B' : '0.5B'); cl.style.color = '#3fb950'; cl.title = sizeLabel + ' · Candle Wasm · CPU · max 512 tok'; }
if (btn) { btn.dataset.state = 'ready'; btn.textContent = '✓ Valmis'; btn.style.borderColor = '#3fb950'; btn.style.color = '#3fb950'; btn.style.cursor = 'default'; btn.title = 'Kielimalli ladattu — oma kone on valmis laskentaan'; }
localStorage.setItem('kpn-coder-loaded', 'true');
// Terminaaliin valmis-viesti (vain kerran)
if (!window._coderReadyLogged) {
window._coderReadyLogged = true;
const term = document.getElementById('agent-terminal');
if (term) {
const sLabel = coderSize === '3b' ? 'Qwen2.5-Coder:1.5B Q4' : 'Qwen2.5-Coder:0.5B';
termLog(` <span style="color:#3fb950">✓</span> ${sLabel} valmis — kpn run coder "prompti"`, '#3fb950');
}
}
}
if (msg.includes('[Coder]') && msg.includes('Syöte:')) {
// Pipeline piiloon kun generointi alkaa
@@ -2832,7 +3308,7 @@
}
}
// Agents-sivun coder-node käynnistetään "Alusta laskentasolmu" -napista tai kpn load -komennolla
// Mallia EI ladata automaattisesti — käyttäjä käynnistää itse: kpn load
// Laskentasolmun käynnistys/pysäytys -nappi
let computeAbortController = null;
@@ -2914,7 +3390,7 @@
if (!coderJoined) {
pendingCodePrompt = promptText;
const dlSize = coderSize === '3b' ? '~6.2 GB' : '~990 MB';
codeLoading.textContent = `Loading Qwen2.5-Coder-${coderSize === '3b' ? '3B' : '0.5B'} (${dlSize} on first run)...`;
codeLoading.textContent = `Loading Qwen2.5-Coder:${coderSize === '3b' ? '3B' : '0.5B'} (${dlSize} on first run)...`;
await ensureCoderNode();
} else {
codeLoading.textContent = 'Generating code...';
@@ -3062,6 +3538,126 @@
if (window.selectAgent) window.selectAgent('client');
}, 100);
});
// GUIDE.md:n lataus ja renderöinti
(async function loadGuide() {
const container = document.getElementById('guide-content');
if (!container) return;
try {
const res = await fetch('/GUIDE.md');
if (!res.ok) { container.innerHTML = '<p style="color:#f85149">Oppaan lataus epäonnistui.</p>'; return; }
const md = await res.text();
container.innerHTML = renderMarkdown(md);
// Syntaksikorostus koodiblokeille
container.querySelectorAll('pre code').forEach(block => {
if (typeof hljs !== 'undefined') hljs.highlightElement(block);
});
// Mermaid-kaaviot
if (typeof mermaid !== 'undefined') {
mermaid.initialize({ startOnLoad: false, theme: 'dark', themeVariables: { primaryColor: '#58a6ff', primaryTextColor: '#c9d1d9', lineColor: '#30363d', background: '#0d1117' } });
container.querySelectorAll('.mermaid-container').forEach(async el => {
try {
const { svg } = await mermaid.render('m-' + el.id, el.textContent.trim());
el.innerHTML = svg;
} catch(e) { /* fallback: jätetään teksti näkyviin */ }
});
}
} catch(e) {
container.innerHTML = '<p style="color:#f85149">Virhe: ' + e.message + '</p>';
}
})();
function renderMarkdown(md) {
const lines = md.split('\n');
let html = '';
let inCode = false;
let codeLang = '';
let codeBuffer = '';
let inTable = false;
let tableRows = [];
function flushTable() {
if (!inTable) return;
inTable = false;
if (tableRows.length < 2) return;
const headerCells = tableRows[0].split('|').filter(c => c.trim());
const bodyRows = tableRows.slice(2); // Skip header + separator
html += '<div style="overflow-x:auto;margin:16px 0"><table style="width:100%;border-collapse:collapse;font-size:14px">';
html += '<thead><tr>' + headerCells.map(c => `<th style="text-align:left;padding:8px 12px;border-bottom:2px solid #30363d;color:#58a6ff;font-weight:600">${inlineFormat(c.trim())}</th>`).join('') + '</tr></thead>';
html += '<tbody>';
for (const row of bodyRows) {
const cells = row.split('|').filter(c => c.trim());
if (cells.length === 0) continue;
html += '<tr>' + cells.map(c => `<td style="padding:6px 12px;border-bottom:1px solid #21262d">${inlineFormat(c.trim())}</td>`).join('') + '</tr>';
}
html += '</tbody></table></div>';
tableRows = [];
}
function inlineFormat(text) {
return text
.replace(/`([^`]+)`/g, '<code style="background:#161b22;padding:2px 6px;border-radius:3px;font-size:13px;color:#e6edf3">$1</code>')
.replace(/\*\*([^*]+)\*\*/g, '<strong style="color:#e6edf3">$1</strong>')
.replace(/\*([^*]+)\*/g, '<em>$1</em>');
}
for (const line of lines) {
// Koodiblokit + Mermaid-kaaviot
if (line.startsWith('```')) {
if (inCode) {
if (codeLang === 'mermaid') {
const mermaidId = 'mermaid-' + Math.random().toString(36).slice(2, 8);
html += `<div class="mermaid-container" id="${mermaidId}" style="margin:16px 0;text-align:center">${codeBuffer.replace(/</g,'&lt;')}</div>`;
} else {
html += `<pre style="background:#0d1117;border:1px solid #30363d;border-radius:6px;padding:14px;margin:12px 0;overflow-x:auto"><code class="language-${codeLang}">${codeBuffer.replace(/</g,'&lt;')}</code></pre>`;
}
inCode = false;
codeBuffer = '';
} else {
flushTable();
inCode = true;
codeLang = line.slice(3).trim() || 'plaintext';
}
continue;
}
if (inCode) { codeBuffer += (codeBuffer ? '\n' : '') + line; continue; }
// Taulukot
if (line.includes('|') && line.trim().startsWith('|')) {
if (!inTable) inTable = true;
tableRows.push(line);
continue;
} else {
flushTable();
}
// Tyhjä rivi
if (!line.trim()) { html += '<div style="height:8px"></div>'; continue; }
// Otsikot
if (line.startsWith('# ')) { html += `<h1 style="color:#e6edf3;font-size:28px;margin:32px 0 12px;border-bottom:1px solid #30363d;padding-bottom:8px">${inlineFormat(line.slice(2))}</h1>`; continue; }
if (line.startsWith('## ')) { html += `<h2 style="color:#e6edf3;font-size:22px;margin:28px 0 10px;border-bottom:1px solid #21262d;padding-bottom:6px">${inlineFormat(line.slice(3))}</h2>`; continue; }
if (line.startsWith('### ')) { html += `<h3 style="color:#e6edf3;font-size:17px;margin:20px 0 8px">${inlineFormat(line.slice(4))}</h3>`; continue; }
// Horisontaalinen viiva
if (line.match(/^-{3,}$/)) { html += '<hr style="border:none;border-top:1px solid #30363d;margin:20px 0">'; continue; }
// Lista
if (line.match(/^[\-\*] /)) {
html += `<div style="padding:2px 0 2px 20px">${inlineFormat(line.replace(/^[\-\*] /, '• '))}</div>`;
continue;
}
if (line.match(/^\d+\. /)) {
html += `<div style="padding:2px 0 2px 20px">${inlineFormat(line)}</div>`;
continue;
}
// Normaali tekstirivi
html += `<p style="margin:4px 0">${inlineFormat(line)}</p>`;
}
flushTable();
return html;
}
</script>
</body>
</html>