138 Commits

Author SHA1 Message Date
afc7f9bcee Versio 0.2.4
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 18:52:21 +03:00
5d2027b2ca Native-node: automaattinen Ollama-haistelu käynnistyksessä
Jos OLLAMA_URL ei ole asetettu, kokeillaan järjestyksessä:
1. localhost:11434 (paikallinen Ollama)
2. 127.0.0.1:11434
3. ollama:11434 (Docker-verkko)
4. host.docker.internal:11434 (Docker-kontti → isäntä)

Ensimmäinen joka vastaa /api/version-kutsuun valitaan.
Timeout 2s per kokeilu. Jos OLLAMA_URL on asetettu, sitä käytetään suoraan.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 17:41:44 +03:00
8a4d515eed Client-compose: Ollama lisätty jokaiseen profiiliin (nvidia/amd/cpu)
Ollama-palvelu puuttui client-composesta — native-node yritti yhdistää
ollamaan jota ei ollut. Nyt jokaisessa profiilissa on oma Ollama
(nvidia: latest+GPU, amd: rocm+/dev/kfd, cpu: latest) network alias
'ollama' jotta native-node löytää sen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 17:40:33 +03:00
987a370a05 Docker: Cargo.lock valinnainen, Ollama AMD ROCm -tuella
- Dockerfile.native-node: Cargo.lock kopioidaan glob-patternilla (ei kaadu
  jos puuttuu, esim. .gitignore poistaa sen)
- docker-compose: Ollama vaihdettu ollama/ollama:rocm -imageen AMD GPU:lle,
  /dev/kfd + /dev/dri laitemappaukset, poistettu nvidia deploy.resources

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 17:33:27 +03:00
f75e7f07e9 Opas: tokenisointiesimerkki korvattu oikealla kuvakaappauksella
- Staattinen tekstitokenisointiesimerkki korvattu kuvalla joka
  näyttää värikoodatut tokenit EN/FI-vertailussa
- Markdown-renderöijään lisätty ![alt](src) kuvatuki
- Kuva: static/images/tokenization-example.png

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 13:20:50 +03:00
eb6f720fcc Wasm tokenize_js() exportti oppaan live-tokenizeria varten
Lisätty #[wasm_bindgen] tokenize_js(text) → JSON-funktio joka lataa
tokenizerin IndexedDB:stä tai HuggingFacesta tarvittaessa.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 13:18:07 +03:00
e25d0ea8f2 Versio 0.2.3
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 13:15:05 +03:00
4d1e89da34 Avatar-kuvakkeet 48px → 50px (+5%)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 13:14:14 +03:00
bf535b6256 Raportti: syntaksikorostus, CSS-tooltipsit rivityksellä, parannettu layout
- Lisätty highlightCode() — regex-pohjainen korostus .py, .html, Dockerfile
- Swimlane-badget: title-attr → CSS ::after tooltip (white-space:pre-wrap)
- Tiedostokortit: kieli + rivimäärä headeriin
- Yleinen ilme: pyöristetyt kulmat, monospace-fontti, parempi line-height
- Pipeline-vaiheet: selkeämmät erottimet ja väripaletti

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 13:10:37 +03:00
29e1c440c6 Native-node osoittaa nyt tuotantohubiin (kipina.studio)
HUB_URL vaihdettu ws://agentic-poc:3000/ws → wss://kipina.studio/ws
jotta lokaali GPU-solmu palvelee tuotantoympäristöä.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 13:05:19 +03:00
560bee1369 Laskentaverkko ja Koodilaboratorio -tabit takaisin näkyviin
Tabit olivat piilotettu display:none-tyylillä, mikä rikkoi myös
hash-navigoinnin (#codelab, #network).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 13:01:21 +03:00
b074e0cb49 Avatar-korttien opacity nostettu 0.5 → 0.8
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 12:55:46 +03:00
9307c75516 Pysty/vaaka-toggle avatareille
Lisätty nappi jolla org-chartin voi vaihtaa vaaka- ja pystyasennon välillä.
Vaaka on oletuksena (kompakti), pysty lisää vertical-CSS-luokan.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 12:55:07 +03:00
86191fbb6c Avatarit vaakariviin: kompakti layout, säästää pystytilaa
Org-chart muutettu vertikaalisesta hierarkiasta horisontaaliseksi riviksi:
Asiakas → Manageri → [Koodari, Data, QA, DevOps, Tarkkailija]
- Connector-viivat muutettu pystysuuntaisista vaakanuoliksi
- Avatar-kortit 72px leveitä (oli 130px), kuvat 48px (oli 80px)
- Roolikuvaus poistettu korteista — pelkkä nimi riittää
- Tarkkailija siirretty absolute-asemasta rivin loppuun
- Responsive-tyylit päivitetty

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 12:53:25 +03:00
a6a94f7688 Avatar-logiikka: poistettu kilpaileva llm_prompt-handler, korjattu vilkkumisjärjestys
Ongelma: kaksi erillistä avatar-aktivointilogiikkaa kilpaili:
1) pipelineStep() — oikea agentti statuksen perusteella
2) llm_prompt-handler — arvasi agentin prompt-tekstistä (usein väärin → DevOps vilkkui QA:n/Datan sijaan)

Korjaus:
- Poistettu llm_prompt- ja llm_done-handlerien avatar-heuristiikat
- pipelineStep() hoitaa kaiken: active → syttyy, done → sammuu heti
- Pipeline-lopussa kaikki avataret sammutetaan eksplisiittisesti

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 12:42:02 +03:00
8d5c5440d2 Avatar-vilahdus: oikea agentti aktivoituu vuorollaan, manageri-bugi korjattu
pipelineStep() aktivoi nyt oikean agentin avatarin (sekä card että gallery-head)
kun status on 'active', ja poistaa 'done'-statuksella sekunnin viiveellä.
Poistettu llm_done-handlerin turha manageri-aktivointi joka vilkutti aina manageria.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 12:38:08 +03:00
a12bd7ce7f One-liner koodi: system prompt vaatii rivinvaihdot + staattinen tarkistus
Ollaman system prompt: 'Use proper newlines and indentation'.
Staattinen analyysi: havaitsee jos .py-tiedosto on yhdellä rivillä.
Native node vaatii rebuildin.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 12:07:51 +03:00
9ac90aa540 pyproject.toml esimerkki: lisätty httpx ja pytest riippuvuuksiin
TestClient vaatii httpx:n, testit vaativat pyTestin.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 12:01:35 +03:00
32065d5818 Korjattu </script> index.html-esimerkissä joka katkaisi pääsivun JS:n
Sama bugi kuin aiemmin: template-literalin </script> sulkee
ulomman script-tagin. Pilkottu: '<'+'/script>'.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 12:00:53 +03:00
321943ee3c Esimerkkiprojektit: täysi CRUD + HTML UI root-osoitteessa
Few-shot esimerkit päivitetty:
- main.py: GET/POST/PUT/DELETE + FileResponse("/") index.html:lle
- index.html: yksinkertainen UI fetch()-kutsuilla API:in
- /api/ -prefiksi JSON-endpointeille
- Esimerkkipromptit kuvaavat CRUD-operaatiot eksplisiittisesti

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 11:49:38 +03:00
1b75c89320 Raportti: Mermaid → custom swimlane (kompakti, tooltipsit)
Agenteittain ryhmitelty visualisointi:
  Manageri  ✓ Suunnittelu
  Koodari   ✓ models.py → ✓ main.py → ✓ index.html
  DevOps    ✓ Review → ✓ Dockerfile → ✓ Compose → ✓ README
  QA        ✓ Testit → ✓ Validointi
