75 Commits

Author SHA1 Message Date
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
Jaakko Vanhala
529a30a6e1 Korjattu harhaanjohtava GPU-viesti: Ollama käyttää GPU:ta automaattisesti
Kun --no-default-features (ei wgpu/nvml), viesti on nyt:
"GPU-tunnistus ei käytössä. Ollama käyttää GPU:ta automaattisesti."
eikä "GPU:ta ei havaittu — CPU-moodissa" (joka oli väärä M2:lla).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:35:00 +03:00
Jaakko Vanhala
7d842529b1 Tarkkailijan raportti: klikkaa avataria → modal + kehäväri arvosanalla
Tarkkailijan vastaus alkaa VERDICT-rivillä:
- GREEN → vihreä kehä → "OK"
- ORANGE → oranssi kehä → "HUOMIOITA"
- RED → punainen kehä → "KRIITTISTÄ"

Kehäväri ja glow jäävät näkyviin pipelinen jälkeen.
Klikkaamalla Tarkkailija-avataria avautuu raportti-modal jossa
README.md renderöidään markdown-muotoiltuna (taulukot, koodi, listat).
Modal sulkeutuu ✕-napista tai klikkaamalla taustaa.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:33:11 +03:00
Jaakko Vanhala
c731c18360 DevOps generoi Dockerfilen + Tarkkailija kirjoittaa README.md-raportin
Uudet pipeline-vaiheet:

DevOps — Dockerfile:
- python:3.12-slim + uv (astral-sh)
- Oikea COPY-järjestys (pyproject.toml → sync → source)
- Expose 8000, CMD uvicorn

Tarkkailija — README.md:
- Markdown-raportti joka sisältää:
  - Tiedostolista ja kuvaukset
  - Käyttöohjeet (uv + Docker)
  - API Endpoints -taulukko
  - Arkkitehtuurihuomiot
  - Riskiarviointi
- README.md lisätään projektikorttin tiedostoihin
  → avattavissa editorissa

Pipeline nyt: Data → Koodari → DevOps review → korjaukset →
QA testit → DevOps Dockerfile → Tarkkailija README → Valmis

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:29:45 +03:00
Jaakko Vanhala
5498eb6cbb Kaikki 6 agenttia osallistuvat pipeline-projektiin
Pipeline-vaiheet:
1. Data-agentti: models.py (tietokanta-asiantuntija)
2. Koodari: schemas.py, main.py (ohjelmistokehittäjä)
3. Koodari: pyproject.toml
4. DevOps: koodikatselmointi (importit, nimeämiset, virheet)
5. Koodari: korjaukset (jos DevOps löysi ongelmia)
6. QA: pytest-testit (test_main.py lisätään projektiin)
7. Tarkkailija: riskianalyysi (arkkitehtuuri, tietoturva)

Data-agentti valitaan automaattisesti models.py/database.py -tiedostoille.
Jokainen vaihe highlightaa oikean avatarin.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:26:57 +03:00
Jaakko Vanhala
43f0aebf54 Poistettu pystylayout — highlight toimii vaakarivillä pipelinen aikana
Avatarit pysyvät aina vaakarivillä. Aktiivinen agentti saa
glow-highlightin kun pipeline etenee (koodari → testaaja → koodari).
Highlight poistuu kun pipeline valmistuu.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:23:06 +03:00
Jaakko Vanhala
6413f0238f Avatarit vaihtuvat pystyyn pipelinen aikana, aktiivinen agentti highlightataan
Pipeline käynnistyessä:
- Avataririvi vaihtuu pystyyn (column) → näkee kuka tekee mitä
- Aktiivinen agentti saa glow-highlightin (coder sininen, tester sininen)
- Korjausvaiheessa koodari highlightataan uudelleen

Pipeline valmistuttua:
- Palautetaan vaakarivi (row)
- Poistetaan highlight

Poistettu manuaalinen layout-toggle-nappi — layout on automaattinen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:17:30 +03:00
Jaakko Vanhala
6b0394586e Mermaid-kaaviot oppaaseen + avatareiden vaaka/pysty-toggle
Mermaid ladataan CDN:stä (esm module). Opas-sivun renderMd tunnistaa
\`\`\`mermaid -koodiblokit ja renderöi ne SVG-kaavioiksi dark-teemalla.

toggleAgentLayout vaihtaa avatareiden suunnan (row/column),
tallennetaan localStorageen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:11:01 +03:00
Jaakko Vanhala
108094b06a Opas-sivun markdown-renderöijä laajennettu: taulukot, inline-muotoilu, listat
Lisätty tuki:
- Taulukot (| header | ... | -parsinta, thead/tbody, border-collapse)
- Inline: **bold**, *italic*, \`code\` (kaikissa elementeissä)
- Numeroidut listat (1. 2. 3.)
- Parempi tyhjien rivien käsittely (8px spacer, ei <br>)
- Otsikkojen border-bottom h1/h2:lle

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:01:54 +03:00
Jaakko Vanhala
d7c974792d Agenttien päivitys kysyy käyttäjältä: OK = oletukset, Peruuta = säilytä omat
Kun AGENTS_VERSION kasvaa eikä localStorage ole tyhjä, näytetään confirm-dialogi:
"Agenttien oletuspromptit on päivitetty. Haluatko ottaa uudet käyttöön?"
- OK: ylikirjoitetaan oletuksilla
- Peruuta: käyttäjän muokkaukset säilyvät

Ensimmäisellä käyttökerralla (tyhjä localStorage) ladataan oletukset ilman kysymystä.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:00:08 +03:00
Jaakko Vanhala
1987eb57a0 Agenttien oletuspromptit päivittyvät automaattisesti (AGENTS_VERSION)
Kun AGENTS_VERSION kasvaa, localStorage ylikirjoitetaan uusilla oletuksilla.
Ei tarvitse enää manuaalisesti tyhjentää localStorage.removeItem('kpn-agents').
Kasvata AGENTS_VERSION aina kun oletusprompteja muutetaan.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 06:58:30 +03:00
Jaakko Vanhala
12ca87415c Poistettu native-noden kovakoodattu system prompt — agentin prompti toimii nyt
Ollaman system-kenttä yliajoi agentin konfiguroiman promptin.
Nyt system-kenttää ei lähetetä ollenkaan — agentin prompti tulee
osana prompt-kenttää (kpnRun koostaa sen frontendissä).

Tämä mahdollistaa per-agentti promptien toimimisen myös natiivilaskennalla.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 06:54:18 +03:00
Jaakko Vanhala
a0e52faa44 Localhost vapautettu IP-yhteysrajasta, tuotannon raja nostettu 20:een
Kehitysympäristössä (127.0.0.1) ei enää yhteysrajaa — useita
selainikkunoita ja native-nodeja voi yhdistää vapaasti.
Tuotannossa raja 10→20 per ulkoinen IP.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 23:14:06 +03:00
Jaakko Vanhala
f910cd8c61 Kattavat promptit + opettavat tooltipit jokaiselle parametrille
Promptit laajennettu moninkertaisiksi jokaiselle agentille:
- Manageri: RULES + EXAMPLE OUTPUT -formaatti
- Koodari: 8 CRITICAL RULES + NEVER-lista (importit, nimeäminen, Pydantic v2)
- Data: SQLAlchemy-spesifit ohjeet (String(length), connect_args, sessionmaker)
- QA: pytest-testirakenne (5 testitapausta enumeroituna)
- DevOps: 7-kohdan CHECKLIST + LGTM/ISSUE-vastausformaatti
- Tarkkailija: 4-alueen arviointi + RISK-formaatti + SHIP IT/NEEDS WORK

Tooltipit (hover 💡):
- Temperature: milloin matala/korkea, suositukset per rooli
- Max tokens: milloin nostaa/laskea, ~1 token ≈ 4 merkkiä
- Top-K: milloin muuttaa, harvoin tarpeen
- Repetition penalty: miksi liian korkea rikkoo koodin
- System prompt: hyvän promptin rakenne (rooli → säännöt → esimerkit → kiellot)

Prompt-tekstikenttä kasvatettu 4 → 8 riviä.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 23:11:11 +03:00
Jaakko Vanhala
91dc7579bc Per-agentti sampling-parametrit: temperature, top-k, max tokens, repetition penalty
Jokainen agentti saa omat parametrit jotka näkyvät avatarin konfigurointipaneelissa:
- Temperature: Manageri 0.5 (tarkka), Koodari 0.7, Testaaja 0.3 (deterministinen)
- Max tokens: Manageri 512, Koodari 1024, Testaaja 512
- Top-K ja Repetition penalty per agentti
- Sliderit reaaliaikaisilla arvoilla

Parametrit tallentuvat localStorageen agentin mukana.
Perustelut: manageri ja testaaja hyötyvät matalasta temperaturesta
(determinismi tärkeää), koodari tarvitsee enemmän tokeneita ja
hieman korkeamman temperaturen luovempiin ratkaisuihin.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 23:03:11 +03:00
Jaakko Vanhala
90c9a7e4fa Asetukset-välilehti: kaikki LLM-parametrit muokattavissa UI:sta
Uusi "Asetukset"-tab jossa:
- System Prompt (tekstikenttä, Courier-fontti)
- Temperature (slider 0-1.5, reaaliaikainen arvo)
- Top-K (slider 1-100)
- Repetition Penalty (slider 1.0-2.0)
- Max Tokens (slider 64-4096)
- Stop-sekvenssit (yksi per rivi)
- Mallinvalinta (dropdown: 1.5B/3B/7B Q4/7B)
- "Palauta oletukset" -nappi

Kaikki tallentuvat localStorageen (kpn-settings).
Jokainen parametri selitetty hint-tekstillä.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 22:44:03 +03:00
Jaakko Vanhala
1216e016c2 Template-pohjainen projektipipeline + opettavat selitykset
Uusi lähestymistapa: sen sijaan, että malli keksii rakenteen tyhjästä,
sille annetaan mallipohja (template) joka sisältää:
- Tiedostojärjestys (models → schemas → main → pyproject.toml)
- Esimerkkikoodi jokaiselle tiedostolle (few-shot)
- Yksityiskohtaiset ohjeet (importit, nimeämiskäytännöt, patternit)

Jokainen vaihe selitetään terminaalissa:
💡 models.py — "Define the SQLAlchemy model. Always include engine
   with check_same_thread=False for SQLite..."
💡 Koodikatselmointi — "Testaaja tarkistaa importit, nimeämiset..."
💡 Tulos — "Aja: uv run uvicorn main:app --reload"

templates/fastapi-crud.json sisältää täydellisen esimerkkiprojektin
jota malli adaptoi käyttäjän kuvaukseen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 22:32:03 +03:00
Jaakko Vanhala
d85cab4bc0 Native-noden vastausten siivous: stop-sekvenssit + selitystekstien poisto
Stop-sekvenssit laajennettu: Please note, This is, Example, ```
strip_code_fences laajennettu poistamaan:
- Selitystekstit lopusta (Please note, This is a basic, Note that, ...)
- Johdantolauseet alusta (Sure!, Here is, Certainly!)
System prompt vahvistettu: "No 'Please note' or 'Here is' text"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 22:04:43 +03:00
Jaakko Vanhala
4fef8824e1 Kattavat oletuspromptit kaikille agenteille
Manageri: arkkitehtuuri + tiedostojako + pyproject.toml
Koodari: importit + Pydantic/SQLAlchemy-erottelu + moderni Python
Data: normalisointi + SQLAlchemy + migration-ystävälliset patternit
QA: pytest + TestClient + edge caset + mock
DevOps: koodireview + importtien tarkistus + LGTM-protokolla
Tarkkailija: riskianalyysi + tietoturva + arkkitehtuurihuomiot

Promptit näkyvät klikkaamalla avataria → konfigurointipaneeli.
Tyhjennä localStorage (kpn-agents) jotta uudet promptit tulevat voimaan.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:59:19 +03:00
Jaakko Vanhala
009bf492c8 Parannetut koodarin promptit + token-raja 512→1024
Koodarin prompti sisältää nyt:
- Import-vihjeen: "from models import ..." aiemmista tiedostoista
- Nimeämisvihjeen: Pydantic-schemat (UserCreate) vs SQLAlchemy (User)
- "Include all necessary imports. Write complete, working code."

Native-noden max_tokens nostettu 512→1024 jotta CRUD-endpointit
mahtuvat yhteen vastaukseen.

Testattu API:n kautta: 3B-malli tuottaa nyt oikeat importit,
erilliset Pydantic-schemat ja kaikki 5 CRUD-endpointtia.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:56:24 +03:00
Jaakko Vanhala
f7e0e8dff8 Status-palkki tunnistaa natiivisolmun: ei enää turhaa Wasm-latausta
Sivulatauksessa tarkistetaan onko hubissa jo laskentasolmu (natiivi/Wasm).
Jos on → "Valmis (natiivi)", llmReady=true, ei Wasm-latausta.
Jos 503 → Wasm-fallback "Alusta"-napista.

Poistettu automaattinen Wasm-käynnistys (ensureNode) sivulatauksessa
koska natiivisolmu hoitaa laskennan.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:48:23 +03:00
Jaakko Vanhala
eb57ee7b92 Avatarit ja tyylit palautettu main-haarasta: glassmorphism-kortit, oikeat hahmot
Avatarit: karhunpentu (Manageri), kipina (Koodari), pesukarhu (Data),
susi (QA), laiskiainen (DevOps), aikuinen_susi (Tarkkailija).

CSS: glassmorphism-taustat, hover-animaatio, active-glow,
pyöristetyt reunat, varjostukset. Sama tyyli kuin main-haarassa.

Huom: tyhjennä localStorage (kpn-agents) selainkonsolin kautta
jotta uudet oletusavatarit tulevat voimaan:
  localStorage.removeItem('kpn-agents')

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:46:21 +03:00
Jaakko Vanhala
84d13153ed Korjattu: agents ja defaultAgents siirretty scriptin alkuun (let hoisting)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:40:35 +03:00
Jaakko Vanhala
8beac57b50 Agenttien konfigurointi: promptit, mallit, järjestys, omat agentit
Klikatessa avataria avautuu konfigurointipaneeli:
- Nimen muokkaus
- Mallinvalinta (0.5B/1.5B/3B/7B)
- System promptin muokkaus
- Pipeline-järjestys (drag & drop avatarit ja pipeline-tagit)
- Agentin poisto

