Compare commits
249 Commits
2e7ddf6f1e
...
revolution
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
6
.gitignore
vendored
@@ -34,3 +34,9 @@ Cargo.lock
|
|||||||
*.pdb
|
*.pdb
|
||||||
|
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/rust,linux
|
# End of https://www.toptal.com/developers/gitignore/api/rust,linux
|
||||||
|
|
||||||
|
# Ajonaikaiset tietokannat
|
||||||
|
*.db
|
||||||
|
|
||||||
|
# Wanha versio
|
||||||
|
temp/
|
||||||
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"
|
||||||
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
|
||||||
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
|
FROM rust:slim AS builder
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
pkg-config libssl-dev g++ \
|
pkg-config libssl-dev g++ libvulkan-dev \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY Cargo.toml Cargo.lock ./
|
COPY Cargo.toml ./
|
||||||
|
COPY Cargo.loc[k] ./
|
||||||
COPY hub/Cargo.toml hub/Cargo.toml
|
COPY hub/Cargo.toml hub/Cargo.toml
|
||||||
COPY node/Cargo.toml node/Cargo.toml
|
COPY node/Cargo.toml node/Cargo.toml
|
||||||
COPY native-node/Cargo.toml native-node/Cargo.toml
|
COPY native-node/Cargo.toml native-node/Cargo.toml
|
||||||
|
COPY cli/Cargo.toml cli/Cargo.toml
|
||||||
|
|
||||||
# Tyhjät src-tiedostot riippuvuuksien esikääntämistä varten
|
# Tyhjät src-tiedostot riippuvuuksien esikääntämistä varten
|
||||||
RUN mkdir -p hub/src node/src native-node/src \
|
RUN mkdir -p hub/src node/src native-node/src cli/src \
|
||||||
&& echo "fn main(){}" > hub/src/main.rs \
|
&& echo "fn main(){}" > hub/src/main.rs \
|
||||||
&& echo "" > node/src/lib.rs \
|
&& echo "" > node/src/lib.rs \
|
||||||
&& echo "fn main(){}" > native-node/src/main.rs \
|
&& echo "fn main(){}" > native-node/src/main.rs \
|
||||||
|
&& echo "fn main(){}" > cli/src/main.rs \
|
||||||
&& cargo build --release -p native-node 2>/dev/null || true
|
&& cargo build --release -p native-node 2>/dev/null || true
|
||||||
|
|
||||||
COPY native-node/src native-node/src
|
COPY native-node/src native-node/src
|
||||||
RUN cargo build --release -p native-node
|
# Touch pakottaa rekompilauksen dummy-binaryn yli
|
||||||
|
RUN touch native-node/src/main.rs && cargo build --release -p native-node
|
||||||
|
|
||||||
FROM debian:bookworm-slim
|
FROM debian:bookworm-slim
|
||||||
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y ca-certificates libvulkan1 && rm -rf /var/lib/apt/lists/*
|
||||||
COPY --from=builder /app/target/release/native-node /usr/local/bin/native-node
|
COPY --from=builder /app/target/release/native-node /usr/local/bin/native-node
|
||||||
|
|
||||||
ENV HUB_URL=ws://hub:3000/ws
|
ENV HUB_URL=ws://agentic-poc:3000/ws
|
||||||
|
ENV OLLAMA_URL=http://ollama:11434
|
||||||
|
ENV OLLAMA_MODEL=qwen2.5-coder:7b
|
||||||
ENV ALLOCATED_GB=4
|
ENV ALLOCATED_GB=4
|
||||||
|
|
||||||
CMD ["native-node"]
|
CMD ["native-node"]
|
||||||
|
|||||||
@@ -1,47 +1,63 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
FROM rust:slim AS builder
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
# --- Vaihe 1: Frontend (Astro) ---
|
||||||
curl pkg-config libssl-dev g++ \
|
FROM node:22-slim AS frontend
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
WORKDIR /app/frontend
|
||||||
|
# Riippuvuudet ensin → cache-kerros (muuttuu harvoin)
|
||||||
|
COPY frontend/package.json frontend/package-lock.json* ./
|
||||||
|
RUN npm install --silent
|
||||||
|
# Lähdekoodi → muuttuu usein, mutta npm install on cachessa
|
||||||
|
COPY frontend/ .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# --- Vaihe 2: Wasm (wasm-pack) ---
|
||||||
|
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
|
RUN curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
COPY Cargo.toml Cargo.lock* ./
|
||||||
# Kopioi kaikki Cargo-tiedostot
|
COPY node/Cargo.toml node/Cargo.toml
|
||||||
COPY Cargo.toml ./
|
COPY node/src node/src
|
||||||
COPY Cargo.lock* ./
|
# Dummy-cratet jotta workspace Cargo.toml on tyytyväinen
|
||||||
COPY hub/Cargo.toml hub/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
|
||||||
|
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
|
||||||
|
|
||||||
|
# --- 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 hub/src hub/src
|
||||||
|
# Tarvitaan dummy-cratet jotta workspace kompiloi
|
||||||
COPY node/Cargo.toml node/Cargo.toml
|
COPY node/Cargo.toml node/Cargo.toml
|
||||||
COPY native-node/Cargo.toml native-node/Cargo.toml
|
COPY native-node/Cargo.toml native-node/Cargo.toml
|
||||||
COPY cli/Cargo.toml cli/Cargo.toml
|
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
|
||||||
# Kopioi lähdekoodi
|
|
||||||
COPY hub/src hub/src
|
|
||||||
COPY node/src node/src
|
|
||||||
COPY native-node/src native-node/src
|
|
||||||
COPY cli/src cli/src
|
|
||||||
COPY static static
|
|
||||||
|
|
||||||
# Rakenna Wasm — cache mount pitää Cargo-rekisterin ja target-kansion buildien välillä
|
|
||||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
|
||||||
--mount=type=cache,target=/app/target \
|
|
||||||
cd node && wasm-pack build --target web --out-dir ../static/pkg
|
|
||||||
|
|
||||||
# Rakenna Hub
|
|
||||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||||
--mount=type=cache,target=/app/target \
|
--mount=type=cache,target=/app/target \
|
||||||
cargo build --release -p hub \
|
cargo build --release -p hub \
|
||||||
&& cp /app/target/release/hub /usr/local/bin/hub
|
&& cp /app/target/release/hub /usr/local/bin/hub
|
||||||
|
|
||||||
|
# --- Vaihe 4: Tuotantoimage ---
|
||||||
FROM debian:bookworm-slim
|
FROM debian:bookworm-slim
|
||||||
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
COPY --from=builder /usr/local/bin/hub /usr/local/bin/hub
|
COPY --from=hub-builder /usr/local/bin/hub /usr/local/bin/hub
|
||||||
COPY --from=builder /app/static /app/static
|
COPY --from=frontend /app/frontend/dist /app/frontend/dist
|
||||||
|
COPY --from=wasm-builder /app/wasm-pkg /app/frontend/dist/pkg
|
||||||
|
|
||||||
|
# Kopioidaan GUIDE.md ja templates
|
||||||
|
COPY frontend/public/GUIDE.md /app/frontend/dist/GUIDE.md
|
||||||
|
COPY frontend/public/templates /app/frontend/dist/templates
|
||||||
|
COPY frontend/public/avatars /app/frontend/dist/avatars
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ENV STATIC_DIR=/app/static
|
ENV STATIC_DIR=/app/frontend/dist
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD ["hub"]
|
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 |
|
||||||
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
|
||||||
38
network-poc/build-binaries.sh
Executable file
@@ -0,0 +1,38 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Käännä kipina-node binäärit kaikille alustoille
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
OUT="$SCRIPT_DIR/frontend/public/download"
|
||||||
|
mkdir -p "$OUT"
|
||||||
|
|
||||||
|
echo "=== Kipinä Node — Binary Build ==="
|
||||||
|
|
||||||
|
# macOS ARM (natiivi)
|
||||||
|
echo "[1/3] macOS ARM64..."
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
cargo build --release -p native-node --no-default-features 2>&1 | tail -1
|
||||||
|
cp target/release/native-node "$OUT/kipina-node-macos-arm64"
|
||||||
|
echo " $(ls -lh "$OUT/kipina-node-macos-arm64" | awk '{print $5}')"
|
||||||
|
|
||||||
|
# Linux x86_64 (Docker)
|
||||||
|
echo "[2/3] Linux x86_64..."
|
||||||
|
docker run --rm \
|
||||||
|
-v "$SCRIPT_DIR":/app -w /app \
|
||||||
|
--platform linux/amd64 \
|
||||||
|
rust:slim \
|
||||||
|
bash -c "apt-get update -qq && apt-get install -y -qq pkg-config libssl-dev >/dev/null 2>&1 && cargo build --release -p native-node --no-default-features 2>&1 | tail -1 && cp target/release/native-node /app/frontend/public/download/kipina-node-linux-x86_64"
|
||||||
|
echo " $(ls -lh "$OUT/kipina-node-linux-x86_64" | awk '{print $5}')"
|
||||||
|
|
||||||
|
# Linux ARM64 (Docker)
|
||||||
|
echo "[3/3] Linux ARM64..."
|
||||||
|
docker run --rm \
|
||||||
|
-v "$SCRIPT_DIR":/app -w /app \
|
||||||
|
--platform linux/arm64 \
|
||||||
|
rust:slim \
|
||||||
|
bash -c "apt-get update -qq && apt-get install -y -qq pkg-config libssl-dev >/dev/null 2>&1 && cargo build --release -p native-node --no-default-features 2>&1 | tail -1 && cp target/release/native-node /app/frontend/public/download/kipina-node-linux-arm64"
|
||||||
|
echo " $(ls -lh "$OUT/kipina-node-linux-arm64" | awk '{print $5}')"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Binäärit valmiina ==="
|
||||||
|
ls -lh "$OUT"/kipina-node-*
|
||||||
28
network-poc/deploy-fast.sh
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Nopea deploy: päivittää vain frontendin (ei kontin uudelleenkäynnistystä)
|
||||||
|
# Hub-binäärin päivitys: käytä deploy.sh tai deploy-light.sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SERVER="ubuntu@86.50.252.98"
|
||||||
|
REMOTE_DIR="~/code/agentic-studio/network-poc"
|
||||||
|
SSH_OPTS="-o StrictHostKeyChecking=no"
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
echo "=== Kipinä Studio — Frontend Deploy ==="
|
||||||
|
|
||||||
|
# 1. Buildaa frontend paikallisesti
|
||||||
|
echo "[1/2] Rakennetaan frontend..."
|
||||||
|
cd "$SCRIPT_DIR/frontend"
|
||||||
|
[ -d node_modules ] || npm install --silent
|
||||||
|
npm run build --silent 2>&1 | tail -1
|
||||||
|
|
||||||
|
# 2. Synkataan dist/ palvelimelle (vain muuttuneet tiedostot)
|
||||||
|
echo "[2/2] Synkataan dist/ → palvelin..."
|
||||||
|
ssh $SSH_OPTS $SERVER "mkdir -p $REMOTE_DIR/frontend/dist"
|
||||||
|
rsync -az --delete -e "ssh $SSH_OPTS" "$SCRIPT_DIR/frontend/dist/" "$SERVER:$REMOTE_DIR/frontend/dist/"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Valmis! Frontend päivitetty — ei uudelleenkäynnistystä ==="
|
||||||
|
echo " https://kipina.studio"
|
||||||
|
echo ""
|
||||||
|
echo "Huom: Jos Rust-koodi (hub/) muuttui, aja: ./deploy.sh"
|
||||||
33
network-poc/deploy-light.sh
Executable file
@@ -0,0 +1,33 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Kevyt deploy: lähetetään vain koodi, palvelin buildaa itse
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SERVER="ubuntu@86.50.252.98"
|
||||||
|
REMOTE_DIR="~/code/agentic-studio/network-poc"
|
||||||
|
SSH_OPTS="-o StrictHostKeyChecking=no"
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
echo "=== Kipinä Studio Deploy (remote build) ==="
|
||||||
|
|
||||||
|
# 1. Synkataan koodi palvelimelle (vain muuttuneet tiedostot)
|
||||||
|
echo "[1/3] Synkataan koodi..."
|
||||||
|
rsync -az --delete \
|
||||||
|
--exclude 'target/' \
|
||||||
|
--exclude 'node_modules/' \
|
||||||
|
--exclude 'dist/' \
|
||||||
|
--exclude '.astro/' \
|
||||||
|
--exclude 'temp/' \
|
||||||
|
--exclude '*.db' \
|
||||||
|
--exclude '.git/' \
|
||||||
|
"$SCRIPT_DIR/" "$SERVER:$REMOTE_DIR/"
|
||||||
|
|
||||||
|
# 2. Rakennetaan image palvelimella
|
||||||
|
echo "[2/3] Rakennetaan image palvelimella..."
|
||||||
|
ssh $SSH_OPTS $SERVER "cd $REMOTE_DIR && docker build -f Dockerfile.prod -t kipina-agentic:latest ."
|
||||||
|
|
||||||
|
# 3. Käynnistetään
|
||||||
|
echo "[3/3] Käynnistetään..."
|
||||||
|
ssh $SSH_OPTS $SERVER "cd $REMOTE_DIR && docker compose -f docker-compose.prod.yml down && docker compose -f docker-compose.prod.yml up -d"
|
||||||
|
|
||||||
|
echo "=== Valmis! https://kipina.studio ==="
|
||||||
@@ -1,6 +1,13 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
if [ "$1" == "local" ]; then
|
||||||
|
echo "=== Kipinä Studio Local Development ==="
|
||||||
|
echo "Käynnistetään kokonaisuus puhtaasti Docker-kontissa..."
|
||||||
|
docker compose up agentic-poc
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
SERVER="ubuntu@86.50.252.98"
|
SERVER="ubuntu@86.50.252.98"
|
||||||
REMOTE_DIR="~/code/agentic-studio/network-poc"
|
REMOTE_DIR="~/code/agentic-studio/network-poc"
|
||||||
KEY="$HOME/.ssh/id_rsa"
|
KEY="$HOME/.ssh/id_rsa"
|
||||||
@@ -14,9 +21,23 @@ fi
|
|||||||
|
|
||||||
echo "=== Kipinä Studio Deploy ==="
|
echo "=== Kipinä Studio Deploy ==="
|
||||||
|
|
||||||
|
# 0. Commitoidaan uncommitted muutokset ennen deployta
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
if ! git -C "$SCRIPT_DIR" diff --quiet HEAD 2>/dev/null || \
|
||||||
|
[ -n "$(git -C "$SCRIPT_DIR" ls-files --others --exclude-standard 2>/dev/null)" ]; then
|
||||||
|
echo "[0] Uncommitted muutoksia havaittu — commitoidaan..."
|
||||||
|
read -rp " Commit-viesti: " DEPLOY_MSG
|
||||||
|
if [ -z "$DEPLOY_MSG" ]; then
|
||||||
|
DEPLOY_MSG="Deploy $(date +%Y-%m-%d\ %H:%M)"
|
||||||
|
fi
|
||||||
|
git -C "$SCRIPT_DIR" add -A
|
||||||
|
git -C "$SCRIPT_DIR" commit -m "$DEPLOY_MSG"
|
||||||
|
echo " Commitoitu: $DEPLOY_MSG"
|
||||||
|
fi
|
||||||
|
|
||||||
# 1. Rakennetaan Docker-image lokaalisti
|
# 1. Rakennetaan Docker-image lokaalisti
|
||||||
echo "[1/4] Rakennetaan image lokaalisti..."
|
echo "[1/4] Rakennetaan image lokaalisti..."
|
||||||
docker build -f Dockerfile.prod -t kipina-agentic:latest .
|
docker build --platform linux/amd64 -f Dockerfile.prod -t kipina-agentic:latest .
|
||||||
|
|
||||||
# 2. Tallennetaan tiedostoon
|
# 2. Tallennetaan tiedostoon
|
||||||
echo "[2/5] Pakataan image..."
|
echo "[2/5] Pakataan image..."
|
||||||
@@ -39,7 +60,11 @@ echo "=== Valmis! https://kipina.studio ==="
|
|||||||
|
|
||||||
# Discord-notifikaatio
|
# Discord-notifikaatio
|
||||||
DISCORD_WEBHOOK="https://discord.com/api/webhooks/1489504066898755687/8U02d0wug-3MkVax0xMmRoj0s_-V1psnNLPWdSOjnGnKRBUpPjaU6XiX9Iu8DgJI69AP"
|
DISCORD_WEBHOOK="https://discord.com/api/webhooks/1489504066898755687/8U02d0wug-3MkVax0xMmRoj0s_-V1psnNLPWdSOjnGnKRBUpPjaU6XiX9Iu8DgJI69AP"
|
||||||
COMMIT_MSG=$(git log -1 --pretty=format:"%s" 2>/dev/null || echo "?")
|
COMMIT_HASH=$(git -C "$SCRIPT_DIR" log -1 --pretty=format:"%h" 2>/dev/null || echo "?")
|
||||||
curl -s -H "Content-Type: application/json" \
|
COMMIT_MSG=$(git -C "$SCRIPT_DIR" log -1 --pretty=format:"%s" 2>/dev/null || echo "?")
|
||||||
-d "{\"content\":\"🚀 **Kipinä Studio julkaistu!**\n> ${COMMIT_MSG}\n> https://kipina.studio\n> Admin: https://kipina.studio/admin (salasana: kipina)\"}" \
|
# python3 escapettaa erikoismerkit JSON-turvallisesti
|
||||||
"$DISCORD_WEBHOOK" > /dev/null
|
PAYLOAD=$(python3 -c "import json,sys; print(json.dumps({'content': sys.argv[1]}))" \
|
||||||
|
"🚀 **Kipinä Studio julkaistu!**
|
||||||
|
> \`${COMMIT_HASH}\` ${COMMIT_MSG}
|
||||||
|
> https://kipina.studio")
|
||||||
|
curl -s -H "Content-Type: application/json" -d "$PAYLOAD" "$DISCORD_WEBHOOK" > /dev/null
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
services:
|
services:
|
||||||
# NVIDIA GPU -solmu
|
# Ollama NVIDIA GPU:lla
|
||||||
native-node-nvidia:
|
ollama-nvidia:
|
||||||
build:
|
image: ollama/ollama:latest
|
||||||
context: .
|
container_name: kipina-ollama
|
||||||
dockerfile: Dockerfile.native-node
|
ports:
|
||||||
container_name: kipina-node-nvidia
|
- "11434:11434"
|
||||||
environment:
|
volumes:
|
||||||
- HUB_URL=wss://kipina.studio/ws
|
- ollama-models:/root/.ollama
|
||||||
- ALLOCATED_GB=4
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
@@ -16,6 +15,65 @@ services:
|
|||||||
- driver: nvidia
|
- driver: nvidia
|
||||||
count: all
|
count: all
|
||||||
capabilities: [gpu]
|
capabilities: [gpu]
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
aliases:
|
||||||
|
- ollama
|
||||||
|
profiles:
|
||||||
|
- nvidia
|
||||||
|
|
||||||
|
# Ollama AMD ROCm GPU:lla
|
||||||
|
ollama-amd:
|
||||||
|
image: ollama/ollama:rocm
|
||||||
|
container_name: kipina-ollama
|
||||||
|
ports:
|
||||||
|
- "11434:11434"
|
||||||
|
volumes:
|
||||||
|
- ollama-models:/root/.ollama
|
||||||
|
restart: unless-stopped
|
||||||
|
devices:
|
||||||
|
- /dev/kfd:/dev/kfd
|
||||||
|
- /dev/dri:/dev/dri
|
||||||
|
group_add:
|
||||||
|
- video
|
||||||
|
- render
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
aliases:
|
||||||
|
- ollama
|
||||||
|
profiles:
|
||||||
|
- amd
|
||||||
|
|
||||||
|
# Ollama CPU:lla
|
||||||
|
ollama-cpu:
|
||||||
|
image: ollama/ollama:latest
|
||||||
|
container_name: kipina-ollama
|
||||||
|
ports:
|
||||||
|
- "11434:11434"
|
||||||
|
volumes:
|
||||||
|
- ollama-models:/root/.ollama
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
aliases:
|
||||||
|
- ollama
|
||||||
|
profiles:
|
||||||
|
- cpu
|
||||||
|
|
||||||
|
# NVIDIA GPU -solmu
|
||||||
|
native-node-nvidia:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.native-node
|
||||||
|
container_name: kipina-node-nvidia
|
||||||
|
environment:
|
||||||
|
- HUB_URL=wss://kipina.studio/ws
|
||||||
|
- OLLAMA_URL=http://ollama:11434
|
||||||
|
- OLLAMA_MODEL=qwen2.5-coder:7b
|
||||||
|
- ALLOCATED_GB=4
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- ollama-nvidia
|
||||||
profiles:
|
profiles:
|
||||||
- nvidia
|
- nvidia
|
||||||
|
|
||||||
@@ -27,14 +85,12 @@ services:
|
|||||||
container_name: kipina-node-amd
|
container_name: kipina-node-amd
|
||||||
environment:
|
environment:
|
||||||
- HUB_URL=wss://kipina.studio/ws
|
- HUB_URL=wss://kipina.studio/ws
|
||||||
|
- OLLAMA_URL=http://ollama:11434
|
||||||
|
- OLLAMA_MODEL=qwen2.5-coder:7b
|
||||||
- ALLOCATED_GB=4
|
- ALLOCATED_GB=4
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
devices:
|
depends_on:
|
||||||
- /dev/kfd:/dev/kfd
|
- ollama-amd
|
||||||
- /dev/dri:/dev/dri
|
|
||||||
group_add:
|
|
||||||
- video
|
|
||||||
- render
|
|
||||||
profiles:
|
profiles:
|
||||||
- amd
|
- amd
|
||||||
|
|
||||||
@@ -46,7 +102,14 @@ services:
|
|||||||
container_name: kipina-node-cpu
|
container_name: kipina-node-cpu
|
||||||
environment:
|
environment:
|
||||||
- HUB_URL=wss://kipina.studio/ws
|
- HUB_URL=wss://kipina.studio/ws
|
||||||
|
- OLLAMA_URL=http://ollama:11434
|
||||||
|
- OLLAMA_MODEL=qwen2.5-coder:7b
|
||||||
- ALLOCATED_GB=2
|
- ALLOCATED_GB=2
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- ollama-cpu
|
||||||
profiles:
|
profiles:
|
||||||
- cpu
|
- cpu
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
ollama-models:
|
||||||
|
|||||||
@@ -19,8 +19,12 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
- DATABASE_PATH=/data/nodes.db
|
- DATABASE_PATH=/data/nodes.db
|
||||||
|
- STATIC_DIR=/app/frontend/dist
|
||||||
|
- ADMIN_PASSWORD=${ADMIN_PASSWORD:-}
|
||||||
|
- NODE_API_KEY=${NODE_API_KEY:-}
|
||||||
volumes:
|
volumes:
|
||||||
- hub_data:/data
|
- hub_data:/data
|
||||||
|
- ./frontend/dist:/app/frontend/dist:ro
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
caddy_data:
|
caddy_data:
|
||||||
|
|||||||
@@ -9,26 +9,39 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- .:/app
|
- .:/app
|
||||||
# Käännetään aina käynnistyksen yhteydessä varmuuden vuoksi Wasm uusimmista koodeista, ja päälle pyöräytetään Hub!
|
# Käännetään aina käynnistyksen yhteydessä varmuuden vuoksi Wasm uusimmista koodeista, ja päälle pyöräytetään Hub!
|
||||||
command: bash -c "cd node && wasm-pack build --target web --out-dir ../static/pkg && cd ../hub && cargo run"
|
command: bash -c "cd node && wasm-pack build --release --target web --out-dir ../static/pkg && cd ../hub && cargo run"
|
||||||
|
|
||||||
# Valinnainen natiivi-solmu — kerää oikeat laitteistotiedot (nvidia-smi-taso)
|
# Ollama — LLM-inferenssi
|
||||||
|
# NVIDIA: vaihda image → ollama/ollama:latest ja lisää deploy.resources (ks. README)
|
||||||
|
# CPU: vaihda image → ollama/ollama:latest ja poista devices
|
||||||
|
ollama:
|
||||||
|
image: ollama/ollama:rocm
|
||||||
|
container_name: kipina_ollama
|
||||||
|
ports:
|
||||||
|
- "11434:11434"
|
||||||
|
volumes:
|
||||||
|
- ollama-models:/root/.ollama
|
||||||
|
devices:
|
||||||
|
- /dev/kfd
|
||||||
|
- /dev/dri
|
||||||
|
profiles:
|
||||||
|
- native
|
||||||
|
|
||||||
|
# Natiivisolmu — yhdistää hubiin ja käyttää Ollamaa inferenssiin
|
||||||
native-node:
|
native-node:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile.native-node
|
dockerfile: Dockerfile.native-node
|
||||||
container_name: kipina_native_node
|
container_name: kipina_native_node
|
||||||
environment:
|
environment:
|
||||||
- HUB_URL=ws://agentic-poc:3000/ws
|
- HUB_URL=wss://kipina.studio/ws
|
||||||
|
- OLLAMA_URL=http://ollama:11434
|
||||||
|
- OLLAMA_MODEL=qwen2.5-coder:7b
|
||||||
- ALLOCATED_GB=4
|
- ALLOCATED_GB=4
|
||||||
depends_on:
|
depends_on:
|
||||||
- agentic-poc
|
- ollama
|
||||||
# GPU passthrough (valinnainen — toimii myös ilman)
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
reservations:
|
|
||||||
devices:
|
|
||||||
- driver: nvidia
|
|
||||||
count: all
|
|
||||||
capabilities: [gpu]
|
|
||||||
profiles:
|
profiles:
|
||||||
- native
|
- native
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
ollama-models:
|
||||||
|
|||||||
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
413
network-poc/frontend/public/GUIDE.md
Normal file
@@ -0,0 +1,413 @@
|
|||||||
|
# Kipinä Agentic Studio — Opas
|
||||||
|
|
||||||
|
Hajautettu AI-laskentaverkko jossa kielimallit ajavat koodia suoraan selaimessa.
|
||||||
|
Tämä opas selittää miten kielimallit toimivat, miten niitä ohjataan, ja miten
|
||||||
|
tuloksia voi parantaa.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Kielimallit ja niiden koot
|
||||||
|
|
||||||
|
Kielimalli on neuroverkko joka ennustaa seuraavan sanan (tokenin) edellisten
|
||||||
|
perusteella. Mallin "koko" tarkoittaa parametrien (painojen) määrää:
|
||||||
|
|
||||||
|
| Malli | Parametrit | Koko levyllä | Nopeus selaimessa | Koodinlaatu |
|
||||||
|
|-------|-----------|-------------|-------------------|-------------|
|
||||||
|
| SmolLM 135M | 135 miljoonaa | ~270 MB | ~5 tok/s | Yksinkertainen teksti |
|
||||||
|
| Qwen2.5-Coder:0.5B | 500 miljoonaa | ~990 MB | ~3-6 tok/s | Pienet funktiot |
|
||||||
|
| Qwen2.5-Coder:3B | 3 miljardia | ~6.2 GB | ~0.4 tok/s | Kokonaiset tiedostot |
|
||||||
|
| GPT-4 (vertailu) | ~1800 miljardia | ~3.6 TB | pilvipalvelu | Kokonaiset projektit |
|
||||||
|
|
||||||
|
**Parametrien vaikutus:** Jokainen parametri on yksi liukuluku (float16 = 2 tavua)
|
||||||
|
joka tallentaa opittua tietoa. 0.5B-malli tietää perusrakenteet mutta tekee
|
||||||
|
loogisia virheitä. 3B-malli ymmärtää kontekstin paremmin. Ero on kuin sanakirjan
|
||||||
|
ja oppikirjan välillä.
|
||||||
|
|
||||||
|
**Miksi selaimessa?** Malli ajetaan käyttäjän omalla laitteella WebAssemblyn
|
||||||
|
kautta. Data ei lähde koneelta, eikä tarvita pilvipalvelua. Haittapuoli on
|
||||||
|
hitaus — GPU-palvelimella sama 0.5B-malli tuottaa ~100 tok/s.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tokenit — kielimallin "sanat"
|
||||||
|
|
||||||
|
Malli ei näe tekstiä kirjaimina vaan **tokeneina**. Tokeni on yleensä
|
||||||
|
sanan osa, kokonainen sana tai välilyönti. Tokenisaatio tehdään
|
||||||
|
BPE-algoritmilla (Byte Pair Encoding) joka oppii yleisimmät
|
||||||
|
merkkijonot harjoitusdatasta.
|
||||||
|
|
||||||
|
### Esimerkki: suomi vs. englanti
|
||||||
|
|
||||||
|
Alla oikea tokenisointitulos Qwen2.5-Coder-tokenisaattorilla. Jokainen
|
||||||
|
värikoodattu lohko on yksi tokeni — huomaa miten suomi vaatii enemmän
|
||||||
|
tokeneita saman merkityksen välittämiseen:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**Huomaa miten:**
|
||||||
|
- Englannin yleiset sanat (`the`, `in`, `a`, `function`) ovat kokonaisia tokeneita
|
||||||
|
- Suomen sanat pilkotaan pienempiin osiin (`Hajautettu` → 4 tokenia, `Distributed` → 2)
|
||||||
|
- Suomi vaatii **30-50% enemmän tokeneita** saman merkityksen välittämiseen
|
||||||
|
- Koodiavainsanat (`function`, `list`, `sort`) ovat tehokkaita molemmilla kielillä
|
||||||
|
|
||||||
|
### Miksi tämä merkitsee?
|
||||||
|
|
||||||
|
**Jokainen tokeni = yksi laskentakierros.** Jos suomi vaatii 50% enemmän tokeneita:
|
||||||
|
|
||||||
|
1. **Hitaampi vastaus:** 100 tokenin englanninkielinen vastaus ≈ 150 tokenia suomeksi
|
||||||
|
→ 50% pidempi odotusaika
|
||||||
|
2. **Pienempi konteksti:** Sama merkityssisältö vie enemmän tilaa konteksti-ikkunasta
|
||||||
|
3. **Huonompi ymmärrys:** Pitkät sanat pilkotaan osiin jotka malli ei välttämättä
|
||||||
|
tunnista → hallusinaatiot lisääntyvät
|
||||||
|
|
||||||
|
**Siksi tekniset promptit ovat englanniksi** — malli saa enemmän informaatiota
|
||||||
|
samassa token-budjetissa ja ymmärtää ohjeet paremmin.
|
||||||
|
|
||||||
|
**Token-budjetti tässä järjestelmässä:**
|
||||||
|
|
||||||
|
| Osa | Tokeneita | Osuus |
|
||||||
|
|-----|-----------|-------|
|
||||||
|
| System prompt | ~30 | kiinteä |
|
||||||
|
| Agent prompt | ~25 | kiinteä |
|
||||||
|
| Konteksti (aiemmat tiedostot) | 0-300 | kasvaa |
|
||||||
|
| Käyttäjän prompti | ~20-50 | vaihtelee |
|
||||||
|
| **Syöte yhteensä** | **~75-400** | |
|
||||||
|
| Generoitu vastaus (max) | 512 | raja |
|
||||||
|
| **Yhteensä** | **~600-900** | /32 768 |
|
||||||
|
|
||||||
|
Konteksti-ikkuna on reilusti riittävä. Pullonkaula ei ole ikkunan koko
|
||||||
|
vaan **mallin kyky ymmärtää pitkää kontekstia** — 0.5B-malli alkaa
|
||||||
|
"unohtaa" ohjeet kun konteksti kasvaa yli ~200 tokenin.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Promptit — miten mallia ohjataan
|
||||||
|
|
||||||
|
### Kolmitasoinen prompttirakenne
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S["System prompt<br/><i>You are a coding assistant. Respond with ONLY code.</i><br/>🔒 Kiinteä, kovakoodattu — malli priorisoi tämän"]
|
||||||
|
A["Agent prompt<br/><i>Olet kokenut ohjelmistokehittäjä...</i><br/>✏️ Käyttäjän muokattavissa UI:ssa"]
|
||||||
|
U["User prompt<br/><i>Write ONLY the file main.py...</i><br/>📋 Vaihtelee joka kutsussa, sisältää kontekstin"]
|
||||||
|
P["Prefill: ``` <br/>🎯 Pakottaa mallin aloittamaan koodilla"]
|
||||||
|
S --> A --> U --> P
|
||||||
|
P -->|malli jatkaa| R["Generoitu koodi"]
|
||||||
|
|
||||||
|
style S fill:#1a1e2e,stroke:#f85149,color:#c9d1d9
|
||||||
|
style A fill:#1a1e2e,stroke:#d29922,color:#c9d1d9
|
||||||
|
style U fill:#1a1e2e,stroke:#3fb950,color:#c9d1d9
|
||||||
|
style P fill:#1a1e2e,stroke:#a371f7,color:#c9d1d9
|
||||||
|
style R fill:#0d1117,stroke:#58a6ff,color:#58a6ff
|
||||||
|
```
|
||||||
|
|
||||||
|
### Miksi promptit ovat englanniksi?
|
||||||
|
|
||||||
|
Qwen2.5-Coder on harjoitettu pääosin englanninkielisellä koodilla ja
|
||||||
|
dokumentaatiolla. Suomenkielinen ohje kuluttaa enemmän tokeneita JA
|
||||||
|
malli ymmärtää sen huonommin. Agenttien nimet ja käyttöliittymä ovat
|
||||||
|
suomeksi, mutta tekniset ohjeet mallille englanniksi.
|
||||||
|
|
||||||
|
Poikkeus: agenttipromptit ovat suomeksi koska ne menevät user-blokkiin
|
||||||
|
(ei system-blokkiin) ja niiden tarkoitus on enemmän "persoonallisuus"
|
||||||
|
kuin tekninen ohje.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prefill-tekniikka
|
||||||
|
|
||||||
|
Normaalisti malli päättää vapaasti miten vastaa:
|
||||||
|
|
||||||
|
```
|
||||||
|
Ilman prefilliä:
|
||||||
|
Malli: "Sure! Here is a Python program that prints Hello World:\n```python\nprint('Hello')\n```"
|
||||||
|
→ 25 tokenia, joista 15 turhia
|
||||||
|
|
||||||
|
Prefillin kanssa:
|
||||||
|
Me syötämme: ```
|
||||||
|
Malli jatkaa: python\nprint('Hello')\n```
|
||||||
|
→ 5 tokenia, kaikki hyödyllisiä
|
||||||
|
```
|
||||||
|
|
||||||
|
Prefill on kuin aloittaisit lauseen toisen puolesta — malli jatkaa
|
||||||
|
siitä mihin jäit sen sijaan, että aloittaisi kohteliaalla johdannolla.
|
||||||
|
|
||||||
|
**Sivuvaikutus:** Malli tuottaa kielitunnisteen (`python`, `rust`) ja
|
||||||
|
sulkevan ` ``` `:n. Nämä siivotaan jälkikäteen `strip_markdown_wrapper`-funktiolla.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sampling — miten malli valitsee seuraavan tokenin
|
||||||
|
|
||||||
|
Malli ei "tiedä" oikeaa vastausta. Se laskee jokaiselle mahdolliselle
|
||||||
|
seuraavalle tokenille todennäköisyyden ja valitsee yhden. Valintaa
|
||||||
|
ohjataan kolmella parametrilla:
|
||||||
|
|
||||||
|
### Temperature (0.7)
|
||||||
|
|
||||||
|
Kontrolloi "luovuutta" vs. "varmuutta":
|
||||||
|
|
||||||
|
```
|
||||||
|
Temperature 0.0 (greedy): Aina todennäköisin tokeni → "def fibonacci(n):"
|
||||||
|
Temperature 0.7 (oletus): Painottaa todennäköisiä mutta sallii vaihtelua
|
||||||
|
Temperature 1.5 (luova): Lähes satunnainen → "async lambda fib = ..."
|
||||||
|
```
|
||||||
|
|
||||||
|
0.7 on kompromissi: tarpeeksi determinististä tuottamaan toimivaa koodia,
|
||||||
|
mutta tarpeeksi vaihtelevaa välttämään toistoa.
|
||||||
|
|
||||||
|
### Top-k (40)
|
||||||
|
|
||||||
|
Rajaa valinnan 40 todennäköisimpään tokeniin. Estää mallia valitsemasta
|
||||||
|
täysin absurdeja vaihtoehtoja:
|
||||||
|
|
||||||
|
```
|
||||||
|
Ilman top-k: 150 936 vaihtoehtoa → voi valita minkä tahansa
|
||||||
|
Top-k 40: 40 vaihtoehtoa → järkevät vaihtoehdot
|
||||||
|
Top-k 1: 1 vaihtoehto → greedy (aina sama vastaus)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Repetition penalty (1.15)
|
||||||
|
|
||||||
|
Vähentää jo tuotettujen tokenien todennäköisyyttä. Estää mallia
|
||||||
|
juuttumasta luuppiin:
|
||||||
|
|
||||||
|
```
|
||||||
|
Ilman rangaistusta: "print print print print print..."
|
||||||
|
Penalty 1.15: "print('Hello')\nprint('World')"
|
||||||
|
```
|
||||||
|
|
||||||
|
1.15 on lievä rangaistus — estää pahimman toiston mutta sallii
|
||||||
|
saman avainsanan (esim. `return`) esiintymisen useasti.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stop-sekvenssit — milloin generointi loppuu
|
||||||
|
|
||||||
|
Malli generoi tokeneita kunnes jokin näistä tapahtuu:
|
||||||
|
|
||||||
|
1. **EOS-tokeni** (151645): Mallin oma "loppu"-merkki
|
||||||
|
2. **Max tokens** (512): Kovakoodattu raja
|
||||||
|
3. **Stop-sekvenssi**: Malli alkaa tuottaa selitystä
|
||||||
|
|
||||||
|
```
|
||||||
|
fn fibonacci(n: usize) -> usize {
|
||||||
|
if n <= 1 { return n; }
|
||||||
|
fibonacci(n-1) + fibonacci(n-2)
|
||||||
|
}
|
||||||
|
← Tähän asti koodia, ok
|
||||||
|
// Example usage: ← Stop! Tämä ei ole enää vastausta
|
||||||
|
let result = fibonacci(10); ← Ei generoida
|
||||||
|
```
|
||||||
|
|
||||||
|
Tunnistetut stop-sekvenssit: `### `, `Explanation`, `Note:`, `Output:`,
|
||||||
|
`// Example`, `# Example`. Generointi katkaistaan ja teksti trimmataan
|
||||||
|
stop-kohtaan.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Projekti-pipeline — miten agenttitiimi toimii
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
U["Käyttäjä: FastAPI + SQLite REST API for users"] --> M
|
||||||
|
M["🟡 Manageri: Pilko tiedostoiksi"] -->|tiedostolista| C1
|
||||||
|
C1["🟢 Koodari: models.py"] -->|"konteksti: models.py"| C2
|
||||||
|
C2["🟢 Koodari: main.py"] -->|"konteksti: models + main"| C3
|
||||||
|
C3["🟢 Koodari: pyproject.toml"] -->|kaikki tiedostot| T1
|
||||||
|
T1["🔵 Testaaja: Review"] -->|bugeja löytyi| C4
|
||||||
|
T1 -->|LGTM| Done["✅ Projekti valmis"]
|
||||||
|
C4["🟡 Koodari: Korjaukset"] --> T2
|
||||||
|
T2["🔵 Testaaja: Uudelleenarviointi"] --> Done
|
||||||
|
```
|
||||||
|
|
||||||
|
**Kontekstin ketjutus** on kriittistä: kun koodari kirjoittaa `main.py`:tä,
|
||||||
|
se saa `models.py`:n sisällön promptissa. Ilman tätä se ei tietäisi
|
||||||
|
mitä luokkia importata.
|
||||||
|
|
||||||
|
**Riippuvuusjärjestys:** Manageria pyydetään listaamaan riippuvuudet ensin
|
||||||
|
(models.py ennen main.py) jotta kontekstiketju toimii oikeaan suuntaan.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Laadun parantaminen
|
||||||
|
|
||||||
|
### 1. Isompi malli (suurin vaikutus)
|
||||||
|
|
||||||
|
| | 0.5B | 3B | Pilvi-API |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Fibonacci | Joskus virheitä | Yleensä oikein | Aina oikein |
|
||||||
|
| FastAPI CRUD | Voi käyttää Flaskia | Oikea kirjasto | Täydellinen |
|
||||||
|
| Monimutkainen logiikka | Hallusinoi | Osaa perusasiat | Syvä ymmärrys |
|
||||||
|
| Nopeus (selain) | ~5 tok/s | ~0.4 tok/s | — |
|
||||||
|
| Latauksen koko | 990 MB | 6.2 GB | 0 (API) |
|
||||||
|
|
||||||
|
**Käytännössä:** `kpn load 2` lataa 3B-mallin. Hitaampi mutta huomattavasti
|
||||||
|
parempi koodinlaatu. Suositus monimutkaisiin projekteihin.
|
||||||
|
|
||||||
|
### 2. Paremmat promptit (ilmaista)
|
||||||
|
|
||||||
|
**Huono:** `"tee fibonacci"`
|
||||||
|
- Malli ei tiedä kieltä, formaattia tai kontekstia
|
||||||
|
|
||||||
|
**Hyvä:** `"Write a fibonacci function in Rust that returns Vec<u64>"`
|
||||||
|
- Kieli, palautustyyppi ja rakenne määritelty
|
||||||
|
|
||||||
|
**Promptin säännöt:**
|
||||||
|
- Englanniksi (tehokkaampi tokenisointi, parempi ymmärrys)
|
||||||
|
- Konkreettinen (mainitse kieli, kirjastot, palautustyyppi)
|
||||||
|
- Lyhyt (jokainen sana kuluttaa tokenin konteksti-ikkunasta)
|
||||||
|
- Positiivinen ("Write X" ei "Don't write Y")
|
||||||
|
|
||||||
|
### 3. Kontekstin hallinta (pipeline-taso)
|
||||||
|
|
||||||
|
**Ongelma:** 0.5B-malli "unohtaa" promptin alun kun konteksti kasvaa.
|
||||||
|
|
||||||
|
**Ratkaisu:** Pienet, kohdennetut promptit:
|
||||||
|
- Yksi tiedosto kerrallaan (ei "kirjoita koko projekti")
|
||||||
|
- Vain relevantit aiemmat tiedostot kontekstina
|
||||||
|
- Max 4 tiedostoa per projekti
|
||||||
|
|
||||||
|
### 4. Iterointi (review-luuppi)
|
||||||
|
|
||||||
|
Yksi generointikierros tuottaa harvoin virheetöntä koodia.
|
||||||
|
Pipeline-arkkitehtuuri mahdollistaa:
|
||||||
|
|
||||||
|
1. **Generointi** — ensimmäinen versio
|
||||||
|
2. **Review** — testaaja löytää ongelmat
|
||||||
|
3. **Korjaus** — koodari saa palautteen ja korjaa
|
||||||
|
4. **Uusi review** — tarkistetaan korjaukset
|
||||||
|
|
||||||
|
Nykyinen järjestelmä tekee max 1 korjauskierroksen. Useampi
|
||||||
|
iteraatio parantaisi laatua mutta kasvattaisi laskenta-aikaa.
|
||||||
|
|
||||||
|
### 5. Erikoistetut system promptit
|
||||||
|
|
||||||
|
Oletuspromptit ovat yleiskäyttöisiä. Projektikohtaiset promptit
|
||||||
|
parantavat laatua merkittävästi:
|
||||||
|
|
||||||
|
```
|
||||||
|
Oletus: "Olet kokenut ohjelmistokehittäjä."
|
||||||
|
|
||||||
|
Parempi: "You are a Python backend developer specializing in FastAPI.
|
||||||
|
Always use Pydantic models for request/response schemas.
|
||||||
|
Always use dependency injection for database sessions.
|
||||||
|
Follow the repository pattern."
|
||||||
|
```
|
||||||
|
|
||||||
|
Agenttikohtaiset promptit voi muokata suoraan UI:ssa.
|
||||||
|
|
||||||
|
### 6. Few-shot esimerkit
|
||||||
|
|
||||||
|
Malli oppii parhaiten esimerkeistä. Sen sijaan, että sanot "kirjoita
|
||||||
|
FastAPI endpoint", näytä miltä haluat tuloksen näyttävän:
|
||||||
|
|
||||||
|
```
|
||||||
|
Write a GET endpoint like this example:
|
||||||
|
|
||||||
|
@app.get("/items")
|
||||||
|
def list_items():
|
||||||
|
db = SessionLocal()
|
||||||
|
return db.query(Item).all()
|
||||||
|
|
||||||
|
Now write a similar endpoint for /users.
|
||||||
|
```
|
||||||
|
|
||||||
|
0.5B-malli jäljittelee rakennetta tehokkaasti — se on parempi kopioimaan
|
||||||
|
kuin keksimään. Nykyinen pyproject.toml-esimerkki promptissa on tätä tekniikkaa.
|
||||||
|
|
||||||
|
### 7. Temperature-säätö tehtävän mukaan
|
||||||
|
|
||||||
|
Nykyinen temperature 0.7 on kompromissi. Eri tehtävät hyötyisivät eri arvoista:
|
||||||
|
|
||||||
|
| Tehtävä | Paras temperature | Miksi |
|
||||||
|
|---------|-------------------|-------|
|
||||||
|
| Tarkka koodi (CRUD, boilerplate) | 0.2-0.4 | Determinismi tärkeää |
|
||||||
|
| Luova koodi (algoritmit, arkkitehtuuri) | 0.6-0.8 | Vaihtelu löytää ratkaisuja |
|
||||||
|
| Vapaa teksti (kommentit, dokumentaatio) | 0.8-1.0 | Luonnollisempi kieli |
|
||||||
|
|
||||||
|
Järjestelmä voisi valita temperaturen automaattisesti tehtävätyypin perusteella.
|
||||||
|
|
||||||
|
### 8. Ensemble — sama prompti usealle mallille
|
||||||
|
|
||||||
|
Lähetetään sama tehtävä kahdelle solmulle ja valitaan parempi vastaus.
|
||||||
|
Nykyinen Proof of Compute -arkkitehtuuri tukee tätä periaatteessa:
|
||||||
|
hub voisi reitittää saman task_id:n kahdelle solmulle ja verrata tuloksia.
|
||||||
|
|
||||||
|
Käytännössä tämä kaksinkertaistaa laskenta-ajan mutta parantaa laatua
|
||||||
|
merkittävästi — virheellinen vastaus harvoin on sama kahdella ajolla
|
||||||
|
koska sampling on stokastinen.
|
||||||
|
|
||||||
|
### 9. Post-processing (nykyinen)
|
||||||
|
|
||||||
|
Mallin raakavastaus siivotaan:
|
||||||
|
1. Kielitunniste poistetaan (`python`, `rust`, ...)
|
||||||
|
2. Sulkeva ` ``` ` poistetaan
|
||||||
|
3. Johdantolauseet poistetaan ("Sure!", "Here is...")
|
||||||
|
4. Selityskommentit poistetaan ("# This is a simple...")
|
||||||
|
5. Stop-sekvenssit katkaisevat generoinnin
|
||||||
|
|
||||||
|
Tämä ei paranna mallin ajattelua mutta poistaa turhan roskan.
|
||||||
|
|
||||||
|
### 10. Mallin hienosäätö (fine-tuning)
|
||||||
|
|
||||||
|
Qwen2.5-Coder on yleiskäyttöinen koodimalli. Jos sitä hienosäätäisi
|
||||||
|
omalla koodiaineistolla (esim. yrityksen koodikanta, tietty framework),
|
||||||
|
se tuottaisi huomattavasti parempaa koodia juuri siihen kontekstiin.
|
||||||
|
|
||||||
|
LoRA-hienosäätö 0.5B-mallille vaatii ~4 GB GPU-muistia ja muutaman
|
||||||
|
tunnin harjoittelua. Tulos on erikoistunut malli joka osaa tuottaa
|
||||||
|
esimerkiksi juuri FastAPI + SQLAlchemy -koodia luotettavasti.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Välimuistiarkkitehtuuri — miksi toinen lataus on nopea
|
||||||
|
|
||||||
|
```
|
||||||
|
Ensimmäinen lataus (hidas):
|
||||||
|
Verkko (HuggingFace CDN) → IndexedDB → RAM → Mallin rakennus
|
||||||
|
~990 MB lataus, ~30-60s
|
||||||
|
|
||||||
|
Toinen lataus samalla sivulatauksella (nopea):
|
||||||
|
RAM-cache → Mallia ei rakenneta uusiksi, vain KV-cache nollataan
|
||||||
|
~0ms
|
||||||
|
|
||||||
|
Refresh jälkeen (keskitaso):
|
||||||
|
IndexedDB → RAM → Mallin rakennus
|
||||||
|
~0 MB lataus, ~2-5s rakennus
|
||||||
|
|
||||||
|
Uusi selain/laite (hidas):
|
||||||
|
Verkko → IndexedDB → RAM → Mallin rakennus
|
||||||
|
Kuten ensimmäinen lataus
|
||||||
|
```
|
||||||
|
|
||||||
|
**KV-cache:** Mallin sisäinen muisti joka tallentaa aiempien tokenien
|
||||||
|
laskenta tulokset. Nollataan (`clear_kv_cache()`) jokaisen promptin
|
||||||
|
välillä jotta edellinen vastaus ei vuoda seuraavaan.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lukuja käytännöstä
|
||||||
|
|
||||||
|
**Yksittäinen funktio** (esim. fibonacci):
|
||||||
|
- Input: ~80 tokenia
|
||||||
|
- Output: ~50-100 tokenia
|
||||||
|
- Aika: ~10-20s (0.5B, selain)
|
||||||
|
- Laatu: Yleensä toimiva, joskus loogisia virheitä
|
||||||
|
|
||||||
|
**3 tiedoston projekti** (esim. FastAPI CRUD):
|
||||||
|
- Manageri: ~30 tok out
|
||||||
|
- Koodari (3x): ~100-150 tok out per tiedosto
|
||||||
|
- Testeri: ~50 tok out
|
||||||
|
- Korjaukset: ~100 tok out (jos tarpeen)
|
||||||
|
- **Yhteensä: ~500-700 tokenia, ~3-5 min**
|
||||||
|
- Laatu: Rakenne oikein, yksittäisiä bugeja
|
||||||
|
|
||||||
|
**Token-kustannus vs. pilvipalvelu:**
|
||||||
|
- Tässä järjestelmässä: 0 euroa (laskenta omalla koneella)
|
||||||
|
- GPT-4 API: ~700 tokenia x $0.03/1K = ~$0.02 per projekti
|
||||||
|
- Claude API: ~700 tokenia x $0.015/1K = ~$0.01 per projekti
|
||||||
|
|
||||||
|
Selaimessa ajettava malli on ilmainen mutta huomattavasti hitaampi
|
||||||
|
ja heikompilaatuinen kuin pilvi-API. Sopii oppimiseen, prototypointiin
|
||||||
|
ja tilanteisiin joissa data ei saa lähteä omalta koneelta.
|
||||||
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 |
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
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
|
||||||
121
network-poc/frontend/public/kipina-node
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
#!/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}"
|
||||||
|
MODEL="${KIPINA_MODEL:-qwen2.5-coder:3b}"
|
||||||
|
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"
|
||||||
|
echo " Malli: $MODEL"
|
||||||
|
|
||||||
|
# Lataa malli (toimii sekä lokaalilla binäärillä että API:n kautta)
|
||||||
|
if ! curl -s "$OLLAMA_URL/api/tags" | grep -q "$MODEL"; then
|
||||||
|
echo " Ladataan $MODEL..."
|
||||||
|
curl -s "$OLLAMA_URL/api/pull" -d "{\"name\":\"$MODEL\"}" > /dev/null
|
||||||
|
fi
|
||||||
|
echo " ✓ Malli $MODEL valmis"
|
||||||
|
|
||||||
|
# Lataa binääri
|
||||||
|
BIN_PATH="./kipina-node-bin"
|
||||||
|
if [ ! -f "$BIN_PATH" ]; then
|
||||||
|
echo " Ladataan $BINARY..."
|
||||||
|
curl -sSL "$BASE_URL/$BINARY" -o "$BIN_PATH"
|
||||||
|
chmod +x "$BIN_PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo " ✓ Yhdistetään laskentaverkkoon..."
|
||||||
|
echo " Ctrl+C pysäyttää"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
HUB_URL="$HUB_URL" OLLAMA_URL="$OLLAMA_URL" OLLAMA_MODEL="$MODEL" 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/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
27
network-poc/frontend/public/templates/fastapi-crud.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "FastAPI CRUD",
|
||||||
|
"description": "REST API with SQLite database",
|
||||||
|
"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 @@
|
|||||||
|
<!-- Agenttigalleria + konfigurointipaneeli -->
|
||||||
|
<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>
|
||||||
|
<!-- + Lisää agentti -->
|
||||||
|
<div id="add-agent-btn" class="agent-avatar" onclick="addCustomAgent()" title="Lisää oma agentti" 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">Lisää</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Agentin konfigurointipaneeli (avautuu klikkaamalla avataria) -->
|
||||||
|
<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="Agentin nimi">
|
||||||
|
<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="Poista agentti">Poista</button>
|
||||||
|
<button class="btn btn-muted" onclick="closeAgentConfig()">Sulje</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Malli -->
|
||||||
|
<div style="margin-bottom:10px">
|
||||||
|
<label style="font-size:12px;color:#8b949e;display:block;margin-bottom:4px">Kielimalli</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 (selain)</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="Agentin perusohje joka lähetetään kielimallille jokaisessa pyynnössä. Hyvän promptin rakenne: 1. Rooli: 'You are an expert...' 2. Säännöt: RULES/CRITICAL RULES listana 3. Esimerkit: EXAMPLE OUTPUT 4. Kiellot: NEVER-lista Vinkki: käytä englantia — malli ymmärtää sen paremmin ja se kuluttaa vähemmän tokeneita.">
|
||||||
|
<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="Kuvaa agentin rooli ja käyttäytyminen..."></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sampling-parametrit -->
|
||||||
|
<div style="margin-bottom:10px">
|
||||||
|
<label style="font-size:12px;color:#8b949e;display:block;margin-bottom:8px">Sampling-parametrit</label>
|
||||||
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
|
||||||
|
<div title="Kontrolloi 'luovuutta'. Matala arvo (0.2-0.4) tuottaa ennustettavaa, toistettavaa koodia — hyvä testaajille ja reviewereille. Keskiarvo (0.6-0.8) on paras koodin generointiin. Korkea arvo (1.0+) lisää vaihtelua mutta myös virheitä. Suositus: • Manageri: 0.5 (tarkat tiedostolistat) • Koodari: 0.7 (toimiva koodi + vaihtelu) • Testaaja: 0.3 (deterministinen arviointi)">
|
||||||
|
<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=tarkka · 0.7=oletus · 1.5=luova</div>
|
||||||
|
</div>
|
||||||
|
<div title="Vastauksen maksimipituus tokeneina (~1 token ≈ 4 merkkiä). Suositus: • Manageri: 256-512 (lyhyet tiedostolistat) • Koodari: 1024-2048 (täydet tiedostot, CRUD-endpointit) • Testaaja: 256-512 (lyhyet arvioinnit) Jos koodi katkeaa kesken, nosta tätä. Jos malli tuottaa turhaa toistoa, laske.">
|
||||||
|
<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">Vastauksen maksimipituus</div>
|
||||||
|
</div>
|
||||||
|
<div title="Montako todennäköisintä tokenia huomioidaan valinnassa. Pieni arvo (1-10) tekee vastauksesta deterministisen. Suuri arvo (50-100) sallii harvinaisempia sanoja. Suositus: • Boilerplate-koodi: 20-30 (tutut patternit) • Yleiskoodi: 40 (hyvä oletus) • Luova teksti: 60-80 Yleensä ei tarvitse muuttaa oletuksesta.">
|
||||||
|
<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=oletus · 100=laaja</div>
|
||||||
|
</div>
|
||||||
|
<div title="Vähentää jo tuotettujen sanojen todennäköisyyttä. Estää mallia toistamasta samaa lausetta. Liian korkea arvo (>1.5) voi rikkoa koodin koska samat avainsanat (return, if, def) ovat tarpeellisia. Suositus: • Koodi: 1.1-1.2 (lievä, sallii toiston) • Teksti: 1.15-1.3 (vahvempi) • Review: 1.0-1.1 (ei rangaistusta, lyhyet vastaukset)">
|
||||||
|
<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=ei · 1.15=oletus · 2.0=vahva</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pipeline-järjestys -->
|
||||||
|
<div>
|
||||||
|
<label style="font-size:12px;color:#8b949e;display:block;margin-bottom:4px">Pipeline-järjestys <span style="color:var(--border)">(vedä järjestääksesi)</span></label>
|
||||||
|
<div id="config-pipeline" style="display:flex;gap:4px;flex-wrap:wrap"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
15
network-poc/frontend/src/components/Editor.astro
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<!-- Monaco Editor paneeli -->
|
||||||
|
<div id="panel-editor" class="panel">
|
||||||
|
<div style="display:flex;height:calc(100vh - 200px);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-y:auto;font-family:'Courier New',monospace;font-size:13px">
|
||||||
|
<div style="padding:10px 12px;color:#8b949e;font-size:11px;text-transform:uppercase;letter-spacing:0.5px;border-bottom:1px solid var(--border)">Tiedostot</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>
|
||||||
67
network-poc/frontend/src/components/Settings.astro
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<!-- 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>
|
||||||
|
|
||||||
|
<!-- 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 -o kipina-node && chmod +x kipina-node && ./kipina-node</code>
|
||||||
|
<button onclick="navigator.clipboard.writeText('curl -sSL https://kipina.studio/kipina-node -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>
|
||||||
1224
network-poc/frontend/src/pages/index.astro
Normal file
200
network-poc/frontend/src/styles/global.css
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
:root {
|
||||||
|
--bg: #0d1117;
|
||||||
|
--panel: #161b22;
|
||||||
|
--text: #c9d1d9;
|
||||||
|
--accent: #58a6ff;
|
||||||
|
--green: #3fb950;
|
||||||
|
--yellow: #d29922;
|
||||||
|
--red: #f85149;
|
||||||
|
--purple: #a371f7;
|
||||||
|
--border: #30363d;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { 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; }
|
||||||
|
|
||||||
|
/* Tabs */
|
||||||
|
.tabs { display: flex; gap: 4px; margin-bottom: 16px; }
|
||||||
|
.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: block; }
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
min-height: 400px; max-height: 70vh; 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); }
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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" }
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "hub"
|
name = "hub"
|
||||||
version = "0.2.0"
|
version = "0.3.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
@@ -15,3 +15,5 @@ uuid = { version = "1.7.0", features = ["v4", "serde"] }
|
|||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
rusqlite = { version = "0.31", features = ["bundled"] }
|
rusqlite = { version = "0.31", features = ["bundled"] }
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
|
base64 = "0.22"
|
||||||
|
reqwest = { version = "0.12", features = ["json"] }
|
||||||
|
|||||||
@@ -26,6 +26,29 @@ impl NodeDb {
|
|||||||
INSERT INTO _schema_version VALUES (2);
|
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);
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
conn.execute_batch("
|
conn.execute_batch("
|
||||||
CREATE TABLE IF NOT EXISTS node_sessions (
|
CREATE TABLE IF NOT EXISTS node_sessions (
|
||||||
@@ -279,6 +302,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(
|
pub fn insert_pair_result(
|
||||||
&self,
|
&self,
|
||||||
node_id: u64,
|
node_id: u64,
|
||||||
|
|||||||
@@ -25,16 +25,24 @@ const ALLOWED_ORIGINS: &[&str] = &[
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Sallitut viestityyypit clientilta
|
// Sallitut viestityyypit clientilta
|
||||||
const ALLOWED_MSG_TYPES: &[&str] = &["auth", "result", "pair_done", "llm_chunk", "llm_done", "download_progress", "user_text", "single_tokenize_done"];
|
const ALLOWED_MSG_TYPES: &[&str] = &["auth", "result", "pair_done", "llm_chunk", "llm_done", "llm_error", "download_progress", "user_text", "single_tokenize_done"];
|
||||||
|
|
||||||
struct AppState {
|
struct AppState {
|
||||||
next_node_id: Mutex<u64>,
|
next_node_id: Mutex<u64>,
|
||||||
nodes_vram: Mutex<HashMap<u64, u32>>,
|
nodes_vram: Mutex<HashMap<u64, u32>>,
|
||||||
|
nodes_tokens: Mutex<HashMap<u64, u32>>, // Gamification: Kipinä Tokens
|
||||||
total_tasks: Mutex<u64>,
|
total_tasks: Mutex<u64>,
|
||||||
stats_tx: broadcast::Sender<String>,
|
stats_tx: broadcast::Sender<String>,
|
||||||
|
node_channels: tokio::sync::RwLock<HashMap<u64, tokio::sync::mpsc::UnboundedSender<String>>>, // Kohdennettu reititys
|
||||||
|
_pending_consensus: tokio::sync::RwLock<HashMap<String, Vec<serde_json::Value>>>, // Proof of Compute -konsensus
|
||||||
|
feature_flags: tokio::sync::RwLock<HashMap<String, bool>>, // Tuntee TODO.md:n ruksit lennosta
|
||||||
ip_connections: Mutex<HashMap<IpAddr, u32>>,
|
ip_connections: Mutex<HashMap<IpAddr, u32>>,
|
||||||
node_ips: Mutex<HashMap<u64, IpAddr>>,
|
node_ips: Mutex<HashMap<u64, IpAddr>>,
|
||||||
node_tasks: Mutex<HashMap<u64, String>>, // node_id → selected_task
|
node_tasks: Mutex<HashMap<u64, String>>, // node_id → selected_task
|
||||||
|
node_types: Mutex<HashMap<u64, String>>, // node_id → "native" | "browser"
|
||||||
|
node_busy: Mutex<std::collections::HashSet<u64>>, // Solmut joilla on aktiivinen tehtävä
|
||||||
|
pending_task_ids: Mutex<std::collections::HashSet<String>>, // Hubin jakamat task_id:t (gamification-validointi)
|
||||||
|
api_rate_limits: Mutex<HashMap<IpAddr, (std::time::Instant, u32)>>, // IP → (ikkuna-alku, pyyntömäärä)
|
||||||
db: db::NodeDb,
|
db: db::NodeDb,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,16 +252,52 @@ async fn main() {
|
|||||||
let state = Arc::new(AppState {
|
let state = Arc::new(AppState {
|
||||||
next_node_id: Mutex::new(1),
|
next_node_id: Mutex::new(1),
|
||||||
nodes_vram: Mutex::new(HashMap::new()),
|
nodes_vram: Mutex::new(HashMap::new()),
|
||||||
|
nodes_tokens: Mutex::new(HashMap::new()),
|
||||||
total_tasks: Mutex::new(0),
|
total_tasks: Mutex::new(0),
|
||||||
stats_tx: stats_tx.clone(),
|
stats_tx: stats_tx.clone(),
|
||||||
|
node_channels: tokio::sync::RwLock::new(HashMap::new()),
|
||||||
|
_pending_consensus: tokio::sync::RwLock::new(HashMap::new()),
|
||||||
|
feature_flags: tokio::sync::RwLock::new(HashMap::new()),
|
||||||
ip_connections: Mutex::new(HashMap::new()),
|
ip_connections: Mutex::new(HashMap::new()),
|
||||||
node_ips: Mutex::new(HashMap::new()),
|
node_ips: Mutex::new(HashMap::new()),
|
||||||
node_tasks: Mutex::new(HashMap::new()),
|
node_tasks: Mutex::new(HashMap::new()),
|
||||||
|
node_types: Mutex::new(HashMap::new()),
|
||||||
|
node_busy: Mutex::new(std::collections::HashSet::new()),
|
||||||
|
pending_task_ids: Mutex::new(std::collections::HashSet::new()),
|
||||||
|
api_rate_limits: Mutex::new(HashMap::new()),
|
||||||
db: db::NodeDb::new(&std::env::var("DATABASE_PATH").unwrap_or_else(|_| "nodes.db".to_string())),
|
db: db::NodeDb::new(&std::env::var("DATABASE_PATH").unwrap_or_else(|_| "nodes.db".to_string())),
|
||||||
});
|
});
|
||||||
|
|
||||||
tracing::info!("Tietokanta alustettu");
|
tracing::info!("Tietokanta alustettu");
|
||||||
|
|
||||||
|
let state_for_watcher = state.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
// Ensimmäinen luku heti, sitten 3s välein
|
||||||
|
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(3));
|
||||||
|
let file_path = std::env::var("FEATURE_FLAGS_FILE").unwrap_or_else(|_| "../TODO.md".to_string());
|
||||||
|
|
||||||
|
loop {
|
||||||
|
interval.tick().await;
|
||||||
|
if let Ok(content) = tokio::fs::read_to_string(&file_path).await {
|
||||||
|
let mut flags = HashMap::new();
|
||||||
|
for line in content.lines() {
|
||||||
|
if line.starts_with("- [ ] **") || line.starts_with("- [x] **") {
|
||||||
|
let is_active = line.starts_with("- [x]");
|
||||||
|
if let Some(start_idx) = line.find("**") {
|
||||||
|
let start = start_idx + 2;
|
||||||
|
if let Some(end_idx) = line[start..].find("**") {
|
||||||
|
let end = end_idx + start;
|
||||||
|
let feature_name = line[start..end].trim_end_matches(':').trim().to_string();
|
||||||
|
flags.insert(feature_name, is_active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*state_for_watcher.feature_flags.write().await = flags;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let state_for_task = state.clone();
|
let state_for_task = state.clone();
|
||||||
|
|
||||||
// Ajastin, joka jakaa satunnaisia tekoälytehtäviä eri pituuksilla
|
// Ajastin, joka jakaa satunnaisia tekoälytehtäviä eri pituuksilla
|
||||||
@@ -286,15 +330,6 @@ async fn main() {
|
|||||||
let idx = (rng_state as usize) % pairs.len();
|
let idx = (rng_state as usize) % pairs.len();
|
||||||
let (en, fi) = pairs[idx];
|
let (en, fi) = pairs[idx];
|
||||||
|
|
||||||
// Tokenisointiparit
|
|
||||||
let pair_msg = serde_json::json!({
|
|
||||||
"type": "pair_task",
|
|
||||||
"en": en,
|
|
||||||
"fi": fi,
|
|
||||||
});
|
|
||||||
let _ = state_for_task.stats_tx.send(pair_msg.to_string());
|
|
||||||
|
|
||||||
// LLM-promptit
|
|
||||||
let llm_prompts = vec![
|
let llm_prompts = vec![
|
||||||
"Tell me a short joke.",
|
"Tell me a short joke.",
|
||||||
"What is WebGPU in one sentence?",
|
"What is WebGPU in one sentence?",
|
||||||
@@ -304,33 +339,39 @@ async fn main() {
|
|||||||
];
|
];
|
||||||
let llm_idx = (rng_state as usize / 7) % llm_prompts.len();
|
let llm_idx = (rng_state as usize / 7) % llm_prompts.len();
|
||||||
|
|
||||||
// SmolLM-prompt
|
// Smart Routing: Lähetetään vain niille, jotka valittuna ja idle
|
||||||
let smollm_msg = serde_json::json!({
|
let mut sends = Vec::new();
|
||||||
"type": "llm_prompt",
|
{
|
||||||
"prompt": llm_prompts[llm_idx],
|
let channels = state_for_task.node_channels.read().await;
|
||||||
"model": "smollm-135m",
|
let tasks = state_for_task.node_tasks.lock().unwrap();
|
||||||
});
|
let mut busy = state_for_task.node_busy.lock().unwrap();
|
||||||
let _ = state_for_task.stats_tx.send(smollm_msg.to_string());
|
|
||||||
|
|
||||||
// Qwen-prompt (sama prompti, eri malli-tagi)
|
for (node_id, task) in tasks.iter() {
|
||||||
let qwen_msg = serde_json::json!({
|
if !busy.contains(node_id) {
|
||||||
"type": "llm_prompt",
|
// Vapaa node -> lähetetään oikea tehtävä
|
||||||
"prompt": llm_prompts[llm_idx],
|
let msg = match task.as_str() {
|
||||||
"model": "qwen-05b",
|
"tokenize" => Some(serde_json::json!({ "type": "pair_task", "en": en, "fi": fi })),
|
||||||
});
|
"smollm-135m" => Some(serde_json::json!({ "type": "llm_prompt", "prompt": llm_prompts[llm_idx], "model": "smollm-135m" })),
|
||||||
let _ = state_for_task.stats_tx.send(qwen_msg.to_string());
|
"qwen-05b" => Some(serde_json::json!({ "type": "llm_prompt", "prompt": llm_prompts[llm_idx], "model": "qwen-05b" })),
|
||||||
|
"phi3-mini" => Some(serde_json::json!({ "type": "llm_prompt", "prompt": llm_prompts[llm_idx], "model": "phi3-mini" })),
|
||||||
|
_ => None, // Coder ja viewer ei saa auto-tehtäviä
|
||||||
|
};
|
||||||
|
|
||||||
// Phi-3 prompt
|
if let Some(payload) = msg {
|
||||||
let phi3_msg = serde_json::json!({
|
if let Some(ch) = channels.get(node_id) {
|
||||||
"type": "llm_prompt",
|
sends.push((ch.clone(), payload.to_string()));
|
||||||
"prompt": llm_prompts[llm_idx],
|
busy.insert(*node_id);
|
||||||
"model": "phi3-mini",
|
}
|
||||||
});
|
}
|
||||||
let _ = state_for_task.stats_tx.send(phi3_msg.to_string());
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Coder ei saa automaattisia tehtäviä — vain käyttäjän user_text
|
for (ch, msg_str) in sends {
|
||||||
|
let _ = ch.send(msg_str);
|
||||||
|
}
|
||||||
|
|
||||||
tracing::debug!("Tehtävät lähetetty: pair + smollm + qwen + phi3");
|
// tracing::debug!("Tehtävät lähetetty reititetysti idle-nodeille");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -340,9 +381,14 @@ async fn main() {
|
|||||||
.route("/api/pairs", get(api_pairs))
|
.route("/api/pairs", get(api_pairs))
|
||||||
.route("/api/stats", get(api_stats))
|
.route("/api/stats", get(api_stats))
|
||||||
.route("/api/v1/chat/completions", axum::routing::post(api_chat_completions))
|
.route("/api/v1/chat/completions", axum::routing::post(api_chat_completions))
|
||||||
|
.route("/api/v1/model", axum::routing::post(api_change_model))
|
||||||
|
.route("/api/v1/hardware", get(api_hardware))
|
||||||
|
.route("/api/v1/ollama/tags", get(api_ollama_tags))
|
||||||
|
.route("/api/v1/agents", get(api_get_agents).post(api_upsert_agent))
|
||||||
|
.route("/api/v1/agents/:id", axum::routing::delete(api_delete_agent))
|
||||||
.route("/admin", get(admin_page))
|
.route("/admin", get(admin_page))
|
||||||
.nest_service("/", {
|
.nest_service("/", {
|
||||||
let static_dir = std::env::var("STATIC_DIR").unwrap_or_else(|_| "../static".to_string());
|
let static_dir = std::env::var("STATIC_DIR").unwrap_or_else(|_| "../frontend/dist".to_string());
|
||||||
ServeDir::new(&static_dir).fallback(ServeFile::new(format!("{}/index.html", static_dir)))
|
ServeDir::new(&static_dir).fallback(ServeFile::new(format!("{}/index.html", static_dir)))
|
||||||
})
|
})
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
@@ -376,39 +422,35 @@ async fn api_stats(
|
|||||||
) -> axum::response::Response {
|
) -> axum::response::Response {
|
||||||
if !check_admin_auth(&headers) { return admin_unauthorized(); }
|
if !check_admin_auth(&headers) { return admin_unauthorized(); }
|
||||||
let mut stats = state.db.get_stats();
|
let mut stats = state.db.get_stats();
|
||||||
stats.as_object_mut().unwrap().insert("version".to_string(), serde_json::json!(env!("CARGO_PKG_VERSION")));
|
if let Some(obj) = stats.as_object_mut() {
|
||||||
|
obj.insert("version".to_string(), serde_json::json!(env!("CARGO_PKG_VERSION")));
|
||||||
|
}
|
||||||
axum::Json(stats).into_response()
|
axum::Json(stats).into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_admin_auth(headers: &axum::http::HeaderMap) -> bool {
|
fn check_admin_auth(headers: &axum::http::HeaderMap) -> bool {
|
||||||
let password = std::env::var("ADMIN_PASSWORD").unwrap_or_else(|_| "kipina".to_string());
|
let password = match std::env::var("ADMIN_PASSWORD") {
|
||||||
|
Ok(p) if !p.is_empty() => p,
|
||||||
|
_ => {
|
||||||
|
tracing::warn!("ADMIN_PASSWORD ei ole asetettu — käytetään oletusta 'kipina' (ÄLÄ käytä tuotannossa!)");
|
||||||
|
"kipina".to_string()
|
||||||
|
}
|
||||||
|
};
|
||||||
if let Some(auth) = headers.get("authorization").and_then(|v| v.to_str().ok()) {
|
if let Some(auth) = headers.get("authorization").and_then(|v| v.to_str().ok()) {
|
||||||
if auth.starts_with("Basic ") {
|
if auth.starts_with("Basic ") {
|
||||||
if let Ok(decoded) = String::from_utf8(
|
use base64::Engine;
|
||||||
base64_decode(auth.trim_start_matches("Basic ").trim())
|
if let Ok(decoded_bytes) = base64::engine::general_purpose::STANDARD
|
||||||
) {
|
.decode(auth.trim_start_matches("Basic ").trim())
|
||||||
// Tarkistetaan "user:password" — käyttäjänimi ei väliä
|
{
|
||||||
|
if let Ok(decoded) = String::from_utf8(decoded_bytes) {
|
||||||
if let Some(pass) = decoded.split(':').nth(1) {
|
if let Some(pass) = decoded.split(':').nth(1) {
|
||||||
return pass == password;
|
return pass == password;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn base64_decode(input: &str) -> Vec<u8> {
|
|
||||||
// Yksinkertainen base64-dekooderi
|
|
||||||
const TABLE: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
||||||
let mut out = Vec::new();
|
|
||||||
let bytes: Vec<u8> = input.bytes().filter(|&b| b != b'=').collect();
|
|
||||||
for chunk in bytes.chunks(4) {
|
|
||||||
let vals: Vec<u8> = chunk.iter().filter_map(|&b| TABLE.iter().position(|&t| t == b).map(|p| p as u8)).collect();
|
|
||||||
if vals.len() >= 2 { out.push((vals[0] << 2) | (vals[1] >> 4)); }
|
|
||||||
if vals.len() >= 3 { out.push((vals[1] << 4) | (vals[2] >> 2)); }
|
|
||||||
if vals.len() >= 4 { out.push((vals[2] << 6) | vals[3]); }
|
|
||||||
}
|
}
|
||||||
out
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn admin_unauthorized() -> axum::response::Response {
|
fn admin_unauthorized() -> axum::response::Response {
|
||||||
@@ -419,6 +461,34 @@ fn admin_unauthorized() -> axum::response::Response {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Agents API ──
|
||||||
|
|
||||||
|
async fn api_get_agents(
|
||||||
|
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
|
||||||
|
) -> axum::response::Response {
|
||||||
|
axum::Json(state.db.get_agents()).into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn api_upsert_agent(
|
||||||
|
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
|
||||||
|
axum::Json(payload): axum::Json<serde_json::Value>,
|
||||||
|
) -> axum::response::Response {
|
||||||
|
match state.db.upsert_agent(&payload) {
|
||||||
|
Ok(()) => axum::Json(serde_json::json!({"ok": true})).into_response(),
|
||||||
|
Err(e) => (axum::http::StatusCode::BAD_REQUEST, e).into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn api_delete_agent(
|
||||||
|
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
|
||||||
|
axum::extract::Path(id): axum::extract::Path<String>,
|
||||||
|
) -> axum::response::Response {
|
||||||
|
match state.db.delete_agent(&id) {
|
||||||
|
Ok(()) => axum::Json(serde_json::json!({"ok": true})).into_response(),
|
||||||
|
Err(e) => (axum::http::StatusCode::BAD_REQUEST, e).into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn admin_page(headers: axum::http::HeaderMap) -> axum::response::Response {
|
async fn admin_page(headers: axum::http::HeaderMap) -> axum::response::Response {
|
||||||
if !check_admin_auth(&headers) { return admin_unauthorized(); }
|
if !check_admin_auth(&headers) { return admin_unauthorized(); }
|
||||||
axum::response::Html(ADMIN_HTML).into_response()
|
axum::response::Html(ADMIN_HTML).into_response()
|
||||||
@@ -432,7 +502,12 @@ async fn ws_handler(
|
|||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
// Origin-tarkistus — estää cross-site WebSocket hijackingin
|
// Origin-tarkistus — estää cross-site WebSocket hijackingin
|
||||||
if let Some(origin) = headers.get("origin").and_then(|v| v.to_str().ok()) {
|
if let Some(origin) = headers.get("origin").and_then(|v| v.to_str().ok()) {
|
||||||
if !ALLOWED_ORIGINS.iter().any(|&allowed| origin == allowed) {
|
let is_allowed = ALLOWED_ORIGINS.iter().any(|&allowed| origin == allowed)
|
||||||
|
|| origin.starts_with("http://192.168.")
|
||||||
|
|| origin.starts_with("http://10.")
|
||||||
|
|| origin.starts_with("http://172."); // LAN-avaruudet
|
||||||
|
|
||||||
|
if !is_allowed {
|
||||||
tracing::warn!("Estetty yhteys väärällä originilla: {}", origin);
|
tracing::warn!("Estetty yhteys väärällä originilla: {}", origin);
|
||||||
return (
|
return (
|
||||||
axum::http::StatusCode::FORBIDDEN,
|
axum::http::StatusCode::FORBIDDEN,
|
||||||
@@ -448,18 +523,21 @@ async fn ws_handler(
|
|||||||
.and_then(|s| s.trim().parse::<IpAddr>().ok())
|
.and_then(|s| s.trim().parse::<IpAddr>().ok())
|
||||||
.unwrap_or_else(|| addr.ip());
|
.unwrap_or_else(|| addr.ip());
|
||||||
|
|
||||||
// Max 2 yhteyttä per IP (dashboard-UI + selainsolmu)
|
// Max yhteyttä per IP (ei rajoiteta localhost/127.0.0.1)
|
||||||
{
|
{
|
||||||
|
let is_local = ip.is_loopback();
|
||||||
|
if !is_local {
|
||||||
let conns = state.ip_connections.lock().unwrap();
|
let conns = state.ip_connections.lock().unwrap();
|
||||||
let count = conns.get(&ip).copied().unwrap_or(0);
|
let count = conns.get(&ip).copied().unwrap_or(0);
|
||||||
if count >= 4 {
|
if count >= 20 {
|
||||||
tracing::warn!("IP {} ylitti yhteysrajan ({}/4) — estetty", ip, count);
|
tracing::warn!("IP {} ylitti yhteysrajan ({}/20) — estetty", ip, count);
|
||||||
return (
|
return (
|
||||||
axum::http::StatusCode::TOO_MANY_REQUESTS,
|
axum::http::StatusCode::TOO_MANY_REQUESTS,
|
||||||
"Max 4 yhteyttä per IP",
|
"Max 20 yhteyttä per IP",
|
||||||
).into_response();
|
).into_response();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ws.max_message_size(MAX_MESSAGE_SIZE)
|
ws.max_message_size(MAX_MESSAGE_SIZE)
|
||||||
.on_upgrade(move |socket| handle_socket(socket, state, ip))
|
.on_upgrade(move |socket| handle_socket(socket, state, ip))
|
||||||
@@ -555,23 +633,36 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
|
|||||||
|
|
||||||
tracing::info!("Solmu {} yhdistyi osoitteesta {}", node_id, ip);
|
tracing::info!("Solmu {} yhdistyi osoitteesta {}", node_id, ip);
|
||||||
|
|
||||||
|
let (node_tx, mut node_rx) = tokio::sync::mpsc::unbounded_channel::<String>();
|
||||||
|
|
||||||
|
// Tallennetaan node channel reititystä varten
|
||||||
|
{
|
||||||
|
state.node_channels.write().await.insert(node_id, node_tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Yksinkertaistettu broadcast tx vastaanotto
|
||||||
let mut rx = state.stats_tx.subscribe();
|
let mut rx = state.stats_tx.subscribe();
|
||||||
|
|
||||||
let sender_task = tokio::spawn(async move {
|
let sender_task = tokio::spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
match rx.recv().await {
|
tokio::select! {
|
||||||
|
result = rx.recv() => {
|
||||||
|
match result {
|
||||||
Ok(msg) => {
|
Ok(msg) => {
|
||||||
if sender.send(Message::Text(msg)).await.is_err() {
|
if sender.send(Message::Text(msg)).await.is_err() { break; }
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
Err(broadcast::error::RecvError::Lagged(n)) => {
|
||||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
|
tracing::debug!("Broadcast lagged {} viestiä — ohitetaan", n);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => break, // Kanava suljettu
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some(direct_msg) = node_rx.recv() => {
|
||||||
|
if sender.send(Message::Text(direct_msg)).await.is_err() { break; }
|
||||||
|
}
|
||||||
|
else => break,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -592,7 +683,8 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
|
|||||||
let json = match validate_message(&text) {
|
let json = match validate_message(&text) {
|
||||||
Ok(j) => j,
|
Ok(j) => j,
|
||||||
Err(reason) => {
|
Err(reason) => {
|
||||||
tracing::warn!("Solmu {} ({}) lähetti virheellisen viestin: {} — {:?}", node_id, ip, reason, &text[..text.len().min(100)]);
|
let preview: String = text.chars().take(100).collect();
|
||||||
|
tracing::warn!("Solmu {} ({}) lähetti virheellisen viestin: {} — {:?}", node_id, ip, reason, preview);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -604,6 +696,18 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
|
|||||||
let allocated = json.get("allocated_gb").and_then(|v| v.as_u64()).unwrap_or(4) as u32;
|
let allocated = json.get("allocated_gb").and_then(|v| v.as_u64()).unwrap_or(4) as u32;
|
||||||
let node_type = json.get("node_type").and_then(|v| v.as_str()).unwrap_or("browser");
|
let node_type = json.get("node_type").and_then(|v| v.as_str()).unwrap_or("browser");
|
||||||
|
|
||||||
|
// API-avain vaaditaan natiivisolmuilta (ei selaimilta)
|
||||||
|
if node_type == "native" {
|
||||||
|
let required_key = std::env::var("NODE_API_KEY").unwrap_or_default();
|
||||||
|
if !required_key.is_empty() {
|
||||||
|
let provided_key = json.get("api_key").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
|
if provided_key != required_key {
|
||||||
|
tracing::warn!("Solmu {} ({}) hylätty: virheellinen API-avain", node_id, ip);
|
||||||
|
break; // Suljetaan WebSocket
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut map = state.nodes_vram.lock().unwrap();
|
let mut map = state.nodes_vram.lock().unwrap();
|
||||||
map.insert(node_id, allocated);
|
map.insert(node_id, allocated);
|
||||||
@@ -625,6 +729,7 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
|
|||||||
state.db.insert_session(node_id, &ip.to_string(), node_type, &json);
|
state.db.insert_session(node_id, &ip.to_string(), node_type, &json);
|
||||||
}
|
}
|
||||||
state.node_tasks.lock().unwrap().insert(node_id, selected_task);
|
state.node_tasks.lock().unwrap().insert(node_id, selected_task);
|
||||||
|
state.node_types.lock().unwrap().insert(node_id, node_type.to_string());
|
||||||
|
|
||||||
if node_type == "native" {
|
if node_type == "native" {
|
||||||
let sys = json.get("system");
|
let sys = json.get("system");
|
||||||
@@ -683,6 +788,7 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
|
|||||||
}
|
}
|
||||||
broadcast_stats(&state).await;
|
broadcast_stats(&state).await;
|
||||||
} else if msg_type == "pair_done" {
|
} else if msg_type == "pair_done" {
|
||||||
|
state.node_busy.lock().unwrap().remove(&node_id);
|
||||||
{
|
{
|
||||||
let mut json = json; // Siirretään omistajuus muokkausta varten
|
let mut json = json; // Siirretään omistajuus muokkausta varten
|
||||||
if let Some(obj) = json.as_object_mut() {
|
if let Some(obj) = json.as_object_mut() {
|
||||||
@@ -722,10 +828,32 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
|
|||||||
}
|
}
|
||||||
let _ = state.stats_tx.send(json.to_string());
|
let _ = state.stats_tx.send(json.to_string());
|
||||||
|
|
||||||
|
let active_incentives = state.feature_flags.read().await.get("Insentiivit").copied().unwrap_or(false);
|
||||||
|
let ui_sync = state.feature_flags.read().await.get("Pelimerkkien UI-synkkaus").copied().unwrap_or(false);
|
||||||
|
let mut current_balance = 0;
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut task_count = state.total_tasks.lock().unwrap();
|
let mut task_count = state.total_tasks.lock().unwrap();
|
||||||
*task_count += 1;
|
*task_count += 1;
|
||||||
|
|
||||||
|
if active_incentives {
|
||||||
|
let mut tokens = state.nodes_tokens.lock().unwrap();
|
||||||
|
let balance = tokens.entry(node_id).or_insert(0);
|
||||||
|
*balance += 5; // Palkkio: 5 Kipinä-merkkiä
|
||||||
|
current_balance = *balance;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if active_incentives && ui_sync {
|
||||||
|
if let Some(tx) = state.node_channels.read().await.get(&node_id) {
|
||||||
|
let msg = serde_json::json!({
|
||||||
|
"type": "token_balance",
|
||||||
|
"balance": current_balance
|
||||||
|
});
|
||||||
|
let _ = tx.send(msg.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
broadcast_stats(&state).await;
|
broadcast_stats(&state).await;
|
||||||
}
|
}
|
||||||
} else if msg_type == "single_tokenize_done" {
|
} else if msg_type == "single_tokenize_done" {
|
||||||
@@ -745,6 +873,13 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
|
|||||||
let _ = state.stats_tx.send(json.to_string());
|
let _ = state.stats_tx.send(json.to_string());
|
||||||
}
|
}
|
||||||
} else if msg_type == "llm_done" {
|
} else if msg_type == "llm_done" {
|
||||||
|
// Vapautetaan solmu ja tarkistetaan task_id:n aitous
|
||||||
|
state.node_busy.lock().unwrap().remove(&node_id);
|
||||||
|
let valid_task = if let Some(tid) = json.get("task_id").and_then(|v| v.as_str()) {
|
||||||
|
state.pending_task_ids.lock().unwrap().remove(tid)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
{
|
{
|
||||||
let mut json = json;
|
let mut json = json;
|
||||||
if let Some(obj) = json.as_object_mut() {
|
if let Some(obj) = json.as_object_mut() {
|
||||||
@@ -766,18 +901,53 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
|
|||||||
}
|
}
|
||||||
let _ = state.stats_tx.send(json.to_string());
|
let _ = state.stats_tx.send(json.to_string());
|
||||||
|
|
||||||
|
let active_incentives = state.feature_flags.read().await.get("Insentiivit").copied().unwrap_or(false);
|
||||||
|
let ui_sync = state.feature_flags.read().await.get("Pelimerkkien UI-synkkaus").copied().unwrap_or(false);
|
||||||
|
let mut current_balance = 0;
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut task_count = state.total_tasks.lock().unwrap();
|
let mut task_count = state.total_tasks.lock().unwrap();
|
||||||
*task_count += 1;
|
*task_count += 1;
|
||||||
|
|
||||||
|
if active_incentives && valid_task {
|
||||||
|
let mut tokens = state.nodes_tokens.lock().unwrap();
|
||||||
|
let balance = tokens.entry(node_id).or_insert(0);
|
||||||
|
*balance += 20; // Palkkio: 20 Kipinä-merkkiä
|
||||||
|
current_balance = *balance;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if active_incentives && ui_sync {
|
||||||
|
if let Some(tx) = state.node_channels.read().await.get(&node_id) {
|
||||||
|
let msg = serde_json::json!({
|
||||||
|
"type": "token_balance",
|
||||||
|
"balance": current_balance
|
||||||
|
});
|
||||||
|
let _ = tx.send(msg.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
broadcast_stats(&state).await;
|
broadcast_stats(&state).await;
|
||||||
}
|
}
|
||||||
|
} else if msg_type == "llm_error" {
|
||||||
|
state.node_busy.lock().unwrap().remove(&node_id);
|
||||||
|
if let Some(tid) = json.get("task_id").and_then(|v| v.as_str()) {
|
||||||
|
state.pending_task_ids.lock().unwrap().remove(tid);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut json = json;
|
||||||
|
if let Some(obj) = json.as_object_mut() {
|
||||||
|
obj.insert("node_id".to_string(), serde_json::json!(node_id));
|
||||||
|
}
|
||||||
|
let _ = state.stats_tx.send(json.to_string());
|
||||||
|
}
|
||||||
} else if msg_type == "user_text" {
|
} else if msg_type == "user_text" {
|
||||||
// Käyttäjän lähettämä teksti — broadcastataan pair_taskina ja llm_promptina
|
// Käyttäjän lähettämä teksti — broadcastataan pair_taskina ja llm_promptina
|
||||||
let text = json.get("text").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
let text = json.get("text").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||||
let task_type = json.get("task_type").and_then(|v| v.as_str()).unwrap_or("tokenize");
|
let task_type = json.get("task_type").and_then(|v| v.as_str()).unwrap_or("tokenize");
|
||||||
if !text.is_empty() {
|
if !text.is_empty() {
|
||||||
tracing::info!("Solmu {} lähetti oman tekstin ({}): \"{}\"", node_id, task_type, &text[..text.len().min(80)]);
|
let preview: String = text.chars().take(80).collect();
|
||||||
|
tracing::info!("Solmu {} lähetti oman tekstin ({}): \"{}\"", node_id, task_type, preview);
|
||||||
match task_type {
|
match task_type {
|
||||||
"tokenize" => {
|
"tokenize" => {
|
||||||
let msg = serde_json::json!({
|
let msg = serde_json::json!({
|
||||||
@@ -787,12 +957,11 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
|
|||||||
let _ = state.stats_tx.send(msg.to_string());
|
let _ = state.stats_tx.send(msg.to_string());
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// LLM-prompti
|
// LLM-prompti: lähetetään VAIN valitulle mallille, ei kaikille (välttää turhaa ruuhkaa ja busy-tiloja)
|
||||||
for model in &["smollm-135m", "qwen-05b", "phi3-mini", "qwen-coder"] {
|
|
||||||
let prompt = serde_json::json!({
|
let prompt = serde_json::json!({
|
||||||
"type": "llm_prompt",
|
"type": "llm_prompt",
|
||||||
"prompt": text,
|
"prompt": text,
|
||||||
"model": model,
|
"model": task_type,
|
||||||
});
|
});
|
||||||
let _ = state.stats_tx.send(prompt.to_string());
|
let _ = state.stats_tx.send(prompt.to_string());
|
||||||
}
|
}
|
||||||
@@ -800,26 +969,26 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Yhteys katkesi — merkitään session päättyneeksi ja siivotaan
|
// Yhteys katkesi — merkitään session päättyneeksi ja siivotaan atomisesti
|
||||||
state.db.close_session(node_id);
|
state.db.close_session(node_id);
|
||||||
state.node_tasks.lock().unwrap().remove(&node_id);
|
|
||||||
{
|
{
|
||||||
|
// Lukitaan kaikki kerralla, jotta solmu ei ole osittain siivottu
|
||||||
|
let mut tasks = state.node_tasks.lock().unwrap();
|
||||||
let mut conns = state.ip_connections.lock().unwrap();
|
let mut conns = state.ip_connections.lock().unwrap();
|
||||||
|
let mut ips = state.node_ips.lock().unwrap();
|
||||||
|
let mut vram = state.nodes_vram.lock().unwrap();
|
||||||
|
let mut busy = state.node_busy.lock().unwrap();
|
||||||
|
tasks.remove(&node_id);
|
||||||
|
busy.remove(&node_id);
|
||||||
if let Some(count) = conns.get_mut(&ip) {
|
if let Some(count) = conns.get_mut(&ip) {
|
||||||
*count = count.saturating_sub(1);
|
*count = count.saturating_sub(1);
|
||||||
if *count == 0 {
|
if *count == 0 { conns.remove(&ip); }
|
||||||
conns.remove(&ip);
|
|
||||||
}
|
}
|
||||||
|
ips.remove(&node_id);
|
||||||
|
vram.remove(&node_id);
|
||||||
}
|
}
|
||||||
}
|
state.node_types.lock().unwrap().remove(&node_id);
|
||||||
{
|
|
||||||
state.node_ips.lock().unwrap().remove(&node_id);
|
|
||||||
}
|
|
||||||
{
|
|
||||||
state.nodes_vram.lock().unwrap().remove(&node_id);
|
|
||||||
}
|
|
||||||
tracing::info!("Solmu {} ({}) poistui verkosta.", node_id, ip);
|
tracing::info!("Solmu {} ({}) poistui verkosta.", node_id, ip);
|
||||||
broadcast_stats(&state).await;
|
broadcast_stats(&state).await;
|
||||||
sender_task.abort();
|
sender_task.abort();
|
||||||
@@ -829,6 +998,8 @@ struct ChatCompletionRequest {
|
|||||||
model: String,
|
model: String,
|
||||||
prompt: String,
|
prompt: String,
|
||||||
task_id: String,
|
task_id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
max_tokens: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
@@ -838,42 +1009,210 @@ struct ChatCompletionResponse {
|
|||||||
tokens_generated: u64,
|
tokens_generated: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn api_ollama_tags() -> axum::response::Response {
|
||||||
|
let ollama_url = std::env::var("OLLAMA_URL").unwrap_or_else(|_| "http://ollama:11434".to_string());
|
||||||
|
match reqwest::get(format!("{}/api/tags", ollama_url)).await {
|
||||||
|
Ok(resp) => {
|
||||||
|
if let Ok(body) = resp.json::<serde_json::Value>().await {
|
||||||
|
axum::Json(body).into_response()
|
||||||
|
} else {
|
||||||
|
axum::Json(serde_json::json!({ "models": [] })).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => axum::Json(serde_json::json!({ "models": [] })).into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn api_hardware(
|
||||||
|
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
|
||||||
|
) -> axum::response::Response {
|
||||||
|
// Etsitään natiivisolmun GPU-tiedot sessiosta
|
||||||
|
let sessions = state.db.get_sessions(50);
|
||||||
|
let native = sessions.iter().find(|s| {
|
||||||
|
s.get("node_type").and_then(|v| v.as_str()) == Some("native")
|
||||||
|
});
|
||||||
|
|
||||||
|
let (mut vram_mb, mut gpu_name, ram_mb) = if let Some(s) = native {
|
||||||
|
let gpus = s.get("gpus").and_then(|v| v.as_array());
|
||||||
|
let gpu = gpus.and_then(|g| g.first());
|
||||||
|
let vram = gpu.and_then(|g| g.get("vram_total_mb")).and_then(|v| v.as_u64()).unwrap_or(0);
|
||||||
|
let name = gpu.and_then(|g| g.get("name")).and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||||
|
let ram = s.get("system").and_then(|v| v.get("ram_total_mb")).and_then(|v| v.as_u64()).unwrap_or(0);
|
||||||
|
(vram, name, ram)
|
||||||
|
} else {
|
||||||
|
(0, String::new(), 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fallback: kysytään Ollamalta onko malleja ladattu (= Ollama on käynnissä)
|
||||||
|
if vram_mb == 0 {
|
||||||
|
let ollama_url = std::env::var("OLLAMA_URL").unwrap_or_else(|_| "http://ollama:11434".to_string());
|
||||||
|
if let Ok(resp) = reqwest::get(format!("{}/api/tags", ollama_url)).await {
|
||||||
|
if let Ok(body) = resp.json::<serde_json::Value>().await {
|
||||||
|
let models = body["models"].as_array().map(|a| a.len()).unwrap_or(0);
|
||||||
|
if models > 0 {
|
||||||
|
gpu_name = "Ollama (GPU/CPU)".to_string();
|
||||||
|
// Natiivisolmun RAM fallbackina
|
||||||
|
vram_mb = if ram_mb > 0 { ram_mb } else { 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if gpu_name.is_empty() { gpu_name = "ei natiivisolmua".to_string(); }
|
||||||
|
|
||||||
|
axum::Json(serde_json::json!({
|
||||||
|
"gpu_name": gpu_name,
|
||||||
|
"vram_mb": vram_mb,
|
||||||
|
"ram_mb": ram_mb,
|
||||||
|
})).into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn api_change_model(
|
||||||
|
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
|
||||||
|
axum::Json(payload): axum::Json<serde_json::Value>,
|
||||||
|
) -> axum::response::Response {
|
||||||
|
let model = payload.get("model").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
|
if model.is_empty() {
|
||||||
|
return (axum::http::StatusCode::BAD_REQUEST, "model puuttuu").into_response();
|
||||||
|
}
|
||||||
|
tracing::info!("Mallin vaihto: {}", model);
|
||||||
|
let msg = serde_json::json!({ "type": "change_model", "model": model });
|
||||||
|
let _ = state.stats_tx.send(msg.to_string());
|
||||||
|
axum::Json(serde_json::json!({ "status": "ok", "model": model })).into_response()
|
||||||
|
}
|
||||||
|
|
||||||
async fn api_chat_completions(
|
async fn api_chat_completions(
|
||||||
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
|
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
|
||||||
|
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||||
axum::Json(payload): axum::Json<ChatCompletionRequest>,
|
axum::Json(payload): axum::Json<ChatCompletionRequest>,
|
||||||
) -> axum::response::Response {
|
) -> axum::response::Response {
|
||||||
let msg = serde_json::json!({
|
// Rate limiting: max 10 pyyntöä per IP per minuutti
|
||||||
|
{
|
||||||
|
let mut limits = state.api_rate_limits.lock().unwrap();
|
||||||
|
let now = std::time::Instant::now();
|
||||||
|
let entry = limits.entry(addr.ip()).or_insert((now, 0));
|
||||||
|
if now.duration_since(entry.0).as_secs() >= 60 {
|
||||||
|
*entry = (now, 1); // Uusi ikkuna
|
||||||
|
} else {
|
||||||
|
entry.1 += 1;
|
||||||
|
if entry.1 > 30 {
|
||||||
|
return (axum::http::StatusCode::TOO_MANY_REQUESTS, "Liian monta pyyntöä — yritä minuutin kuluttua").into_response();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Etsitään vapaa solmu — priorisoidaan natiivisolmut (GPU) selaimen edelle
|
||||||
|
let (target_node, _total_matching) = {
|
||||||
|
let tasks = state.node_tasks.lock().unwrap();
|
||||||
|
let _busy = state.node_busy.lock().unwrap();
|
||||||
|
let node_types = state.node_types.lock().unwrap();
|
||||||
|
let matching: Vec<u64> = tasks.iter().filter(|(_, task)| {
|
||||||
|
// Eksakti match tai qwen-perheen yhteensopivuus (selain: qwen-coder-05b, natiivi: qwen2.5-coder:7b)
|
||||||
|
let req_model = payload.model.to_lowercase();
|
||||||
|
let node_task = task.to_lowercase();
|
||||||
|
if req_model.starts_with("qwen") {
|
||||||
|
node_task.starts_with("qwen")
|
||||||
|
} else if req_model.starts_with("phi") {
|
||||||
|
node_task.starts_with("phi")
|
||||||
|
} else {
|
||||||
|
**task == payload.model
|
||||||
|
}
|
||||||
|
}).map(|(k, _)| *k).collect();
|
||||||
|
// Etsitään mikä tahansa matchaava solmu (natiivi priorisoidaan)
|
||||||
|
let native = matching.iter().find(|id| {
|
||||||
|
node_types.get(id).map(|t| t == "native").unwrap_or(false)
|
||||||
|
}).copied();
|
||||||
|
let any = native.or_else(|| matching.first().copied());
|
||||||
|
(any, matching.len())
|
||||||
|
};
|
||||||
|
|
||||||
|
let task_id = payload.task_id.clone();
|
||||||
|
|
||||||
|
let target_node_id = match target_node {
|
||||||
|
Some(id) => id,
|
||||||
|
None => {
|
||||||
|
return (axum::http::StatusCode::SERVICE_UNAVAILABLE, "Ei solmua tälle mallille (käynnistä malli selaimessa)").into_response();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reititystila UI:lle
|
||||||
|
{
|
||||||
|
let routing_msg = serde_json::json!({
|
||||||
|
"type": "task_routed",
|
||||||
|
"task_id": task_id,
|
||||||
|
"node_id": target_node_id,
|
||||||
|
"status": "routed",
|
||||||
|
"message": format!("Reititetty solmulle #{}", target_node_id),
|
||||||
|
});
|
||||||
|
let _ = state.stats_tx.send(routing_msg.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merkitään solmu varatuksi ja task_id jaetuksi
|
||||||
|
state.node_busy.lock().unwrap().insert(target_node_id);
|
||||||
|
state.pending_task_ids.lock().unwrap().insert(payload.task_id.clone());
|
||||||
|
|
||||||
|
let mut msg = serde_json::json!({
|
||||||
"type": "llm_prompt",
|
"type": "llm_prompt",
|
||||||
"prompt": payload.prompt,
|
"prompt": payload.prompt,
|
||||||
"model": payload.model,
|
"model": payload.model,
|
||||||
"task_id": payload.task_id,
|
"task_id": payload.task_id,
|
||||||
});
|
});
|
||||||
|
if let Some(mt) = payload.max_tokens {
|
||||||
|
msg.as_object_mut().unwrap().insert("max_tokens".to_string(), serde_json::json!(mt));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Odotuskanava valmiiksi (solmu palauttaa tuloksen stats_tx kautta)
|
||||||
let mut rx = state.stats_tx.subscribe();
|
let mut rx = state.stats_tx.subscribe();
|
||||||
let _ = state.stats_tx.send(msg.to_string());
|
|
||||||
|
|
||||||
let timeout = tokio::time::timeout(std::time::Duration::from_secs(120), async move {
|
// Kohdennettu reititys: lähetetään AI-tehtävä suoraan VAIN valitulle solmulle
|
||||||
while let Ok(msg_str) = rx.recv().await {
|
{
|
||||||
|
let channels = state.node_channels.read().await;
|
||||||
|
if let Some(tx) = channels.get(&target_node_id) {
|
||||||
|
let _ = tx.send(msg.to_string());
|
||||||
|
tracing::info!("Reititettiin API-pyyntö solmulle {} (Malli: {})", target_node_id, payload.model);
|
||||||
|
} else {
|
||||||
|
return (axum::http::StatusCode::SERVICE_UNAVAILABLE, "Verkkovirhe: solmun yhteys katkesi reitityksen aikana").into_response();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let timeout = tokio::time::timeout(std::time::Duration::from_secs(600), async move {
|
||||||
|
loop {
|
||||||
|
let msg_str = match rx.recv().await {
|
||||||
|
Ok(msg) => msg,
|
||||||
|
Err(broadcast::error::RecvError::Lagged(n)) => {
|
||||||
|
tracing::debug!("API-kanava lagged {} viestiä", n);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Err(_) => return Ok(None), // Kanava suljettu
|
||||||
|
};
|
||||||
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&msg_str) {
|
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&msg_str) {
|
||||||
if v["type"].as_str() == Some("llm_done") {
|
if v["type"].as_str() == Some("llm_done") {
|
||||||
if let Some(tid) = v["task_id"].as_str() {
|
if let Some(tid) = v["task_id"].as_str() {
|
||||||
if tid == payload.task_id {
|
if tid == payload.task_id {
|
||||||
return Some(ChatCompletionResponse {
|
return Ok(Some(ChatCompletionResponse {
|
||||||
response: v["response"].as_str().unwrap_or("").to_string(),
|
response: v["response"].as_str().unwrap_or("").to_string(),
|
||||||
model: v["model"].as_str().unwrap_or("").to_string(),
|
model: v["model"].as_str().unwrap_or("").to_string(),
|
||||||
tokens_generated: v["tokens_generated"].as_u64().unwrap_or(0),
|
tokens_generated: v["tokens_generated"].as_u64().unwrap_or(0),
|
||||||
});
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if v["type"].as_str() == Some("llm_error") {
|
||||||
|
if let Some(tid) = v["task_id"].as_str() {
|
||||||
|
if tid == payload.task_id {
|
||||||
|
return Err(v["error"].as_str().unwrap_or("Määrittelemätön virhe solmussa").to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
#[allow(unreachable_code)]
|
||||||
|
Ok(None)
|
||||||
}).await;
|
}).await;
|
||||||
|
|
||||||
match timeout {
|
match timeout {
|
||||||
Ok(Some(res)) => axum::Json(res).into_response(),
|
Ok(Ok(Some(res))) => axum::Json(res).into_response(),
|
||||||
Ok(None) => (axum::http::StatusCode::INTERNAL_SERVER_ERROR, "Verkkovirhe: yhteys katkesi").into_response(),
|
Ok(Ok(None)) => (axum::http::StatusCode::INTERNAL_SERVER_ERROR, "Verkkovirhe: yhteys katkesi").into_response(),
|
||||||
Err(_) => (axum::http::StatusCode::GATEWAY_TIMEOUT, "Aikakatkaisu: yksikään solmu ei vastannut 120s sisällä").into_response(),
|
Ok(Err(err)) => (axum::http::StatusCode::CONFLICT, err).into_response(),
|
||||||
|
Err(_) => (axum::http::StatusCode::GATEWAY_TIMEOUT, "Aikakatkaisu: solmu ei saanut tehtävää ajoissa valmiiksi").into_response(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
59
network-poc/install.sh
Executable file
@@ -0,0 +1,59 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Kipinä Agentic Studio — asennusskripti (Debian/Ubuntu)
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "=== Kipinä Agentic Studio — Asennus ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Tarkistetaan käyttöjärjestelmä
|
||||||
|
if [ ! -f /etc/debian_version ]; then
|
||||||
|
echo "⚠ Tämä skripti on suunniteltu Debian/Ubuntu-järjestelmille."
|
||||||
|
echo " Muilla jakeluilla voit asentaa riippuvuudet manuaalisesti."
|
||||||
|
read -p " Jatketaanko? (k/e) " -n 1 -r; echo
|
||||||
|
[[ $REPLY =~ ^[Kk]$ ]] || exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[1/6] Päivitetään pakettilistaus..."
|
||||||
|
sudo apt-get update -qq
|
||||||
|
|
||||||
|
echo "[2/6] Asennetaan peruspaketteja..."
|
||||||
|
sudo apt-get install -y -qq curl git build-essential pkg-config libssl-dev
|
||||||
|
|
||||||
|
# Rust
|
||||||
|
if command -v rustc &>/dev/null; then
|
||||||
|
echo "[3/6] Rust löytyi: $(rustc --version)"
|
||||||
|
else
|
||||||
|
echo "[3/6] Asennetaan Rust..."
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||||
|
source "$HOME/.cargo/env"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Node.js (Astro-frontend vaatii)
|
||||||
|
if command -v node &>/dev/null; then
|
||||||
|
echo "[4/6] Node.js löytyi: $(node --version)"
|
||||||
|
else
|
||||||
|
echo "[4/6] Asennetaan Node.js 22..."
|
||||||
|
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
|
||||||
|
sudo apt-get install -y -qq nodejs
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ollama
|
||||||
|
if command -v ollama &>/dev/null; then
|
||||||
|
echo "[5/6] Ollama löytyi"
|
||||||
|
else
|
||||||
|
echo "[5/6] Asennetaan Ollama..."
|
||||||
|
curl -fsSL https://ollama.ai/install.sh | sh
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Malli
|
||||||
|
echo "[6/6] Ladataan kielimalli (qwen2.5-coder:3b)..."
|
||||||
|
ollama pull qwen2.5-coder:3b
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Asennus valmis! ==="
|
||||||
|
echo ""
|
||||||
|
echo "Käynnistä:"
|
||||||
|
echo " cd $(pwd)"
|
||||||
|
echo " ./network-poc/local.sh"
|
||||||
|
echo ""
|
||||||
|
echo "Avaa selaimessa: http://localhost:3000"
|
||||||
37
network-poc/local.sh
Executable file
@@ -0,0 +1,37 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
echo "=== Kipinä Studio Local Development ==="
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
echo "[1/3] Rakennetaan frontend..."
|
||||||
|
cd "$SCRIPT_DIR/frontend"
|
||||||
|
[ -d node_modules ] || npm install --silent
|
||||||
|
npm run build --silent 2>&1 | tail -1
|
||||||
|
|
||||||
|
# Hub
|
||||||
|
echo "[2/3] Käynnistetään hub..."
|
||||||
|
cd "$SCRIPT_DIR/hub"
|
||||||
|
cargo run &
|
||||||
|
HUB_PID=$!
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
# Native-node (jos Ollama on käynnissä)
|
||||||
|
if curl -s http://localhost:11434/api/tags >/dev/null 2>&1; then
|
||||||
|
echo "[3/3] Ollama löytyi — käynnistetään native-node..."
|
||||||
|
cd "$SCRIPT_DIR/native-node"
|
||||||
|
HUB_URL=ws://localhost:3000/ws cargo run --no-default-features &
|
||||||
|
NODE_PID=$!
|
||||||
|
echo " Native-node PID: $NODE_PID"
|
||||||
|
else
|
||||||
|
echo "[3/3] Ollama ei käynnissä — käytetään selaimen Wasm-laskentaa"
|
||||||
|
echo " Nopeampi: ollama serve & ollama pull qwen2.5-coder:7b && ./local.sh"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== http://localhost:3000 ==="
|
||||||
|
echo " Ctrl+C pysäyttää"
|
||||||
|
|
||||||
|
# Odotetaan hub-prosessia
|
||||||
|
wait $HUB_PID
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "native-node"
|
name = "native-node"
|
||||||
version = "0.1.0"
|
version = "0.2.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["gpu-detect"]
|
||||||
|
gpu-detect = ["nvml-wrapper", "wgpu"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tokio = { version = "1.36", features = ["full"] }
|
tokio = { version = "1.36", features = ["full"] }
|
||||||
tokio-tungstenite = { version = "0.21", features = ["native-tls"] }
|
tokio-tungstenite = { version = "0.21", features = ["native-tls"] }
|
||||||
@@ -10,12 +14,8 @@ futures-util = "0.3"
|
|||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
sysinfo = "0.30"
|
sysinfo = "0.30"
|
||||||
nvml-wrapper = "0.10"
|
nvml-wrapper = { version = "0.10", optional = true }
|
||||||
wgpu = "24"
|
wgpu = { version = "24", optional = true }
|
||||||
candle-core = { version = "0.8", features = ["cuda"] }
|
reqwest = { version = "0.12", features = ["json"] }
|
||||||
candle-nn = "0.8"
|
|
||||||
candle-transformers = "0.8"
|
|
||||||
hf-hub = "0.4"
|
|
||||||
tokenizers = "0.19"
|
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
|||||||
@@ -1,178 +1,168 @@
|
|||||||
use candle_core::{Device, Tensor, DType};
|
|
||||||
use candle_nn::VarBuilder;
|
|
||||||
use candle_transformers::models::qwen2::{Config as QwenConfig, ModelForCausalLM as QwenModel};
|
|
||||||
use hf_hub::{api::sync::Api, Repo, RepoType};
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
|
||||||
pub struct LlmEngine {
|
pub struct LlmEngine {
|
||||||
tokenizer: tokenizers::Tokenizer,
|
ollama_url: String,
|
||||||
model_path: PathBuf,
|
model: RefCell<String>,
|
||||||
device: Device,
|
client: reqwest::Client,
|
||||||
dtype: DType,
|
|
||||||
config: QwenConfig,
|
|
||||||
eos_token: u32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LlmEngine {
|
impl LlmEngine {
|
||||||
pub fn load() -> Result<Self, String> {
|
pub async fn load() -> Result<Self, String> {
|
||||||
let device = Device::cuda_if_available(0).map_err(|e| format!("Device: {}", e))?;
|
let model = std::env::var("OLLAMA_MODEL").unwrap_or_else(|_| "qwen2.5-coder:3b".to_string());
|
||||||
let device_name = if device.is_cuda() { "CUDA" } else { "CPU" };
|
|
||||||
tracing::info!("LLM device: {}", device_name);
|
|
||||||
|
|
||||||
let dtype = if device.is_cuda() { DType::F16 } else { DType::F32 };
|
let client = reqwest::Client::builder()
|
||||||
|
.timeout(std::time::Duration::from_secs(600))
|
||||||
|
.connect_timeout(std::time::Duration::from_secs(3))
|
||||||
|
.build()
|
||||||
|
.map_err(|e| format!("HTTP client: {}", e))?;
|
||||||
|
|
||||||
tracing::info!("Ladataan Qwen2.5-0.5B-Instruct...");
|
// Jos OLLAMA_URL on asetettu, käytetään sitä suoraan
|
||||||
let api = Api::new().map_err(|e| format!("HF API: {}", e))?;
|
let ollama_url = if let Ok(url) = std::env::var("OLLAMA_URL") {
|
||||||
let repo = api.repo(Repo::with_revision(
|
tracing::info!("Ollama backend (env): {}", url);
|
||||||
"Qwen/Qwen2.5-0.5B-Instruct".to_string(),
|
url
|
||||||
RepoType::Model,
|
} else {
|
||||||
"main".to_string(),
|
// Haistellaan Ollamaa tunnetuista osoitteista
|
||||||
));
|
let candidates = [
|
||||||
|
"http://localhost:11434",
|
||||||
let tokenizer_path = repo.get("tokenizer.json").map_err(|e| format!("Tokenizer lataus: {}", e))?;
|
"http://127.0.0.1:11434",
|
||||||
let model_path = repo.get("model.safetensors").map_err(|e| format!("Malli lataus: {}", e))?;
|
"http://ollama:11434",
|
||||||
|
"http://host.docker.internal:11434",
|
||||||
tracing::info!("Ladataan tokenizer: {:?}", tokenizer_path);
|
];
|
||||||
let tokenizer = tokenizers::Tokenizer::from_file(&tokenizer_path)
|
let mut found = None;
|
||||||
.map_err(|e| format!("Tokenizer: {}", e))?;
|
for url in &candidates {
|
||||||
|
let probe = reqwest::Client::builder()
|
||||||
let config = QwenConfig {
|
.connect_timeout(std::time::Duration::from_secs(2))
|
||||||
vocab_size: 151936,
|
.build().unwrap_or(client.clone());
|
||||||
hidden_size: 896,
|
if let Ok(resp) = probe.get(format!("{}/api/version", url)).send().await {
|
||||||
intermediate_size: 4864,
|
if resp.status().is_success() {
|
||||||
num_hidden_layers: 24,
|
tracing::info!("Ollama löytyi osoitteesta: {}", url);
|
||||||
num_attention_heads: 14,
|
found = Some(url.to_string());
|
||||||
num_key_value_heads: 2,
|
break;
|
||||||
max_position_embeddings: 32768,
|
}
|
||||||
sliding_window: 32768,
|
}
|
||||||
max_window_layers: 21,
|
}
|
||||||
tie_word_embeddings: true,
|
found.unwrap_or_else(|| {
|
||||||
rope_theta: 1000000.0,
|
tracing::warn!("Ollamaa ei löytynyt — käytetään oletusta http://localhost:11434");
|
||||||
rms_norm_eps: 1e-6,
|
"http://localhost:11434".to_string()
|
||||||
use_sliding_window: false,
|
|
||||||
hidden_act: candle_nn::Activation::Silu,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Testi-lataus varmistaa, että painot toimivat
|
|
||||||
let start = Instant::now();
|
|
||||||
let vb = unsafe {
|
|
||||||
VarBuilder::from_mmaped_safetensors(&[model_path.clone()], dtype, &device)
|
|
||||||
.map_err(|e| format!("VarBuilder: {}", e))?
|
|
||||||
};
|
|
||||||
let _model = QwenModel::new(&config, vb).map_err(|e| format!("Malli: {}", e))?;
|
|
||||||
tracing::info!("Malli ladattu ({:.1}s) — {}", start.elapsed().as_secs_f64(), device_name);
|
|
||||||
|
|
||||||
Ok(LlmEngine {
|
|
||||||
tokenizer,
|
|
||||||
model_path,
|
|
||||||
device,
|
|
||||||
dtype,
|
|
||||||
config,
|
|
||||||
eos_token: 151645,
|
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
/// Luo tuore malliinstanssi (nollaa KV-cachen)
|
|
||||||
fn fresh_model(&self) -> Result<QwenModel, String> {
|
|
||||||
let vb = unsafe {
|
|
||||||
VarBuilder::from_mmaped_safetensors(&[self.model_path.clone()], self.dtype, &self.device)
|
|
||||||
.map_err(|e| format!("VarBuilder: {}", e))?
|
|
||||||
};
|
};
|
||||||
QwenModel::new(&self.config, vb).map_err(|e| format!("Malli: {}", e))
|
|
||||||
|
tracing::info!("Ollama backend: {} | malli: {}", ollama_url, model);
|
||||||
|
Ok(LlmEngine { ollama_url, model: RefCell::new(model), client })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate(&mut self, prompt: &str, max_tokens: usize) -> Result<GenerateResult, String> {
|
pub fn model_name(&self) -> String {
|
||||||
let formatted = format!("<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n", prompt);
|
self.model.borrow().clone()
|
||||||
|
}
|
||||||
|
|
||||||
let encoding = self.tokenizer.encode(formatted.as_str(), true)
|
pub fn set_model(&self, new_model: String) {
|
||||||
.map_err(|e| format!("Encode: {}", e))?;
|
*self.model.borrow_mut() = new_model;
|
||||||
let input_ids: Vec<u32> = encoding.get_ids().to_vec();
|
}
|
||||||
let input_len = input_ids.len();
|
|
||||||
|
|
||||||
// Tuore malli joka promptille (nollaa KV-cachen)
|
/// Varmistaa että malli on ladattu Ollamaan (ollama pull)
|
||||||
let mut model = self.fresh_model()?;
|
pub async fn ensure_model(&self) -> Result<(), String> {
|
||||||
|
let model = self.model.borrow().clone();
|
||||||
|
tracing::info!("Tarkistetaan malli {}...", model);
|
||||||
|
let resp = self.client.post(format!("{}/api/pull", self.ollama_url))
|
||||||
|
.json(&serde_json::json!({ "name": model, "stream": false }))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Ollama pull: {}", e))?;
|
||||||
|
|
||||||
|
if resp.status().is_success() {
|
||||||
|
tracing::info!("Malli {} valmis", model);
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(format!("Ollama pull epäonnistui: {}", resp.status()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn generate(&self, prompt: &str, max_tokens: usize) -> Result<GenerateResult, String> {
|
||||||
|
// System prompt tulee agentin konfiguraatiosta (frontend lähettää sen osana promptia).
|
||||||
|
// Tässä ei yliajeta sitä — Ollama saa vain prompt-kentän.
|
||||||
|
let model = self.model.borrow().clone();
|
||||||
|
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
|
let resp = self.client.post(format!("{}/api/generate", self.ollama_url))
|
||||||
// Prefill
|
.json(&serde_json::json!({
|
||||||
let input = Tensor::new(input_ids.as_slice(), &self.device)
|
"model": model,
|
||||||
.and_then(|t| t.unsqueeze(0))
|
"prompt": prompt,
|
||||||
.map_err(|e| format!("Tensor: {}", e))?;
|
"stream": false,
|
||||||
|
"options": {
|
||||||
let logits = model.forward(&input, 0)
|
"num_predict": max_tokens,
|
||||||
.map_err(|e| format!("Forward prefill: {}", e))?;
|
"temperature": 0.7,
|
||||||
|
"top_k": 40,
|
||||||
let logits = logits.squeeze(0).map_err(|e| format!("Squeeze: {}", e))?;
|
"repeat_penalty": 1.15,
|
||||||
let logits = if logits.dims().len() == 2 {
|
"stop": ["<|im_end|>", "\n###", "\nExplanation", "\nNote:", "\nPlease note", "\nThis is", "\n```\n\n", "\n// Example", "\n# Example"]
|
||||||
logits.get(logits.dim(0).unwrap() - 1).map_err(|e| format!("Get: {}", e))?
|
|
||||||
} else {
|
|
||||||
logits
|
|
||||||
};
|
|
||||||
let mut next_token = logits.argmax(0)
|
|
||||||
.map_err(|e| format!("Argmax: {}", e))?
|
|
||||||
.to_vec0::<u32>()
|
|
||||||
.map_err(|e| format!("to_vec0: {}", e))?;
|
|
||||||
|
|
||||||
let mut generated_text = String::new();
|
|
||||||
let mut tokens_generated: usize = 0;
|
|
||||||
let mut all_tokens: Vec<u32> = Vec::new();
|
|
||||||
|
|
||||||
if next_token != self.eos_token {
|
|
||||||
if let Ok(text) = self.tokenizer.decode(&[next_token], true) {
|
|
||||||
generated_text.push_str(&text);
|
|
||||||
}
|
}
|
||||||
all_tokens.push(next_token);
|
}))
|
||||||
tokens_generated += 1;
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Ollama generate: {}", e))?;
|
||||||
|
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
return Err(format!("Ollama HTTP {}", resp.status()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Autoregressive
|
let body: serde_json::Value = resp.json().await
|
||||||
let mut pos = input_len;
|
.map_err(|e| format!("Ollama JSON: {}", e))?;
|
||||||
for _ in 1..max_tokens {
|
|
||||||
if next_token == self.eos_token { break; }
|
|
||||||
|
|
||||||
let input = Tensor::new(&[next_token], &self.device)
|
let text = body["response"].as_str().unwrap_or("").to_string();
|
||||||
.and_then(|t| t.unsqueeze(0))
|
let _total_duration_ns = body["total_duration"].as_u64().unwrap_or(0);
|
||||||
.map_err(|e| format!("Tensor: {}", e))?;
|
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 logits = model.forward(&input, pos)
|
let duration_ms = start.elapsed().as_millis() as f64;
|
||||||
.map_err(|e| format!("Forward pos {}: {}", pos, e))?;
|
let tokens_per_sec = if eval_duration_ns > 0 {
|
||||||
|
eval_count as f64 / (eval_duration_ns as f64 / 1_000_000_000.0)
|
||||||
let logits = logits.squeeze(0).map_err(|e| format!("Squeeze: {}", e))?;
|
|
||||||
let logits = if logits.dims().len() == 2 {
|
|
||||||
logits.get(logits.dim(0).unwrap() - 1).map_err(|e| format!("Get: {}", e))?
|
|
||||||
} else {
|
|
||||||
logits
|
|
||||||
};
|
|
||||||
next_token = logits.argmax(0)
|
|
||||||
.map_err(|e| format!("Argmax: {}", e))?
|
|
||||||
.to_vec0::<u32>()
|
|
||||||
.map_err(|e| format!("to_vec0: {}", e))?;
|
|
||||||
pos += 1;
|
|
||||||
|
|
||||||
if next_token == self.eos_token { break; }
|
|
||||||
|
|
||||||
if let Ok(text) = self.tokenizer.decode(&[next_token], true) {
|
|
||||||
generated_text.push_str(&text);
|
|
||||||
}
|
|
||||||
all_tokens.push(next_token);
|
|
||||||
tokens_generated += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
let gen_time = start.elapsed();
|
|
||||||
let tokens_per_sec = if gen_time.as_secs_f64() > 0.0 {
|
|
||||||
tokens_generated as f64 / gen_time.as_secs_f64()
|
|
||||||
} else { 0.0 };
|
} else { 0.0 };
|
||||||
|
|
||||||
Ok(GenerateResult {
|
Ok(GenerateResult {
|
||||||
text: generated_text,
|
text: strip_code_fences(&text),
|
||||||
tokens_generated,
|
tokens_generated: eval_count,
|
||||||
duration_ms: gen_time.as_millis() as f64,
|
duration_ms,
|
||||||
tokens_per_sec,
|
tokens_per_sec,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Siivoa markdown-koodiblokki-merkit ja selitystekstit
|
||||||
|
fn strip_code_fences(text: &str) -> String {
|
||||||
|
// Poistetaan kaikki ```-rivit ja kielitunnisteet (```python, ```rust jne.)
|
||||||
|
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
|
||||||
|
if trimmed.starts_with("```") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}).collect();
|
||||||
|
let mut result = filtered.join("\n").trim().to_string();
|
||||||
|
|
||||||
|
// Poista selitysteksti lopusta (kaikki rivin "\nPlease note" jälkeen jne.)
|
||||||
|
let lower = result.to_lowercase();
|
||||||
|
for stop in &["\nplease note", "\nthis is a basic", "\nthis code", "\nnote that", "\nremember to", "\nyou can", "\nto run"] {
|
||||||
|
if let Some(pos) = lower.find(stop) {
|
||||||
|
result = result[..pos].trim_end().to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Poista johdantolauseet alusta
|
||||||
|
let lower = result.to_lowercase();
|
||||||
|
for prefix in &["sure!", "here is", "here's", "certainly!", "below is"] {
|
||||||
|
if lower.starts_with(prefix) {
|
||||||
|
if let Some(nl) = result.find('\n') {
|
||||||
|
result = result[nl + 1..].to_string();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.trim().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
pub struct GenerateResult {
|
pub struct GenerateResult {
|
||||||
pub text: String,
|
pub text: String,
|
||||||
pub tokens_generated: usize,
|
pub tokens_generated: usize,
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ impl GpuInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "gpu-detect")]
|
||||||
/// Tunnistaa kaikki GPU:t wgpu:lla (NVIDIA/AMD/Apple/Intel)
|
/// Tunnistaa kaikki GPU:t wgpu:lla (NVIDIA/AMD/Apple/Intel)
|
||||||
fn collect_gpus_wgpu() -> Vec<GpuInfo> {
|
fn collect_gpus_wgpu() -> Vec<GpuInfo> {
|
||||||
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
|
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
|
||||||
@@ -84,6 +85,7 @@ fn collect_gpus_wgpu() -> Vec<GpuInfo> {
|
|||||||
gpus
|
gpus
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "gpu-detect")]
|
||||||
/// Täydentää NVIDIA-GPU:iden tiedot NVML:llä (VRAM, lämpötila, kuormitus)
|
/// Täydentää NVIDIA-GPU:iden tiedot NVML:llä (VRAM, lämpötila, kuormitus)
|
||||||
fn enrich_nvidia_gpus(gpus: &mut [GpuInfo]) {
|
fn enrich_nvidia_gpus(gpus: &mut [GpuInfo]) {
|
||||||
let Ok(nvml) = nvml_wrapper::Nvml::init() else { return };
|
let Ok(nvml) = nvml_wrapper::Nvml::init() else { return };
|
||||||
@@ -109,6 +111,7 @@ fn enrich_nvidia_gpus(gpus: &mut [GpuInfo]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "gpu-detect")]
|
||||||
/// AMD GPU-tiedot Linuxin sysfs:stä (/sys/class/drm/)
|
/// AMD GPU-tiedot Linuxin sysfs:stä (/sys/class/drm/)
|
||||||
fn enrich_amd_gpus(gpus: &mut [GpuInfo]) {
|
fn enrich_amd_gpus(gpus: &mut [GpuInfo]) {
|
||||||
let Ok(entries) = std::fs::read_dir("/sys/class/drm") else { return };
|
let Ok(entries) = std::fs::read_dir("/sys/class/drm") else { return };
|
||||||
@@ -150,10 +153,12 @@ fn enrich_amd_gpus(gpus: &mut [GpuInfo]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "gpu-detect")]
|
||||||
fn read_sysfs_u64(path: &std::path::Path) -> Option<u64> {
|
fn read_sysfs_u64(path: &std::path::Path) -> Option<u64> {
|
||||||
std::fs::read_to_string(path).ok()?.trim().parse().ok()
|
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> {
|
fn find_hwmon_temp(device_path: &std::path::Path) -> Option<u64> {
|
||||||
let hwmon_dir = device_path.join("hwmon");
|
let hwmon_dir = device_path.join("hwmon");
|
||||||
let entries = std::fs::read_dir(&hwmon_dir).ok()?;
|
let entries = std::fs::read_dir(&hwmon_dir).ok()?;
|
||||||
@@ -166,8 +171,8 @@ fn find_hwmon_temp(device_path: &std::path::Path) -> Option<u64> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "gpu-detect")]
|
||||||
/// Apple GPU-tiedot — wgpu/Metal antaa nimen, tarkempaa dataa ei saa ilman IOKit:ia
|
/// 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]) {
|
fn enrich_apple_gpus(gpus: &mut [GpuInfo]) {
|
||||||
// Apple Silicon -koneiden unified memory: koko RAM on GPU:n käytettävissä
|
// Apple Silicon -koneiden unified memory: koko RAM on GPU:n käytettävissä
|
||||||
// Arvioidaan system RAM:sta
|
// Arvioidaan system RAM:sta
|
||||||
@@ -187,13 +192,18 @@ fn enrich_apple_gpus(gpus: &mut [GpuInfo]) {
|
|||||||
|
|
||||||
/// Kerää kaikki GPU:t ja täydentää valmistajakohtaiset tiedot
|
/// Kerää kaikki GPU:t ja täydentää valmistajakohtaiset tiedot
|
||||||
fn collect_all_gpus() -> Vec<GpuInfo> {
|
fn collect_all_gpus() -> Vec<GpuInfo> {
|
||||||
|
#[cfg(feature = "gpu-detect")]
|
||||||
|
{
|
||||||
let mut gpus = collect_gpus_wgpu();
|
let mut gpus = collect_gpus_wgpu();
|
||||||
|
|
||||||
enrich_nvidia_gpus(&mut gpus);
|
enrich_nvidia_gpus(&mut gpus);
|
||||||
enrich_amd_gpus(&mut gpus);
|
enrich_amd_gpus(&mut gpus);
|
||||||
enrich_apple_gpus(&mut gpus);
|
enrich_apple_gpus(&mut gpus);
|
||||||
|
return gpus;
|
||||||
gpus
|
}
|
||||||
|
#[cfg(not(feature = "gpu-detect"))]
|
||||||
|
{
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Kerää järjestelmätiedot (CPU, RAM, OS)
|
/// Kerää järjestelmätiedot (CPU, RAM, OS)
|
||||||
@@ -222,14 +232,21 @@ fn build_auth_message(allocated_gb: u32) -> String {
|
|||||||
v
|
v
|
||||||
}).collect();
|
}).collect();
|
||||||
|
|
||||||
|
let api_key = std::env::var("NODE_API_KEY").unwrap_or_default();
|
||||||
|
|
||||||
let mut msg = json!({
|
let mut msg = json!({
|
||||||
"type": "auth",
|
"type": "auth",
|
||||||
"status": "agent_ready",
|
"status": "agent_ready",
|
||||||
"node_type": "native",
|
"node_type": "native",
|
||||||
"allocated_gb": allocated_gb,
|
"allocated_gb": allocated_gb,
|
||||||
|
"selected_task": "qwen2.5-coder:7b",
|
||||||
"system": sys,
|
"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() {
|
if !gpu_json.is_empty() {
|
||||||
msg.as_object_mut().unwrap().insert("gpus".to_string(), json!(gpu_json));
|
msg.as_object_mut().unwrap().insert("gpus".to_string(), json!(gpu_json));
|
||||||
}
|
}
|
||||||
@@ -268,6 +285,9 @@ async fn main() {
|
|||||||
|
|
||||||
let gpus = collect_all_gpus();
|
let gpus = collect_all_gpus();
|
||||||
if gpus.is_empty() {
|
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");
|
tracing::info!("GPU:ta ei havaittu — toimitaan CPU-moodissa");
|
||||||
} else {
|
} else {
|
||||||
for (i, gpu) in gpus.iter().enumerate() {
|
for (i, gpu) in gpus.iter().enumerate() {
|
||||||
@@ -284,15 +304,19 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ladataan LLM-malli
|
// Ollama-backend
|
||||||
tracing::info!("Ladataan LLM-mallia...");
|
tracing::info!("Alustetaan Ollama-yhteyttä...");
|
||||||
let mut llm = match inference::LlmEngine::load() {
|
let llm = match inference::LlmEngine::load().await {
|
||||||
Ok(engine) => {
|
Ok(engine) => {
|
||||||
tracing::info!("LLM valmis inferenssiin!");
|
// Varmistetaan malli (ollama pull) — odotetaan kunnes valmis
|
||||||
|
match engine.ensure_model().await {
|
||||||
|
Ok(()) => tracing::info!("Ollama valmis inferenssiin!"),
|
||||||
|
Err(e) => tracing::warn!("Mallin lataus: {} — yritetään silti", e),
|
||||||
|
}
|
||||||
Some(engine)
|
Some(engine)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!("LLM-lataus epäonnistui: {} — toimitaan ilman inferenssiä", e);
|
tracing::warn!("Ollama-alustus epäonnistui: {} — toimitaan ilman inferenssiä", e);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -310,38 +334,46 @@ async fn main() {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut busy = false;
|
|
||||||
|
|
||||||
while let Some(Ok(msg)) = read.next().await {
|
while let Some(Ok(msg)) = read.next().await {
|
||||||
if let Message::Text(text) = msg {
|
if let Message::Text(text) = msg {
|
||||||
// LLM-promptit
|
// LLM-promptit
|
||||||
if text.contains("llm_prompt") && !busy {
|
if text.contains("llm_prompt") {
|
||||||
if let Ok(task) = serde_json::from_str::<serde_json::Value>(&text) {
|
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 prompt = task.get("prompt").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
if !prompt.is_empty() {
|
let task_id = task.get("task_id").and_then(|v| v.as_str()).unwrap_or("?");
|
||||||
if let Some(ref mut engine) = llm {
|
let msg_model = task.get("model").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
busy = true;
|
|
||||||
tracing::info!("Generoidaan: \"{}\"", prompt);
|
|
||||||
|
|
||||||
match engine.generate(prompt, 64) {
|
if !prompt.is_empty() && (msg_model.starts_with("qwen-coder") || msg_model.starts_with("qwen2.5-coder")) {
|
||||||
|
|
||||||
|
if let Some(ref engine) = llm {
|
||||||
|
let max_tokens = task.get("max_tokens").and_then(|v| v.as_u64()).unwrap_or(1024) as usize;
|
||||||
|
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 model_name = engine.model_name();
|
||||||
|
match engine.generate(prompt, max_tokens).await {
|
||||||
Ok(result) => {
|
Ok(result) => {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"Tulos: {} tokenia | {:.0}ms | {:.1} tok/s | \"{}\"",
|
"✓ {} | {} tok | {:.0}ms | {:.1} tok/s",
|
||||||
|
model_name,
|
||||||
result.tokens_generated,
|
result.tokens_generated,
|
||||||
result.duration_ms,
|
result.duration_ms,
|
||||||
result.tokens_per_sec,
|
result.tokens_per_sec,
|
||||||
&result.text[..result.text.len().min(80)]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Lähetetään vain lyhyt prompti-esikatselu (ei koko kontekstia)
|
||||||
|
let prompt_short: String = prompt.lines().last().unwrap_or("").chars().take(100).collect();
|
||||||
let done = json!({
|
let done = json!({
|
||||||
"type": "llm_done",
|
"type": "llm_done",
|
||||||
"prompt": prompt,
|
"prompt": prompt_short,
|
||||||
"model": "Qwen2.5-0.5B-Instruct (native/GPU)",
|
"model": format!("{} (Ollama)", model_name),
|
||||||
"response": result.text,
|
"response": result.text,
|
||||||
"tokens_generated": result.tokens_generated,
|
"tokens_generated": result.tokens_generated,
|
||||||
"duration_ms": result.duration_ms,
|
"duration_ms": result.duration_ms,
|
||||||
"tokens_per_sec": (result.tokens_per_sec * 10.0).round() / 10.0,
|
"tokens_per_sec": (result.tokens_per_sec * 10.0).round() / 10.0,
|
||||||
"load_time_ms": 0,
|
"load_time_ms": 0,
|
||||||
|
"task_id": task_id,
|
||||||
});
|
});
|
||||||
let _ = write.send(Message::Text(done.to_string())).await;
|
let _ = write.send(Message::Text(done.to_string())).await;
|
||||||
}
|
}
|
||||||
@@ -349,12 +381,25 @@ async fn main() {
|
|||||||
tracing::error!("Inferenssivirhe: {}", e);
|
tracing::error!("Inferenssivirhe: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
busy = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Ohitetaan pair_task, stats jne.
|
// Mallin vaihto lennossa
|
||||||
|
if text.contains("change_model") {
|
||||||
|
if let Ok(task) = serde_json::from_str::<serde_json::Value>(&text) {
|
||||||
|
if let Some(new_model) = task.get("model").and_then(|v| v.as_str()) {
|
||||||
|
if let Some(ref engine) = llm {
|
||||||
|
tracing::info!("Vaihdetaan malli: {}", new_model);
|
||||||
|
engine.set_model(new_model.to_string());
|
||||||
|
match engine.ensure_model().await {
|
||||||
|
Ok(()) => tracing::info!("Malli {} valmis!", new_model),
|
||||||
|
Err(e) => tracing::error!("Mallin lataus epäonnistui: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tracing::warn!("Yhteys hubiin katkesi — yritetään uudelleen 5s...");
|
tracing::warn!("Yhteys hubiin katkesi — yritetään uudelleen 5s...");
|
||||||
|
|||||||
BIN
network-poc/node/nodes.db
Normal file
@@ -38,17 +38,50 @@ pub fn set_gpu_load(load: u32) {
|
|||||||
console_log!("[Wasm] GPU Kuormitusraja vaihdettu -> {}%", load);
|
console_log!("[Wasm] GPU Kuormitusraja vaihdettu -> {}%", load);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Asynkroninen odotus WebAssemblylle
|
// Worker-yhteensopiva setTimeout — toimii sekä Window- että Worker-kontekstissa
|
||||||
async fn sleep_ms(ms: i32) {
|
#[wasm_bindgen]
|
||||||
|
extern "C" {
|
||||||
|
#[wasm_bindgen(js_name = setTimeout)]
|
||||||
|
fn set_timeout(closure: &js_sys::Function, ms: i32);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asynkroninen odotus WebAssemblylle (Window + Worker)
|
||||||
|
pub async fn sleep_ms(ms: i32) {
|
||||||
let promise = js_sys::Promise::new(&mut |resolve, _| {
|
let promise = js_sys::Promise::new(&mut |resolve, _| {
|
||||||
web_sys::window()
|
set_timeout(&resolve, ms);
|
||||||
.unwrap()
|
|
||||||
.set_timeout_with_callback_and_timeout_and_arguments_0(&resolve, ms)
|
|
||||||
.unwrap();
|
|
||||||
});
|
});
|
||||||
let _ = wasm_bindgen_futures::JsFuture::from(promise).await;
|
let _ = wasm_bindgen_futures::JsFuture::from(promise).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Worker-yhteensopiva Performance — käyttää globalThis.performance
|
||||||
|
pub fn perf_now() -> f64 {
|
||||||
|
js_sys::Reflect::get(&js_sys::global(), &"performance".into())
|
||||||
|
.ok()
|
||||||
|
.and_then(|p| js_sys::Reflect::get(&p, &"now".into()).ok())
|
||||||
|
.and_then(|f| f.dyn_into::<js_sys::Function>().ok())
|
||||||
|
.and_then(|f| {
|
||||||
|
let perf = js_sys::Reflect::get(&js_sys::global(), &"performance".into()).unwrap();
|
||||||
|
f.call0(&perf).ok()
|
||||||
|
})
|
||||||
|
.and_then(|v| v.as_f64())
|
||||||
|
.unwrap_or(0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Worker-yhteensopiva fetch — käyttää globalThis.fetch
|
||||||
|
pub async fn worker_fetch(url: &str) -> Result<web_sys::Response, String> {
|
||||||
|
let promise = js_sys::Reflect::get(&js_sys::global(), &"fetch".into())
|
||||||
|
.map_err(|_| "fetch ei saatavilla".to_string())?
|
||||||
|
.dyn_into::<js_sys::Function>()
|
||||||
|
.map_err(|_| "fetch ei funktio".to_string())?
|
||||||
|
.call1(&JsValue::NULL, &url.into())
|
||||||
|
.map_err(|e| format!("fetch: {:?}", e))?;
|
||||||
|
let resp = wasm_bindgen_futures::JsFuture::from(js_sys::Promise::from(promise))
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("fetch await: {:?}", e))?;
|
||||||
|
resp.dyn_into::<web_sys::Response>()
|
||||||
|
.map_err(|_| "ei Response".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
// Geneerinen tensorilaskenta — toimii millä tahansa Burn-backendillä
|
// Geneerinen tensorilaskenta — toimii millä tahansa Burn-backendillä
|
||||||
fn run_matmul<B: burn::tensor::backend::Backend>(size: usize) -> String {
|
fn run_matmul<B: burn::tensor::backend::Backend>(size: usize) -> String {
|
||||||
let device = Default::default();
|
let device = Default::default();
|
||||||
@@ -85,6 +118,27 @@ async fn run_ai_tensor_inference(difficulty: usize) -> String {
|
|||||||
format!("PoC {} Matmul ({}x{}) >> {}", backend_name, active_workload_size, active_workload_size, result)
|
format!("PoC {} Matmul ({}x{}) >> {}", backend_name, active_workload_size, active_workload_size, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// JS-exportti: tokenisoi tekstin ja palauttaa JSON-merkkijonon
|
||||||
|
/// Tokenizer ladataan IndexedDB:stä (täytyy olla ladattu aiemmin)
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub async fn tokenize_js(text: String) -> Result<String, JsValue> {
|
||||||
|
let cached_tok = storage::load_from_idb("tokenizer.json").await.unwrap_or(None);
|
||||||
|
let Some(bytes) = cached_tok else {
|
||||||
|
// Yritetään ladata verkosta
|
||||||
|
let resp = reqwest::get("https://huggingface.co/Qwen/Qwen2.5-Coder-0.5B/resolve/main/tokenizer.json").await
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("Tokenizer-lataus epäonnistui: {}", e)))?;
|
||||||
|
let bytes = resp.bytes().await
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("Tokenizer-lataus epäonnistui: {}", e)))?;
|
||||||
|
let _ = storage::save_to_idb("tokenizer.json", &bytes).await;
|
||||||
|
let tokenizer = tokenizers::Tokenizer::from_bytes(&bytes)
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("Tokenizer-parsinta: {}", e)))?;
|
||||||
|
return Ok(tokenize_text(&tokenizer, &text).to_string());
|
||||||
|
};
|
||||||
|
let tokenizer = tokenizers::Tokenizer::from_bytes(&bytes)
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("Tokenizer-parsinta: {}", e)))?;
|
||||||
|
Ok(tokenize_text(&tokenizer, &text).to_string())
|
||||||
|
}
|
||||||
|
|
||||||
/// Tokenisoi yhden tekstin ja palauttaa metriikat
|
/// Tokenisoi yhden tekstin ja palauttaa metriikat
|
||||||
fn tokenize_text(tokenizer: &tokenizers::Tokenizer, text: &str) -> serde_json::Value {
|
fn tokenize_text(tokenizer: &tokenizers::Tokenizer, text: &str) -> serde_json::Value {
|
||||||
let char_count = text.chars().count();
|
let char_count = text.chars().count();
|
||||||
@@ -123,15 +177,15 @@ async fn run_single_tokenize(text: String, ws: Rc<RefCell<WebSocket>>) {
|
|||||||
let Some(bytes) = cached_tok else { return; };
|
let Some(bytes) = cached_tok else { return; };
|
||||||
let Ok(tokenizer) = tokenizers::Tokenizer::from_bytes(&bytes) else { return; };
|
let Ok(tokenizer) = tokenizers::Tokenizer::from_bytes(&bytes) else { return; };
|
||||||
|
|
||||||
let perf = web_sys::window().unwrap().performance().unwrap();
|
let start = perf_now();
|
||||||
let start = perf.now();
|
|
||||||
let result = tokenize_text(&tokenizer, &text);
|
let result = tokenize_text(&tokenizer, &text);
|
||||||
let duration_ms = perf.now() - start;
|
let duration_ms = perf_now() - start;
|
||||||
|
|
||||||
let token_count = result["token_count"].as_u64().unwrap_or(0);
|
let token_count = result["token_count"].as_u64().unwrap_or(0);
|
||||||
let cpt = result["chars_per_token"].as_f64().unwrap_or(0.0);
|
let cpt = result["chars_per_token"].as_f64().unwrap_or(0.0);
|
||||||
|
let preview: String = text.chars().take(50).collect();
|
||||||
console_log!("Tokenisaatio: \"{}\" → {} tokenia | {:.2} m/t | {:.2}ms",
|
console_log!("Tokenisaatio: \"{}\" → {} tokenia | {:.2} m/t | {:.2}ms",
|
||||||
&text[..text.len().min(50)], token_count, cpt, duration_ms);
|
preview, token_count, cpt, duration_ms);
|
||||||
|
|
||||||
let msg = serde_json::json!({
|
let msg = serde_json::json!({
|
||||||
"type": "single_tokenize_done",
|
"type": "single_tokenize_done",
|
||||||
@@ -156,11 +210,10 @@ async fn run_pair_comparison(en_text: String, fi_text: String, ws: Rc<RefCell<We
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let perf = web_sys::window().unwrap().performance().unwrap();
|
let start_time = perf_now();
|
||||||
let start_time = perf.now();
|
|
||||||
let en_result = tokenize_text(&tokenizer, &en_text);
|
let en_result = tokenize_text(&tokenizer, &en_text);
|
||||||
let fi_result = tokenize_text(&tokenizer, &fi_text);
|
let fi_result = tokenize_text(&tokenizer, &fi_text);
|
||||||
let duration_ms = perf.now() - start_time; // millisekunteja desimaalitarkkuudella
|
let duration_ms = perf_now() - start_time;
|
||||||
|
|
||||||
let en_cpt = en_result["chars_per_token"].as_f64().unwrap_or(0.0);
|
let en_cpt = en_result["chars_per_token"].as_f64().unwrap_or(0.0);
|
||||||
let fi_cpt = fi_result["chars_per_token"].as_f64().unwrap_or(0.0);
|
let fi_cpt = fi_result["chars_per_token"].as_f64().unwrap_or(0.0);
|
||||||
@@ -270,7 +323,8 @@ pub async fn start_agent_node(hub_url: String, has_webgpu: bool, device_info_jso
|
|||||||
if LLM_BUSY.load(Ordering::SeqCst) {
|
if LLM_BUSY.load(Ordering::SeqCst) {
|
||||||
} else if let Ok(task) = serde_json::from_str::<serde_json::Value>(&msg) {
|
} 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 prompt = task.get("prompt").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||||
if !prompt.is_empty() {
|
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);
|
LLM_BUSY.store(true, Ordering::SeqCst);
|
||||||
let ws_for_async = ws_clone.clone();
|
let ws_for_async = ws_clone.clone();
|
||||||
wasm_bindgen_futures::spawn_local(async move {
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
@@ -284,7 +338,8 @@ pub async fn start_agent_node(hub_url: String, has_webgpu: bool, device_info_jso
|
|||||||
if LLM_BUSY.load(Ordering::SeqCst) {
|
if LLM_BUSY.load(Ordering::SeqCst) {
|
||||||
} else if let Ok(task) = serde_json::from_str::<serde_json::Value>(&msg) {
|
} 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 prompt = task.get("prompt").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||||
if !prompt.is_empty() {
|
let model = task.get("model").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||||
|
if !prompt.is_empty() && model.starts_with("phi3-mini") {
|
||||||
LLM_BUSY.store(true, Ordering::SeqCst);
|
LLM_BUSY.store(true, Ordering::SeqCst);
|
||||||
let ws_for_async = ws_clone.clone();
|
let ws_for_async = ws_clone.clone();
|
||||||
wasm_bindgen_futures::spawn_local(async move {
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
@@ -293,13 +348,26 @@ pub async fn start_agent_node(hub_url: String, has_webgpu: bool, device_info_jso
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if msg.contains("llm_prompt") && (current_task == 4 || current_task == 5) {
|
} 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
|
// Qwen2.5-Coder: 4 = 0.5B, 5 = 3B
|
||||||
if LLM_BUSY.load(Ordering::SeqCst) {
|
if let Ok(task) = serde_json::from_str::<serde_json::Value>(&msg) {
|
||||||
} 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 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());
|
let task_id = task.get("task_id").and_then(|v| v.as_str()).map(|s| s.to_string());
|
||||||
if !prompt.is_empty() {
|
|
||||||
|
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 {
|
||||||
let use_3b = current_task == 5;
|
let use_3b = current_task == 5;
|
||||||
LLM_BUSY.store(true, Ordering::SeqCst);
|
LLM_BUSY.store(true, Ordering::SeqCst);
|
||||||
let ws_for_async = ws_clone.clone();
|
let ws_for_async = ws_clone.clone();
|
||||||
@@ -309,6 +377,8 @@ pub async fn start_agent_node(hub_url: String, has_webgpu: bool, device_info_jso
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} // current_task == 4 || 5
|
||||||
} else if msg.contains("ai_task") {
|
} else if msg.contains("ai_task") {
|
||||||
console_log!("Hub task vastaanotettu, ajetaan GPU:lla...");
|
console_log!("Hub task vastaanotettu, ajetaan GPU:lla...");
|
||||||
let ws_for_async = ws_clone.clone();
|
let ws_for_async = ws_clone.clone();
|
||||||
|
|||||||
@@ -24,10 +24,7 @@ async fn ensure_cached(key: &str, url: &str, ws: &Rc<RefCell<WebSocket>>) -> Res
|
|||||||
|
|
||||||
console_log!("[Qwen] Ladataan {}...", key);
|
console_log!("[Qwen] Ladataan {}...", key);
|
||||||
|
|
||||||
let window = web_sys::window().unwrap();
|
let resp = crate::worker_fetch(url).await?;
|
||||||
let resp_val = wasm_bindgen_futures::JsFuture::from(window.fetch_with_str(url))
|
|
||||||
.await.map_err(|e| format!("Fetch epäonnistui: {:?}", e))?;
|
|
||||||
let resp: web_sys::Response = resp_val.dyn_into().map_err(|_| "Ei Response".to_string())?;
|
|
||||||
if !resp.ok() { return Err(format!("HTTP {}", resp.status())); }
|
if !resp.ok() { return Err(format!("HTTP {}", resp.status())); }
|
||||||
|
|
||||||
let total_size: usize = resp.headers()
|
let total_size: usize = resp.headers()
|
||||||
@@ -71,7 +68,7 @@ async fn ensure_cached(key: &str, url: &str, ws: &Rc<RefCell<WebSocket>>) -> Res
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run_qwen_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
|
pub async fn run_qwen_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
|
||||||
let perf = web_sys::window().unwrap().performance().unwrap();
|
// performance via crate::perf_now()
|
||||||
|
|
||||||
let tok_bytes = match ensure_cached("qwen05b-tokenizer.json", TOKENIZER_URL, &ws).await {
|
let tok_bytes = match ensure_cached("qwen05b-tokenizer.json", TOKENIZER_URL, &ws).await {
|
||||||
Ok(b) => b,
|
Ok(b) => b,
|
||||||
@@ -88,7 +85,7 @@ pub async fn run_qwen_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
console_log!("[Qwen] Rakennetaan mallia...");
|
console_log!("[Qwen] Rakennetaan mallia...");
|
||||||
let start_load = perf.now();
|
let start_load = crate::perf_now();
|
||||||
let device = Device::Cpu;
|
let device = Device::Cpu;
|
||||||
let dtype = DType::F32;
|
let dtype = DType::F32;
|
||||||
|
|
||||||
@@ -120,7 +117,7 @@ pub async fn run_qwen_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
|
|||||||
Err(e) => { console_log!("[Qwen] Mallin lataus: {}", e); return; }
|
Err(e) => { console_log!("[Qwen] Mallin lataus: {}", e); return; }
|
||||||
};
|
};
|
||||||
|
|
||||||
let load_time = perf.now() - start_load;
|
let load_time = crate::perf_now() - start_load;
|
||||||
console_log!("[Qwen] Malli ladattu ({:.0}ms). Generoidaan...", load_time);
|
console_log!("[Qwen] Malli ladattu ({:.0}ms). Generoidaan...", load_time);
|
||||||
|
|
||||||
let encoding = match tokenizer.encode(prompt.as_str(), true) {
|
let encoding = match tokenizer.encode(prompt.as_str(), true) {
|
||||||
@@ -131,7 +128,7 @@ pub async fn run_qwen_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
|
|||||||
let input_len = input_ids.len();
|
let input_len = input_ids.len();
|
||||||
console_log!("[Qwen] Syöte: {} tokenia", input_len);
|
console_log!("[Qwen] Syöte: {} tokenia", input_len);
|
||||||
|
|
||||||
let start_gen = perf.now();
|
let start_gen = crate::perf_now();
|
||||||
let max_new_tokens = 32;
|
let max_new_tokens = 32;
|
||||||
let mut generated_text = String::new();
|
let mut generated_text = String::new();
|
||||||
let mut tokens_generated: usize = 0;
|
let mut tokens_generated: usize = 0;
|
||||||
@@ -202,7 +199,7 @@ pub async fn run_qwen_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
|
|||||||
crate::sleep_ms(0).await;
|
crate::sleep_ms(0).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
let gen_time = perf.now() - start_gen;
|
let gen_time = crate::perf_now() - start_gen;
|
||||||
let tokens_per_sec = if gen_time > 0.0 { (tokens_generated as f64 / gen_time) * 1000.0 } else { 0.0 };
|
let tokens_per_sec = if gen_time > 0.0 { (tokens_generated as f64 / gen_time) * 1000.0 } else { 0.0 };
|
||||||
console_log!("[Qwen] {} tokenia | {:.0}ms | {:.1} tok/s", tokens_generated, gen_time, tokens_per_sec);
|
console_log!("[Qwen] {} tokenia | {:.0}ms | {:.1} tok/s", tokens_generated, gen_time, tokens_per_sec);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use candle_core::{Device, Tensor, DType};
|
use candle_core::{Device, Tensor, DType};
|
||||||
|
use candle_core::quantized::gguf_file;
|
||||||
use candle_nn::VarBuilder;
|
use candle_nn::VarBuilder;
|
||||||
use candle_transformers::models::qwen2::{Config as QwenConfig, ModelForCausalLM as QwenModel};
|
use candle_transformers::models::qwen2::{Config as QwenConfig, ModelForCausalLM as QwenModel};
|
||||||
|
use candle_transformers::models::quantized_qwen2::ModelWeights as QwenQuantizedModel;
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
@@ -16,23 +18,129 @@ macro_rules! console_log {
|
|||||||
const MODEL_05B_URL: &str = "https://huggingface.co/Qwen/Qwen2.5-Coder-0.5B-Instruct/resolve/main/model.safetensors";
|
const MODEL_05B_URL: &str = "https://huggingface.co/Qwen/Qwen2.5-Coder-0.5B-Instruct/resolve/main/model.safetensors";
|
||||||
const TOKENIZER_05B_URL: &str = "https://huggingface.co/Qwen/Qwen2.5-Coder-0.5B-Instruct/resolve/main/tokenizer.json";
|
const TOKENIZER_05B_URL: &str = "https://huggingface.co/Qwen/Qwen2.5-Coder-0.5B-Instruct/resolve/main/tokenizer.json";
|
||||||
|
|
||||||
// 3B — parempi laatu, vaatii enemmän muistia (~6 GB lataus, ~12 GB RAM)
|
// 1.5B GGUF Q4_K_M — kvantisoidtu, mahtuu selaimeen (~1 GB)
|
||||||
const MODEL_3B_PART1_URL: &str = "https://huggingface.co/Qwen/Qwen2.5-Coder-3B-Instruct/resolve/main/model-00001-of-00002.safetensors";
|
const MODEL_GGUF_URL: &str = "https://huggingface.co/Qwen/Qwen2.5-Coder-1.5B-Instruct-GGUF/resolve/main/qwen2.5-coder-1.5b-instruct-q4_k_m.gguf";
|
||||||
const MODEL_3B_PART2_URL: &str = "https://huggingface.co/Qwen/Qwen2.5-Coder-3B-Instruct/resolve/main/model-00002-of-00002.safetensors";
|
const TOKENIZER_GGUF_URL: &str = "https://huggingface.co/Qwen/Qwen2.5-Coder-1.5B-Instruct/resolve/main/tokenizer.json";
|
||||||
const TOKENIZER_3B_URL: &str = "https://huggingface.co/Qwen/Qwen2.5-Coder-3B-Instruct/resolve/main/tokenizer.json";
|
|
||||||
|
|
||||||
async fn ensure_cached(key: &str, url: &str, ws: &Rc<RefCell<WebSocket>>) -> Result<Vec<u8>, String> {
|
enum CoderModel {
|
||||||
if let Ok(Some(bytes)) = storage::load_from_idb(key).await {
|
Full(QwenModel),
|
||||||
console_log!("[Coder] {} löytyi välimuistista ({} MB)", key, bytes.len() / 1024 / 1024);
|
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);
|
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);
|
console_log!("[Coder] Ladataan {}...", key);
|
||||||
|
|
||||||
let window = web_sys::window().unwrap();
|
let resp = crate::worker_fetch(url).await?;
|
||||||
let resp_val = wasm_bindgen_futures::JsFuture::from(window.fetch_with_str(url))
|
|
||||||
.await.map_err(|e| format!("Fetch: {:?}", e))?;
|
|
||||||
let resp: web_sys::Response = resp_val.dyn_into().map_err(|_| "Ei Response".to_string())?;
|
|
||||||
if !resp.ok() { return Err(format!("HTTP {}", resp.status())); }
|
if !resp.ok() { return Err(format!("HTTP {}", resp.status())); }
|
||||||
|
|
||||||
let total_size: usize = resp.headers()
|
let total_size: usize = resp.headers()
|
||||||
@@ -68,167 +176,161 @@ async fn ensure_cached(key: &str, url: &str, ws: &Rc<RefCell<WebSocket>>) -> Res
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console_log!("[Coder] Tallennetaan {} ({} MB)...", key, data.len() / 1024 / 1024);
|
console_log!("[Coder] Tallennetaan {} ({} MB) IndexedDB:hen...", key, data.len() / 1024 / 1024);
|
||||||
let _ = storage::save_to_idb(key, &data).await;
|
let _ = storage::save_to_idb(key, &data).await;
|
||||||
console_log!("[Coder] {} tallennettu!", key);
|
console_log!("[Coder] {} tallennettu!", key);
|
||||||
|
|
||||||
Ok(data)
|
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)
|
/// use_3b: false = 0.5B (nopea), true = 3B (laadukas)
|
||||||
pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use_3b: bool, task_id: Option<String>) {
|
pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use_3b: bool, task_id: Option<String>) {
|
||||||
let perf = web_sys::window().unwrap().performance().unwrap();
|
console_log!("[Coder] run_coder_inference alkaa! prompt={}", &prompt[..prompt.len().min(50)]);
|
||||||
let size_label = if use_3b { "3B" } else { "0.5B" };
|
let size_label = if use_3b { "3B" } else { "0.5B" };
|
||||||
|
|
||||||
// Tokenizer (sama molemmille)
|
let start_load = crate::perf_now();
|
||||||
let tok_url = if use_3b { TOKENIZER_3B_URL } else { TOKENIZER_05B_URL };
|
|
||||||
let tok_key = if use_3b { "coder3b-tokenizer.json" } else { "coder05b-tokenizer.json" };
|
|
||||||
let tok_bytes = match ensure_cached(tok_key, tok_url, &ws).await {
|
|
||||||
Ok(b) => b,
|
|
||||||
Err(e) => { console_log!("[Coder] Tokenizer-virhe: {}", e); return; }
|
|
||||||
};
|
|
||||||
let tokenizer = match tokenizers::Tokenizer::from_bytes(&tok_bytes) {
|
|
||||||
Ok(t) => t,
|
|
||||||
Err(e) => { console_log!("[Coder] Tokenizer-parsinta: {}", e); return; }
|
|
||||||
};
|
|
||||||
|
|
||||||
// Mallin painot
|
console_log!("[Coder] Kutsutaan get_or_build_model...");
|
||||||
let device = Device::Cpu;
|
if let Err(e) = get_or_build_model(use_3b, &ws).await {
|
||||||
let dtype = DType::F32;
|
console_log!("[Coder] Mallin lataus epäonnistui: {}", e);
|
||||||
|
return;
|
||||||
let tensors = if use_3b {
|
|
||||||
// 3B: kaksi osaa
|
|
||||||
let part1 = match ensure_cached("coder3b-model-part1.safetensors", MODEL_3B_PART1_URL, &ws).await {
|
|
||||||
Ok(b) => b,
|
|
||||||
Err(e) => { console_log!("[Coder] Malli osa 1 virhe: {}", e); return; }
|
|
||||||
};
|
|
||||||
let part2 = match ensure_cached("coder3b-model-part2.safetensors", MODEL_3B_PART2_URL, &ws).await {
|
|
||||||
Ok(b) => b,
|
|
||||||
Err(e) => { console_log!("[Coder] Malli osa 2 virhe: {}", e); return; }
|
|
||||||
};
|
|
||||||
console_log!("[Coder] Rakennetaan 3B-mallia...");
|
|
||||||
let mut all_tensors = candle_core::safetensors::load_buffer(&part1, &device)
|
|
||||||
.map_err(|e| format!("Part1: {}", e)).unwrap();
|
|
||||||
let tensors2 = candle_core::safetensors::load_buffer(&part2, &device)
|
|
||||||
.map_err(|e| format!("Part2: {}", e)).unwrap();
|
|
||||||
all_tensors.extend(tensors2);
|
|
||||||
all_tensors
|
|
||||||
} else {
|
|
||||||
// 0.5B: yksi osa
|
|
||||||
let model_bytes = match ensure_cached("coder05b-model.safetensors", MODEL_05B_URL, &ws).await {
|
|
||||||
Ok(b) => b,
|
|
||||||
Err(e) => { console_log!("[Coder] Malli-virhe: {}", e); return; }
|
|
||||||
};
|
|
||||||
console_log!("[Coder] Rakennetaan 0.5B-mallia...");
|
|
||||||
match candle_core::safetensors::load_buffer(&model_bytes, &device) {
|
|
||||||
Ok(t) => t,
|
|
||||||
Err(e) => { console_log!("[Coder] Safetensors: {}", e); return; }
|
|
||||||
}
|
}
|
||||||
};
|
console_log!("[Coder] Malli valmis, aloitetaan inferenssi");
|
||||||
|
|
||||||
let start_load = perf.now();
|
let load_time = crate::perf_now() - start_load;
|
||||||
let vb = VarBuilder::from_tensors(tensors, dtype, &device);
|
if load_time > 100.0 {
|
||||||
|
|
||||||
let config = if use_3b {
|
|
||||||
QwenConfig {
|
|
||||||
vocab_size: 151936,
|
|
||||||
hidden_size: 2048,
|
|
||||||
intermediate_size: 11008,
|
|
||||||
num_hidden_layers: 36,
|
|
||||||
num_attention_heads: 16,
|
|
||||||
num_key_value_heads: 2,
|
|
||||||
max_position_embeddings: 32768,
|
|
||||||
sliding_window: 32768,
|
|
||||||
max_window_layers: 36,
|
|
||||||
tie_word_embeddings: true,
|
|
||||||
rope_theta: 1000000.0,
|
|
||||||
rms_norm_eps: 1e-6,
|
|
||||||
use_sliding_window: false,
|
|
||||||
hidden_act: candle_nn::Activation::Silu,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
QwenConfig {
|
|
||||||
vocab_size: 151936,
|
|
||||||
hidden_size: 896,
|
|
||||||
intermediate_size: 4864,
|
|
||||||
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!("[Coder] Mallin lataus: {}", e); return; }
|
|
||||||
};
|
|
||||||
|
|
||||||
let load_time = perf.now() - start_load;
|
|
||||||
console_log!("[Coder] Malli ladattu ({:.0}ms). Generoidaan...", load_time);
|
console_log!("[Coder] Malli ladattu ({:.0}ms). Generoidaan...", load_time);
|
||||||
|
}
|
||||||
|
|
||||||
// Parsitaan JSON-prompti tai käytetään teksti sellaisenaan
|
// 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('{') {
|
let (actual_prompt, system_msg, max_new_tokens) = if prompt.starts_with('{') {
|
||||||
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&prompt) {
|
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&prompt) {
|
||||||
let p = json.get("prompt").and_then(|v| v.as_str()).unwrap_or(&prompt).to_string();
|
let p = json.get("prompt").and_then(|v| v.as_str()).unwrap_or(&prompt).to_string();
|
||||||
let s = json.get("system").and_then(|v| v.as_str())
|
let s = json.get("system").and_then(|v| v.as_str()).unwrap_or(default_system).to_string();
|
||||||
.unwrap_or("You are a Python coding assistant. Write only code, no explanations.").to_string();
|
let m = json.get("max_tokens").and_then(|v| v.as_u64()).unwrap_or(512) as usize;
|
||||||
let m = json.get("max_tokens").and_then(|v| v.as_u64()).unwrap_or(128) as usize;
|
|
||||||
(p, s, m)
|
(p, s, m)
|
||||||
} else {
|
} else {
|
||||||
(prompt.clone(), "You are a Python coding assistant. Write only code, no explanations.".to_string(), 128)
|
(prompt.clone(), default_system.to_string(), 512)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
(prompt.clone(), "You are a Python coding assistant. Write only code, no explanations.".to_string(), 128)
|
(prompt.clone(), default_system.to_string(), 512)
|
||||||
};
|
};
|
||||||
|
|
||||||
let formatted = format!("<|im_start|>system\n{}<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n", system_msg, actual_prompt);
|
// 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);
|
||||||
|
|
||||||
let encoding = match tokenizer.encode(formatted.as_str(), true) {
|
// Inferenssi: käytetään välimuistissa olevaa mallia
|
||||||
Ok(e) => e,
|
let (generated_text, tokens_generated, gen_time) = MODEL_CACHE.with(|cache| {
|
||||||
Err(e) => { console_log!("[Coder] Tokenisointivirhe: {}", e); return; }
|
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_ids: Vec<u32> = encoding.get_ids().to_vec();
|
||||||
let input_len = input_ids.len();
|
let input_len = input_ids.len();
|
||||||
console_log!("[Coder] Syöte: {} tokenia", input_len);
|
console_log!("[Coder] Syöte: {} tokenia", input_len);
|
||||||
|
|
||||||
let start_gen = perf.now();
|
let device = Device::Cpu;
|
||||||
// max_new_tokens tulee JSON-promptista tai oletuksena 128
|
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 generated_text = String::new();
|
||||||
let mut tokens_generated: usize = 0;
|
let mut tokens_generated: usize = 0;
|
||||||
let eos_token = 151645u32;
|
let mut all_generated: Vec<u32> = Vec::new();
|
||||||
|
|
||||||
// Prefill
|
// Prefill
|
||||||
let input = match Tensor::new(input_ids.as_slice(), &device).and_then(|t| t.unsqueeze(0)) {
|
let input = Tensor::new(input_ids.as_slice(), &device).and_then(|t| t.unsqueeze(0)).unwrap();
|
||||||
Ok(t) => t,
|
let logits = cached.model.forward(&input, 0).unwrap();
|
||||||
Err(e) => { console_log!("[Coder] Tensor: {}", e); return; }
|
|
||||||
};
|
|
||||||
let logits = match model.forward(&input, 0) {
|
|
||||||
Ok(l) => l,
|
|
||||||
Err(e) => { console_log!("[Coder] Forward (prefill): {}", e); return; }
|
|
||||||
};
|
|
||||||
|
|
||||||
let logits = logits.squeeze(0).unwrap();
|
let logits = logits.squeeze(0).unwrap();
|
||||||
let logits = if logits.dims().len() == 2 {
|
let logits = if logits.dims().len() == 2 {
|
||||||
logits.get(logits.dim(0).unwrap() - 1).unwrap()
|
logits.get(logits.dim(0).unwrap() - 1).unwrap()
|
||||||
} else {
|
} else { logits };
|
||||||
logits
|
|
||||||
};
|
let mut next_token = crate::sampling::sample_top_k_with_penalty(&logits, top_k, temperature, &all_generated, repetition_penalty);
|
||||||
let mut next_token = crate::sampling::sample_top_k(&logits, 10, 5.0);
|
|
||||||
|
|
||||||
if next_token != eos_token {
|
if next_token != eos_token {
|
||||||
if let Ok(text) = tokenizer.decode(&[next_token], true) {
|
if let Ok(text) = cached.tokenizer.decode(&[next_token], true) {
|
||||||
generated_text.push_str(&text);
|
generated_text.push_str(&text);
|
||||||
let mut chunk = serde_json::json!({ "type": "llm_chunk", "token": text, "prompt": prompt, "model": "Qwen2.5-Coder" });
|
let mut chunk = serde_json::json!({ "type": "llm_chunk", "token": text, "prompt": prompt, "model": "Qwen2.5-Coder" });
|
||||||
if let Some(ref tid) = task_id { chunk.as_object_mut().unwrap().insert("task_id".to_string(), serde_json::json!(tid)); }
|
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());
|
let _ = ws.borrow().send_with_str(&chunk.to_string());
|
||||||
}
|
}
|
||||||
|
all_generated.push(next_token);
|
||||||
tokens_generated += 1;
|
tokens_generated += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,11 +339,8 @@ pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use
|
|||||||
for _ in 1..max_new_tokens {
|
for _ in 1..max_new_tokens {
|
||||||
if next_token == eos_token { break; }
|
if next_token == eos_token { break; }
|
||||||
|
|
||||||
let input = match Tensor::new(&[next_token], &device).and_then(|t| t.unsqueeze(0)) {
|
let input = Tensor::new(&[next_token], &device).and_then(|t| t.unsqueeze(0)).unwrap();
|
||||||
Ok(t) => t,
|
let logits = match cached.model.forward(&input, pos) {
|
||||||
Err(e) => { console_log!("[Coder] Tensor: {}", e); break; }
|
|
||||||
};
|
|
||||||
let logits = match model.forward(&input, pos) {
|
|
||||||
Ok(l) => l,
|
Ok(l) => l,
|
||||||
Err(e) => { console_log!("[Coder] Forward pos {}: {}", pos, e); break; }
|
Err(e) => { console_log!("[Coder] Forward pos {}: {}", pos, e); break; }
|
||||||
};
|
};
|
||||||
@@ -249,27 +348,46 @@ pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use
|
|||||||
let logits = logits.squeeze(0).unwrap();
|
let logits = logits.squeeze(0).unwrap();
|
||||||
let logits = if logits.dims().len() == 2 {
|
let logits = if logits.dims().len() == 2 {
|
||||||
logits.get(logits.dim(0).unwrap() - 1).unwrap()
|
logits.get(logits.dim(0).unwrap() - 1).unwrap()
|
||||||
} else {
|
} else { logits };
|
||||||
logits
|
next_token = crate::sampling::sample_top_k_with_penalty(&logits, top_k, temperature, &all_generated, repetition_penalty);
|
||||||
};
|
|
||||||
next_token = crate::sampling::sample_top_k(&logits, 10, 5.0);
|
|
||||||
pos += 1;
|
pos += 1;
|
||||||
|
|
||||||
if next_token == eos_token { break; }
|
if next_token == eos_token { break; }
|
||||||
|
|
||||||
if let Ok(text) = tokenizer.decode(&[next_token], true) {
|
if let Ok(text) = cached.tokenizer.decode(&[next_token], true) {
|
||||||
generated_text.push_str(&text);
|
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" });
|
let mut chunk = serde_json::json!({ "type": "llm_chunk", "token": text, "prompt": prompt, "model": "Qwen2.5-Coder" });
|
||||||
if let Some(ref tid) = task_id { chunk.as_object_mut().unwrap().insert("task_id".to_string(), serde_json::json!(tid)); }
|
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());
|
let _ = ws.borrow().send_with_str(&chunk.to_string());
|
||||||
}
|
}
|
||||||
|
all_generated.push(next_token);
|
||||||
tokens_generated += 1;
|
tokens_generated += 1;
|
||||||
|
|
||||||
// Yield — vapautetaan selaimen event loop joka tokenin jälkeen
|
|
||||||
crate::sleep_ms(0).await;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let gen_time = perf.now() - start_gen;
|
let gen_time = crate::perf_now() - start_gen;
|
||||||
|
|
||||||
|
// Siivotaan vastaus: poista markdown-koodiblokit ja johdantotekstit
|
||||||
|
let cleaned = strip_markdown_wrapper(&generated_text);
|
||||||
|
|
||||||
|
(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 };
|
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);
|
console_log!("[Coder] {} tokenia | {:.0}ms | {:.1} tok/s", tokens_generated, gen_time, tokens_per_sec);
|
||||||
|
|
||||||
@@ -284,7 +402,9 @@ pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use
|
|||||||
"load_time_ms": (load_time * 100.0).round() / 100.0,
|
"load_time_ms": (load_time * 100.0).round() / 100.0,
|
||||||
});
|
});
|
||||||
if let Some(tid) = task_id {
|
if let Some(tid) = task_id {
|
||||||
done.as_object_mut().unwrap().insert("task_id".to_string(), serde_json::json!(tid));
|
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());
|
let _ = ws.borrow().send_with_str(&done.to_string());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +1,105 @@
|
|||||||
use candle_core::Tensor;
|
use candle_core::Tensor;
|
||||||
|
use std::cell::Cell;
|
||||||
|
|
||||||
/// Top-k sampling ilman softmaxia — kiertää Candlen SoftmaxLastDim Wasm-bugin.
|
thread_local! {
|
||||||
/// Valitsee top-k logiteista ja poimii satunnaisen (painotettu).
|
static RNG_STATE: Cell<u64> = Cell::new(0);
|
||||||
/// Jos k=1, toimii kuten argmax (greedy).
|
}
|
||||||
pub fn sample_top_k(logits: &Tensor, k: usize, eos_penalty: f32) -> u32 {
|
|
||||||
// Muunnetaan Vec<f32>:ksi
|
fn next_rand() -> f32 {
|
||||||
let logits_vec: Vec<f32> = logits.to_vec1::<f32>().unwrap_or_default();
|
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; }
|
if logits_vec.is_empty() { return 0; }
|
||||||
|
|
||||||
// Rangotaan ja otetaan top-k indeksit
|
// 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();
|
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.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
|
||||||
indexed.truncate(k);
|
indexed.truncate(k);
|
||||||
|
|
||||||
// EOS-penaltti: vähennetään EOS-tokenin logitia
|
if k == 1 || temperature == 0.0 {
|
||||||
for item in indexed.iter_mut() {
|
|
||||||
if item.0 == 2 || item.0 == 151645 { // SmolLM EOS=2, Qwen EOS=151645
|
|
||||||
item.1 -= eos_penalty;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if k == 1 {
|
|
||||||
return indexed[0].0 as u32;
|
return indexed[0].0 as u32;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Yksinkertainen "softmax" top-k:lle CPU:lla
|
// Softmax top-k:lle
|
||||||
let max_logit = indexed.iter().map(|x| x.1).fold(f32::NEG_INFINITY, f32::max);
|
let max_logit = indexed[0].1;
|
||||||
let exps: Vec<f32> = indexed.iter().map(|x| (x.1 - max_logit).exp()).collect();
|
let exps: Vec<f32> = indexed.iter().map(|x| (x.1 - max_logit).exp()).collect();
|
||||||
let sum: f32 = exps.iter().sum();
|
let sum: f32 = exps.iter().sum();
|
||||||
let probs: Vec<f32> = exps.iter().map(|e| e / sum).collect();
|
let probs: Vec<f32> = exps.iter().map(|e| e / sum).collect();
|
||||||
|
|
||||||
// Satunnainen valinta kumulatiivisella todennäköisyydellä
|
let rand_val = next_rand();
|
||||||
// Käytetään yksinkertaista XorShift-satunnaislukugeneraattoria (ei tarvita getrandom)
|
|
||||||
let seed = (js_sys::Date::now() * 1000.0) as u64;
|
let mut cumulative = 0.0;
|
||||||
let rand_val = ((seed ^ (seed >> 13) ^ (seed << 7)) % 10000) as f32 / 10000.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;
|
let mut cumulative = 0.0;
|
||||||
for (i, p) in probs.iter().enumerate() {
|
for (i, p) in probs.iter().enumerate() {
|
||||||
|
|||||||
@@ -28,10 +28,7 @@ async fn ensure_cached(key: &str, url: &str, ws: &Rc<RefCell<WebSocket>>) -> Res
|
|||||||
send_progress(ws, key, 0, 0, 0);
|
send_progress(ws, key, 0, 0, 0);
|
||||||
|
|
||||||
// Fetch API:lla saadaan Content-Length ja streaming-luku
|
// Fetch API:lla saadaan Content-Length ja streaming-luku
|
||||||
let window = web_sys::window().unwrap();
|
let resp = crate::worker_fetch(url).await?;
|
||||||
let resp_val = wasm_bindgen_futures::JsFuture::from(window.fetch_with_str(url))
|
|
||||||
.await.map_err(|e| format!("Fetch epäonnistui: {:?}", e))?;
|
|
||||||
let resp: web_sys::Response = resp_val.dyn_into().map_err(|_| "Ei Response-objekti".to_string())?;
|
|
||||||
|
|
||||||
if !resp.ok() {
|
if !resp.ok() {
|
||||||
return Err(format!("HTTP {}", resp.status()));
|
return Err(format!("HTTP {}", resp.status()));
|
||||||
@@ -99,7 +96,7 @@ fn send_progress(ws: &Rc<RefCell<WebSocket>>, file: &str, pct: u32, loaded: usiz
|
|||||||
|
|
||||||
/// Lataa malli ja tokenizer, suorita inferenssi ja streamaa tokenit hubille
|
/// Lataa malli ja tokenizer, suorita inferenssi ja streamaa tokenit hubille
|
||||||
pub async fn run_smollm_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
|
pub async fn run_smollm_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
|
||||||
let perf = web_sys::window().unwrap().performance().unwrap();
|
// performance via crate::perf_now()
|
||||||
|
|
||||||
// 1. Lataa tokenizer
|
// 1. Lataa tokenizer
|
||||||
let tok_bytes = match ensure_cached("smollm-tokenizer.json", TOKENIZER_URL, &ws).await {
|
let tok_bytes = match ensure_cached("smollm-tokenizer.json", TOKENIZER_URL, &ws).await {
|
||||||
@@ -122,7 +119,7 @@ pub async fn run_smollm_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
|
|||||||
// Burn 0.21-pre.2 cubecl-runtime ei käänny Wasmille (println! puuttuu)
|
// Burn 0.21-pre.2 cubecl-runtime ei käänny Wasmille (println! puuttuu)
|
||||||
// → NdArray kunnes Burn 0.21 stable + Wasm-tuki
|
// → NdArray kunnes Burn 0.21 stable + Wasm-tuki
|
||||||
console_log!("[SmolLM] Burn NdArray (CPU) inferenssi...");
|
console_log!("[SmolLM] Burn NdArray (CPU) inferenssi...");
|
||||||
run_burn_inference::<burn::backend::NdArray>(prompt, model_bytes, tokenizer, ws, perf.clone()).await;
|
run_burn_inference::<burn::backend::NdArray>(prompt, model_bytes, tokenizer, ws).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_burn_inference<B: burn::tensor::backend::Backend>(
|
async fn run_burn_inference<B: burn::tensor::backend::Backend>(
|
||||||
@@ -130,9 +127,8 @@ async fn run_burn_inference<B: burn::tensor::backend::Backend>(
|
|||||||
model_bytes: Vec<u8>,
|
model_bytes: Vec<u8>,
|
||||||
tokenizer: tokenizers::Tokenizer,
|
tokenizer: tokenizers::Tokenizer,
|
||||||
ws: Rc<RefCell<WebSocket>>,
|
ws: Rc<RefCell<WebSocket>>,
|
||||||
perf: web_sys::Performance, // Korjattu Wasm-performanssi välitettäväksi
|
|
||||||
) {
|
) {
|
||||||
let start_load = perf.now();
|
let start_load = crate::perf_now();
|
||||||
|
|
||||||
let device = Default::default();
|
let device = Default::default();
|
||||||
let config = crate::burn_smollm::config::SmolLMConfig::default();
|
let config = crate::burn_smollm::config::SmolLMConfig::default();
|
||||||
@@ -143,7 +139,7 @@ async fn run_burn_inference<B: burn::tensor::backend::Backend>(
|
|||||||
Err(e) => { console_log!("[SmolLM] Lataus epäonnistui: {}", e); return; }
|
Err(e) => { console_log!("[SmolLM] Lataus epäonnistui: {}", e); return; }
|
||||||
};
|
};
|
||||||
|
|
||||||
let load_time = perf.now() - start_load;
|
let load_time = crate::perf_now() - start_load;
|
||||||
console_log!("[SmolLM] Burn-malli ladattu ({:.0}ms). Generoidaan...", load_time);
|
console_log!("[SmolLM] Burn-malli ladattu ({:.0}ms). Generoidaan...", load_time);
|
||||||
|
|
||||||
let formatted_prompt = format!("<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n", prompt);
|
let formatted_prompt = format!("<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n", prompt);
|
||||||
@@ -156,7 +152,7 @@ async fn run_burn_inference<B: burn::tensor::backend::Backend>(
|
|||||||
let input_len = input_ids.len();
|
let input_len = input_ids.len();
|
||||||
console_log!("[SmolLM] Syöte: {} tokenia", input_len);
|
console_log!("[SmolLM] Syöte: {} tokenia", input_len);
|
||||||
|
|
||||||
let start_gen = perf.now();
|
let start_gen = crate::perf_now();
|
||||||
let max_new_tokens = 32;
|
let max_new_tokens = 32;
|
||||||
let mut generated_text = String::new();
|
let mut generated_text = String::new();
|
||||||
let mut tokens_generated: usize = 0;
|
let mut tokens_generated: usize = 0;
|
||||||
@@ -219,7 +215,7 @@ async fn run_burn_inference<B: burn::tensor::backend::Backend>(
|
|||||||
tokens_generated += 1;
|
tokens_generated += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
let gen_time = perf.now() - start_gen;
|
let gen_time = crate::perf_now() - start_gen;
|
||||||
let tokens_per_sec = if gen_time > 0.0 { (tokens_generated as f64 / gen_time) * 1000.0 } else { 0.0 };
|
let tokens_per_sec = if gen_time > 0.0 { (tokens_generated as f64 / gen_time) * 1000.0 } else { 0.0 };
|
||||||
|
|
||||||
let done = serde_json::json!({
|
let done = serde_json::json!({
|
||||||
|
|||||||
1
network-poc/target-check/.rustc_info.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"rustc_fingerprint":15841952146704291179,"outputs":{"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.94.1 (e408947bf 2026-03-25)\nbinary: rustc\ncommit-hash: e408947bfd200af42db322daf0fadfe7e26d3bd1\ncommit-date: 2026-03-25\nhost: x86_64-unknown-linux-gnu\nrelease: 1.94.1\nLLVM version: 21.1.8\n","stderr":""},"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/jaakko/.rustup/toolchains/stable-x86_64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"unknown\"\nunix\n","stderr":""}},"successes":{}}
|
||||||
3
network-poc/target-check/CACHEDIR.TAG
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
Signature: 8a477f597d28d172789f06886806bc55
|
||||||
|
# This file is a cache directory tag created by cargo.
|
||||||
|
# For information about cache directory tags see https://bford.info/cachedir/
|
||||||
24
network-poc/temp/frontend-old/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# build output
|
||||||
|
dist/
|
||||||
|
# generated types
|
||||||
|
.astro/
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
|
||||||
|
# environment variables
|
||||||
|
.env
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# macOS-specific files
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# jetbrains setting folder
|
||||||
|
.idea/
|
||||||
4
network-poc/temp/frontend-old/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["astro-build.astro-vscode"],
|
||||||
|
"unwantedRecommendations": []
|
||||||
|
}
|
||||||
11
network-poc/temp/frontend-old/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"command": "./node_modules/.bin/astro dev",
|
||||||
|
"name": "Development server",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "node-terminal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
43
network-poc/temp/frontend-old/README.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Astro Starter Kit: Minimal
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm create astro@latest -- --template minimal
|
||||||
|
```
|
||||||
|
|
||||||
|
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||||
|
|
||||||
|
## 🚀 Project Structure
|
||||||
|
|
||||||
|
Inside of your Astro project, you'll see the following folders and files:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/
|
||||||
|
├── public/
|
||||||
|
├── src/
|
||||||
|
│ └── pages/
|
||||||
|
│ └── index.astro
|
||||||
|
└── package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
|
||||||
|
|
||||||
|
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
|
||||||
|
|
||||||
|
Any static assets, like images, can be placed in the `public/` directory.
|
||||||
|
|
||||||
|
## 🧞 Commands
|
||||||
|
|
||||||
|
All commands are run from the root of the project, from a terminal:
|
||||||
|
|
||||||
|
| Command | Action |
|
||||||
|
| :------------------------ | :----------------------------------------------- |
|
||||||
|
| `npm install` | Installs dependencies |
|
||||||
|
| `npm run dev` | Starts local dev server at `localhost:4321` |
|
||||||
|
| `npm run build` | Build your production site to `./dist/` |
|
||||||
|
| `npm run preview` | Preview your build locally, before deploying |
|
||||||
|
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||||
|
| `npm run astro -- --help` | Get help using the Astro CLI |
|
||||||
|
|
||||||
|
## 👀 Want to learn more?
|
||||||
|
|
||||||
|
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
|
||||||
5
network-poc/temp/frontend-old/astro.config.mjs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// @ts-check
|
||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({});
|
||||||
4731
network-poc/temp/frontend-old/package-lock.json
generated
Normal file
18
network-poc/temp/frontend-old/package.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"type": "module",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.12.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"build": "astro build",
|
||||||
|
"preview": "astro preview",
|
||||||
|
"astro": "astro"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"astro": "^6.1.5",
|
||||||
|
"three": "^0.183.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
34
network-poc/temp/frontend-old/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ä.
|
||||||
|
Before Width: | Height: | Size: 696 KiB After Width: | Height: | Size: 696 KiB |
BIN
network-poc/temp/frontend-old/public/avatars/bear.png
Normal file
|
After Width: | Height: | Size: 757 KiB |
BIN
network-poc/temp/frontend-old/public/avatars/beaver.png
Normal file
|
After Width: | Height: | Size: 700 KiB |
BIN
network-poc/temp/frontend-old/public/avatars/chameleon.png
Normal file
|
After Width: | Height: | Size: 731 KiB |
BIN
network-poc/temp/frontend-old/public/avatars/elephant.png
Normal file
|
After Width: | Height: | Size: 711 KiB |
BIN
network-poc/temp/frontend-old/public/avatars/gecko.png
Normal file
|
After Width: | Height: | Size: 695 KiB |
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 432 KiB After Width: | Height: | Size: 432 KiB |
|
Before Width: | Height: | Size: 650 KiB After Width: | Height: | Size: 650 KiB |