15 Commits

Author SHA1 Message Date
6b756e2e83 Prompt-editori modal: avain-arvo-parit, editoitavat kentät
Klikkaa agenttia → 'Näytä viimeisin prompti' → modal-ikkuna jossa
prompti on pilkottu rakenteellisiin kenttiin (Project, CONSTRAINTS,
EXAMPLE jne.). Editoitavat kentät sinisellä ✏️, lukitut harmaalla 🔒.
'Aja uudelleen' kokoaa promptin kentistä ja ajaa sen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:24:29 +03:00
5a52f5113c QA validointi: listaa jokaisen tarkistuksen tuloksen ✓/✗
Aiemmin QA vastasi vain 'OK'. Nyt prompti vaatii raportin jokaisesta
6 tarkistuksesta (Dockerfile, deps, ports, README, testit, pyproject)
esimerkkivastauksen kanssa.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:18:46 +03:00
7b0660e46e Korjattu illegal break: if(!task_id) break → if(task_id) { ... }
break ei ole sallittu if/else-lohkossa. Kääritty avatar-aktivointi
if(data.task_id) -ehtoon.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:16:02 +03:00
b35600b417 Few-shot esimerkit pipeline-prompteissa: manageri, koodari, QA
Pienet mallit tuottavat huomattavasti parempaa koodia kun promptissa
on konkreettinen esimerkki oikeasta vastauksesta. Lisätty:
- Manageri: esimerkki tiedostolistasta
- Koodari: esimerkki main.py ja models.py -tiedostoista
- QA: esimerkki pytest + TestClient -testeistä

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:12:25 +03:00
7693269e5d Dockerfile generoidaan templatesta, ei LLM:llä — ei enää pip/uv sekaannuksia
Malli sekoitti pip:n ja uv:n syntaksin (pip install --system ei toimi).
Nyt Dockerfile rakennetaan suoraan templatesta generoiduista tiedostoista:
pyproject.toml → uv sync, requirements.txt → uv pip install, tai fallback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:11:00 +03:00
702c9170ad Avatareiden aktivointi vain task_id:llisistä viesteistä
Hubin automaattiset 10s-broadcastit aktivoivat managerin avatarin.
Nyt tarkistetaan data.task_id ennen avatar-päivitystä.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:10:00 +03:00
3feed22055 Agenttien promptit näkyvissä ja editoitavissa + Aja uudelleen -nappi
Klikkaa agenttia → näet viimeisimmän pipeline-promptin tekstikentässä.
Voit editoida promptia ja painaa 'Aja uudelleen' ajamaan sen samalla
mallilla. Pipeline tallentaa nyt koko promptin (ei vain kuvausta).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:03:48 +03:00
75310c989e QA validointivaihe: tarkistaa tiedostojen yhteensopivuuden
Uusi vaihe DevOps-vaiheiden jälkeen: QA tarkistaa että
Dockerfile, docker-compose, README ja testit viittaavat
oikeisiin tiedostoihin ja riippuvuuksiin. Jos ongelmia löytyy,
DevOps korjaa Dockerfilen automaattisesti.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 07:43:13 +03:00
743946a391 Dockerfile-prompti dynaaminen: tarkistaa onko pyproject.toml generoitu
Jos pyproject.toml puuttuu, käytetään uv pip install suoraan.
COPY-rivi listaa vain oikeasti olemassa olevat .py-tiedostot.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 07:41:54 +03:00
0bd5faa684 API rate limit 10→30 pyyntöä/min: pipeline tarvitsee ~12 vaihetta
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 07:35:26 +03:00
e0c8c3586b Mallin vaihto: spinner-indikaattori + pelkkä numero oikotienä
kpn load näyttää spinnerin kun Ollama lataa mallia.
Pelkkä numero (esim. '4') toimii oikotienä 'kpn load 4':lle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 07:33:14 +03:00
3a1c5c723c kpn models: numerot + ladattu-tila yhtenäisessä listassa
Sama lista kuin kpn load, mutta näyttää myös mitkä mallit
on ladattu Ollamaan (✓) ja WASM-tilan. Numerot toimivat
suoraan kpn load -komennolla.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 07:30:39 +03:00
3139d1ac65 kpn models: näyttää Ollamasta ladatut mallit + WASM-tilan
Hakee Ollaman /api/tags-endpointista ladatut mallit kokoneen,
parametreineen ja kvantisointitasoineen. WASM-tila näkyy myös.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 07:29:09 +03:00
49a1629646 TODO.md: turvallisuus, yksityisyys ja väärinkäytön esto
Hajautetun verkon riskit dokumentoitu: tulosten validointi,
promptien salaus, reputaatiojärjestelmä, rate limiting, token-talous.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 07:26:52 +03:00
13008ac693 ny mänöö hyvin 2026-04-07 07:20:57 +03:00
4 changed files with 374 additions and 63 deletions

26
network-poc/TODO.md Normal file
View 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

