Compare commits
187 Commits
agentic-co
...
529a30a6e1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
3
.gitignore
vendored
@@ -34,3 +34,6 @@ Cargo.lock
|
||||
*.pdb
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/rust,linux
|
||||
|
||||
# Ajonaikaiset tietokannat
|
||||
*.db
|
||||
|
||||
475
docker-errors.log
Normal file
@@ -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) │
|
||||
└──────────────────────────────────────────────────┘
|
||||
```
|
||||
@@ -1,30 +1,36 @@
|
||||
FROM rust:slim AS builder
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
pkg-config libssl-dev g++ \
|
||||
pkg-config libssl-dev g++ libvulkan-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY Cargo.toml ./
|
||||
COPY Cargo.loc[k] ./
|
||||
COPY hub/Cargo.toml hub/Cargo.toml
|
||||
COPY node/Cargo.toml node/Cargo.toml
|
||||
COPY native-node/Cargo.toml native-node/Cargo.toml
|
||||
COPY cli/Cargo.toml cli/Cargo.toml
|
||||
|
||||
# Tyhjät src-tiedostot riippuvuuksien esikääntämistä varten
|
||||
RUN mkdir -p hub/src node/src native-node/src \
|
||||
RUN mkdir -p hub/src node/src native-node/src cli/src \
|
||||
&& echo "fn main(){}" > hub/src/main.rs \
|
||||
&& echo "" > node/src/lib.rs \
|
||||
&& echo "fn main(){}" > native-node/src/main.rs \
|
||||
&& echo "fn main(){}" > cli/src/main.rs \
|
||||
&& cargo build --release -p native-node 2>/dev/null || true
|
||||
|
||||
COPY native-node/src native-node/src
|
||||
RUN cargo build --release -p native-node
|
||||
# Touch pakottaa rekompilauksen dummy-binaryn yli
|
||||
RUN touch native-node/src/main.rs && cargo build --release -p native-node
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||
RUN apt-get update && apt-get install -y ca-certificates libvulkan1 && rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=builder /app/target/release/native-node /usr/local/bin/native-node
|
||||
|
||||
ENV HUB_URL=ws://hub:3000/ws
|
||||
ENV HUB_URL=ws://agentic-poc:3000/ws
|
||||
ENV OLLAMA_URL=http://ollama:11434
|
||||
ENV OLLAMA_MODEL=qwen2.5-coder:7b
|
||||
ENV ALLOCATED_GB=4
|
||||
|
||||
CMD ["native-node"]
|
||||
|
||||
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
|
||||
@@ -1,13 +1,12 @@
|
||||
services:
|
||||
# NVIDIA GPU -solmu
|
||||
native-node-nvidia:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.native-node
|
||||
container_name: kipina-node-nvidia
|
||||
environment:
|
||||
- HUB_URL=wss://kipina.studio/ws
|
||||
- ALLOCATED_GB=4
|
||||
# Ollama NVIDIA GPU:lla
|
||||
ollama-nvidia:
|
||||
image: ollama/ollama:latest
|
||||
container_name: kipina-ollama
|
||||
ports:
|
||||
- "11434:11434"
|
||||
volumes:
|
||||
- ollama-models:/root/.ollama
|
||||
restart: unless-stopped
|
||||
deploy:
|
||||
resources:
|
||||
@@ -16,6 +15,65 @@ services:
|
||||
- driver: nvidia
|
||||
count: all
|
||||
capabilities: [gpu]
|
||||
networks:
|
||||
default:
|
||||
aliases:
|
||||
- ollama
|
||||
profiles:
|
||||
- nvidia
|
||||
|
||||
# Ollama AMD ROCm GPU:lla
|
||||
ollama-amd:
|
||||
image: ollama/ollama:rocm
|
||||
container_name: kipina-ollama
|
||||
ports:
|
||||
- "11434:11434"
|
||||
volumes:
|
||||
- ollama-models:/root/.ollama
|
||||
restart: unless-stopped
|
||||
devices:
|
||||
- /dev/kfd:/dev/kfd
|
||||
- /dev/dri:/dev/dri
|
||||
group_add:
|
||||
- video
|
||||
- render
|
||||
networks:
|
||||
default:
|
||||
aliases:
|
||||
- ollama
|
||||
profiles:
|
||||
- amd
|
||||
|
||||
# Ollama CPU:lla
|
||||
ollama-cpu:
|
||||
image: ollama/ollama:latest
|
||||
container_name: kipina-ollama
|
||||
ports:
|
||||
- "11434:11434"
|
||||
volumes:
|
||||
- ollama-models:/root/.ollama
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
default:
|
||||
aliases:
|
||||
- ollama
|
||||
profiles:
|
||||
- cpu
|
||||
|
||||
# NVIDIA GPU -solmu
|
||||
native-node-nvidia:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.native-node
|
||||
container_name: kipina-node-nvidia
|
||||
environment:
|
||||
- HUB_URL=wss://kipina.studio/ws
|
||||
- OLLAMA_URL=http://ollama:11434
|
||||
- OLLAMA_MODEL=qwen2.5-coder:7b
|
||||
- ALLOCATED_GB=4
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- ollama-nvidia
|
||||
profiles:
|
||||
- nvidia
|
||||
|
||||
@@ -27,14 +85,12 @@ services:
|
||||
container_name: kipina-node-amd
|
||||
environment:
|
||||
- HUB_URL=wss://kipina.studio/ws
|
||||
- OLLAMA_URL=http://ollama:11434
|
||||
- OLLAMA_MODEL=qwen2.5-coder:7b
|
||||
- ALLOCATED_GB=4
|
||||
restart: unless-stopped
|
||||
devices:
|
||||
- /dev/kfd:/dev/kfd
|
||||
- /dev/dri:/dev/dri
|
||||
group_add:
|
||||
- video
|
||||
- render
|
||||
depends_on:
|
||||
- ollama-amd
|
||||
profiles:
|
||||
- amd
|
||||
|
||||
@@ -46,7 +102,14 @@ services:
|
||||
container_name: kipina-node-cpu
|
||||
environment:
|
||||
- HUB_URL=wss://kipina.studio/ws
|
||||
- OLLAMA_URL=http://ollama:11434
|
||||
- OLLAMA_MODEL=qwen2.5-coder:7b
|
||||
- ALLOCATED_GB=2
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- ollama-cpu
|
||||
profiles:
|
||||
- cpu
|
||||
|
||||
volumes:
|
||||
ollama-models:
|
||||
|
||||
@@ -9,26 +9,39 @@ services:
|
||||
volumes:
|
||||
- .:/app
|
||||
# Käännetään aina käynnistyksen yhteydessä varmuuden vuoksi Wasm uusimmista koodeista, ja päälle pyöräytetään Hub!
|
||||
command: bash -c "cd node && wasm-pack build --target web --out-dir ../static/pkg && cd ../hub && cargo run"
|
||||
command: bash -c "cd node && wasm-pack build --release --target web --out-dir ../static/pkg && cd ../hub && cargo run"
|
||||
|
||||
# Valinnainen natiivi-solmu — kerää oikeat laitteistotiedot (nvidia-smi-taso)
|
||||
# Ollama — LLM-inferenssi
|
||||
# NVIDIA: vaihda image → ollama/ollama:latest ja lisää deploy.resources (ks. README)
|
||||
# CPU: vaihda image → ollama/ollama:latest ja poista devices
|
||||
ollama:
|
||||
image: ollama/ollama:rocm
|
||||
container_name: kipina_ollama
|
||||
ports:
|
||||
- "11434:11434"
|
||||
volumes:
|
||||
- ollama-models:/root/.ollama
|
||||
devices:
|
||||
- /dev/kfd
|
||||
- /dev/dri
|
||||
profiles:
|
||||
- native
|
||||
|
||||
# Natiivisolmu — yhdistää hubiin ja käyttää Ollamaa inferenssiin
|
||||
native-node:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.native-node
|
||||
container_name: kipina_native_node
|
||||
environment:
|
||||
- HUB_URL=ws://agentic-poc:3000/ws
|
||||
- HUB_URL=wss://kipina.studio/ws
|
||||
- OLLAMA_URL=http://ollama:11434
|
||||
- OLLAMA_MODEL=qwen2.5-coder:7b
|
||||
- ALLOCATED_GB=4
|
||||
depends_on:
|
||||
- agentic-poc
|
||||
# GPU passthrough (valinnainen — toimii myös ilman)
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: all
|
||||
capabilities: [gpu]
|
||||
- ollama
|
||||
profiles:
|
||||
- native
|
||||
|
||||
volumes:
|
||||
ollama-models:
|
||||
|
||||
3
network-poc/frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.astro/
|
||||
2
network-poc/frontend/astro.config.mjs
Normal file
@@ -0,0 +1,2 @@
|
||||
import { defineConfig } from 'astro/config';
|
||||
export default defineConfig({});
|
||||
4721
network-poc/frontend/package-lock.json
generated
Normal file
13
network-poc/frontend/package.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "kipina-frontend",
|
||||
"type": "module",
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"astro": "^6.1.5"
|
||||
}
|
||||
}
|
||||
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.
|
||||
34
network-poc/frontend/public/avatars/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Kipinä Agentic Playground - Animaatioiden käyttöönotto
|
||||
|
||||
Koska Kipinä-verkon agenttien avatarit tällä erää ovat staattisia PNG-kuvatiedostoja, käyttöliittymä hyödyntää CSS-pohjaista pomppimisilmiötä (sekä pulppuavaa 💬 puhekuplaa) "puhumisen" merkkinä. Olemme kuitenkin koodanneet taustalle piilotetun tuen aivioiduille videoloopeille myöhempää käyttöä varten!
|
||||
|
||||
Näin saat UI:n tukemaan oikeasti animoituja kasvoja/videoita.
|
||||
|
||||
## 1. Luo Animoidut GIF-tiedostot
|
||||
Valitse mikä tahansa ulkoinen AI-työkalu (kuten HeyGen, Pika v1.0, tai Midjourney+Runway yhdistelmä) ja muunna avatar-kuvat (esim. `kettu_notext.png`) 3-5 sekunnin kestäviksi GIF-loopeiksi. Hahmon leuka tulisi pyöriä tai naama vääntyillä puhuessaan.
|
||||
|
||||
## 2. Nimeä Tiedostot Oikein ja Lisää Ne Kansioon
|
||||
Siirrä uudet GIF-animaatiot samaan kansioon alkuperäisten kuvien kanssa. Muuta niiden nimi siten, että se päättyy tunnisteeseen `_puhuva.gif`.
|
||||
|
||||
Esimerkkejä:
|
||||
- Koodari `kipina_notext.png` → `kipina_notext_puhuva.gif`
|
||||
- Manageri `karhunpentu.png` → `karhunpentu_puhuva.gif`
|
||||
- Asiakas `kettu_notext.png` → `kettu_notext_puhuva.gif`
|
||||
|
||||
## 3. Aktivoi Koodi
|
||||
Käännä Kipinä Playground -ohjaimen JavaScript-koodista piilotettu ominaisuus päälle.
|
||||
|
||||
Etsi tiedostosta `../index.html` (noin riviltä 1084, `updatePromptEditor`-funktiosta):
|
||||
```javascript
|
||||
// Piilotettu ominaisuus: Puhuvien videoiden / gif-animaatioiden kytkentä
|
||||
window.USE_ANIMATED_GIFS = false;
|
||||
```
|
||||
Muuta tuo `false` arvoon `true`:
|
||||
```javascript
|
||||
window.USE_ANIMATED_GIFS = true;
|
||||
```
|
||||
|
||||
**Mitä logiikka tekee?**
|
||||
Aina kun valitset agentin kaaviosta, koodi korvaa aktiivisen kuvakkeen lopussa olevan `.png` -päätteen sanalla `_puhuva.gif` – lennosta! Jos poistut agentin valinnasta tai valitset jonkun toisen, koodi vaihtaa kuvan välittömästi takaisin staattiseen `.png`-versioon ja sulkee ilmentymän suun.
|
||||
|
||||
Näin saat kaikkien asiantuntijoiden face-track looppeja hallittua yhdellä kädenkäänteellä.
|
||||
BIN
network-poc/frontend/public/avatars/aikuinen_susi.png
Normal file
|
After Width: | Height: | Size: 696 KiB |
BIN
network-poc/frontend/public/avatars/bear.png
Normal file
|
After Width: | Height: | Size: 757 KiB |
BIN
network-poc/frontend/public/avatars/beaver.png
Normal file
|
After Width: | Height: | Size: 700 KiB |
BIN
network-poc/frontend/public/avatars/chameleon.png
Normal file
|
After Width: | Height: | Size: 731 KiB |
BIN
network-poc/frontend/public/avatars/elephant.png
Normal file
|
After Width: | Height: | Size: 711 KiB |
BIN
network-poc/frontend/public/avatars/gecko.png
Normal file
|
After Width: | Height: | Size: 695 KiB |
BIN
network-poc/frontend/public/avatars/gecko_notext.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
network-poc/frontend/public/avatars/karhunpentu.png
Normal file
|
After Width: | Height: | Size: 432 KiB |
BIN
network-poc/frontend/public/avatars/kettu_notext.png
Normal file
|
After Width: | Height: | Size: 650 KiB |
BIN
network-poc/frontend/public/avatars/kipina_notext.png
Normal file
|
After Width: | Height: | Size: 389 KiB |
BIN
network-poc/frontend/public/avatars/laiskiainen.png
Normal file
|
After Width: | Height: | Size: 596 KiB |
BIN
network-poc/frontend/public/avatars/laiskiainen_notext.png
Normal file
|
After Width: | Height: | Size: 496 KiB |
BIN
network-poc/frontend/public/avatars/lion.png
Normal file
|
After Width: | Height: | Size: 872 KiB |
BIN
network-poc/frontend/public/avatars/mantis.png
Normal file
|
After Width: | Height: | Size: 738 KiB |
BIN
network-poc/frontend/public/avatars/old/forge_hero.png
Normal file
|
After Width: | Height: | Size: 109 KiB |
BIN
network-poc/frontend/public/avatars/old/gecko_hero.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
network-poc/frontend/public/avatars/old/kipina.png
Normal file
|
After Width: | Height: | Size: 3.4 MiB |
BIN
network-poc/frontend/public/avatars/old/serpent_hero.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
network-poc/frontend/public/avatars/owl.png
Normal file
|
After Width: | Height: | Size: 813 KiB |
BIN
network-poc/frontend/public/avatars/penguin.png
Normal file
|
After Width: | Height: | Size: 658 KiB |
BIN
network-poc/frontend/public/avatars/pesukarhu.png
Normal file
|
After Width: | Height: | Size: 593 KiB |
BIN
network-poc/frontend/public/avatars/pesukarhu_notext.png
Normal file
|
After Width: | Height: | Size: 563 KiB |
BIN
network-poc/frontend/public/avatars/serpent.png
Normal file
|
After Width: | Height: | Size: 696 KiB |
BIN
network-poc/frontend/public/avatars/spider.png
Normal file
|
After Width: | Height: | Size: 718 KiB |
BIN
network-poc/frontend/public/avatars/susi_notext.png
Normal file
|
After Width: | Height: | Size: 513 KiB |
BIN
network-poc/frontend/public/avatars/tortoise.png
Normal file
|
After Width: | Height: | Size: 780 KiB |
BIN
network-poc/frontend/public/avatars/walrus.png
Normal file
|
After Width: | Height: | Size: 826 KiB |
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": "List the exact dependencies needed. Use [project.scripts] for run commands."
|
||||
}
|
||||
},
|
||||
"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" rows="8" 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" 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>
|
||||
15
network-poc/frontend/src/components/StatusBar.astro
Normal file
@@ -0,0 +1,15 @@
|
||||
<!-- 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">Alusta</button>
|
||||
</span>
|
||||
</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>
|
||||
1167
network-poc/frontend/src/pages/index.astro
Normal file
198
network-poc/frontend/src/styles/global.css
Normal file
@@ -0,0 +1,198 @@
|
||||
: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;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
|
||||
|
||||
/* Tabs */
|
||||
.tabs { display: flex; gap: 4px; margin-bottom: 16px; }
|
||||
.tab {
|
||||
padding: 8px 16px; border-radius: 6px 6px 0 0; cursor: pointer;
|
||||
border: 1px solid var(--border); border-bottom: none;
|
||||
background: var(--bg); color: #8b949e; font-size: 14px;
|
||||
}
|
||||
.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: 8px 14px; background: var(--bg);
|
||||
border: 1px solid var(--border); border-radius: 6px 6px 0 0;
|
||||
font-family: 'Courier New', monospace; font-size: 13px;
|
||||
}
|
||||
.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: 14px;
|
||||
min-height: 300px; max-height: 60vh; overflow-y: auto;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
.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: #010409; border: 1px solid var(--border); border-top: none;
|
||||
border-radius: 0 0 6px 6px; padding: 8px 12px;
|
||||
font-family: 'Courier New', monospace; font-size: 14px;
|
||||
}
|
||||
.terminal-input {
|
||||
flex: 1; background: transparent; border: none; outline: none;
|
||||
color: var(--green); font-family: inherit; font-size: inherit;
|
||||
}
|
||||
.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: 12px;
|
||||
padding: 6px 6px 4px;
|
||||
text-align: center;
|
||||
width: 72px;
|
||||
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: 50px; height: 50px; border-radius: 12px;
|
||||
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: 10px; 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]
|
||||
name = "hub"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
@@ -16,3 +16,4 @@ futures = "0.3"
|
||||
rusqlite = { version = "0.31", features = ["bundled"] }
|
||||
chrono = "0.4"
|
||||
base64 = "0.22"
|
||||
reqwest = { version = "0.12", features = ["json"] }
|
||||
|
||||
@@ -26,6 +26,29 @@ impl NodeDb {
|
||||
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("
|
||||
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(
|
||||
&self,
|
||||
node_id: u64,
|
||||
|
||||
@@ -39,6 +39,7 @@ struct AppState {
|
||||
ip_connections: Mutex<HashMap<IpAddr, u32>>,
|
||||
node_ips: Mutex<HashMap<u64, IpAddr>>,
|
||||
node_tasks: Mutex<HashMap<u64, String>>, // node_id → selected_task
|
||||
node_types: Mutex<HashMap<u64, String>>, // node_id → "native" | "browser"
|
||||
node_busy: Mutex<std::collections::HashSet<u64>>, // Solmut joilla on aktiivinen tehtävä
|
||||
pending_task_ids: Mutex<std::collections::HashSet<String>>, // Hubin jakamat task_id:t (gamification-validointi)
|
||||
api_rate_limits: Mutex<HashMap<IpAddr, (std::time::Instant, u32)>>, // IP → (ikkuna-alku, pyyntömäärä)
|
||||
@@ -260,6 +261,7 @@ async fn main() {
|
||||
ip_connections: Mutex::new(HashMap::new()),
|
||||
node_ips: Mutex::new(HashMap::new()),
|
||||
node_tasks: Mutex::new(HashMap::new()),
|
||||
node_types: Mutex::new(HashMap::new()),
|
||||
node_busy: Mutex::new(std::collections::HashSet::new()),
|
||||
pending_task_ids: Mutex::new(std::collections::HashSet::new()),
|
||||
api_rate_limits: Mutex::new(HashMap::new()),
|
||||
@@ -328,15 +330,6 @@ async fn main() {
|
||||
let idx = (rng_state as usize) % pairs.len();
|
||||
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![
|
||||
"Tell me a short joke.",
|
||||
"What is WebGPU in one sentence?",
|
||||
@@ -346,33 +339,39 @@ async fn main() {
|
||||
];
|
||||
let llm_idx = (rng_state as usize / 7) % llm_prompts.len();
|
||||
|
||||
// SmolLM-prompt
|
||||
let smollm_msg = serde_json::json!({
|
||||
"type": "llm_prompt",
|
||||
"prompt": llm_prompts[llm_idx],
|
||||
"model": "smollm-135m",
|
||||
});
|
||||
let _ = state_for_task.stats_tx.send(smollm_msg.to_string());
|
||||
// Smart Routing: Lähetetään vain niille, jotka valittuna ja idle
|
||||
let mut sends = Vec::new();
|
||||
{
|
||||
let channels = state_for_task.node_channels.read().await;
|
||||
let tasks = state_for_task.node_tasks.lock().unwrap();
|
||||
let mut busy = state_for_task.node_busy.lock().unwrap();
|
||||
|
||||
// Qwen-prompt (sama prompti, eri malli-tagi)
|
||||
let qwen_msg = serde_json::json!({
|
||||
"type": "llm_prompt",
|
||||
"prompt": llm_prompts[llm_idx],
|
||||
"model": "qwen-05b",
|
||||
});
|
||||
let _ = state_for_task.stats_tx.send(qwen_msg.to_string());
|
||||
for (node_id, task) in tasks.iter() {
|
||||
if !busy.contains(node_id) {
|
||||
// Vapaa node -> lähetetään oikea tehtävä
|
||||
let msg = match task.as_str() {
|
||||
"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" })),
|
||||
"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
|
||||
let phi3_msg = serde_json::json!({
|
||||
"type": "llm_prompt",
|
||||
"prompt": llm_prompts[llm_idx],
|
||||
"model": "phi3-mini",
|
||||
});
|
||||
let _ = state_for_task.stats_tx.send(phi3_msg.to_string());
|
||||
if let Some(payload) = msg {
|
||||
if let Some(ch) = channels.get(node_id) {
|
||||
sends.push((ch.clone(), payload.to_string()));
|
||||
busy.insert(*node_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -382,9 +381,14 @@ async fn main() {
|
||||
.route("/api/pairs", get(api_pairs))
|
||||
.route("/api/stats", get(api_stats))
|
||||
.route("/api/v1/chat/completions", axum::routing::post(api_chat_completions))
|
||||
.route("/api/v1/model", axum::routing::post(api_change_model))
|
||||
.route("/api/v1/hardware", get(api_hardware))
|
||||
.route("/api/v1/ollama/tags", get(api_ollama_tags))
|
||||
.route("/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))
|
||||
.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)))
|
||||
})
|
||||
.with_state(state);
|
||||
@@ -457,6 +461,34 @@ fn admin_unauthorized() -> axum::response::Response {
|
||||
.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 {
|
||||
if !check_admin_auth(&headers) { return admin_unauthorized(); }
|
||||
axum::response::Html(ADMIN_HTML).into_response()
|
||||
@@ -486,16 +518,19 @@ async fn ws_handler(
|
||||
.and_then(|s| s.trim().parse::<IpAddr>().ok())
|
||||
.unwrap_or_else(|| addr.ip());
|
||||
|
||||
// Max 2 yhteyttä per IP (dashboard-UI + selainsolmu)
|
||||
// Max yhteyttä per IP (ei rajoiteta localhost/127.0.0.1)
|
||||
{
|
||||
let conns = state.ip_connections.lock().unwrap();
|
||||
let count = conns.get(&ip).copied().unwrap_or(0);
|
||||
if count >= 4 {
|
||||
tracing::warn!("IP {} ylitti yhteysrajan ({}/4) — estetty", ip, count);
|
||||
return (
|
||||
axum::http::StatusCode::TOO_MANY_REQUESTS,
|
||||
"Max 4 yhteyttä per IP",
|
||||
).into_response();
|
||||
let is_local = ip.is_loopback();
|
||||
if !is_local {
|
||||
let conns = state.ip_connections.lock().unwrap();
|
||||
let count = conns.get(&ip).copied().unwrap_or(0);
|
||||
if count >= 20 {
|
||||
tracing::warn!("IP {} ylitti yhteysrajan ({}/20) — estetty", ip, count);
|
||||
return (
|
||||
axum::http::StatusCode::TOO_MANY_REQUESTS,
|
||||
"Max 20 yhteyttä per IP",
|
||||
).into_response();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -656,6 +691,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 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();
|
||||
map.insert(node_id, allocated);
|
||||
@@ -677,6 +724,7 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
|
||||
state.db.insert_session(node_id, &ip.to_string(), node_type, &json);
|
||||
}
|
||||
state.node_tasks.lock().unwrap().insert(node_id, selected_task);
|
||||
state.node_types.lock().unwrap().insert(node_id, node_type.to_string());
|
||||
|
||||
if node_type == "native" {
|
||||
let sys = json.get("system");
|
||||
@@ -735,6 +783,7 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
|
||||
}
|
||||
broadcast_stats(&state).await;
|
||||
} else if msg_type == "pair_done" {
|
||||
state.node_busy.lock().unwrap().remove(&node_id);
|
||||
{
|
||||
let mut json = json; // Siirretään omistajuus muokkausta varten
|
||||
if let Some(obj) = json.as_object_mut() {
|
||||
@@ -934,6 +983,7 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
|
||||
ips.remove(&node_id);
|
||||
vram.remove(&node_id);
|
||||
}
|
||||
state.node_types.lock().unwrap().remove(&node_id);
|
||||
tracing::info!("Solmu {} ({}) poistui verkosta.", node_id, ip);
|
||||
broadcast_stats(&state).await;
|
||||
sender_task.abort();
|
||||
@@ -943,6 +993,8 @@ struct ChatCompletionRequest {
|
||||
model: String,
|
||||
prompt: String,
|
||||
task_id: String,
|
||||
#[serde(default)]
|
||||
max_tokens: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
@@ -952,6 +1004,78 @@ struct ChatCompletionResponse {
|
||||
tokens_generated: u64,
|
||||
}
|
||||
|
||||
async fn api_ollama_tags() -> axum::response::Response {
|
||||
let ollama_url = std::env::var("OLLAMA_URL").unwrap_or_else(|_| "http://ollama:11434".to_string());
|
||||
match reqwest::get(format!("{}/api/tags", ollama_url)).await {
|
||||
Ok(resp) => {
|
||||
if let Ok(body) = resp.json::<serde_json::Value>().await {
|
||||
axum::Json(body).into_response()
|
||||
} else {
|
||||
axum::Json(serde_json::json!({ "models": [] })).into_response()
|
||||
}
|
||||
}
|
||||
Err(_) => axum::Json(serde_json::json!({ "models": [] })).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn api_hardware(
|
||||
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
|
||||
) -> axum::response::Response {
|
||||
// Etsitään natiivisolmun GPU-tiedot sessiosta
|
||||
let sessions = state.db.get_sessions(50);
|
||||
let native = sessions.iter().find(|s| {
|
||||
s.get("node_type").and_then(|v| v.as_str()) == Some("native")
|
||||
});
|
||||
|
||||
let (mut vram_mb, mut gpu_name, ram_mb) = if let Some(s) = native {
|
||||
let gpus = s.get("gpus").and_then(|v| v.as_array());
|
||||
let gpu = gpus.and_then(|g| g.first());
|
||||
let vram = gpu.and_then(|g| g.get("vram_total_mb")).and_then(|v| v.as_u64()).unwrap_or(0);
|
||||
let name = gpu.and_then(|g| g.get("name")).and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let ram = s.get("system").and_then(|v| v.get("ram_total_mb")).and_then(|v| v.as_u64()).unwrap_or(0);
|
||||
(vram, name, ram)
|
||||
} else {
|
||||
(0, String::new(), 0)
|
||||
};
|
||||
|
||||
// Fallback: kysytään Ollamalta onko malleja ladattu (= Ollama on käynnissä)
|
||||
if vram_mb == 0 {
|
||||
let ollama_url = std::env::var("OLLAMA_URL").unwrap_or_else(|_| "http://ollama:11434".to_string());
|
||||
if let Ok(resp) = reqwest::get(format!("{}/api/tags", ollama_url)).await {
|
||||
if let Ok(body) = resp.json::<serde_json::Value>().await {
|
||||
let models = body["models"].as_array().map(|a| a.len()).unwrap_or(0);
|
||||
if models > 0 {
|
||||
gpu_name = "Ollama (GPU/CPU)".to_string();
|
||||
// Natiivisolmun RAM fallbackina
|
||||
vram_mb = if ram_mb > 0 { ram_mb } else { 0 };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if gpu_name.is_empty() { gpu_name = "ei natiivisolmua".to_string(); }
|
||||
|
||||
axum::Json(serde_json::json!({
|
||||
"gpu_name": gpu_name,
|
||||
"vram_mb": vram_mb,
|
||||
"ram_mb": ram_mb,
|
||||
})).into_response()
|
||||
}
|
||||
|
||||
async fn api_change_model(
|
||||
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
|
||||
axum::Json(payload): axum::Json<serde_json::Value>,
|
||||
) -> axum::response::Response {
|
||||
let model = payload.get("model").and_then(|v| v.as_str()).unwrap_or("");
|
||||
if model.is_empty() {
|
||||
return (axum::http::StatusCode::BAD_REQUEST, "model puuttuu").into_response();
|
||||
}
|
||||
tracing::info!("Mallin vaihto: {}", model);
|
||||
let msg = serde_json::json!({ "type": "change_model", "model": model });
|
||||
let _ = state.stats_tx.send(msg.to_string());
|
||||
axum::Json(serde_json::json!({ "status": "ok", "model": model })).into_response()
|
||||
}
|
||||
|
||||
async fn api_chat_completions(
|
||||
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
|
||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||
@@ -966,105 +1090,71 @@ async fn api_chat_completions(
|
||||
*entry = (now, 1); // Uusi ikkuna
|
||||
} else {
|
||||
entry.1 += 1;
|
||||
if entry.1 > 10 {
|
||||
if entry.1 > 30 {
|
||||
return (axum::http::StatusCode::TOO_MANY_REQUESTS, "Liian monta pyyntöä — yritä minuutin kuluttua").into_response();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Etsitään vapaa tai varattu solmu, joka vastaa pyydettyä mallia
|
||||
let (target_node_free, target_node_any, total_matching) = {
|
||||
// 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)| {
|
||||
if payload.model == "qwen-coder" {
|
||||
*task == "qwen-coder-05b" || *task == "qwen-coder"
|
||||
// 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();
|
||||
let free = matching.iter().find(|id| !busy.contains(id)).copied();
|
||||
let any = matching.first().copied();
|
||||
(free, any, matching.len())
|
||||
// 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())
|
||||
};
|
||||
|
||||
// Broadcastataan reititystila UI:lle
|
||||
let task_id = payload.task_id.clone();
|
||||
|
||||
if target_node_any.is_none() {
|
||||
// Ei yhtään solmua tälle mallille
|
||||
return (axum::http::StatusCode::SERVICE_UNAVAILABLE, "Ei solmua tälle mallille (käynnistä malli selaimessa)").into_response();
|
||||
}
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
let target_node_id;
|
||||
if let Some(free_id) = target_node_free {
|
||||
// Vapaa solmu löytyi — reititetään suoraan
|
||||
target_node_id = free_id;
|
||||
let node_type = if state.node_tasks.lock().unwrap().get(&free_id).map(|t| t.contains("native")).unwrap_or(false) { "natiivi" } else { "selain" };
|
||||
// Reititystila UI:lle
|
||||
{
|
||||
let routing_msg = serde_json::json!({
|
||||
"type": "task_routed",
|
||||
"task_id": task_id,
|
||||
"node_id": free_id,
|
||||
"node_type": node_type,
|
||||
"node_id": target_node_id,
|
||||
"status": "routed",
|
||||
"message": format!("Reititetty solmulle #{}", free_id),
|
||||
"message": format!("Reititetty solmulle #{}", target_node_id),
|
||||
});
|
||||
let _ = state.stats_tx.send(routing_msg.to_string());
|
||||
} else {
|
||||
// Kaikki solmut varattuja — odotetaan vapautumista (max 30s)
|
||||
let queue_msg = serde_json::json!({
|
||||
"type": "task_routed",
|
||||
"task_id": task_id,
|
||||
"status": "queued",
|
||||
"message": format!("Kaikki {} solmua varattuja — odotetaan vapautumista...", total_matching),
|
||||
});
|
||||
let _ = state.stats_tx.send(queue_msg.to_string());
|
||||
|
||||
// Pollaa busy-tilaa 500ms välein, max 30s
|
||||
let mut waited = 0u32;
|
||||
loop {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
|
||||
waited += 500;
|
||||
let free = {
|
||||
let tasks = state.node_tasks.lock().unwrap();
|
||||
let busy = state.node_busy.lock().unwrap();
|
||||
tasks.iter().find(|(node_id, task)| {
|
||||
let model_match = if payload.model == "qwen-coder" {
|
||||
*task == "qwen-coder-05b" || *task == "qwen-coder"
|
||||
} else {
|
||||
**task == payload.model
|
||||
};
|
||||
model_match && !busy.contains(node_id)
|
||||
}).map(|(k, _)| *k)
|
||||
};
|
||||
if let Some(id) = free {
|
||||
target_node_id = id;
|
||||
let routing_msg = serde_json::json!({
|
||||
"type": "task_routed",
|
||||
"task_id": task_id,
|
||||
"node_id": id,
|
||||
"status": "routed",
|
||||
"message": format!("Solmu #{} vapautui — reititetään ({:.1}s jonossa)", id, waited as f64 / 1000.0),
|
||||
});
|
||||
let _ = state.stats_tx.send(routing_msg.to_string());
|
||||
break;
|
||||
}
|
||||
if waited >= 30000 {
|
||||
return (axum::http::StatusCode::SERVICE_UNAVAILABLE, "Aikakatkaisu: kaikki solmut varattuja 30s ajan").into_response();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 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 msg = serde_json::json!({
|
||||
let mut msg = serde_json::json!({
|
||||
"type": "llm_prompt",
|
||||
"prompt": payload.prompt,
|
||||
"model": payload.model,
|
||||
"task_id": payload.task_id,
|
||||
});
|
||||
if let Some(mt) = payload.max_tokens {
|
||||
msg.as_object_mut().unwrap().insert("max_tokens".to_string(), serde_json::json!(mt));
|
||||
}
|
||||
|
||||
// Odotuskanava valmiiksi (solmu palauttaa tuloksen stats_tx kautta)
|
||||
let mut rx = state.stats_tx.subscribe();
|
||||
@@ -1080,7 +1170,7 @@ async fn api_chat_completions(
|
||||
}
|
||||
}
|
||||
|
||||
let timeout = tokio::time::timeout(std::time::Duration::from_secs(120), async move {
|
||||
let timeout = tokio::time::timeout(std::time::Duration::from_secs(600), async move {
|
||||
loop {
|
||||
let msg_str = match rx.recv().await {
|
||||
Ok(msg) => msg,
|
||||
|
||||
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]
|
||||
name = "native-node"
|
||||
version = "0.1.0"
|
||||
version = "0.2.2"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
default = ["gpu-detect"]
|
||||
gpu-detect = ["nvml-wrapper", "wgpu"]
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1.36", features = ["full"] }
|
||||
tokio-tungstenite = { version = "0.21", features = ["native-tls"] }
|
||||
@@ -10,12 +14,8 @@ futures-util = "0.3"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
sysinfo = "0.30"
|
||||
nvml-wrapper = "0.10"
|
||||
wgpu = "24"
|
||||
candle-core = { version = "0.8", features = ["cuda"] }
|
||||
candle-nn = "0.8"
|
||||
candle-transformers = "0.8"
|
||||
hf-hub = "0.4"
|
||||
tokenizers = "0.19"
|
||||
nvml-wrapper = { version = "0.10", optional = true }
|
||||
wgpu = { version = "24", optional = true }
|
||||
reqwest = { version = "0.12", features = ["json"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
@@ -1,261 +1,144 @@
|
||||
use candle_core::{Device, Tensor, DType};
|
||||
use candle_nn::VarBuilder;
|
||||
use candle_transformers::models::qwen2::{Config as QwenConfig, ModelForCausalLM as QwenModel};
|
||||
use hf_hub::{api::sync::Api, Repo, RepoType};
|
||||
use std::time::Instant;
|
||||
|
||||
/// Top-k sampling with temperature and repetition penalty
|
||||
fn sample_top_k(logits: &Tensor, k: usize, temperature: f64, generated_tokens: &[u32], repetition_penalty: f64, rng_state: &mut u64) -> Result<u32, String> {
|
||||
let mut logits_vec: Vec<f32> = logits.to_vec1::<f32>().map_err(|e| format!("to_vec1: {}", e))?;
|
||||
if logits_vec.is_empty() { return Err("Tyhjä logits".to_string()); }
|
||||
|
||||
// Repetition penalty: rankaisee jo generoituja tokeneita
|
||||
for &token_id in generated_tokens {
|
||||
if (token_id as usize) < logits_vec.len() {
|
||||
let logit = &mut logits_vec[token_id as usize];
|
||||
if *logit > 0.0 {
|
||||
*logit /= repetition_penalty as f32;
|
||||
} else {
|
||||
*logit *= repetition_penalty as f32;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Temperature scaling
|
||||
if temperature > 0.0 && temperature != 1.0 {
|
||||
for logit in logits_vec.iter_mut() {
|
||||
*logit /= temperature as f32;
|
||||
}
|
||||
}
|
||||
|
||||
// Top-k: etsitään k suurinta
|
||||
let mut indexed: Vec<(usize, f32)> = logits_vec.iter().enumerate().map(|(i, &v)| (i, v)).collect();
|
||||
indexed.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
|
||||
indexed.truncate(k);
|
||||
|
||||
if k == 1 || temperature == 0.0 {
|
||||
return Ok(indexed[0].0 as u32);
|
||||
}
|
||||
|
||||
// Softmax top-k:lle
|
||||
let max_logit = indexed[0].1;
|
||||
let exps: Vec<f32> = indexed.iter().map(|x| (x.1 - max_logit).exp()).collect();
|
||||
let sum: f32 = exps.iter().sum();
|
||||
let probs: Vec<f32> = exps.iter().map(|e| e / sum).collect();
|
||||
|
||||
// XorShift64 RNG
|
||||
*rng_state ^= *rng_state << 13;
|
||||
*rng_state ^= *rng_state >> 7;
|
||||
*rng_state ^= *rng_state << 17;
|
||||
let rand_val = (*rng_state % 10000) as f32 / 10000.0;
|
||||
|
||||
let mut cumulative = 0.0;
|
||||
for (i, p) in probs.iter().enumerate() {
|
||||
cumulative += p;
|
||||
if rand_val < cumulative {
|
||||
return Ok(indexed[i].0 as u32);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(indexed[0].0 as u32)
|
||||
}
|
||||
use std::cell::RefCell;
|
||||
|
||||
pub struct LlmEngine {
|
||||
tokenizer: tokenizers::Tokenizer,
|
||||
model: QwenModel,
|
||||
device: Device,
|
||||
eos_token: u32,
|
||||
ollama_url: String,
|
||||
model: RefCell<String>,
|
||||
client: reqwest::Client,
|
||||
}
|
||||
|
||||
impl LlmEngine {
|
||||
pub fn load() -> Result<Self, String> {
|
||||
let device = Device::cuda_if_available(0).map_err(|e| format!("Device: {}", e))?;
|
||||
let device_name = if device.is_cuda() { "CUDA" } else { "CPU" };
|
||||
tracing::info!("LLM device: {}", device_name);
|
||||
pub async fn load() -> Result<Self, String> {
|
||||
let model = std::env::var("OLLAMA_MODEL").unwrap_or_else(|_| "qwen2.5-coder:3b".to_string());
|
||||
|
||||
let dtype = if device.is_cuda() { DType::F16 } else { DType::F32 };
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(600))
|
||||
.connect_timeout(std::time::Duration::from_secs(3))
|
||||
.build()
|
||||
.map_err(|e| format!("HTTP client: {}", e))?;
|
||||
|
||||
tracing::info!("Ladataan Qwen2.5-Coder-0.5B-Instruct...");
|
||||
let api = Api::new().map_err(|e| format!("HF API: {}", e))?;
|
||||
let repo = api.repo(Repo::with_revision(
|
||||
"Qwen/Qwen2.5-Coder-0.5B-Instruct".to_string(),
|
||||
RepoType::Model,
|
||||
"main".to_string(),
|
||||
));
|
||||
|
||||
let tokenizer_path = repo.get("tokenizer.json").map_err(|e| format!("Tokenizer lataus: {}", e))?;
|
||||
let model_path = repo.get("model.safetensors").map_err(|e| format!("Malli lataus: {}", e))?;
|
||||
|
||||
tracing::info!("Ladataan tokenizer: {:?}", tokenizer_path);
|
||||
let tokenizer = tokenizers::Tokenizer::from_file(&tokenizer_path)
|
||||
.map_err(|e| format!("Tokenizer: {}", e))?;
|
||||
|
||||
let config = QwenConfig {
|
||||
vocab_size: 151936,
|
||||
hidden_size: 896,
|
||||
intermediate_size: 4864,
|
||||
num_hidden_layers: 24,
|
||||
num_attention_heads: 14,
|
||||
num_key_value_heads: 2,
|
||||
max_position_embeddings: 32768,
|
||||
sliding_window: 32768,
|
||||
max_window_layers: 21,
|
||||
tie_word_embeddings: true,
|
||||
rope_theta: 1000000.0,
|
||||
rms_norm_eps: 1e-6,
|
||||
use_sliding_window: false,
|
||||
hidden_act: candle_nn::Activation::Silu,
|
||||
};
|
||||
|
||||
let start = Instant::now();
|
||||
let vb = unsafe {
|
||||
VarBuilder::from_mmaped_safetensors(&[model_path.clone()], dtype, &device)
|
||||
.map_err(|e| format!("VarBuilder: {}", e))?
|
||||
};
|
||||
let model = QwenModel::new(&config, vb).map_err(|e| format!("Malli: {}", e))?;
|
||||
tracing::info!("Malli ladattu ({:.1}s) — {}", start.elapsed().as_secs_f64(), device_name);
|
||||
|
||||
Ok(LlmEngine {
|
||||
tokenizer,
|
||||
model,
|
||||
device,
|
||||
eos_token: 151645,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn generate(&mut self, prompt: &str, max_tokens: usize) -> Result<GenerateResult, String> {
|
||||
// Prefill: aloitetaan vastaus ```-koodiblokkilla → malli jatkaa suoraan koodilla
|
||||
let formatted = format!("<|im_start|>system\nYou are a coding assistant. Respond with ONLY code. No explanations, no markdown, no comments unless asked.<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n```\n", prompt);
|
||||
|
||||
let encoding = self.tokenizer.encode(formatted.as_str(), true)
|
||||
.map_err(|e| format!("Encode: {}", e))?;
|
||||
let input_ids: Vec<u32> = encoding.get_ids().to_vec();
|
||||
let input_len = input_ids.len();
|
||||
|
||||
// Nollataan KV-cache edellisestä promptista
|
||||
self.model.clear_kv_cache();
|
||||
|
||||
// Sampling-parametrit
|
||||
let temperature = 0.7;
|
||||
let top_k = 40;
|
||||
let repetition_penalty = 1.15;
|
||||
let mut rng_state: u64 = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos() as u64;
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
// Prefill
|
||||
let input = Tensor::new(input_ids.as_slice(), &self.device)
|
||||
.and_then(|t| t.unsqueeze(0))
|
||||
.map_err(|e| format!("Tensor: {}", e))?;
|
||||
|
||||
let logits = self.model.forward(&input, 0)
|
||||
.map_err(|e| format!("Forward prefill: {}", e))?;
|
||||
|
||||
let logits = logits.squeeze(0).map_err(|e| format!("Squeeze: {}", e))?;
|
||||
let logits = if logits.dims().len() == 2 {
|
||||
let seq_len = logits.dim(0).map_err(|e| format!("Dim: {}", e))?;
|
||||
if seq_len == 0 { return Err("Tyhjä tensori".to_string()); }
|
||||
logits.get(seq_len - 1).map_err(|e| format!("Get: {}", e))?
|
||||
// Jos OLLAMA_URL on asetettu, käytetään sitä suoraan
|
||||
let ollama_url = if let Ok(url) = std::env::var("OLLAMA_URL") {
|
||||
tracing::info!("Ollama backend (env): {}", url);
|
||||
url
|
||||
} else {
|
||||
logits
|
||||
};
|
||||
|
||||
let mut generated_text = String::new();
|
||||
let mut tokens_generated: usize = 0;
|
||||
let mut all_tokens: Vec<u32> = Vec::new();
|
||||
|
||||
let mut next_token = sample_top_k(&logits, top_k, temperature, &all_tokens, repetition_penalty, &mut rng_state)?;
|
||||
|
||||
if next_token != self.eos_token {
|
||||
if let Ok(text) = self.tokenizer.decode(&[next_token], true) {
|
||||
generated_text.push_str(&text);
|
||||
}
|
||||
all_tokens.push(next_token);
|
||||
tokens_generated += 1;
|
||||
}
|
||||
|
||||
// Autoregressive
|
||||
let mut pos = input_len;
|
||||
for _ in 1..max_tokens {
|
||||
if next_token == self.eos_token { break; }
|
||||
|
||||
let input = Tensor::new(&[next_token], &self.device)
|
||||
.and_then(|t| t.unsqueeze(0))
|
||||
.map_err(|e| format!("Tensor: {}", e))?;
|
||||
|
||||
let logits = self.model.forward(&input, pos)
|
||||
.map_err(|e| format!("Forward pos {}: {}", pos, e))?;
|
||||
|
||||
let logits = logits.squeeze(0).map_err(|e| format!("Squeeze: {}", e))?;
|
||||
let logits = if logits.dims().len() == 2 {
|
||||
let seq_len = logits.dim(0).map_err(|e| format!("Dim: {}", e))?;
|
||||
if seq_len == 0 { break; }
|
||||
logits.get(seq_len - 1).map_err(|e| format!("Get: {}", e))?
|
||||
} else {
|
||||
logits
|
||||
};
|
||||
next_token = sample_top_k(&logits, top_k, temperature, &all_tokens, repetition_penalty, &mut rng_state)?;
|
||||
pos += 1;
|
||||
|
||||
if next_token == self.eos_token { break; }
|
||||
|
||||
if let Ok(text) = self.tokenizer.decode(&[next_token], true) {
|
||||
generated_text.push_str(&text);
|
||||
|
||||
// Stop-sekvenssit: katkaistaan kun malli alkaa selittää
|
||||
let lower = generated_text.to_lowercase();
|
||||
if lower.contains("\n###") || lower.contains("\nexplanation") || lower.contains("\nnote:") || lower.contains("\noutput:") || lower.contains("\n```\n\n") || lower.contains("\n// example") || lower.contains("\n# example") {
|
||||
for stop in &["\n###", "\nExplanation", "\nNote:", "\nOutput:", "\n```\n\n", "\n// Example", "\n// example", "\n# Example", "\n# example"] {
|
||||
if let Some(pos) = generated_text.find(stop) {
|
||||
generated_text.truncate(pos);
|
||||
}
|
||||
// Haistellaan Ollamaa tunnetuista osoitteista
|
||||
let candidates = [
|
||||
"http://localhost:11434",
|
||||
"http://127.0.0.1:11434",
|
||||
"http://ollama:11434",
|
||||
"http://host.docker.internal:11434",
|
||||
];
|
||||
let mut found = None;
|
||||
for url in &candidates {
|
||||
let probe = reqwest::Client::builder()
|
||||
.connect_timeout(std::time::Duration::from_secs(2))
|
||||
.build().unwrap_or(client.clone());
|
||||
if let Ok(resp) = probe.get(format!("{}/api/version", url)).send().await {
|
||||
if resp.status().is_success() {
|
||||
tracing::info!("Ollama löytyi osoitteesta: {}", url);
|
||||
found = Some(url.to_string());
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
all_tokens.push(next_token);
|
||||
tokens_generated += 1;
|
||||
found.unwrap_or_else(|| {
|
||||
tracing::warn!("Ollamaa ei löytynyt — käytetään oletusta http://localhost:11434");
|
||||
"http://localhost:11434".to_string()
|
||||
})
|
||||
};
|
||||
|
||||
tracing::info!("Ollama backend: {} | malli: {}", ollama_url, model);
|
||||
Ok(LlmEngine { ollama_url, model: RefCell::new(model), client })
|
||||
}
|
||||
|
||||
pub fn model_name(&self) -> String {
|
||||
self.model.borrow().clone()
|
||||
}
|
||||
|
||||
pub fn set_model(&self, new_model: String) {
|
||||
*self.model.borrow_mut() = new_model;
|
||||
}
|
||||
|
||||
/// Varmistaa että malli on ladattu Ollamaan (ollama pull)
|
||||
pub async fn ensure_model(&self) -> Result<(), String> {
|
||||
let model = self.model.borrow().clone();
|
||||
tracing::info!("Tarkistetaan malli {}...", model);
|
||||
let resp = self.client.post(format!("{}/api/pull", self.ollama_url))
|
||||
.json(&serde_json::json!({ "name": model, "stream": false }))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Ollama pull: {}", e))?;
|
||||
|
||||
if resp.status().is_success() {
|
||||
tracing::info!("Malli {} valmis", model);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("Ollama pull epäonnistui: {}", resp.status()))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn generate(&self, prompt: &str, max_tokens: usize) -> Result<GenerateResult, String> {
|
||||
// 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 resp = self.client.post(format!("{}/api/generate", self.ollama_url))
|
||||
.json(&serde_json::json!({
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"stream": false,
|
||||
"options": {
|
||||
"num_predict": max_tokens,
|
||||
"temperature": 0.7,
|
||||
"top_k": 40,
|
||||
"repeat_penalty": 1.15,
|
||||
"stop": ["<|im_end|>", "\n###", "\nExplanation", "\nNote:", "\nPlease note", "\nThis is", "\n```\n\n", "\n// Example", "\n# Example"]
|
||||
}
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Ollama generate: {}", e))?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
return Err(format!("Ollama HTTP {}", resp.status()));
|
||||
}
|
||||
|
||||
let gen_time = start.elapsed();
|
||||
let tokens_per_sec = if gen_time.as_secs_f64() > 0.0 {
|
||||
tokens_generated as f64 / gen_time.as_secs_f64()
|
||||
let body: serde_json::Value = resp.json().await
|
||||
.map_err(|e| format!("Ollama JSON: {}", e))?;
|
||||
|
||||
let text = body["response"].as_str().unwrap_or("").to_string();
|
||||
let total_duration_ns = body["total_duration"].as_u64().unwrap_or(0);
|
||||
let eval_count = body["eval_count"].as_u64().unwrap_or(0) as usize;
|
||||
let eval_duration_ns = body["eval_duration"].as_u64().unwrap_or(1);
|
||||
|
||||
let duration_ms = start.elapsed().as_millis() as f64;
|
||||
let tokens_per_sec = if eval_duration_ns > 0 {
|
||||
eval_count as f64 / (eval_duration_ns as f64 / 1_000_000_000.0)
|
||||
} else { 0.0 };
|
||||
|
||||
Ok(GenerateResult {
|
||||
text: strip_markdown_wrapper(&generated_text),
|
||||
tokens_generated,
|
||||
duration_ms: gen_time.as_millis() as f64,
|
||||
text: strip_code_fences(&text),
|
||||
tokens_generated: eval_count,
|
||||
duration_ms,
|
||||
tokens_per_sec,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const LANG_TAGS: &[&str] = &[
|
||||
"python", "py", "rust", "rs", "javascript", "js", "typescript", "ts",
|
||||
"java", "kotlin", "scala", "go", "ruby", "rb", "php", "swift",
|
||||
"c", "cpp", "c++", "c#", "csharp", "r", "sql", "bash", "sh", "zsh",
|
||||
"html", "css", "json", "yaml", "yml", "toml", "xml", "markdown", "md",
|
||||
"lua", "perl", "dart", "elixir", "haskell", "hs", "ocaml", "zig",
|
||||
"plaintext", "text", "txt",
|
||||
];
|
||||
|
||||
/// Siivoa mallin tuottama vastaus (prefill-yhteensopiva).
|
||||
fn strip_markdown_wrapper(text: &str) -> String {
|
||||
/// Siivoa markdown-koodiblokki-merkit ja selitystekstit
|
||||
fn strip_code_fences(text: &str) -> String {
|
||||
let mut result = text.trim().to_string();
|
||||
|
||||
// 1. Kielitunniste — VAIN tunnettu kieli
|
||||
if let Some(nl) = result.find('\n') {
|
||||
let first = result[..nl].trim().to_lowercase();
|
||||
if LANG_TAGS.contains(&first.as_str()) {
|
||||
// Poista aloittava ```lang
|
||||
if result.starts_with("```") {
|
||||
if let Some(nl) = result.find('\n') {
|
||||
result = result[nl + 1..].to_string();
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Sulkeva ``` — VAIN omalla rivillään lopussa
|
||||
// Poista sulkeva ```
|
||||
let trimmed = result.trim_end();
|
||||
if trimmed.ends_with("```") {
|
||||
let before = &trimmed[..trimmed.len() - 3];
|
||||
@@ -264,29 +147,26 @@ fn strip_markdown_wrapper(text: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Johdantolauseet
|
||||
let lower = result.trim().to_lowercase();
|
||||
// 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(); }
|
||||
if let Some(nl) = result.find('\n') {
|
||||
result = result[nl + 1..].to_string();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Selityskommentit alusta
|
||||
let mut lines: Vec<&str> = result.trim().lines().collect();
|
||||
while !lines.is_empty() {
|
||||
let first = lines[0].trim();
|
||||
let is_preamble = first.starts_with("# ") && !first.starts_with("#!")
|
||||
&& (first.to_lowercase().contains("this is")
|
||||
|| first.to_lowercase().contains("simple")
|
||||
|| first.to_lowercase().contains("program that")
|
||||
|| first.to_lowercase().contains("here is")
|
||||
|| first.to_lowercase().contains("the following")
|
||||
|| first.to_lowercase().contains("below"));
|
||||
if is_preamble { lines.remove(0); } else { break; }
|
||||
}
|
||||
lines.join("\n").trim().to_string()
|
||||
result.trim().to_string()
|
||||
}
|
||||
|
||||
pub struct GenerateResult {
|
||||
|
||||
@@ -33,6 +33,7 @@ impl GpuInfo {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "gpu-detect")]
|
||||
/// Tunnistaa kaikki GPU:t wgpu:lla (NVIDIA/AMD/Apple/Intel)
|
||||
fn collect_gpus_wgpu() -> Vec<GpuInfo> {
|
||||
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
|
||||
@@ -84,6 +85,7 @@ fn collect_gpus_wgpu() -> Vec<GpuInfo> {
|
||||
gpus
|
||||
}
|
||||
|
||||
#[cfg(feature = "gpu-detect")]
|
||||
/// Täydentää NVIDIA-GPU:iden tiedot NVML:llä (VRAM, lämpötila, kuormitus)
|
||||
fn enrich_nvidia_gpus(gpus: &mut [GpuInfo]) {
|
||||
let Ok(nvml) = nvml_wrapper::Nvml::init() else { return };
|
||||
@@ -109,6 +111,7 @@ fn enrich_nvidia_gpus(gpus: &mut [GpuInfo]) {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "gpu-detect")]
|
||||
/// AMD GPU-tiedot Linuxin sysfs:stä (/sys/class/drm/)
|
||||
fn enrich_amd_gpus(gpus: &mut [GpuInfo]) {
|
||||
let Ok(entries) = std::fs::read_dir("/sys/class/drm") else { return };
|
||||
@@ -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> {
|
||||
std::fs::read_to_string(path).ok()?.trim().parse().ok()
|
||||
}
|
||||
|
||||
#[cfg(feature = "gpu-detect")]
|
||||
fn find_hwmon_temp(device_path: &std::path::Path) -> Option<u64> {
|
||||
let hwmon_dir = device_path.join("hwmon");
|
||||
let entries = std::fs::read_dir(&hwmon_dir).ok()?;
|
||||
@@ -166,8 +171,8 @@ fn find_hwmon_temp(device_path: &std::path::Path) -> Option<u64> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(feature = "gpu-detect")]
|
||||
/// Apple GPU-tiedot — wgpu/Metal antaa nimen, tarkempaa dataa ei saa ilman IOKit:ia
|
||||
/// mutta Metal adapter_info sisältää jo olennaiset tiedot
|
||||
fn enrich_apple_gpus(gpus: &mut [GpuInfo]) {
|
||||
// Apple Silicon -koneiden unified memory: koko RAM on GPU:n käytettävissä
|
||||
// Arvioidaan system RAM:sta
|
||||
@@ -187,13 +192,18 @@ fn enrich_apple_gpus(gpus: &mut [GpuInfo]) {
|
||||
|
||||
/// Kerää kaikki GPU:t ja täydentää valmistajakohtaiset tiedot
|
||||
fn collect_all_gpus() -> Vec<GpuInfo> {
|
||||
let mut gpus = collect_gpus_wgpu();
|
||||
|
||||
enrich_nvidia_gpus(&mut gpus);
|
||||
enrich_amd_gpus(&mut gpus);
|
||||
enrich_apple_gpus(&mut gpus);
|
||||
|
||||
gpus
|
||||
#[cfg(feature = "gpu-detect")]
|
||||
{
|
||||
let mut gpus = collect_gpus_wgpu();
|
||||
enrich_nvidia_gpus(&mut gpus);
|
||||
enrich_amd_gpus(&mut gpus);
|
||||
enrich_apple_gpus(&mut gpus);
|
||||
return gpus;
|
||||
}
|
||||
#[cfg(not(feature = "gpu-detect"))]
|
||||
{
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Kerää järjestelmätiedot (CPU, RAM, OS)
|
||||
@@ -222,15 +232,21 @@ fn build_auth_message(allocated_gb: u32) -> String {
|
||||
v
|
||||
}).collect();
|
||||
|
||||
let api_key = std::env::var("NODE_API_KEY").unwrap_or_default();
|
||||
|
||||
let mut msg = json!({
|
||||
"type": "auth",
|
||||
"status": "agent_ready",
|
||||
"node_type": "native",
|
||||
"allocated_gb": allocated_gb,
|
||||
"selected_task": "qwen-coder-05b",
|
||||
"selected_task": "qwen2.5-coder:7b",
|
||||
"system": sys,
|
||||
});
|
||||
|
||||
if !api_key.is_empty() {
|
||||
msg.as_object_mut().unwrap().insert("api_key".to_string(), json!(api_key));
|
||||
}
|
||||
|
||||
if !gpu_json.is_empty() {
|
||||
msg.as_object_mut().unwrap().insert("gpus".to_string(), json!(gpu_json));
|
||||
}
|
||||
@@ -269,6 +285,9 @@ async fn main() {
|
||||
|
||||
let gpus = collect_all_gpus();
|
||||
if gpus.is_empty() {
|
||||
#[cfg(not(feature = "gpu-detect"))]
|
||||
tracing::info!("GPU-tunnistus ei käytössä (--no-default-features). Ollama käyttää GPU:ta automaattisesti jos saatavilla.");
|
||||
#[cfg(feature = "gpu-detect")]
|
||||
tracing::info!("GPU:ta ei havaittu — toimitaan CPU-moodissa");
|
||||
} else {
|
||||
for (i, gpu) in gpus.iter().enumerate() {
|
||||
@@ -285,15 +304,19 @@ async fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Ladataan LLM-malli
|
||||
tracing::info!("Ladataan LLM-mallia...");
|
||||
let mut llm = match inference::LlmEngine::load() {
|
||||
// Ollama-backend
|
||||
tracing::info!("Alustetaan Ollama-yhteyttä...");
|
||||
let llm = match inference::LlmEngine::load().await {
|
||||
Ok(engine) => {
|
||||
tracing::info!("LLM valmis inferenssiin!");
|
||||
// Varmistetaan malli (ollama pull) — odotetaan kunnes valmis
|
||||
match engine.ensure_model().await {
|
||||
Ok(()) => tracing::info!("Ollama valmis inferenssiin!"),
|
||||
Err(e) => tracing::warn!("Mallin lataus: {} — yritetään silti", e),
|
||||
}
|
||||
Some(engine)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("LLM-lataus epäonnistui: {} — toimitaan ilman inferenssiä", e);
|
||||
tracing::warn!("Ollama-alustus epäonnistui: {} — toimitaan ilman inferenssiä", e);
|
||||
None
|
||||
}
|
||||
};
|
||||
@@ -311,24 +334,23 @@ async fn main() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut busy = false;
|
||||
|
||||
while let Some(Ok(msg)) = read.next().await {
|
||||
if let Message::Text(text) = msg {
|
||||
// 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) {
|
||||
let prompt = task.get("prompt").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let task_id = task.get("task_id").and_then(|v| v.as_str()).unwrap_or("?");
|
||||
let msg_model = task.get("model").and_then(|v| v.as_str()).unwrap_or("");
|
||||
|
||||
if !prompt.is_empty() && msg_model.starts_with("qwen-coder") {
|
||||
if !prompt.is_empty() && (msg_model.starts_with("qwen-coder") || msg_model.starts_with("qwen2.5-coder")) {
|
||||
|
||||
if let Some(ref mut engine) = llm {
|
||||
busy = true;
|
||||
tracing::info!("Generoidaan (task_id: {}): \"{}\"", task_id, prompt);
|
||||
if let Some(ref engine) = llm {
|
||||
let max_tokens = task.get("max_tokens").and_then(|v| v.as_u64()).unwrap_or(1024) as usize;
|
||||
tracing::info!("Generoidaan (task_id: {}, max_tokens: {}): \"{}\"", task_id, max_tokens, &prompt[..prompt.len().min(100)]);
|
||||
|
||||
match engine.generate(prompt, 64) {
|
||||
let model_name = engine.model_name();
|
||||
match engine.generate(prompt, max_tokens).await {
|
||||
Ok(result) => {
|
||||
tracing::info!(
|
||||
"Tulos: {} tokenia | {:.0}ms | {:.1} tok/s | \"{}\"",
|
||||
@@ -341,7 +363,7 @@ async fn main() {
|
||||
let done = json!({
|
||||
"type": "llm_done",
|
||||
"prompt": prompt,
|
||||
"model": "Qwen2.5-Coder-0.5B (native/GPU)",
|
||||
"model": format!("{} (Ollama)", model_name),
|
||||
"response": result.text,
|
||||
"tokens_generated": result.tokens_generated,
|
||||
"duration_ms": result.duration_ms,
|
||||
@@ -355,12 +377,25 @@ async fn main() {
|
||||
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...");
|
||||
|
||||
@@ -38,17 +38,50 @@ pub fn set_gpu_load(load: u32) {
|
||||
console_log!("[Wasm] GPU Kuormitusraja vaihdettu -> {}%", load);
|
||||
}
|
||||
|
||||
// Asynkroninen odotus WebAssemblylle
|
||||
async fn sleep_ms(ms: i32) {
|
||||
// Worker-yhteensopiva setTimeout — toimii sekä Window- että Worker-kontekstissa
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
#[wasm_bindgen(js_name = setTimeout)]
|
||||
fn set_timeout(closure: &js_sys::Function, ms: i32);
|
||||
}
|
||||
|
||||
// Asynkroninen odotus WebAssemblylle (Window + Worker)
|
||||
pub async fn sleep_ms(ms: i32) {
|
||||
let promise = js_sys::Promise::new(&mut |resolve, _| {
|
||||
web_sys::window()
|
||||
.unwrap()
|
||||
.set_timeout_with_callback_and_timeout_and_arguments_0(&resolve, ms)
|
||||
.unwrap();
|
||||
set_timeout(&resolve, ms);
|
||||
});
|
||||
let _ = wasm_bindgen_futures::JsFuture::from(promise).await;
|
||||
}
|
||||
|
||||
// Worker-yhteensopiva Performance — käyttää globalThis.performance
|
||||
pub fn perf_now() -> f64 {
|
||||
js_sys::Reflect::get(&js_sys::global(), &"performance".into())
|
||||
.ok()
|
||||
.and_then(|p| js_sys::Reflect::get(&p, &"now".into()).ok())
|
||||
.and_then(|f| f.dyn_into::<js_sys::Function>().ok())
|
||||
.and_then(|f| {
|
||||
let perf = js_sys::Reflect::get(&js_sys::global(), &"performance".into()).unwrap();
|
||||
f.call0(&perf).ok()
|
||||
})
|
||||
.and_then(|v| v.as_f64())
|
||||
.unwrap_or(0.0)
|
||||
}
|
||||
|
||||
// Worker-yhteensopiva fetch — käyttää globalThis.fetch
|
||||
pub async fn worker_fetch(url: &str) -> Result<web_sys::Response, String> {
|
||||
let promise = js_sys::Reflect::get(&js_sys::global(), &"fetch".into())
|
||||
.map_err(|_| "fetch ei saatavilla".to_string())?
|
||||
.dyn_into::<js_sys::Function>()
|
||||
.map_err(|_| "fetch ei funktio".to_string())?
|
||||
.call1(&JsValue::NULL, &url.into())
|
||||
.map_err(|e| format!("fetch: {:?}", e))?;
|
||||
let resp = wasm_bindgen_futures::JsFuture::from(js_sys::Promise::from(promise))
|
||||
.await
|
||||
.map_err(|e| format!("fetch await: {:?}", e))?;
|
||||
resp.dyn_into::<web_sys::Response>()
|
||||
.map_err(|_| "ei Response".to_string())
|
||||
}
|
||||
|
||||
// Geneerinen tensorilaskenta — toimii millä tahansa Burn-backendillä
|
||||
fn run_matmul<B: burn::tensor::backend::Backend>(size: usize) -> String {
|
||||
let device = Default::default();
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
/// 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
|
||||
fn tokenize_text(tokenizer: &tokenizers::Tokenizer, text: &str) -> serde_json::Value {
|
||||
let char_count = text.chars().count();
|
||||
@@ -123,10 +177,9 @@ async fn run_single_tokenize(text: String, ws: Rc<RefCell<WebSocket>>) {
|
||||
let Some(bytes) = cached_tok else { return; };
|
||||
let Ok(tokenizer) = tokenizers::Tokenizer::from_bytes(&bytes) else { return; };
|
||||
|
||||
let perf = web_sys::window().unwrap().performance().unwrap();
|
||||
let start = perf.now();
|
||||
let start = perf_now();
|
||||
let result = tokenize_text(&tokenizer, &text);
|
||||
let duration_ms = perf.now() - start;
|
||||
let duration_ms = perf_now() - start;
|
||||
|
||||
let token_count = result["token_count"].as_u64().unwrap_or(0);
|
||||
let cpt = result["chars_per_token"].as_f64().unwrap_or(0.0);
|
||||
@@ -157,11 +210,10 @@ async fn run_pair_comparison(en_text: String, fi_text: String, ws: Rc<RefCell<We
|
||||
return;
|
||||
};
|
||||
|
||||
let perf = web_sys::window().unwrap().performance().unwrap();
|
||||
let start_time = perf.now();
|
||||
let start_time = perf_now();
|
||||
let en_result = tokenize_text(&tokenizer, &en_text);
|
||||
let fi_result = tokenize_text(&tokenizer, &fi_text);
|
||||
let duration_ms = perf.now() - start_time; // millisekunteja desimaalitarkkuudella
|
||||
let duration_ms = perf_now() - start_time;
|
||||
|
||||
let en_cpt = en_result["chars_per_token"].as_f64().unwrap_or(0.0);
|
||||
let fi_cpt = fi_result["chars_per_token"].as_f64().unwrap_or(0.0);
|
||||
|
||||
@@ -24,10 +24,7 @@ async fn ensure_cached(key: &str, url: &str, ws: &Rc<RefCell<WebSocket>>) -> Res
|
||||
|
||||
console_log!("[Qwen] Ladataan {}...", key);
|
||||
|
||||
let window = web_sys::window().unwrap();
|
||||
let resp_val = wasm_bindgen_futures::JsFuture::from(window.fetch_with_str(url))
|
||||
.await.map_err(|e| format!("Fetch epäonnistui: {:?}", e))?;
|
||||
let resp: web_sys::Response = resp_val.dyn_into().map_err(|_| "Ei Response".to_string())?;
|
||||
let resp = crate::worker_fetch(url).await?;
|
||||
if !resp.ok() { return Err(format!("HTTP {}", resp.status())); }
|
||||
|
||||
let total_size: usize = resp.headers()
|
||||
@@ -71,7 +68,7 @@ async fn ensure_cached(key: &str, url: &str, ws: &Rc<RefCell<WebSocket>>) -> Res
|
||||
}
|
||||
|
||||
pub async fn run_qwen_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
|
||||
let perf = web_sys::window().unwrap().performance().unwrap();
|
||||
// performance via crate::perf_now()
|
||||
|
||||
let tok_bytes = match ensure_cached("qwen05b-tokenizer.json", TOKENIZER_URL, &ws).await {
|
||||
Ok(b) => b,
|
||||
@@ -88,7 +85,7 @@ pub async fn run_qwen_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
|
||||
};
|
||||
|
||||
console_log!("[Qwen] Rakennetaan mallia...");
|
||||
let start_load = perf.now();
|
||||
let start_load = crate::perf_now();
|
||||
let device = Device::Cpu;
|
||||
let dtype = DType::F32;
|
||||
|
||||
@@ -120,7 +117,7 @@ pub async fn run_qwen_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
|
||||
Err(e) => { console_log!("[Qwen] Mallin lataus: {}", e); return; }
|
||||
};
|
||||
|
||||
let load_time = perf.now() - start_load;
|
||||
let load_time = crate::perf_now() - start_load;
|
||||
console_log!("[Qwen] Malli ladattu ({:.0}ms). Generoidaan...", load_time);
|
||||
|
||||
let encoding = match tokenizer.encode(prompt.as_str(), true) {
|
||||
@@ -131,7 +128,7 @@ pub async fn run_qwen_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
|
||||
let input_len = input_ids.len();
|
||||
console_log!("[Qwen] Syöte: {} tokenia", input_len);
|
||||
|
||||
let start_gen = perf.now();
|
||||
let start_gen = crate::perf_now();
|
||||
let max_new_tokens = 32;
|
||||
let mut generated_text = String::new();
|
||||
let mut tokens_generated: usize = 0;
|
||||
@@ -202,7 +199,7 @@ pub async fn run_qwen_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
|
||||
crate::sleep_ms(0).await;
|
||||
}
|
||||
|
||||
let gen_time = perf.now() - start_gen;
|
||||
let gen_time = crate::perf_now() - start_gen;
|
||||
let tokens_per_sec = if gen_time > 0.0 { (tokens_generated as f64 / gen_time) * 1000.0 } else { 0.0 };
|
||||
console_log!("[Qwen] {} tokenia | {:.0}ms | {:.1} tok/s", tokens_generated, gen_time, tokens_per_sec);
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use candle_core::{Device, Tensor, DType};
|
||||
use candle_core::quantized::gguf_file;
|
||||
use candle_nn::VarBuilder;
|
||||
use candle_transformers::models::qwen2::{Config as QwenConfig, ModelForCausalLM as QwenModel};
|
||||
use candle_transformers::models::quantized_qwen2::ModelWeights as QwenQuantizedModel;
|
||||
use wasm_bindgen::JsCast;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
@@ -16,13 +18,36 @@ macro_rules! console_log {
|
||||
const MODEL_05B_URL: &str = "https://huggingface.co/Qwen/Qwen2.5-Coder-0.5B-Instruct/resolve/main/model.safetensors";
|
||||
const TOKENIZER_05B_URL: &str = "https://huggingface.co/Qwen/Qwen2.5-Coder-0.5B-Instruct/resolve/main/tokenizer.json";
|
||||
|
||||
// 3B — parempi laatu, vaatii enemmän muistia (~6 GB lataus, ~12 GB RAM)
|
||||
const MODEL_3B_PART1_URL: &str = "https://huggingface.co/Qwen/Qwen2.5-Coder-3B-Instruct/resolve/main/model-00001-of-00002.safetensors";
|
||||
const MODEL_3B_PART2_URL: &str = "https://huggingface.co/Qwen/Qwen2.5-Coder-3B-Instruct/resolve/main/model-00002-of-00002.safetensors";
|
||||
const TOKENIZER_3B_URL: &str = "https://huggingface.co/Qwen/Qwen2.5-Coder-3B-Instruct/resolve/main/tokenizer.json";
|
||||
// 1.5B GGUF Q4_K_M — kvantisoidtu, mahtuu selaimeen (~1 GB)
|
||||
const MODEL_GGUF_URL: &str = "https://huggingface.co/Qwen/Qwen2.5-Coder-1.5B-Instruct-GGUF/resolve/main/qwen2.5-coder-1.5b-instruct-q4_k_m.gguf";
|
||||
const TOKENIZER_GGUF_URL: &str = "https://huggingface.co/Qwen/Qwen2.5-Coder-1.5B-Instruct/resolve/main/tokenizer.json";
|
||||
|
||||
enum CoderModel {
|
||||
Full(QwenModel),
|
||||
Quantized(QwenQuantizedModel),
|
||||
}
|
||||
|
||||
impl CoderModel {
|
||||
fn forward(&mut self, x: &Tensor, pos: usize) -> candle_core::Result<Tensor> {
|
||||
match self {
|
||||
CoderModel::Full(m) => m.forward(x, pos),
|
||||
CoderModel::Quantized(m) => m.forward(x, pos),
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_kv_cache(&mut self) {
|
||||
match self {
|
||||
CoderModel::Full(m) => m.clear_kv_cache(),
|
||||
CoderModel::Quantized(_) => {
|
||||
// Quantized model nollaa KV-cachen automaattisesti kun forward kutsutaan pos=0:lla
|
||||
// (ks. quantized_qwen2.rs rivi 118: if index_pos == 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CachedModel {
|
||||
model: QwenModel,
|
||||
model: CoderModel,
|
||||
tokenizer: tokenizers::Tokenizer,
|
||||
is_3b: bool,
|
||||
}
|
||||
@@ -115,10 +140,7 @@ async fn ensure_cached(key: &str, url: &str, ws: &Rc<RefCell<WebSocket>>) -> Res
|
||||
|
||||
console_log!("[Coder] Ladataan {}...", key);
|
||||
|
||||
let window = web_sys::window().unwrap();
|
||||
let resp_val = wasm_bindgen_futures::JsFuture::from(window.fetch_with_str(url))
|
||||
.await.map_err(|e| format!("Fetch: {:?}", e))?;
|
||||
let resp: web_sys::Response = resp_val.dyn_into().map_err(|_| "Ei Response".to_string())?;
|
||||
let resp = crate::worker_fetch(url).await?;
|
||||
if !resp.ok() { return Err(format!("HTTP {}", resp.status())); }
|
||||
|
||||
let total_size: usize = resp.headers()
|
||||
@@ -182,50 +204,39 @@ async fn get_or_build_model(use_3b: bool, ws: &Rc<RefCell<WebSocket>>) -> Result
|
||||
let dtype = DType::F32;
|
||||
|
||||
// Tokenizer
|
||||
let tok_url = if use_3b { TOKENIZER_3B_URL } else { TOKENIZER_05B_URL };
|
||||
let tok_key = if use_3b { "coder3b-tokenizer.json" } else { "coder05b-tokenizer.json" };
|
||||
let tok_url = if use_3b { TOKENIZER_GGUF_URL } else { TOKENIZER_05B_URL };
|
||||
let tok_key = if use_3b { "coder15b-tokenizer.json" } else { "coder05b-tokenizer.json" };
|
||||
let tok_bytes = ensure_cached(tok_key, tok_url, ws).await?;
|
||||
let tokenizer = tokenizers::Tokenizer::from_bytes(&tok_bytes[..])
|
||||
.map_err(|e| format!("Tokenizer: {}", e))?;
|
||||
|
||||
// Painot
|
||||
let tensors = if use_3b {
|
||||
let part1 = ensure_cached("coder3b-model-part1.safetensors", MODEL_3B_PART1_URL, ws).await?;
|
||||
let part2 = ensure_cached("coder3b-model-part2.safetensors", MODEL_3B_PART2_URL, ws).await?;
|
||||
console_log!("[Coder] Rakennetaan 3B-mallia...");
|
||||
let mut all_tensors = candle_core::safetensors::load_buffer(&part1[..], &device)
|
||||
.map_err(|e| format!("Part1: {}", e))?;
|
||||
let tensors2 = candle_core::safetensors::load_buffer(&part2[..], &device)
|
||||
.map_err(|e| format!("Part2: {}", e))?;
|
||||
all_tensors.extend(tensors2);
|
||||
all_tensors
|
||||
let model = if use_3b {
|
||||
// GGUF Q4_K_M — kvantisoidtu 3B-malli (~1.9 GB)
|
||||
let gguf_bytes = ensure_cached("coder15b-q4km.gguf", MODEL_GGUF_URL, ws).await?;
|
||||
console_log!("[Coder] Rakennetaan kvantisoidun 1.5B-mallia (Q4_K_M)...");
|
||||
let mut cursor = std::io::Cursor::new(&gguf_bytes[..]);
|
||||
let content = gguf_file::Content::read(&mut cursor)
|
||||
.map_err(|e| format!("GGUF parse: {}", e))?;
|
||||
let qmodel = QwenQuantizedModel::from_gguf(content, &mut cursor, &device)
|
||||
.map_err(|e| format!("GGUF model: {}", e))?;
|
||||
CoderModel::Quantized(qmodel)
|
||||
} else {
|
||||
let model_bytes = ensure_cached("coder05b-model.safetensors", MODEL_05B_URL, ws).await?;
|
||||
console_log!("[Coder] Rakennetaan 0.5B-mallia...");
|
||||
candle_core::safetensors::load_buffer(&model_bytes[..], &device)
|
||||
.map_err(|e| format!("Safetensors: {}", e))?
|
||||
};
|
||||
|
||||
let vb = VarBuilder::from_tensors(tensors, dtype, &device);
|
||||
let config = if use_3b {
|
||||
QwenConfig {
|
||||
vocab_size: 151936, hidden_size: 2048, intermediate_size: 11008,
|
||||
num_hidden_layers: 36, num_attention_heads: 16, num_key_value_heads: 2,
|
||||
max_position_embeddings: 32768, sliding_window: 32768, max_window_layers: 36,
|
||||
tie_word_embeddings: true, rope_theta: 1000000.0, rms_norm_eps: 1e-6,
|
||||
use_sliding_window: false, hidden_act: candle_nn::Activation::Silu,
|
||||
}
|
||||
} else {
|
||||
QwenConfig {
|
||||
let tensors = candle_core::safetensors::load_buffer(&model_bytes[..], &device)
|
||||
.map_err(|e| format!("Safetensors: {}", e))?;
|
||||
let config = QwenConfig {
|
||||
vocab_size: 151936, hidden_size: 896, intermediate_size: 4864,
|
||||
num_hidden_layers: 24, num_attention_heads: 14, num_key_value_heads: 2,
|
||||
max_position_embeddings: 32768, sliding_window: 32768, max_window_layers: 21,
|
||||
tie_word_embeddings: true, rope_theta: 1000000.0, rms_norm_eps: 1e-6,
|
||||
use_sliding_window: false, hidden_act: candle_nn::Activation::Silu,
|
||||
}
|
||||
};
|
||||
let vb = VarBuilder::from_tensors(tensors, dtype, &device);
|
||||
let qwen = QwenModel::new(&config, vb).map_err(|e| format!("Malli: {}", e))?;
|
||||
CoderModel::Full(qwen)
|
||||
};
|
||||
|
||||
let model = QwenModel::new(&config, vb).map_err(|e| format!("Malli: {}", e))?;
|
||||
console_log!("[Coder] Malli ladattu ja välimuistitettu");
|
||||
|
||||
MODEL_CACHE.with(|c| {
|
||||
@@ -237,17 +248,16 @@ async fn get_or_build_model(use_3b: bool, ws: &Rc<RefCell<WebSocket>>) -> Result
|
||||
|
||||
/// use_3b: false = 0.5B (nopea), true = 3B (laadukas)
|
||||
pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use_3b: bool, task_id: Option<String>) {
|
||||
let perf = web_sys::window().unwrap().performance().unwrap();
|
||||
let size_label = if use_3b { "3B" } else { "0.5B" };
|
||||
|
||||
let start_load = perf.now();
|
||||
let start_load = crate::perf_now();
|
||||
|
||||
if let Err(e) = get_or_build_model(use_3b, &ws).await {
|
||||
console_log!("[Coder] Mallin lataus: {}", e);
|
||||
return;
|
||||
}
|
||||
|
||||
let load_time = perf.now() - start_load;
|
||||
let load_time = crate::perf_now() - start_load;
|
||||
if load_time > 100.0 {
|
||||
console_log!("[Coder] Malli ladattu ({:.0}ms). Generoidaan...", load_time);
|
||||
}
|
||||
@@ -258,13 +268,13 @@ pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use
|
||||
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&prompt) {
|
||||
let p = json.get("prompt").and_then(|v| v.as_str()).unwrap_or(&prompt).to_string();
|
||||
let s = json.get("system").and_then(|v| v.as_str()).unwrap_or(default_system).to_string();
|
||||
let m = json.get("max_tokens").and_then(|v| v.as_u64()).unwrap_or(256) as usize;
|
||||
let m = json.get("max_tokens").and_then(|v| v.as_u64()).unwrap_or(512) as usize;
|
||||
(p, s, m)
|
||||
} else {
|
||||
(prompt.clone(), default_system.to_string(), 256)
|
||||
(prompt.clone(), default_system.to_string(), 512)
|
||||
}
|
||||
} else {
|
||||
(prompt.clone(), default_system.to_string(), 256)
|
||||
(prompt.clone(), default_system.to_string(), 512)
|
||||
};
|
||||
|
||||
// Prefill: aloitetaan vastaus ```-koodiblokkilla, jolloin malli jatkaa suoraan koodilla
|
||||
@@ -283,7 +293,7 @@ pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use
|
||||
console_log!("[Coder] Syöte: {} tokenia", input_len);
|
||||
|
||||
let device = Device::Cpu;
|
||||
let start_gen = perf.now();
|
||||
let start_gen = crate::perf_now();
|
||||
let eos_token = 151645u32;
|
||||
let temperature: f32 = 0.7;
|
||||
let top_k: usize = 40;
|
||||
@@ -310,7 +320,11 @@ pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use
|
||||
if let Ok(text) = cached.tokenizer.decode(&[next_token], true) {
|
||||
generated_text.push_str(&text);
|
||||
let mut chunk = serde_json::json!({ "type": "llm_chunk", "token": text, "prompt": prompt, "model": "Qwen2.5-Coder" });
|
||||
if let Some(ref tid) = task_id { 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());
|
||||
}
|
||||
all_generated.push(next_token);
|
||||
@@ -352,14 +366,18 @@ pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
all_generated.push(next_token);
|
||||
tokens_generated += 1;
|
||||
}
|
||||
|
||||
let gen_time = perf.now() - start_gen;
|
||||
let gen_time = crate::perf_now() - start_gen;
|
||||
|
||||
// Siivotaan vastaus: poista markdown-koodiblokit ja johdantotekstit
|
||||
let cleaned = strip_markdown_wrapper(&generated_text);
|
||||
@@ -381,7 +399,9 @@ pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use
|
||||
"load_time_ms": (load_time * 100.0).round() / 100.0,
|
||||
});
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -28,10 +28,7 @@ async fn ensure_cached(key: &str, url: &str, ws: &Rc<RefCell<WebSocket>>) -> Res
|
||||
send_progress(ws, key, 0, 0, 0);
|
||||
|
||||
// Fetch API:lla saadaan Content-Length ja streaming-luku
|
||||
let window = web_sys::window().unwrap();
|
||||
let resp_val = wasm_bindgen_futures::JsFuture::from(window.fetch_with_str(url))
|
||||
.await.map_err(|e| format!("Fetch epäonnistui: {:?}", e))?;
|
||||
let resp: web_sys::Response = resp_val.dyn_into().map_err(|_| "Ei Response-objekti".to_string())?;
|
||||
let resp = crate::worker_fetch(url).await?;
|
||||
|
||||
if !resp.ok() {
|
||||
return Err(format!("HTTP {}", resp.status()));
|
||||
@@ -99,7 +96,7 @@ fn send_progress(ws: &Rc<RefCell<WebSocket>>, file: &str, pct: u32, loaded: usiz
|
||||
|
||||
/// Lataa malli ja tokenizer, suorita inferenssi ja streamaa tokenit hubille
|
||||
pub async fn run_smollm_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
|
||||
let perf = web_sys::window().unwrap().performance().unwrap();
|
||||
// performance via crate::perf_now()
|
||||
|
||||
// 1. Lataa tokenizer
|
||||
let tok_bytes = match ensure_cached("smollm-tokenizer.json", TOKENIZER_URL, &ws).await {
|
||||
@@ -122,7 +119,7 @@ pub async fn run_smollm_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
|
||||
// Burn 0.21-pre.2 cubecl-runtime ei käänny Wasmille (println! puuttuu)
|
||||
// → NdArray kunnes Burn 0.21 stable + Wasm-tuki
|
||||
console_log!("[SmolLM] Burn NdArray (CPU) inferenssi...");
|
||||
run_burn_inference::<burn::backend::NdArray>(prompt, model_bytes, tokenizer, ws, perf.clone()).await;
|
||||
run_burn_inference::<burn::backend::NdArray>(prompt, model_bytes, tokenizer, ws).await;
|
||||
}
|
||||
|
||||
async fn run_burn_inference<B: burn::tensor::backend::Backend>(
|
||||
@@ -130,9 +127,8 @@ async fn run_burn_inference<B: burn::tensor::backend::Backend>(
|
||||
model_bytes: Vec<u8>,
|
||||
tokenizer: tokenizers::Tokenizer,
|
||||
ws: Rc<RefCell<WebSocket>>,
|
||||
perf: web_sys::Performance, // Korjattu Wasm-performanssi välitettäväksi
|
||||
) {
|
||||
let start_load = perf.now();
|
||||
let start_load = crate::perf_now();
|
||||
|
||||
let device = Default::default();
|
||||
let config = crate::burn_smollm::config::SmolLMConfig::default();
|
||||
@@ -143,7 +139,7 @@ async fn run_burn_inference<B: burn::tensor::backend::Backend>(
|
||||
Err(e) => { console_log!("[SmolLM] Lataus epäonnistui: {}", e); return; }
|
||||
};
|
||||
|
||||
let load_time = perf.now() - start_load;
|
||||
let load_time = crate::perf_now() - start_load;
|
||||
console_log!("[SmolLM] Burn-malli ladattu ({:.0}ms). Generoidaan...", load_time);
|
||||
|
||||
let formatted_prompt = format!("<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n", prompt);
|
||||
@@ -156,7 +152,7 @@ async fn run_burn_inference<B: burn::tensor::backend::Backend>(
|
||||
let input_len = input_ids.len();
|
||||
console_log!("[SmolLM] Syöte: {} tokenia", input_len);
|
||||
|
||||
let start_gen = perf.now();
|
||||
let start_gen = crate::perf_now();
|
||||
let max_new_tokens = 32;
|
||||
let mut generated_text = String::new();
|
||||
let mut tokens_generated: usize = 0;
|
||||
@@ -219,7 +215,7 @@ async fn run_burn_inference<B: burn::tensor::backend::Backend>(
|
||||
tokens_generated += 1;
|
||||
}
|
||||
|
||||
let gen_time = perf.now() - start_gen;
|
||||
let gen_time = crate::perf_now() - start_gen;
|
||||
let tokens_per_sec = if gen_time > 0.0 { (tokens_generated as f64 / gen_time) * 1000.0 } else { 0.0 };
|
||||
|
||||
let done = serde_json::json!({
|
||||
|
||||
413
network-poc/static/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/static/avatars/bear.png
Normal file
|
After Width: | Height: | Size: 757 KiB |
BIN
network-poc/static/avatars/beaver.png
Normal file
|
After Width: | Height: | Size: 700 KiB |
BIN
network-poc/static/avatars/chameleon.png
Normal file
|
After Width: | Height: | Size: 731 KiB |
BIN
network-poc/static/avatars/elephant.png
Normal file
|
After Width: | Height: | Size: 711 KiB |
BIN
network-poc/static/avatars/gecko.png
Normal file
|
After Width: | Height: | Size: 695 KiB |
BIN
network-poc/static/avatars/gecko_notext.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
network-poc/static/avatars/lion.png
Normal file
|
After Width: | Height: | Size: 872 KiB |
BIN
network-poc/static/avatars/mantis.png
Normal file
|
After Width: | Height: | Size: 738 KiB |
BIN
network-poc/static/avatars/owl.png
Normal file
|
After Width: | Height: | Size: 813 KiB |
BIN
network-poc/static/avatars/penguin.png
Normal file
|
After Width: | Height: | Size: 658 KiB |
BIN
network-poc/static/avatars/serpent.png
Normal file
|
After Width: | Height: | Size: 696 KiB |
BIN
network-poc/static/avatars/spider.png
Normal file
|
After Width: | Height: | Size: 718 KiB |
BIN
network-poc/static/avatars/tortoise.png
Normal file
|
After Width: | Height: | Size: 780 KiB |
BIN
network-poc/static/avatars/walrus.png
Normal file
|
After Width: | Height: | Size: 826 KiB |
43
network-poc/static/docs/README.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# OpenTofu Core Codebase Documentation
|
||||
|
||||
This directory contains some documentation about the OpenTofu Core codebase,
|
||||
aimed at readers who are interested in making code contributions.
|
||||
|
||||
If you're looking for information on _using_ OpenTofu, please instead refer
|
||||
to [the main OpenTofu CLI documentation](https://opentofu.org/docs/cli/index.html).
|
||||
|
||||
## OpenTofu Core Architecture Documents
|
||||
|
||||
* [OpenTofu Core Architecture Summary](./architecture.md): an overview of the
|
||||
main components of OpenTofu Core and how they interact. This is the best
|
||||
starting point if you are diving in to this codebase for the first time.
|
||||
|
||||
* [Resource Instance Change Lifecycle](./resource-instance-change-lifecycle.md):
|
||||
a description of the steps in validating, planning, and applying a change
|
||||
to a resource instance, from the perspective of the provider plugin RPC
|
||||
operations. This may be useful for understanding the various expectations
|
||||
OpenTofu enforces about provider behavior, either if you intend to make
|
||||
changes to those behaviors or if you are implementing a new OpenTofu plugin
|
||||
SDK and so wish to conform to them.
|
||||
|
||||
(If you are planning to write a new provider using the _official_ SDK then
|
||||
please refer to [the Extend documentation](https://github.com/hashicorp/terraform-docs-common)
|
||||
instead; it presents similar information from the perspective of the SDK
|
||||
API, rather than the plugin wire protocol.)
|
||||
|
||||
* [Diagnostics](./diagnostics): how we report errors and warnings to end-users
|
||||
in OpenTofu.
|
||||
|
||||
* [Plugin Protocol](./plugin-protocol/): gRPC/protobuf definitions for the
|
||||
plugin wire protocol and information about its versioning strategy.
|
||||
|
||||
This documentation is for SDK developers, and is not necessary reading for
|
||||
those implementing a provider using the official SDK.
|
||||
|
||||
* [How OpenTofu Uses Unicode](./unicode.md): an overview of the various
|
||||
features of OpenTofu that rely on Unicode and how to change those features
|
||||
to adopt new versions of Unicode.
|
||||
|
||||
## Contribution Guides
|
||||
|
||||
* [Contributing to OpenTofu](../CONTRIBUTING.md): a complete guideline for those who want to contribute to this project.
|
||||
374
network-poc/static/docs/architecture.md
Normal file
@@ -0,0 +1,374 @@
|
||||
# OpenTofu Core Architecture Summary
|
||||
|
||||
This document is a summary of the main components of OpenTofu Core and how
|
||||
data and requests flow between these components. It's intended as a primer
|
||||
to help navigate the codebase to dig into more details.
|
||||
|
||||
We assume some familiarity with user-facing OpenTofu concepts like
|
||||
configuration, state, CLI workflow, etc. The OpenTofu website has
|
||||
documentation on these ideas.
|
||||
|
||||
## OpenTofu Request Flow
|
||||
|
||||
The following diagram shows an approximation of how a user command is
|
||||
executed in OpenTofu:
|
||||
|
||||

|
||||
|
||||
Each of the different subsystems (solid boxes) in this diagram is described
|
||||
in more detail in a corresponding section below.
|
||||
|
||||
## CLI (`command` package)
|
||||
|
||||
Each time a user runs the `tofu` program, aside from some initial
|
||||
bootstrapping in the root package (not shown in the diagram) execution
|
||||
transfers immediately into one of the "command" implementations in
|
||||
[the `command` package](https://pkg.go.dev/github.com/opentofu/opentofu/internal/command).
|
||||
The mapping between the user-facing command names and
|
||||
their corresponding `command` package types can be found in the `commands.go`
|
||||
file under the `cmd/tofu` directory (package `main`).
|
||||
|
||||
The full flow illustrated above does not actually apply to _all_ commands,
|
||||
but it applies to the main OpenTofu workflow commands `tofu plan` and
|
||||
`tofu apply`, along with a few others.
|
||||
|
||||
For these commands, the role of the command implementation is to read and parse
|
||||
any command line arguments, command line options, and environment variables
|
||||
that are needed for the given command and use them to produce a
|
||||
[`backend.Operation`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/backend#Operation)
|
||||
object that describes an action to be taken.
|
||||
|
||||
An _operation_ consists of:
|
||||
|
||||
* The action to be taken (e.g. "plan", "apply").
|
||||
* The name of the [workspace](https://opentofu.org/docs/language/state/workspaces)
|
||||
where the action will be taken.
|
||||
* Root module input variables to use for the action.
|
||||
* For the "plan" operation, a path to the directory containing the configuration's root module.
|
||||
* For the "apply" operation, the plan to apply.
|
||||
* Various other less-common options/settings such as `-target` addresses, the
|
||||
"force" flag, etc.
|
||||
|
||||
The operation is then passed to the currently-selected
|
||||
[backend](https://opentofu.org/docs/language/settings/backends/configuration). Each backend name
|
||||
corresponds to an implementation of
|
||||
[`backend.Backend`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/backend#Backend), using a
|
||||
mapping table in
|
||||
[the `backend/init` package](https://pkg.go.dev/github.com/opentofu/opentofu/internal/backend/init).
|
||||
|
||||
Backends that are able to execute operations additionally implement
|
||||
[`backend.Enhanced`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/backend#Enhanced);
|
||||
the command-handling code calls `Operation` with the operation it has
|
||||
constructed, and then the backend is responsible for executing that action.
|
||||
|
||||
Backends that execute operations, however, do so as an architectural implementation detail and not a
|
||||
general feature of backends. That is, the term 'backend' as a OpenTofu feature is used to refer to
|
||||
a plugin that determines where OpenTofu stores its state snapshots - only the default `local`, `remote` and `cloud` backends perform operations.
|
||||
|
||||
Thus, most backends do _not_ implement this interface, and so the `command` package wraps these
|
||||
backends in an instance of
|
||||
[`local.Local`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/backend/local#Local),
|
||||
causing the operation to be executed locally within the `tofu` process itself.
|
||||
|
||||
## Backends
|
||||
|
||||
A _backend_ determines where OpenTofu should store its state snapshots.
|
||||
|
||||
As described above, the `local` backend also executes operations on behalf of most other
|
||||
backends. It uses a _state manager_
|
||||
(either
|
||||
[`statemgr.Filesystem`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/states/statemgr#Filesystem) if the
|
||||
local backend is being used directly, or an implementation provided by whatever
|
||||
backend is being wrapped) to retrieve the current state for the workspace
|
||||
specified in the operation, then uses the _config loader_ to load and do
|
||||
initial processing/validation of the configuration specified in the
|
||||
operation. It then uses these, along with the other settings given in the
|
||||
operation, to construct a
|
||||
[`tofu.Context`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#Context),
|
||||
which is the main object that actually performs OpenTofu operations.
|
||||
|
||||
The `local` backend finally calls an appropriate method on that context to
|
||||
begin execution of the relevant command, such as
|
||||
[`Plan`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#Context.Plan)
|
||||
or
|
||||
[`Apply`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#Context.Apply), which in turn constructs a graph using a _graph builder_,
|
||||
described in a later section.
|
||||
|
||||
## Configuration Loader
|
||||
|
||||
The top-level configuration structure is represented by model types in
|
||||
[package `configs`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/configs).
|
||||
A whole configuration (the root module plus all of its descendent modules)
|
||||
is represented by
|
||||
[`configs.Config`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/configs#Config).
|
||||
|
||||
The `configs` package contains some low-level functionality for constructing
|
||||
configuration objects, but the main entry point is in the sub-package
|
||||
[`configload`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/configs/configload]),
|
||||
via
|
||||
[`configload.Loader`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/configs/configload#Loader).
|
||||
A loader deals with all of the details of installing child modules
|
||||
(during `tofu init`) and then locating those modules again when a
|
||||
configuration is loaded by a backend. It takes the path to a root module
|
||||
and recursively loads all of the child modules to produce a single
|
||||
[`configs.Config`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/configs#Config)
|
||||
representing the entire configuration.
|
||||
|
||||
OpenTofu expects configuration files written in the OpenTofu language, which
|
||||
is a DSL built on top of
|
||||
[HCL](https://github.com/hashicorp/hcl). Some parts of the configuration
|
||||
cannot be interpreted until we build and walk the graph, since they depend
|
||||
on the outcome of other parts of the configuration, and so these parts of
|
||||
the configuration remain represented as the low-level HCL types
|
||||
[`hcl.Body`](https://pkg.go.dev/github.com/hashicorp/hcl/v2/#Body)
|
||||
and
|
||||
[`hcl.Expression`](https://pkg.go.dev/github.com/hashicorp/hcl/v2/#Expression),
|
||||
allowing OpenTofu to interpret them at a more appropriate time.
|
||||
|
||||
## State Manager
|
||||
|
||||
A _state manager_ is responsible for storing and retrieving snapshots of the
|
||||
[OpenTofu state](https://opentofu.org/docs/language/state/index.html)
|
||||
for a particular workspace. Each manager is an implementation of
|
||||
some combination of interfaces in
|
||||
[the `statemgr` package](https://pkg.go.dev/github.com/opentofu/opentofu/internal/states/statemgr),
|
||||
with most practical managers implementing the full set of operations
|
||||
described by
|
||||
[`statemgr.Full`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/states/statemgr#Full)
|
||||
provided by a _backend_. The smaller interfaces exist primarily for use in
|
||||
other function signatures to be explicit about what actions the function might
|
||||
take on the state manager; there is little reason to write a state manager
|
||||
that does not implement all of `statemgr.Full`.
|
||||
|
||||
The implementation
|
||||
[`statemgr.Filesystem`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/states/statemgr#Filesystem) is used
|
||||
by default (by the `local` backend) and is responsible for the familiar
|
||||
`terraform.tfstate` local file that most OpenTofu users start with, before
|
||||
they switch to [remote state](https://opentofu.org/docs/language/state/remote).
|
||||
Other implementations of `statemgr.Full` are used to implement remote state.
|
||||
Each of these saves and retrieves state via a remote network service
|
||||
appropriate to the backend that creates it.
|
||||
|
||||
A state manager accepts and returns a state snapshot as a
|
||||
[`states.State`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/states#State)
|
||||
object. The state manager is responsible for exactly how that object is
|
||||
serialized and stored, but all state managers at the time of writing use
|
||||
the same JSON serialization format, storing the resulting JSON bytes in some
|
||||
kind of arbitrary blob store.
|
||||
|
||||
## Graph Builder
|
||||
|
||||
A _graph builder_ is called by a
|
||||
[`tofu.Context`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#Context)
|
||||
method (e.g. `Plan` or `Apply`) to produce the graph that will be used
|
||||
to represent the necessary steps for that operation and the dependency
|
||||
relationships between them.
|
||||
|
||||
In most cases, the
|
||||
[vertices](https://en.wikipedia.org/wiki/Vertex_(graph_theory)) of OpenTofu's
|
||||
graphs each represent a specific object in the configuration, or something
|
||||
derived from those configuration objects. For example, each `resource` block
|
||||
in the configuration has one corresponding
|
||||
[`GraphNodeConfigResource`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#GraphNodeConfigResource)
|
||||
vertex representing it in the "plan" graph. (OpenTofu Core uses terminology
|
||||
inconsistently, describing graph _vertices_ also as graph _nodes_ in various
|
||||
places. These both describe the same concept.)
|
||||
|
||||
The [edges](https://en.wikipedia.org/wiki/Glossary_of_graph_theory_terms#edge)
|
||||
in the graph represent "must happen after" relationships. These define the
|
||||
order in which the vertices are evaluated, ensuring that e.g. one resource is
|
||||
created before another resource that depends on it.
|
||||
|
||||
Each operation has its own graph builder, because the graph building process
|
||||
is different for each. For example, a "plan" operation needs a graph built
|
||||
directly from the configuration, but an "apply" operation instead builds its
|
||||
graph from the set of changes described in the plan that is being applied.
|
||||
|
||||
The graph builders all work in terms of a sequence of _transforms_, which
|
||||
are implementations of
|
||||
[`tofu.GraphTransformer`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#GraphTransformer).
|
||||
Implementations of this interface just take a graph and mutate it in any
|
||||
way needed, and so the set of available transforms is quite varied. Some
|
||||
important examples include:
|
||||
|
||||
* [`ConfigTransformer`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#ConfigTransformer),
|
||||
which creates a graph vertex for each `resource` block in the configuration.
|
||||
|
||||
* [`StateTransformer`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#StateTransformer),
|
||||
which creates a graph vertex for each resource instance currently tracked
|
||||
in the state.
|
||||
|
||||
* [`ReferenceTransformer`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#ReferenceTransformer),
|
||||
which analyses the configuration to find dependencies between resources and
|
||||
other objects and creates any necessary "happens after" edges for these.
|
||||
|
||||
* [`ProviderTransformer`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#ProviderTransformer),
|
||||
which associates each resource or resource instance with exactly one
|
||||
provider configuration (implementing
|
||||
[the inheritance rules](https://opentofu.org/docs/language/providers/))
|
||||
and then creates "happens after" edges to ensure that the providers are
|
||||
initialized before taking any actions with the resources that belong to
|
||||
them.
|
||||
|
||||
There are many more different graph transforms, which can be discovered
|
||||
by reading the source code for the different graph builders. Each graph
|
||||
builder uses a different subset of these depending on the needs of the
|
||||
operation that is being performed.
|
||||
|
||||
The result of graph building is a
|
||||
[`tofu.Graph`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#Graph), which
|
||||
can then be processed using a _graph walker_.
|
||||
|
||||
## Graph Walk
|
||||
|
||||
The process of walking the graph visits each vertex of that graph in a way
|
||||
which respects the "happens after" edges in the graph. The walk algorithm
|
||||
itself is implemented in
|
||||
[the low-level `dag` package](https://pkg.go.dev/github.com/opentofu/opentofu/internal/dag#AcyclicGraph.Walk)
|
||||
(where "DAG" is short for [_Directed Acyclic Graph_](https://en.wikipedia.org/wiki/Directed_acyclic_graph)), in
|
||||
[`AcyclicGraph.Walk`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/dag#AcyclicGraph.Walk).
|
||||
However, the "interesting" OpenTofu walk functionality is implemented in
|
||||
[`tofu.ContextGraphWalker`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#ContextGraphWalker),
|
||||
which implements a small set of higher-level operations that are performed
|
||||
during the graph walk:
|
||||
|
||||
* `EnterPath` is called once for each module in the configuration, taking a
|
||||
module address and returning a
|
||||
[`tofu.EvalContext`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#EvalContext)
|
||||
that tracks objects within that module. `tofu.Context` is the _global_
|
||||
context for the entire operation, while `tofu.EvalContext` is a
|
||||
context for processing within a single module, and is the primary means
|
||||
by which the namespaces in each module are kept separate.
|
||||
|
||||
Each vertex in the graph is evaluated, in an order that guarantees that the
|
||||
"happens after" edges will be respected. If possible, the graph walk algorithm
|
||||
will evaluate multiple vertices concurrently. Vertex evaluation code must
|
||||
therefore make careful use of concurrency primitives such as mutexes in order
|
||||
to coordinate access to shared objects such as the `states.State` object.
|
||||
In most cases, we use the helper wrapper
|
||||
[`states.SyncState`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/states#SyncState)
|
||||
to safely implement concurrent reads and writes from the shared state.
|
||||
|
||||
## Vertex Evaluation
|
||||
|
||||
The action taken for each vertex during the graph walk is called
|
||||
_execution_. Execution runs a sequence of arbitrary actions that make sense
|
||||
for a particular vertex type.
|
||||
|
||||
For example, evaluation of a vertex representing a resource instance during
|
||||
a plan operation would include the following high-level steps:
|
||||
|
||||
* Retrieve the resource's associated provider from the `EvalContext`. This
|
||||
should already be initialized earlier by the provider's own graph vertex,
|
||||
due to the "happens after" edge between the resource node and the provider
|
||||
node.
|
||||
|
||||
* Retrieve from the state the portion relevant to the specific resource
|
||||
instance being evaluated.
|
||||
|
||||
* Evaluate the attribute expressions given for the resource in configuration.
|
||||
This often involves retrieving the state of _other_ resource instances so
|
||||
that their values can be copied or transformed into the current instance's
|
||||
attributes, which is coordinated by the `EvalContext`.
|
||||
|
||||
* Pass the current instance state and the resource configuration to the
|
||||
provider, asking the provider to produce an _instance diff_ representing the
|
||||
differences between the state and the configuration.
|
||||
|
||||
* Save the instance diff as part of the plan that is being constructed by
|
||||
this operation.
|
||||
|
||||
Each execution step for a vertex is an implementation of
|
||||
[`tofu.Execute`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#GraphNodeExecutable.Execute).
|
||||
As with graph transforms, the behavior of these implementations varies widely:
|
||||
whereas graph transforms can take any action against the graph, an `Execute`
|
||||
implementation can take any action against the `EvalContext`.
|
||||
|
||||
The implementation of `tofu.EvalContext` used in real processing
|
||||
(as opposed to testing) is
|
||||
[`tofu.BuiltinEvalContext`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#BuiltinEvalContext).
|
||||
It provides coordinated access to plugins, the current state, and the current
|
||||
plan via the `EvalContext` interface methods.
|
||||
|
||||
In order to be executed, a vertex must implement
|
||||
[`tofu.GraphNodeExecutable`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#GraphNodeExecutable),
|
||||
which has a single `Execute` method that handles. There are numerous `Execute`
|
||||
implementations with different behaviors, but some prominent examples are:
|
||||
|
||||
* [NodePlannableResource.Execute](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#NodePlannableResourceInstance.Execute), which handles the `plan` operation.
|
||||
|
||||
* [`NodeApplyableResourceInstance.Execute`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#NodeApplyableResourceInstance.Execute), which handles the main `apply` operation.
|
||||
|
||||
* [`NodeDestroyResourceInstance.Execute`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#EvalWriteState), which handles the main `destroy` operation.
|
||||
|
||||
A vertex must complete successfully before the graph walk will begin evaluation
|
||||
for other vertices that have "happens after" edges. Evaluation can fail with one
|
||||
or more errors, in which case the graph walk is halted and the errors are
|
||||
returned to the user.
|
||||
|
||||
### Expression Evaluation
|
||||
|
||||
An important part of vertex evaluation for most vertex types is evaluating
|
||||
any expressions in the configuration block associated with the vertex. This
|
||||
completes the processing of the portions of the configuration that were not
|
||||
processed by the configuration loader.
|
||||
|
||||
The high-level process for expression evaluation is:
|
||||
|
||||
1. Analyze the configuration expressions to see which other objects they refer
|
||||
to. For example, the expression `aws_instance.example[1]` refers to one of
|
||||
the instances created by a `resource "aws_instance" "example"` block in
|
||||
configuration. This analysis is performed by
|
||||
[`lang.References`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/lang#References),
|
||||
or more often one of the helper wrappers around it:
|
||||
[`lang.ReferencesInBlock`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/lang#ReferencesInBlock)
|
||||
or
|
||||
[`lang.ReferencesInExpr`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/lang#ReferencesInExpr)
|
||||
|
||||
1. Retrieve from the state the data for the objects that are referred to and
|
||||
create a lookup table of the values from these objects that the
|
||||
HCL evaluation code can refer to.
|
||||
|
||||
1. Prepare the table of built-in functions so that HCL evaluation can refer to
|
||||
them.
|
||||
|
||||
1. Ask HCL to evaluate each attribute's expression (a
|
||||
[`hcl.Expression`](https://pkg.go.dev/github.com/hashicorp/hcl/v2/#Expression)
|
||||
object) against the data and function lookup tables.
|
||||
|
||||
In practice, steps 2 through 4 are usually run all together using one
|
||||
of the methods on [`lang.Scope`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/lang#Scope);
|
||||
most commonly,
|
||||
[`lang.EvalBlock`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/lang#Scope.EvalBlock)
|
||||
or
|
||||
[`lang.EvalExpr`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/lang#Scope.EvalExpr).
|
||||
|
||||
Expression evaluation produces a dynamic value represented as a
|
||||
[`cty.Value`](https://pkg.go.dev/github.com/zclconf/go-cty/cty#Value).
|
||||
This Go type represents values from the OpenTofu language and such values
|
||||
are eventually passed to provider plugins.
|
||||
|
||||
### Sub-graphs
|
||||
|
||||
Some vertices have a special additional behavior that happens after their
|
||||
evaluation steps are complete, where the vertex implementation is given
|
||||
the opportunity to build another separate graph which will be walked as part
|
||||
of the evaluation of the vertex.
|
||||
|
||||
The main example of this is when a `resource` block has the `count` argument
|
||||
set. In that case, the plan graph initially contains one vertex for each
|
||||
`resource` block, but that graph then _dynamically expands_ to have a sub-graph
|
||||
containing one vertex for each instance requested by the count. That is, the
|
||||
sub-graph of `aws_instance.example` might contain vertices for
|
||||
`aws_instance.example[0]`, `aws_instance.example[1]`, etc. This is necessary
|
||||
because the `count` argument may refer to other objects whose values are not
|
||||
known when the main graph is constructed, but become known while evaluating
|
||||
other vertices in the main graph.
|
||||
|
||||
This special behavior applies to vertex objects that implement
|
||||
[`tofu.GraphNodeDynamicExpandable`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#GraphNodeDynamicExpandable).
|
||||
Such vertices have their own nested _graph builder_, _graph walk_,
|
||||
and _vertex evaluation_ steps, with the same behaviors as described in these
|
||||
sections for the main graph. The difference is in which graph transforms
|
||||
are used to construct the graph and in which evaluation steps apply to the
|
||||
nodes in that sub-graph.
|
||||
361
network-poc/static/docs/destroying.md
Normal file
@@ -0,0 +1,361 @@
|
||||
# OpenTofu Core Resource Destruction Notes
|
||||
|
||||
This document intends to describe some of the details and complications
|
||||
involved in the destruction of resources. It covers the ordering defined for
|
||||
related create and destroy operations, as well as changes to the lifecycle
|
||||
ordering imposed by `create_before_destroy`. It is not intended to enumerate
|
||||
all possible combinations of dependency ordering, only to outline the basics
|
||||
and document some of the more complicated aspects of resource destruction.
|
||||
|
||||
The graph diagrams here will continue to use the inverted graph structure used
|
||||
internally by OpenTofu, where edges represent dependencies rather than order
|
||||
of operations.
|
||||
|
||||
## Simple Resource Creation
|
||||
|
||||
In order to describe resource destruction, we first need to create the
|
||||
resources and define their order. The order of creation is that which fulfills
|
||||
the dependencies for each resource. In this example, `A` has no dependencies,
|
||||
`B` depends on `A`, and `C` depends on `B`, and transitively depends on `A`.
|
||||
|
||||

|
||||
<!--
|
||||
digraph create {
|
||||
subgraph nodes {
|
||||
rank=same;
|
||||
a [label="A create"];
|
||||
b [label="B create"];
|
||||
c [label="C create"];
|
||||
b -> c [dir=back];
|
||||
a -> b [dir=back];
|
||||
}
|
||||
}
|
||||
-->
|
||||
|
||||
Order of operations:
|
||||
1. `A` is created
|
||||
1. `B` is created
|
||||
1. `C` is created
|
||||
|
||||
## Resource Updates
|
||||
|
||||
An existing resource may be updated with references to a newly created
|
||||
resource. The ordering here is exactly the same as one would expect for
|
||||
creation.
|
||||
|
||||

|
||||
<!--
|
||||
digraph update {
|
||||
subgraph nodes {
|
||||
rank=same;
|
||||
a [label="A create"];
|
||||
b [label="B update"];
|
||||
c [label="C update"];
|
||||
b -> c [dir=back];
|
||||
a -> b [dir=back];
|
||||
}
|
||||
}
|
||||
-->
|
||||
|
||||
Order of operations:
|
||||
1. `A` is created
|
||||
1. `B` is created
|
||||
1. `C` is created
|
||||
|
||||
## Simple Resource Destruction
|
||||
|
||||
The order for destroying resource is exactly the inverse used to create them.
|
||||
This example shows the graph for the destruction of the same nodes defined
|
||||
above. While destroy nodes will not contain attribute references, we will
|
||||
continue to use the inverted edges showing dependencies for destroy, so the
|
||||
operational ordering is still opposite the flow of the arrows.
|
||||
|
||||

|
||||
<!--
|
||||
digraph destroy {
|
||||
subgraph nodes {
|
||||
rank=same;
|
||||
a [label="A destroy"];
|
||||
b [label="B destroy"];
|
||||
c [label="C destroy"];
|
||||
a -> b;
|
||||
b -> c;
|
||||
}
|
||||
}
|
||||
-->
|
||||
|
||||
Order of operations:
|
||||
1. `C` is destroyed
|
||||
1. `B` is destroyed
|
||||
1. `A` is Destroyed
|
||||
|
||||
## Resource Replacement
|
||||
|
||||
Resource replacement is the logical combination of the above scenarios. Here we
|
||||
will show the replacement steps involved when `B` depends on `A`.
|
||||
|
||||
In this first example, we simultaneously replace both `A` and `B`. Here `B` is
|
||||
destroyed before `A`, then `A` is recreated before `B`.
|
||||
|
||||

|
||||
<!--
|
||||
digraph replacement {
|
||||
subgraph create {
|
||||
rank=same;
|
||||
a [label="A create"];
|
||||
b [label="B create"];
|
||||
a -> b [dir=back];
|
||||
}
|
||||
subgraph destroy {
|
||||
rank=same;
|
||||
a_d [label="A destroy"];
|
||||
b_d [label="B destroy"];
|
||||
a_d -> b_d;
|
||||
}
|
||||
|
||||
a -> a_d;
|
||||
a -> b_d [style=dotted];
|
||||
b -> a_d [style=dotted];
|
||||
b -> b_d;
|
||||
}
|
||||
-->
|
||||
|
||||
Order of operations:
|
||||
1. `B` is destroyed
|
||||
1. `A` is destroyed
|
||||
1. `A` is created
|
||||
1. `B` is created
|
||||
|
||||
|
||||
This second example replaces only `A`, while updating `B`. Resource `B` is only
|
||||
updated once `A` has been destroyed and recreated.
|
||||
|
||||

|
||||
<!--
|
||||
digraph replacement {
|
||||
subgraph create {
|
||||
rank=same;
|
||||
a [label="A create"];
|
||||
b [label="B update"];
|
||||
a -> b [dir=back];
|
||||
}
|
||||
subgraph destroy {
|
||||
rank=same;
|
||||
a_d [label="A destroy"];
|
||||
}
|
||||
|
||||
a -> a_d;
|
||||
b -> a_d [style=dotted];
|
||||
}
|
||||
-->
|
||||
|
||||
Order of operations:
|
||||
1. `A` is destroyed
|
||||
1. `A` is created
|
||||
1. `B` is updated
|
||||
|
||||
|
||||
While the dependency edge from `B update` to `A destroy` isn't necessary in
|
||||
these examples, it is shown here as an implementation detail which will be
|
||||
mentioned later on.
|
||||
|
||||
A final example based on the replacement graph; starting with the above
|
||||
configuration where `B` depends on `A`. The graph is reduced to an update of
|
||||
`A` while only destroying `B`. The interesting feature here is the remaining
|
||||
dependency of `A update` on `B destroy`. We can derive this ordering of
|
||||
operations from the full replacement example above, by replacing `A create`
|
||||
with `A update` and removing the unused nodes.
|
||||
|
||||

|
||||
<!--
|
||||
digraph destroy_then_update {
|
||||
subgraph update {
|
||||
rank=same;
|
||||
a [label="A update"];
|
||||
}
|
||||
subgraph destroy {
|
||||
rank=same;
|
||||
b_d [label="B destroy"];
|
||||
}
|
||||
|
||||
a -> b_d;
|
||||
}
|
||||
-->
|
||||
## Create Before Destroy
|
||||
|
||||
Currently, the only user-controllable method for changing the ordering of
|
||||
create and destroy operations is with the `create_before_destroy` resource
|
||||
`lifecycle` attribute. This has the obvious effect of causing a resource to be
|
||||
created before it is destroyed when replacement is required, but has a couple
|
||||
of other effects we will detail here.
|
||||
|
||||
Taking the previous replacement examples, we can change the behavior of `A` to
|
||||
be that of `create_before_destroy`.
|
||||
|
||||

|
||||
<!--
|
||||
digraph replacement {
|
||||
subgraph create {
|
||||
rank=same;
|
||||
a [label="A create"];
|
||||
b [label="B create"];
|
||||
a -> b [dir=back];
|
||||
}
|
||||
subgraph destroy {
|
||||
rank=same;
|
||||
a_d [label="A destroy"];
|
||||
b_d [label="B destroy"];
|
||||
a_d -> b_d;
|
||||
}
|
||||
|
||||
a -> a_d [dir=back];
|
||||
a -> b_d;
|
||||
b -> a_d [dir=back];
|
||||
b -> b_d;
|
||||
}
|
||||
-->
|
||||
|
||||
|
||||
Order of operations:
|
||||
1. `B` is destroyed
|
||||
2. `A` is created
|
||||
1. `B` is created
|
||||
1. `A` is destroyed
|
||||
|
||||
Note that in this first example, the creation of `B` is inserted in between the
|
||||
creation of `A` and the destruction of `A`. This becomes more important in the
|
||||
update example below.
|
||||
|
||||
|
||||

|
||||
<!--
|
||||
digraph replacement {
|
||||
subgraph create {
|
||||
rank=same;
|
||||
a [label="A create"];
|
||||
b [label="B update"];
|
||||
a -> b [dir=back];
|
||||
}
|
||||
subgraph destroy {
|
||||
rank=same;
|
||||
a_d [label="A destroy"];
|
||||
}
|
||||
|
||||
a -> a_d [dir=back, style=dotted];
|
||||
b -> a_d [dir=back];
|
||||
}
|
||||
-->
|
||||
|
||||
Order of operations:
|
||||
1. `A` is created
|
||||
1. `B` is updated
|
||||
1. `A` is destroyed
|
||||
|
||||
Here we can see clearly how `B` is updated after the creation of `A` and before
|
||||
the destruction of the _deposed_ resource `A`. (The prior resource `A` is
|
||||
sometimes referred to as "deposed" before it is destroyed, to disambiguate it
|
||||
from the newly created `A`.) This ordering is important for resource that
|
||||
"register" other resources, and require updating before the dependent resource
|
||||
can be destroyed.
|
||||
|
||||
The transformation used to create these graphs is also where we use the extra
|
||||
edges mentioned above connecting `B` to `A destroy`. The algorithm to change a
|
||||
resource from the default ordering to `create_before_destroy` simply inverts
|
||||
any incoming edges from other resources, which automatically creates the
|
||||
necessary dependency ordering for dependent updates. This also ensures that
|
||||
reduced versions of this example still adhere to the same ordering rules, such
|
||||
as when the dependency is only being removed:
|
||||
|
||||

|
||||
<!--
|
||||
digraph update {
|
||||
subgraph create {
|
||||
rank=same;
|
||||
b [label="B update"];
|
||||
}
|
||||
subgraph destroy {
|
||||
rank=same;
|
||||
a_d [label="A destroy"];
|
||||
}
|
||||
|
||||
b -> a_d [dir=back];
|
||||
}
|
||||
-->
|
||||
|
||||
Order of operations:
|
||||
1. `B` is updated
|
||||
1. `A` is destroyed
|
||||
|
||||
### Forced Create Before Destroy
|
||||
|
||||
In the previous examples, only resource `A` was being used as is it were
|
||||
`create_before_destroy`. The minimal graphs used show that it works in
|
||||
isolation, but that is only when the `create_before_destroy` resource has no
|
||||
dependencies of it own. When a `create_before_resource` depends on another
|
||||
resource, that dependency is "infected" by the `create_before_destroy`
|
||||
lifecycle attribute.
|
||||
|
||||
This example demonstrates why forcing `create_before_destroy` is necessary. `B`
|
||||
has `create_before_destroy` while `A` does not. If we only invert the ordering
|
||||
for `B`, we can see that results in a cycle.
|
||||
|
||||

|
||||
<!--
|
||||
digraph replacement {
|
||||
subgraph create {
|
||||
rank=same;
|
||||
a [label="A create"];
|
||||
b [label="B create"];
|
||||
a -> b [dir=back];
|
||||
}
|
||||
subgraph destroy {
|
||||
rank=same;
|
||||
a_d [label="A destroy"];
|
||||
b_d [label="B destroy"];
|
||||
a_d -> b_d;
|
||||
}
|
||||
|
||||
a -> a_d;
|
||||
a -> b_d [style=dotted];
|
||||
b -> a_d [style=dotted];
|
||||
b -> b_d [dir=back];
|
||||
}
|
||||
-->
|
||||
|
||||
In order to resolve these cycles, all resources that precede a resource
|
||||
with `create_before_destroy` must in turn be handled in the same manner.
|
||||
Reversing the incoming edges to `A destroy` resolves the problem:
|
||||
|
||||

|
||||
<!--
|
||||
digraph replacement {
|
||||
subgraph create {
|
||||
rank=same;
|
||||
a [label="A create"];
|
||||
b [label="B create"];
|
||||
a -> b [dir=back];
|
||||
}
|
||||
subgraph destroy {
|
||||
rank=same;
|
||||
a_d [label="A destroy"];
|
||||
b_d [label="B destroy"];
|
||||
a_d -> b_d;
|
||||
}
|
||||
|
||||
a -> a_d [dir=back];
|
||||
a -> b_d [dir=back, style=dotted];
|
||||
b -> a_d [dir=back, style=dotted];
|
||||
b -> b_d [dir=back];
|
||||
}
|
||||
-->
|
||||
|
||||
Order of operations:
|
||||
1. `A` is created
|
||||
1. `B` is created
|
||||
1. `B` is destroyed
|
||||
1. `A` is destroyed
|
||||
|
||||
This also demonstrates why `create_before_destroy` cannot be overridden when
|
||||
it is inherited; changing the behavior here isn't possible without removing
|
||||
the initial reason for `create_before_destroy`; otherwise cycles are always
|
||||
introduced into the graph.
|
||||
495
network-poc/static/docs/diagnostics.md
Normal file
@@ -0,0 +1,495 @@
|
||||
# OpenTofu Diagnostics Guide
|
||||
|
||||
"Diagnostics" is the general term we use to describe the error and warning
|
||||
messages that OpenTofu returns when there are problems with the configuration,
|
||||
or when interactions with external systems fail.
|
||||
|
||||
This document is an overview of how we typically use diagnostics in OpenTofu.
|
||||
It includes both some technical information about how we represent diagnostics
|
||||
in code, and some more subjective information about the writing style we most
|
||||
often use in diagnostic messages.
|
||||
|
||||
## Diagnostics in Code
|
||||
|
||||
Diagnostics are modelled using the types from
|
||||
[the `tfdiags` package](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags).
|
||||
|
||||
In particular:
|
||||
- [`tfdiags.Diagnostics`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#Diagnostics)
|
||||
represents a set of zero or more diagnostics.
|
||||
|
||||
A total lack of diagnostics is usually represented by a `nil` value of this
|
||||
type.
|
||||
|
||||
When constructing sets of diagnostics to return we typically don't worry
|
||||
about the order they are returned in, even though we return them using a
|
||||
slice type. The UI-layer code uses
|
||||
[`tfdiags.Diagnostics.Sort`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#Diagnostics.Sort)
|
||||
to place all of the collected diagnostics into a predictable order before
|
||||
rendering them, and so that function effectively turns the set of
|
||||
diagnostics into an ordered list of diagnostics _just in time_.
|
||||
|
||||
- [`tfdiags.Diagnostic`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#Diagnostic)
|
||||
is an interface type that all diagnostic values implement.
|
||||
|
||||
In practice values of this type are often created automatically as an
|
||||
implementation detail of [`Diagnostics.Append`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#Diagnostics.Append),
|
||||
which accepts various types that _don't_ directly implement
|
||||
`Diagnostic` and then automatically wraps them in a type that does.
|
||||
In particular:
|
||||
|
||||
- We often use [`hcl.Diagnostic`](https://pkg.go.dev/github.com/hashicorp/hcl/v2#Diagnostic)
|
||||
to describe problems related to the configuration or operations that are
|
||||
strongly related to parts of the configuration, because it is the most
|
||||
fully-fledged type of diagnostic we allow including support for source
|
||||
ranges and relevant expressions as described later.
|
||||
|
||||
It's also acceptable to append a whole `hcl.Diagnostics` (the HCL
|
||||
equivalent of `tfdiags.Diagnostics`) in which case each diagnostic
|
||||
will be wrapped and appended in turn. This is common when calling
|
||||
HCL's own functions and passing on its diagnostics verbatim.
|
||||
- Normal `error` values can be appended to a `tfdiags.Diagnostics`, but
|
||||
that's mainly for historical reasons -- adapting code that was present
|
||||
before the diagnostic models were added -- and should not be used in new
|
||||
code because it typically results in low-quality diagnostics that don't
|
||||
meet the style guidelines later in this document.
|
||||
|
||||
One exception is for "should never happen" cases: we sometimes use
|
||||
`error` directly in that case to avoid overwhelming the surrounding
|
||||
code with the construction of a full diagnostic.
|
||||
|
||||
Package `tfdiags` also includes some functions for constructing other kinds
|
||||
of diagnostics, including:
|
||||
|
||||
- [`tfdiags.Sourceless`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#Sourceless)
|
||||
is good for diagnostics that don't relate to any part of the configuration,
|
||||
such as when reporting incorrect usage of a command line argument.
|
||||
- [`tfdiags.AttributeValue`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#AttributeValue) and
|
||||
[`tfdiags.WholeContainingBody`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#WholeContainingBody)
|
||||
produce special "contextual diagnostics" that must be transformed by
|
||||
calling [`InConfigBody`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#Diagnostics.InConfigBody)
|
||||
on the resulting `Diagnostics` value. This is a special mechanism used
|
||||
when the subsystem generating the diagnostic does not have direct access
|
||||
to the configuration itself, such as when a provider returns a diagnostic
|
||||
via the provider wire protocol.
|
||||
- [`tfdiags.Severity`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#Severity)
|
||||
(and its HCL equivalent [`hcl.DiagnosticSeverity`](https://pkg.go.dev/github.com/hashicorp/hcl/v2#DiagnosticSeverity))
|
||||
are how we distinguish between "error" and "warning" diagnostics.
|
||||
|
||||
The `tfdiags.Diagnostics.HasErrors` method returns true if the diagnostics
|
||||
contains at least one with the severity `tfdiags.Error`.
|
||||
|
||||
The most common pattern for handling diagnostics in code is:
|
||||
1. Declare `var diags tfdiags.Diagnostics` at the very start of a function.
|
||||
2. During the function's body, whenever calling another function that might
|
||||
produce its own diagnostics, capture them into a separate variable
|
||||
(often called `moreDiags`, or `hclDiags` if the return type is
|
||||
`hcl.Diagnostics`) and then immediately append them to the main `diags`
|
||||
using `tfdiags.Diagnostics.Append`.
|
||||
|
||||
If subsequent code depends on the success of the call, check
|
||||
`moreDiags.HasErrors()` (or similar) and return early if it returns `true`.
|
||||
3. If the function generates any diagnostics of its own, append them directly
|
||||
to `diags`.
|
||||
4. At all exit points of the function, return `diags` regardless of whether
|
||||
it has been assigned to or whether it contains errors. This ensures that
|
||||
we always return any warnings that might have been produced and avoids
|
||||
the risk of missing certain return paths under future maintenance if we
|
||||
introduce additional diagnostics later.
|
||||
|
||||
Here's a code-example version of the above advice:
|
||||
|
||||
```go
|
||||
func Example() (anything, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
somethingElse, moreDiags := otherFunction()
|
||||
diags = diags.Append(moreDiags)
|
||||
if moreDiags.HasErrors() {
|
||||
// NOTE: it isn't _always_ necessary to return immediately when there
|
||||
// are errors, as long as the callee clearly documents what it
|
||||
// guarantees about an errored result and the caller is able to
|
||||
// work within those limitations. Collecting multiple errors to
|
||||
// return together is often desirable.
|
||||
//
|
||||
// If the caller cannot continue at all though, or if continuing is
|
||||
// likely to cause redundant errors that just restate the same problem
|
||||
// in more confusing terms, then...
|
||||
return nil, diags
|
||||
}
|
||||
if isProblematic(somethingElse) {
|
||||
// A function might need to generate its own diagnostics if it detects
|
||||
// a problem directly.
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
// ...
|
||||
})
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
// The final return statement should include diags even if no errors
|
||||
// were detected along the way, because it might contain warnings.
|
||||
return something, diags
|
||||
}
|
||||
```
|
||||
|
||||
Some functions diverge from this pattern for special reasons, such as capturing
|
||||
multiple sets of child function diagnostics and then using some logic to decide
|
||||
which ones to append, or processing multiple items in a loop and appending
|
||||
new diagnostics for each iteration. The above is just a general example of the
|
||||
most common case, not a fixed template to follow in all cases.
|
||||
|
||||
## Information in a Diagnostic
|
||||
|
||||
The general model of `tfdiags.Diagnostic` has the following parts, though not
|
||||
all implementations of the interface make use of all of them:
|
||||
|
||||
- Severity: either `tfdiags.Error` or `tfdiags.Warning`.
|
||||
- Description: the main human-readable text describing the problem. This
|
||||
has the following fields:
|
||||
|
||||
- Summary: A short, terse description of the general type of problem
|
||||
that has occurred.
|
||||
- Detail: A longer description of the problem, sometimes including multiple
|
||||
paragraphs of information.
|
||||
- Address: The address of some object that the error relates to, which
|
||||
is most often a resource instance address.
|
||||
|
||||
OpenTofu does not currently have a localized UI, so built-in diagnostics
|
||||
always have their summary and detail written in US English. There's more
|
||||
subjective guidance about the content of these fields in sections below.
|
||||
- Source location information: optional references to parts of the configuration
|
||||
that the problem relates to. This has the following fields:
|
||||
|
||||
- Subject: source range for the part of the configuration that caused the
|
||||
problem or that the problem is directly about.
|
||||
- Context: optional source range of a larger section of configuration that
|
||||
might make the cause of the problem easier to quickly understand if
|
||||
included in the diagnostic message. The Context source range must always
|
||||
contain the Subject source range within it.
|
||||
|
||||
The UI uses the context and subject together to display a source code
|
||||
snippet. The lines of code included in the snippet cover both the context
|
||||
and the subject, and then the subject itself is rendered with an underline
|
||||
if we're rendering into a terminal that supports that style.
|
||||
|
||||
We don't use "context" very often, but it can be useful if the problem
|
||||
we're describing is that just one part of a larger source element is
|
||||
problematic. For example, if one of the operands to the `+` operator
|
||||
isn't a number then that operand would be the "subject" but the entire
|
||||
addition operation could be returned as "context", so that both of the
|
||||
operands and the `+` symbol will definitely be included in the rendered
|
||||
diagnostic too.
|
||||
- Expression-related information: optional information about an expression whose
|
||||
evaluation cause the problem. This has the following fields:
|
||||
|
||||
- Expression: The `hcl.Expression` representing the expression itself.
|
||||
- EvalContext: The `hcl.EvalContext` that the expression was being evaluated
|
||||
in.
|
||||
|
||||
The diagnostic renderer for the UI uses this information, when available,
|
||||
to offer some extra hints about the values of any symbols that were used
|
||||
in the expression, because it's often the dynamic values that cause a
|
||||
problem, rather than the syntax used to obtain them.
|
||||
- Extra info: this is a rather underspecified collection of assorted other
|
||||
information that's only relevant in very specific contexts. Refer to the
|
||||
`tfdiags` package documentation for more information.
|
||||
|
||||
There's _some_ guidance on this later in this document, but it's focused
|
||||
only on a few main cases.
|
||||
|
||||
## Diagnostic Description Writing Style
|
||||
|
||||
Although there is some variation in diagnostic writing style, particularly in
|
||||
parts of the system like state storage backends which were originally written by
|
||||
third-parties, most of the _built-in_ diagnostics follow a relatively consistent
|
||||
writing style that is in turn based on the writing style used by HCL itself in
|
||||
its own diagnostics, because HCL and OpenTofu diagnostics often mix together
|
||||
in the same set of problems.
|
||||
|
||||
The "summary" should typically be a very short and concise description of
|
||||
what was wrong and what was wrong about it. Our summaries typically don't
|
||||
include any user-chosen information such as symbol names, because that means a
|
||||
particular kind of problem is always described using the same text and so
|
||||
readers can become familiar enough with the summaries of problems they see
|
||||
frequently to skip reading the rest of the diagnostic when skimming.
|
||||
|
||||
The following are some real examples of summaries currently used across both
|
||||
HCL and OpenTofu:
|
||||
|
||||
- Unsupported operator
|
||||
- Duplicate argument
|
||||
- Invalid index
|
||||
- Unexpected end of template
|
||||
- Invalid template interpolation value
|
||||
- Invalid default value for variable
|
||||
- Required variable not set
|
||||
- Invalid "count" attribute
|
||||
|
||||
The "detail" text is where we tend to put most of the information, and so
|
||||
there's a lot more variation here but ideally a good diagnostic detail
|
||||
should mention the following information, usually in the following order:
|
||||
|
||||
- What was wrong and what was wrong about it: similar to the summary but this
|
||||
time including information about specifically what was wrong, such as the
|
||||
name of the input variable whose default value was invalid.
|
||||
- Why the situation is problematic, if knowing that relies on some
|
||||
characteristic of OpenTofu's design that might not be obvious to a newcomer.
|
||||
- What should be done to fix it, or (if it's unclear what the author's intention
|
||||
was) a question-sentence that implies a _possible_ solution, often starting
|
||||
with the words "Did you mean" and ending with a question mark.
|
||||
|
||||
While the summary message is often terse and uses only minimal punctuation,
|
||||
the detail message should always be written in full sentences including
|
||||
end-of-sentence punctuation (`.`, `?`). If "what was wrong about it" is
|
||||
coming from the string representation of an `error` value, we typically
|
||||
present it with a prefix ending with a colon and then append a period `.`
|
||||
after the error string, and format the error itself using `tfdiags.FormatError`,
|
||||
like this:
|
||||
|
||||
```go
|
||||
Detail: fmt.Sprintf("Unsuitable value for thingy: %s.", tfdiags.FormatError(err))
|
||||
```
|
||||
|
||||
If the second and third items in the above take more than a few words, it's
|
||||
helpful to split them into their own paragraphs for easier scanning. When
|
||||
writing multiple paragraphs in a detail message they should be separated by
|
||||
`\n\n` -- two newline characters.
|
||||
|
||||
In many cases our diagnostics only include a subset of this information because
|
||||
either the reason why it's problematic is relatively clear or because we don't
|
||||
have any specific suggestion for how to solve the problem, but the following
|
||||
is an example of a real diagnostic message from OpenTofu at the time of writing
|
||||
this documentation which includes all of these parts:
|
||||
|
||||
```
|
||||
Error: Invalid for_each argument
|
||||
|
||||
The "for_each" map includes keys derived from resource attributes that cannot
|
||||
be determined until apply, and so OpenTofu cannot determine the full set of keys
|
||||
that will identify the instances of this resource.
|
||||
|
||||
When working with unknown values in for_each, it's better to define the map keys
|
||||
statically in your configuration and place apply-time results only in the map
|
||||
values.
|
||||
|
||||
Alternatively, you could use the planning option -exclude=aws_instance.example
|
||||
to first apply without this object, and then apply normally to converge.
|
||||
```
|
||||
|
||||
The text immediately after "Error:" above is the summary for this diagnostic.
|
||||
The paragraphs that follow are all a single "detail" string.
|
||||
|
||||
That was a particularly extreme diagnostic message with lots of information to
|
||||
communicate. Most diagnostics are not so complicated; the following is an
|
||||
example with less information to communicate:
|
||||
|
||||
```
|
||||
Error: Invalid value for input variable
|
||||
|
||||
The given value is not suitable for var.example declared
|
||||
at example.tf:12,1: a string is required.
|
||||
```
|
||||
|
||||
This example also illustrates a situation where there are two different source
|
||||
locations that could be relevant: the input variable's declaration or the
|
||||
expression that's used to define its value. Because this message is talking
|
||||
about a problem with the _value_, the diagnostic should have the source
|
||||
"Subject" set to the expression that defined it, but it also mentions the
|
||||
location of the declaration as part of the detail text as some additional
|
||||
context.
|
||||
|
||||
Some other notes about some other specific situations that arise sometimes:
|
||||
|
||||
- If a diagnostic message includes a suggestion for a shell command to run
|
||||
or a URL to visit for more information, use a paragraph that ends with a
|
||||
colon, followed by a single newline, four spaces for indentation, and then the
|
||||
command or URL:
|
||||
|
||||
```
|
||||
To view the root module output values, run:
|
||||
tofu output
|
||||
```
|
||||
|
||||
The goal of this formatting is to make it very clear what part of the
|
||||
message is intended to be copied and used elsewhere, by placing it on a
|
||||
line of its own without any surrounding punctuation. The indented text
|
||||
should ideally be formatted so that the user can copy it _verbatim_ into
|
||||
whatever place it will be used.
|
||||
|
||||
The diagnostic renderer also has a special case where it will not try to
|
||||
word-wrap a line that begins with spaces, and so this layout has the
|
||||
useful side-effect of avoiding introducing extra newline characters into
|
||||
a command line that is intended to be copied.
|
||||
|
||||
- There are some terminology choices we use to refer to some OpenTofu-specific
|
||||
ideas and concepts that disagree slightly with terminology used in the code.
|
||||
These differences are the result of learning from feedback from folks who
|
||||
had been confused by the original terminology, even though the code still
|
||||
often uses the original terminology:
|
||||
|
||||
- Instead of referring to "unknown values" or "computed values" we say that
|
||||
values are "known after apply" or "cannot be determined until apply".
|
||||
- In HCL the word "variable" means anything that's available to refer to
|
||||
in the current evaluation context, which is confusing because OpenTofu
|
||||
itself uses that word to refer only to input variables.
|
||||
|
||||
Sometimes messages are generated by HCL itself and so it's unavoidably
|
||||
confusing, but when we're generating messages _inside OpenTofu_ we
|
||||
use the two words "input variable" to refer to an input variable,
|
||||
and "symbol" or "object" (depending on whether we're talking about
|
||||
the name itself or what the name refers to) as the general word for
|
||||
something you can refer to in an expression.
|
||||
- For consistency with our use of "input variable" to distinguish from
|
||||
HCL's more general meaning of "variable", we also tend to write
|
||||
"local value" and "output value" when referring to those concepts, rather
|
||||
than using the shorthands "locals" and "outputs".
|
||||
- HCL distinguishes between "attributes" meaning the named keys inside an
|
||||
object type, and "arguments" meaning the names used for individual
|
||||
settings inside a configuration block.
|
||||
|
||||
OpenTofu itself uses those words a little more interchangeably because
|
||||
in _many_ cases the configuration arguments in a block directly
|
||||
correspond to the attributes of an object created by evaluating that
|
||||
block.
|
||||
|
||||
However, if a particular error message is talking about a configuration
|
||||
setting inside a block it's better to use "argument" rather than
|
||||
"attribute" because that's then consistent with error messages that
|
||||
HCL itself might generate.
|
||||
|
||||
Go uses the term "field" to describe an element of a struct type, and
|
||||
JavaScript and JSON use the word "property" to describe an element of
|
||||
an object type. We don't use either of those words in OpenTofu: the
|
||||
elements of an object are its _attributes_, and the settings available
|
||||
in a configuration block are its _arguments_. The string values that
|
||||
identify elements of a map are called "keys".
|
||||
- The `cty` terminology "marks" or "value marks" refers to an implementation
|
||||
detail that should never be mentioned directly in an error message.
|
||||
|
||||
Instead, we use specific terminology related to what each mark type
|
||||
is representing: "sensitive values", "ephemeral values", etc.
|
||||
- `aws_instance` is an example of a "resource _type_", not of a "resource",
|
||||
even though the provider protocol uses the single noun "resource" to refer
|
||||
to both ideas.
|
||||
|
||||
A "resource" is what's declared by a `resource`, `data`, or `ephemeral`
|
||||
block. A "resource _instance_" is what such a block can declare zero
|
||||
or more of, when using the `count`, `for_each`, or `enabled` arguments.
|
||||
- Although there are certainly some historical diagnostic messages that
|
||||
predate this adjustment of terminology, new error messages should use
|
||||
"managed resource" to refer to the kind of resource that's declared
|
||||
using a `resource` block, "data resource" for `data` blocks, and
|
||||
"ephemeral resource" for an `ephemeral` block.
|
||||
|
||||
In the code we refer to these three as "resource _modes_", but that is
|
||||
internal terminology that should never appear in a diagnostic message.
|
||||
- When a file or directory path appears as part of a diagnostic message, it
|
||||
should typically be presented relative to the current working directory and
|
||||
should use the syntax conventions of the platform where OpenTofu is running.
|
||||
|
||||
In particular, we return paths using backslashes as the separator when we
|
||||
are running on Windows, but normal slashes otherwise. Using the Go
|
||||
`filepath` package is a good way to get this right, though you might need
|
||||
to add some complexity to your tests to make them pass on all platforms.
|
||||
- If an error message is describing a "should never happen" case, we typically
|
||||
end the detail string with the sentence "This is a bug in OpenTofu.". This
|
||||
hopefully prompts the reader that this wasn't directly caused by something
|
||||
they did, and so they should probably open a bug report in the
|
||||
OpenTofu repository instead of just trying to solve it themselves.
|
||||
|
||||
For this kind of error message we often relax our preference against
|
||||
mentioning implementation details in the error message, because the most
|
||||
likely next step is for the user to copy-paste the entire message into their
|
||||
bug report text and so the final reader of the message is OpenTofu
|
||||
maintainers rather than OpenTofu users.
|
||||
|
||||
For example, it can be okay to use internal terminology like "cty marks" and
|
||||
use the `GoString` representations of values in a "This is a bug in
|
||||
OpenTofu" detail message, if that's the most concise way to capture the
|
||||
information the OpenTofu maintainers would need to debug the problem.
|
||||
|
||||
## Diagnostics caused by unknown or sensitive values
|
||||
|
||||
When a diagnostic has expression information associated with it, the diagnostic
|
||||
renderer for the UI includes some additional information about the values
|
||||
that were in scope, like this:
|
||||
|
||||
```
|
||||
var.greeting is "Hello"
|
||||
var.items is list of string with 5 elements
|
||||
```
|
||||
|
||||
By default, this renderer will not mention any symbol which refers to an unknown
|
||||
or sensitive value. That was not historically true: originally, this could
|
||||
say something like "var.example is a string, known only after apply".
|
||||
|
||||
Those who are less familiar with these concepts often misunderstood the
|
||||
"known only after apply" part of the message as being _the problem itself_,
|
||||
rather than just context to help diagnose the problem, and so the UI no longer
|
||||
mentions "unknown-ness" or "sensitive-ness" in most cases.
|
||||
|
||||
However, there are some diagnostics messages that _are_ directly caused by the
|
||||
presence of an unknown or sensitive value, in which case it's helpful to
|
||||
mention that in the summary of values that were in scope.
|
||||
|
||||
To allow for this, we set the "extra info" field of a diagnostic to contain
|
||||
an implementation of one of the following interfaces:
|
||||
|
||||
- [`tfdiags.DiagnosticExtraBecauseUnknown`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#DiagnosticExtraBecauseUnknown)
|
||||
for a problem that's caused by an unknown value.
|
||||
|
||||
(Remember that the _text_ of the error message should refer to this as "known
|
||||
only after apply", or similar.)
|
||||
- [`tfdiags.DiagnosticExtraBecauseSensitive`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#DiagnosticExtraBecauseSensitive)
|
||||
for situations where a sensitive value was used in a location that OpenTofu
|
||||
cannot permit it, such as in the instance key of a resource instance.
|
||||
|
||||
These extra markers should be used only when mentioning the unknown or sensitive
|
||||
values in the diagnostic message is likely to help with debugging a problem.
|
||||
If the problem is not directly caused by unknown or sensitive values then
|
||||
neither of these should be used, to avoid creating a distracting
|
||||
[red herring](https://en.wikipedia.org/wiki/Red_herring) for the reader.
|
||||
|
||||
## Consolidation of Diagnostics
|
||||
|
||||
The UI layer has some special rules for finding sets of similar diagnostics
|
||||
and showing them as just a single diagnostic referring to the first example
|
||||
of a problem, with a short extra note about how many other similar diagnostics
|
||||
there are.
|
||||
|
||||
```
|
||||
(and 2 similar warnings elsewhere)
|
||||
```
|
||||
|
||||
The main implementation of this behavior is in
|
||||
[`tfdiags.Diagnostics.Consolidate`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#Diagnostics.Consolidate),
|
||||
but we allow end-users to customize (using command line options) whether this
|
||||
consolidation applies to errors or warnings separately. By default, we
|
||||
consolidate only warnings.
|
||||
|
||||
For a severity that is subject to consolidation, the main behavior is to group
|
||||
together diagnostics that have the same "summary" text, and this is part of
|
||||
why we tend to use terse, fixed strings in the summary field.
|
||||
|
||||
There are two extra mechanisms for customizing this behavior for specific
|
||||
diagnostic messages:
|
||||
|
||||
- If the "extra info" of a diagnostic contains an implementation of
|
||||
[`tfdiags.DiagnosticExtraDoNotConsolidate`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#DiagnosticExtraDoNotConsolidate)
|
||||
then that diagnostic is not eligible for consolidation at all, regardless
|
||||
of how similar it might be to other diagnostics in the same set.
|
||||
- If the "extra info" of a diagnostic contains an implementation of
|
||||
[`tfdiags.Keyable`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#Keyable)
|
||||
then the string returned by its `ExtraInfoKey` method is used _in addition to_
|
||||
the summary text for deciding what to consolidate.
|
||||
|
||||
For example, if there were three warnings with the same summary text but
|
||||
two of them have the same `ExtraInfoKey` and the third has a different
|
||||
one then only the first two would be able to consolidate.
|
||||
|
||||
The `ExtraInfoKey` is an internal key used for comparison only and is never
|
||||
exposed in the UI, so it can be set to whatever makes sense to define
|
||||
separate consolidation groups for diagnostics with a specific summary.
|
||||
78
network-poc/static/docs/glossary.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# OpenTofu glossary
|
||||
This document is intended for anyone who wants to gain more knowledge about the terms and vocabulary used to talk about different concepts in OpenTofu.
|
||||
|
||||
> [!NOTE]
|
||||
> This was created with the intent of gathering more knowledge over time.
|
||||
> The state that you find this in right now could be incomplete.
|
||||
> Once you discover and learn about a new concept that would benefit others,
|
||||
> feel free to open a PR to update this.
|
||||
|
||||
> [!NOTE]
|
||||
> When adding new content to this document, try to place it in such a way to match the alphabetical order of the already existing content.
|
||||
>
|
||||
> Optionally, also add a reference link if possible (GitHub conversation, issue, other docs, etc).
|
||||
|
||||
## OpenTofu
|
||||
### Attribute/argument/field
|
||||
* Attribute - a named key inside an object type.
|
||||
* Argument - a name used for an individual setting inside a configuration block.
|
||||
|
||||
It is recommended to avoid using popular programming language terms such as "field" or "property" to describe an element of an object in OpenTofu.
|
||||
|
||||
Reference: [link](./diagnostics.md#diagnostic-description-writing-style)
|
||||
|
||||
### Data sources/data resource
|
||||
* Data source - the remote thing that the data resource reads from.
|
||||
* Data resource - refers to a block of type `data`, and the associated object it declares.
|
||||
* Data resource type - is what is represented by the first label in a `data` block header, and the associated declarations and code for it in the provider plugin.
|
||||
|
||||
Reference: [link](https://github.com/opentofu/opentofu/pull/3389#discussion_r2440264786)
|
||||
|
||||
### Diagnostic
|
||||
"Diagnostic" is the general term we use to describe the error or warning
|
||||
message that OpenTofu returns when there are problems with the configuration,
|
||||
or when interactions with external systems fail.
|
||||
|
||||
Reference: [link](./diagnostics.md)
|
||||
|
||||
### Mark/value mark
|
||||
OpenTofu uses cty.Value to represent the result of expressions (and other data).
|
||||
Occasionally, we will want to annotate that data with additional properties, without actually modifying the underlying value.
|
||||
Marks are used for that purpose and are the preferred method of doing so with go-cty.
|
||||
|
||||
### Resource/resource instance/resource type
|
||||
* Resource - is what is declared by a `resource`, `data` or `ephemeral` block.
|
||||
* Resource instance - is what such a block can declare zero or more of, when using the `count`, `for_each`, or `enabled` arguments.
|
||||
* Resource type - the type of a "resource". E.g., `aws_instance` is a "resource type".
|
||||
|
||||
It is recommended to use the following terms when discussing about "resource" blocks:
|
||||
* managed resource - a block declared as `resource "type" "name" {}`
|
||||
* data resource - a block declared as `data "type" "name" {}`
|
||||
* ephemeral resource - a block declared as `ephemeral "type" "name" {}`
|
||||
|
||||
Reference: [link](./diagnostics.md#diagnostic-description-writing-style)
|
||||
|
||||
### Unknown value/Computed value
|
||||
* Unknown value - Unknown values are the result of expressions that have unknown inputs. E.g.: a value that will not be known until a resource is created.
|
||||
Right now the main source of this type of values is resources, but we are considering adding others like unknown inputs.
|
||||
Another place where an unknown value can be encountered is from using some of the built-in functions like `timestamp`, `bcrypt` and `uuid`.
|
||||
* Computed value - Computed is more of a resource specific concept that a provider can specify in its resource schema.
|
||||
When set to true, the provider does not expect a value and may instead produce one that may or may not be unknown.
|
||||
With other flags, the actual functionality is a bit more subtle.
|
||||
|
||||
References: [link](https://github.com/opentofu/opentofu/blob/490762343322eff42c0586f7a4c267b579fe80ef/internal/configs/configschema/schema.go#L65), [link](https://github.com/opentofu/opentofu/blob/490762343322eff42c0586f7a4c267b579fe80ef/internal/lang/functions.go#L22)
|
||||
## HCL
|
||||
### Evaluation context (HCL)
|
||||
A set of already known functions, input values, local values, resources, etc. that is used to evaluate an expression that can reference any of the concepts listed above.
|
||||
|
||||
The list of concepts above, in the context of HCL evaluation, are called [variables](#variable-hcl).
|
||||
|
||||
### Expression
|
||||
An expression is any right hand side of an assignment that will be evaluated to generate the value that will be associated with key on the left hand side of the assignment.
|
||||
The simplest expressions are just literal values, like "hello" or 5, but the OpenTofu language also allows more complex
|
||||
expressions such as references to data exported by resources, arithmetic, conditional evaluation, and a number of built-in and provider-defined functions.
|
||||
|
||||
Reference: [link](https://opentofu.org/docs/language/expressions/)
|
||||
|
||||
### Variable (HCL)
|
||||
Anything that's available to refer to in the current evaluation context.
|
||||
BIN
network-poc/static/docs/images/architecture-overview.png
Normal file
|
After Width: | Height: | Size: 228 KiB |
BIN
network-poc/static/docs/images/destroy_then_update.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
network-poc/static/docs/images/replace_all.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
network-poc/static/docs/images/replace_all_cbd.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
network-poc/static/docs/images/replace_all_cbd_dep.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
network-poc/static/docs/images/replace_cbd_incorrect.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
network-poc/static/docs/images/replace_dep_cbd_dep.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
network-poc/static/docs/images/replace_one.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 194 KiB |
BIN
network-poc/static/docs/images/simple_create.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
network-poc/static/docs/images/simple_destroy.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
network-poc/static/docs/images/simple_update.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |