Compare commits
47 Commits
pre-worker
...
6b756e2e83
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -1,7 +1,7 @@
|
|||||||
FROM rust:slim AS builder
|
FROM rust:slim AS builder
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
pkg-config libssl-dev g++ \
|
pkg-config libssl-dev g++ libvulkan-dev \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -9,22 +9,27 @@ COPY Cargo.toml Cargo.lock ./
|
|||||||
COPY hub/Cargo.toml hub/Cargo.toml
|
COPY hub/Cargo.toml hub/Cargo.toml
|
||||||
COPY node/Cargo.toml node/Cargo.toml
|
COPY node/Cargo.toml node/Cargo.toml
|
||||||
COPY native-node/Cargo.toml native-node/Cargo.toml
|
COPY native-node/Cargo.toml native-node/Cargo.toml
|
||||||
|
COPY cli/Cargo.toml cli/Cargo.toml
|
||||||
|
|
||||||
# Tyhjät src-tiedostot riippuvuuksien esikääntämistä varten
|
# Tyhjät src-tiedostot riippuvuuksien esikääntämistä varten
|
||||||
RUN mkdir -p hub/src node/src native-node/src \
|
RUN mkdir -p hub/src node/src native-node/src cli/src \
|
||||||
&& echo "fn main(){}" > hub/src/main.rs \
|
&& echo "fn main(){}" > hub/src/main.rs \
|
||||||
&& echo "" > node/src/lib.rs \
|
&& echo "" > node/src/lib.rs \
|
||||||
&& echo "fn main(){}" > native-node/src/main.rs \
|
&& echo "fn main(){}" > native-node/src/main.rs \
|
||||||
|
&& echo "fn main(){}" > cli/src/main.rs \
|
||||||
&& cargo build --release -p native-node 2>/dev/null || true
|
&& cargo build --release -p native-node 2>/dev/null || true
|
||||||
|
|
||||||
COPY native-node/src native-node/src
|
COPY native-node/src native-node/src
|
||||||
RUN cargo build --release -p native-node
|
# Touch pakottaa rekompilauksen dummy-binaryn yli
|
||||||
|
RUN touch native-node/src/main.rs && cargo build --release -p native-node
|
||||||
|
|
||||||
FROM debian:bookworm-slim
|
FROM debian:bookworm-slim
|
||||||
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y ca-certificates libvulkan1 && rm -rf /var/lib/apt/lists/*
|
||||||
COPY --from=builder /app/target/release/native-node /usr/local/bin/native-node
|
COPY --from=builder /app/target/release/native-node /usr/local/bin/native-node
|
||||||
|
|
||||||
ENV HUB_URL=ws://hub:3000/ws
|
ENV HUB_URL=ws://agentic-poc:3000/ws
|
||||||
|
ENV OLLAMA_URL=http://ollama:11434
|
||||||
|
ENV OLLAMA_MODEL=qwen2.5-coder:7b
|
||||||
ENV ALLOCATED_GB=4
|
ENV ALLOCATED_GB=4
|
||||||
|
|
||||||
CMD ["native-node"]
|
CMD ["native-node"]
|
||||||
|
|||||||
26
network-poc/TODO.md
Normal file
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
|
||||||
@@ -11,18 +11,14 @@ services:
|
|||||||
# Käännetään aina käynnistyksen yhteydessä varmuuden vuoksi Wasm uusimmista koodeista, ja päälle pyöräytetään Hub!
|
# Käännetään aina käynnistyksen yhteydessä varmuuden vuoksi Wasm uusimmista koodeista, ja päälle pyöräytetään Hub!
|
||||||
command: bash -c "cd node && wasm-pack build --release --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 GPU:lla (NVIDIA/AMD/Apple)
|
||||||
native-node:
|
ollama:
|
||||||
build:
|
image: ollama/ollama:latest
|
||||||
context: .
|
container_name: kipina_ollama
|
||||||
dockerfile: Dockerfile.native-node
|
ports:
|
||||||
container_name: kipina_native_node
|
- "11434:11434"
|
||||||
environment:
|
volumes:
|
||||||
- HUB_URL=ws://agentic-poc:3000/ws
|
- ollama-models:/root/.ollama
|
||||||
- ALLOCATED_GB=4
|
|
||||||
depends_on:
|
|
||||||
- agentic-poc
|
|
||||||
# GPU passthrough (valinnainen — toimii myös ilman)
|
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
reservations:
|
reservations:
|
||||||
@@ -32,3 +28,23 @@ services:
|
|||||||
capabilities: [gpu]
|
capabilities: [gpu]
|
||||||
profiles:
|
profiles:
|
||||||
- native
|
- native
|
||||||
|
|
||||||
|
# Natiivisolmu — yhdistää hubiin ja käyttää Ollamaa inferenssiin
|
||||||
|
native-node:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.native-node
|
||||||
|
container_name: kipina_native_node
|
||||||
|
environment:
|
||||||
|
- HUB_URL=ws://agentic-poc:3000/ws
|
||||||
|
- OLLAMA_URL=http://ollama:11434
|
||||||
|
- OLLAMA_MODEL=qwen2.5-coder:7b
|
||||||
|
- ALLOCATED_GB=4
|
||||||
|
depends_on:
|
||||||
|
- agentic-poc
|
||||||
|
- ollama
|
||||||
|
profiles:
|
||||||
|
- native
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
ollama-models:
|
||||||
|
|||||||
Binary file not shown.
@@ -39,6 +39,7 @@ struct AppState {
|
|||||||
ip_connections: Mutex<HashMap<IpAddr, u32>>,
|
ip_connections: Mutex<HashMap<IpAddr, u32>>,
|
||||||
node_ips: Mutex<HashMap<u64, IpAddr>>,
|
node_ips: Mutex<HashMap<u64, IpAddr>>,
|
||||||
node_tasks: Mutex<HashMap<u64, String>>, // node_id → selected_task
|
node_tasks: Mutex<HashMap<u64, String>>, // node_id → selected_task
|
||||||
|
node_types: Mutex<HashMap<u64, String>>, // node_id → "native" | "browser"
|
||||||
node_busy: Mutex<std::collections::HashSet<u64>>, // Solmut joilla on aktiivinen tehtävä
|
node_busy: Mutex<std::collections::HashSet<u64>>, // Solmut joilla on aktiivinen tehtävä
|
||||||
pending_task_ids: Mutex<std::collections::HashSet<String>>, // Hubin jakamat task_id:t (gamification-validointi)
|
pending_task_ids: Mutex<std::collections::HashSet<String>>, // Hubin jakamat task_id:t (gamification-validointi)
|
||||||
api_rate_limits: Mutex<HashMap<IpAddr, (std::time::Instant, u32)>>, // IP → (ikkuna-alku, pyyntömäärä)
|
api_rate_limits: Mutex<HashMap<IpAddr, (std::time::Instant, u32)>>, // IP → (ikkuna-alku, pyyntömäärä)
|
||||||
@@ -260,6 +261,7 @@ async fn main() {
|
|||||||
ip_connections: Mutex::new(HashMap::new()),
|
ip_connections: Mutex::new(HashMap::new()),
|
||||||
node_ips: Mutex::new(HashMap::new()),
|
node_ips: Mutex::new(HashMap::new()),
|
||||||
node_tasks: Mutex::new(HashMap::new()),
|
node_tasks: Mutex::new(HashMap::new()),
|
||||||
|
node_types: Mutex::new(HashMap::new()),
|
||||||
node_busy: Mutex::new(std::collections::HashSet::new()),
|
node_busy: Mutex::new(std::collections::HashSet::new()),
|
||||||
pending_task_ids: Mutex::new(std::collections::HashSet::new()),
|
pending_task_ids: Mutex::new(std::collections::HashSet::new()),
|
||||||
api_rate_limits: Mutex::new(HashMap::new()),
|
api_rate_limits: Mutex::new(HashMap::new()),
|
||||||
@@ -382,6 +384,8 @@ async fn main() {
|
|||||||
.route("/api/pairs", get(api_pairs))
|
.route("/api/pairs", get(api_pairs))
|
||||||
.route("/api/stats", get(api_stats))
|
.route("/api/stats", get(api_stats))
|
||||||
.route("/api/v1/chat/completions", axum::routing::post(api_chat_completions))
|
.route("/api/v1/chat/completions", axum::routing::post(api_chat_completions))
|
||||||
|
.route("/api/v1/model", axum::routing::post(api_change_model))
|
||||||
|
.route("/api/v1/hardware", get(api_hardware))
|
||||||
.route("/admin", get(admin_page))
|
.route("/admin", get(admin_page))
|
||||||
.nest_service("/", {
|
.nest_service("/", {
|
||||||
let static_dir = std::env::var("STATIC_DIR").unwrap_or_else(|_| "../static".to_string());
|
let static_dir = std::env::var("STATIC_DIR").unwrap_or_else(|_| "../static".to_string());
|
||||||
@@ -677,6 +681,7 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
|
|||||||
state.db.insert_session(node_id, &ip.to_string(), node_type, &json);
|
state.db.insert_session(node_id, &ip.to_string(), node_type, &json);
|
||||||
}
|
}
|
||||||
state.node_tasks.lock().unwrap().insert(node_id, selected_task);
|
state.node_tasks.lock().unwrap().insert(node_id, selected_task);
|
||||||
|
state.node_types.lock().unwrap().insert(node_id, node_type.to_string());
|
||||||
|
|
||||||
if node_type == "native" {
|
if node_type == "native" {
|
||||||
let sys = json.get("system");
|
let sys = json.get("system");
|
||||||
@@ -934,6 +939,7 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
|
|||||||
ips.remove(&node_id);
|
ips.remove(&node_id);
|
||||||
vram.remove(&node_id);
|
vram.remove(&node_id);
|
||||||
}
|
}
|
||||||
|
state.node_types.lock().unwrap().remove(&node_id);
|
||||||
tracing::info!("Solmu {} ({}) poistui verkosta.", node_id, ip);
|
tracing::info!("Solmu {} ({}) poistui verkosta.", node_id, ip);
|
||||||
broadcast_stats(&state).await;
|
broadcast_stats(&state).await;
|
||||||
sender_task.abort();
|
sender_task.abort();
|
||||||
@@ -943,6 +949,8 @@ struct ChatCompletionRequest {
|
|||||||
model: String,
|
model: String,
|
||||||
prompt: String,
|
prompt: String,
|
||||||
task_id: String,
|
task_id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
max_tokens: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
@@ -952,6 +960,47 @@ struct ChatCompletionResponse {
|
|||||||
tokens_generated: u64,
|
tokens_generated: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 (vram_mb, 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("?");
|
||||||
|
let ram = s.get("system").and_then(|v| v.get("ram_total_mb")).and_then(|v| v.as_u64()).unwrap_or(0);
|
||||||
|
(vram, name.to_string(), ram)
|
||||||
|
} else {
|
||||||
|
(0, "ei natiivisolmua".to_string(), 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
axum::Json(serde_json::json!({
|
||||||
|
"gpu_name": gpu_name,
|
||||||
|
"vram_mb": vram_mb,
|
||||||
|
"ram_mb": ram_mb,
|
||||||
|
})).into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn api_change_model(
|
||||||
|
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
|
||||||
|
axum::Json(payload): axum::Json<serde_json::Value>,
|
||||||
|
) -> axum::response::Response {
|
||||||
|
let model = payload.get("model").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
|
if model.is_empty() {
|
||||||
|
return (axum::http::StatusCode::BAD_REQUEST, "model puuttuu").into_response();
|
||||||
|
}
|
||||||
|
tracing::info!("Mallin vaihto: {}", model);
|
||||||
|
let msg = serde_json::json!({ "type": "change_model", "model": model });
|
||||||
|
let _ = state.stats_tx.send(msg.to_string());
|
||||||
|
axum::Json(serde_json::json!({ "status": "ok", "model": model })).into_response()
|
||||||
|
}
|
||||||
|
|
||||||
async fn api_chat_completions(
|
async fn api_chat_completions(
|
||||||
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
|
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
|
||||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||||
@@ -966,16 +1015,17 @@ async fn api_chat_completions(
|
|||||||
*entry = (now, 1); // Uusi ikkuna
|
*entry = (now, 1); // Uusi ikkuna
|
||||||
} else {
|
} else {
|
||||||
entry.1 += 1;
|
entry.1 += 1;
|
||||||
if entry.1 > 10 {
|
if entry.1 > 30 {
|
||||||
return (axum::http::StatusCode::TOO_MANY_REQUESTS, "Liian monta pyyntöä — yritä minuutin kuluttua").into_response();
|
return (axum::http::StatusCode::TOO_MANY_REQUESTS, "Liian monta pyyntöä — yritä minuutin kuluttua").into_response();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Etsitään vapaa tai varattu solmu, joka vastaa pyydettyä mallia
|
// Etsitään vapaa solmu — priorisoidaan natiivisolmut (GPU) selaimen edelle
|
||||||
let (target_node_free, target_node_any, total_matching) = {
|
let (target_node_free, target_node_any, total_matching) = {
|
||||||
let tasks = state.node_tasks.lock().unwrap();
|
let tasks = state.node_tasks.lock().unwrap();
|
||||||
let busy = state.node_busy.lock().unwrap();
|
let busy = state.node_busy.lock().unwrap();
|
||||||
|
let node_types = state.node_types.lock().unwrap();
|
||||||
let matching: Vec<u64> = tasks.iter().filter(|(_, task)| {
|
let matching: Vec<u64> = tasks.iter().filter(|(_, task)| {
|
||||||
if payload.model == "qwen-coder" {
|
if payload.model == "qwen-coder" {
|
||||||
task.starts_with("qwen-coder")
|
task.starts_with("qwen-coder")
|
||||||
@@ -983,7 +1033,12 @@ async fn api_chat_completions(
|
|||||||
**task == payload.model
|
**task == payload.model
|
||||||
}
|
}
|
||||||
}).map(|(k, _)| *k).collect();
|
}).map(|(k, _)| *k).collect();
|
||||||
let free = matching.iter().find(|id| !busy.contains(id)).copied();
|
// Vapaat solmut: natiivi ensin, sitten selain
|
||||||
|
let free_native = matching.iter().find(|id| {
|
||||||
|
!busy.contains(id) && node_types.get(id).map(|t| t == "native").unwrap_or(false)
|
||||||
|
}).copied();
|
||||||
|
let free_any = matching.iter().find(|id| !busy.contains(id)).copied();
|
||||||
|
let free = free_native.or(free_any);
|
||||||
let any = matching.first().copied();
|
let any = matching.first().copied();
|
||||||
(free, any, matching.len())
|
(free, any, matching.len())
|
||||||
};
|
};
|
||||||
@@ -1059,12 +1114,15 @@ async fn api_chat_completions(
|
|||||||
state.node_busy.lock().unwrap().insert(target_node_id);
|
state.node_busy.lock().unwrap().insert(target_node_id);
|
||||||
state.pending_task_ids.lock().unwrap().insert(payload.task_id.clone());
|
state.pending_task_ids.lock().unwrap().insert(payload.task_id.clone());
|
||||||
|
|
||||||
let msg = serde_json::json!({
|
let mut msg = serde_json::json!({
|
||||||
"type": "llm_prompt",
|
"type": "llm_prompt",
|
||||||
"prompt": payload.prompt,
|
"prompt": payload.prompt,
|
||||||
"model": payload.model,
|
"model": payload.model,
|
||||||
"task_id": payload.task_id,
|
"task_id": payload.task_id,
|
||||||
});
|
});
|
||||||
|
if let Some(mt) = payload.max_tokens {
|
||||||
|
msg.as_object_mut().unwrap().insert("max_tokens".to_string(), serde_json::json!(mt));
|
||||||
|
}
|
||||||
|
|
||||||
// Odotuskanava valmiiksi (solmu palauttaa tuloksen stats_tx kautta)
|
// Odotuskanava valmiiksi (solmu palauttaa tuloksen stats_tx kautta)
|
||||||
let mut rx = state.stats_tx.subscribe();
|
let mut rx = state.stats_tx.subscribe();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "native-node"
|
name = "native-node"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
@@ -12,10 +12,6 @@ serde_json = "1.0"
|
|||||||
sysinfo = "0.30"
|
sysinfo = "0.30"
|
||||||
nvml-wrapper = "0.10"
|
nvml-wrapper = "0.10"
|
||||||
wgpu = "24"
|
wgpu = "24"
|
||||||
candle-core = { version = "0.8", features = ["cuda"] }
|
reqwest = { version = "0.12", features = ["json"] }
|
||||||
candle-nn = "0.8"
|
|
||||||
candle-transformers = "0.8"
|
|
||||||
hf-hub = "0.4"
|
|
||||||
tokenizers = "0.19"
|
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
|||||||
@@ -1,261 +1,114 @@
|
|||||||
use candle_core::{Device, Tensor, DType};
|
|
||||||
use candle_nn::VarBuilder;
|
|
||||||
use candle_transformers::models::qwen2::{Config as QwenConfig, ModelForCausalLM as QwenModel};
|
|
||||||
use hf_hub::{api::sync::Api, Repo, RepoType};
|
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
use std::cell::RefCell;
|
||||||
/// Top-k sampling with temperature and repetition penalty
|
|
||||||
fn sample_top_k(logits: &Tensor, k: usize, temperature: f64, generated_tokens: &[u32], repetition_penalty: f64, rng_state: &mut u64) -> Result<u32, String> {
|
|
||||||
let mut logits_vec: Vec<f32> = logits.to_vec1::<f32>().map_err(|e| format!("to_vec1: {}", e))?;
|
|
||||||
if logits_vec.is_empty() { return Err("Tyhjä logits".to_string()); }
|
|
||||||
|
|
||||||
// Repetition penalty: rankaisee jo generoituja tokeneita
|
|
||||||
for &token_id in generated_tokens {
|
|
||||||
if (token_id as usize) < logits_vec.len() {
|
|
||||||
let logit = &mut logits_vec[token_id as usize];
|
|
||||||
if *logit > 0.0 {
|
|
||||||
*logit /= repetition_penalty as f32;
|
|
||||||
} else {
|
|
||||||
*logit *= repetition_penalty as f32;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Temperature scaling
|
|
||||||
if temperature > 0.0 && temperature != 1.0 {
|
|
||||||
for logit in logits_vec.iter_mut() {
|
|
||||||
*logit /= temperature as f32;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Top-k: etsitään k suurinta
|
|
||||||
let mut indexed: Vec<(usize, f32)> = logits_vec.iter().enumerate().map(|(i, &v)| (i, v)).collect();
|
|
||||||
indexed.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
|
|
||||||
indexed.truncate(k);
|
|
||||||
|
|
||||||
if k == 1 || temperature == 0.0 {
|
|
||||||
return Ok(indexed[0].0 as u32);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Softmax top-k:lle
|
|
||||||
let max_logit = indexed[0].1;
|
|
||||||
let exps: Vec<f32> = indexed.iter().map(|x| (x.1 - max_logit).exp()).collect();
|
|
||||||
let sum: f32 = exps.iter().sum();
|
|
||||||
let probs: Vec<f32> = exps.iter().map(|e| e / sum).collect();
|
|
||||||
|
|
||||||
// XorShift64 RNG
|
|
||||||
*rng_state ^= *rng_state << 13;
|
|
||||||
*rng_state ^= *rng_state >> 7;
|
|
||||||
*rng_state ^= *rng_state << 17;
|
|
||||||
let rand_val = (*rng_state % 10000) as f32 / 10000.0;
|
|
||||||
|
|
||||||
let mut cumulative = 0.0;
|
|
||||||
for (i, p) in probs.iter().enumerate() {
|
|
||||||
cumulative += p;
|
|
||||||
if rand_val < cumulative {
|
|
||||||
return Ok(indexed[i].0 as u32);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(indexed[0].0 as u32)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct LlmEngine {
|
pub struct LlmEngine {
|
||||||
tokenizer: tokenizers::Tokenizer,
|
ollama_url: String,
|
||||||
model: QwenModel,
|
model: RefCell<String>,
|
||||||
device: Device,
|
client: reqwest::Client,
|
||||||
eos_token: u32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LlmEngine {
|
impl LlmEngine {
|
||||||
pub fn load() -> Result<Self, String> {
|
pub fn load() -> Result<Self, String> {
|
||||||
let device = Device::cuda_if_available(0).map_err(|e| format!("Device: {}", e))?;
|
let ollama_url = std::env::var("OLLAMA_URL").unwrap_or_else(|_| "http://localhost:11434".to_string());
|
||||||
let device_name = if device.is_cuda() { "CUDA" } else { "CPU" };
|
let model = std::env::var("OLLAMA_MODEL").unwrap_or_else(|_| "qwen2.5-coder:7b".to_string());
|
||||||
tracing::info!("LLM device: {}", device_name);
|
|
||||||
|
|
||||||
let dtype = if device.is_cuda() { DType::F16 } else { DType::F32 };
|
tracing::info!("Ollama backend: {} | malli: {}", ollama_url, model);
|
||||||
|
|
||||||
tracing::info!("Ladataan Qwen2.5-Coder-0.5B-Instruct...");
|
let client = reqwest::Client::builder()
|
||||||
let api = Api::new().map_err(|e| format!("HF API: {}", e))?;
|
.timeout(std::time::Duration::from_secs(600))
|
||||||
let repo = api.repo(Repo::with_revision(
|
.build()
|
||||||
"Qwen/Qwen2.5-Coder-0.5B-Instruct".to_string(),
|
.map_err(|e| format!("HTTP client: {}", e))?;
|
||||||
RepoType::Model,
|
|
||||||
"main".to_string(),
|
|
||||||
));
|
|
||||||
|
|
||||||
let tokenizer_path = repo.get("tokenizer.json").map_err(|e| format!("Tokenizer lataus: {}", e))?;
|
Ok(LlmEngine { ollama_url, model: RefCell::new(model), client })
|
||||||
let model_path = repo.get("model.safetensors").map_err(|e| format!("Malli lataus: {}", e))?;
|
}
|
||||||
|
|
||||||
tracing::info!("Ladataan tokenizer: {:?}", tokenizer_path);
|
pub fn model_name(&self) -> String {
|
||||||
let tokenizer = tokenizers::Tokenizer::from_file(&tokenizer_path)
|
self.model.borrow().clone()
|
||||||
.map_err(|e| format!("Tokenizer: {}", e))?;
|
}
|
||||||
|
|
||||||
let config = QwenConfig {
|
pub fn set_model(&self, new_model: String) {
|
||||||
vocab_size: 151936,
|
*self.model.borrow_mut() = new_model;
|
||||||
hidden_size: 896,
|
}
|
||||||
intermediate_size: 4864,
|
|
||||||
num_hidden_layers: 24,
|
/// Varmistaa että malli on ladattu Ollamaan (ollama pull)
|
||||||
num_attention_heads: 14,
|
pub async fn ensure_model(&self) -> Result<(), String> {
|
||||||
num_key_value_heads: 2,
|
let model = self.model.borrow().clone();
|
||||||
max_position_embeddings: 32768,
|
tracing::info!("Tarkistetaan malli {}...", model);
|
||||||
sliding_window: 32768,
|
let resp = self.client.post(format!("{}/api/pull", self.ollama_url))
|
||||||
max_window_layers: 21,
|
.json(&serde_json::json!({ "name": model, "stream": false }))
|
||||||
tie_word_embeddings: true,
|
.send()
|
||||||
rope_theta: 1000000.0,
|
.await
|
||||||
rms_norm_eps: 1e-6,
|
.map_err(|e| format!("Ollama pull: {}", e))?;
|
||||||
use_sliding_window: false,
|
|
||||||
hidden_act: candle_nn::Activation::Silu,
|
if resp.status().is_success() {
|
||||||
};
|
tracing::info!("Malli {} valmis", model);
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(format!("Ollama pull epäonnistui: {}", resp.status()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn generate(&self, prompt: &str, max_tokens: usize) -> Result<GenerateResult, String> {
|
||||||
|
let system = "You are a coding assistant. Respond with ONLY code. No explanations, no markdown, no comments unless asked.";
|
||||||
|
let model = self.model.borrow().clone();
|
||||||
|
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let vb = unsafe {
|
let resp = self.client.post(format!("{}/api/generate", self.ollama_url))
|
||||||
VarBuilder::from_mmaped_safetensors(&[model_path.clone()], dtype, &device)
|
.json(&serde_json::json!({
|
||||||
.map_err(|e| format!("VarBuilder: {}", e))?
|
"model": model,
|
||||||
};
|
"prompt": prompt,
|
||||||
let model = QwenModel::new(&config, vb).map_err(|e| format!("Malli: {}", e))?;
|
"system": system,
|
||||||
tracing::info!("Malli ladattu ({:.1}s) — {}", start.elapsed().as_secs_f64(), device_name);
|
"stream": false,
|
||||||
|
"options": {
|
||||||
|
"num_predict": max_tokens,
|
||||||
|
"temperature": 0.7,
|
||||||
|
"top_k": 40,
|
||||||
|
"repeat_penalty": 1.15,
|
||||||
|
"stop": ["<|im_end|>", "\n###", "\nExplanation", "\nNote:"]
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Ollama generate: {}", e))?;
|
||||||
|
|
||||||
Ok(LlmEngine {
|
if !resp.status().is_success() {
|
||||||
tokenizer,
|
return Err(format!("Ollama HTTP {}", resp.status()));
|
||||||
model,
|
|
||||||
device,
|
|
||||||
eos_token: 151645,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate(&mut self, prompt: &str, max_tokens: usize) -> Result<GenerateResult, String> {
|
let body: serde_json::Value = resp.json().await
|
||||||
// Prefill: aloitetaan vastaus ```-koodiblokkilla → malli jatkaa suoraan koodilla
|
.map_err(|e| format!("Ollama JSON: {}", e))?;
|
||||||
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)
|
let text = body["response"].as_str().unwrap_or("").to_string();
|
||||||
.map_err(|e| format!("Encode: {}", e))?;
|
let total_duration_ns = body["total_duration"].as_u64().unwrap_or(0);
|
||||||
let input_ids: Vec<u32> = encoding.get_ids().to_vec();
|
let eval_count = body["eval_count"].as_u64().unwrap_or(0) as usize;
|
||||||
let input_len = input_ids.len();
|
let eval_duration_ns = body["eval_duration"].as_u64().unwrap_or(1);
|
||||||
|
|
||||||
// Nollataan KV-cache edellisestä promptista
|
let duration_ms = start.elapsed().as_millis() as f64;
|
||||||
self.model.clear_kv_cache();
|
let tokens_per_sec = if eval_duration_ns > 0 {
|
||||||
|
eval_count as f64 / (eval_duration_ns as f64 / 1_000_000_000.0)
|
||||||
// Sampling-parametrit
|
|
||||||
let temperature = 0.7;
|
|
||||||
let top_k = 40;
|
|
||||||
let repetition_penalty = 1.15;
|
|
||||||
let mut rng_state: u64 = std::time::SystemTime::now()
|
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
|
||||||
.unwrap()
|
|
||||||
.as_nanos() as u64;
|
|
||||||
|
|
||||||
let start = Instant::now();
|
|
||||||
|
|
||||||
// Prefill
|
|
||||||
let input = Tensor::new(input_ids.as_slice(), &self.device)
|
|
||||||
.and_then(|t| t.unsqueeze(0))
|
|
||||||
.map_err(|e| format!("Tensor: {}", e))?;
|
|
||||||
|
|
||||||
let logits = self.model.forward(&input, 0)
|
|
||||||
.map_err(|e| format!("Forward prefill: {}", e))?;
|
|
||||||
|
|
||||||
let logits = logits.squeeze(0).map_err(|e| format!("Squeeze: {}", e))?;
|
|
||||||
let logits = if logits.dims().len() == 2 {
|
|
||||||
let seq_len = logits.dim(0).map_err(|e| format!("Dim: {}", e))?;
|
|
||||||
if seq_len == 0 { return Err("Tyhjä tensori".to_string()); }
|
|
||||||
logits.get(seq_len - 1).map_err(|e| format!("Get: {}", e))?
|
|
||||||
} else {
|
|
||||||
logits
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut generated_text = String::new();
|
|
||||||
let mut tokens_generated: usize = 0;
|
|
||||||
let mut all_tokens: Vec<u32> = Vec::new();
|
|
||||||
|
|
||||||
let mut next_token = sample_top_k(&logits, top_k, temperature, &all_tokens, repetition_penalty, &mut rng_state)?;
|
|
||||||
|
|
||||||
if next_token != self.eos_token {
|
|
||||||
if let Ok(text) = self.tokenizer.decode(&[next_token], true) {
|
|
||||||
generated_text.push_str(&text);
|
|
||||||
}
|
|
||||||
all_tokens.push(next_token);
|
|
||||||
tokens_generated += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Autoregressive
|
|
||||||
let mut pos = input_len;
|
|
||||||
for _ in 1..max_tokens {
|
|
||||||
if next_token == self.eos_token { break; }
|
|
||||||
|
|
||||||
let input = Tensor::new(&[next_token], &self.device)
|
|
||||||
.and_then(|t| t.unsqueeze(0))
|
|
||||||
.map_err(|e| format!("Tensor: {}", e))?;
|
|
||||||
|
|
||||||
let logits = self.model.forward(&input, pos)
|
|
||||||
.map_err(|e| format!("Forward pos {}: {}", pos, e))?;
|
|
||||||
|
|
||||||
let logits = logits.squeeze(0).map_err(|e| format!("Squeeze: {}", e))?;
|
|
||||||
let logits = if logits.dims().len() == 2 {
|
|
||||||
let seq_len = logits.dim(0).map_err(|e| format!("Dim: {}", e))?;
|
|
||||||
if seq_len == 0 { break; }
|
|
||||||
logits.get(seq_len - 1).map_err(|e| format!("Get: {}", e))?
|
|
||||||
} else {
|
|
||||||
logits
|
|
||||||
};
|
|
||||||
next_token = sample_top_k(&logits, top_k, temperature, &all_tokens, repetition_penalty, &mut rng_state)?;
|
|
||||||
pos += 1;
|
|
||||||
|
|
||||||
if next_token == self.eos_token { break; }
|
|
||||||
|
|
||||||
if let Ok(text) = self.tokenizer.decode(&[next_token], true) {
|
|
||||||
generated_text.push_str(&text);
|
|
||||||
|
|
||||||
// Stop-sekvenssit: katkaistaan kun malli alkaa selittää
|
|
||||||
let lower = generated_text.to_lowercase();
|
|
||||||
if lower.contains("\n###") || lower.contains("\nexplanation") || lower.contains("\nnote:") || lower.contains("\noutput:") || lower.contains("\n```\n\n") || lower.contains("\n// example") || lower.contains("\n# example") {
|
|
||||||
for stop in &["\n###", "\nExplanation", "\nNote:", "\nOutput:", "\n```\n\n", "\n// Example", "\n// example", "\n# Example", "\n# example"] {
|
|
||||||
if let Some(pos) = generated_text.find(stop) {
|
|
||||||
generated_text.truncate(pos);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
all_tokens.push(next_token);
|
|
||||||
tokens_generated += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
let gen_time = start.elapsed();
|
|
||||||
let tokens_per_sec = if gen_time.as_secs_f64() > 0.0 {
|
|
||||||
tokens_generated as f64 / gen_time.as_secs_f64()
|
|
||||||
} else { 0.0 };
|
} else { 0.0 };
|
||||||
|
|
||||||
Ok(GenerateResult {
|
Ok(GenerateResult {
|
||||||
text: strip_markdown_wrapper(&generated_text),
|
text: strip_code_fences(&text),
|
||||||
tokens_generated,
|
tokens_generated: eval_count,
|
||||||
duration_ms: gen_time.as_millis() as f64,
|
duration_ms,
|
||||||
tokens_per_sec,
|
tokens_per_sec,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const LANG_TAGS: &[&str] = &[
|
/// Siivoa mahdolliset markdown-koodiblokki-merkit
|
||||||
"python", "py", "rust", "rs", "javascript", "js", "typescript", "ts",
|
fn strip_code_fences(text: &str) -> String {
|
||||||
"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 {
|
|
||||||
let mut result = text.trim().to_string();
|
let mut result = text.trim().to_string();
|
||||||
|
|
||||||
// 1. Kielitunniste — VAIN tunnettu kieli
|
// Poista aloittava ```lang
|
||||||
|
if result.starts_with("```") {
|
||||||
if let Some(nl) = result.find('\n') {
|
if let Some(nl) = result.find('\n') {
|
||||||
let first = result[..nl].trim().to_lowercase();
|
|
||||||
if LANG_TAGS.contains(&first.as_str()) {
|
|
||||||
result = result[nl + 1..].to_string();
|
result = result[nl + 1..].to_string();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Sulkeva ``` — VAIN omalla rivillään lopussa
|
// Poista sulkeva ```
|
||||||
let trimmed = result.trim_end();
|
let trimmed = result.trim_end();
|
||||||
if trimmed.ends_with("```") {
|
if trimmed.ends_with("```") {
|
||||||
let before = &trimmed[..trimmed.len() - 3];
|
let before = &trimmed[..trimmed.len() - 3];
|
||||||
@@ -264,29 +117,7 @@ fn strip_markdown_wrapper(text: &str) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Johdantolauseet
|
result
|
||||||
let lower = result.trim().to_lowercase();
|
|
||||||
for prefix in &["sure!", "here is", "here's", "certainly!", "below is"] {
|
|
||||||
if lower.starts_with(prefix) {
|
|
||||||
if let Some(nl) = result.find('\n') { result = result[nl + 1..].to_string(); }
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Selityskommentit alusta
|
|
||||||
let mut lines: Vec<&str> = result.trim().lines().collect();
|
|
||||||
while !lines.is_empty() {
|
|
||||||
let first = lines[0].trim();
|
|
||||||
let is_preamble = first.starts_with("# ") && !first.starts_with("#!")
|
|
||||||
&& (first.to_lowercase().contains("this is")
|
|
||||||
|| first.to_lowercase().contains("simple")
|
|
||||||
|| first.to_lowercase().contains("program that")
|
|
||||||
|| first.to_lowercase().contains("here is")
|
|
||||||
|| first.to_lowercase().contains("the following")
|
|
||||||
|| first.to_lowercase().contains("below"));
|
|
||||||
if is_preamble { lines.remove(0); } else { break; }
|
|
||||||
}
|
|
||||||
lines.join("\n").trim().to_string()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct GenerateResult {
|
pub struct GenerateResult {
|
||||||
|
|||||||
@@ -285,15 +285,19 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ladataan LLM-malli
|
// Ollama-backend
|
||||||
tracing::info!("Ladataan LLM-mallia...");
|
tracing::info!("Alustetaan Ollama-yhteyttä...");
|
||||||
let mut llm = match inference::LlmEngine::load() {
|
let llm = match inference::LlmEngine::load() {
|
||||||
Ok(engine) => {
|
Ok(engine) => {
|
||||||
tracing::info!("LLM valmis inferenssiin!");
|
// Varmistetaan malli (ollama pull) — odotetaan kunnes valmis
|
||||||
|
match engine.ensure_model().await {
|
||||||
|
Ok(()) => tracing::info!("Ollama valmis inferenssiin!"),
|
||||||
|
Err(e) => tracing::warn!("Mallin lataus: {} — yritetään silti", e),
|
||||||
|
}
|
||||||
Some(engine)
|
Some(engine)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!("LLM-lataus epäonnistui: {} — toimitaan ilman inferenssiä", e);
|
tracing::warn!("Ollama-alustus epäonnistui: {} — toimitaan ilman inferenssiä", e);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -324,11 +328,13 @@ async fn main() {
|
|||||||
|
|
||||||
if !prompt.is_empty() && msg_model.starts_with("qwen-coder") {
|
if !prompt.is_empty() && msg_model.starts_with("qwen-coder") {
|
||||||
|
|
||||||
if let Some(ref mut engine) = llm {
|
if let Some(ref engine) = llm {
|
||||||
busy = true;
|
busy = true;
|
||||||
tracing::info!("Generoidaan (task_id: {}): \"{}\"", task_id, prompt);
|
let max_tokens = task.get("max_tokens").and_then(|v| v.as_u64()).unwrap_or(512) as usize;
|
||||||
|
tracing::info!("Generoidaan (task_id: {}, max_tokens: {}): \"{}\"", task_id, max_tokens, &prompt[..prompt.len().min(100)]);
|
||||||
|
|
||||||
match engine.generate(prompt, 64) {
|
let model_name = engine.model_name();
|
||||||
|
match engine.generate(prompt, max_tokens).await {
|
||||||
Ok(result) => {
|
Ok(result) => {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"Tulos: {} tokenia | {:.0}ms | {:.1} tok/s | \"{}\"",
|
"Tulos: {} tokenia | {:.0}ms | {:.1} tok/s | \"{}\"",
|
||||||
@@ -341,7 +347,7 @@ async fn main() {
|
|||||||
let done = json!({
|
let done = json!({
|
||||||
"type": "llm_done",
|
"type": "llm_done",
|
||||||
"prompt": prompt,
|
"prompt": prompt,
|
||||||
"model": "Qwen2.5-Coder-0.5B (native/GPU)",
|
"model": format!("{} (Ollama)", model_name),
|
||||||
"response": result.text,
|
"response": result.text,
|
||||||
"tokens_generated": result.tokens_generated,
|
"tokens_generated": result.tokens_generated,
|
||||||
"duration_ms": result.duration_ms,
|
"duration_ms": result.duration_ms,
|
||||||
@@ -360,7 +366,21 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Ohitetaan pair_task, stats jne.
|
// Mallin vaihto lennossa
|
||||||
|
if text.contains("change_model") {
|
||||||
|
if let Ok(task) = serde_json::from_str::<serde_json::Value>(&text) {
|
||||||
|
if let Some(new_model) = task.get("model").and_then(|v| v.as_str()) {
|
||||||
|
if let Some(ref engine) = llm {
|
||||||
|
tracing::info!("Vaihdetaan malli: {}", new_model);
|
||||||
|
engine.set_model(new_model.to_string());
|
||||||
|
match engine.ensure_model().await {
|
||||||
|
Ok(()) => tracing::info!("Malli {} valmis!", new_model),
|
||||||
|
Err(e) => tracing::error!("Mallin lataus epäonnistui: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tracing::warn!("Yhteys hubiin katkesi — yritetään uudelleen 5s...");
|
tracing::warn!("Yhteys hubiin katkesi — yritetään uudelleen 5s...");
|
||||||
|
|||||||
@@ -38,17 +38,50 @@ pub fn set_gpu_load(load: u32) {
|
|||||||
console_log!("[Wasm] GPU Kuormitusraja vaihdettu -> {}%", load);
|
console_log!("[Wasm] GPU Kuormitusraja vaihdettu -> {}%", load);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Asynkroninen odotus WebAssemblylle
|
// Worker-yhteensopiva setTimeout — toimii sekä Window- että Worker-kontekstissa
|
||||||
async fn sleep_ms(ms: i32) {
|
#[wasm_bindgen]
|
||||||
|
extern "C" {
|
||||||
|
#[wasm_bindgen(js_name = setTimeout)]
|
||||||
|
fn set_timeout(closure: &js_sys::Function, ms: i32);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asynkroninen odotus WebAssemblylle (Window + Worker)
|
||||||
|
pub async fn sleep_ms(ms: i32) {
|
||||||
let promise = js_sys::Promise::new(&mut |resolve, _| {
|
let promise = js_sys::Promise::new(&mut |resolve, _| {
|
||||||
web_sys::window()
|
set_timeout(&resolve, ms);
|
||||||
.unwrap()
|
|
||||||
.set_timeout_with_callback_and_timeout_and_arguments_0(&resolve, ms)
|
|
||||||
.unwrap();
|
|
||||||
});
|
});
|
||||||
let _ = wasm_bindgen_futures::JsFuture::from(promise).await;
|
let _ = wasm_bindgen_futures::JsFuture::from(promise).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Worker-yhteensopiva Performance — käyttää globalThis.performance
|
||||||
|
pub fn perf_now() -> f64 {
|
||||||
|
js_sys::Reflect::get(&js_sys::global(), &"performance".into())
|
||||||
|
.ok()
|
||||||
|
.and_then(|p| js_sys::Reflect::get(&p, &"now".into()).ok())
|
||||||
|
.and_then(|f| f.dyn_into::<js_sys::Function>().ok())
|
||||||
|
.and_then(|f| {
|
||||||
|
let perf = js_sys::Reflect::get(&js_sys::global(), &"performance".into()).unwrap();
|
||||||
|
f.call0(&perf).ok()
|
||||||
|
})
|
||||||
|
.and_then(|v| v.as_f64())
|
||||||
|
.unwrap_or(0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Worker-yhteensopiva fetch — käyttää globalThis.fetch
|
||||||
|
pub async fn worker_fetch(url: &str) -> Result<web_sys::Response, String> {
|
||||||
|
let promise = js_sys::Reflect::get(&js_sys::global(), &"fetch".into())
|
||||||
|
.map_err(|_| "fetch ei saatavilla".to_string())?
|
||||||
|
.dyn_into::<js_sys::Function>()
|
||||||
|
.map_err(|_| "fetch ei funktio".to_string())?
|
||||||
|
.call1(&JsValue::NULL, &url.into())
|
||||||
|
.map_err(|e| format!("fetch: {:?}", e))?;
|
||||||
|
let resp = wasm_bindgen_futures::JsFuture::from(js_sys::Promise::from(promise))
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("fetch await: {:?}", e))?;
|
||||||
|
resp.dyn_into::<web_sys::Response>()
|
||||||
|
.map_err(|_| "ei Response".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
// Geneerinen tensorilaskenta — toimii millä tahansa Burn-backendillä
|
// Geneerinen tensorilaskenta — toimii millä tahansa Burn-backendillä
|
||||||
fn run_matmul<B: burn::tensor::backend::Backend>(size: usize) -> String {
|
fn run_matmul<B: burn::tensor::backend::Backend>(size: usize) -> String {
|
||||||
let device = Default::default();
|
let device = Default::default();
|
||||||
@@ -123,10 +156,9 @@ async fn run_single_tokenize(text: String, ws: Rc<RefCell<WebSocket>>) {
|
|||||||
let Some(bytes) = cached_tok else { return; };
|
let Some(bytes) = cached_tok else { return; };
|
||||||
let Ok(tokenizer) = tokenizers::Tokenizer::from_bytes(&bytes) else { return; };
|
let Ok(tokenizer) = tokenizers::Tokenizer::from_bytes(&bytes) else { return; };
|
||||||
|
|
||||||
let perf = web_sys::window().unwrap().performance().unwrap();
|
let start = perf_now();
|
||||||
let start = perf.now();
|
|
||||||
let result = tokenize_text(&tokenizer, &text);
|
let result = tokenize_text(&tokenizer, &text);
|
||||||
let duration_ms = perf.now() - start;
|
let duration_ms = perf_now() - start;
|
||||||
|
|
||||||
let token_count = result["token_count"].as_u64().unwrap_or(0);
|
let token_count = result["token_count"].as_u64().unwrap_or(0);
|
||||||
let cpt = result["chars_per_token"].as_f64().unwrap_or(0.0);
|
let cpt = result["chars_per_token"].as_f64().unwrap_or(0.0);
|
||||||
@@ -157,11 +189,10 @@ async fn run_pair_comparison(en_text: String, fi_text: String, ws: Rc<RefCell<We
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let perf = web_sys::window().unwrap().performance().unwrap();
|
let start_time = perf_now();
|
||||||
let start_time = perf.now();
|
|
||||||
let en_result = tokenize_text(&tokenizer, &en_text);
|
let en_result = tokenize_text(&tokenizer, &en_text);
|
||||||
let fi_result = tokenize_text(&tokenizer, &fi_text);
|
let fi_result = tokenize_text(&tokenizer, &fi_text);
|
||||||
let duration_ms = perf.now() - start_time; // millisekunteja desimaalitarkkuudella
|
let duration_ms = perf_now() - start_time;
|
||||||
|
|
||||||
let en_cpt = en_result["chars_per_token"].as_f64().unwrap_or(0.0);
|
let en_cpt = en_result["chars_per_token"].as_f64().unwrap_or(0.0);
|
||||||
let fi_cpt = fi_result["chars_per_token"].as_f64().unwrap_or(0.0);
|
let fi_cpt = fi_result["chars_per_token"].as_f64().unwrap_or(0.0);
|
||||||
|
|||||||
@@ -24,10 +24,7 @@ async fn ensure_cached(key: &str, url: &str, ws: &Rc<RefCell<WebSocket>>) -> Res
|
|||||||
|
|
||||||
console_log!("[Qwen] Ladataan {}...", key);
|
console_log!("[Qwen] Ladataan {}...", key);
|
||||||
|
|
||||||
let window = web_sys::window().unwrap();
|
let resp = crate::worker_fetch(url).await?;
|
||||||
let resp_val = wasm_bindgen_futures::JsFuture::from(window.fetch_with_str(url))
|
|
||||||
.await.map_err(|e| format!("Fetch epäonnistui: {:?}", e))?;
|
|
||||||
let resp: web_sys::Response = resp_val.dyn_into().map_err(|_| "Ei Response".to_string())?;
|
|
||||||
if !resp.ok() { return Err(format!("HTTP {}", resp.status())); }
|
if !resp.ok() { return Err(format!("HTTP {}", resp.status())); }
|
||||||
|
|
||||||
let total_size: usize = resp.headers()
|
let total_size: usize = resp.headers()
|
||||||
@@ -71,7 +68,7 @@ async fn ensure_cached(key: &str, url: &str, ws: &Rc<RefCell<WebSocket>>) -> Res
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run_qwen_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
|
pub async fn run_qwen_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
|
||||||
let perf = web_sys::window().unwrap().performance().unwrap();
|
// performance via crate::perf_now()
|
||||||
|
|
||||||
let tok_bytes = match ensure_cached("qwen05b-tokenizer.json", TOKENIZER_URL, &ws).await {
|
let tok_bytes = match ensure_cached("qwen05b-tokenizer.json", TOKENIZER_URL, &ws).await {
|
||||||
Ok(b) => b,
|
Ok(b) => b,
|
||||||
@@ -88,7 +85,7 @@ pub async fn run_qwen_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
console_log!("[Qwen] Rakennetaan mallia...");
|
console_log!("[Qwen] Rakennetaan mallia...");
|
||||||
let start_load = perf.now();
|
let start_load = crate::perf_now();
|
||||||
let device = Device::Cpu;
|
let device = Device::Cpu;
|
||||||
let dtype = DType::F32;
|
let dtype = DType::F32;
|
||||||
|
|
||||||
@@ -120,7 +117,7 @@ pub async fn run_qwen_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
|
|||||||
Err(e) => { console_log!("[Qwen] Mallin lataus: {}", e); return; }
|
Err(e) => { console_log!("[Qwen] Mallin lataus: {}", e); return; }
|
||||||
};
|
};
|
||||||
|
|
||||||
let load_time = perf.now() - start_load;
|
let load_time = crate::perf_now() - start_load;
|
||||||
console_log!("[Qwen] Malli ladattu ({:.0}ms). Generoidaan...", load_time);
|
console_log!("[Qwen] Malli ladattu ({:.0}ms). Generoidaan...", load_time);
|
||||||
|
|
||||||
let encoding = match tokenizer.encode(prompt.as_str(), true) {
|
let encoding = match tokenizer.encode(prompt.as_str(), true) {
|
||||||
@@ -131,7 +128,7 @@ pub async fn run_qwen_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
|
|||||||
let input_len = input_ids.len();
|
let input_len = input_ids.len();
|
||||||
console_log!("[Qwen] Syöte: {} tokenia", input_len);
|
console_log!("[Qwen] Syöte: {} tokenia", input_len);
|
||||||
|
|
||||||
let start_gen = perf.now();
|
let start_gen = crate::perf_now();
|
||||||
let max_new_tokens = 32;
|
let max_new_tokens = 32;
|
||||||
let mut generated_text = String::new();
|
let mut generated_text = String::new();
|
||||||
let mut tokens_generated: usize = 0;
|
let mut tokens_generated: usize = 0;
|
||||||
@@ -202,7 +199,7 @@ pub async fn run_qwen_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
|
|||||||
crate::sleep_ms(0).await;
|
crate::sleep_ms(0).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
let gen_time = perf.now() - start_gen;
|
let gen_time = crate::perf_now() - start_gen;
|
||||||
let tokens_per_sec = if gen_time > 0.0 { (tokens_generated as f64 / gen_time) * 1000.0 } else { 0.0 };
|
let tokens_per_sec = if gen_time > 0.0 { (tokens_generated as f64 / gen_time) * 1000.0 } else { 0.0 };
|
||||||
console_log!("[Qwen] {} tokenia | {:.0}ms | {:.1} tok/s", tokens_generated, gen_time, tokens_per_sec);
|
console_log!("[Qwen] {} tokenia | {:.0}ms | {:.1} tok/s", tokens_generated, gen_time, tokens_per_sec);
|
||||||
|
|
||||||
|
|||||||
@@ -140,10 +140,7 @@ async fn ensure_cached(key: &str, url: &str, ws: &Rc<RefCell<WebSocket>>) -> Res
|
|||||||
|
|
||||||
console_log!("[Coder] Ladataan {}...", key);
|
console_log!("[Coder] Ladataan {}...", key);
|
||||||
|
|
||||||
let window = web_sys::window().unwrap();
|
let resp = crate::worker_fetch(url).await?;
|
||||||
let resp_val = wasm_bindgen_futures::JsFuture::from(window.fetch_with_str(url))
|
|
||||||
.await.map_err(|e| format!("Fetch: {:?}", e))?;
|
|
||||||
let resp: web_sys::Response = resp_val.dyn_into().map_err(|_| "Ei Response".to_string())?;
|
|
||||||
if !resp.ok() { return Err(format!("HTTP {}", resp.status())); }
|
if !resp.ok() { return Err(format!("HTTP {}", resp.status())); }
|
||||||
|
|
||||||
let total_size: usize = resp.headers()
|
let total_size: usize = resp.headers()
|
||||||
@@ -251,17 +248,16 @@ async fn get_or_build_model(use_3b: bool, ws: &Rc<RefCell<WebSocket>>) -> Result
|
|||||||
|
|
||||||
/// use_3b: false = 0.5B (nopea), true = 3B (laadukas)
|
/// use_3b: false = 0.5B (nopea), true = 3B (laadukas)
|
||||||
pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use_3b: bool, task_id: Option<String>) {
|
pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use_3b: bool, task_id: Option<String>) {
|
||||||
let perf = web_sys::window().unwrap().performance().unwrap();
|
|
||||||
let size_label = if use_3b { "3B" } else { "0.5B" };
|
let size_label = if use_3b { "3B" } else { "0.5B" };
|
||||||
|
|
||||||
let start_load = perf.now();
|
let start_load = crate::perf_now();
|
||||||
|
|
||||||
if let Err(e) = get_or_build_model(use_3b, &ws).await {
|
if let Err(e) = get_or_build_model(use_3b, &ws).await {
|
||||||
console_log!("[Coder] Mallin lataus: {}", e);
|
console_log!("[Coder] Mallin lataus: {}", e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let load_time = perf.now() - start_load;
|
let load_time = crate::perf_now() - start_load;
|
||||||
if load_time > 100.0 {
|
if load_time > 100.0 {
|
||||||
console_log!("[Coder] Malli ladattu ({:.0}ms). Generoidaan...", load_time);
|
console_log!("[Coder] Malli ladattu ({:.0}ms). Generoidaan...", load_time);
|
||||||
}
|
}
|
||||||
@@ -297,7 +293,7 @@ pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use
|
|||||||
console_log!("[Coder] Syöte: {} tokenia", input_len);
|
console_log!("[Coder] Syöte: {} tokenia", input_len);
|
||||||
|
|
||||||
let device = Device::Cpu;
|
let device = Device::Cpu;
|
||||||
let start_gen = perf.now();
|
let start_gen = crate::perf_now();
|
||||||
let eos_token = 151645u32;
|
let eos_token = 151645u32;
|
||||||
let temperature: f32 = 0.7;
|
let temperature: f32 = 0.7;
|
||||||
let top_k: usize = 40;
|
let top_k: usize = 40;
|
||||||
@@ -373,7 +369,7 @@ pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use
|
|||||||
tokens_generated += 1;
|
tokens_generated += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
let gen_time = perf.now() - start_gen;
|
let gen_time = crate::perf_now() - start_gen;
|
||||||
|
|
||||||
// Siivotaan vastaus: poista markdown-koodiblokit ja johdantotekstit
|
// Siivotaan vastaus: poista markdown-koodiblokit ja johdantotekstit
|
||||||
let cleaned = strip_markdown_wrapper(&generated_text);
|
let cleaned = strip_markdown_wrapper(&generated_text);
|
||||||
|
|||||||
@@ -28,10 +28,7 @@ async fn ensure_cached(key: &str, url: &str, ws: &Rc<RefCell<WebSocket>>) -> Res
|
|||||||
send_progress(ws, key, 0, 0, 0);
|
send_progress(ws, key, 0, 0, 0);
|
||||||
|
|
||||||
// Fetch API:lla saadaan Content-Length ja streaming-luku
|
// Fetch API:lla saadaan Content-Length ja streaming-luku
|
||||||
let window = web_sys::window().unwrap();
|
let resp = crate::worker_fetch(url).await?;
|
||||||
let resp_val = wasm_bindgen_futures::JsFuture::from(window.fetch_with_str(url))
|
|
||||||
.await.map_err(|e| format!("Fetch epäonnistui: {:?}", e))?;
|
|
||||||
let resp: web_sys::Response = resp_val.dyn_into().map_err(|_| "Ei Response-objekti".to_string())?;
|
|
||||||
|
|
||||||
if !resp.ok() {
|
if !resp.ok() {
|
||||||
return Err(format!("HTTP {}", resp.status()));
|
return Err(format!("HTTP {}", resp.status()));
|
||||||
@@ -99,7 +96,7 @@ fn send_progress(ws: &Rc<RefCell<WebSocket>>, file: &str, pct: u32, loaded: usiz
|
|||||||
|
|
||||||
/// Lataa malli ja tokenizer, suorita inferenssi ja streamaa tokenit hubille
|
/// Lataa malli ja tokenizer, suorita inferenssi ja streamaa tokenit hubille
|
||||||
pub async fn run_smollm_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
|
pub async fn run_smollm_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
|
||||||
let perf = web_sys::window().unwrap().performance().unwrap();
|
// performance via crate::perf_now()
|
||||||
|
|
||||||
// 1. Lataa tokenizer
|
// 1. Lataa tokenizer
|
||||||
let tok_bytes = match ensure_cached("smollm-tokenizer.json", TOKENIZER_URL, &ws).await {
|
let tok_bytes = match ensure_cached("smollm-tokenizer.json", TOKENIZER_URL, &ws).await {
|
||||||
@@ -122,7 +119,7 @@ pub async fn run_smollm_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
|
|||||||
// Burn 0.21-pre.2 cubecl-runtime ei käänny Wasmille (println! puuttuu)
|
// Burn 0.21-pre.2 cubecl-runtime ei käänny Wasmille (println! puuttuu)
|
||||||
// → NdArray kunnes Burn 0.21 stable + Wasm-tuki
|
// → NdArray kunnes Burn 0.21 stable + Wasm-tuki
|
||||||
console_log!("[SmolLM] Burn NdArray (CPU) inferenssi...");
|
console_log!("[SmolLM] Burn NdArray (CPU) inferenssi...");
|
||||||
run_burn_inference::<burn::backend::NdArray>(prompt, model_bytes, tokenizer, ws, perf.clone()).await;
|
run_burn_inference::<burn::backend::NdArray>(prompt, model_bytes, tokenizer, ws).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_burn_inference<B: burn::tensor::backend::Backend>(
|
async fn run_burn_inference<B: burn::tensor::backend::Backend>(
|
||||||
@@ -130,9 +127,8 @@ async fn run_burn_inference<B: burn::tensor::backend::Backend>(
|
|||||||
model_bytes: Vec<u8>,
|
model_bytes: Vec<u8>,
|
||||||
tokenizer: tokenizers::Tokenizer,
|
tokenizer: tokenizers::Tokenizer,
|
||||||
ws: Rc<RefCell<WebSocket>>,
|
ws: Rc<RefCell<WebSocket>>,
|
||||||
perf: web_sys::Performance, // Korjattu Wasm-performanssi välitettäväksi
|
|
||||||
) {
|
) {
|
||||||
let start_load = perf.now();
|
let start_load = crate::perf_now();
|
||||||
|
|
||||||
let device = Default::default();
|
let device = Default::default();
|
||||||
let config = crate::burn_smollm::config::SmolLMConfig::default();
|
let config = crate::burn_smollm::config::SmolLMConfig::default();
|
||||||
@@ -143,7 +139,7 @@ async fn run_burn_inference<B: burn::tensor::backend::Backend>(
|
|||||||
Err(e) => { console_log!("[SmolLM] Lataus epäonnistui: {}", e); return; }
|
Err(e) => { console_log!("[SmolLM] Lataus epäonnistui: {}", e); return; }
|
||||||
};
|
};
|
||||||
|
|
||||||
let load_time = perf.now() - start_load;
|
let load_time = crate::perf_now() - start_load;
|
||||||
console_log!("[SmolLM] Burn-malli ladattu ({:.0}ms). Generoidaan...", load_time);
|
console_log!("[SmolLM] Burn-malli ladattu ({:.0}ms). Generoidaan...", load_time);
|
||||||
|
|
||||||
let formatted_prompt = format!("<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n", prompt);
|
let formatted_prompt = format!("<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n", prompt);
|
||||||
@@ -156,7 +152,7 @@ async fn run_burn_inference<B: burn::tensor::backend::Backend>(
|
|||||||
let input_len = input_ids.len();
|
let input_len = input_ids.len();
|
||||||
console_log!("[SmolLM] Syöte: {} tokenia", input_len);
|
console_log!("[SmolLM] Syöte: {} tokenia", input_len);
|
||||||
|
|
||||||
let start_gen = perf.now();
|
let start_gen = crate::perf_now();
|
||||||
let max_new_tokens = 32;
|
let max_new_tokens = 32;
|
||||||
let mut generated_text = String::new();
|
let mut generated_text = String::new();
|
||||||
let mut tokens_generated: usize = 0;
|
let mut tokens_generated: usize = 0;
|
||||||
@@ -219,7 +215,7 @@ async fn run_burn_inference<B: burn::tensor::backend::Backend>(
|
|||||||
tokens_generated += 1;
|
tokens_generated += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
let gen_time = perf.now() - start_gen;
|
let gen_time = crate::perf_now() - start_gen;
|
||||||
let tokens_per_sec = if gen_time > 0.0 { (tokens_generated as f64 / gen_time) * 1000.0 } else { 0.0 };
|
let tokens_per_sec = if gen_time > 0.0 { (tokens_generated as f64 / gen_time) * 1000.0 } else { 0.0 };
|
||||||
|
|
||||||
let done = serde_json::json!({
|
let done = serde_json::json!({
|
||||||
|
|||||||
@@ -1076,6 +1076,9 @@
|
|||||||
<div id="shared-prompt-section" style="display:none;margin-top:8px;font-size:12px;color:#8b949e">
|
<div id="shared-prompt-section" style="display:none;margin-top:8px;font-size:12px;color:#8b949e">
|
||||||
Yhteinen konteksti liitetään jokaisen valitun agentin oman promptin alkuun.
|
Yhteinen konteksti liitetään jokaisen valitun agentin oman promptin alkuun.
|
||||||
</div>
|
</div>
|
||||||
|
<div id="agent-last-prompt" style="display:none;margin-top:8px">
|
||||||
|
<button id="agent-open-modal-btn" style="background:#161b22;border:1px solid var(--accent-color);color:var(--accent-color);font-size:12px;padding:4px 12px;border-radius:4px;cursor:pointer;width:100%">📋 Näytä viimeisin prompti</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1157,7 +1160,7 @@
|
|||||||
coder: { name: 'Koodari — System Prompt', model: 'qwen-coder', default: 'Olet kokenut ohjelmistokehittäjä. Kirjoita selkeää, testattavaa koodia ja vastaa aina koodilla.' },
|
coder: { name: 'Koodari — System Prompt', model: 'qwen-coder', default: 'Olet kokenut ohjelmistokehittäjä. Kirjoita selkeää, testattavaa koodia ja vastaa aina koodilla.' },
|
||||||
data: { name: 'Data-Agentti — System Prompt', model: 'qwen-coder', default: 'Olet tietokanta-asiantuntija. Vastaat skeemojen suunnittelusta, SQL-kyselyiden optimoinnista ja datamalleista.' },
|
data: { name: 'Data-Agentti — System Prompt', model: 'qwen-coder', default: 'Olet tietokanta-asiantuntija. Vastaat skeemojen suunnittelusta, SQL-kyselyiden optimoinnista ja datamalleista.' },
|
||||||
qa: { name: 'QA — System Prompt', model: 'qwen-coder', default: 'Olet laadunvarmistaja (QA). Kirjoitat testejä, etsit virheitä ja varmistat, että kaikki reunatapaukset on huomioitu.' },
|
qa: { name: 'QA — System Prompt', model: 'qwen-coder', default: 'Olet laadunvarmistaja (QA). Kirjoitat testejä, etsit virheitä ja varmistat, että kaikki reunatapaukset on huomioitu.' },
|
||||||
tester: { name: 'DevOps — System Prompt', model: 'qwen-coder', default: 'Olet DevOps-insinööri. Vastaat koodin julkaisuputkista, serveri-infrastruktuurista ja ympäristön suorituskyvystä.' },
|
tester: { name: 'DevOps — System Prompt', model: 'qwen-coder', default: 'Olet DevOps-insinööri. Kirjoitat Dockerfile- ja docker-compose.yml-tiedostot, README:t ja käynnistysohjeet. Käytä aina multi-stage Docker buildia ja docker compose -orkestrointia.' },
|
||||||
};
|
};
|
||||||
const selectedAgents = new Set();
|
const selectedAgents = new Set();
|
||||||
let sharedPrompt = localStorage.getItem('kpn-shared-prompt') || '';
|
let sharedPrompt = localStorage.getItem('kpn-shared-prompt') || '';
|
||||||
@@ -1246,6 +1249,17 @@
|
|||||||
textEl.value = sharedPrompt;
|
textEl.value = sharedPrompt;
|
||||||
sharedEl.style.display = 'block';
|
sharedEl.style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Näytetään viimeisin pipeline-prompti valitulle agentille
|
||||||
|
const lastPromptDiv = document.getElementById('agent-last-prompt');
|
||||||
|
const lastPromptText = document.getElementById('agent-last-prompt-text');
|
||||||
|
if (selectedAgents.size === 1) {
|
||||||
|
const agent = [...selectedAgents][0];
|
||||||
|
const lastStep = [...pipelineSteps].reverse().find(s => s.agent === agent && s.status === 'done' && s.input);
|
||||||
|
lastPromptDiv.style.display = lastStep ? 'block' : 'none';
|
||||||
|
} else {
|
||||||
|
lastPromptDiv.style.display = 'none';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.selectAgent = function(agent) {
|
window.selectAgent = function(agent) {
|
||||||
@@ -1302,6 +1316,96 @@
|
|||||||
saved._t = setTimeout(() => saved.style.opacity = '0', 1500);
|
saved._t = setTimeout(() => saved.style.opacity = '0', 1500);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Prompt-editori modal
|
||||||
|
let modalAgent = null;
|
||||||
|
let modalPromptParts = [];
|
||||||
|
|
||||||
|
function parsePromptToFields(prompt) {
|
||||||
|
// Pilkotaan prompti avain-arvo-pareiksi
|
||||||
|
const fields = [];
|
||||||
|
const lines = prompt.split('\n');
|
||||||
|
let currentKey = null;
|
||||||
|
let currentVal = [];
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
// Tunnistetaan avain: KEYWORD: tai KEYWORD — tai rivin alku isolla
|
||||||
|
const keyMatch = line.match(/^(Project|CONSTRAINTS|EXAMPLE|RULES|IMPORTANT|Check|Files in project|Main app|Already written files|PROJECT CODE|Current code|Review feedback|Feedback)[\s:—]*(.*)/i);
|
||||||
|
if (keyMatch) {
|
||||||
|
if (currentKey) fields.push({ key: currentKey, value: currentVal.join('\n').trim(), editable: isEditable(currentKey) });
|
||||||
|
currentKey = keyMatch[1];
|
||||||
|
currentVal = keyMatch[2] ? [keyMatch[2]] : [];
|
||||||
|
} else {
|
||||||
|
currentVal.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentKey) fields.push({ key: currentKey, value: currentVal.join('\n').trim(), editable: isEditable(currentKey) });
|
||||||
|
|
||||||
|
// Jos ei löytynyt rakenteellisia avaimia, näytetään koko prompti yhtenä
|
||||||
|
if (fields.length === 0) fields.push({ key: 'Prompti', value: prompt, editable: true });
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEditable(key) {
|
||||||
|
const editableKeys = ['Project', 'CONSTRAINTS', 'IMPORTANT', 'Feedback', 'Review feedback'];
|
||||||
|
return editableKeys.some(k => key.toLowerCase().includes(k.toLowerCase()));
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPromptModal(agent, label, prompt) {
|
||||||
|
modalAgent = agent;
|
||||||
|
modalPromptParts = parsePromptToFields(prompt);
|
||||||
|
const modal = document.getElementById('prompt-modal');
|
||||||
|
const title = document.getElementById('prompt-modal-title');
|
||||||
|
const fields = document.getElementById('prompt-modal-fields');
|
||||||
|
|
||||||
|
const agentNames = { manager: 'Manageri', coder: 'Koodari', tester: 'DevOps', qa: 'QA', data: 'Data' };
|
||||||
|
title.textContent = `${agentNames[agent] || agent} — ${label}`;
|
||||||
|
|
||||||
|
fields.innerHTML = modalPromptParts.map((f, i) => `
|
||||||
|
<div style="border:1px solid #21262d;border-radius:6px;overflow:hidden">
|
||||||
|
<div style="background:#161b22;padding:6px 10px;font-size:12px;font-weight:600;color:${f.editable ? '#58a6ff' : '#8b949e'};display:flex;align-items:center;gap:6px">
|
||||||
|
${f.editable ? '✏️' : '🔒'} ${f.key}
|
||||||
|
</div>
|
||||||
|
<textarea data-field-idx="${i}" ${f.editable ? '' : 'readonly'} style="width:100%;background:${f.editable ? '#010409' : '#0d1117'};border:none;color:${f.editable ? '#c9d1d9' : '#6e7681'};font-size:12px;font-family:'Courier New',monospace;padding:8px;resize:vertical;min-height:${f.value.split('\n').length > 3 ? '100' : '40'}px;outline:none;box-sizing:border-box">${f.value.replace(/</g,'<')}</textarea>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
modal.style.display = 'block';
|
||||||
|
// Sulje klikatessa taustaa
|
||||||
|
modal.onclick = (e) => { if (e.target === modal) closePromptModal(); };
|
||||||
|
}
|
||||||
|
window.openPromptModal = openPromptModal;
|
||||||
|
|
||||||
|
function closePromptModal() {
|
||||||
|
document.getElementById('prompt-modal').style.display = 'none';
|
||||||
|
}
|
||||||
|
window.closePromptModal = closePromptModal;
|
||||||
|
|
||||||
|
function rerunFromModal() {
|
||||||
|
// Kootaan prompti takaisin kentistä
|
||||||
|
const fields = document.getElementById('prompt-modal-fields');
|
||||||
|
const textareas = fields.querySelectorAll('textarea');
|
||||||
|
const parts = [];
|
||||||
|
textareas.forEach((ta, i) => {
|
||||||
|
const key = modalPromptParts[i]?.key || '';
|
||||||
|
const val = ta.value.trim();
|
||||||
|
if (val) parts.push(`${key}: ${val}`);
|
||||||
|
});
|
||||||
|
const prompt = parts.join('\n\n');
|
||||||
|
const model = agentPrompts[modalAgent]?.model || 'qwen-coder';
|
||||||
|
closePromptModal();
|
||||||
|
termLog(`<span class="terminal-prompt">$</span> <span style="color:#a371f7">↻ Aja uudelleen:</span> ${esc(modalAgent)}`);
|
||||||
|
kpnRun(model, prompt);
|
||||||
|
}
|
||||||
|
window.rerunFromModal = rerunFromModal;
|
||||||
|
|
||||||
|
// "Näytä prompti" -nappi avaa modalin
|
||||||
|
document.getElementById('agent-open-modal-btn')?.addEventListener('click', () => {
|
||||||
|
if (selectedAgents.size !== 1) return;
|
||||||
|
const agent = [...selectedAgents][0];
|
||||||
|
const lastStep = [...pipelineSteps].reverse().find(s => s.agent === agent && s.status === 'done' && s.input);
|
||||||
|
if (lastStep) openPromptModal(agent, lastStep.label, lastStep.input);
|
||||||
|
});
|
||||||
|
|
||||||
function checkAgentConfusion() {
|
function checkAgentConfusion() {
|
||||||
Object.keys(agentPrompts).forEach(agent => {
|
Object.keys(agentPrompts).forEach(agent => {
|
||||||
const prompt = agentPrompts[agent].prompt || "";
|
const prompt = agentPrompts[agent].prompt || "";
|
||||||
@@ -1672,15 +1776,28 @@
|
|||||||
if (e.key === 'Enter') sendUserText();
|
if (e.key === 'Enter') sendUserText();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Kytkemme sivuston UI-puolen (JS) omaan passiiviseen WebSocket-kuuntelijaan.
|
// WebSocket-yhteys hubiin — automaattinen reconnect
|
||||||
const uiSocket = new WebSocket(`${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws`);
|
const wsUrl = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws`;
|
||||||
|
let uiSocket = null;
|
||||||
|
let wsReconnectTimer = null;
|
||||||
|
|
||||||
|
function connectHub() {
|
||||||
|
if (uiSocket && (uiSocket.readyState === 0 || uiSocket.readyState === 1)) return;
|
||||||
|
uiSocket = new WebSocket(wsUrl);
|
||||||
window._uiSocket = uiSocket;
|
window._uiSocket = uiSocket;
|
||||||
|
// Kytketään onmessage uudelleen (handler määritellään myöhemmin, asetetaan kun valmis)
|
||||||
|
setTimeout(() => {
|
||||||
|
if (window._wsMessageHandler) uiSocket.onmessage = window._wsMessageHandler;
|
||||||
|
}, 0);
|
||||||
uiSocket.onopen = async () => {
|
uiSocket.onopen = async () => {
|
||||||
// Päivitetään agents-näkymän hub-status
|
// Päivitetään agents-näkymän hub-status
|
||||||
const hubDot = document.getElementById('agent-hub-dot');
|
const hubDot = document.getElementById('agent-hub-dot');
|
||||||
const hubLabel = document.getElementById('agent-hub-label');
|
const hubLabel = document.getElementById('agent-hub-label');
|
||||||
const hubStatus = document.getElementById('agent-hub-status');
|
const hubStatus = document.getElementById('agent-hub-status');
|
||||||
if (hubDot) hubDot.style.background = '#3fb950';
|
if (hubDot) hubDot.style.background = '#3fb950';
|
||||||
|
// Poistetaan reconnect-rivi
|
||||||
|
const reconnLine = document.getElementById('agent-terminal')?.querySelector('.term-reconnect');
|
||||||
|
if (reconnLine) reconnLine.remove();
|
||||||
if (hubLabel) { hubLabel.textContent = 'Yhdistetty'; hubLabel.style.color = '#3fb950'; }
|
if (hubLabel) { hubLabel.textContent = 'Yhdistetty'; hubLabel.style.color = '#3fb950'; }
|
||||||
if (hubStatus) hubStatus.title = 'Yhdistetty Kipinä Hubiin — tehtävien jakelu ja solmujen koordinointi aktiivinen';
|
if (hubStatus) hubStatus.title = 'Yhdistetty Kipinä Hubiin — tehtävien jakelu ja solmujen koordinointi aktiivinen';
|
||||||
|
|
||||||
@@ -1746,7 +1863,31 @@
|
|||||||
coderEl.textContent = 'Disconnected';
|
coderEl.textContent = 'Disconnected';
|
||||||
coderEl.style.color = '#f85149';
|
coderEl.style.color = '#f85149';
|
||||||
}
|
}
|
||||||
|
// Automaattinen reconnect 3s kuluttua
|
||||||
|
if (!wsReconnectTimer) {
|
||||||
|
// Päivitetään samaa riviä eikä floodata uusia
|
||||||
|
let reconnLine = termPanel?.querySelector('.term-reconnect');
|
||||||
|
let reconnCount = 0;
|
||||||
|
if (!reconnLine) {
|
||||||
|
reconnLine = document.createElement('div');
|
||||||
|
reconnLine.className = 'terminal-line term-reconnect';
|
||||||
|
termPanel?.appendChild(reconnLine);
|
||||||
|
} else {
|
||||||
|
reconnCount = parseInt(reconnLine.dataset.count || '0');
|
||||||
|
}
|
||||||
|
wsReconnectTimer = setTimeout(() => {
|
||||||
|
wsReconnectTimer = null;
|
||||||
|
reconnCount++;
|
||||||
|
reconnLine.dataset.count = reconnCount;
|
||||||
|
reconnLine.innerHTML = ` <span style="color:#d29922">↻ Yhdistetään uudelleen...${reconnCount > 1 ? ' (' + reconnCount + ')' : ''}</span>`;
|
||||||
|
termPanel.scrollTop = termPanel.scrollHeight;
|
||||||
|
connectHub();
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
} // connectHub()
|
||||||
|
connectHub();
|
||||||
|
|
||||||
// Terminaalin komentorivi
|
// Terminaalin komentorivi
|
||||||
const termInput = document.getElementById('term-input');
|
const termInput = document.getElementById('term-input');
|
||||||
const termPanel = document.getElementById('agent-terminal');
|
const termPanel = document.getElementById('agent-terminal');
|
||||||
@@ -1767,7 +1908,7 @@
|
|||||||
const activeStreams = {};
|
const activeStreams = {};
|
||||||
|
|
||||||
// Lähettää promptin mallille ja palauttaa vastauksen (tai null virhetilanteessa)
|
// Lähettää promptin mallille ja palauttaa vastauksen (tai null virhetilanteessa)
|
||||||
async function kpnRun(model, prompt, silent) {
|
async function kpnRun(model, prompt, silent, maxTokens) {
|
||||||
const taskId = crypto.randomUUID();
|
const taskId = crypto.randomUUID();
|
||||||
// Yksittäinen status-rivi jota päivitetään läpi pyynnön elinkaaren
|
// Yksittäinen status-rivi jota päivitetään läpi pyynnön elinkaaren
|
||||||
const statusDiv = document.createElement('div');
|
const statusDiv = document.createElement('div');
|
||||||
@@ -1801,7 +1942,7 @@
|
|||||||
const res = await fetch('/api/v1/chat/completions', {
|
const res = await fetch('/api/v1/chat/completions', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ model, prompt: fullPrompt, task_id: taskId }),
|
body: JSON.stringify({ model, prompt: fullPrompt, task_id: taskId, ...(maxTokens ? { max_tokens: maxTokens } : {}) }),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
@@ -1905,11 +2046,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Projektikortti: tiedostovälilehdet + kopioi + lataa ZIP
|
// Projektikortti: tiedostovälilehdet + kopioi + lataa ZIP
|
||||||
|
// Globaali storage projektikorttien tiedostoille (välttää JSON data-attribuuttien ongelmat)
|
||||||
|
const projectFiles = {};
|
||||||
|
|
||||||
function renderProjectCard(files, projectName) {
|
function renderProjectCard(files, projectName) {
|
||||||
const fileEntries = Object.entries(files);
|
const fileEntries = Object.entries(files);
|
||||||
if (fileEntries.length === 0) return;
|
if (fileEntries.length === 0) return;
|
||||||
|
|
||||||
const cardId = 'proj-' + Date.now();
|
const cardId = 'proj-' + Date.now();
|
||||||
|
projectFiles[cardId] = files;
|
||||||
const tabsHtml = fileEntries.map(([name], i) =>
|
const tabsHtml = fileEntries.map(([name], i) =>
|
||||||
`<span class="proj-tab" data-card="${cardId}" data-idx="${i}" style="padding:4px 10px;cursor:pointer;border-radius:4px 4px 0 0;font-size:12px;${i === 0 ? 'background:#161b22;color:#58a6ff;border:1px solid #30363d;border-bottom:none' : 'color:#8b949e'}" onclick="switchProjectTab('${cardId}',${i})">${esc(name)}</span>`
|
`<span class="proj-tab" data-card="${cardId}" data-idx="${i}" style="padding:4px 10px;cursor:pointer;border-radius:4px 4px 0 0;font-size:12px;${i === 0 ? 'background:#161b22;color:#58a6ff;border:1px solid #30363d;border-bottom:none' : 'color:#8b949e'}" onclick="switchProjectTab('${cardId}',${i})">${esc(name)}</span>`
|
||||||
).join('');
|
).join('');
|
||||||
@@ -1926,7 +2071,7 @@
|
|||||||
const allText = fileEntries.map(([name, code]) => `# --- ${name} ---\n${code}`).join('\n\n');
|
const allText = fileEntries.map(([name, code]) => `# --- ${name} ---\n${code}`).join('\n\n');
|
||||||
|
|
||||||
const cardHtml = `
|
const cardHtml = `
|
||||||
<div id="${cardId}" style="margin:8px 0;border:1px solid #30363d;border-radius:6px;background:#161b22;overflow:hidden" data-files='${esc(JSON.stringify(files))}'>
|
<div id="${cardId}" style="margin:8px 0;border:1px solid #30363d;border-radius:6px;background:#161b22;overflow:hidden">
|
||||||
<div style="display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background:#0d1117;border-bottom:1px solid #30363d">
|
<div style="display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background:#0d1117;border-bottom:1px solid #30363d">
|
||||||
<span style="color:#a371f7;font-weight:600;font-size:13px">${esc(projectName || 'Projekti')} <span style="color:#8b949e;font-weight:normal">(${fileEntries.length} tiedostoa)</span></span>
|
<span style="color:#a371f7;font-weight:600;font-size:13px">${esc(projectName || 'Projekti')} <span style="color:#8b949e;font-weight:normal">(${fileEntries.length} tiedostoa)</span></span>
|
||||||
<span style="display:flex;gap:6px">
|
<span style="display:flex;gap:6px">
|
||||||
@@ -1960,7 +2105,7 @@
|
|||||||
window.copyFileContent = function(cardId, idx) {
|
window.copyFileContent = function(cardId, idx) {
|
||||||
const card = document.getElementById(cardId);
|
const card = document.getElementById(cardId);
|
||||||
if (!card) return;
|
if (!card) return;
|
||||||
const files = JSON.parse(card.dataset.files);
|
const files = projectFiles[cardId];
|
||||||
const entries = Object.entries(files);
|
const entries = Object.entries(files);
|
||||||
if (entries[idx]) {
|
if (entries[idx]) {
|
||||||
navigator.clipboard.writeText(entries[idx][1]);
|
navigator.clipboard.writeText(entries[idx][1]);
|
||||||
@@ -1973,7 +2118,7 @@
|
|||||||
window.copyAllFiles = function(cardId) {
|
window.copyAllFiles = function(cardId) {
|
||||||
const card = document.getElementById(cardId);
|
const card = document.getElementById(cardId);
|
||||||
if (!card) return;
|
if (!card) return;
|
||||||
const files = JSON.parse(card.dataset.files);
|
const files = projectFiles[cardId];
|
||||||
const text = Object.entries(files).map(([name, code]) => `# --- ${name} ---\n${code}`).join('\n\n');
|
const text = Object.entries(files).map(([name, code]) => `# --- ${name} ---\n${code}`).join('\n\n');
|
||||||
navigator.clipboard.writeText(text);
|
navigator.clipboard.writeText(text);
|
||||||
const btn = card.querySelector('[onclick*="copyAllFiles"]');
|
const btn = card.querySelector('[onclick*="copyAllFiles"]');
|
||||||
@@ -1983,9 +2128,20 @@
|
|||||||
window.downloadZip = async function(cardId) {
|
window.downloadZip = async function(cardId) {
|
||||||
const card = document.getElementById(cardId);
|
const card = document.getElementById(cardId);
|
||||||
if (!card) return;
|
if (!card) return;
|
||||||
const files = JSON.parse(card.dataset.files);
|
const files = projectFiles[cardId];
|
||||||
|
|
||||||
|
// CRC-32 laskenta ZIP-tiedostoille
|
||||||
|
function crc32(bytes) {
|
||||||
|
let crc = 0xFFFFFFFF;
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
crc ^= bytes[i];
|
||||||
|
for (let j = 0; j < 8; j++) {
|
||||||
|
crc = (crc >>> 1) ^ (crc & 1 ? 0xEDB88320 : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (crc ^ 0xFFFFFFFF) >>> 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Luodaan ZIP ilman ulkoisia kirjastoja (yksinkertainen uncompressed ZIP)
|
|
||||||
const entries = Object.entries(files);
|
const entries = Object.entries(files);
|
||||||
const parts = [];
|
const parts = [];
|
||||||
const centralDir = [];
|
const centralDir = [];
|
||||||
@@ -1994,6 +2150,7 @@
|
|||||||
for (const [name, content] of entries) {
|
for (const [name, content] of entries) {
|
||||||
const nameBytes = new TextEncoder().encode(name);
|
const nameBytes = new TextEncoder().encode(name);
|
||||||
const contentBytes = new TextEncoder().encode(content);
|
const contentBytes = new TextEncoder().encode(content);
|
||||||
|
const crc = crc32(contentBytes);
|
||||||
|
|
||||||
// Local file header
|
// Local file header
|
||||||
const header = new Uint8Array(30 + nameBytes.length);
|
const header = new Uint8Array(30 + nameBytes.length);
|
||||||
@@ -2001,8 +2158,9 @@
|
|||||||
view.setUint32(0, 0x04034b50, true); // Signature
|
view.setUint32(0, 0x04034b50, true); // Signature
|
||||||
view.setUint16(4, 20, true); // Version needed
|
view.setUint16(4, 20, true); // Version needed
|
||||||
view.setUint16(8, 0, true); // Method: store
|
view.setUint16(8, 0, true); // Method: store
|
||||||
view.setUint32(18, contentBytes.length, true); // Compressed size
|
view.setUint32(14, crc, true); // CRC-32
|
||||||
view.setUint32(22, contentBytes.length, true); // Uncompressed size
|
view.setUint32(18, contentBytes.length, true);
|
||||||
|
view.setUint32(22, contentBytes.length, true);
|
||||||
view.setUint16(26, nameBytes.length, true);
|
view.setUint16(26, nameBytes.length, true);
|
||||||
header.set(nameBytes, 30);
|
header.set(nameBytes, 30);
|
||||||
|
|
||||||
@@ -2012,6 +2170,7 @@
|
|||||||
cdView.setUint32(0, 0x02014b50, true);
|
cdView.setUint32(0, 0x02014b50, true);
|
||||||
cdView.setUint16(4, 20, true);
|
cdView.setUint16(4, 20, true);
|
||||||
cdView.setUint16(6, 20, true);
|
cdView.setUint16(6, 20, true);
|
||||||
|
cdView.setUint32(16, crc, true); // CRC-32
|
||||||
cdView.setUint32(20, contentBytes.length, true);
|
cdView.setUint32(20, contentBytes.length, true);
|
||||||
cdView.setUint32(24, contentBytes.length, true);
|
cdView.setUint32(24, contentBytes.length, true);
|
||||||
cdView.setUint16(28, nameBytes.length, true);
|
cdView.setUint16(28, nameBytes.length, true);
|
||||||
@@ -2052,22 +2211,27 @@
|
|||||||
termLog(`<span style="color:#a371f7;font-weight:bold">━━━ Pipeline käynnistyy ━━━</span>`);
|
termLog(`<span style="color:#a371f7;font-weight:bold">━━━ Pipeline käynnistyy ━━━</span>`);
|
||||||
|
|
||||||
// Vaihe 1: Manageri pilkkoo projektin tiedostoiksi
|
// Vaihe 1: Manageri pilkkoo projektin tiedostoiksi
|
||||||
termLog(`\n<span style="color:#d29922;font-weight:bold">[1] Manageri</span> — projektin suunnittelu`);
|
|
||||||
pipelineStep('manager', 'Suunnittelu', 'active', task);
|
|
||||||
const managerPrompt = `List the source files needed for this project. One file per line, format:
|
const managerPrompt = `List the source files needed for this project. One file per line, format:
|
||||||
filename.py: what this file contains
|
filename.py: one-line description
|
||||||
|
|
||||||
Rules:
|
CONSTRAINTS — the coder can only generate ~400 tokens per file:
|
||||||
- Max 4 files
|
- Max 3 files (keep it minimal)
|
||||||
- Only .py, .toml, .json, .html files
|
- Each file must be SHORT: one clear responsibility, no boilerplate
|
||||||
|
- Only .py and pyproject.toml files
|
||||||
- No directories, no paths, just filenames
|
- No directories, no paths, just filenames
|
||||||
- List dependencies first, then main app (e.g. models.py before main.py)
|
- List dependencies first, then main app
|
||||||
- Use pyproject.toml for dependencies (not requirements.txt)
|
|
||||||
|
EXAMPLE for "FastAPI todo app with SQLite":
|
||||||
|
pyproject.toml: project metadata and dependencies
|
||||||
|
models.py: SQLAlchemy models and database setup
|
||||||
|
main.py: FastAPI app with CRUD endpoints
|
||||||
|
|
||||||
Project: ${task}`;
|
Project: ${task}`;
|
||||||
const plan = await kpnRun(agentPrompts.manager.model, managerPrompt);
|
termLog(`\n<span style="color:#d29922;font-weight:bold">[1] Manageri</span> — projektin suunnittelu`);
|
||||||
|
pipelineStep('manager', 'Suunnittelu', 'active', managerPrompt);
|
||||||
|
const plan = await kpnRun(agentPrompts.manager.model, managerPrompt, false, 200);
|
||||||
if (!plan) { termLog(' ✗ Pipeline keskeytyi (manageri)', '#f85149'); return; }
|
if (!plan) { termLog(' ✗ Pipeline keskeytyi (manageri)', '#f85149'); return; }
|
||||||
pipelineStep('manager', 'Suunnittelu', 'done', task, plan);
|
pipelineStep('manager', 'Suunnittelu', 'done', managerPrompt, plan);
|
||||||
|
|
||||||
// Parsitaan tiedostolista: "filename.py: description" TAI pelkkä "filename.py"
|
// Parsitaan tiedostolista: "filename.py: description" TAI pelkkä "filename.py"
|
||||||
const fileList = plan.split('\n')
|
const fileList = plan.split('\n')
|
||||||
@@ -2104,7 +2268,7 @@ Project: ${task}`;
|
|||||||
for (let i = 0; i < fileList.length; i++) {
|
for (let i = 0; i < fileList.length; i++) {
|
||||||
const file = fileList[i];
|
const file = fileList[i];
|
||||||
termLog(`\n<span style="color:#3fb950;font-weight:bold">[${i + 2}] Koodari</span> — ${esc(file.name)}`);
|
termLog(`\n<span style="color:#3fb950;font-weight:bold">[${i + 2}] Koodari</span> — ${esc(file.name)}`);
|
||||||
pipelineStep('coder', file.name, 'active', file.desc);
|
pipelineStep('coder', file.name, 'active', '');
|
||||||
|
|
||||||
// Rakennetaan konteksti: aiemmin generoidut tiedostot
|
// Rakennetaan konteksti: aiemmin generoidut tiedostot
|
||||||
let context = '';
|
let context = '';
|
||||||
@@ -2131,16 +2295,55 @@ start = "uvicorn main:app --reload"`;
|
|||||||
extraInstructions = '\nList one dependency per line. No version pins unless necessary.';
|
extraInstructions = '\nList one dependency per line. No version pins unless necessary.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const coderExample = file.name.includes('main') || file.name.includes('app')
|
||||||
|
? `\nEXAMPLE output for a main.py:
|
||||||
|
from fastapi import FastAPI, Depends
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from models import get_db, User
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
@app.get("/users")
|
||||||
|
def list_users(db: Session = Depends(get_db)):
|
||||||
|
return db.query(User).all()
|
||||||
|
|
||||||
|
@app.post("/users")
|
||||||
|
def create_user(name: str, db: Session = Depends(get_db)):
|
||||||
|
user = User(name=name)
|
||||||
|
db.add(user)
|
||||||
|
db.commit()
|
||||||
|
return {"id": user.id, "name": user.name}`
|
||||||
|
: file.name.includes('model')
|
||||||
|
? `\nEXAMPLE output for a models.py:
|
||||||
|
from sqlalchemy import create_engine, Column, Integer, String
|
||||||
|
from sqlalchemy.orm import sessionmaker, declarative_base
|
||||||
|
|
||||||
|
engine = create_engine("sqlite:///app.db")
|
||||||
|
SessionLocal = sessionmaker(bind=engine)
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
class User(Base):
|
||||||
|
__tablename__ = "users"
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
name = Column(String)
|
||||||
|
|
||||||
|
Base.metadata.create_all(engine)
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
db = SessionLocal()
|
||||||
|
try: yield db
|
||||||
|
finally: db.close()`
|
||||||
|
: '';
|
||||||
const coderPrompt = `${context}Project: ${task}
|
const coderPrompt = `${context}Project: ${task}
|
||||||
Write ONLY the file "${file.name}"${file.desc ? ': ' + file.desc : ''}.${extraInstructions}
|
Write ONLY the file "${file.name}"${file.desc ? ': ' + file.desc : ''}.${extraInstructions}${coderExample}
|
||||||
Use the exact libraries mentioned in the project description. Write correct, working code.`;
|
IMPORTANT: Keep the code SHORT. Max ~50 lines. No comments, no docstrings. Write minimal, working code. Output ONLY code.`;
|
||||||
const code = await kpnRun(agentPrompts.coder.model, coderPrompt);
|
const code = await kpnRun(agentPrompts.coder.model, coderPrompt);
|
||||||
if (!code) {
|
if (!code) {
|
||||||
termLog(` ✗ Pipeline keskeytyi (${file.name})`, '#f85149');
|
termLog(` ✗ Pipeline keskeytyi (${file.name})`, '#f85149');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
generatedFiles[file.name] = code;
|
generatedFiles[file.name] = code;
|
||||||
pipelineStep('coder', file.name, 'done', file.desc, code);
|
pipelineStep('coder', file.name, 'done', coderPrompt, code);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vaihe 3: Testaaja arvioi koko projektin
|
// Vaihe 3: Testaaja arvioi koko projektin
|
||||||
@@ -2154,7 +2357,7 @@ Use the exact libraries mentioned in the project description. Write correct, wor
|
|||||||
If the code is correct, say "LGTM".
|
If the code is correct, say "LGTM".
|
||||||
|
|
||||||
${allCode}`;
|
${allCode}`;
|
||||||
const review = await kpnRun(agentPrompts.tester.model, reviewPrompt);
|
const review = await kpnRun(agentPrompts.tester.model, reviewPrompt, false, 200);
|
||||||
pipelineStep('tester', 'Review', 'done', `${Object.keys(generatedFiles).length} tiedostoa`, review);
|
pipelineStep('tester', 'Review', 'done', `${Object.keys(generatedFiles).length} tiedostoa`, review);
|
||||||
|
|
||||||
// Vaihe 4: Korjausluuppi — jos testaaja löysi ongelmia
|
// Vaihe 4: Korjausluuppi — jos testaaja löysi ongelmia
|
||||||
@@ -2173,11 +2376,145 @@ Write the corrected code.`;
|
|||||||
if (fixedCode) {
|
if (fixedCode) {
|
||||||
termLog(`\n<span style="color:#58a6ff;font-weight:bold">[${fileList.length + 4}] Testaaja</span> — uudelleenarviointi`);
|
termLog(`\n<span style="color:#58a6ff;font-weight:bold">[${fileList.length + 4}] Testaaja</span> — uudelleenarviointi`);
|
||||||
pipelineStep('tester', 'Re-review', 'active', fixedCode);
|
pipelineStep('tester', 'Re-review', 'active', fixedCode);
|
||||||
const reReview = await kpnRun(agentPrompts.tester.model, `Review the corrected code briefly:\n${fixedCode}`);
|
const reReview = await kpnRun(agentPrompts.tester.model, `Review the corrected code briefly:\n${fixedCode}`, false, 128);
|
||||||
pipelineStep('tester', 'Re-review', 'done', fixedCode, reReview);
|
pipelineStep('tester', 'Re-review', 'done', fixedCode, reReview);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vaihe 5: QA kirjoittaa testit
|
||||||
|
const step5 = fileList.length + (review && !review.toLowerCase().includes('lgtm') ? 5 : 3);
|
||||||
|
termLog(`\n<span style="color:#3fb950;font-weight:bold">[${step5}] QA</span> — testit`);
|
||||||
|
pipelineStep('qa', 'Testit', 'active', 'Kirjoitetaan testejä');
|
||||||
|
const qaPrompt = `Write test_app.py using pytest and FastAPI TestClient. Max 3 tests. Output ONLY code.
|
||||||
|
|
||||||
|
EXAMPLE:
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from main import app
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
def test_create():
|
||||||
|
r = client.post("/users", params={"name": "Test"})
|
||||||
|
assert r.status_code == 200
|
||||||
|
|
||||||
|
def test_list():
|
||||||
|
r = client.get("/users")
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert isinstance(r.json(), list)
|
||||||
|
|
||||||
|
PROJECT CODE:
|
||||||
|
${Object.entries(generatedFiles).map(([n, c]) => `--- ${n} ---\n${c}`).join('\n\n')}`;
|
||||||
|
const tests = await kpnRun(agentPrompts.qa.model, qaPrompt, false, 512);
|
||||||
|
if (tests) generatedFiles['test_app.py'] = tests;
|
||||||
|
pipelineStep('qa', 'Testit', 'done', 'test_app.py', tests);
|
||||||
|
|
||||||
|
// Vaihe 6: DevOps — Dockerfile
|
||||||
|
const step6 = step5 + 1;
|
||||||
|
termLog(`\n<span style="color:#d29922;font-weight:bold">[${step6}] DevOps</span> — Dockerfile`);
|
||||||
|
pipelineStep('tester', 'Dockerfile', 'active', 'Dockerfile');
|
||||||
|
const mainFile = Object.keys(generatedFiles).find(f => f.includes('main') || f.includes('app')) || Object.keys(generatedFiles)[0];
|
||||||
|
const hasPyproject = 'pyproject.toml' in generatedFiles;
|
||||||
|
const hasRequirements = 'requirements.txt' in generatedFiles;
|
||||||
|
const pyFiles = Object.keys(generatedFiles).filter(f => f.endsWith('.py'));
|
||||||
|
// Dockerfile-templatti: ei anneta mallin keksiä omaa
|
||||||
|
let depLines;
|
||||||
|
if (hasPyproject) {
|
||||||
|
depLines = 'COPY pyproject.toml .\nRUN uv sync --no-dev';
|
||||||
|
} else if (hasRequirements) {
|
||||||
|
depLines = 'COPY requirements.txt .\nRUN uv pip install --system -r requirements.txt';
|
||||||
|
} else {
|
||||||
|
depLines = 'RUN uv pip install --system fastapi uvicorn';
|
||||||
|
}
|
||||||
|
const dockerfileContent = `FROM python:3.12-slim
|
||||||
|
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
||||||
|
WORKDIR /app
|
||||||
|
${depLines}
|
||||||
|
COPY ${pyFiles.join(' ')} ./
|
||||||
|
EXPOSE 8000
|
||||||
|
CMD ["uv", "run", "uvicorn", "${mainFile.replace('.py','')}:app", "--host", "0.0.0.0", "--port", "8000"]`;
|
||||||
|
// Generoidaan Dockerfile suoraan templatesta, ei mallilla
|
||||||
|
generatedFiles['Dockerfile'] = dockerfileContent;
|
||||||
|
termLog(` <span style="color:#3fb950">✓</span> Dockerfile generoitu (template)`);
|
||||||
|
pipelineStep('tester', 'Dockerfile', 'done', dockerfileContent, dockerfileContent);
|
||||||
|
|
||||||
|
// Vaihe 7: DevOps — docker-compose.yml
|
||||||
|
const step7 = step6 + 1;
|
||||||
|
termLog(`\n<span style="color:#d29922;font-weight:bold">[${step7}] DevOps</span> — docker-compose.yml`);
|
||||||
|
pipelineStep('tester', 'Compose', 'active', 'docker-compose.yml');
|
||||||
|
const composePrompt = `Write a docker-compose.yml for this project. Include:
|
||||||
|
- app service (build from Dockerfile, port mapping, restart: unless-stopped)
|
||||||
|
- db service if SQLite/PostgreSQL is used (volume for data persistence)
|
||||||
|
- Named volumes for persistent data
|
||||||
|
Only output the YAML content, nothing else.
|
||||||
|
|
||||||
|
Files: ${Object.keys(generatedFiles).join(', ')}`;
|
||||||
|
const compose = await kpnRun(agentPrompts.tester.model, composePrompt, false, 256);
|
||||||
|
if (compose) generatedFiles['docker-compose.yml'] = compose;
|
||||||
|
pipelineStep('tester', 'Compose', 'done', 'docker-compose.yml', compose);
|
||||||
|
|
||||||
|
// Vaihe 8: DevOps — README
|
||||||
|
const step8 = step7 + 1;
|
||||||
|
termLog(`\n<span style="color:#d29922;font-weight:bold">[${step8}] DevOps</span> — README`);
|
||||||
|
pipelineStep('tester', 'README', 'active', 'README.md');
|
||||||
|
const readmePrompt = `Write a minimal README.md. Include ONLY:
|
||||||
|
1. One-line description
|
||||||
|
2. Quick start: docker compose up
|
||||||
|
3. Development: uv sync && uv run uvicorn main:app --reload
|
||||||
|
4. API endpoints (if applicable)
|
||||||
|
5. Testing: uv run pytest
|
||||||
|
Max 20 lines.
|
||||||
|
|
||||||
|
Files: ${Object.keys(generatedFiles).join(', ')}`;
|
||||||
|
const readme = await kpnRun(agentPrompts.tester.model, readmePrompt, false, 256);
|
||||||
|
if (readme) generatedFiles['README.md'] = readme;
|
||||||
|
pipelineStep('tester', 'README', 'done', 'README.md', readme);
|
||||||
|
|
||||||
|
// Validointivaihe: QA tarkistaa kaikkien tiedostojen yhteensopivuuden
|
||||||
|
const stepV = step8 + 1;
|
||||||
|
termLog(`\n<span style="color:#3fb950;font-weight:bold">[${stepV}] QA</span> — validointi`);
|
||||||
|
pipelineStep('qa', 'Validointi', 'active', 'Tarkistetaan yhteensopivuus');
|
||||||
|
const allFiles = Object.entries(generatedFiles).map(([n, c]) => `--- ${n} ---\n${c}`).join('\n\n');
|
||||||
|
const validatePrompt = `You are a QA engineer. Check EVERY item below and report the result for each. Use this EXACT format:
|
||||||
|
|
||||||
|
1. Dockerfile COPY: ✓ OK / ✗ problem description
|
||||||
|
2. Dockerfile deps vs imports: ✓ OK / ✗ problem description
|
||||||
|
3. docker-compose ports: ✓ OK / ✗ problem description
|
||||||
|
4. README commands: ✓ OK / ✗ problem description
|
||||||
|
5. Test imports: ✓ OK / ✗ problem description
|
||||||
|
6. pyproject.toml deps: ✓ OK / ✗ problem description
|
||||||
|
|
||||||
|
EXAMPLE output:
|
||||||
|
1. Dockerfile COPY: ✓ OK — copies main.py and models.py which both exist
|
||||||
|
2. Dockerfile deps: ✗ missing "sqlalchemy" in pip install
|
||||||
|
3. docker-compose ports: ✓ OK — maps 8000:8000 matching EXPOSE
|
||||||
|
4. README commands: ✓ OK — uvicorn main:app matches main.py
|
||||||
|
5. Test imports: ✓ OK — imports main.app which exists
|
||||||
|
6. pyproject.toml deps: ✓ OK — includes fastapi, uvicorn, sqlalchemy
|
||||||
|
|
||||||
|
Files in project: ${Object.keys(generatedFiles).join(', ')}
|
||||||
|
|
||||||
|
${allFiles}`;
|
||||||
|
const validation = await kpnRun(agentPrompts.qa.model, validatePrompt, false, 256);
|
||||||
|
pipelineStep('qa', 'Validointi', 'done', 'Yhteensopivuus', validation);
|
||||||
|
|
||||||
|
// Jos QA löysi ongelmia, korjataan
|
||||||
|
if (validation && !validation.toLowerCase().startsWith('ok') && !validation.toLowerCase().includes('no issues') && !validation.toLowerCase().includes('everything is fine')) {
|
||||||
|
const stepFix = stepV + 1;
|
||||||
|
termLog(`\n<span style="color:#d29922;font-weight:bold">[${stepFix}] DevOps</span> — korjaukset`);
|
||||||
|
pipelineStep('tester', 'Korjaukset', 'active', validation);
|
||||||
|
// Korjataan vain Dockerfile ja docker-compose
|
||||||
|
const fixPrompt = `Fix ONLY the Dockerfile based on this feedback. Output the corrected Dockerfile, nothing else.
|
||||||
|
|
||||||
|
Feedback: ${validation}
|
||||||
|
|
||||||
|
Current files: ${Object.keys(generatedFiles).join(', ')}
|
||||||
|
Current Dockerfile:
|
||||||
|
${generatedFiles['Dockerfile'] || '(puuttuu)'}`;
|
||||||
|
const fixedDockerfile = await kpnRun(agentPrompts.tester.model, fixPrompt, false, 256);
|
||||||
|
if (fixedDockerfile) generatedFiles['Dockerfile'] = fixedDockerfile;
|
||||||
|
pipelineStep('tester', 'Korjaukset', 'done', 'Dockerfile korjattu', fixedDockerfile);
|
||||||
|
}
|
||||||
|
|
||||||
termLog(`\n<span style="color:#a371f7;font-weight:bold">━━━ Pipeline valmis (${Object.keys(generatedFiles).length} tiedostoa) ━━━</span>`);
|
termLog(`\n<span style="color:#a371f7;font-weight:bold">━━━ Pipeline valmis (${Object.keys(generatedFiles).length} tiedostoa) ━━━</span>`);
|
||||||
renderProjectCard(generatedFiles, task);
|
renderProjectCard(generatedFiles, task);
|
||||||
}
|
}
|
||||||
@@ -2196,11 +2533,78 @@ Write the corrected code.`;
|
|||||||
termLog(`\n<span style="color:#a371f7;font-weight:bold">━━━ Pipeline valmis ━━━</span>`);
|
termLog(`\n<span style="color:#a371f7;font-weight:bold">━━━ Pipeline valmis ━━━</span>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Autokorjaus: tunnetut kirjoitusvirheet ja lähimmän komennon ehdotus
|
||||||
|
function autocorrect(input) {
|
||||||
|
const typos = {
|
||||||
|
'knp': 'kpn', 'kpb': 'kpn', 'kpm': 'kpn', 'kn': 'kpn', 'kp': 'kpn',
|
||||||
|
'kpn rnu': 'kpn run', 'kpn rn': 'kpn run', 'kpn ru': 'kpn run',
|
||||||
|
'kpn laod': 'kpn load', 'kpn lod': 'kpn load', 'kpn loa': 'kpn load',
|
||||||
|
'kpn porject': 'kpn project', 'kpn projcet': 'kpn project', 'kpn proejct': 'kpn project',
|
||||||
|
'kpn pipelien': 'kpn pipeline', 'kpn pipline': 'kpn pipeline',
|
||||||
|
'kpn staus': 'kpn status', 'kpn stauts': 'kpn status',
|
||||||
|
'kpn modles': 'kpn models', 'kpn mdoels': 'kpn models',
|
||||||
|
'kpn hlep': 'kpn help', 'kpn hep': 'kpn help',
|
||||||
|
'kpn clera': 'kpn clear', 'kpn claer': 'kpn clear',
|
||||||
|
'kpn helo': 'kpn hello', 'kpn hell': 'kpn hello',
|
||||||
|
};
|
||||||
|
// Tarkista koko komento ja ensimmäinen sana + alikomento
|
||||||
|
const lower = input.toLowerCase();
|
||||||
|
for (const [typo, fix] of Object.entries(typos)) {
|
||||||
|
if (lower === typo || lower.startsWith(typo + ' ')) {
|
||||||
|
return fix + input.slice(typo.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Levenshtein-etäisyys ensimmäiselle sanalle
|
||||||
|
const words = input.trim().split(/\s+/);
|
||||||
|
const firstWord = words[0].toLowerCase();
|
||||||
|
if (firstWord !== 'kpn' && firstWord.length >= 2 && firstWord.length <= 5) {
|
||||||
|
const dist = levenshtein(firstWord, 'kpn');
|
||||||
|
if (dist <= 2) return 'kpn' + input.slice(firstWord.length);
|
||||||
|
}
|
||||||
|
// Fuzzy-korjaus alikomentotasolla: "kpn rnu" → "kpn run"
|
||||||
|
if (firstWord === 'kpn' && words.length >= 2) {
|
||||||
|
const sub = words[1].toLowerCase();
|
||||||
|
const subCommands = ['help', 'run', 'project', 'pipeline', 'load', 'status', 'models', 'hello', 'clear'];
|
||||||
|
let bestMatch = null, bestDist = 3;
|
||||||
|
for (const cmd of subCommands) {
|
||||||
|
const d = levenshtein(sub, cmd);
|
||||||
|
if (d > 0 && d < bestDist) { bestDist = d; bestMatch = cmd; }
|
||||||
|
}
|
||||||
|
if (bestMatch) {
|
||||||
|
words[1] = bestMatch;
|
||||||
|
return words.join(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function levenshtein(a, b) {
|
||||||
|
const m = a.length, n = b.length;
|
||||||
|
const d = Array.from({length: m + 1}, (_, i) => [i]);
|
||||||
|
for (let j = 1; j <= n; j++) d[0][j] = j;
|
||||||
|
for (let i = 1; i <= m; i++)
|
||||||
|
for (let j = 1; j <= n; j++)
|
||||||
|
d[i][j] = Math.min(d[i-1][j] + 1, d[i][j-1] + 1, d[i-1][j-1] + (a[i-1] !== b[j-1] ? 1 : 0));
|
||||||
|
return d[m][n];
|
||||||
|
}
|
||||||
|
|
||||||
function termExec(cmd) {
|
function termExec(cmd) {
|
||||||
termLog(`<span class="terminal-prompt">$</span> ${esc(cmd)}`);
|
termLog(`<span class="terminal-prompt">$</span> ${esc(cmd)}`);
|
||||||
termHistory.unshift(cmd);
|
termHistory.unshift(cmd);
|
||||||
termHistIdx = -1;
|
termHistIdx = -1;
|
||||||
|
|
||||||
|
// Autokorjaus
|
||||||
|
const corrected = autocorrect(cmd.trim());
|
||||||
|
if (corrected && corrected !== cmd.trim()) {
|
||||||
|
cmd = corrected;
|
||||||
|
termLog(` <span style="color:#d29922">→ korjattu: ${esc(cmd)}</span>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Oikotie: pelkkä numero → kpn load <numero>
|
||||||
|
if (/^\d+$/.test(cmd.trim())) {
|
||||||
|
cmd = 'kpn load ' + cmd.trim();
|
||||||
|
termLog(` <span style="color:#d29922">→ ${esc(cmd)}</span>`);
|
||||||
|
}
|
||||||
const parts = cmd.trim().split(/\s+/);
|
const parts = cmd.trim().split(/\s+/);
|
||||||
if (parts[0] !== 'kpn') {
|
if (parts[0] !== 'kpn') {
|
||||||
termLog('kpn: tuntematon komento. Kokeile: kpn help', '#f85149');
|
termLog('kpn: tuntematon komento. Kokeile: kpn help', '#f85149');
|
||||||
@@ -2227,38 +2631,107 @@ Write the corrected code.`;
|
|||||||
|
|
||||||
if (sub === 'load') {
|
if (sub === 'load') {
|
||||||
const arg = parts[2];
|
const arg = parts[2];
|
||||||
const btn = document.getElementById('agent-compute-btn');
|
const ollamaModels = [
|
||||||
// Mallikatalogista valinta numerolla tai nimellä
|
{ id: '1', name: 'qwen2.5-coder:0.5b', size: '~400 MB', vram_mb: 0, type: 'selain + Ollama' },
|
||||||
const loadModels = [
|
{ id: '2', name: 'qwen2.5-coder:1.5b', size: '~1 GB', vram_mb: 1500, type: 'Ollama GPU' },
|
||||||
{ id: '1', key: '05b', name: 'Qwen2.5-Coder:0.5B', size: '~990 MB', coderSize: '05b' },
|
{ id: '3', name: 'qwen2.5-coder:7b', size: '~4.7 GB', vram_mb: 5500, type: 'Ollama GPU', default: true },
|
||||||
{ id: '2', key: '3b', name: 'Qwen2.5-Coder:1.5B Q4', size: '~1 GB', coderSize: '3b' },
|
{ id: '4', name: 'qwen2.5-coder:14b', size: '~9 GB', vram_mb: 10000, type: 'Ollama GPU' },
|
||||||
|
{ id: '5', name: 'qwen2.5-coder:32b', size: '~20 GB', vram_mb: 21000, type: 'Ollama GPU' },
|
||||||
];
|
];
|
||||||
if (!arg) {
|
if (!arg) {
|
||||||
// Näytetään lista
|
// Haetaan laitteistotiedot ja näytetään sopivat mallit
|
||||||
termLog(' Ladattavat mallit:', '#c9d1d9');
|
fetch('/api/v1/hardware').then(r => r.json()).then(hw => {
|
||||||
for (const m of loadModels) {
|
const vram = hw.vram_mb || 0;
|
||||||
const active = (btn?.dataset.state === 'ready' && coderSize === m.coderSize) ? ' <span style="color:#3fb950">✓ ladattu</span>' : '';
|
const ram = hw.ram_mb || 0;
|
||||||
termLog(` <span style="color:#58a6ff">${m.id}</span> ${m.name} <span style="color:#8b949e">(${m.size})</span>${active}`);
|
const gpu = hw.gpu_name || '?';
|
||||||
|
const available = vram || ram; // CPU-fallback käyttää RAM:ia
|
||||||
|
if (vram > 0) {
|
||||||
|
termLog(` <span style="color:#8b949e">GPU: ${gpu} | VRAM: ${Math.round(vram/1024)} GB | RAM: ${Math.round(ram/1024)} GB</span>`);
|
||||||
|
} else if (ram > 0) {
|
||||||
|
termLog(` <span style="color:#8b949e">Ei GPU:ta | RAM: ${Math.round(ram/1024)} GB (CPU-moodi)</span>`);
|
||||||
|
}
|
||||||
|
termLog(' Mallit:', '#c9d1d9');
|
||||||
|
for (const m of ollamaModels) {
|
||||||
|
const fits = m.vram_mb === 0 || m.vram_mb < available;
|
||||||
|
const active = m.default ? ' <span style="color:#3fb950">← aktiivinen</span>' : '';
|
||||||
|
const icon = fits ? `<span style="color:#58a6ff">${m.id}</span>` : `<span style="color:#8b949e;text-decoration:line-through">${m.id}</span>`;
|
||||||
|
const warn = !fits ? ' <span style="color:#f85149">⚠ ei mahdu</span>' : '';
|
||||||
|
termLog(` ${icon} ${fits ? '' : '<span style="color:#8b949e">'}${m.name} ${m.size} | ${m.type}${fits ? '' : '</span>'}${active}${warn}`);
|
||||||
}
|
}
|
||||||
termLog(' Käyttö: kpn load <numero>', '#8b949e');
|
termLog(' Käyttö: kpn load <numero>', '#8b949e');
|
||||||
|
}).catch(() => {
|
||||||
|
termLog(' Mallit:', '#c9d1d9');
|
||||||
|
for (const m of ollamaModels) {
|
||||||
|
const active = m.default ? ' <span style="color:#3fb950">← aktiivinen</span>' : '';
|
||||||
|
termLog(` <span style="color:#58a6ff">${m.id}</span> ${m.name} <span style="color:#8b949e">${m.size} | ${m.type}</span>${active}`);
|
||||||
|
}
|
||||||
|
termLog(' Käyttö: kpn load <numero>', '#8b949e');
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const selected = loadModels.find(m => m.id === arg || m.key === arg || m.coderSize === arg);
|
const selected = ollamaModels.find(m => m.id === arg || m.name === arg);
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
termLog(` Tuntematon malli "${esc(arg)}". Kokeile: kpn load`, '#f85149');
|
termLog(` Tuntematon malli "${esc(arg)}". Kokeile: kpn load`, '#f85149');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (btn?.dataset.state === 'ready' && coderSize === selected.coderSize) {
|
// Selain-WASM (vain 0.5b)
|
||||||
termLog(` ✓ ${selected.name} on jo ladattu ja valmis`, '#3fb950');
|
if (selected.id === '1') {
|
||||||
|
const btn = document.getElementById('agent-compute-btn');
|
||||||
|
if (btn?.dataset.state === 'ready') {
|
||||||
|
termLog(' ✓ Qwen2.5-Coder:0.5B on jo ladattu (selain)', '#3fb950');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
coderSize = selected.coderSize;
|
coderSize = '05b';
|
||||||
localStorage.setItem('kpn-coder-size', coderSize);
|
termLog(' Ladataan Qwen2.5-Coder:0.5B selaimeen...', '#d29922');
|
||||||
termLog(` Alustetaan ${selected.name} (${selected.size})...`, '#d29922');
|
|
||||||
if (btn) btn.click();
|
if (btn) btn.click();
|
||||||
else ensureCoderNode();
|
else ensureCoderNode();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Ollama: vaihdetaan malli hubin kautta
|
||||||
|
termLog(` Vaihdetaan Ollama-malli: ${selected.name} (${selected.size})...`, '#d29922');
|
||||||
|
// Tilaindikaattori
|
||||||
|
const pullLine = document.createElement('div');
|
||||||
|
pullLine.className = 'terminal-line term-pull';
|
||||||
|
pullLine.innerHTML = ' <span style="color:#d29922">⠋ Ladataan...</span>';
|
||||||
|
termPanel.appendChild(pullLine);
|
||||||
|
termPanel.scrollTop = termPanel.scrollHeight;
|
||||||
|
const spinFrames = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
|
||||||
|
let spinIdx = 0;
|
||||||
|
const spinTimer = setInterval(() => {
|
||||||
|
spinIdx = (spinIdx + 1) % spinFrames.length;
|
||||||
|
const content = pullLine.querySelector('span');
|
||||||
|
if (content) content.textContent = spinFrames[spinIdx] + ' Ladataan ' + selected.name + '...';
|
||||||
|
}, 100);
|
||||||
|
// Vaihdetaan malli hubille + Ollama pull
|
||||||
|
Promise.all([
|
||||||
|
fetch('/api/v1/model', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ model: selected.name }),
|
||||||
|
}).then(r => r.json()),
|
||||||
|
// Suora pull Ollamasta — odotetaan kunnes malli on ladattu
|
||||||
|
fetch('http://' + window.location.hostname + ':11434/api/pull', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name: selected.name, stream: false }),
|
||||||
|
}).then(r => r.json()).catch(() => ({ status: 'ok' })),
|
||||||
|
]).then(([hubData, _]) => {
|
||||||
|
clearInterval(spinTimer);
|
||||||
|
pullLine.remove();
|
||||||
|
if (hubData.status === 'ok') {
|
||||||
|
termLog(` <span style="color:#3fb950">✓</span> ${selected.name} ladattu ja aktiivinen`, '#3fb950');
|
||||||
|
ollamaModels.forEach(m => m.default = false);
|
||||||
|
selected.default = true;
|
||||||
|
} else {
|
||||||
|
termLog(' ✗ Mallin vaihto epäonnistui', '#f85149');
|
||||||
|
}
|
||||||
|
}).catch(e => {
|
||||||
|
clearInterval(spinTimer);
|
||||||
|
pullLine.remove();
|
||||||
|
termLog(` ✗ ${e.message}`, '#f85149');
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (sub === 'status') {
|
if (sub === 'status') {
|
||||||
const nodes = statNodes.textContent || '0';
|
const nodes = statNodes.textContent || '0';
|
||||||
@@ -2268,14 +2741,31 @@ Write the corrected code.`;
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (sub === 'models') {
|
if (sub === 'models') {
|
||||||
termLog(' Käytettävissä olevat mallit:', '#c9d1d9');
|
const allModels = [
|
||||||
termLog(' <span style="color:#58a6ff">1</span> qwen-coder Qwen2.5-Coder:0.5B <span style="color:#8b949e">~990 MB | koodin generointi</span>');
|
{ id: '1', name: 'qwen2.5-coder:0.5b', size: '~400 MB', type: 'selain + Ollama' },
|
||||||
termLog(' <span style="color:#58a6ff">2</span> qwen-coder-3b Qwen2.5-Coder:1.5B Q4 <span style="color:#8b949e">~1 GB | kvantisoidtu, parempi laatu</span>');
|
{ id: '2', name: 'qwen2.5-coder:1.5b', size: '~1 GB', type: 'Ollama GPU' },
|
||||||
termLog(' <span style="color:#58a6ff">3</span> smollm-135m SmolLM 135M <span style="color:#8b949e">~270 MB | kevyt, nopea</span>');
|
{ id: '3', name: 'qwen2.5-coder:7b', size: '~4.7 GB', type: 'Ollama GPU' },
|
||||||
termLog(' <span style="color:#58a6ff">4</span> qwen-05b Qwen2.5:0.5B <span style="color:#8b949e">~990 MB | yleismalli</span>');
|
{ id: '4', name: 'qwen2.5-coder:14b', size: '~9 GB', type: 'Ollama GPU' },
|
||||||
termLog(' <span style="color:#58a6ff">5</span> phi3-mini Phi-3 Mini <span style="color:#8b949e">~2.2 GB | Microsoftin malli</span>');
|
{ id: '5', name: 'qwen2.5-coder:32b', size: '~20 GB', type: 'Ollama GPU' },
|
||||||
termLog(' Käyttö: kpn run <malli> "<prompti>"', '#8b949e');
|
];
|
||||||
termLog(' Lataus: kpn load <numero>', '#8b949e');
|
// Haetaan ladatut mallit Ollamasta
|
||||||
|
Promise.all([
|
||||||
|
fetch('/api/v1/hardware').then(r => r.json()).catch(() => ({})),
|
||||||
|
fetch('http://' + window.location.hostname + ':11434/api/tags').then(r => r.json()).catch(() => ({ models: [] })),
|
||||||
|
]).then(([hw, ollama]) => {
|
||||||
|
const loadedNames = (ollama.models || []).map(m => m.name.replace(':latest', ''));
|
||||||
|
const btn = document.getElementById('agent-compute-btn');
|
||||||
|
const wasmLoaded = btn?.dataset.state === 'ready';
|
||||||
|
if (hw.gpu_name && hw.gpu_name !== 'ei natiivisolmua') {
|
||||||
|
termLog(` <span style="color:#8b949e">GPU: ${hw.gpu_name} | VRAM: ${Math.round((hw.vram_mb||0)/1024)} GB</span>`);
|
||||||
|
}
|
||||||
|
termLog(' Mallit <span style="color:#8b949e">(kpn load <numero>)</span>:', '#c9d1d9');
|
||||||
|
for (const m of allModels) {
|
||||||
|
const loaded = (m.id === '1' && wasmLoaded) || loadedNames.some(n => m.name.includes(n) || n.includes(m.name.split(':')[1]));
|
||||||
|
const status = loaded ? ' <span style="color:#3fb950">✓ ladattu</span>' : '';
|
||||||
|
termLog(` <span style="color:#58a6ff">${m.id}</span> ${m.name} <span style="color:#8b949e">${m.size} | ${m.type}</span>${status}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2352,6 +2842,13 @@ Write the corrected code.`;
|
|||||||
};
|
};
|
||||||
|
|
||||||
function tabComplete(input) {
|
function tabComplete(input) {
|
||||||
|
// Autokorjaus ensin: korjaa typo ja palauta true jos korjattiin
|
||||||
|
const corrected = autocorrect(input.value.trim());
|
||||||
|
if (corrected && corrected !== input.value.trim()) {
|
||||||
|
input.value = corrected;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const val = input.value;
|
const val = input.value;
|
||||||
const words = val.trimEnd().split(/\s+/);
|
const words = val.trimEnd().split(/\s+/);
|
||||||
|
|
||||||
@@ -2512,7 +3009,14 @@ Write the corrected code.`;
|
|||||||
}
|
}
|
||||||
} else if (e.key === 'Tab') {
|
} else if (e.key === 'Tab') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
// Näytä dropdown tai täydennä jos vain yksi vaihtoehto
|
// 1. Autokorjaus ensin
|
||||||
|
const corrected = autocorrect(termInput.value.trim());
|
||||||
|
if (corrected && corrected !== termInput.value.trim()) {
|
||||||
|
termInput.value = corrected;
|
||||||
|
hideDropdown();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 2. Dropdown / täydennys
|
||||||
const { items, prefix } = getCandidates(termInput.value);
|
const { items, prefix } = getCandidates(termInput.value);
|
||||||
if (items.length === 1) {
|
if (items.length === 1) {
|
||||||
termInput.value = prefix + items[0] + (items[0].startsWith('"') ? '' : ' ');
|
termInput.value = prefix + items[0] + (items[0].startsWith('"') ? '' : ' ');
|
||||||
@@ -2551,7 +3055,8 @@ Write the corrected code.`;
|
|||||||
// Klikkaa terminaalipaneelia → fokusoi input
|
// Klikkaa terminaalipaneelia → fokusoi input
|
||||||
termPanel?.addEventListener('click', () => termInput?.focus());
|
termPanel?.addEventListener('click', () => termInput?.focus());
|
||||||
|
|
||||||
uiSocket.onmessage = (event) => {
|
// Tallennetaan message-handler funktioon jotta reconnect voi käyttää samaa
|
||||||
|
const _wsHandler = (event) => {
|
||||||
try {
|
try {
|
||||||
const raw = event.data;
|
const raw = event.data;
|
||||||
if (raw.includes('"single_tokenize"')) return;
|
if (raw.includes('"single_tokenize"')) return;
|
||||||
@@ -2743,8 +3248,11 @@ Write the corrected code.`;
|
|||||||
while (term.children.length > 50 && !term.firstChild.querySelector('.stream-content')) term.removeChild(term.firstChild);
|
while (term.children.length > 50 && !term.firstChild.querySelector('.stream-content')) term.removeChild(term.firstChild);
|
||||||
term.scrollTop = term.scrollHeight;
|
term.scrollTop = term.scrollHeight;
|
||||||
|
|
||||||
|
// Avatar-aktivointi vain oikeille käyttäjäpyynnöille
|
||||||
|
if (data.task_id) {
|
||||||
document.querySelectorAll('.avatar-card').forEach(c => c.classList.remove('active'));
|
document.querySelectorAll('.avatar-card').forEach(c => c.classList.remove('active'));
|
||||||
document.getElementById('avatar-kpn').classList.add('active');
|
document.getElementById('avatar-kpn')?.classList.add('active');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (isCoder) {
|
} else if (isCoder) {
|
||||||
// Codelab: erillinen addCodeResult-handler käsittelee (rivi 2364)
|
// Codelab: erillinen addCodeResult-handler käsittelee (rivi 2364)
|
||||||
@@ -2891,7 +3399,8 @@ Write the corrected code.`;
|
|||||||
term.scrollTop = term.scrollHeight;
|
term.scrollTop = term.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Avatar-aktivointi vain omille tehtäville
|
// Avatar-aktivointi vain oikeille käyttäjäpyynnöille (task_id)
|
||||||
|
if (data.task_id) {
|
||||||
document.querySelectorAll('.avatar-card').forEach(c => c.classList.remove('active'));
|
document.querySelectorAll('.avatar-card').forEach(c => c.classList.remove('active'));
|
||||||
const model = data.model || '';
|
const model = data.model || '';
|
||||||
const p = data.prompt ? data.prompt.toLowerCase() : '';
|
const p = data.prompt ? data.prompt.toLowerCase() : '';
|
||||||
@@ -2911,8 +3420,11 @@ Write the corrected code.`;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
};
|
};
|
||||||
|
window._wsMessageHandler = _wsHandler;
|
||||||
|
if (uiSocket) uiSocket.onmessage = _wsHandler;
|
||||||
|
|
||||||
btn.addEventListener('click', async () => {
|
btn.addEventListener('click', async () => {
|
||||||
// Käytetään viewer-authissa jo tunnistettua WebGPU-tilaa
|
// Käytetään viewer-authissa jo tunnistettua WebGPU-tilaa
|
||||||
@@ -3233,7 +3745,9 @@ Write the corrected code.`;
|
|||||||
const _prevConsoleLog = console.log;
|
const _prevConsoleLog = console.log;
|
||||||
console.log = function(...args) { _prevConsoleLog.apply(console, args); codeLogListener(...args); };
|
console.log = function(...args) { _prevConsoleLog.apply(console, args); codeLogListener(...args); };
|
||||||
|
|
||||||
// Käynnistä Coder-node automaattisesti ensimmäisellä kerralla
|
// Web Worker -pohjainen laskentasolmu — UI ei jäädy inferenssin aikana
|
||||||
|
let coderWorker = null;
|
||||||
|
|
||||||
async function ensureCoderNode() {
|
async function ensureCoderNode() {
|
||||||
if (coderJoined) return;
|
if (coderJoined) return;
|
||||||
coderJoined = true;
|
coderJoined = true;
|
||||||
@@ -3243,10 +3757,21 @@ Write the corrected code.`;
|
|||||||
setStep('step-wasm', 'active');
|
setStep('step-wasm', 'active');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!wasmInitialized) {
|
// Käynnistetään WASM Web Workerissa
|
||||||
await init();
|
coderWorker = new Worker('./worker.js', { type: 'module' });
|
||||||
wasmInitialized = true;
|
|
||||||
}
|
// Workerin console.log-viestit → pääsäikeen kuuntelija
|
||||||
|
// Worker ei voi kutsua console.log näkyvästi, joten WASM:n console_log
|
||||||
|
// ei näy automaattisesti. Workerissa console.log menee Workerin konsoliin.
|
||||||
|
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
coderWorker.onmessage = (e) => {
|
||||||
|
if (e.data.type === 'ready') resolve();
|
||||||
|
else if (e.data.type === 'error') reject(new Error(e.data.message));
|
||||||
|
};
|
||||||
|
coderWorker.postMessage({ type: 'init' });
|
||||||
|
});
|
||||||
|
|
||||||
setStep('step-wasm', 'done');
|
setStep('step-wasm', 'done');
|
||||||
setStep('step-tokenizer', 'active');
|
setStep('step-tokenizer', 'active');
|
||||||
|
|
||||||
@@ -3260,30 +3785,26 @@ Write the corrected code.`;
|
|||||||
selected_task: coderSize === '3b' ? 'qwen-coder-3b' : 'qwen-coder-05b'
|
selected_task: coderSize === '3b' ? 'qwen-coder-3b' : 'qwen-coder-05b'
|
||||||
};
|
};
|
||||||
const taskId = coderSize === '3b' ? 5 : 4;
|
const taskId = coderSize === '3b' ? 5 : 4;
|
||||||
// Tunnistetaan WebGPU myös koodilaboratorion puolella
|
|
||||||
let coderHasWebGPU = false;
|
// Käynnistetään node Workerissa
|
||||||
if (navigator.gpu) {
|
coderWorker.onmessage = (e) => {
|
||||||
try {
|
if (e.data.type === 'started') {
|
||||||
const adapter = await navigator.gpu.requestAdapter();
|
|
||||||
if (adapter) {
|
|
||||||
try {
|
|
||||||
const testDevice = await adapter.requestDevice({ requiredLimits: { maxInterStageShaderComponents: 60 } });
|
|
||||||
coderHasWebGPU = true;
|
|
||||||
testDevice.destroy();
|
|
||||||
} catch(e) {
|
|
||||||
coderHasWebGPU = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch(e) {}
|
|
||||||
}
|
|
||||||
await start_agent_node(wsUrl, coderHasWebGPU, JSON.stringify(deviceInfo), taskId);
|
|
||||||
document.getElementById('coder-status').textContent = 'Connected';
|
document.getElementById('coder-status').textContent = 'Connected';
|
||||||
document.getElementById('coder-status').style.color = '#d29922';
|
document.getElementById('coder-status').style.color = '#d29922';
|
||||||
coderWsReady = true;
|
coderWsReady = true;
|
||||||
|
} else if (e.data.type === 'log') {
|
||||||
|
// Workerin console.log → pääsäikeen kuuntelijat (tilaindikaattori, pipeline-stepit)
|
||||||
|
console.log(e.data.message);
|
||||||
|
} else if (e.data.type === 'error') {
|
||||||
|
console.log('[Worker] Virhe: ' + e.data.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
coderWorker.postMessage({
|
||||||
|
type: 'start',
|
||||||
|
data: { hubUrl: wsUrl, hasWebGPU: false, deviceInfo: JSON.stringify(deviceInfo), taskId }
|
||||||
|
});
|
||||||
|
|
||||||
// Proaktiivinen mallin esilataus: lähetetään tyhjä warmup-prompt
|
// Warmup
|
||||||
// joka triggeröi get_or_build_model:n ilman varsinaista generointia.
|
|
||||||
// Pipeline-tilakone seuraa logeja ja merkkaa vaiheet valmiiksi.
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (uiSocket && uiSocket.readyState === 1) {
|
if (uiSocket && uiSocket.readyState === 1) {
|
||||||
uiSocket.send(JSON.stringify({
|
uiSocket.send(JSON.stringify({
|
||||||
@@ -3297,7 +3818,7 @@ Write the corrected code.`;
|
|||||||
if (pendingCodePrompt) {
|
if (pendingCodePrompt) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
sendCodeToHub(pendingCodePrompt);
|
sendCodeToHub(pendingCodePrompt);
|
||||||
}, 2000); // Hieman pidempi odotus jotta warmup ehtii ensin
|
}, 2000);
|
||||||
pendingCodePrompt = null;
|
pendingCodePrompt = null;
|
||||||
}
|
}
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
@@ -3659,5 +4180,20 @@ Write the corrected code.`;
|
|||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Prompt-editori modal -->
|
||||||
|
<div id="prompt-modal" style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);z-index:1000;backdrop-filter:blur(4px)">
|
||||||
|
<div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:#0d1117;border:1px solid #30363d;border-radius:8px;width:700px;max-width:90vw;max-height:85vh;overflow-y:auto;padding:20px">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
|
||||||
|
<span id="prompt-modal-title" style="font-weight:600;font-size:15px;color:#58a6ff"></span>
|
||||||
|
<button onclick="closePromptModal()" style="background:none;border:none;color:#8b949e;font-size:20px;cursor:pointer;padding:0 4px">×</button>
|
||||||
|
</div>
|
||||||
|
<div id="prompt-modal-fields" style="display:flex;flex-direction:column;gap:10px"></div>
|
||||||
|
<div style="display:flex;gap:8px;margin-top:16px;justify-content:flex-end">
|
||||||
|
<button onclick="closePromptModal()" style="background:#161b22;border:1px solid #30363d;color:#8b949e;padding:6px 16px;border-radius:4px;cursor:pointer">Sulje</button>
|
||||||
|
<button onclick="rerunFromModal()" style="background:#238636;border:1px solid #2ea043;color:white;padding:6px 16px;border-radius:4px;cursor:pointer;font-weight:600">▶ Aja uudelleen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
38
network-poc/static/worker.js
Normal file
38
network-poc/static/worker.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
// Kipinä WASM Worker (ES module) — ajaa kielimallin inferenssin erillisessä säikeessä
|
||||||
|
import init, { start_agent_node, set_gpu_load, set_auto_tasks } from './pkg/node.js';
|
||||||
|
|
||||||
|
let wasmReady = false;
|
||||||
|
|
||||||
|
// Välitetään console.log -viestit pääsäikeelle jotta UI-kuuntelijat näkevät ne
|
||||||
|
const _origLog = console.log;
|
||||||
|
console.log = function(...args) {
|
||||||
|
_origLog.apply(console, args);
|
||||||
|
self.postMessage({ type: 'log', message: args.join(' ') });
|
||||||
|
};
|
||||||
|
|
||||||
|
self.onmessage = async (e) => {
|
||||||
|
const { type, data } = e.data;
|
||||||
|
|
||||||
|
if (type === 'init') {
|
||||||
|
try {
|
||||||
|
await init();
|
||||||
|
wasmReady = true;
|
||||||
|
self.postMessage({ type: 'ready' });
|
||||||
|
} catch (err) {
|
||||||
|
self.postMessage({ type: 'error', message: 'WASM init: ' + err.message });
|
||||||
|
}
|
||||||
|
} else if (type === 'start') {
|
||||||
|
if (!wasmReady) return;
|
||||||
|
const { hubUrl, hasWebGPU, deviceInfo, taskId } = data;
|
||||||
|
try {
|
||||||
|
await start_agent_node(hubUrl, hasWebGPU, deviceInfo, taskId);
|
||||||
|
self.postMessage({ type: 'started' });
|
||||||
|
} catch (err) {
|
||||||
|
self.postMessage({ type: 'error', message: 'Node: ' + err.message });
|
||||||
|
}
|
||||||
|
} else if (type === 'set_gpu_load') {
|
||||||
|
if (wasmReady) set_gpu_load(data.load);
|
||||||
|
} else if (type === 'set_auto_tasks') {
|
||||||
|
if (wasmReady) set_auto_tasks(data.enabled);
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user