Hover näyttää selityksen + output-esikatselun.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 11:39:23 +03:00
01622a960f Mermaid-kaavio LR (vaakasuunta) + tooltipsit joka vaiheelle
graph TD → graph LR: kaavio kiemurtelee vasemmalta oikealle.
Hover näyttää vaiheen selityksen ja output-esikatselun.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 11:33:23 +03:00
4e4efda67d Korjattu </script> template-literalissa joka katkaisi pääsivun JS:n
Template-stringin sisällä oleva </script> sulki ulomman script-tagin.
Pilkottu string-konkatenoimalla: '</'+' script>'.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 11:25:36 +03:00
f5db2eb034 Esimerkkiprojektit HTML UI:lla + Mermaid-kaavio raporttiin + tooltips
- Esimerkkipromptit sisältävät nyt HTML-käyttöliittymän
- Manageri generoi index.html tiedoston, Dockerfile kopioi sen
- README: docker compose up → http://localhost:8000
- Raporttiin Mermaid-kaavio agenttien workflowsta (CDN)
- Pipeline-vaiheiden hover näyttää selityksen

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 11:06:15 +03:00
77c8d46e7b Staattinen analyysi: tiedostojen väliset importit tarkistetaan
from db import get_db → tarkistaa onko get_db määritelty db.py:ssä
(def, class tai muuttuja). Löytää puuttuvat exportit ennen Docker-buildia.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 10:34:41 +03:00
f14eba1b49 Projektiraportti: HTML-dokumentaatio pipeline-vaiheista ja tiedostoista
Pipeline generoi HTML-raportin joka sisältää:
- Pipeline-vaiheet prompteineen ja tuloksineen (avattavat details)
- Staattisen analyysin tulokset
- Kaikki generoidut tiedostot korostettuina
- Raportti avautuu uuteen välilehteen linkistä terminaalissa
- Projektikorttiin lisätty 📄 Raportti -nappi

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 10:22:03 +03:00
6d15298418 Parannettu staattinen analyysi + docker-compose template
Staattinen analyysi: tarkistaa 15 yleistä symbolia (Boolean, Text,
Depends, HTTPException jne.) puuttuvista importeista.
Models-esimerkki: lisätty Boolean ja Text importteihin.
docker-compose.yml: template ilman LLM:ää — ei enää version tai
turhaa PostgreSQL-containeria SQLite-projektissa.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 10:16:19 +03:00
cea1961183 Testaaja: staattinen analyysi + strukturoitu LLM-arviointi
Staattinen analyysi selaimessa (ennen LLM:ää):
- Käyttämättömät importit
- Puuttuvat importit (FastAPI, Session)
- Tyhjät funktiot

LLM-arviointi 5 kohdalla (esimerkkivastauksineen):
1. Imports, 2. Database, 3. Endpoints, 4. Error handling, 5. Security

Korjausluuppi käynnistyy jos ✗ löytyy tai staattinen analyysi huomauttaa.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 10:13:15 +03:00
21a8015ea3 SQLAlchemy esimerkki: declarative_base() → DeclarativeBase (v2.0+)
declarative_base() on poistettu SQLAlchemy 2.0:sta.
Päivitetty molemmat esimerkit käyttämään class Base(DeclarativeBase).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 10:07:23 +03:00
c3991193d9 Pipeline-vaiheet rivittyvät + fixedDockerfile viittaus korjattu
Poistettu fixedDockerfile-viittaus joka kaatoi pipelinen ennen
renderProjectCard:ia → ZIP ei generoitunut.
Pipeline-vaiheet käyttävät nyt flex-wrap:ia eikä overflow-x:ää.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 10:03:14 +03:00
02c6d67218 Korjausvaihe ei ylikirjoita Dockerfilea — template pysyy
QA-validoinnin korjausvaihe antoi LLM:n generoida uuden Dockerfilen
joka sekoitti pip:n ja uv:n. Nyt korjaus kohdistuu vain .py ja
pyproject.toml -tiedostoihin. Dockerfile pysyy templatena.

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 13:55:58 +03:00
Jaakko Vanhala
7eca426e77 strip_markdown_wrapper robustimmaksi: whitelist-kielitunnisteet + tarkempi ```-poisto
Edelliset heuristiikat olivat hauraita:
- Kielitunniste tunnistettiin "lyhyt alphanumeerinen rivi" → osui koodiin (i, 42)
- rfind("```") poisti koodin sisäisiä backtickejä

Korjaukset:
- Kielitunniste poistetaan VAIN jos se on tunnettu (LANG_TAGS whitelist, 50+ kieltä)
- Sulkeva ``` poistetaan VAIN jos se on omalla rivillään tiedoston lopussa
  (ends_with tarkistus + edeltävä rivinvaihto)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 10:10:48 +03:00
Jaakko Vanhala
7a1352ead7 Korjattu strip_markdown_wrapper yhteensopivaksi prefill-tekniikan kanssa
Prefill lisää ``` prompttiin jolloin malli tuottaa: "rust\nfn main()...\n```"
Vanha stripperi etsi aloittavaa ```-blokkia ja palautti tyhjän.
Uusi logiikka:
1. Poistaa kielitunnisteen ensimmäiseltä riviltä (rust, python jne.)
2. Poistaa sulkevan ``` lopusta (rfind, varmistaa ettei ole koodin sisällä)
3. Poistaa johdantolauseet ja selityskommentit kuten ennenkin

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 10:07:19 +03:00
Jaakko Vanhala
b9017448d8 BUILDING_BLOCKS.md: rakennuspalaset ja työnkulut jatkokehitystä varten
Dokumentoi kaikki arkkitehtuuripatternit, UI-komponentit ja työnkulut:
- WebSocket-reaaliaikakommunikaatio (broadcast, reititys, busy-state, työjono)
- Wasm-laskentasolmun elinkaari ja kolmitasoinen cache
- LLM-inferenssipipeline (prefill, sampling, stop-sekvenssit, streaming)
- Terminaaliemulaattori (tab-completion, dropdown, historia)
- Status-palkit ja tilaindikaattorit
- Tietoturva (XSS, rate limiting, viestityyppivalidointi, gamification-esto)
- Agenttien orkestrointi (pipeline, promptien hallinta)
- Teknologiapino ja jatkokehitysideat

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 10:06:05 +03:00
Jaakko Vanhala
3d1b406e8d Alasvetovalikko kpn-terminaalin autocompletioniin
TAB avaa dropdown-valikon käytettävissä olevista vaihtoehdoista:
- Nuolilla (ylös/alas) navigointi
- Enter tai TAB valitsee korostetun vaihtoehdon
- Esc sulkee valikon
- Klikkaus valitsee suoraan
- Yksi vaihtoehto → täydennetään suoraan ilman valikkoa

