Compare commits
337 Commits
02f6684378
...
projekti1
| Author | SHA1 | Date | |
|---|---|---|---|
| 20cea8f268 | |||
| 38a18c555b | |||
| 8138e41aa1 | |||
| 6ee5bdf960 | |||
| cf3bf54bf8 | |||
| 56f21a96c9 | |||
| 763b93396c | |||
| e09962940a | |||
| 5e44b63b0c | |||
| 0f3881aa02 | |||
| fa85dcc5b3 | |||
| 58d93613f0 | |||
| 66b4435362 | |||
| 3a00de9b8e | |||
| 670141c8c3 | |||
| 59daebbd38 | |||
| 42b71dbf77 | |||
| b88a741f85 | |||
|
|
68c7195d54 | ||
|
|
3d20238eef | ||
|
|
8b8ba01af3 | ||
|
|
a3b95a56e8 | ||
|
|
5b20ebe800 | ||
|
|
ffe9bd6902 | ||
|
|
d27068b11a | ||
|
|
8468724a4c | ||
|
|
6ef71b7e5c | ||
|
|
b2ee8b9031 | ||
|
|
c1a5f8aff5 | ||
|
|
8ee997cb56 | ||
|
|
cd67562a67 | ||
|
|
1f85c03624 | ||
|
|
74a2045def | ||
|
|
9b2b7767b5 | ||
|
|
1718805978 | ||
|
|
7fcc97f525 | ||
|
|
7ce990b42a | ||
|
|
dc71829430 | ||
|
|
5d4a553520 | ||
|
|
5e82c798b1 | ||
|
|
5f147b774f | ||
|
|
4983217ee0 | ||
|
|
27c33e41c3 | ||
|
|
2b33980be4 | ||
|
|
8995bcef30 | ||
|
|
2f140c8a15 | ||
|
|
094b183c17 | ||
|
|
a91b9539b3 | ||
|
|
6e2f85daa8 | ||
|
|
466e61d730 | ||
|
|
5f00582053 | ||
|
|
e272b0d124 | ||
|
|
d3affb3a09 | ||
|
|
1377e72f78 | ||
|
|
403f35efdc | ||
|
|
ce0ccbddd3 | ||
|
|
80806498e0 | ||
|
|
660e80c2bc | ||
|
|
591cfcb04b | ||
|
|
3cda57f0bc | ||
|
|
23e7b92d03 | ||
|
|
9f58febe21 | ||
|
|
b1de0d37f7 | ||
|
|
4ff626ab88 | ||
|
|
a45616046d | ||
|
|
ee048b0b68 | ||
|
|
4e83569194 | ||
|
|
f42b692eeb | ||
|
|
f79bb16f3d | ||
|
|
e81fc33faf | ||
|
|
433726c553 | ||
|
|
dec2e24e2f | ||
|
|
9058033669 | ||
|
|
8bd86e6325 | ||
|
|
c1133bb075 | ||
|
|
6502d75efc | ||
|
|
9f8b7fe920 | ||
|
|
746bc20fcb | ||
|
|
93f6baa0ea | ||
|
|
cc8e871735 | ||
|
|
e90f3460c3 | ||
|
|
4d74c38618 | ||
|
|
8a1b204179 | ||
|
|
b19f5a3518 | ||
|
|
38dc36e846 | ||
|
|
4fe6931b5f | ||
|
|
b8e8a83e49 | ||
|
|
3d6914974d | ||
|
|
9aff2ec154 | ||
|
|
ecd4525a7f | ||
|
|
7a3e5278b9 | ||
|
|
8dcf269b42 | ||
|
|
cb16f35265 | ||
|
|
b9d340b4b4 | ||
|
|
dd07e536f0 | ||
|
|
9af481a022 | ||
|
|
529a30a6e1 | ||
|
|
7d842529b1 | ||
|
|
c731c18360 | ||
|
|
5498eb6cbb | ||
|
|
43f0aebf54 | ||
|
|
6413f0238f | ||
|
|
6b0394586e | ||
|
|
108094b06a | ||
|
|
d7c974792d | ||
|
|
1987eb57a0 | ||
|
|
12ca87415c | ||
|
|
a0e52faa44 | ||
|
|
f910cd8c61 | ||
|
|
91dc7579bc | ||
|
|
90c9a7e4fa | ||
|
|
1216e016c2 | ||
|
|
d85cab4bc0 | ||
|
|
4fef8824e1 | ||
|
|
009bf492c8 | ||
|
|
f7e0e8dff8 | ||
|
|
eb57ee7b92 | ||
|
|
84d13153ed | ||
|
|
8beac57b50 | ||
|
|
44067efdb6 | ||
|
|
5528be1812 | ||
|
|
f4cf4c73b9 | ||
|
|
e19852a509 | ||
|
|
6de0df365e | ||
|
|
28f620f901 | ||
|
|
3497f66db7 | ||
|
|
1c7362c9b0 | ||
|
|
9983c80ef1 | ||
|
|
fc1fb33d5e | ||
|
|
3bee8e8020 | ||
|
|
f8ea5ed76e | ||
|
|
6c7c2d6dd3 | ||
|
|
c179b4ab7e | ||
|
|
a8c4af0975 | ||
|
|
e3fdb91ac5 | ||
|
|
9925079729 | ||
|
|
6031737f83 | ||
|
|
b6a8fa2671 | ||
|
|
0dc53dba1c | ||
|
|
857afbe111 | ||
|
|
84b78eb9c6 | ||
|
|
4f18377a3b | ||
|
|
7f5bb45138 | ||
| 973d7a69c7 | |||
| aebc64e76e | |||
| 48c832c61b | |||
| 8435bd32a9 | |||
| ece41dd622 | |||
| c7f3b0d79f | |||
| 8905b50f41 | |||
| 43b0612004 | |||
| 599ac2d2d9 | |||
| d1975bd55c | |||
| 24a8139d3e | |||
| 21aac49a52 | |||
| 8a5f1b753c | |||
| 1b0b5eb198 | |||
| 44c8a189b6 | |||
| 1a58324689 | |||
| afc7f9bcee | |||
| 5d2027b2ca | |||
| 8a4d515eed | |||
| 987a370a05 | |||
| f75e7f07e9 | |||
| eb6f720fcc | |||
| e25d0ea8f2 | |||
| 4d1e89da34 | |||
| bf535b6256 | |||
| 29e1c440c6 | |||
| 560bee1369 | |||
| b074e0cb49 | |||
| 9307c75516 | |||
| 86191fbb6c | |||
| a6a94f7688 | |||
| 8d5c5440d2 | |||
| a12bd7ce7f | |||
| 9ac90aa540 | |||
| 32065d5818 | |||
| 321943ee3c | |||
| 1b75c89320 | |||
| 01622a960f | |||
| 4e4efda67d | |||
| f5db2eb034 | |||
| 77c8d46e7b | |||
| f14eba1b49 | |||
| 6d15298418 | |||
| cea1961183 | |||
| 21a8015ea3 | |||
| c3991193d9 | |||
| 02c6d67218 | |||
| de1cf009fa | |||
| 060f36f479 | |||
| e2ec0fa43d | |||
| 8752c0f465 | |||
| 8c95282654 | |||
| a1bc1af646 | |||
| 6b27cbbade | |||
| 4d9c51a86f | |||
| 66d1e8c4b1 | |||
| 2eeac255f6 | |||
| 6097cfc263 | |||
| 8aed9f97a2 | |||
| c0ccd76a4c | |||
| d2edb38879 | |||
| 2755794554 | |||
| dbb37b3c60 | |||
| 0e7497b627 | |||
| 6b756e2e83 | |||
| 5a52f5113c | |||
| 7b0660e46e | |||
| b35600b417 | |||
| 7693269e5d | |||
| 702c9170ad | |||
| 3feed22055 | |||
| 75310c989e | |||
| 743946a391 | |||
| 0bd5faa684 | |||
| e0c8c3586b | |||
| 3a1c5c723c | |||
| 3139d1ac65 | |||
| 49a1629646 | |||
| 13008ac693 | |||
| 30e81875db | |||
| 73bcd3143a | |||
| 216b95d15c | |||
| 34ef19472a | |||
| 54a5af96c7 | |||
| 842153a7ec | |||
| 5c25c7f9c1 | |||
| ac698a766e | |||
| f1b57a6c53 | |||
| b70cdbd24d | |||
| 01d8b597e1 | |||
| f2ca4890df | |||
| 3eb0c4d939 | |||
| d8443792a3 | |||
| ae379bdda4 | |||
| ed02e47158 | |||
| 959dc532bb | |||
| 1ef7f7c956 | |||
| e6e1f60935 | |||
| 322c98ff59 | |||
| 406e2226f0 | |||
| 9d7496157c | |||
| d332b7e910 | |||
| 8e55a15d66 | |||
| 4e3134d908 | |||
| cd45db001a | |||
| 4ad8a8793e | |||
| b2694c232e | |||
| ba58236c52 | |||
| 861f2a6902 | |||
| 11fd5b0c9e | |||
| b3646ae5d3 | |||
| fc95cf8c1b | |||
| 1ae1bf98e2 | |||
| f567fd3f8a | |||
| 38367eac97 | |||
| 20716186bc | |||
| 4e810ed4a2 | |||
| 91ff9e00f9 | |||
| e652bf7ab6 | |||
| eb69893124 | |||
| d18314bfc8 | |||
| 99b011e399 | |||
|
|
3976bb6251 | ||
|
|
0c32fecdc4 | ||
|
|
801cc0371d | ||
|
|
176f2d6915 | ||
|
|
dd1945ab28 | ||
|
|
262fee3b49 | ||
|
|
aa7540a6bf | ||
|
|
762066102a | ||
|
|
bef5b6fc3c | ||
|
|
095b72d2d6 | ||
|
|
4cb6128a27 | ||
|
|
4dff534fbf | ||
|
|
d5ab6272d3 | ||
|
|
2e7b86deeb | ||
|
|
a6e49870d6 | ||
|
|
d68882249e | ||
|
|
6a587cd080 | ||
|
|
f17fcf0f9d | ||
|
|
ac15336c9f | ||
|
|
7a15cacebf | ||
|
|
27135a8f14 | ||
|
|
e28a715f32 | ||
|
|
24d29d9ba9 | ||
|
|
7eca426e77 | ||
|
|
7a1352ead7 | ||
|
|
b9017448d8 | ||
|
|
3d1b406e8d | ||
|
|
aa6c4739dd | ||
|
|
cbbf427a93 | ||
|
|
0a216f19e2 | ||
|
|
a2e7ed53ff | ||
|
|
950cae9d96 | ||
|
|
ff3a720b8d | ||
|
|
6f14614af8 | ||
|
|
518c6dc5cb | ||
|
|
b48eeb6f5f | ||
|
|
6bc7d03676 | ||
|
|
13b2911d38 | ||
|
|
38054452e2 | ||
|
|
50ff34cb09 | ||
|
|
949f34833f | ||
|
|
88fd31ca8c | ||
|
|
57c6506f91 | ||
|
|
12fd3c4eae | ||
|
|
1845ddf8c2 | ||
|
|
0ef1a3f7cd | ||
|
|
4e49cfbbfa | ||
|
|
133ff38fa4 | ||
|
|
3ada8949d0 | ||
| 2e7ddf6f1e | |||
| f266c121c2 | |||
| f83ea9d90a | |||
| 2ea24c6248 | |||
| 2d065ee0bf | |||
| 3d2b2342e0 | |||
| 2a242efbd8 | |||
| f687351f69 | |||
| 0d3e0ff86a | |||
| 057d464fdd | |||
| 185a40dbdf | |||
| 2ad20bdc62 | |||
| 31995fb278 | |||
| 6cdd695a3b | |||
| 92c952c07a | |||
| e1326b145e | |||
| d2920e5ab4 | |||
| 8e20b06344 | |||
| 11bd802be5 | |||
| b693542116 | |||
| e55ff64565 | |||
| f5e4883915 | |||
| 9a72d35081 |
13
.gitignore
vendored
@@ -21,6 +21,7 @@
|
||||
# will have compiled files and executables
|
||||
debug/
|
||||
target/
|
||||
target-native/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
@@ -33,3 +34,15 @@ Cargo.lock
|
||||
*.pdb
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/rust,linux
|
||||
|
||||
# Ajonaikaiset tietokannat
|
||||
*.db
|
||||
|
||||
# Lokitiedostot
|
||||
*.log
|
||||
|
||||
# Wanha versio
|
||||
temp/
|
||||
|
||||
# Muut
|
||||
zipit/**
|
||||
157
TEMPLATING.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# Templating — rakennuspalaset koodigeneroinnissa
|
||||
|
||||
## Perusperiaate
|
||||
|
||||
Kielimalli päättää **mitä** rakennetaan (entiteetit, kentät, tyypit, yhteydet).
|
||||
Template-funktiot päättävät **miten** se rakennetaan (importit, engine setup, testikonfiguraatio).
|
||||
|
||||
```
|
||||
Projektikuvaus → LLM → JSON-speksi → Templateit → Koodi → Validointi
|
||||
```
|
||||
|
||||
LLM:n kontribuutio on yksi JSON-rakenne. Kaikki muu on determinististä —
|
||||
sama speksi tuottaa aina saman koodin.
|
||||
|
||||
## Miksi tämä toimii
|
||||
|
||||
Pienen kielimallin (0.5B–7B) vahvuudet ja heikkoudet ovat epäsymmetrisiä:
|
||||
|
||||
| Tehtävä | LLM:n kyky | Ratkaisu |
|
||||
|---------|-----------|----------|
|
||||
| Tunnista entiteetit kuvauksesta | Hyvä | LLM tekee |
|
||||
| Valitse kenttätyypit | Hyvä | LLM tekee |
|
||||
| Muista importit oikein | Huono | Template tekee |
|
||||
| SQLite connect_args | Huono | Template tekee |
|
||||
| Testikonfiguraatio | Huono | Template tekee |
|
||||
| Dockerfile-rakenne | Huono | Template tekee |
|
||||
|
||||
Annetaan mallin tehdä se missä se on hyvä. Hoidetaan loput mekaanisesti.
|
||||
|
||||
## JSON-speksi
|
||||
|
||||
Kielimallin ainoa tuotos on JSON joka kuvaa projektin rakenteen:
|
||||
|
||||
```json
|
||||
{
|
||||
"project_name": "library-app",
|
||||
"entities": [
|
||||
{
|
||||
"name": "Author",
|
||||
"table_name": "authors",
|
||||
"fields": [
|
||||
{"name": "name", "sa_type": "String(255)", "py_type": "str", "nullable": false, "default": null}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Book",
|
||||
"table_name": "books",
|
||||
"fields": [
|
||||
{"name": "title", "sa_type": "String(255)", "py_type": "str", "nullable": false, "default": null},
|
||||
{"name": "author_id", "sa_type": "Integer", "py_type": "int", "nullable": false, "default": null}
|
||||
]
|
||||
}
|
||||
],
|
||||
"relationships": [
|
||||
{"from": "Book", "field": "author_id", "to": "Author", "type": "many-to-one"}
|
||||
],
|
||||
"extra_imports": []
|
||||
}
|
||||
```
|
||||
|
||||
Speksin laatu ratkaisee kaiken. Hyvä speksi → hyvä projekti. Huono speksi →
|
||||
teknisesti toimiva mutta sisällöllisesti väärä projekti.
|
||||
|
||||
## Architect-promptin rooli
|
||||
|
||||
Architect-agentti (JSON-speksin generoija) on kriittisin kohta koko pipelinessa.
|
||||
Sitä ohjataan neljällä keinolla:
|
||||
|
||||
1. **Chain-of-thought** — malli miettii ensin entiteetit, sitten kentät,
|
||||
sitten yhteydet, vasta lopuksi JSON
|
||||
2. **Domain-esimerkit** — Todo, verkkokauppa, blogi — malli näkee miltä
|
||||
hyvä speksi näyttää eri domaineissa
|
||||
3. **Anti-patternit** — turhat ID-kentät, Enum-tyypit, suomenkieliset nimet
|
||||
4. **Yhteyssäännöt** — jokainen `_id`-kenttä tarvitsee relationship-merkinnän
|
||||
|
||||
Isompi malli tässä yhdessä kohdassa parantaisi kaikkien projektien laatua.
|
||||
|
||||
## Templateit
|
||||
|
||||
Jokainen template on funktio joka ottaa speksin ja palauttaa koodia:
|
||||
|
||||
```
|
||||
tmplModels(spec) → models.py (SQLAlchemy, ForeignKey, relationship)
|
||||
tmplSchemas(spec) → schemas.py (Pydantic Create/Response/Detail)
|
||||
tmplMain(spec) → main.py (FastAPI CRUD + nested endpoints + FK-validointi)
|
||||
tmplTests(spec) → test_main.py (pytest + TestClient + helper-funktiot)
|
||||
tmplPyproject(spec) → pyproject.toml (PEP 621)
|
||||
tmplDockerfile() → Dockerfile (uv + non-root user)
|
||||
```
|
||||
|
||||
Templateit generoivat automaattisesti:
|
||||
- ForeignKey-constraintit ja relationship()-määrittelyt
|
||||
- Nested endpointit (`GET /authors/{id}/books/`)
|
||||
- FK-validointi (404 jos parent-entiteettiä ei ole)
|
||||
- Detail-schemat (Book + author-data mukana)
|
||||
- Test-helperit jotka luovat parent-entiteetit ensin
|
||||
- Bad FK -testit (varmistaa että orpo-validointi toimii)
|
||||
|
||||
## Validointi
|
||||
|
||||
Generoitu koodi validoidaan mekaanisesti ennen käyttöä:
|
||||
|
||||
- Syntaksitarkistus (AST parse)
|
||||
- Projektin sisäiset importit (löytyykö nimi lähdetiedostosta)
|
||||
- SQLite connect_args
|
||||
- Relatiiviset importit (kielletty)
|
||||
- Testien rakenne (ei saa kopioida appia)
|
||||
- pyproject.toml (ei poetryä)
|
||||
- Dockerfile (ei poetryä, uv cache -oikeudet)
|
||||
|
||||
Docker-testi ajaa koko projektin: build → pytest → API smoke test.
|
||||
|
||||
## Rajoitukset
|
||||
|
||||
Templateit kattavat rakenteellisesti tunnetut projektit:
|
||||
|
||||
| Stack | Kattavuus |
|
||||
|-------|-----------|
|
||||
| FastAPI + SQLAlchemy CRUD | Toimii hyvin |
|
||||
| Streamlit + DuckDB dashboard | Toimii hyvin |
|
||||
| Muu | Ei templatea → ei toimi |
|
||||
|
||||
**Ei kata:**
|
||||
- Custom business-logiikka (algoritmit, laskenta, ML)
|
||||
- Epätyypilliset arkkitehtuurit (WebSocket, graafit, tapahtumapohjaiset)
|
||||
- Frontend-sovellukset (React, Vue)
|
||||
- Mikä tahansa mitä template ei tunne
|
||||
|
||||
Arvio: templateit kattavat ~20% kaikista mahdollisista projekteista, mutta juuri
|
||||
sen 20% mitä opiskelu- ja prototyyppiympäristöissä tarvitaan useimmin.
|
||||
|
||||
## Laajentaminen
|
||||
|
||||
Uuden stackin lisääminen vaatii:
|
||||
|
||||
1. Uudet template-funktiot (käsityö, ~200–400 riviä per stack)
|
||||
2. JSON-speksin laajennos (uudet kentät jos tarvitaan)
|
||||
3. Validointisäännöt uudelle stackille
|
||||
4. Docker-testikonfiguraatio
|
||||
|
||||
Jokainen template on staattinen — se ei opi eikä sopeudu. Kattavuus kasvaa
|
||||
vain kirjoittamalla lisää templateja.
|
||||
|
||||
## Hybridi: seuraava askel
|
||||
|
||||
Paras lopputulos syntyisi yhdistelmällä:
|
||||
|
||||
```
|
||||
Speksi → Template (runko) → LLM (business-logiikka) → Validointi
|
||||
```
|
||||
|
||||
Template tuottaa toimivan CRUD-pohjan. LLM lisää domain-kohtaisen logiikan
|
||||
pienissä palasissa (yksi funktio kerrallaan). Mekaaninen validointi
|
||||
tarkistaa jokaisen lisäyksen.
|
||||
|
||||
Tämä palauttaa LLM:n epäluotettavuuden takaisin peliin, mutta rajattuna:
|
||||
virheet ovat paikallisia (yksi funktio) eivätkä rakenteellisia (koko projekti).
|
||||
42
TODO.md
@@ -1 +1,41 @@
|
||||
Lisää viesteihin tietoturvallinen kryptaus - mitään selkokielistä ei ole hyvä lähettää.
|
||||
# Kipinä Agentic Network: TODO-lista
|
||||
|
||||
- [x] **Tietoturva & yksityisyys:** Lisää viesteihin tietoturvallinen kryptaus (E2E-salaus / Blind Orchestrator). Mitään selkokielistä ei ole hyvä lähettää vieraalle solmulle.
|
||||
- [x] **Reititysarkkitehtuuri:** Hubin kohdennettu reititys. Broadcastin sijaan tehtävät ohjataan vain parhaalle vapana olevalle solmulle (Node Registry & Matchmaking) tehtävän tyypin ja resurssien perusteella.
|
||||
- [x] **P2P-jakelu:** WebRTC Data Channels mallipainojen jakamiseen suoraan solmujen välillä kaistan ja latausaikojen säästämiseksi.
|
||||
- [x] **Tulosten varmentaminen:** Proof of Compute / Konsensus-mekanismi, jossa sama tehtävä annetaan kahdelle solmulle, ja tila hyväksytään vasta kun ristiintarkastus täsmää.
|
||||
- [x] **Optimaalinen laitekiihdytys:** Selainpuolen laajennus tulevaa WebNN-standardia (NPU API) varten WebGPU:n rinnalle.
|
||||
- [x] **Insentiivit:** Gamifikaatio, pistetaulukko tai token-talous (esim. Kipinä Tokens), joka motivoi käyttäjiä tarjoamaan laitteensa laskentatehoa verkoston käyttöön pidemmäksi aikaa.
|
||||
- [x] **Pelimerkkien UI-synkkaus:** Pelimerkkien saldon synkronointi reaaliajassa Hubista takaisin valikossa olevalle selainsolmulle ja luvun visuaalinen näyttäminen.
|
||||
- [x] **XSS-suojaus:** HTML-escape kaikelle backend-datalle joka renderöidään DOM:iin (prompt, response, tokenisaatiotekstit).
|
||||
- [x] **System prompt -vuoto:** Agents-pipelinen system prompt ei enää näy käyttäjälle vastauksissa.
|
||||
- [x] **Token-saldon data race:** Korjattu atomiseksi operaatioksi.
|
||||
- [x] **UTF-8 slicing panic:** Korjattu kaikki `&text[..n]` → `text.chars().take(n)`.
|
||||
- [x] **Tensor dim unwrap:** Lisätty virheenkäsittely tyhjälle tensorille natiivisolmussa.
|
||||
- [x] **llm_error-viestien tuki:** Lisätty hubiin ja frontendiin, streaming-kortti siivoutuu virhetilanteessa.
|
||||
- [x] **Malli-cache (selain):** QwenModel pidetään muistissa `thread_local! MODEL_CACHE`:ssa, `clear_kv_cache()` promptien välillä.
|
||||
- [x] **Malli-cache (natiivi):** `LlmEngine` pitää mallin muistissa, `fresh_model()` poistettu.
|
||||
- [x] **Sampling:** Greedy argmax korvattu temperature + top-k + repetition penalty -samplingillä (sekä selain että natiivi).
|
||||
- [x] **Stop-sekvenssit:** Generointi katkaistaan kun malli alkaa tuottaa selityksiä.
|
||||
- [x] **Codelab/Agents-reititys:** `llm_done` ja `llm_chunk` reitittyy `task_id`:n perusteella oikeaan näkymään.
|
||||
- [x] **Broadcast Lag:** `RecvError::Lagged` käsitellään gracefully sekä sender-taskissa että API-endpointissa — solmu ei enää tipu verkosta.
|
||||
- [x] **Busy-tila reititys:** Hub seuraa solmujen busy-tilaa (`node_busy`). Tehtäviä ei enää reititetä varatuille solmuille.
|
||||
- [x] **Rate limiting:** `/api/v1/chat/completions` rajoittaa max 10 pyyntöä/minuutti per IP.
|
||||
- [x] **Gamification-validointi:** Kipinä-merkkejä jaetaan vain tehtävistä joiden `task_id` on hubin jakama (`pending_task_ids`).
|
||||
- [x] **Base64:** Oma base64-dekooderi korvattu `base64`-cratella.
|
||||
- [x] **Atominen siivous:** Solmun disconnect-siivouksessa kaikki lukot otetaan kerralla.
|
||||
- [x] **DOM-vuoto:** Terminaalin trim ei enää poista aktiivista streaming-riviä.
|
||||
|
||||
## Havaitut Bugaavat Ominaisuudet ja Arkkitehtuuriongelmat
|
||||
|
||||
### Keskitaso (eivät estä käyttöä)
|
||||
|
||||
- [ ] **Origin-headerin validoinnin ohitus:** Natiivisolmut eivät lähetä Origin-headeria, joten tarkistus ohitetaan. Hyökkääjä voi esiintyä natiivisolmuna. Korjaus: vaadi autentikaatio natiivisolmuilta (API-avain tai token).
|
||||
- [ ] **Kovakoodattu oletussalasana:** Admin-paneelin oletussalasana on `"kipina"` jos `ADMIN_PASSWORD`-ympäristömuuttujaa ei aseta. Tuotannossa pitää asettaa pakollisesti. Varoitus logitetaan.
|
||||
|
||||
### Arkkitehtuuriparannukset (tulevaisuus)
|
||||
|
||||
- [ ] **E2E-salaus:** Promptit ja vastaukset kulkevat selkokielisinä WebSocketin yli. Placeholder-kommentti koodissa, mutta ei toteutusta.
|
||||
- [ ] **Proof of Work / konsensus:** Solmu voi lähettää väärennettyjä tuloksia. Merkitty TODO:ksi, mutta ei toteutusta.
|
||||
- [ ] **WebGPU-inferenssi Candle-mallille:** Selainsolmu käyttää aina CPU:ta Candle-inferenssiin. Candle ei vielä tue WebGPU:ta.
|
||||
- [ ] **Streaming yield -optimointi:** Pitkillä generoinneilla (>128 tok) selaimen event loop voi jäätyä hetkeksi koska generointilooppi ajetaan synkronisessa closuressa. Korjaus: pilko generointilooppi eriin ja yield joka N:s token.
|
||||
|
||||
475
docker-errors.log
Normal 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"
|
||||
131
kipina-node
Executable file
@@ -0,0 +1,131 @@
|
||||
#!/bin/bash
|
||||
# Kipinä Node — lataa oikea binääri ja käynnistä
|
||||
set -e
|
||||
|
||||
BASE_URL="https://kipina.studio/download"
|
||||
HUB_URL="${KIPINA_HUB:-wss://kipina.studio/ws}"
|
||||
OLLAMA_URL="${OLLAMA_URL:-http://localhost:11434}"
|
||||
|
||||
# Tunnista OS ja arkkitehtuuri
|
||||
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
ARCH=$(uname -m)
|
||||
|
||||
case "$OS-$ARCH" in
|
||||
darwin-arm64) BINARY="kipina-node-macos-arm64" ;;
|
||||
darwin-x86_64) BINARY="kipina-node-macos-arm64" ;; # Rosetta
|
||||
linux-x86_64) BINARY="kipina-node-linux-x86_64" ;;
|
||||
linux-aarch64) BINARY="kipina-node-linux-arm64" ;;
|
||||
*) echo "Ei tuettu: $OS-$ARCH"; exit 1 ;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
echo " ╔══════════════════════════════════════╗"
|
||||
echo " ║ Kipinä Agentic Node ║"
|
||||
echo " ╚══════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo " OS: $OS ($ARCH)"
|
||||
echo ""
|
||||
|
||||
# Etsi Ollama-instanssit
|
||||
CANDIDATES=(
|
||||
"http://localhost:11434"
|
||||
"http://127.0.0.1:11434"
|
||||
"http://ollama:11434"
|
||||
"http://host.docker.internal:11434"
|
||||
)
|
||||
|
||||
# Lisää OLLAMA_URL listaan jos asetettu ja ei jo mukana
|
||||
if [ -n "$OLLAMA_URL" ]; then
|
||||
ALREADY=false
|
||||
for c in "${CANDIDATES[@]}"; do
|
||||
[ "$c" = "$OLLAMA_URL" ] && ALREADY=true
|
||||
done
|
||||
$ALREADY || CANDIDATES=("$OLLAMA_URL" "${CANDIDATES[@]}")
|
||||
fi
|
||||
|
||||
echo " Etsitään Ollama-instansseja..."
|
||||
FOUND=()
|
||||
for url in "${CANDIDATES[@]}"; do
|
||||
if curl -s --connect-timeout 1 "$url/api/tags" &>/dev/null; then
|
||||
FOUND+=("$url")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#FOUND[@]} -eq 0 ]; then
|
||||
# Ei löytynyt — yritä käynnistää lokaali
|
||||
if command -v ollama &>/dev/null; then
|
||||
echo " Käynnistetään Ollama..."
|
||||
ollama serve &>/dev/null &
|
||||
sleep 3
|
||||
if curl -s --connect-timeout 1 "http://localhost:11434/api/tags" &>/dev/null; then
|
||||
OLLAMA_URL="http://localhost:11434"
|
||||
echo " ✓ Ollama käynnistetty ($OLLAMA_URL)"
|
||||
else
|
||||
echo " ✗ Ollaman käynnistys epäonnistui."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo ""
|
||||
echo " ✗ Ollamaa ei löytynyt."
|
||||
echo " Kontti/remote: OLLAMA_URL=http://HOST:11434 ./kipina-node"
|
||||
echo " Asenna: curl -fsSL https://ollama.ai/install.sh | sh"
|
||||
exit 1
|
||||
fi
|
||||
elif [ ${#FOUND[@]} -eq 1 ]; then
|
||||
OLLAMA_URL="${FOUND[0]}"
|
||||
echo " ✓ Ollama löytyi: $OLLAMA_URL"
|
||||
else
|
||||
echo ""
|
||||
echo " Löytyi ${#FOUND[@]} Ollama-instanssia:"
|
||||
echo ""
|
||||
for i in "${!FOUND[@]}"; do
|
||||
echo " $((i+1))) ${FOUND[$i]}"
|
||||
done
|
||||
echo ""
|
||||
read -p " Valitse [1-${#FOUND[@]}]: " -r CHOICE
|
||||
if [[ "$CHOICE" =~ ^[0-9]+$ ]] && [ "$CHOICE" -ge 1 ] && [ "$CHOICE" -le ${#FOUND[@]} ]; then
|
||||
OLLAMA_URL="${FOUND[$((CHOICE-1))]}"
|
||||
else
|
||||
OLLAMA_URL="${FOUND[0]}"
|
||||
echo " Käytetään oletusta: $OLLAMA_URL"
|
||||
fi
|
||||
echo " ✓ Valittu: $OLLAMA_URL"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " Hub: $HUB_URL"
|
||||
echo " Ollama: $OLLAMA_URL"
|
||||
if [ -n "$KIPINA_MODEL" ]; then
|
||||
echo " Malli: $KIPINA_MODEL (Ympäristömuuttujasta)"
|
||||
fi
|
||||
|
||||
# Lataa binääri
|
||||
BIN_PATH="./kipina-node-bin"
|
||||
if [ -f "$BIN_PATH" ]; then
|
||||
echo ""
|
||||
read -p " Löydettiin vanha kipina-node-bin lokaalisti. Haluatko poistaa sen ja ladata uusimman version? [Y/n] " -r DEL_CHOICE
|
||||
if [[ "$DEL_CHOICE" =~ ^[Nn]$ ]]; then
|
||||
echo " ✓ Käytetään lokaalia versiota."
|
||||
else
|
||||
rm -f "$BIN_PATH"
|
||||
echo " ✓ Vanha binääri poistettu ja korvataan uudella."
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -f "$BIN_PATH" ]; then
|
||||
echo " Ladataan tuorein $BINARY..."
|
||||
curl -sSL "$BASE_URL/$BINARY" -o "$BIN_PATH"
|
||||
chmod +x "$BIN_PATH"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " ✓ Siirrytään Kipinä Noden hallintaan..."
|
||||
echo " Ctrl+C pysäyttää"
|
||||
echo ""
|
||||
|
||||
if [ -n "$KIPINA_MODEL" ]; then
|
||||
export OLLAMA_MODEL="$KIPINA_MODEL"
|
||||
fi
|
||||
export HUB_URL="$HUB_URL"
|
||||
export OLLAMA_URL="$OLLAMA_URL"
|
||||
exec "$BIN_PATH"
|
||||
BIN
kipina-node-bin
Executable file
215
network-poc/AGENTBUILDER.md
Normal file
@@ -0,0 +1,215 @@
|
||||
# Kipinä Agent Builder — Suunnitelma
|
||||
|
||||
Käyttäjä voi rakentaa omia agentteja "hahmolomakkeella": valitsee avatarin, roolin, kielimallin ja muokkaa prompteja. Agentit tallentuvat localStorageen ja ovat käytettävissä pipelineissa.
|
||||
|
||||
## Nykytila
|
||||
|
||||
```js
|
||||
// Kovakoodattu agentPrompts-objekti
|
||||
const agentPrompts = {
|
||||
manager: { name: 'Manageri', model: 'qwen2.5-coder:7b', default: '...' },
|
||||
coder: { name: 'Koodari', model: 'qwen2.5-coder:7b', default: '...' },
|
||||
tofuist: { name: 'Tofuist', model: 'qwen2.5-coder:7b', docs: '/docs/tofu-cheatsheet.md', default: '...' },
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
**Ongelma:** Uuden agentin lisääminen vaatii koodimuutoksen index.html:ään.
|
||||
|
||||
## Tavoite
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Agent Builder -lomake │
|
||||
│ │
|
||||
│ ┌─────────┐ Nimi: [Tofuist ] │
|
||||
│ │ 🦎 │ Rooli: [IaC / Infra ▼] │
|
||||
│ │ avatar │ Malli: [qwen2.5-coder:7b ▼] │
|
||||
│ └─────────┘ Docs: [/docs/tofu-cheatsheet.md] │
|
||||
│ │
|
||||
│ System Prompt: │
|
||||
│ ┌─────────────────────────────────────────────┐ │
|
||||
│ │ You are an OpenTofu/Terraform IaC specialist│ │
|
||||
│ │ ... │ │
|
||||
│ └─────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ LLM-parametrit: │
|
||||
│ Temperature: [0.7] Top-k: [40] Max tokens: [512]│
|
||||
│ │
|
||||
│ [💾 Tallenna] [🗑️ Poista] [📤 Export JSON] │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Building Blocks
|
||||
|
||||
### 1. Agenttiskeema
|
||||
|
||||
```js
|
||||
{
|
||||
id: 'tofuist', // uniikki tunniste
|
||||
name: 'Tofuist', // näyttönimi
|
||||
avatar: '/avatars/gecko_notext.png', // avatar-kuvan polku
|
||||
role: 'iac', // rooli-template
|
||||
model: 'qwen2.5-coder:7b', // eksakti Ollama-mallinimi
|
||||
color: '#e3a336', // teemaväri UI:ssa
|
||||
docs: '/docs/tofu-cheatsheet.md', // valinnainen referenssidokumentti
|
||||
prompt: 'You are an OpenTofu...', // system prompt
|
||||
params: { // LLM-parametrit
|
||||
temperature: 0.7,
|
||||
top_k: 40,
|
||||
max_tokens: 512,
|
||||
repetition_penalty: 1.15
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Rooli-templatet (alasvetovalikko)
|
||||
|
||||
Valmiit pohjat jotka tuovat oletuspromptit ja parametrit:
|
||||
|
||||
| Rooli | Oletusprompt | Parametrit |
|
||||
|-------|-------------|------------|
|
||||
| Koodari | "Kirjoita selkeää, testattavaa koodia" | temp 0.7, max 512 |
|
||||
| QA / Testaus | "Kirjoita testejä, etsi virheitä" | temp 0.4, max 512 |
|
||||
| DevOps | "Dockerfile, Compose, CI/CD" | temp 0.5, max 512 |
|
||||
| DevSecOps | "Tietoturva-auditointi, OWASP" | temp 0.3, max 512 |
|
||||
| Arkkitehti | "Järjestelmäsuunnittelu, rajapinnat" | temp 0.6, max 512 |
|
||||
| IaC / Infra | "OpenTofu/Terraform HCL-koodi" | temp 0.5, max 512 |
|
||||
| Data | "Tietokannat, SQL, datamallit" | temp 0.5, max 512 |
|
||||
| Manageri | "Tehtävien jako ja koordinointi" | temp 0.8, max 200 |
|
||||
| Kirjoittaja | "Dokumentaatio, README, ohjeet" | temp 0.8, max 512 |
|
||||
| Vapaa | (tyhjä, käyttäjä kirjoittaa) | temp 0.7, max 512 |
|
||||
|
||||
### 3. Malli-valitsin
|
||||
|
||||
Lista saatavilla olevista malleista — haetaan dynaamisesti:
|
||||
|
||||
```
|
||||
Hub-kysely: GET /api/models → palauttaa yhdistettyjen solmujen mallit
|
||||
|
||||
Tai staattinen lista:
|
||||
- qwen2.5-coder:7b (oletus, natiivi GPU)
|
||||
- qwen2.5-coder:1.5b (kevyt)
|
||||
- qwen2.5-coder:0.5b (selain Wasm)
|
||||
- deepseek-r1 (reasoning)
|
||||
- llama3.2:3b (yleiskäyttö)
|
||||
```
|
||||
|
||||
Pitkän aikavälin tavoite: hub ilmoittaa WebSocketin kautta mitkä mallit ovat saatavilla.
|
||||
|
||||
### 4. Avatar-valitsin
|
||||
|
||||
Valmiit avatarit + mahdollisuus ladata oma:
|
||||
|
||||
| Hahmo | Tiedosto | Eläin |
|
||||
|-------|----------|-------|
|
||||
| Asiakas | kettu_notext.png | Kettu |
|
||||
| Manageri | karhunpentu.png | Karhunpentu |
|
||||
| Koodari | kipina_notext.png | Salamanteri |
|
||||
| Data | pesukarhu_notext.png | Pesukarhu |
|
||||
| QA | susi_notext.png | Pikkususi |
|
||||
| DevOps | laiskiainen_notext.png | Laiskiainen |
|
||||
| Tarkkailija | aikuinen_susi.png | Aikuinen susi |
|
||||
| Tofuist | gecko_notext.png | Gecko/Lisko |
|
||||
| Arkkitehti | ??? | (tulossa) |
|
||||
| DevSecOps | ??? | (tulossa) |
|
||||
|
||||
### 5. Docs-kenttä (referenssidokumentti)
|
||||
|
||||
Agentti voi viitata ulkoiseen dokumenttiin joka ladataan promptiin:
|
||||
|
||||
```
|
||||
docs: '/docs/tofu-cheatsheet.md' → haetaan fetch():llä, cachetetaan _docsCache-kenttään
|
||||
```
|
||||
|
||||
**Toiminta:**
|
||||
1. Ensimmäisellä `kpnRun`-kutsulla ladataan docs-URL
|
||||
2. Sisältö cachetetaan `agent._docsCache`-kenttään
|
||||
3. Liitetään promptiin: `"Reference:\n" + docsContent`
|
||||
4. Ei ladata uudelleen saman session aikana
|
||||
|
||||
**Rajoitukset:**
|
||||
- Max ~3000 tokenia (~10 KB) — pidempi docs tiivistetään
|
||||
- Vain tekstitiedostot (.md, .txt)
|
||||
|
||||
### 6. Tallennus (localStorage)
|
||||
|
||||
```js
|
||||
// Tallennusavain
|
||||
'kpn-custom-agents' → JSON.stringify([ agentSkeema1, agentSkeema2, ... ])
|
||||
|
||||
// Ladattaessa
|
||||
const customAgents = JSON.parse(localStorage.getItem('kpn-custom-agents') || '[]');
|
||||
const defaultAgents = { manager: {...}, coder: {...}, ... };
|
||||
const agentPrompts = { ...defaultAgents };
|
||||
for (const agent of customAgents) {
|
||||
agentPrompts[agent.id] = agent;
|
||||
}
|
||||
```
|
||||
|
||||
**Oletusagentit** (manager, coder, tester, qa, data) ovat aina mukana — niitä ei voi poistaa, mutta prompteja voi muokata.
|
||||
|
||||
**Käyttäjäagentit** (tofuist, arkkitehti, devsecops, ...) tallentuvat localStorageen ja latautuvat käynnistyksessä.
|
||||
|
||||
### 7. Export / Import
|
||||
|
||||
```js
|
||||
// Export — JSON-tiedosto
|
||||
const blob = new Blob([JSON.stringify(agent, null, 2)], { type: 'application/json' });
|
||||
// → agent-tofuist.json
|
||||
|
||||
// Import — tiedoston valinta tai drag & drop
|
||||
// Validoidaan skeema, lisätään agentPrompts-objektiin
|
||||
```
|
||||
|
||||
Mahdollistaa agenttien jakamisen tiimin kesken.
|
||||
|
||||
## Toteutusvaiheet
|
||||
|
||||
### Vaihe 1: Hahmolomake UI
|
||||
- Avatar-grid valitsin
|
||||
- Rooli-template alasvetovalikko (täyttää oletuspromptit)
|
||||
- Malli-valitsin
|
||||
- System prompt -tekstikenttä
|
||||
- LLM-parametrit (temperature, top-k, max_tokens)
|
||||
- Tallenna/Poista-napit
|
||||
|
||||
### Vaihe 2: Dynaaminen agenttirekisteri
|
||||
- `agentPrompts` ladataan localStoragesta
|
||||
- Oletusagentit + käyttäjän agentit yhdistetään
|
||||
- Avatar-kortit renderöidään dynaamisesti (ei HTML:ssä)
|
||||
- Värimapit generoidaan agenttiskeemasta
|
||||
|
||||
### Vaihe 3: Pipeline käyttää dynaamisia agentteja
|
||||
- Pipeline-vaiheet viittaavat agentin id:hen (ei kovakoodattuun nimeen)
|
||||
- Käyttäjä voi valita mitkä agentit osallistuvat pipelineen
|
||||
- Tofuist voi korvata DevOpsin IaC-projekteissa
|
||||
|
||||
### Vaihe 4: Mallirekisteri (hub-integraatio)
|
||||
- Hub tarjoaa `/api/models`-endpointin
|
||||
- Saatavilla olevat mallit näkyvät valitsimessa reaaliajassa
|
||||
- Solmun liittyessä/poistuessa mallit päivittyvät
|
||||
|
||||
## Arkkitehtuurikaavio
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────┐
|
||||
│ Agent Builder UI │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
|
||||
│ │ Avatar │ │ Rooli │ │ Malli-valitsin │ │
|
||||
│ │ Grid │ │ Template │ │ (hub/staattinen) │ │
|
||||
│ └────┬─────┘ └────┬─────┘ └────────┬─────────┘ │
|
||||
│ └─────────────┼───────────────┘ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────────────────────────────┐ │
|
||||
│ │ Agent Schema { id, name, avatar, model, │ │
|
||||
│ │ role, color, docs, prompt, │ │
|
||||
│ │ params } │ │
|
||||
│ └──────────────────┬───────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌─────────────┼─────────────┐ │
|
||||
│ ▼ ▼ ▼ │
|
||||
│ localStorage Org Chart Pipeline │
|
||||
│ (persist) (render) (execute) │
|
||||
└──────────────────────────────────────────────────┘
|
||||
```
|
||||
525
network-poc/BUILDING_BLOCKS.md
Normal 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,'&').replace(/</g,'<')
|
||||
.replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
```
|
||||
|
||||
**Käyttöpaikat:** Kaikki `innerHTML`-insertoinnit joissa on käyttäjä- tai backend-dataa.
|
||||
|
||||
### 6.2 System prompt -piilotus
|
||||
|
||||
```javascript
|
||||
function stripSystemPrompt(prompt) {
|
||||
const parts = prompt.split('\n\n');
|
||||
return parts[parts.length - 1] || prompt;
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 Viestityyppivalidointi (backend)
|
||||
|
||||
```rust
|
||||
const ALLOWED_MSG_TYPES: &[&str] = &[
|
||||
"auth", "result", "pair_done", "llm_chunk", "llm_done",
|
||||
"llm_error", "download_progress", "user_text", "single_tokenize_done"
|
||||
];
|
||||
|
||||
fn validate_message(text: &str) -> Result<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
|
||||
@@ -4,4 +4,4 @@ members = [
|
||||
"hub",
|
||||
"node",
|
||||
"native-node"
|
||||
]
|
||||
, "cli"]
|
||||
|
||||
21
network-poc/Dockerfile.native
Normal file
@@ -0,0 +1,21 @@
|
||||
# Native-node: Rust + Ollama-client (ei GPU-tunnistusta)
|
||||
FROM rust:slim AS builder
|
||||
RUN apt-get update && apt-get install -y pkg-config libssl-dev && rm -rf /var/lib/apt/lists/*
|
||||
WORKDIR /app
|
||||
COPY Cargo.toml Cargo.lock* ./
|
||||
COPY native-node/Cargo.toml native-node/Cargo.toml
|
||||
COPY native-node/src native-node/src
|
||||
# Dummy-cratet workspace-yhteensopivuuteen
|
||||
COPY hub/Cargo.toml hub/Cargo.toml
|
||||
COPY node/Cargo.toml node/Cargo.toml
|
||||
COPY cli/Cargo.toml cli/Cargo.toml
|
||||
RUN mkdir -p hub/src node/src cli/src && touch hub/src/main.rs node/src/lib.rs cli/src/main.rs
|
||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,target=/app/target \
|
||||
cargo build --release -p native-node --no-default-features \
|
||||
&& cp /app/target/release/native-node /usr/local/bin/native-node
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=builder /usr/local/bin/native-node /usr/local/bin/native-node
|
||||
CMD ["native-node"]
|
||||
@@ -1,30 +1,36 @@
|
||||
FROM rust:slim AS builder
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
pkg-config libssl-dev g++ \
|
||||
pkg-config libssl-dev g++ libvulkan-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY Cargo.toml ./
|
||||
COPY Cargo.loc[k] ./
|
||||
COPY hub/Cargo.toml hub/Cargo.toml
|
||||
COPY node/Cargo.toml node/Cargo.toml
|
||||
COPY native-node/Cargo.toml native-node/Cargo.toml
|
||||
COPY cli/Cargo.toml cli/Cargo.toml
|
||||
|
||||
# Tyhjät src-tiedostot riippuvuuksien esikääntämistä varten
|
||||
RUN mkdir -p hub/src node/src native-node/src \
|
||||
RUN mkdir -p hub/src node/src native-node/src cli/src \
|
||||
&& echo "fn main(){}" > hub/src/main.rs \
|
||||
&& echo "" > node/src/lib.rs \
|
||||
&& echo "fn main(){}" > native-node/src/main.rs \
|
||||
&& echo "fn main(){}" > cli/src/main.rs \
|
||||
&& cargo build --release -p native-node 2>/dev/null || true
|
||||
|
||||
COPY native-node/src native-node/src
|
||||
RUN cargo build --release -p native-node
|
||||
# Touch pakottaa rekompilauksen dummy-binaryn yli
|
||||
RUN touch native-node/src/main.rs && cargo build --release -p native-node
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||
RUN apt-get update && apt-get install -y ca-certificates libvulkan1 && rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=builder /app/target/release/native-node /usr/local/bin/native-node
|
||||
|
||||
ENV HUB_URL=ws://hub:3000/ws
|
||||
ENV HUB_URL=ws://agentic-poc:3000/ws
|
||||
ENV OLLAMA_URL=http://ollama:11434
|
||||
ENV OLLAMA_MODEL=qwen2.5-coder:7b
|
||||
ENV ALLOCATED_GB=4
|
||||
|
||||
CMD ["native-node"]
|
||||
|
||||
@@ -1,50 +1,61 @@
|
||||
FROM rust:slim AS builder
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
curl pkg-config libssl-dev g++ \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
# --- Vaihe 1: Frontend (Astro) ---
|
||||
FROM node:22-slim AS frontend
|
||||
WORKDIR /app/frontend
|
||||
COPY frontend/package.json frontend/package-lock.json* ./
|
||||
RUN npm install --silent
|
||||
# Cache-buster: git hash pakottaa rebuildin kun koodi muuttuu
|
||||
ARG CACHEBUST=0
|
||||
COPY frontend/src/ ./src/
|
||||
COPY frontend/public/ ./public/
|
||||
COPY frontend/astro.config.mjs frontend/tsconfig.json ./
|
||||
RUN npm run build
|
||||
|
||||
# --- Vaihe 2: Wasm (wasm-pack) ---
|
||||
# Cargo registry cachetetaan mount-cachella, lähdekoodi kopioidaan tuoreena
|
||||
FROM rust:slim AS wasm-builder
|
||||
RUN apt-get update && apt-get install -y curl pkg-config libssl-dev g++ && rm -rf /var/lib/apt/lists/*
|
||||
RUN curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
|
||||
WORKDIR /app
|
||||
COPY Cargo.toml Cargo.lock* ./
|
||||
COPY node/Cargo.toml node/Cargo.toml
|
||||
COPY hub/Cargo.toml hub/Cargo.toml
|
||||
COPY native-node/Cargo.toml native-node/Cargo.toml
|
||||
COPY cli/Cargo.toml cli/Cargo.toml
|
||||
RUN mkdir -p hub/src native-node/src cli/src && touch hub/src/main.rs native-node/src/main.rs cli/src/main.rs
|
||||
ARG CACHEBUST=0
|
||||
COPY node/src node/src
|
||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,target=/app/target \
|
||||
cd node && wasm-pack build --target web --out-dir /app/wasm-pkg
|
||||
|
||||
# 1. Kopioi vain Cargo-tiedostot → riippuvuudet cacheen
|
||||
COPY Cargo.toml ./
|
||||
COPY Cargo.lock* ./
|
||||
# --- Vaihe 3: Hub (Rust) ---
|
||||
FROM rust:slim AS hub-builder
|
||||
RUN apt-get update && apt-get install -y pkg-config libssl-dev && rm -rf /var/lib/apt/lists/*
|
||||
WORKDIR /app
|
||||
COPY Cargo.toml Cargo.lock* ./
|
||||
COPY hub/Cargo.toml hub/Cargo.toml
|
||||
COPY node/Cargo.toml node/Cargo.toml
|
||||
COPY native-node/Cargo.toml native-node/Cargo.toml
|
||||
|
||||
# Tyhjät lähteet riippuvuuksien esikääntämistä varten
|
||||
RUN mkdir -p hub/src node/src native-node/src \
|
||||
&& echo "fn main(){}" > hub/src/main.rs \
|
||||
&& echo "" > node/src/lib.rs \
|
||||
&& mkdir -p node/src && touch node/src/storage.rs \
|
||||
&& echo "fn main(){}" > native-node/src/main.rs \
|
||||
&& cargo build --release -p hub 2>/dev/null || true \
|
||||
&& wasm-pack build node --target web --out-dir ../static/pkg 2>/dev/null || true
|
||||
|
||||
# 2. Kopioi oikea lähdekoodi → vain src käännetään uudelleen
|
||||
COPY cli/Cargo.toml cli/Cargo.toml
|
||||
RUN mkdir -p node/src native-node/src cli/src && touch node/src/lib.rs native-node/src/main.rs cli/src/main.rs
|
||||
ARG CACHEBUST=0
|
||||
COPY hub/src hub/src
|
||||
COPY node/src node/src
|
||||
COPY static static
|
||||
|
||||
# Pakota uudelleenkäännös
|
||||
RUN touch hub/src/main.rs node/src/lib.rs
|
||||
|
||||
# Rakenna Wasm-paketti
|
||||
RUN cd node && wasm-pack build --target web --out-dir ../static/pkg
|
||||
|
||||
# Rakenna Hub release-binääri
|
||||
RUN cargo build --release -p hub
|
||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,target=/app/target \
|
||||
cargo build --release -p hub \
|
||||
&& cp /app/target/release/hub /usr/local/bin/hub
|
||||
|
||||
# --- Vaihe 4: Tuotantoimage ---
|
||||
FROM debian:bookworm-slim
|
||||
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=builder /app/target/release/hub /usr/local/bin/hub
|
||||
COPY --from=builder /app/static /app/static
|
||||
COPY --from=hub-builder /usr/local/bin/hub /usr/local/bin/hub
|
||||
COPY --from=frontend /app/frontend/dist /app/frontend/dist
|
||||
COPY --from=wasm-builder /app/wasm-pkg /app/frontend/dist/pkg
|
||||
|
||||
WORKDIR /app
|
||||
ENV STATIC_DIR=/app/static
|
||||
ENV STATIC_DIR=/app/frontend/dist
|
||||
EXPOSE 3000
|
||||
CMD ["hub"]
|
||||
|
||||
348
network-poc/PROMPTS.md
Normal 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 |
|
||||
@@ -1,75 +1,134 @@
|
||||
# Kipinä Agentic Network PoC (WebGPU Edition)
|
||||
# Kipinä Agentic Network PoC
|
||||
|
||||
Tämä on hajautetun tekoälylaskennan (Agentic Compute) kokeilulaboratorio. Projekti koostuu Rust-pohjaisesta keskuksesta (Hub) ja selainpohjaisista työntekijöistä (Nodet), jotka suorittavat tekoälytensoreiden matriisilaskentaa **WebGPU**-rajapintaa ja **Burn AI** -koneoppimiskirjastoa hyödyntäen.
|
||||
Hajautettu AI-laskentaverkko selaimessa ja natiivina. Käyttäjät tarjoavat GPU/CPU-laskentatehoa avaamalla verkkosivun tai ajamalla natiivi-noden.
|
||||
|
||||
Normaalin keskitetyn palvelimen sijaan tämä kokeilu hyödyntää selaimeen kytkettyjen lukemattomien laitteiden vapaana olevaa tehokapasiteettia hajautetusti P2P-tyylillä.
|
||||
**Tuotanto:** https://kipina.studio | **Admin:** https://kipina.studio/admin
|
||||
|
||||
## Kuinka käynnistää projekti paikallisesti
|
||||
## Arkkitehtuuri
|
||||
|
||||
1. **Rakenna solmun WebAssembly-binääri**
|
||||
Paketoi Rust WebAssemblyksi (vaatii `wasm-pack`-työkalun):
|
||||
```bash
|
||||
cd node
|
||||
wasm-pack build --target web --out-dir ../static/pkg
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ Hub (Axum) │
|
||||
│ :3000 / Caddy │
|
||||
│ SQLite, WS BC │
|
||||
└────────┬────────┘
|
||||
WebSocket │ WebSocket
|
||||
┌────────────────────┼────────────────────┐
|
||||
▼ ▼ ▼
|
||||
┌────────────────┐ ┌────────────────┐ ┌─────────────────┐
|
||||
│ Selainsolmu │ │ Selainsolmu │ │ Native Node │
|
||||
│ Wasm + Burn │ │ Wasm + Candle │ │ Rust + Candle │
|
||||
│ WebGPU/NdArray │ │ SmolLM/Qwen │ │ CPU/CUDA │
|
||||
└────────────────┘ └────────────────┘ └─────────────────┘
|
||||
```
|
||||
|
||||
2. **Käynnistä Hub-Keskuspalvelin**
|
||||
```bash
|
||||
cd hub
|
||||
cargo run
|
||||
```
|
||||
Palvelin lähtee pyörimään ja tarjoamaan sekä WebSocket-reititintä että staattista Dashboard-sivustoa lokaalisti portissa `3000`.
|
||||
**Hub** broadcastaa tehtäviä (tokenisointiparit, LLM-promptit) kaikille solmuille WebSocketin kautta. Solmut käsittelevät vain oman tehtävätyyppinsä mukaiset viestit.
|
||||
|
||||
---
|
||||
## Cratet
|
||||
|
||||
## ⚠️ WebGPU Ota-Käyttöön -ohjeet (Linux / Mac / Win)
|
||||
| Crate | Polku | Kuvaus |
|
||||
|---|---|---|
|
||||
| `hub` | `hub/` | Axum WebSocket -palvelin, tehtävien jakelu, admin-API, SQLite |
|
||||
| `node` | `node/` | Wasm-selainsolmu: Burn (tensorit), Candle (LLM), tokenizer |
|
||||
| `native-node` | `native-node/` | Natiivi Rust-solmu: Candle LLM, NVML/wgpu GPU-tunnistus, sysinfo |
|
||||
|
||||
Selainvalmistajat rajoittavat tällä hetkellä uuden WebGPU-rajapinnan hardware-yhteyttä (fyysiseen näytönohjaimeen) turvallisuus- ja vakaussyistä, erityisesti Linuxin Wayland-ympäristöissä (kuten Pop!_OS, Ubuntu).
|
||||
### Hub (`hub/src/`)
|
||||
|
||||
Päästäksesi hyödyntämään solmun laskentatehoa selaimesi ja tietokoneesi näytönohjaimen läpi, joudut todennäköisesti pakottamaan sen käyntiin.
|
||||
- `main.rs` — WebSocket-reititin, tehtäväjakelu (10s intervalli), origin-tarkistus, IP-rajoitus, admin HTML
|
||||
- `db.rs` — SQLite: `node_sessions` + `pair_results` taulut, skeemaversiointi
|
||||
|
||||
### Chromium-pohjaiset selaimet (Google Chrome, Brave, Chromium)
|
||||
### Node (`node/src/`)
|
||||
|
||||
**Vaihtoehto 1: Käynnistys lipuilla (Suositeltu Linuxille ja Waylandille)**
|
||||
Jos Chromesi tuottaa Wasm-kaatumisia tai väittää ettei adapteria löydy, laitteesi Wayland-palvelin estää Vulkan-rajapinnan oletuksena. Käynnistä selaimesi komentoriviltä pakottamalla vanha X11-ikkunointi ja Vulkan:
|
||||
- `lib.rs` — Wasm-entrypoint, tehtävävalinta (`SELECTED_TASK`), WebSocket-handler, GPU/CPU-valinta
|
||||
- `storage.rs` — IndexedDB read/write (tokenizer, mallin painot)
|
||||
- `sampling.rs` — Top-k sampling EOS-penaltilla (kiertää Candlen softmax Wasm-bugin)
|
||||
- `smollm.rs` — SmolLM 135M Candle-inferenssi (Llama-arkkitehtuuri)
|
||||
- `qwen.rs` — Qwen2.5 0.5B Candle-inferenssi (Qwen2-arkkitehtuuri)
|
||||
- `qwen_coder.rs` — Qwen2.5-Coder 0.5B/3B koodigenerointi (sama arkkitehtuuri, koodikoulutettu)
|
||||
- `phi3.rs` — Phi-3 placeholder (liian iso selaimelle)
|
||||
|
||||
### Native Node (`native-node/src/`)
|
||||
|
||||
- `main.rs` — GPU-tunnistus (wgpu + NVML + sysfs + Apple), HF Hub -lataus, WS-yhteys
|
||||
- `inference.rs` — Qwen2.5-0.5B Candle-inferenssi, CUDA/CPU, KV-cache reset per prompti, mmap-lataus
|
||||
|
||||
## Kehitysympäristö
|
||||
|
||||
```bash
|
||||
# Google Chrome
|
||||
google-chrome --enable-unsafe-webgpu --enable-features=Vulkan --ignore-gpu-blocklist --use-angle=vulkan --ozone-platform=x11
|
||||
# Vaatimukset
|
||||
rustup target add wasm32-unknown-unknown
|
||||
cargo install wasm-pack
|
||||
|
||||
# Brave Browser
|
||||
brave-browser --enable-unsafe-webgpu --enable-features=Vulkan --ignore-gpu-blocklist --use-angle=vulkan --ozone-platform=x11
|
||||
# Kehitys (Docker — Wasm buildataan automaattisesti)
|
||||
docker compose up
|
||||
|
||||
# Chromium
|
||||
chromium-browser --enable-unsafe-webgpu --enable-features=Vulkan --ignore-gpu-blocklist --use-angle=vulkan --ozone-platform=x11
|
||||
# Kehitys (ilman Dockeria)
|
||||
cd node && wasm-pack build --dev --target web --out-dir ../static/pkg && cd ..
|
||||
cargo run -p hub
|
||||
# → http://localhost:3000
|
||||
|
||||
# Native node (erillinen terminaali)
|
||||
CARGO_TARGET_DIR=target-native HUB_URL=ws://localhost:3000/ws cargo run --release -p native-node
|
||||
```
|
||||
|
||||
*(Voit halutessasi testata puhdasta testi-ikkunaa erillisen profiilin kera, lisäämällä perään `--user-data-dir=/tmp/kipin-webgpu-test` jottei asetus sotke tai ohjaudu vanhaan auki olevaan sessioosi).*
|
||||
## Viestityyypit (WebSocket JSON)
|
||||
|
||||
**Vaihtoehto 2: Sisäänrakennetun Flagin kääntö (Windows / Mac / Osittain Linux)**
|
||||
1. Kirjoita selaimen osoiteriville `chrome://flags` (tai `brave://flags`)
|
||||
2. Etsi hakusanalla **WebGPU** (Unsafe WebGPU / WebGPU Developer Features) ja vaihda tilaksi `Enabled`
|
||||
3. Etsi hakusanalla **Vulkan** ja vaihda tilaan `Enabled`
|
||||
4. Uudelleenkäynnistä selain pienen napin kautta.
|
||||
Hub → solmut:
|
||||
| Tyyppi | Kuvaus |
|
||||
|---|---|
|
||||
| `pair_task` | `{en, fi}` — tokenisointipari |
|
||||
| `llm_prompt` | `{prompt, model}` — LLM-tehtävä |
|
||||
| `stats` | `{nodes, vram_gb, tasks, version}` |
|
||||
| `node_joined` | `{node_id}` |
|
||||
|
||||
---
|
||||
Solmu → hub:
|
||||
| Tyyppi | Kuvaus |
|
||||
|---|---|
|
||||
| `auth` | Laitetiedot, `selected_task`, `allocated_gb` |
|
||||
| `pair_done` | Tokenisointitulos: `{en, fi, overhead_pct, duration_ms}` |
|
||||
| `llm_done` | LLM-tulos: `{response, tokens_generated, tokens_per_sec}` |
|
||||
| `llm_chunk` | Streaming-token |
|
||||
| `download_progress` | Mallin latauksen edistyminen |
|
||||
| `user_text` | Käyttäjän oma teksti: `{text, task_type}` |
|
||||
|
||||
### Mozilla Firefox
|
||||
## API-endpointit
|
||||
|
||||
Firefox tukee WebGPU:ta toistaiseksi vahvasti vain Nightly-versioissa, mutta sitä voi yrittää aktivoida Config-asetuksista.
|
||||
1. Kirjoita osoiteriville `about:config` ja ymmärrä riskit.
|
||||
2. Etsi `dom.webgpu.enabled` ja tuplaklikkaa arvoksi `true`.
|
||||
3. Etsi `gfx.webrender.all` ja aseta se `true`.
|
||||
4. Uudelleenkäynnistä Firefox.
|
||||
| Polku | Kuvaus |
|
||||
|---|---|
|
||||
| `GET /` | Dashboard (staattinen HTML) |
|
||||
| `GET /ws` | WebSocket-yhteys |
|
||||
| `GET /admin` | Admin-dashboard |
|
||||
| `GET /api/sessions` | Node-sessiot (JSON) |
|
||||
| `GET /api/pairs` | Tokenisointitulokset (JSON) |
|
||||
| `GET /api/stats` | Yhteenvetotilastot (JSON) |
|
||||
|
||||
*(Huomio Linux-käyttäjille: Firefox saattaa edellyttää MOZ_ENABLE_WAYLAND ympäristömuuttujaa).*
|
||||
## Tietoturva
|
||||
|
||||
---
|
||||
- **Origin-tarkistus** — vain `https://kipina.studio` ja `localhost:3000`
|
||||
- **IP-rajoitus** — max 4 WS-yhteyttä per IP, X-Forwarded-For -tuki
|
||||
- **Viestivalidointi** — pakollinen `type`, sallitut tyypit, kenttäkohtaiset rajat
|
||||
- **Viestikoko** — max 16 KB per WebSocket-viesti
|
||||
- **Admin Basic Auth** — `/admin` ja `/api/*` salasanan takana (`ADMIN_PASSWORD` env, oletus: `kipina`)
|
||||
- **Caddy** — automaattinen TLS (Let's Encrypt)
|
||||
|
||||
### Apple Safari (Mac)
|
||||
## Tuotanto-deploy
|
||||
|
||||
Apple käyttää konepellin alla vahvaa omaa Metal-rajapintaansa ja tukee WebGPU:ta uudemmissa Safari-versioissa kehittäjäasetusten takaa:
|
||||
1. Varmista ensin Safarin asetuksista (Preferences -> Advanced) , että ruutu on ruksittu kohdasta `"Show Develop menu in menu bar"`.
|
||||
2. Valitse yläpalkista avautuva **Develop**-valikko -> **Feature Flags**.
|
||||
3. Etsi listalta **WebGPU** ja laita siihen täppä pelastamaan tilanne.
|
||||
4. Päivitä Dashboard-sivu.
|
||||
```bash
|
||||
# Buildaa lokaalisti, siirrä palvelimelle, käynnistä
|
||||
./deploy.sh
|
||||
|
||||
# Manuaalisesti palvelimella
|
||||
docker compose -f docker-compose.prod.yml down && docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
## Tiedossa olevat rajoitukset
|
||||
|
||||
- LLM-inferenssi käyttää **top-k samplingia** (k=10, EOS-penaltti) — ei täyttä temperature/top-p -tukea Wasmissa
|
||||
- Qwen selaimessa: ~0.4 tok/s CPU — käyttökelpoinen demona mutta ei tuotantoon
|
||||
- Native node + CUDA: ~50-100 tok/s (RTX 4090)
|
||||
- Hub broadcastaa kaikki viestit kaikille — ei kohdennettu reititystä
|
||||
- 3B Coder-malli vaatii ~12 GB RAM selaimessa (Wasm)
|
||||
|
||||
## Lisenssi
|
||||
|
||||
Kipinä Technologies Oy — sisäinen projekti.
|
||||
|
||||
@@ -26,9 +26,14 @@ Tässä on kooste projektin vaatimuksista, työtehtävistä ja niiden nykytilant
|
||||
- Sijoittaa Hub-palvelin julkisesti saatavuusosoitteeseen `kipina.studio`.
|
||||
|
||||
### Tehtävät
|
||||
- [ ] Tuotantopalvelimen käyttöönotto Nginxin tai Docker-compose kautta ehtojen täytyttyä
|
||||
- [ ] Turvamekanismin lisäys: Varmistetaan, ettei kukaan lähetä "falskeja" vastauksia nodeilta
|
||||
- [ ] Solmuille rekisteröitymismekanismi tai tulostaulukko
|
||||
- [x] Tuotantopalvelimen käyttöönotto Docker-compose + Caddy TLS kautta (`kipina.studio`)
|
||||
- [x] Deploy-skripti (`deploy.sh`) + Discord-webhook-notifikaatio julkaisuista
|
||||
- [x] Admin-dashboard (`/admin`) Basic Auth -suojattuna, live-sessiot ja metriikat
|
||||
- [x] REST API (`POST /api/v1/chat/completions`) task_id-pohjaisella vastausten reitityksellä
|
||||
- [x] API timeout (120s) + selkeät virheilmoitukset (504 Gateway Timeout)
|
||||
- [x] IP-pohjainen rate limiting (max 4 yhteyttä/IP) + origin-validointi
|
||||
- [ ] Turvamekanismin lisäys: Varmistetaan, ettei kukaan lähetä "falskeja" vastauksia nodeilta (PoW/challenge-response)
|
||||
- [x] SQLite-sessioseuranta (node_sessions + pair_results)
|
||||
|
||||
---
|
||||
|
||||
@@ -53,7 +58,38 @@ Tässä on kooste projektin vaatimuksista, työtehtävistä ja niiden nykytilant
|
||||
- Kyetä lataamaan selaimen IndexedDB:hen satojen megatavujen painot massivisena fetch-hakuna, kääntää ne WebGPU-puskureihin (Buffers) ja suorittaa tekstigeneraatiota etänä ohjattuna verkosta käsin WebSocketia myöden.
|
||||
|
||||
### Tehtävät
|
||||
- [ ] Refaktoroi Wasm-Noden (Burn.rs) paketti tuomaan Text-Tokenizerit (esim. BPE) ja kielimallin arkkitehtuuri käyttöön
|
||||
- [ ] Koodaa Nodeen logiikka hakea / kasata mallin painot välimuistista "Chunk"-lohkoina valmiiksi
|
||||
- [ ] Hub uudistetaan generoimaan pelkkien matikkavaikeuksien sijasta Text Prompts (esim. "Kirjoita haiku Suomesta") ja reitittämään työkuorman vapaalle solmulle
|
||||
- [ ] Kipinän käyttöliittymään Chat-ikkuna Hubin striimaamien tulossanojen tarkkailuun reaaliajassa
|
||||
- [x] Refaktoroi Wasm-Noden (Burn.rs) paketti tuomaan Text-Tokenizerit (BPE, Qwen2.5-Coder) ja kielimallin arkkitehtuuri käyttöön
|
||||
- [x] Koodaa Nodeen logiikka hakea / kasata mallin painot välimuistista IndexedDB:hen (tokenizer.json + model weights)
|
||||
- [x] Hub uudistetaan generoimaan Text Prompts ja reitittämään työkuorman vapaalle solmulle (broadcast + task_id-matching)
|
||||
- [x] Kipinän käyttöliittymään Chat-ikkuna Hubin striimaamien tulossanojen tarkkailuun reaaliajassa (llm_chunk streaming)
|
||||
- [x] SmolLM 135M — täysi transformer (Burn), ~1.2 tok/s CPU
|
||||
- [x] Qwen2.5 0.5B — Candle-inferenssi, ChatML-muotoilu, ~0.4 tok/s CPU
|
||||
- [x] Qwen2.5-Coder 0.5B & 3B — koodigeneraatio, streaming-tokenit, task_id-tuki
|
||||
- [x] Phi-3 Mini — placeholder (liian suuri selaimelle, natiivisolmulle suunnitteilla)
|
||||
- [x] EN/FI tokenisaatiovertailu overhead-laskennalla
|
||||
- [x] Natiivisolmu (Rust + CUDA) — Qwen2.5 0.5B, ~50-100 tok/s RTX 4090, NVML GPU-metriikat
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Vaihe 6: Agent Workspace & CLI (KÄYNNISSÄ)
|
||||
|
||||
### Tavoitteet
|
||||
- Interaktiivinen terminaalipohjainen käyttöliittymä `kpn`-komennoilla.
|
||||
- Agenttitiimi (Koodari, Testaaja, Manageri) muokattavilla system prompteilla.
|
||||
- Agenttien ketjutus: manageri analysoi → koodari toteuttaa → testaaja arvioi.
|
||||
|
||||
### Tehtävät
|
||||
- [x] KPN-terminaali selaimeen (interaktiivinen komentorivi, komentohistoria)
|
||||
- [x] `kpn run <malli> "<prompti>"` — tehtävän lähetys REST API:n kautta
|
||||
- [x] `kpn hello` — tervehdyskomento
|
||||
- [x] `kpn pipeline "<tehtävä>"` — manageri → koodari → testaaja -ketjutus
|
||||
- [x] `kpn status`, `kpn models`, `kpn clear`, `kpn help`
|
||||
- [x] Agenttikortit (Koodari/Qwen-Coder, Testaaja/SmolLM, Manageri/KPN CLI)
|
||||
- [x] Muokattavat system promptit per agentti (localStorage-tallennus)
|
||||
- [x] Multi-select: yhteinen konteksti useammalle agentille
|
||||
- [x] Streaming-vastaukset terminaalissa (llm_chunk + vilkkuva kursori)
|
||||
- [x] URL-hash navigointi (`#agents`, `#codelab`, `#network`)
|
||||
- [x] SPA fallback (ServeDir + ServeFile)
|
||||
- [ ] Agenttien välinen keskustelu (manageri ohjaa koodaria ja testaajaa dynaamisesti)
|
||||
- [ ] Tehtävähistoria ja tulosten tallennus
|
||||
- [ ] CLI-työkalu (`kpn` binary) lokaaliin käyttöön
|
||||
|
||||
26
network-poc/TODO.md
Normal 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
|
||||
@@ -15,20 +15,29 @@ Kipinä Agentic Network on hajautettu tekoälylaskentaverkko, jossa selaimet ja
|
||||
jos WebGPU ei tuettu
|
||||
```
|
||||
|
||||
**Hub** jakaa tokenisointitehtäviä satunnaisesti 10 sekunnin välein. Solmut tokenisoivat syötteen Qwen2.5-Coder-tokenizerin avulla ja palauttavat tuloksen. Hub näyttää tulokset terminaalissa ja välittää ne dashboardiin.
|
||||
**Hub** jakaa tehtäviä (tokenisointiparit, LLM-promptit, kooditehtävät) 10 sekunnin välein. Solmut käsittelevät vain valitsemansa tehtävätyypin mukaisia viestejä.
|
||||
|
||||
## Kaksi tapaa osallistua verkkoon
|
||||
## Kolme tapaa osallistua verkkoon
|
||||
|
||||
### 1. Selainsolmu (Wasm + WebGPU)
|
||||
- Avaa `http://localhost:3000` selaimessa ja klikkaa "Liity laskentaverkkoon"
|
||||
- Selain tunnistaa automaattisesti WebGPU-tuen — jos ei löydy, käytetään CPU-fallbackia
|
||||
- Tokenizer ladataan HuggingFacesta ensimmäisellä kerralla ja tallennetaan IndexedDB:hen
|
||||
- GPU-kuormitusta voi säätää sliderilla (0–75 %)
|
||||
### 1. Selainsolmu — Laskentaverkko
|
||||
- Avaa `http://localhost:3000` | `https://kipina.studio` ja valitse tehtävä:
|
||||
- **Tokenisointivertailu** — EN/FI-kieliparien BPE-tokenisointitehokkuus (~7 MB lataus)
|
||||
- **SmolLM 135M** — kevyt LLM-inferenssi (~269 MB, ~1.2 tok/s)
|
||||
- **Qwen2.5 0.5B** — tehokkaampi LLM (~990 MB, ~0.4 tok/s)
|
||||
- **Phi-3 Mini 3.8B** — vain native-nodella
|
||||
- WebGPU tunnistetaan automaattisesti, CPU-fallback jos ei tuettu
|
||||
- Mallit ja tokenizerit cachetetaan IndexedDB:hen
|
||||
|
||||
### 2. Natiivi-node (Rust + NVML)
|
||||
### 2. Selainsolmu — Koodilaboratorio
|
||||
- Erillinen välilehti: **Qwen2.5-Coder** koodigenerointi
|
||||
- Valittavissa **0.5B** (nopea) tai **3B** (laadukas, 6.2 GB lataus)
|
||||
- Oma promptti: kirjoita Python-ohjelmointitehtävä ja paina "Generate"
|
||||
- Syntaksikorostettu koodivastaus
|
||||
|
||||
### 3. Natiivi-node (Rust + CUDA/CPU)
|
||||
- Qwen2.5-0.5B-Instruct inferenssi CUDA:lla (~50-100 tok/s RTX 4090) tai CPU:lla (~11 tok/s)
|
||||
- Kerää nvidia-smi-tason laitteistotiedot: GPU-nimi, VRAM, lämpötila, kuormitus
|
||||
- Raportoi järjestelmätiedot: CPU-malli, ytimet, RAM, OS
|
||||
- Yhdistää hubiin ja vastaanottaa tehtäviä
|
||||
- Lataa mallin automaattisesti HuggingFace Hubista (~990 MB, cachetetaan)
|
||||
|
||||
## Käynnistys
|
||||
|
||||
@@ -42,7 +51,7 @@ docker compose up
|
||||
docker compose --profile native up
|
||||
```
|
||||
|
||||
Dashboard avautuu osoitteessa http://localhost:3000
|
||||
Dashboard avautuu osoitteessa http://localhost:3000 | https://kipina.studio
|
||||
|
||||
### Ilman Dockeria
|
||||
|
||||
@@ -53,48 +62,83 @@ cd node && wasm-pack build --target web --out-dir ../static/pkg && cd ..
|
||||
# 2. Käynnistä hub (terminaali 1)
|
||||
cargo run -p hub
|
||||
|
||||
# 3. Avaa selain: http://localhost:3000
|
||||
# 3. Avaa selain: http://localhost:3000 | https://kipina.studio
|
||||
|
||||
# 4. Valinnainen: natiivi-node (terminaali 2)
|
||||
HUB_URL=ws://localhost:3000/ws ALLOCATED_GB=4 cargo run -p native-node
|
||||
# 4. Valinnainen: natiivi-node LLM-inferenssillä (terminaali 2)
|
||||
# Lataa Qwen2.5-0.5B automaattisesti HuggingFacesta (~990 MB, cachetetaan)
|
||||
# Release-moodissa ~11 tok/s CPU:lla (32 ydintä)
|
||||
CARGO_TARGET_DIR=target-native HUB_URL=ws://localhost:3000/ws ALLOCATED_GB=4 cargo run --release -p native-node
|
||||
|
||||
|
||||
# Tai yhdistä tuotantopalvelimeen:
|
||||
CARGO_TARGET_DIR=target-native HUB_URL=wss://kipina.studio/ws ALLOCATED_GB=4 cargo run --release -p native-node
|
||||
```
|
||||
|
||||
## WebGPU-asetukset selaimessa
|
||||
### CUDA-tuki
|
||||
|
||||
WebGPU ei ole oletuksena päällä kaikissa selaimissa. Jos "Liity laskentaverkkoon" -nappi käynnistää CPU-fallbackin vaikka koneessa on näytönohjain:
|
||||
CUDA on oletuksena päällä native-nodessa. Vaatii `nvidia-cuda-toolkit`:n:
|
||||
|
||||
**Chrome / Brave (Linux + Wayland):**
|
||||
```bash
|
||||
google-chrome --enable-unsafe-webgpu --enable-features=Vulkan --ignore-gpu-blocklist --use-angle=vulkan --ozone-platform=x11
|
||||
# Asenna (Ubuntu/Pop!_OS)
|
||||
sudo apt install nvidia-cuda-toolkit
|
||||
|
||||
# Tarkista
|
||||
nvcc --version
|
||||
|
||||
# Aja — tunnistaa CUDA:n automaattisesti, fallback CPU:lle
|
||||
CARGO_TARGET_DIR=target-native HUB_URL=ws://localhost:3000/ws cargo run --release -p native-node
|
||||
|
||||
# Tuotantoon
|
||||
CARGO_TARGET_DIR=target-native HUB_URL=wss://kipina.studio/ws cargo run --release -p native-node
|
||||
```
|
||||
|
||||
**Chrome / Brave (Windows / Mac):**
|
||||
1. Avaa `chrome://flags`
|
||||
2. Ota käyttöön "WebGPU" ja "Vulkan"
|
||||
3. Käynnistä selain uudelleen
|
||||
Jos CUDA:a ei ole, poista feature: `candle-core = { version = "0.8" }` (ilman `features = ["cuda"]`).
|
||||
|
||||
**Firefox:** `about:config` → `dom.webgpu.enabled` = `true`
|
||||
## Kuinka saat WebGPU:n aktivoitua selaimessasi:
|
||||
|
||||
**Safari:** Develop → Feature Flags → WebGPU
|
||||
Jos käytät Chromea, Bravea tai Edgeä (Chromium-pohjainen):
|
||||
|
||||
- Kirjoita selaimen osoiteriville: `chrome://flags` (tai `brave://flags` / `edge://flags`)
|
||||
- Etsi hakusanalla **WebGPU** tai **Unsafe WebGPU** (`#enable-unsafe-webgpu`).
|
||||
- Vaihda asetus tilaan **Enabled**.
|
||||
- *(Linuxilla erityisesti saatat joutua käynnistämään selaimen terminaalin kautta komennoilla `--enable-unsafe-webgpu --enable-features=Vulkan`, aivan kuten olit tehnyt tämän kehityssession alussa!)*
|
||||
|
||||
Jos käytät Firefoxia:
|
||||
|
||||
- Kirjoita osoiteriville: `about:config`
|
||||
- Etsi `dom.webgpu.enabled` ja aseta se arvoon `true`.
|
||||
- Etsi `gfx.webgpu.force-enabled` ja aseta se arvoon `true`.
|
||||
|
||||
## Projektin rakenne
|
||||
|
||||
```
|
||||
network-poc/
|
||||
├── hub/ # Keskuspalvelin (Rust + Axum)
|
||||
│ └── src/main.rs # WebSocket-reititin, tehtävien jakelu, statistiikat
|
||||
│ └── src/
|
||||
│ ├── main.rs # WebSocket-reititin, tehtävien jakelu, admin HTML, Basic Auth
|
||||
│ └── db.rs # SQLite: node_sessions, pair_results
|
||||
├── node/ # Selainsolmu (Rust → Wasm)
|
||||
│ └── src/
|
||||
│ ├── lib.rs # WebGPU/NdArray-laskenta, tokenisaatio, WS-yhteys
|
||||
│ └── storage.rs # IndexedDB-välimuisti (tokenizer)
|
||||
├── native-node/ # Natiivi-solmu (Rust)
|
||||
│ └── src/main.rs # NVML GPU-tunnistus, sysinfo, WS-yhteys
|
||||
│ ├── lib.rs # Wasm-entrypoint, tehtävävalinta, WS-handler
|
||||
│ ├── storage.rs # IndexedDB-välimuisti
|
||||
│ ├── sampling.rs # Top-k sampling (EOS-penaltti)
|
||||
│ ├── smollm.rs # SmolLM 135M inferenssi
|
||||
│ ├── qwen.rs # Qwen2.5 0.5B inferenssi
|
||||
│ ├── qwen_coder.rs # Qwen2.5-Coder 0.5B/3B koodigenerointi
|
||||
│ └── phi3.rs # Phi-3 placeholder
|
||||
├── native-node/ # Natiivi-solmu (Rust + CUDA)
|
||||
│ └── src/
|
||||
│ ├── main.rs # GPU-tunnistus, WS-yhteys, tehtäväkäsittely
|
||||
│ └── inference.rs # Qwen2.5-0.5B Candle-inferenssi (CUDA/CPU)
|
||||
├── static/
|
||||
│ ├── index.html # Dashboard-käyttöliittymä
|
||||
│ ├── index.html # Dashboard + Koodilaboratorio
|
||||
│ └── pkg/ # Wasm-build (generoidaan)
|
||||
├── docker-compose.yml
|
||||
├── Dockerfile.dev # Hub + Wasm-build
|
||||
└── Dockerfile.native-node
|
||||
├── deploy.sh # Lokaali build → palvelimelle
|
||||
├── docker-compose.yml # Kehitys
|
||||
├── docker-compose.prod.yml # Tuotanto (Caddy + Hub)
|
||||
├── docker-compose.client.yml # Client-nodejen Docker
|
||||
├── Dockerfile.prod # Tuotanto-image (cache mount)
|
||||
└── Caddyfile.prod # TLS + reverse proxy
|
||||
```
|
||||
|
||||
## Ympäristömuuttujat
|
||||
@@ -103,15 +147,27 @@ network-poc/
|
||||
|---|---|---|
|
||||
| `HUB_URL` | `ws://hub:3000/ws` | Hub-palvelimen WebSocket-osoite (native-node) |
|
||||
| `ALLOCATED_GB` | `4` | Solmun varaama muisti verkosta (GB) |
|
||||
| `ADMIN_PASSWORD` | `kipina` | Admin-sivun ja API:n salasana (Basic Auth) |
|
||||
| `DATABASE_PATH` | `nodes.db` | SQLite-tietokannan polku |
|
||||
| `STATIC_DIR` | `../static` | Staattisten tiedostojen kansio |
|
||||
|
||||
## Kehitysvaihe
|
||||
## Admin-sivu
|
||||
|
||||
Tämä on proof-of-concept. Toimivat osat:
|
||||
- Hub-palvelin, WebSocket-viestintä, dashboard
|
||||
- WebGPU-tensorilaskenta selaimessa (Burn + Wgpu)
|
||||
- CPU-fallback selaimissa ilman WebGPU-tukea (Burn + NdArray)
|
||||
- Natiivi-node nvidia-smi-tason laitteistotiedoilla
|
||||
- Qwen2.5-Coder-tokenizer + IndexedDB-välimuisti
|
||||
- GPU-kuormituksen säätö (duty cycle throttling)
|
||||
`https://kipina.studio/admin` (Basic Auth, salasana: `ADMIN_PASSWORD`)
|
||||
|
||||
Seuraavaksi: oikea LLM-inferenssi hajautetusti (mallin painojen lataus, transformer-arkkitehtuuri Wasm/WebGPU:lla).
|
||||
Sisältää:
|
||||
- Node-sessiot: IP, laitetiedot, GPU, WebGPU-tuki, tehtävätyyppi, uptime
|
||||
- Tokenisointitulokset: EN/FI-vertailut, ylikustannus-%
|
||||
- Yhteenvetotilastot: sessiot, WebGPU vs CPU, keskiarvot
|
||||
|
||||
## Projektin tila
|
||||
|
||||
Toimivat ominaisuudet:
|
||||
- Tokenisointivertailu (EN/FI, BPE, top-k sampling)
|
||||
- SmolLM 135M inferenssi selaimessa (Candle + Wasm)
|
||||
- Qwen2.5 0.5B inferenssi selaimessa (Candle + Wasm)
|
||||
- Qwen2.5-Coder 0.5B/3B koodigenerointi (Koodilaboratorio-välilehti)
|
||||
- Native node + CUDA (RTX 4090: ~50-100 tok/s)
|
||||
- Admin-dashboard + SQLite + Basic Auth
|
||||
- Deploy-skripti (lokaali build → palvelin)
|
||||
- WebGPU + CPU fallback, GPU-tunnistus (NVIDIA/AMD/Apple)
|
||||
|
||||
4
network-poc/cargo-errors.log
Normal file
@@ -0,0 +1,4 @@
|
||||
error: failed to write `/home/jaakko/code/kipinä/digikipinae/agentic-office/network-poc/target/wasm32-unknown-unknown/debug/.fingerprint/num-traits-0a015ef9fd3732e0/run-build-script-build-script-build`
|
||||
|
||||
Caused by:
|
||||
Permission denied (os error 13)
|
||||
15
network-poc/cli/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "cli"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.6.0", features = ["derive"] }
|
||||
console = "0.16.3"
|
||||
indicatif = "0.18.4"
|
||||
reqwest = { version = "0.13.2", features = ["json"] }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.149"
|
||||
serde_yaml = "0.9.34"
|
||||
tokio = { version = "1.50.0", features = ["rt-multi-thread", "macros"] }
|
||||
uuid = { version = "1.23.0", features = ["v4"] }
|
||||
165
network-poc/cli/src/main.rs
Normal file
@@ -0,0 +1,165 @@
|
||||
use clap::{Parser, Subcommand};
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "kpn")]
|
||||
#[command(about = "Kipinä Agent Local CLI", long_about = None)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Alustaa uuden Kipinä-agenttikansion nykyiseen projektiin
|
||||
Init {
|
||||
#[arg(short, long, default_value = "kipina-tasks")]
|
||||
dir: String,
|
||||
},
|
||||
/// Ajaa `.md` tiedostossa kuvatun tehtävän Kipinä-verkoston kautta
|
||||
Run {
|
||||
/// Polku `.md` työtiedostoon
|
||||
file: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
struct Frontmatter {
|
||||
agent: Option<String>,
|
||||
status: Option<String>,
|
||||
context: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct CompletionRequest {
|
||||
model: String,
|
||||
prompt: String,
|
||||
task_id: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CompletionResponse {
|
||||
response: String,
|
||||
model: String,
|
||||
tokens_generated: u64,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let cli = Cli::parse();
|
||||
|
||||
match &cli.command {
|
||||
Commands::Init { dir } => {
|
||||
let path = Path::new(dir);
|
||||
if !path.exists() {
|
||||
fs::create_dir_all(path).unwrap();
|
||||
let example = format!("---\nstatus: open\nagent: qwen-coder-3b\ncontext: []\n---\n\n# Tehtävä\nKirjoita tähän mitä haluat verkon koodaavan.");
|
||||
fs::write(path.join("01-esimerkki.md"), example).unwrap();
|
||||
println!("✅ Alustettu lokaali agenttikansio: {}", dir);
|
||||
} else {
|
||||
println!("⚠️ Kansio {} on jo olemassa.", dir);
|
||||
}
|
||||
}
|
||||
Commands::Run { file } => {
|
||||
if let Err(e) = run_workflow(file).await {
|
||||
eprintln!("❌ Virhe: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_workflow(filepath: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let content = fs::read_to_string(filepath)?;
|
||||
|
||||
// Yksinkertainen frontmatter-parseri
|
||||
let mut frontmatter_str = String::new();
|
||||
let mut body = String::new();
|
||||
let mut in_frontmatter = false;
|
||||
let mut fm_found = false;
|
||||
|
||||
for line in content.lines() {
|
||||
if line.trim() == "---" {
|
||||
if !fm_found {
|
||||
in_frontmatter = true;
|
||||
fm_found = true;
|
||||
continue;
|
||||
} else if in_frontmatter {
|
||||
in_frontmatter = false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if in_frontmatter {
|
||||
frontmatter_str.push_str(line);
|
||||
frontmatter_str.push('\n');
|
||||
} else {
|
||||
body.push_str(line);
|
||||
body.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
let meta: Frontmatter = if fm_found {
|
||||
serde_yaml::from_str(&frontmatter_str).unwrap_or(Frontmatter { agent: None, status: None, context: None })
|
||||
} else {
|
||||
Frontmatter { agent: None, status: None, context: None }
|
||||
};
|
||||
|
||||
let model = meta.agent.unwrap_or_else(|| "qwen-coder-05b".to_string());
|
||||
|
||||
// Kerätään kontekstitiedostot
|
||||
let mut mega_prompt = body.trim().to_string();
|
||||
if let Some(ctx_files) = meta.context {
|
||||
mega_prompt.push_str("\n\n=== KONTEKSTI ===\n");
|
||||
for ctx in ctx_files {
|
||||
if let Ok(c) = fs::read_to_string(&ctx) {
|
||||
mega_prompt.push_str(&format!("\n--- Tiedosto: {} ---\n{}\n", ctx, c));
|
||||
} else {
|
||||
println!("⚠️ Varoitus: Kontekstitiedostoa {} ei löytynyt.", ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("\n🚀 Lähetetään tehtävä Kipinäverkkoon (Malli: {})", model);
|
||||
|
||||
let pb = ProgressBar::new_spinner();
|
||||
pb.enable_steady_tick(Duration::from_millis(100));
|
||||
pb.set_style(
|
||||
ProgressStyle::with_template("{spinner:.green} [{elapsed_precise}] {msg}")
|
||||
.unwrap()
|
||||
.tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]),
|
||||
);
|
||||
pb.set_message("Odotetaan verkon solmua ja laskentaa...");
|
||||
|
||||
let task_id = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let req = CompletionRequest {
|
||||
model: model.clone(),
|
||||
prompt: mega_prompt.clone(),
|
||||
task_id: task_id.clone(),
|
||||
};
|
||||
|
||||
let res = client.post("http://localhost:3000/api/v1/chat/completions")
|
||||
.json(&req)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if res.status().is_success() {
|
||||
let completion: CompletionResponse = res.json().await?;
|
||||
pb.finish_with_message(format!("Tulos saapui verkolta! ({} tokenia)", completion.tokens_generated));
|
||||
|
||||
let new_content = format!("{}\n\n## Kipinä Agentin Ratkaisu\n{}\n", content, completion.response);
|
||||
let updated_content = new_content.replace("status: open", "status: done");
|
||||
fs::write(filepath, updated_content)?;
|
||||
println!("✅ Vastaus tallennettu tiedostoon: {}", filepath);
|
||||
} else {
|
||||
pb.finish_with_message("❌ Verkkopyyntö epäonnistui!");
|
||||
println!("Virhekoodi: {}", res.status());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
56
network-poc/deploy-local.sh
Executable file
@@ -0,0 +1,56 @@
|
||||
#!/bin/bash
|
||||
# Kipinä Studio — paikallinen kehitysympäristö
|
||||
# Buildaa frontendin, käynnistää hubin ja native-noden (Ollama)
|
||||
# Käyttö: ./deploy-local.sh
|
||||
set -e
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
cleanup() { echo ""; echo "Pysäytetään..."; kill $HUB_PID $NODE_PID 2>/dev/null; exit 0; }
|
||||
trap cleanup INT TERM
|
||||
|
||||
# Portti vapaaksi
|
||||
lsof -ti:3000 | xargs kill -9 2>/dev/null || true
|
||||
|
||||
# Frontend
|
||||
echo "[1/3] Frontend..."
|
||||
cd "$SCRIPT_DIR/frontend"
|
||||
[ -d node_modules ] || npm install --silent
|
||||
npm run build 2>&1 | tail -1
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# Hub
|
||||
echo "[2/3] Hub..."
|
||||
STATIC_DIR="$SCRIPT_DIR/frontend/dist" cargo run -p hub 2>&1 &
|
||||
HUB_PID=$!
|
||||
until curl -sf http://localhost:3000 >/dev/null 2>&1; do sleep 1; done
|
||||
|
||||
# Native-node
|
||||
NODE_PID=""
|
||||
if curl -sf http://localhost:11434/api/tags >/dev/null 2>&1; then
|
||||
MODEL=$(curl -s http://localhost:11434/api/tags | python3 -c "
|
||||
import sys,json
|
||||
ms=json.load(sys.stdin).get('models',[])
|
||||
for m in ms:
|
||||
n=m['name']
|
||||
if '7b' in n and 'coder' in n: print(n); exit()
|
||||
for m in ms:
|
||||
if 'coder' in m['name']: print(m['name']); exit()
|
||||
if ms: print(ms[0]['name'])
|
||||
" 2>/dev/null)
|
||||
if [ -n "$MODEL" ]; then
|
||||
echo "[3/3] Native-node ($MODEL)..."
|
||||
HUB_URL=ws://localhost:3000/ws OLLAMA_MODEL="$MODEL" \
|
||||
cargo run -p native-node --no-default-features 2>&1 &
|
||||
NODE_PID=$!
|
||||
else
|
||||
echo "[3/3] Ollama: ei malleja (ollama pull qwen2.5-coder:7b)"
|
||||
fi
|
||||
else
|
||||
echo "[3/3] Ei Ollamaa — Wasm-fallback selaimessa"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== http://localhost:3000 === Ctrl+C pysäyttää"
|
||||
open http://localhost:3000 2>/dev/null || xdg-open http://localhost:3000 2>/dev/null || true
|
||||
wait $HUB_PID
|
||||
59
network-poc/deploy-remote.sh
Executable file
@@ -0,0 +1,59 @@
|
||||
#!/bin/bash
|
||||
# Kipinä Studio — tuotanto-deploy kipina.studioon
|
||||
# Buildaa Docker-imagen (frontend + hub + wasm) ja vie palvelimelle
|
||||
# Käyttö: ./deploy-remote.sh
|
||||
set -e
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
SERVER="ubuntu@86.50.252.98"
|
||||
REMOTE_DIR="~/code/agentic-studio/network-poc"
|
||||
SSH_OPTS="-o StrictHostKeyChecking=no"
|
||||
|
||||
# SSH-avain — yritetään yhdistää, jos ei onnistu, pyydetään avainta
|
||||
if ! ssh $SSH_OPTS "$SERVER" "echo ok" >/dev/null 2>&1; then
|
||||
echo "SSH-yhteys ei onnistu, lisätään avain..."
|
||||
ssh-add "$HOME/.ssh/id_rsa" 2>/dev/null || ssh-add
|
||||
fi
|
||||
|
||||
# Auto-commit
|
||||
if ! git diff --quiet HEAD 2>/dev/null || \
|
||||
[ -n "$(git ls-files --others --exclude-standard 2>/dev/null)" ]; then
|
||||
echo "Uncommitted muutoksia — commitoidaan..."
|
||||
read -rp " Commit-viesti: " msg
|
||||
[ -z "$msg" ] && msg="Deploy $(date +%Y-%m-%d\ %H:%M)"
|
||||
git add -A && git commit -m "$msg"
|
||||
fi
|
||||
|
||||
echo "=== Kipinä Studio Deploy → kipina.studio ==="
|
||||
|
||||
# 1. Docker-image (CACHEBUST pakottaa lähdekoodin uudelleenkopioinnin)
|
||||
echo "[1/4] Docker build..."
|
||||
docker build --platform linux/amd64 -f Dockerfile.prod \
|
||||
--build-arg CACHEBUST="$(git rev-parse HEAD)" \
|
||||
-t kipina-agentic:latest .
|
||||
|
||||
# 2. Pakkaus
|
||||
echo "[2/4] Pakataan..."
|
||||
docker save kipina-agentic:latest | gzip > /tmp/kipina-agentic.tar.gz
|
||||
echo " $(du -h /tmp/kipina-agentic.tar.gz | cut -f1)"
|
||||
|
||||
# 3. Siirto
|
||||
echo "[3/4] Siirretään..."
|
||||
scp $SSH_OPTS /tmp/kipina-agentic.tar.gz "$SERVER:/tmp/"
|
||||
scp $SSH_OPTS docker-compose.prod.yml Caddyfile.prod "$SERVER:$REMOTE_DIR/"
|
||||
|
||||
# 4. Käynnistys
|
||||
echo "[4/4] Käynnistetään..."
|
||||
ssh $SSH_OPTS "$SERVER" "gunzip -c /tmp/kipina-agentic.tar.gz | docker load && rm /tmp/kipina-agentic.tar.gz"
|
||||
ssh $SSH_OPTS "$SERVER" "cd $REMOTE_DIR && docker compose -f docker-compose.prod.yml down && docker compose -f docker-compose.prod.yml up -d"
|
||||
|
||||
# Discord
|
||||
WEBHOOK="https://discord.com/api/webhooks/1489504066898755687/8U02d0wug-3MkVax0xMmRoj0s_-V1psnNLPWdSOjnGnKRBUpPjaU6XiX9Iu8DgJI69AP"
|
||||
HASH=$(git log -1 --pretty=format:"%h" 2>/dev/null || echo "?")
|
||||
MSG=$(git log -1 --pretty=format:"%s" 2>/dev/null || echo "?")
|
||||
PAYLOAD=$(python3 -c "import json,sys; print(json.dumps({'content':sys.argv[1]}))" \
|
||||
"🚀 **Kipinä Studio julkaistu!** \`${HASH}\` ${MSG} https://kipina.studio")
|
||||
curl -sf -H "Content-Type: application/json" -d "$PAYLOAD" "$WEBHOOK" >/dev/null || true
|
||||
|
||||
echo "=== Valmis! https://kipina.studio ==="
|
||||
@@ -1,26 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
SERVER="ubuntu@86.50.252.98"
|
||||
REMOTE_DIR="~/code/agentic-studio/network-poc"
|
||||
SSH_OPTS="-o StrictHostKeyChecking=no"
|
||||
|
||||
echo "=== Kipinä Studio Deploy ==="
|
||||
|
||||
# 1. Rakennetaan Docker-image lokaalisti
|
||||
echo "[1/4] Rakennetaan image lokaalisti..."
|
||||
docker build -f Dockerfile.prod -t kipina-agentic:latest .
|
||||
|
||||
# 2. Tallennetaan ja siirretään
|
||||
echo "[2/4] Siirretään image palvelimelle..."
|
||||
docker save kipina-agentic:latest | gzip | ssh $SSH_OPTS $SERVER "gunzip | docker load"
|
||||
|
||||
# 3. Päivitetään konfiguraatiot
|
||||
echo "[3/4] Päivitetään konfiguraatiot..."
|
||||
scp $SSH_OPTS docker-compose.prod.yml Caddyfile.prod $SERVER:$REMOTE_DIR/
|
||||
|
||||
# 4. Käynnistetään uudelleen
|
||||
echo "[4/4] Käynnistetään palvelut..."
|
||||
ssh $SSH_OPTS $SERVER "cd $REMOTE_DIR && docker compose -f docker-compose.prod.yml up -d"
|
||||
|
||||
echo "=== Valmis! https://kipina.studio ==="
|
||||
@@ -1,13 +1,12 @@
|
||||
services:
|
||||
# NVIDIA GPU -solmu
|
||||
native-node-nvidia:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.native-node
|
||||
container_name: kipina-node-nvidia
|
||||
environment:
|
||||
- HUB_URL=wss://kipina.studio/ws
|
||||
- ALLOCATED_GB=4
|
||||
# Ollama NVIDIA GPU:lla
|
||||
ollama-nvidia:
|
||||
image: ollama/ollama:latest
|
||||
container_name: kipina-ollama
|
||||
ports:
|
||||
- "11434:11434"
|
||||
volumes:
|
||||
- ollama-models:/root/.ollama
|
||||
restart: unless-stopped
|
||||
deploy:
|
||||
resources:
|
||||
@@ -16,6 +15,65 @@ services:
|
||||
- driver: nvidia
|
||||
count: all
|
||||
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:
|
||||
- nvidia
|
||||
|
||||
@@ -27,14 +85,12 @@ services:
|
||||
container_name: kipina-node-amd
|
||||
environment:
|
||||
- HUB_URL=wss://kipina.studio/ws
|
||||
- OLLAMA_URL=http://ollama:11434
|
||||
- OLLAMA_MODEL=qwen2.5-coder:7b
|
||||
- ALLOCATED_GB=4
|
||||
restart: unless-stopped
|
||||
devices:
|
||||
- /dev/kfd:/dev/kfd
|
||||
- /dev/dri:/dev/dri
|
||||
group_add:
|
||||
- video
|
||||
- render
|
||||
depends_on:
|
||||
- ollama-amd
|
||||
profiles:
|
||||
- amd
|
||||
|
||||
@@ -46,7 +102,14 @@ services:
|
||||
container_name: kipina-node-cpu
|
||||
environment:
|
||||
- HUB_URL=wss://kipina.studio/ws
|
||||
- OLLAMA_URL=http://ollama:11434
|
||||
- OLLAMA_MODEL=qwen2.5-coder:7b
|
||||
- ALLOCATED_GB=2
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- ollama-cpu
|
||||
profiles:
|
||||
- cpu
|
||||
|
||||
volumes:
|
||||
ollama-models:
|
||||
|
||||
@@ -19,6 +19,9 @@ services:
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- DATABASE_PATH=/data/nodes.db
|
||||
- STATIC_DIR=/app/frontend/dist
|
||||
- ADMIN_PASSWORD=${ADMIN_PASSWORD:-}
|
||||
- NODE_API_KEY=${NODE_API_KEY:-}
|
||||
volumes:
|
||||
- hub_data:/data
|
||||
|
||||
|
||||
@@ -9,26 +9,39 @@ services:
|
||||
volumes:
|
||||
- .:/app
|
||||
# Käännetään aina käynnistyksen yhteydessä varmuuden vuoksi Wasm uusimmista koodeista, ja päälle pyöräytetään Hub!
|
||||
command: bash -c "cd node && wasm-pack build --target web --out-dir ../static/pkg && cd ../hub && cargo run"
|
||||
command: bash -c "cd node && wasm-pack build --release --target web --out-dir ../static/pkg && cd ../hub && cargo run"
|
||||
|
||||
# Valinnainen natiivi-solmu — kerää oikeat laitteistotiedot (nvidia-smi-taso)
|
||||
# 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:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.native-node
|
||||
container_name: kipina_native_node
|
||||
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
|
||||
depends_on:
|
||||
- agentic-poc
|
||||
# GPU passthrough (valinnainen — toimii myös ilman)
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: all
|
||||
capabilities: [gpu]
|
||||
- ollama
|
||||
profiles:
|
||||
- native
|
||||
|
||||
volumes:
|
||||
ollama-models:
|
||||
|
||||
3
network-poc/frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.astro/
|
||||
2
network-poc/frontend/astro.config.mjs
Normal file
@@ -0,0 +1,2 @@
|
||||
import { defineConfig } from 'astro/config';
|
||||
export default defineConfig({});
|
||||
4721
network-poc/frontend/package-lock.json
generated
Normal file
13
network-poc/frontend/package.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "kipina-frontend",
|
||||
"type": "module",
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"astro": "^6.1.5"
|
||||
}
|
||||
}
|
||||
595
network-poc/frontend/public/GUIDE.md
Normal file
@@ -0,0 +1,595 @@
|
||||
# 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:
|
||||
|
||||

|
||||
|
||||
**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.
|
||||
|
||||
---
|
||||
|
||||
## Rakennuspalaset vs. vapaa generointi
|
||||
|
||||
Kielimalli voi generoida koodia kahdella perustavanlaatuisesti eri tavalla.
|
||||
Ymmärtäminen milloin kumpikin toimii on avain luotettavaan koodigenerointi-pipelineen.
|
||||
|
||||
### Tapa 1: Vapaa generointi (naivi)
|
||||
|
||||
LLM generoi jokaisen tiedoston tyhjästä. Prompti kuvaa mitä halutaan,
|
||||
malli tuottaa koko tiedoston — importeista lähtien.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
P["Prompti"] --> LLM1["LLM: models.py"]
|
||||
LLM1 --> V1{"Validointi"}
|
||||
V1 -->|virhe| LLM1
|
||||
V1 -->|ok| LLM2["LLM: schemas.py"]
|
||||
LLM2 --> V2{"Validointi"}
|
||||
V2 -->|virhe| LLM2
|
||||
V2 -->|ok| LLM3["LLM: main.py"]
|
||||
LLM3 --> V3{"..."}
|
||||
|
||||
style V1 fill:#1a1e2e,stroke:#f85149,color:#c9d1d9
|
||||
style V2 fill:#1a1e2e,stroke:#f85149,color:#c9d1d9
|
||||
style V3 fill:#1a1e2e,stroke:#f85149,color:#c9d1d9
|
||||
```
|
||||
|
||||
**Ongelma:** Pieni malli (0.5B–7B) tekee toistuvia rakenteellisia virheitä:
|
||||
|
||||
| Virhe | Esiintymistiheys | Selitys |
|
||||
|-------|:---:|------|
|
||||
| Puuttuva import | ~60% | `from datetime import date` unohtuu |
|
||||
| SQLite `connect_args` | ~80% | Malli ei muista SQLite-erityisyyttä |
|
||||
| Väärä Enum-käyttö | ~50% | Sekoittaa `sqlalchemy.Enum` ja `enum.Enum` |
|
||||
| Poetry pyproject.toml:ssa | ~40% | Malli suosii Poetryä vaikka ohje sanoo uv |
|
||||
| Testit kopioivat koko appin | ~70% | Malli ei osaa importata, luo uudet reitit |
|
||||
|
||||
Retry-loopilla (virhe → uusi yritys virheviestin kanssa) osa korjautuu,
|
||||
mutta **sama malli toistaa samoja virheitä** koska ne johtuvat harjoitusdatasta.
|
||||
7 tiedoston projekti vaatii 7–14 LLM-kutsua ja 80–120 sekuntia.
|
||||
|
||||
### Tapa 2: Rakennuspalaset (template pipeline)
|
||||
|
||||
LLM:ltä pyydetään **vain JSON-speksi** — entiteetit, kentät ja tyypit.
|
||||
Koodi kootaan mekaanisesti valmiista pohjista joiden rakenne on todistettavasti
|
||||
oikein.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
P["Projektin kuvaus"] --> LLM["LLM: JSON-speksi"]
|
||||
LLM --> S["{ entities: [...] }"]
|
||||
S --> T1["Template: models.py"]
|
||||
S --> T2["Template: schemas.py"]
|
||||
S --> T3["Template: main.py"]
|
||||
S --> T4["Template: test_main.py"]
|
||||
S --> T5["Template: Dockerfile"]
|
||||
T1 & T2 & T3 & T4 & T5 --> D["Docker build + pytest"]
|
||||
|
||||
style LLM fill:#1a1e2e,stroke:#d29922,color:#c9d1d9
|
||||
style S fill:#1a1e2e,stroke:#3fb950,color:#c9d1d9
|
||||
style D fill:#1a1e2e,stroke:#58a6ff,color:#c9d1d9
|
||||
```
|
||||
|
||||
**Idea:** Malli on hyvä päättämään *mitä* (entiteetit, kentät), mutta huono
|
||||
muistamaan *miten* (importit, engine setup, testikonfiguraatio). Annetaan
|
||||
mallin tehdä se missä se on hyvä, ja hoidetaan loput mekaanisesti.
|
||||
|
||||
### LLM:n ainoa tehtävä
|
||||
|
||||
Malli tuottaa JSON-rakenteen kuten:
|
||||
|
||||
```json
|
||||
{
|
||||
"project_name": "todo-app",
|
||||
"entities": [
|
||||
{
|
||||
"name": "Todo",
|
||||
"table_name": "todos",
|
||||
"fields": [
|
||||
{"name": "title", "sa_type": "String(255)", "py_type": "str", "nullable": false},
|
||||
{"name": "due_date", "sa_type": "Date", "py_type": "date | None", "nullable": true},
|
||||
{"name": "status", "sa_type": "String(20)", "py_type": "str", "default": "pending"}
|
||||
]
|
||||
}
|
||||
],
|
||||
"extra_imports": ["from datetime import date"]
|
||||
}
|
||||
```
|
||||
|
||||
Tämä on yksinkertainen tehtävä jossa pienikin malli onnistuu luotettavasti:
|
||||
entiteettien tunnistus projektin kuvauksesta ja kenttätyyppien valinta.
|
||||
|
||||
Speksi sisältää myös **taulujen väliset yhteydet** (relationships):
|
||||
|
||||
```json
|
||||
{
|
||||
"entities": [
|
||||
{"name": "Author", "table_name": "authors", "fields": [...]},
|
||||
{"name": "Book", "table_name": "books", "fields": [
|
||||
{"name": "title", "sa_type": "String(255)", "py_type": "str", "nullable": false},
|
||||
{"name": "author_id", "sa_type": "Integer", "py_type": "int", "nullable": false}
|
||||
]}
|
||||
],
|
||||
"relationships": [
|
||||
{"from": "Book", "field": "author_id", "to": "Author", "type": "many-to-one"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Templateit generoivat yhteyksistä automaattisesti:
|
||||
- `ForeignKey('authors.id')` models.py:hin
|
||||
- `relationship("Book", back_populates="author")` molempiin suuntiin
|
||||
- `BookDetail`-schema jossa author-data mukana
|
||||
- `GET /authors/{id}/books/` nested endpoint
|
||||
- FK-validointi: 404 jos parent-entiteettiä ei ole
|
||||
|
||||
### Architect-agentti: speksin laatu ratkaisee
|
||||
|
||||
Arkkitehti on **kriittisin agentti** koko pipelinessa. Jos speksi on hyvä
|
||||
(oikeat taulut, kentät, yhteydet), kaikki muu seuraa automaattisesti.
|
||||
Jos speksi on huono, templateitkaan eivät pelasta.
|
||||
|
||||
Arkkitehtia ohjataan:
|
||||
1. **Chain-of-thought**: "Mieti ensin taulut, sitten kentät, sitten yhteydet"
|
||||
2. **Domain-esimerkit**: Todo, verkkokauppa, blogi — malli näkee miltä hyvä speksi näyttää
|
||||
3. **Anti-patternit**: "Ei turhia ID-kenttiä, ei Enumeita, ei suomenkielisiä nimiä koodissa"
|
||||
4. **Yhteyssäännöt**: "Jokainen `_id`-kenttä tarvitsee vastaavan relationship-merkinnän"
|
||||
|
||||
Isompi malli (tai API) tässä yhdessä kohdassa parantaa kaikkien projektien laatua
|
||||
koska speksi on ainoa paikka jossa LLM:n ymmärrys vaikuttaa.
|
||||
|
||||
### Template täyttää loput
|
||||
|
||||
Jokainen template on kuin madlib — aukot täytetään speksin datalla:
|
||||
|
||||
**models.py template (yksinkertaistettu):**
|
||||
```python
|
||||
from sqlalchemy import create_engine, Column, Integer, {sa_types}, ForeignKey
|
||||
from sqlalchemy.orm import sessionmaker, relationship
|
||||
# ... aina samat importit, engine setup, SessionLocal ...
|
||||
|
||||
class {entity.name}(Base):
|
||||
__tablename__ = "{entity.table_name}"
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
{field.name} = Column({field.sa_type}, nullable={field.nullable})
|
||||
# FK-kentät: ForeignKey + relationship automaattisesti
|
||||
{fk_field} = Column(Integer, ForeignKey('{parent_table}.id'))
|
||||
{parent_lower} = relationship("{Parent}", back_populates="{children}")
|
||||
```
|
||||
|
||||
Tulos: importit ovat aina oikein, `connect_args` on aina mukana,
|
||||
taulujen yhteydet generoituvat oikein, testit importoivat `main.py`:stä eivätkä kopioi sitä.
|
||||
|
||||
### Vertailu: mittaustulokset
|
||||
|
||||
| | Vapaa generointi | Rakennuspalaset |
|
||||
|---|:---:|:---:|
|
||||
| LLM-kutsuja | 7–14 | **3** (speksi + requirements + README) |
|
||||
| Aika | 80–120s | **~25s** |
|
||||
| Syntaksi OK | ~70% | **100%** |
|
||||
| Docker build | vaihteleva | **100%** |
|
||||
| Pytest läpi | 0% | **100%** |
|
||||
| API toimii | ~30% | **100%** |
|
||||
| Taulujen yhteydet (FK) | ei koskaan | **100%** |
|
||||
| Nested endpointit | ei koskaan | **automaattisesti** |
|
||||
|
||||
### Milloin kumpikin toimii
|
||||
|
||||
**Rakennuspalaset** kun:
|
||||
- Projektin rakenne on tunnettu (FastAPI + SQLAlchemy CRUD)
|
||||
- Laatu ja luotettavuus ovat tärkeitä
|
||||
- Malli on pieni (0.5B–7B)
|
||||
|
||||
**Vapaa generointi** kun:
|
||||
- Projektin rakenne on epätavallinen
|
||||
- Tarvitaan custom-logiikkaa jota template ei kata
|
||||
- Malli on riittävän iso (>70B tai pilvi-API)
|
||||
|
||||
Paras lopputulos syntyy yhdistelmällä: **rakennuspalaset perusrakenteelle,
|
||||
vapaa generointi business-logiikalle**.
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
34
network-poc/frontend/public/avatars/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Kipinä Agentic Playground - Animaatioiden käyttöönotto
|
||||
|
||||
Koska Kipinä-verkon agenttien avatarit tällä erää ovat staattisia PNG-kuvatiedostoja, käyttöliittymä hyödyntää CSS-pohjaista pomppimisilmiötä (sekä pulppuavaa 💬 puhekuplaa) "puhumisen" merkkinä. Olemme kuitenkin koodanneet taustalle piilotetun tuen aivioiduille videoloopeille myöhempää käyttöä varten!
|
||||
|
||||
Näin saat UI:n tukemaan oikeasti animoituja kasvoja/videoita.
|
||||
|
||||
## 1. Luo Animoidut GIF-tiedostot
|
||||
Valitse mikä tahansa ulkoinen AI-työkalu (kuten HeyGen, Pika v1.0, tai Midjourney+Runway yhdistelmä) ja muunna avatar-kuvat (esim. `kettu_notext.png`) 3-5 sekunnin kestäviksi GIF-loopeiksi. Hahmon leuka tulisi pyöriä tai naama vääntyillä puhuessaan.
|
||||
|
||||
## 2. Nimeä Tiedostot Oikein ja Lisää Ne Kansioon
|
||||
Siirrä uudet GIF-animaatiot samaan kansioon alkuperäisten kuvien kanssa. Muuta niiden nimi siten, että se päättyy tunnisteeseen `_puhuva.gif`.
|
||||
|
||||
Esimerkkejä:
|
||||
- Koodari `kipina_notext.png` → `kipina_notext_puhuva.gif`
|
||||
- Manageri `karhunpentu.png` → `karhunpentu_puhuva.gif`
|
||||
- Asiakas `kettu_notext.png` → `kettu_notext_puhuva.gif`
|
||||
|
||||
## 3. Aktivoi Koodi
|
||||
Käännä Kipinä Playground -ohjaimen JavaScript-koodista piilotettu ominaisuus päälle.
|
||||
|
||||
Etsi tiedostosta `../index.html` (noin riviltä 1084, `updatePromptEditor`-funktiosta):
|
||||
```javascript
|
||||
// Piilotettu ominaisuus: Puhuvien videoiden / gif-animaatioiden kytkentä
|
||||
window.USE_ANIMATED_GIFS = false;
|
||||
```
|
||||
Muuta tuo `false` arvoon `true`:
|
||||
```javascript
|
||||
window.USE_ANIMATED_GIFS = true;
|
||||
```
|
||||
|
||||
**Mitä logiikka tekee?**
|
||||
Aina kun valitset agentin kaaviosta, koodi korvaa aktiivisen kuvakkeen lopussa olevan `.png` -päätteen sanalla `_puhuva.gif` – lennosta! Jos poistut agentin valinnasta tai valitset jonkun toisen, koodi vaihtaa kuvan välittömästi takaisin staattiseen `.png`-versioon ja sulkee ilmentymän suun.
|
||||
|
||||
Näin saat kaikkien asiantuntijoiden face-track looppeja hallittua yhdellä kädenkäänteellä.
|
||||
BIN
network-poc/frontend/public/avatars/aikuinen_susi.webp
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
network-poc/frontend/public/avatars/bear.webp
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
network-poc/frontend/public/avatars/beaver.webp
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
network-poc/frontend/public/avatars/chameleon.webp
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
network-poc/frontend/public/avatars/elephant.webp
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
network-poc/frontend/public/avatars/gecko.webp
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
network-poc/frontend/public/avatars/gecko_notext.webp
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
network-poc/frontend/public/avatars/karhunpentu.webp
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
network-poc/frontend/public/avatars/kettu_notext.webp
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
network-poc/frontend/public/avatars/kipina_notext.webp
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
network-poc/frontend/public/avatars/laiskiainen.webp
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
network-poc/frontend/public/avatars/laiskiainen_notext.webp
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
network-poc/frontend/public/avatars/lion.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
network-poc/frontend/public/avatars/mantis.webp
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
network-poc/frontend/public/avatars/owl.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
network-poc/frontend/public/avatars/penguin.webp
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
network-poc/frontend/public/avatars/pesukarhu.webp
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
network-poc/frontend/public/avatars/pesukarhu_notext.webp
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
network-poc/frontend/public/avatars/serpent.webp
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
network-poc/frontend/public/avatars/spider.webp
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
network-poc/frontend/public/avatars/susi_notext.webp
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
network-poc/frontend/public/avatars/tortoise.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
network-poc/frontend/public/avatars/walrus.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
1
network-poc/frontend/public/download/.build-hash
Normal file
@@ -0,0 +1 @@
|
||||
cf3bf54
|
||||
BIN
network-poc/frontend/public/download/kipina-node-linux-arm64
Executable file
BIN
network-poc/frontend/public/download/kipina-node-linux-x86_64
Executable file
BIN
network-poc/frontend/public/download/kipina-node-macos-arm64
Executable file
BIN
network-poc/frontend/public/download/kipina-node-windows-x86_64.exe
Executable file
BIN
network-poc/frontend/public/forge_hero.webp
Normal file
|
After Width: | Height: | Size: 91 KiB |
BIN
network-poc/frontend/public/gecko_hero.webp
Normal file
|
After Width: | Height: | Size: 105 KiB |
73
network-poc/frontend/public/join.sh
Normal file
@@ -0,0 +1,73 @@
|
||||
#!/bin/bash
|
||||
# Kipinä — liitä koneesi laskentaverkkoon
|
||||
set -e
|
||||
|
||||
HUB_URL="${KIPINA_HUB:-wss://kipina.studio/ws}"
|
||||
MODEL="${KIPINA_MODEL:-qwen2.5-coder:3b}"
|
||||
|
||||
echo ""
|
||||
echo " ╔══════════════════════════════════════╗"
|
||||
echo " ║ Kipinä Agentic Network — Node Join ║"
|
||||
echo " ╚══════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# 1. Ollama
|
||||
if command -v ollama &>/dev/null; then
|
||||
echo " ✓ Ollama löytyi: $(ollama --version 2>/dev/null || echo 'asennettu')"
|
||||
else
|
||||
echo " Ollama ei ole asennettu."
|
||||
echo ""
|
||||
read -p " Asennetaanko Ollama? (k/e) " -n 1 -r; echo
|
||||
if [[ $REPLY =~ ^[Kk]$ ]]; then
|
||||
echo " Asennetaan Ollama..."
|
||||
curl -fsSL https://ollama.ai/install.sh | sh
|
||||
else
|
||||
echo " Ollama vaaditaan laskentaan. Asenna: https://ollama.ai"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 2. Varmistetaan että Ollama on käynnissä
|
||||
if ! curl -s http://localhost:11434/api/tags &>/dev/null; then
|
||||
echo " Käynnistetään Ollama..."
|
||||
ollama serve &>/dev/null &
|
||||
sleep 3
|
||||
if ! curl -s http://localhost:11434/api/tags &>/dev/null; then
|
||||
echo " ✗ Ollama ei käynnistynyt. Aja: ollama serve"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
echo " ✓ Ollama käynnissä"
|
||||
|
||||
# 3. Malli
|
||||
if ollama list 2>/dev/null | grep -q "$MODEL"; then
|
||||
echo " ✓ Malli $MODEL ladattu"
|
||||
else
|
||||
echo " Ladataan malli $MODEL..."
|
||||
ollama pull "$MODEL"
|
||||
fi
|
||||
|
||||
# 4. Native-node
|
||||
echo ""
|
||||
echo " Yhdistetään hubiin: $HUB_URL"
|
||||
echo " Malli: $MODEL"
|
||||
echo " Ctrl+C pysäyttää"
|
||||
echo ""
|
||||
|
||||
# Tarkistetaan onko native-node käännetty
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
NATIVE_BIN="$SCRIPT_DIR/target/release/native-node"
|
||||
|
||||
if [ -f "$NATIVE_BIN" ]; then
|
||||
HUB_URL="$HUB_URL" OLLAMA_MODEL="$MODEL" "$NATIVE_BIN"
|
||||
elif command -v cargo &>/dev/null && [ -f "$SCRIPT_DIR/native-node/Cargo.toml" ]; then
|
||||
echo " Käännetään native-node..."
|
||||
cd "$SCRIPT_DIR"
|
||||
cargo build --release -p native-node --no-default-features 2>&1 | tail -1
|
||||
HUB_URL="$HUB_URL" OLLAMA_MODEL="$MODEL" "$NATIVE_BIN"
|
||||
else
|
||||
echo " ✗ native-node binääriä ei löydy eikä Rust ole asennettu."
|
||||
echo " Asenna Rust: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"
|
||||
echo " Tai lataa valmis binääri: https://kipina.studio/download"
|
||||
exit 1
|
||||
fi
|
||||
135
network-poc/frontend/public/kipina-node
Normal file
@@ -0,0 +1,135 @@
|
||||
#!/bin/bash
|
||||
# Kipinä Node — lataa oikea binääri ja käynnistä
|
||||
set -e
|
||||
|
||||
BASE_URL="https://kipina.studio/download"
|
||||
HUB_URL="${KIPINA_HUB:-wss://kipina.studio/ws}"
|
||||
OLLAMA_URL="${OLLAMA_URL:-http://localhost:11434}"
|
||||
|
||||
# Tunnista OS ja arkkitehtuuri
|
||||
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
ARCH=$(uname -m)
|
||||
|
||||
case "$OS-$ARCH" in
|
||||
darwin-arm64) BINARY="kipina-node-macos-arm64" ;;
|
||||
darwin-x86_64) BINARY="kipina-node-macos-arm64" ;; # Rosetta
|
||||
linux-x86_64) BINARY="kipina-node-linux-x86_64" ;;
|
||||
linux-aarch64) BINARY="kipina-node-linux-arm64" ;;
|
||||
*) echo "Ei tuettu: $OS-$ARCH"; exit 1 ;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
echo " ╔══════════════════════════════════════╗"
|
||||
echo " ║ Kipinä Agentic Node ║"
|
||||
echo " ╚══════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo " OS: $OS ($ARCH)"
|
||||
echo ""
|
||||
|
||||
# Etsi Ollama-instanssit
|
||||
CANDIDATES=(
|
||||
"http://localhost:11434"
|
||||
"http://127.0.0.1:11434"
|
||||
"http://ollama:11434"
|
||||
"http://host.docker.internal:11434"
|
||||
)
|
||||
|
||||
# Lisää OLLAMA_URL listaan jos asetettu ja ei jo mukana
|
||||
if [ -n "$OLLAMA_URL" ]; then
|
||||
ALREADY=false
|
||||
for c in "${CANDIDATES[@]}"; do
|
||||
[ "$c" = "$OLLAMA_URL" ] && ALREADY=true
|
||||
done
|
||||
$ALREADY || CANDIDATES=("$OLLAMA_URL" "${CANDIDATES[@]}")
|
||||
fi
|
||||
|
||||
echo " Etsitään Ollama-instansseja..."
|
||||
FOUND=()
|
||||
for url in "${CANDIDATES[@]}"; do
|
||||
if curl -s --connect-timeout 1 "$url/api/tags" &>/dev/null; then
|
||||
FOUND+=("$url")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#FOUND[@]} -eq 0 ]; then
|
||||
# Ei löytynyt — yritä käynnistää lokaali
|
||||
if command -v ollama &>/dev/null; then
|
||||
echo " Käynnistetään Ollama..."
|
||||
ollama serve &>/dev/null &
|
||||
sleep 3
|
||||
if curl -s --connect-timeout 1 "http://localhost:11434/api/tags" &>/dev/null; then
|
||||
OLLAMA_URL="http://localhost:11434"
|
||||
echo " ✓ Ollama käynnistetty ($OLLAMA_URL)"
|
||||
else
|
||||
echo " ✗ Ollaman käynnistys epäonnistui."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo ""
|
||||
echo " ✗ Ollamaa ei löytynyt."
|
||||
echo " Kontti/remote: OLLAMA_URL=http://HOST:11434 ./kipina-node"
|
||||
echo " Asenna: curl -fsSL https://ollama.ai/install.sh | sh"
|
||||
exit 1
|
||||
fi
|
||||
elif [ ${#FOUND[@]} -eq 1 ]; then
|
||||
OLLAMA_URL="${FOUND[0]}"
|
||||
echo " ✓ Ollama löytyi: $OLLAMA_URL"
|
||||
else
|
||||
echo ""
|
||||
echo " Löytyi ${#FOUND[@]} Ollama-instanssia:"
|
||||
echo ""
|
||||
for i in "${!FOUND[@]}"; do
|
||||
echo " $((i+1))) ${FOUND[$i]}"
|
||||
done
|
||||
echo ""
|
||||
read -p " Valitse [1-${#FOUND[@]}]: " -r CHOICE
|
||||
if [[ "$CHOICE" =~ ^[0-9]+$ ]] && [ "$CHOICE" -ge 1 ] && [ "$CHOICE" -le ${#FOUND[@]} ]; then
|
||||
OLLAMA_URL="${FOUND[$((CHOICE-1))]}"
|
||||
else
|
||||
OLLAMA_URL="${FOUND[0]}"
|
||||
echo " Käytetään oletusta: $OLLAMA_URL"
|
||||
fi
|
||||
echo " ✓ Valittu: $OLLAMA_URL"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " Hub: $HUB_URL"
|
||||
echo " Ollama: $OLLAMA_URL"
|
||||
if [ -n "$KIPINA_MODEL" ]; then
|
||||
echo " Malli: $KIPINA_MODEL (Ympäristömuuttujasta)"
|
||||
fi
|
||||
|
||||
# Binäärin automaattinen päivitys — vertaa build-hashia palvelimeen
|
||||
BIN_PATH="./kipina-node-bin"
|
||||
HASH_PATH="./kipina-node-bin.hash"
|
||||
|
||||
REMOTE_HASH=$(curl -sSL "$BASE_URL/.build-hash?v=$(date +%s)" 2>/dev/null | tr -d '[:space:]')
|
||||
LOCAL_HASH=""
|
||||
[ -f "$HASH_PATH" ] && LOCAL_HASH=$(cat "$HASH_PATH" | tr -d '[:space:]')
|
||||
|
||||
if [ -f "$BIN_PATH" ] && [ -n "$REMOTE_HASH" ] && [ "$REMOTE_HASH" = "$LOCAL_HASH" ]; then
|
||||
echo " ✓ Binääri ajan tasalla (versio: $LOCAL_HASH)"
|
||||
else
|
||||
if [ -f "$BIN_PATH" ]; then
|
||||
echo " ↻ Uusi versio saatavilla ($LOCAL_HASH → $REMOTE_HASH)"
|
||||
else
|
||||
echo " Ladataan $BINARY..."
|
||||
fi
|
||||
rm -f "$BIN_PATH"
|
||||
curl -sSL "$BASE_URL/$BINARY?v=$(date +%s)" -o "$BIN_PATH"
|
||||
chmod +x "$BIN_PATH"
|
||||
echo "$REMOTE_HASH" > "$HASH_PATH"
|
||||
echo " ✓ Päivitetty versioon $REMOTE_HASH"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " ✓ Siirrytään Kipinä Noden hallintaan..."
|
||||
echo " Ctrl+C pysäyttää"
|
||||
echo ""
|
||||
|
||||
if [ -n "$KIPINA_MODEL" ]; then
|
||||
export OLLAMA_MODEL="$KIPINA_MODEL"
|
||||
fi
|
||||
export HUB_URL="$HUB_URL"
|
||||
export OLLAMA_URL="$OLLAMA_URL"
|
||||
exec "$BIN_PATH"
|
||||
63
network-poc/frontend/public/pkg/node.d.ts
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
export function set_auto_tasks(enabled: boolean): void;
|
||||
|
||||
export function set_gpu_load(load: number): void;
|
||||
|
||||
export function start_agent_node(hub_url: string, has_webgpu: boolean, device_info_json: string, task_id: number): Promise<void>;
|
||||
|
||||
/**
|
||||
* JS-exportti: tokenisoi tekstin ja palauttaa JSON-merkkijonon
|
||||
* Tokenizer ladataan IndexedDB:stä (täytyy olla ladattu aiemmin)
|
||||
*/
|
||||
export function tokenize_js(text: string): Promise<string>;
|
||||
|
||||
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
||||
|
||||
export interface InitOutput {
|
||||
readonly memory: WebAssembly.Memory;
|
||||
readonly set_auto_tasks: (a: number) => void;
|
||||
readonly set_gpu_load: (a: number) => void;
|
||||
readonly start_agent_node: (a: number, b: number, c: number, d: number, e: number, f: number) => any;
|
||||
readonly tokenize_js: (a: number, b: number) => any;
|
||||
readonly wasm_bindgen__convert__closures_____invoke__h6ec112f0342d232e: (a: number, b: number, c: any) => [number, number];
|
||||
readonly wasm_bindgen__convert__closures_____invoke__h737e63bacb96714d: (a: number, b: number, c: any, d: any) => void;
|
||||
readonly wasm_bindgen__convert__closures_____invoke__ha390eb51fa5285b4: (a: number, b: number, c: any) => void;
|
||||
readonly wasm_bindgen__convert__closures_____invoke__h9cacd8a9a6ca46c2: (a: number, b: number, c: any) => void;
|
||||
readonly wasm_bindgen__convert__closures_____invoke__ha390eb51fa5285b4_3: (a: number, b: number, c: any) => void;
|
||||
readonly wasm_bindgen__convert__closures_____invoke__h0afc19def95e993a: (a: number, b: number, c: any) => void;
|
||||
readonly wasm_bindgen__convert__closures_____invoke__h0afc19def95e993a_5: (a: number, b: number, c: any) => void;
|
||||
readonly wasm_bindgen__convert__closures_____invoke__h698aa4c8c2e7db1b: (a: number, b: number) => void;
|
||||
readonly __wbindgen_malloc: (a: number, b: number) => number;
|
||||
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||
readonly __wbindgen_exn_store: (a: number) => void;
|
||||
readonly __externref_table_alloc: () => number;
|
||||
readonly __wbindgen_externrefs: WebAssembly.Table;
|
||||
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
|
||||
readonly __wbindgen_destroy_closure: (a: number, b: number) => void;
|
||||
readonly __externref_table_dealloc: (a: number) => void;
|
||||
readonly __wbindgen_start: () => void;
|
||||
}
|
||||
|
||||
export type SyncInitInput = BufferSource | WebAssembly.Module;
|
||||
|
||||
/**
|
||||
* Instantiates the given `module`, which can either be bytes or
|
||||
* a precompiled `WebAssembly.Module`.
|
||||
*
|
||||
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
|
||||
*
|
||||
* @returns {InitOutput}
|
||||
*/
|
||||
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
|
||||
|
||||
/**
|
||||
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
|
||||
* for everything else, calls `WebAssembly.instantiate` directly.
|
||||
*
|
||||
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
|
||||
*
|
||||
* @returns {Promise<InitOutput>}
|
||||
*/
|
||||
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;
|
||||
1741
network-poc/frontend/public/pkg/node.js
Normal file
BIN
network-poc/frontend/public/pkg/node_bg.wasm
Normal file
24
network-poc/frontend/public/pkg/node_bg.wasm.d.ts
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export const memory: WebAssembly.Memory;
|
||||
export const set_auto_tasks: (a: number) => void;
|
||||
export const set_gpu_load: (a: number) => void;
|
||||
export const start_agent_node: (a: number, b: number, c: number, d: number, e: number, f: number) => any;
|
||||
export const tokenize_js: (a: number, b: number) => any;
|
||||
export const wasm_bindgen__convert__closures_____invoke__h6ec112f0342d232e: (a: number, b: number, c: any) => [number, number];
|
||||
export const wasm_bindgen__convert__closures_____invoke__h737e63bacb96714d: (a: number, b: number, c: any, d: any) => void;
|
||||
export const wasm_bindgen__convert__closures_____invoke__ha390eb51fa5285b4: (a: number, b: number, c: any) => void;
|
||||
export const wasm_bindgen__convert__closures_____invoke__h9cacd8a9a6ca46c2: (a: number, b: number, c: any) => void;
|
||||
export const wasm_bindgen__convert__closures_____invoke__ha390eb51fa5285b4_3: (a: number, b: number, c: any) => void;
|
||||
export const wasm_bindgen__convert__closures_____invoke__h0afc19def95e993a: (a: number, b: number, c: any) => void;
|
||||
export const wasm_bindgen__convert__closures_____invoke__h0afc19def95e993a_5: (a: number, b: number, c: any) => void;
|
||||
export const wasm_bindgen__convert__closures_____invoke__h698aa4c8c2e7db1b: (a: number, b: number) => void;
|
||||
export const __wbindgen_malloc: (a: number, b: number) => number;
|
||||
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||
export const __wbindgen_exn_store: (a: number) => void;
|
||||
export const __externref_table_alloc: () => number;
|
||||
export const __wbindgen_externrefs: WebAssembly.Table;
|
||||
export const __wbindgen_free: (a: number, b: number, c: number) => void;
|
||||
export const __wbindgen_destroy_closure: (a: number, b: number) => void;
|
||||
export const __externref_table_dealloc: (a: number) => void;
|
||||
export const __wbindgen_start: () => void;
|
||||
15
network-poc/frontend/public/pkg/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "node",
|
||||
"type": "module",
|
||||
"version": "0.1.0",
|
||||
"files": [
|
||||
"node_bg.wasm",
|
||||
"node.js",
|
||||
"node.d.ts"
|
||||
],
|
||||
"main": "node.js",
|
||||
"types": "node.d.ts",
|
||||
"sideEffects": [
|
||||
"./snippets/*"
|
||||
]
|
||||
}
|
||||
BIN
network-poc/frontend/public/serpent_hero.webp
Normal file
|
After Width: | Height: | Size: 79 KiB |
33
network-poc/frontend/public/templates/data-analytics.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "Data Analytics Pipeline",
|
||||
"description": "ETL, analysis, and visualization with Docker (MariaDB + Jupyter)",
|
||||
"keywords": ["data", "analytics", "csv", "etl", "visualization", "statistics", "dashboard", "jupyter", "pandas", "matplotlib"],
|
||||
"files": {
|
||||
"etl.py": {
|
||||
"description": "Data loading, cleaning, and transformation",
|
||||
"example": "import pandas as pd\nfrom pathlib import Path\nfrom sqlalchemy import create_engine\n\nDB_URL = \"mysql+pymysql://root:secret@localhost:3306/analytics\"\nengine = create_engine(DB_URL)\n\ndef load_csv(path: str) -> pd.DataFrame:\n df = pd.read_csv(path)\n print(f\"Loaded {len(df)} rows from {path}\")\n return df\n\ndef clean(df: pd.DataFrame) -> pd.DataFrame:\n df = df.dropna(subset=[\"x\", \"y\"])\n df = df[(df[\"x\"] >= 0) & (df[\"y\"] >= 0)] # Remove outliers\n df[\"timestamp\"] = pd.to_datetime(df[\"timestamp\"])\n return df.sort_values(\"timestamp\").reset_index(drop=True)\n\ndef to_database(df: pd.DataFrame, table: str):\n df.to_sql(table, engine, if_exists=\"replace\", index=False)\n print(f\"Wrote {len(df)} rows to {table}\")\n\nif __name__ == \"__main__\":\n for csv_file in sorted(Path(\"data\").glob(\"*.csv\")):\n df = load_csv(str(csv_file))\n df = clean(df)\n to_database(df, \"measurements\")",
|
||||
"instructions": "Write the ETL pipeline:\n- Load CSV files from data/ directory using pandas\n- Clean: remove nulls, filter outliers, parse timestamps\n- Transform: convert units, compute derived columns\n- Load into MariaDB via SQLAlchemy\n- Make it runnable as a standalone script"
|
||||
},
|
||||
"analysis.py": {
|
||||
"description": "Statistical analysis and metrics computation",
|
||||
"example": "import pandas as pd\nfrom sqlalchemy import create_engine\n\nDB_URL = \"mysql+pymysql://root:secret@localhost:3306/analytics\"\nengine = create_engine(DB_URL)\n\ndef load_data() -> pd.DataFrame:\n return pd.read_sql(\"SELECT * FROM measurements\", engine)\n\ndef summary_stats(df: pd.DataFrame) -> dict:\n return {\n \"total_rows\": len(df),\n \"date_range\": f\"{df['timestamp'].min()} to {df['timestamp'].max()}\",\n \"unique_entities\": df[\"entity_id\"].nunique(),\n }\n\ndef hourly_distribution(df: pd.DataFrame) -> pd.DataFrame:\n df[\"hour\"] = df[\"timestamp\"].dt.hour\n return df.groupby(\"hour\").size().reset_index(name=\"count\")\n\nif __name__ == \"__main__\":\n df = load_data()\n stats = summary_stats(df)\n for k, v in stats.items():\n print(f\"{k}: {v}\")",
|
||||
"instructions": "Write analysis functions:\n- Load cleaned data from MariaDB\n- Compute summary statistics (counts, date ranges, distributions)\n- Time-based analysis (hourly, daily, weekly patterns)\n- Group-level metrics (per entity, per zone)\n- Return DataFrames and dicts suitable for visualization"
|
||||
},
|
||||
"visualize.py": {
|
||||
"description": "Charts and visualizations with matplotlib",
|
||||
"example": "import matplotlib.pyplot as plt\nimport pandas as pd\nfrom analysis import load_data, hourly_distribution\n\ndef plot_heatmap(df: pd.DataFrame, title: str, output: str):\n fig, ax = plt.subplots(figsize=(12, 8))\n scatter = ax.scatter(df[\"x\"], df[\"y\"], c=df[\"density\"], cmap=\"hot\", alpha=0.5, s=2)\n ax.set_title(title)\n ax.set_xlabel(\"x\")\n ax.set_ylabel(\"y\")\n ax.invert_yaxis()\n plt.colorbar(scatter, label=\"Density\")\n plt.tight_layout()\n plt.savefig(output, dpi=150)\n print(f\"Saved {output}\")\n\ndef plot_bar(df: pd.DataFrame, x: str, y: str, title: str, output: str):\n fig, ax = plt.subplots(figsize=(10, 5))\n ax.bar(df[x], df[y], color=\"steelblue\")\n ax.set_title(title)\n ax.set_xlabel(x)\n ax.set_ylabel(y)\n plt.tight_layout()\n plt.savefig(output, dpi=150)\n\nif __name__ == \"__main__\":\n df = load_data()\n hourly = hourly_distribution(df)\n plot_bar(hourly, \"hour\", \"count\", \"Hourly Distribution\", \"output/hourly.png\")",
|
||||
"instructions": "Write visualization functions:\n- Import analysis functions for data\n- Heatmaps, bar charts, line charts as appropriate\n- Save figures to output/ directory (PNG, 150 DPI)\n- Use matplotlib with clear titles, labels, colorbars\n- Make it runnable as standalone to generate all charts"
|
||||
},
|
||||
"docker-compose.yml": {
|
||||
"description": "Docker Compose stack for database and Jupyter",
|
||||
"example": "services:\n db:\n image: mariadb:11\n environment:\n MYSQL_ROOT_PASSWORD: secret\n MYSQL_DATABASE: analytics\n ports:\n - \"3306:3306\"\n volumes:\n - db_data:/var/lib/mysql\n\n jupyter:\n image: jupyter/scipy-notebook:latest\n ports:\n - \"8888:8888\"\n volumes:\n - .:/home/jovyan/work\n environment:\n JUPYTER_TOKEN: kipina\n depends_on:\n - db\n\nvolumes:\n db_data:",
|
||||
"instructions": "Write docker-compose.yml:\n- MariaDB service with persistent volume\n- JupyterLab service with project mounted\n- Correct environment variables\n- Port mappings for local development\n- Write ONLY the YAML, no explanations"
|
||||
},
|
||||
"pyproject.toml": {
|
||||
"description": "Project dependencies",
|
||||
"example": "[project]\nname = \"analytics\"\nversion = \"0.1.0\"\nrequires-python = \">=3.11\"\ndependencies = [\n \"pandas\",\n \"matplotlib\",\n \"sqlalchemy\",\n \"pymysql\",\n]\n\n[project.scripts]\netl = \"python etl.py\"\nanalyze = \"python analysis.py\"\nvisualize = \"python visualize.py\"",
|
||||
"instructions": "Use [project] format (PEP 621). List all data science dependencies. Add scripts for ETL, analysis, and visualization."
|
||||
}
|
||||
},
|
||||
"order": ["etl.py", "analysis.py", "visualize.py", "docker-compose.yml", "pyproject.toml"]
|
||||
}
|
||||
28
network-poc/frontend/public/templates/fastapi-crud.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "FastAPI CRUD",
|
||||
"description": "REST API with SQLite database",
|
||||
"keywords": ["api", "rest", "crud", "endpoint", "fastapi", "web", "backend", "server", "database", "sqlite"],
|
||||
"files": {
|
||||
"models.py": {
|
||||
"description": "SQLAlchemy models, engine, and session",
|
||||
"example": "from sqlalchemy import create_engine, Column, Integer, String\nfrom sqlalchemy.ext.declarative import declarative_base\nfrom sqlalchemy.orm import sessionmaker\n\nDATABASE_URL = \"sqlite:///./app.db\"\nengine = create_engine(DATABASE_URL, connect_args={\"check_same_thread\": False})\nSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)\nBase = declarative_base()\n\nclass Item(Base):\n __tablename__ = \"items\"\n id = Column(Integer, primary_key=True, index=True)\n name = Column(String(100), nullable=False)\n description = Column(String(500))",
|
||||
"instructions": "Define the SQLAlchemy model based on the project description. Always include:\n- engine with check_same_thread=False for SQLite\n- SessionLocal with autocommit=False\n- Base = declarative_base()\n- Model class with __tablename__, primary key, and fields"
|
||||
},
|
||||
"schemas.py": {
|
||||
"description": "Pydantic request/response schemas",
|
||||
"example": "from pydantic import BaseModel\n\nclass ItemCreate(BaseModel):\n name: str\n description: str | None = None\n\nclass ItemResponse(ItemCreate):\n id: int\n\n class Config:\n from_attributes = True",
|
||||
"instructions": "Create Pydantic schemas that match the SQLAlchemy model:\n- Create schema: fields without id (user provides these)\n- Response schema: inherits from Create, adds id\n- Add class Config with from_attributes = True (required for SQLAlchemy ORM)"
|
||||
},
|
||||
"main.py": {
|
||||
"description": "FastAPI app with CRUD endpoints",
|
||||
"example": "from fastapi import FastAPI, Depends, HTTPException\nfrom sqlalchemy.orm import Session\nfrom models import Base, engine, SessionLocal, Item\nfrom schemas import ItemCreate, ItemResponse\n\nBase.metadata.create_all(bind=engine)\napp = FastAPI()\n\ndef get_db():\n db = SessionLocal()\n try:\n yield db\n finally:\n db.close()\n\n@app.post(\"/items/\", response_model=ItemResponse, status_code=201)\ndef create_item(item: ItemCreate, db: Session = Depends(get_db)):\n db_item = Item(**item.model_dump())\n db.add(db_item)\n db.commit()\n db.refresh(db_item)\n return db_item\n\n@app.get(\"/items/\", response_model=list[ItemResponse])\ndef list_items(db: Session = Depends(get_db)):\n return db.query(Item).all()\n\n@app.get(\"/items/{item_id}\", response_model=ItemResponse)\ndef get_item(item_id: int, db: Session = Depends(get_db)):\n item = db.query(Item).filter(Item.id == item_id).first()\n if not item:\n raise HTTPException(status_code=404, detail=\"Not found\")\n return item\n\n@app.put(\"/items/{item_id}\", response_model=ItemResponse)\ndef update_item(item_id: int, item: ItemCreate, db: Session = Depends(get_db)):\n db_item = db.query(Item).filter(Item.id == item_id).first()\n if not db_item:\n raise HTTPException(status_code=404, detail=\"Not found\")\n for key, value in item.model_dump().items():\n setattr(db_item, key, value)\n db.commit()\n db.refresh(db_item)\n return db_item\n\n@app.delete(\"/items/{item_id}\", status_code=204)\ndef delete_item(item_id: int, db: Session = Depends(get_db)):\n db_item = db.query(Item).filter(Item.id == item_id).first()\n if not db_item:\n raise HTTPException(status_code=404, detail=\"Not found\")\n db.delete(db_item)\n db.commit()",
|
||||
"instructions": "Create the FastAPI app with all CRUD endpoints:\n- Import from models.py and schemas.py (use exact class names)\n- create_all(bind=engine) at module level\n- get_db dependency with yield pattern\n- POST (201), GET list, GET by id, PUT, DELETE (204)\n- Use response_model for type safety\n- Use model_dump() not dict() (Pydantic v2)"
|
||||
},
|
||||
"pyproject.toml": {
|
||||
"description": "Project dependencies",
|
||||
"example": "[project]\nname = \"myapp\"\nversion = \"0.1.0\"\nrequires-python = \">=3.11\"\ndependencies = [\n \"fastapi\",\n \"uvicorn[standard]\",\n \"sqlalchemy\",\n]\n\n[project.scripts]\ndev = \"uvicorn main:app --reload\"",
|
||||
"instructions": "Use [project] format (PEP 621, compatible with uv). List dependencies under [project.dependencies]. Add [project.scripts] with dev command. Never use requirements.txt or Poetry format. Run with: uv run uvicorn main:app --reload"
|
||||
}
|
||||
},
|
||||
"order": ["models.py", "schemas.py", "main.py", "pyproject.toml"]
|
||||
}
|
||||
79
network-poc/frontend/src/components/AgentBar.astro
Normal file
@@ -0,0 +1,79 @@
|
||||
<!-- Agent gallery + configuration panel -->
|
||||
<div style="display:flex;gap:16px;padding:10px 0;align-items:flex-start">
|
||||
<!-- Agenttilista (drag & drop) -->
|
||||
<div id="agent-bar" style="display:flex;gap:6px;align-items:flex-end;flex-wrap:wrap">
|
||||
<!-- Renderöidään JS:stä -->
|
||||
</div>
|
||||
<!-- + Add agent -->
|
||||
<div id="add-agent-btn" class="agent-avatar" onclick="addCustomAgent()" title="Add custom agent" style="opacity:0.4">
|
||||
<div style="width:48px;height:48px;border-radius:50%;border:2px dashed var(--border);display:flex;align-items:center;justify-content:center;font-size:24px;color:var(--border)">+</div>
|
||||
<span style="font-size:10px;color:#8b949e;text-align:center;display:block">Add</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Agent configuration panel (opens clicking avatar) -->
|
||||
<div id="agent-config" style="display:none;background:var(--panel);border:1px solid var(--border);border-radius:6px;padding:16px;margin-bottom:10px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
|
||||
<div style="display:flex;align-items:center;gap:10px">
|
||||
<img id="config-avatar" src="" style="width:40px;height:40px;border-radius:50%">
|
||||
<div>
|
||||
<input id="config-name" style="background:transparent;border:none;color:var(--text);font-size:16px;font-weight:600;outline:none;width:200px" placeholder="Agent Name">
|
||||
<div id="config-role" style="font-size:11px;color:#8b949e"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:6px">
|
||||
<button class="btn btn-red" onclick="deleteAgent()" title="Delete agent">Delete</button>
|
||||
<button class="btn btn-muted" onclick="closeAgentConfig()">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Model -->
|
||||
<div style="margin-bottom:10px">
|
||||
<label style="font-size:12px;color:#8b949e;display:block;margin-bottom:4px">Model</label>
|
||||
<select id="config-model" style="background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:4px;padding:6px 10px;font-size:13px;width:100%">
|
||||
<option value="qwen-coder">Qwen2.5-Coder:0.5B (browser)</option>
|
||||
<option value="qwen-coder-3b">Qwen2.5-Coder:3B (Ollama)</option>
|
||||
<option value="qwen2.5-coder:7b">Qwen2.5-Coder:7B (Ollama)</option>
|
||||
<option value="qwen2.5-coder:1.5b">Qwen2.5-Coder:1.5B (Ollama)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- System prompt -->
|
||||
<div style="margin-bottom:10px" title="System prompt sent to the LLM on every request. Good prompt structure: 1. Role: 'You are an expert...' 2. Rules: RULES/CRITICAL RULES as list 3. Examples: EXAMPLE OUTPUT 4. Restrictions: NEVER-list ">
|
||||
<label style="font-size:12px;color:#8b949e;display:block;margin-bottom:4px;cursor:help">System prompt 💡</label>
|
||||
<textarea id="config-prompt" style="width:100%;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:4px;padding:8px;font-size:13px;font-family:'Courier New',monospace;resize:vertical;overflow:hidden;min-height:60px" placeholder="Describe the agent's role and behavior..."></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Sampling Parameters -->
|
||||
<div style="margin-bottom:10px">
|
||||
<label style="font-size:12px;color:#8b949e;display:block;margin-bottom:8px">Sampling Parameters</label>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
|
||||
<div title="Controls 'creativity'. Low value (0.2-0.4) produces predictable, repeatable code — good for testers and reviewers. Medium value (0.6-0.8) is best for generating code. High value (1.0+) adds variation but also errors. Recommendation: • Manager: 0.5 (precise file lists) • Coder: 0.7 (working code + variation) • Tester: 0.3 (deterministic evaluation)">
|
||||
<label style="font-size:11px;color:#8b949e;cursor:help">Temperature 💡 <span id="config-temp-val" style="color:var(--accent);float:right">0.7</span></label>
|
||||
<input type="range" id="config-temperature" min="0" max="1.5" step="0.1" value="0.7" style="width:100%;accent-color:var(--accent)">
|
||||
<div style="font-size:10px;color:#30363d">0=strict · 0.7=default · 1.5=creative</div>
|
||||
</div>
|
||||
<div title="Maximum response length in tokens (~1 token ≈ 4 chars). Recommendation: • Manager: 256-512 (short lists) • Coder: 1024-2048 (full files, CRUD endpoints) • Tester: 256-512 (short evaluations) If code cuts off early, increase this.">
|
||||
<label style="font-size:11px;color:#8b949e;cursor:help">Max tokens 💡 <span id="config-maxtok-val" style="color:var(--accent);float:right">1024</span></label>
|
||||
<input type="range" id="config-maxtokens" min="64" max="4096" step="64" value="1024" style="width:100%;accent-color:var(--accent)">
|
||||
<div style="font-size:10px;color:#30363d">Maximum response length</div>
|
||||
</div>
|
||||
<div title="How many most probable tokens are considered. Low value (1-10) makes response deterministic. High value (50-100) allows rarer words. Recommendation: • Boilerplate code: 20-30 (familiar patterns) • General code: 40 (good default) • Creative text: 60-80">
|
||||
<label style="font-size:11px;color:#8b949e;cursor:help">Top-K 💡 <span id="config-topk-val" style="color:var(--accent);float:right">40</span></label>
|
||||
<input type="range" id="config-topk" min="1" max="100" step="1" value="40" style="width:100%;accent-color:var(--accent)">
|
||||
<div style="font-size:10px;color:#30363d">1=greedy · 40=default · 100=wide</div>
|
||||
</div>
|
||||
<div title="Reduces the probability of already generated words. Prevents model from repeating same sentences. Too high value (>1.5) can break code because common keywords (return, if, def) are necessary. Recommendation: • Code: 1.1-1.2 (mild, allows repetition) • Text: 1.15-1.3 (stronger penalty) • Review: 1.0-1.1 (no penalty, short answers)">
|
||||
<label style="font-size:11px;color:#8b949e;cursor:help">Repetition penalty 💡 <span id="config-rep-val" style="color:var(--accent);float:right">1.15</span></label>
|
||||
<input type="range" id="config-repeat" min="1.0" max="2.0" step="0.05" value="1.15" style="width:100%;accent-color:var(--accent)">
|
||||
<div style="font-size:10px;color:#30363d">1.0=none · 1.15=default · 2.0=strong</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pipeline order -->
|
||||
<div>
|
||||
<label style="font-size:12px;color:#8b949e;display:block;margin-bottom:4px">Pipeline Order <span style="color:var(--border)">(drag to sort)</span></label>
|
||||
<div id="config-pipeline" style="display:flex;gap:4px;flex-wrap:wrap"></div>
|
||||
</div>
|
||||
</div>
|
||||
18
network-poc/frontend/src/components/Editor.astro
Normal file
@@ -0,0 +1,18 @@
|
||||
<!-- Monaco Editor paneeli -->
|
||||
<div id="panel-editor" class="panel">
|
||||
<div style="display:flex;flex:1;min-height:0;gap:0;border:1px solid var(--border);border-radius:6px;overflow:hidden">
|
||||
<div id="editor-filetree" style="width:200px;min-width:150px;background:var(--bg);border-right:1px solid var(--border);overflow:auto;resize:horizontal;font-family:'Courier New',monospace;font-size:13px">
|
||||
<div style="padding:10px 12px;color:#8b949e;font-size:11px;display:flex;justify-content:space-between;align-items:center;text-transform:uppercase;letter-spacing:0.5px;border-bottom:1px solid var(--border)">
|
||||
<span>Tiedostot</span>
|
||||
<button class="btn btn-green" style="padding:2px 6px;font-size:10px" onclick="downloadProjectZip()">.ZIP</button>
|
||||
</div>
|
||||
<div id="editor-file-list" style="padding:4px 0">
|
||||
<div style="padding:8px 16px;color:#8b949e;font-size:12px">Generoi projekti:<br><code style="color:var(--accent)">kpn project "..."</code></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="flex:1;display:flex;flex-direction:column">
|
||||
<div id="editor-tabs" style="display:flex;background:var(--bg);border-bottom:1px solid var(--border);min-height:35px;align-items:flex-end;padding:0 8px;gap:2px;overflow-x:auto"></div>
|
||||
<div id="monaco-container" style="flex:1"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
6
network-poc/frontend/src/components/Guide.astro
Normal file
@@ -0,0 +1,6 @@
|
||||
<!-- Opas-paneeli: ladataan GUIDE.md fetchillä -->
|
||||
<div id="panel-guide" class="panel">
|
||||
<div id="guide-content" style="max-width:800px;margin:0 auto;padding:20px;line-height:1.7;font-size:15px">
|
||||
<p style="color:#8b949e">Ladataan opasta...</p>
|
||||
</div>
|
||||
</div>
|
||||
110
network-poc/frontend/src/components/Settings.astro
Normal file
@@ -0,0 +1,110 @@
|
||||
<!-- Asetukset-paneeli: kaikki LLM-parametrit muokattavissa -->
|
||||
<div id="panel-settings" class="panel">
|
||||
<div style="max-width:800px;margin:0 auto;padding:20px">
|
||||
<h2 style="color:#e6edf3;margin-bottom:16px">Asetukset</h2>
|
||||
<p style="color:#8b949e;margin-bottom:20px;font-size:14px">Kaikki kielimallin toimintaan vaikuttavat parametrit. Muutokset tallentuvat automaattisesti.</p>
|
||||
|
||||
<!-- System prompt -->
|
||||
<div class="settings-section">
|
||||
<h3 class="settings-title">System Prompt</h3>
|
||||
<p class="settings-desc">Kielimallin perusohje joka lähetetään jokaisessa pyynnössä. Määrittää mallin käyttäytymisen.</p>
|
||||
<textarea id="set-system-prompt" class="settings-textarea" rows="4"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Sampling -->
|
||||
<div class="settings-section">
|
||||
<h3 class="settings-title">Sampling-parametrit</h3>
|
||||
<p class="settings-desc">Kontrolloi miten malli valitsee seuraavan tokenin. <a href="#guide" onclick="switchTab('guide')" style="color:var(--accent)">Lue lisää oppaasta.</a></p>
|
||||
<div class="settings-grid">
|
||||
<div>
|
||||
<label class="settings-label">Temperature <span id="set-temp-val" class="settings-val">0.7</span></label>
|
||||
<input type="range" id="set-temperature" min="0" max="1.5" step="0.1" value="0.7" class="settings-slider">
|
||||
<div class="settings-hint">0 = deterministic, 0.7 = balanced, 1.5 = creative</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="settings-label">Top-K <span id="set-topk-val" class="settings-val">40</span></label>
|
||||
<input type="range" id="set-topk" min="1" max="100" step="1" value="40" class="settings-slider">
|
||||
<div class="settings-hint">Montako tokenia huomioidaan. 1 = greedy, 40 = oletus</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="settings-label">Repetition Penalty <span id="set-rep-val" class="settings-val">1.15</span></label>
|
||||
<input type="range" id="set-repeat" min="1.0" max="2.0" step="0.05" value="1.15" class="settings-slider">
|
||||
<div class="settings-hint">Estää toistoa. 1.0 = ei rangaistusta, 1.15 = oletus</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="settings-label">Max Tokens <span id="set-maxtok-val" class="settings-val">1024</span></label>
|
||||
<input type="range" id="set-maxtokens" min="64" max="4096" step="64" value="1024" class="settings-slider">
|
||||
<div class="settings-hint">Vastauksen maksimipituus tokeneina</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stop-sekvenssit -->
|
||||
<div class="settings-section">
|
||||
<h3 class="settings-title">Stop-sekvenssit</h3>
|
||||
<p class="settings-desc">Generointi katkeaa kun malli tuottaa jonkin näistä. Yksi per rivi.</p>
|
||||
<textarea id="set-stop-sequences" class="settings-textarea" rows="4"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Malli -->
|
||||
<div class="settings-section">
|
||||
<h3 class="settings-title">Malli (Ollama)</h3>
|
||||
<p class="settings-desc">Natiivisolmun käyttämä kielimalli. Muutos vaatii native-noden uudelleenkäynnistyksen.</p>
|
||||
<select id="set-model" class="settings-select">
|
||||
<option value="qwen2.5-coder:1.5b">Qwen2.5-Coder:1.5B (~80 tok/s, ~1GB)</option>
|
||||
<option value="qwen2.5-coder:3b">Qwen2.5-Coder:3B (~50 tok/s, ~2GB)</option>
|
||||
<option value="qwen2.5-coder:7b-instruct-q4_K_M">Qwen2.5-Coder:7B Q4 (~30 tok/s, ~4GB)</option>
|
||||
<option value="qwen2.5-coder:7b">Qwen2.5-Coder:7B (~20 tok/s, ~7GB)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Pipeline-rajoitteet -->
|
||||
<div class="settings-section">
|
||||
<h3 class="settings-title">Pipeline-rajoitteet</h3>
|
||||
<p class="settings-desc">Projektin generoinnin rajat. Suuremmat arvot = rikkaampi output, hitaampi suoritus.</p>
|
||||
<div class="settings-grid">
|
||||
<div>
|
||||
<label class="settings-label">Client: max sanat <span id="set-plc-words-val" class="settings-val">400</span></label>
|
||||
<input type="range" id="set-plc-words" min="100" max="800" step="50" value="400" class="settings-slider">
|
||||
<div class="settings-hint">Vaatimusmäärittelyn maksimipituus sanoina</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="settings-label">Client: max ominaisuudet <span id="set-plc-feats-val" class="settings-val">8</span></label>
|
||||
<input type="range" id="set-plc-feats" min="3" max="15" step="1" value="8" class="settings-slider">
|
||||
<div class="settings-hint">Montako ominaisuutta vaatimuksiin</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="settings-label">Manager: max tiedostot <span id="set-plc-mfiles-val" class="settings-val">8</span></label>
|
||||
<input type="range" id="set-plc-mfiles" min="3" max="15" step="1" value="8" class="settings-slider">
|
||||
<div class="settings-hint">Managerin suunnittelemien tiedostojen yläraja</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="settings-label">Vapaa tila: max tiedostot <span id="set-plc-ffiles-val" class="settings-val">8</span></label>
|
||||
<input type="range" id="set-plc-ffiles" min="3" max="15" step="1" value="8" class="settings-slider">
|
||||
<div class="settings-hint">Tiedostoraja kun ei mallipohjaa</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="settings-label">Review-kierrokset <span id="set-plc-review-val" class="settings-val">3</span></label>
|
||||
<input type="range" id="set-plc-review" min="1" max="5" step="1" value="3" class="settings-slider">
|
||||
<div class="settings-hint">Katselmointi-korjaus-syklien max määrä</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="settings-label">Terminaali: max rivit <span id="set-plc-term-val" class="settings-val">300</span></label>
|
||||
<input type="range" id="set-plc-term" min="50" max="1000" step="50" value="300" class="settings-slider">
|
||||
<div class="settings-hint">Terminaalin näyttämien rivien yläraja</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="settings-label">CrewAI: prompt-rivit <span id="set-plc-crew-val" class="settings-val">50</span></label>
|
||||
<input type="range" id="set-plc-crew" min="10" max="200" step="10" value="50" class="settings-slider">
|
||||
<div class="settings-hint">tasks.yaml:n promptin max rivimäärä</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reset -->
|
||||
<div style="margin-top:24px;padding-top:16px;border-top:1px solid var(--border)">
|
||||
<button class="btn btn-red" onclick="resetSettings()" style="padding:6px 16px">Palauta oletukset</button>
|
||||
<span style="color:#8b949e;font-size:12px;margin-left:8px">Palauttaa kaikki parametrit oletusarvoihin</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
48
network-poc/frontend/src/components/StatusBar.astro
Normal file
@@ -0,0 +1,48 @@
|
||||
<!-- Hub-yhteys + laskentasolmun tila -->
|
||||
<div class="status-bar">
|
||||
<span class="status-group" title="Hub-yhteyden tila">
|
||||
<span id="hub-dot" class="status-dot" style="background:#d29922"></span>
|
||||
<span style="color:#8b949e">Hub:</span>
|
||||
<span id="hub-label" style="color:#d29922">Yhdistetään...</span>
|
||||
</span>
|
||||
<span class="status-separator">│</span>
|
||||
<span class="status-group">
|
||||
<span id="compute-dot" class="status-dot" style="background:#30363d"></span>
|
||||
<span style="color:#8b949e">Laskenta:</span>
|
||||
<span id="compute-label" style="color:#8b949e">—</span>
|
||||
<button id="compute-btn" class="btn btn-accent" title="Käynnistä kielimalli selaimessa">Alusta</button>
|
||||
</span>
|
||||
<span class="status-separator">│</span>
|
||||
<span class="status-group">
|
||||
<button id="join-btn" class="btn btn-green" onclick="showJoinDialog()" title="Liitä oma koneesi laskentaverkkoon (natiivi, nopea)">+ Liitä koneesi</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Join-dialogi -->
|
||||
<div id="join-dialog" style="display:none;margin-top:8px;padding:16px;background:var(--panel);border:1px solid var(--border);border-radius:6px;font-size:14px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
|
||||
<span style="color:#e6edf3;font-weight:600;font-size:16px">Liitä koneesi laskentaverkkoon</span>
|
||||
<button onclick="document.getElementById('join-dialog').style.display='none'" style="background:none;border:none;color:#8b949e;cursor:pointer;font-size:18px">✕</button>
|
||||
</div>
|
||||
<p style="color:#8b949e;margin-bottom:16px">Koneesi suorittaa tehtäviä ~10-50x nopeammin kuin selainlaskenta. Kaksi vaihetta:</p>
|
||||
|
||||
<!-- Vaihe 1: Ollama -->
|
||||
<div style="margin-bottom:14px;padding:12px;background:var(--bg);border-radius:4px;border-left:3px solid var(--accent)">
|
||||
<div style="color:#e6edf3;font-weight:600;margin-bottom:6px">1. Asenna Ollama <span style="color:#8b949e;font-weight:normal">(kielimallimoottori)</span></div>
|
||||
<div style="display:flex;gap:6px;align-items:center;margin-bottom:6px">
|
||||
<code style="flex:1;background:#010409;padding:8px 12px;border-radius:4px;color:var(--green);font-family:'Courier New',monospace;font-size:13px;user-select:all">curl -fsSL https://ollama.ai/install.sh | sh</code>
|
||||
<button onclick="navigator.clipboard.writeText('curl -fsSL https://ollama.ai/install.sh | sh');this.textContent='✓';setTimeout(()=>this.textContent='Kopioi',1500)" class="btn btn-accent" style="padding:6px 10px">Kopioi</button>
|
||||
</div>
|
||||
<div style="color:#8b949e;font-size:12px">macOS: <code style="color:var(--accent)">brew install ollama</code> · Windows: <a href="https://ollama.ai/download" target="_blank" style="color:var(--accent)">ollama.ai/download</a> · Jos jo asennettu → siirry vaiheeseen 2.</div>
|
||||
</div>
|
||||
|
||||
<!-- Vaihe 2: Kipinä-node -->
|
||||
<div style="padding:12px;background:var(--bg);border-radius:4px;border-left:3px solid var(--green)">
|
||||
<div style="color:#e6edf3;font-weight:600;margin-bottom:6px">2. Käynnistä Kipinä-node</div>
|
||||
<div style="display:flex;gap:6px;align-items:center;margin-bottom:6px">
|
||||
<code style="flex:1;background:#010409;padding:8px 12px;border-radius:4px;color:var(--green);font-family:'Courier New',monospace;font-size:13px;user-select:all">curl -sSL "https://kipina.studio/kipina-node?v=$(date +%s)" -o kipina-node && chmod +x kipina-node && ./kipina-node</code>
|
||||
<button onclick="navigator.clipboard.writeText('curl -sSL "https://kipina.studio/kipina-node?v=$(date +%s)" -o kipina-node && chmod +x kipina-node && ./kipina-node');this.textContent='✓';setTimeout(()=>this.textContent='Kopioi',1500)" class="btn btn-green" style="padding:6px 10px">Kopioi</button>
|
||||
</div>
|
||||
<div style="color:#8b949e;font-size:12px">Lataa kielimallin (~2GB) automaattisesti ensimmäisellä kerralla. Ctrl+C pysäyttää.</div>
|
||||
</div>
|
||||
</div>
|
||||
10
network-poc/frontend/src/components/Terminal.astro
Normal file
@@ -0,0 +1,10 @@
|
||||
<!-- Pipeline-palkki + Terminaali + Input -->
|
||||
<div id="pipeline-bar" class="pipeline-bar"></div>
|
||||
<div id="terminal" class="terminal"></div>
|
||||
<div class="terminal-input-row">
|
||||
<span class="terminal-prompt">$</span>
|
||||
<input id="term-input" class="terminal-input" type="text"
|
||||
placeholder='kpn run coder "hello world in python"'
|
||||
spellcheck="false" autocomplete="off">
|
||||
<div id="term-dropdown" class="terminal-dropdown"></div>
|
||||
</div>
|
||||
1963
network-poc/frontend/src/pages/index.astro
Normal file
488
network-poc/frontend/src/styles/global.css
Normal file
@@ -0,0 +1,488 @@
|
||||
/* Oletusvärit — ylikirjoitetaan teemalla */
|
||||
:root {
|
||||
--bg: #0d1117;
|
||||
--panel: #161b22;
|
||||
--text: #c9d1d9;
|
||||
--accent: #58a6ff;
|
||||
--green: #3fb950;
|
||||
--yellow: #d29922;
|
||||
--red: #f85149;
|
||||
--purple: #a371f7;
|
||||
--border: #30363d;
|
||||
--hero-accent: #ff6b00;
|
||||
--hero-glow: rgba(255, 107, 0, 0.15);
|
||||
}
|
||||
|
||||
/* Gecko — lämmin kulta/oranssi (kipina.tech) */
|
||||
[data-theme="gecko"] {
|
||||
--bg: #0a0500;
|
||||
--panel: #1f1000;
|
||||
--text: #fff5e6;
|
||||
--accent: #ff7b00;
|
||||
--green: #3fb950;
|
||||
--yellow: #ffae00;
|
||||
--red: #f85149;
|
||||
--purple: #ff9d4d;
|
||||
--border: rgba(255, 174, 0, 0.2);
|
||||
--hero-accent: #ff7b00;
|
||||
--hero-glow: rgba(255, 123, 0, 0.15);
|
||||
}
|
||||
|
||||
/* Forge — kyber-sininen/syaani (kipina.tech) */
|
||||
[data-theme="forge"] {
|
||||
--bg: #060b11;
|
||||
--panel: #121e2d;
|
||||
--text: #e0f2fe;
|
||||
--accent: #00e5ff;
|
||||
--green: #3fb950;
|
||||
--yellow: #ff5e3a;
|
||||
--red: #f85149;
|
||||
--purple: #7dd3fc;
|
||||
--border: rgba(0, 229, 255, 0.15);
|
||||
--hero-accent: #00e5ff;
|
||||
--hero-glow: rgba(0, 229, 255, 0.15);
|
||||
}
|
||||
|
||||
/* Serpent — neon-turkoosi/teal (kipina.tech) */
|
||||
[data-theme="serpent"] {
|
||||
--bg: #000808;
|
||||
--panel: #001e1e;
|
||||
--text: #ccffff;
|
||||
--accent: #00ffff;
|
||||
--green: #00ffaa;
|
||||
--yellow: #d29922;
|
||||
--red: #f85149;
|
||||
--purple: #66cccc;
|
||||
--border: rgba(0, 255, 255, 0.15);
|
||||
--hero-accent: #00ffff;
|
||||
--hero-glow: rgba(0, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 16px;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px 40px;
|
||||
}
|
||||
|
||||
#app.container {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#app:not(.active) { display: none; }
|
||||
#landing.hidden { display: none; }
|
||||
|
||||
/* Tabs */
|
||||
.tabs { display: flex; gap: 4px; margin-bottom: 16px; flex-shrink: 0; }
|
||||
.tab {
|
||||
padding: 10px 20px; border-radius: 6px 6px 0 0; cursor: pointer;
|
||||
border: 1px solid var(--border); border-bottom: none;
|
||||
background: var(--bg); color: #8b949e; font-size: 15px;
|
||||
}
|
||||
.tab.active { background: var(--panel); color: var(--accent); border-color: var(--border); }
|
||||
|
||||
/* Panels */
|
||||
.panel { display: none; }
|
||||
.panel.active { display: flex; flex-direction: column; flex: 1; min-height: 0; overflow-y: auto; }
|
||||
|
||||
/* Status bar */
|
||||
.status-bar {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
padding: 10px 16px; background: var(--bg);
|
||||
border: 1px solid var(--border); border-radius: 6px 6px 0 0;
|
||||
font-family: 'Courier New', monospace; font-size: 14px;
|
||||
}
|
||||
.status-dot {
|
||||
width: 8px; height: 8px; border-radius: 50%; display: inline-block;
|
||||
}
|
||||
.status-group { display: flex; align-items: center; gap: 6px; }
|
||||
.status-separator { color: var(--border); }
|
||||
|
||||
/* Terminal */
|
||||
.terminal {
|
||||
background: #010409; border: 1px solid var(--border); border-top: none;
|
||||
font-family: 'Courier New', monospace; font-size: 16px;
|
||||
flex: 1; min-height: 0; max-height: none; overflow-y: auto;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
.terminal-line { padding: 1px 0; white-space: pre-wrap; word-break: break-word; }
|
||||
.terminal-prompt { color: var(--yellow); margin-right: 8px; }
|
||||
.terminal-input-row {
|
||||
display: flex; align-items: center; position: relative;
|
||||
background: #0d1117; border: 1px solid var(--accent); border-top: none;
|
||||
border-radius: 0 0 6px 6px; padding: 10px 14px;
|
||||
font-family: 'Courier New', monospace; font-size: 15px;
|
||||
box-shadow: 0 2px 8px rgba(88,166,255,0.1);
|
||||
}
|
||||
.terminal-input {
|
||||
flex: 1; background: transparent; border: none; outline: none;
|
||||
color: var(--green); font-family: inherit; font-size: 16px;
|
||||
}
|
||||
.terminal-dropdown {
|
||||
display: none; position: absolute; bottom: 100%; left: 30px;
|
||||
background: var(--panel); border: 1px solid var(--border);
|
||||
border-radius: 6px; max-height: 200px; overflow-y: auto;
|
||||
font-size: 13px; min-width: 200px; z-index: 100;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
|
||||
}
|
||||
.dd-item {
|
||||
padding: 6px 12px; cursor: pointer; color: var(--text);
|
||||
white-space: nowrap; border-bottom: 1px solid #21262d;
|
||||
}
|
||||
.dd-item:hover, .dd-item.active { background: var(--border); color: var(--accent); }
|
||||
|
||||
#editor-file-list .dd-item {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Pipeline progress */
|
||||
.pipeline-bar {
|
||||
display: none; padding: 8px 14px; background: var(--bg);
|
||||
border: 1px solid var(--border); border-top: none;
|
||||
font-family: 'Courier New', monospace; font-size: 12px;
|
||||
overflow-x: auto; white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Project card */
|
||||
.project-card {
|
||||
margin: 8px 0; border: 1px solid var(--border);
|
||||
border-radius: 6px; background: var(--panel); overflow: hidden;
|
||||
}
|
||||
.project-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 8px 12px; background: var(--bg); border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.project-tabs { display: flex; gap: 2px; padding: 6px 8px 0; background: var(--bg); }
|
||||
.project-tab {
|
||||
padding: 4px 10px; cursor: pointer; border-radius: 4px 4px 0 0;
|
||||
font-size: 12px; color: #8b949e;
|
||||
white-space: nowrap; flex-shrink: 0;
|
||||
}
|
||||
.project-tab.active { background: var(--panel); color: var(--accent); border: 1px solid var(--border); border-bottom: none; }
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
padding: 2px 10px; border-radius: 4px;
|
||||
border: 1px solid var(--border); background: var(--panel);
|
||||
font-size: 12px; font-family: inherit; cursor: pointer;
|
||||
}
|
||||
.btn-accent { color: var(--accent); }
|
||||
.btn-green { color: var(--green); border-color: var(--green); }
|
||||
.btn-red { color: var(--red); border-color: var(--red); }
|
||||
.btn-muted { color: #8b949e; background: none; }
|
||||
|
||||
/* Code display */
|
||||
.code-block {
|
||||
font-family: 'Courier New', monospace; background: #010409;
|
||||
border: 1px solid var(--border); border-radius: 6px;
|
||||
padding: 14px; font-size: 13px; line-height: 1.6;
|
||||
white-space: pre-wrap; overflow-x: auto; max-height: 400px; overflow-y: auto;
|
||||
}
|
||||
.code-block .hljs { background: transparent; padding: 0; }
|
||||
|
||||
/* Agent avatars */
|
||||
.agent-avatar {
|
||||
background: linear-gradient(145deg, rgba(33,38,45,0.4) 0%, rgba(13,17,23,0.8) 100%);
|
||||
backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(240,246,252,0.1);
|
||||
border-radius: 14px;
|
||||
padding: 8px 8px 6px;
|
||||
text-align: center;
|
||||
width: 90px;
|
||||
opacity: 0.8;
|
||||
cursor: pointer;
|
||||
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
|
||||
}
|
||||
.agent-avatar:hover {
|
||||
opacity: 0.85;
|
||||
transform: translateY(-2px) scale(1.02);
|
||||
border-color: rgba(240,246,252,0.3);
|
||||
box-shadow: 0 8px 14px rgba(0,0,0,0.4);
|
||||
}
|
||||
.agent-avatar img {
|
||||
width: 64px; height: 64px; border-radius: 14px;
|
||||
margin-bottom: 4px; border: 2px solid rgba(240,246,252,0.1);
|
||||
transition: all 0.4s ease; object-fit: cover;
|
||||
}
|
||||
.agent-avatar .avatar-name {
|
||||
font-size: 11px; color: #8b949e; white-space: nowrap;
|
||||
overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.agent-avatar.active {
|
||||
opacity: 1;
|
||||
transform: translateY(-8px) scale(1.05);
|
||||
border-color: var(--accent);
|
||||
background: linear-gradient(145deg, rgba(88,166,255,0.15) 0%, rgba(13,17,23,0.9) 100%);
|
||||
box-shadow: 0 16px 24px rgba(0,0,0,0.5), 0 0 20px rgba(88,166,255,0.3);
|
||||
z-index: 2;
|
||||
}
|
||||
.agent-avatar.active img {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 25px rgba(88,166,255,0.8);
|
||||
animation: agentBlink 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes agentBlink {
|
||||
0% { opacity: 0.8; box-shadow: 0 0 15px rgba(88,166,255,0.5); }
|
||||
50% { opacity: 1.0; box-shadow: 0 0 35px rgba(88,166,255,1.0); }
|
||||
100% { opacity: 0.8; box-shadow: 0 0 15px rgba(88,166,255,0.5); }
|
||||
}
|
||||
|
||||
/* Settings */
|
||||
.settings-section {
|
||||
margin-bottom: 24px; padding: 16px; background: var(--panel);
|
||||
border: 1px solid var(--border); border-radius: 6px;
|
||||
}
|
||||
.settings-title { color: #e6edf3; font-size: 15px; margin-bottom: 4px; }
|
||||
.settings-desc { color: #8b949e; font-size: 13px; margin-bottom: 12px; }
|
||||
.settings-label { color: var(--text); font-size: 13px; display: block; margin-bottom: 4px; }
|
||||
.settings-val { color: var(--accent); font-weight: 600; float: right; }
|
||||
.settings-hint { color: #8b949e; font-size: 11px; margin-top: 2px; }
|
||||
.settings-textarea {
|
||||
width: 100%; background: var(--bg); color: var(--text);
|
||||
border: 1px solid var(--border); border-radius: 4px;
|
||||
padding: 8px; font-size: 13px; font-family: 'Courier New', monospace;
|
||||
resize: vertical;
|
||||
}
|
||||
.settings-select {
|
||||
width: 100%; background: var(--bg); color: var(--text);
|
||||
border: 1px solid var(--border); border-radius: 4px;
|
||||
padding: 8px; font-size: 13px;
|
||||
}
|
||||
.settings-slider {
|
||||
width: 100%; accent-color: var(--accent);
|
||||
}
|
||||
.settings-grid {
|
||||
display: grid; grid-template-columns: 1fr 1fr; gap: 16px;
|
||||
}
|
||||
|
||||
/* ===== LANDING PAGE ===== */
|
||||
|
||||
#landing {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bg-mesh {
|
||||
position: fixed; inset: 0; z-index: -1;
|
||||
background:
|
||||
radial-gradient(ellipse 80% 60% at 20% 40%, var(--hero-glow) 0%, transparent 70%),
|
||||
radial-gradient(ellipse 60% 50% at 80% 20%, rgba(88,166,255,0.06) 0%, transparent 70%),
|
||||
var(--bg);
|
||||
}
|
||||
|
||||
.landing-nav {
|
||||
padding: 20px 40px;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
}
|
||||
.landing-logo { text-decoration: none; font-size: 18px; font-weight: 700; }
|
||||
.logo-accent { color: var(--hero-accent); }
|
||||
.logo-sub { color: #8b949e; font-weight: 400; }
|
||||
.theme-cycle-btn {
|
||||
background: none; border: 1px solid var(--border); border-radius: 8px;
|
||||
width: 38px; height: 38px; font-size: 20px; cursor: pointer;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
transition: border-color 0.2s, transform 0.15s;
|
||||
}
|
||||
.theme-cycle-btn:hover {
|
||||
border-color: var(--hero-accent); transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Hero */
|
||||
.hero {
|
||||
padding: 60px 40px 40px;
|
||||
}
|
||||
.hero-container {
|
||||
max-width: 1200px; margin: 0 auto;
|
||||
display: grid; grid-template-columns: 1fr 400px; gap: 60px; align-items: center;
|
||||
}
|
||||
.hero-title {
|
||||
font-size: clamp(2rem, 4vw, 3rem); font-weight: 800;
|
||||
line-height: 1.15; color: #e6edf3; margin-bottom: 16px;
|
||||
}
|
||||
.hero-divider {
|
||||
width: 60px; height: 3px; background: var(--hero-accent);
|
||||
border-radius: 2px; margin-bottom: 20px;
|
||||
}
|
||||
.hero-desc {
|
||||
font-size: 1.05rem; color: #8b949e; line-height: 1.7; margin-bottom: 12px;
|
||||
}
|
||||
.hero-notice {
|
||||
font-size: 0.9rem; color: #6e7681; line-height: 1.6;
|
||||
border-left: 2px solid var(--border); padding-left: 12px; margin-bottom: 28px;
|
||||
}
|
||||
|
||||
/* Hero input */
|
||||
.hero-input-group {
|
||||
display: flex; gap: 8px; margin-bottom: 20px;
|
||||
}
|
||||
.hero-input {
|
||||
flex: 1; padding: 14px 18px; font-size: 16px;
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
background: var(--panel); color: var(--text);
|
||||
border: 1px solid var(--border); border-radius: 8px;
|
||||
outline: none; transition: border-color 0.2s;
|
||||
}
|
||||
.hero-input:focus {
|
||||
border-color: var(--hero-accent); box-shadow: 0 0 0 3px var(--hero-glow);
|
||||
}
|
||||
.hero-input::placeholder { color: #484f58; }
|
||||
.hero-input.shake {
|
||||
animation: shake 0.4s ease;
|
||||
border-color: #f85149;
|
||||
box-shadow: 0 0 0 3px rgba(248,81,73,0.2);
|
||||
}
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
20%, 60% { transform: translateX(-6px); }
|
||||
40%, 80% { transform: translateX(6px); }
|
||||
}
|
||||
.hero-btn {
|
||||
padding: 14px 28px; font-size: 16px; font-weight: 600;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: var(--hero-accent); color: #fff; border: none; border-radius: 8px;
|
||||
cursor: pointer; transition: background 0.2s, transform 0.1s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.hero-btn:hover { filter: brightness(0.85); transform: translateY(-1px); }
|
||||
.hero-btn:active { transform: translateY(0); }
|
||||
|
||||
/* Example buttons */
|
||||
.hero-examples { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
||||
.hero-examples-label { color: #6e7681; font-size: 14px; margin-right: 4px; }
|
||||
.example-btn {
|
||||
padding: 8px 16px; font-size: 13px; font-family: 'Inter', sans-serif;
|
||||
background: transparent; color: var(--accent);
|
||||
border: 1px solid var(--border); border-radius: 6px;
|
||||
cursor: pointer; transition: all 0.2s;
|
||||
}
|
||||
.example-btn:hover {
|
||||
border-color: var(--accent); background: rgba(88,166,255,0.08);
|
||||
}
|
||||
|
||||
/* Hero orb */
|
||||
.hero-orb-wrapper {
|
||||
display: flex; justify-content: center; align-items: center;
|
||||
}
|
||||
.hero-orb {
|
||||
width: 340px; height: 340px; border-radius: 50%;
|
||||
background: radial-gradient(circle at 30% 30%, var(--hero-glow) 0%, transparent 70%);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
animation: orb-float 6s ease-in-out infinite;
|
||||
}
|
||||
.hero-orb-img {
|
||||
width: 100%; height: 100%; object-fit: contain;
|
||||
filter: drop-shadow(0 0 40px var(--hero-glow));
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
@keyframes orb-float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-12px); }
|
||||
}
|
||||
|
||||
/* How section */
|
||||
.how-section {
|
||||
padding: 60px 40px;
|
||||
background: rgba(22,27,34,0.6);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.how-container { max-width: 900px; margin: 0 auto; }
|
||||
.how-title {
|
||||
text-align: center; font-size: 1.5rem; font-weight: 700;
|
||||
color: #e6edf3; margin-bottom: 40px;
|
||||
}
|
||||
.how-steps {
|
||||
display: grid; grid-template-columns: repeat(3, 1fr); gap: 32px;
|
||||
}
|
||||
.how-step {
|
||||
text-align: center; padding: 24px;
|
||||
background: var(--panel); border: 1px solid var(--border);
|
||||
border-radius: 12px; transition: border-color 0.3s;
|
||||
}
|
||||
.how-step:hover { border-color: var(--hero-accent); }
|
||||
.how-step-num {
|
||||
width: 40px; height: 40px; line-height: 40px;
|
||||
border-radius: 50%; background: var(--hero-glow);
|
||||
color: var(--hero-accent); font-weight: 700; font-size: 18px;
|
||||
margin: 0 auto 14px;
|
||||
}
|
||||
.how-step h3 { color: #e6edf3; font-size: 1rem; margin-bottom: 8px; }
|
||||
.how-step p { color: #8b949e; font-size: 0.9rem; line-height: 1.5; }
|
||||
|
||||
/* Landing footer */
|
||||
.landing-footer {
|
||||
text-align: center; padding: 32px 40px;
|
||||
color: #484f58; font-size: 13px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.landing-footer a { color: #8b949e; }
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 860px) {
|
||||
.hero-container { grid-template-columns: 1fr; gap: 32px; }
|
||||
.hero-orb-wrapper { order: -1; }
|
||||
.hero-orb { width: 220px; height: 220px; }
|
||||
.how-steps { grid-template-columns: 1fr; }
|
||||
.hero-input-group { flex-direction: column; }
|
||||
}
|
||||
|
||||
/* ===== OPPIMISPOLKU ===== */
|
||||
.learn-step {
|
||||
margin: 12px 0; border: 1px solid var(--border);
|
||||
border-radius: 8px; background: var(--panel); overflow: hidden;
|
||||
}
|
||||
.learn-step-header {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
padding: 12px 16px; cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.learn-step-header:hover { background: rgba(88,166,255,0.04); }
|
||||
.learn-step-num {
|
||||
width: 28px; height: 28px; line-height: 28px; text-align: center;
|
||||
border-radius: 50%; background: var(--hero-glow);
|
||||
color: var(--hero-accent); font-weight: 700; font-size: 13px; flex-shrink: 0;
|
||||
}
|
||||
.learn-step-agent {
|
||||
font-weight: 600; color: #e6edf3; font-size: 14px;
|
||||
}
|
||||
.learn-step-label {
|
||||
color: #8b949e; font-size: 13px; margin-left: auto;
|
||||
}
|
||||
.learn-step-body {
|
||||
display: none; padding: 0 16px 16px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.learn-step-body.open { display: block; }
|
||||
.learn-section-title {
|
||||
color: var(--accent); font-size: 12px; font-weight: 600;
|
||||
text-transform: uppercase; letter-spacing: 0.5px;
|
||||
margin: 14px 0 6px;
|
||||
}
|
||||
.learn-code {
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
font-size: 12px; line-height: 1.6;
|
||||
background: #010409; border: 1px solid var(--border);
|
||||
border-radius: 6px; padding: 12px; overflow-x: auto;
|
||||
max-height: 300px; overflow-y: auto; white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes blink { 0%,100% { opacity:1 } 50% { opacity:0 } }
|
||||
@keyframes spin { to { transform: rotate(360deg) } }
|
||||
1
network-poc/frontend/tsconfig.json
Normal file
@@ -0,0 +1 @@
|
||||
{ "extends": "astro/tsconfigs/strict" }
|
||||
34
network-poc/hub-local.log
Normal file
@@ -0,0 +1,34 @@
|
||||
Compiling hub v0.3.1 (/Users/jaakko/code/kipina-codes/playground/agentic-studio/network-poc/hub)
|
||||
Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.95s
|
||||
Running `target/debug/hub`
|
||||
[2m2026-04-12T04:56:09.723604Z[0m [32m INFO[0m [2mhub[0m[2m:[0m Tietokanta alustettu
|
||||
[2m2026-04-12T04:56:09.725088Z[0m [32m INFO[0m [2mhub[0m[2m:[0m Kipinä Agent Hub v0.3.1 käynnistyy osoitteessa http://localhost:3000
|
||||
[2m2026-04-12T04:56:18.997935Z[0m [32m INFO[0m [2mhub[0m[2m:[0m Solmu 1 yhdistyi osoitteesta 127.0.0.1
|
||||
[2m2026-04-12T04:56:19.027478Z[0m [32m INFO[0m [2mhub[0m[2m:[0m Solmu 1 (natiivi) | 127.0.0.1 | Mac | Darwin 26.3.1 | 12 ydintä | 32768 MB RAM | varaus: 4 GB
|
||||
[2m2026-04-12T04:56:19.029931Z[0m [32m INFO[0m [2mhub[0m[2m:[0m GPU 0: Apple M2 Max | VRAM: 0/24576 MB | 0°C | 0%
|
||||
[2m2026-04-12T04:56:31.260470Z[0m [32m INFO[0m [2mhub[0m[2m:[0m Solmu 2 yhdistyi osoitteesta 127.0.0.1
|
||||
[2m2026-04-12T04:56:31.281759Z[0m [32m INFO[0m [2mhub[0m[2m:[0m Solmu 2 (selain) | 127.0.0.1 | MacIntel | 11 ydintä | ~8 GB RAM | GPU: ei GPU:ta | tehtävä: viewer | varaus: 0 GB
|
||||
[2m2026-04-12T04:56:31.283313Z[0m [32m INFO[0m [2mhub[0m[2m:[0m Reititettiin API-pyyntö solmulle 1 (Malli: qwen-coder)
|
||||
|
||||
[35m━━━ Solmu 1 ━━━ qwen2.5-coder:7b-instruct-q4_K_M (Ollama) ━━━[0m
|
||||
Prompt: [33m"ping"[0m
|
||||
Vastaus: [32mPong! How can I assist you today?[0m
|
||||
11 tokenia | 4502ms | [36m56.3 tok/s[0m
|
||||
[2m2026-04-12T04:56:36.419646Z[0m [32m INFO[0m [2mhub[0m[2m:[0m Solmu 2 (127.0.0.1) poistui verkosta.
|
||||
[2m2026-04-12T04:56:36.433155Z[0m [32m INFO[0m [2mhub[0m[2m:[0m Solmu 3 yhdistyi osoitteesta 127.0.0.1
|
||||
[2m2026-04-12T04:56:36.445127Z[0m [32m INFO[0m [2mhub[0m[2m:[0m Solmu 3 (selain) | 127.0.0.1 | MacIntel | 11 ydintä | ~8 GB RAM | GPU: ei GPU:ta | tehtävä: viewer | varaus: 0 GB
|
||||
[2m2026-04-12T04:56:36.445818Z[0m [32m INFO[0m [2mhub[0m[2m:[0m Reititettiin API-pyyntö solmulle 1 (Malli: qwen-coder)
|
||||
|
||||
[35m━━━ Solmu 1 ━━━ qwen2.5-coder:7b-instruct-q4_K_M (Ollama) ━━━[0m
|
||||
Prompt: [33m"ping"[0m
|
||||
Vastaus: [32mPong! How can I assist you today? If you have any questions or need information on a specific topic, feel free to let me know.[0m
|
||||
31 tokenia | 679ms | [36m57.5 tok/s[0m
|
||||
[2m2026-04-12T04:56:39.466711Z[0m [32m INFO[0m [2mhub[0m[2m:[0m Solmu 3 (127.0.0.1) poistui verkosta.
|
||||
[2m2026-04-12T04:56:43.881216Z[0m [32m INFO[0m [2mhub[0m[2m:[0m Solmu 4 yhdistyi osoitteesta 127.0.0.1
|
||||
[2m2026-04-12T04:56:43.894385Z[0m [32m INFO[0m [2mhub[0m[2m:[0m Solmu 4 (selain) | 127.0.0.1 | MacIntel | 3 ydintä | ~16 GB RAM | GPU: ei GPU:ta | tehtävä: viewer | varaus: 0 GB
|
||||
[2m2026-04-12T04:56:43.894960Z[0m [32m INFO[0m [2mhub[0m[2m:[0m Reititettiin API-pyyntö solmulle 1 (Malli: qwen-coder)
|
||||
|
||||
[35m━━━ Solmu 1 ━━━ qwen2.5-coder:7b-instruct-q4_K_M (Ollama) ━━━[0m
|
||||
Prompt: [33m"ping"[0m
|
||||
Vastaus: [32mPong! How can I assist you today?[0m
|
||||
11 tokenia | 333ms | [36m58.7 tok/s[0m
|
||||
@@ -1,17 +1,18 @@
|
||||
[package]
|
||||
name = "hub"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
version = "0.3.2"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.7.4", features = ["ws", "macros"] }
|
||||
tokio = { version = "1.36.0", features = ["full"] }
|
||||
tokio = { version = "1.36.0", features = ["full", "sync"] }
|
||||
tower-http = { version = "0.5.2", features = ["fs", "cors", "trace"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
uuid = { version = "1.7.0", features = ["v4", "serde"] }
|
||||
futures = "0.3"
|
||||
rusqlite = { version = "0.31", features = ["bundled"] }
|
||||
chrono = "0.4"
|
||||
base64 = "0.22"
|
||||
reqwest = { version = "0.12", features = ["json"] }
|
||||
|
||||
@@ -9,6 +9,54 @@ impl NodeDb {
|
||||
pub fn new(path: &str) -> Self {
|
||||
let conn = Connection::open(path).expect("SQLite-tietokantaa ei voitu avata");
|
||||
|
||||
// Poista vanha tietokanta jos skeema on rikki — PoC, ei tuotantodata
|
||||
let _ = conn.execute_batch("
|
||||
CREATE TABLE IF NOT EXISTS _schema_version (version INTEGER);
|
||||
");
|
||||
let version: i64 = conn.query_row(
|
||||
"SELECT COALESCE(MAX(version), 0) FROM _schema_version", [], |r| r.get(0)
|
||||
).unwrap_or(0);
|
||||
|
||||
if version < 2 {
|
||||
// Pudotetaan vanhat taulut ja luodaan uudet
|
||||
let _ = conn.execute_batch("
|
||||
DROP TABLE IF EXISTS node_sessions;
|
||||
DROP TABLE IF EXISTS pair_results;
|
||||
DELETE FROM _schema_version;
|
||||
INSERT INTO _schema_version VALUES (2);
|
||||
");
|
||||
}
|
||||
if version < 3 {
|
||||
let _ = conn.execute_batch("
|
||||
CREATE TABLE IF NOT EXISTS agents (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
avatar TEXT NOT NULL DEFAULT '/avatars/kipina_notext.png',
|
||||
role TEXT NOT NULL DEFAULT 'coder',
|
||||
model TEXT NOT NULL DEFAULT 'qwen2.5-coder:7b',
|
||||
color TEXT NOT NULL DEFAULT '#3fb950',
|
||||
docs TEXT,
|
||||
prompt TEXT NOT NULL DEFAULT '',
|
||||
temperature REAL DEFAULT 0.7,
|
||||
top_k INTEGER DEFAULT 40,
|
||||
max_tokens INTEGER DEFAULT 512,
|
||||
repetition_penalty REAL DEFAULT 1.15,
|
||||
is_default BOOLEAN DEFAULT 0,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
DELETE FROM _schema_version;
|
||||
INSERT INTO _schema_version VALUES (3);
|
||||
");
|
||||
}
|
||||
if version < 4 {
|
||||
let _ = conn.execute_batch("
|
||||
ALTER TABLE node_sessions ADD COLUMN is_paused BOOLEAN DEFAULT 0;
|
||||
DELETE FROM _schema_version;
|
||||
INSERT INTO _schema_version VALUES (4);
|
||||
");
|
||||
}
|
||||
|
||||
conn.execute_batch("
|
||||
CREATE TABLE IF NOT EXISTS node_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -35,14 +83,18 @@ impl NodeDb {
|
||||
gpu_temp_c INTEGER,
|
||||
gpu_util_pct INTEGER,
|
||||
|
||||
-- Varaus
|
||||
-- Varaus ja tehtävä
|
||||
allocated_gb INTEGER,
|
||||
selected_task TEXT DEFAULT 'tokenize',
|
||||
|
||||
-- WebGPU-tuki
|
||||
has_webgpu BOOLEAN,
|
||||
|
||||
-- Tehtävätilastot
|
||||
tasks_completed INTEGER DEFAULT 0
|
||||
tasks_completed INTEGER DEFAULT 0,
|
||||
|
||||
-- Ohjaustilat
|
||||
is_paused BOOLEAN DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pair_results (
|
||||
@@ -70,7 +122,7 @@ impl NodeDb {
|
||||
node_type: &str,
|
||||
auth_data: &serde_json::Value,
|
||||
) -> i64 {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let conn = self.conn.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
// Selainsolmun tiedot
|
||||
@@ -78,6 +130,7 @@ impl NodeDb {
|
||||
let cpu_cores = auth_data.get("cpu_cores").and_then(|v| v.as_u64());
|
||||
let ram = auth_data.get("device_memory_gb").and_then(|v| v.as_f64()).map(|v| (v * 1024.0) as i64);
|
||||
let allocated = auth_data.get("allocated_gb").and_then(|v| v.as_u64());
|
||||
let selected_task = auth_data.get("selected_task").and_then(|v| v.as_str());
|
||||
|
||||
// GPU (selain)
|
||||
let gpu_vendor = auth_data.get("gpu").and_then(|g| g.get("vendor")).and_then(|v| v.as_str());
|
||||
@@ -108,8 +161,8 @@ impl NodeDb {
|
||||
node_id, ip, node_type, connected_at,
|
||||
platform, hostname, os, cpu_cores, cpu_model, ram_mb,
|
||||
gpu_name, gpu_vendor, gpu_backend, vram_total_mb, vram_used_mb, gpu_temp_c, gpu_util_pct,
|
||||
allocated_gb, has_webgpu
|
||||
) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,?17,?18,?19)",
|
||||
allocated_gb, selected_task, has_webgpu
|
||||
) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,?17,?18,?19,?20)",
|
||||
params![
|
||||
node_id as i64, ip, node_type, now,
|
||||
platform, hostname, os,
|
||||
@@ -124,6 +177,7 @@ impl NodeDb {
|
||||
gpu_temp.map(|v| v as i64),
|
||||
gpu_util.map(|v| v as i64),
|
||||
allocated.map(|v| v as i64),
|
||||
selected_task,
|
||||
has_webgpu,
|
||||
],
|
||||
).expect("Session insert epäonnistui");
|
||||
@@ -131,8 +185,34 @@ impl NodeDb {
|
||||
conn.last_insert_rowid()
|
||||
}
|
||||
|
||||
pub fn update_session_task(&self, node_id: u64, task: &str) {
|
||||
let conn = self.conn.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let _ = conn.execute(
|
||||
"UPDATE node_sessions SET selected_task = ?1 WHERE node_id = ?2 AND disconnected_at IS NULL",
|
||||
params![task, node_id as i64],
|
||||
);
|
||||
}
|
||||
|
||||
pub fn update_session_status(&self, node_id: u64, is_paused: bool) {
|
||||
let conn = self.conn.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let _ = conn.execute(
|
||||
"UPDATE node_sessions SET is_paused = ?1 WHERE node_id = ?2 AND disconnected_at IS NULL",
|
||||
params![is_paused as i64, node_id as i64],
|
||||
);
|
||||
}
|
||||
|
||||
/// Sulkee saman IP:n viewer-sessiot kun aktiivinen node liittyy
|
||||
pub fn close_viewers_by_ip(&self, ip: &str) {
|
||||
let conn = self.conn.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let _ = conn.execute(
|
||||
"UPDATE node_sessions SET disconnected_at = ?1 WHERE ip = ?2 AND disconnected_at IS NULL AND (selected_task = 'viewer' OR selected_task = 'codelab-viewer')",
|
||||
params![now, ip],
|
||||
);
|
||||
}
|
||||
|
||||
pub fn close_session(&self, node_id: u64) {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let conn = self.conn.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let _ = conn.execute(
|
||||
"UPDATE node_sessions SET disconnected_at = ?1 WHERE node_id = ?2 AND disconnected_at IS NULL",
|
||||
@@ -141,7 +221,7 @@ impl NodeDb {
|
||||
}
|
||||
|
||||
pub fn increment_tasks(&self, node_id: u64) {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let conn = self.conn.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let _ = conn.execute(
|
||||
"UPDATE node_sessions SET tasks_completed = tasks_completed + 1 WHERE node_id = ?1 AND disconnected_at IS NULL",
|
||||
params![node_id as i64],
|
||||
@@ -149,12 +229,12 @@ impl NodeDb {
|
||||
}
|
||||
|
||||
pub fn get_sessions(&self, limit: u32) -> Vec<serde_json::Value> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let conn = self.conn.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, node_id, ip, node_type, connected_at, disconnected_at,
|
||||
platform, hostname, os, cpu_cores, cpu_model, ram_mb,
|
||||
gpu_name, gpu_vendor, gpu_backend, vram_total_mb, gpu_temp_c, gpu_util_pct,
|
||||
allocated_gb, has_webgpu, tasks_completed
|
||||
allocated_gb, selected_task, has_webgpu, tasks_completed, is_paused
|
||||
FROM node_sessions ORDER BY id DESC LIMIT ?1"
|
||||
).unwrap();
|
||||
|
||||
@@ -179,14 +259,16 @@ impl NodeDb {
|
||||
"gpu_temp_c": row.get::<_, Option<i64>>(16)?,
|
||||
"gpu_util_pct": row.get::<_, Option<i64>>(17)?,
|
||||
"allocated_gb": row.get::<_, Option<i64>>(18)?,
|
||||
"has_webgpu": row.get::<_, Option<bool>>(19)?,
|
||||
"tasks_completed": row.get::<_, i64>(20)?,
|
||||
"selected_task": row.get::<_, Option<String>>(19)?,
|
||||
"has_webgpu": row.get::<_, Option<bool>>(20)?,
|
||||
"tasks_completed": row.get::<_, i64>(21)?,
|
||||
"is_paused": row.get::<_, Option<bool>>(22)?.unwrap_or(false),
|
||||
}))
|
||||
}).unwrap().filter_map(|r| r.ok()).collect()
|
||||
}
|
||||
|
||||
pub fn get_pair_results(&self, limit: u32) -> Vec<serde_json::Value> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let conn = self.conn.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, node_id, created_at, en_text, fi_text,
|
||||
en_tokens, fi_tokens, en_chars_per_token, fi_chars_per_token,
|
||||
@@ -212,7 +294,7 @@ impl NodeDb {
|
||||
}
|
||||
|
||||
pub fn get_stats(&self) -> serde_json::Value {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let conn = self.conn.lock().unwrap_or_else(|e| e.into_inner());
|
||||
|
||||
let total_sessions: i64 = conn.query_row("SELECT COUNT(*) FROM node_sessions", [], |r| r.get(0)).unwrap_or(0);
|
||||
let active_sessions: i64 = conn.query_row("SELECT COUNT(*) FROM node_sessions WHERE disconnected_at IS NULL", [], |r| r.get(0)).unwrap_or(0);
|
||||
@@ -239,6 +321,82 @@ impl NodeDb {
|
||||
})
|
||||
}
|
||||
|
||||
// ── Agents CRUD ──
|
||||
|
||||
pub fn upsert_agent(&self, agent: &serde_json::Value) -> Result<(), String> {
|
||||
let conn = self.conn.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let id = agent.get("id").and_then(|v| v.as_str()).ok_or("id puuttuu")?;
|
||||
let name = agent.get("name").and_then(|v| v.as_str()).ok_or("name puuttuu")?;
|
||||
conn.execute(
|
||||
"INSERT INTO agents (id, name, avatar, role, model, color, docs, prompt,
|
||||
temperature, top_k, max_tokens, repetition_penalty, is_default, created_at, updated_at)
|
||||
VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?14)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
name=?2, avatar=?3, role=?4, model=?5, color=?6, docs=?7, prompt=?8,
|
||||
temperature=?9, top_k=?10, max_tokens=?11, repetition_penalty=?12, updated_at=?14",
|
||||
params![
|
||||
id, name,
|
||||
agent.get("avatar").and_then(|v| v.as_str()).unwrap_or("/avatars/kipina_notext.png"),
|
||||
agent.get("role").and_then(|v| v.as_str()).unwrap_or("coder"),
|
||||
agent.get("model").and_then(|v| v.as_str()).unwrap_or("qwen2.5-coder:7b"),
|
||||
agent.get("color").and_then(|v| v.as_str()).unwrap_or("#3fb950"),
|
||||
agent.get("docs").and_then(|v| v.as_str()),
|
||||
agent.get("prompt").and_then(|v| v.as_str()).unwrap_or(""),
|
||||
agent.get("temperature").and_then(|v| v.as_f64()).unwrap_or(0.7),
|
||||
agent.get("top_k").and_then(|v| v.as_u64()).unwrap_or(40) as i64,
|
||||
agent.get("max_tokens").and_then(|v| v.as_u64()).unwrap_or(512) as i64,
|
||||
agent.get("repetition_penalty").and_then(|v| v.as_f64()).unwrap_or(1.15),
|
||||
agent.get("is_default").and_then(|v| v.as_bool()).unwrap_or(false),
|
||||
now,
|
||||
],
|
||||
).map_err(|e| format!("Agent upsert: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_agents(&self) -> Vec<serde_json::Value> {
|
||||
let conn = self.conn.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, name, avatar, role, model, color, docs, prompt,
|
||||
temperature, top_k, max_tokens, repetition_penalty, is_default,
|
||||
created_at, updated_at
|
||||
FROM agents ORDER BY is_default DESC, name"
|
||||
).unwrap();
|
||||
|
||||
stmt.query_map([], |row| {
|
||||
Ok(serde_json::json!({
|
||||
"id": row.get::<_, String>(0)?,
|
||||
"name": row.get::<_, String>(1)?,
|
||||
"avatar": row.get::<_, String>(2)?,
|
||||
"role": row.get::<_, String>(3)?,
|
||||
"model": row.get::<_, String>(4)?,
|
||||
"color": row.get::<_, String>(5)?,
|
||||
"docs": row.get::<_, Option<String>>(6)?,
|
||||
"prompt": row.get::<_, String>(7)?,
|
||||
"temperature": row.get::<_, f64>(8)?,
|
||||
"top_k": row.get::<_, i64>(9)?,
|
||||
"max_tokens": row.get::<_, i64>(10)?,
|
||||
"repetition_penalty": row.get::<_, f64>(11)?,
|
||||
"is_default": row.get::<_, bool>(12)?,
|
||||
"created_at": row.get::<_, String>(13)?,
|
||||
"updated_at": row.get::<_, String>(14)?,
|
||||
}))
|
||||
}).unwrap().filter_map(|r| r.ok()).collect()
|
||||
}
|
||||
|
||||
pub fn delete_agent(&self, id: &str) -> Result<(), String> {
|
||||
let conn = self.conn.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let deleted = conn.execute(
|
||||
"DELETE FROM agents WHERE id = ?1 AND is_default = 0",
|
||||
params![id],
|
||||
).map_err(|e| format!("Agent delete: {}", e))?;
|
||||
if deleted == 0 {
|
||||
Err("Agenttia ei löydy tai se on oletusagentti".to_string())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_pair_result(
|
||||
&self,
|
||||
node_id: u64,
|
||||
@@ -247,7 +405,7 @@ impl NodeDb {
|
||||
overhead: f64,
|
||||
duration_ms: f64,
|
||||
) {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let conn = self.conn.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let _ = conn.execute(
|
||||
"INSERT INTO pair_results (
|
||||
|
||||
135
network-poc/kipina-node
Normal file
@@ -0,0 +1,135 @@
|
||||
#!/bin/bash
|
||||
# Kipinä Node — lataa oikea binääri ja käynnistä
|
||||
set -e
|
||||
|
||||
BASE_URL="https://kipina.studio/download"
|
||||
HUB_URL="${KIPINA_HUB:-wss://kipina.studio/ws}"
|
||||
OLLAMA_URL="${OLLAMA_URL:-http://localhost:11434}"
|
||||
|
||||
# Tunnista OS ja arkkitehtuuri
|
||||
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
ARCH=$(uname -m)
|
||||
|
||||
case "$OS-$ARCH" in
|
||||
darwin-arm64) BINARY="kipina-node-macos-arm64" ;;
|
||||
darwin-x86_64) BINARY="kipina-node-macos-arm64" ;; # Rosetta
|
||||
linux-x86_64) BINARY="kipina-node-linux-x86_64" ;;
|
||||
linux-aarch64) BINARY="kipina-node-linux-arm64" ;;
|
||||
*) echo "Ei tuettu: $OS-$ARCH"; exit 1 ;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
echo " ╔══════════════════════════════════════╗"
|
||||
echo " ║ Kipinä Agentic Node ║"
|
||||
echo " ╚══════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo " OS: $OS ($ARCH)"
|
||||
echo ""
|
||||
|
||||
# Etsi Ollama-instanssit
|
||||
CANDIDATES=(
|
||||
"http://localhost:11434"
|
||||
"http://127.0.0.1:11434"
|
||||
"http://ollama:11434"
|
||||
"http://host.docker.internal:11434"
|
||||
)
|
||||
|
||||
# Lisää OLLAMA_URL listaan jos asetettu ja ei jo mukana
|
||||
if [ -n "$OLLAMA_URL" ]; then
|
||||
ALREADY=false
|
||||
for c in "${CANDIDATES[@]}"; do
|
||||
[ "$c" = "$OLLAMA_URL" ] && ALREADY=true
|
||||
done
|
||||
$ALREADY || CANDIDATES=("$OLLAMA_URL" "${CANDIDATES[@]}")
|
||||
fi
|
||||
|
||||
echo " Etsitään Ollama-instansseja..."
|
||||
FOUND=()
|
||||
for url in "${CANDIDATES[@]}"; do
|
||||
if curl -s --connect-timeout 1 "$url/api/tags" &>/dev/null; then
|
||||
FOUND+=("$url")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#FOUND[@]} -eq 0 ]; then
|
||||
# Ei löytynyt — yritä käynnistää lokaali
|
||||
if command -v ollama &>/dev/null; then
|
||||
echo " Käynnistetään Ollama..."
|
||||
ollama serve &>/dev/null &
|
||||
sleep 3
|
||||
if curl -s --connect-timeout 1 "http://localhost:11434/api/tags" &>/dev/null; then
|
||||
OLLAMA_URL="http://localhost:11434"
|
||||
echo " ✓ Ollama käynnistetty ($OLLAMA_URL)"
|
||||
else
|
||||
echo " ✗ Ollaman käynnistys epäonnistui."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo ""
|
||||
echo " ✗ Ollamaa ei löytynyt."
|
||||
echo " Kontti/remote: OLLAMA_URL=http://HOST:11434 ./kipina-node"
|
||||
echo " Asenna: curl -fsSL https://ollama.ai/install.sh | sh"
|
||||
exit 1
|
||||
fi
|
||||
elif [ ${#FOUND[@]} -eq 1 ]; then
|
||||
OLLAMA_URL="${FOUND[0]}"
|
||||
echo " ✓ Ollama löytyi: $OLLAMA_URL"
|
||||
else
|
||||
echo ""
|
||||
echo " Löytyi ${#FOUND[@]} Ollama-instanssia:"
|
||||
echo ""
|
||||
for i in "${!FOUND[@]}"; do
|
||||
echo " $((i+1))) ${FOUND[$i]}"
|
||||
done
|
||||
echo ""
|
||||
read -p " Valitse [1-${#FOUND[@]}]: " -r CHOICE
|
||||
if [[ "$CHOICE" =~ ^[0-9]+$ ]] && [ "$CHOICE" -ge 1 ] && [ "$CHOICE" -le ${#FOUND[@]} ]; then
|
||||
OLLAMA_URL="${FOUND[$((CHOICE-1))]}"
|
||||
else
|
||||
OLLAMA_URL="${FOUND[0]}"
|
||||
echo " Käytetään oletusta: $OLLAMA_URL"
|
||||
fi
|
||||
echo " ✓ Valittu: $OLLAMA_URL"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " Hub: $HUB_URL"
|
||||
echo " Ollama: $OLLAMA_URL"
|
||||
if [ -n "$KIPINA_MODEL" ]; then
|
||||
echo " Malli: $KIPINA_MODEL (Ympäristömuuttujasta)"
|
||||
fi
|
||||
|
||||
# Binäärin automaattinen päivitys — vertaa build-hashia palvelimeen
|
||||
BIN_PATH="./kipina-node-bin"
|
||||
HASH_PATH="./kipina-node-bin.hash"
|
||||
|
||||
REMOTE_HASH=$(curl -sSL "$BASE_URL/.build-hash?v=$(date +%s)" 2>/dev/null | tr -d '[:space:]')
|
||||
LOCAL_HASH=""
|
||||
[ -f "$HASH_PATH" ] && LOCAL_HASH=$(cat "$HASH_PATH" | tr -d '[:space:]')
|
||||
|
||||
if [ -f "$BIN_PATH" ] && [ -n "$REMOTE_HASH" ] && [ "$REMOTE_HASH" = "$LOCAL_HASH" ]; then
|
||||
echo " ✓ Binääri ajan tasalla (versio: $LOCAL_HASH)"
|
||||
else
|
||||
if [ -f "$BIN_PATH" ]; then
|
||||
echo " ↻ Uusi versio saatavilla ($LOCAL_HASH → $REMOTE_HASH)"
|
||||
else
|
||||
echo " Ladataan $BINARY..."
|
||||
fi
|
||||
rm -f "$BIN_PATH"
|
||||
curl -sSL "$BASE_URL/$BINARY?v=$(date +%s)" -o "$BIN_PATH"
|
||||
chmod +x "$BIN_PATH"
|
||||
echo "$REMOTE_HASH" > "$HASH_PATH"
|
||||
echo " ✓ Päivitetty versioon $REMOTE_HASH"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " ✓ Siirrytään Kipinä Noden hallintaan..."
|
||||
echo " Ctrl+C pysäyttää"
|
||||
echo ""
|
||||
|
||||
if [ -n "$KIPINA_MODEL" ]; then
|
||||
export OLLAMA_MODEL="$KIPINA_MODEL"
|
||||
fi
|
||||
export HUB_URL="$HUB_URL"
|
||||
export OLLAMA_URL="$OLLAMA_URL"
|
||||
exec "$BIN_PATH"
|
||||
@@ -1,7 +1,11 @@
|
||||
[package]
|
||||
name = "native-node"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
version = "0.2.2"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
default = ["gpu-detect"]
|
||||
gpu-detect = ["nvml-wrapper", "wgpu"]
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1.36", features = ["full"] }
|
||||
@@ -10,7 +14,13 @@ futures-util = "0.3"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
sysinfo = "0.30"
|
||||
nvml-wrapper = "0.10"
|
||||
wgpu = "24"
|
||||
nvml-wrapper = { version = "0.10", optional = true }
|
||||
wgpu = { version = "24", optional = true }
|
||||
reqwest = { version = "0.12", features = ["json"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
dialoguer = "0.12.0"
|
||||
ratatui = "0.29.0"
|
||||
crossterm = { version = "0.28.1", features = ["event-stream"] }
|
||||
tracing-appender = "0.2.4"
|
||||
chrono = "0.4"
|
||||
|
||||
246
network-poc/native-node/src/inference.rs
Normal file
@@ -0,0 +1,246 @@
|
||||
use std::time::Instant;
|
||||
use std::cell::RefCell;
|
||||
|
||||
pub struct GenerateOptions {
|
||||
pub max_tokens: usize,
|
||||
pub system_prompt: Option<String>,
|
||||
pub temperature: Option<f64>,
|
||||
pub top_k: Option<u64>,
|
||||
pub repeat_penalty: Option<f64>,
|
||||
pub stop: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
pub struct LlmEngine {
|
||||
ollama_url: String,
|
||||
model: RefCell<String>,
|
||||
client: reqwest::Client,
|
||||
}
|
||||
|
||||
impl LlmEngine {
|
||||
pub async fn load() -> Result<Self, String> {
|
||||
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))?;
|
||||
|
||||
// Jos OLLAMA_URL on asetettu, käytetään sitä suoraan
|
||||
let ollama_url = if let Ok(url) = std::env::var("OLLAMA_URL") {
|
||||
tracing::info!("Ollama backend (env): {}", url);
|
||||
url
|
||||
} else {
|
||||
// Haistellaan Ollamaa tunnetuista osoitteista
|
||||
let candidates = [
|
||||
"http://localhost:11434",
|
||||
"http://127.0.0.1:11434",
|
||||
"http://ollama:11434",
|
||||
"http://host.docker.internal:11434",
|
||||
];
|
||||
let mut found = None;
|
||||
for url in &candidates {
|
||||
let probe = reqwest::Client::builder()
|
||||
.connect_timeout(std::time::Duration::from_secs(2))
|
||||
.build().unwrap_or(client.clone());
|
||||
if let Ok(resp) = probe.get(format!("{}/api/version", url)).send().await {
|
||||
if resp.status().is_success() {
|
||||
tracing::info!("Ollama löytyi osoitteesta: {}", url);
|
||||
found = Some(url.to_string());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
found.unwrap_or_else(|| {
|
||||
tracing::warn!("Ollamaa ei löytynyt — käytetään oletusta http://localhost:11434");
|
||||
"http://localhost:11434".to_string()
|
||||
})
|
||||
};
|
||||
|
||||
// Kysytään malli TUI:lla jos ei pakotettu ympäristöstä
|
||||
let model = match std::env::var("OLLAMA_MODEL") {
|
||||
Ok(m) if !m.is_empty() => m,
|
||||
_ => crate::tui::select_model(&ollama_url, &client).await?
|
||||
};
|
||||
|
||||
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 ollama_url(&self) -> &str {
|
||||
&self.ollama_url
|
||||
}
|
||||
|
||||
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()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Hakee käynnissä olevan mallin VRAM-tilan (ollama ps)
|
||||
pub async fn fetch_ps(&self) -> Result<Option<ModelVramStatus>, String> {
|
||||
let resp = self.client.get(format!("{}/api/ps", self.ollama_url))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Ollama ps: {}", e))?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
return Err(format!("Ollama ps HTTP {}", resp.status()));
|
||||
}
|
||||
|
||||
let body: serde_json::Value = resp.json().await
|
||||
.map_err(|e| format!("Ollama ps json: {}", e))?;
|
||||
|
||||
let models = body["models"].as_array();
|
||||
if let Some(arr) = models {
|
||||
if let Some(m) = arr.first() {
|
||||
let name = m["name"].as_str().unwrap_or("?").to_string();
|
||||
let size = m["size"].as_u64().unwrap_or(0);
|
||||
let size_vram = m["size_vram"].as_u64().unwrap_or(0);
|
||||
return Ok(Some(ModelVramStatus { name, size, size_vram }));
|
||||
}
|
||||
}
|
||||
Ok(None) // ei ladattua mallia
|
||||
}
|
||||
|
||||
/// Hakee kaikki Ollamaan asennetut mallit
|
||||
pub async fn fetch_models(&self) -> Result<serde_json::Value, String> {
|
||||
let resp = self.client.get(format!("{}/api/tags", self.ollama_url))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Ollama tags fetch: {}", e))?;
|
||||
|
||||
if resp.status().is_success() {
|
||||
resp.json().await.map_err(|e| format!("Ollama tags json: {}", e))
|
||||
} else {
|
||||
Err(format!("Ollama tags epäonnistui: {}", resp.status()))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn generate(&self, prompt: &str, opts: &GenerateOptions) -> Result<GenerateResult, String> {
|
||||
let model = self.model.borrow().clone();
|
||||
|
||||
let default_stop: Vec<String> = vec![
|
||||
"<|im_end|>".into(),
|
||||
];
|
||||
|
||||
// Rakennetaan messages-lista (chat API)
|
||||
let mut messages = Vec::new();
|
||||
if let Some(ref sp) = opts.system_prompt {
|
||||
if !sp.is_empty() {
|
||||
messages.push(serde_json::json!({"role": "system", "content": sp}));
|
||||
}
|
||||
}
|
||||
messages.push(serde_json::json!({"role": "user", "content": prompt}));
|
||||
|
||||
let body = serde_json::json!({
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
"stream": false,
|
||||
"options": {
|
||||
"num_ctx": 16384,
|
||||
"num_predict": opts.max_tokens,
|
||||
"temperature": opts.temperature.unwrap_or(0.7),
|
||||
"top_k": opts.top_k.unwrap_or(40),
|
||||
"repeat_penalty": opts.repeat_penalty.unwrap_or(1.15),
|
||||
"stop": opts.stop.as_ref().unwrap_or(&default_stop),
|
||||
}
|
||||
});
|
||||
|
||||
let start = Instant::now();
|
||||
let resp = self.client.post(format!("{}/api/chat", self.ollama_url))
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Ollama chat: {}", e))?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
return Err(format!("Ollama HTTP {}", resp.status()));
|
||||
}
|
||||
|
||||
let body: serde_json::Value = resp.json().await
|
||||
.map_err(|e| format!("Ollama JSON: {}", e))?;
|
||||
|
||||
let text = body["message"]["content"].as_str().unwrap_or("").to_string();
|
||||
let _total_duration_ns = body["total_duration"].as_u64().unwrap_or(0);
|
||||
let eval_count = body["eval_count"].as_u64().unwrap_or(0) as usize;
|
||||
let eval_duration_ns = body["eval_duration"].as_u64().unwrap_or(1);
|
||||
|
||||
let duration_ms = start.elapsed().as_millis() as f64;
|
||||
let tokens_per_sec = if eval_duration_ns > 0 {
|
||||
eval_count as f64 / (eval_duration_ns as f64 / 1_000_000_000.0)
|
||||
} else { 0.0 };
|
||||
|
||||
Ok(GenerateResult {
|
||||
text: strip_code_fences(&text),
|
||||
tokens_generated: eval_count,
|
||||
duration_ms,
|
||||
tokens_per_sec,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Siivoa markdown-koodiblokki-merkit vastauksesta
|
||||
fn strip_code_fences(text: &str) -> String {
|
||||
let lines: Vec<&str> = text.lines().collect();
|
||||
let filtered: Vec<&str> = lines.into_iter().filter(|line| {
|
||||
let trimmed = line.trim();
|
||||
// Poista rivit jotka ovat pelkkiä ``` tai ```kielitunniste
|
||||
trimmed != "```" && !(trimmed.starts_with("```") && !trimmed[3..].contains('`'))
|
||||
}).collect();
|
||||
filtered.join("\n").trim().to_string()
|
||||
}
|
||||
|
||||
pub struct GenerateResult {
|
||||
pub text: String,
|
||||
pub tokens_generated: usize,
|
||||
pub duration_ms: f64,
|
||||
pub tokens_per_sec: f64,
|
||||
}
|
||||
|
||||
pub struct ModelVramStatus {
|
||||
pub name: String,
|
||||
pub size: u64, // kokonaiskoko (tavuina)
|
||||
pub size_vram: u64, // VRAM:ssa oleva osuus (tavuina)
|
||||
}
|
||||
|
||||
impl ModelVramStatus {
|
||||
pub fn fully_in_vram(&self) -> bool {
|
||||
self.size > 0 && self.size_vram >= self.size
|
||||
}
|
||||
|
||||
pub fn vram_percent(&self) -> f64 {
|
||||
if self.size == 0 { return 0.0; }
|
||||
(self.size_vram as f64 / self.size as f64) * 100.0
|
||||
}
|
||||
|
||||
pub fn display(&self) -> String {
|
||||
let size_gb = self.size as f64 / 1_073_741_824.0;
|
||||
let vram_gb = self.size_vram as f64 / 1_073_741_824.0;
|
||||
if self.fully_in_vram() {
|
||||
format!("✓ {} ({:.1} GB) — 100% GPU", self.name, size_gb)
|
||||
} else if self.size_vram == 0 {
|
||||
format!("✗ {} ({:.1} GB) — 100% CPU", self.name, size_gb)
|
||||
} else {
|
||||
format!("◐ {} ({:.1}/{:.1} GB VRAM, {:.0}% GPU)", self.name, vram_gb, size_gb, self.vram_percent())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use serde_json::json;
|
||||
use std::io::IsTerminal;
|
||||
use sysinfo::System;
|
||||
use tokio_tungstenite::connect_async;
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
|
||||
mod inference;
|
||||
mod tui;
|
||||
mod tui_dashboard;
|
||||
|
||||
/// GPU-tietorakenne — yhtenäinen kaikille valmistajille
|
||||
struct GpuInfo {
|
||||
name: String,
|
||||
@@ -31,6 +36,7 @@ impl GpuInfo {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "gpu-detect")]
|
||||
/// Tunnistaa kaikki GPU:t wgpu:lla (NVIDIA/AMD/Apple/Intel)
|
||||
fn collect_gpus_wgpu() -> Vec<GpuInfo> {
|
||||
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
|
||||
@@ -82,6 +88,7 @@ fn collect_gpus_wgpu() -> Vec<GpuInfo> {
|
||||
gpus
|
||||
}
|
||||
|
||||
#[cfg(feature = "gpu-detect")]
|
||||
/// Täydentää NVIDIA-GPU:iden tiedot NVML:llä (VRAM, lämpötila, kuormitus)
|
||||
fn enrich_nvidia_gpus(gpus: &mut [GpuInfo]) {
|
||||
let Ok(nvml) = nvml_wrapper::Nvml::init() else { return };
|
||||
@@ -107,6 +114,7 @@ fn enrich_nvidia_gpus(gpus: &mut [GpuInfo]) {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "gpu-detect")]
|
||||
/// AMD GPU-tiedot Linuxin sysfs:stä (/sys/class/drm/)
|
||||
fn enrich_amd_gpus(gpus: &mut [GpuInfo]) {
|
||||
let Ok(entries) = std::fs::read_dir("/sys/class/drm") else { return };
|
||||
@@ -148,10 +156,12 @@ fn enrich_amd_gpus(gpus: &mut [GpuInfo]) {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "gpu-detect")]
|
||||
fn read_sysfs_u64(path: &std::path::Path) -> Option<u64> {
|
||||
std::fs::read_to_string(path).ok()?.trim().parse().ok()
|
||||
}
|
||||
|
||||
#[cfg(feature = "gpu-detect")]
|
||||
fn find_hwmon_temp(device_path: &std::path::Path) -> Option<u64> {
|
||||
let hwmon_dir = device_path.join("hwmon");
|
||||
let entries = std::fs::read_dir(&hwmon_dir).ok()?;
|
||||
@@ -164,8 +174,8 @@ fn find_hwmon_temp(device_path: &std::path::Path) -> Option<u64> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(feature = "gpu-detect")]
|
||||
/// Apple GPU-tiedot — wgpu/Metal antaa nimen, tarkempaa dataa ei saa ilman IOKit:ia
|
||||
/// mutta Metal adapter_info sisältää jo olennaiset tiedot
|
||||
fn enrich_apple_gpus(gpus: &mut [GpuInfo]) {
|
||||
// Apple Silicon -koneiden unified memory: koko RAM on GPU:n käytettävissä
|
||||
// Arvioidaan system RAM:sta
|
||||
@@ -185,13 +195,18 @@ fn enrich_apple_gpus(gpus: &mut [GpuInfo]) {
|
||||
|
||||
/// Kerää kaikki GPU:t ja täydentää valmistajakohtaiset tiedot
|
||||
fn collect_all_gpus() -> Vec<GpuInfo> {
|
||||
let mut gpus = collect_gpus_wgpu();
|
||||
|
||||
enrich_nvidia_gpus(&mut gpus);
|
||||
enrich_amd_gpus(&mut gpus);
|
||||
enrich_apple_gpus(&mut gpus);
|
||||
|
||||
gpus
|
||||
#[cfg(feature = "gpu-detect")]
|
||||
{
|
||||
let mut gpus = collect_gpus_wgpu();
|
||||
enrich_nvidia_gpus(&mut gpus);
|
||||
enrich_amd_gpus(&mut gpus);
|
||||
enrich_apple_gpus(&mut gpus);
|
||||
return gpus;
|
||||
}
|
||||
#[cfg(not(feature = "gpu-detect"))]
|
||||
{
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Kerää järjestelmätiedot (CPU, RAM, OS)
|
||||
@@ -210,7 +225,7 @@ fn collect_system_info() -> serde_json::Value {
|
||||
}
|
||||
|
||||
/// Koko auth-viesti hubille
|
||||
fn build_auth_message(allocated_gb: u32) -> String {
|
||||
fn build_auth_message(allocated_gb: u32, model_name: &str, models_data: Option<serde_json::Value>) -> String {
|
||||
let sys = collect_system_info();
|
||||
let gpus = collect_all_gpus();
|
||||
|
||||
@@ -220,18 +235,29 @@ fn build_auth_message(allocated_gb: u32) -> String {
|
||||
v
|
||||
}).collect();
|
||||
|
||||
let api_key = std::env::var("NODE_API_KEY").unwrap_or_default();
|
||||
|
||||
let mut msg = json!({
|
||||
"type": "auth",
|
||||
"status": "agent_ready",
|
||||
"node_type": "native",
|
||||
"allocated_gb": allocated_gb,
|
||||
"selected_task": model_name,
|
||||
"system": sys,
|
||||
});
|
||||
|
||||
if !api_key.is_empty() {
|
||||
msg.as_object_mut().unwrap().insert("api_key".to_string(), json!(api_key));
|
||||
}
|
||||
|
||||
if !gpu_json.is_empty() {
|
||||
msg.as_object_mut().unwrap().insert("gpus".to_string(), json!(gpu_json));
|
||||
}
|
||||
|
||||
if let Some(models) = models_data {
|
||||
msg.as_object_mut().unwrap().insert("models".to_string(), models);
|
||||
}
|
||||
|
||||
msg.to_string()
|
||||
}
|
||||
|
||||
@@ -244,10 +270,24 @@ fn format_optional<T: std::fmt::Display>(val: Option<T>, suffix: &str) -> String
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let file_appender = tracing_appender::rolling::never(".", "native-node.log");
|
||||
let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
|
||||
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter("native_node=debug")
|
||||
.with_writer(non_blocking)
|
||||
.init();
|
||||
|
||||
// Hookataan paniikkitilanteet palauttamaan terminaalin raw-moodista
|
||||
let original_hook = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(move |panic_info| {
|
||||
tui_dashboard::restore_terminal();
|
||||
original_hook(panic_info);
|
||||
}));
|
||||
|
||||
let tui_state = std::sync::Arc::new(tokio::sync::RwLock::new(tui_dashboard::DashboardState::new()));
|
||||
let (cmd_tx, mut cmd_rx) = tokio::sync::mpsc::unbounded_channel::<String>();
|
||||
|
||||
let hub_url = std::env::var("HUB_URL").unwrap_or_else(|_| "ws://hub:3000/ws".to_string());
|
||||
let allocated_gb: u32 = std::env::var("ALLOCATED_GB")
|
||||
.ok()
|
||||
@@ -263,9 +303,24 @@ async fn main() {
|
||||
sys["cpu_cores"],
|
||||
sys["ram_total_mb"]
|
||||
);
|
||||
|
||||
{
|
||||
let mut st = tui_state.write().await;
|
||||
st.sys_info = format!("{} | {} | {} ydintä | {} MB RAM",
|
||||
sys["hostname"].as_str().unwrap_or("?"),
|
||||
sys["os"].as_str().unwrap_or("?"),
|
||||
sys["cpu_cores"],
|
||||
sys["ram_total_mb"]
|
||||
);
|
||||
let i = st.sys_info.clone();
|
||||
st.push_log("System", format!("Järjestelmä: {}", i), None);
|
||||
}
|
||||
|
||||
let gpus = collect_all_gpus();
|
||||
if gpus.is_empty() {
|
||||
#[cfg(not(feature = "gpu-detect"))]
|
||||
tracing::info!("GPU-tunnistus ei käytössä (--no-default-features). Ollama käyttää GPU:ta automaattisesti jos saatavilla.");
|
||||
#[cfg(feature = "gpu-detect")]
|
||||
tracing::info!("GPU:ta ei havaittu — toimitaan CPU-moodissa");
|
||||
} else {
|
||||
for (i, gpu) in gpus.iter().enumerate() {
|
||||
@@ -282,35 +337,366 @@ async fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Yhdistetään hubiin — yritetään uudelleen katkon sattuessa
|
||||
// Ollama-backend
|
||||
tracing::info!("Alustetaan Ollama-yhteyttä...");
|
||||
let llm = match inference::LlmEngine::load().await {
|
||||
Ok(engine) => {
|
||||
// Varmistetaan malli (ollama pull) — odotetaan kunnes valmis
|
||||
match engine.ensure_model().await {
|
||||
Ok(()) => tracing::info!("Ollama valmis inferenssiin!"),
|
||||
Err(e) => tracing::warn!("Mallin lataus: {} — yritetään silti", e),
|
||||
}
|
||||
Some(engine)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Ollama-alustus epäonnistui: {} — toimitaan ilman inferenssiä", e);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let active_model = llm.as_ref().map(|e| e.model_name()).unwrap_or_else(|| "unknown".to_string());
|
||||
tracing::info!("Käytettävä kielimalli konfiguroitu (selected_task): {}", active_model);
|
||||
|
||||
{
|
||||
let mut st = tui_state.write().await;
|
||||
st.model_name = active_model.clone();
|
||||
st.push_log("System", format!("Malli valmis: {}", active_model), None);
|
||||
}
|
||||
|
||||
// Lämmittelykutsu: ladataan malli VRAM:iin ja haetaan VRAM-tila
|
||||
if let Some(ref engine) = llm {
|
||||
{
|
||||
let mut st = tui_state.write().await;
|
||||
st.vram_status = "Ladataan VRAM:iin...".to_string();
|
||||
st.push_log("System", "Ladataan mallia VRAM:iin...".to_string(), None);
|
||||
}
|
||||
// Lyhyt generate-kutsu pakottaa Ollaman lataamaan mallin GPU:lle
|
||||
let _ = engine.generate("hi", &inference::GenerateOptions {
|
||||
max_tokens: 1, system_prompt: None, temperature: Some(0.0),
|
||||
top_k: Some(1), repeat_penalty: None, stop: None,
|
||||
}).await;
|
||||
if let Ok(Some(ps)) = engine.fetch_ps().await {
|
||||
let mut st = tui_state.write().await;
|
||||
st.vram_status = ps.display();
|
||||
st.push_log("System", format!("VRAM: {}", ps.display()), None);
|
||||
}
|
||||
let vram_engine_url = engine.ollama_url().to_string();
|
||||
let vram_state = tui_state.clone();
|
||||
tokio::spawn(async move {
|
||||
let client = reqwest::Client::new();
|
||||
loop {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(30)).await;
|
||||
if let Ok(resp) = client.get(format!("{}/api/ps", vram_engine_url)).send().await {
|
||||
if let Ok(body) = resp.json::<serde_json::Value>().await {
|
||||
if let Some(arr) = body["models"].as_array() {
|
||||
if let Some(m) = arr.first() {
|
||||
let name = m["name"].as_str().unwrap_or("?").to_string();
|
||||
let size = m["size"].as_u64().unwrap_or(0);
|
||||
let size_vram = m["size_vram"].as_u64().unwrap_or(0);
|
||||
let status = inference::ModelVramStatus { name, size, size_vram };
|
||||
vram_state.write().await.vram_status = status.display();
|
||||
} else {
|
||||
vram_state.write().await.vram_status = "Ei ladattua mallia".to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Käynnistetään graafinen TUI vain jos stdin on terminaali (ei taustaprosessina)
|
||||
let ui_state = tui_state.clone();
|
||||
if std::io::stdin().is_terminal() {
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = tui_dashboard::run_dashboard(ui_state, cmd_tx).await {
|
||||
tracing::error!("Pääluupin TUI kaatui: {}", e);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
tracing::info!("Ei terminaalia — TUI ohitettu, lokitetaan stdoutiin");
|
||||
};
|
||||
|
||||
// Haetaan paikalliset mallit hubille lähetettäväksi
|
||||
let mut available_models = None;
|
||||
if let Some(ref engine) = llm {
|
||||
match engine.fetch_models().await {
|
||||
Ok(models) => {
|
||||
available_models = Some(models);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Mallilistauksen haku epäonnistui: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Yhdistetään hubiin
|
||||
loop {
|
||||
match connect_async(&hub_url).await {
|
||||
Ok((ws_stream, _)) => {
|
||||
tracing::info!("Yhdistetty hubiin!");
|
||||
let (mut write, mut read) = ws_stream.split();
|
||||
|
||||
let auth = build_auth_message(allocated_gb);
|
||||
let auth = build_auth_message(allocated_gb, &active_model, available_models.clone());
|
||||
if write.send(Message::Text(auth)).await.is_err() {
|
||||
tracing::error!("Auth-viestin lähetys epäonnistui");
|
||||
continue;
|
||||
}
|
||||
|
||||
while let Some(Ok(msg)) = read.next().await {
|
||||
if let Message::Text(text) = msg {
|
||||
if text.contains("pair_task") || text.contains("ai_task") {
|
||||
tracing::debug!("Tehtävä vastaanotettu: {}", &text[..text.len().min(80)]);
|
||||
let reply = json!({
|
||||
"type": "result",
|
||||
"status": "success",
|
||||
"data": "native-node: ei vielä laskentaa"
|
||||
});
|
||||
let _ = write.send(Message::Text(reply.to_string())).await;
|
||||
// Merkitään yhdistetyksi TUI:ssa
|
||||
{
|
||||
let mut st = tui_state.write().await;
|
||||
st.status = "ACTIVE".to_string();
|
||||
st.push_log("Network", "Yhdistetty hubiin".to_string(), None);
|
||||
}
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
cmd = cmd_rx.recv() => {
|
||||
if let Some(cmd_str) = cmd {
|
||||
if cmd_str == "pause" {
|
||||
tracing::info!("Tauotetaan solmun suoritus (Hub ei lähetä tehtäviä)...");
|
||||
let req = json!({"type": "status_update", "status": "paused"});
|
||||
let _ = write.send(Message::Text(req.to_string())).await;
|
||||
{
|
||||
let mut st = tui_state.write().await;
|
||||
st.status = "PAUSED".to_string();
|
||||
st.push_log("Network", "Solmu siirretty taukotilaan".to_string(), None);
|
||||
}
|
||||
} else if cmd_str == "resume" {
|
||||
tracing::info!("Jatketaan solmun suoritusta...");
|
||||
let req = json!({"type": "status_update", "status": "active"});
|
||||
let _ = write.send(Message::Text(req.to_string())).await;
|
||||
{
|
||||
let mut st = tui_state.write().await;
|
||||
st.status = "ACTIVE".to_string();
|
||||
st.push_log("System", "Suoritus jatkuu...".to_string(), None);
|
||||
}
|
||||
} else if cmd_str == "fetch_models" {
|
||||
// Haetaan mallit Ollamasta ja avataan valikkö
|
||||
if let Some(ref engine) = llm {
|
||||
match engine.fetch_models().await {
|
||||
Ok(tags) => {
|
||||
let models: Vec<String> = tags.get("models")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| arr.iter()
|
||||
.filter_map(|m| m.get("name").and_then(|n| n.as_str()).map(|s| s.to_string()))
|
||||
.collect())
|
||||
.unwrap_or_default();
|
||||
let mut st = tui_state.write().await;
|
||||
st.model_picker_items = models;
|
||||
st.model_picker_idx = 0;
|
||||
st.model_picker_open = true;
|
||||
}
|
||||
Err(e) => {
|
||||
let mut st = tui_state.write().await;
|
||||
st.push_log("System", format!("Mallilistan haku epäonnistui: {}", e), None);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Some(model) = cmd_str.strip_prefix("change_model:") {
|
||||
// TUI:sta valittu malli — vaihdetaan
|
||||
if let Some(ref engine) = llm {
|
||||
engine.set_model(model.to_string());
|
||||
match engine.ensure_model().await {
|
||||
Ok(()) => {
|
||||
tracing::info!("Malli vaihdettu: {}", model);
|
||||
let mut st = tui_state.write().await;
|
||||
st.model_name = model.to_string();
|
||||
st.push_log("System", format!("Malli vaihdettu: {}", model), None);
|
||||
// Ilmoitetaan hubille
|
||||
let auth = build_auth_message(allocated_gb, model, available_models.clone());
|
||||
let _ = write.send(Message::Text(auth)).await;
|
||||
}
|
||||
Err(e) => {
|
||||
let mut st = tui_state.write().await;
|
||||
st.push_log("System", format!("Mallin vaihto epäonnistui: {}", e), None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ws_msg = read.next() => {
|
||||
match ws_msg {
|
||||
Some(Ok(Message::Text(text))) => {
|
||||
// Hubin control-viestit
|
||||
if text.contains(r#""type":"control""#) {
|
||||
if let Ok(task) = serde_json::from_str::<serde_json::Value>(&text) {
|
||||
if let Some(action) = task.get("action").and_then(|v| v.as_str()) {
|
||||
if action == "pause" {
|
||||
tracing::info!("Hub pakotti solmun tauolle (Pause)");
|
||||
let req = json!({"type": "status_update", "status": "paused"});
|
||||
let _ = write.send(Message::Text(req.to_string())).await;
|
||||
{
|
||||
let mut st = tui_state.write().await;
|
||||
st.status = "PAUSED".to_string();
|
||||
st.push_log("Network", "Hub kytki solmun tauolle".to_string(), None);
|
||||
}
|
||||
} else if action == "resume" {
|
||||
tracing::info!("Hub aktivoi solmun suorituksen (Resume)");
|
||||
let req = json!({"type": "status_update", "status": "active"});
|
||||
let _ = write.send(Message::Text(req.to_string())).await;
|
||||
{
|
||||
let mut st = tui_state.write().await;
|
||||
st.status = "ACTIVE".to_string();
|
||||
st.push_log("Network", "Hub palautti solmun töihin".to_string(), None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Node joined → oma node_id
|
||||
if text.contains(r#""type":"node_joined""#) {
|
||||
if let Ok(msg) = serde_json::from_str::<serde_json::Value>(&text) {
|
||||
if let Some(nid) = msg.get("node_id").and_then(|v| v.as_u64()) {
|
||||
let mut st = tui_state.write().await;
|
||||
if st.node_id.is_none() {
|
||||
st.node_id = Some(nid);
|
||||
st.push_log("Network", format!("Node ID: #{}", nid), None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Verkon globaali tila
|
||||
if text.contains(r#""type":"network_status""#) {
|
||||
if let Ok(status) = serde_json::from_str::<serde_json::Value>(&text) {
|
||||
if let Some(nodes) = status.get("active_nodes").and_then(|v| v.as_u64()) {
|
||||
if let Some(tasks) = status.get("tasks").and_then(|v| v.as_u64()) {
|
||||
let mut st = tui_state.write().await;
|
||||
st.network_active_nodes = nodes as usize;
|
||||
st.network_total_tasks = tasks;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LLM-promptit
|
||||
if text.contains("llm_prompt") {
|
||||
if let Ok(task) = serde_json::from_str::<serde_json::Value>(&text) {
|
||||
let prompt = task.get("prompt").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let task_id = task.get("task_id").and_then(|v| v.as_str()).unwrap_or("?");
|
||||
let msg_model = task.get("model").and_then(|v| v.as_str()).unwrap_or("");
|
||||
|
||||
if !prompt.is_empty() && (msg_model.starts_with("qwen-coder") || msg_model.starts_with("qwen2.5-coder") || msg_model.starts_with("phi")) {
|
||||
if let Some(ref engine) = llm {
|
||||
let gen_opts = inference::GenerateOptions {
|
||||
max_tokens: task.get("max_tokens").and_then(|v| v.as_u64()).unwrap_or(1024) as usize,
|
||||
system_prompt: task.get("system_prompt").and_then(|v| v.as_str()).map(|s| s.to_string()),
|
||||
temperature: task.get("temperature").and_then(|v| v.as_f64()),
|
||||
top_k: task.get("top_k").and_then(|v| v.as_u64()),
|
||||
repeat_penalty: task.get("repeat_penalty").and_then(|v| v.as_f64()),
|
||||
stop: task.get("stop").and_then(|v| v.as_array()).map(|a| a.iter().filter_map(|s| s.as_str().map(|s| s.to_string())).collect()),
|
||||
};
|
||||
let prompt_lines = prompt.lines().count();
|
||||
let prompt_last: String = prompt.lines().last().unwrap_or("").chars().take(60).collect();
|
||||
tracing::info!("→ task_id:{} | {}r prompti | \"{}...\"", task_id, prompt_lines, prompt_last);
|
||||
{
|
||||
let mut st = tui_state.write().await;
|
||||
st.cur_task_id = Some(task_id.to_string());
|
||||
st.cur_prompt = Some(format!("→ {} riviä | \"{}...\"", prompt_lines, prompt_last));
|
||||
}
|
||||
|
||||
let model_name = engine.model_name();
|
||||
match engine.generate(prompt, &gen_opts).await {
|
||||
Ok(result) => {
|
||||
let tokens_sec = (result.tokens_per_sec * 10.0).round() / 10.0;
|
||||
tracing::info!(
|
||||
"✓ {} | {} tok | {:.0}ms | {:.1} tok/s",
|
||||
model_name,
|
||||
result.tokens_generated,
|
||||
result.duration_ms,
|
||||
tokens_sec,
|
||||
);
|
||||
{
|
||||
let mut st = tui_state.write().await;
|
||||
st.tasks_completed += 1;
|
||||
st.last_tokens_sec = tokens_sec as f64;
|
||||
st.cur_task_id = None;
|
||||
st.cur_prompt = None;
|
||||
|
||||
let msg_type = if task_id == "status-check" { "Ping" } else { "Task" };
|
||||
let msg_text = format!("{} ({} tok)", task_id, result.tokens_generated);
|
||||
st.push_log(msg_type, msg_text, Some(tokens_sec as f64));
|
||||
}
|
||||
let prompt_short: String = prompt.lines().last().unwrap_or("").chars().take(100).collect();
|
||||
let done = json!({
|
||||
"type": "llm_done",
|
||||
"prompt": prompt_short,
|
||||
"model": format!("{} (Ollama)", model_name),
|
||||
"response": result.text,
|
||||
"tokens_generated": result.tokens_generated,
|
||||
"duration_ms": result.duration_ms,
|
||||
"tokens_per_sec": tokens_sec,
|
||||
"load_time_ms": 0,
|
||||
"task_id": task_id,
|
||||
});
|
||||
let _ = write.send(Message::Text(done.to_string())).await;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Inferenssivirhe: {}", e);
|
||||
{
|
||||
let mut st = tui_state.write().await;
|
||||
st.cur_task_id = None;
|
||||
st.cur_prompt = None;
|
||||
st.push_log("System", format!("Virhe inferenssissä: {}", e), None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
let mut st = tui_state.write().await;
|
||||
st.model_name = new_model.to_string();
|
||||
st.push_log("System", format!("Malli {} ladattu & valmis!", new_model), None);
|
||||
}
|
||||
Err(e) => tracing::error!("Mallin lataus epäonnistui: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Ok(_)) => {} // Muut viestityypit (binary/ping)
|
||||
Some(Err(_)) | None => break, // Yhteys poikki
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Yhteys katkesi — nollataan TUI:n busy-tila
|
||||
{
|
||||
let mut st = tui_state.write().await;
|
||||
let lost_task = st.cur_task_id.clone();
|
||||
if let Some(tid) = lost_task {
|
||||
st.push_log("Network", format!("Tehtävä {} keskeytyi yhteyden katketessa", tid), None);
|
||||
}
|
||||
st.cur_task_id = None;
|
||||
st.cur_prompt = None;
|
||||
st.node_id = None;
|
||||
st.status = "RECONNECTING".to_string();
|
||||
st.push_log("Network", "Yhteys hubiin katkesi — yhdistetään uudelleen 5s...".to_string(), None);
|
||||
}
|
||||
tracing::warn!("Yhteys hubiin katkesi — yritetään uudelleen 5s...");
|
||||
}
|
||||
Err(e) => {
|
||||
{
|
||||
let mut st = tui_state.write().await;
|
||||
st.status = "RECONNECTING".to_string();
|
||||
st.push_log("Network", format!("Yhdistäminen epäonnistui: {} — yritetään 5s...", e), None);
|
||||
}
|
||||
tracing::warn!("Hubiin yhdistäminen epäonnistui: {} — yritetään uudelleen 5s...", e);
|
||||
}
|
||||
}
|
||||
|
||||
67
network-poc/native-node/src/tui.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use dialoguer::{Select, Input, theme::ColorfulTheme};
|
||||
use reqwest::Client;
|
||||
|
||||
pub async fn select_model(ollama_url: &str, client: &Client) -> Result<String, String> {
|
||||
// 1. Hae tagit
|
||||
let mut models = vec![];
|
||||
println!(" Haetaan asennettuja malleja osoitteesta {}...", ollama_url);
|
||||
if let Ok(resp) = client.get(&format!("{}/api/tags", ollama_url)).send().await {
|
||||
if resp.status().is_success() {
|
||||
if let Ok(json) = resp.json::<serde_json::Value>().await {
|
||||
if let Some(arr) = json.get("models").and_then(|v| v.as_array()) {
|
||||
for m in arr {
|
||||
if let Some(name) = m.get("name").and_then(|v| v.as_str()) {
|
||||
models.push(name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let download_opt = "[➕ Lataa uusi malli internetistä]";
|
||||
let mut options = vec![download_opt.to_string()];
|
||||
options.extend(models);
|
||||
|
||||
// 2. Kysy käyttäjältä Selectillä
|
||||
let theme = ColorfulTheme::default();
|
||||
let selection = Select::with_theme(&theme)
|
||||
.with_prompt("Valitse Ollama-malli Kipinä-verkkoa varten:")
|
||||
.default(if options.len() > 1 { 1 } else { 0 })
|
||||
.items(&options)
|
||||
.interact()
|
||||
.map_err(|e| format!("TUI virhe: {}", e))?;
|
||||
|
||||
let selected = &options[selection];
|
||||
|
||||
// 3. Jos käyttäjä haluaa uuden, kysy nimeä
|
||||
if selected == download_opt {
|
||||
let new_model: String = Input::with_theme(&theme)
|
||||
.with_prompt("Syötä ladattavan mallin nimi (esim. llama3 tai qwen2.5-coder:3b)")
|
||||
.interact_text()
|
||||
.map_err(|e| format!("TUI virhe: {}", e))?;
|
||||
|
||||
let new_model = new_model.trim().to_string();
|
||||
if new_model.is_empty() {
|
||||
return Err("Mallin nimi ei voi olla tyhjä".to_string());
|
||||
}
|
||||
|
||||
println!(" Ladataan malleja taustalla... Tämä voi kestää hetken ({})", new_model);
|
||||
// Odotetaan että pull on valmis
|
||||
let pull_body = serde_json::json!({ "name": &new_model });
|
||||
let resp = client.post(&format!("{}/api/pull", ollama_url))
|
||||
.json(&pull_body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Pull req virhe: {}", e))?;
|
||||
|
||||
if resp.status().is_success() {
|
||||
println!(" ✓ Malli {} ladattu onnistuneesti!", new_model);
|
||||
return Ok(new_model);
|
||||
} else {
|
||||
return Err(format!("Ollama pull epäonnistui: {}", resp.status()));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(selected.clone())
|
||||
}
|
||||
330
network-poc/native-node/src/tui_dashboard.rs
Normal file
@@ -0,0 +1,330 @@
|
||||
use crossterm::{
|
||||
event::{Event, EventStream, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::CrosstermBackend,
|
||||
layout::{Constraint, Direction, Layout, Alignment},
|
||||
style::{Color, Modifier, Style},
|
||||
widgets::{Block, Borders, Paragraph, Wrap},
|
||||
Terminal,
|
||||
};
|
||||
use std::io;
|
||||
use tokio::sync::RwLock;
|
||||
use std::sync::Arc;
|
||||
use futures_util::StreamExt;
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LogEntry {
|
||||
pub ty: String,
|
||||
pub msg: String,
|
||||
pub speed: Option<f64>,
|
||||
pub timestamp: String,
|
||||
}
|
||||
|
||||
pub struct DashboardState {
|
||||
pub logs: Vec<LogEntry>,
|
||||
pub status: String,
|
||||
pub node_id: Option<u64>,
|
||||
pub sys_info: String,
|
||||
pub model_name: String,
|
||||
pub cur_task_id: Option<String>,
|
||||
pub cur_prompt: Option<String>,
|
||||
pub tasks_completed: u32,
|
||||
pub last_tokens_sec: f64,
|
||||
pub network_active_nodes: usize,
|
||||
pub network_total_tasks: u64,
|
||||
// VRAM-tila (ollama ps)
|
||||
pub vram_status: String,
|
||||
// Mallivalikko
|
||||
pub model_picker_open: bool,
|
||||
pub model_picker_items: Vec<String>,
|
||||
pub model_picker_idx: usize,
|
||||
}
|
||||
|
||||
impl DashboardState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
logs: Vec::new(),
|
||||
status: "ACTIVE".to_string(),
|
||||
node_id: None,
|
||||
sys_info: "".to_string(),
|
||||
model_name: "Yhdistetään...".to_string(),
|
||||
cur_task_id: None,
|
||||
cur_prompt: None,
|
||||
tasks_completed: 0,
|
||||
last_tokens_sec: 0.0,
|
||||
network_active_nodes: 1, // oletetaan itsemme
|
||||
network_total_tasks: 0,
|
||||
vram_status: "Haetaan...".to_string(),
|
||||
model_picker_open: false,
|
||||
model_picker_items: Vec::new(),
|
||||
model_picker_idx: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_log(&mut self, ty: &str, msg: String, speed: Option<f64>) {
|
||||
let now = chrono::Local::now().format("%H:%M:%S").to_string();
|
||||
self.logs.push(LogEntry {
|
||||
timestamp: now,
|
||||
ty: ty.to_string(),
|
||||
msg,
|
||||
speed,
|
||||
});
|
||||
if self.logs.len() > 100 {
|
||||
self.logs.remove(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_dashboard(
|
||||
state: Arc<RwLock<DashboardState>>,
|
||||
cmd_tx: tokio::sync::mpsc::UnboundedSender<String>,
|
||||
) -> Result<(), io::Error> {
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
terminal.clear()?;
|
||||
|
||||
let mut reader = EventStream::new();
|
||||
let mut interval = tokio::time::interval(Duration::from_millis(100));
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = interval.tick() => {
|
||||
let st = state.read().await;
|
||||
terminal.draw(|f| ui(f, &st))?;
|
||||
}
|
||||
ev = reader.next() => {
|
||||
if let Some(Ok(Event::Key(key))) = ev {
|
||||
let picker_open = state.read().await.model_picker_open;
|
||||
|
||||
if picker_open {
|
||||
// Mallivalikko auki — navigointi
|
||||
match key.code {
|
||||
KeyCode::Up | KeyCode::Char('k') => {
|
||||
let mut st = state.write().await;
|
||||
if st.model_picker_idx > 0 { st.model_picker_idx -= 1; }
|
||||
}
|
||||
KeyCode::Down | KeyCode::Char('j') => {
|
||||
let mut st = state.write().await;
|
||||
let max = st.model_picker_items.len().saturating_sub(1);
|
||||
if st.model_picker_idx < max { st.model_picker_idx += 1; }
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
let mut st = state.write().await;
|
||||
let idx = st.model_picker_idx;
|
||||
if let Some(model) = st.model_picker_items.get(idx).cloned() {
|
||||
st.model_picker_open = false;
|
||||
st.push_log("System", format!("Vaihdetaan malliin: {}...", model), None);
|
||||
let _ = cmd_tx.send(format!("change_model:{}", model));
|
||||
}
|
||||
}
|
||||
KeyCode::Esc | KeyCode::Char('m') | KeyCode::Char('M') => {
|
||||
state.write().await.model_picker_open = false;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
// Normaali tila
|
||||
match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => {
|
||||
disable_raw_mode()?;
|
||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
||||
std::process::exit(0);
|
||||
}
|
||||
KeyCode::Char('p') | KeyCode::Char('P') => {
|
||||
let _ = cmd_tx.send("pause".to_string());
|
||||
}
|
||||
KeyCode::Char('r') | KeyCode::Char('R') | KeyCode::Char('s') => {
|
||||
let _ = cmd_tx.send("resume".to_string());
|
||||
}
|
||||
KeyCode::Char('m') | KeyCode::Char('M') => {
|
||||
let _ = cmd_tx.send("fetch_models".to_string());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn restore_terminal() {
|
||||
let _ = disable_raw_mode();
|
||||
let _ = execute!(io::stdout(), LeaveAlternateScreen);
|
||||
}
|
||||
|
||||
fn ui(f: &mut ratatui::Frame, st: &DashboardState) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(3), // Header
|
||||
Constraint::Min(0), // Body
|
||||
Constraint::Length(3), // Footer / Status
|
||||
].as_ref())
|
||||
.split(f.area());
|
||||
|
||||
// --- Header ---
|
||||
let header_text = match st.node_id {
|
||||
Some(id) => format!(" Kipinä Agentic Node #{} ", id),
|
||||
None => " Kipinä Agentic Node (Yhdistää...) ".to_string(),
|
||||
};
|
||||
let header = Paragraph::new(header_text)
|
||||
.style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD))
|
||||
.alignment(Alignment::Center)
|
||||
.block(Block::default().borders(Borders::ALL).style(Style::default().fg(Color::DarkGray)));
|
||||
f.render_widget(header, chunks[0]);
|
||||
|
||||
// --- Body ---
|
||||
let body_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(8), // Yläosan info ja tehtävä
|
||||
Constraint::Min(0), // Lokit / Chat alas
|
||||
].as_ref())
|
||||
.split(chunks[1]);
|
||||
|
||||
let top_panels = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([
|
||||
Constraint::Percentage(40), // Vasen paneeli (Info)
|
||||
Constraint::Percentage(60), // Oikea paneeli (Tehtävä)
|
||||
].as_ref())
|
||||
.split(body_chunks[0]);
|
||||
|
||||
// Vasen paneeli: Laitteisto, Malli & Verkosto — VRAM-rivi värikoodattu
|
||||
let vram_color = if st.vram_status.starts_with('✓') {
|
||||
Color::Green
|
||||
} else if st.vram_status.starts_with('◐') {
|
||||
Color::Yellow
|
||||
} else if st.vram_status.starts_with('✗') {
|
||||
Color::Red
|
||||
} else {
|
||||
Color::DarkGray
|
||||
};
|
||||
|
||||
let info_lines = vec![
|
||||
ratatui::text::Line::from(vec![
|
||||
ratatui::text::Span::raw("🚀 Malli: "),
|
||||
ratatui::text::Span::styled(&st.model_name, Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
|
||||
]),
|
||||
ratatui::text::Line::from(vec![
|
||||
ratatui::text::Span::raw("🎮 VRAM: "),
|
||||
ratatui::text::Span::styled(&st.vram_status, Style::default().fg(vram_color)),
|
||||
]),
|
||||
ratatui::text::Line::from(vec![
|
||||
ratatui::text::Span::raw("💻 Järjestelmä: "),
|
||||
ratatui::text::Span::styled(&st.sys_info, Style::default().fg(Color::White)),
|
||||
]),
|
||||
ratatui::text::Line::from(format!(
|
||||
"📊 Tehdyt: {} | Nopeus: {:.1} t/s", st.tasks_completed, st.last_tokens_sec
|
||||
)),
|
||||
ratatui::text::Line::from(format!(
|
||||
"🌐 Verkosto: {} solmua | {} tehtävää", st.network_active_nodes, st.network_total_tasks
|
||||
)),
|
||||
];
|
||||
let left_panel = Paragraph::new(info_lines)
|
||||
.block(Block::default().title(" Laitteisto ja AI ").borders(Borders::ALL))
|
||||
.style(Style::default().fg(Color::White))
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(left_panel, top_panels[0]);
|
||||
|
||||
// Oikea paneeli: Käynnissä oleva tehtävä
|
||||
let task_title = match &st.cur_task_id {
|
||||
Some(id) => format!(" Työn alla: {} ", id),
|
||||
None => " Vapaana ".to_string(),
|
||||
};
|
||||
let task_content = st.cur_prompt.clone().unwrap_or_else(|| "Odotetaan tehtäviä Hubilta...".to_string());
|
||||
|
||||
let task_style = if st.cur_task_id.is_some() {
|
||||
Style::default().fg(Color::Magenta)
|
||||
} else {
|
||||
Style::default().fg(Color::DarkGray)
|
||||
};
|
||||
|
||||
let task_panel = Paragraph::new(task_content)
|
||||
.wrap(Wrap { trim: true })
|
||||
.block(Block::default().title(task_title).borders(Borders::ALL).style(task_style));
|
||||
f.render_widget(task_panel, top_panels[1]);
|
||||
|
||||
// Alaosan paneeli: Tapahtumaloki koko leveydeltä
|
||||
let area_height = body_chunks[1].height.saturating_sub(2) as usize;
|
||||
let skip_count = if st.logs.len() > area_height { st.logs.len() - area_height } else { 0 };
|
||||
|
||||
let visible_logs: Vec<ratatui::text::Line> = st.logs.iter().skip(skip_count).map(|log| {
|
||||
let ty_color = match log.ty.as_str() {
|
||||
"System" => Color::Yellow,
|
||||
"Network" => Color::Blue,
|
||||
"Task" => Color::Magenta,
|
||||
"Ping" => Color::DarkGray,
|
||||
_ => Color::White,
|
||||
};
|
||||
|
||||
let speed_str = if let Some(s) = log.speed {
|
||||
format!(" | {:.1} tok/s", s)
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
|
||||
ratatui::text::Line::from(vec![
|
||||
ratatui::text::Span::styled(&log.timestamp, Style::default().fg(Color::DarkGray)),
|
||||
ratatui::text::Span::raw(" "),
|
||||
ratatui::text::Span::styled(format!("{: <8}", log.ty), Style::default().fg(ty_color).add_modifier(Modifier::BOLD)),
|
||||
ratatui::text::Span::raw(" | "),
|
||||
ratatui::text::Span::styled(log.msg.clone(), Style::default().fg(Color::White)),
|
||||
ratatui::text::Span::styled(speed_str, Style::default().fg(ty_color)),
|
||||
])
|
||||
}).collect();
|
||||
|
||||
let logs_panel = Paragraph::new(visible_logs)
|
||||
.block(Block::default().title(" Tapahtumaloki ").borders(Borders::ALL).style(Style::default().fg(Color::Cyan)));
|
||||
f.render_widget(logs_panel, body_chunks[1]);
|
||||
|
||||
// --- Footer / Status ---
|
||||
let status_color = if st.status == "ACTIVE" { Color::Green } else { Color::Yellow };
|
||||
let status_text = format!(" Tila: {} | [P] Pause [R] Työhön [M] Malli [Q] Sulje ", st.status);
|
||||
let footer = Paragraph::new(status_text)
|
||||
.style(Style::default().fg(status_color).add_modifier(Modifier::BOLD))
|
||||
.alignment(Alignment::Center)
|
||||
.block(Block::default().borders(Borders::ALL));
|
||||
f.render_widget(footer, chunks[2]);
|
||||
|
||||
// --- Mallivalikko-overlay ---
|
||||
if st.model_picker_open && !st.model_picker_items.is_empty() {
|
||||
let area = f.area();
|
||||
let popup_h = (st.model_picker_items.len() as u16 + 4).min(area.height - 4);
|
||||
let popup_w = 50.min(area.width - 4);
|
||||
let popup = ratatui::layout::Rect::new(
|
||||
(area.width - popup_w) / 2,
|
||||
(area.height - popup_h) / 2,
|
||||
popup_w,
|
||||
popup_h,
|
||||
);
|
||||
|
||||
// Tausta
|
||||
f.render_widget(ratatui::widgets::Clear, popup);
|
||||
|
||||
let items: Vec<ratatui::text::Line> = st.model_picker_items.iter().enumerate().map(|(i, name)| {
|
||||
if i == st.model_picker_idx {
|
||||
ratatui::text::Line::from(format!(" ▸ {} ", name))
|
||||
.style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD))
|
||||
} else {
|
||||
ratatui::text::Line::from(format!(" {} ", name))
|
||||
.style(Style::default().fg(Color::White))
|
||||
}
|
||||
}).collect();
|
||||
|
||||
let picker = Paragraph::new(items)
|
||||
.block(Block::default()
|
||||
.title(" Vaihda malli [↑↓] Enter=valitse Esc=peruuta ")
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().fg(Color::Cyan)));
|
||||
f.render_widget(picker, popup);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "node"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
@@ -10,23 +10,23 @@ crate-type = ["cdylib"]
|
||||
wasm-bindgen = "0.2.91"
|
||||
js-sys = "0.3.68"
|
||||
web-sys = { version = "0.3.68", features = [
|
||||
"Window",
|
||||
"Document",
|
||||
"HtmlElement",
|
||||
"WebSocket",
|
||||
"MessageEvent",
|
||||
"Performance",
|
||||
"console",
|
||||
"Response",
|
||||
"ReadableStream",
|
||||
"ReadableStreamDefaultReader",
|
||||
] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
burn = { version = "0.14.0", features = ["wgpu", "ndarray"] }
|
||||
burn-wgpu = "0.14.0"
|
||||
burn-ndarray = "0.14.0"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json"] }
|
||||
tokenizers = { version = "0.19.1", default-features = false, features = ["unstable_wasm"] }
|
||||
rexie = "0.6"
|
||||
log = "0.4"
|
||||
candle-core = "0.8"
|
||||
candle-nn = "0.8"
|
||||
candle-transformers = "0.8"
|
||||
getrandom = { version = "0.3", features = ["wasm_js"] }
|
||||
|
||||
|
||||
BIN
network-poc/node/nodes.db
Normal file
@@ -1,21 +1,31 @@
|
||||
use wasm_bindgen::prelude::*;
|
||||
use web_sys::{console, WebSocket, MessageEvent};
|
||||
use web_sys::{WebSocket, MessageEvent};
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::sync::atomic::{AtomicU32, AtomicBool, Ordering};
|
||||
use burn::tensor::Tensor;
|
||||
use burn::backend::{Wgpu, NdArray};
|
||||
|
||||
pub mod storage;
|
||||
pub mod sampling;
|
||||
pub mod qwen;
|
||||
pub mod qwen_coder;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! console_log {
|
||||
($($t:tt)*) => (console::log_1(&format_args!($($t)*).to_string().into()))
|
||||
($($t:tt)*) => (web_sys::console::log_1(&format_args!($($t)*).to_string().into()))
|
||||
}
|
||||
|
||||
// Globaali muuttuja GPU Load Sliderille (25-100%)
|
||||
static GPU_LOAD_PERCENT: AtomicU32 = AtomicU32::new(50);
|
||||
// Onko WebGPU käytettävissä — asetetaan JS-puolelta käynnistyksessä
|
||||
static HAS_WEBGPU: AtomicBool = AtomicBool::new(true);
|
||||
static SELECTED_TASK: AtomicU32 = AtomicU32::new(0);
|
||||
static LLM_BUSY: AtomicBool = AtomicBool::new(false);
|
||||
// Käsitelläänkö hubin automaattisia tehtäviä
|
||||
static AUTO_TASKS: AtomicBool = AtomicBool::new(true);
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn set_auto_tasks(enabled: bool) {
|
||||
AUTO_TASKS.store(enabled, Ordering::SeqCst);
|
||||
console_log!("[Wasm] Automaattiset tehtävät: {}", if enabled { "päällä" } else { "pois" });
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn set_gpu_load(load: u32) {
|
||||
@@ -23,51 +33,70 @@ pub fn set_gpu_load(load: u32) {
|
||||
console_log!("[Wasm] GPU Kuormitusraja vaihdettu -> {}%", load);
|
||||
}
|
||||
|
||||
// Asynkroninen odotus WebAssemblylle
|
||||
async fn sleep_ms(ms: i32) {
|
||||
// Worker-yhteensopiva setTimeout — toimii sekä Window- että Worker-kontekstissa
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
#[wasm_bindgen(js_name = setTimeout)]
|
||||
fn set_timeout(closure: &js_sys::Function, ms: i32);
|
||||
}
|
||||
|
||||
// Asynkroninen odotus WebAssemblylle (Window + Worker)
|
||||
pub async fn sleep_ms(ms: i32) {
|
||||
let promise = js_sys::Promise::new(&mut |resolve, _| {
|
||||
web_sys::window()
|
||||
.unwrap()
|
||||
.set_timeout_with_callback_and_timeout_and_arguments_0(&resolve, ms)
|
||||
.unwrap();
|
||||
set_timeout(&resolve, ms);
|
||||
});
|
||||
let _ = wasm_bindgen_futures::JsFuture::from(promise).await;
|
||||
}
|
||||
|
||||
// Geneerinen tensorilaskenta — toimii millä tahansa Burn-backendillä
|
||||
fn run_matmul<B: burn::tensor::backend::Backend>(size: usize) -> String {
|
||||
let device = Default::default();
|
||||
let dist = burn::tensor::Distribution::Default;
|
||||
let t1: Tensor<B, 2> = Tensor::random([size, size], dist, &device);
|
||||
let t2: Tensor<B, 2> = Tensor::random([size, size], dist, &device);
|
||||
let sum = t1.matmul(t2).sum();
|
||||
format!("{:?}", sum)
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Päättelyfunktio — valitsee backendin automaattisesti
|
||||
async fn run_ai_tensor_inference(difficulty: usize) -> String {
|
||||
let load_pct = GPU_LOAD_PERCENT.load(Ordering::SeqCst);
|
||||
// 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())
|
||||
}
|
||||
|
||||
if load_pct == 0 {
|
||||
sleep_ms(2000).await;
|
||||
return format!("Paused (0%). Lepäillään zZz..");
|
||||
}
|
||||
|
||||
let active_workload_size = (difficulty as f32 * (load_pct as f32 / 100.0)) as usize;
|
||||
|
||||
let sleep_delay = (100 - load_pct) * 10;
|
||||
if sleep_delay > 0 {
|
||||
sleep_ms(sleep_delay as i32).await;
|
||||
}
|
||||
|
||||
let use_gpu = HAS_WEBGPU.load(Ordering::SeqCst);
|
||||
let (backend_name, result) = if use_gpu {
|
||||
("WebGPU", run_matmul::<Wgpu>(active_workload_size))
|
||||
} else {
|
||||
("CPU/NdArray", run_matmul::<NdArray>(active_workload_size))
|
||||
/// 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());
|
||||
};
|
||||
|
||||
format!("PoC {} Matmul ({}x{}) >> {}", backend_name, active_workload_size, active_workload_size, result)
|
||||
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
|
||||
@@ -102,6 +131,30 @@ fn tokenize_text(tokenizer: &tokenizers::Tokenizer, text: &str) -> serde_json::V
|
||||
}
|
||||
}
|
||||
|
||||
/// Tokenisoi yksittäisen tekstin ja lähettää tuloksen hubille
|
||||
async fn run_single_tokenize(text: String, ws: Rc<RefCell<WebSocket>>) {
|
||||
let cached_tok = storage::load_from_idb("tokenizer.json").await.unwrap_or(None);
|
||||
let Some(bytes) = cached_tok else { return; };
|
||||
let Ok(tokenizer) = tokenizers::Tokenizer::from_bytes(&bytes) else { return; };
|
||||
|
||||
let start = perf_now();
|
||||
let result = tokenize_text(&tokenizer, &text);
|
||||
let duration_ms = perf_now() - start;
|
||||
|
||||
let token_count = result["token_count"].as_u64().unwrap_or(0);
|
||||
let cpt = result["chars_per_token"].as_f64().unwrap_or(0.0);
|
||||
let preview: String = text.chars().take(50).collect();
|
||||
console_log!("Tokenisaatio: \"{}\" → {} tokenia | {:.2} m/t | {:.2}ms",
|
||||
preview, token_count, cpt, duration_ms);
|
||||
|
||||
let msg = serde_json::json!({
|
||||
"type": "single_tokenize_done",
|
||||
"result": result,
|
||||
"duration_ms": (duration_ms * 100.0).round() / 100.0,
|
||||
});
|
||||
let _ = ws.borrow().send_with_str(&msg.to_string());
|
||||
}
|
||||
|
||||
/// Tokenisoi en/fi-parin, vertaa tehokkuutta ja lähettää tuloksen hubille
|
||||
async fn run_pair_comparison(en_text: String, fi_text: String, ws: Rc<RefCell<WebSocket>>) {
|
||||
let load_pct = GPU_LOAD_PERCENT.load(Ordering::SeqCst);
|
||||
@@ -117,11 +170,10 @@ async fn run_pair_comparison(en_text: String, fi_text: String, ws: Rc<RefCell<We
|
||||
return;
|
||||
};
|
||||
|
||||
let perf = web_sys::window().unwrap().performance().unwrap();
|
||||
let start_time = perf.now();
|
||||
let start_time = perf_now();
|
||||
let en_result = tokenize_text(&tokenizer, &en_text);
|
||||
let fi_result = tokenize_text(&tokenizer, &fi_text);
|
||||
let duration_ms = perf.now() - start_time; // millisekunteja desimaalitarkkuudella
|
||||
let duration_ms = perf_now() - start_time;
|
||||
|
||||
let en_cpt = en_result["chars_per_token"].as_f64().unwrap_or(0.0);
|
||||
let fi_cpt = fi_result["chars_per_token"].as_f64().unwrap_or(0.0);
|
||||
@@ -148,12 +200,15 @@ async fn run_pair_comparison(en_text: String, fi_text: String, ws: Rc<RefCell<We
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub async fn start_agent_node(hub_url: String, has_webgpu: bool, device_info_json: String) -> Result<(), JsValue> {
|
||||
pub async fn start_agent_node(hub_url: String, has_webgpu: bool, device_info_json: String, task_id: u32) -> Result<(), JsValue> {
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
HAS_WEBGPU.store(has_webgpu, Ordering::SeqCst);
|
||||
SELECTED_TASK.store(task_id, Ordering::SeqCst);
|
||||
let backend_name = if has_webgpu { "WebGPU" } else { "CPU (NdArray)" };
|
||||
console_log!("Kipinä Agent Node käynnistyy — backend: {}", backend_name);
|
||||
let task_names = ["tokenize", "qwen-05b", "qwen-coder-05b", "qwen-coder-3b"];
|
||||
let task_name = task_names.get(task_id as usize).unwrap_or(&"tokenize");
|
||||
console_log!("Kipinä Agent Node käynnistyy — backend: {} | tehtävä: {}", backend_name, task_name);
|
||||
|
||||
let device_info = device_info_json.clone();
|
||||
|
||||
@@ -182,7 +237,11 @@ pub async fn start_agent_node(hub_url: String, has_webgpu: bool, device_info_jso
|
||||
if let Ok(txt) = e.data().dyn_into::<js_sys::JsString>() {
|
||||
let msg: String = txt.into();
|
||||
|
||||
if msg.contains("pair_task") {
|
||||
let current_task = SELECTED_TASK.load(Ordering::SeqCst);
|
||||
let auto_on = AUTO_TASKS.load(Ordering::SeqCst);
|
||||
|
||||
if msg.contains("pair_task") && current_task == 0 && auto_on {
|
||||
// Vain tokenisaatiosolmut käsittelevät pair_task-viestejä
|
||||
if let Ok(task) = serde_json::from_str::<serde_json::Value>(&msg) {
|
||||
let en = task.get("en").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let fi = task.get("fi").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
@@ -193,17 +252,68 @@ pub async fn start_agent_node(hub_url: String, has_webgpu: bool, device_info_jso
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if msg.contains("ai_task") {
|
||||
console_log!("Hub task vastaanotettu, ajetaan GPU:lla...");
|
||||
let ws_for_async = ws_clone.clone();
|
||||
let diff = if msg.contains(r#""difficulty":1024"#) { 1024 } else { 512 };
|
||||
|
||||
// Suoritetaan inference asynkronisesti erillisessä taaskissa välttääksemme UI-jäätymisen kokonaan
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let result = run_ai_tensor_inference(diff).await;
|
||||
let reply = format!("{{\"type\":\"result\", \"status\":\"success\", \"data\":\"{}\"}}", result);
|
||||
let _ = ws_for_async.borrow().send_with_str(&reply);
|
||||
});
|
||||
} else if msg.contains("single_tokenize") && current_task == 0 {
|
||||
if let Ok(task) = serde_json::from_str::<serde_json::Value>(&msg) {
|
||||
let text = task.get("text").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
if !text.is_empty() {
|
||||
let ws_for_async = ws_clone.clone();
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
run_single_tokenize(text, ws_for_async).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if msg.contains("llm_prompt") && current_task == 1 && auto_on {
|
||||
// Qwen2.5-0.5B
|
||||
if LLM_BUSY.load(Ordering::SeqCst) {
|
||||
} else if let Ok(task) = serde_json::from_str::<serde_json::Value>(&msg) {
|
||||
let prompt = task.get("prompt").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let model = task.get("model").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
if !prompt.is_empty() && model == "qwen-05b" {
|
||||
LLM_BUSY.store(true, Ordering::SeqCst);
|
||||
let ws_for_async = ws_clone.clone();
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
qwen::run_qwen_inference(prompt, ws_for_async).await;
|
||||
LLM_BUSY.store(false, Ordering::SeqCst);
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if msg.contains("llm_prompt") {
|
||||
console_log!("[DEBUG] llm_prompt vastaanotettu! current_task={}, busy={}", current_task, LLM_BUSY.load(Ordering::SeqCst));
|
||||
if current_task == 4 || current_task == 5 {
|
||||
// Qwen2.5-Coder: 4 = 0.5B, 5 = 3B
|
||||
if let Ok(task) = serde_json::from_str::<serde_json::Value>(&msg) {
|
||||
let prompt = task.get("prompt").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let model = task.get("model").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let task_id = task.get("task_id").and_then(|v| v.as_str()).map(|s| s.to_string());
|
||||
|
||||
if !prompt.is_empty() && model.starts_with("qwen-coder") {
|
||||
if LLM_BUSY.load(Ordering::SeqCst) {
|
||||
if let Some(tid) = task_id {
|
||||
let err_msg = serde_json::json!({
|
||||
"type": "llm_error",
|
||||
"task_id": tid,
|
||||
"error": "Solmu on paraikaa varattuna toisen tehtävän suorittamiseen"
|
||||
});
|
||||
let _ = ws_clone.borrow().send_with_str(&err_msg.to_string());
|
||||
}
|
||||
} else {
|
||||
// Välitetään parametrit JSON-promptina coderille
|
||||
let coder_prompt = serde_json::json!({
|
||||
"prompt": prompt,
|
||||
"system": task.get("system_prompt").and_then(|v| v.as_str()).unwrap_or(""),
|
||||
"max_tokens": task.get("max_tokens").and_then(|v| v.as_u64()).unwrap_or(512),
|
||||
}).to_string();
|
||||
let use_3b = current_task == 5;
|
||||
LLM_BUSY.store(true, Ordering::SeqCst);
|
||||
let ws_for_async = ws_clone.clone();
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
qwen_coder::run_coder_inference(coder_prompt, ws_for_async, use_3b, task_id).await;
|
||||
LLM_BUSY.store(false, Ordering::SeqCst);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} // current_task == 4 || 5
|
||||
} else if msg.contains("stats") {
|
||||
// Sivuutetaan statsit täällä, UI hallitsee ne aivan itse HTML:n puolella
|
||||
}
|
||||
|
||||
217
network-poc/node/src/qwen.rs
Normal file
@@ -0,0 +1,217 @@
|
||||
use candle_core::{Device, Tensor, DType};
|
||||
use candle_nn::VarBuilder;
|
||||
use candle_transformers::models::qwen2::{Config as QwenConfig, ModelForCausalLM as QwenModel};
|
||||
use wasm_bindgen::JsCast;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use web_sys::WebSocket;
|
||||
|
||||
use crate::storage;
|
||||
|
||||
macro_rules! console_log {
|
||||
($($t:tt)*) => (web_sys::console::log_1(&format_args!($($t)*).to_string().into()))
|
||||
}
|
||||
|
||||
const MODEL_URL: &str = "https://huggingface.co/Qwen/Qwen2.5-0.5B-Instruct/resolve/main/model.safetensors";
|
||||
const TOKENIZER_URL: &str = "https://huggingface.co/Qwen/Qwen2.5-0.5B-Instruct/resolve/main/tokenizer.json";
|
||||
|
||||
/// Streaming-lataus HuggingFacesta IndexedDB-cacheen
|
||||
async fn ensure_cached(key: &str, url: &str, ws: &Rc<RefCell<WebSocket>>) -> Result<Vec<u8>, String> {
|
||||
if let Ok(Some(bytes)) = storage::load_from_idb(key).await {
|
||||
console_log!("[Qwen] {} löytyi välimuistista ({} MB)", key, bytes.len() / 1024 / 1024);
|
||||
return Ok(bytes);
|
||||
}
|
||||
|
||||
console_log!("[Qwen] Ladataan {}...", key);
|
||||
|
||||
let resp = crate::worker_fetch(url).await?;
|
||||
if !resp.ok() { return Err(format!("HTTP {}", resp.status())); }
|
||||
|
||||
let total_size: usize = resp.headers()
|
||||
.get("content-length").ok().flatten()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
let body = resp.body().ok_or("Ei bodyä")?;
|
||||
let reader: web_sys::ReadableStreamDefaultReader = body.get_reader().dyn_into().map_err(|_| "Ei reader".to_string())?;
|
||||
|
||||
let mut data: Vec<u8> = Vec::with_capacity(total_size);
|
||||
let mut last_pct: u32 = 0;
|
||||
|
||||
loop {
|
||||
let chunk = wasm_bindgen_futures::JsFuture::from(reader.read())
|
||||
.await.map_err(|e| format!("Read: {:?}", e))?;
|
||||
let done = js_sys::Reflect::get(&chunk, &"done".into()).ok().and_then(|v| v.as_bool()).unwrap_or(true);
|
||||
if done { break; }
|
||||
let value = js_sys::Reflect::get(&chunk, &"value".into()).map_err(|_| "value puuttuu".to_string())?;
|
||||
let array = js_sys::Uint8Array::new(&value);
|
||||
let mut buf = vec![0u8; array.length() as usize];
|
||||
array.copy_to(&mut buf);
|
||||
data.extend_from_slice(&buf);
|
||||
|
||||
if total_size > 0 {
|
||||
let pct = ((data.len() as f64 / total_size as f64) * 100.0) as u32;
|
||||
if pct >= last_pct + 5 || pct == 100 {
|
||||
last_pct = pct;
|
||||
console_log!("[Qwen] {} lataus: {}%", key, pct);
|
||||
let msg = serde_json::json!({ "type": "download_progress", "file": key, "pct": pct, "loaded_mb": data.len()/1024/1024, "total_mb": total_size/1024/1024 });
|
||||
let _ = ws.borrow().send_with_str(&msg.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console_log!("[Qwen] Tallennetaan {} ({} MB)...", key, data.len() / 1024 / 1024);
|
||||
let _ = storage::save_to_idb(key, &data).await;
|
||||
console_log!("[Qwen] {} tallennettu!", key);
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
pub async fn run_qwen_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
|
||||
// performance via crate::perf_now()
|
||||
|
||||
let tok_bytes = match ensure_cached("qwen05b-tokenizer.json", TOKENIZER_URL, &ws).await {
|
||||
Ok(b) => b,
|
||||
Err(e) => { console_log!("[Qwen] Tokenizer-virhe: {}", e); return; }
|
||||
};
|
||||
let tokenizer = match tokenizers::Tokenizer::from_bytes(&tok_bytes) {
|
||||
Ok(t) => t,
|
||||
Err(e) => { console_log!("[Qwen] Tokenizer-parsinta: {}", e); return; }
|
||||
};
|
||||
|
||||
let model_bytes = match ensure_cached("qwen05b-model.safetensors", MODEL_URL, &ws).await {
|
||||
Ok(b) => b,
|
||||
Err(e) => { console_log!("[Qwen] Malli-virhe: {}", e); return; }
|
||||
};
|
||||
|
||||
console_log!("[Qwen] Rakennetaan mallia...");
|
||||
let start_load = crate::perf_now();
|
||||
let device = Device::Cpu;
|
||||
let dtype = DType::F32;
|
||||
|
||||
let tensors = match candle_core::safetensors::load_buffer(&model_bytes, &device) {
|
||||
Ok(t) => t,
|
||||
Err(e) => { console_log!("[Qwen] Safetensors: {}", e); return; }
|
||||
};
|
||||
let vb = VarBuilder::from_tensors(tensors, dtype, &device);
|
||||
|
||||
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 mut model = match QwenModel::new(&config, vb) {
|
||||
Ok(m) => m,
|
||||
Err(e) => { console_log!("[Qwen] Mallin lataus: {}", e); return; }
|
||||
};
|
||||
|
||||
let load_time = crate::perf_now() - start_load;
|
||||
console_log!("[Qwen] Malli ladattu ({:.0}ms). Generoidaan...", load_time);
|
||||
|
||||
let encoding = match tokenizer.encode(prompt.as_str(), true) {
|
||||
Ok(e) => e,
|
||||
Err(e) => { console_log!("[Qwen] Tokenisointivirhe: {}", e); return; }
|
||||
};
|
||||
let input_ids: Vec<u32> = encoding.get_ids().to_vec();
|
||||
let input_len = input_ids.len();
|
||||
console_log!("[Qwen] Syöte: {} tokenia", input_len);
|
||||
|
||||
let start_gen = crate::perf_now();
|
||||
let max_new_tokens = 32;
|
||||
let mut generated_text = String::new();
|
||||
let mut tokens_generated: usize = 0;
|
||||
|
||||
// Prefill
|
||||
let input = match Tensor::new(input_ids.as_slice(), &device).and_then(|t| t.unsqueeze(0)) {
|
||||
Ok(t) => t,
|
||||
Err(e) => { console_log!("[Qwen] Tensor: {}", e); return; }
|
||||
};
|
||||
let logits = match model.forward(&input, 0) {
|
||||
Ok(l) => l,
|
||||
Err(e) => { console_log!("[Qwen] Forward (prefill): {}", e); return; }
|
||||
};
|
||||
|
||||
// Forward palauttaa [batch, vocab_size] tai [batch, seq_len, vocab_size]
|
||||
let logits = logits.squeeze(0).unwrap();
|
||||
let logits = if logits.dims().len() == 2 {
|
||||
// [seq_len, vocab_size] — ota viimeinen
|
||||
logits.get(logits.dim(0).unwrap() - 1).unwrap()
|
||||
} else {
|
||||
logits // jo [vocab_size]
|
||||
};
|
||||
let mut next_token = crate::sampling::sample_top_k(&logits, 10, 5.0);
|
||||
console_log!("[Qwen] Ensimmäinen token: {}", next_token);
|
||||
|
||||
let eos_token = 151645u32; // <|endoftext|> for Qwen2.5
|
||||
|
||||
if next_token != eos_token {
|
||||
if let Ok(text) = tokenizer.decode(&[next_token], true) {
|
||||
generated_text.push_str(&text);
|
||||
let chunk = serde_json::json!({ "type": "llm_chunk", "token": text, "prompt": prompt, "model": "Qwen2.5-0.5B" });
|
||||
let _ = ws.borrow().send_with_str(&chunk.to_string());
|
||||
}
|
||||
tokens_generated += 1;
|
||||
}
|
||||
|
||||
// Autoregressive
|
||||
let mut pos = input_len;
|
||||
for _ in 1..max_new_tokens {
|
||||
if next_token == eos_token { break; }
|
||||
|
||||
let input = match Tensor::new(&[next_token], &device).and_then(|t| t.unsqueeze(0)) {
|
||||
Ok(t) => t,
|
||||
Err(e) => { console_log!("[Qwen] Tensor: {}", e); break; }
|
||||
};
|
||||
let logits = match model.forward(&input, pos) {
|
||||
Ok(l) => l,
|
||||
Err(e) => { console_log!("[Qwen] Forward pos {}: {}", pos, e); break; }
|
||||
};
|
||||
|
||||
let logits = logits.squeeze(0).unwrap();
|
||||
let logits = if logits.dims().len() == 2 {
|
||||
logits.get(logits.dim(0).unwrap() - 1).unwrap()
|
||||
} else {
|
||||
logits
|
||||
};
|
||||
next_token = crate::sampling::sample_top_k(&logits, 10, 5.0);
|
||||
pos += 1;
|
||||
|
||||
if next_token == eos_token { break; }
|
||||
|
||||
if let Ok(text) = tokenizer.decode(&[next_token], true) {
|
||||
generated_text.push_str(&text);
|
||||
let chunk = serde_json::json!({ "type": "llm_chunk", "token": text, "prompt": prompt, "model": "Qwen2.5-0.5B" });
|
||||
let _ = ws.borrow().send_with_str(&chunk.to_string());
|
||||
}
|
||||
tokens_generated += 1;
|
||||
crate::sleep_ms(0).await;
|
||||
}
|
||||
|
||||
let gen_time = crate::perf_now() - start_gen;
|
||||
let tokens_per_sec = if gen_time > 0.0 { (tokens_generated as f64 / gen_time) * 1000.0 } else { 0.0 };
|
||||
console_log!("[Qwen] {} tokenia | {:.0}ms | {:.1} tok/s", tokens_generated, gen_time, tokens_per_sec);
|
||||
|
||||
let done = serde_json::json!({
|
||||
"type": "llm_done",
|
||||
"prompt": prompt,
|
||||
"model": "Qwen2.5-0.5B-Instruct",
|
||||
"response": generated_text,
|
||||
"tokens_generated": tokens_generated,
|
||||
"duration_ms": (gen_time * 100.0).round() / 100.0,
|
||||
"tokens_per_sec": (tokens_per_sec * 10.0).round() / 10.0,
|
||||
"load_time_ms": (load_time * 100.0).round() / 100.0,
|
||||
});
|
||||
let _ = ws.borrow().send_with_str(&done.to_string());
|
||||
}
|
||||
410
network-poc/node/src/qwen_coder.rs
Normal file
@@ -0,0 +1,410 @@
|
||||
use candle_core::{Device, Tensor, DType};
|
||||
use candle_core::quantized::gguf_file;
|
||||
use candle_nn::VarBuilder;
|
||||
use candle_transformers::models::qwen2::{Config as QwenConfig, ModelForCausalLM as QwenModel};
|
||||
use candle_transformers::models::quantized_qwen2::ModelWeights as QwenQuantizedModel;
|
||||
use wasm_bindgen::JsCast;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use web_sys::WebSocket;
|
||||
|
||||
use crate::storage;
|
||||
|
||||
macro_rules! console_log {
|
||||
($($t:tt)*) => (web_sys::console::log_1(&format_args!($($t)*).to_string().into()))
|
||||
}
|
||||
|
||||
// 0.5B — nopea, sopii kaikille laitteille
|
||||
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";
|
||||
|
||||
// 1.5B GGUF Q4_K_M — kvantisoidtu, mahtuu selaimeen (~1 GB)
|
||||
const MODEL_GGUF_URL: &str = "https://huggingface.co/Qwen/Qwen2.5-Coder-1.5B-Instruct-GGUF/resolve/main/qwen2.5-coder-1.5b-instruct-q4_k_m.gguf";
|
||||
const TOKENIZER_GGUF_URL: &str = "https://huggingface.co/Qwen/Qwen2.5-Coder-1.5B-Instruct/resolve/main/tokenizer.json";
|
||||
|
||||
enum CoderModel {
|
||||
Full(QwenModel),
|
||||
Quantized(QwenQuantizedModel),
|
||||
}
|
||||
|
||||
impl CoderModel {
|
||||
fn forward(&mut self, x: &Tensor, pos: usize) -> candle_core::Result<Tensor> {
|
||||
match self {
|
||||
CoderModel::Full(m) => m.forward(x, pos),
|
||||
CoderModel::Quantized(m) => m.forward(x, pos),
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_kv_cache(&mut self) {
|
||||
match self {
|
||||
CoderModel::Full(m) => m.clear_kv_cache(),
|
||||
CoderModel::Quantized(_) => {
|
||||
// Quantized model nollaa KV-cachen automaattisesti kun forward kutsutaan pos=0:lla
|
||||
// (ks. quantized_qwen2.rs rivi 118: if index_pos == 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CachedModel {
|
||||
model: CoderModel,
|
||||
tokenizer: tokenizers::Tokenizer,
|
||||
is_3b: bool,
|
||||
}
|
||||
|
||||
/// Tunnetut kielitunnisteet joita malli voi tuottaa prefill-backtickien jälkeen.
|
||||
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 {
|
||||
let mut result = text.trim().to_string();
|
||||
|
||||
// 1. Poistetaan kielitunniste ensimmäiseltä riviltä — VAIN jos se on tunnettu kieli
|
||||
if let Some(first_newline) = result.find('\n') {
|
||||
let first_line = result[..first_newline].trim().to_lowercase();
|
||||
if LANG_TAGS.contains(&first_line.as_str()) {
|
||||
result = result[first_newline + 1..].to_string();
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Poistetaan sulkeva ``` VAIN jos se on omalla rivillään lopussa
|
||||
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"] {
|
||||
if lower.starts_with(prefix) {
|
||||
if let Some(newline) = result.find('\n') {
|
||||
result = result[newline + 1..].to_string();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Poistetaan selityskommentit alusta: "# This is a simple program..."
|
||||
let mut lines: Vec<&str> = result.trim().lines().collect();
|
||||
while !lines.is_empty() {
|
||||
let first = lines[0].trim();
|
||||
let is_preamble = first.starts_with("# ")
|
||||
&& !first.starts_with("#!")
|
||||
&& (first.to_lowercase().contains("this is")
|
||||
|| first.to_lowercase().contains("simple")
|
||||
|| first.to_lowercase().contains("program that")
|
||||
|| first.to_lowercase().contains("here is")
|
||||
|| first.to_lowercase().contains("the following")
|
||||
|| first.to_lowercase().contains("below"));
|
||||
if is_preamble { lines.remove(0); } else { break; }
|
||||
}
|
||||
|
||||
lines.join("\n").trim().to_string()
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static RAM_CACHE: RefCell<std::collections::HashMap<String, Rc<Vec<u8>>>> = RefCell::new(std::collections::HashMap::new());
|
||||
static MODEL_CACHE: RefCell<Option<CachedModel>> = RefCell::new(None);
|
||||
}
|
||||
|
||||
async fn ensure_cached(key: &str, url: &str, ws: &Rc<RefCell<WebSocket>>) -> Result<Rc<Vec<u8>>, String> {
|
||||
// 1. Tarkistetaan RAM välimuisti (estää OOM ja levy-I/O pullonkaulat)
|
||||
let ram_hit = RAM_CACHE.with(|cache| {
|
||||
cache.borrow().get(key).cloned()
|
||||
});
|
||||
if let Some(bytes) = ram_hit {
|
||||
console_log!("[Coder] {} löytyi nopeasta RAM-välimuistista!", key);
|
||||
return Ok(bytes);
|
||||
}
|
||||
|
||||
// 2. Tarkistetaan IndexedDB (jos selain on suljettu aikaisemmin)
|
||||
if let Ok(Some(bytes)) = storage::load_from_idb(key).await {
|
||||
console_log!("[Coder] {} löytyi IndexedDB-välimuistista ({} MB)", key, bytes.len() / 1024 / 1024);
|
||||
let rc_bytes = Rc::new(bytes);
|
||||
RAM_CACHE.with(|cache| cache.borrow_mut().insert(key.to_string(), rc_bytes.clone()));
|
||||
return Ok(rc_bytes);
|
||||
}
|
||||
|
||||
console_log!("[Coder] Ladataan {}...", key);
|
||||
|
||||
let resp = crate::worker_fetch(url).await?;
|
||||
if !resp.ok() { return Err(format!("HTTP {}", resp.status())); }
|
||||
|
||||
let total_size: usize = resp.headers()
|
||||
.get("content-length").ok().flatten()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
let body = resp.body().ok_or("Ei bodyä")?;
|
||||
let reader: web_sys::ReadableStreamDefaultReader = body.get_reader().dyn_into().map_err(|_| "Ei reader".to_string())?;
|
||||
|
||||
let mut data: Vec<u8> = Vec::with_capacity(total_size);
|
||||
let mut last_pct: u32 = 0;
|
||||
|
||||
loop {
|
||||
let chunk = wasm_bindgen_futures::JsFuture::from(reader.read())
|
||||
.await.map_err(|e| format!("Read: {:?}", e))?;
|
||||
let done = js_sys::Reflect::get(&chunk, &"done".into()).ok().and_then(|v| v.as_bool()).unwrap_or(true);
|
||||
if done { break; }
|
||||
let value = js_sys::Reflect::get(&chunk, &"value".into()).map_err(|_| "value puuttuu".to_string())?;
|
||||
let array = js_sys::Uint8Array::new(&value);
|
||||
let mut buf = vec![0u8; array.length() as usize];
|
||||
array.copy_to(&mut buf);
|
||||
data.extend_from_slice(&buf);
|
||||
|
||||
if total_size > 0 {
|
||||
let pct = ((data.len() as f64 / total_size as f64) * 100.0) as u32;
|
||||
if pct >= last_pct + 5 || pct == 100 {
|
||||
last_pct = pct;
|
||||
console_log!("[Coder] {} lataus: {}%", key, pct);
|
||||
let msg = serde_json::json!({ "type": "download_progress", "file": key, "pct": pct, "loaded_mb": data.len()/1024/1024, "total_mb": total_size/1024/1024 });
|
||||
let _ = ws.borrow().send_with_str(&msg.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console_log!("[Coder] Tallennetaan {} ({} MB) IndexedDB:hen...", key, data.len() / 1024 / 1024);
|
||||
let _ = storage::save_to_idb(key, &data).await;
|
||||
console_log!("[Coder] {} tallennettu!", key);
|
||||
|
||||
let rc_data = Rc::new(data);
|
||||
RAM_CACHE.with(|cache| cache.borrow_mut().insert(key.to_string(), rc_data.clone()));
|
||||
|
||||
Ok(rc_data)
|
||||
}
|
||||
|
||||
/// Lataa tai palauttaa välimuistista valmiin mallin + tokenizerin
|
||||
async fn get_or_build_model(use_3b: bool, ws: &Rc<RefCell<WebSocket>>) -> Result<(), String> {
|
||||
// Tarkistetaan onko oikea malli jo muistissa
|
||||
let cache_hit = MODEL_CACHE.with(|c| {
|
||||
c.borrow().as_ref().map(|m| m.is_3b == use_3b).unwrap_or(false)
|
||||
});
|
||||
if cache_hit {
|
||||
// Logitetaan kaikki välivaiheet valmiiksi, jotta pipeline-UI päivittyy
|
||||
console_log!("[Coder] tokenizer löytyi (cache)");
|
||||
console_log!("[Coder] model löytyi (cache)");
|
||||
console_log!("[Coder] Malli ladattu (välimuistista)");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let device = Device::Cpu;
|
||||
let dtype = DType::F32;
|
||||
|
||||
// Tokenizer
|
||||
let tok_url = if use_3b { TOKENIZER_GGUF_URL } else { TOKENIZER_05B_URL };
|
||||
let tok_key = if use_3b { "coder15b-tokenizer.json" } else { "coder05b-tokenizer.json" };
|
||||
let tok_bytes = ensure_cached(tok_key, tok_url, ws).await?;
|
||||
let tokenizer = tokenizers::Tokenizer::from_bytes(&tok_bytes[..])
|
||||
.map_err(|e| format!("Tokenizer: {}", e))?;
|
||||
|
||||
// Painot
|
||||
let model = if use_3b {
|
||||
// GGUF Q4_K_M — kvantisoidtu 3B-malli (~1.9 GB)
|
||||
let gguf_bytes = ensure_cached("coder15b-q4km.gguf", MODEL_GGUF_URL, ws).await?;
|
||||
console_log!("[Coder] Rakennetaan kvantisoidun 1.5B-mallia (Q4_K_M)...");
|
||||
let mut cursor = std::io::Cursor::new(&gguf_bytes[..]);
|
||||
let content = gguf_file::Content::read(&mut cursor)
|
||||
.map_err(|e| format!("GGUF parse: {}", e))?;
|
||||
let qmodel = QwenQuantizedModel::from_gguf(content, &mut cursor, &device)
|
||||
.map_err(|e| format!("GGUF model: {}", e))?;
|
||||
CoderModel::Quantized(qmodel)
|
||||
} else {
|
||||
let model_bytes = ensure_cached("coder05b-model.safetensors", MODEL_05B_URL, ws).await?;
|
||||
console_log!("[Coder] Rakennetaan 0.5B-mallia...");
|
||||
let tensors = candle_core::safetensors::load_buffer(&model_bytes[..], &device)
|
||||
.map_err(|e| format!("Safetensors: {}", e))?;
|
||||
let config = QwenConfig {
|
||||
vocab_size: 151936, hidden_size: 896, intermediate_size: 4864,
|
||||
num_hidden_layers: 24, num_attention_heads: 14, num_key_value_heads: 2,
|
||||
max_position_embeddings: 32768, sliding_window: 32768, max_window_layers: 21,
|
||||
tie_word_embeddings: true, rope_theta: 1000000.0, rms_norm_eps: 1e-6,
|
||||
use_sliding_window: false, hidden_act: candle_nn::Activation::Silu,
|
||||
};
|
||||
let vb = VarBuilder::from_tensors(tensors, dtype, &device);
|
||||
let qwen = QwenModel::new(&config, vb).map_err(|e| format!("Malli: {}", e))?;
|
||||
CoderModel::Full(qwen)
|
||||
};
|
||||
console_log!("[Coder] Malli ladattu ja välimuistitettu");
|
||||
|
||||
MODEL_CACHE.with(|c| {
|
||||
*c.borrow_mut() = Some(CachedModel { model, tokenizer, is_3b: use_3b });
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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>) {
|
||||
console_log!("[Coder] run_coder_inference alkaa! prompt={}", &prompt[..prompt.len().min(50)]);
|
||||
let size_label = if use_3b { "3B" } else { "0.5B" };
|
||||
|
||||
let start_load = crate::perf_now();
|
||||
|
||||
console_log!("[Coder] Kutsutaan get_or_build_model...");
|
||||
if let Err(e) = get_or_build_model(use_3b, &ws).await {
|
||||
console_log!("[Coder] Mallin lataus epäonnistui: {}", e);
|
||||
return;
|
||||
}
|
||||
console_log!("[Coder] Malli valmis, aloitetaan inferenssi");
|
||||
|
||||
let load_time = crate::perf_now() - start_load;
|
||||
if load_time > 100.0 {
|
||||
console_log!("[Coder] Malli ladattu ({:.0}ms). Generoidaan...", load_time);
|
||||
}
|
||||
|
||||
// Parsitaan JSON-prompti tai käytetään teksti sellaisenaan
|
||||
let default_system = "You are a coding assistant. Respond with ONLY code. No explanations, no markdown, no comments unless asked.";
|
||||
let (actual_prompt, system_msg, max_new_tokens) = if prompt.starts_with('{') {
|
||||
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&prompt) {
|
||||
let p = json.get("prompt").and_then(|v| v.as_str()).unwrap_or(&prompt).to_string();
|
||||
let s = json.get("system").and_then(|v| v.as_str()).unwrap_or(default_system).to_string();
|
||||
let m = json.get("max_tokens").and_then(|v| v.as_u64()).unwrap_or(512) as usize;
|
||||
(p, s, m)
|
||||
} else {
|
||||
(prompt.clone(), default_system.to_string(), 512)
|
||||
}
|
||||
} else {
|
||||
(prompt.clone(), default_system.to_string(), 512)
|
||||
};
|
||||
|
||||
// Prefill: aloitetaan vastaus ```-koodiblokkilla, jolloin malli jatkaa suoraan koodilla
|
||||
// eikä tuota "Sure! Here is..." -johdantoa. strip_markdown_wrapper poistaa ``` jälkikäteen.
|
||||
let formatted = format!("<|im_start|>system\n{}<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n```\n", system_msg, actual_prompt);
|
||||
|
||||
// Inferenssi: käytetään välimuistissa olevaa mallia
|
||||
let (generated_text, tokens_generated, gen_time) = MODEL_CACHE.with(|cache| {
|
||||
let mut cache = cache.borrow_mut();
|
||||
let cached = cache.as_mut().expect("Malli pitää olla ladattu");
|
||||
|
||||
let encoding = cached.tokenizer.encode(formatted.as_str(), true)
|
||||
.map_err(|e| format!("Encode: {}", e)).unwrap();
|
||||
let input_ids: Vec<u32> = encoding.get_ids().to_vec();
|
||||
let input_len = input_ids.len();
|
||||
console_log!("[Coder] Syöte: {} tokenia", input_len);
|
||||
|
||||
let device = Device::Cpu;
|
||||
let start_gen = crate::perf_now();
|
||||
let eos_token = 151645u32;
|
||||
let temperature: f32 = 0.7;
|
||||
let top_k: usize = 40;
|
||||
let repetition_penalty: f32 = 1.15;
|
||||
|
||||
// Nollataan KV-cache edellisestä promptista
|
||||
cached.model.clear_kv_cache();
|
||||
|
||||
let mut generated_text = String::new();
|
||||
let mut tokens_generated: usize = 0;
|
||||
let mut all_generated: Vec<u32> = Vec::new();
|
||||
|
||||
// Prefill
|
||||
let input = Tensor::new(input_ids.as_slice(), &device).and_then(|t| t.unsqueeze(0)).unwrap();
|
||||
let logits = cached.model.forward(&input, 0).unwrap();
|
||||
let logits = logits.squeeze(0).unwrap();
|
||||
let logits = if logits.dims().len() == 2 {
|
||||
logits.get(logits.dim(0).unwrap() - 1).unwrap()
|
||||
} else { logits };
|
||||
|
||||
let mut next_token = crate::sampling::sample_top_k_with_penalty(&logits, top_k, temperature, &all_generated, repetition_penalty);
|
||||
|
||||
if next_token != eos_token {
|
||||
if let Ok(text) = cached.tokenizer.decode(&[next_token], true) {
|
||||
generated_text.push_str(&text);
|
||||
let mut chunk = serde_json::json!({ "type": "llm_chunk", "token": text, "prompt": prompt, "model": "Qwen2.5-Coder" });
|
||||
if let Some(ref tid) = task_id {
|
||||
if let Some(obj) = chunk.as_object_mut() {
|
||||
obj.insert("task_id".to_string(), serde_json::json!(tid));
|
||||
}
|
||||
}
|
||||
let _ = ws.borrow().send_with_str(&chunk.to_string());
|
||||
}
|
||||
all_generated.push(next_token);
|
||||
tokens_generated += 1;
|
||||
}
|
||||
|
||||
// Autoregressive
|
||||
let mut pos = input_len;
|
||||
for _ in 1..max_new_tokens {
|
||||
if next_token == eos_token { break; }
|
||||
|
||||
let input = Tensor::new(&[next_token], &device).and_then(|t| t.unsqueeze(0)).unwrap();
|
||||
let logits = match cached.model.forward(&input, pos) {
|
||||
Ok(l) => l,
|
||||
Err(e) => { console_log!("[Coder] Forward pos {}: {}", pos, e); break; }
|
||||
};
|
||||
|
||||
let logits = logits.squeeze(0).unwrap();
|
||||
let logits = if logits.dims().len() == 2 {
|
||||
logits.get(logits.dim(0).unwrap() - 1).unwrap()
|
||||
} else { logits };
|
||||
next_token = crate::sampling::sample_top_k_with_penalty(&logits, top_k, temperature, &all_generated, repetition_penalty);
|
||||
pos += 1;
|
||||
|
||||
if next_token == eos_token { break; }
|
||||
|
||||
if let Ok(text) = cached.tokenizer.decode(&[next_token], true) {
|
||||
generated_text.push_str(&text);
|
||||
|
||||
// Stop-sekvenssit: katkaistaan kun malli alkaa selittää
|
||||
let lower = generated_text.to_lowercase();
|
||||
if lower.contains("\n###") || lower.contains("\nexplanation") || lower.contains("\nnote:") || lower.contains("\noutput:") || lower.contains("\n```\n\n") || lower.contains("\n// example") || lower.contains("\n# example") {
|
||||
for stop in &["\n###", "\nExplanation", "\nNote:", "\nOutput:", "\n```\n\n", "\n// Example", "\n// example", "\n# Example", "\n# example"] {
|
||||
if let Some(pos) = generated_text.find(stop) {
|
||||
generated_text.truncate(pos);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
let mut chunk = serde_json::json!({ "type": "llm_chunk", "token": text, "prompt": prompt, "model": "Qwen2.5-Coder" });
|
||||
if let Some(ref tid) = task_id {
|
||||
if let Some(obj) = chunk.as_object_mut() {
|
||||
obj.insert("task_id".to_string(), serde_json::json!(tid));
|
||||
}
|
||||
}
|
||||
let _ = ws.borrow().send_with_str(&chunk.to_string());
|
||||
}
|
||||
all_generated.push(next_token);
|
||||
tokens_generated += 1;
|
||||
}
|
||||
|
||||
let gen_time = crate::perf_now() - start_gen;
|
||||
|
||||
// Siivotaan vastaus: poista markdown-koodiblokit ja johdantotekstit
|
||||
let cleaned = strip_markdown_wrapper(&generated_text);
|
||||
|
||||
(cleaned, tokens_generated, gen_time)
|
||||
});
|
||||
|
||||
let tokens_per_sec = if gen_time > 0.0 { (tokens_generated as f64 / gen_time) * 1000.0 } else { 0.0 };
|
||||
console_log!("[Coder] {} tokenia | {:.0}ms | {:.1} tok/s", tokens_generated, gen_time, tokens_per_sec);
|
||||
|
||||
let mut done = serde_json::json!({
|
||||
"type": "llm_done",
|
||||
"prompt": prompt,
|
||||
"model": format!("Qwen2.5-Coder-{}-Instruct", size_label),
|
||||
"response": generated_text,
|
||||
"tokens_generated": tokens_generated,
|
||||
"duration_ms": (gen_time * 100.0).round() / 100.0,
|
||||
"tokens_per_sec": (tokens_per_sec * 10.0).round() / 10.0,
|
||||
"load_time_ms": (load_time * 100.0).round() / 100.0,
|
||||
});
|
||||
if let Some(tid) = task_id {
|
||||
if let Some(obj) = done.as_object_mut() {
|
||||
obj.insert("task_id".to_string(), serde_json::json!(tid));
|
||||
}
|
||||
}
|
||||
let _ = ws.borrow().send_with_str(&done.to_string());
|
||||
}
|
||||
113
network-poc/node/src/sampling.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
use candle_core::Tensor;
|
||||
use std::cell::Cell;
|
||||
|
||||
thread_local! {
|
||||
static RNG_STATE: Cell<u64> = Cell::new(0);
|
||||
}
|
||||
|
||||
fn next_rand() -> f32 {
|
||||
RNG_STATE.with(|state| {
|
||||
let mut s = state.get();
|
||||
if s == 0 {
|
||||
s = (js_sys::Date::now() * 1000.0) as u64 | 1;
|
||||
}
|
||||
s ^= s << 13;
|
||||
s ^= s >> 7;
|
||||
s ^= s << 17;
|
||||
state.set(s);
|
||||
(s % 10000) as f32 / 10000.0
|
||||
})
|
||||
}
|
||||
|
||||
/// Top-k sampling with temperature and repetition penalty.
|
||||
/// `generated_tokens` sisältää aiemmin generoidut token-id:t toiston estämiseksi.
|
||||
pub fn sample_top_k_with_penalty(logits: &Tensor, k: usize, temperature: f32, generated_tokens: &[u32], repetition_penalty: f32) -> u32 {
|
||||
let mut logits_vec: Vec<f32> = logits.to_vec1::<f32>().unwrap_or_default();
|
||||
if logits_vec.is_empty() { return 0; }
|
||||
|
||||
// Repetition penalty
|
||||
if repetition_penalty != 1.0 {
|
||||
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;
|
||||
} else {
|
||||
*logit *= repetition_penalty;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Temperature scaling
|
||||
if temperature > 0.0 && temperature != 1.0 {
|
||||
for logit in logits_vec.iter_mut() {
|
||||
*logit /= temperature;
|
||||
}
|
||||
}
|
||||
|
||||
// Top-k
|
||||
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 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();
|
||||
|
||||
let rand_val = next_rand();
|
||||
|
||||
let mut cumulative = 0.0;
|
||||
for (i, p) in probs.iter().enumerate() {
|
||||
cumulative += p;
|
||||
if rand_val < cumulative {
|
||||
return indexed[i].0 as u32;
|
||||
}
|
||||
}
|
||||
|
||||
indexed[0].0 as u32
|
||||
}
|
||||
|
||||
/// Alkuperäinen API yhteensopivuudeksi SmolLM/Qwen-moduulien kanssa
|
||||
pub fn sample_top_k(logits: &Tensor, k: usize, eos_penalty: f32) -> u32 {
|
||||
let mut logits_vec: Vec<f32> = logits.to_vec1::<f32>().unwrap_or_default();
|
||||
if logits_vec.is_empty() { return 0; }
|
||||
|
||||
// EOS-penaltti
|
||||
for &eos_id in &[2u32, 151645] {
|
||||
if (eos_id as usize) < logits_vec.len() {
|
||||
logits_vec[eos_id as usize] -= eos_penalty;
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
return indexed[0].0 as u32;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
let rand_val = next_rand();
|
||||
|
||||
let mut cumulative = 0.0;
|
||||
for (i, p) in probs.iter().enumerate() {
|
||||
cumulative += p;
|
||||
if rand_val < cumulative {
|
||||
return indexed[i].0 as u32;
|
||||
}
|
||||
}
|
||||
|
||||
indexed[0].0 as u32
|
||||
}
|
||||
BIN
network-poc/nodes.db
Normal file
@@ -1,635 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fi">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Kipinä Agent Dashboard</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg-color: #0d1117;
|
||||
--panel-bg: #161b22;
|
||||
--text-color: #c9d1d9;
|
||||
--accent-color: #58a6ff;
|
||||
--success-color: #3fb950;
|
||||
--border-color: #30363d;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: var(--panel-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 30px;
|
||||
width: 100%;
|
||||
max-width: 1400px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.device-info {
|
||||
background-color: #0d1117;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 20px;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 14px;
|
||||
color: #8b949e;
|
||||
text-align: left;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.device-info span { color: var(--text-color); }
|
||||
|
||||
.dashboard-panel {
|
||||
background-color: #0d1117;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-box {
|
||||
text-align: center;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.stat-box h3 {
|
||||
margin: 0;
|
||||
color: var(--accent-color);
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.stat-box p {
|
||||
margin: 5px 0 0 0;
|
||||
font-size: 14px;
|
||||
color: #8b949e;
|
||||
}
|
||||
|
||||
.slider-container {
|
||||
margin: 20px 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
input[type=range] {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
accent-color: var(--accent-color);
|
||||
}
|
||||
|
||||
h1 { margin-bottom: 5px; }
|
||||
h1 span { color: var(--accent-color); }
|
||||
.sub { color: #8b949e; margin-bottom: 25px; }
|
||||
|
||||
.status-box {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
background-color: #010409;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
height: 120px;
|
||||
overflow-y: auto;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.status-box p {
|
||||
margin: 0 0 5px 0;
|
||||
color: var(--success-color);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background-color: #238636;
|
||||
color: #ffffff;
|
||||
border: 1px solid rgba(240, 246, 252, 0.1);
|
||||
border-radius: 6px;
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.btn:hover { background-color: #2ea043; }
|
||||
.hidden { display: none; }
|
||||
|
||||
.compat-banner {
|
||||
border-radius: 6px;
|
||||
padding: 14px 18px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
display: none;
|
||||
}
|
||||
.compat-banner.gpu {
|
||||
background: #23392020;
|
||||
border: 1px solid #3fb95040;
|
||||
color: var(--success-color);
|
||||
}
|
||||
.compat-banner.cpu {
|
||||
background: #d2992215;
|
||||
border: 1px solid #d2992240;
|
||||
color: #d29922;
|
||||
}
|
||||
.compat-banner code {
|
||||
background: #0d1117;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
color: var(--text-color);
|
||||
}
|
||||
.compat-banner summary {
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.compat-banner details[open] summary {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.chat-box {
|
||||
background-color: var(--panel-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
height: 500px;
|
||||
overflow-y: auto;
|
||||
text-align: left;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.chat-msg {
|
||||
background-color: #0d1117;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid var(--accent-color);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.chat-prompt {
|
||||
color: #8b949e;
|
||||
font-size: 13px;
|
||||
margin-bottom: 5px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.token-detail {
|
||||
background: #010409;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
padding: 10px 12px;
|
||||
margin-top: 8px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.8;
|
||||
display: none;
|
||||
}
|
||||
.token-detail.visible { display: block; }
|
||||
.token-detail .tok {
|
||||
background: #1c2333;
|
||||
border: 1px solid #30363d;
|
||||
border-radius: 3px;
|
||||
padding: 2px 5px;
|
||||
margin: 2px;
|
||||
display: inline-block;
|
||||
color: var(--text-color);
|
||||
}
|
||||
.token-detail .tok-en { border-color: #58a6ff44; }
|
||||
.token-detail .tok-fi { border-color: #d2992244; }
|
||||
.toggle-tokens {
|
||||
background: none;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
color: #8b949e;
|
||||
font-size: 12px;
|
||||
padding: 3px 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.toggle-tokens:hover { color: var(--text-color); border-color: #8b949e; }
|
||||
|
||||
.metric-card {
|
||||
background: var(--panel-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
.metric-val {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: var(--accent-color);
|
||||
}
|
||||
.metric-label {
|
||||
font-size: 11px;
|
||||
color: #8b949e;
|
||||
margin-top: 2px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Kipinä <span>Agent Dashboard</span></h1>
|
||||
<p class="sub">Hajautettu WebGPU Laskentaverkko · <span id="hub-version" style="color:#58a6ff">-</span></p>
|
||||
|
||||
<!-- Global Cluster Statistics (UI) -->
|
||||
<div class="dashboard-panel">
|
||||
<div class="stat-box" style="border-right: 1px solid #30363d;">
|
||||
<h3 id="stat-nodes">0</h3>
|
||||
<p>Aktiivisia Nodeja</p>
|
||||
</div>
|
||||
<div class="stat-box" style="border-right: 1px solid #30363d;">
|
||||
<h3 id="stat-tasks">0</h3>
|
||||
<p>Verkossa Suoritettua Tehtävää (Globaali)</p>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<h3 id="stat-vram">0 GB</h3>
|
||||
<p>Verkon yhteis-VRAM</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="device-info" class="device-info"></div>
|
||||
<div id="compat-banner" class="compat-banner"></div>
|
||||
|
||||
<div id="initial-state">
|
||||
<button id="start-btn" class="btn">Liity laskentaverkkoon</button>
|
||||
</div>
|
||||
|
||||
<div id="active-state" class="hidden">
|
||||
<!-- Resurssipaneeli -->
|
||||
<div style="background:#0d1117;border:1px solid var(--border-color);border-radius:6px;padding:16px;margin-bottom:16px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
|
||||
<span style="font-weight:600;font-size:15px">Resurssien hallinta</span>
|
||||
<span id="node-status" style="font-size:12px;color:var(--success-color)">Aktiivinen</span>
|
||||
</div>
|
||||
|
||||
<!-- Kuormitussäädin -->
|
||||
<div style="margin-bottom:14px">
|
||||
<div style="display:flex;justify-content:space-between;font-size:13px;margin-bottom:4px">
|
||||
<span>Laskentatehon rajoitin</span>
|
||||
<strong id="load-display" style="color:var(--accent-color)">50%</strong>
|
||||
</div>
|
||||
<input type="range" id="gpu-load" min="0" max="100" value="50" style="width:100%;accent-color:var(--accent-color)">
|
||||
<div style="display:flex;justify-content:space-between;font-size:11px;color:#8b949e;margin-top:2px">
|
||||
<span>Pysäytetty</span><span>Säästö</span><span>Tasapaino</span><span>Suorituskyky</span><span>Maksimi</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reaaliaikaiset metriikat -->
|
||||
<div id="metrics-grid" style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-top:12px">
|
||||
<div class="metric-card">
|
||||
<div class="metric-val" id="m-tasks">0</div>
|
||||
<div class="metric-label">Tehtäviä</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-val" id="m-avg-time">-</div>
|
||||
<div class="metric-label">Ka. aika</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-val" id="m-tokens">0</div>
|
||||
<div class="metric-label">Tokeneita</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-val" id="m-uptime">0s</div>
|
||||
<div class="metric-label">Käynnissä</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="chat-box" class="chat-box hidden">
|
||||
<div style="color: #8b949e; text-align: center; margin-top: 80px;">Odotetaan Generointitehtäviä Hubilta...</div>
|
||||
</div>
|
||||
|
||||
<div id="log-box" class="status-box">
|
||||
<p>> Odotetaan uusia tehtäviä Hubulta...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import init, { start_agent_node, set_gpu_load } from './pkg/node.js';
|
||||
|
||||
const btn = document.getElementById('start-btn');
|
||||
const logBox = document.getElementById('log-box');
|
||||
const loadSlider = document.getElementById('gpu-load');
|
||||
const loadDisplay = document.getElementById('load-display');
|
||||
const statNodes = document.getElementById('stat-nodes');
|
||||
const statVram = document.getElementById('stat-vram');
|
||||
const statTasks = document.getElementById('stat-tasks');
|
||||
const chatBox = document.getElementById('chat-box');
|
||||
|
||||
let currentChatMsg = null;
|
||||
|
||||
// Reaaliaikaiset metriikat
|
||||
const metrics = {
|
||||
tasks: 0,
|
||||
totalTokens: 0,
|
||||
totalTimeMs: 0,
|
||||
startTime: null,
|
||||
};
|
||||
|
||||
function updateMetrics() {
|
||||
document.getElementById('m-tasks').textContent = metrics.tasks;
|
||||
document.getElementById('m-tokens').textContent = metrics.totalTokens.toLocaleString('fi-FI');
|
||||
document.getElementById('m-avg-time').textContent = metrics.tasks > 0
|
||||
? (metrics.totalTimeMs / metrics.tasks).toFixed(1) + 'ms'
|
||||
: '-';
|
||||
if (metrics.startTime) {
|
||||
const sec = Math.floor((Date.now() - metrics.startTime) / 1000);
|
||||
if (sec < 60) document.getElementById('m-uptime').textContent = sec + 's';
|
||||
else if (sec < 3600) document.getElementById('m-uptime').textContent = Math.floor(sec/60) + 'min';
|
||||
else document.getElementById('m-uptime').textContent = Math.floor(sec/3600) + 'h ' + (Math.floor(sec/60)%60) + 'min';
|
||||
}
|
||||
}
|
||||
setInterval(updateMetrics, 1000);
|
||||
|
||||
// Ylikirjoitetaan console.log uppoamaan lokilaatikkoon
|
||||
const originalLog = console.log;
|
||||
console.log = function(...args) {
|
||||
originalLog.apply(console, args);
|
||||
// Älä tulosta teknisiä WGPU warningeja suoraan AI:n näytölle jos niitä on
|
||||
let msg = args.join(' ');
|
||||
if (msg.includes("wgpu") || msg.includes("vastaanotettu")) return; // Siistitään spämmäävät lokit näkymästä, koska niitä tulee nyt sata sekunnissa
|
||||
|
||||
const p = document.createElement('p');
|
||||
p.textContent = '> ' + msg;
|
||||
logBox.appendChild(p);
|
||||
|
||||
// Ehkäistään selaimen jumittuminen sadoista tuhansista lokiriveistä pitkässä GPU-ajossa
|
||||
if (logBox.children.length > 30) {
|
||||
logBox.removeChild(logBox.firstChild);
|
||||
}
|
||||
logBox.scrollTop = logBox.scrollHeight;
|
||||
};
|
||||
|
||||
// UI Slider Listener -> Lähettää arvon suoraan WebAssemblyn ytimeen!
|
||||
loadSlider.addEventListener('input', (e) => {
|
||||
const val = parseInt(e.target.value);
|
||||
loadDisplay.textContent = val + '%';
|
||||
if (window.wasm_active) {
|
||||
set_gpu_load(val);
|
||||
}
|
||||
// Tilapäivitys
|
||||
const statusEl = document.getElementById('node-status');
|
||||
if (val === 0) {
|
||||
statusEl.textContent = 'Pysäytetty';
|
||||
statusEl.style.color = '#f85149';
|
||||
} else if (val <= 25) {
|
||||
statusEl.textContent = 'Säästötila';
|
||||
statusEl.style.color = '#d29922';
|
||||
} else {
|
||||
statusEl.textContent = 'Aktiivinen';
|
||||
statusEl.style.color = 'var(--success-color)';
|
||||
}
|
||||
});
|
||||
|
||||
// Kytkemme sivuston UI-puolen (JS) omaan passiiviseen WebSocket-kuuntelijaan.
|
||||
const uiSocket = new WebSocket(`${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws`);
|
||||
uiSocket.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.type === "stats") {
|
||||
statNodes.textContent = data.nodes;
|
||||
statVram.textContent = data.vram_gb + " GB";
|
||||
if (data.tasks !== undefined) {
|
||||
statTasks.textContent = data.tasks;
|
||||
}
|
||||
if (data.version) {
|
||||
document.getElementById('hub-version').textContent = 'v' + data.version;
|
||||
}
|
||||
} else if (data.type === "node_joined") {
|
||||
chatBox.classList.remove('hidden');
|
||||
const msgDiv = document.createElement('div');
|
||||
msgDiv.className = 'chat-msg';
|
||||
msgDiv.style.borderLeftColor = 'var(--success-color)';
|
||||
msgDiv.innerHTML = `<span style="color:var(--success-color)">[Järjestelmä] Uusi solmu (ID: ${data.node_id}) liittyi verkon työjohdon piiriin!</span>`;
|
||||
chatBox.appendChild(msgDiv);
|
||||
if (chatBox.children.length > 5) chatBox.removeChild(chatBox.firstChild);
|
||||
chatBox.scrollTop = chatBox.scrollHeight;
|
||||
} else if (data.type === "pair_task") {
|
||||
chatBox.classList.remove('hidden');
|
||||
if (chatBox.children.length === 1 && chatBox.children[0].textContent.includes('Odotetaan')) {
|
||||
chatBox.innerHTML = '';
|
||||
}
|
||||
const msgDiv = document.createElement('div');
|
||||
msgDiv.className = 'chat-msg';
|
||||
msgDiv.innerHTML = `<span class="chat-prompt">Tokenisoidaan...</span>
|
||||
<div style="font-size:12px;color:#8b949e">
|
||||
<div><strong style="color:#58a6ff">EN</strong> "${data.en}"</div>
|
||||
<div><strong style="color:#d29922">FI</strong> "${data.fi}"</div>
|
||||
</div>`;
|
||||
chatBox.appendChild(msgDiv);
|
||||
if (chatBox.children.length > 8) chatBox.removeChild(chatBox.firstChild);
|
||||
chatBox.scrollTop = chatBox.scrollHeight;
|
||||
} else if (data.type === "pair_done") {
|
||||
chatBox.classList.remove('hidden');
|
||||
const en = data.en || {};
|
||||
const fi = data.fi || {};
|
||||
const overhead = data.overhead_pct || 0;
|
||||
const nodeId = data.node_id || "?";
|
||||
const ms = data.duration_ms || 0;
|
||||
|
||||
// Päivitetään metriikat
|
||||
metrics.tasks++;
|
||||
metrics.totalTokens += (en.token_count || 0) + (fi.token_count || 0);
|
||||
metrics.totalTimeMs += ms;
|
||||
updateMetrics();
|
||||
|
||||
const enCpt = parseFloat((en.chars_per_token || 0).toFixed(2));
|
||||
const fiCpt = parseFloat((fi.chars_per_token || 0).toFixed(2));
|
||||
|
||||
// Värit tehokkuudelle
|
||||
const cptColor = (v) => v >= 4 ? "#3fb950" : v >= 3 ? "#d29922" : "#f85149";
|
||||
// Ylikustannuksen väri
|
||||
const ovColor = overhead > 20 ? "#f85149" : overhead > 0 ? "#d29922" : "#3fb950";
|
||||
|
||||
// Korvataan viimeisin "Tokenisoidaan..."-viesti, tai luodaan uusi
|
||||
const lastMsg = chatBox.lastElementChild;
|
||||
const msgDiv = (lastMsg && lastMsg.querySelector('.chat-prompt')?.textContent === 'Tokenisoidaan...')
|
||||
? lastMsg : document.createElement('div');
|
||||
msgDiv.className = 'chat-msg';
|
||||
|
||||
// Tokenilistat renderöitäväksi
|
||||
const renderTokens = (tokens, cls) => (tokens || []).map(t =>
|
||||
`<span class="tok ${cls}">${t.replace(/</g,'<')}</span>`
|
||||
).join('');
|
||||
const enTokHtml = renderTokens(en.tokens, 'tok-en');
|
||||
const fiTokHtml = renderTokens(fi.tokens, 'tok-fi');
|
||||
const detailId = 'tok-' + Date.now();
|
||||
|
||||
msgDiv.innerHTML = `
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
|
||||
<span style="color:var(--accent-color);font-weight:600;font-size:15px">Solmu #${nodeId}</span>
|
||||
<div style="display:flex;gap:8px;align-items:center">
|
||||
<button class="toggle-tokens" onclick="document.getElementById('${detailId}').classList.toggle('visible')">Tokenit</button>
|
||||
<span style="color:#8b949e;font-size:13px">${ms}ms</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-size:14px;display:grid;grid-template-columns:32px 1fr auto auto auto;gap:6px 10px;align-items:baseline">
|
||||
<strong style="color:#58a6ff">EN</strong>
|
||||
<span style="color:#79b8ff">"${en.text || ''}"</span>
|
||||
<span style="color:#8b949e">${en.char_count} m</span>
|
||||
<span style="color:var(--accent-color);font-weight:600">${en.token_count} tok</span>
|
||||
<span style="color:${cptColor(enCpt)};font-weight:600">${enCpt} m/t</span>
|
||||
|
||||
<strong style="color:#d29922">FI</strong>
|
||||
<span style="color:#e3b341">"${fi.text || ''}"</span>
|
||||
<span style="color:#8b949e">${fi.char_count} m</span>
|
||||
<span style="color:var(--accent-color);font-weight:600">${fi.token_count} tok</span>
|
||||
<span style="color:${cptColor(fiCpt)};font-weight:600">${fiCpt} m/t</span>
|
||||
</div>
|
||||
<div id="${detailId}" class="token-detail">
|
||||
<div style="margin-bottom:6px"><strong style="color:#58a6ff;font-size:12px">EN (${en.token_count})</strong> ${enTokHtml}</div>
|
||||
<div><strong style="color:#d29922;font-size:12px">FI (${fi.token_count})</strong> ${fiTokHtml}</div>
|
||||
</div>
|
||||
<div style="margin-top:10px;display:flex;justify-content:space-between;align-items:baseline;font-size:14px">
|
||||
<span style="color:#8b949e">(<span style="color:#d29922">${fi.token_count}</span> / <span style="color:#58a6ff">${en.token_count}</span> − 1) × 100 = <strong style="color:${ovColor}">${overhead > 0 ? '+' : ''}${overhead}%</strong></span>
|
||||
<span style="font-size:15px">FI ylikustannus: <strong style="color:${ovColor}">${overhead > 0 ? '+' : ''}${overhead}%</strong></span>
|
||||
</div>`;
|
||||
|
||||
if (!msgDiv.parentNode) chatBox.appendChild(msgDiv);
|
||||
if (chatBox.children.length > 8) chatBox.removeChild(chatBox.firstChild);
|
||||
chatBox.scrollTop = chatBox.scrollHeight;
|
||||
}
|
||||
} catch(e) {}
|
||||
};
|
||||
|
||||
btn.addEventListener('click', async () => {
|
||||
// Kerätään laitteistotiedot
|
||||
let hasWebGPU = false;
|
||||
const deviceInfo = {
|
||||
allocated_gb: 4,
|
||||
cpu_cores: navigator.hardwareConcurrency || 0,
|
||||
device_memory_gb: navigator.deviceMemory || 0,
|
||||
platform: navigator.platform || "",
|
||||
gpu: null
|
||||
};
|
||||
|
||||
if (navigator.gpu) {
|
||||
try {
|
||||
const adapter = await navigator.gpu.requestAdapter();
|
||||
if (adapter) {
|
||||
hasWebGPU = true;
|
||||
const info = adapter.info || {};
|
||||
const maxBuf = Number(adapter.limits.maxBufferSize || 0);
|
||||
// maxBufferSize antaa arvion VRAM:sta — tyypillisesti ~25% todellisesta
|
||||
const estimatedVramGb = maxBuf > 0 ? Math.round(maxBuf / 1024 / 1024 / 1024 * 4) : 0;
|
||||
deviceInfo.gpu = {
|
||||
vendor: info.vendor || "",
|
||||
architecture: info.architecture || "",
|
||||
device: info.device || "",
|
||||
description: info.description || "",
|
||||
max_buffer_size: maxBuf,
|
||||
max_compute_workgroups: adapter.limits.maxComputeWorkgroupsPerDimension || 0,
|
||||
estimated_vram_gb: estimatedVramGb
|
||||
};
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
const gpuStr = hasWebGPU ? (deviceInfo.gpu?.description || deviceInfo.gpu?.vendor || "WebGPU") : "ei GPU:ta";
|
||||
const backendStr = hasWebGPU ? "WebGPU" : "CPU (NdArray)";
|
||||
const vramStr = deviceInfo.gpu?.estimated_vram_gb ? `~${deviceInfo.gpu.estimated_vram_gb} GB` : "?";
|
||||
|
||||
// navigator.deviceMemory on rajoitettu max 8 GB:iin — merkitään arvio
|
||||
const ramNote = deviceInfo.device_memory_gb >= 8 ? "8+ GB (selaimen raja)" : `~${deviceInfo.device_memory_gb} GB`;
|
||||
|
||||
// Näytetään laitetiedot paneelissa
|
||||
const diPanel = document.getElementById('device-info');
|
||||
diPanel.style.display = 'block';
|
||||
diPanel.innerHTML = [
|
||||
`Backend: <span>${backendStr}</span>`,
|
||||
`GPU: <span>${gpuStr}</span>`,
|
||||
hasWebGPU ? `VRAM: <span>${vramStr}</span>` : null,
|
||||
`CPU: <span>${deviceInfo.cpu_cores} ydintä</span>`,
|
||||
`RAM: <span>${ramNote}</span>`,
|
||||
`Varaus: <span>${deviceInfo.allocated_gb} GB</span>`
|
||||
].filter(Boolean).join(' · ');
|
||||
|
||||
// Yhteensopivuusbanneri
|
||||
const banner = document.getElementById('compat-banner');
|
||||
banner.style.display = 'block';
|
||||
|
||||
if (hasWebGPU) {
|
||||
banner.className = 'compat-banner gpu';
|
||||
banner.innerHTML = `GPU-kiihdytys aktiivinen — ${gpuStr}`;
|
||||
} else {
|
||||
// Tunnistetaan selain ohjeen personointia varten
|
||||
const ua = navigator.userAgent;
|
||||
const isFirefox = ua.includes('Firefox');
|
||||
const isChrome = ua.includes('Chrome') && !ua.includes('Edg');
|
||||
const isBrave = ua.includes('Brave') || (navigator.brave && navigator.brave.isBrave);
|
||||
const isSafari = ua.includes('Safari') && !ua.includes('Chrome');
|
||||
const isLinux = ua.includes('Linux');
|
||||
|
||||
let browserTip = '';
|
||||
if (isFirefox) {
|
||||
browserTip = `
|
||||
<p><strong>Firefox</strong> ei tue WebGPU:ta oletuksena.</p>
|
||||
<p>Ota käyttöön: <code>about:config</code> → <code>dom.webgpu.enabled</code> = <code>true</code> → käynnistä uudelleen.</p>
|
||||
<p>Tai vaihda Chromeen/Braveen — niissä WebGPU toimii oletuksena.</p>`;
|
||||
} else if ((isChrome || isBrave) && isLinux) {
|
||||
const browser = isBrave ? 'brave-browser' : 'google-chrome';
|
||||
browserTip = `
|
||||
<p><strong>${isBrave ? 'Brave' : 'Chrome'} + Linux</strong>: GPU-ajuri ei ehkä tarjoa WebGPU:ta Wayland-ympäristössä.</p>
|
||||
<p>Kokeile käynnistää selain komentoriviltä:</p>
|
||||
<code>${browser} --enable-unsafe-webgpu --enable-features=Vulkan --ignore-gpu-blocklist --use-angle=vulkan --ozone-platform=x11</code>`;
|
||||
} else if (isSafari) {
|
||||
browserTip = `
|
||||
<p><strong>Safari</strong>: WebGPU on tuettu versiosta 26 alkaen (macOS Tahoe).</p>
|
||||
<p>Vanhemmissa versioissa: Develop → Feature Flags → WebGPU.</p>`;
|
||||
} else {
|
||||
browserTip = `
|
||||
<p>Selaimesi ei tue WebGPU:ta. Kokeile <strong>Chrome 113+</strong> tai <strong>Brave</strong>.</p>`;
|
||||
}
|
||||
|
||||
banner.className = 'compat-banner cpu';
|
||||
banner.innerHTML = `
|
||||
<details>
|
||||
<summary>CPU-laskenta (WebGPU ei käytettävissä) — klikkaa ohjeita</summary>
|
||||
${browserTip}
|
||||
<p style="margin-top:8px;color:#8b949e;font-size:12px">Laskenta toimii silti CPU:lla, mutta GPU-kiihdytys olisi nopeampi.</p>
|
||||
</details>`;
|
||||
}
|
||||
|
||||
document.getElementById('initial-state').classList.add('hidden');
|
||||
document.getElementById('active-state').classList.remove('hidden');
|
||||
btn.style.display = 'none';
|
||||
|
||||
try {
|
||||
console.log("Ladataan Burn Wasm -binääriä...");
|
||||
await init();
|
||||
window.wasm_active = true;
|
||||
metrics.startTime = Date.now();
|
||||
|
||||
// Varmistetaan, että Wasm saa nykyisen sliderin arvon heti kärkeen
|
||||
set_gpu_load(parseInt(loadSlider.value));
|
||||
|
||||
// WebAssembly yhdistää oikeaksi Agent Nodeksi
|
||||
const wsUrl = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws`;
|
||||
await start_agent_node(wsUrl, hasWebGPU, JSON.stringify(deviceInfo));
|
||||
} catch(e) {
|
||||
console.log("Virhe GPU-käynnistyksessä: " + e);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||