93 Commits

Author SHA1 Message Date
de1cf009fa pyproject.toml: stdlib-moduulit (sqlite3, os, sys) kielletty riippuvuuksista
Malli laittoi sqlite3:n dependencies-listaan → uv sync epäonnistui.
Koodarin, QA:n ja validoinnin prompteihin lisätty selkeä kielto.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:53:10 +03:00
060f36f479 ZIP-lataus: null-tarkistus tiedostoille + virheilmoitus
Lisätty guard puuttuvalle projectFiles-datalle ja null-safe content.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:46:34 +03:00
e2ec0fa43d v0.2.2: responsiivinen UI, Ollama-proxy, mixed content korjaus
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:23:35 +03:00
8752c0f465 Responsiivinen korkeus: terminaali ja UI skaalautuvat viewport-korkeuteen
Terminaali: clamp(200px, 35vh, 500px) — skaalautuu ikkunan mukaan.
<900px korkeus: pienempi otsikko, tiiviimmät avataret, matalampi terminaali.
>1200px korkeus: isompi terminaali ja promptikenttä.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:22:15 +03:00
8c95282654 Tiivistetty layout: terminaali 500→300px, pienemmät marginaalit
Mahtuu 1440px korkeuteen ilman vieritystä.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:20:06 +03:00
a1bc1af646 Hardware API: Ollama-fallback kun wgpu ei tunnista GPU:ta Dockerissa
/api/v1/hardware tarkistaa nyt myös Ollaman tilan fallbackina.
kpn models näyttää ladattujen mallien määrän ja ✓ oikein.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:18:00 +03:00
6b27cbbade kpn load: rehellinen viesti — 'valittu' eikä 'ladattu ja aktiivinen'
Frontend ei tiedä onko malli oikeasti ladattu Ollamaan.
Nyt näytetään 'valittu — natiivisolmu lataa mallin' ja
varoitus ensimmäisen pyynnön hitaudesta.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:04:11 +03:00
4d9c51a86f .gitignore: *.db — ajonaikaiset tietokannat pois versionhallinnasta
nodes.db muuttui jatkuvasti ja esti deployn.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:59:35 +03:00
66d1e8c4b1 Ollama-kutsut hubin kautta: ei mixed content HTTPS-sivulla
Lisätty GET /api/v1/ollama/tags proxy-endpoint hubiin.
Poistettu suorat http://hostname:11434 -kutsut frontendistä.
Hub välittää Ollama-kutsut sisäisessä Docker-verkossa.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:57:48 +03:00
2eeac255f6 Piilotetut paneelit tavoitettavissa hashilla: #network, #laskentaverkko, #codelab
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:52:57 +03:00
6097cfc263 Ylimääräiset rönsyt karsittu. Playground suht ok. 2026-04-07 08:51:47 +03:00
8aed9f97a2 Puhuvat päät ja simulaatio-nappi piilotettu (koodi säilytetty)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:45:39 +03:00
c0ccd76a4c v0.2.1: Ollama-integraatio, pipeline, prompt-editori
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:41:28 +03:00
d2edb38879 Alaotsikko: 'Hajautettu WebGPU Laskentaverkko' → 'AI-ohjelmistokehitystiimi'
Simulaatio-viittaukset poistettu näkyvistä. Käännökset päivitetty.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:40:22 +03:00
2755794554 Agenttien valinta: klikkaus = yksi, Shift+klikkaus = multi-select
Normaali klikkaus valitsee yhden agentin (poistaa muut valinnat).
Shift+klikkaus lisää/poistaa agentin valinnasta yhteistä promptia varten.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:36:52 +03:00
dbb37b3c60 Laskentaverkko ja Koodilaboratorio piilotettu, Agents oletuksena
Simulaatio-välilehdet piilotettu display:none:lla (koodi säilytetty).
Agents & CLI on nyt oletusvälilehti.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:34:57 +03:00
0e7497b627 Oletuspromptit agentille + klikattavat pipeline-vaiheet
- Jokaisella agentilla on nyt oletusprompt joka näkyy heti modalissa
- Muokatut promptit tallentuvat localStorageen
- Pipeline-vaiherivin (✓ Suunnittelu → ✓ models.py → ...) klikkaus
  avaa modalin jossa näkyy kyseisen vaiheen prompti + tulos
