Compare commits
18 Commits
agentic-co
...
3976bb6251
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3976bb6251 | ||
|
|
0c32fecdc4 | ||
|
|
801cc0371d | ||
|
|
176f2d6915 | ||
|
|
dd1945ab28 | ||
|
|
262fee3b49 | ||
|
|
aa7540a6bf | ||
|
|
762066102a | ||
|
|
bef5b6fc3c | ||
|
|
095b72d2d6 | ||
|
|
4cb6128a27 | ||
|
|
4dff534fbf | ||
|
|
d5ab6272d3 | ||
|
|
2e7b86deeb | ||
|
|
a6e49870d6 | ||
|
|
d68882249e | ||
|
|
6a587cd080 | ||
|
|
f17fcf0f9d |
348
network-poc/PROMPTS.md
Normal file
348
network-poc/PROMPTS.md
Normal file
@@ -0,0 +1,348 @@
|
||||
# Kipinä Agentic Studio — Promptit
|
||||
|
||||
Kaikki järjestelmässä käytetyt promptit. Jokainen on dokumentoitu eksaktisti
|
||||
niin kuin se lähetetään mallille, muuttujat merkitty `${...}`-syntaksilla.
|
||||
|
||||
---
|
||||
|
||||
## 1. Inferenssin system prompt (Wasm + natiivi)
|
||||
|
||||
**Sijainti:** `node/src/qwen_coder.rs` rivi 256, `native-node/src/inference.rs` rivi 127
|
||||
**Malli:** Qwen2.5-Coder-0.5B/3B
|
||||
**ChatML-rooli:** `<|im_start|>system`
|
||||
|
||||
```
|
||||
You are a coding assistant. Respond with ONLY code. No explanations, no markdown, no comments unless asked.
|
||||
```
|
||||
|
||||
**Tarkoitus:** Pakottaa malli tuottamaan pelkkää koodia ilman selityksiä.
|
||||
**Prefill:** Assistantin vastaus alkaa ` ``` ` joka ohjaa mallin koodiblokkiin.
|
||||
|
||||
---
|
||||
|
||||
## 2. Agenttikohtaiset system promptit (frontend)
|
||||
|
||||
**Sijainti:** `static/index.html` rivit 1136-1144
|
||||
**Tallennus:** localStorage (`kpn-agent-prompt-{key}`)
|
||||
**ChatML-rooli:** Liitetään `<|im_start|>user` -blokkiin osaksi promptia
|
||||
|
||||
### 2.1 Manageri (manager)
|
||||
```
|
||||
Olet projektipäällikkö. Jaa tehtävät osiin, priorisoi ja koordinoi tiimin työtä.
|
||||
```
|
||||
**Malli:** qwen-coder
|
||||
|
||||
### 2.2 Koodari (coder)
|
||||
```
|
||||
Olet kokenut ohjelmistokehittäjä. Kirjoita selkeää, testattavaa koodia ja vastaa aina koodilla.
|
||||
```
|
||||
**Malli:** qwen-coder
|
||||
|
||||
### 2.3 Data-agentti (data)
|
||||
```
|
||||
Olet tietokanta-asiantuntija. Vastaat skeemojen suunnittelusta, SQL-kyselyiden optimoinnista ja datamalleista.
|
||||
```
|
||||
**Malli:** qwen-coder
|
||||
|
||||
### 2.4 QA (qa)
|
||||
```
|
||||
Olet laadunvarmistaja (QA). Kirjoitat testejä, etsit virheitä ja varmistat, että kaikki reunatapaukset on huomioitu.
|
||||
```
|
||||
**Malli:** smollm-135m
|
||||
|
||||
### 2.5 DevOps / Testaaja (tester)
|
||||
```
|
||||
Olet DevOps-insinööri. Vastaat koodin julkaisuputkista, serveri-infrastruktuurista ja ympäristön suorituskyvystä.
|
||||
```
|
||||
**Malli:** smollm-135m
|
||||
|
||||
### 2.6 Tarkkailija (observer)
|
||||
```
|
||||
Olet ohjelmistoprojektin riippumaton valvoja. Sinulla on täysi pääsy kaikkiin projektin tietoihin ja muiden agenttien keskusteluihin. Valvo tiimin (Manageri, Koodari, Data, QA, DevOps) toimintaa asiantuntijana kokonaisuutena ja huomauta välittömästi visio- tai turvallisuusriskeistä.
|
||||
```
|
||||
**Malli:** deepseek-r1
|
||||
|
||||
### 2.7 Asiakas (client)
|
||||
```
|
||||
Kirjoita tähän asiakkaan toiveet ja projektin vaatimukset. Orkestraattori (Manageri) purkaa ja delegoi nämä työt asiantuntijoille.
|
||||
```
|
||||
**Malli:** user-input (ei LLM:ää, käyttäjän teksti)
|
||||
|
||||
---
|
||||
|
||||
## 3. Projekti-pipeline (`kpn project`)
|
||||
|
||||
### 3.1 Vaihe 1: Managerin tiedostojako
|
||||
|
||||
**Konteksti:** Käyttäjä on antanut projektin kuvauksen.
|
||||
**Tavoite:** Pilkotaan projekti yksittäisiksi tiedostoiksi oikeassa riippuvuusjärjestyksessä.
|
||||
|
||||
```
|
||||
List the source files needed for this project. One file per line, format:
|
||||
filename.py: what this file contains
|
||||
|
||||
Rules:
|
||||
- Max 4 files
|
||||
- Only .py, .toml, .json, .html files
|
||||
- No directories, no paths, just filenames
|
||||
- List dependencies first, then main app (e.g. models.py before main.py)
|
||||
- Use pyproject.toml for dependencies (not requirements.txt)
|
||||
|
||||
Project: ${task}
|
||||
```
|
||||
|
||||
**Odotettu vastausformaatti:**
|
||||
```
|
||||
models.py: SQLAlchemy User model and database setup
|
||||
main.py: FastAPI app with CRUD endpoints
|
||||
pyproject.toml: project dependencies
|
||||
```
|
||||
|
||||
**Parsintasäännöt:**
|
||||
- Rivi voi olla `filename.ext: kuvaus` tai pelkkä `filename.ext`
|
||||
- Tiedostonimessä ei saa olla `/`, välilyöntejä tai polkuja
|
||||
- Päättyy tiedostopäätteeseen (`/\.\w{1,5}$/`)
|
||||
- Numerot, `-`, `*` ja `` ` `` strippataan rivin alusta
|
||||
- Max 40 merkin tiedostonimi
|
||||
|
||||
### 3.2 Vaihe 2: Koodarin tiedostogenerointi (per tiedosto)
|
||||
|
||||
**Konteksti:** Managerin tiedostolista on parsittu. Jokaiselle tiedostolle generoidaan koodi erikseen. Aiemmin generoidut tiedostot annetaan kontekstina.
|
||||
|
||||
**Perusmuoto:**
|
||||
```
|
||||
${context}Project: ${task}
|
||||
Write ONLY the file "${filename}"${description ? ': ' + description : ''}.
|
||||
Use the exact libraries mentioned in the project description. Write correct, working code.
|
||||
```
|
||||
|
||||
**`${context}` (kun aiempia tiedostoja on generoitu):**
|
||||
```
|
||||
Already written files:
|
||||
--- models.py ---
|
||||
from sqlalchemy import ...
|
||||
...
|
||||
|
||||
--- main.py ---
|
||||
from fastapi import ...
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
**Erikoistapaus: pyproject.toml**
|
||||
|
||||
Koska 0.5B-malli ei tunne uv/pyproject.toml-formaattia, annetaan eksplisiittinen esimerkki:
|
||||
```
|
||||
${context}Project: ${task}
|
||||
Write ONLY the file "pyproject.toml": ${description}.
|
||||
Use this exact format:
|
||||
[project]
|
||||
name = "projectname"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = ["fastapi", "uvicorn"]
|
||||
|
||||
[project.scripts]
|
||||
start = "uvicorn main:app --reload"
|
||||
Use the exact libraries mentioned in the project description. Write correct, working code.
|
||||
```
|
||||
|
||||
**Erikoistapaus: requirements.txt (fallback)**
|
||||
```
|
||||
...
|
||||
List one dependency per line. No version pins unless necessary.
|
||||
...
|
||||
```
|
||||
|
||||
### 3.3 Vaihe 2 (fallback): Yhtenä kokonaisuutena
|
||||
|
||||
Jos managerin vastaus ei tuota parsittavaa tiedostolistaa:
|
||||
```
|
||||
Project: ${task}
|
||||
Files: ${managerin_vastaus}
|
||||
|
||||
Write all the code for this project. Use the exact libraries mentioned in the project description. Use pyproject.toml for dependencies (not requirements.txt).
|
||||
```
|
||||
|
||||
### 3.4 Vaihe 3: Testerin arviointi
|
||||
|
||||
**Konteksti:** Kaikki generoidut tiedostot yhdistettynä.
|
||||
|
||||
```
|
||||
Review this project. List bugs or issues. Be brief.
|
||||
If the code is correct, say "LGTM".
|
||||
|
||||
--- models.py ---
|
||||
from sqlalchemy import ...
|
||||
|
||||
--- main.py ---
|
||||
from fastapi import ...
|
||||
```
|
||||
|
||||
**Odotettu vastaus:** Bugilista tai `LGTM`.
|
||||
**Trigger korjausluuppiin:** Jos vastaus EI sisällä "lgtm" tai "looks good" (case-insensitive).
|
||||
|
||||
### 3.5 Vaihe 4: Koodarin korjaukset (ehdollinen)
|
||||
|
||||
Ajetaan vain jos testeri löysi ongelmia.
|
||||
|
||||
```
|
||||
Fix the issues found in the review.
|
||||
Review feedback: ${review}
|
||||
|
||||
Current code:
|
||||
--- models.py ---
|
||||
...
|
||||
|
||||
--- main.py ---
|
||||
...
|
||||
|
||||
Write the corrected code.
|
||||
```
|
||||
|
||||
### 3.6 Vaihe 5: Testerin uudelleenarviointi (ehdollinen)
|
||||
|
||||
```
|
||||
Review the corrected code briefly:
|
||||
${fixedCode}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Yksinkertainen pipeline (`kpn pipeline`)
|
||||
|
||||
### 4.1 Manageri
|
||||
```
|
||||
Analyse this task briefly and write a technical spec for a coder:
|
||||
${task}
|
||||
```
|
||||
|
||||
### 4.2 Koodari
|
||||
```
|
||||
${managerin_vastaus}
|
||||
|
||||
Write the code.
|
||||
```
|
||||
|
||||
### 4.3 Testaaja
|
||||
```
|
||||
Review briefly:
|
||||
${koodarin_vastaus}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Yksittäiset komennot
|
||||
|
||||
### 5.1 `kpn run <malli> "<prompti>"`
|
||||
|
||||
Promptin koostaminen `kpnRun`-funktiossa:
|
||||
```
|
||||
${sharedPrompt} ← Kaikille agenteille yhteinen (jos asetettu)
|
||||
|
||||
${agentPrompt} ← Valitun agentin system prompt (jos löytyy)
|
||||
|
||||
${käyttäjän_prompti} ← Käyttäjän kirjoittama teksti
|
||||
```
|
||||
|
||||
Osat yhdistetään `\n\n`-erottimella ja lähetetään `<|im_start|>user`-blokkiin.
|
||||
|
||||
### 5.2 `kpn hello`
|
||||
|
||||
Kiinteä prompti SmolLM-135M -mallille:
|
||||
```
|
||||
Tervehdi käyttäjää iloisesti ja lyhyesti suomeksi. Ole innostunut ja energinen! Vastaa yhdellä lauseella.
|
||||
```
|
||||
|
||||
### 5.3 Warmup (automaattinen)
|
||||
|
||||
Lähetetään automaattisesti kun laskentasolmu käynnistyy. Triggeröi mallin latauksen ilman näkyvää tulosta.
|
||||
```json
|
||||
{"prompt": "warmup", "max_tokens": 1}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Stop-sekvenssit (inferenssi)
|
||||
|
||||
**Sijainti:** `node/src/qwen_coder.rs` rivi 345, `native-node/src/inference.rs` rivi 210
|
||||
|
||||
Generointi katkaistaan ja teksti trimmataan kun malli tuottaa minkä tahansa näistä:
|
||||
|
||||
| Sekvenssi | Tarkoitus |
|
||||
|-----------|-----------|
|
||||
| `\n###` | Markdown-otsikko (selitysosio alkaa) |
|
||||
| `\nExplanation` | Selitysosio |
|
||||
| `\nNote:` | Huomautus |
|
||||
| `\nOutput:` | Esimerkkitulostus |
|
||||
| `` \n```\n\n `` | Koodiblokin loppu + tyhjä rivi |
|
||||
| `\n// Example` | Esimerkkikoodi (C/Rust/JS) |
|
||||
| `\n// example` | Sama pienellä |
|
||||
| `\n# Example` | Esimerkkikoodi (Python/Ruby) |
|
||||
| `\n# example` | Sama pienellä |
|
||||
|
||||
---
|
||||
|
||||
## 7. Vastauksen siivous (post-processing)
|
||||
|
||||
**Sijainti:** `strip_markdown_wrapper()` molemmissa inferenssimoduuleissa
|
||||
|
||||
### 7.1 Kielitunnisteen poisto
|
||||
|
||||
Jos ensimmäinen rivi on tunnettu kielitunniste, se poistetaan.
|
||||
Tunnistetut: `python`, `py`, `rust`, `rs`, `javascript`, `js`, `typescript`, `ts`,
|
||||
`java`, `kotlin`, `scala`, `go`, `ruby`, `rb`, `php`, `swift`,
|
||||
`c`, `cpp`, `c++`, `c#`, `csharp`, `r`, `sql`, `bash`, `sh`, `zsh`,
|
||||
`html`, `css`, `json`, `yaml`, `yml`, `toml`, `xml`, `markdown`, `md`,
|
||||
`lua`, `perl`, `dart`, `elixir`, `haskell`, `hs`, `ocaml`, `zig`,
|
||||
`plaintext`, `text`, `txt`
|
||||
|
||||
### 7.2 Sulkevan ` ``` ` poisto
|
||||
|
||||
Poistetaan VAIN jos ` ``` ` on omalla rivillään tiedoston lopussa
|
||||
(edeltävä merkki on rivinvaihto tai alku).
|
||||
|
||||
### 7.3 Johdantolauseiden poisto
|
||||
|
||||
Ensimmäinen rivi poistetaan jos se alkaa (case-insensitive):
|
||||
`Sure!`, `Here is`, `Here's`, `Certainly!`, `Below is`
|
||||
|
||||
### 7.4 Selityskommenttien poisto
|
||||
|
||||
Alun `# `-alkuiset rivit poistetaan jos ne sisältävät (case-insensitive):
|
||||
`this is`, `simple`, `program that`, `here is`, `the following`, `below`
|
||||
|
||||
Shebang (`#!`) säilytetään.
|
||||
|
||||
---
|
||||
|
||||
## 8. Promptin kulku mallille (ChatML)
|
||||
|
||||
Lopullinen viesti mallille koostetaan näin:
|
||||
|
||||
```
|
||||
<|im_start|>system
|
||||
You are a coding assistant. Respond with ONLY code. No explanations, no markdown, no comments unless asked.<|im_end|>
|
||||
<|im_start|>user
|
||||
${sharedPrompt}
|
||||
|
||||
${agentPrompt}
|
||||
|
||||
${käyttäjän/pipelinen prompti}<|im_end|>
|
||||
<|im_start|>assistant
|
||||
```
|
||||
```
|
||||
|
||||
**Huomio:** ` ``` ` assistantin alussa on prefill — se on osa syötettä eikä mallin tuottamaa. Malli jatkaa suoraan koodilla.
|
||||
|
||||
---
|
||||
|
||||
## 9. Sampling-parametrit
|
||||
|
||||
| Parametri | Arvo | Kuvaus |
|
||||
|-----------|------|--------|
|
||||
| `temperature` | 0.7 | Jakaumaa pehmentävä kerroin |
|
||||
| `top_k` | 40 | Valinnan rajoitus 40 todennäköisimpään tokeniin |
|
||||
| `repetition_penalty` | 1.15 | Aiemmin generoitujen tokenien rankaisu |
|
||||
| `max_tokens` | 512 (oletus) | Konfiguroitavissa JSON-promptilla |
|
||||
| `eos_token` | 151645 | Qwen2.5:n päätöstokeni |
|
||||
@@ -486,15 +486,15 @@ async fn ws_handler(
|
||||
.and_then(|s| s.trim().parse::<IpAddr>().ok())
|
||||
.unwrap_or_else(|| addr.ip());
|
||||
|
||||
// Max 2 yhteyttä per IP (dashboard-UI + selainsolmu)
|
||||
// Max yhteyttä per IP: jokainen selain tarvitsee 2 (UI + coder-node)
|
||||
{
|
||||
let conns = state.ip_connections.lock().unwrap();
|
||||
let count = conns.get(&ip).copied().unwrap_or(0);
|
||||
if count >= 4 {
|
||||
tracing::warn!("IP {} ylitti yhteysrajan ({}/4) — estetty", ip, count);
|
||||
if count >= 10 {
|
||||
tracing::warn!("IP {} ylitti yhteysrajan ({}/10) — estetty", ip, count);
|
||||
return (
|
||||
axum::http::StatusCode::TOO_MANY_REQUESTS,
|
||||
"Max 4 yhteyttä per IP",
|
||||
"Max 10 yhteyttä per IP",
|
||||
).into_response();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,13 +258,13 @@ pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use
|
||||
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&prompt) {
|
||||
let p = json.get("prompt").and_then(|v| v.as_str()).unwrap_or(&prompt).to_string();
|
||||
let s = json.get("system").and_then(|v| v.as_str()).unwrap_or(default_system).to_string();
|
||||
let m = json.get("max_tokens").and_then(|v| v.as_u64()).unwrap_or(256) as usize;
|
||||
let m = json.get("max_tokens").and_then(|v| v.as_u64()).unwrap_or(512) as usize;
|
||||
(p, s, m)
|
||||
} else {
|
||||
(prompt.clone(), default_system.to_string(), 256)
|
||||
(prompt.clone(), default_system.to_string(), 512)
|
||||
}
|
||||
} else {
|
||||
(prompt.clone(), default_system.to_string(), 256)
|
||||
(prompt.clone(), default_system.to_string(), 512)
|
||||
};
|
||||
|
||||
// Prefill: aloitetaan vastaus ```-koodiblokkilla, jolloin malli jatkaa suoraan koodilla
|
||||
|
||||
429
network-poc/static/GUIDE.md
Normal file
429
network-poc/static/GUIDE.md
Normal file
@@ -0,0 +1,429 @@
|
||||
# Kipinä Agentic Studio — Opas
|
||||
|
||||
Hajautettu AI-laskentaverkko jossa kielimallit ajavat koodia suoraan selaimessa.
|
||||
Tämä opas selittää miten kielimallit toimivat, miten niitä ohjataan, ja miten
|
||||
tuloksia voi parantaa.
|
||||
|
||||
---
|
||||
|
||||
## Kielimallit ja niiden koot
|
||||
|
||||
Kielimalli on neuroverkko joka ennustaa seuraavan sanan (tokenin) edellisten
|
||||
perusteella. Mallin "koko" tarkoittaa parametrien (painojen) määrää:
|
||||
|
||||
| Malli | Parametrit | Koko levyllä | Nopeus selaimessa | Koodinlaatu |
|
||||
|-------|-----------|-------------|-------------------|-------------|
|
||||
| SmolLM 135M | 135 miljoonaa | ~270 MB | ~5 tok/s | Yksinkertainen teksti |
|
||||
| Qwen2.5-Coder:0.5B | 500 miljoonaa | ~990 MB | ~3-6 tok/s | Pienet funktiot |
|
||||
| Qwen2.5-Coder:3B | 3 miljardia | ~6.2 GB | ~0.4 tok/s | Kokonaiset tiedostot |
|
||||
| GPT-4 (vertailu) | ~1800 miljardia | ~3.6 TB | pilvipalvelu | Kokonaiset projektit |
|
||||
|
||||
**Parametrien vaikutus:** Jokainen parametri on yksi liukuluku (float16 = 2 tavua)
|
||||
joka tallentaa opittua tietoa. 0.5B-malli tietää perusrakenteet mutta tekee
|
||||
loogisia virheitä. 3B-malli ymmärtää kontekstin paremmin. Ero on kuin sanakirjan
|
||||
ja oppikirjan välillä.
|
||||
|
||||
**Miksi selaimessa?** Malli ajetaan käyttäjän omalla laitteella WebAssemblyn
|
||||
kautta. Data ei lähde koneelta, eikä tarvita pilvipalvelua. Haittapuoli on
|
||||
hitaus — GPU-palvelimella sama 0.5B-malli tuottaa ~100 tok/s.
|
||||
|
||||
---
|
||||
|
||||
## Tokenit — kielimallin "sanat"
|
||||
|
||||
Malli ei näe tekstiä kirjaimina vaan **tokeneina**. Tokeni on yleensä
|
||||
sanan osa, kokonainen sana tai välilyönti. Tokenisaatio tehdään
|
||||
BPE-algoritmilla (Byte Pair Encoding) joka oppii yleisimmät
|
||||
merkkijonot harjoitusdatasta.
|
||||
|
||||
### Esimerkki: koodi
|
||||
|
||||
```
|
||||
"print('Hello')" → [print] [(' ] [Hello] [')] = 4 tokenia
|
||||
"tulosta('Hei')" → [tul] [osta] [(' ] [He] [i] [')] = 6 tokenia
|
||||
```
|
||||
|
||||
Koodi tokenisoidaan tehokkaasti koska `print`, `def`, `return` yms.
|
||||
ovat kokonaisia tokeneita. Suomenkielinen `tulosta` joudutaan pilkkomaan
|
||||
osiin koska se ei esiinny harjoitusdatassa kokonaisena.
|
||||
|
||||
### Esimerkki: suomi vs. englanti
|
||||
|
||||
Sama lause kahdella kielellä Qwen2.5-Coder -tokenisaattorilla:
|
||||
|
||||
| | Teksti | Tokenit | Määrä | Merkkejä/token |
|
||||
|---|---|---|---|---|
|
||||
| EN | The cat sat on the mat | [The] [ cat] [ sat] [ on] [ the] [ mat] | **6** | 3.7 |
|
||||
| FI | Kissa istui matolla | [K] [issa] [ ist] [ui] [ mat] [olla] | **6** | 3.2 |
|
||||
| EN | Distributed computing in the browser | [Dist] [ributed] [ computing] [ in] [ the] [ browser] | **6** | 6.0 |
|
||||
| FI | Hajautettu laskenta selaimessa | [H] [aj] [au] [tettu] [ las] [kenta] [ sel] [aim] [essa] | **9** | 3.3 |
|
||||
| EN | Write a function that sorts a list | [Write] [ a] [ function] [ that] [ sorts] [ a] [ list] | **7** | 5.0 |
|
||||
| FI | Kirjoita funktio joka lajittelee listan | [K] [irj] [oita] [ funkt] [io] [ joka] [ laj] [ittel] [ee] [ listan] | **10** | 4.0 |
|
||||
|
||||
**Huomaa miten:**
|
||||
- Englannin yleiset sanat (`the`, `in`, `a`, `function`) ovat kokonaisia tokeneita
|
||||
- Suomen sanat pilkotaan pienempiin osiin (`Hajautettu` → 4 tokenia, `Distributed` → 2)
|
||||
- Suomi vaatii **30-50% enemmän tokeneita** saman merkityksen välittämiseen
|
||||
- Koodiavainsanat (`function`, `list`, `sort`) ovat tehokkaita molemmilla kielillä
|
||||
|
||||
### Miksi tämä merkitsee?
|
||||
|
||||
**Jokainen tokeni = yksi laskentakierros.** Jos suomi vaatii 50% enemmän tokeneita:
|
||||
|
||||
1. **Hitaampi vastaus:** 100 tokenin englanninkielinen vastaus ≈ 150 tokenia suomeksi
|
||||
→ 50% pidempi odotusaika
|
||||
2. **Pienempi konteksti:** Sama merkityssisältö vie enemmän tilaa konteksti-ikkunasta
|
||||
3. **Huonompi ymmärrys:** Pitkät sanat pilkotaan osiin jotka malli ei välttämättä
|
||||
tunnista → hallusinaatiot lisääntyvät
|
||||
|
||||
**Siksi tekniset promptit ovat englanniksi** — malli saa enemmän informaatiota
|
||||
samassa token-budjetissa ja ymmärtää ohjeet paremmin.
|
||||
|
||||
**Token-budjetti tässä järjestelmässä:**
|
||||
|
||||
| Osa | Tokeneita | Osuus |
|
||||
|-----|-----------|-------|
|
||||
| System prompt | ~30 | kiinteä |
|
||||
| Agent prompt | ~25 | kiinteä |
|
||||
| Konteksti (aiemmat tiedostot) | 0-300 | kasvaa |
|
||||
| Käyttäjän prompti | ~20-50 | vaihtelee |
|
||||
| **Syöte yhteensä** | **~75-400** | |
|
||||
| Generoitu vastaus (max) | 512 | raja |
|
||||
| **Yhteensä** | **~600-900** | /32 768 |
|
||||
|
||||
Konteksti-ikkuna on reilusti riittävä. Pullonkaula ei ole ikkunan koko
|
||||
vaan **mallin kyky ymmärtää pitkää kontekstia** — 0.5B-malli alkaa
|
||||
"unohtaa" ohjeet kun konteksti kasvaa yli ~200 tokenin.
|
||||
|
||||
---
|
||||
|
||||
## Promptit — miten mallia ohjataan
|
||||
|
||||
### Kolmitasoinen prompttirakenne
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S["System prompt<br/><i>You are a coding assistant. Respond with ONLY code.</i><br/>🔒 Kiinteä, kovakoodattu — malli priorisoi tämän"]
|
||||
A["Agent prompt<br/><i>Olet kokenut ohjelmistokehittäjä...</i><br/>✏️ Käyttäjän muokattavissa UI:ssa"]
|
||||
U["User prompt<br/><i>Write ONLY the file main.py...</i><br/>📋 Vaihtelee joka kutsussa, sisältää kontekstin"]
|
||||
P["Prefill: ``` <br/>🎯 Pakottaa mallin aloittamaan koodilla"]
|
||||
S --> A --> U --> P
|
||||
P -->|malli jatkaa| R["Generoitu koodi"]
|
||||
|
||||
style S fill:#1a1e2e,stroke:#f85149,color:#c9d1d9
|
||||
style A fill:#1a1e2e,stroke:#d29922,color:#c9d1d9
|
||||
style U fill:#1a1e2e,stroke:#3fb950,color:#c9d1d9
|
||||
style P fill:#1a1e2e,stroke:#a371f7,color:#c9d1d9
|
||||
style R fill:#0d1117,stroke:#58a6ff,color:#58a6ff
|
||||
```
|
||||
|
||||
### Miksi promptit ovat englanniksi?
|
||||
|
||||
Qwen2.5-Coder on harjoitettu pääosin englanninkielisellä koodilla ja
|
||||
dokumentaatiolla. Suomenkielinen ohje kuluttaa enemmän tokeneita JA
|
||||
malli ymmärtää sen huonommin. Agenttien nimet ja käyttöliittymä ovat
|
||||
suomeksi, mutta tekniset ohjeet mallille englanniksi.
|
||||
|
||||
Poikkeus: agenttipromptit ovat suomeksi koska ne menevät user-blokkiin
|
||||
(ei system-blokkiin) ja niiden tarkoitus on enemmän "persoonallisuus"
|
||||
kuin tekninen ohje.
|
||||
|
||||
---
|
||||
|
||||
## Prefill-tekniikka
|
||||
|
||||
Normaalisti malli päättää vapaasti miten vastaa:
|
||||
|
||||
```
|
||||
Ilman prefilliä:
|
||||
Malli: "Sure! Here is a Python program that prints Hello World:\n```python\nprint('Hello')\n```"
|
||||
→ 25 tokenia, joista 15 turhia
|
||||
|
||||
Prefillin kanssa:
|
||||
Me syötämme: ```
|
||||
Malli jatkaa: python\nprint('Hello')\n```
|
||||
→ 5 tokenia, kaikki hyödyllisiä
|
||||
```
|
||||
|
||||
Prefill on kuin aloittaisit lauseen toisen puolesta — malli jatkaa
|
||||
siitä mihin jäit sen sijaan, että aloittaisi kohteliaalla johdannolla.
|
||||
|
||||
**Sivuvaikutus:** Malli tuottaa kielitunnisteen (`python`, `rust`) ja
|
||||
sulkevan ` ``` `:n. Nämä siivotaan jälkikäteen `strip_markdown_wrapper`-funktiolla.
|
||||
|
||||
---
|
||||
|
||||
## Sampling — miten malli valitsee seuraavan tokenin
|
||||
|
||||
Malli ei "tiedä" oikeaa vastausta. Se laskee jokaiselle mahdolliselle
|
||||
seuraavalle tokenille todennäköisyyden ja valitsee yhden. Valintaa
|
||||
ohjataan kolmella parametrilla:
|
||||
|
||||
### Temperature (0.7)
|
||||
|
||||
Kontrolloi "luovuutta" vs. "varmuutta":
|
||||
|
||||
```
|
||||
Temperature 0.0 (greedy): Aina todennäköisin tokeni → "def fibonacci(n):"
|
||||
Temperature 0.7 (oletus): Painottaa todennäköisiä mutta sallii vaihtelua
|
||||
Temperature 1.5 (luova): Lähes satunnainen → "async lambda fib = ..."
|
||||
```
|
||||
|
||||
0.7 on kompromissi: tarpeeksi determinististä tuottamaan toimivaa koodia,
|
||||
mutta tarpeeksi vaihtelevaa välttämään toistoa.
|
||||
|
||||
### Top-k (40)
|
||||
|
||||
Rajaa valinnan 40 todennäköisimpään tokeniin. Estää mallia valitsemasta
|
||||
täysin absurdeja vaihtoehtoja:
|
||||
|
||||
```
|
||||
Ilman top-k: 150 936 vaihtoehtoa → voi valita minkä tahansa
|
||||
Top-k 40: 40 vaihtoehtoa → järkevät vaihtoehdot
|
||||
Top-k 1: 1 vaihtoehto → greedy (aina sama vastaus)
|
||||
```
|
||||
|
||||
### Repetition penalty (1.15)
|
||||
|
||||
Vähentää jo tuotettujen tokenien todennäköisyyttä. Estää mallia
|
||||
juuttumasta luuppiin:
|
||||
|
||||
```
|
||||
Ilman rangaistusta: "print print print print print..."
|
||||
Penalty 1.15: "print('Hello')\nprint('World')"
|
||||
```
|
||||
|
||||
1.15 on lievä rangaistus — estää pahimman toiston mutta sallii
|
||||
saman avainsanan (esim. `return`) esiintymisen useasti.
|
||||
|
||||
---
|
||||
|
||||
## Stop-sekvenssit — milloin generointi loppuu
|
||||
|
||||
Malli generoi tokeneita kunnes jokin näistä tapahtuu:
|
||||
|
||||
1. **EOS-tokeni** (151645): Mallin oma "loppu"-merkki
|
||||
2. **Max tokens** (512): Kovakoodattu raja
|
||||
3. **Stop-sekvenssi**: Malli alkaa tuottaa selitystä
|
||||
|
||||
```
|
||||
fn fibonacci(n: usize) -> usize {
|
||||
if n <= 1 { return n; }
|
||||
fibonacci(n-1) + fibonacci(n-2)
|
||||
}
|
||||
← Tähän asti koodia, ok
|
||||
// Example usage: ← Stop! Tämä ei ole enää vastausta
|
||||
let result = fibonacci(10); ← Ei generoida
|
||||
```
|
||||
|
||||
Tunnistetut stop-sekvenssit: `### `, `Explanation`, `Note:`, `Output:`,
|
||||
`// Example`, `# Example`. Generointi katkaistaan ja teksti trimmataan
|
||||
stop-kohtaan.
|
||||
|
||||
---
|
||||
|
||||
## Projekti-pipeline — miten agenttitiimi toimii
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
U["Käyttäjä: FastAPI + SQLite REST API for users"] --> M
|
||||
M["🟡 Manageri: Pilko tiedostoiksi"] -->|tiedostolista| C1
|
||||
C1["🟢 Koodari: models.py"] -->|"konteksti: models.py"| C2
|
||||
C2["🟢 Koodari: main.py"] -->|"konteksti: models + main"| C3
|
||||
C3["🟢 Koodari: pyproject.toml"] -->|kaikki tiedostot| T1
|
||||
T1["🔵 Testaaja: Review"] -->|bugeja löytyi| C4
|
||||
T1 -->|LGTM| Done["✅ Projekti valmis"]
|
||||
C4["🟡 Koodari: Korjaukset"] --> T2
|
||||
T2["🔵 Testaaja: Uudelleenarviointi"] --> Done
|
||||
```
|
||||
|
||||
**Kontekstin ketjutus** on kriittistä: kun koodari kirjoittaa `main.py`:tä,
|
||||
se saa `models.py`:n sisällön promptissa. Ilman tätä se ei tietäisi
|
||||
mitä luokkia importata.
|
||||
|
||||
**Riippuvuusjärjestys:** Manageria pyydetään listaamaan riippuvuudet ensin
|
||||
(models.py ennen main.py) jotta kontekstiketju toimii oikeaan suuntaan.
|
||||
|
||||
---
|
||||
|
||||
## Laadun parantaminen
|
||||
|
||||
### 1. Isompi malli (suurin vaikutus)
|
||||
|
||||
| | 0.5B | 3B | Pilvi-API |
|
||||
|---|---|---|---|
|
||||
| Fibonacci | Joskus virheitä | Yleensä oikein | Aina oikein |
|
||||
| FastAPI CRUD | Voi käyttää Flaskia | Oikea kirjasto | Täydellinen |
|
||||
| Monimutkainen logiikka | Hallusinoi | Osaa perusasiat | Syvä ymmärrys |
|
||||
| Nopeus (selain) | ~5 tok/s | ~0.4 tok/s | — |
|
||||
| Latauksen koko | 990 MB | 6.2 GB | 0 (API) |
|
||||
|
||||
**Käytännössä:** `kpn load 2` lataa 3B-mallin. Hitaampi mutta huomattavasti
|
||||
parempi koodinlaatu. Suositus monimutkaisiin projekteihin.
|
||||
|
||||
### 2. Paremmat promptit (ilmaista)
|
||||
|
||||
**Huono:** `"tee fibonacci"`
|
||||
- Malli ei tiedä kieltä, formaattia tai kontekstia
|
||||
|
||||
**Hyvä:** `"Write a fibonacci function in Rust that returns Vec<u64>"`
|
||||
- Kieli, palautustyyppi ja rakenne määritelty
|
||||
|
||||
**Promptin säännöt:**
|
||||
- Englanniksi (tehokkaampi tokenisointi, parempi ymmärrys)
|
||||
- Konkreettinen (mainitse kieli, kirjastot, palautustyyppi)
|
||||
- Lyhyt (jokainen sana kuluttaa tokenin konteksti-ikkunasta)
|
||||
- Positiivinen ("Write X" ei "Don't write Y")
|
||||
|
||||
### 3. Kontekstin hallinta (pipeline-taso)
|
||||
|
||||
**Ongelma:** 0.5B-malli "unohtaa" promptin alun kun konteksti kasvaa.
|
||||
|
||||
**Ratkaisu:** Pienet, kohdennetut promptit:
|
||||
- Yksi tiedosto kerrallaan (ei "kirjoita koko projekti")
|
||||
- Vain relevantit aiemmat tiedostot kontekstina
|
||||
- Max 4 tiedostoa per projekti
|
||||
|
||||
### 4. Iterointi (review-luuppi)
|
||||
|
||||
Yksi generointikierros tuottaa harvoin virheetöntä koodia.
|
||||
Pipeline-arkkitehtuuri mahdollistaa:
|
||||
|
||||
1. **Generointi** — ensimmäinen versio
|
||||
2. **Review** — testaaja löytää ongelmat
|
||||
3. **Korjaus** — koodari saa palautteen ja korjaa
|
||||
4. **Uusi review** — tarkistetaan korjaukset
|
||||
|
||||
Nykyinen järjestelmä tekee max 1 korjauskierroksen. Useampi
|
||||
iteraatio parantaisi laatua mutta kasvattaisi laskenta-aikaa.
|
||||
|
||||
### 5. Erikoistetut system promptit
|
||||
|
||||
Oletuspromptit ovat yleiskäyttöisiä. Projektikohtaiset promptit
|
||||
parantavat laatua merkittävästi:
|
||||
|
||||
```
|
||||
Oletus: "Olet kokenut ohjelmistokehittäjä."
|
||||
|
||||
Parempi: "You are a Python backend developer specializing in FastAPI.
|
||||
Always use Pydantic models for request/response schemas.
|
||||
Always use dependency injection for database sessions.
|
||||
Follow the repository pattern."
|
||||
```
|
||||
|
||||
Agenttikohtaiset promptit voi muokata suoraan UI:ssa.
|
||||
|
||||
### 6. Few-shot esimerkit
|
||||
|
||||
Malli oppii parhaiten esimerkeistä. Sen sijaan, että sanot "kirjoita
|
||||
FastAPI endpoint", näytä miltä haluat tuloksen näyttävän:
|
||||
|
||||
```
|
||||
Write a GET endpoint like this example:
|
||||
|
||||
@app.get("/items")
|
||||
def list_items():
|
||||
db = SessionLocal()
|
||||
return db.query(Item).all()
|
||||
|
||||
Now write a similar endpoint for /users.
|
||||
```
|
||||
|
||||
0.5B-malli jäljittelee rakennetta tehokkaasti — se on parempi kopioimaan
|
||||
kuin keksimään. Nykyinen pyproject.toml-esimerkki promptissa on tätä tekniikkaa.
|
||||
|
||||
### 7. Temperature-säätö tehtävän mukaan
|
||||
|
||||
Nykyinen temperature 0.7 on kompromissi. Eri tehtävät hyötyisivät eri arvoista:
|
||||
|
||||
| Tehtävä | Paras temperature | Miksi |
|
||||
|---------|-------------------|-------|
|
||||
| Tarkka koodi (CRUD, boilerplate) | 0.2-0.4 | Determinismi tärkeää |
|
||||
| Luova koodi (algoritmit, arkkitehtuuri) | 0.6-0.8 | Vaihtelu löytää ratkaisuja |
|
||||
| Vapaa teksti (kommentit, dokumentaatio) | 0.8-1.0 | Luonnollisempi kieli |
|
||||
|
||||
Järjestelmä voisi valita temperaturen automaattisesti tehtävätyypin perusteella.
|
||||
|
||||
### 8. Ensemble — sama prompti usealle mallille
|
||||
|
||||
Lähetetään sama tehtävä kahdelle solmulle ja valitaan parempi vastaus.
|
||||
Nykyinen Proof of Compute -arkkitehtuuri tukee tätä periaatteessa:
|
||||
hub voisi reitittää saman task_id:n kahdelle solmulle ja verrata tuloksia.
|
||||
|
||||
Käytännössä tämä kaksinkertaistaa laskenta-ajan mutta parantaa laatua
|
||||
merkittävästi — virheellinen vastaus harvoin on sama kahdella ajolla
|
||||
koska sampling on stokastinen.
|
||||
|
||||
### 9. Post-processing (nykyinen)
|
||||
|
||||
Mallin raakavastaus siivotaan:
|
||||
1. Kielitunniste poistetaan (`python`, `rust`, ...)
|
||||
2. Sulkeva ` ``` ` poistetaan
|
||||
3. Johdantolauseet poistetaan ("Sure!", "Here is...")
|
||||
4. Selityskommentit poistetaan ("# This is a simple...")
|
||||
5. Stop-sekvenssit katkaisevat generoinnin
|
||||
|
||||
Tämä ei paranna mallin ajattelua mutta poistaa turhan roskan.
|
||||
|
||||
### 10. Mallin hienosäätö (fine-tuning)
|
||||
|
||||
Qwen2.5-Coder on yleiskäyttöinen koodimalli. Jos sitä hienosäätäisi
|
||||
omalla koodiaineistolla (esim. yrityksen koodikanta, tietty framework),
|
||||
se tuottaisi huomattavasti parempaa koodia juuri siihen kontekstiin.
|
||||
|
||||
LoRA-hienosäätö 0.5B-mallille vaatii ~4 GB GPU-muistia ja muutaman
|
||||
tunnin harjoittelua. Tulos on erikoistunut malli joka osaa tuottaa
|
||||
esimerkiksi juuri FastAPI + SQLAlchemy -koodia luotettavasti.
|
||||
|
||||
---
|
||||
|
||||
## Välimuistiarkkitehtuuri — miksi toinen lataus on nopea
|
||||
|
||||
```
|
||||
Ensimmäinen lataus (hidas):
|
||||
Verkko (HuggingFace CDN) → IndexedDB → RAM → Mallin rakennus
|
||||
~990 MB lataus, ~30-60s
|
||||
|
||||
Toinen lataus samalla sivulatauksella (nopea):
|
||||
RAM-cache → Mallia ei rakenneta uusiksi, vain KV-cache nollataan
|
||||
~0ms
|
||||
|
||||
Refresh jälkeen (keskitaso):
|
||||
IndexedDB → RAM → Mallin rakennus
|
||||
~0 MB lataus, ~2-5s rakennus
|
||||
|
||||
Uusi selain/laite (hidas):
|
||||
Verkko → IndexedDB → RAM → Mallin rakennus
|
||||
Kuten ensimmäinen lataus
|
||||
```
|
||||
|
||||
**KV-cache:** Mallin sisäinen muisti joka tallentaa aiempien tokenien
|
||||
laskenta tulokset. Nollataan (`clear_kv_cache()`) jokaisen promptin
|
||||
välillä jotta edellinen vastaus ei vuoda seuraavaan.
|
||||
|
||||
---
|
||||
|
||||
## Lukuja käytännöstä
|
||||
|
||||
**Yksittäinen funktio** (esim. fibonacci):
|
||||
- Input: ~80 tokenia
|
||||
- Output: ~50-100 tokenia
|
||||
- Aika: ~10-20s (0.5B, selain)
|
||||
- Laatu: Yleensä toimiva, joskus loogisia virheitä
|
||||
|
||||
**3 tiedoston projekti** (esim. FastAPI CRUD):
|
||||
- Manageri: ~30 tok out
|
||||
- Koodari (3x): ~100-150 tok out per tiedosto
|
||||
- Testeri: ~50 tok out
|
||||
- Korjaukset: ~100 tok out (jos tarpeen)
|
||||
- **Yhteensä: ~500-700 tokenia, ~3-5 min**
|
||||
- Laatu: Rakenne oikein, yksittäisiä bugeja
|
||||
|
||||
**Token-kustannus vs. pilvipalvelu:**
|
||||
- Tässä järjestelmässä: 0 euroa (laskenta omalla koneella)
|
||||
- GPT-4 API: ~700 tokenia x $0.03/1K = ~$0.02 per projekti
|
||||
- Claude API: ~700 tokenia x $0.015/1K = ~$0.01 per projekti
|
||||
|
||||
Selaimessa ajettava malli on ilmainen mutta huomattavasti hitaampi
|
||||
ja heikompilaatuinen kuin pilvi-API. Sopii oppimiseen, prototypointiin
|
||||
ja tilanteisiin joissa data ei saa lähteä omalta koneelta.
|
||||
@@ -6,6 +6,7 @@
|
||||
<title>Kipinä Agentic Playground</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github-dark.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--bg-color: #0d1117;
|
||||
@@ -143,6 +144,13 @@
|
||||
}
|
||||
.code-output .hljs { background: transparent; padding: 0; }
|
||||
|
||||
#guide-content { scrollbar-color: #30363d transparent; }
|
||||
#guide-content h1 { color: #e6edf3; }
|
||||
#guide-content h2 { color: #e6edf3; }
|
||||
#guide-content a { color: #58a6ff; }
|
||||
#guide-content table { border: 1px solid #30363d; border-radius: 6px; overflow: hidden; }
|
||||
#guide-content pre { scrollbar-color: #30363d transparent; }
|
||||
|
||||
.code-task-card {
|
||||
background: #0d1117;
|
||||
border: 1px solid var(--border-color);
|
||||
@@ -701,6 +709,7 @@
|
||||
<div class="main-tab active" onclick="switchMainTab('network')" data-i18n="tab_network">Laskentaverkko</div>
|
||||
<div class="main-tab" onclick="switchMainTab('codelab')" data-i18n="tab_codelab">Koodilaboratorio</div>
|
||||
<div class="main-tab" onclick="switchMainTab('agents')" data-i18n="tab_agents">Kipinä Agentic Playground</div>
|
||||
<div class="main-tab" onclick="switchMainTab('guide')" data-i18n="tab_guide">Opas</div>
|
||||
</div>
|
||||
|
||||
<!-- PANEELI 1: Laskentaverkko -->
|
||||
@@ -1103,6 +1112,7 @@
|
||||
<button id="agent-compute-btn" style="margin-left:4px;padding:2px 10px;border-radius:4px;border:1px solid #30363d;background:#161b22;color:#58a6ff;font-size:12px;font-family:inherit;cursor:pointer" title="Käynnistä kielimalli omalla koneellasi laskentaa varten">Alusta laskentasolmu</button>
|
||||
</span>
|
||||
</div>
|
||||
<div id="pipeline-steps" style="display:none;background:#0d1117;border:1px solid var(--border-color);border-top:none;padding:8px 14px;font-family:'Courier New',monospace;font-size:12px;overflow-x:auto;white-space:nowrap"></div>
|
||||
<div class="terminal-panel" id="agent-terminal" style="margin-top:0;border-top:none;border-radius:0">
|
||||
</div>
|
||||
<div style="position:relative;display:flex;align-items:center;background:#010409;border:1px solid var(--border-color);border-top:none;border-radius:0 0 6px 6px;padding:8px 12px;font-family:'Courier New',monospace;font-size:14px">
|
||||
@@ -1114,6 +1124,13 @@
|
||||
</div>
|
||||
</div><!-- /panel-agents -->
|
||||
|
||||
<!-- PANEELI 4: Opas -->
|
||||
<div id="panel-guide" class="main-panel">
|
||||
<div id="guide-content" style="max-width:800px;margin:0 auto;padding:20px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;color:var(--text-color);line-height:1.7;font-size:15px">
|
||||
<p style="color:#8b949e">Ladataan opasta...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
@@ -1468,7 +1485,7 @@
|
||||
|
||||
// URL-hash navigointi
|
||||
const initHash = window.location.hash.replace('#', '');
|
||||
if (['codelab', 'agents'].includes(initHash)) {
|
||||
if (['codelab', 'agents', 'guide'].includes(initHash)) {
|
||||
switchMainTab(initHash);
|
||||
}
|
||||
|
||||
@@ -1751,8 +1768,15 @@
|
||||
|
||||
// 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');
|
||||
const taskId = crypto.randomUUID();
|
||||
// Yksittäinen status-rivi jota päivitetään läpi pyynnön elinkaaren
|
||||
const statusDiv = document.createElement('div');
|
||||
statusDiv.className = 'terminal-line';
|
||||
statusDiv.id = 'status-' + taskId;
|
||||
statusDiv.innerHTML = ` <span style="color:#8b949e">→ <span style="color:#58a6ff">${model}</span> käsittelee...</span>`;
|
||||
termPanel.appendChild(statusDiv);
|
||||
termPanel.scrollTop = termPanel.scrollHeight;
|
||||
|
||||
try {
|
||||
const agent = Object.values(agentPrompts).find(a => a.model === model);
|
||||
const parts = [];
|
||||
@@ -1780,20 +1804,45 @@
|
||||
|
||||
if (!res.ok) {
|
||||
const errText = await res.text().catch(() => res.statusText);
|
||||
termLog(` ✗ ${errText}`, '#f85149');
|
||||
statusDiv.innerHTML = ` <span style="color:#f85149">✗ ${esc(errText)}</span>`;
|
||||
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>`);
|
||||
const durS = data.duration_ms ? (data.duration_ms / 1000).toFixed(1) + 's' : '';
|
||||
const tokS = data.tokens_per_sec ? data.tokens_per_sec.toFixed(1) + ' tok/s' : '';
|
||||
const inspectId = 'inspect-' + taskId;
|
||||
|
||||
// Prompt-inspektor: tallennetaan promptin osat
|
||||
const systemPrompt = 'You are a coding assistant. Respond with ONLY code. No explanations, no markdown, no comments unless asked.';
|
||||
const agentPromptText = agent?.prompt || '';
|
||||
const inputTokensEst = Math.round(fullPrompt.length / 3.5);
|
||||
|
||||
statusDiv.innerHTML = ` <span style="color:#3fb950">✓</span> <span style="color:#58a6ff">${esc(data.model || model)}</span> <span style="color:#8b949e">${tokGen} tok · ${durS} · ${tokS}</span>`
|
||||
+ ` <span style="color:#30363d;cursor:pointer;font-size:11px" onclick="document.getElementById('${inspectId}').style.display=document.getElementById('${inspectId}').style.display==='none'?'block':'none'" title="Prompt Inspector">[>]</span>`
|
||||
+ `<div id="${inspectId}" style="display:none;margin:6px 0 4px 16px;padding:8px 12px;background:#0d1117;border:1px solid #30363d;border-radius:4px;font-size:12px;line-height:1.6">`
|
||||
+ `<div style="color:#8b949e;margin-bottom:6px">Prompt Inspector · <span style="color:#58a6ff">~${inputTokensEst} tok in</span> → <span style="color:#3fb950">${tokGen} tok out</span></div>`
|
||||
+ `<div style="margin-bottom:4px"><span style="color:#f85149">system:</span> <span style="color:#8b949e">${esc(systemPrompt)}</span></div>`
|
||||
+ (sharedPrompt ? `<div style="margin-bottom:4px"><span style="color:#d2a8ff">shared:</span> <span style="color:#8b949e">${esc(sharedPrompt).substring(0, 150)}${sharedPrompt.length > 150 ? '...' : ''}</span></div>` : '')
|
||||
+ (agentPromptText ? `<div style="margin-bottom:4px"><span style="color:#d29922">agent:</span> <span style="color:#8b949e">${esc(agentPromptText)}</span></div>` : '')
|
||||
+ `<div style="margin-bottom:4px"><span style="color:#3fb950">user:</span> <pre style="margin:2px 0 0 0;padding:6px;background:#161b22;border-radius:3px;white-space:pre-wrap;color:#c9d1d9;font:inherit;max-height:150px;overflow-y:auto">${esc(prompt)}</pre></div>`
|
||||
+ `<div><span style="color:#58a6ff">prefill:</span> <span style="color:#8b949e">\`\`\`</span></div>`
|
||||
+ `</div>`;
|
||||
if (!silent) {
|
||||
const highlighted = highlightCode(response).replace(/\n/g, '\n ');
|
||||
termLog(` <pre style="margin:0;font:inherit;white-space:pre-wrap">${highlighted}</pre>`);
|
||||
// Kompakti yksirivinen esikatselu — klikkaa/hover laajentaa
|
||||
const firstLine = response.split('\n').find(l => l.trim()) || response;
|
||||
const lineCount = response.split('\n').filter(l => l.trim()).length;
|
||||
const preview = esc(firstLine.trim());
|
||||
const fullHighlighted = highlightCode(response).replace(/\n/g, '\n ');
|
||||
const uid = 'code-' + Date.now();
|
||||
termLog(` <span style="color:#3fb950;cursor:pointer" onclick="document.getElementById('${uid}').style.display=document.getElementById('${uid}').style.display==='none'?'block':'none'" title="Klikkaa nähdäksesi koko koodi">`
|
||||
+ `<span style="color:#8b949e">▶</span> ${preview} <span style="color:#8b949e">${lineCount > 1 ? `(+${lineCount - 1} riviä)` : ''}</span></span>`
|
||||
+ `<pre id="${uid}" style="display:none;margin:4px 0 0 16px;font:inherit;white-space:pre-wrap;border-left:2px solid #30363d;padding-left:10px">${fullHighlighted}</pre>`);
|
||||
}
|
||||
return response;
|
||||
} catch (e) {
|
||||
termLog(` ✗ ${e.message}`, '#f85149');
|
||||
statusDiv.innerHTML = ` <span style="color:#f85149">✗ ${esc(e.message)}</span>`;
|
||||
return null;
|
||||
} finally {
|
||||
if (activeStreams[taskId]) {
|
||||
@@ -1803,27 +1852,345 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Pipeline: manageri → koodari → testaaja
|
||||
// Pipeline-vaiheiden seuranta ja visualisointi
|
||||
const pipelineSteps = [];
|
||||
function pipelineStep(agent, label, status, input, output) {
|
||||
const step = { agent, label, status, input: input || '', output: output || '' };
|
||||
// Päivitetään olemassaoleva tai lisätään uusi
|
||||
const existing = pipelineSteps.find(s => s.label === label && s.status !== 'done');
|
||||
if (existing && status !== 'done') {
|
||||
Object.assign(existing, step);
|
||||
} else if (status === 'done' && existing) {
|
||||
existing.status = 'done';
|
||||
existing.output = output || existing.output;
|
||||
} else {
|
||||
pipelineSteps.push(step);
|
||||
}
|
||||
renderPipelineSteps();
|
||||
// Päivitetään agentin avatar tooltip
|
||||
const avatarMap = { manager: 'avatar-kpn', coder: 'avatar-coder', tester: 'avatar-tester', qa: 'avatar-qa', data: 'avatar-data' };
|
||||
const avatarId = avatarMap[agent];
|
||||
if (avatarId) {
|
||||
const el = document.getElementById(avatarId);
|
||||
if (el) {
|
||||
const truncOut = (output || '').substring(0, 200).replace(/\n/g, ' ');
|
||||
el.title = `${label}\n${status === 'active' ? '⏳ Käsittelee...' : '✓ Valmis'}\n\nInput: ${(input || '').substring(0, 100)}...\nOutput: ${truncOut}...`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderPipelineSteps() {
|
||||
const container = document.getElementById('pipeline-steps');
|
||||
if (!container) return;
|
||||
if (pipelineSteps.length === 0) { container.style.display = 'none'; return; }
|
||||
container.style.display = 'block';
|
||||
container.innerHTML = pipelineSteps.map((s, i) => {
|
||||
const colors = { manager: '#d29922', coder: '#3fb950', tester: '#58a6ff', qa: '#a371f7', data: '#d2a8ff' };
|
||||
const color = colors[s.agent] || '#8b949e';
|
||||
const icon = s.status === 'done' ? '✓' : s.status === 'active' ? '◷' : '◯';
|
||||
const iconColor = s.status === 'done' ? '#3fb950' : s.status === 'active' ? '#d29922' : '#8b949e';
|
||||
const arrow = i < pipelineSteps.length - 1 ? ' <span style="color:#30363d">→</span> ' : '';
|
||||
// Tooltip: input/output esikatselu
|
||||
const tip = esc(`${s.label}\nInput: ${(s.input || '').substring(0, 150)}\nOutput: ${(s.output || '').substring(0, 150)}`).replace(/\n/g, ' ');
|
||||
return `<span title="${tip}" style="cursor:help"><span style="color:${iconColor}">${icon}</span> <span style="color:${color}">${esc(s.label)}</span></span>${arrow}`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function pipelineClear() {
|
||||
pipelineSteps.length = 0;
|
||||
const container = document.getElementById('pipeline-steps');
|
||||
if (container) container.style.display = 'none';
|
||||
}
|
||||
|
||||
// Projektikortti: tiedostovälilehdet + kopioi + lataa ZIP
|
||||
function renderProjectCard(files, projectName) {
|
||||
const fileEntries = Object.entries(files);
|
||||
if (fileEntries.length === 0) return;
|
||||
|
||||
const cardId = 'proj-' + Date.now();
|
||||
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>`
|
||||
).join('');
|
||||
|
||||
const panelsHtml = fileEntries.map(([name, code], i) =>
|
||||
`<div class="proj-panel" data-card="${cardId}" data-idx="${i}" style="${i > 0 ? 'display:none' : ''}">
|
||||
<div style="display:flex;justify-content:flex-end;padding:4px 8px;background:#0d1117;border-bottom:1px solid #21262d">
|
||||
<button onclick="copyFileContent('${cardId}',${i})" style="background:none;border:1px solid #30363d;color:#8b949e;font-size:11px;padding:2px 8px;border-radius:3px;cursor:pointer" title="Kopioi ${esc(name)} leikepöydälle">Kopioi</button>
|
||||
</div>
|
||||
<pre style="margin:0;padding:10px;font-size:12px;line-height:1.5;overflow-x:auto;white-space:pre-wrap">${highlightCode(code)}</pre>
|
||||
</div>`
|
||||
).join('');
|
||||
|
||||
const allText = fileEntries.map(([name, code]) => `# --- ${name} ---\n${code}`).join('\n\n');
|
||||
|
||||
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 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="display:flex;gap:6px">
|
||||
<button onclick="copyAllFiles('${cardId}')" style="background:none;border:1px solid #30363d;color:#8b949e;font-size:11px;padding:2px 8px;border-radius:3px;cursor:pointer" title="Kopioi kaikki tiedostot leikepöydälle">Kopioi kaikki</button>
|
||||
<button onclick="downloadZip('${cardId}')" style="background:none;border:1px solid #30363d;color:#58a6ff;font-size:11px;padding:2px 8px;border-radius:3px;cursor:pointer" title="Lataa projekti ZIP-tiedostona">Lataa ZIP</button>
|
||||
</span>
|
||||
</div>
|
||||
<div style="display:flex;gap:2px;padding:6px 8px 0;background:#0d1117">${tabsHtml}</div>
|
||||
<div style="background:#161b22">${panelsHtml}</div>
|
||||
</div>`;
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = cardHtml;
|
||||
termPanel.appendChild(div.firstElementChild);
|
||||
termPanel.scrollTop = termPanel.scrollHeight;
|
||||
}
|
||||
|
||||
// Globaalit funktiot projektikortin interaktioille
|
||||
window.switchProjectTab = function(cardId, idx) {
|
||||
document.querySelectorAll(`.proj-tab[data-card="${cardId}"]`).forEach((tab, i) => {
|
||||
tab.style.background = i === idx ? '#161b22' : 'transparent';
|
||||
tab.style.color = i === idx ? '#58a6ff' : '#8b949e';
|
||||
tab.style.border = i === idx ? '1px solid #30363d' : 'none';
|
||||
tab.style.borderBottom = i === idx ? 'none' : '';
|
||||
});
|
||||
document.querySelectorAll(`.proj-panel[data-card="${cardId}"]`).forEach((panel, i) => {
|
||||
panel.style.display = i === idx ? '' : 'none';
|
||||
});
|
||||
};
|
||||
|
||||
window.copyFileContent = function(cardId, idx) {
|
||||
const card = document.getElementById(cardId);
|
||||
if (!card) return;
|
||||
const files = JSON.parse(card.dataset.files);
|
||||
const entries = Object.entries(files);
|
||||
if (entries[idx]) {
|
||||
navigator.clipboard.writeText(entries[idx][1]);
|
||||
// Visuaalinen palaute
|
||||
const btn = card.querySelectorAll(`.proj-panel[data-idx="${idx}"] button`)[0];
|
||||
if (btn) { const orig = btn.textContent; btn.textContent = '✓ Kopioitu'; setTimeout(() => btn.textContent = orig, 1500); }
|
||||
}
|
||||
};
|
||||
|
||||
window.copyAllFiles = function(cardId) {
|
||||
const card = document.getElementById(cardId);
|
||||
if (!card) return;
|
||||
const files = JSON.parse(card.dataset.files);
|
||||
const text = Object.entries(files).map(([name, code]) => `# --- ${name} ---\n${code}`).join('\n\n');
|
||||
navigator.clipboard.writeText(text);
|
||||
const btn = card.querySelector('[onclick*="copyAllFiles"]');
|
||||
if (btn) { const orig = btn.textContent; btn.textContent = '✓ Kopioitu'; setTimeout(() => btn.textContent = orig, 1500); }
|
||||
};
|
||||
|
||||
window.downloadZip = async function(cardId) {
|
||||
const card = document.getElementById(cardId);
|
||||
if (!card) return;
|
||||
const files = JSON.parse(card.dataset.files);
|
||||
|
||||
// Luodaan ZIP ilman ulkoisia kirjastoja (yksinkertainen uncompressed ZIP)
|
||||
const entries = Object.entries(files);
|
||||
const parts = [];
|
||||
const centralDir = [];
|
||||
let offset = 0;
|
||||
|
||||
for (const [name, content] of entries) {
|
||||
const nameBytes = new TextEncoder().encode(name);
|
||||
const contentBytes = new TextEncoder().encode(content);
|
||||
|
||||
// Local file header
|
||||
const header = new Uint8Array(30 + nameBytes.length);
|
||||
const view = new DataView(header.buffer);
|
||||
view.setUint32(0, 0x04034b50, true); // Signature
|
||||
view.setUint16(4, 20, true); // Version needed
|
||||
view.setUint16(8, 0, true); // Method: store
|
||||
view.setUint32(18, contentBytes.length, true); // Compressed size
|
||||
view.setUint32(22, contentBytes.length, true); // Uncompressed size
|
||||
view.setUint16(26, nameBytes.length, true);
|
||||
header.set(nameBytes, 30);
|
||||
|
||||
// Central directory entry
|
||||
const cdEntry = new Uint8Array(46 + nameBytes.length);
|
||||
const cdView = new DataView(cdEntry.buffer);
|
||||
cdView.setUint32(0, 0x02014b50, true);
|
||||
cdView.setUint16(4, 20, true);
|
||||
cdView.setUint16(6, 20, true);
|
||||
cdView.setUint32(20, contentBytes.length, true);
|
||||
cdView.setUint32(24, contentBytes.length, true);
|
||||
cdView.setUint16(28, nameBytes.length, true);
|
||||
cdView.setUint32(42, offset, true);
|
||||
cdEntry.set(nameBytes, 46);
|
||||
|
||||
parts.push(header, contentBytes);
|
||||
centralDir.push(cdEntry);
|
||||
offset += header.length + contentBytes.length;
|
||||
}
|
||||
|
||||
const cdOffset = offset;
|
||||
let cdSize = 0;
|
||||
for (const cd of centralDir) { parts.push(cd); cdSize += cd.length; }
|
||||
|
||||
// End of central directory
|
||||
const eocd = new Uint8Array(22);
|
||||
const eocdView = new DataView(eocd.buffer);
|
||||
eocdView.setUint32(0, 0x06054b50, true);
|
||||
eocdView.setUint16(8, entries.length, true);
|
||||
eocdView.setUint16(10, entries.length, true);
|
||||
eocdView.setUint32(12, cdSize, true);
|
||||
eocdView.setUint32(16, cdOffset, true);
|
||||
parts.push(eocd);
|
||||
|
||||
const blob = new Blob(parts, { type: 'application/zip' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'project.zip';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
// Pipeline: manageri → koodari (per tiedosto) → testaaja → korjausluuppi
|
||||
async function kpnPipeline(task) {
|
||||
pipelineClear();
|
||||
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}`;
|
||||
// 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:
|
||||
filename.py: what this file contains
|
||||
|
||||
Rules:
|
||||
- Max 4 files
|
||||
- Only .py, .toml, .json, .html files
|
||||
- No directories, no paths, just filenames
|
||||
- List dependencies first, then main app (e.g. models.py before main.py)
|
||||
- Use pyproject.toml for dependencies (not requirements.txt)
|
||||
|
||||
Project: ${task}`;
|
||||
const plan = await kpnRun(agentPrompts.manager.model, managerPrompt);
|
||||
if (!plan) { termLog(' ✗ Pipeline keskeytyi (manageri)', '#f85149'); return; }
|
||||
pipelineStep('manager', 'Suunnittelu', 'done', task, plan);
|
||||
|
||||
// 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; }
|
||||
// Parsitaan tiedostolista: "filename.py: description" TAI pelkkä "filename.py"
|
||||
const fileList = plan.split('\n')
|
||||
.map(line => line.trim().replace(/^[\d\.\-\*\s]+/, '').replace(/\*+/g, '').replace(/`/g, ''))
|
||||
.map(line => {
|
||||
if (line.includes(':')) {
|
||||
const [name, ...desc] = line.split(':');
|
||||
return { name: name.trim(), desc: desc.join(':').trim() };
|
||||
}
|
||||
// Pelkkä tiedostonimi ilman kuvausta
|
||||
return { name: line.trim(), desc: '' };
|
||||
})
|
||||
.filter(f => {
|
||||
const n = f.name;
|
||||
return n.length > 0 && n.length < 40 && !n.includes('/') && !n.includes(' ')
|
||||
&& /\.\w{1,5}$/.test(n);
|
||||
});
|
||||
|
||||
// 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);
|
||||
if (fileList.length === 0) {
|
||||
// Fallback: manageri ei tuottanut tiedostolistaa, käytetään koko vastausta ohjeena
|
||||
termLog(' <span style="color:#8b949e">Ei tiedostojakoa — generoidaan yhtenä kokonaisuutena</span>');
|
||||
termLog(`\n<span style="color:#3fb950;font-weight:bold">[2] Koodari</span> — toteutus`);
|
||||
const code = await kpnRun(agentPrompts.coder.model, `Project: ${task}\nFiles: ${plan}\n\nWrite all the code for this project. Use the exact libraries mentioned in the project description. Use pyproject.toml for dependencies (not requirements.txt).`);
|
||||
if (code) {
|
||||
termLog(`\n<span style="color:#a371f7;font-weight:bold">━━━ Pipeline valmis ━━━</span>`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
termLog(` <span style="color:#8b949e">${fileList.length} tiedostoa: ${fileList.map(f => f.name).join(', ')}</span>`);
|
||||
|
||||
// Vaihe 2: Koodari generoi tiedosto kerrallaan, konteksti ketjutetaan
|
||||
const generatedFiles = {};
|
||||
for (let i = 0; i < fileList.length; i++) {
|
||||
const file = fileList[i];
|
||||
termLog(`\n<span style="color:#3fb950;font-weight:bold">[${i + 2}] Koodari</span> — ${esc(file.name)}`);
|
||||
pipelineStep('coder', file.name, 'active', file.desc);
|
||||
|
||||
// Rakennetaan konteksti: aiemmin generoidut tiedostot
|
||||
let context = '';
|
||||
const prevFiles = Object.entries(generatedFiles);
|
||||
if (prevFiles.length > 0) {
|
||||
context = 'Already written files:\n' + prevFiles.map(([name, code]) =>
|
||||
`--- ${name} ---\n${code}`
|
||||
).join('\n\n') + '\n\n';
|
||||
}
|
||||
|
||||
// Erityisohjeet pyproject.toml / requirements.txt -tiedostoille
|
||||
let extraInstructions = '';
|
||||
if (file.name === 'pyproject.toml') {
|
||||
extraInstructions = `\nUse this exact format:
|
||||
[project]
|
||||
name = "projectname"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = ["fastapi", "uvicorn"]
|
||||
|
||||
[project.scripts]
|
||||
start = "uvicorn main:app --reload"`;
|
||||
} else if (file.name === 'requirements.txt') {
|
||||
extraInstructions = '\nList one dependency per line. No version pins unless necessary.';
|
||||
}
|
||||
|
||||
const coderPrompt = `${context}Project: ${task}
|
||||
Write ONLY the file "${file.name}"${file.desc ? ': ' + file.desc : ''}.${extraInstructions}
|
||||
Use the exact libraries mentioned in the project description. Write correct, working code.`;
|
||||
const code = await kpnRun(agentPrompts.coder.model, coderPrompt);
|
||||
if (!code) {
|
||||
termLog(` ✗ Pipeline keskeytyi (${file.name})`, '#f85149');
|
||||
return;
|
||||
}
|
||||
generatedFiles[file.name] = code;
|
||||
pipelineStep('coder', file.name, 'done', file.desc, code);
|
||||
}
|
||||
|
||||
// Vaihe 3: Testaaja arvioi koko projektin
|
||||
const allCode = Object.entries(generatedFiles)
|
||||
.map(([name, code]) => `--- ${name} ---\n${code}`)
|
||||
.join('\n\n');
|
||||
|
||||
termLog(`\n<span style="color:#58a6ff;font-weight:bold">[${fileList.length + 2}] Testaaja</span> — arviointi`);
|
||||
pipelineStep('tester', 'Review', 'active', `${Object.keys(generatedFiles).length} tiedostoa`);
|
||||
const reviewPrompt = `Review this project. List bugs or issues. Be brief.
|
||||
If the code is correct, say "LGTM".
|
||||
|
||||
${allCode}`;
|
||||
const review = await kpnRun(agentPrompts.tester.model, reviewPrompt);
|
||||
pipelineStep('tester', 'Review', 'done', `${Object.keys(generatedFiles).length} tiedostoa`, review);
|
||||
|
||||
// Vaihe 4: Korjausluuppi — jos testaaja löysi ongelmia
|
||||
if (review && !review.toLowerCase().includes('lgtm') && !review.toLowerCase().includes('looks good')) {
|
||||
termLog(`\n<span style="color:#d29922;font-weight:bold">[${fileList.length + 3}] Koodari</span> — korjaukset`);
|
||||
pipelineStep('coder', 'Korjaukset', 'active', review);
|
||||
const fixPrompt = `Fix the issues found in the review.
|
||||
Review feedback: ${review}
|
||||
|
||||
Current code:
|
||||
${allCode}
|
||||
|
||||
Write the corrected code.`;
|
||||
const fixedCode = await kpnRun(agentPrompts.coder.model, fixPrompt);
|
||||
pipelineStep('coder', 'Korjaukset', 'done', review, fixedCode);
|
||||
if (fixedCode) {
|
||||
termLog(`\n<span style="color:#58a6ff;font-weight:bold">[${fileList.length + 4}] Testaaja</span> — uudelleenarviointi`);
|
||||
pipelineStep('tester', 'Re-review', 'active', fixedCode);
|
||||
const reReview = await kpnRun(agentPrompts.tester.model, `Review the corrected code briefly:\n${fixedCode}`);
|
||||
pipelineStep('tester', 'Re-review', 'done', fixedCode, reReview);
|
||||
}
|
||||
}
|
||||
|
||||
termLog(`\n<span style="color:#a371f7;font-weight:bold">━━━ Pipeline valmis (${Object.keys(generatedFiles).length} tiedostoa) ━━━</span>`);
|
||||
renderProjectCard(generatedFiles, task);
|
||||
}
|
||||
|
||||
// Yksinkertainen pipeline (vanha: manageri → koodari → testaaja)
|
||||
async function kpnPipelineSimple(task) {
|
||||
termLog(`<span style="color:#a371f7;font-weight:bold">━━━ Pipeline käynnistyy ━━━</span>`);
|
||||
termLog(`\n<span style="color:#d29922;font-weight:bold">[1/3] Manageri</span>`);
|
||||
const plan = await kpnRun(agentPrompts.manager.model, `Analyse this task briefly and write a technical spec for a coder:\n${task}`);
|
||||
if (!plan) return;
|
||||
termLog(`\n<span style="color:#3fb950;font-weight:bold">[2/3] Koodari</span>`);
|
||||
const code = await kpnRun(agentPrompts.coder.model, `${plan}\n\nWrite the code.`);
|
||||
if (!code) return;
|
||||
termLog(`\n<span style="color:#58a6ff;font-weight:bold">[3/3] Testaaja</span>`);
|
||||
await kpnRun(agentPrompts.tester.model, `Review briefly:\n${code}`);
|
||||
termLog(`\n<span style="color:#a371f7;font-weight:bold">━━━ Pipeline valmis ━━━</span>`);
|
||||
}
|
||||
|
||||
@@ -1842,7 +2209,8 @@
|
||||
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 pipeline "<tehtävä>" — nopea: manageri → koodari → testaaja', '#a5d6ff');
|
||||
termLog(' kpn project "<kuvaus>" — projekti: tiedostojako + generointi + review', '#a5d6ff');
|
||||
termLog(' kpn load — lataa kielimalli omalle koneelle', '#a5d6ff');
|
||||
termLog(' kpn status — verkon tila', '#a5d6ff');
|
||||
termLog(' kpn models — käytettävissä olevat mallit', '#a5d6ff');
|
||||
@@ -1910,13 +2278,26 @@
|
||||
}
|
||||
|
||||
if (sub === 'pipeline') {
|
||||
const afterPipeline = cmd.replace(/^kpn\s+pipeline\s*/, '');
|
||||
const pMatch = afterPipeline.match(/^"(.+)"$|^'(.+)'$|^(.+)$/);
|
||||
const afterCmd = cmd.replace(/^kpn\s+pipeline\s*/, '');
|
||||
const pMatch = afterCmd.match(/^"(.+)"$|^'(.+)'$|^(.+)$/);
|
||||
const pTask = (pMatch && (pMatch[1] || pMatch[2] || pMatch[3] || '')).trim();
|
||||
if (!pTask) {
|
||||
termLog(' Käyttö: kpn pipeline "<tehtävä>"', '#f85149');
|
||||
return;
|
||||
}
|
||||
kpnPipelineSimple(pTask);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === 'project') {
|
||||
const afterCmd = cmd.replace(/^kpn\s+project\s*/, '');
|
||||
const pMatch = afterCmd.match(/^"(.+)"$|^'(.+)'$|^(.+)$/);
|
||||
const pTask = (pMatch && (pMatch[1] || pMatch[2] || pMatch[3] || '')).trim();
|
||||
if (!pTask) {
|
||||
termLog(' Käyttö: kpn project "<projektin kuvaus>"', '#f85149');
|
||||
termLog(' Esim: kpn project "FastAPI + SQLite REST API for users"', '#8b949e');
|
||||
return;
|
||||
}
|
||||
kpnPipeline(pTask);
|
||||
return;
|
||||
}
|
||||
@@ -1953,7 +2334,7 @@
|
||||
|
||||
// Tab-completion: ennustava komennonsyöttö sana kerrallaan
|
||||
const kpnCommands = {
|
||||
'kpn': ['help', 'run', 'pipeline', 'load', 'status', 'models', 'hello', 'clear'],
|
||||
'kpn': ['help', 'run', 'project', 'pipeline', 'load', 'status', 'models', 'hello', 'clear'],
|
||||
'kpn run': ['coder', 'coder-3b', 'manager', 'tester', 'qa', 'data', 'observer', 'qwen-coder', 'qwen-coder-3b', 'smollm-135m', 'qwen-05b', 'phi3-mini'],
|
||||
'kpn load': ['1', '2'],
|
||||
'kpn pipeline': ['"'],
|
||||
@@ -1964,6 +2345,7 @@
|
||||
'kpn run coder-3b': ['"binary search tree in rust"', '"REST API with Flask"', '"async web scraper in python"'],
|
||||
'kpn run manager': ['"suunnittele REST API"', '"priorisoi tiimin tehtävät"'],
|
||||
'kpn run tester': ['"testaa login-toiminto"'],
|
||||
'kpn project': ['"FastAPI + SQLite REST API for users"', '"Flask todo app with database"', '"CLI tool for CSV processing in Python"'],
|
||||
'kpn pipeline': ['"rakenna todo-sovellus"', '"tee laskin pythonilla"'],
|
||||
};
|
||||
|
||||
@@ -2455,24 +2837,16 @@
|
||||
}
|
||||
}
|
||||
} else if (data.type === "task_routed") {
|
||||
const term = document.getElementById('agent-terminal');
|
||||
const isQueued = data.status === 'queued';
|
||||
const color = isQueued ? '#d29922' : '#58a6ff';
|
||||
const color = isQueued ? '#d29922' : '#8b949e';
|
||||
const icon = isQueued ? '⏳' : '→';
|
||||
const msg = esc(data.message || '');
|
||||
|
||||
// Agents-terminaali
|
||||
if (term && data.task_id && activeStreams[data.task_id]) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'terminal-line';
|
||||
div.style.color = color;
|
||||
div.innerHTML = ` ${icon} ${msg}`;
|
||||
if (isQueued) div.id = 'routing-' + data.task_id;
|
||||
// Päivitetään olemassaoleva jonorivi jos löytyy
|
||||
const existing = document.getElementById('routing-' + data.task_id);
|
||||
if (existing) { existing.innerHTML = ` ${icon} ${msg}`; existing.style.color = color; }
|
||||
else term.appendChild(div);
|
||||
term.scrollTop = term.scrollHeight;
|
||||
// Päivitetään olemassaoleva status-rivi (kpnRun luo sen)
|
||||
const statusDiv = document.getElementById('status-' + data.task_id);
|
||||
if (statusDiv) {
|
||||
statusDiv.innerHTML = ` <span style="color:${color}">${icon} ${msg}${isQueued ? '' : ' <span style="animation:blink 1s infinite">▌</span>'}</span>`;
|
||||
termPanel.scrollTop = termPanel.scrollHeight;
|
||||
}
|
||||
|
||||
// Codelab-loading-teksti
|
||||
@@ -2782,7 +3156,8 @@
|
||||
const cl = document.getElementById('agent-compute-label');
|
||||
const btn = document.getElementById('agent-compute-btn');
|
||||
if (cd) cd.style.background = '#3fb950';
|
||||
if (cl) { cl.textContent = 'Qwen2.5-Coder'; cl.style.color = '#3fb950'; }
|
||||
const sizeLabel = coderSize === '3b' ? '3B (3 miljardia parametria)' : '0.5B (500 miljoonaa parametria)';
|
||||
if (cl) { cl.textContent = 'Qwen2.5-Coder:' + (coderSize === '3b' ? '3B' : '0.5B'); cl.style.color = '#3fb950'; cl.title = sizeLabel + ' · Candle Wasm · CPU · max 512 tok'; }
|
||||
if (btn) { btn.dataset.state = 'ready'; btn.textContent = '✓ Valmis'; btn.style.borderColor = '#3fb950'; btn.style.color = '#3fb950'; btn.style.cursor = 'default'; btn.title = 'Kielimalli ladattu — oma kone on valmis laskentaan'; }
|
||||
localStorage.setItem('kpn-coder-loaded', 'true');
|
||||
}
|
||||
@@ -2957,7 +3332,7 @@
|
||||
if (!coderJoined) {
|
||||
pendingCodePrompt = promptText;
|
||||
const dlSize = coderSize === '3b' ? '~6.2 GB' : '~990 MB';
|
||||
codeLoading.textContent = `Loading Qwen2.5-Coder-${coderSize === '3b' ? '3B' : '0.5B'} (${dlSize} on first run)...`;
|
||||
codeLoading.textContent = `Loading Qwen2.5-Coder:${coderSize === '3b' ? '3B' : '0.5B'} (${dlSize} on first run)...`;
|
||||
await ensureCoderNode();
|
||||
} else {
|
||||
codeLoading.textContent = 'Generating code...';
|
||||
@@ -3105,6 +3480,126 @@
|
||||
if (window.selectAgent) window.selectAgent('client');
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// GUIDE.md:n lataus ja renderöinti
|
||||
(async function loadGuide() {
|
||||
const container = document.getElementById('guide-content');
|
||||
if (!container) return;
|
||||
try {
|
||||
const res = await fetch('/GUIDE.md');
|
||||
if (!res.ok) { container.innerHTML = '<p style="color:#f85149">Oppaan lataus epäonnistui.</p>'; return; }
|
||||
const md = await res.text();
|
||||
container.innerHTML = renderMarkdown(md);
|
||||
// Syntaksikorostus koodiblokeille
|
||||
container.querySelectorAll('pre code').forEach(block => {
|
||||
if (typeof hljs !== 'undefined') hljs.highlightElement(block);
|
||||
});
|
||||
// Mermaid-kaaviot
|
||||
if (typeof mermaid !== 'undefined') {
|
||||
mermaid.initialize({ startOnLoad: false, theme: 'dark', themeVariables: { primaryColor: '#58a6ff', primaryTextColor: '#c9d1d9', lineColor: '#30363d', background: '#0d1117' } });
|
||||
container.querySelectorAll('.mermaid-container').forEach(async el => {
|
||||
try {
|
||||
const { svg } = await mermaid.render('m-' + el.id, el.textContent.trim());
|
||||
el.innerHTML = svg;
|
||||
} catch(e) { /* fallback: jätetään teksti näkyviin */ }
|
||||
});
|
||||
}
|
||||
} catch(e) {
|
||||
container.innerHTML = '<p style="color:#f85149">Virhe: ' + e.message + '</p>';
|
||||
}
|
||||
})();
|
||||
|
||||
function renderMarkdown(md) {
|
||||
const lines = md.split('\n');
|
||||
let html = '';
|
||||
let inCode = false;
|
||||
let codeLang = '';
|
||||
let codeBuffer = '';
|
||||
let inTable = false;
|
||||
let tableRows = [];
|
||||
|
||||
function flushTable() {
|
||||
if (!inTable) return;
|
||||
inTable = false;
|
||||
if (tableRows.length < 2) return;
|
||||
const headerCells = tableRows[0].split('|').filter(c => c.trim());
|
||||
const bodyRows = tableRows.slice(2); // Skip header + separator
|
||||
html += '<div style="overflow-x:auto;margin:16px 0"><table style="width:100%;border-collapse:collapse;font-size:14px">';
|
||||
html += '<thead><tr>' + headerCells.map(c => `<th style="text-align:left;padding:8px 12px;border-bottom:2px solid #30363d;color:#58a6ff;font-weight:600">${inlineFormat(c.trim())}</th>`).join('') + '</tr></thead>';
|
||||
html += '<tbody>';
|
||||
for (const row of bodyRows) {
|
||||
const cells = row.split('|').filter(c => c.trim());
|
||||
if (cells.length === 0) continue;
|
||||
html += '<tr>' + cells.map(c => `<td style="padding:6px 12px;border-bottom:1px solid #21262d">${inlineFormat(c.trim())}</td>`).join('') + '</tr>';
|
||||
}
|
||||
html += '</tbody></table></div>';
|
||||
tableRows = [];
|
||||
}
|
||||
|
||||
function inlineFormat(text) {
|
||||
return text
|
||||
.replace(/`([^`]+)`/g, '<code style="background:#161b22;padding:2px 6px;border-radius:3px;font-size:13px;color:#e6edf3">$1</code>')
|
||||
.replace(/\*\*([^*]+)\*\*/g, '<strong style="color:#e6edf3">$1</strong>')
|
||||
.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
||||
}
|
||||
|
||||
for (const line of lines) {
|
||||
// Koodiblokit + Mermaid-kaaviot
|
||||
if (line.startsWith('```')) {
|
||||
if (inCode) {
|
||||
if (codeLang === 'mermaid') {
|
||||
const mermaidId = 'mermaid-' + Math.random().toString(36).slice(2, 8);
|
||||
html += `<div class="mermaid-container" id="${mermaidId}" style="margin:16px 0;text-align:center">${codeBuffer.replace(/</g,'<')}</div>`;
|
||||
} else {
|
||||
html += `<pre style="background:#0d1117;border:1px solid #30363d;border-radius:6px;padding:14px;margin:12px 0;overflow-x:auto"><code class="language-${codeLang}">${codeBuffer.replace(/</g,'<')}</code></pre>`;
|
||||
}
|
||||
inCode = false;
|
||||
codeBuffer = '';
|
||||
} else {
|
||||
flushTable();
|
||||
inCode = true;
|
||||
codeLang = line.slice(3).trim() || 'plaintext';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (inCode) { codeBuffer += (codeBuffer ? '\n' : '') + line; continue; }
|
||||
|
||||
// Taulukot
|
||||
if (line.includes('|') && line.trim().startsWith('|')) {
|
||||
if (!inTable) inTable = true;
|
||||
tableRows.push(line);
|
||||
continue;
|
||||
} else {
|
||||
flushTable();
|
||||
}
|
||||
|
||||
// Tyhjä rivi
|
||||
if (!line.trim()) { html += '<div style="height:8px"></div>'; continue; }
|
||||
|
||||
// Otsikot
|
||||
if (line.startsWith('# ')) { html += `<h1 style="color:#e6edf3;font-size:28px;margin:32px 0 12px;border-bottom:1px solid #30363d;padding-bottom:8px">${inlineFormat(line.slice(2))}</h1>`; continue; }
|
||||
if (line.startsWith('## ')) { html += `<h2 style="color:#e6edf3;font-size:22px;margin:28px 0 10px;border-bottom:1px solid #21262d;padding-bottom:6px">${inlineFormat(line.slice(3))}</h2>`; continue; }
|
||||
if (line.startsWith('### ')) { html += `<h3 style="color:#e6edf3;font-size:17px;margin:20px 0 8px">${inlineFormat(line.slice(4))}</h3>`; continue; }
|
||||
|
||||
// Horisontaalinen viiva
|
||||
if (line.match(/^-{3,}$/)) { html += '<hr style="border:none;border-top:1px solid #30363d;margin:20px 0">'; continue; }
|
||||
|
||||
// Lista
|
||||
if (line.match(/^[\-\*] /)) {
|
||||
html += `<div style="padding:2px 0 2px 20px">${inlineFormat(line.replace(/^[\-\*] /, '• '))}</div>`;
|
||||
continue;
|
||||
}
|
||||
if (line.match(/^\d+\. /)) {
|
||||
html += `<div style="padding:2px 0 2px 20px">${inlineFormat(line)}</div>`;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Normaali tekstirivi
|
||||
html += `<p style="margin:4px 0">${inlineFormat(line)}</p>`;
|
||||
}
|
||||
flushTable();
|
||||
return html;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user