31 Commits

Author SHA1 Message Date
Jaakko Vanhala
ee048b0b68 kipina-node: automaattinen Ollama-instanssien haku + konttituki
Skripti skannaa localhost, 127.0.0.1, ollama, host.docker.internal
ja tarjoaa valikon jos useampi löytyy. Ei vaadi enää paikallista
ollama-binääriä — toimii myös Docker-konttia tai remote-instanssia
vasten. OLLAMA_URL välitetään Rust-binäärille.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 10:19:00 +03:00
Jaakko Vanhala
4e83569194 Konsoliloki näyttää mallin nimen: ✓ qwen2.5-coder:3b | 438 tok | 4952ms | 93.4 tok/s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 10:01:32 +03:00
Jaakko Vanhala
f42b692eeb Lyhennetty konsolilogi: yksi rivi per pyyntö + yksi rivi per tulos
Ennen: koko prompti + vastaus logitettiin (satoja rivejä)
Jälkeen:
  → task_id:abc | 42r prompti | "Write ONLY models.py..."
  ✓ 128 tok | 3200ms | 40.0 tok/s | "from sqlalchemy import..."

llm_done-viestissä prompt lyhennetty viimeiseen riviin (ei koko kontekstia).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 10:00:39 +03:00
Jaakko Vanhala
f79bb16f3d kipina-node binäärijakelu: download-skripti + macOS ARM64 binääri
kipina.studio/kipina-node — shell-skripti joka:
1. Tunnistaa OS/arch (macOS ARM, Linux x86/ARM)
2. Tarkistaa Ollaman (asennettu? käynnissä?)
3. Lataa kielimallin automaattisesti
4. Lataa oikean binäärin kipina.studio/download/
5. Käynnistää noden → yhdistää hubiin

Käyttö: curl -sSL https://kipina.studio/kipina-node | bash
Tai:    curl -sSL https://kipina.studio/kipina-node -o kipina-node && chmod +x kipina-node && ./kipina-node

build-binaries.sh — kääntää binäärit kaikille alustoille (Docker).
macOS ARM64 binääri (4.9MB) valmis, Linux x86_64 build käynnissä.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 09:51:31 +03:00
Jaakko Vanhala
e81fc33faf Join-dialogi: kaksi selkeää vaihetta (Ollama + kipina-node binääri)
Vaihe 1: Asenna Ollama
  curl -fsSL https://ollama.ai/install.sh | sh
  (+ brew/Windows-vaihtoehdot)

Vaihe 2: Lataa ja käynnistä kipina-node
  curl -sSL https://kipina.studio/kipina-node -o kipina-node && chmod +x kipina-node && ./kipina-node

Ei vaadi Rustia — valmis binääri ladataan suoraan.
Molemmat komennot kopioitavissa yhdellä klikkauksella.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 08:56:00 +03:00
Jaakko Vanhala
433726c553 Palautettu docker-compose.prod.yml: vain Caddy + Hub (ei Ollamaa palvelimella)
Ollama ajetaan käyttäjien omilla koneilla join.sh:n kautta,
ei palvelimella. Selain-Wasm toimii fallbackina.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 08:53:06 +03:00
Jaakko Vanhala
dec2e24e2f "Liitä koneesi" -nappi + join.sh + Docker native-node
UI: status-palkissa vihreä "+ Liitä koneesi" -nappi joka avaa dialogin:
  curl -sSL https://kipina.studio/join.sh | bash

join.sh:
- Tarkistaa Ollaman → tarjoaa asennusta jos puuttuu
- Käynnistää Ollaman jos ei pyöri
- Lataa kielimallin (qwen2.5-coder:3b)
- Käynnistää native-noden → yhdistää wss://kipina.studio/ws

Docker: Dockerfile.native + docker-compose.prod.yml päivitetty
ollama + native-node -konteilla palvelinpuolelle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 08:46:22 +03:00
Jaakko Vanhala
9058033669 Poistettu fonttiskaalaus (A-/A+) — ei vaikuttanut terminaaliin
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 08:33:20 +03:00
Jaakko Vanhala
8bd86e6325 Fonttikoon A-/A+ säädin: ±20% viidessä askeleessa
Oikeassa yläkulmassa A- ja A+ napit. Skaalaa 80-120%, tallennetaan localStorageen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 08:31:33 +03:00
Jaakko Vanhala
c1133bb075 Terminaalin fontti 15→16px
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 08:27:31 +03:00
Jaakko Vanhala
6502d75efc Terminaalin syöttökenttä korostettu: sininen reunus, varjo, isompi fontti 16px
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 08:25:37 +03:00
Jaakko Vanhala
9f8b7fe920 UI-fonttikoot kasvatettu: body 16px, terminaali 15px, tabit 15px, status 14px
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 08:23:46 +03:00
Jaakko Vanhala
746bc20fcb Agenttikuvakkeet kasvatettu: 50→64px kuva, 72→90px kortti, isompi fontti
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 08:22:42 +03:00
Jaakko Vanhala
93f6baa0ea UI kasvatettu: container 1200→1600px, terminaali korkeampi, padding leveämpi
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 08:18:34 +03:00
Jaakko Vanhala
cc8e871735 deploy-fast.sh: luo hakemisto palvelimelle ennen rsync:iä
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 08:15:19 +03:00
Jaakko Vanhala
e90f3460c3 deploy-fast.sh: päivitä vain frontend ilman kontin uudelleenkäynnistystä
docker-compose.prod.yml: frontend/dist mountataan volumena (read-only).
Hub servaa tiedostot suoraan — rsync päivittää ne lennossa.

Kolme deploy-tasoa:
1. deploy-fast.sh — vain frontend (sekunteja, ei downtime)
2. deploy-light.sh — rsync + remote Docker build (minuutteja)
3. deploy.sh — lokaali build + image siirto (hidas mutta varma)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 08:12:10 +03:00
Jaakko Vanhala
4d74c38618 Dockerfile: poistettu turha COPY pkg (Astro kopioi public/:n automaattisesti)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 08:06:39 +03:00
Jaakko Vanhala
8a1b204179 v0.3.1: Avatarit WebP (18MB→256KB), PNG:t temp-kansioon
Kaikki avatar-viittaukset .png → .webp (200px, quality 80).
Alkuperäiset PNG:t siirretty temp/avatars-png/ (gitignored).
Hub-versio 0.3.0 → 0.3.1.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 08:06:02 +03:00
Jaakko Vanhala
b19f5a3518 deploy-light.sh: rsync + remote build (ei image-siirtoa)
Lähettää vain lähdekoodin rsync:llä (~2MB muuttuneet tiedostot),
palvelin buildaa Docker-imagen itse. Nopeampi kuin 80MB imagen siirto.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 21:56:01 +03:00
Jaakko Vanhala
38dc36e846 Dockerfile wasm-builder: lisätty dummy-cratet workspace-yhteensopivuuteen
cargo metadata vaatii kaikkien workspace-jäsenten Cargo.toml:n.
Lisätty hub/, native-node/, cli/ dummy-tiedostot wasm-builder-vaiheeseen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 08:07:28 +03:00
Jaakko Vanhala
4fe6931b5f Revolutionized 2026-04-10 08:06:15 +03:00
Jaakko Vanhala
b8e8a83e49 Dockerfile.prod päivitetty Astro-frontendille
Muutokset:
- Vaihe 1: Node.js buildaa Astro-frontendin (frontend/dist/)
- Vaihe 2: wasm-pack buildaa Wasm-moduulin erikseen
- Vaihe 3: Hub rakennetaan Rustilla
- Vaihe 4: Yhdistetään dist/ + pkg/ + avatars/ + templates/ + GUIDE.md

STATIC_DIR=/app/frontend/dist (ei enää /app/static)
Avatarit, templates ja GUIDE.md kopioidaan erikseen koska
Astro ei kopioi public/-tiedostoja dist/:iin buildin yhteydessä.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 08:04:49 +03:00
Jaakko Vanhala
3d6914974d Prompti-textarea kasvaa automaattisesti sisällön mukaan
Koko prompti näkyy kerralla kun avatarin klikkaa — ei scrollausta.
Textarea saa overflow:hidden + auto-height sekä avatessa että kirjoittaessa.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 08:03:18 +03:00
Jaakko Vanhala
9aff2ec154 uv kauttaaltaan + Tarkkailijan raportti rakenteelliseksi markdowniksi
uv-päivitykset:
- Koodarin NEVER-lista: ei requirements.txt, ei pip, käytä uv
- Template pyproject.toml: PEP 621, uv-yhteensopiva
- Raportin Quick Start: uv sync + uv run uvicorn

Tarkkailijan raportti uudessa formaatissa:
- Overview (yksi kappale)
- Files (taulukko: tiedosto + tarkoitus)
- Quick Start (uv-komennot koodiblokissa)
- Docker (build + run koodiblokissa)
- API Endpoints (taulukko: method, path, description)
- Architecture (rakenne ja päätökset)
- Risk Assessment (taulukko: severity, issue)