- 📋 Näytä prompti -nappi näkyy aina kun agentti on valittu

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:30:56 +03:00
6b756e2e83 Prompt-editori modal: avain-arvo-parit, editoitavat kentät
Klikkaa agenttia → 'Näytä viimeisin prompti' → modal-ikkuna jossa
prompti on pilkottu rakenteellisiin kenttiin (Project, CONSTRAINTS,
EXAMPLE jne.). Editoitavat kentät sinisellä ✏️, lukitut harmaalla 🔒.
'Aja uudelleen' kokoaa promptin kentistä ja ajaa sen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:24:29 +03:00
5a52f5113c QA validointi: listaa jokaisen tarkistuksen tuloksen ✓/✗
Aiemmin QA vastasi vain 'OK'. Nyt prompti vaatii raportin jokaisesta
6 tarkistuksesta (Dockerfile, deps, ports, README, testit, pyproject)
esimerkkivastauksen kanssa.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:18:46 +03:00
7b0660e46e Korjattu illegal break: if(!task_id) break → if(task_id) { ... }
break ei ole sallittu if/else-lohkossa. Kääritty avatar-aktivointi
if(data.task_id) -ehtoon.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:16:02 +03:00
b35600b417 Few-shot esimerkit pipeline-prompteissa: manageri, koodari, QA
Pienet mallit tuottavat huomattavasti parempaa koodia kun promptissa
on konkreettinen esimerkki oikeasta vastauksesta. Lisätty:
- Manageri: esimerkki tiedostolistasta
- Koodari: esimerkki main.py ja models.py -tiedostoista
- QA: esimerkki pytest + TestClient -testeistä

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:12:25 +03:00
7693269e5d Dockerfile generoidaan templatesta, ei LLM:llä — ei enää pip/uv sekaannuksia
Malli sekoitti pip:n ja uv:n syntaksin (pip install --system ei toimi).
Nyt Dockerfile rakennetaan suoraan templatesta generoiduista tiedostoista:
pyproject.toml → uv sync, requirements.txt → uv pip install, tai fallback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:11:00 +03:00
702c9170ad Avatareiden aktivointi vain task_id:llisistä viesteistä
Hubin automaattiset 10s-broadcastit aktivoivat managerin avatarin.
Nyt tarkistetaan data.task_id ennen avatar-päivitystä.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:10:00 +03:00
3feed22055 Agenttien promptit näkyvissä ja editoitavissa + Aja uudelleen -nappi
Klikkaa agenttia → näet viimeisimmän pipeline-promptin tekstikentässä.
Voit editoida promptia ja painaa 'Aja uudelleen' ajamaan sen samalla
mallilla. Pipeline tallentaa nyt koko promptin (ei vain kuvausta).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:03:48 +03:00
75310c989e QA validointivaihe: tarkistaa tiedostojen yhteensopivuuden
Uusi vaihe DevOps-vaiheiden jälkeen: QA tarkistaa että
Dockerfile, docker-compose, README ja testit viittaavat
oikeisiin tiedostoihin ja riippuvuuksiin. Jos ongelmia löytyy,
DevOps korjaa Dockerfilen automaattisesti.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 07:43:13 +03:00
743946a391 Dockerfile-prompti dynaaminen: tarkistaa onko pyproject.toml generoitu
Jos pyproject.toml puuttuu, käytetään uv pip install suoraan.
COPY-rivi listaa vain oikeasti olemassa olevat .py-tiedostot.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 07:41:54 +03:00
0bd5faa684 API rate limit 10→30 pyyntöä/min: pipeline tarvitsee ~12 vaihetta
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 07:35:26 +03:00
e0c8c3586b Mallin vaihto: spinner-indikaattori + pelkkä numero oikotienä
kpn load näyttää spinnerin kun Ollama lataa mallia.
Pelkkä numero (esim. '4') toimii oikotienä 'kpn load 4':lle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 07:33:14 +03:00
3a1c5c723c kpn models: numerot + ladattu-tila yhtenäisessä listassa
Sama lista kuin kpn load, mutta näyttää myös mitkä mallit
on ladattu Ollamaan (✓) ja WASM-tilan. Numerot toimivat
suoraan kpn load -komennolla.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 07:30:39 +03:00
3139d1ac65 kpn models: näyttää Ollamasta ladatut mallit + WASM-tilan
Hakee Ollaman /api/tags-endpointista ladatut mallit kokoneen,
parametreineen ja kvantisointitasoineen. WASM-tila näkyy myös.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 07:29:09 +03:00
49a1629646 TODO.md: turvallisuus, yksityisyys ja väärinkäytön esto
Hajautetun verkon riskit dokumentoitu: tulosten validointi,
promptien salaus, reputaatiojärjestelmä, rate limiting, token-talous.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 07:26:52 +03:00
13008ac693 ny mänöö hyvin 2026-04-07 07:20:57 +03:00
30e81875db Reconnect yhdellä rivillä: ei floodata terminaalia
Sama rivi päivittyy laskurilla: '↻ Yhdistetään uudelleen... (3)'
Rivi poistetaan kun yhteys palautuu.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 07:18:13 +03:00
73bcd3143a WebSocket auto-reconnect: yhteys palautuu 3s kuluttua katkoksesta
connectHub() luo uuden WebSocketin ja asettaa onopen/onclose/onmessage.
onclose käynnistää 3s timerin joka kutsuu connectHub() uudelleen.
Terminaaliin tulee '↻ Yhdistetään uudelleen...' -viesti.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 07:14:52 +03:00
216b95d15c kpn load: laitteiston VRAM/RAM tarkistus, liian isot mallit merkitään
Hub: uusi GET /api/v1/hardware palauttaa natiivisolmun GPU/RAM-tiedot.
Frontend: kpn load hakee laitteistotiedon ja näyttää mallit joihin
laite riittää. Liian isot mallit näkyvät yliviivattuina + varoitus.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 07:08:34 +03:00
34ef19472a kpn load: Ollama-mallin vaihto lennossa (0.5b → 32b)
- Hub: uusi POST /api/v1/model endpoint, broadcastaa change_model
- Native node: kuuntelee change_model, kutsuu Ollaman pull + vaihtaa mallin
- Frontend: kpn load näyttää 5 mallia, numero vaihtaa Ollaman mallin
- Selain-WASM pysyy 0.5B:nä (kpn load 1)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 07:05:57 +03:00
54a5af96c7 Tab-autokorjaus: korjattu ohitettu autocorrect Tab-handlerissa
Tab-painallus meni suoraan dropdown-getCandidatesiin eikä kutsunut
autocorrectiä. Nyt Tab yrittää ensin korjata typon, sitten täydentää.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 07:00:06 +03:00
842153a7ec uv-paketinhallinta: Dockerfile, README ja pyproject.toml käyttävät uv:tä
Dockerfile kopioi uv:n ghcr.io/astral-sh/uv:latest -imagesta.
README ohjeistaa uv sync + uv run. pyproject.toml pysyy ennallaan
(uv-yhteensopiva formaatti).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 06:45:08 +03:00
5c25c7f9c1 DevOps Dockerfile-prompti: pip-only, ei poetryä/condaa
Malli generoi poetry.lock-riippuvaisen Dockerfilen. Nyt prompti
kertoo tarkan riippuvuuksien asennustavan (pyproject.toml/requirements.txt/pip)
ja antaa valmiin CMD-rivin. Yksivaiheinen build riittää Pythonille.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 06:44:03 +03:00
ac698a766e DevOps-agentti: Dockerfile + docker-compose.yml + README pipeline-vaiheina
DevOps generoi nyt kolme tiedostoa:
- Dockerfile (multi-stage build, python:3.12-slim)
- docker-compose.yml (palvelut, volumet, portit)
- README.md (quick start docker compose up)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 06:41:34 +03:00
f1b57a6c53 Tab korjaa kirjoitusvirheet + fuzzy-match alikomennoille
Tab-painallus yrittää ensin autokorjausta (typo-taulukko + Levenshtein),
sitten normaalia tab-completionia. Myös alikomennot korjautuvat
fuzzy-matchilla (esim. "kpn rnu" → "kpn run").

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 06:37:51 +03:00
b70cdbd24d Terminaalin autokorjaus: knp→kpn, kpn rnu→kpn run jne.
Typo-taulukko yleisimmille kirjoitusvirheille + Levenshtein-etäisyys
tuntemattomille ensimmäisille sanoille (max 2 merkin ero → kpn).
Korjaus näytetään terminaalissa keltaisella.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 06:35:30 +03:00
01d8b597e1 ZIP CRC-32 checksum lisätty: purkaminen ei enää epäonnistu
Local file header ja central directory entry -tietueista puuttui
CRC-32 kenttä. Lisätty crc32()-funktio ja kirjoitetaan checksum
molempiin ZIP-rakenteisiin.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 06:31:42 +03:00
f2ca4890df Dockerfile: touch main.rs ennen buildia, estää stub-binaryn jäämisen
Cargo ei rekompiloi jos vanha binääri on olemassa.
touch pakottaa uudelleenkäännöksen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 06:26:33 +03:00
3eb0c4d939 Ollama-integraatio: GPU-inferenssi NVIDIA/AMD/Apple, ei Candle-rajoitteita
- docker-compose: Ollama-container GPU:lla + persistent volume malleille
- native-node: Candle poistettu, kutsuu Ollaman HTTP API:a (async)
- Dockerfile: yksinkertaistettu, ei CUDA SDK:ta (Ollama hoitaa GPU:n)
- Tukee kaikkia malleja: qwen2.5-coder:1.5b/3b/7b/14b/32b
- OLLAMA_MODEL ympäristömuuttujalla vaihdetaan malli
- kpn models näyttää Ollama-mallit nopeustiedoilla

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 06:22:11 +03:00
d8443792a3 kpn load ja kpn models selkeytetty: selain vs natiivi
kpn load lataa 0.5B selaimeen (ainoa joka toimii WASM:ssa).
kpn models näyttää molemmat vaihtoehdot nopeustiedoilla.
Ei enää harhaanjohtavia numerovalintoja.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 06:06:12 +03:00
ae379bdda4 Zippi korjattu 2026-04-07 06:00:49 +03:00
ed02e47158 ZIP-lataus korjattu: tiedostot globaaliin muuttujaan data-attribuutin sijaan
JSON data-attribuutissa heittomerkit katkaisivat HTML:n.
Nyt projectFiles[cardId] tallentaa tiedostot muistiin.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 06:00:17 +03:00
959dc532bb native-laskentaan säätöä 2026-04-07 05:20:54 +03:00
1ef7f7c956 max_tokens per vaihe: manageri 200, koodari 512, testaaja 200, QA 512, DevOps 256
Hub ja natiivisolmu tukevat nyt max_tokens-kenttää API-pyynnöissä.
Pipeline-vaiheet käyttävät sopivan kokoisia token-rajoja.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 22:03:26 +03:00
e6e1f60935 Pipeline: QA kirjoittaa testit + DevOps tekee README:n
Uudet vaiheet koodiarvioinnin jälkeen:
- QA: kirjoittaa test_app.py (pytest, max 3 testiä)
- DevOps: kirjoittaa README.md (asennus, käynnistys, testaus)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 22:00:04 +03:00
322c98ff59 Pipeline-promptit: rajoitteet kerrottu managerille ja koodarille
Manageri tietää nyt 400 tokenin rajan per tiedosto ja pitää
tiedostomäärän max 3:ssa. Koodari kirjoittaa lyhyttä, fokusoidusti.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 21:58:51 +03:00
406e2226f0 Native node max_tokens 64→512: koodi ei jää kesken
64 tokenia riitti vain funktion alkuun. 512 mahdollistaa
kokonaisten tiedostojen generoinnin pipeline-vaiheissa.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 21:57:42 +03:00
9d7496157c Native node CPU-moodi: Candle 0.8 RMS-norm ei tue CUDA:a
candle-core 0.8 ei sisällä rms-norm CUDA-kerneliä → inferenssi epäonnistui.
Vaihdettu CPU:ksi joka on silti ~10-20× nopeampi kuin selaimen WASM.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 21:52:50 +03:00
d332b7e910 Hub priorisoi natiivisolmut (GPU) selainsolmujen edelle
Lisätty node_types HashMap joka seuraa solmutyyppiä (native/browser).
API reitittää tehtävät ensin vapaalle natiivisolmulle, sitten selaimelle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 21:45:20 +03:00
8e55a15d66 bugifiksejä 2026-04-06 21:34:03 +03:00
4e3134d908 CUDA_COMPUTE_CAP=89: bindgen_cuda ei tarvitse nvidia-smi:tä buildissa
candle-kernels build vaatii GPU-arkkitehtuurin tunnistusta.
nvidia-smi ei ole saatavilla Docker build -vaiheessa, joten asetetaan
CUDA_COMPUTE_CAP manuaalisesti (RTX 4090 = sm_89).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 21:26:03 +03:00
cd45db001a Dockerfile.native-node: lisätty cli/ workspace-jäsen
Cargo workspace vaatii kaikkien jäsenten Cargo.toml:n kopioinnin.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 21:23:48 +03:00
4ad8a8793e Native node CUDA Docker: nvidia/cuda base + GPU runtime
Dockerfile käyttää nvidia/cuda:12.6.3 -imagea jossa CUDA-kirjastot
ovat valmiina. docker-compose lisää runtime: nvidia + NVIDIA_VISIBLE_DEVICES.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 21:02:43 +03:00
b2694c232e Poistettu 1.5B Q4 -vaihtoehto: GGUF dequantisointi liian hidas WASM:ssa
1.5B Q4_K_M: ~33s/token (0.03 tok/s) — käyttökelvoton
0.5B F32:    ~2.5s/token (0.4 tok/s)  — käyttökelpoinen