"+" -nappi luo uuden agentin satunnaisella avatarilla.
Konfiguraatio tallennetaan localStorageen (kpn-agents).
Pipeline (kpn project) käyttää agenttien prompteja ja malleja.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:38:30 +03:00
Jaakko Vanhala
44067efdb6 Monaco-lataus refaktoroitu: singleton Promise, parempi virheenkäsittely
initMonaco palauttaa nyt aina saman Promisen (ei moninkertaista latausta).
AMD-loader virheet ja CDN-virheet napataan ja logitetaan.
monacoLoaded-lippu korvattu Promise-pohjaisella tilanhallinnalla.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:24:08 +03:00
Jaakko Vanhala
5528be1812 Korjattu monacoLoaded: siirretty scriptin alkuun ennen switchTab-kutsua
let ei hoistu — monacoLoaded pitää olla määritelty ennen initMonaco-kutsua.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:21:22 +03:00
Jaakko Vanhala
f4cf4c73b9 Korjattu syntaksivirhe: ylimääräinen }); poistettu openInEditorista
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:18:58 +03:00
Jaakko Vanhala
e19852a509 Korjattu Monaco: window.monaco asetetaan eksplisiittisesti AMD-loaderin jälkeen
openInEditor käyttää nyt async/await initMonacoa ja window.monaco-globaalia
eikä oleta AMD-moduulin scopea. Korjaa "monaco is not defined" -virheen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:16:20 +03:00
Jaakko Vanhala
6de0df365e Korjattu projektikortin JSON-parsintavirhe: tiedostot globaaliin muuttujaan
Koodin sisältämät lainausmerkit ja erikoismerkit rikkoivat data-files
HTML-attribuutin. Nyt tiedostot tallennetaan window._projectFiles[id]:hen
ja onclick-handlerit viittaavat siihen suoraan. Ei JSON DOM:ssa.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:11:11 +03:00
Jaakko Vanhala
28f620f901 kpnRun lähettää suoraan hubille, Wasm-fallback vain 503:lla
Ei enää odoteta Wasm-latausta ennen API-kutsua. Jos natiivisolmu
on hubissa, vastaus tulee suoraan. Jos hub palauttaa 503 (ei solmua),
käynnistetään Wasm-fallback ja yritetään uudelleen.

Tämä korjaa tilanteen jossa native-node on jo käynnissä mutta
selain yritti silti ladata Wasmia ensin.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:09:26 +03:00
Jaakko Vanhala
3497f66db7 Agenttiavatarit palautettu: AgentBar-komponentti viidellä roolilla
Manageri (pöllö), Koodari (kameleontti), Testaaja (rukoilijasirkka),
QA (kilpikonna), Data (norsu). Klikattavat, highlight aktiiviselle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:53:35 +03:00
Jaakko Vanhala
1c7362c9b0 Native-node oletusmalli: qwen2.5-coder:3b
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:46:48 +03:00
Jaakko Vanhala
9983c80ef1 Native-noden oletusmalli vaihdettu kvantisoiduksi: qwen2.5-coder:7b-instruct-q4_K_M
Q4-kvantisointi: ~4GB (vs. 7GB), ~40 tok/s M2:lla (vs. ~25 tok/s).
Parempi nopeus/laatu-suhde.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:35:11 +03:00
Jaakko Vanhala
fc1fb33d5e local.sh: käynnistää native-noden automaattisesti jos Ollama on käynnissä
Käynnistysjärjestys: frontend build → hub → native-node (jos Ollama löytyy).
Ilman Ollamaa käytetään selaimen Wasm-laskentaa.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:28:56 +03:00
Jaakko Vanhala
3bee8e8020 Kaikki pipeline-vaiheet käyttävät qwen-coder -mallia (smollm-135m poistettu)
Testaaja ja QA saivat 503 koska smollm-solmua ei ole ladattu.
Kaikki agentit käyttävät nyt samaa qwen-coder -mallia.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:25:04 +03:00
Jaakko Vanhala
f8ea5ed76e Yksinkertaistettu reititys: poistettu busy-tila ja jonotus
Pipelinen peräkkäiset kpnRun-kutsut saivat 503 koska hub merkitsi
solmun busyksi eikä vapauttanut sitä ajoissa. Reititetään aina
ensimmäiselle matchaavalle solmulle. LLM_BUSY suojaa Wasm-puolella.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:22:53 +03:00
Jaakko Vanhala
6c7c2d6dd3 local.sh: npm install automaattisesti jos node_modules puuttuu
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:18:47 +03:00
Jaakko Vanhala
c179b4ab7e .gitignore: node_modules, dist, .astro poistettu gitistä
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:18:16 +03:00
Jaakko Vanhala
a8c4af0975 Frontend uudelleenrakennettu: Astro-komponentit, Wasm pääsäikeessä, ei Workeria
Vanha frontend siirretty temp/. Uusi rakenne:
- StatusBar.astro, Terminal.astro, Editor.astro, Guide.astro
- global.css erillinen
- Wasm pääsäikeessä (ei Worker — yksinkertainen, debugattava)
- Tab-completion, dropdown, projektikortti, Monaco, GUIDE.md
- Ei tokenisointia eikä koodilaboratoriota

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:17:39 +03:00
Jaakko Vanhala
e3fdb91ac5 local.sh: buildaa frontendin ja käynnistää hubin yhdellä komennolla
Käyttö: ./network-poc/local.sh → http://localhost:3000

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 19:42:24 +03:00
Jaakko Vanhala
9925079729 Korjattu laskenta: poistettu warmup (aiheutti busy-konfliktin) + parempi solmun odotus
Ongelma: warmup-prompt käynnisti inferenssin heti yhdistymisen jälkeen,
jolloin oikea kpnRun-prompt tuli solmulle kun se oli vielä busy.
Solmu lähetti llm_error mutta UI:ssa ei ollut käsittelijää → ikuinen odotus.

Korjaukset:
1. Poistettu warmup ensureCoderNode:sta — ei tarvita koska kpnRun
   käynnistää solmun automaattisesti
2. kpnRun odottaa coderWsReady-lippua max 15s (pollaus 500ms välein)
   kiinteän 1s viiveen sijaan
3. Selkeä virheilmoitus jos solmu ei käynnisty

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 19:37:28 +03:00
Jaakko Vanhala
6031737f83 Laskentasolmu käynnistyy automaattisesti: kpnRun + refresh-autostart
Kaksi korjausta laskentaan:
1. kpnRun kutsuu ensureCoderNode() automaattisesti jos solmu ei ole
   vielä käynnissä — käyttäjän ei tarvitse muistaa kpn load
2. localStorage-autostart: jos malli oli ladattu ennen refreshiä,
   ensureCoderNode() ajetaan automaattisesti sivulatauksessa

Tämä korjaa "Ei vapaata solmua" -virheen kpn run coder -komennoissa.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 17:06:51 +03:00
Jaakko Vanhala
b6a8fa2671 Monaco Editor -välilehti: selainpohjainen koodieditori agenttien tuottamalle koodille
Uusi "Editor"-välilehti jossa:
- Monaco Editor (VS Coden ydin) CDN:stä, dark-teema
- Tiedostopuu vasemmalla (klikataan tiedostoa)
- Välilehdet ylhäällä (useita tiedostoja auki)
- Kielitunnistus tiedostopäätteestä (Python, Rust, JS, ...)
- "Avaa editorissa" -nappi projektikorteissa

Monaco ladataan taustalla requestIdleCallback:llä — ei hidasta
sivun käynnistymistä. Editor alustetaan vasta kun sitä tarvitaan.

Projektikortin "Avaa editorissa" -nappi:
1. Avaa Editor-välilehden
2. Luo Monaco-mallit jokaiselle tiedostolle
3. Renderöi tiedostopuun ja välilehdet
4. Avaa ensimmäisen tiedoston editoriin

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 17:04:59 +03:00
Jaakko Vanhala
0dc53dba1c Korjattu Worker-laskentasolmun timing: odotetaan WS-yhteyden avautumista
Ongelma: start_agent_node palautui heti ennen kuin WebSocket ehti avautua.
Worker lähetti 'started' ja warmup lähti liian aikaisin → hub ei löytänyt
solmua → "Ei vapaata solmua" -virhe.

Korjaukset:
1. worker.js: kuuntelee "Yhteys Hubiin avattu" -logia Wasmista ja
   resolveaa started-Promisen vasta sen jälkeen (15s timeout)
2. worker.js: onerror + onunhandledrejection käsittelijät
3. worker.js: console.error välitetään pääsäikeelle
4. index.astro: ensureCoderNode odottaa (await) workerStarted-Promisea
   ennen warmupia ja pending-promptin lähetystä

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 16:55:22 +03:00
Jaakko Vanhala
857afbe111 feat: complete revolution architecture modernization
- Decoupled robust frontend into an Astro framework in `frontend/`.
- Replaced direct WebSocket broadcast with Smart Routing to distribute workload only to idle capable nodes, preventing 503 errors and duplicate responses.
- Rewrote WASM panic points (`unwrap()` handling) into panic-safe match blocks in qwen_coder.rs preventing Node Web Workers from crashing.
- Integrated robust dynamic Three.js 3D visualization.
- Resolved mermaid and THREE.js frontend hydration issues.
2026-04-09 16:38:24 +03:00
Jaakko Vanhala
84b78eb9c6 GPU-tunnistus valinnainen: cargo run --no-default-features toimii ilman nvml/wgpu
Native-node kääntyy nyt macOS:llä ja muilla koneilla ilman NVIDIA-ajureita:
  cargo run --no-default-features  ← vain Ollama, ei GPU-tunnistusta
  cargo run                        ← oletus: GPU-tunnistus mukana (nvml + wgpu)

Feature flag "gpu-detect" kontrolloi nvml-wrapper ja wgpu -riippuvuuksia.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 15:42:35 +03:00
Jaakko Vanhala
4f18377a3b Native-node lähettää NODE_API_KEY auth-viestissä hubille
Luetaan NODE_API_KEY-ympäristömuuttuja ja lisätään api_key-kenttä
auth-viestiin. Hub tarkistaa avaimen ja hylkää solmun jos se ei täsmää.

Käyttö:
  NODE_API_KEY=kpn_sk_abc123 HUB_URL=ws://hub:3000/ws cargo run

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 15:39:48 +03:00
Jaakko Vanhala
7f5bb45138 API-avain -autentikaatio natiivisolmuille
Natiivisolmujen (node_type: native) auth-viesti vaatii api_key-kentän
joka vastaa hubin NODE_API_KEY-ympäristömuuttujaa. Virheellinen avain
sulkee WebSocket-yhteyden.

Selainsolmut eivät vaadi avainta (Origin-validointi suojaa niitä).
Jos NODE_API_KEY ei ole asetettu, kaikki natiivisolmut hyväksytään
(kehitysympäristö).

