FI/SV/EN kielituki about valmis (testaamatta)
This commit is contained in:
@@ -26,9 +26,14 @@ Tässä on kooste projektin vaatimuksista, työtehtävistä ja niiden nykytilant
|
||||
- Sijoittaa Hub-palvelin julkisesti saatavuusosoitteeseen `kipina.studio`.
|
||||
|
||||
### Tehtävät
|
||||
- [ ] Tuotantopalvelimen käyttöönotto Nginxin tai Docker-compose kautta ehtojen täytyttyä
|
||||
- [ ] Turvamekanismin lisäys: Varmistetaan, ettei kukaan lähetä "falskeja" vastauksia nodeilta
|
||||
- [ ] Solmuille rekisteröitymismekanismi tai tulostaulukko
|
||||
- [x] Tuotantopalvelimen käyttöönotto Docker-compose + Caddy TLS kautta (`kipina.studio`)
|
||||
- [x] Deploy-skripti (`deploy.sh`) + Discord-webhook-notifikaatio julkaisuista
|
||||
- [x] Admin-dashboard (`/admin`) Basic Auth -suojattuna, live-sessiot ja metriikat
|
||||
- [x] REST API (`POST /api/v1/chat/completions`) task_id-pohjaisella vastausten reitityksellä
|
||||
- [x] API timeout (120s) + selkeät virheilmoitukset (504 Gateway Timeout)
|
||||
- [x] IP-pohjainen rate limiting (max 4 yhteyttä/IP) + origin-validointi
|
||||
- [ ] Turvamekanismin lisäys: Varmistetaan, ettei kukaan lähetä "falskeja" vastauksia nodeilta (PoW/challenge-response)
|
||||
- [x] SQLite-sessioseuranta (node_sessions + pair_results)
|
||||
|
||||
---
|
||||
|
||||
@@ -53,7 +58,38 @@ Tässä on kooste projektin vaatimuksista, työtehtävistä ja niiden nykytilant
|
||||
- Kyetä lataamaan selaimen IndexedDB:hen satojen megatavujen painot massivisena fetch-hakuna, kääntää ne WebGPU-puskureihin (Buffers) ja suorittaa tekstigeneraatiota etänä ohjattuna verkosta käsin WebSocketia myöden.
|
||||
|
||||
### Tehtävät
|
||||
- [ ] Refaktoroi Wasm-Noden (Burn.rs) paketti tuomaan Text-Tokenizerit (esim. BPE) ja kielimallin arkkitehtuuri käyttöön
|
||||
- [ ] Koodaa Nodeen logiikka hakea / kasata mallin painot välimuistista "Chunk"-lohkoina valmiiksi
|
||||
- [ ] Hub uudistetaan generoimaan pelkkien matikkavaikeuksien sijasta Text Prompts (esim. "Kirjoita haiku Suomesta") ja reitittämään työkuorman vapaalle solmulle
|
||||
- [ ] Kipinän käyttöliittymään Chat-ikkuna Hubin striimaamien tulossanojen tarkkailuun reaaliajassa
|
||||
- [x] Refaktoroi Wasm-Noden (Burn.rs) paketti tuomaan Text-Tokenizerit (BPE, Qwen2.5-Coder) ja kielimallin arkkitehtuuri käyttöön
|
||||
- [x] Koodaa Nodeen logiikka hakea / kasata mallin painot välimuistista IndexedDB:hen (tokenizer.json + model weights)
|
||||
- [x] Hub uudistetaan generoimaan Text Prompts ja reitittämään työkuorman vapaalle solmulle (broadcast + task_id-matching)
|
||||
- [x] Kipinän käyttöliittymään Chat-ikkuna Hubin striimaamien tulossanojen tarkkailuun reaaliajassa (llm_chunk streaming)
|
||||
- [x] SmolLM 135M — täysi transformer (Burn), ~1.2 tok/s CPU
|
||||
- [x] Qwen2.5 0.5B — Candle-inferenssi, ChatML-muotoilu, ~0.4 tok/s CPU
|
||||
- [x] Qwen2.5-Coder 0.5B & 3B — koodigeneraatio, streaming-tokenit, task_id-tuki
|
||||
- [x] Phi-3 Mini — placeholder (liian suuri selaimelle, natiivisolmulle suunnitteilla)
|
||||
- [x] EN/FI tokenisaatiovertailu overhead-laskennalla
|
||||
- [x] Natiivisolmu (Rust + CUDA) — Qwen2.5 0.5B, ~50-100 tok/s RTX 4090, NVML GPU-metriikat
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Vaihe 6: Agent Workspace & CLI (KÄYNNISSÄ)
|
||||
|
||||
### Tavoitteet
|
||||
- Interaktiivinen terminaalipohjainen käyttöliittymä `kpn`-komennoilla.
|
||||
- Agenttitiimi (Koodari, Testaaja, Manageri) muokattavilla system prompteilla.
|
||||
- Agenttien ketjutus: manageri analysoi → koodari toteuttaa → testaaja arvioi.
|
||||
|
||||
### Tehtävät
|
||||
- [x] KPN-terminaali selaimeen (interaktiivinen komentorivi, komentohistoria)
|
||||
- [x] `kpn run <malli> "<prompti>"` — tehtävän lähetys REST API:n kautta
|
||||
- [x] `kpn hello` — tervehdyskomento
|
||||
- [x] `kpn pipeline "<tehtävä>"` — manageri → koodari → testaaja -ketjutus
|
||||
- [x] `kpn status`, `kpn models`, `kpn clear`, `kpn help`
|
||||
- [x] Agenttikortit (Koodari/Qwen-Coder, Testaaja/SmolLM, Manageri/KPN CLI)
|
||||
- [x] Muokattavat system promptit per agentti (localStorage-tallennus)
|
||||
- [x] Multi-select: yhteinen konteksti useammalle agentille
|
||||
- [x] Streaming-vastaukset terminaalissa (llm_chunk + vilkkuva kursori)
|
||||
- [x] URL-hash navigointi (`#agents`, `#codelab`, `#network`)
|
||||
- [x] SPA fallback (ServeDir + ServeFile)
|
||||
- [ ] Agenttien välinen keskustelu (manageri ohjaa koodaria ja testaajaa dynaamisesti)
|
||||
- [ ] Tehtävähistoria ja tulosten tallennus
|
||||
- [ ] CLI-työkalu (`kpn` binary) lokaaliin käyttöön
|
||||
|
||||
@@ -851,23 +851,29 @@ async fn api_chat_completions(
|
||||
|
||||
let mut rx = state.stats_tx.subscribe();
|
||||
let _ = state.stats_tx.send(msg.to_string());
|
||||
|
||||
while let Ok(msg_str) = rx.recv().await {
|
||||
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&msg_str) {
|
||||
if v["type"].as_str() == Some("llm_done") {
|
||||
if let Some(tid) = v["task_id"].as_str() {
|
||||
if tid == payload.task_id {
|
||||
let res = ChatCompletionResponse {
|
||||
response: v["response"].as_str().unwrap_or("").to_string(),
|
||||
model: v["model"].as_str().unwrap_or("").to_string(),
|
||||
tokens_generated: v["tokens_generated"].as_u64().unwrap_or(0),
|
||||
};
|
||||
return axum::Json(res).into_response();
|
||||
|
||||
let timeout = tokio::time::timeout(std::time::Duration::from_secs(120), async move {
|
||||
while let Ok(msg_str) = rx.recv().await {
|
||||
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&msg_str) {
|
||||
if v["type"].as_str() == Some("llm_done") {
|
||||
if let Some(tid) = v["task_id"].as_str() {
|
||||
if tid == payload.task_id {
|
||||
return Some(ChatCompletionResponse {
|
||||
response: v["response"].as_str().unwrap_or("").to_string(),
|
||||
model: v["model"].as_str().unwrap_or("").to_string(),
|
||||
tokens_generated: v["tokens_generated"].as_u64().unwrap_or(0),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}).await;
|
||||
|
||||
match timeout {
|
||||
Ok(Some(res)) => axum::Json(res).into_response(),
|
||||
Ok(None) => (axum::http::StatusCode::INTERNAL_SERVER_ERROR, "Verkkovirhe: yhteys katkesi").into_response(),
|
||||
Err(_) => (axum::http::StatusCode::GATEWAY_TIMEOUT, "Aikakatkaisu: yksikään solmu ei vastannut 120s sisällä").into_response(),
|
||||
}
|
||||
|
||||
(axum::http::StatusCode::INTERNAL_SERVER_ERROR, "Network Error").into_response()
|
||||
}
|
||||
|
||||
@@ -225,7 +225,8 @@ pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use
|
||||
if next_token != eos_token {
|
||||
if let Ok(text) = tokenizer.decode(&[next_token], true) {
|
||||
generated_text.push_str(&text);
|
||||
let chunk = serde_json::json!({ "type": "llm_chunk", "token": text, "prompt": prompt, "model": "Qwen2.5-Coder" });
|
||||
let mut chunk = serde_json::json!({ "type": "llm_chunk", "token": text, "prompt": prompt, "model": "Qwen2.5-Coder" });
|
||||
if let Some(ref tid) = task_id { chunk.as_object_mut().unwrap().insert("task_id".to_string(), serde_json::json!(tid)); }
|
||||
let _ = ws.borrow().send_with_str(&chunk.to_string());
|
||||
}
|
||||
tokens_generated += 1;
|
||||
@@ -258,7 +259,8 @@ pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use
|
||||
|
||||
if let Ok(text) = tokenizer.decode(&[next_token], true) {
|
||||
generated_text.push_str(&text);
|
||||
let chunk = serde_json::json!({ "type": "llm_chunk", "token": text, "prompt": prompt, "model": "Qwen2.5-Coder" });
|
||||
let mut chunk = serde_json::json!({ "type": "llm_chunk", "token": text, "prompt": prompt, "model": "Qwen2.5-Coder" });
|
||||
if let Some(ref tid) = task_id { chunk.as_object_mut().unwrap().insert("task_id".to_string(), serde_json::json!(tid)); }
|
||||
let _ = ws.borrow().send_with_str(&chunk.to_string());
|
||||
}
|
||||
tokens_generated += 1;
|
||||
|
||||
@@ -120,6 +120,7 @@
|
||||
.main-panel.active { display: block; }
|
||||
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
@keyframes blink { 0%,100% { opacity:1; } 50% { opacity:0; } }
|
||||
|
||||
.code-output {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
@@ -1286,36 +1287,87 @@
|
||||
termPanel.scrollTop = termPanel.scrollHeight;
|
||||
}
|
||||
|
||||
async function kpnRun(model, prompt) {
|
||||
// Aktiiviset streaming-rivit task_id:n mukaan
|
||||
const activeStreams = {};
|
||||
|
||||
// Lähettää promptin mallille ja palauttaa vastauksen (tai null virhetilanteessa)
|
||||
async function kpnRun(model, prompt, silent) {
|
||||
termLog(` → <span style="color:#58a6ff">${model}</span> käsittelee...`, '#8b949e');
|
||||
try {
|
||||
const taskId = crypto.randomUUID();
|
||||
// Liitetään yhteinen konteksti + agentin oma system prompt
|
||||
const agent = Object.values(agentPrompts).find(a => a.model === model);
|
||||
const parts = [];
|
||||
if (sharedPrompt) parts.push(sharedPrompt);
|
||||
if (agent && agent.prompt) parts.push(agent.prompt);
|
||||
parts.push(prompt);
|
||||
const fullPrompt = parts.join('\n\n');
|
||||
|
||||
// Luodaan streaming-rivi terminaaliin
|
||||
if (!silent) {
|
||||
const streamDiv = document.createElement('div');
|
||||
streamDiv.className = 'terminal-line';
|
||||
streamDiv.style.color = '#c9d1d9';
|
||||
streamDiv.innerHTML = ' <span class="stream-content"></span><span style="color:#8b949e;animation:blink 1s infinite">▌</span>';
|
||||
termPanel.appendChild(streamDiv);
|
||||
termPanel.scrollTop = termPanel.scrollHeight;
|
||||
activeStreams[taskId] = streamDiv;
|
||||
}
|
||||
|
||||
const res = await fetch('/api/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ model, prompt: fullPrompt, task_id: taskId }),
|
||||
});
|
||||
|
||||
// Poistetaan streaming-rivi
|
||||
if (activeStreams[taskId]) {
|
||||
activeStreams[taskId].remove();
|
||||
delete activeStreams[taskId];
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
termLog(` ✗ Virhe: ${res.status} ${res.statusText}`, '#f85149');
|
||||
return;
|
||||
const errText = await res.text().catch(() => res.statusText);
|
||||
termLog(` ✗ ${errText}`, '#f85149');
|
||||
return null;
|
||||
}
|
||||
const data = await res.json();
|
||||
const response = (data.response || '').trim();
|
||||
const tokGen = data.tokens_generated || 0;
|
||||
termLog(` <span style="color:#3fb950">✓</span> <span style="color:#58a6ff">${data.model || model}</span> <span style="color:#8b949e">(${tokGen} tok)</span>`);
|
||||
termLog(` ${response.replace(/</g,'<').replace(/\n/g,'\n ')}`, '#c9d1d9');
|
||||
if (!silent) {
|
||||
termLog(` ${response.replace(/</g,'<').replace(/\n/g,'\n ')}`, '#c9d1d9');
|
||||
}
|
||||
return response;
|
||||
} catch (e) {
|
||||
termLog(` ✗ ${e.message}`, '#f85149');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Pipeline: manageri → koodari → testaaja
|
||||
async function kpnPipeline(task) {
|
||||
termLog(`<span style="color:#a371f7;font-weight:bold">━━━ Pipeline käynnistyy ━━━</span>`);
|
||||
|
||||
// Vaihe 1: Manageri analysoi
|
||||
termLog(`\n<span style="color:#d29922;font-weight:bold">[1/3] Manageri</span> — tehtävän analyysi`);
|
||||
const managerPrompt = `Analysoi seuraava ohjelmistokehitystehtävä ja kirjoita koodarille selkeä tekninen ohje mitä koodata. Vastaa lyhyesti.\n\nTehtävä: ${task}`;
|
||||
const plan = await kpnRun(agentPrompts.manager.model, managerPrompt);
|
||||
if (!plan) { termLog(' ✗ Pipeline keskeytyi (manageri)', '#f85149'); return; }
|
||||
|
||||
// Vaihe 2: Koodari toteuttaa
|
||||
termLog(`\n<span style="color:#3fb950;font-weight:bold">[2/3] Koodari</span> — toteutus`);
|
||||
const coderPrompt = `${plan}\n\nKirjoita koodi yllä olevan ohjeen mukaisesti.`;
|
||||
const code = await kpnRun(agentPrompts.coder.model, coderPrompt);
|
||||
if (!code) { termLog(' ✗ Pipeline keskeytyi (koodari)', '#f85149'); return; }
|
||||
|
||||
// Vaihe 3: Testaaja arvioi
|
||||
termLog(`\n<span style="color:#58a6ff;font-weight:bold">[3/3] Testaaja</span> — arviointi`);
|
||||
const testerPrompt = `Arvioi seuraava koodi lyhyesti. Onko siinä bugeja? Puuttuuko testejä? Anna arvosana 1-5.\n\nTehtävä: ${task}\n\nKoodi:\n${code}`;
|
||||
await kpnRun(agentPrompts.tester.model, testerPrompt);
|
||||
|
||||
termLog(`\n<span style="color:#a371f7;font-weight:bold">━━━ Pipeline valmis ━━━</span>`);
|
||||
}
|
||||
|
||||
function termExec(cmd) {
|
||||
termLog(`<span class="terminal-prompt">$</span> ${cmd.replace(/</g,'<')}`);
|
||||
termHistory.unshift(cmd);
|
||||
@@ -1331,6 +1383,7 @@
|
||||
if (sub === 'help' || !sub) {
|
||||
termLog(' kpn hello — iloinen tervehdys verkosta', '#a5d6ff');
|
||||
termLog(' kpn run <malli> "<prompti>" — aja tehtävä verkossa', '#a5d6ff');
|
||||
termLog(' kpn pipeline "<tehtävä>" — manageri → koodari → testaaja', '#a5d6ff');
|
||||
termLog(' kpn status — verkon tila', '#a5d6ff');
|
||||
termLog(' kpn models — käytettävissä olevat mallit', '#a5d6ff');
|
||||
termLog(' kpn clear — tyhjennä terminaali', '#a5d6ff');
|
||||
@@ -1358,6 +1411,18 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === 'pipeline') {
|
||||
const afterPipeline = cmd.replace(/^kpn\s+pipeline\s*/, '');
|
||||
const pMatch = afterPipeline.match(/^"(.+)"$|^'(.+)'$|^(.+)$/);
|
||||
const pTask = (pMatch && (pMatch[1] || pMatch[2] || pMatch[3] || '')).trim();
|
||||
if (!pTask) {
|
||||
termLog(' Käyttö: kpn pipeline "<tehtävä>"', '#f85149');
|
||||
return;
|
||||
}
|
||||
kpnPipeline(pTask);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === 'hello') {
|
||||
kpnRun('smollm-135m', 'Tervehdi käyttäjää iloisesti ja lyhyesti suomeksi. Ole innostunut ja energinen! Vastaa yhdellä lauseella.');
|
||||
return;
|
||||
@@ -1617,6 +1682,16 @@
|
||||
|
||||
console.log(`[${model}] ${tokGen} tokenia | ${typeof durMs === 'number' ? durMs.toFixed(0) : durMs}ms | ${tokS} tok/s | "${(data.response || '').substring(0, 60)}..."`);
|
||||
} else if (data.type === "llm_chunk") {
|
||||
// Terminaalin streaming: päivitetään aktiivinen rivi
|
||||
if (data.task_id && activeStreams[data.task_id]) {
|
||||
const streamDiv = activeStreams[data.task_id];
|
||||
const contentEl = streamDiv.querySelector('.stream-content');
|
||||
if (contentEl) {
|
||||
contentEl.textContent += data.token || '';
|
||||
termPanel.scrollTop = termPanel.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// Streaming: näytetään generointi reaaliaikaisesti
|
||||
const model = data.model || '';
|
||||
const isCoder = model.includes('Coder');
|
||||
|
||||
Reference in New Issue
Block a user