kpn load lataa nyt suoraan 0.5B:n ilman valintalistaa.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 20:19:34 +03:00
ba58236c52 Worker console.log välitetään pääsäikeelle → UI-kuuntelijat toimivat
Workerin WASM-logit (lataus, malli valmis, inferenssi) eivät näkyneet
pääsäikeessä. Nyt console.log on ylikirjoitettu Workerissa lähettämään
viestit postMessage:lla, ja pääsäie syöttää ne omaan console.log:iin.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 20:12:42 +03:00
861f2a6902 Worker ES module: importScripts → import (wasm-pack --target web)
wasm-pack --target web generoi ES module -syntaksia (export).
Worker käyttää nyt type:'module' ja import-lauseita importScripts:n sijaan.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 20:04:53 +03:00
11fd5b0c9e jotain tulee 2026-04-06 20:00:55 +03:00
b3646ae5d3 Web Worker: WASM-inferenssi erillisessä säikeessä, UI ei jäädy
- Poistettu kaikki web_sys::window() -kutsut Rust WASM:sta
- Uudet Worker-yhteensopivat apufunktiot: perf_now(), worker_fetch(), sleep_ms()
- worker.js lataa ja ajaa WASM-moduulin erillisessä säikeessä
- ensureCoderNode käynnistää Workerin pääsäikeen sijaan
- Selaimen UI pysyy responsiivisena inferenssin aikana

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 19:59:09 +03:00
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
19 changed files with 3081 additions and 534 deletions

3
.gitignore vendored
View File

@@ -34,3 +34,6 @@ Cargo.lock
*.pdb
# End of https://www.toptal.com/developers/gitignore/api/rust,linux
# Ajonaikaiset tietokannat
*.db

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"

View File

@@ -1,7 +1,7 @@
FROM rust:slim AS builder
RUN apt-get update && apt-get install -y \
pkg-config libssl-dev g++ \
pkg-config libssl-dev g++ libvulkan-dev \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
@@ -9,22 +9,27 @@ COPY Cargo.toml Cargo.lock ./
COPY hub/Cargo.toml hub/Cargo.toml
COPY node/Cargo.toml node/Cargo.toml
COPY native-node/Cargo.toml native-node/Cargo.toml
COPY cli/Cargo.toml cli/Cargo.toml
# Tyhjät src-tiedostot riippuvuuksien esikääntämistä varten
RUN mkdir -p hub/src node/src native-node/src \
RUN mkdir -p hub/src node/src native-node/src cli/src \
&& echo "fn main(){}" > hub/src/main.rs \
&& echo "" > node/src/lib.rs \
&& echo "fn main(){}" > native-node/src/main.rs \
&& echo "fn main(){}" > cli/src/main.rs \
&& cargo build --release -p native-node 2>/dev/null || true
COPY native-node/src native-node/src
RUN cargo build --release -p native-node
# Touch pakottaa rekompilauksen dummy-binaryn yli
RUN touch native-node/src/main.rs && cargo build --release -p native-node
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
RUN apt-get update && apt-get install -y ca-certificates libvulkan1 && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/native-node /usr/local/bin/native-node
ENV HUB_URL=ws://hub:3000/ws
ENV HUB_URL=ws://agentic-poc:3000/ws
ENV OLLAMA_URL=http://ollama:11434
ENV OLLAMA_MODEL=qwen2.5-coder:7b
ENV ALLOCATED_GB=4
CMD ["native-node"]

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 |

26
network-poc/TODO.md Normal file
View File

@@ -0,0 +1,26 @@
# TODO — Kipinä Agentic Network
## Turvallisuus
- [ ] **Tulosten validointi** — solmu voi palauttaa haitallista koodia. Tarvitaan proof-of-work tai challenge-response -mekanismi
- [ ] **Reputaatiojärjestelmä** — solmujen luotettavuuden seuranta: onnistuneet tehtävät, vasteaika, laatu
- [ ] **Koodin sandboxaus** — generoitu koodi pitää ajaa eristetyssä ympäristössä ennen käyttäjälle näyttämistä
- [ ] **Solmun identiteetti** — rekisteröityminen ja tunnistautuminen (API-avain / token)
## Yksityisyys
- [ ] **Promptien salaus** — käyttäjän promptit menevät tuntemattomalle solmulle selkotekstinä
- [ ] **End-to-end enkryptio** — hub ei näe promptin sisältöä, vain reitittää
- [ ] **Tietosuojaseloste** — käyttäjille kerrottava miten data kulkee ja kuka sen näkee
- [ ] **Opt-in malli** — käyttäjä valitsee haluaako käyttää yhteisösolmuja vai vain omaa
## Väärinkäytön esto
- [ ] **Rate limiting per käyttäjä** — nykyinen IP-pohjainen ei riitä, tarvitaan autentikointi
- [ ] **Solmun kuormitusraja** — solmu voi asettaa max tehtävät/minuutti
- [ ] **Token-talous** — laskentaresurssien käyttö vaatii Kipinä-tokeneita (gamification jo aloitettu)
- [ ] **Abuse reporting** — mekanismi haitallisten solmujen ilmiantamiseen
## Seuraavat ominaisuudet
- [ ] Agenttien välinen keskustelu (manageri ohjaa dynaamisesti)
- [ ] Tehtävähistoria ja tulosten tallennus
- [ ] Prometheus/OpenTelemetry -metriikat
- [ ] Solmujen terveystarkistukset (ping/pong)
- [ ] Streaming-vastaukset Ollaman kautta

View File