Binary file not shown.

View File

@@ -1015,7 +1015,7 @@ async fn api_chat_completions(
*entry = (now, 1); // Uusi ikkuna
} else {
entry.1 += 1;
if entry.1 > 10 {
if entry.1 > 30 {
return (axum::http::StatusCode::TOO_MANY_REQUESTS, "Liian monta pyyntöä — yritä minuutin kuluttua").into_response();
}
}

View File

@@ -1076,6 +1076,9 @@
<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.
</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>
@@ -1246,6 +1249,17 @@
textEl.value = sharedPrompt;
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) {
@@ -1302,6 +1316,96 @@
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,'&lt;')}</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() {
Object.keys(agentPrompts).forEach(agent => {
const prompt = agentPrompts[agent].prompt || "";
@@ -2107,8 +2211,6 @@
termLog(`<span style="color:#a371f7;font-weight:bold">━━━ Pipeline käynnistyy ━━━</span>`);
// 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: one-line description
@@ -2118,12 +2220,18 @@ CONSTRAINTS — the coder can only generate ~400 tokens per file:
- Only .py and pyproject.toml files
- No directories, no paths, just filenames
- List dependencies first, then main app
- Prefer fewer, focused files over many small ones
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}`;
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; }
pipelineStep('manager', 'Suunnittelu', 'done', task, plan);
pipelineStep('manager', 'Suunnittelu', 'done', managerPrompt, plan);
// Parsitaan tiedostolista: "filename.py: description" TAI pelkkä "filename.py"
const fileList = plan.split('\n')
@@ -2160,7 +2268,7 @@ Project: ${task}`;
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);
pipelineStep('coder', file.name, 'active', '');
// Rakennetaan konteksti: aiemmin generoidut tiedostot
let context = '';
@@ -2187,16 +2295,55 @@ start = "uvicorn main:app --reload"`;
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}
Write ONLY the file "${file.name}"${file.desc ? ': ' + file.desc : ''}.${extraInstructions}
IMPORTANT: Keep the code SHORT and focused. Max ~50 lines. No comments, no docstrings, no type hints unless essential. Write minimal, working code.`;
Write ONLY the file "${file.name}"${file.desc ? ': ' + file.desc : ''}.${extraInstructions}${coderExample}
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);
if (!code) {
termLog(` ✗ Pipeline keskeytyi (${file.name})`, '#f85149');
return;
}
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
@@ -2238,8 +2385,24 @@ Write the corrected code.`;
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 a short test file (test_app.py) for this project. Use pytest. Max 3 test functions. Keep it minimal.
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;
@@ -2250,21 +2413,29 @@ ${Object.entries(generatedFiles).map(([n, c]) => `--- ${n} ---\n${c}`).join('\n\
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 dockerPrompt = `Write a Dockerfile for this Python project using uv package manager.
RULES:
- Base: python:3.12-slim
- Install uv: COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
- COPY pyproject.toml and then: RUN uv sync --no-dev
- COPY all .py files
- EXPOSE 8000
- CMD ["uv", "run", "uvicorn", "${mainFile.replace('.py','')}:app", "--host", "0.0.0.0", "--port", "8000"]
- Only output Dockerfile content, no explanations
Files: ${Object.keys(generatedFiles).join(', ')}`;
const dockerfile = await kpnRun(agentPrompts.tester.model, dockerPrompt, false, 256);
if (dockerfile) generatedFiles['Dockerfile'] = dockerfile;
pipelineStep('tester', 'Dockerfile', 'done', 'Dockerfile', dockerfile);
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;
@@ -2298,6 +2469,52 @@ Files: ${Object.keys(generatedFiles).join(', ')}`;
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>`);
renderProjectCard(generatedFiles, task);
}
@@ -2383,6 +2600,11 @@ Files: ${Object.keys(generatedFiles).join(', ')}`;
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+/);
if (parts[0] !== 'kpn') {
termLog('kpn: tuntematon komento. Kokeile: kpn help', '#f85149');
@@ -2467,21 +2689,47 @@ Files: ${Object.keys(generatedFiles).join(', ')}`;
}
// Ollama: vaihdetaan malli hubin kautta
termLog(` Vaihdetaan Ollama-malli: ${selected.name} (${selected.size})...`, '#d29922');
fetch('/api/v1/model', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: selected.name }),
}).then(r => r.json()).then(data => {
if (data.status === 'ok') {
termLog(` <span style="color:#3fb950">✓</span> Malli vaihdettu: ${selected.name}`, '#3fb950');
termLog(' <span style="color:#8b949e">Ollama lataa mallin ensimmäisellä pyynnöllä</span>');
// Päivitetään aktiivinen default
// 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');
termLog(' ✗ Mallin vaihto epäonnistui', '#f85149');
}
}).catch(e => termLog(`${e.message}`, '#f85149'));
}).catch(e => {
clearInterval(spinTimer);
pullLine.remove();
termLog(`${e.message}`, '#f85149');
});
return;
}
@@ -2493,14 +2741,31 @@ Files: ${Object.keys(generatedFiles).join(', ')}`;
}
if (sub === 'models') {
termLog(' <span style="color:#d29922">Selain (kpn load):</span>', '#c9d1d9');
termLog(' qwen-coder:0.5b <span style="color:#8b949e">~990 MB | WASM ~0.4 tok/s</span>');
termLog(' <span style="color:#3fb950">Natiivi (Ollama + GPU):</span>', '#c9d1d9');
termLog(' qwen2.5-coder:7b <span style="color:#8b949e">~4.7 GB | NVIDIA ~80 tok/s | AMD ~40 tok/s | Apple ~30 tok/s</span>');
termLog(' qwen2.5-coder:3b <span style="color:#8b949e">~1.9 GB | NVIDIA ~120 tok/s</span>');
termLog(' qwen2.5-coder:1.5b <span style="color:#8b949e">~1 GB | NVIDIA ~150 tok/s</span>');
termLog(' Vaihda malli: <span style="color:#58a6ff">OLLAMA_MODEL=qwen2.5-coder:7b</span>', '#8b949e');
termLog(' Hub reitittää automaattisesti nopeimmalle solmulle', '#8b949e');
const allModels = [
{ id: '1', name: 'qwen2.5-coder:0.5b', size: '~400 MB', type: 'selain + Ollama' },
{ id: '2', name: 'qwen2.5-coder:1.5b', size: '~1 GB', type: 'Ollama GPU' },
{ id: '3', name: 'qwen2.5-coder:7b', size: '~4.7 GB', type: 'Ollama GPU' },
{ id: '4', name: 'qwen2.5-coder:14b', size: '~9 GB', type: 'Ollama GPU' },
{ id: '5', name: 'qwen2.5-coder:32b', size: '~20 GB', type: 'Ollama GPU' },
];
// 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 &lt;numero&gt;)</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;
}
@@ -2983,8 +3248,11 @@ Files: ${Object.keys(generatedFiles).join(', ')}`;
while (term.children.length > 50 && !term.firstChild.querySelector('.stream-content')) term.removeChild(term.firstChild);
term.scrollTop = term.scrollHeight;
document.querySelectorAll('.avatar-card').forEach(c => c.classList.remove('active'));
document.getElementById('avatar-kpn').classList.add('active');
// Avatar-aktivointi vain oikeille käyttäjäpyynnöille
if (data.task_id) {
document.querySelectorAll('.avatar-card').forEach(c => c.classList.remove('active'));
document.getElementById('avatar-kpn')?.classList.add('active');
}
}
} else if (isCoder) {
// Codelab: erillinen addCodeResult-handler käsittelee (rivi 2364)
@@ -3131,23 +3399,25 @@ Files: ${Object.keys(generatedFiles).join(', ')}`;
term.scrollTop = term.scrollHeight;
}
// Avatar-aktivointi vain omille tehtäville
document.querySelectorAll('.avatar-card').forEach(c => c.classList.remove('active'));
const model = data.model || '';
const p = data.prompt ? data.prompt.toLowerCase() : '';
// 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'));
const model = data.model || '';
const p = data.prompt ? data.prompt.toLowerCase() : '';
if (p.includes('tiiminvetäjä') || p.includes('pilko')) {
document.getElementById('avatar-kpn')?.classList.add('active');
} else if (p.includes('arvioi seuraava koodi') || p.includes('ohjelmiston julkaisu')) {
document.getElementById('avatar-tester')?.classList.add('active');
} else if (p.includes('tervehdi')) {
document.getElementById('avatar-client')?.classList.add('active');
} else if (p.includes('test')) {
document.getElementById('avatar-qa')?.classList.add('active');
} else if (model.includes('coder') || model.includes('Coder')) {
document.getElementById('avatar-coder')?.classList.add('active');
} else if (model.includes('deepseek') || model.includes('r1')) {
document.getElementById('avatar-observer')?.classList.add('active');
if (p.includes('tiiminvetäjä') || p.includes('pilko')) {
document.getElementById('avatar-kpn')?.classList.add('active');
} else if (p.includes('arvioi seuraava koodi') || p.includes('ohjelmiston julkaisu')) {
document.getElementById('avatar-tester')?.classList.add('active');
} else if (p.includes('tervehdi')) {
document.getElementById('avatar-client')?.classList.add('active');
} else if (p.includes('test')) {
document.getElementById('avatar-qa')?.classList.add('active');
} else if (model.includes('coder') || model.includes('Coder')) {
document.getElementById('avatar-coder')?.classList.add('active');
} else if (model.includes('deepseek') || model.includes('r1')) {
document.getElementById('avatar-observer')?.classList.add('active');
}
}
}
}
@@ -3910,5 +4180,20 @@ Files: ${Object.keys(generatedFiles).join(', ')}`;
return html;
}
</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">&times;</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>
</html>