Malli saa taulukkopohjat valmiina → täyttää ne oikealla datalla.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:59:10 +03:00
Jaakko Vanhala
ecd4525a7f Review-korjauskierrokset nostettu 2→3
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:51:41 +03:00
Jaakko Vanhala
7a3e5278b9 Review-korjausluuppi: DevOps tarkistaa korjaukset, max 2 kierrosta
Pipeline:
1. DevOps review → löytää virheitä
2. Koodari korjaa → päivittää files-objektin
3. DevOps review (kierros 2) → tarkistaa korjaukset
4. Jos yhä virheitä → Koodari korjaa uudelleen
5. LGTM tai max 2 kierrosta → eteenpäin

Terminaalissa näkyy kierrosnumero: "koodikatselmointi (kierros 2)"
LGTM merkitään vihreällä ✓-merkillä.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:50:43 +03:00
Jaakko Vanhala
8dcf269b42 strip_code_fences: poistetaan kaikki backtick-rivit aggressiivisesti
Ollama tuottaa \`\`\`python ... \`\`\` -blokkeja vaikka system prompt
kieltää ne. Nyt kaikki rivit jotka alkavat \`\`\` suodatetaan pois,
myös keskeltä vastausta (useita koodiblokkeja per vastaus).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:47:03 +03:00
Jaakko Vanhala
cb16f35265 Tiedostot kulkevat agentilta toiselle: korjaukset päivittyvät, QA saa korjatun koodin
Kontekstiketju nyt:
1. Data → models.py
2. Koodari → schemas.py (saa models.py kontekstina)
3. Koodari → main.py (saa models.py + schemas.py)
4. Koodari → pyproject.toml
5. DevOps → review (saa kaikki tiedostot)
6. Koodari → korjaukset → parsitaan takaisin files-objektiin (--- filename ---)
7. QA → testit (saa KORJATUT tiedostot, ei alkuperäisiä)
8. DevOps → Dockerfile (saa kaikki tiedostot + testit)
9. Tarkkailija → README (saa kaiken)

Aiemmin: korjaukset menivät terminaaliin mutta eivät päivittyneet files-objektiin.
Nyt: korjattu koodi parsitaan --- filename --- -erottimilla takaisin.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:44:01 +03:00
Jaakko Vanhala
b9d340b4b4 install.sh: Debian/Ubuntu-asennusskripti
Asentaa automaattisesti:
1. Build-työkalut (build-essential, pkg-config, libssl-dev)
2. Rust (rustup)
3. Node.js 22 (nodesource)
4. Ollama
5. qwen2.5-coder:3b -malli

Käyttö: ./network-poc/install.sh && ./network-poc/local.sh

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:42:27 +03:00
Jaakko Vanhala
dd07e536f0 Korjattu duplikaatti const tst -määrittely kpnProject:ssa
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:39:51 +03:00
Jaakko Vanhala
9af481a022 DevOps-agentin prompti laajennettu staattiseksi koodianalyysiksi
9-kohdan checklist: importit, nimeämiset, tyypit, virheenkäsittely,
resurssivuodot, tietoturva, endpointit, Pydantic v2, täydellisyys.

Aiemmin 7 kohtaa, nyt 9 — lisätty: type hints, tietoturva (raw SQL,
hardcoded secrets), Pydantic v2 (model_dump, from_attributes).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:36:57 +03:00
155 changed files with 619 additions and 22030 deletions

3
.gitignore vendored
View File

@@ -37,3 +37,6 @@ Cargo.lock
# Ajonaikaiset tietokannat
*.db
# Wanha versio
temp/

View File

@@ -0,0 +1,21 @@
# Native-node: Rust + Ollama-client (ei GPU-tunnistusta)
FROM rust:slim AS builder
RUN apt-get update && apt-get install -y pkg-config libssl-dev && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY Cargo.toml Cargo.lock* ./
COPY native-node/Cargo.toml native-node/Cargo.toml
COPY native-node/src native-node/src
# Dummy-cratet workspace-yhteensopivuuteen
COPY hub/Cargo.toml hub/Cargo.toml
COPY node/Cargo.toml node/Cargo.toml
COPY cli/Cargo.toml cli/Cargo.toml
RUN mkdir -p hub/src node/src cli/src && touch hub/src/main.rs node/src/lib.rs cli/src/main.rs
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/app/target \
cargo build --release -p native-node --no-default-features \
&& cp /app/target/release/native-node /usr/local/bin/native-node
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /usr/local/bin/native-node /usr/local/bin/native-node
CMD ["native-node"]

View File

@@ -1,47 +1,63 @@
# syntax=docker/dockerfile:1
FROM rust:slim AS builder
RUN apt-get update && apt-get install -y \
curl pkg-config libssl-dev g++ \
&& rm -rf /var/lib/apt/lists/*
# --- Vaihe 1: Frontend (Astro) ---
FROM node:22-slim AS frontend
WORKDIR /app/frontend
# Riippuvuudet ensin → cache-kerros (muuttuu harvoin)
COPY frontend/package.json frontend/package-lock.json* ./
RUN npm install --silent
# Lähdekoodi → muuttuu usein, mutta npm install on cachessa
COPY frontend/ .
RUN npm run build
# --- Vaihe 2: Wasm (wasm-pack) ---
FROM rust:slim AS wasm-builder
RUN apt-get update && apt-get install -y curl pkg-config libssl-dev g++ && rm -rf /var/lib/apt/lists/*
RUN curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
WORKDIR /app
# Kopioi kaikki Cargo-tiedostot
COPY Cargo.toml ./
COPY Cargo.lock* ./
COPY Cargo.toml Cargo.lock* ./
COPY node/Cargo.toml node/Cargo.toml
COPY node/src node/src
# Dummy-cratet jotta workspace Cargo.toml on tyytyväinen
COPY hub/Cargo.toml hub/Cargo.toml
COPY native-node/Cargo.toml native-node/Cargo.toml
COPY cli/Cargo.toml cli/Cargo.toml
RUN mkdir -p hub/src native-node/src cli/src && touch hub/src/main.rs native-node/src/main.rs cli/src/main.rs
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/app/target \
cd node && wasm-pack build --target web --out-dir /app/wasm-pkg
# --- Vaihe 3: Hub (Rust) ---
FROM rust:slim AS hub-builder
RUN apt-get update && apt-get install -y pkg-config libssl-dev && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY Cargo.toml Cargo.lock* ./
COPY hub/Cargo.toml hub/Cargo.toml
COPY hub/src hub/src
# Tarvitaan dummy-cratet jotta workspace kompiloi
COPY node/Cargo.toml node/Cargo.toml
COPY native-node/Cargo.toml native-node/Cargo.toml
COPY cli/Cargo.toml cli/Cargo.toml
# Kopioi lähdekoodi
COPY hub/src hub/src
COPY node/src node/src
COPY native-node/src native-node/src
COPY cli/src cli/src
COPY static static
# Rakenna Wasm — cache mount pitää Cargo-rekisterin ja target-kansion buildien välillä
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/app/target \
cd node && wasm-pack build --target web --out-dir ../static/pkg
# Rakenna Hub
RUN mkdir -p node/src native-node/src cli/src && touch node/src/lib.rs native-node/src/main.rs cli/src/main.rs
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/app/target \
cargo build --release -p hub \
&& cp /app/target/release/hub /usr/local/bin/hub
# --- Vaihe 4: Tuotantoimage ---
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /usr/local/bin/hub /usr/local/bin/hub
COPY --from=builder /app/static /app/static
COPY --from=hub-builder /usr/local/bin/hub /usr/local/bin/hub
COPY --from=frontend /app/frontend/dist /app/frontend/dist
COPY --from=wasm-builder /app/wasm-pkg /app/frontend/dist/pkg
# Kopioidaan GUIDE.md ja templates
COPY frontend/public/GUIDE.md /app/frontend/dist/GUIDE.md
COPY frontend/public/templates /app/frontend/dist/templates
COPY frontend/public/avatars /app/frontend/dist/avatars
WORKDIR /app
ENV STATIC_DIR=/app/static
ENV STATIC_DIR=/app/frontend/dist
EXPOSE 3000
CMD ["hub"]

38
network-poc/build-binaries.sh Executable file
View File

@@ -0,0 +1,38 @@
#!/bin/bash
# Käännä kipina-node binäärit kaikille alustoille
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
OUT="$SCRIPT_DIR/frontend/public/download"
mkdir -p "$OUT"
echo "=== Kipinä Node — Binary Build ==="
# macOS ARM (natiivi)
echo "[1/3] macOS ARM64..."
cd "$SCRIPT_DIR"
cargo build --release -p native-node --no-default-features 2>&1 | tail -1
cp target/release/native-node "$OUT/kipina-node-macos-arm64"
echo " $(ls -lh "$OUT/kipina-node-macos-arm64" | awk '{print $5}')"
# Linux x86_64 (Docker)
echo "[2/3] Linux x86_64..."
docker run --rm \
-v "$SCRIPT_DIR":/app -w /app \
--platform linux/amd64 \
rust:slim \
bash -c "apt-get update -qq && apt-get install -y -qq pkg-config libssl-dev >/dev/null 2>&1 && cargo build --release -p native-node --no-default-features 2>&1 | tail -1 && cp target/release/native-node /app/frontend/public/download/kipina-node-linux-x86_64"
echo " $(ls -lh "$OUT/kipina-node-linux-x86_64" | awk '{print $5}')"
# Linux ARM64 (Docker)
echo "[3/3] Linux ARM64..."
docker run --rm \
-v "$SCRIPT_DIR":/app -w /app \
--platform linux/arm64 \
rust:slim \
bash -c "apt-get update -qq && apt-get install -y -qq pkg-config libssl-dev >/dev/null 2>&1 && cargo build --release -p native-node --no-default-features 2>&1 | tail -1 && cp target/release/native-node /app/frontend/public/download/kipina-node-linux-arm64"
echo " $(ls -lh "$OUT/kipina-node-linux-arm64" | awk '{print $5}')"
echo ""
echo "=== Binäärit valmiina ==="
ls -lh "$OUT"/kipina-node-*

28
network-poc/deploy-fast.sh Executable file
View File

@@ -0,0 +1,28 @@
#!/bin/bash
# Nopea deploy: päivittää vain frontendin (ei kontin uudelleenkäynnistystä)
# Hub-binäärin päivitys: käytä deploy.sh tai deploy-light.sh
set -e
SERVER="ubuntu@86.50.252.98"
REMOTE_DIR="~/code/agentic-studio/network-poc"
SSH_OPTS="-o StrictHostKeyChecking=no"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
echo "=== Kipinä Studio — Frontend Deploy ==="
# 1. Buildaa frontend paikallisesti
echo "[1/2] Rakennetaan frontend..."
cd "$SCRIPT_DIR/frontend"
[ -d node_modules ] || npm install --silent
npm run build --silent 2>&1 | tail -1
# 2. Synkataan dist/ palvelimelle (vain muuttuneet tiedostot)
echo "[2/2] Synkataan dist/ → palvelin..."
ssh $SSH_OPTS $SERVER "mkdir -p $REMOTE_DIR/frontend/dist"
rsync -az --delete -e "ssh $SSH_OPTS" "$SCRIPT_DIR/frontend/dist/" "$SERVER:$REMOTE_DIR/frontend/dist/"
echo ""
echo "=== Valmis! Frontend päivitetty — ei uudelleenkäynnistystä ==="
echo " https://kipina.studio"
echo ""
echo "Huom: Jos Rust-koodi (hub/) muuttui, aja: ./deploy.sh"

33
network-poc/deploy-light.sh Executable file
View File

@@ -0,0 +1,33 @@
#!/bin/bash
# Kevyt deploy: lähetetään vain koodi, palvelin buildaa itse
set -e
SERVER="ubuntu@86.50.252.98"
REMOTE_DIR="~/code/agentic-studio/network-poc"
SSH_OPTS="-o StrictHostKeyChecking=no"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
echo "=== Kipinä Studio Deploy (remote build) ==="
# 1. Synkataan koodi palvelimelle (vain muuttuneet tiedostot)
echo "[1/3] Synkataan koodi..."
rsync -az --delete \
--exclude 'target/' \
--exclude 'node_modules/' \
--exclude 'dist/' \
--exclude '.astro/' \
--exclude 'temp/' \
--exclude '*.db' \
--exclude '.git/' \
"$SCRIPT_DIR/" "$SERVER:$REMOTE_DIR/"
# 2. Rakennetaan image palvelimella
echo "[2/3] Rakennetaan image palvelimella..."
ssh $SSH_OPTS $SERVER "cd $REMOTE_DIR && docker build -f Dockerfile.prod -t kipina-agentic:latest ."
# 3. Käynnistetään
echo "[3/3] Käynnistetään..."
ssh $SSH_OPTS $SERVER "cd $REMOTE_DIR && docker compose -f docker-compose.prod.yml down && docker compose -f docker-compose.prod.yml up -d"
echo "=== Valmis! https://kipina.studio ==="

View File

@@ -19,8 +19,12 @@ services:
restart: unless-stopped
environment:
- DATABASE_PATH=/data/nodes.db
- STATIC_DIR=/app/frontend/dist
- ADMIN_PASSWORD=${ADMIN_PASSWORD:-}
- NODE_API_KEY=${NODE_API_KEY:-}
volumes:
- hub_data:/data
- ./frontend/dist:/app/frontend/dist:ro
volumes:
caddy_data:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 696 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 757 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 700 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 731 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 711 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 695 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 432 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 650 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 389 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 596 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 496 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 872 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 738 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 813 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 658 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 593 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 563 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 696 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 718 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 513 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 780 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 826 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

View File

@@ -0,0 +1,73 @@
#!/bin/bash
# Kipinä — liitä koneesi laskentaverkkoon
set -e
HUB_URL="${KIPINA_HUB:-wss://kipina.studio/ws}"
MODEL="${KIPINA_MODEL:-qwen2.5-coder:3b}"
echo ""
echo " ╔══════════════════════════════════════╗"
echo " ║ Kipinä Agentic Network — Node Join ║"
echo " ╚══════════════════════════════════════╝"
echo ""
# 1. Ollama
if command -v ollama &>/dev/null; then
echo " ✓ Ollama löytyi: $(ollama --version 2>/dev/null || echo 'asennettu')"
else
echo " Ollama ei ole asennettu."
echo ""
read -p " Asennetaanko Ollama? (k/e) " -n 1 -r; echo
if [[ $REPLY =~ ^[Kk]$ ]]; then
echo " Asennetaan Ollama..."
curl -fsSL https://ollama.ai/install.sh | sh
else
echo " Ollama vaaditaan laskentaan. Asenna: https://ollama.ai"
exit 1
fi
fi
# 2. Varmistetaan että Ollama on käynnissä
if ! curl -s http://localhost:11434/api/tags &>/dev/null; then
echo " Käynnistetään Ollama..."
ollama serve &>/dev/null &
sleep 3
if ! curl -s http://localhost:11434/api/tags &>/dev/null; then
echo " ✗ Ollama ei käynnistynyt. Aja: ollama serve"
exit 1
fi
fi
echo " ✓ Ollama käynnissä"
# 3. Malli
if ollama list 2>/dev/null | grep -q "$MODEL"; then
echo " ✓ Malli $MODEL ladattu"
else
echo " Ladataan malli $MODEL..."
ollama pull "$MODEL"
fi
# 4. Native-node
echo ""
echo " Yhdistetään hubiin: $HUB_URL"
echo " Malli: $MODEL"
echo " Ctrl+C pysäyttää"
echo ""
# Tarkistetaan onko native-node käännetty
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
NATIVE_BIN="$SCRIPT_DIR/target/release/native-node"
if [ -f "$NATIVE_BIN" ]; then
HUB_URL="$HUB_URL" OLLAMA_MODEL="$MODEL" "$NATIVE_BIN"
elif command -v cargo &>/dev/null && [ -f "$SCRIPT_DIR/native-node/Cargo.toml" ]; then
echo " Käännetään native-node..."
cd "$SCRIPT_DIR"
cargo build --release -p native-node --no-default-features 2>&1 | tail -1
HUB_URL="$HUB_URL" OLLAMA_MODEL="$MODEL" "$NATIVE_BIN"
else
echo " ✗ native-node binääriä ei löydy eikä Rust ole asennettu."
echo " Asenna Rust: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"
echo " Tai lataa valmis binääri: https://kipina.studio/download"
exit 1
fi

View File

@@ -0,0 +1,121 @@
#!/bin/bash
# Kipinä Node — lataa oikea binääri ja käynnistä
set -e
BASE_URL="https://kipina.studio/download"
HUB_URL="${KIPINA_HUB:-wss://kipina.studio/ws}"
MODEL="${KIPINA_MODEL:-qwen2.5-coder:3b}"
OLLAMA_URL="${OLLAMA_URL:-http://localhost:11434}"
# Tunnista OS ja arkkitehtuuri
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m)
case "$OS-$ARCH" in
darwin-arm64) BINARY="kipina-node-macos-arm64" ;;
darwin-x86_64) BINARY="kipina-node-macos-arm64" ;; # Rosetta
linux-x86_64) BINARY="kipina-node-linux-x86_64" ;;
linux-aarch64) BINARY="kipina-node-linux-arm64" ;;
*) echo "Ei tuettu: $OS-$ARCH"; exit 1 ;;
esac
echo ""
echo " ╔══════════════════════════════════════╗"
echo " ║ Kipinä Agentic Node ║"
echo " ╚══════════════════════════════════════╝"
echo ""
echo " OS: $OS ($ARCH)"
echo ""
# Etsi Ollama-instanssit
CANDIDATES=(
"http://localhost:11434"
"http://127.0.0.1:11434"
"http://ollama:11434"
"http://host.docker.internal:11434"
)
# Lisää OLLAMA_URL listaan jos asetettu ja ei jo mukana
if [ -n "$OLLAMA_URL" ]; then
ALREADY=false
for c in "${CANDIDATES[@]}"; do
[ "$c" = "$OLLAMA_URL" ] && ALREADY=true
done
$ALREADY || CANDIDATES=("$OLLAMA_URL" "${CANDIDATES[@]}")
fi
echo " Etsitään Ollama-instansseja..."
FOUND=()
for url in "${CANDIDATES[@]}"; do
if curl -s --connect-timeout 1 "$url/api/tags" &>/dev/null; then
FOUND+=("$url")
fi
done
if [ ${#FOUND[@]} -eq 0 ]; then
# Ei löytynyt — yritä käynnistää lokaali
if command -v ollama &>/dev/null; then
echo " Käynnistetään Ollama..."
ollama serve &>/dev/null &
sleep 3
if curl -s --connect-timeout 1 "http://localhost:11434/api/tags" &>/dev/null; then
OLLAMA_URL="http://localhost:11434"
echo " ✓ Ollama käynnistetty ($OLLAMA_URL)"
else
echo " ✗ Ollaman käynnistys epäonnistui."
exit 1
fi
else
echo ""
echo " ✗ Ollamaa ei löytynyt."
echo " Kontti/remote: OLLAMA_URL=http://HOST:11434 ./kipina-node"
echo " Asenna: curl -fsSL https://ollama.ai/install.sh | sh"
exit 1
fi
elif [ ${#FOUND[@]} -eq 1 ]; then
OLLAMA_URL="${FOUND[0]}"
echo " ✓ Ollama löytyi: $OLLAMA_URL"
else
echo ""
echo " Löytyi ${#FOUND[@]} Ollama-instanssia:"
echo ""
for i in "${!FOUND[@]}"; do
echo " $((i+1))) ${FOUND[$i]}"
done
echo ""
read -p " Valitse [1-${#FOUND[@]}]: " -r CHOICE
if [[ "$CHOICE" =~ ^[0-9]+$ ]] && [ "$CHOICE" -ge 1 ] && [ "$CHOICE" -le ${#FOUND[@]} ]; then
OLLAMA_URL="${FOUND[$((CHOICE-1))]}"
else
OLLAMA_URL="${FOUND[0]}"
echo " Käytetään oletusta: $OLLAMA_URL"
fi
echo " ✓ Valittu: $OLLAMA_URL"
fi
echo ""
echo " Hub: $HUB_URL"
echo " Ollama: $OLLAMA_URL"
echo " Malli: $MODEL"
# Lataa malli (toimii sekä lokaalilla binäärillä että API:n kautta)
if ! curl -s "$OLLAMA_URL/api/tags" | grep -q "$MODEL"; then
echo " Ladataan $MODEL..."
curl -s "$OLLAMA_URL/api/pull" -d "{\"name\":\"$MODEL\"}" > /dev/null
fi
echo " ✓ Malli $MODEL valmis"
# Lataa binääri
BIN_PATH="./kipina-node-bin"
if [ ! -f "$BIN_PATH" ]; then
echo " Ladataan $BINARY..."
curl -sSL "$BASE_URL/$BINARY" -o "$BIN_PATH"
chmod +x "$BIN_PATH"
fi
echo ""
echo " ✓ Yhdistetään laskentaverkkoon..."
echo " Ctrl+C pysäyttää"
echo ""
HUB_URL="$HUB_URL" OLLAMA_URL="$OLLAMA_URL" OLLAMA_MODEL="$MODEL" exec "$BIN_PATH"

View File

@@ -20,7 +20,7 @@
"pyproject.toml": {
"description": "Project dependencies",
"example": "[project]\nname = \"myapp\"\nversion = \"0.1.0\"\nrequires-python = \">=3.11\"\ndependencies = [\n \"fastapi\",\n \"uvicorn[standard]\",\n \"sqlalchemy\",\n]\n\n[project.scripts]\ndev = \"uvicorn main:app --reload\"",
"instructions": "List the exact dependencies needed. Use [project.scripts] for run commands."
"instructions": "Use [project] format (PEP 621, compatible with uv). List dependencies under [project.dependencies]. Add [project.scripts] with dev command. Never use requirements.txt or Poetry format. Run with: uv run uvicorn main:app --reload"
}
},
"order": ["models.py", "schemas.py", "main.py", "pyproject.toml"]

View File

@@ -41,7 +41,7 @@
<!-- System prompt -->
<div style="margin-bottom:10px" title="Agentin perusohje joka lähetetään kielimallille jokaisessa pyynnössä.&#10;&#10;Hyvän promptin rakenne:&#10;1. Rooli: 'You are an expert...'&#10;2. Säännöt: RULES/CRITICAL RULES listana&#10;3. Esimerkit: EXAMPLE OUTPUT&#10;4. Kiellot: NEVER-lista&#10;&#10;Vinkki: käytä englantia — malli ymmärtää sen paremmin ja se kuluttaa vähemmän tokeneita.">
<label style="font-size:12px;color:#8b949e;display:block;margin-bottom:4px;cursor:help">System prompt 💡</label>
<textarea id="config-prompt" rows="8" style="width:100%;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:4px;padding:8px;font-size:13px;font-family:'Courier New',monospace;resize:vertical" placeholder="Kuvaa agentin rooli ja käyttäytyminen..."></textarea>
<textarea id="config-prompt" style="width:100%;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:4px;padding:8px;font-size:13px;font-family:'Courier New',monospace;resize:vertical;overflow:hidden;min-height:60px" placeholder="Kuvaa agentin rooli ja käyttäytyminen..."></textarea>
</div>
<!-- Sampling-parametrit -->

View File

@@ -10,6 +10,39 @@
<span id="compute-dot" class="status-dot" style="background:#30363d"></span>
<span style="color:#8b949e">Laskenta:</span>
<span id="compute-label" style="color:#8b949e">—</span>
<button id="compute-btn" class="btn btn-accent" title="Käynnistä kielimalli">Alusta</button>
<button id="compute-btn" class="btn btn-accent" title="Käynnistä kielimalli selaimessa">Alusta</button>
</span>
<span class="status-separator">│</span>
<span class="status-group">
<button id="join-btn" class="btn btn-green" onclick="showJoinDialog()" title="Liitä oma koneesi laskentaverkkoon (natiivi, nopea)">+ Liitä koneesi</button>
</span>
</div>
<!-- Join-dialogi -->
<div id="join-dialog" style="display:none;margin-top:8px;padding:16px;background:var(--panel);border:1px solid var(--border);border-radius:6px;font-size:14px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
<span style="color:#e6edf3;font-weight:600;font-size:16px">Liitä koneesi laskentaverkkoon</span>
<button onclick="document.getElementById('join-dialog').style.display='none'" style="background:none;border:none;color:#8b949e;cursor:pointer;font-size:18px">✕</button>
</div>
<p style="color:#8b949e;margin-bottom:16px">Koneesi suorittaa tehtäviä ~10-50x nopeammin kuin selainlaskenta. Kaksi vaihetta:</p>
<!-- Vaihe 1: Ollama -->
<div style="margin-bottom:14px;padding:12px;background:var(--bg);border-radius:4px;border-left:3px solid var(--accent)">
<div style="color:#e6edf3;font-weight:600;margin-bottom:6px">1. Asenna Ollama <span style="color:#8b949e;font-weight:normal">(kielimallimoottori)</span></div>
<div style="display:flex;gap:6px;align-items:center;margin-bottom:6px">
<code style="flex:1;background:#010409;padding:8px 12px;border-radius:4px;color:var(--green);font-family:'Courier New',monospace;font-size:13px;user-select:all">curl -fsSL https://ollama.ai/install.sh | sh</code>
<button onclick="navigator.clipboard.writeText('curl -fsSL https://ollama.ai/install.sh | sh');this.textContent='✓';setTimeout(()=>this.textContent='Kopioi',1500)" class="btn btn-accent" style="padding:6px 10px">Kopioi</button>
</div>
<div style="color:#8b949e;font-size:12px">macOS: <code style="color:var(--accent)">brew install ollama</code> · Windows: <a href="https://ollama.ai/download" target="_blank" style="color:var(--accent)">ollama.ai/download</a> · Jos jo asennettu → siirry vaiheeseen 2.</div>
</div>
<!-- Vaihe 2: Kipinä-node -->
<div style="padding:12px;background:var(--bg);border-radius:4px;border-left:3px solid var(--green)">
<div style="color:#e6edf3;font-weight:600;margin-bottom:6px">2. Käynnistä Kipinä-node</div>
<div style="display:flex;gap:6px;align-items:center;margin-bottom:6px">
<code style="flex:1;background:#010409;padding:8px 12px;border-radius:4px;color:var(--green);font-family:'Courier New',monospace;font-size:13px;user-select:all">curl -sSL https://kipina.studio/kipina-node -o kipina-node && chmod +x kipina-node && ./kipina-node</code>
<button onclick="navigator.clipboard.writeText('curl -sSL https://kipina.studio/kipina-node -o kipina-node && chmod +x kipina-node && ./kipina-node');this.textContent='✓';setTimeout(()=>this.textContent='Kopioi',1500)" class="btn btn-green" style="padding:6px 10px">Kopioi</button>
</div>
<div style="color:#8b949e;font-size:12px">Lataa kielimallin (~2GB) automaattisesti ensimmäisellä kerralla. Ctrl+C pysäyttää.</div>
</div>
</div>

View File

@@ -53,6 +53,11 @@ import Settings from "../components/Settings.astro";
<script is:inline>
// === Helpers ===
window.showJoinDialog = function() {
const d = document.getElementById('join-dialog');
d.style.display = d.style.display === 'none' ? 'block' : 'none';
};
function esc(str) {
if (!str) return '';
return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
@@ -66,7 +71,7 @@ import Settings from "../components/Settings.astro";
// === Globaalit tilat ===
const defaultAgents = {
manager: { name: 'Manageri', avatar: '/avatars/karhunpentu.png', model: 'qwen-coder', order: 0,
manager: { name: 'Manageri', avatar: '/avatars/karhunpentu.webp', model: 'qwen-coder', order: 0,
temperature: 0.5, topK: 40, repeatPenalty: 1.15, maxTokens: 512,
prompt: `You are a senior project manager and software architect. Your job is to plan the file structure of a software project.
@@ -83,7 +88,7 @@ models.py: SQLAlchemy database models and engine setup
schemas.py: Pydantic request/response schemas
main.py: FastAPI application with CRUD endpoints
pyproject.toml: project dependencies` },
coder: { name: 'Koodari', avatar: '/avatars/kipina_notext.png', model: 'qwen-coder', order: 1,
coder: { name: 'Koodari', avatar: '/avatars/kipina_notext.webp', model: 'qwen-coder', order: 1,
temperature: 0.7, topK: 40, repeatPenalty: 1.15, maxTokens: 1024,
prompt: `You are an expert Python developer. Write complete, production-ready code.
@@ -101,8 +106,10 @@ NEVER:
- Add explanations or comments like "# Add routes here"
- Leave placeholder code or TODO comments
- Use Flask syntax (app.run) in FastAPI projects
- Forget to import from other project files` },
data: { name: 'Data', avatar: '/avatars/pesukarhu_notext.png', model: 'qwen-coder', order: 2,
- Forget to import from other project files
- Use requirements.txt or Poetry — always use pyproject.toml with [project] format (PEP 621)
- Use pip install — use uv (e.g. uv run uvicorn main:app --reload)` },
data: { name: 'Data', avatar: '/avatars/pesukarhu_notext.webp', model: 'qwen-coder', order: 2,
temperature: 0.5, topK: 40, repeatPenalty: 1.15, maxTokens: 1024,
prompt: `You are a database architect specializing in SQLAlchemy and relational databases.
@@ -119,7 +126,7 @@ ALWAYS INCLUDE:
- from sqlalchemy.ext.declarative import declarative_base
- from sqlalchemy.orm import sessionmaker
- DATABASE_URL, engine, SessionLocal, Base` },
qa: { name: 'QA', avatar: '/avatars/susi_notext.png', model: 'qwen-coder', order: 3,
qa: { name: 'QA', avatar: '/avatars/susi_notext.webp', model: 'qwen-coder', order: 3,
temperature: 0.4, topK: 40, repeatPenalty: 1.15, maxTokens: 1024,
prompt: `You are a QA engineer writing automated tests.
@@ -136,24 +143,26 @@ TEST STRUCTURE:
5. test_delete: DELETE → 204, verify GET returns 404 after
ALWAYS: from fastapi.testclient import TestClient` },
tester: { name: 'DevOps', avatar: '/avatars/laiskiainen_notext.png', model: 'qwen-coder', order: 4,
tester: { name: 'DevOps', avatar: '/avatars/laiskiainen_notext.webp', model: 'qwen-coder', order: 4,
temperature: 0.3, topK: 40, repeatPenalty: 1.1, maxTokens: 512,
prompt: `You are a strict code reviewer. Review the provided code and check for these issues:
prompt: `You are a strict code reviewer and static analysis expert. Analyze the code line by line.
CHECKLIST:
1. ✓ All imports exist (no missing "from X import Y")
2. ✓ Import names match: if models.py exports "User", main.py imports "User" (not "UserModel")
3. ✓ Pydantic schema names don't conflict with SQLAlchemy model names
4. ✓ All CRUD endpoints have error handling (404 for not found)
5. Database session is properly closed (get_db with yield + finally)
6. ✓ Response models are specified for type safety
7. ✓ No placeholder comments like "# Add routes here"
STATIC ANALYSIS CHECKLIST:
1. IMPORTS: Every "from X import Y" must match an actual export in file X
2. NAMES: Pydantic schemas (UserCreate) must not shadow SQLAlchemy models (User)
3. TYPES: All function parameters have type hints, return types specified
4. ERRORS: Every db query that can return None has a 404 check
5. RESOURCES: Database session uses yield+finally pattern (no leaks)
6. SECURITY: No raw SQL, no hardcoded secrets, inputs validated via Pydantic
7. ENDPOINTS: All CRUD operations exist (POST/GET/GET-by-id/PUT/DELETE)
8. MODELS: Pydantic Config has from_attributes=True, uses model_dump() not dict()
9. COMPLETENESS: No placeholder comments, no "TODO", no "pass" in handlers
RESPOND:
- If all checks pass: "LGTM"
- If issues found: list each as "ISSUE: filename.py: description"
- Be specific and actionable, not vague` },
observer: { name: 'Tarkkailija', avatar: '/avatars/aikuinen_susi.png', model: 'qwen-coder', order: 5,
observer: { name: 'Tarkkailija', avatar: '/avatars/aikuinen_susi.webp', model: 'qwen-coder', order: 5,
temperature: 0.6, topK: 40, repeatPenalty: 1.15, maxTokens: 512,
prompt: `You are an independent technical observer and risk analyst.
@@ -254,7 +263,11 @@ OUTPUT FORMAT:
document.getElementById('config-name').value = a.name;
document.getElementById('config-role').textContent = key;
document.getElementById('config-model').value = a.model;
document.getElementById('config-prompt').value = a.prompt || '';
const promptEl = document.getElementById('config-prompt');
promptEl.value = a.prompt || '';
// Auto-resize: textarea kasvaa sisällön mukaan
promptEl.style.height = 'auto';
promptEl.style.height = promptEl.scrollHeight + 'px';
// Sampling-parametrit
const tempEl = document.getElementById('config-temperature');
@@ -281,7 +294,7 @@ OUTPUT FORMAT:
// Muutosten tallennus
document.getElementById('config-name').oninput = () => { agents[key].name = document.getElementById('config-name').value; saveAgents(); renderAgentBar(); };
document.getElementById('config-model').onchange = () => { agents[key].model = document.getElementById('config-model').value; saveAgents(); };
document.getElementById('config-prompt').oninput = () => { agents[key].prompt = document.getElementById('config-prompt').value; saveAgents(); };
promptEl.oninput = () => { agents[key].prompt = promptEl.value; saveAgents(); promptEl.style.height = 'auto'; promptEl.style.height = promptEl.scrollHeight + 'px'; };
tempEl.oninput = () => { agents[key].temperature = +tempEl.value; tempValEl.textContent = tempEl.value; saveAgents(); };
maxtokEl.oninput = () => { agents[key].maxTokens = +maxtokEl.value; maxtokValEl.textContent = maxtokEl.value; saveAgents(); };
topkEl.oninput = () => { agents[key].topK = +topkEl.value; topkValEl.textContent = topkEl.value; saveAgents(); };
@@ -307,7 +320,7 @@ OUTPUT FORMAT:
// Uuden agentin luonti
window.addCustomAgent = function() {
const id = 'custom_' + Date.now();
const avatars = ['/avatars/bear.png','/avatars/beaver.png','/avatars/gecko.png','/avatars/lion.png','/avatars/penguin.png','/avatars/spider.png','/avatars/walrus.png','/avatars/serpent.png'];
const avatars = ['/avatars/bear.webp','/avatars/beaver.webp','/avatars/gecko.webp','/avatars/lion.webp','/avatars/penguin.webp','/avatars/spider.webp','/avatars/walrus.webp','/avatars/serpent.webp'];
agents[id] = {
name: 'Uusi agentti',
avatar: avatars[Math.floor(Math.random() * avatars.length)],
@@ -724,7 +737,6 @@ OUTPUT FORMAT:
async function kpnProject(task) {
const cdr = agents.coder || Object.values(agents)[1];
const tst = agents.tester || Object.values(agents)[2];
// Etsitään sopivin mallipohja
const template = Object.values(templates)[0]; // Toistaiseksi vain FastAPI CRUD
@@ -791,43 +803,74 @@ OUTPUT FORMAT:
const allCode = Object.entries(files).map(([n,c]) => `--- ${n} ---\n${c}`).join('\n\n');
let stepN = template.order.length + 1;
// DevOps/Testaaja: koodikatselmointi
// Review-korjausluuppi: max 2 kierrosta
const tst = agents.tester || Object.values(agents)[4];
termLog(`\n<span style="color:var(--accent);font-weight:bold">[${stepN}] ${esc(tst.name)}</span> — koodikatselmointi`);
highlightAgent('tester');
explainStep('Koodikatselmointi', `${tst.name} tarkistaa importit, nimeämiset, virheenkäsittelyn ja tiedostojen yhteensopivuuden.`);
const tstPrompt = (tst.prompt ? tst.prompt+'\n\n' : '') + `Review this project:\n\n${allCode}`;
const review = await kpnRun(tst.model, tstPrompt);
stepN++;
const MAX_REVIEW_ROUNDS = 3;
// Korjausluuppi (jos tarpeen)
if (review && !review.toLowerCase().includes('lgtm')) {
termLog(`\n<span style="color:#d29922;font-weight:bold">[${stepN}] ${esc(cdr.name)}</span> — korjaukset`);
highlightAgent('coder');
explainStep('Korjausluuppi', `${tst.name} löysi ongelmia. ${cdr.name} saa palautteen ja korjaa koodin.`);
await kpnRun(cdr.model, `${cdr.prompt ? cdr.prompt+'\n\n' : ''}Fix these issues:\n${review}\n\nCurrent code:\n${allCode}\n\nWrite the corrected files.`);
for (let round = 0; round < MAX_REVIEW_ROUNDS; round++) {
const currentCode = Object.entries(files).map(([n,c]) => `--- ${n} ---\n${c}`).join('\n\n');
// DevOps review
termLog(`\n<span style="color:var(--accent);font-weight:bold">[${stepN}] ${esc(tst.name)}</span> — koodikatselmointi${round > 0 ? ' (kierros '+(round+1)+')' : ''}`);
highlightAgent('tester');
if (round === 0) explainStep('Koodikatselmointi', `${tst.name} analysoi koodin rivi riviltä: importit, nimeämiset, virheenkäsittely, tietoturva.`);
else explainStep('Uudelleentarkistus', `${tst.name} tarkistaa korjaukset.`);
const reviewPrompt = (tst.prompt ? tst.prompt+'\n\n' : '') + `Review this project:\n\n${currentCode}`;
const review = await kpnRun(tst.model, reviewPrompt);
stepN++;
}
// QA: testit
// LGTM → ei korjauksia tarvita
if (!review || review.toLowerCase().includes('lgtm')) {
termLog(` <span style="color:#3fb950">✓ ${esc(tst.name)}: LGTM</span>`);
break;
}
// Korjaukset
termLog(`\n<span style="color:#d29922;font-weight:bold">[${stepN}] ${esc(cdr.name)}</span> — korjaukset${round > 0 ? ' (kierros '+(round+1)+')' : ''}`);
highlightAgent('coder');
explainStep('Korjaus', `${tst.name} löysi ongelmia. ${cdr.name} saa palautteen ja korjaa.`);
const fixPrompt = `${cdr.prompt ? cdr.prompt+'\n\n' : ''}Fix these issues:\n${review}\n\nCurrent code:\n${currentCode}\n\nWrite ALL corrected files. Start each file with: --- filename.py ---`;
const fixedCode = await kpnRun(cdr.model, fixPrompt);
// Parsitaan korjatut tiedostot takaisin files-objektiin
if (fixedCode) {
const fixedParts = fixedCode.split(/^---\s*(\S+)\s*---$/m);
for (let j = 1; j < fixedParts.length; j += 2) {
const fname = fixedParts[j].trim();
const fcode = (fixedParts[j+1] || '').trim();
if (fname && fcode && files[fname] !== undefined) {
files[fname] = fcode;
}
}
}
stepN++;
} // for review round
// Päivitetään allCode korjausten jälkeen
const updatedCode = Object.entries(files).map(([n,c]) => `--- ${n} ---\n${c}`).join('\n\n');
// QA: testit (saa korjatut tiedostot)
const qaAgent = agents.qa || Object.values(agents)[3];
if (qaAgent) {
termLog(`\n<span style="color:#d2a8ff;font-weight:bold">[${stepN}] ${esc(qaAgent.name)}</span> — testit`);
highlightAgent('qa');
explainStep('Testit', `${qaAgent.name} kirjoittaa pytest-testit kaikille endpointeille.`);
const qaPrompt = (qaAgent.prompt ? qaAgent.prompt+'\n\n' : '') + `Write pytest tests for this project:\n\n${allCode}\n\nWrite a complete test_main.py file with TestClient.`;
explainStep('Testit', `${qaAgent.name} kirjoittaa pytest-testit korjatulle koodille.`);
const qaPrompt = (qaAgent.prompt ? qaAgent.prompt+'\n\n' : '') + `Write pytest tests for this project:\n\n${updatedCode}\n\nWrite a complete test_main.py file with TestClient.`;
const tests = await kpnRun(qaAgent.model, qaPrompt);
if (tests) files['test_main.py'] = tests;
stepN++;
}
// DevOps: Dockerfile
// DevOps: Dockerfile (saa kaikki tiedostot mukaan lukien testit)
const allFilesNow = Object.keys(files).join(', ');
termLog(`\n<span style="color:var(--accent);font-weight:bold">[${stepN}] ${esc(tst.name)}</span> — Dockerfile`);
highlightAgent('tester');
explainStep('Dockerfile', `${tst.name} generoi Docker-kontin joka pakkaa projektin ajettavaksi.`);
explainStep('Dockerfile', `${tst.name} generoi Docker-kontin kaikista ${Object.keys(files).length} tiedostosta: ${allFilesNow}`);
const dockerPrompt = (tst.prompt ? tst.prompt+'\n\n' : '') +
`Write a Dockerfile for this Python FastAPI project.\n\n` +
`Project files: ${Object.keys(files).join(', ')}\n\n` +
`Project files: ${allFilesNow}\n\n` +
`Requirements:\n` +
`- Use python:3.12-slim as base\n` +
`- Install uv: COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv\n` +
@@ -846,15 +889,29 @@ OUTPUT FORMAT:
highlightAgent('observer');
explainStep('Raportti', `${obs.name} kokoaa yhteenvedon ja antaa arvosanan.`);
const finalCode = Object.entries(files).map(([n,c]) => `--- ${n} ---\n${c}`).join('\n\n');
const fileList = Object.keys(files).join(', ');
const obsPrompt = (obs.prompt ? obs.prompt+'\n\n' : '') +
`Write a project README.md report in markdown for: ${task}\n\n` +
`IMPORTANT: Start the FIRST LINE with exactly one of these verdicts:\n` +
`VERDICT: GREEN — project is production-ready, no issues\n` +
`VERDICT: ORANGE — project works but has warnings or improvements needed\n` +
`VERDICT: RED — project has critical issues that must be fixed\n\n` +
`Then include:\n` +
`# Project: ${task}\n` +
`## Files\n## How to run\n## API Endpoints\n## Architecture\n## Risk assessment\n\n` +
`Write a project report in clean markdown for: ${task}\n\n` +
`FIRST LINE must be exactly one of:\n` +
`VERDICT: GREEN\nVERDICT: ORANGE\nVERDICT: RED\n\n` +
`Then write this report:\n\n` +
`# ${task}\n\n` +
`## Overview\nOne paragraph describing what this project does.\n\n` +
`## Files\n| File | Purpose |\n|------|---------|` +
Object.entries(files).map(([n]) => `\n| ${n} | ... |`).join('') + `\n\n` +
`## Quick Start\n` +
'```bash\n' +
`git clone <repo>\ncd project\nuv sync\nuv run uvicorn main:app --reload\n` +
'```\n\n' +
`## Docker\n` +
'```bash\n' +
`docker build -t ${task.toLowerCase().replace(/[^a-z0-9]/g, '-')} .\ndocker run -p 8000:8000 ${task.toLowerCase().replace(/[^a-z0-9]/g, '-')}\n` +
'```\n\n' +
`## API Endpoints\n| Method | Path | Description |\n|--------|------|-------------|` +
`\n| POST | /items/ | Create |\n| GET | /items/ | List all |\n| GET | /items/{id} | Get by ID |\n| PUT | /items/{id} | Update |\n| DELETE | /items/{id} | Delete |\n` +
`(Adapt paths and descriptions to match the actual code)\n\n` +
`## Architecture\nDescribe the project structure and design decisions.\n\n` +
`## Risk Assessment\n| Severity | Issue |\n|----------|-------|\n| ... | ... |\n\n` +
`Project code:\n${finalCode}`;
const readme = await kpnRun(obs.model, obsPrompt);
if (readme) {

View File

@@ -14,19 +14,20 @@
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 16px;
background: var(--bg);
color: var(--text);
min-height: 100vh;
}
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
.container { max-width: 1600px; margin: 0 auto; padding: 20px 40px; }
/* Tabs */
.tabs { display: flex; gap: 4px; margin-bottom: 16px; }
.tab {
padding: 8px 16px; border-radius: 6px 6px 0 0; cursor: pointer;
padding: 10px 20px; border-radius: 6px 6px 0 0; cursor: pointer;
border: 1px solid var(--border); border-bottom: none;
background: var(--bg); color: #8b949e; font-size: 14px;
background: var(--bg); color: #8b949e; font-size: 15px;
}
.tab.active { background: var(--panel); color: var(--accent); border-color: var(--border); }
@@ -37,9 +38,9 @@ body {
/* Status bar */
.status-bar {
display: flex; align-items: center; gap: 12px;
padding: 8px 14px; background: var(--bg);
padding: 10px 16px; background: var(--bg);
border: 1px solid var(--border); border-radius: 6px 6px 0 0;
font-family: 'Courier New', monospace; font-size: 13px;
font-family: 'Courier New', monospace; font-size: 14px;
}
.status-dot {
width: 8px; height: 8px; border-radius: 50%; display: inline-block;
@@ -50,21 +51,22 @@ body {
/* Terminal */
.terminal {
background: #010409; border: 1px solid var(--border); border-top: none;
font-family: 'Courier New', monospace; font-size: 14px;
min-height: 300px; max-height: 60vh; overflow-y: auto;
padding: 8px 12px;
font-family: 'Courier New', monospace; font-size: 16px;
min-height: 400px; max-height: 70vh; overflow-y: auto;
padding: 12px 16px;
}
.terminal-line { padding: 1px 0; white-space: pre-wrap; word-break: break-word; }
.terminal-prompt { color: var(--yellow); margin-right: 8px; }
.terminal-input-row {
display: flex; align-items: center; position: relative;
background: #010409; border: 1px solid var(--border); border-top: none;
border-radius: 0 0 6px 6px; padding: 8px 12px;
font-family: 'Courier New', monospace; font-size: 14px;
background: #0d1117; border: 1px solid var(--accent); border-top: none;
border-radius: 0 0 6px 6px; padding: 10px 14px;
font-family: 'Courier New', monospace; font-size: 15px;
box-shadow: 0 2px 8px rgba(88,166,255,0.1);
}
.terminal-input {
flex: 1; background: transparent; border: none; outline: none;
color: var(--green); font-family: inherit; font-size: inherit;
color: var(--green); font-family: inherit; font-size: 16px;
}
.terminal-dropdown {
display: none; position: absolute; bottom: 100%; left: 30px;
@@ -128,10 +130,10 @@ body {
background: linear-gradient(145deg, rgba(33,38,45,0.4) 0%, rgba(13,17,23,0.8) 100%);
backdrop-filter: blur(12px);
border: 1px solid rgba(240,246,252,0.1);
border-radius: 12px;
padding: 6px 6px 4px;
border-radius: 14px;
padding: 8px 8px 6px;
text-align: center;
width: 72px;
width: 90px;
opacity: 0.8;
cursor: pointer;
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
@@ -144,12 +146,12 @@ body {
box-shadow: 0 8px 14px rgba(0,0,0,0.4);
}
.agent-avatar img {
width: 50px; height: 50px; border-radius: 12px;
width: 64px; height: 64px; border-radius: 14px;
margin-bottom: 4px; border: 2px solid rgba(240,246,252,0.1);
transition: all 0.4s ease; object-fit: cover;
}
.agent-avatar .avatar-name {
font-size: 10px; color: #8b949e; white-space: nowrap;
font-size: 11px; color: #8b949e; white-space: nowrap;
overflow: hidden; text-overflow: ellipsis;
}
.agent-avatar.active {

View File

@@ -1,6 +1,6 @@
[package]
name = "hub"
version = "0.3.0"
version = "0.3.1"
edition = "2024"
[dependencies]

View File

@@ -34,7 +34,7 @@ struct AppState {
total_tasks: Mutex<u64>,
stats_tx: broadcast::Sender<String>,
node_channels: tokio::sync::RwLock<HashMap<u64, tokio::sync::mpsc::UnboundedSender<String>>>, // Kohdennettu reititys
pending_consensus: tokio::sync::RwLock<HashMap<String, Vec<serde_json::Value>>>, // Proof of Compute -konsensus
_pending_consensus: tokio::sync::RwLock<HashMap<String, Vec<serde_json::Value>>>, // Proof of Compute -konsensus
feature_flags: tokio::sync::RwLock<HashMap<String, bool>>, // Tuntee TODO.md:n ruksit lennosta
ip_connections: Mutex<HashMap<IpAddr, u32>>,
node_ips: Mutex<HashMap<u64, IpAddr>>,
@@ -256,7 +256,7 @@ async fn main() {
total_tasks: Mutex::new(0),
stats_tx: stats_tx.clone(),
node_channels: tokio::sync::RwLock::new(HashMap::new()),
pending_consensus: tokio::sync::RwLock::new(HashMap::new()),
_pending_consensus: tokio::sync::RwLock::new(HashMap::new()),
feature_flags: tokio::sync::RwLock::new(HashMap::new()),
ip_connections: Mutex::new(HashMap::new()),
node_ips: Mutex::new(HashMap::new()),
@@ -502,7 +502,12 @@ async fn ws_handler(
) -> impl IntoResponse {
// Origin-tarkistus — estää cross-site WebSocket hijackingin
if let Some(origin) = headers.get("origin").and_then(|v| v.to_str().ok()) {
if !ALLOWED_ORIGINS.iter().any(|&allowed| origin == allowed) {
let is_allowed = ALLOWED_ORIGINS.iter().any(|&allowed| origin == allowed)
|| origin.starts_with("http://192.168.")
|| origin.starts_with("http://10.")
|| origin.starts_with("http://172."); // LAN-avaruudet
if !is_allowed {
tracing::warn!("Estetty yhteys väärällä originilla: {}", origin);
return (
axum::http::StatusCode::FORBIDDEN,
@@ -1099,7 +1104,7 @@ async fn api_chat_completions(
// Etsitään vapaa solmu — priorisoidaan natiivisolmut (GPU) selaimen edelle
let (target_node, _total_matching) = {
let tasks = state.node_tasks.lock().unwrap();
let busy = state.node_busy.lock().unwrap();
let _busy = state.node_busy.lock().unwrap();
let node_types = state.node_types.lock().unwrap();
let matching: Vec<u64> = tasks.iter().filter(|(_, task)| {
// Eksakti match tai qwen-perheen yhteensopivuus (selain: qwen-coder-05b, natiivi: qwen2.5-coder:7b)

59
network-poc/install.sh Executable file
View File

@@ -0,0 +1,59 @@
#!/bin/bash
# Kipinä Agentic Studio — asennusskripti (Debian/Ubuntu)
set -e
echo "=== Kipinä Agentic Studio — Asennus ==="
echo ""
# Tarkistetaan käyttöjärjestelmä
if [ ! -f /etc/debian_version ]; then
echo "⚠ Tämä skripti on suunniteltu Debian/Ubuntu-järjestelmille."
echo " Muilla jakeluilla voit asentaa riippuvuudet manuaalisesti."
read -p " Jatketaanko? (k/e) " -n 1 -r; echo
[[ $REPLY =~ ^[Kk]$ ]] || exit 1
fi
echo "[1/6] Päivitetään pakettilistaus..."
sudo apt-get update -qq
echo "[2/6] Asennetaan peruspaketteja..."
sudo apt-get install -y -qq curl git build-essential pkg-config libssl-dev
# Rust
if command -v rustc &>/dev/null; then
echo "[3/6] Rust löytyi: $(rustc --version)"
else
echo "[3/6] Asennetaan Rust..."
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source "$HOME/.cargo/env"
fi
# Node.js (Astro-frontend vaatii)
if command -v node &>/dev/null; then
echo "[4/6] Node.js löytyi: $(node --version)"
else
echo "[4/6] Asennetaan Node.js 22..."
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y -qq nodejs
fi
# Ollama
if command -v ollama &>/dev/null; then
echo "[5/6] Ollama löytyi"
else
echo "[5/6] Asennetaan Ollama..."
curl -fsSL https://ollama.ai/install.sh | sh
fi
# Malli
echo "[6/6] Ladataan kielimalli (qwen2.5-coder:3b)..."
ollama pull qwen2.5-coder:3b
echo ""
echo "=== Asennus valmis! ==="
echo ""
echo "Käynnistä:"
echo " cd $(pwd)"
echo " ./network-poc/local.sh"
echo ""
echo "Avaa selaimessa: http://localhost:3000"

View File

@@ -109,7 +109,7 @@ impl LlmEngine {
.map_err(|e| format!("Ollama JSON: {}", e))?;
let text = body["response"].as_str().unwrap_or("").to_string();
let total_duration_ns = body["total_duration"].as_u64().unwrap_or(0);
let _total_duration_ns = body["total_duration"].as_u64().unwrap_or(0);
let eval_count = body["eval_count"].as_u64().unwrap_or(0) as usize;
let eval_duration_ns = body["eval_duration"].as_u64().unwrap_or(1);
@@ -129,23 +129,17 @@ impl LlmEngine {
/// Siivoa markdown-koodiblokki-merkit ja selitystekstit
fn strip_code_fences(text: &str) -> String {
let mut result = text.trim().to_string();
// Poista aloittava ```lang
if result.starts_with("```") {
if let Some(nl) = result.find('\n') {
result = result[nl + 1..].to_string();
// Poistetaan kaikki ```-rivit ja kielitunnisteet (```python, ```rust jne.)
let lines: Vec<&str> = text.lines().collect();
let filtered: Vec<&str> = lines.into_iter().filter(|line| {
let trimmed = line.trim();
// Poista rivit jotka ovat pelkkiä ``` tai ```kielitunniste
if trimmed.starts_with("```") {
return false;
}
}
// Poista sulkeva ```
let trimmed = result.trim_end();
if trimmed.ends_with("```") {
let before = &trimmed[..trimmed.len() - 3];
if before.is_empty() || before.ends_with('\n') {
result = before.trim_end().to_string();
}
}
true
}).collect();
let mut result = filtered.join("\n").trim().to_string();
// Poista selitysteksti lopusta (kaikki rivin "\nPlease note" jälkeen jne.)
let lower = result.to_lowercase();

View File

@@ -347,22 +347,26 @@ async fn main() {
if let Some(ref engine) = llm {
let max_tokens = task.get("max_tokens").and_then(|v| v.as_u64()).unwrap_or(1024) as usize;
tracing::info!("Generoidaan (task_id: {}, max_tokens: {}): \"{}\"", task_id, max_tokens, &prompt[..prompt.len().min(100)]);
let prompt_lines = prompt.lines().count();
let prompt_last: String = prompt.lines().last().unwrap_or("").chars().take(60).collect();
tracing::info!("→ task_id:{} | {}r prompti | \"{}...\"", task_id, prompt_lines, prompt_last);
let model_name = engine.model_name();
match engine.generate(prompt, max_tokens).await {
Ok(result) => {
tracing::info!(
"Tulos: {} tokenia | {:.0}ms | {:.1} tok/s | \"{}\"",
"✓ {} | {} tok | {:.0}ms | {:.1} tok/s",
model_name,
result.tokens_generated,
result.duration_ms,
result.tokens_per_sec,
&result.text[..result.text.len().min(80)]
);
// Lähetetään vain lyhyt prompti-esikatselu (ei koko kontekstia)
let prompt_short: String = prompt.lines().last().unwrap_or("").chars().take(100).collect();
let done = json!({
"type": "llm_done",
"prompt": prompt,
"prompt": prompt_short,
"model": format!("{} (Ollama)", model_name),
"response": result.text,
"tokens_generated": result.tokens_generated,

View File

@@ -348,7 +348,9 @@ pub async fn start_agent_node(hub_url: String, has_webgpu: bool, device_info_jso
});
}
}
} else if msg.contains("llm_prompt") && (current_task == 4 || current_task == 5) {
} else if msg.contains("llm_prompt") {
console_log!("[DEBUG] llm_prompt vastaanotettu! current_task={}, busy={}", current_task, LLM_BUSY.load(Ordering::SeqCst));
if current_task == 4 || current_task == 5 {
// Qwen2.5-Coder: 4 = 0.5B, 5 = 3B
if let Ok(task) = serde_json::from_str::<serde_json::Value>(&msg) {
let prompt = task.get("prompt").and_then(|v| v.as_str()).unwrap_or("").to_string();
@@ -376,6 +378,7 @@ pub async fn start_agent_node(hub_url: String, has_webgpu: bool, device_info_jso
}
}
}
} // current_task == 4 || 5
} else if msg.contains("ai_task") {
console_log!("Hub task vastaanotettu, ajetaan GPU:lla...");
let ws_for_async = ws_clone.clone();

View File

@@ -248,14 +248,17 @@ async fn get_or_build_model(use_3b: bool, ws: &Rc<RefCell<WebSocket>>) -> Result
/// use_3b: false = 0.5B (nopea), true = 3B (laadukas)
pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use_3b: bool, task_id: Option<String>) {
console_log!("[Coder] run_coder_inference alkaa! prompt={}", &prompt[..prompt.len().min(50)]);
let size_label = if use_3b { "3B" } else { "0.5B" };
let start_load = crate::perf_now();
console_log!("[Coder] Kutsutaan get_or_build_model...");
if let Err(e) = get_or_build_model(use_3b, &ws).await {
console_log!("[Coder] Mallin lataus: {}", e);
console_log!("[Coder] Mallin lataus epäonnistui: {}", e);
return;
}
console_log!("[Coder] Malli valmis, aloitetaan inferenssi");
let load_time = crate::perf_now() - start_load;
if load_time > 100.0 {

View File

@@ -1,413 +0,0 @@
# Kipinä Agentic Studio — Opas
Hajautettu AI-laskentaverkko jossa kielimallit ajavat koodia suoraan selaimessa.
Tämä opas selittää miten kielimallit toimivat, miten niitä ohjataan, ja miten
tuloksia voi parantaa.
---
## Kielimallit ja niiden koot
Kielimalli on neuroverkko joka ennustaa seuraavan sanan (tokenin) edellisten
perusteella. Mallin "koko" tarkoittaa parametrien (painojen) määrää:
| Malli | Parametrit | Koko levyllä | Nopeus selaimessa | Koodinlaatu |
|-------|-----------|-------------|-------------------|-------------|
| SmolLM 135M | 135 miljoonaa | ~270 MB | ~5 tok/s | Yksinkertainen teksti |
| Qwen2.5-Coder:0.5B | 500 miljoonaa | ~990 MB | ~3-6 tok/s | Pienet funktiot |
| Qwen2.5-Coder:3B | 3 miljardia | ~6.2 GB | ~0.4 tok/s | Kokonaiset tiedostot |
| GPT-4 (vertailu) | ~1800 miljardia | ~3.6 TB | pilvipalvelu | Kokonaiset projektit |
**Parametrien vaikutus:** Jokainen parametri on yksi liukuluku (float16 = 2 tavua)
joka tallentaa opittua tietoa. 0.5B-malli tietää perusrakenteet mutta tekee
loogisia virheitä. 3B-malli ymmärtää kontekstin paremmin. Ero on kuin sanakirjan
ja oppikirjan välillä.
**Miksi selaimessa?** Malli ajetaan käyttäjän omalla laitteella WebAssemblyn
kautta. Data ei lähde koneelta, eikä tarvita pilvipalvelua. Haittapuoli on
hitaus — GPU-palvelimella sama 0.5B-malli tuottaa ~100 tok/s.
---
## Tokenit — kielimallin "sanat"
Malli ei näe tekstiä kirjaimina vaan **tokeneina**. Tokeni on yleensä
sanan osa, kokonainen sana tai välilyönti. Tokenisaatio tehdään
BPE-algoritmilla (Byte Pair Encoding) joka oppii yleisimmät
merkkijonot harjoitusdatasta.
### Esimerkki: suomi vs. englanti
Alla oikea tokenisointitulos Qwen2.5-Coder-tokenisaattorilla. Jokainen
värikoodattu lohko on yksi tokeni — huomaa miten suomi vaatii enemmän
tokeneita saman merkityksen välittämiseen:
![Tokenisointivertailu EN/FI](/images/tokenization-example.png)
**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.

View File

@@ -1,34 +0,0 @@
# Kipinä Agentic Playground - Animaatioiden käyttöönotto
Koska Kipinä-verkon agenttien avatarit tällä erää ovat staattisia PNG-kuvatiedostoja, käyttöliittymä hyödyntää CSS-pohjaista pomppimisilmiötä (sekä pulppuavaa 💬 puhekuplaa) "puhumisen" merkkinä. Olemme kuitenkin koodanneet taustalle piilotetun tuen aivioiduille videoloopeille myöhempää käyttöä varten!
Näin saat UI:n tukemaan oikeasti animoituja kasvoja/videoita.
## 1. Luo Animoidut GIF-tiedostot
Valitse mikä tahansa ulkoinen AI-työkalu (kuten HeyGen, Pika v1.0, tai Midjourney+Runway yhdistelmä) ja muunna avatar-kuvat (esim. `kettu_notext.png`) 3-5 sekunnin kestäviksi GIF-loopeiksi. Hahmon leuka tulisi pyöriä tai naama vääntyillä puhuessaan.
## 2. Nimeä Tiedostot Oikein ja Lisää Ne Kansioon
Siirrä uudet GIF-animaatiot samaan kansioon alkuperäisten kuvien kanssa. Muuta niiden nimi siten, että se päättyy tunnisteeseen `_puhuva.gif`.
Esimerkkejä:
- Koodari `kipina_notext.png``kipina_notext_puhuva.gif`
- Manageri `karhunpentu.png``karhunpentu_puhuva.gif`
- Asiakas `kettu_notext.png``kettu_notext_puhuva.gif`
## 3. Aktivoi Koodi
Käännä Kipinä Playground -ohjaimen JavaScript-koodista piilotettu ominaisuus päälle.
Etsi tiedostosta `../index.html` (noin riviltä 1084, `updatePromptEditor`-funktiosta):
```javascript
// Piilotettu ominaisuus: Puhuvien videoiden / gif-animaatioiden kytkentä
window.USE_ANIMATED_GIFS = false;
```
Muuta tuo `false` arvoon `true`:
```javascript
window.USE_ANIMATED_GIFS = true;
```
**Mitä logiikka tekee?**
Aina kun valitset agentin kaaviosta, koodi korvaa aktiivisen kuvakkeen lopussa olevan `.png` -päätteen sanalla `_puhuva.gif` lennosta! Jos poistut agentin valinnasta tai valitset jonkun toisen, koodi vaihtaa kuvan välittömästi takaisin staattiseen `.png`-versioon ja sulkee ilmentymän suun.
Näin saat kaikkien asiantuntijoiden face-track looppeja hallittua yhdellä kädenkäänteellä.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 696 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 757 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 700 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 731 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 711 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 695 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 432 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 650 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 389 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 596 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 496 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 872 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 738 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 813 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 658 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 593 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 563 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 696 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 718 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 513 KiB

Some files were not shown because too many files have changed in this diff Show More