@@ -9,20 +9,16 @@ 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:
build:
context: .
dockerfile: Dockerfile.native-node
container_name: kipina_native_node
environment:
- HUB_URL=ws://agentic-poc:3000/ws
- ALLOCATED_GB=4
depends_on:
- agentic-poc
# GPU passthrough (valinnainen — toimii myös ilman)
# Ollama — LLM-inferenssi GPU:lla (NVIDIA/AMD/Apple)
ollama:
image: ollama/ollama:latest
container_name: kipina_ollama
ports:
- "11434:11434"
volumes:
- ollama-models:/root/.ollama
deploy:
resources:
reservations:
@@ -32,3 +28,23 @@ services:
capabilities: [gpu]
profiles:
- native
# Natiivisolmu — yhdistää hubiin ja käyttää Ollamaa inferenssiin
native-node:
build:
context: .
dockerfile: Dockerfile.native-node
container_name: kipina_native_node
environment:
- HUB_URL=ws://agentic-poc:3000/ws
- OLLAMA_URL=http://ollama:11434
- OLLAMA_MODEL=qwen2.5-coder:7b
- ALLOCATED_GB=4
depends_on:
- agentic-poc
- ollama
profiles:
- native
volumes:
ollama-models:

View File

@@ -1,6 +1,6 @@
[package]
name = "hub"
version = "0.2.0"
version = "0.2.2"
edition = "2024"
[dependencies]
@@ -16,3 +16,4 @@ futures = "0.3"
rusqlite = { version = "0.31", features = ["bundled"] }
chrono = "0.4"
base64 = "0.22"
reqwest = { version = "0.12", features = ["json"] }

Binary file not shown.

View File