Käyttö:
  Hub:  NODE_API_KEY=kpn_sk_abc123 cargo run
  Node: NODE_API_KEY=kpn_sk_abc123 cargo run

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 15:39:05 +03:00
973d7a69c7 Galleria: hahmokuvaukset ja ehdotetut roolit kaikille 21 avatarille
Jokaiselle avatarille lisätty:
- Ehdotettu rooli (Arkkitehti, CI/CD, UI/UX, Tech Lead, SRE jne.)
- Persoonakuvaus joka kuvaa hahmon luonnetta ja erikoisosaamista

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 11:57:21 +03:00
aebc64e76e Tofuist poistettu Playgroundilta (jää Agent Builder -esimerkiksi)
Poistettu: avatar-kortti, gallery-head, agentPrompts-entry,
avatarMap, värimapit. Tofuist on edelleen Agent Builderin
esimerkkiagentti ja docs/tofu-cheatsheet.md säilytetään.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 11:43:15 +03:00
48c832c61b Module import absoluuttiseksi: ./pkg/node.js → /pkg/node.js
Suhteellinen polku rikkoi sivun kun navigoitiin suoraan
/avatars/ tai muuhun alihakemistoon (selain yritti ladata
/avatars/pkg/node.js jota ei ollut).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 11:37:47 +03:00
8435bd32a9 Hahmogalleria: #gallery -tabi kaikilla 21 avatarilla
- Uusi välilehti: Galleria (navigointi #gallery)
- Grid-näkymä: avatar, nimi, rooli, tiedostonimi
- Klikkaa korttia → kopioi polku leikepöydälle

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 11:33:25 +03:00
ece41dd622 Export CrewAI: generoi ZIP-projektin agenttitiimistä
Export-nappi Agent Builderissa generoi kipina-crew.zip sisältäen:
- crew.py: CrewAI agentit, tehtävät ja sequential pipeline
- Dockerfile: Python 3.12 + uv
- docker-compose.yml: Ollama + healthcheck + mallilataus + crew
- pyproject.toml: crewai[tools] riippuvuus
- .env: Ollama-asetukset
- README.md: käynnistysohjeet

Käynnistys: docker compose run crew uv run python crew.py "projekti"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 11:26:27 +03:00
c7f3b0d79f Agent Builder: tooltip-ohjeistukset kaikille kentille
- System Prompt: miten kirjoittaa hyvä prompti, esimerkit hyvä/huono
- Temperature: mitä arvot tarkoittavat (0.0–1.0+)
- Top-k: tokenivalinnan laajuus
- Max tokens: vastauksen pituus
- Malli: Ollama-malliesimerkit
- Docs: referenssidokumentin käyttö
- CSS .builder-tip ::after tooltip (white-space: pre-wrap)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 11:20:28 +03:00
8905b50f41 Builder-funktiot window-scopeen (onclick vaatii globaalin)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:54:01 +03:00
43b0612004 Agent Builder 2026-04-08 10:51:35 +03:00
599ac2d2d9 Agent Builder: kaikki 21 avataria valittavissa
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:50:49 +03:00
d1975bd55c Uudet Gemini-generoidut avatar-kuvat (13 kpl)
bear, beaver, chameleon, elephant, gecko, lion, mantis, owl,
penguin, serpent, spider, tortoise, walrus

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:50:21 +03:00
24a8139d3e Agent Builder UI: #builder -tabi, lomake, CRUD-integraatio
- Uusi välilehti: Agent Builder (navigointi #builder)
- Agenttilista: ladataan /api/v1/agents, näytetään kortteina
- Lomake: avatar-valitsin, rooli-template, malli, väri, docs, prompt, parametrit
- Tallenna → POST /api/v1/agents, Poista → DELETE /api/v1/agents/:id
- Avatar-grid: 8 valmista hahmoa valittavissa

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:45:31 +03:00
21aac49a52 Agent Builder: SQLite-taulu + REST API (GET/POST/DELETE)
- DB skeemaversio 3: agents-taulu (id, name, avatar, role, model, color,
  docs, prompt, temperature, top_k, max_tokens, repetition_penalty)
- CRUD: upsert_agent, get_agents, delete_agent
- API: GET/POST /api/v1/agents, DELETE /api/v1/agents/:id
- Oletusagentteja (is_default=1) ei voi poistaa

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:43:22 +03:00
8a5f1b753c AGENTBUILDER.md: Agent Builder -suunnitelma ja building blockit
Kuvaa hahmolomakkeen arkkitehtuurin: agenttiskeema, rooli-templatet,
malli-valitsin, avatar-grid, docs-kenttä, localStorage-tallennus,
export/import ja 4-vaiheinen toteutussuunnitelma.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:39:25 +03:00
1b0b5eb198 Eksaktit mallinimet agenteille: qwen-coder → qwen2.5-coder:7b
- Kaikki agentPrompts.model vaihdettu 'qwen-coder' → 'qwen2.5-coder:7b'
- Native-node selected_task: 'qwen2.5-coder:7b'
- Hub-reititys: qwen-perhe matchaa keskenään (selain qwen-coder-05b,
  natiivi qwen2.5-coder:7b) taaksepäin yhteensopivuuden vuoksi

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:33:43 +03:00
44c8a189b6 Tofuist malli qwen2.5-coder:7b, hub-reititys laajennettu
- Tofuist-agentin model vaihdettu qwen-coder → qwen2.5-coder:7b
- Hub: qwen2.5-coder:* matchaa nyt qwen-coder*-solmuille ja päinvastoin

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:31:53 +03:00
1a58324689 Tofuist-agentti: OpenTofu/IaC-asiantuntija gecko-avatarilla
- Uusi agentti: Tofuist (gecko-avatar, oranssinkulta #e3a336)
- System prompt: HCL-koodi, moduulit, lifecycle, state encryption
- docs-kenttä: lataa automaattisesti /docs/tofu-cheatsheet.md referenssiksi
- kpnRun: tukee nyt agentin docs-kenttää (haetaan kerran, cachetetaan)
- OpenTofu-dokumentaatio haettu GitHubista + tiivistetty cheatsheet
- Avatar, gallery-head, värimapit ja pipeline-tuet lisätty

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:29:42 +03:00
239 changed files with 52917 additions and 190 deletions

3
.gitignore vendored
View File

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

215
network-poc/AGENTBUILDER.md Normal file
View File

@@ -0,0 +1,215 @@
# Kipinä Agent Builder — Suunnitelma
Käyttäjä voi rakentaa omia agentteja "hahmolomakkeella": valitsee avatarin, roolin, kielimallin ja muokkaa prompteja. Agentit tallentuvat localStorageen ja ovat käytettävissä pipelineissa.
## Nykytila
```js
// Kovakoodattu agentPrompts-objekti
const agentPrompts = {
manager: { name: 'Manageri', model: 'qwen2.5-coder:7b', default: '...' },
coder: { name: 'Koodari', model: 'qwen2.5-coder:7b', default: '...' },
tofuist: { name: 'Tofuist', model: 'qwen2.5-coder:7b', docs: '/docs/tofu-cheatsheet.md', default: '...' },
// ...
};
```
**Ongelma:** Uuden agentin lisääminen vaatii koodimuutoksen index.html:ään.
## Tavoite
```
┌─────────────────────────────────────────────────────┐
│ Agent Builder -lomake │
│ │
│ ┌─────────┐ Nimi: [Tofuist ] │
│ │ 🦎 │ Rooli: [IaC / Infra ▼] │
│ │ avatar │ Malli: [qwen2.5-coder:7b ▼] │
│ └─────────┘ Docs: [/docs/tofu-cheatsheet.md] │
│ │
│ System Prompt: │
│ ┌─────────────────────────────────────────────┐ │
│ │ You are an OpenTofu/Terraform IaC specialist│ │
│ │ ... │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ LLM-parametrit: │
│ Temperature: [0.7] Top-k: [40] Max tokens: [512]│
│ │
│ [💾 Tallenna] [🗑️ Poista] [📤 Export JSON] │
└─────────────────────────────────────────────────────┘
```
## Building Blocks
### 1. Agenttiskeema
```js
{
id: 'tofuist', // uniikki tunniste
name: 'Tofuist', // näyttönimi
avatar: '/avatars/gecko_notext.png', // avatar-kuvan polku
role: 'iac', // rooli-template
model: 'qwen2.5-coder:7b', // eksakti Ollama-mallinimi
color: '#e3a336', // teemaväri UI:ssa
docs: '/docs/tofu-cheatsheet.md', // valinnainen referenssidokumentti
prompt: 'You are an OpenTofu...', // system prompt
params: { // LLM-parametrit
temperature: 0.7,
top_k: 40,
max_tokens: 512,
repetition_penalty: 1.15
}
}
```
### 2. Rooli-templatet (alasvetovalikko)
Valmiit pohjat jotka tuovat oletuspromptit ja parametrit:
| Rooli | Oletusprompt | Parametrit |
|-------|-------------|------------|
| Koodari | "Kirjoita selkeää, testattavaa koodia" | temp 0.7, max 512 |
| QA / Testaus | "Kirjoita testejä, etsi virheitä" | temp 0.4, max 512 |
| DevOps | "Dockerfile, Compose, CI/CD" | temp 0.5, max 512 |
| DevSecOps | "Tietoturva-auditointi, OWASP" | temp 0.3, max 512 |
| Arkkitehti | "Järjestelmäsuunnittelu, rajapinnat" | temp 0.6, max 512 |
| IaC / Infra | "OpenTofu/Terraform HCL-koodi" | temp 0.5, max 512 |
| Data | "Tietokannat, SQL, datamallit" | temp 0.5, max 512 |
| Manageri | "Tehtävien jako ja koordinointi" | temp 0.8, max 200 |
| Kirjoittaja | "Dokumentaatio, README, ohjeet" | temp 0.8, max 512 |
| Vapaa | (tyhjä, käyttäjä kirjoittaa) | temp 0.7, max 512 |
### 3. Malli-valitsin
Lista saatavilla olevista malleista — haetaan dynaamisesti:
```
Hub-kysely: GET /api/models → palauttaa yhdistettyjen solmujen mallit
Tai staattinen lista:
- qwen2.5-coder:7b (oletus, natiivi GPU)
- qwen2.5-coder:1.5b (kevyt)
- qwen2.5-coder:0.5b (selain Wasm)
- deepseek-r1 (reasoning)
- llama3.2:3b (yleiskäyttö)
```
Pitkän aikavälin tavoite: hub ilmoittaa WebSocketin kautta mitkä mallit ovat saatavilla.
### 4. Avatar-valitsin
Valmiit avatarit + mahdollisuus ladata oma:
| Hahmo | Tiedosto | Eläin |
|-------|----------|-------|
| Asiakas | kettu_notext.png | Kettu |
| Manageri | karhunpentu.png | Karhunpentu |
| Koodari | kipina_notext.png | Salamanteri |
| Data | pesukarhu_notext.png | Pesukarhu |
| QA | susi_notext.png | Pikkususi |
| DevOps | laiskiainen_notext.png | Laiskiainen |
| Tarkkailija | aikuinen_susi.png | Aikuinen susi |
| Tofuist | gecko_notext.png | Gecko/Lisko |
| Arkkitehti | ??? | (tulossa) |
| DevSecOps | ??? | (tulossa) |
### 5. Docs-kenttä (referenssidokumentti)
Agentti voi viitata ulkoiseen dokumenttiin joka ladataan promptiin:
```
docs: '/docs/tofu-cheatsheet.md' → haetaan fetch():llä, cachetetaan _docsCache-kenttään
```
**Toiminta:**
1. Ensimmäisellä `kpnRun`-kutsulla ladataan docs-URL
2. Sisältö cachetetaan `agent._docsCache`-kenttään
3. Liitetään promptiin: `"Reference:\n" + docsContent`
4. Ei ladata uudelleen saman session aikana
**Rajoitukset:**
- Max ~3000 tokenia (~10 KB) — pidempi docs tiivistetään
- Vain tekstitiedostot (.md, .txt)
### 6. Tallennus (localStorage)
```js
// Tallennusavain
'kpn-custom-agents' JSON.stringify([ agentSkeema1, agentSkeema2, ... ])
// Ladattaessa
const customAgents = JSON.parse(localStorage.getItem('kpn-custom-agents') || '[]');
const defaultAgents = { manager: {...}, coder: {...}, ... };
const agentPrompts = { ...defaultAgents };
for (const agent of customAgents) {
agentPrompts[agent.id] = agent;
}
```
**Oletusagentit** (manager, coder, tester, qa, data) ovat aina mukana — niitä ei voi poistaa, mutta prompteja voi muokata.
**Käyttäjäagentit** (tofuist, arkkitehti, devsecops, ...) tallentuvat localStorageen ja latautuvat käynnistyksessä.
### 7. Export / Import
```js
// Export — JSON-tiedosto
const blob = new Blob([JSON.stringify(agent, null, 2)], { type: 'application/json' });
// → agent-tofuist.json
// Import — tiedoston valinta tai drag & drop
// Validoidaan skeema, lisätään agentPrompts-objektiin
```
Mahdollistaa agenttien jakamisen tiimin kesken.
## Toteutusvaiheet
### Vaihe 1: Hahmolomake UI
- Avatar-grid valitsin
- Rooli-template alasvetovalikko (täyttää oletuspromptit)
- Malli-valitsin
- System prompt -tekstikenttä
- LLM-parametrit (temperature, top-k, max_tokens)
- Tallenna/Poista-napit
### Vaihe 2: Dynaaminen agenttirekisteri
- `agentPrompts` ladataan localStoragesta
- Oletusagentit + käyttäjän agentit yhdistetään
- Avatar-kortit renderöidään dynaamisesti (ei HTML:ssä)
- Värimapit generoidaan agenttiskeemasta
### Vaihe 3: Pipeline käyttää dynaamisia agentteja
- Pipeline-vaiheet viittaavat agentin id:hen (ei kovakoodattuun nimeen)
- Käyttäjä voi valita mitkä agentit osallistuvat pipelineen
- Tofuist voi korvata DevOpsin IaC-projekteissa
### Vaihe 4: Mallirekisteri (hub-integraatio)
- Hub tarjoaa `/api/models`-endpointin
- Saatavilla olevat mallit näkyvät valitsimessa reaaliajassa
- Solmun liittyessä/poistuessa mallit päivittyvät
## Arkkitehtuurikaavio
```
┌──────────────────────────────────────────────────┐
│ Agent Builder UI │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ Avatar │ │ Rooli │ │ Malli-valitsin │ │
│ │ Grid │ │ Template │ │ (hub/staattinen) │ │
│ └────┬─────┘ └────┬─────┘ └────────┬─────────┘ │
│ └─────────────┼───────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────────┐ │
│ │ Agent Schema { id, name, avatar, model, │ │
│ │ role, color, docs, prompt, │ │
│ │ params } │ │
│ └──────────────────┬───────────────────────────┘ │
│ │ │
│ ┌─────────────┼─────────────┐ │
│ ▼ ▼ ▼ │
│ localStorage Org Chart Pipeline │
│ (persist) (render) (execute) │
└──────────────────────────────────────────────────┘
```

View File

@@ -1,47 +1,62 @@
# 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
COPY frontend/package.json frontend/package-lock.json* ./
RUN npm install --silent
COPY frontend/ .
COPY frontend/public/pkg public/pkg
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"]

3
network-poc/frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
node_modules/
dist/
.astro/

View File

@@ -0,0 +1,2 @@
import { defineConfig } from 'astro/config';
export default defineConfig({});

4721
network-poc/frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
{
"name": "kipina-frontend",
"type": "module",
"version": "0.1.0",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview"
},
"dependencies": {
"astro": "^6.1.5"
}
}

View File

Before

Width:  |  Height:  |  Size: 696 KiB

After

Width:  |  Height:  |  Size: 696 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 757 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 700 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 731 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 711 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 KiB

View File

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 130 KiB

View File

Before

Width:  |  Height:  |  Size: 432 KiB

After

Width:  |  Height:  |  Size: 432 KiB

View File

Before

Width:  |  Height:  |  Size: 650 KiB

After

Width:  |  Height:  |  Size: 650 KiB

View File

Before

Width:  |  Height:  |  Size: 389 KiB

After

Width:  |  Height:  |  Size: 389 KiB

View File

Before

Width:  |  Height:  |  Size: 596 KiB

After

Width:  |  Height:  |  Size: 596 KiB

View File

Before

Width:  |  Height:  |  Size: 496 KiB

After

Width:  |  Height:  |  Size: 496 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 872 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 738 KiB

View File

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

View File

Before

Width:  |  Height:  |  Size: 3.4 MiB

After

Width:  |  Height:  |  Size: 3.4 MiB

View File

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 813 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 KiB

View File

Before

Width:  |  Height:  |  Size: 593 KiB

After

Width:  |  Height:  |  Size: 593 KiB

View File

Before

Width:  |  Height:  |  Size: 563 KiB

After

Width:  |  Height:  |  Size: 563 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 718 KiB

View File

Before

Width:  |  Height:  |  Size: 513 KiB

After

Width:  |  Height:  |  Size: 513 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 780 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 826 KiB

View File

@@ -0,0 +1,63 @@
/* tslint:disable */
/* eslint-disable */
export function set_auto_tasks(enabled: boolean): void;
export function set_gpu_load(load: number): void;
export function start_agent_node(hub_url: string, has_webgpu: boolean, device_info_json: string, task_id: number): Promise<void>;
/**
* JS-exportti: tokenisoi tekstin ja palauttaa JSON-merkkijonon
* Tokenizer ladataan IndexedDB:stä (täytyy olla ladattu aiemmin)
*/
export function tokenize_js(text: string): Promise<string>;
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
export interface InitOutput {
readonly memory: WebAssembly.Memory;
readonly set_auto_tasks: (a: number) => void;
readonly set_gpu_load: (a: number) => void;
readonly start_agent_node: (a: number, b: number, c: number, d: number, e: number, f: number) => any;
readonly tokenize_js: (a: number, b: number) => any;
readonly wasm_bindgen__convert__closures_____invoke__h6ec112f0342d232e: (a: number, b: number, c: any) => [number, number];
readonly wasm_bindgen__convert__closures_____invoke__h737e63bacb96714d: (a: number, b: number, c: any, d: any) => void;
readonly wasm_bindgen__convert__closures_____invoke__ha390eb51fa5285b4: (a: number, b: number, c: any) => void;
readonly wasm_bindgen__convert__closures_____invoke__h9cacd8a9a6ca46c2: (a: number, b: number, c: any) => void;
readonly wasm_bindgen__convert__closures_____invoke__ha390eb51fa5285b4_3: (a: number, b: number, c: any) => void;
readonly wasm_bindgen__convert__closures_____invoke__h0afc19def95e993a: (a: number, b: number, c: any) => void;
readonly wasm_bindgen__convert__closures_____invoke__h0afc19def95e993a_5: (a: number, b: number, c: any) => void;
readonly wasm_bindgen__convert__closures_____invoke__h698aa4c8c2e7db1b: (a: number, b: number) => void;
readonly __wbindgen_malloc: (a: number, b: number) => number;
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
readonly __wbindgen_exn_store: (a: number) => void;
readonly __externref_table_alloc: () => number;
readonly __wbindgen_externrefs: WebAssembly.Table;
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
readonly __wbindgen_destroy_closure: (a: number, b: number) => void;
readonly __externref_table_dealloc: (a: number) => void;
readonly __wbindgen_start: () => void;
}
export type SyncInitInput = BufferSource | WebAssembly.Module;
/**
* Instantiates the given `module`, which can either be bytes or
* a precompiled `WebAssembly.Module`.
*
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
*
* @returns {InitOutput}
*/
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
/**
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
* for everything else, calls `WebAssembly.instantiate` directly.
*
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
*
* @returns {Promise<InitOutput>}
*/
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -0,0 +1,24 @@
/* tslint:disable */
/* eslint-disable */
export const memory: WebAssembly.Memory;
export const set_auto_tasks: (a: number) => void;
export const set_gpu_load: (a: number) => void;
export const start_agent_node: (a: number, b: number, c: number, d: number, e: number, f: number) => any;
export const tokenize_js: (a: number, b: number) => any;
export const wasm_bindgen__convert__closures_____invoke__h6ec112f0342d232e: (a: number, b: number, c: any) => [number, number];
export const wasm_bindgen__convert__closures_____invoke__h737e63bacb96714d: (a: number, b: number, c: any, d: any) => void;
export const wasm_bindgen__convert__closures_____invoke__ha390eb51fa5285b4: (a: number, b: number, c: any) => void;
export const wasm_bindgen__convert__closures_____invoke__h9cacd8a9a6ca46c2: (a: number, b: number, c: any) => void;
export const wasm_bindgen__convert__closures_____invoke__ha390eb51fa5285b4_3: (a: number, b: number, c: any) => void;
export const wasm_bindgen__convert__closures_____invoke__h0afc19def95e993a: (a: number, b: number, c: any) => void;
export const wasm_bindgen__convert__closures_____invoke__h0afc19def95e993a_5: (a: number, b: number, c: any) => void;
export const wasm_bindgen__convert__closures_____invoke__h698aa4c8c2e7db1b: (a: number, b: number) => void;
export const __wbindgen_malloc: (a: number, b: number) => number;
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
export const __wbindgen_exn_store: (a: number) => void;
export const __externref_table_alloc: () => number;
export const __wbindgen_externrefs: WebAssembly.Table;
export const __wbindgen_free: (a: number, b: number, c: number) => void;
export const __wbindgen_destroy_closure: (a: number, b: number) => void;
export const __externref_table_dealloc: (a: number) => void;
export const __wbindgen_start: () => void;

View File

@@ -0,0 +1,15 @@
{
"name": "node",
"type": "module",
"version": "0.1.0",
"files": [
"node_bg.wasm",
"node.js",
"node.d.ts"
],
"main": "node.js",
"types": "node.d.ts",
"sideEffects": [
"./snippets/*"
]
}

View File

@@ -0,0 +1,27 @@
{
"name": "FastAPI CRUD",
"description": "REST API with SQLite database",
"files": {
"models.py": {
"description": "SQLAlchemy models, engine, and session",
"example": "from sqlalchemy import create_engine, Column, Integer, String\nfrom sqlalchemy.ext.declarative import declarative_base\nfrom sqlalchemy.orm import sessionmaker\n\nDATABASE_URL = \"sqlite:///./app.db\"\nengine = create_engine(DATABASE_URL, connect_args={\"check_same_thread\": False})\nSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)\nBase = declarative_base()\n\nclass Item(Base):\n __tablename__ = \"items\"\n id = Column(Integer, primary_key=True, index=True)\n name = Column(String(100), nullable=False)\n description = Column(String(500))",
"instructions": "Define the SQLAlchemy model based on the project description. Always include:\n- engine with check_same_thread=False for SQLite\n- SessionLocal with autocommit=False\n- Base = declarative_base()\n- Model class with __tablename__, primary key, and fields"
},
"schemas.py": {
"description": "Pydantic request/response schemas",
"example": "from pydantic import BaseModel\n\nclass ItemCreate(BaseModel):\n name: str\n description: str | None = None\n\nclass ItemResponse(ItemCreate):\n id: int\n\n class Config:\n from_attributes = True",
"instructions": "Create Pydantic schemas that match the SQLAlchemy model:\n- Create schema: fields without id (user provides these)\n- Response schema: inherits from Create, adds id\n- Add class Config with from_attributes = True (required for SQLAlchemy ORM)"
},
"main.py": {
"description": "FastAPI app with CRUD endpoints",
"example": "from fastapi import FastAPI, Depends, HTTPException\nfrom sqlalchemy.orm import Session\nfrom models import Base, engine, SessionLocal, Item\nfrom schemas import ItemCreate, ItemResponse\n\nBase.metadata.create_all(bind=engine)\napp = FastAPI()\n\ndef get_db():\n db = SessionLocal()\n try:\n yield db\n finally:\n db.close()\n\n@app.post(\"/items/\", response_model=ItemResponse, status_code=201)\ndef create_item(item: ItemCreate, db: Session = Depends(get_db)):\n db_item = Item(**item.model_dump())\n db.add(db_item)\n db.commit()\n db.refresh(db_item)\n return db_item\n\n@app.get(\"/items/\", response_model=list[ItemResponse])\ndef list_items(db: Session = Depends(get_db)):\n return db.query(Item).all()\n\n@app.get(\"/items/{item_id}\", response_model=ItemResponse)\ndef get_item(item_id: int, db: Session = Depends(get_db)):\n item = db.query(Item).filter(Item.id == item_id).first()\n if not item:\n raise HTTPException(status_code=404, detail=\"Not found\")\n return item\n\n@app.put(\"/items/{item_id}\", response_model=ItemResponse)\ndef update_item(item_id: int, item: ItemCreate, db: Session = Depends(get_db)):\n db_item = db.query(Item).filter(Item.id == item_id).first()\n if not db_item:\n raise HTTPException(status_code=404, detail=\"Not found\")\n for key, value in item.model_dump().items():\n setattr(db_item, key, value)\n db.commit()\n db.refresh(db_item)\n return db_item\n\n@app.delete(\"/items/{item_id}\", status_code=204)\ndef delete_item(item_id: int, db: Session = Depends(get_db)):\n db_item = db.query(Item).filter(Item.id == item_id).first()\n if not db_item:\n raise HTTPException(status_code=404, detail=\"Not found\")\n db.delete(db_item)\n db.commit()",
"instructions": "Create the FastAPI app with all CRUD endpoints:\n- Import from models.py and schemas.py (use exact class names)\n- create_all(bind=engine) at module level\n- get_db dependency with yield pattern\n- POST (201), GET list, GET by id, PUT, DELETE (204)\n- Use response_model for type safety\n- Use model_dump() not dict() (Pydantic v2)"
},
"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": "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

@@ -0,0 +1,79 @@
<!-- Agenttigalleria + konfigurointipaneeli -->
<div style="display:flex;gap:16px;padding:10px 0;align-items:flex-start">
<!-- Agenttilista (drag & drop) -->
<div id="agent-bar" style="display:flex;gap:6px;align-items:flex-end;flex-wrap:wrap">
<!-- Renderöidään JS:stä -->
</div>
<!-- + Lisää agentti -->
<div id="add-agent-btn" class="agent-avatar" onclick="addCustomAgent()" title="Lisää oma agentti" style="opacity:0.4">
<div style="width:48px;height:48px;border-radius:50%;border:2px dashed var(--border);display:flex;align-items:center;justify-content:center;font-size:24px;color:var(--border)">+</div>
<span style="font-size:10px;color:#8b949e;text-align:center;display:block">Lisää</span>
</div>
</div>
<!-- Agentin konfigurointipaneeli (avautuu klikkaamalla avataria) -->
<div id="agent-config" style="display:none;background:var(--panel);border:1px solid var(--border);border-radius:6px;padding:16px;margin-bottom:10px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
<div style="display:flex;align-items:center;gap:10px">
<img id="config-avatar" src="" style="width:40px;height:40px;border-radius:50%">
<div>
<input id="config-name" style="background:transparent;border:none;color:var(--text);font-size:16px;font-weight:600;outline:none;width:200px" placeholder="Agentin nimi">
<div id="config-role" style="font-size:11px;color:#8b949e"></div>
</div>
</div>
<div style="display:flex;gap:6px">
<button class="btn btn-red" onclick="deleteAgent()" title="Poista agentti">Poista</button>
<button class="btn btn-muted" onclick="closeAgentConfig()">Sulje</button>
</div>
</div>
<!-- Malli -->
<div style="margin-bottom:10px">
<label style="font-size:12px;color:#8b949e;display:block;margin-bottom:4px">Kielimalli</label>
<select id="config-model" style="background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:4px;padding:6px 10px;font-size:13px;width:100%">
<option value="qwen-coder">Qwen2.5-Coder:0.5B (selain)</option>
<option value="qwen-coder-3b">Qwen2.5-Coder:3B (Ollama)</option>
<option value="qwen2.5-coder:7b">Qwen2.5-Coder:7B (Ollama)</option>
<option value="qwen2.5-coder:1.5b">Qwen2.5-Coder:1.5B (Ollama)</option>
</select>
</div>
<!-- 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" 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 -->
<div style="margin-bottom:10px">
<label style="font-size:12px;color:#8b949e;display:block;margin-bottom:8px">Sampling-parametrit</label>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<div title="Kontrolloi 'luovuutta'. Matala arvo (0.2-0.4) tuottaa ennustettavaa, toistettavaa koodia — hyvä testaajille ja reviewereille. Keskiarvo (0.6-0.8) on paras koodin generointiin. Korkea arvo (1.0+) lisää vaihtelua mutta myös virheitä.&#10;&#10;Suositus:&#10;• Manageri: 0.5 (tarkat tiedostolistat)&#10;• Koodari: 0.7 (toimiva koodi + vaihtelu)&#10;• Testaaja: 0.3 (deterministinen arviointi)">
<label style="font-size:11px;color:#8b949e;cursor:help">Temperature 💡 <span id="config-temp-val" style="color:var(--accent);float:right">0.7</span></label>
<input type="range" id="config-temperature" min="0" max="1.5" step="0.1" value="0.7" style="width:100%;accent-color:var(--accent)">
<div style="font-size:10px;color:#30363d">0=tarkka · 0.7=oletus · 1.5=luova</div>
</div>
<div title="Vastauksen maksimipituus tokeneina (~1 token ≈ 4 merkkiä).&#10;&#10;Suositus:&#10;• Manageri: 256-512 (lyhyet tiedostolistat)&#10;• Koodari: 1024-2048 (täydet tiedostot, CRUD-endpointit)&#10;• Testaaja: 256-512 (lyhyet arvioinnit)&#10;&#10;Jos koodi katkeaa kesken, nosta tätä. Jos malli tuottaa turhaa toistoa, laske.">
<label style="font-size:11px;color:#8b949e;cursor:help">Max tokens 💡 <span id="config-maxtok-val" style="color:var(--accent);float:right">1024</span></label>
<input type="range" id="config-maxtokens" min="64" max="4096" step="64" value="1024" style="width:100%;accent-color:var(--accent)">
<div style="font-size:10px;color:#30363d">Vastauksen maksimipituus</div>
</div>
<div title="Montako todennäköisintä tokenia huomioidaan valinnassa. Pieni arvo (1-10) tekee vastauksesta deterministisen. Suuri arvo (50-100) sallii harvinaisempia sanoja.&#10;&#10;Suositus:&#10;• Boilerplate-koodi: 20-30 (tutut patternit)&#10;• Yleiskoodi: 40 (hyvä oletus)&#10;• Luova teksti: 60-80&#10;&#10;Yleensä ei tarvitse muuttaa oletuksesta.">
<label style="font-size:11px;color:#8b949e;cursor:help">Top-K 💡 <span id="config-topk-val" style="color:var(--accent);float:right">40</span></label>
<input type="range" id="config-topk" min="1" max="100" step="1" value="40" style="width:100%;accent-color:var(--accent)">
<div style="font-size:10px;color:#30363d">1=greedy · 40=oletus · 100=laaja</div>
</div>
<div title="Vähentää jo tuotettujen sanojen todennäköisyyttä. Estää mallia toistamasta samaa lausetta. Liian korkea arvo (>1.5) voi rikkoa koodin koska samat avainsanat (return, if, def) ovat tarpeellisia.&#10;&#10;Suositus:&#10;• Koodi: 1.1-1.2 (lievä, sallii toiston)&#10;• Teksti: 1.15-1.3 (vahvempi)&#10;• Review: 1.0-1.1 (ei rangaistusta, lyhyet vastaukset)">
<label style="font-size:11px;color:#8b949e;cursor:help">Repetition penalty 💡 <span id="config-rep-val" style="color:var(--accent);float:right">1.15</span></label>
<input type="range" id="config-repeat" min="1.0" max="2.0" step="0.05" value="1.15" style="width:100%;accent-color:var(--accent)">
<div style="font-size:10px;color:#30363d">1.0=ei · 1.15=oletus · 2.0=vahva</div>
</div>
</div>
</div>
<!-- Pipeline-järjestys -->
<div>
<label style="font-size:12px;color:#8b949e;display:block;margin-bottom:4px">Pipeline-järjestys <span style="color:var(--border)">(vedä järjestääksesi)</span></label>
<div id="config-pipeline" style="display:flex;gap:4px;flex-wrap:wrap"></div>
</div>
</div>

View File

@@ -0,0 +1,15 @@
<!-- Monaco Editor paneeli -->
<div id="panel-editor" class="panel">
<div style="display:flex;height:calc(100vh - 200px);gap:0;border:1px solid var(--border);border-radius:6px;overflow:hidden">
<div id="editor-filetree" style="width:200px;min-width:150px;background:var(--bg);border-right:1px solid var(--border);overflow-y:auto;font-family:'Courier New',monospace;font-size:13px">
<div style="padding:10px 12px;color:#8b949e;font-size:11px;text-transform:uppercase;letter-spacing:0.5px;border-bottom:1px solid var(--border)">Tiedostot</div>
<div id="editor-file-list" style="padding:4px 0">
<div style="padding:8px 16px;color:#8b949e;font-size:12px">Generoi projekti:<br><code style="color:var(--accent)">kpn project "..."</code></div>
</div>
</div>
<div style="flex:1;display:flex;flex-direction:column">
<div id="editor-tabs" style="display:flex;background:var(--bg);border-bottom:1px solid var(--border);min-height:35px;align-items:flex-end;padding:0 8px;gap:2px;overflow-x:auto"></div>
<div id="monaco-container" style="flex:1"></div>
</div>
</div>
</div>

View File

@@ -0,0 +1,6 @@
<!-- Opas-paneeli: ladataan GUIDE.md fetchillä -->
<div id="panel-guide" class="panel">
<div id="guide-content" style="max-width:800px;margin:0 auto;padding:20px;line-height:1.7;font-size:15px">
<p style="color:#8b949e">Ladataan opasta...</p>
</div>
</div>

View File

@@ -0,0 +1,67 @@
<!-- Asetukset-paneeli: kaikki LLM-parametrit muokattavissa -->
<div id="panel-settings" class="panel">
<div style="max-width:800px;margin:0 auto;padding:20px">
<h2 style="color:#e6edf3;margin-bottom:16px">Asetukset</h2>
<p style="color:#8b949e;margin-bottom:20px;font-size:14px">Kaikki kielimallin toimintaan vaikuttavat parametrit. Muutokset tallentuvat automaattisesti.</p>
<!-- System prompt -->
<div class="settings-section">
<h3 class="settings-title">System Prompt</h3>
<p class="settings-desc">Kielimallin perusohje joka lähetetään jokaisessa pyynnössä. Määrittää mallin käyttäytymisen.</p>
<textarea id="set-system-prompt" class="settings-textarea" rows="4"></textarea>
</div>
<!-- Sampling -->
<div class="settings-section">
<h3 class="settings-title">Sampling-parametrit</h3>
<p class="settings-desc">Kontrolloi miten malli valitsee seuraavan tokenin. <a href="#guide" onclick="switchTab('guide')" style="color:var(--accent)">Lue lisää oppaasta.</a></p>
<div class="settings-grid">
<div>
<label class="settings-label">Temperature <span id="set-temp-val" class="settings-val">0.7</span></label>
<input type="range" id="set-temperature" min="0" max="1.5" step="0.1" value="0.7" class="settings-slider">
<div class="settings-hint">0 = deterministic, 0.7 = balanced, 1.5 = creative</div>
</div>
<div>
<label class="settings-label">Top-K <span id="set-topk-val" class="settings-val">40</span></label>
<input type="range" id="set-topk" min="1" max="100" step="1" value="40" class="settings-slider">
<div class="settings-hint">Montako tokenia huomioidaan. 1 = greedy, 40 = oletus</div>
</div>
<div>
<label class="settings-label">Repetition Penalty <span id="set-rep-val" class="settings-val">1.15</span></label>
<input type="range" id="set-repeat" min="1.0" max="2.0" step="0.05" value="1.15" class="settings-slider">
<div class="settings-hint">Estää toistoa. 1.0 = ei rangaistusta, 1.15 = oletus</div>
</div>
<div>
<label class="settings-label">Max Tokens <span id="set-maxtok-val" class="settings-val">1024</span></label>
<input type="range" id="set-maxtokens" min="64" max="4096" step="64" value="1024" class="settings-slider">
<div class="settings-hint">Vastauksen maksimipituus tokeneina</div>
</div>
</div>
</div>
<!-- Stop-sekvenssit -->
<div class="settings-section">
<h3 class="settings-title">Stop-sekvenssit</h3>
<p class="settings-desc">Generointi katkeaa kun malli tuottaa jonkin näistä. Yksi per rivi.</p>
<textarea id="set-stop-sequences" class="settings-textarea" rows="4"></textarea>
</div>
<!-- Malli -->
<div class="settings-section">
<h3 class="settings-title">Malli (Ollama)</h3>
<p class="settings-desc">Natiivisolmun käyttämä kielimalli. Muutos vaatii native-noden uudelleenkäynnistyksen.</p>
<select id="set-model" class="settings-select">
<option value="qwen2.5-coder:1.5b">Qwen2.5-Coder:1.5B (~80 tok/s, ~1GB)</option>
<option value="qwen2.5-coder:3b">Qwen2.5-Coder:3B (~50 tok/s, ~2GB)</option>
<option value="qwen2.5-coder:7b-instruct-q4_K_M">Qwen2.5-Coder:7B Q4 (~30 tok/s, ~4GB)</option>
<option value="qwen2.5-coder:7b">Qwen2.5-Coder:7B (~20 tok/s, ~7GB)</option>
</select>
</div>
<!-- Reset -->
<div style="margin-top:24px;padding-top:16px;border-top:1px solid var(--border)">
<button class="btn btn-red" onclick="resetSettings()" style="padding:6px 16px">Palauta oletukset</button>
<span style="color:#8b949e;font-size:12px;margin-left:8px">Palauttaa kaikki parametrit oletusarvoihin</span>
</div>
</div>
</div>

View File

@@ -0,0 +1,15 @@
<!-- Hub-yhteys + laskentasolmun tila -->
<div class="status-bar">
<span class="status-group" title="Hub-yhteyden tila">
<span id="hub-dot" class="status-dot" style="background:#d29922"></span>
<span style="color:#8b949e">Hub:</span>
<span id="hub-label" style="color:#d29922">Yhdistetään...</span>
</span>
<span class="status-separator">│</span>
<span class="status-group">
<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>
</span>
</div>

View File

@@ -0,0 +1,10 @@
<!-- Pipeline-palkki + Terminaali + Input -->
<div id="pipeline-bar" class="pipeline-bar"></div>
<div id="terminal" class="terminal"></div>
<div class="terminal-input-row">
<span class="terminal-prompt">$</span>
<input id="term-input" class="terminal-input" type="text"
placeholder='kpn run coder "hello world in python"'
spellcheck="false" autocomplete="off">
<div id="term-dropdown" class="terminal-dropdown"></div>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,198 @@
:root {
--bg: #0d1117;
--panel: #161b22;
--text: #c9d1d9;
--accent: #58a6ff;
--green: #3fb950;
--yellow: #d29922;
--red: #f85149;
--purple: #a371f7;
--border: #30363d;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
}
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
/* Tabs */
.tabs { display: flex; gap: 4px; margin-bottom: 16px; }
.tab {
padding: 8px 16px; border-radius: 6px 6px 0 0; cursor: pointer;
border: 1px solid var(--border); border-bottom: none;
background: var(--bg); color: #8b949e; font-size: 14px;
}
.tab.active { background: var(--panel); color: var(--accent); border-color: var(--border); }
/* Panels */
.panel { display: none; }
.panel.active { display: block; }
/* Status bar */
.status-bar {
display: flex; align-items: center; gap: 12px;
padding: 8px 14px; background: var(--bg);
border: 1px solid var(--border); border-radius: 6px 6px 0 0;
font-family: 'Courier New', monospace; font-size: 13px;
}
.status-dot {
width: 8px; height: 8px; border-radius: 50%; display: inline-block;
}
.status-group { display: flex; align-items: center; gap: 6px; }
.status-separator { color: var(--border); }
/* 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;
}
.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;
}
.terminal-input {
flex: 1; background: transparent; border: none; outline: none;
color: var(--green); font-family: inherit; font-size: inherit;
}
.terminal-dropdown {
display: none; position: absolute; bottom: 100%; left: 30px;
background: var(--panel); border: 1px solid var(--border);
border-radius: 6px; max-height: 200px; overflow-y: auto;
font-size: 13px; min-width: 200px; z-index: 100;
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
}
.dd-item {
padding: 6px 12px; cursor: pointer; color: var(--text);
white-space: nowrap; border-bottom: 1px solid #21262d;
}
.dd-item:hover, .dd-item.active { background: var(--border); color: var(--accent); }
/* Pipeline progress */
.pipeline-bar {
display: none; padding: 8px 14px; background: var(--bg);
border: 1px solid var(--border); border-top: none;
font-family: 'Courier New', monospace; font-size: 12px;
overflow-x: auto; white-space: nowrap;
}
/* Project card */
.project-card {
margin: 8px 0; border: 1px solid var(--border);
border-radius: 6px; background: var(--panel); overflow: hidden;
}
.project-header {
display: flex; align-items: center; justify-content: space-between;
padding: 8px 12px; background: var(--bg); border-bottom: 1px solid var(--border);
}
.project-tabs { display: flex; gap: 2px; padding: 6px 8px 0; background: var(--bg); }
.project-tab {
padding: 4px 10px; cursor: pointer; border-radius: 4px 4px 0 0;
font-size: 12px; color: #8b949e;
}
.project-tab.active { background: var(--panel); color: var(--accent); border: 1px solid var(--border); border-bottom: none; }
/* Buttons */
.btn {
padding: 2px 10px; border-radius: 4px;
border: 1px solid var(--border); background: var(--panel);
font-size: 12px; font-family: inherit; cursor: pointer;
}
.btn-accent { color: var(--accent); }
.btn-green { color: var(--green); border-color: var(--green); }
.btn-red { color: var(--red); border-color: var(--red); }
.btn-muted { color: #8b949e; background: none; }
/* Code display */
.code-block {
font-family: 'Courier New', monospace; background: #010409;
border: 1px solid var(--border); border-radius: 6px;
padding: 14px; font-size: 13px; line-height: 1.6;
white-space: pre-wrap; overflow-x: auto; max-height: 400px; overflow-y: auto;
}
.code-block .hljs { background: transparent; padding: 0; }
/* Agent avatars */
.agent-avatar {
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;
text-align: center;
width: 72px;
opacity: 0.8;
cursor: pointer;
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
}
.agent-avatar:hover {
opacity: 0.85;
transform: translateY(-2px) scale(1.02);
border-color: rgba(240,246,252,0.3);
box-shadow: 0 8px 14px rgba(0,0,0,0.4);
}
.agent-avatar img {
width: 50px; height: 50px; border-radius: 12px;
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;
overflow: hidden; text-overflow: ellipsis;
}
.agent-avatar.active {
opacity: 1;
transform: translateY(-8px) scale(1.05);
border-color: var(--accent);
background: linear-gradient(145deg, rgba(88,166,255,0.15) 0%, rgba(13,17,23,0.9) 100%);
box-shadow: 0 16px 24px rgba(0,0,0,0.5), 0 0 20px rgba(88,166,255,0.3);
z-index: 2;
}
.agent-avatar.active img {
border-color: var(--accent);
box-shadow: 0 0 25px rgba(88,166,255,0.8);
}
/* Settings */
.settings-section {
margin-bottom: 24px; padding: 16px; background: var(--panel);
border: 1px solid var(--border); border-radius: 6px;
}
.settings-title { color: #e6edf3; font-size: 15px; margin-bottom: 4px; }
.settings-desc { color: #8b949e; font-size: 13px; margin-bottom: 12px; }
.settings-label { color: var(--text); font-size: 13px; display: block; margin-bottom: 4px; }
.settings-val { color: var(--accent); font-weight: 600; float: right; }
.settings-hint { color: #8b949e; font-size: 11px; margin-top: 2px; }
.settings-textarea {
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;
}
.settings-select {
width: 100%; background: var(--bg); color: var(--text);
border: 1px solid var(--border); border-radius: 4px;
padding: 8px; font-size: 13px;
}
.settings-slider {
width: 100%; accent-color: var(--accent);
}
.settings-grid {
display: grid; grid-template-columns: 1fr 1fr; gap: 16px;
}
/* Animations */
@keyframes blink { 0%,100% { opacity:1 } 50% { opacity:0 } }
@keyframes spin { to { transform: rotate(360deg) } }

View File

@@ -0,0 +1 @@
{ "extends": "astro/tsconfigs/strict" }

View File

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

View File

@@ -26,6 +26,29 @@ impl NodeDb {
INSERT INTO _schema_version VALUES (2);
");
}
if version < 3 {
let _ = conn.execute_batch("
CREATE TABLE IF NOT EXISTS agents (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
avatar TEXT NOT NULL DEFAULT '/avatars/kipina_notext.png',
role TEXT NOT NULL DEFAULT 'coder',
model TEXT NOT NULL DEFAULT 'qwen2.5-coder:7b',
color TEXT NOT NULL DEFAULT '#3fb950',
docs TEXT,
prompt TEXT NOT NULL DEFAULT '',
temperature REAL DEFAULT 0.7,
top_k INTEGER DEFAULT 40,
max_tokens INTEGER DEFAULT 512,
repetition_penalty REAL DEFAULT 1.15,
is_default BOOLEAN DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
DELETE FROM _schema_version;
INSERT INTO _schema_version VALUES (3);
");
}
conn.execute_batch("
CREATE TABLE IF NOT EXISTS node_sessions (
@@ -279,6 +302,82 @@ impl NodeDb {
})
}
// ── Agents CRUD ──
pub fn upsert_agent(&self, agent: &serde_json::Value) -> Result<(), String> {
let conn = self.conn.lock().unwrap_or_else(|e| e.into_inner());
let now = chrono::Utc::now().to_rfc3339();
let id = agent.get("id").and_then(|v| v.as_str()).ok_or("id puuttuu")?;
let name = agent.get("name").and_then(|v| v.as_str()).ok_or("name puuttuu")?;
conn.execute(
"INSERT INTO agents (id, name, avatar, role, model, color, docs, prompt,
temperature, top_k, max_tokens, repetition_penalty, is_default, created_at, updated_at)
VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?14)
ON CONFLICT(id) DO UPDATE SET
name=?2, avatar=?3, role=?4, model=?5, color=?6, docs=?7, prompt=?8,
temperature=?9, top_k=?10, max_tokens=?11, repetition_penalty=?12, updated_at=?14",
params![
id, name,
agent.get("avatar").and_then(|v| v.as_str()).unwrap_or("/avatars/kipina_notext.png"),
agent.get("role").and_then(|v| v.as_str()).unwrap_or("coder"),
agent.get("model").and_then(|v| v.as_str()).unwrap_or("qwen2.5-coder:7b"),
agent.get("color").and_then(|v| v.as_str()).unwrap_or("#3fb950"),
agent.get("docs").and_then(|v| v.as_str()),
agent.get("prompt").and_then(|v| v.as_str()).unwrap_or(""),
agent.get("temperature").and_then(|v| v.as_f64()).unwrap_or(0.7),
agent.get("top_k").and_then(|v| v.as_u64()).unwrap_or(40) as i64,
agent.get("max_tokens").and_then(|v| v.as_u64()).unwrap_or(512) as i64,
agent.get("repetition_penalty").and_then(|v| v.as_f64()).unwrap_or(1.15),
agent.get("is_default").and_then(|v| v.as_bool()).unwrap_or(false),
now,
],
).map_err(|e| format!("Agent upsert: {}", e))?;
Ok(())
}
pub fn get_agents(&self) -> Vec<serde_json::Value> {
let conn = self.conn.lock().unwrap_or_else(|e| e.into_inner());
let mut stmt = conn.prepare(
"SELECT id, name, avatar, role, model, color, docs, prompt,
temperature, top_k, max_tokens, repetition_penalty, is_default,
created_at, updated_at
FROM agents ORDER BY is_default DESC, name"
).unwrap();
stmt.query_map([], |row| {
Ok(serde_json::json!({
"id": row.get::<_, String>(0)?,
"name": row.get::<_, String>(1)?,
"avatar": row.get::<_, String>(2)?,
"role": row.get::<_, String>(3)?,
"model": row.get::<_, String>(4)?,
"color": row.get::<_, String>(5)?,
"docs": row.get::<_, Option<String>>(6)?,
"prompt": row.get::<_, String>(7)?,
"temperature": row.get::<_, f64>(8)?,
"top_k": row.get::<_, i64>(9)?,
"max_tokens": row.get::<_, i64>(10)?,
"repetition_penalty": row.get::<_, f64>(11)?,
"is_default": row.get::<_, bool>(12)?,
"created_at": row.get::<_, String>(13)?,
"updated_at": row.get::<_, String>(14)?,
}))
}).unwrap().filter_map(|r| r.ok()).collect()
}
pub fn delete_agent(&self, id: &str) -> Result<(), String> {
let conn = self.conn.lock().unwrap_or_else(|e| e.into_inner());
let deleted = conn.execute(
"DELETE FROM agents WHERE id = ?1 AND is_default = 0",
params![id],
).map_err(|e| format!("Agent delete: {}", e))?;
if deleted == 0 {
Err("Agenttia ei löydy tai se on oletusagentti".to_string())
} else {
Ok(())
}
}
pub fn insert_pair_result(
&self,
node_id: u64,

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()),
@@ -330,15 +330,6 @@ async fn main() {
let idx = (rng_state as usize) % pairs.len();
let (en, fi) = pairs[idx];
// Tokenisointiparit
let pair_msg = serde_json::json!({
"type": "pair_task",
"en": en,
"fi": fi,
});
let _ = state_for_task.stats_tx.send(pair_msg.to_string());
// LLM-promptit
let llm_prompts = vec![
"Tell me a short joke.",
"What is WebGPU in one sentence?",
@@ -348,33 +339,39 @@ async fn main() {
];
let llm_idx = (rng_state as usize / 7) % llm_prompts.len();
// SmolLM-prompt
let smollm_msg = serde_json::json!({
"type": "llm_prompt",
"prompt": llm_prompts[llm_idx],
"model": "smollm-135m",
});
let _ = state_for_task.stats_tx.send(smollm_msg.to_string());
// Smart Routing: Lähetetään vain niille, jotka valittuna ja idle
let mut sends = Vec::new();
{
let channels = state_for_task.node_channels.read().await;
let tasks = state_for_task.node_tasks.lock().unwrap();
let mut busy = state_for_task.node_busy.lock().unwrap();
// Qwen-prompt (sama prompti, eri malli-tagi)
let qwen_msg = serde_json::json!({
"type": "llm_prompt",
"prompt": llm_prompts[llm_idx],
"model": "qwen-05b",
});
let _ = state_for_task.stats_tx.send(qwen_msg.to_string());
for (node_id, task) in tasks.iter() {
if !busy.contains(node_id) {
// Vapaa node -> lähetetään oikea tehtävä
let msg = match task.as_str() {
"tokenize" => Some(serde_json::json!({ "type": "pair_task", "en": en, "fi": fi })),
"smollm-135m" => Some(serde_json::json!({ "type": "llm_prompt", "prompt": llm_prompts[llm_idx], "model": "smollm-135m" })),
"qwen-05b" => Some(serde_json::json!({ "type": "llm_prompt", "prompt": llm_prompts[llm_idx], "model": "qwen-05b" })),
"phi3-mini" => Some(serde_json::json!({ "type": "llm_prompt", "prompt": llm_prompts[llm_idx], "model": "phi3-mini" })),
_ => None, // Coder ja viewer ei saa auto-tehtäviä
};
// Phi-3 prompt
let phi3_msg = serde_json::json!({
"type": "llm_prompt",
"prompt": llm_prompts[llm_idx],
"model": "phi3-mini",
});
let _ = state_for_task.stats_tx.send(phi3_msg.to_string());
if let Some(payload) = msg {
if let Some(ch) = channels.get(node_id) {
sends.push((ch.clone(), payload.to_string()));
busy.insert(*node_id);
}
}
}
}
}
// Coder ei saa automaattisia tehtäviä — vain käyttäjän user_text
for (ch, msg_str) in sends {
let _ = ch.send(msg_str);
}
tracing::debug!("Tehtävät lähetetty: pair + smollm + qwen + phi3");
// tracing::debug!("Tehtävät lähetetty reititetysti idle-nodeille");
}
});
@@ -387,9 +384,11 @@ async fn main() {
.route("/api/v1/model", axum::routing::post(api_change_model))
.route("/api/v1/hardware", get(api_hardware))
.route("/api/v1/ollama/tags", get(api_ollama_tags))
.route("/api/v1/agents", get(api_get_agents).post(api_upsert_agent))
.route("/api/v1/agents/:id", axum::routing::delete(api_delete_agent))
.route("/admin", get(admin_page))
.nest_service("/", {
let static_dir = std::env::var("STATIC_DIR").unwrap_or_else(|_| "../static".to_string());
let static_dir = std::env::var("STATIC_DIR").unwrap_or_else(|_| "../frontend/dist".to_string());
ServeDir::new(&static_dir).fallback(ServeFile::new(format!("{}/index.html", static_dir)))
})
.with_state(state);
@@ -462,6 +461,34 @@ fn admin_unauthorized() -> axum::response::Response {
.unwrap()
}
// ── Agents API ──
async fn api_get_agents(
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
) -> axum::response::Response {
axum::Json(state.db.get_agents()).into_response()
}
async fn api_upsert_agent(
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
axum::Json(payload): axum::Json<serde_json::Value>,
) -> axum::response::Response {
match state.db.upsert_agent(&payload) {
Ok(()) => axum::Json(serde_json::json!({"ok": true})).into_response(),
Err(e) => (axum::http::StatusCode::BAD_REQUEST, e).into_response(),
}
}
async fn api_delete_agent(
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
axum::extract::Path(id): axum::extract::Path<String>,
) -> axum::response::Response {
match state.db.delete_agent(&id) {
Ok(()) => axum::Json(serde_json::json!({"ok": true})).into_response(),
Err(e) => (axum::http::StatusCode::BAD_REQUEST, e).into_response(),
}
}
async fn admin_page(headers: axum::http::HeaderMap) -> axum::response::Response {
if !check_admin_auth(&headers) { return admin_unauthorized(); }
axum::response::Html(ADMIN_HTML).into_response()
@@ -475,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,
@@ -491,16 +523,19 @@ async fn ws_handler(
.and_then(|s| s.trim().parse::<IpAddr>().ok())
.unwrap_or_else(|| addr.ip());
// Max yhteyttä per IP: jokainen selain tarvitsee 2 (UI + coder-node)
// Max yhteyttä per IP (ei rajoiteta localhost/127.0.0.1)
{
let conns = state.ip_connections.lock().unwrap();
let count = conns.get(&ip).copied().unwrap_or(0);
if count >= 10 {
tracing::warn!("IP {} ylitti yhteysrajan ({}/10) — estetty", ip, count);
return (
axum::http::StatusCode::TOO_MANY_REQUESTS,
"Max 10 yhteyttä per IP",
).into_response();
let is_local = ip.is_loopback();
if !is_local {
let conns = state.ip_connections.lock().unwrap();
let count = conns.get(&ip).copied().unwrap_or(0);
if count >= 20 {
tracing::warn!("IP {} ylitti yhteysrajan ({}/20) — estetty", ip, count);
return (
axum::http::StatusCode::TOO_MANY_REQUESTS,
"Max 20 yhteyttä per IP",
).into_response();
}
}
}
@@ -661,6 +696,18 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
let allocated = json.get("allocated_gb").and_then(|v| v.as_u64()).unwrap_or(4) as u32;
let node_type = json.get("node_type").and_then(|v| v.as_str()).unwrap_or("browser");
// API-avain vaaditaan natiivisolmuilta (ei selaimilta)
if node_type == "native" {
let required_key = std::env::var("NODE_API_KEY").unwrap_or_default();
if !required_key.is_empty() {
let provided_key = json.get("api_key").and_then(|v| v.as_str()).unwrap_or("");
if provided_key != required_key {
tracing::warn!("Solmu {} ({}) hylätty: virheellinen API-avain", node_id, ip);
break; // Suljetaan WebSocket
}
}
}
{
let mut map = state.nodes_vram.lock().unwrap();
map.insert(node_id, allocated);
@@ -741,6 +788,7 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
}
broadcast_stats(&state).await;
} else if msg_type == "pair_done" {
state.node_busy.lock().unwrap().remove(&node_id);
{
let mut json = json; // Siirretään omistajuus muokkausta varten
if let Some(obj) = json.as_object_mut() {
@@ -1054,93 +1102,50 @@ async fn api_chat_completions(
}
// Etsitään vapaa solmu — priorisoidaan natiivisolmut (GPU) selaimen edelle
let (target_node_free, target_node_any, total_matching) = {
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)| {
if payload.model == "qwen-coder" {
task.starts_with("qwen-coder")
// Eksakti match tai qwen-perheen yhteensopivuus (selain: qwen-coder-05b, natiivi: qwen2.5-coder:7b)
let req_model = payload.model.to_lowercase();
let node_task = task.to_lowercase();
if req_model.starts_with("qwen") {
node_task.starts_with("qwen")
} else if req_model.starts_with("phi") {
node_task.starts_with("phi")
} else {
**task == payload.model
}
}).map(|(k, _)| *k).collect();
// Vapaat solmut: natiivi ensin, sitten selain
let free_native = matching.iter().find(|id| {
!busy.contains(id) && node_types.get(id).map(|t| t == "native").unwrap_or(false)
// Etsitään mikä tahansa matchaava solmu (natiivi priorisoidaan)
let native = matching.iter().find(|id| {
node_types.get(id).map(|t| t == "native").unwrap_or(false)
}).copied();
let free_any = matching.iter().find(|id| !busy.contains(id)).copied();
let free = free_native.or(free_any);
let any = matching.first().copied();
(free, any, matching.len())
let any = native.or_else(|| matching.first().copied());
(any, matching.len())
};
// Broadcastataan reititystila UI:lle
let task_id = payload.task_id.clone();
if target_node_any.is_none() {
// Ei yhtään solmua tälle mallille
return (axum::http::StatusCode::SERVICE_UNAVAILABLE, "Ei solmua tälle mallille (käynnistä malli selaimessa)").into_response();
}
let target_node_id = match target_node {
Some(id) => id,
None => {
return (axum::http::StatusCode::SERVICE_UNAVAILABLE, "Ei solmua tälle mallille (käynnistä malli selaimessa)").into_response();
}
};
let target_node_id;
if let Some(free_id) = target_node_free {
// Vapaa solmu löytyi — reititetään suoraan
target_node_id = free_id;
let node_type = if state.node_tasks.lock().unwrap().get(&free_id).map(|t| t.contains("native")).unwrap_or(false) { "natiivi" } else { "selain" };
// Reititystila UI:lle
{
let routing_msg = serde_json::json!({
"type": "task_routed",
"task_id": task_id,
"node_id": free_id,
"node_type": node_type,
"node_id": target_node_id,
"status": "routed",
"message": format!("Reititetty solmulle #{}", free_id),
"message": format!("Reititetty solmulle #{}", target_node_id),
});
let _ = state.stats_tx.send(routing_msg.to_string());
} else {
// Kaikki solmut varattuja — odotetaan vapautumista (max 30s)
let queue_msg = serde_json::json!({
"type": "task_routed",
"task_id": task_id,
"status": "queued",
"message": format!("Kaikki {} solmua varattuja — odotetaan vapautumista...", total_matching),
});
let _ = state.stats_tx.send(queue_msg.to_string());
// Pollaa busy-tilaa 500ms välein, max 30s
let mut waited = 0u32;
loop {
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
waited += 500;
let free = {
let tasks = state.node_tasks.lock().unwrap();
let busy = state.node_busy.lock().unwrap();
tasks.iter().find(|(node_id, task)| {
let model_match = if payload.model == "qwen-coder" {
*task == "qwen-coder-05b" || *task == "qwen-coder"
} else {
**task == payload.model
};
model_match && !busy.contains(node_id)
}).map(|(k, _)| *k)
};
if let Some(id) = free {
target_node_id = id;
let routing_msg = serde_json::json!({
"type": "task_routed",
"task_id": task_id,
"node_id": id,
"status": "routed",
"message": format!("Solmu #{} vapautui — reititetään ({:.1}s jonossa)", id, waited as f64 / 1000.0),
});
let _ = state.stats_tx.send(routing_msg.to_string());
break;
}
if waited >= 30000 {
return (axum::http::StatusCode::SERVICE_UNAVAILABLE, "Aikakatkaisu: kaikki solmut varattuja 30s ajan").into_response();
}
}
};
}
// Merkitään solmu varatuksi ja task_id jaetuksi
state.node_busy.lock().unwrap().insert(target_node_id);

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"

37
network-poc/local.sh Executable file
View File

@@ -0,0 +1,37 @@
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
echo "=== Kipinä Studio Local Development ==="
# Frontend
echo "[1/3] Rakennetaan frontend..."
cd "$SCRIPT_DIR/frontend"
[ -d node_modules ] || npm install --silent
npm run build --silent 2>&1 | tail -1
# Hub
echo "[2/3] Käynnistetään hub..."
cd "$SCRIPT_DIR/hub"
cargo run &
HUB_PID=$!
sleep 3
# Native-node (jos Ollama on käynnissä)
if curl -s http://localhost:11434/api/tags >/dev/null 2>&1; then
echo "[3/3] Ollama löytyi — käynnistetään native-node..."
cd "$SCRIPT_DIR/native-node"
HUB_URL=ws://localhost:3000/ws cargo run --no-default-features &
NODE_PID=$!
echo " Native-node PID: $NODE_PID"
else
echo "[3/3] Ollama ei käynnissä — käytetään selaimen Wasm-laskentaa"
echo " Nopeampi: ollama serve & ollama pull qwen2.5-coder:7b && ./local.sh"
fi
echo ""
echo "=== http://localhost:3000 ==="
echo " Ctrl+C pysäyttää"
# Odotetaan hub-prosessia
wait $HUB_PID

View File

@@ -3,6 +3,10 @@ name = "native-node"
version = "0.2.2"
edition = "2024"
[features]
default = ["gpu-detect"]
gpu-detect = ["nvml-wrapper", "wgpu"]
[dependencies]
tokio = { version = "1.36", features = ["full"] }
tokio-tungstenite = { version = "0.21", features = ["native-tls"] }
@@ -10,8 +14,8 @@ futures-util = "0.3"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sysinfo = "0.30"
nvml-wrapper = "0.10"
wgpu = "24"
nvml-wrapper = { version = "0.10", optional = true }
wgpu = { version = "24", optional = true }
reqwest = { version = "0.12", features = ["json"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

View File

@@ -9,7 +9,7 @@ pub struct LlmEngine {
impl LlmEngine {
pub async fn load() -> Result<Self, String> {
let model = std::env::var("OLLAMA_MODEL").unwrap_or_else(|_| "qwen2.5-coder:7b".to_string());
let model = std::env::var("OLLAMA_MODEL").unwrap_or_else(|_| "qwen2.5-coder:3b".to_string());
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(600))
@@ -79,7 +79,8 @@ impl LlmEngine {
}
pub async fn generate(&self, prompt: &str, max_tokens: usize) -> Result<GenerateResult, String> {
let system = "You are a coding assistant. Respond with ONLY code. Use proper newlines and indentation. No explanations, no markdown fences, no comments unless asked.";
// System prompt tulee agentin konfiguraatiosta (frontend lähettää sen osana promptia).
// Tässä ei yliajeta sitä — Ollama saa vain prompt-kentän.
let model = self.model.borrow().clone();
let start = Instant::now();
@@ -87,14 +88,13 @@ impl LlmEngine {
.json(&serde_json::json!({
"model": model,
"prompt": prompt,
"system": system,
"stream": false,
"options": {
"num_predict": max_tokens,
"temperature": 0.7,
"top_k": 40,
"repeat_penalty": 1.15,
"stop": ["<|im_end|>", "\n###", "\nExplanation", "\nNote:"]
"stop": ["<|im_end|>", "\n###", "\nExplanation", "\nNote:", "\nPlease note", "\nThis is", "\n```\n\n", "\n// Example", "\n# Example"]
}
}))
.send()
@@ -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);
@@ -127,27 +127,40 @@ impl LlmEngine {
}
}
/// Siivoa mahdolliset markdown-koodiblokki-merkit
/// Siivoa markdown-koodiblokki-merkit ja selitystekstit
fn strip_code_fences(text: &str) -> String {
let mut result = text.trim().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;
}
true
}).collect();
let mut result = filtered.join("\n").trim().to_string();
// Poista aloittava ```lang
if result.starts_with("```") {
if let Some(nl) = result.find('\n') {
result = result[nl + 1..].to_string();
// Poista selitysteksti lopusta (kaikki rivin "\nPlease note" jälkeen jne.)
let lower = result.to_lowercase();
for stop in &["\nplease note", "\nthis is a basic", "\nthis code", "\nnote that", "\nremember to", "\nyou can", "\nto run"] {
if let Some(pos) = lower.find(stop) {
result = result[..pos].trim_end().to_string();
}
}
// 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();
// Poista johdantolauseet alusta
let lower = result.to_lowercase();
for prefix in &["sure!", "here is", "here's", "certainly!", "below is"] {
if lower.starts_with(prefix) {
if let Some(nl) = result.find('\n') {
result = result[nl + 1..].to_string();
}
break;
}
}
result
result.trim().to_string()
}
pub struct GenerateResult {

View File

@@ -33,6 +33,7 @@ impl GpuInfo {
}
}
#[cfg(feature = "gpu-detect")]
/// Tunnistaa kaikki GPU:t wgpu:lla (NVIDIA/AMD/Apple/Intel)
fn collect_gpus_wgpu() -> Vec<GpuInfo> {
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
@@ -84,6 +85,7 @@ fn collect_gpus_wgpu() -> Vec<GpuInfo> {
gpus
}
#[cfg(feature = "gpu-detect")]
/// Täydentää NVIDIA-GPU:iden tiedot NVML:llä (VRAM, lämpötila, kuormitus)
fn enrich_nvidia_gpus(gpus: &mut [GpuInfo]) {
let Ok(nvml) = nvml_wrapper::Nvml::init() else { return };
@@ -109,6 +111,7 @@ fn enrich_nvidia_gpus(gpus: &mut [GpuInfo]) {
}
}
#[cfg(feature = "gpu-detect")]
/// AMD GPU-tiedot Linuxin sysfs:stä (/sys/class/drm/)
fn enrich_amd_gpus(gpus: &mut [GpuInfo]) {
let Ok(entries) = std::fs::read_dir("/sys/class/drm") else { return };
@@ -150,10 +153,12 @@ fn enrich_amd_gpus(gpus: &mut [GpuInfo]) {
}
}
#[cfg(feature = "gpu-detect")]
fn read_sysfs_u64(path: &std::path::Path) -> Option<u64> {
std::fs::read_to_string(path).ok()?.trim().parse().ok()
}
#[cfg(feature = "gpu-detect")]
fn find_hwmon_temp(device_path: &std::path::Path) -> Option<u64> {
let hwmon_dir = device_path.join("hwmon");
let entries = std::fs::read_dir(&hwmon_dir).ok()?;
@@ -166,8 +171,8 @@ fn find_hwmon_temp(device_path: &std::path::Path) -> Option<u64> {
None
}
#[cfg(feature = "gpu-detect")]
/// Apple GPU-tiedot — wgpu/Metal antaa nimen, tarkempaa dataa ei saa ilman IOKit:ia
/// mutta Metal adapter_info sisältää jo olennaiset tiedot
fn enrich_apple_gpus(gpus: &mut [GpuInfo]) {
// Apple Silicon -koneiden unified memory: koko RAM on GPU:n käytettävissä
// Arvioidaan system RAM:sta
@@ -187,13 +192,18 @@ fn enrich_apple_gpus(gpus: &mut [GpuInfo]) {
/// Kerää kaikki GPU:t ja täydentää valmistajakohtaiset tiedot
fn collect_all_gpus() -> Vec<GpuInfo> {
let mut gpus = collect_gpus_wgpu();
enrich_nvidia_gpus(&mut gpus);
enrich_amd_gpus(&mut gpus);
enrich_apple_gpus(&mut gpus);
gpus
#[cfg(feature = "gpu-detect")]
{
let mut gpus = collect_gpus_wgpu();
enrich_nvidia_gpus(&mut gpus);
enrich_amd_gpus(&mut gpus);
enrich_apple_gpus(&mut gpus);
return gpus;
}
#[cfg(not(feature = "gpu-detect"))]
{
Vec::new()
}
}
/// Kerää järjestelmätiedot (CPU, RAM, OS)
@@ -222,15 +232,21 @@ fn build_auth_message(allocated_gb: u32) -> String {
v
}).collect();
let api_key = std::env::var("NODE_API_KEY").unwrap_or_default();
let mut msg = json!({
"type": "auth",
"status": "agent_ready",
"node_type": "native",
"allocated_gb": allocated_gb,
"selected_task": "qwen-coder-05b",
"selected_task": "qwen2.5-coder:7b",
"system": sys,
});
if !api_key.is_empty() {
msg.as_object_mut().unwrap().insert("api_key".to_string(), json!(api_key));
}
if !gpu_json.is_empty() {
msg.as_object_mut().unwrap().insert("gpus".to_string(), json!(gpu_json));
}
@@ -269,6 +285,9 @@ async fn main() {
let gpus = collect_all_gpus();
if gpus.is_empty() {
#[cfg(not(feature = "gpu-detect"))]
tracing::info!("GPU-tunnistus ei käytössä (--no-default-features). Ollama käyttää GPU:ta automaattisesti jos saatavilla.");
#[cfg(feature = "gpu-detect")]
tracing::info!("GPU:ta ei havaittu — toimitaan CPU-moodissa");
} else {
for (i, gpu) in gpus.iter().enumerate() {
@@ -315,22 +334,19 @@ async fn main() {
continue;
}
let mut busy = false;
while let Some(Ok(msg)) = read.next().await {
if let Message::Text(text) = msg {
// LLM-promptit
if text.contains("llm_prompt") && !busy {
if text.contains("llm_prompt") {
if let Ok(task) = serde_json::from_str::<serde_json::Value>(&text) {
let prompt = task.get("prompt").and_then(|v| v.as_str()).unwrap_or("");
let task_id = task.get("task_id").and_then(|v| v.as_str()).unwrap_or("?");
let msg_model = task.get("model").and_then(|v| v.as_str()).unwrap_or("");
if !prompt.is_empty() && msg_model.starts_with("qwen-coder") {
if !prompt.is_empty() && (msg_model.starts_with("qwen-coder") || msg_model.starts_with("qwen2.5-coder")) {
if let Some(ref engine) = llm {
busy = true;
let max_tokens = task.get("max_tokens").and_then(|v| v.as_u64()).unwrap_or(512) as usize;
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 model_name = engine.model_name();
@@ -361,7 +377,6 @@ async fn main() {
tracing::error!("Inferenssivirhe: {}", e);
}
}
busy = false;
}
}
}

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 {
@@ -320,7 +323,11 @@ pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use
if let Ok(text) = cached.tokenizer.decode(&[next_token], true) {
generated_text.push_str(&text);
let mut chunk = serde_json::json!({ "type": "llm_chunk", "token": text, "prompt": prompt, "model": "Qwen2.5-Coder" });
if let Some(ref tid) = task_id { chunk.as_object_mut().unwrap().insert("task_id".to_string(), serde_json::json!(tid)); }
if let Some(ref tid) = task_id {
if let Some(obj) = chunk.as_object_mut() {
obj.insert("task_id".to_string(), serde_json::json!(tid));
}
}
let _ = ws.borrow().send_with_str(&chunk.to_string());
}
all_generated.push(next_token);
@@ -362,7 +369,11 @@ pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use
}
let mut chunk = serde_json::json!({ "type": "llm_chunk", "token": text, "prompt": prompt, "model": "Qwen2.5-Coder" });
if let Some(ref tid) = task_id { chunk.as_object_mut().unwrap().insert("task_id".to_string(), serde_json::json!(tid)); }
if let Some(ref tid) = task_id {
if let Some(obj) = chunk.as_object_mut() {
obj.insert("task_id".to_string(), serde_json::json!(tid));
}
}
let _ = ws.borrow().send_with_str(&chunk.to_string());
}
all_generated.push(next_token);
@@ -391,7 +402,9 @@ pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use
"load_time_ms": (load_time * 100.0).round() / 100.0,
});
if let Some(tid) = task_id {
done.as_object_mut().unwrap().insert("task_id".to_string(), serde_json::json!(tid));
if let Some(obj) = done.as_object_mut() {
obj.insert("task_id".to_string(), serde_json::json!(tid));
}
}
let _ = ws.borrow().send_with_str(&done.to_string());
}

View File

@@ -0,0 +1 @@
{"rustc_fingerprint":15841952146704291179,"outputs":{"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.94.1 (e408947bf 2026-03-25)\nbinary: rustc\ncommit-hash: e408947bfd200af42db322daf0fadfe7e26d3bd1\ncommit-date: 2026-03-25\nhost: x86_64-unknown-linux-gnu\nrelease: 1.94.1\nLLVM version: 21.1.8\n","stderr":""},"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/jaakko/.rustup/toolchains/stable-x86_64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"unknown\"\nunix\n","stderr":""}},"successes":{}}

View File

@@ -0,0 +1,3 @@
Signature: 8a477f597d28d172789f06886806bc55
# This file is a cache directory tag created by cargo.
# For information about cache directory tags see https://bford.info/cachedir/

View File

@@ -0,0 +1,24 @@
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store
# jetbrains setting folder
.idea/

View File

@@ -0,0 +1,4 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}

View File

@@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}

View File

@@ -0,0 +1,43 @@
# Astro Starter Kit: Minimal
```sh
npm create astro@latest -- --template minimal
```
> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
## 🚀 Project Structure
Inside of your Astro project, you'll see the following folders and files:
```text
/
├── public/
├── src/
│ └── pages/
│ └── index.astro
└── package.json
```
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
Any static assets, like images, can be placed in the `public/` directory.
## 🧞 Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `npm install` | Installs dependencies |
| `npm run dev` | Starts local dev server at `localhost:4321` |
| `npm run build` | Build your production site to `./dist/` |
| `npm run preview` | Preview your build locally, before deploying |
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI |
## 👀 Want to learn more?
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).

View File

@@ -0,0 +1,5 @@
// @ts-check
import { defineConfig } from 'astro/config';
// https://astro.build/config
export default defineConfig({});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
{
"name": "frontend",
"type": "module",
"version": "0.0.1",
"engines": {
"node": ">=22.12.0"
},
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"astro": "^6.1.5",
"three": "^0.183.2"
}
}

View File

@@ -0,0 +1,34 @@
# 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.

After

Width:  |  Height:  |  Size: 696 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 757 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 700 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 731 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 711 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 872 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 738 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 813 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 718 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 780 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 826 KiB

View File

@@ -0,0 +1,43 @@
# OpenTofu Core Codebase Documentation
This directory contains some documentation about the OpenTofu Core codebase,
aimed at readers who are interested in making code contributions.
If you're looking for information on _using_ OpenTofu, please instead refer
to [the main OpenTofu CLI documentation](https://opentofu.org/docs/cli/index.html).
## OpenTofu Core Architecture Documents
* [OpenTofu Core Architecture Summary](./architecture.md): an overview of the
main components of OpenTofu Core and how they interact. This is the best
starting point if you are diving in to this codebase for the first time.
* [Resource Instance Change Lifecycle](./resource-instance-change-lifecycle.md):
a description of the steps in validating, planning, and applying a change
to a resource instance, from the perspective of the provider plugin RPC
operations. This may be useful for understanding the various expectations
OpenTofu enforces about provider behavior, either if you intend to make
changes to those behaviors or if you are implementing a new OpenTofu plugin
SDK and so wish to conform to them.
(If you are planning to write a new provider using the _official_ SDK then
please refer to [the Extend documentation](https://github.com/hashicorp/terraform-docs-common)
instead; it presents similar information from the perspective of the SDK
API, rather than the plugin wire protocol.)
* [Diagnostics](./diagnostics): how we report errors and warnings to end-users
in OpenTofu.
* [Plugin Protocol](./plugin-protocol/): gRPC/protobuf definitions for the
plugin wire protocol and information about its versioning strategy.
This documentation is for SDK developers, and is not necessary reading for
those implementing a provider using the official SDK.
* [How OpenTofu Uses Unicode](./unicode.md): an overview of the various
features of OpenTofu that rely on Unicode and how to change those features
to adopt new versions of Unicode.
## Contribution Guides
* [Contributing to OpenTofu](../CONTRIBUTING.md): a complete guideline for those who want to contribute to this project.

View File

@@ -0,0 +1,374 @@
# OpenTofu Core Architecture Summary
This document is a summary of the main components of OpenTofu Core and how
data and requests flow between these components. It's intended as a primer
to help navigate the codebase to dig into more details.
We assume some familiarity with user-facing OpenTofu concepts like
configuration, state, CLI workflow, etc. The OpenTofu website has
documentation on these ideas.
## OpenTofu Request Flow
The following diagram shows an approximation of how a user command is
executed in OpenTofu:
![OpenTofu Architecture Diagram, described in text below](./images/architecture-overview.png)
Each of the different subsystems (solid boxes) in this diagram is described
in more detail in a corresponding section below.
## CLI (`command` package)
Each time a user runs the `tofu` program, aside from some initial
bootstrapping in the root package (not shown in the diagram) execution
transfers immediately into one of the "command" implementations in
[the `command` package](https://pkg.go.dev/github.com/opentofu/opentofu/internal/command).
The mapping between the user-facing command names and
their corresponding `command` package types can be found in the `commands.go`
file under the `cmd/tofu` directory (package `main`).
The full flow illustrated above does not actually apply to _all_ commands,
but it applies to the main OpenTofu workflow commands `tofu plan` and
`tofu apply`, along with a few others.
For these commands, the role of the command implementation is to read and parse
any command line arguments, command line options, and environment variables
that are needed for the given command and use them to produce a
[`backend.Operation`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/backend#Operation)
object that describes an action to be taken.
An _operation_ consists of:
* The action to be taken (e.g. "plan", "apply").
* The name of the [workspace](https://opentofu.org/docs/language/state/workspaces)
where the action will be taken.
* Root module input variables to use for the action.
* For the "plan" operation, a path to the directory containing the configuration's root module.
* For the "apply" operation, the plan to apply.
* Various other less-common options/settings such as `-target` addresses, the
"force" flag, etc.
The operation is then passed to the currently-selected
[backend](https://opentofu.org/docs/language/settings/backends/configuration). Each backend name
corresponds to an implementation of
[`backend.Backend`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/backend#Backend), using a
mapping table in
[the `backend/init` package](https://pkg.go.dev/github.com/opentofu/opentofu/internal/backend/init).
Backends that are able to execute operations additionally implement
[`backend.Enhanced`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/backend#Enhanced);
the command-handling code calls `Operation` with the operation it has
constructed, and then the backend is responsible for executing that action.
Backends that execute operations, however, do so as an architectural implementation detail and not a
general feature of backends. That is, the term 'backend' as a OpenTofu feature is used to refer to
a plugin that determines where OpenTofu stores its state snapshots - only the default `local`, `remote` and `cloud` backends perform operations.
Thus, most backends do _not_ implement this interface, and so the `command` package wraps these
backends in an instance of
[`local.Local`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/backend/local#Local),
causing the operation to be executed locally within the `tofu` process itself.
## Backends
A _backend_ determines where OpenTofu should store its state snapshots.
As described above, the `local` backend also executes operations on behalf of most other
backends. It uses a _state manager_
(either
[`statemgr.Filesystem`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/states/statemgr#Filesystem) if the
local backend is being used directly, or an implementation provided by whatever
backend is being wrapped) to retrieve the current state for the workspace
specified in the operation, then uses the _config loader_ to load and do
initial processing/validation of the configuration specified in the
operation. It then uses these, along with the other settings given in the
operation, to construct a
[`tofu.Context`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#Context),
which is the main object that actually performs OpenTofu operations.
The `local` backend finally calls an appropriate method on that context to
begin execution of the relevant command, such as
[`Plan`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#Context.Plan)
or
[`Apply`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#Context.Apply), which in turn constructs a graph using a _graph builder_,
described in a later section.
## Configuration Loader
The top-level configuration structure is represented by model types in
[package `configs`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/configs).
A whole configuration (the root module plus all of its descendent modules)
is represented by
[`configs.Config`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/configs#Config).
The `configs` package contains some low-level functionality for constructing
configuration objects, but the main entry point is in the sub-package
[`configload`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/configs/configload]),
via
[`configload.Loader`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/configs/configload#Loader).
A loader deals with all of the details of installing child modules
(during `tofu init`) and then locating those modules again when a
configuration is loaded by a backend. It takes the path to a root module
and recursively loads all of the child modules to produce a single
[`configs.Config`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/configs#Config)
representing the entire configuration.
OpenTofu expects configuration files written in the OpenTofu language, which
is a DSL built on top of
[HCL](https://github.com/hashicorp/hcl). Some parts of the configuration
cannot be interpreted until we build and walk the graph, since they depend
on the outcome of other parts of the configuration, and so these parts of
the configuration remain represented as the low-level HCL types
[`hcl.Body`](https://pkg.go.dev/github.com/hashicorp/hcl/v2/#Body)
and
[`hcl.Expression`](https://pkg.go.dev/github.com/hashicorp/hcl/v2/#Expression),
allowing OpenTofu to interpret them at a more appropriate time.
## State Manager
A _state manager_ is responsible for storing and retrieving snapshots of the
[OpenTofu state](https://opentofu.org/docs/language/state/index.html)
for a particular workspace. Each manager is an implementation of
some combination of interfaces in
[the `statemgr` package](https://pkg.go.dev/github.com/opentofu/opentofu/internal/states/statemgr),
with most practical managers implementing the full set of operations
described by
[`statemgr.Full`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/states/statemgr#Full)
provided by a _backend_. The smaller interfaces exist primarily for use in
other function signatures to be explicit about what actions the function might
take on the state manager; there is little reason to write a state manager
that does not implement all of `statemgr.Full`.
The implementation
[`statemgr.Filesystem`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/states/statemgr#Filesystem) is used
by default (by the `local` backend) and is responsible for the familiar
`terraform.tfstate` local file that most OpenTofu users start with, before
they switch to [remote state](https://opentofu.org/docs/language/state/remote).
Other implementations of `statemgr.Full` are used to implement remote state.
Each of these saves and retrieves state via a remote network service
appropriate to the backend that creates it.
A state manager accepts and returns a state snapshot as a
[`states.State`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/states#State)
object. The state manager is responsible for exactly how that object is
serialized and stored, but all state managers at the time of writing use
the same JSON serialization format, storing the resulting JSON bytes in some
kind of arbitrary blob store.
## Graph Builder
A _graph builder_ is called by a
[`tofu.Context`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#Context)
method (e.g. `Plan` or `Apply`) to produce the graph that will be used
to represent the necessary steps for that operation and the dependency
relationships between them.
In most cases, the
[vertices](https://en.wikipedia.org/wiki/Vertex_(graph_theory)) of OpenTofu's
graphs each represent a specific object in the configuration, or something
derived from those configuration objects. For example, each `resource` block
in the configuration has one corresponding
[`GraphNodeConfigResource`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#GraphNodeConfigResource)
vertex representing it in the "plan" graph. (OpenTofu Core uses terminology
inconsistently, describing graph _vertices_ also as graph _nodes_ in various
places. These both describe the same concept.)
The [edges](https://en.wikipedia.org/wiki/Glossary_of_graph_theory_terms#edge)
in the graph represent "must happen after" relationships. These define the
order in which the vertices are evaluated, ensuring that e.g. one resource is
created before another resource that depends on it.
Each operation has its own graph builder, because the graph building process
is different for each. For example, a "plan" operation needs a graph built
directly from the configuration, but an "apply" operation instead builds its
graph from the set of changes described in the plan that is being applied.
The graph builders all work in terms of a sequence of _transforms_, which
are implementations of
[`tofu.GraphTransformer`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#GraphTransformer).
Implementations of this interface just take a graph and mutate it in any
way needed, and so the set of available transforms is quite varied. Some
important examples include:
* [`ConfigTransformer`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#ConfigTransformer),
which creates a graph vertex for each `resource` block in the configuration.
* [`StateTransformer`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#StateTransformer),
which creates a graph vertex for each resource instance currently tracked
in the state.
* [`ReferenceTransformer`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#ReferenceTransformer),
which analyses the configuration to find dependencies between resources and
other objects and creates any necessary "happens after" edges for these.
* [`ProviderTransformer`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#ProviderTransformer),
which associates each resource or resource instance with exactly one
provider configuration (implementing
[the inheritance rules](https://opentofu.org/docs/language/providers/))
and then creates "happens after" edges to ensure that the providers are
initialized before taking any actions with the resources that belong to
them.
There are many more different graph transforms, which can be discovered
by reading the source code for the different graph builders. Each graph
builder uses a different subset of these depending on the needs of the
operation that is being performed.
The result of graph building is a
[`tofu.Graph`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#Graph), which
can then be processed using a _graph walker_.
## Graph Walk
The process of walking the graph visits each vertex of that graph in a way
which respects the "happens after" edges in the graph. The walk algorithm
itself is implemented in
[the low-level `dag` package](https://pkg.go.dev/github.com/opentofu/opentofu/internal/dag#AcyclicGraph.Walk)
(where "DAG" is short for [_Directed Acyclic Graph_](https://en.wikipedia.org/wiki/Directed_acyclic_graph)), in
[`AcyclicGraph.Walk`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/dag#AcyclicGraph.Walk).
However, the "interesting" OpenTofu walk functionality is implemented in
[`tofu.ContextGraphWalker`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#ContextGraphWalker),
which implements a small set of higher-level operations that are performed
during the graph walk:
* `EnterPath` is called once for each module in the configuration, taking a
module address and returning a
[`tofu.EvalContext`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#EvalContext)
that tracks objects within that module. `tofu.Context` is the _global_
context for the entire operation, while `tofu.EvalContext` is a
context for processing within a single module, and is the primary means
by which the namespaces in each module are kept separate.
Each vertex in the graph is evaluated, in an order that guarantees that the
"happens after" edges will be respected. If possible, the graph walk algorithm
will evaluate multiple vertices concurrently. Vertex evaluation code must
therefore make careful use of concurrency primitives such as mutexes in order
to coordinate access to shared objects such as the `states.State` object.
In most cases, we use the helper wrapper
[`states.SyncState`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/states#SyncState)
to safely implement concurrent reads and writes from the shared state.
## Vertex Evaluation
The action taken for each vertex during the graph walk is called
_execution_. Execution runs a sequence of arbitrary actions that make sense
for a particular vertex type.
For example, evaluation of a vertex representing a resource instance during
a plan operation would include the following high-level steps:
* Retrieve the resource's associated provider from the `EvalContext`. This
should already be initialized earlier by the provider's own graph vertex,
due to the "happens after" edge between the resource node and the provider
node.
* Retrieve from the state the portion relevant to the specific resource
instance being evaluated.
* Evaluate the attribute expressions given for the resource in configuration.
This often involves retrieving the state of _other_ resource instances so
that their values can be copied or transformed into the current instance's
attributes, which is coordinated by the `EvalContext`.
* Pass the current instance state and the resource configuration to the
provider, asking the provider to produce an _instance diff_ representing the
differences between the state and the configuration.
* Save the instance diff as part of the plan that is being constructed by
this operation.
Each execution step for a vertex is an implementation of
[`tofu.Execute`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#GraphNodeExecutable.Execute).
As with graph transforms, the behavior of these implementations varies widely:
whereas graph transforms can take any action against the graph, an `Execute`
implementation can take any action against the `EvalContext`.
The implementation of `tofu.EvalContext` used in real processing
(as opposed to testing) is
[`tofu.BuiltinEvalContext`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#BuiltinEvalContext).
It provides coordinated access to plugins, the current state, and the current
plan via the `EvalContext` interface methods.
In order to be executed, a vertex must implement
[`tofu.GraphNodeExecutable`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#GraphNodeExecutable),
which has a single `Execute` method that handles. There are numerous `Execute`
implementations with different behaviors, but some prominent examples are:
* [NodePlannableResource.Execute](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#NodePlannableResourceInstance.Execute), which handles the `plan` operation.
* [`NodeApplyableResourceInstance.Execute`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#NodeApplyableResourceInstance.Execute), which handles the main `apply` operation.
* [`NodeDestroyResourceInstance.Execute`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#EvalWriteState), which handles the main `destroy` operation.
A vertex must complete successfully before the graph walk will begin evaluation
for other vertices that have "happens after" edges. Evaluation can fail with one
or more errors, in which case the graph walk is halted and the errors are
returned to the user.
### Expression Evaluation
An important part of vertex evaluation for most vertex types is evaluating
any expressions in the configuration block associated with the vertex. This
completes the processing of the portions of the configuration that were not
processed by the configuration loader.
The high-level process for expression evaluation is:
1. Analyze the configuration expressions to see which other objects they refer
to. For example, the expression `aws_instance.example[1]` refers to one of
the instances created by a `resource "aws_instance" "example"` block in
configuration. This analysis is performed by
[`lang.References`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/lang#References),
or more often one of the helper wrappers around it:
[`lang.ReferencesInBlock`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/lang#ReferencesInBlock)
or
[`lang.ReferencesInExpr`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/lang#ReferencesInExpr)
1. Retrieve from the state the data for the objects that are referred to and
create a lookup table of the values from these objects that the
HCL evaluation code can refer to.
1. Prepare the table of built-in functions so that HCL evaluation can refer to
them.
1. Ask HCL to evaluate each attribute's expression (a
[`hcl.Expression`](https://pkg.go.dev/github.com/hashicorp/hcl/v2/#Expression)
object) against the data and function lookup tables.
In practice, steps 2 through 4 are usually run all together using one
of the methods on [`lang.Scope`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/lang#Scope);
most commonly,
[`lang.EvalBlock`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/lang#Scope.EvalBlock)
or
[`lang.EvalExpr`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/lang#Scope.EvalExpr).
Expression evaluation produces a dynamic value represented as a
[`cty.Value`](https://pkg.go.dev/github.com/zclconf/go-cty/cty#Value).
This Go type represents values from the OpenTofu language and such values
are eventually passed to provider plugins.
### Sub-graphs
Some vertices have a special additional behavior that happens after their
evaluation steps are complete, where the vertex implementation is given
the opportunity to build another separate graph which will be walked as part
of the evaluation of the vertex.
The main example of this is when a `resource` block has the `count` argument
set. In that case, the plan graph initially contains one vertex for each
`resource` block, but that graph then _dynamically expands_ to have a sub-graph
containing one vertex for each instance requested by the count. That is, the
sub-graph of `aws_instance.example` might contain vertices for
`aws_instance.example[0]`, `aws_instance.example[1]`, etc. This is necessary
because the `count` argument may refer to other objects whose values are not
known when the main graph is constructed, but become known while evaluating
other vertices in the main graph.
This special behavior applies to vertex objects that implement
[`tofu.GraphNodeDynamicExpandable`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#GraphNodeDynamicExpandable).
Such vertices have their own nested _graph builder_, _graph walk_,
and _vertex evaluation_ steps, with the same behaviors as described in these
sections for the main graph. The difference is in which graph transforms
are used to construct the graph and in which evaluation steps apply to the
nodes in that sub-graph.

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