Valikko näyttää kontekstin mukaan: alikomennot, mallit/agentit
tai esimerkkiprompteja. Sulkeutuu automaattisesti kun klikataan muualle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 09:53:47 +03:00
Jaakko Vanhala
aa6c4739dd Shift-TAB poistaa viimeisen sanan kpn-terminaalissa
Poistaa viimeisen sanan tai lainausmerkeissä olevan kokonaisuuden:
- "kpn run coder " → Shift-TAB → "kpn run "
- 'kpn run coder "hello world"' → Shift-TAB → "kpn run coder "

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 09:51:39 +03:00
Jaakko Vanhala
cbbf427a93 Tab-completion kpn-terminaaliin: ennustava komennonsyöttö sana kerrallaan
TAB täydentää kontekstin mukaan:
- tyhjä → "kpn "
- "kpn " → "kpn help", "kpn run", "kpn pipeline" jne.
- "kpn run " → agentit ja mallit (coder, manager, qwen-coder...)
- "kpn run coder " → esimerkkiprompteja ("hello world in python")
- "kpn pi" → "kpn pipeline "
- osittainen sana → yhteinen etuliite tai ainoa vaihtoehto

Tukee myös kpn pipeline -esimerkkiprompteja.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 09:51:01 +03:00
Jaakko Vanhala
0a216f19e2 Laskentasolmun käynnistys käyttäjän hallinnassa: nappi + cancel + kpn load
Status-palkissa "Alusta laskentasolmu" -nappi joka:
- Klikkaa → käynnistää kielimallin latauksen omalle koneelle
- Latauksen aikana muuttuu "Peruuta"-napiksi (punainen)
- Valmis → vihreä "✓ Valmis" -tila
Myös kpn load -komento terminaalissa tekee saman.
Agents-sivulla ei enää automaattista käynnistystä — käyttäjä valitsee itse.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 09:50:07 +03:00
Jaakko Vanhala
a2e7ed53ff Laskentavalmiuden indikaattori agents-sivulle + kpn load -komento
Status-palkissa näkyy nyt Hub-yhteyden lisäksi laskentasolmun tila:
- Harmaa "Ei käynnissä ⟩" — klikkaa käynnistääksesi
- Keltainen "Ladataan mallia..." — malli latautuu
- Vihreä "Valmis (Qwen2.5-Coder)" — valmis laskentaan

Kaksi tapaa käynnistää:
1. Klikkaa compute-statusta status-palkissa
2. Kirjoita terminaaliin: kpn load

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 09:48:50 +03:00
Jaakko Vanhala
950cae9d96 Agents-sivu käynnistää oman laskentasolmun: käyttäjän kone valjastetaan laskentaan
Kun käyttäjä avaa #agents-sivun, käynnistetään automaattisesti Wasm coder-node
jotta tehtävät reitittyvät omalle koneelle eikä ulkoisille solmuille.
Sama logiikka kuin codelabissa (ensureCoderNode + warmup).
Toimii sekä suoralla #agents-navigoinnilla että tab-vaihdolla.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 09:45:29 +03:00
22 changed files with 4442 additions and 633 deletions

3
.gitignore vendored
View File

@@ -34,3 +34,6 @@ Cargo.lock
*.pdb *.pdb
# End of https://www.toptal.com/developers/gitignore/api/rust,linux # 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

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

View File