@@ -39,6 +39,7 @@ struct AppState {
ip_connections: Mutex<HashMap<IpAddr, u32>>,
node_ips: Mutex<HashMap<u64, IpAddr>>,
node_tasks: Mutex<HashMap<u64, String>>, // node_id → selected_task
node_types: Mutex<HashMap<u64, String>>, // node_id → "native" | "browser"
node_busy: Mutex<std::collections::HashSet<u64>>, // Solmut joilla on aktiivinen tehtävä
pending_task_ids: Mutex<std::collections::HashSet<String>>, // Hubin jakamat task_id:t (gamification-validointi)
api_rate_limits: Mutex<HashMap<IpAddr, (std::time::Instant, u32)>>, // IP → (ikkuna-alku, pyyntömäärä)
@@ -260,6 +261,7 @@ async fn main() {
ip_connections: Mutex::new(HashMap::new()),
node_ips: Mutex::new(HashMap::new()),
node_tasks: Mutex::new(HashMap::new()),
node_types: Mutex::new(HashMap::new()),
node_busy: Mutex::new(std::collections::HashSet::new()),
pending_task_ids: Mutex::new(std::collections::HashSet::new()),
api_rate_limits: Mutex::new(HashMap::new()),
@@ -382,6 +384,9 @@ async fn main() {
.route("/api/pairs", get(api_pairs))
.route("/api/stats", get(api_stats))
.route("/api/v1/chat/completions", axum::routing::post(api_chat_completions))
.route("/api/v1/model", axum::routing::post(api_change_model))
.route("/api/v1/hardware", get(api_hardware))
.route("/api/v1/ollama/tags", get(api_ollama_tags))
.route("/admin", get(admin_page))
.nest_service("/", {
let static_dir = std::env::var("STATIC_DIR").unwrap_or_else(|_| "../static".to_string());
@@ -486,15 +491,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();
}
}
@@ -677,6 +682,7 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
state.db.insert_session(node_id, &ip.to_string(), node_type, &json);
}
state.node_tasks.lock().unwrap().insert(node_id, selected_task);
state.node_types.lock().unwrap().insert(node_id, node_type.to_string());
if node_type == "native" {
let sys = json.get("system");
@@ -934,6 +940,7 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
ips.remove(&node_id);
vram.remove(&node_id);
}
state.node_types.lock().unwrap().remove(&node_id);
tracing::info!("Solmu {} ({}) poistui verkosta.", node_id, ip);
broadcast_stats(&state).await;
sender_task.abort();
@@ -943,6 +950,8 @@ struct ChatCompletionRequest {
model: String,
prompt: String,
task_id: String,
#[serde(default)]
max_tokens: Option<u64>,
}
#[derive(serde::Serialize)]
@@ -952,6 +961,78 @@ struct ChatCompletionResponse {
tokens_generated: u64,
}
async fn api_ollama_tags() -> axum::response::Response {
let ollama_url = std::env::var("OLLAMA_URL").unwrap_or_else(|_| "http://ollama:11434".to_string());
match reqwest::get(format!("{}/api/tags", ollama_url)).await {
Ok(resp) => {
if let Ok(body) = resp.json::<serde_json::Value>().await {
axum::Json(body).into_response()
} else {
axum::Json(serde_json::json!({ "models": [] })).into_response()
}
}
Err(_) => axum::Json(serde_json::json!({ "models": [] })).into_response(),
}
}
async fn api_hardware(
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
) -> axum::response::Response {
// Etsitään natiivisolmun GPU-tiedot sessiosta
let sessions = state.db.get_sessions(50);
let native = sessions.iter().find(|s| {
s.get("node_type").and_then(|v| v.as_str()) == Some("native")
});
let (mut vram_mb, mut gpu_name, ram_mb) = if let Some(s) = native {
let gpus = s.get("gpus").and_then(|v| v.as_array());
let gpu = gpus.and_then(|g| g.first());
let vram = gpu.and_then(|g| g.get("vram_total_mb")).and_then(|v| v.as_u64()).unwrap_or(0);
let name = gpu.and_then(|g| g.get("name")).and_then(|v| v.as_str()).unwrap_or("").to_string();
let ram = s.get("system").and_then(|v| v.get("ram_total_mb")).and_then(|v| v.as_u64()).unwrap_or(0);
(vram, name, ram)
} else {
(0, String::new(), 0)
};
// Fallback: kysytään Ollamalta onko malleja ladattu (= Ollama on käynnissä)
if vram_mb == 0 {
let ollama_url = std::env::var("OLLAMA_URL").unwrap_or_else(|_| "http://ollama:11434".to_string());
if let Ok(resp) = reqwest::get(format!("{}/api/tags", ollama_url)).await {
if let Ok(body) = resp.json::<serde_json::Value>().await {
let models = body["models"].as_array().map(|a| a.len()).unwrap_or(0);
if models > 0 {
gpu_name = "Ollama (GPU/CPU)".to_string();
// Natiivisolmun RAM fallbackina
vram_mb = if ram_mb > 0 { ram_mb } else { 0 };
}
}
}
}
if gpu_name.is_empty() { gpu_name = "ei natiivisolmua".to_string(); }
axum::Json(serde_json::json!({
"gpu_name": gpu_name,
"vram_mb": vram_mb,
"ram_mb": ram_mb,
})).into_response()
}
async fn api_change_model(
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
axum::Json(payload): axum::Json<serde_json::Value>,
) -> axum::response::Response {
let model = payload.get("model").and_then(|v| v.as_str()).unwrap_or("");
if model.is_empty() {
return (axum::http::StatusCode::BAD_REQUEST, "model puuttuu").into_response();
}
tracing::info!("Mallin vaihto: {}", model);
let msg = serde_json::json!({ "type": "change_model", "model": model });
let _ = state.stats_tx.send(msg.to_string());
axum::Json(serde_json::json!({ "status": "ok", "model": model })).into_response()
}
async fn api_chat_completions(
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
@@ -966,24 +1047,30 @@ async fn api_chat_completions(
*entry = (now, 1); // Uusi ikkuna
} else {
entry.1 += 1;
if entry.1 > 10 {
if entry.1 > 30 {
return (axum::http::StatusCode::TOO_MANY_REQUESTS, "Liian monta pyyntöä — yritä minuutin kuluttua").into_response();
}
}
}
// Etsitään vapaa tai varattu solmu, joka vastaa pyydettyä mallia
// Etsitään vapaa solmu — priorisoidaan natiivisolmut (GPU) selaimen edelle
let (target_node_free, target_node_any, total_matching) = {
let tasks = state.node_tasks.lock().unwrap();
let busy = state.node_busy.lock().unwrap();
let node_types = state.node_types.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
}
}).map(|(k, _)| *k).collect();
let free = matching.iter().find(|id| !busy.contains(id)).copied();
// Vapaat solmut: natiivi ensin, sitten selain
let free_native = matching.iter().find(|id| {
!busy.contains(id) && node_types.get(id).map(|t| t == "native").unwrap_or(false)
}).copied();
let free_any = matching.iter().find(|id| !busy.contains(id)).copied();
let free = free_native.or(free_any);
let any = matching.first().copied();
(free, any, matching.len())
};
@@ -1059,12 +1146,15 @@ async fn api_chat_completions(
state.node_busy.lock().unwrap().insert(target_node_id);
state.pending_task_ids.lock().unwrap().insert(payload.task_id.clone());
let msg = serde_json::json!({
let mut msg = serde_json::json!({
"type": "llm_prompt",
"prompt": payload.prompt,
"model": payload.model,
"task_id": payload.task_id,
});
if let Some(mt) = payload.max_tokens {
msg.as_object_mut().unwrap().insert("max_tokens".to_string(), serde_json::json!(mt));
}
// Odotuskanava valmiiksi (solmu palauttaa tuloksen stats_tx kautta)
let mut rx = state.stats_tx.subscribe();
@@ -1080,7 +1170,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

@@ -1,6 +1,6 @@
[package]
name = "native-node"
version = "0.1.0"
version = "0.2.2"
edition = "2024"
[dependencies]
@@ -12,10 +12,6 @@ serde_json = "1.0"
sysinfo = "0.30"
nvml-wrapper = "0.10"
wgpu = "24"
candle-core = { version = "0.8", features = ["cuda"] }
candle-nn = "0.8"
candle-transformers = "0.8"
hf-hub = "0.4"
tokenizers = "0.19"
reqwest = { version = "0.12", features = ["json"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

View File

@@ -1,261 +1,114 @@
use candle_core::{Device, Tensor, DType};
use candle_nn::VarBuilder;
use candle_transformers::models::qwen2::{Config as QwenConfig, ModelForCausalLM as QwenModel};
use hf_hub::{api::sync::Api, Repo, RepoType};
use std::time::Instant;
/// Top-k sampling with temperature and repetition penalty
fn sample_top_k(logits: &Tensor, k: usize, temperature: f64, generated_tokens: &[u32], repetition_penalty: f64, rng_state: &mut u64) -> Result<u32, String> {
let mut logits_vec: Vec<f32> = logits.to_vec1::<f32>().map_err(|e| format!("to_vec1: {}", e))?;
if logits_vec.is_empty() { return Err("Tyhjä logits".to_string()); }
// Repetition penalty: rankaisee jo generoituja tokeneita
for &token_id in generated_tokens {
if (token_id as usize) < logits_vec.len() {
let logit = &mut logits_vec[token_id as usize];
if *logit > 0.0 {
*logit /= repetition_penalty as f32;
} else {
*logit *= repetition_penalty as f32;
}
}
}
// Temperature scaling
if temperature > 0.0 && temperature != 1.0 {
for logit in logits_vec.iter_mut() {
*logit /= temperature as f32;
}
}
// Top-k: etsitään k suurinta
let mut indexed: Vec<(usize, f32)> = logits_vec.iter().enumerate().map(|(i, &v)| (i, v)).collect();
indexed.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
indexed.truncate(k);
if k == 1 || temperature == 0.0 {
return Ok(indexed[0].0 as u32);
}
// Softmax top-k:lle
let max_logit = indexed[0].1;
let exps: Vec<f32> = indexed.iter().map(|x| (x.1 - max_logit).exp()).collect();
let sum: f32 = exps.iter().sum();
let probs: Vec<f32> = exps.iter().map(|e| e / sum).collect();
// XorShift64 RNG
*rng_state ^= *rng_state << 13;
*rng_state ^= *rng_state >> 7;
*rng_state ^= *rng_state << 17;
let rand_val = (*rng_state % 10000) as f32 / 10000.0;
let mut cumulative = 0.0;
for (i, p) in probs.iter().enumerate() {
cumulative += p;
if rand_val < cumulative {
return Ok(indexed[i].0 as u32);
}
}
Ok(indexed[0].0 as u32)
}
use std::cell::RefCell;
pub struct LlmEngine {
tokenizer: tokenizers::Tokenizer,
model: QwenModel,
device: Device,
eos_token: u32,
ollama_url: String,
model: RefCell<String>,
client: reqwest::Client,
}
impl LlmEngine {
pub fn load() -> Result<Self, String> {
let device = Device::cuda_if_available(0).map_err(|e| format!("Device: {}", e))?;
let device_name = if device.is_cuda() { "CUDA" } else { "CPU" };
tracing::info!("LLM device: {}", device_name);
let ollama_url = std::env::var("OLLAMA_URL").unwrap_or_else(|_| "http://localhost:11434".to_string());
let model = std::env::var("OLLAMA_MODEL").unwrap_or_else(|_| "qwen2.5-coder:7b".to_string());
let dtype = if device.is_cuda() { DType::F16 } else { DType::F32 };
tracing::info!("Ollama backend: {} | malli: {}", ollama_url, model);
tracing::info!("Ladataan Qwen2.5-Coder-0.5B-Instruct...");
let api = Api::new().map_err(|e| format!("HF API: {}", e))?;
let repo = api.repo(Repo::with_revision(
"Qwen/Qwen2.5-Coder-0.5B-Instruct".to_string(),
RepoType::Model,
"main".to_string(),
));
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(600))
.build()
.map_err(|e| format!("HTTP client: {}", e))?;
let tokenizer_path = repo.get("tokenizer.json").map_err(|e| format!("Tokenizer lataus: {}", e))?;
let model_path = repo.get("model.safetensors").map_err(|e| format!("Malli lataus: {}", e))?;
tracing::info!("Ladataan tokenizer: {:?}", tokenizer_path);
let tokenizer = tokenizers::Tokenizer::from_file(&tokenizer_path)
.map_err(|e| format!("Tokenizer: {}", 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 start = Instant::now();
let vb = unsafe {
VarBuilder::from_mmaped_safetensors(&[model_path.clone()], dtype, &device)
.map_err(|e| format!("VarBuilder: {}", e))?
};
let model = QwenModel::new(&config, vb).map_err(|e| format!("Malli: {}", e))?;
tracing::info!("Malli ladattu ({:.1}s) — {}", start.elapsed().as_secs_f64(), device_name);
Ok(LlmEngine {
tokenizer,
model,
device,
eos_token: 151645,
})
Ok(LlmEngine { ollama_url, model: RefCell::new(model), client })
}
pub fn generate(&mut self, prompt: &str, max_tokens: usize) -> Result<GenerateResult, String> {
// Prefill: aloitetaan vastaus ```-koodiblokkilla → malli jatkaa suoraan koodilla
let formatted = format!("<|im_start|>system\nYou are a coding assistant. Respond with ONLY code. No explanations, no markdown, no comments unless asked.<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n```\n", prompt);
pub fn model_name(&self) -> String {
self.model.borrow().clone()
}
let encoding = self.tokenizer.encode(formatted.as_str(), true)
.map_err(|e| format!("Encode: {}", e))?;
let input_ids: Vec<u32> = encoding.get_ids().to_vec();
let input_len = input_ids.len();
pub fn set_model(&self, new_model: String) {
*self.model.borrow_mut() = new_model;
}
// Nollataan KV-cache edellisestä promptista
self.model.clear_kv_cache();
/// Varmistaa että malli on ladattu Ollamaan (ollama pull)
pub async fn ensure_model(&self) -> Result<(), String> {
let model = self.model.borrow().clone();
tracing::info!("Tarkistetaan malli {}...", model);
let resp = self.client.post(format!("{}/api/pull", self.ollama_url))
.json(&serde_json::json!({ "name": model, "stream": false }))
.send()
.await
.map_err(|e| format!("Ollama pull: {}", e))?;
// Sampling-parametrit
let temperature = 0.7;
let top_k = 40;
let repetition_penalty = 1.15;
let mut rng_state: u64 = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos() as u64;
if resp.status().is_success() {
tracing::info!("Malli {} valmis", model);
Ok(())
} else {
Err(format!("Ollama pull epäonnistui: {}", resp.status()))
}
}
pub async fn generate(&self, prompt: &str, max_tokens: usize) -> Result<GenerateResult, String> {
let system = "You are a coding assistant. Respond with ONLY code. No explanations, no markdown, no comments unless asked.";
let model = self.model.borrow().clone();
let start = Instant::now();
// Prefill
let input = Tensor::new(input_ids.as_slice(), &self.device)
.and_then(|t| t.unsqueeze(0))
.map_err(|e| format!("Tensor: {}", e))?;
let logits = self.model.forward(&input, 0)
.map_err(|e| format!("Forward prefill: {}", e))?;
let logits = logits.squeeze(0).map_err(|e| format!("Squeeze: {}", e))?;
let logits = if logits.dims().len() == 2 {
let seq_len = logits.dim(0).map_err(|e| format!("Dim: {}", e))?;
if seq_len == 0 { return Err("Tyhjä tensori".to_string()); }
logits.get(seq_len - 1).map_err(|e| format!("Get: {}", e))?
} else {
logits
};
let mut generated_text = String::new();
let mut tokens_generated: usize = 0;
let mut all_tokens: Vec<u32> = Vec::new();
let mut next_token = sample_top_k(&logits, top_k, temperature, &all_tokens, repetition_penalty, &mut rng_state)?;
if next_token != self.eos_token {
if let Ok(text) = self.tokenizer.decode(&[next_token], true) {
generated_text.push_str(&text);
}
all_tokens.push(next_token);
tokens_generated += 1;
}
// Autoregressive
let mut pos = input_len;
for _ in 1..max_tokens {
if next_token == self.eos_token { break; }
let input = Tensor::new(&[next_token], &self.device)
.and_then(|t| t.unsqueeze(0))
.map_err(|e| format!("Tensor: {}", e))?;
let logits = self.model.forward(&input, pos)
.map_err(|e| format!("Forward pos {}: {}", pos, e))?;
let logits = logits.squeeze(0).map_err(|e| format!("Squeeze: {}", e))?;
let logits = if logits.dims().len() == 2 {
let seq_len = logits.dim(0).map_err(|e| format!("Dim: {}", e))?;
if seq_len == 0 { break; }
logits.get(seq_len - 1).map_err(|e| format!("Get: {}", e))?
} else {
logits
};
next_token = sample_top_k(&logits, top_k, temperature, &all_tokens, repetition_penalty, &mut rng_state)?;
pos += 1;
if next_token == self.eos_token { break; }
if let Ok(text) = self.tokenizer.decode(&[next_token], true) {
generated_text.push_str(&text);
// 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") || 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);
}
}
break;
let resp = self.client.post(format!("{}/api/generate", self.ollama_url))
.json(&serde_json::json!({
"model": model,
"prompt": prompt,
"system": system,
"stream": false,
"options": {
"num_predict": max_tokens,
"temperature": 0.7,
"top_k": 40,
"repeat_penalty": 1.15,
"stop": ["<|im_end|>", "\n###", "\nExplanation", "\nNote:"]
}
}
all_tokens.push(next_token);
tokens_generated += 1;
}))
.send()
.await
.map_err(|e| format!("Ollama generate: {}", e))?;
if !resp.status().is_success() {
return Err(format!("Ollama HTTP {}", resp.status()));
}
let gen_time = start.elapsed();
let tokens_per_sec = if gen_time.as_secs_f64() > 0.0 {
tokens_generated as f64 / gen_time.as_secs_f64()
let body: serde_json::Value = resp.json().await
.map_err(|e| format!("Ollama JSON: {}", e))?;
let text = body["response"].as_str().unwrap_or("").to_string();
let total_duration_ns = body["total_duration"].as_u64().unwrap_or(0);
let eval_count = body["eval_count"].as_u64().unwrap_or(0) as usize;
let eval_duration_ns = body["eval_duration"].as_u64().unwrap_or(1);
let duration_ms = start.elapsed().as_millis() as f64;
let tokens_per_sec = if eval_duration_ns > 0 {
eval_count as f64 / (eval_duration_ns as f64 / 1_000_000_000.0)
} else { 0.0 };
Ok(GenerateResult {
text: strip_markdown_wrapper(&generated_text),
tokens_generated,
duration_ms: gen_time.as_millis() as f64,
text: strip_code_fences(&text),
tokens_generated: eval_count,
duration_ms,
tokens_per_sec,
})
}
}
const LANG_TAGS: &[&str] = &[
"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",
];
/// Siivoa mallin tuottama vastaus (prefill-yhteensopiva).
fn strip_markdown_wrapper(text: &str) -> String {
/// Siivoa mahdolliset markdown-koodiblokki-merkit
fn strip_code_fences(text: &str) -> String {
let mut result = text.trim().to_string();
// 1. Kielitunniste — VAIN tunnettu kieli
if let Some(nl) = result.find('\n') {
let first = result[..nl].trim().to_lowercase();
if LANG_TAGS.contains(&first.as_str()) {
// Poista aloittava ```lang
if result.starts_with("```") {
if let Some(nl) = result.find('\n') {
result = result[nl + 1..].to_string();
}
}
// 2. Sulkeva ``` — VAIN omalla rivillään lopussa
// Poista sulkeva ```
let trimmed = result.trim_end();
if trimmed.ends_with("```") {
let before = &trimmed[..trimmed.len() - 3];
@@ -264,29 +117,7 @@ fn strip_markdown_wrapper(text: &str) -> String {
}
}
// 3. Johdantolauseet
let lower = result.trim().to_lowercase();
for prefix in &["sure!", "here is", "here's", "certainly!", "below is"] {
if lower.starts_with(prefix) {
if let Some(nl) = result.find('\n') { result = result[nl + 1..].to_string(); }
break;
}
}
// 4. Selityskommentit alusta
let mut lines: Vec<&str> = result.trim().lines().collect();
while !lines.is_empty() {
let first = lines[0].trim();
let is_preamble = first.starts_with("# ") && !first.starts_with("#!")
&& (first.to_lowercase().contains("this is")
|| first.to_lowercase().contains("simple")
|| first.to_lowercase().contains("program that")
|| first.to_lowercase().contains("here is")
|| first.to_lowercase().contains("the following")
|| first.to_lowercase().contains("below"));
if is_preamble { lines.remove(0); } else { break; }
}
lines.join("\n").trim().to_string()
result
}
pub struct GenerateResult {

View File

@@ -285,15 +285,19 @@ async fn main() {
}
}
// Ladataan LLM-malli
tracing::info!("Ladataan LLM-mallia...");
let mut llm = match inference::LlmEngine::load() {
// Ollama-backend
tracing::info!("Alustetaan Ollama-yhteyttä...");
let llm = match inference::LlmEngine::load() {
Ok(engine) => {
tracing::info!("LLM valmis inferenssiin!");
// Varmistetaan malli (ollama pull) — odotetaan kunnes valmis
match engine.ensure_model().await {
Ok(()) => tracing::info!("Ollama valmis inferenssiin!"),
Err(e) => tracing::warn!("Mallin lataus: {} — yritetään silti", e),
}
Some(engine)
}
Err(e) => {
tracing::warn!("LLM-lataus epäonnistui: {} — toimitaan ilman inferenssiä", e);
tracing::warn!("Ollama-alustus epäonnistui: {} — toimitaan ilman inferenssiä", e);
None
}
};
@@ -324,11 +328,13 @@ async fn main() {
if !prompt.is_empty() && msg_model.starts_with("qwen-coder") {
if let Some(ref mut engine) = llm {
if let Some(ref engine) = llm {
busy = true;
tracing::info!("Generoidaan (task_id: {}): \"{}\"", task_id, prompt);
let max_tokens = task.get("max_tokens").and_then(|v| v.as_u64()).unwrap_or(512) as usize;
tracing::info!("Generoidaan (task_id: {}, max_tokens: {}): \"{}\"", task_id, max_tokens, &prompt[..prompt.len().min(100)]);
match engine.generate(prompt, 64) {
let model_name = engine.model_name();
match engine.generate(prompt, max_tokens).await {
Ok(result) => {
tracing::info!(
"Tulos: {} tokenia | {:.0}ms | {:.1} tok/s | \"{}\"",
@@ -341,7 +347,7 @@ async fn main() {
let done = json!({
"type": "llm_done",
"prompt": prompt,
"model": "Qwen2.5-Coder-0.5B (native/GPU)",
"model": format!("{} (Ollama)", model_name),
"response": result.text,
"tokens_generated": result.tokens_generated,
"duration_ms": result.duration_ms,
@@ -360,7 +366,21 @@ async fn main() {
}
}
}
// Ohitetaan pair_task, stats jne.
// Mallin vaihto lennossa
if text.contains("change_model") {
if let Ok(task) = serde_json::from_str::<serde_json::Value>(&text) {
if let Some(new_model) = task.get("model").and_then(|v| v.as_str()) {
if let Some(ref engine) = llm {
tracing::info!("Vaihdetaan malli: {}", new_model);
engine.set_model(new_model.to_string());
match engine.ensure_model().await {
Ok(()) => tracing::info!("Malli {} valmis!", new_model),
Err(e) => tracing::error!("Mallin lataus epäonnistui: {}", e),
}
}
}
}
}
}
}
tracing::warn!("Yhteys hubiin katkesi — yritetään uudelleen 5s...");

View File

@@ -38,17 +38,50 @@ pub fn set_gpu_load(load: u32) {
console_log!("[Wasm] GPU Kuormitusraja vaihdettu -> {}%", load);
}
// Asynkroninen odotus WebAssemblylle
async fn sleep_ms(ms: i32) {
// Worker-yhteensopiva setTimeout — toimii sekä Window- että Worker-kontekstissa
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_name = setTimeout)]
fn set_timeout(closure: &js_sys::Function, ms: i32);
}
// Asynkroninen odotus WebAssemblylle (Window + Worker)
pub async fn sleep_ms(ms: i32) {
let promise = js_sys::Promise::new(&mut |resolve, _| {
web_sys::window()
.unwrap()
.set_timeout_with_callback_and_timeout_and_arguments_0(&resolve, ms)
.unwrap();
set_timeout(&resolve, ms);
});
let _ = wasm_bindgen_futures::JsFuture::from(promise).await;
}
// Worker-yhteensopiva Performance — käyttää globalThis.performance
pub fn perf_now() -> f64 {
js_sys::Reflect::get(&js_sys::global(), &"performance".into())
.ok()
.and_then(|p| js_sys::Reflect::get(&p, &"now".into()).ok())
.and_then(|f| f.dyn_into::<js_sys::Function>().ok())
.and_then(|f| {
let perf = js_sys::Reflect::get(&js_sys::global(), &"performance".into()).unwrap();
f.call0(&perf).ok()
})
.and_then(|v| v.as_f64())
.unwrap_or(0.0)
}
// Worker-yhteensopiva fetch — käyttää globalThis.fetch
pub async fn worker_fetch(url: &str) -> Result<web_sys::Response, String> {
let promise = js_sys::Reflect::get(&js_sys::global(), &"fetch".into())
.map_err(|_| "fetch ei saatavilla".to_string())?
.dyn_into::<js_sys::Function>()
.map_err(|_| "fetch ei funktio".to_string())?
.call1(&JsValue::NULL, &url.into())
.map_err(|e| format!("fetch: {:?}", e))?;
let resp = wasm_bindgen_futures::JsFuture::from(js_sys::Promise::from(promise))
.await
.map_err(|e| format!("fetch await: {:?}", e))?;
resp.dyn_into::<web_sys::Response>()
.map_err(|_| "ei Response".to_string())
}
// Geneerinen tensorilaskenta — toimii millä tahansa Burn-backendillä
fn run_matmul<B: burn::tensor::backend::Backend>(size: usize) -> String {
let device = Default::default();
@@ -123,10 +156,9 @@ async fn run_single_tokenize(text: String, ws: Rc<RefCell<WebSocket>>) {
let Some(bytes) = cached_tok else { return; };
let Ok(tokenizer) = tokenizers::Tokenizer::from_bytes(&bytes) else { return; };
let perf = web_sys::window().unwrap().performance().unwrap();
let start = perf.now();
let start = perf_now();
let result = tokenize_text(&tokenizer, &text);
let duration_ms = perf.now() - start;
let duration_ms = perf_now() - start;
let token_count = result["token_count"].as_u64().unwrap_or(0);
let cpt = result["chars_per_token"].as_f64().unwrap_or(0.0);
@@ -157,11 +189,10 @@ async fn run_pair_comparison(en_text: String, fi_text: String, ws: Rc<RefCell<We
return;
};
let perf = web_sys::window().unwrap().performance().unwrap();
let start_time = perf.now();
let start_time = perf_now();
let en_result = tokenize_text(&tokenizer, &en_text);
let fi_result = tokenize_text(&tokenizer, &fi_text);
let duration_ms = perf.now() - start_time; // millisekunteja desimaalitarkkuudella
let duration_ms = perf_now() - start_time;
let en_cpt = en_result["chars_per_token"].as_f64().unwrap_or(0.0);
let fi_cpt = fi_result["chars_per_token"].as_f64().unwrap_or(0.0);

View File

@@ -24,10 +24,7 @@ async fn ensure_cached(key: &str, url: &str, ws: &Rc<RefCell<WebSocket>>) -> Res
console_log!("[Qwen] Ladataan {}...", key);
let window = web_sys::window().unwrap();
let resp_val = wasm_bindgen_futures::JsFuture::from(window.fetch_with_str(url))
.await.map_err(|e| format!("Fetch epäonnistui: {:?}", e))?;
let resp: web_sys::Response = resp_val.dyn_into().map_err(|_| "Ei Response".to_string())?;
let resp = crate::worker_fetch(url).await?;
if !resp.ok() { return Err(format!("HTTP {}", resp.status())); }
let total_size: usize = resp.headers()
@@ -71,7 +68,7 @@ async fn ensure_cached(key: &str, url: &str, ws: &Rc<RefCell<WebSocket>>) -> Res
}
pub async fn run_qwen_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
let perf = web_sys::window().unwrap().performance().unwrap();
// performance via crate::perf_now()
let tok_bytes = match ensure_cached("qwen05b-tokenizer.json", TOKENIZER_URL, &ws).await {
Ok(b) => b,
@@ -88,7 +85,7 @@ pub async fn run_qwen_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
};
console_log!("[Qwen] Rakennetaan mallia...");
let start_load = perf.now();
let start_load = crate::perf_now();
let device = Device::Cpu;
let dtype = DType::F32;
@@ -120,7 +117,7 @@ pub async fn run_qwen_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
Err(e) => { console_log!("[Qwen] Mallin lataus: {}", e); return; }
};
let load_time = perf.now() - start_load;
let load_time = crate::perf_now() - start_load;
console_log!("[Qwen] Malli ladattu ({:.0}ms). Generoidaan...", load_time);
let encoding = match tokenizer.encode(prompt.as_str(), true) {
@@ -131,7 +128,7 @@ pub async fn run_qwen_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
let input_len = input_ids.len();
console_log!("[Qwen] Syöte: {} tokenia", input_len);
let start_gen = perf.now();
let start_gen = crate::perf_now();
let max_new_tokens = 32;
let mut generated_text = String::new();
let mut tokens_generated: usize = 0;
@@ -202,7 +199,7 @@ pub async fn run_qwen_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
crate::sleep_ms(0).await;
}
let gen_time = perf.now() - start_gen;
let gen_time = crate::perf_now() - start_gen;
let tokens_per_sec = if gen_time > 0.0 { (tokens_generated as f64 / gen_time) * 1000.0 } else { 0.0 };
console_log!("[Qwen] {} tokenia | {:.0}ms | {:.1} tok/s", tokens_generated, gen_time, tokens_per_sec);

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,
}
@@ -115,10 +140,7 @@ async fn ensure_cached(key: &str, url: &str, ws: &Rc<RefCell<WebSocket>>) -> Res
console_log!("[Coder] Ladataan {}...", key);
let window = web_sys::window().unwrap();
let resp_val = wasm_bindgen_futures::JsFuture::from(window.fetch_with_str(url))
.await.map_err(|e| format!("Fetch: {:?}", e))?;
let resp: web_sys::Response = resp_val.dyn_into().map_err(|_| "Ei Response".to_string())?;
let resp = crate::worker_fetch(url).await?;
if !resp.ok() { return Err(format!("HTTP {}", resp.status())); }
let total_size: usize = resp.headers()
@@ -182,50 +204,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| {
@@ -237,17 +248,16 @@ async fn get_or_build_model(use_3b: bool, ws: &Rc<RefCell<WebSocket>>) -> Result
/// use_3b: false = 0.5B (nopea), true = 3B (laadukas)
pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use_3b: bool, task_id: Option<String>) {
let perf = web_sys::window().unwrap().performance().unwrap();
let size_label = if use_3b { "3B" } else { "0.5B" };
let start_load = perf.now();
let start_load = crate::perf_now();
if let Err(e) = get_or_build_model(use_3b, &ws).await {
console_log!("[Coder] Mallin lataus: {}", e);
return;
}
let load_time = perf.now() - start_load;
let load_time = crate::perf_now() - start_load;
if load_time > 100.0 {
console_log!("[Coder] Malli ladattu ({:.0}ms). Generoidaan...", load_time);
}
@@ -258,13 +268,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(256) 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(), 256)
(prompt.clone(), default_system.to_string(), 512)
}
} else {
(prompt.clone(), default_system.to_string(), 256)
(prompt.clone(), default_system.to_string(), 512)
};
// Prefill: aloitetaan vastaus ```-koodiblokkilla, jolloin malli jatkaa suoraan koodilla
@@ -283,7 +293,7 @@ pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use
console_log!("[Coder] Syöte: {} tokenia", input_len);
let device = Device::Cpu;
let start_gen = perf.now();
let start_gen = crate::perf_now();
let eos_token = 151645u32;
let temperature: f32 = 0.7;
let top_k: usize = 40;
@@ -359,7 +369,7 @@ pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use
tokens_generated += 1;
}
let gen_time = perf.now() - start_gen;
let gen_time = crate::perf_now() - start_gen;
// Siivotaan vastaus: poista markdown-koodiblokit ja johdantotekstit
let cleaned = strip_markdown_wrapper(&generated_text);

View File

@@ -28,10 +28,7 @@ async fn ensure_cached(key: &str, url: &str, ws: &Rc<RefCell<WebSocket>>) -> Res
send_progress(ws, key, 0, 0, 0);
// Fetch API:lla saadaan Content-Length ja streaming-luku
let window = web_sys::window().unwrap();
let resp_val = wasm_bindgen_futures::JsFuture::from(window.fetch_with_str(url))
.await.map_err(|e| format!("Fetch epäonnistui: {:?}", e))?;
let resp: web_sys::Response = resp_val.dyn_into().map_err(|_| "Ei Response-objekti".to_string())?;
let resp = crate::worker_fetch(url).await?;
if !resp.ok() {
return Err(format!("HTTP {}", resp.status()));
@@ -99,7 +96,7 @@ fn send_progress(ws: &Rc<RefCell<WebSocket>>, file: &str, pct: u32, loaded: usiz
/// Lataa malli ja tokenizer, suorita inferenssi ja streamaa tokenit hubille
pub async fn run_smollm_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
let perf = web_sys::window().unwrap().performance().unwrap();
// performance via crate::perf_now()
// 1. Lataa tokenizer
let tok_bytes = match ensure_cached("smollm-tokenizer.json", TOKENIZER_URL, &ws).await {
@@ -122,7 +119,7 @@ pub async fn run_smollm_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
// Burn 0.21-pre.2 cubecl-runtime ei käänny Wasmille (println! puuttuu)
// → NdArray kunnes Burn 0.21 stable + Wasm-tuki
console_log!("[SmolLM] Burn NdArray (CPU) inferenssi...");
run_burn_inference::<burn::backend::NdArray>(prompt, model_bytes, tokenizer, ws, perf.clone()).await;
run_burn_inference::<burn::backend::NdArray>(prompt, model_bytes, tokenizer, ws).await;
}
async fn run_burn_inference<B: burn::tensor::backend::Backend>(
@@ -130,9 +127,8 @@ async fn run_burn_inference<B: burn::tensor::backend::Backend>(
model_bytes: Vec<u8>,
tokenizer: tokenizers::Tokenizer,
ws: Rc<RefCell<WebSocket>>,
perf: web_sys::Performance, // Korjattu Wasm-performanssi välitettäväksi
) {
let start_load = perf.now();
let start_load = crate::perf_now();
let device = Default::default();
let config = crate::burn_smollm::config::SmolLMConfig::default();
@@ -143,7 +139,7 @@ async fn run_burn_inference<B: burn::tensor::backend::Backend>(
Err(e) => { console_log!("[SmolLM] Lataus epäonnistui: {}", e); return; }
};
let load_time = perf.now() - start_load;
let load_time = crate::perf_now() - start_load;
console_log!("[SmolLM] Burn-malli ladattu ({:.0}ms). Generoidaan...", load_time);
let formatted_prompt = format!("<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n", prompt);
@@ -156,7 +152,7 @@ async fn run_burn_inference<B: burn::tensor::backend::Backend>(
let input_len = input_ids.len();
console_log!("[SmolLM] Syöte: {} tokenia", input_len);
let start_gen = perf.now();
let start_gen = crate::perf_now();
let max_new_tokens = 32;
let mut generated_text = String::new();
let mut tokens_generated: usize = 0;
@@ -219,7 +215,7 @@ async fn run_burn_inference<B: burn::tensor::backend::Backend>(
tokens_generated += 1;
}
let gen_time = perf.now() - start_gen;
let gen_time = crate::perf_now() - start_gen;
let tokens_per_sec = if gen_time > 0.0 { (tokens_generated as f64 / gen_time) * 1000.0 } else { 0.0 };
let done = serde_json::json!({

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.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,38 @@
// Kipinä WASM Worker (ES module) — ajaa kielimallin inferenssin erillisessä säikeessä
import init, { start_agent_node, set_gpu_load, set_auto_tasks } from './pkg/node.js';
let wasmReady = false;
// Välitetään console.log -viestit pääsäikeelle jotta UI-kuuntelijat näkevät ne
const _origLog = console.log;
console.log = function(...args) {
_origLog.apply(console, args);
self.postMessage({ type: 'log', message: args.join(' ') });
};
self.onmessage = async (e) => {
const { type, data } = e.data;
if (type === 'init') {
try {
await init();
wasmReady = true;
self.postMessage({ type: 'ready' });
} catch (err) {
self.postMessage({ type: 'error', message: 'WASM init: ' + err.message });
}
} else if (type === 'start') {
if (!wasmReady) return;
const { hubUrl, hasWebGPU, deviceInfo, taskId } = data;
try {
await start_agent_node(hubUrl, hasWebGPU, deviceInfo, taskId);
self.postMessage({ type: 'started' });
} catch (err) {
self.postMessage({ type: 'error', message: 'Node: ' + err.message });
}
} else if (type === 'set_gpu_load') {
if (wasmReady) set_gpu_load(data.load);
} else if (type === 'set_auto_tasks') {
if (wasmReady) set_auto_tasks(data.enabled);
}
};