@@ -1,30 +1,36 @@
FROM rust:slim AS builder FROM rust:slim AS builder
RUN apt-get update && apt-get install -y \ 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/* && rm -rf /var/lib/apt/lists/*
WORKDIR /app WORKDIR /app
COPY Cargo.toml Cargo.lock ./ COPY Cargo.toml ./
COPY Cargo.loc[k] ./
COPY hub/Cargo.toml hub/Cargo.toml COPY hub/Cargo.toml hub/Cargo.toml
COPY node/Cargo.toml node/Cargo.toml COPY node/Cargo.toml node/Cargo.toml
COPY native-node/Cargo.toml native-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 # 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 "fn main(){}" > hub/src/main.rs \
&& echo "" > node/src/lib.rs \ && echo "" > node/src/lib.rs \
&& echo "fn main(){}" > native-node/src/main.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 && cargo build --release -p native-node 2>/dev/null || true
COPY native-node/src native-node/src 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 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 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 ENV ALLOCATED_GB=4
CMD ["native-node"] 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

@@ -1,13 +1,12 @@
services: services:
# NVIDIA GPU -solmu # Ollama NVIDIA GPU:lla
native-node-nvidia: ollama-nvidia:
build: image: ollama/ollama:latest
context: . container_name: kipina-ollama
dockerfile: Dockerfile.native-node ports:
container_name: kipina-node-nvidia - "11434:11434"
environment: volumes:
- HUB_URL=wss://kipina.studio/ws - ollama-models:/root/.ollama
- ALLOCATED_GB=4
restart: unless-stopped restart: unless-stopped
deploy: deploy:
resources: resources:
@@ -16,6 +15,65 @@ services:
- driver: nvidia - driver: nvidia
count: all count: all
capabilities: [gpu] capabilities: [gpu]
networks:
default:
aliases:
- ollama
profiles:
- nvidia
# Ollama AMD ROCm GPU:lla
ollama-amd:
image: ollama/ollama:rocm
container_name: kipina-ollama
ports:
- "11434:11434"
volumes:
- ollama-models:/root/.ollama
restart: unless-stopped
devices:
- /dev/kfd:/dev/kfd
- /dev/dri:/dev/dri
group_add:
- video
- render
networks:
default:
aliases:
- ollama
profiles:
- amd
# Ollama CPU:lla
ollama-cpu:
image: ollama/ollama:latest
container_name: kipina-ollama
ports:
- "11434:11434"
volumes:
- ollama-models:/root/.ollama
restart: unless-stopped
networks:
default:
aliases:
- ollama
profiles:
- cpu
# NVIDIA GPU -solmu
native-node-nvidia:
build:
context: .
dockerfile: Dockerfile.native-node
container_name: kipina-node-nvidia
environment:
- HUB_URL=wss://kipina.studio/ws
- OLLAMA_URL=http://ollama:11434
- OLLAMA_MODEL=qwen2.5-coder:7b
- ALLOCATED_GB=4
restart: unless-stopped
depends_on:
- ollama-nvidia
profiles: profiles:
- nvidia - nvidia
@@ -27,14 +85,12 @@ services:
container_name: kipina-node-amd container_name: kipina-node-amd
environment: environment:
- HUB_URL=wss://kipina.studio/ws - HUB_URL=wss://kipina.studio/ws
- OLLAMA_URL=http://ollama:11434
- OLLAMA_MODEL=qwen2.5-coder:7b
- ALLOCATED_GB=4 - ALLOCATED_GB=4
restart: unless-stopped restart: unless-stopped
devices: depends_on:
- /dev/kfd:/dev/kfd - ollama-amd
- /dev/dri:/dev/dri
group_add:
- video
- render
profiles: profiles:
- amd - amd
@@ -46,7 +102,14 @@ services:
container_name: kipina-node-cpu container_name: kipina-node-cpu
environment: environment:
- HUB_URL=wss://kipina.studio/ws - HUB_URL=wss://kipina.studio/ws
- OLLAMA_URL=http://ollama:11434
- OLLAMA_MODEL=qwen2.5-coder:7b
- ALLOCATED_GB=2 - ALLOCATED_GB=2
restart: unless-stopped restart: unless-stopped
depends_on:
- ollama-cpu
profiles: profiles:
- cpu - cpu
volumes:
ollama-models:

View File

@@ -9,26 +9,39 @@ services:
volumes: volumes:
- .:/app - .:/app
# Käännetään aina käynnistyksen yhteydessä varmuuden vuoksi Wasm uusimmista koodeista, ja päälle pyöräytetään Hub! # 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) # Ollama — LLM-inferenssi
# NVIDIA: vaihda image → ollama/ollama:latest ja lisää deploy.resources (ks. README)
# CPU: vaihda image → ollama/ollama:latest ja poista devices
ollama:
image: ollama/ollama:rocm
container_name: kipina_ollama
ports:
- "11434:11434"
volumes:
- ollama-models:/root/.ollama
devices:
- /dev/kfd
- /dev/dri
profiles:
- native
# Natiivisolmu — yhdistää hubiin ja käyttää Ollamaa inferenssiin
native-node: native-node:
build: build:
context: . context: .
dockerfile: Dockerfile.native-node dockerfile: Dockerfile.native-node
container_name: kipina_native_node container_name: kipina_native_node
environment: environment:
- HUB_URL=ws://agentic-poc:3000/ws - HUB_URL=wss://kipina.studio/ws
- OLLAMA_URL=http://ollama:11434
- OLLAMA_MODEL=qwen2.5-coder:7b
- ALLOCATED_GB=4 - ALLOCATED_GB=4
depends_on: depends_on:
- agentic-poc - ollama
# GPU passthrough (valinnainen — toimii myös ilman)
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
profiles: profiles:
- native - native
volumes:
ollama-models:

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "hub" name = "hub"
version = "0.2.0" version = "0.2.4"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
@@ -16,3 +16,4 @@ futures = "0.3"
rusqlite = { version = "0.31", features = ["bundled"] } rusqlite = { version = "0.31", features = ["bundled"] }
chrono = "0.4" chrono = "0.4"
base64 = "0.22" 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>>, ip_connections: Mutex<HashMap<IpAddr, u32>>,
node_ips: Mutex<HashMap<u64, IpAddr>>, node_ips: Mutex<HashMap<u64, IpAddr>>,
node_tasks: Mutex<HashMap<u64, String>>, // node_id → selected_task 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ä 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) 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ä) 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()), ip_connections: Mutex::new(HashMap::new()),
node_ips: Mutex::new(HashMap::new()), node_ips: Mutex::new(HashMap::new()),
node_tasks: Mutex::new(HashMap::new()), node_tasks: Mutex::new(HashMap::new()),
node_types: Mutex::new(HashMap::new()),
node_busy: Mutex::new(std::collections::HashSet::new()), node_busy: Mutex::new(std::collections::HashSet::new()),
pending_task_ids: Mutex::new(std::collections::HashSet::new()), pending_task_ids: Mutex::new(std::collections::HashSet::new()),
api_rate_limits: Mutex::new(HashMap::new()), api_rate_limits: Mutex::new(HashMap::new()),
@@ -382,6 +384,9 @@ async fn main() {
.route("/api/pairs", get(api_pairs)) .route("/api/pairs", get(api_pairs))
.route("/api/stats", get(api_stats)) .route("/api/stats", get(api_stats))
.route("/api/v1/chat/completions", axum::routing::post(api_chat_completions)) .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)) .route("/admin", get(admin_page))
.nest_service("/", { .nest_service("/", {
let static_dir = std::env::var("STATIC_DIR").unwrap_or_else(|_| "../static".to_string()); 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()) .and_then(|s| s.trim().parse::<IpAddr>().ok())
.unwrap_or_else(|| addr.ip()); .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 conns = state.ip_connections.lock().unwrap();
let count = conns.get(&ip).copied().unwrap_or(0); let count = conns.get(&ip).copied().unwrap_or(0);
if count >= 4 { if count >= 10 {
tracing::warn!("IP {} ylitti yhteysrajan ({}/4) — estetty", ip, count); tracing::warn!("IP {} ylitti yhteysrajan ({}/10) — estetty", ip, count);
return ( return (
axum::http::StatusCode::TOO_MANY_REQUESTS, axum::http::StatusCode::TOO_MANY_REQUESTS,
"Max 4 yhteyttä per IP", "Max 10 yhteyttä per IP",
).into_response(); ).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.db.insert_session(node_id, &ip.to_string(), node_type, &json);
} }
state.node_tasks.lock().unwrap().insert(node_id, selected_task); 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" { if node_type == "native" {
let sys = json.get("system"); let sys = json.get("system");
@@ -934,6 +940,7 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
ips.remove(&node_id); ips.remove(&node_id);
vram.remove(&node_id); vram.remove(&node_id);
} }
state.node_types.lock().unwrap().remove(&node_id);
tracing::info!("Solmu {} ({}) poistui verkosta.", node_id, ip); tracing::info!("Solmu {} ({}) poistui verkosta.", node_id, ip);
broadcast_stats(&state).await; broadcast_stats(&state).await;
sender_task.abort(); sender_task.abort();
@@ -943,6 +950,8 @@ struct ChatCompletionRequest {
model: String, model: String,
prompt: String, prompt: String,
task_id: String, task_id: String,
#[serde(default)]
max_tokens: Option<u64>,
} }
#[derive(serde::Serialize)] #[derive(serde::Serialize)]
@@ -952,6 +961,78 @@ struct ChatCompletionResponse {
tokens_generated: u64, 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( async fn api_chat_completions(
axum::extract::State(state): axum::extract::State<Arc<AppState>>, axum::extract::State(state): axum::extract::State<Arc<AppState>>,
ConnectInfo(addr): ConnectInfo<SocketAddr>, ConnectInfo(addr): ConnectInfo<SocketAddr>,
@@ -966,24 +1047,30 @@ async fn api_chat_completions(
*entry = (now, 1); // Uusi ikkuna *entry = (now, 1); // Uusi ikkuna
} else { } else {
entry.1 += 1; 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(); 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 (target_node_free, target_node_any, total_matching) = {
let tasks = state.node_tasks.lock().unwrap(); let tasks = state.node_tasks.lock().unwrap();
let busy = state.node_busy.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)| { let matching: Vec<u64> = tasks.iter().filter(|(_, task)| {
if payload.model == "qwen-coder" { if payload.model == "qwen-coder" {
*task == "qwen-coder-05b" || *task == "qwen-coder" task.starts_with("qwen-coder")
} else { } else {
**task == payload.model **task == payload.model
} }
}).map(|(k, _)| *k).collect(); }).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(); let any = matching.first().copied();
(free, any, matching.len()) (free, any, matching.len())
}; };
@@ -1059,12 +1146,15 @@ async fn api_chat_completions(
state.node_busy.lock().unwrap().insert(target_node_id); state.node_busy.lock().unwrap().insert(target_node_id);
state.pending_task_ids.lock().unwrap().insert(payload.task_id.clone()); 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", "type": "llm_prompt",
"prompt": payload.prompt, "prompt": payload.prompt,
"model": payload.model, "model": payload.model,
"task_id": payload.task_id, "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) // Odotuskanava valmiiksi (solmu palauttaa tuloksen stats_tx kautta)
let mut rx = state.stats_tx.subscribe(); 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 { loop {
let msg_str = match rx.recv().await { let msg_str = match rx.recv().await {
Ok(msg) => msg, Ok(msg) => msg,

View File

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

View File

@@ -1,275 +1,153 @@
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; use std::time::Instant;
use std::cell::RefCell;
/// 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)
}
pub struct LlmEngine { pub struct LlmEngine {
tokenizer: tokenizers::Tokenizer, ollama_url: String,
model: QwenModel, model: RefCell<String>,
device: Device, client: reqwest::Client,
eos_token: u32,
} }
impl LlmEngine { impl LlmEngine {
pub fn load() -> Result<Self, String> { pub async fn load() -> Result<Self, String> {
let device = Device::cuda_if_available(0).map_err(|e| format!("Device: {}", e))?; let model = std::env::var("OLLAMA_MODEL").unwrap_or_else(|_| "qwen2.5-coder:7b".to_string());
let device_name = if device.is_cuda() { "CUDA" } else { "CPU" };
tracing::info!("LLM device: {}", device_name);
let dtype = if device.is_cuda() { DType::F16 } else { DType::F32 }; let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(600))
.connect_timeout(std::time::Duration::from_secs(3))
.build()
.map_err(|e| format!("HTTP client: {}", e))?;
tracing::info!("Ladataan Qwen2.5-Coder-0.5B-Instruct..."); // Jos OLLAMA_URL on asetettu, käytetään sitä suoraan
let api = Api::new().map_err(|e| format!("HF API: {}", e))?; let ollama_url = if let Ok(url) = std::env::var("OLLAMA_URL") {
let repo = api.repo(Repo::with_revision( tracing::info!("Ollama backend (env): {}", url);
"Qwen/Qwen2.5-Coder-0.5B-Instruct".to_string(), url
RepoType::Model,
"main".to_string(),
));
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,
})
}
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);
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();
// Nollataan KV-cache edellisestä promptista
self.model.clear_kv_cache();
// 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;
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 { } else {
logits // Haistellaan Ollamaa tunnetuista osoitteista
}; let candidates = [
"http://localhost:11434",
let mut generated_text = String::new(); "http://127.0.0.1:11434",
let mut tokens_generated: usize = 0; "http://ollama:11434",
let mut all_tokens: Vec<u32> = Vec::new(); "http://host.docker.internal:11434",
];
let mut next_token = sample_top_k(&logits, top_k, temperature, &all_tokens, repetition_penalty, &mut rng_state)?; let mut found = None;
for url in &candidates {
if next_token != self.eos_token { let probe = reqwest::Client::builder()
if let Ok(text) = self.tokenizer.decode(&[next_token], true) { .connect_timeout(std::time::Duration::from_secs(2))
generated_text.push_str(&text); .build().unwrap_or(client.clone());
} if let Ok(resp) = probe.get(format!("{}/api/version", url)).send().await {
all_tokens.push(next_token); if resp.status().is_success() {
tokens_generated += 1; tracing::info!("Ollama löytyi osoitteesta: {}", url);
} found = Some(url.to_string());
break;
// 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") {
for stop in &["\n###", "\nExplanation", "\nNote:", "\nOutput:", "\n```\n\n"] {
if let Some(pos) = generated_text.find(stop) {
generated_text.truncate(pos);
}
} }
break;
} }
} }
all_tokens.push(next_token); found.unwrap_or_else(|| {
tokens_generated += 1; tracing::warn!("Ollamaa ei löytynyt — käytetään oletusta http://localhost:11434");
"http://localhost:11434".to_string()
})
};
tracing::info!("Ollama backend: {} | malli: {}", ollama_url, model);
Ok(LlmEngine { ollama_url, model: RefCell::new(model), client })
}
pub fn model_name(&self) -> String {
self.model.borrow().clone()
}
pub fn set_model(&self, new_model: String) {
*self.model.borrow_mut() = new_model;
}
/// 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))?;
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. Use proper newlines and indentation. No explanations, no markdown fences, no comments unless asked.";
let model = self.model.borrow().clone();
let start = Instant::now();
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:"]
}
}))
.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 body: serde_json::Value = resp.json().await
let tokens_per_sec = if gen_time.as_secs_f64() > 0.0 { .map_err(|e| format!("Ollama JSON: {}", e))?;
tokens_generated as f64 / gen_time.as_secs_f64()
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 }; } else { 0.0 };
Ok(GenerateResult { Ok(GenerateResult {
text: strip_markdown_wrapper(&generated_text), text: strip_code_fences(&text),
tokens_generated, tokens_generated: eval_count,
duration_ms: gen_time.as_millis() as f64, duration_ms,
tokens_per_sec, tokens_per_sec,
}) })
} }
} }
/// Poistaa mallin tuottaman markdown-wrapperin ja johdantotekstin. /// Siivoa mahdolliset markdown-koodiblokki-merkit
fn strip_markdown_wrapper(text: &str) -> String { fn strip_code_fences(text: &str) -> String {
let text = text.trim(); let mut result = text.trim().to_string();
if let Some(start) = text.find("```") {
let after = &text[start + 3..]; // Poista aloittava ```lang
let code_start = after.find('\n').map(|i| i + 1).unwrap_or(0); if result.starts_with("```") {
let code = &after[code_start..]; if let Some(nl) = result.find('\n') {
if let Some(end) = code.find("```") { result = result[nl + 1..].to_string();
return code[..end].trim().to_string();
}
return code.trim().to_string();
}
let mut result = text.to_string();
let lower = result.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;
} }
} }
let mut lines: Vec<&str> = result.trim().lines().collect();
while !lines.is_empty() { // Poista sulkeva ```
let first = lines[0].trim(); let trimmed = result.trim_end();
let is_preamble = first.starts_with("# ") if trimmed.ends_with("```") {
&& !first.starts_with("#!") let before = &trimmed[..trimmed.len() - 3];
&& (first.to_lowercase().contains("this is") if before.is_empty() || before.ends_with('\n') {
|| first.to_lowercase().contains("simple") result = before.trim_end().to_string();
|| 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 { pub struct GenerateResult {

View File

@@ -285,15 +285,19 @@ async fn main() {
} }
} }
// Ladataan LLM-malli // Ollama-backend
tracing::info!("Ladataan LLM-mallia..."); tracing::info!("Alustetaan Ollama-yhteyttä...");
let mut llm = match inference::LlmEngine::load() { let llm = match inference::LlmEngine::load().await {
Ok(engine) => { 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) Some(engine)
} }
Err(e) => { Err(e) => {
tracing::warn!("LLM-lataus epäonnistui: {} — toimitaan ilman inferenssiä", e); tracing::warn!("Ollama-alustus epäonnistui: {} — toimitaan ilman inferenssiä", e);
None None
} }
}; };
@@ -324,11 +328,13 @@ async fn main() {
if !prompt.is_empty() && msg_model.starts_with("qwen-coder") { 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; 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) => { Ok(result) => {
tracing::info!( tracing::info!(
"Tulos: {} tokenia | {:.0}ms | {:.1} tok/s | \"{}\"", "Tulos: {} tokenia | {:.0}ms | {:.1} tok/s | \"{}\"",
@@ -341,7 +347,7 @@ async fn main() {
let done = json!({ let done = json!({
"type": "llm_done", "type": "llm_done",
"prompt": prompt, "prompt": prompt,
"model": "Qwen2.5-Coder-0.5B (native/GPU)", "model": format!("{} (Ollama)", model_name),
"response": result.text, "response": result.text,
"tokens_generated": result.tokens_generated, "tokens_generated": result.tokens_generated,
"duration_ms": result.duration_ms, "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..."); 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); console_log!("[Wasm] GPU Kuormitusraja vaihdettu -> {}%", load);
} }
// Asynkroninen odotus WebAssemblylle // Worker-yhteensopiva setTimeout — toimii sekä Window- että Worker-kontekstissa
async fn sleep_ms(ms: i32) { #[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, _| { let promise = js_sys::Promise::new(&mut |resolve, _| {
web_sys::window() set_timeout(&resolve, ms);
.unwrap()
.set_timeout_with_callback_and_timeout_and_arguments_0(&resolve, ms)
.unwrap();
}); });
let _ = wasm_bindgen_futures::JsFuture::from(promise).await; 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ä // Geneerinen tensorilaskenta — toimii millä tahansa Burn-backendillä
fn run_matmul<B: burn::tensor::backend::Backend>(size: usize) -> String { fn run_matmul<B: burn::tensor::backend::Backend>(size: usize) -> String {
let device = Default::default(); let device = Default::default();
@@ -85,6 +118,27 @@ async fn run_ai_tensor_inference(difficulty: usize) -> String {
format!("PoC {} Matmul ({}x{}) >> {}", backend_name, active_workload_size, active_workload_size, result) format!("PoC {} Matmul ({}x{}) >> {}", backend_name, active_workload_size, active_workload_size, result)
} }
/// JS-exportti: tokenisoi tekstin ja palauttaa JSON-merkkijonon
/// Tokenizer ladataan IndexedDB:stä (täytyy olla ladattu aiemmin)
#[wasm_bindgen]
pub async fn tokenize_js(text: String) -> Result<String, JsValue> {
let cached_tok = storage::load_from_idb("tokenizer.json").await.unwrap_or(None);
let Some(bytes) = cached_tok else {
// Yritetään ladata verkosta
let resp = reqwest::get("https://huggingface.co/Qwen/Qwen2.5-Coder-0.5B/resolve/main/tokenizer.json").await
.map_err(|e| JsValue::from_str(&format!("Tokenizer-lataus epäonnistui: {}", e)))?;
let bytes = resp.bytes().await
.map_err(|e| JsValue::from_str(&format!("Tokenizer-lataus epäonnistui: {}", e)))?;
let _ = storage::save_to_idb("tokenizer.json", &bytes).await;
let tokenizer = tokenizers::Tokenizer::from_bytes(&bytes)
.map_err(|e| JsValue::from_str(&format!("Tokenizer-parsinta: {}", e)))?;
return Ok(tokenize_text(&tokenizer, &text).to_string());
};
let tokenizer = tokenizers::Tokenizer::from_bytes(&bytes)
.map_err(|e| JsValue::from_str(&format!("Tokenizer-parsinta: {}", e)))?;
Ok(tokenize_text(&tokenizer, &text).to_string())
}
/// Tokenisoi yhden tekstin ja palauttaa metriikat /// Tokenisoi yhden tekstin ja palauttaa metriikat
fn tokenize_text(tokenizer: &tokenizers::Tokenizer, text: &str) -> serde_json::Value { fn tokenize_text(tokenizer: &tokenizers::Tokenizer, text: &str) -> serde_json::Value {
let char_count = text.chars().count(); let char_count = text.chars().count();
@@ -123,10 +177,9 @@ async fn run_single_tokenize(text: String, ws: Rc<RefCell<WebSocket>>) {
let Some(bytes) = cached_tok else { return; }; let Some(bytes) = cached_tok else { return; };
let Ok(tokenizer) = tokenizers::Tokenizer::from_bytes(&bytes) 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 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 token_count = result["token_count"].as_u64().unwrap_or(0);
let cpt = result["chars_per_token"].as_f64().unwrap_or(0.0); let cpt = result["chars_per_token"].as_f64().unwrap_or(0.0);
@@ -157,11 +210,10 @@ async fn run_pair_comparison(en_text: String, fi_text: String, ws: Rc<RefCell<We
return; 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 en_result = tokenize_text(&tokenizer, &en_text);
let fi_result = tokenize_text(&tokenizer, &fi_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 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); 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); console_log!("[Qwen] Ladataan {}...", key);
let window = web_sys::window().unwrap(); let resp = crate::worker_fetch(url).await?;
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())?;
if !resp.ok() { return Err(format!("HTTP {}", resp.status())); } if !resp.ok() { return Err(format!("HTTP {}", resp.status())); }
let total_size: usize = resp.headers() 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>>) { 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 { let tok_bytes = match ensure_cached("qwen05b-tokenizer.json", TOKENIZER_URL, &ws).await {
Ok(b) => b, Ok(b) => b,
@@ -88,7 +85,7 @@ pub async fn run_qwen_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
}; };
console_log!("[Qwen] Rakennetaan mallia..."); console_log!("[Qwen] Rakennetaan mallia...");
let start_load = perf.now(); let start_load = crate::perf_now();
let device = Device::Cpu; let device = Device::Cpu;
let dtype = DType::F32; 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; } 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); console_log!("[Qwen] Malli ladattu ({:.0}ms). Generoidaan...", load_time);
let encoding = match tokenizer.encode(prompt.as_str(), true) { 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(); let input_len = input_ids.len();
console_log!("[Qwen] Syöte: {} tokenia", input_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 max_new_tokens = 32;
let mut generated_text = String::new(); let mut generated_text = String::new();
let mut tokens_generated: usize = 0; 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; 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 }; 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); 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::{Device, Tensor, DType};
use candle_core::quantized::gguf_file;
use candle_nn::VarBuilder; use candle_nn::VarBuilder;
use candle_transformers::models::qwen2::{Config as QwenConfig, ModelForCausalLM as QwenModel}; 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 wasm_bindgen::JsCast;
use std::cell::RefCell; use std::cell::RefCell;
use std::rc::Rc; use std::rc::Rc;
@@ -16,37 +18,76 @@ macro_rules! console_log {
const MODEL_05B_URL: &str = "https://huggingface.co/Qwen/Qwen2.5-Coder-0.5B-Instruct/resolve/main/model.safetensors"; 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"; 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) // 1.5B GGUF Q4_K_M — kvantisoidtu, mahtuu selaimeen (~1 GB)
const MODEL_3B_PART1_URL: &str = "https://huggingface.co/Qwen/Qwen2.5-Coder-3B-Instruct/resolve/main/model-00001-of-00002.safetensors"; 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 MODEL_3B_PART2_URL: &str = "https://huggingface.co/Qwen/Qwen2.5-Coder-3B-Instruct/resolve/main/model-00002-of-00002.safetensors"; const TOKENIZER_GGUF_URL: &str = "https://huggingface.co/Qwen/Qwen2.5-Coder-1.5B-Instruct/resolve/main/tokenizer.json";
const TOKENIZER_3B_URL: &str = "https://huggingface.co/Qwen/Qwen2.5-Coder-3B-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 { struct CachedModel {
model: QwenModel, model: CoderModel,
tokenizer: tokenizers::Tokenizer, tokenizer: tokenizers::Tokenizer,
is_3b: bool, is_3b: bool,
} }
/// Poistaa mallin tuottaman markdown-wrapperin ja johdantotekstin. /// Tunnetut kielitunnisteet joita malli voi tuottaa prefill-backtickien jälkeen.
/// "Sure! Here is...\n```python\nprint('hi')\n```" → "print('hi')" 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-tekniikan vuoksi malli tuottaa: "rust\nfn main() {...}\n```"
/// eli kielitunniste alussa + sulkeva ``` lopussa. Molemmat poistetaan.
fn strip_markdown_wrapper(text: &str) -> String { fn strip_markdown_wrapper(text: &str) -> String {
let text = text.trim(); let mut result = text.trim().to_string();
// Jos vastaus sisältää ```-koodiblokin, ota vain sen sisältö
if let Some(start) = text.find("```") { // 1. Poistetaan kielitunniste ensimmäiseltä riviltä — VAIN jos se on tunnettu kieli
let after_backticks = &text[start + 3..]; if let Some(first_newline) = result.find('\n') {
// Ohita mahdollinen kielitunniste (```python, ```rust jne.) let first_line = result[..first_newline].trim().to_lowercase();
let code_start = after_backticks.find('\n').map(|i| i + 1).unwrap_or(0); if LANG_TAGS.contains(&first_line.as_str()) {
let code = &after_backticks[code_start..]; result = result[first_newline + 1..].to_string();
// Etsi sulkeva ```
if let Some(end) = code.find("```") {
return code[..end].trim().to_string();
} }
// Ei sulkevaa ``` — ota kaikki loput
return code.trim().to_string();
} }
// Ei koodiblokkia — poista yleiset johdantolauseet ja selityskommentit alusta
let mut result = text.to_string(); // 2. Poistetaan sulkeva ``` VAIN jos se on omalla rivillään lopussa
let lower = result.to_lowercase(); let trimmed = result.trim_end();
if trimmed.ends_with("```") {
let before = &trimmed[..trimmed.len() - 3];
// Varmistetaan: edellinen merkki on rivinvaihto tai alku (eli ``` on oma rivinsä)
if before.is_empty() || before.ends_with('\n') {
result = before.trim_end().to_string();
}
}
// 3. Poistetaan johdantolauseet: "Sure! Here is...", "Certainly!" jne.
let lower = result.trim().to_lowercase();
for prefix in &["sure!", "here is", "here's", "certainly!", "below is"] { for prefix in &["sure!", "here is", "here's", "certainly!", "below is"] {
if lower.starts_with(prefix) { if lower.starts_with(prefix) {
if let Some(newline) = result.find('\n') { if let Some(newline) = result.find('\n') {
@@ -55,12 +96,12 @@ fn strip_markdown_wrapper(text: &str) -> String {
break; break;
} }
} }
// Poistetaan alun selityskommentit: "# This is a simple..." -tyyppiset rivit
// jotka eivät ole osa varsinaista koodia (esim. shebangia #! pidetään) // 4. Poistetaan selityskommentit alusta: "# This is a simple program..."
let mut lines: Vec<&str> = result.trim().lines().collect(); let mut lines: Vec<&str> = result.trim().lines().collect();
while !lines.is_empty() { while !lines.is_empty() {
let first = lines[0].trim(); let first = lines[0].trim();
let is_preamble_comment = first.starts_with("# ") let is_preamble = first.starts_with("# ")
&& !first.starts_with("#!") && !first.starts_with("#!")
&& (first.to_lowercase().contains("this is") && (first.to_lowercase().contains("this is")
|| first.to_lowercase().contains("simple") || first.to_lowercase().contains("simple")
@@ -68,12 +109,9 @@ fn strip_markdown_wrapper(text: &str) -> String {
|| first.to_lowercase().contains("here is") || first.to_lowercase().contains("here is")
|| first.to_lowercase().contains("the following") || first.to_lowercase().contains("the following")
|| first.to_lowercase().contains("below")); || first.to_lowercase().contains("below"));
if is_preamble_comment { if is_preamble { lines.remove(0); } else { break; }
lines.remove(0);
} else {
break;
}
} }
lines.join("\n").trim().to_string() lines.join("\n").trim().to_string()
} }
@@ -102,10 +140,7 @@ async fn ensure_cached(key: &str, url: &str, ws: &Rc<RefCell<WebSocket>>) -> Res
console_log!("[Coder] Ladataan {}...", key); console_log!("[Coder] Ladataan {}...", key);
let window = web_sys::window().unwrap(); let resp = crate::worker_fetch(url).await?;
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())?;
if !resp.ok() { return Err(format!("HTTP {}", resp.status())); } if !resp.ok() { return Err(format!("HTTP {}", resp.status())); }
let total_size: usize = resp.headers() let total_size: usize = resp.headers()
@@ -169,50 +204,39 @@ async fn get_or_build_model(use_3b: bool, ws: &Rc<RefCell<WebSocket>>) -> Result
let dtype = DType::F32; let dtype = DType::F32;
// Tokenizer // Tokenizer
let tok_url = if use_3b { TOKENIZER_3B_URL } else { TOKENIZER_05B_URL }; let tok_url = if use_3b { TOKENIZER_GGUF_URL } else { TOKENIZER_05B_URL };
let tok_key = if use_3b { "coder3b-tokenizer.json" } else { "coder05b-tokenizer.json" }; 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 tok_bytes = ensure_cached(tok_key, tok_url, ws).await?;
let tokenizer = tokenizers::Tokenizer::from_bytes(&tok_bytes[..]) let tokenizer = tokenizers::Tokenizer::from_bytes(&tok_bytes[..])
.map_err(|e| format!("Tokenizer: {}", e))?; .map_err(|e| format!("Tokenizer: {}", e))?;
// Painot // Painot
let tensors = if use_3b { let model = if use_3b {
let part1 = ensure_cached("coder3b-model-part1.safetensors", MODEL_3B_PART1_URL, ws).await?; // GGUF Q4_K_M — kvantisoidtu 3B-malli (~1.9 GB)
let part2 = ensure_cached("coder3b-model-part2.safetensors", MODEL_3B_PART2_URL, ws).await?; let gguf_bytes = ensure_cached("coder15b-q4km.gguf", MODEL_GGUF_URL, ws).await?;
console_log!("[Coder] Rakennetaan 3B-mallia..."); console_log!("[Coder] Rakennetaan kvantisoidun 1.5B-mallia (Q4_K_M)...");
let mut all_tensors = candle_core::safetensors::load_buffer(&part1[..], &device) let mut cursor = std::io::Cursor::new(&gguf_bytes[..]);
.map_err(|e| format!("Part1: {}", e))?; let content = gguf_file::Content::read(&mut cursor)
let tensors2 = candle_core::safetensors::load_buffer(&part2[..], &device) .map_err(|e| format!("GGUF parse: {}", e))?;
.map_err(|e| format!("Part2: {}", e))?; let qmodel = QwenQuantizedModel::from_gguf(content, &mut cursor, &device)
all_tensors.extend(tensors2); .map_err(|e| format!("GGUF model: {}", e))?;
all_tensors CoderModel::Quantized(qmodel)
} else { } else {
let model_bytes = ensure_cached("coder05b-model.safetensors", MODEL_05B_URL, ws).await?; let model_bytes = ensure_cached("coder05b-model.safetensors", MODEL_05B_URL, ws).await?;
console_log!("[Coder] Rakennetaan 0.5B-mallia..."); console_log!("[Coder] Rakennetaan 0.5B-mallia...");
candle_core::safetensors::load_buffer(&model_bytes[..], &device) let tensors = candle_core::safetensors::load_buffer(&model_bytes[..], &device)
.map_err(|e| format!("Safetensors: {}", e))? .map_err(|e| format!("Safetensors: {}", e))?;
}; let config = QwenConfig {
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 {
vocab_size: 151936, hidden_size: 896, intermediate_size: 4864, vocab_size: 151936, hidden_size: 896, intermediate_size: 4864,
num_hidden_layers: 24, num_attention_heads: 14, num_key_value_heads: 2, num_hidden_layers: 24, num_attention_heads: 14, num_key_value_heads: 2,
max_position_embeddings: 32768, sliding_window: 32768, max_window_layers: 21, max_position_embeddings: 32768, sliding_window: 32768, max_window_layers: 21,
tie_word_embeddings: true, rope_theta: 1000000.0, rms_norm_eps: 1e-6, tie_word_embeddings: true, rope_theta: 1000000.0, rms_norm_eps: 1e-6,
use_sliding_window: false, hidden_act: candle_nn::Activation::Silu, 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"); console_log!("[Coder] Malli ladattu ja välimuistitettu");
MODEL_CACHE.with(|c| { MODEL_CACHE.with(|c| {
@@ -224,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) /// 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>) { 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 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 { if let Err(e) = get_or_build_model(use_3b, &ws).await {
console_log!("[Coder] Mallin lataus: {}", e); console_log!("[Coder] Mallin lataus: {}", e);
return; return;
} }
let load_time = perf.now() - start_load; let load_time = crate::perf_now() - start_load;
if load_time > 100.0 { if load_time > 100.0 {
console_log!("[Coder] Malli ladattu ({:.0}ms). Generoidaan...", load_time); console_log!("[Coder] Malli ladattu ({:.0}ms). Generoidaan...", load_time);
} }
@@ -245,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) { 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 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 s = json.get("system").and_then(|v| v.as_str()).unwrap_or(default_system).to_string();
let m = json.get("max_tokens").and_then(|v| v.as_u64()).unwrap_or(128) as usize; let m = json.get("max_tokens").and_then(|v| v.as_u64()).unwrap_or(512) as usize;
(p, s, m) (p, s, m)
} else { } else {
(prompt.clone(), default_system.to_string(), 128) (prompt.clone(), default_system.to_string(), 512)
} }
} else { } else {
(prompt.clone(), default_system.to_string(), 128) (prompt.clone(), default_system.to_string(), 512)
}; };
// Prefill: aloitetaan vastaus ```-koodiblokkilla, jolloin malli jatkaa suoraan koodilla // Prefill: aloitetaan vastaus ```-koodiblokkilla, jolloin malli jatkaa suoraan koodilla
@@ -270,7 +293,7 @@ pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use
console_log!("[Coder] Syöte: {} tokenia", input_len); console_log!("[Coder] Syöte: {} tokenia", input_len);
let device = Device::Cpu; let device = Device::Cpu;
let start_gen = perf.now(); let start_gen = crate::perf_now();
let eos_token = 151645u32; let eos_token = 151645u32;
let temperature: f32 = 0.7; let temperature: f32 = 0.7;
let top_k: usize = 40; let top_k: usize = 40;
@@ -329,8 +352,8 @@ pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use
// Stop-sekvenssit: katkaistaan kun malli alkaa selittää // Stop-sekvenssit: katkaistaan kun malli alkaa selittää
let lower = generated_text.to_lowercase(); let lower = generated_text.to_lowercase();
if lower.contains("\n###") || lower.contains("\nexplanation") || lower.contains("\nnote:") || lower.contains("\noutput:") || lower.contains("\n```\n\n") { if lower.contains("\n###") || lower.contains("\nexplanation") || lower.contains("\nnote:") || lower.contains("\noutput:") || lower.contains("\n```\n\n") || lower.contains("\n// example") || lower.contains("\n# example") {
for stop in &["\n###", "\nExplanation", "\nNote:", "\nOutput:", "\n```\n\n"] { 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) { if let Some(pos) = generated_text.find(stop) {
generated_text.truncate(pos); generated_text.truncate(pos);
} }
@@ -346,7 +369,7 @@ pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use
tokens_generated += 1; 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 // Siivotaan vastaus: poista markdown-koodiblokit ja johdantotekstit
let cleaned = strip_markdown_wrapper(&generated_text); 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); send_progress(ws, key, 0, 0, 0);
// Fetch API:lla saadaan Content-Length ja streaming-luku // Fetch API:lla saadaan Content-Length ja streaming-luku
let window = web_sys::window().unwrap(); let resp = crate::worker_fetch(url).await?;
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())?;
if !resp.ok() { if !resp.ok() {
return Err(format!("HTTP {}", resp.status())); 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 /// Lataa malli ja tokenizer, suorita inferenssi ja streamaa tokenit hubille
pub async fn run_smollm_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) { 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 // 1. Lataa tokenizer
let tok_bytes = match ensure_cached("smollm-tokenizer.json", TOKENIZER_URL, &ws).await { 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) // Burn 0.21-pre.2 cubecl-runtime ei käänny Wasmille (println! puuttuu)
// → NdArray kunnes Burn 0.21 stable + Wasm-tuki // → NdArray kunnes Burn 0.21 stable + Wasm-tuki
console_log!("[SmolLM] Burn NdArray (CPU) inferenssi..."); 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>( 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>, model_bytes: Vec<u8>,
tokenizer: tokenizers::Tokenizer, tokenizer: tokenizers::Tokenizer,
ws: Rc<RefCell<WebSocket>>, 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 device = Default::default();
let config = crate::burn_smollm::config::SmolLMConfig::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; } 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); 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); 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(); let input_len = input_ids.len();
console_log!("[SmolLM] Syöte: {} tokenia", input_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 max_new_tokens = 32;
let mut generated_text = String::new(); let mut generated_text = String::new();
let mut tokens_generated: usize = 0; let mut tokens_generated: usize = 0;
@@ -219,7 +215,7 @@ async fn run_burn_inference<B: burn::tensor::backend::Backend>(
tokens_generated += 1; 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 tokens_per_sec = if gen_time > 0.0 { (tokens_generated as f64 / gen_time) * 1000.0 } else { 0.0 };
let done = serde_json::json!({ let done = serde_json::json!({

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

@@ -0,0 +1,413 @@
# 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: suomi vs. englanti
Alla oikea tokenisointitulos Qwen2.5-Coder-tokenisaattorilla. Jokainen
värikoodattu lohko on yksi tokeni — huomaa miten suomi vaatii enemmän
tokeneita saman merkityksen välittämiseen:
![Tokenisointivertailu EN/FI](/images/tokenization-example.png)
**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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

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);
}
};