Compare commits
111 Commits
v0.2.4
...
6e2f85daa8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e2f85daa8 | ||
|
|
466e61d730 | ||
|
|
5f00582053 | ||
|
|
e272b0d124 | ||
|
|
d3affb3a09 | ||
|
|
1377e72f78 | ||
|
|
403f35efdc | ||
|
|
ce0ccbddd3 | ||
|
|
80806498e0 | ||
|
|
660e80c2bc | ||
|
|
591cfcb04b | ||
|
|
3cda57f0bc | ||
|
|
23e7b92d03 | ||
|
|
9f58febe21 | ||
|
|
b1de0d37f7 | ||
|
|
4ff626ab88 | ||
|
|
a45616046d | ||
|
|
ee048b0b68 | ||
|
|
4e83569194 | ||
|
|
f42b692eeb | ||
|
|
f79bb16f3d | ||
|
|
e81fc33faf | ||
|
|
433726c553 | ||
|
|
dec2e24e2f | ||
|
|
9058033669 | ||
|
|
8bd86e6325 | ||
|
|
c1133bb075 | ||
|
|
6502d75efc | ||
|
|
9f8b7fe920 | ||
|
|
746bc20fcb | ||
|
|
93f6baa0ea | ||
|
|
cc8e871735 | ||
|
|
e90f3460c3 | ||
|
|
4d74c38618 | ||
|
|
8a1b204179 | ||
|
|
b19f5a3518 | ||
|
|
38dc36e846 | ||
|
|
4fe6931b5f | ||
|
|
b8e8a83e49 | ||
|
|
3d6914974d | ||
|
|
9aff2ec154 | ||
|
|
ecd4525a7f | ||
|
|
7a3e5278b9 | ||
|
|
8dcf269b42 | ||
|
|
cb16f35265 | ||
|
|
b9d340b4b4 | ||
|
|
dd07e536f0 | ||
|
|
9af481a022 | ||
|
|
529a30a6e1 | ||
|
|
7d842529b1 | ||
|
|
c731c18360 | ||
|
|
5498eb6cbb | ||
|
|
43f0aebf54 | ||
|
|
6413f0238f | ||
|
|
6b0394586e | ||
|
|
108094b06a | ||
|
|
d7c974792d | ||
|
|
1987eb57a0 | ||
|
|
12ca87415c | ||
|
|
a0e52faa44 | ||
|
|
f910cd8c61 | ||
|
|
91dc7579bc | ||
|
|
90c9a7e4fa | ||
|
|
1216e016c2 | ||
|
|
d85cab4bc0 | ||
|
|
4fef8824e1 | ||
|
|
009bf492c8 | ||
|
|
f7e0e8dff8 | ||
|
|
eb57ee7b92 | ||
|
|
84d13153ed | ||
|
|
8beac57b50 | ||
|
|
44067efdb6 | ||
|
|
5528be1812 | ||
|
|
f4cf4c73b9 | ||
|
|
e19852a509 | ||
|
|
6de0df365e | ||
|
|
28f620f901 | ||
|
|
3497f66db7 | ||
|
|
1c7362c9b0 | ||
|
|
9983c80ef1 | ||
|
|
fc1fb33d5e | ||
|
|
3bee8e8020 | ||
|
|
f8ea5ed76e | ||
|
|
6c7c2d6dd3 | ||
|
|
c179b4ab7e | ||
|
|
a8c4af0975 | ||
|
|
e3fdb91ac5 | ||
|
|
9925079729 | ||
|
|
6031737f83 | ||
|
|
b6a8fa2671 | ||
|
|
0dc53dba1c | ||
|
|
857afbe111 | ||
|
|
84b78eb9c6 | ||
|
|
4f18377a3b | ||
|
|
7f5bb45138 | ||
| 973d7a69c7 | |||
| aebc64e76e | |||
| 48c832c61b | |||
| 8435bd32a9 | |||
| ece41dd622 | |||
| c7f3b0d79f | |||
| 8905b50f41 | |||
| 43b0612004 | |||
| 599ac2d2d9 | |||
| d1975bd55c | |||
| 24a8139d3e | |||
| 21aac49a52 | |||
| 8a5f1b753c | |||
| 1b0b5eb198 | |||
| 44c8a189b6 | |||
| 1a58324689 |
6
.gitignore
vendored
@@ -37,3 +37,9 @@ Cargo.lock
|
|||||||
|
|
||||||
# Ajonaikaiset tietokannat
|
# Ajonaikaiset tietokannat
|
||||||
*.db
|
*.db
|
||||||
|
|
||||||
|
# Lokitiedostot
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Wanha versio
|
||||||
|
temp/
|
||||||
215
network-poc/AGENTBUILDER.md
Normal 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) │
|
||||||
|
└──────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
21
network-poc/Dockerfile.native
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Native-node: Rust + Ollama-client (ei GPU-tunnistusta)
|
||||||
|
FROM rust:slim AS builder
|
||||||
|
RUN apt-get update && apt-get install -y pkg-config libssl-dev && rm -rf /var/lib/apt/lists/*
|
||||||
|
WORKDIR /app
|
||||||
|
COPY Cargo.toml Cargo.lock* ./
|
||||||
|
COPY native-node/Cargo.toml native-node/Cargo.toml
|
||||||
|
COPY native-node/src native-node/src
|
||||||
|
# Dummy-cratet workspace-yhteensopivuuteen
|
||||||
|
COPY hub/Cargo.toml hub/Cargo.toml
|
||||||
|
COPY node/Cargo.toml node/Cargo.toml
|
||||||
|
COPY cli/Cargo.toml cli/Cargo.toml
|
||||||
|
RUN mkdir -p hub/src node/src cli/src && touch hub/src/main.rs node/src/lib.rs cli/src/main.rs
|
||||||
|
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||||
|
--mount=type=cache,target=/app/target \
|
||||||
|
cargo build --release -p native-node --no-default-features \
|
||||||
|
&& cp /app/target/release/native-node /usr/local/bin/native-node
|
||||||
|
|
||||||
|
FROM debian:bookworm-slim
|
||||||
|
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||||
|
COPY --from=builder /usr/local/bin/native-node /usr/local/bin/native-node
|
||||||
|
CMD ["native-node"]
|
||||||
@@ -1,47 +1,63 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
FROM rust:slim AS builder
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
# --- Vaihe 1: Frontend (Astro) ---
|
||||||
curl pkg-config libssl-dev g++ \
|
FROM node:22-slim AS frontend
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
WORKDIR /app/frontend
|
||||||
|
# Riippuvuudet ensin → cache-kerros (muuttuu harvoin)
|
||||||
|
COPY frontend/package.json frontend/package-lock.json* ./
|
||||||
|
RUN npm install --silent
|
||||||
|
# Lähdekoodi → muuttuu usein, mutta npm install on cachessa
|
||||||
|
COPY frontend/ .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# --- Vaihe 2: Wasm (wasm-pack) ---
|
||||||
|
FROM rust:slim AS wasm-builder
|
||||||
|
RUN apt-get update && apt-get install -y curl pkg-config libssl-dev g++ && rm -rf /var/lib/apt/lists/*
|
||||||
RUN curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
RUN curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
COPY Cargo.toml Cargo.lock* ./
|
||||||
# Kopioi kaikki Cargo-tiedostot
|
COPY node/Cargo.toml node/Cargo.toml
|
||||||
COPY Cargo.toml ./
|
COPY node/src node/src
|
||||||
COPY Cargo.lock* ./
|
# Dummy-cratet jotta workspace Cargo.toml on tyytyväinen
|
||||||
COPY hub/Cargo.toml hub/Cargo.toml
|
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 node/Cargo.toml node/Cargo.toml
|
||||||
COPY native-node/Cargo.toml native-node/Cargo.toml
|
COPY native-node/Cargo.toml native-node/Cargo.toml
|
||||||
COPY cli/Cargo.toml cli/Cargo.toml
|
COPY cli/Cargo.toml cli/Cargo.toml
|
||||||
|
RUN mkdir -p node/src native-node/src cli/src && touch node/src/lib.rs native-node/src/main.rs cli/src/main.rs
|
||||||
# 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 --mount=type=cache,target=/usr/local/cargo/registry \
|
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||||
--mount=type=cache,target=/app/target \
|
--mount=type=cache,target=/app/target \
|
||||||
cargo build --release -p hub \
|
cargo build --release -p hub \
|
||||||
&& cp /app/target/release/hub /usr/local/bin/hub
|
&& cp /app/target/release/hub /usr/local/bin/hub
|
||||||
|
|
||||||
|
# --- Vaihe 4: Tuotantoimage ---
|
||||||
FROM debian:bookworm-slim
|
FROM debian:bookworm-slim
|
||||||
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
|
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=hub-builder /usr/local/bin/hub /usr/local/bin/hub
|
||||||
COPY --from=builder /app/static /app/static
|
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
|
WORKDIR /app
|
||||||
ENV STATIC_DIR=/app/static
|
ENV STATIC_DIR=/app/frontend/dist
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD ["hub"]
|
CMD ["hub"]
|
||||||
|
|||||||
63
network-poc/build-binaries.sh
Executable file
@@ -0,0 +1,63 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Käännä kipina-node binäärit kaikille alustoille
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
OUT="$SCRIPT_DIR/frontend/public/download"
|
||||||
|
HASH_FILE="$OUT/.build-hash"
|
||||||
|
mkdir -p "$OUT"
|
||||||
|
|
||||||
|
# Tarkistetaan onko native-node muuttunut edellisen buildin jälkeen
|
||||||
|
CURRENT_HASH=$(git -C "$SCRIPT_DIR" log -1 --format=%H -- native-node/ Cargo.toml Cargo.lock)
|
||||||
|
if [ -n "$(git -C "$SCRIPT_DIR" status --porcelain -- native-node/ Cargo.toml Cargo.lock)" ]; then
|
||||||
|
CURRENT_HASH="dirty-$(git -C "$SCRIPT_DIR" status --porcelain | md5sum | awk '{print $1}')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$HASH_FILE" ] && [ "$(cat "$HASH_FILE")" = "$CURRENT_HASH" ]; then
|
||||||
|
echo "=== Kipinä Node — ei muutoksia, ohitetaan build ==="
|
||||||
|
ls -lh "$OUT"/kipina-node-* 2>/dev/null || true
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== Kipinä Node — Binary Build ==="
|
||||||
|
|
||||||
|
# macOS ARM (natiivi)
|
||||||
|
echo "[1/4] macOS ARM64..."
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
cargo build --release -p native-node --no-default-features 2>&1 | tail -1
|
||||||
|
cp target/release/native-node "$OUT/kipina-node-macos-arm64"
|
||||||
|
echo " $(ls -lh "$OUT/kipina-node-macos-arm64" | awk '{print $5}')"
|
||||||
|
|
||||||
|
# Linux x86_64 (Docker)
|
||||||
|
echo "[2/4] Linux x86_64..."
|
||||||
|
docker run --rm \
|
||||||
|
-v "$SCRIPT_DIR":/app -w /app \
|
||||||
|
--platform linux/amd64 \
|
||||||
|
rust:slim \
|
||||||
|
bash -c "apt-get update -qq && apt-get install -y -qq pkg-config libssl-dev >/dev/null 2>&1 && cargo build --release -p native-node --no-default-features --target-dir target/docker_linux_amd64 && cp target/docker_linux_amd64/release/native-node /app/frontend/public/download/kipina-node-linux-x86_64"
|
||||||
|
echo " $(ls -lh "$OUT/kipina-node-linux-x86_64" | awk '{print $5}')"
|
||||||
|
|
||||||
|
# Linux ARM64 (Docker)
|
||||||
|
echo "[3/4] Linux ARM64..."
|
||||||
|
docker run --rm \
|
||||||
|
-v "$SCRIPT_DIR":/app -w /app \
|
||||||
|
--platform linux/arm64 \
|
||||||
|
rust:slim \
|
||||||
|
bash -c "apt-get update -qq && apt-get install -y -qq pkg-config libssl-dev >/dev/null 2>&1 && cargo build --release -p native-node --no-default-features --target-dir target/docker_linux_arm64 && cp target/docker_linux_arm64/release/native-node /app/frontend/public/download/kipina-node-linux-arm64"
|
||||||
|
echo " $(ls -lh "$OUT/kipina-node-linux-arm64" | awk '{print $5}')"
|
||||||
|
|
||||||
|
# Windows x86_64 (Docker + mingw-w64)
|
||||||
|
echo "[4/4] Windows x86_64..."
|
||||||
|
docker run --rm \
|
||||||
|
-v "$SCRIPT_DIR":/app -w /app \
|
||||||
|
--platform linux/amd64 \
|
||||||
|
rust:slim \
|
||||||
|
bash -c "apt-get update -qq && apt-get install -y -qq gcc-mingw-w64-x86-64 pkg-config libssl-dev >/dev/null 2>&1 && rustup target add x86_64-pc-windows-gnu && cargo build --release -p native-node --no-default-features --target x86_64-pc-windows-gnu && cp target/x86_64-pc-windows-gnu/release/native-node.exe /app/frontend/public/download/kipina-node-windows-x86_64.exe"
|
||||||
|
echo " $(ls -lh "$OUT/kipina-node-windows-x86_64.exe" | awk '{print $5}')"
|
||||||
|
|
||||||
|
# Tallennetaan onnistuneen buildin hash
|
||||||
|
echo "$CURRENT_HASH" > "$HASH_FILE"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Binäärit valmiina ==="
|
||||||
|
ls -lh "$OUT"/kipina-node-*
|
||||||
30
network-poc/deploy-binaries.sh
Executable file
@@ -0,0 +1,30 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
SERVER="ubuntu@86.50.252.98"
|
||||||
|
REMOTE_DIR="~/code/agentic-studio/network-poc"
|
||||||
|
KEY="$HOME/.ssh/id_rsa"
|
||||||
|
SSH_OPTS="-o StrictHostKeyChecking=no -i $KEY"
|
||||||
|
|
||||||
|
if ! ssh-add -l 2>/dev/null | grep -q id_rsa; then
|
||||||
|
echo "SSH-avain ei ole agentissa. Lisätään..."
|
||||||
|
ssh-add "$KEY"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== Kipinä Node - Vain Binäärien Päivitys ==="
|
||||||
|
|
||||||
|
# 1. Käännetään binäärit (hyödyntää korjattua build-binaries.sh cache logiikkaa)
|
||||||
|
"$SCRIPT_DIR/build-binaries.sh"
|
||||||
|
|
||||||
|
# 2. Siirretään binäärit suoraan kohdekoneen hakemistoon ohittaen Docker-imagen täyden rakennuksen
|
||||||
|
echo ""
|
||||||
|
echo "[Vieminen uuteen kohteeseen...]"
|
||||||
|
ssh $SSH_OPTS "$SERVER" "mkdir -p $REMOTE_DIR/frontend/dist/download"
|
||||||
|
scp $SSH_OPTS "$SCRIPT_DIR/frontend/public/download"/kipina-node-* "$SERVER:$REMOTE_DIR/frontend/dist/download/"
|
||||||
|
scp $SSH_OPTS "$SCRIPT_DIR/frontend/public"/kipina-node "$SERVER:$REMOTE_DIR/frontend/dist/"
|
||||||
|
|
||||||
|
# 3. Luvat kuntoon
|
||||||
|
ssh $SSH_OPTS "$SERVER" "chmod +x $REMOTE_DIR/frontend/dist/download/kipina-node-*"
|
||||||
|
|
||||||
|
echo "=== Valmis! Binäärit ovat nyt asennettu livenä ja ladattavissa kipina.studiosta ==="
|
||||||
28
network-poc/deploy-fast.sh
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Nopea deploy: päivittää vain frontendin (ei kontin uudelleenkäynnistystä)
|
||||||
|
# Hub-binäärin päivitys: käytä deploy.sh tai deploy-light.sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SERVER="ubuntu@86.50.252.98"
|
||||||
|
REMOTE_DIR="~/code/agentic-studio/network-poc"
|
||||||
|
SSH_OPTS="-o StrictHostKeyChecking=no"
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
echo "=== Kipinä Studio — Frontend Deploy ==="
|
||||||
|
|
||||||
|
# 1. Buildaa frontend paikallisesti
|
||||||
|
echo "[1/2] Rakennetaan frontend..."
|
||||||
|
cd "$SCRIPT_DIR/frontend"
|
||||||
|
[ -d node_modules ] || npm install --silent
|
||||||
|
npm run build --silent 2>&1 | tail -1
|
||||||
|
|
||||||
|
# 2. Synkataan dist/ palvelimelle (vain muuttuneet tiedostot)
|
||||||
|
echo "[2/2] Synkataan dist/ → palvelin..."
|
||||||
|
ssh $SSH_OPTS $SERVER "mkdir -p $REMOTE_DIR/frontend/dist"
|
||||||
|
rsync -az --delete -e "ssh $SSH_OPTS" "$SCRIPT_DIR/frontend/dist/" "$SERVER:$REMOTE_DIR/frontend/dist/"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Valmis! Frontend päivitetty — ei uudelleenkäynnistystä ==="
|
||||||
|
echo " https://kipina.studio"
|
||||||
|
echo ""
|
||||||
|
echo "Huom: Jos Rust-koodi (hub/) muuttui, aja: ./deploy.sh"
|
||||||
33
network-poc/deploy-light.sh
Executable file
@@ -0,0 +1,33 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Kevyt deploy: lähetetään vain koodi, palvelin buildaa itse
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SERVER="ubuntu@86.50.252.98"
|
||||||
|
REMOTE_DIR="~/code/agentic-studio/network-poc"
|
||||||
|
SSH_OPTS="-o StrictHostKeyChecking=no"
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
echo "=== Kipinä Studio Deploy (remote build) ==="
|
||||||
|
|
||||||
|
# 1. Synkataan koodi palvelimelle (vain muuttuneet tiedostot)
|
||||||
|
echo "[1/3] Synkataan koodi..."
|
||||||
|
rsync -az --delete \
|
||||||
|
--exclude 'target/' \
|
||||||
|
--exclude 'node_modules/' \
|
||||||
|
--exclude 'dist/' \
|
||||||
|
--exclude '.astro/' \
|
||||||
|
--exclude 'temp/' \
|
||||||
|
--exclude '*.db' \
|
||||||
|
--exclude '.git/' \
|
||||||
|
"$SCRIPT_DIR/" "$SERVER:$REMOTE_DIR/"
|
||||||
|
|
||||||
|
# 2. Rakennetaan image palvelimella
|
||||||
|
echo "[2/3] Rakennetaan image palvelimella..."
|
||||||
|
ssh $SSH_OPTS $SERVER "cd $REMOTE_DIR && docker build -f Dockerfile.prod -t kipina-agentic:latest ."
|
||||||
|
|
||||||
|
# 3. Käynnistetään
|
||||||
|
echo "[3/3] Käynnistetään..."
|
||||||
|
ssh $SSH_OPTS $SERVER "cd $REMOTE_DIR && docker compose -f docker-compose.prod.yml down && docker compose -f docker-compose.prod.yml up -d"
|
||||||
|
|
||||||
|
echo "=== Valmis! https://kipina.studio ==="
|
||||||
13
network-poc/deploy-with-native.sh
Executable file
@@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Deploy + native-node-binäärien käännös (jos muutoksia)
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
echo "=== Kipinä Studio Deploy (+ native binäärit) ==="
|
||||||
|
|
||||||
|
# Käännetään native-node-binäärit (ohittaa automaattisesti jos ei muutoksia)
|
||||||
|
"$SCRIPT_DIR/build-binaries.sh"
|
||||||
|
|
||||||
|
# Ajetaan normaali deploy
|
||||||
|
exec "$SCRIPT_DIR/deploy.sh"
|
||||||
@@ -40,20 +40,20 @@ echo "[1/4] Rakennetaan image lokaalisti..."
|
|||||||
docker build --platform linux/amd64 -f Dockerfile.prod -t kipina-agentic:latest .
|
docker build --platform linux/amd64 -f Dockerfile.prod -t kipina-agentic:latest .
|
||||||
|
|
||||||
# 2. Tallennetaan tiedostoon
|
# 2. Tallennetaan tiedostoon
|
||||||
echo "[2/5] Pakataan image..."
|
echo "[2/4] Pakataan image..."
|
||||||
docker save kipina-agentic:latest | gzip > /tmp/kipina-agentic.tar.gz
|
docker save kipina-agentic:latest | gzip > /tmp/kipina-agentic.tar.gz
|
||||||
echo " Koko: $(du -h /tmp/kipina-agentic.tar.gz | cut -f1)"
|
echo " Koko: $(du -h /tmp/kipina-agentic.tar.gz | cut -f1)"
|
||||||
|
|
||||||
# 3. Siirretään palvelimelle
|
# 3. Siirretään palvelimelle
|
||||||
echo "[3/5] Siirretään palvelimelle..."
|
echo "[3/4] Siirretään palvelimelle..."
|
||||||
scp $SSH_OPTS /tmp/kipina-agentic.tar.gz $SERVER:/tmp/
|
scp $SSH_OPTS /tmp/kipina-agentic.tar.gz $SERVER:/tmp/
|
||||||
scp $SSH_OPTS docker-compose.prod.yml Caddyfile.prod $SERVER:$REMOTE_DIR/
|
scp $SSH_OPTS docker-compose.prod.yml Caddyfile.prod $SERVER:$REMOTE_DIR/
|
||||||
|
|
||||||
# 4. Ladataan image ja käynnistetään
|
# 4. Ladataan image ja käynnistetään
|
||||||
echo "[4/5] Ladataan image palvelimella..."
|
echo "[4/4] Ladataan image palvelimella..."
|
||||||
ssh $SSH_OPTS $SERVER "gunzip -c /tmp/kipina-agentic.tar.gz | docker load && rm /tmp/kipina-agentic.tar.gz"
|
ssh $SSH_OPTS $SERVER "gunzip -c /tmp/kipina-agentic.tar.gz | docker load && rm /tmp/kipina-agentic.tar.gz"
|
||||||
|
|
||||||
echo "[5/5] Käynnistetään palvelut uudelleen..."
|
echo "[4/4] Käynnistetään palvelut uudelleen..."
|
||||||
ssh $SSH_OPTS $SERVER "cd $REMOTE_DIR && docker compose -f docker-compose.prod.yml down && docker compose -f docker-compose.prod.yml up -d"
|
ssh $SSH_OPTS $SERVER "cd $REMOTE_DIR && docker compose -f docker-compose.prod.yml down && docker compose -f docker-compose.prod.yml up -d"
|
||||||
|
|
||||||
echo "=== Valmis! https://kipina.studio ==="
|
echo "=== Valmis! https://kipina.studio ==="
|
||||||
|
|||||||
@@ -19,8 +19,12 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
- DATABASE_PATH=/data/nodes.db
|
- DATABASE_PATH=/data/nodes.db
|
||||||
|
- STATIC_DIR=/app/frontend/dist
|
||||||
|
- ADMIN_PASSWORD=${ADMIN_PASSWORD:-}
|
||||||
|
- NODE_API_KEY=${NODE_API_KEY:-}
|
||||||
volumes:
|
volumes:
|
||||||
- hub_data:/data
|
- hub_data:/data
|
||||||
|
- ./frontend/dist:/app/frontend/dist:ro
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
caddy_data:
|
caddy_data:
|
||||||
|
|||||||
3
network-poc/frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.astro/
|
||||||
2
network-poc/frontend/astro.config.mjs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
export default defineConfig({});
|
||||||
4721
network-poc/frontend/package-lock.json
generated
Normal file
13
network-poc/frontend/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
network-poc/frontend/public/avatars/aikuinen_susi.webp
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
network-poc/frontend/public/avatars/bear.webp
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
network-poc/frontend/public/avatars/beaver.webp
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
network-poc/frontend/public/avatars/chameleon.webp
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
network-poc/frontend/public/avatars/elephant.webp
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
network-poc/frontend/public/avatars/gecko.webp
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
network-poc/frontend/public/avatars/gecko_notext.webp
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
network-poc/frontend/public/avatars/karhunpentu.webp
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
network-poc/frontend/public/avatars/kettu_notext.webp
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
network-poc/frontend/public/avatars/kipina_notext.webp
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
network-poc/frontend/public/avatars/laiskiainen.webp
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
network-poc/frontend/public/avatars/laiskiainen_notext.webp
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
network-poc/frontend/public/avatars/lion.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
network-poc/frontend/public/avatars/mantis.webp
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
network-poc/frontend/public/avatars/owl.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
network-poc/frontend/public/avatars/penguin.webp
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
network-poc/frontend/public/avatars/pesukarhu.webp
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
network-poc/frontend/public/avatars/pesukarhu_notext.webp
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
network-poc/frontend/public/avatars/serpent.webp
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
network-poc/frontend/public/avatars/spider.webp
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
network-poc/frontend/public/avatars/susi_notext.webp
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
network-poc/frontend/public/avatars/tortoise.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
network-poc/frontend/public/avatars/walrus.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
1
network-poc/frontend/public/download/.build-hash
Normal file
@@ -0,0 +1 @@
|
|||||||
|
403f35efdcdf6317309f2e6b450c0ea257550b6a
|
||||||
BIN
network-poc/frontend/public/download/kipina-node-linux-arm64
Executable file
BIN
network-poc/frontend/public/download/kipina-node-linux-x86_64
Executable file
BIN
network-poc/frontend/public/download/kipina-node-macos-arm64
Executable file
BIN
network-poc/frontend/public/download/kipina-node-windows-x86_64.exe
Executable file
73
network-poc/frontend/public/join.sh
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Kipinä — liitä koneesi laskentaverkkoon
|
||||||
|
set -e
|
||||||
|
|
||||||
|
HUB_URL="${KIPINA_HUB:-wss://kipina.studio/ws}"
|
||||||
|
MODEL="${KIPINA_MODEL:-qwen2.5-coder:3b}"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo " ╔══════════════════════════════════════╗"
|
||||||
|
echo " ║ Kipinä Agentic Network — Node Join ║"
|
||||||
|
echo " ╚══════════════════════════════════════╝"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 1. Ollama
|
||||||
|
if command -v ollama &>/dev/null; then
|
||||||
|
echo " ✓ Ollama löytyi: $(ollama --version 2>/dev/null || echo 'asennettu')"
|
||||||
|
else
|
||||||
|
echo " Ollama ei ole asennettu."
|
||||||
|
echo ""
|
||||||
|
read -p " Asennetaanko Ollama? (k/e) " -n 1 -r; echo
|
||||||
|
if [[ $REPLY =~ ^[Kk]$ ]]; then
|
||||||
|
echo " Asennetaan Ollama..."
|
||||||
|
curl -fsSL https://ollama.ai/install.sh | sh
|
||||||
|
else
|
||||||
|
echo " Ollama vaaditaan laskentaan. Asenna: https://ollama.ai"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Varmistetaan että Ollama on käynnissä
|
||||||
|
if ! curl -s http://localhost:11434/api/tags &>/dev/null; then
|
||||||
|
echo " Käynnistetään Ollama..."
|
||||||
|
ollama serve &>/dev/null &
|
||||||
|
sleep 3
|
||||||
|
if ! curl -s http://localhost:11434/api/tags &>/dev/null; then
|
||||||
|
echo " ✗ Ollama ei käynnistynyt. Aja: ollama serve"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo " ✓ Ollama käynnissä"
|
||||||
|
|
||||||
|
# 3. Malli
|
||||||
|
if ollama list 2>/dev/null | grep -q "$MODEL"; then
|
||||||
|
echo " ✓ Malli $MODEL ladattu"
|
||||||
|
else
|
||||||
|
echo " Ladataan malli $MODEL..."
|
||||||
|
ollama pull "$MODEL"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. Native-node
|
||||||
|
echo ""
|
||||||
|
echo " Yhdistetään hubiin: $HUB_URL"
|
||||||
|
echo " Malli: $MODEL"
|
||||||
|
echo " Ctrl+C pysäyttää"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Tarkistetaan onko native-node käännetty
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
NATIVE_BIN="$SCRIPT_DIR/target/release/native-node"
|
||||||
|
|
||||||
|
if [ -f "$NATIVE_BIN" ]; then
|
||||||
|
HUB_URL="$HUB_URL" OLLAMA_MODEL="$MODEL" "$NATIVE_BIN"
|
||||||
|
elif command -v cargo &>/dev/null && [ -f "$SCRIPT_DIR/native-node/Cargo.toml" ]; then
|
||||||
|
echo " Käännetään native-node..."
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
cargo build --release -p native-node --no-default-features 2>&1 | tail -1
|
||||||
|
HUB_URL="$HUB_URL" OLLAMA_MODEL="$MODEL" "$NATIVE_BIN"
|
||||||
|
else
|
||||||
|
echo " ✗ native-node binääriä ei löydy eikä Rust ole asennettu."
|
||||||
|
echo " Asenna Rust: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"
|
||||||
|
echo " Tai lataa valmis binääri: https://kipina.studio/download"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
131
network-poc/frontend/public/kipina-node
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Kipinä Node — lataa oikea binääri ja käynnistä
|
||||||
|
set -e
|
||||||
|
|
||||||
|
BASE_URL="https://kipina.studio/download"
|
||||||
|
HUB_URL="${KIPINA_HUB:-wss://kipina.studio/ws}"
|
||||||
|
OLLAMA_URL="${OLLAMA_URL:-http://localhost:11434}"
|
||||||
|
|
||||||
|
# Tunnista OS ja arkkitehtuuri
|
||||||
|
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||||
|
ARCH=$(uname -m)
|
||||||
|
|
||||||
|
case "$OS-$ARCH" in
|
||||||
|
darwin-arm64) BINARY="kipina-node-macos-arm64" ;;
|
||||||
|
darwin-x86_64) BINARY="kipina-node-macos-arm64" ;; # Rosetta
|
||||||
|
linux-x86_64) BINARY="kipina-node-linux-x86_64" ;;
|
||||||
|
linux-aarch64) BINARY="kipina-node-linux-arm64" ;;
|
||||||
|
*) echo "Ei tuettu: $OS-$ARCH"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo " ╔══════════════════════════════════════╗"
|
||||||
|
echo " ║ Kipinä Agentic Node ║"
|
||||||
|
echo " ╚══════════════════════════════════════╝"
|
||||||
|
echo ""
|
||||||
|
echo " OS: $OS ($ARCH)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Etsi Ollama-instanssit
|
||||||
|
CANDIDATES=(
|
||||||
|
"http://localhost:11434"
|
||||||
|
"http://127.0.0.1:11434"
|
||||||
|
"http://ollama:11434"
|
||||||
|
"http://host.docker.internal:11434"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Lisää OLLAMA_URL listaan jos asetettu ja ei jo mukana
|
||||||
|
if [ -n "$OLLAMA_URL" ]; then
|
||||||
|
ALREADY=false
|
||||||
|
for c in "${CANDIDATES[@]}"; do
|
||||||
|
[ "$c" = "$OLLAMA_URL" ] && ALREADY=true
|
||||||
|
done
|
||||||
|
$ALREADY || CANDIDATES=("$OLLAMA_URL" "${CANDIDATES[@]}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " Etsitään Ollama-instansseja..."
|
||||||
|
FOUND=()
|
||||||
|
for url in "${CANDIDATES[@]}"; do
|
||||||
|
if curl -s --connect-timeout 1 "$url/api/tags" &>/dev/null; then
|
||||||
|
FOUND+=("$url")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ${#FOUND[@]} -eq 0 ]; then
|
||||||
|
# Ei löytynyt — yritä käynnistää lokaali
|
||||||
|
if command -v ollama &>/dev/null; then
|
||||||
|
echo " Käynnistetään Ollama..."
|
||||||
|
ollama serve &>/dev/null &
|
||||||
|
sleep 3
|
||||||
|
if curl -s --connect-timeout 1 "http://localhost:11434/api/tags" &>/dev/null; then
|
||||||
|
OLLAMA_URL="http://localhost:11434"
|
||||||
|
echo " ✓ Ollama käynnistetty ($OLLAMA_URL)"
|
||||||
|
else
|
||||||
|
echo " ✗ Ollaman käynnistys epäonnistui."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo " ✗ Ollamaa ei löytynyt."
|
||||||
|
echo " Kontti/remote: OLLAMA_URL=http://HOST:11434 ./kipina-node"
|
||||||
|
echo " Asenna: curl -fsSL https://ollama.ai/install.sh | sh"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
elif [ ${#FOUND[@]} -eq 1 ]; then
|
||||||
|
OLLAMA_URL="${FOUND[0]}"
|
||||||
|
echo " ✓ Ollama löytyi: $OLLAMA_URL"
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo " Löytyi ${#FOUND[@]} Ollama-instanssia:"
|
||||||
|
echo ""
|
||||||
|
for i in "${!FOUND[@]}"; do
|
||||||
|
echo " $((i+1))) ${FOUND[$i]}"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
read -p " Valitse [1-${#FOUND[@]}]: " -r CHOICE
|
||||||
|
if [[ "$CHOICE" =~ ^[0-9]+$ ]] && [ "$CHOICE" -ge 1 ] && [ "$CHOICE" -le ${#FOUND[@]} ]; then
|
||||||
|
OLLAMA_URL="${FOUND[$((CHOICE-1))]}"
|
||||||
|
else
|
||||||
|
OLLAMA_URL="${FOUND[0]}"
|
||||||
|
echo " Käytetään oletusta: $OLLAMA_URL"
|
||||||
|
fi
|
||||||
|
echo " ✓ Valittu: $OLLAMA_URL"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo " Hub: $HUB_URL"
|
||||||
|
echo " Ollama: $OLLAMA_URL"
|
||||||
|
if [ -n "$KIPINA_MODEL" ]; then
|
||||||
|
echo " Malli: $KIPINA_MODEL (Ympäristömuuttujasta)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Lataa binääri
|
||||||
|
BIN_PATH="./kipina-node-bin"
|
||||||
|
if [ -f "$BIN_PATH" ]; then
|
||||||
|
echo ""
|
||||||
|
read -p " Löydettiin vanha kipina-node-bin lokaalisti. Haluatko poistaa sen ja ladata uusimman version? [Y/n] " -r DEL_CHOICE
|
||||||
|
if [[ "$DEL_CHOICE" =~ ^[Nn]$ ]]; then
|
||||||
|
echo " ✓ Käytetään lokaalia versiota."
|
||||||
|
else
|
||||||
|
rm -f "$BIN_PATH"
|
||||||
|
echo " ✓ Vanha binääri poistettu ja korvataan uudella."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$BIN_PATH" ]; then
|
||||||
|
echo " Ladataan tuorein $BINARY..."
|
||||||
|
curl -sSL "$BASE_URL/$BINARY?v=$(date +%s)" -o "$BIN_PATH"
|
||||||
|
chmod +x "$BIN_PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo " ✓ Siirrytään Kipinä Noden hallintaan..."
|
||||||
|
echo " Ctrl+C pysäyttää"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ -n "$KIPINA_MODEL" ]; then
|
||||||
|
export OLLAMA_MODEL="$KIPINA_MODEL"
|
||||||
|
fi
|
||||||
|
export HUB_URL="$HUB_URL"
|
||||||
|
export OLLAMA_URL="$OLLAMA_URL"
|
||||||
|
exec "$BIN_PATH"
|
||||||
63
network-poc/frontend/public/pkg/node.d.ts
vendored
Normal 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>;
|
||||||
1741
network-poc/frontend/public/pkg/node.js
Normal file
BIN
network-poc/frontend/public/pkg/node_bg.wasm
Normal file
24
network-poc/frontend/public/pkg/node_bg.wasm.d.ts
vendored
Normal 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;
|
||||||
15
network-poc/frontend/public/pkg/package.json
Normal 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/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
27
network-poc/frontend/public/templates/fastapi-crud.json
Normal 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"]
|
||||||
|
}
|
||||||
79
network-poc/frontend/src/components/AgentBar.astro
Normal 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ä. Hyvän promptin rakenne: 1. Rooli: 'You are an expert...' 2. Säännöt: RULES/CRITICAL RULES listana 3. Esimerkit: EXAMPLE OUTPUT 4. Kiellot: NEVER-lista 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ä. Suositus: • Manageri: 0.5 (tarkat tiedostolistat) • Koodari: 0.7 (toimiva koodi + vaihtelu) • 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ä). Suositus: • Manageri: 256-512 (lyhyet tiedostolistat) • Koodari: 1024-2048 (täydet tiedostot, CRUD-endpointit) • Testaaja: 256-512 (lyhyet arvioinnit) 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. Suositus: • Boilerplate-koodi: 20-30 (tutut patternit) • Yleiskoodi: 40 (hyvä oletus) • Luova teksti: 60-80 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. Suositus: • Koodi: 1.1-1.2 (lievä, sallii toiston) • Teksti: 1.15-1.3 (vahvempi) • 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>
|
||||||
15
network-poc/frontend/src/components/Editor.astro
Normal 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>
|
||||||
6
network-poc/frontend/src/components/Guide.astro
Normal 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>
|
||||||
67
network-poc/frontend/src/components/Settings.astro
Normal 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>
|
||||||
48
network-poc/frontend/src/components/StatusBar.astro
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<!-- 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 selaimessa">Alusta</button>
|
||||||
|
</span>
|
||||||
|
<span class="status-separator">│</span>
|
||||||
|
<span class="status-group">
|
||||||
|
<button id="join-btn" class="btn btn-green" onclick="showJoinDialog()" title="Liitä oma koneesi laskentaverkkoon (natiivi, nopea)">+ Liitä koneesi</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Join-dialogi -->
|
||||||
|
<div id="join-dialog" style="display:none;margin-top:8px;padding:16px;background:var(--panel);border:1px solid var(--border);border-radius:6px;font-size:14px">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
|
||||||
|
<span style="color:#e6edf3;font-weight:600;font-size:16px">Liitä koneesi laskentaverkkoon</span>
|
||||||
|
<button onclick="document.getElementById('join-dialog').style.display='none'" style="background:none;border:none;color:#8b949e;cursor:pointer;font-size:18px">✕</button>
|
||||||
|
</div>
|
||||||
|
<p style="color:#8b949e;margin-bottom:16px">Koneesi suorittaa tehtäviä ~10-50x nopeammin kuin selainlaskenta. Kaksi vaihetta:</p>
|
||||||
|
|
||||||
|
<!-- Vaihe 1: Ollama -->
|
||||||
|
<div style="margin-bottom:14px;padding:12px;background:var(--bg);border-radius:4px;border-left:3px solid var(--accent)">
|
||||||
|
<div style="color:#e6edf3;font-weight:600;margin-bottom:6px">1. Asenna Ollama <span style="color:#8b949e;font-weight:normal">(kielimallimoottori)</span></div>
|
||||||
|
<div style="display:flex;gap:6px;align-items:center;margin-bottom:6px">
|
||||||
|
<code style="flex:1;background:#010409;padding:8px 12px;border-radius:4px;color:var(--green);font-family:'Courier New',monospace;font-size:13px;user-select:all">curl -fsSL https://ollama.ai/install.sh | sh</code>
|
||||||
|
<button onclick="navigator.clipboard.writeText('curl -fsSL https://ollama.ai/install.sh | sh');this.textContent='✓';setTimeout(()=>this.textContent='Kopioi',1500)" class="btn btn-accent" style="padding:6px 10px">Kopioi</button>
|
||||||
|
</div>
|
||||||
|
<div style="color:#8b949e;font-size:12px">macOS: <code style="color:var(--accent)">brew install ollama</code> · Windows: <a href="https://ollama.ai/download" target="_blank" style="color:var(--accent)">ollama.ai/download</a> · Jos jo asennettu → siirry vaiheeseen 2.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Vaihe 2: Kipinä-node -->
|
||||||
|
<div style="padding:12px;background:var(--bg);border-radius:4px;border-left:3px solid var(--green)">
|
||||||
|
<div style="color:#e6edf3;font-weight:600;margin-bottom:6px">2. Käynnistä Kipinä-node</div>
|
||||||
|
<div style="display:flex;gap:6px;align-items:center;margin-bottom:6px">
|
||||||
|
<code style="flex:1;background:#010409;padding:8px 12px;border-radius:4px;color:var(--green);font-family:'Courier New',monospace;font-size:13px;user-select:all">curl -sSL "https://kipina.studio/kipina-node?v=$(date +%s)" -o kipina-node && chmod +x kipina-node && ./kipina-node</code>
|
||||||
|
<button onclick="navigator.clipboard.writeText('curl -sSL \"https://kipina.studio/kipina-node?v=$(date +%s)\" -o kipina-node && chmod +x kipina-node && ./kipina-node');this.textContent='✓';setTimeout(()=>this.textContent='Kopioi',1500)" class="btn btn-green" style="padding:6px 10px">Kopioi</button>
|
||||||
|
</div>
|
||||||
|
<div style="color:#8b949e;font-size:12px">Lataa kielimallin (~2GB) automaattisesti ensimmäisellä kerralla. Ctrl+C pysäyttää.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
10
network-poc/frontend/src/components/Terminal.astro
Normal 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>
|
||||||
1238
network-poc/frontend/src/pages/index.astro
Normal file
200
network-poc/frontend/src/styles/global.css
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
: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;
|
||||||
|
font-size: 16px;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container { max-width: 1600px; margin: 0 auto; padding: 20px 40px; }
|
||||||
|
|
||||||
|
/* Tabs */
|
||||||
|
.tabs { display: flex; gap: 4px; margin-bottom: 16px; }
|
||||||
|
.tab {
|
||||||
|
padding: 10px 20px; border-radius: 6px 6px 0 0; cursor: pointer;
|
||||||
|
border: 1px solid var(--border); border-bottom: none;
|
||||||
|
background: var(--bg); color: #8b949e; font-size: 15px;
|
||||||
|
}
|
||||||
|
.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: 10px 16px; background: var(--bg);
|
||||||
|
border: 1px solid var(--border); border-radius: 6px 6px 0 0;
|
||||||
|
font-family: 'Courier New', monospace; font-size: 14px;
|
||||||
|
}
|
||||||
|
.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: 16px;
|
||||||
|
min-height: 400px; max-height: 70vh; overflow-y: auto;
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
.terminal-line { padding: 1px 0; white-space: pre-wrap; word-break: break-word; }
|
||||||
|
.terminal-prompt { color: var(--yellow); margin-right: 8px; }
|
||||||
|
.terminal-input-row {
|
||||||
|
display: flex; align-items: center; position: relative;
|
||||||
|
background: #0d1117; border: 1px solid var(--accent); border-top: none;
|
||||||
|
border-radius: 0 0 6px 6px; padding: 10px 14px;
|
||||||
|
font-family: 'Courier New', monospace; font-size: 15px;
|
||||||
|
box-shadow: 0 2px 8px rgba(88,166,255,0.1);
|
||||||
|
}
|
||||||
|
.terminal-input {
|
||||||
|
flex: 1; background: transparent; border: none; outline: none;
|
||||||
|
color: var(--green); font-family: inherit; font-size: 16px;
|
||||||
|
}
|
||||||
|
.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: 14px;
|
||||||
|
padding: 8px 8px 6px;
|
||||||
|
text-align: center;
|
||||||
|
width: 90px;
|
||||||
|
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: 64px; height: 64px; border-radius: 14px;
|
||||||
|
margin-bottom: 4px; border: 2px solid rgba(240,246,252,0.1);
|
||||||
|
transition: all 0.4s ease; object-fit: cover;
|
||||||
|
}
|
||||||
|
.agent-avatar .avatar-name {
|
||||||
|
font-size: 11px; 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) } }
|
||||||
1
network-poc/frontend/tsconfig.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{ "extends": "astro/tsconfigs/strict" }
|
||||||
7
network-poc/hub-local.log
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.14s
|
||||||
|
Running `target/debug/hub`
|
||||||
|
[2m2026-04-12T03:21:35.058711Z[0m [32m INFO[0m [2mhub[0m[2m:[0m Tietokanta alustettu
|
||||||
|
[2m2026-04-12T03:21:35.061635Z[0m [32m INFO[0m [2mhub[0m[2m:[0m Kipinä Agent Hub v0.3.1 käynnistyy osoitteessa http://localhost:3000
|
||||||
|
[2m2026-04-12T03:21:49.577628Z[0m [32m INFO[0m [2mhub[0m[2m:[0m Solmu 1 yhdistyi osoitteesta 127.0.0.1
|
||||||
|
[2m2026-04-12T03:21:49.600021Z[0m [32m INFO[0m [2mhub[0m[2m:[0m Solmu 1 (natiivi) | 127.0.0.1 | Mac | Darwin 26.3.1 | 12 ydintä | 32768 MB RAM | varaus: 4 GB
|
||||||
|
[2m2026-04-12T03:21:49.600099Z[0m [32m INFO[0m [2mhub[0m[2m:[0m GPU 0: Apple M2 Max | VRAM: 0/24576 MB | 0°C | 0%
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "hub"
|
name = "hub"
|
||||||
version = "0.2.4"
|
version = "0.3.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@@ -26,6 +26,36 @@ impl NodeDb {
|
|||||||
INSERT INTO _schema_version VALUES (2);
|
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);
|
||||||
|
");
|
||||||
|
}
|
||||||
|
if version < 4 {
|
||||||
|
let _ = conn.execute_batch("
|
||||||
|
ALTER TABLE node_sessions ADD COLUMN is_paused BOOLEAN DEFAULT 0;
|
||||||
|
DELETE FROM _schema_version;
|
||||||
|
INSERT INTO _schema_version VALUES (4);
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
conn.execute_batch("
|
conn.execute_batch("
|
||||||
CREATE TABLE IF NOT EXISTS node_sessions (
|
CREATE TABLE IF NOT EXISTS node_sessions (
|
||||||
@@ -61,7 +91,10 @@ impl NodeDb {
|
|||||||
has_webgpu BOOLEAN,
|
has_webgpu BOOLEAN,
|
||||||
|
|
||||||
-- Tehtävätilastot
|
-- Tehtävätilastot
|
||||||
tasks_completed INTEGER DEFAULT 0
|
tasks_completed INTEGER DEFAULT 0,
|
||||||
|
|
||||||
|
-- Ohjaustilat
|
||||||
|
is_paused BOOLEAN DEFAULT 0
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS pair_results (
|
CREATE TABLE IF NOT EXISTS pair_results (
|
||||||
@@ -160,6 +193,14 @@ impl NodeDb {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn update_session_status(&self, node_id: u64, is_paused: bool) {
|
||||||
|
let conn = self.conn.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
|
let _ = conn.execute(
|
||||||
|
"UPDATE node_sessions SET is_paused = ?1 WHERE node_id = ?2 AND disconnected_at IS NULL",
|
||||||
|
params![is_paused as i64, node_id as i64],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Sulkee saman IP:n viewer-sessiot kun aktiivinen node liittyy
|
/// Sulkee saman IP:n viewer-sessiot kun aktiivinen node liittyy
|
||||||
pub fn close_viewers_by_ip(&self, ip: &str) {
|
pub fn close_viewers_by_ip(&self, ip: &str) {
|
||||||
let conn = self.conn.lock().unwrap_or_else(|e| e.into_inner());
|
let conn = self.conn.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
@@ -193,7 +234,7 @@ impl NodeDb {
|
|||||||
"SELECT id, node_id, ip, node_type, connected_at, disconnected_at,
|
"SELECT id, node_id, ip, node_type, connected_at, disconnected_at,
|
||||||
platform, hostname, os, cpu_cores, cpu_model, ram_mb,
|
platform, hostname, os, cpu_cores, cpu_model, ram_mb,
|
||||||
gpu_name, gpu_vendor, gpu_backend, vram_total_mb, gpu_temp_c, gpu_util_pct,
|
gpu_name, gpu_vendor, gpu_backend, vram_total_mb, gpu_temp_c, gpu_util_pct,
|
||||||
allocated_gb, selected_task, has_webgpu, tasks_completed
|
allocated_gb, selected_task, has_webgpu, tasks_completed, is_paused
|
||||||
FROM node_sessions ORDER BY id DESC LIMIT ?1"
|
FROM node_sessions ORDER BY id DESC LIMIT ?1"
|
||||||
).unwrap();
|
).unwrap();
|
||||||
|
|
||||||
@@ -221,6 +262,7 @@ impl NodeDb {
|
|||||||
"selected_task": row.get::<_, Option<String>>(19)?,
|
"selected_task": row.get::<_, Option<String>>(19)?,
|
||||||
"has_webgpu": row.get::<_, Option<bool>>(20)?,
|
"has_webgpu": row.get::<_, Option<bool>>(20)?,
|
||||||
"tasks_completed": row.get::<_, i64>(21)?,
|
"tasks_completed": row.get::<_, i64>(21)?,
|
||||||
|
"is_paused": row.get::<_, Option<bool>>(22)?.unwrap_or(false),
|
||||||
}))
|
}))
|
||||||
}).unwrap().filter_map(|r| r.ok()).collect()
|
}).unwrap().filter_map(|r| r.ok()).collect()
|
||||||
}
|
}
|
||||||
@@ -279,6 +321,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(
|
pub fn insert_pair_result(
|
||||||
&self,
|
&self,
|
||||||
node_id: u64,
|
node_id: u64,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const ALLOWED_ORIGINS: &[&str] = &[
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Sallitut viestityyypit clientilta
|
// Sallitut viestityyypit clientilta
|
||||||
const ALLOWED_MSG_TYPES: &[&str] = &["auth", "result", "pair_done", "llm_chunk", "llm_done", "llm_error", "download_progress", "user_text", "single_tokenize_done"];
|
const ALLOWED_MSG_TYPES: &[&str] = &["auth", "result", "pair_done", "llm_chunk", "llm_done", "llm_error", "download_progress", "user_text", "single_tokenize_done", "status_update"];
|
||||||
|
|
||||||
struct AppState {
|
struct AppState {
|
||||||
next_node_id: Mutex<u64>,
|
next_node_id: Mutex<u64>,
|
||||||
@@ -34,15 +34,18 @@ struct AppState {
|
|||||||
total_tasks: Mutex<u64>,
|
total_tasks: Mutex<u64>,
|
||||||
stats_tx: broadcast::Sender<String>,
|
stats_tx: broadcast::Sender<String>,
|
||||||
node_channels: tokio::sync::RwLock<HashMap<u64, tokio::sync::mpsc::UnboundedSender<String>>>, // Kohdennettu reititys
|
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
|
feature_flags: tokio::sync::RwLock<HashMap<String, bool>>, // Tuntee TODO.md:n ruksit lennosta
|
||||||
ip_connections: Mutex<HashMap<IpAddr, u32>>,
|
ip_connections: Mutex<HashMap<IpAddr, u32>>,
|
||||||
node_ips: Mutex<HashMap<u64, IpAddr>>,
|
node_ips: Mutex<HashMap<u64, IpAddr>>,
|
||||||
node_tasks: Mutex<HashMap<u64, String>>, // node_id → selected_task
|
node_tasks: Mutex<HashMap<u64, String>>, // node_id → selected_task
|
||||||
node_types: Mutex<HashMap<u64, String>>, // node_id → "native" | "browser"
|
node_types: Mutex<HashMap<u64, String>>, // node_id → "native" | "browser"
|
||||||
|
node_paused: Mutex<std::collections::HashSet<u64>>, // node_id → onko tauolla
|
||||||
node_busy: Mutex<std::collections::HashSet<u64>>, // Solmut joilla on aktiivinen tehtävä
|
node_busy: Mutex<std::collections::HashSet<u64>>, // Solmut joilla on aktiivinen tehtävä
|
||||||
pending_task_ids: Mutex<std::collections::HashSet<String>>, // Hubin jakamat task_id:t (gamification-validointi)
|
pending_task_ids: Mutex<std::collections::HashSet<String>>, // Hubin jakamat task_id:t (gamification-validointi)
|
||||||
|
pending_responses: Mutex<HashMap<String, tokio::sync::oneshot::Sender<serde_json::Value>>>, // task_id → oneshot API-vastaukselle
|
||||||
api_rate_limits: Mutex<HashMap<IpAddr, (std::time::Instant, u32)>>, // IP → (ikkuna-alku, pyyntömäärä)
|
api_rate_limits: Mutex<HashMap<IpAddr, (std::time::Instant, u32)>>, // IP → (ikkuna-alku, pyyntömäärä)
|
||||||
|
node_models: tokio::sync::RwLock<HashMap<u64, serde_json::Value>>, // node_id → ollama tags JSON
|
||||||
db: db::NodeDb,
|
db: db::NodeDb,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +83,8 @@ tr:hover td { background:#1c2333; }
|
|||||||
.table-wrap { overflow-x:auto; max-height:70vh; overflow-y:auto; }
|
.table-wrap { overflow-x:auto; max-height:70vh; overflow-y:auto; }
|
||||||
.online { color:var(--green); }
|
.online { color:var(--green); }
|
||||||
.offline { color:#8b949e; }
|
.offline { color:#8b949e; }
|
||||||
|
.pause-btn { background:var(--panel); border:1px solid var(--border); color:var(--text); padding:4px 8px; border-radius:4px; cursor:pointer; font-size:12px; }
|
||||||
|
.pause-btn:hover { border-color:var(--yellow); }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -91,6 +96,7 @@ tr:hover td { background:#1c2333; }
|
|||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<div class="tab active" onclick="showTab('sessions')">Sessiot</div>
|
<div class="tab active" onclick="showTab('sessions')">Sessiot</div>
|
||||||
<div class="tab" onclick="showTab('pairs')">Tokenisointiparit</div>
|
<div class="tab" onclick="showTab('pairs')">Tokenisointiparit</div>
|
||||||
|
<div class="tab" onclick="showTab('hardware')">Laitteisto & Mallit</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="sessions" class="panel active">
|
<div id="sessions" class="panel active">
|
||||||
@@ -99,12 +105,12 @@ tr:hover td { background:#1c2333; }
|
|||||||
<colgroup>
|
<colgroup>
|
||||||
<col style="width:35px"><col style="width:85px"><col style="width:95px"><col style="width:65px"><col style="width:110px"><col style="width:80px">
|
<col style="width:35px"><col style="width:85px"><col style="width:95px"><col style="width:65px"><col style="width:110px"><col style="width:80px">
|
||||||
<col style="width:65px"><col style="width:40px"><col style="width:70px"><col style="width:90px"><col style="width:60px">
|
<col style="width:65px"><col style="width:40px"><col style="width:70px"><col style="width:90px"><col style="width:60px">
|
||||||
<col style="width:65px"><col style="width:40px"><col style="width:130px"><col style="width:60px">
|
<col style="width:65px"><col style="width:40px"><col style="width:130px"><col style="width:60px"><col style="width:80px">
|
||||||
</colgroup>
|
</colgroup>
|
||||||
<thead><tr>
|
<thead><tr>
|
||||||
<th>ID</th><th>Tila</th><th>Tehtävä</th><th>Tyyppi</th><th>IP</th><th>Alusta</th>
|
<th>ID</th><th>Tila</th><th>Tehtävä</th><th>Tyyppi</th><th>IP</th><th>Alusta</th>
|
||||||
<th>OS</th><th>CPU</th><th>RAM</th><th>GPU</th><th>VRAM</th>
|
<th>OS</th><th>CPU</th><th>RAM</th><th>GPU</th><th>VRAM</th>
|
||||||
<th>WebGPU</th><th>Teht.</th><th>Yhdistetty</th><th>Kesto</th>
|
<th>WebGPU</th><th>Teht.</th><th>Yhdistetty</th><th>Kesto</th><th>Toiminnot</th>
|
||||||
</tr></thead><tbody id="sessions-body"></tbody></table>
|
</tr></thead><tbody id="sessions-body"></tbody></table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -118,6 +124,19 @@ tr:hover td { background:#1c2333; }
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="hardware" class="panel">
|
||||||
|
<div class="stats-grid" id="hardware-stats"></div>
|
||||||
|
<h2 style="margin-top: 10px; margin-bottom: 10px; color: var(--accent); font-size: 16px;">Käytettävissä olevat paikalliset kielimallit</h2>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table>
|
||||||
|
<thead><tr>
|
||||||
|
<th>Nimi</th><th>Koko</th><th>Parametrit</th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody id="models-body"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function showTab(name) {
|
function showTab(name) {
|
||||||
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
|
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
|
||||||
@@ -149,12 +168,16 @@ function duration(start, end) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
const [statsRes, sessionsRes, pairsRes] = await Promise.all([
|
const [statsRes, sessionsRes, pairsRes, hwRes, modelsRes] = await Promise.all([
|
||||||
fetch('/api/stats'), fetch('/api/sessions'), fetch('/api/pairs')
|
fetch('/api/stats'), fetch('/api/sessions'), fetch('/api/pairs'),
|
||||||
|
fetch('/api/v1/hardware').catch(() => ({json: async()=>({gpu_name:'', vram_mb:0, ram_mb:0})})),
|
||||||
|
fetch('/api/v1/ollama/tags').catch(() => ({json: async()=>({models:[]})}))
|
||||||
]);
|
]);
|
||||||
const stats = await statsRes.json();
|
const stats = await statsRes.json();
|
||||||
const sessions = await sessionsRes.json();
|
const sessions = await sessionsRes.json();
|
||||||
const pairs = await pairsRes.json();
|
const pairs = await pairsRes.json();
|
||||||
|
const hw = await hwRes.json().catch(() => ({gpu_name:'', vram_mb:0, ram_mb:0}));
|
||||||
|
const modelsData = await modelsRes.json().catch(() => ({models:[]}));
|
||||||
|
|
||||||
// Versio
|
// Versio
|
||||||
if (stats.version) document.getElementById('admin-version').textContent = 'v' + stats.version;
|
if (stats.version) document.getElementById('admin-version').textContent = 'v' + stats.version;
|
||||||
@@ -190,9 +213,17 @@ async function load() {
|
|||||||
document.getElementById('sessions-body').innerHTML = sessions.map(s => {
|
document.getElementById('sessions-body').innerHTML = sessions.map(s => {
|
||||||
const online = !s.disconnected_at;
|
const online = !s.disconnected_at;
|
||||||
const isViewer = s.selected_task === 'viewer';
|
const isViewer = s.selected_task === 'viewer';
|
||||||
const status = online
|
let status;
|
||||||
? (isViewer ? '<span style="color:#d29922">CONNECTED</span>' : '<span class="online">ACTIVE</span>')
|
if (!online) {
|
||||||
: '<span class="offline">offline</span>';
|
status = '<span class="offline">offline</span>';
|
||||||
|
} else if (isViewer) {
|
||||||
|
status = '<span style="color:#d29922">CONNECTED</span>';
|
||||||
|
} else if (s.is_paused) {
|
||||||
|
status = '<span style="color:#8b949e">PAUSED</span>';
|
||||||
|
} else {
|
||||||
|
status = '<span class="online">ACTIVE</span>';
|
||||||
|
}
|
||||||
|
|
||||||
const typeBadge = s.node_type === 'native' ? badge('native','blue') : badge('browser','yellow');
|
const typeBadge = s.node_type === 'native' ? badge('native','blue') : badge('browser','yellow');
|
||||||
const taskColor = isViewer ? 'yellow' : s.selected_task === 'tokenize' ? 'green' : 'blue';
|
const taskColor = isViewer ? 'yellow' : s.selected_task === 'tokenize' ? 'green' : 'blue';
|
||||||
const taskBadge = badge(taskNames[s.selected_task] || s.selected_task || '?', taskColor);
|
const taskBadge = badge(taskNames[s.selected_task] || s.selected_task || '?', taskColor);
|
||||||
@@ -205,11 +236,16 @@ async function load() {
|
|||||||
const os = s.os || '-';
|
const os = s.os || '-';
|
||||||
const time = s.connected_at ? new Date(s.connected_at).toLocaleString('fi-FI') : '';
|
const time = s.connected_at ? new Date(s.connected_at).toLocaleString('fi-FI') : '';
|
||||||
const dur = duration(s.connected_at, s.disconnected_at);
|
const dur = duration(s.connected_at, s.disconnected_at);
|
||||||
|
const actionBtn = online && !isViewer
|
||||||
|
? `<button class="pause-btn" onclick="togglePause(${s.node_id}, ${s.is_paused})">${s.is_paused ? '▶ Työhön' : '⏸ Tauolle'}</button>`
|
||||||
|
: '';
|
||||||
|
|
||||||
return `<tr>
|
return `<tr>
|
||||||
<td>${s.node_id}</td><td>${status}</td><td>${taskBadge}</td><td>${typeBadge}</td><td>${s.ip}</td>
|
<td>${s.node_id}</td><td>${status}</td><td>${taskBadge}</td><td>${typeBadge}</td><td>${s.ip}</td>
|
||||||
<td>${plat}</td><td>${os}</td><td>${cores}</td><td>${ram}</td>
|
<td>${plat}</td><td>${os}</td><td>${cores}</td><td>${ram}</td>
|
||||||
<td>${gpu}</td><td>${vram}</td><td>${gpuBadge}</td>
|
<td>${gpu}</td><td>${vram}</td><td>${gpuBadge}</td>
|
||||||
<td>${s.tasks_completed}</td><td>${time}</td><td>${dur}</td>
|
<td>${s.tasks_completed}</td><td>${time}</td><td>${dur}</td>
|
||||||
|
<td>${actionBtn}</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
@@ -229,6 +265,35 @@ async function load() {
|
|||||||
<td>${p.duration_ms||0}ms</td>
|
<td>${p.duration_ms||0}ms</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
|
// Hardware
|
||||||
|
document.getElementById('hardware-stats').innerHTML = [
|
||||||
|
{v: hw.gpu_name || '-', l: 'Paikallinen GPU tila'},
|
||||||
|
{v: hw.vram_mb ? hw.vram_mb + ' MB' : '-', l: 'GPU Muisti (VRAM)'},
|
||||||
|
{v: hw.ram_mb ? hw.ram_mb + ' MB' : '-', l: 'RAM'},
|
||||||
|
].map(s => `<div class="stat-card"><div class="val">${s.v}</div><div class="label">${s.l}</div></div>`).join('');
|
||||||
|
|
||||||
|
// Models
|
||||||
|
document.getElementById('models-body').innerHTML = (modelsData.models || []).map(m => {
|
||||||
|
const sizeGb = (m.size / (1024*1024*1024)).toFixed(2) + ' GB';
|
||||||
|
const params = m.details?.parameter_size || '-';
|
||||||
|
return `<tr>
|
||||||
|
<td><strong>${m.name}</strong></td>
|
||||||
|
<td>${sizeGb}</td>
|
||||||
|
<td>${params}</td>
|
||||||
|
</tr>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function togglePause(nodeId, isPaused) {
|
||||||
|
try {
|
||||||
|
await fetch('/api/v1/control/' + nodeId, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ action: isPaused ? 'resume' : 'pause' })
|
||||||
|
});
|
||||||
|
load(); // virkistetään
|
||||||
|
} catch(e) { console.error(e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
load();
|
load();
|
||||||
@@ -256,15 +321,18 @@ async fn main() {
|
|||||||
total_tasks: Mutex::new(0),
|
total_tasks: Mutex::new(0),
|
||||||
stats_tx: stats_tx.clone(),
|
stats_tx: stats_tx.clone(),
|
||||||
node_channels: tokio::sync::RwLock::new(HashMap::new()),
|
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()),
|
feature_flags: tokio::sync::RwLock::new(HashMap::new()),
|
||||||
ip_connections: Mutex::new(HashMap::new()),
|
ip_connections: Mutex::new(HashMap::new()),
|
||||||
node_ips: Mutex::new(HashMap::new()),
|
node_ips: Mutex::new(HashMap::new()),
|
||||||
node_tasks: Mutex::new(HashMap::new()),
|
node_tasks: Mutex::new(HashMap::new()),
|
||||||
node_types: Mutex::new(HashMap::new()),
|
node_types: Mutex::new(HashMap::new()),
|
||||||
|
node_paused: Mutex::new(std::collections::HashSet::new()),
|
||||||
node_busy: Mutex::new(std::collections::HashSet::new()),
|
node_busy: Mutex::new(std::collections::HashSet::new()),
|
||||||
pending_task_ids: Mutex::new(std::collections::HashSet::new()),
|
pending_task_ids: Mutex::new(std::collections::HashSet::new()),
|
||||||
|
pending_responses: Mutex::new(HashMap::new()),
|
||||||
api_rate_limits: Mutex::new(HashMap::new()),
|
api_rate_limits: Mutex::new(HashMap::new()),
|
||||||
|
node_models: tokio::sync::RwLock::new(HashMap::new()),
|
||||||
db: db::NodeDb::new(&std::env::var("DATABASE_PATH").unwrap_or_else(|_| "nodes.db".to_string())),
|
db: db::NodeDb::new(&std::env::var("DATABASE_PATH").unwrap_or_else(|_| "nodes.db".to_string())),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -330,15 +398,6 @@ async fn main() {
|
|||||||
let idx = (rng_state as usize) % pairs.len();
|
let idx = (rng_state as usize) % pairs.len();
|
||||||
let (en, fi) = pairs[idx];
|
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![
|
let llm_prompts = vec![
|
||||||
"Tell me a short joke.",
|
"Tell me a short joke.",
|
||||||
"What is WebGPU in one sentence?",
|
"What is WebGPU in one sentence?",
|
||||||
@@ -348,33 +407,39 @@ async fn main() {
|
|||||||
];
|
];
|
||||||
let llm_idx = (rng_state as usize / 7) % llm_prompts.len();
|
let llm_idx = (rng_state as usize / 7) % llm_prompts.len();
|
||||||
|
|
||||||
// SmolLM-prompt
|
// Smart Routing: Lähetetään vain niille, jotka valittuna ja idle
|
||||||
let smollm_msg = serde_json::json!({
|
let mut sends = Vec::new();
|
||||||
"type": "llm_prompt",
|
{
|
||||||
"prompt": llm_prompts[llm_idx],
|
let channels = state_for_task.node_channels.read().await;
|
||||||
"model": "smollm-135m",
|
let tasks = state_for_task.node_tasks.lock().unwrap();
|
||||||
});
|
let mut busy = state_for_task.node_busy.lock().unwrap();
|
||||||
let _ = state_for_task.stats_tx.send(smollm_msg.to_string());
|
|
||||||
|
|
||||||
// Qwen-prompt (sama prompti, eri malli-tagi)
|
for (node_id, task) in tasks.iter() {
|
||||||
let qwen_msg = serde_json::json!({
|
if !busy.contains(node_id) {
|
||||||
"type": "llm_prompt",
|
// Vapaa node -> lähetetään oikea tehtävä
|
||||||
"prompt": llm_prompts[llm_idx],
|
let msg = match task.as_str() {
|
||||||
"model": "qwen-05b",
|
"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" })),
|
||||||
let _ = state_for_task.stats_tx.send(qwen_msg.to_string());
|
"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
|
if let Some(payload) = msg {
|
||||||
let phi3_msg = serde_json::json!({
|
if let Some(ch) = channels.get(node_id) {
|
||||||
"type": "llm_prompt",
|
sends.push((ch.clone(), payload.to_string()));
|
||||||
"prompt": llm_prompts[llm_idx],
|
busy.insert(*node_id);
|
||||||
"model": "phi3-mini",
|
}
|
||||||
});
|
}
|
||||||
let _ = state_for_task.stats_tx.send(phi3_msg.to_string());
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -384,12 +449,15 @@ async fn main() {
|
|||||||
.route("/api/pairs", get(api_pairs))
|
.route("/api/pairs", get(api_pairs))
|
||||||
.route("/api/stats", get(api_stats))
|
.route("/api/stats", get(api_stats))
|
||||||
.route("/api/v1/chat/completions", axum::routing::post(api_chat_completions))
|
.route("/api/v1/chat/completions", axum::routing::post(api_chat_completions))
|
||||||
|
.route("/api/v1/control/:id", axum::routing::post(api_control_node))
|
||||||
.route("/api/v1/model", axum::routing::post(api_change_model))
|
.route("/api/v1/model", axum::routing::post(api_change_model))
|
||||||
.route("/api/v1/hardware", get(api_hardware))
|
.route("/api/v1/hardware", get(api_hardware))
|
||||||
.route("/api/v1/ollama/tags", get(api_ollama_tags))
|
.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))
|
.route("/admin", get(admin_page))
|
||||||
.nest_service("/", {
|
.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)))
|
ServeDir::new(&static_dir).fallback(ServeFile::new(format!("{}/index.html", static_dir)))
|
||||||
})
|
})
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
@@ -401,6 +469,26 @@ async fn main() {
|
|||||||
axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>()).await.unwrap();
|
axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>()).await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn api_control_node(
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
|
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
|
||||||
|
axum::extract::Path(id): axum::extract::Path<u64>,
|
||||||
|
axum::Json(payload): axum::Json<serde_json::Value>,
|
||||||
|
) -> axum::response::Response {
|
||||||
|
if !check_admin_auth(&headers) { return admin_unauthorized(); }
|
||||||
|
let action = payload.get("action").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
|
if action == "pause" || action == "resume" {
|
||||||
|
let msg = serde_json::json!({ "type": "control", "action": action });
|
||||||
|
let channels = state.node_channels.read().await;
|
||||||
|
if let Some(tx) = channels.get(&id) {
|
||||||
|
let _ = tx.send(msg.to_string());
|
||||||
|
tracing::info!("Lähetetty control: {} solmulle {}", action, id);
|
||||||
|
return axum::Json(serde_json::json!({"status": "ok"})).into_response();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(axum::http::StatusCode::BAD_REQUEST, "Invalid action or node offline").into_response()
|
||||||
|
}
|
||||||
|
|
||||||
async fn api_sessions(
|
async fn api_sessions(
|
||||||
headers: axum::http::HeaderMap,
|
headers: axum::http::HeaderMap,
|
||||||
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
|
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
|
||||||
@@ -462,6 +550,34 @@ fn admin_unauthorized() -> axum::response::Response {
|
|||||||
.unwrap()
|
.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 {
|
async fn admin_page(headers: axum::http::HeaderMap) -> axum::response::Response {
|
||||||
if !check_admin_auth(&headers) { return admin_unauthorized(); }
|
if !check_admin_auth(&headers) { return admin_unauthorized(); }
|
||||||
axum::response::Html(ADMIN_HTML).into_response()
|
axum::response::Html(ADMIN_HTML).into_response()
|
||||||
@@ -475,7 +591,12 @@ async fn ws_handler(
|
|||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
// Origin-tarkistus — estää cross-site WebSocket hijackingin
|
// Origin-tarkistus — estää cross-site WebSocket hijackingin
|
||||||
if let Some(origin) = headers.get("origin").and_then(|v| v.to_str().ok()) {
|
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);
|
tracing::warn!("Estetty yhteys väärällä originilla: {}", origin);
|
||||||
return (
|
return (
|
||||||
axum::http::StatusCode::FORBIDDEN,
|
axum::http::StatusCode::FORBIDDEN,
|
||||||
@@ -491,16 +612,19 @@ async fn ws_handler(
|
|||||||
.and_then(|s| s.trim().parse::<IpAddr>().ok())
|
.and_then(|s| s.trim().parse::<IpAddr>().ok())
|
||||||
.unwrap_or_else(|| addr.ip());
|
.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 is_local = ip.is_loopback();
|
||||||
let count = conns.get(&ip).copied().unwrap_or(0);
|
if !is_local {
|
||||||
if count >= 10 {
|
let conns = state.ip_connections.lock().unwrap();
|
||||||
tracing::warn!("IP {} ylitti yhteysrajan ({}/10) — estetty", ip, count);
|
let count = conns.get(&ip).copied().unwrap_or(0);
|
||||||
return (
|
if count >= 20 {
|
||||||
axum::http::StatusCode::TOO_MANY_REQUESTS,
|
tracing::warn!("IP {} ylitti yhteysrajan ({}/20) — estetty", ip, count);
|
||||||
"Max 10 yhteyttä per IP",
|
return (
|
||||||
).into_response();
|
axum::http::StatusCode::TOO_MANY_REQUESTS,
|
||||||
|
"Max 20 yhteyttä per IP",
|
||||||
|
).into_response();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -528,6 +652,17 @@ async fn broadcast_stats(state: &Arc<AppState>) {
|
|||||||
"tasks": completed
|
"tasks": completed
|
||||||
});
|
});
|
||||||
let _ = state.stats_tx.send(stats_msg.to_string());
|
let _ = state.stats_tx.send(stats_msg.to_string());
|
||||||
|
|
||||||
|
// Uutta: Laitetaan sama tieto myös kaikille yhdistyneille solmuille (viesti Hubilta Solmuille)
|
||||||
|
let node_status = serde_json::json!({
|
||||||
|
"type": "network_status",
|
||||||
|
"active_nodes": total_nodes,
|
||||||
|
"tasks": completed
|
||||||
|
});
|
||||||
|
let msg_str = node_status.to_string();
|
||||||
|
for tx in state.node_channels.read().await.values() {
|
||||||
|
let _ = tx.send(msg_str.clone());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validoi client-viesti: pakollinen "type"-kenttä, sallittu tyyppi, validi JSON
|
/// Validoi client-viesti: pakollinen "type"-kenttä, sallittu tyyppi, validi JSON
|
||||||
@@ -661,6 +796,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 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");
|
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();
|
let mut map = state.nodes_vram.lock().unwrap();
|
||||||
map.insert(node_id, allocated);
|
map.insert(node_id, allocated);
|
||||||
@@ -683,6 +830,9 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
|
|||||||
}
|
}
|
||||||
state.node_tasks.lock().unwrap().insert(node_id, selected_task);
|
state.node_tasks.lock().unwrap().insert(node_id, selected_task);
|
||||||
state.node_types.lock().unwrap().insert(node_id, node_type.to_string());
|
state.node_types.lock().unwrap().insert(node_id, node_type.to_string());
|
||||||
|
// Uudelleen-kirjautuessa nollataan tauko
|
||||||
|
state.node_paused.lock().unwrap().remove(&node_id);
|
||||||
|
state.db.update_session_status(node_id, false);
|
||||||
|
|
||||||
if node_type == "native" {
|
if node_type == "native" {
|
||||||
let sys = json.get("system");
|
let sys = json.get("system");
|
||||||
@@ -696,6 +846,12 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
|
|||||||
node_id, ip, hostname, os, cores, ram, allocated
|
node_id, ip, hostname, os, cores, ram, allocated
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Tallennetaan välitetyt mallit muistiin
|
||||||
|
if let Some(models) = json.get("models") {
|
||||||
|
let mut nm = state.node_models.write().await;
|
||||||
|
nm.insert(node_id, models.clone());
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(gpus) = json.get("gpus").and_then(|v| v.as_array()) {
|
if let Some(gpus) = json.get("gpus").and_then(|v| v.as_array()) {
|
||||||
for gpu in gpus {
|
for gpu in gpus {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
@@ -733,6 +889,18 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
|
|||||||
});
|
});
|
||||||
let _ = state.stats_tx.send(join_msg.to_string());
|
let _ = state.stats_tx.send(join_msg.to_string());
|
||||||
|
|
||||||
|
} else if msg_type == "status_update" {
|
||||||
|
let status = json.get("status").and_then(|v| v.as_str()).unwrap_or("active");
|
||||||
|
if status == "paused" {
|
||||||
|
state.node_paused.lock().unwrap().insert(node_id);
|
||||||
|
state.db.update_session_status(node_id, true);
|
||||||
|
tracing::info!("Solmu {} ({}) asettui tauolle.", node_id, ip);
|
||||||
|
} else {
|
||||||
|
state.node_paused.lock().unwrap().remove(&node_id);
|
||||||
|
state.db.update_session_status(node_id, false);
|
||||||
|
tracing::info!("Solmu {} ({}) on taas aktiivinen.", node_id, ip);
|
||||||
|
}
|
||||||
|
broadcast_stats(&state).await;
|
||||||
} else if msg_type == "result" {
|
} else if msg_type == "result" {
|
||||||
tracing::info!("Solmu {} sai tuloksen: {}", node_id, text);
|
tracing::info!("Solmu {} sai tuloksen: {}", node_id, text);
|
||||||
{
|
{
|
||||||
@@ -741,6 +909,7 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
|
|||||||
}
|
}
|
||||||
broadcast_stats(&state).await;
|
broadcast_stats(&state).await;
|
||||||
} else if msg_type == "pair_done" {
|
} else if msg_type == "pair_done" {
|
||||||
|
state.node_busy.lock().unwrap().remove(&node_id);
|
||||||
{
|
{
|
||||||
let mut json = json; // Siirretään omistajuus muokkausta varten
|
let mut json = json; // Siirretään omistajuus muokkausta varten
|
||||||
if let Some(obj) = json.as_object_mut() {
|
if let Some(obj) = json.as_object_mut() {
|
||||||
@@ -827,11 +996,18 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
|
|||||||
} else if msg_type == "llm_done" {
|
} else if msg_type == "llm_done" {
|
||||||
// Vapautetaan solmu ja tarkistetaan task_id:n aitous
|
// Vapautetaan solmu ja tarkistetaan task_id:n aitous
|
||||||
state.node_busy.lock().unwrap().remove(&node_id);
|
state.node_busy.lock().unwrap().remove(&node_id);
|
||||||
let valid_task = if let Some(tid) = json.get("task_id").and_then(|v| v.as_str()) {
|
let task_id = json.get("task_id").and_then(|v| v.as_str()).map(|s| s.to_string());
|
||||||
state.pending_task_ids.lock().unwrap().remove(tid)
|
let valid_task = if let Some(ref tid) = task_id {
|
||||||
|
state.pending_task_ids.lock().unwrap().remove(tid.as_str())
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Jos API-pyyntö odottaa tätä vastausta, reititetään suoraan oneshot-kanavaan
|
||||||
|
let api_sender = task_id.as_ref().and_then(|tid| {
|
||||||
|
state.pending_responses.lock().unwrap().remove(tid)
|
||||||
|
});
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut json = json;
|
let mut json = json;
|
||||||
if let Some(obj) = json.as_object_mut() {
|
if let Some(obj) = json.as_object_mut() {
|
||||||
@@ -851,6 +1027,12 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
|
|||||||
state.db.increment_tasks(node_id);
|
state.db.increment_tasks(node_id);
|
||||||
obj.insert("node_id".to_string(), serde_json::json!(node_id));
|
obj.insert("node_id".to_string(), serde_json::json!(node_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(sender) = api_sender {
|
||||||
|
// API-pyyntö: reititetään vastaus suoraan odottajalle
|
||||||
|
let _ = sender.send(json.clone());
|
||||||
|
}
|
||||||
|
// UI-broadcast jatkuu normaalisti
|
||||||
let _ = state.stats_tx.send(json.to_string());
|
let _ = state.stats_tx.send(json.to_string());
|
||||||
|
|
||||||
let active_incentives = state.feature_flags.read().await.get("Insentiivit").copied().unwrap_or(false);
|
let active_incentives = state.feature_flags.read().await.get("Insentiivit").copied().unwrap_or(false);
|
||||||
@@ -860,7 +1042,7 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
|
|||||||
{
|
{
|
||||||
let mut task_count = state.total_tasks.lock().unwrap();
|
let mut task_count = state.total_tasks.lock().unwrap();
|
||||||
*task_count += 1;
|
*task_count += 1;
|
||||||
|
|
||||||
if active_incentives && valid_task {
|
if active_incentives && valid_task {
|
||||||
let mut tokens = state.nodes_tokens.lock().unwrap();
|
let mut tokens = state.nodes_tokens.lock().unwrap();
|
||||||
let balance = tokens.entry(node_id).or_insert(0);
|
let balance = tokens.entry(node_id).or_insert(0);
|
||||||
@@ -868,7 +1050,7 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
|
|||||||
current_balance = *balance;
|
current_balance = *balance;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if active_incentives && ui_sync {
|
if active_incentives && ui_sync {
|
||||||
if let Some(tx) = state.node_channels.read().await.get(&node_id) {
|
if let Some(tx) = state.node_channels.read().await.get(&node_id) {
|
||||||
let msg = serde_json::json!({
|
let msg = serde_json::json!({
|
||||||
@@ -878,45 +1060,50 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
|
|||||||
let _ = tx.send(msg.to_string());
|
let _ = tx.send(msg.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
broadcast_stats(&state).await;
|
broadcast_stats(&state).await;
|
||||||
}
|
}
|
||||||
} else if msg_type == "llm_error" {
|
} else if msg_type == "llm_error" {
|
||||||
state.node_busy.lock().unwrap().remove(&node_id);
|
state.node_busy.lock().unwrap().remove(&node_id);
|
||||||
if let Some(tid) = json.get("task_id").and_then(|v| v.as_str()) {
|
let task_id = json.get("task_id").and_then(|v| v.as_str()).map(|s| s.to_string());
|
||||||
state.pending_task_ids.lock().unwrap().remove(tid);
|
if let Some(ref tid) = task_id {
|
||||||
|
state.pending_task_ids.lock().unwrap().remove(tid.as_str());
|
||||||
}
|
}
|
||||||
|
// Jos API-pyyntö odottaa, reititetään virhe oneshot-kanavaan
|
||||||
|
let api_sender = task_id.as_ref().and_then(|tid| {
|
||||||
|
state.pending_responses.lock().unwrap().remove(tid)
|
||||||
|
});
|
||||||
{
|
{
|
||||||
let mut json = json;
|
let mut json = json;
|
||||||
if let Some(obj) = json.as_object_mut() {
|
if let Some(obj) = json.as_object_mut() {
|
||||||
obj.insert("node_id".to_string(), serde_json::json!(node_id));
|
obj.insert("node_id".to_string(), serde_json::json!(node_id));
|
||||||
}
|
}
|
||||||
|
if let Some(sender) = api_sender {
|
||||||
|
let _ = sender.send(json.clone());
|
||||||
|
}
|
||||||
let _ = state.stats_tx.send(json.to_string());
|
let _ = state.stats_tx.send(json.to_string());
|
||||||
}
|
}
|
||||||
} else if msg_type == "user_text" {
|
} else if msg_type == "user_text" {
|
||||||
// Käyttäjän lähettämä teksti — broadcastataan pair_taskina ja llm_promptina
|
// Käyttäjän lähettämä teksti — kohdennettu reititys lähettäjäsolmulle
|
||||||
let text = json.get("text").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
let text = json.get("text").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||||
let task_type = json.get("task_type").and_then(|v| v.as_str()).unwrap_or("tokenize");
|
let task_type = json.get("task_type").and_then(|v| v.as_str()).unwrap_or("tokenize");
|
||||||
if !text.is_empty() {
|
if !text.is_empty() {
|
||||||
let preview: String = text.chars().take(80).collect();
|
let preview: String = text.chars().take(80).collect();
|
||||||
tracing::info!("Solmu {} lähetti oman tekstin ({}): \"{}\"", node_id, task_type, preview);
|
tracing::info!("Solmu {} lähetti oman tekstin ({}): \"{}\"", node_id, task_type, preview);
|
||||||
match task_type {
|
let msg = match task_type {
|
||||||
"tokenize" => {
|
"tokenize" => serde_json::json!({
|
||||||
let msg = serde_json::json!({
|
"type": "single_tokenize",
|
||||||
"type": "single_tokenize",
|
"text": text,
|
||||||
"text": text,
|
}),
|
||||||
});
|
_ => serde_json::json!({
|
||||||
let _ = state.stats_tx.send(msg.to_string());
|
"type": "llm_prompt",
|
||||||
}
|
"prompt": text,
|
||||||
_ => {
|
"model": task_type,
|
||||||
// LLM-prompti: lähetetään VAIN valitulle mallille, ei kaikille (välttää turhaa ruuhkaa ja busy-tiloja)
|
}),
|
||||||
let prompt = serde_json::json!({
|
};
|
||||||
"type": "llm_prompt",
|
// Lähetetään takaisin lähettäjäsolmulle (käyttäjä haluaa oman tekstinsä tuloksen)
|
||||||
"prompt": text,
|
if let Some(tx) = state.node_channels.read().await.get(&node_id) {
|
||||||
"model": task_type,
|
let _ = tx.send(msg.to_string());
|
||||||
});
|
|
||||||
let _ = state.stats_tx.send(prompt.to_string());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -941,6 +1128,8 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
|
|||||||
vram.remove(&node_id);
|
vram.remove(&node_id);
|
||||||
}
|
}
|
||||||
state.node_types.lock().unwrap().remove(&node_id);
|
state.node_types.lock().unwrap().remove(&node_id);
|
||||||
|
state.node_paused.lock().unwrap().remove(&node_id);
|
||||||
|
state.node_models.write().await.remove(&node_id);
|
||||||
tracing::info!("Solmu {} ({}) poistui verkosta.", node_id, ip);
|
tracing::info!("Solmu {} ({}) poistui verkosta.", node_id, ip);
|
||||||
broadcast_stats(&state).await;
|
broadcast_stats(&state).await;
|
||||||
sender_task.abort();
|
sender_task.abort();
|
||||||
@@ -952,6 +1141,16 @@ struct ChatCompletionRequest {
|
|||||||
task_id: String,
|
task_id: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
max_tokens: Option<u64>,
|
max_tokens: Option<u64>,
|
||||||
|
#[serde(default)]
|
||||||
|
system_prompt: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
temperature: Option<f64>,
|
||||||
|
#[serde(default)]
|
||||||
|
top_k: Option<u64>,
|
||||||
|
#[serde(default)]
|
||||||
|
repeat_penalty: Option<f64>,
|
||||||
|
#[serde(default)]
|
||||||
|
stop: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
@@ -961,7 +1160,16 @@ struct ChatCompletionResponse {
|
|||||||
tokens_generated: u64,
|
tokens_generated: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn api_ollama_tags() -> axum::response::Response {
|
async fn api_ollama_tags(
|
||||||
|
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
|
||||||
|
) -> axum::response::Response {
|
||||||
|
// Haetaan natiivisolmun tila muistista — priorisoidaan aito verkko-solmu
|
||||||
|
let node_models = state.node_models.read().await;
|
||||||
|
if let Some((_, models_json)) = node_models.iter().next() {
|
||||||
|
return axum::Json(models_json.clone()).into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: Haetaan lokaalista infra-Ollamasta ohjaimesta käsin (esim dev ympäristö)
|
||||||
let ollama_url = std::env::var("OLLAMA_URL").unwrap_or_else(|_| "http://ollama:11434".to_string());
|
let ollama_url = std::env::var("OLLAMA_URL").unwrap_or_else(|_| "http://ollama:11434".to_string());
|
||||||
match reqwest::get(format!("{}/api/tags", ollama_url)).await {
|
match reqwest::get(format!("{}/api/tags", ollama_url)).await {
|
||||||
Ok(resp) => {
|
Ok(resp) => {
|
||||||
@@ -985,11 +1193,10 @@ async fn api_hardware(
|
|||||||
});
|
});
|
||||||
|
|
||||||
let (mut vram_mb, mut gpu_name, ram_mb) = if let Some(s) = native {
|
let (mut vram_mb, mut gpu_name, ram_mb) = if let Some(s) = native {
|
||||||
let gpus = s.get("gpus").and_then(|v| v.as_array());
|
// Tieto on tietokannassa litteänä
|
||||||
let gpu = gpus.and_then(|g| g.first());
|
let vram = s.get("vram_total_mb").and_then(|v| v.as_u64()).unwrap_or(0);
|
||||||
let vram = gpu.and_then(|g| g.get("vram_total_mb")).and_then(|v| v.as_u64()).unwrap_or(0);
|
let name = s.get("gpu_name").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||||
let name = gpu.and_then(|g| g.get("name")).and_then(|v| v.as_str()).unwrap_or("").to_string();
|
let ram = s.get("ram_mb").and_then(|v| v.as_u64()).unwrap_or(0);
|
||||||
let ram = s.get("system").and_then(|v| v.get("ram_total_mb")).and_then(|v| v.as_u64()).unwrap_or(0);
|
|
||||||
(vram, name, ram)
|
(vram, name, ram)
|
||||||
} else {
|
} else {
|
||||||
(0, String::new(), 0)
|
(0, String::new(), 0)
|
||||||
@@ -1054,93 +1261,52 @@ async fn api_chat_completions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Etsitään vapaa solmu — priorisoidaan natiivisolmut (GPU) selaimen edelle
|
// 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 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 node_types = state.node_types.lock().unwrap();
|
||||||
let matching: Vec<u64> = tasks.iter().filter(|(_, task)| {
|
let paused = state.node_paused.lock().unwrap();
|
||||||
if payload.model == "qwen-coder" {
|
let matching: Vec<u64> = tasks.iter().filter(|(k, task)| {
|
||||||
task.starts_with("qwen-coder")
|
if paused.contains(k) { return false; } // Ei sallita tauotettuja
|
||||||
|
// 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 {
|
} else {
|
||||||
**task == payload.model
|
**task == payload.model
|
||||||
}
|
}
|
||||||
}).map(|(k, _)| *k).collect();
|
}).map(|(k, _)| *k).collect();
|
||||||
// Vapaat solmut: natiivi ensin, sitten selain
|
// Etsitään mikä tahansa matchaava solmu (natiivi priorisoidaan)
|
||||||
let free_native = matching.iter().find(|id| {
|
let native = matching.iter().find(|id| {
|
||||||
!busy.contains(id) && node_types.get(id).map(|t| t == "native").unwrap_or(false)
|
node_types.get(id).map(|t| t == "native").unwrap_or(false)
|
||||||
}).copied();
|
}).copied();
|
||||||
let free_any = matching.iter().find(|id| !busy.contains(id)).copied();
|
let any = native.or_else(|| matching.first().copied());
|
||||||
let free = free_native.or(free_any);
|
(any, matching.len())
|
||||||
let any = matching.first().copied();
|
|
||||||
(free, any, matching.len())
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Broadcastataan reititystila UI:lle
|
|
||||||
let task_id = payload.task_id.clone();
|
let task_id = payload.task_id.clone();
|
||||||
|
|
||||||
if target_node_any.is_none() {
|
let target_node_id = match target_node {
|
||||||
// Ei yhtään solmua tälle mallille
|
Some(id) => id,
|
||||||
return (axum::http::StatusCode::SERVICE_UNAVAILABLE, "Ei solmua tälle mallille (käynnistä malli selaimessa)").into_response();
|
None => {
|
||||||
}
|
return (axum::http::StatusCode::SERVICE_UNAVAILABLE, "Ei solmua tälle mallille (käynnistä malli selaimessa)").into_response();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let target_node_id;
|
// Reititystila UI:lle
|
||||||
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" };
|
|
||||||
let routing_msg = serde_json::json!({
|
let routing_msg = serde_json::json!({
|
||||||
"type": "task_routed",
|
"type": "task_routed",
|
||||||
"task_id": task_id,
|
"task_id": task_id,
|
||||||
"node_id": free_id,
|
"node_id": target_node_id,
|
||||||
"node_type": node_type,
|
|
||||||
"status": "routed",
|
"status": "routed",
|
||||||
"message": format!("Reititetty solmulle #{}", free_id),
|
"message": format!("Reititetty solmulle #{}", target_node_id),
|
||||||
});
|
});
|
||||||
let _ = state.stats_tx.send(routing_msg.to_string());
|
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
|
// Merkitään solmu varatuksi ja task_id jaetuksi
|
||||||
state.node_busy.lock().unwrap().insert(target_node_id);
|
state.node_busy.lock().unwrap().insert(target_node_id);
|
||||||
@@ -1152,12 +1318,17 @@ async fn api_chat_completions(
|
|||||||
"model": payload.model,
|
"model": payload.model,
|
||||||
"task_id": payload.task_id,
|
"task_id": payload.task_id,
|
||||||
});
|
});
|
||||||
if let Some(mt) = payload.max_tokens {
|
let obj = msg.as_object_mut().unwrap();
|
||||||
msg.as_object_mut().unwrap().insert("max_tokens".to_string(), serde_json::json!(mt));
|
if let Some(mt) = payload.max_tokens { obj.insert("max_tokens".to_string(), serde_json::json!(mt)); }
|
||||||
}
|
if let Some(ref sp) = payload.system_prompt { obj.insert("system_prompt".to_string(), serde_json::json!(sp)); }
|
||||||
|
if let Some(t) = payload.temperature { obj.insert("temperature".to_string(), serde_json::json!(t)); }
|
||||||
|
if let Some(k) = payload.top_k { obj.insert("top_k".to_string(), serde_json::json!(k)); }
|
||||||
|
if let Some(rp) = payload.repeat_penalty { obj.insert("repeat_penalty".to_string(), serde_json::json!(rp)); }
|
||||||
|
if let Some(ref s) = payload.stop { obj.insert("stop".to_string(), serde_json::json!(s)); }
|
||||||
|
|
||||||
// Odotuskanava valmiiksi (solmu palauttaa tuloksen stats_tx kautta)
|
// Oneshot-kanava: solmu palauttaa tuloksen suoraan tälle pyynnölle
|
||||||
let mut rx = state.stats_tx.subscribe();
|
let (resp_tx, resp_rx) = tokio::sync::oneshot::channel::<serde_json::Value>();
|
||||||
|
state.pending_responses.lock().unwrap().insert(payload.task_id.clone(), resp_tx);
|
||||||
|
|
||||||
// Kohdennettu reititys: lähetetään AI-tehtävä suoraan VAIN valitulle solmulle
|
// Kohdennettu reititys: lähetetään AI-tehtävä suoraan VAIN valitulle solmulle
|
||||||
{
|
{
|
||||||
@@ -1166,48 +1337,34 @@ async fn api_chat_completions(
|
|||||||
let _ = tx.send(msg.to_string());
|
let _ = tx.send(msg.to_string());
|
||||||
tracing::info!("Reititettiin API-pyyntö solmulle {} (Malli: {})", target_node_id, payload.model);
|
tracing::info!("Reititettiin API-pyyntö solmulle {} (Malli: {})", target_node_id, payload.model);
|
||||||
} else {
|
} else {
|
||||||
|
state.pending_responses.lock().unwrap().remove(&payload.task_id);
|
||||||
return (axum::http::StatusCode::SERVICE_UNAVAILABLE, "Verkkovirhe: solmun yhteys katkesi reitityksen aikana").into_response();
|
return (axum::http::StatusCode::SERVICE_UNAVAILABLE, "Verkkovirhe: solmun yhteys katkesi reitityksen aikana").into_response();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let timeout = tokio::time::timeout(std::time::Duration::from_secs(600), async move {
|
let timeout = tokio::time::timeout(std::time::Duration::from_secs(600), resp_rx).await;
|
||||||
loop {
|
|
||||||
let msg_str = match rx.recv().await {
|
|
||||||
Ok(msg) => msg,
|
|
||||||
Err(broadcast::error::RecvError::Lagged(n)) => {
|
|
||||||
tracing::debug!("API-kanava lagged {} viestiä", n);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Err(_) => return Ok(None), // Kanava suljettu
|
|
||||||
};
|
|
||||||
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&msg_str) {
|
|
||||||
if v["type"].as_str() == Some("llm_done") {
|
|
||||||
if let Some(tid) = v["task_id"].as_str() {
|
|
||||||
if tid == payload.task_id {
|
|
||||||
return Ok(Some(ChatCompletionResponse {
|
|
||||||
response: v["response"].as_str().unwrap_or("").to_string(),
|
|
||||||
model: v["model"].as_str().unwrap_or("").to_string(),
|
|
||||||
tokens_generated: v["tokens_generated"].as_u64().unwrap_or(0),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if v["type"].as_str() == Some("llm_error") {
|
|
||||||
if let Some(tid) = v["task_id"].as_str() {
|
|
||||||
if tid == payload.task_id {
|
|
||||||
return Err(v["error"].as_str().unwrap_or("Määrittelemätön virhe solmussa").to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[allow(unreachable_code)]
|
|
||||||
Ok(None)
|
|
||||||
}).await;
|
|
||||||
|
|
||||||
match timeout {
|
match timeout {
|
||||||
Ok(Ok(Some(res))) => axum::Json(res).into_response(),
|
Ok(Ok(v)) => {
|
||||||
Ok(Ok(None)) => (axum::http::StatusCode::INTERNAL_SERVER_ERROR, "Verkkovirhe: yhteys katkesi").into_response(),
|
if v["type"].as_str() == Some("llm_error") {
|
||||||
Ok(Err(err)) => (axum::http::StatusCode::CONFLICT, err).into_response(),
|
let err = v["error"].as_str().unwrap_or("Määrittelemätön virhe solmussa").to_string();
|
||||||
Err(_) => (axum::http::StatusCode::GATEWAY_TIMEOUT, "Aikakatkaisu: solmu ei saanut tehtävää ajoissa valmiiksi").into_response(),
|
(axum::http::StatusCode::CONFLICT, err).into_response()
|
||||||
|
} else {
|
||||||
|
axum::Json(ChatCompletionResponse {
|
||||||
|
response: v["response"].as_str().unwrap_or("").to_string(),
|
||||||
|
model: v["model"].as_str().unwrap_or("").to_string(),
|
||||||
|
tokens_generated: v["tokens_generated"].as_u64().unwrap_or(0),
|
||||||
|
}).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Err(_)) => {
|
||||||
|
// Oneshot-kanava sulkeutui (solmu katosi)
|
||||||
|
state.pending_responses.lock().unwrap().remove(&payload.task_id);
|
||||||
|
(axum::http::StatusCode::INTERNAL_SERVER_ERROR, "Verkkovirhe: yhteys katkesi").into_response()
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
state.pending_responses.lock().unwrap().remove(&payload.task_id);
|
||||||
|
(axum::http::StatusCode::GATEWAY_TIMEOUT, "Aikakatkaisu: solmu ei saanut tehtävää ajoissa valmiiksi").into_response()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
59
network-poc/install.sh
Executable 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
@@ -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
|
||||||
@@ -3,6 +3,10 @@ name = "native-node"
|
|||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["gpu-detect"]
|
||||||
|
gpu-detect = ["nvml-wrapper", "wgpu"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tokio = { version = "1.36", features = ["full"] }
|
tokio = { version = "1.36", features = ["full"] }
|
||||||
tokio-tungstenite = { version = "0.21", features = ["native-tls"] }
|
tokio-tungstenite = { version = "0.21", features = ["native-tls"] }
|
||||||
@@ -10,8 +14,13 @@ futures-util = "0.3"
|
|||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
sysinfo = "0.30"
|
sysinfo = "0.30"
|
||||||
nvml-wrapper = "0.10"
|
nvml-wrapper = { version = "0.10", optional = true }
|
||||||
wgpu = "24"
|
wgpu = { version = "24", optional = true }
|
||||||
reqwest = { version = "0.12", features = ["json"] }
|
reqwest = { version = "0.12", features = ["json"] }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
dialoguer = "0.12.0"
|
||||||
|
console = "0.16.3"
|
||||||
|
ratatui = "0.29.0"
|
||||||
|
crossterm = { version = "0.28.1", features = ["event-stream"] }
|
||||||
|
tracing-appender = "0.2.4"
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
|
|
||||||
|
pub struct GenerateOptions {
|
||||||
|
pub max_tokens: usize,
|
||||||
|
pub system_prompt: Option<String>,
|
||||||
|
pub temperature: Option<f64>,
|
||||||
|
pub top_k: Option<u64>,
|
||||||
|
pub repeat_penalty: Option<f64>,
|
||||||
|
pub stop: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct LlmEngine {
|
pub struct LlmEngine {
|
||||||
ollama_url: String,
|
ollama_url: String,
|
||||||
model: RefCell<String>,
|
model: RefCell<String>,
|
||||||
@@ -9,8 +18,6 @@ pub struct LlmEngine {
|
|||||||
|
|
||||||
impl LlmEngine {
|
impl LlmEngine {
|
||||||
pub async fn load() -> Result<Self, String> {
|
pub async fn load() -> Result<Self, String> {
|
||||||
let model = std::env::var("OLLAMA_MODEL").unwrap_or_else(|_| "qwen2.5-coder:7b".to_string());
|
|
||||||
|
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
.timeout(std::time::Duration::from_secs(600))
|
.timeout(std::time::Duration::from_secs(600))
|
||||||
.connect_timeout(std::time::Duration::from_secs(3))
|
.connect_timeout(std::time::Duration::from_secs(3))
|
||||||
@@ -48,6 +55,12 @@ impl LlmEngine {
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Kysytään malli TUI:lla jos ei pakotettu ympäristöstä
|
||||||
|
let model = match std::env::var("OLLAMA_MODEL") {
|
||||||
|
Ok(m) if !m.is_empty() => m,
|
||||||
|
_ => crate::tui::select_model(&ollama_url, &client).await?
|
||||||
|
};
|
||||||
|
|
||||||
tracing::info!("Ollama backend: {} | malli: {}", ollama_url, model);
|
tracing::info!("Ollama backend: {} | malli: {}", ollama_url, model);
|
||||||
Ok(LlmEngine { ollama_url, model: RefCell::new(model), client })
|
Ok(LlmEngine { ollama_url, model: RefCell::new(model), client })
|
||||||
}
|
}
|
||||||
@@ -78,25 +91,48 @@ impl LlmEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn generate(&self, prompt: &str, max_tokens: usize) -> Result<GenerateResult, String> {
|
/// Hakee kaikki Ollamaan asennetut mallit
|
||||||
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.";
|
pub async fn fetch_models(&self) -> Result<serde_json::Value, String> {
|
||||||
|
let resp = self.client.get(format!("{}/api/tags", self.ollama_url))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Ollama tags fetch: {}", e))?;
|
||||||
|
|
||||||
|
if resp.status().is_success() {
|
||||||
|
resp.json().await.map_err(|e| format!("Ollama tags json: {}", e))
|
||||||
|
} else {
|
||||||
|
Err(format!("Ollama tags epäonnistui: {}", resp.status()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn generate(&self, prompt: &str, opts: &GenerateOptions) -> Result<GenerateResult, String> {
|
||||||
let model = self.model.borrow().clone();
|
let model = self.model.borrow().clone();
|
||||||
|
|
||||||
|
let default_stop: Vec<String> = vec![
|
||||||
|
"<|im_end|>".into(), "\n###".into(), "\nExplanation".into(),
|
||||||
|
"\nNote:".into(), "\nPlease note".into(), "\nThis is".into(),
|
||||||
|
"\n```\n\n".into(), "\n// Example".into(), "\n# Example".into(),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut body = serde_json::json!({
|
||||||
|
"model": model,
|
||||||
|
"prompt": prompt,
|
||||||
|
"stream": false,
|
||||||
|
"options": {
|
||||||
|
"num_predict": opts.max_tokens,
|
||||||
|
"temperature": opts.temperature.unwrap_or(0.7),
|
||||||
|
"top_k": opts.top_k.unwrap_or(40),
|
||||||
|
"repeat_penalty": opts.repeat_penalty.unwrap_or(1.15),
|
||||||
|
"stop": opts.stop.as_ref().unwrap_or(&default_stop),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if let Some(ref sp) = opts.system_prompt {
|
||||||
|
body.as_object_mut().unwrap().insert("system".to_string(), serde_json::json!(sp));
|
||||||
|
}
|
||||||
|
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let resp = self.client.post(format!("{}/api/generate", self.ollama_url))
|
let resp = self.client.post(format!("{}/api/generate", self.ollama_url))
|
||||||
.json(&serde_json::json!({
|
.json(&body)
|
||||||
"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:"]
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Ollama generate: {}", e))?;
|
.map_err(|e| format!("Ollama generate: {}", e))?;
|
||||||
@@ -109,7 +145,7 @@ impl LlmEngine {
|
|||||||
.map_err(|e| format!("Ollama JSON: {}", e))?;
|
.map_err(|e| format!("Ollama JSON: {}", e))?;
|
||||||
|
|
||||||
let text = body["response"].as_str().unwrap_or("").to_string();
|
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_count = body["eval_count"].as_u64().unwrap_or(0) as usize;
|
||||||
let eval_duration_ns = body["eval_duration"].as_u64().unwrap_or(1);
|
let eval_duration_ns = body["eval_duration"].as_u64().unwrap_or(1);
|
||||||
|
|
||||||
@@ -127,27 +163,40 @@ impl LlmEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Siivoa mahdolliset markdown-koodiblokki-merkit
|
/// Siivoa markdown-koodiblokki-merkit ja selitystekstit
|
||||||
fn strip_code_fences(text: &str) -> String {
|
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
|
// Poista selitysteksti lopusta (kaikki rivin "\nPlease note" jälkeen jne.)
|
||||||
if result.starts_with("```") {
|
let lower = result.to_lowercase();
|
||||||
if let Some(nl) = result.find('\n') {
|
for stop in &["\nplease note", "\nthis is a basic", "\nthis code", "\nnote that", "\nremember to", "\nyou can", "\nto run"] {
|
||||||
result = result[nl + 1..].to_string();
|
if let Some(pos) = lower.find(stop) {
|
||||||
|
result = result[..pos].trim_end().to_string();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Poista sulkeva ```
|
// Poista johdantolauseet alusta
|
||||||
let trimmed = result.trim_end();
|
let lower = result.to_lowercase();
|
||||||
if trimmed.ends_with("```") {
|
for prefix in &["sure!", "here is", "here's", "certainly!", "below is"] {
|
||||||
let before = &trimmed[..trimmed.len() - 3];
|
if lower.starts_with(prefix) {
|
||||||
if before.is_empty() || before.ends_with('\n') {
|
if let Some(nl) = result.find('\n') {
|
||||||
result = before.trim_end().to_string();
|
result = result[nl + 1..].to_string();
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result
|
result.trim().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct GenerateResult {
|
pub struct GenerateResult {
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ use tokio_tungstenite::connect_async;
|
|||||||
use tokio_tungstenite::tungstenite::Message;
|
use tokio_tungstenite::tungstenite::Message;
|
||||||
|
|
||||||
mod inference;
|
mod inference;
|
||||||
|
mod tui;
|
||||||
|
mod tui_dashboard;
|
||||||
|
|
||||||
/// GPU-tietorakenne — yhtenäinen kaikille valmistajille
|
/// GPU-tietorakenne — yhtenäinen kaikille valmistajille
|
||||||
struct GpuInfo {
|
struct GpuInfo {
|
||||||
@@ -33,6 +35,7 @@ impl GpuInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "gpu-detect")]
|
||||||
/// Tunnistaa kaikki GPU:t wgpu:lla (NVIDIA/AMD/Apple/Intel)
|
/// Tunnistaa kaikki GPU:t wgpu:lla (NVIDIA/AMD/Apple/Intel)
|
||||||
fn collect_gpus_wgpu() -> Vec<GpuInfo> {
|
fn collect_gpus_wgpu() -> Vec<GpuInfo> {
|
||||||
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
|
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
|
||||||
@@ -84,6 +87,7 @@ fn collect_gpus_wgpu() -> Vec<GpuInfo> {
|
|||||||
gpus
|
gpus
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "gpu-detect")]
|
||||||
/// Täydentää NVIDIA-GPU:iden tiedot NVML:llä (VRAM, lämpötila, kuormitus)
|
/// Täydentää NVIDIA-GPU:iden tiedot NVML:llä (VRAM, lämpötila, kuormitus)
|
||||||
fn enrich_nvidia_gpus(gpus: &mut [GpuInfo]) {
|
fn enrich_nvidia_gpus(gpus: &mut [GpuInfo]) {
|
||||||
let Ok(nvml) = nvml_wrapper::Nvml::init() else { return };
|
let Ok(nvml) = nvml_wrapper::Nvml::init() else { return };
|
||||||
@@ -109,6 +113,7 @@ fn enrich_nvidia_gpus(gpus: &mut [GpuInfo]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "gpu-detect")]
|
||||||
/// AMD GPU-tiedot Linuxin sysfs:stä (/sys/class/drm/)
|
/// AMD GPU-tiedot Linuxin sysfs:stä (/sys/class/drm/)
|
||||||
fn enrich_amd_gpus(gpus: &mut [GpuInfo]) {
|
fn enrich_amd_gpus(gpus: &mut [GpuInfo]) {
|
||||||
let Ok(entries) = std::fs::read_dir("/sys/class/drm") else { return };
|
let Ok(entries) = std::fs::read_dir("/sys/class/drm") else { return };
|
||||||
@@ -150,10 +155,12 @@ fn enrich_amd_gpus(gpus: &mut [GpuInfo]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "gpu-detect")]
|
||||||
fn read_sysfs_u64(path: &std::path::Path) -> Option<u64> {
|
fn read_sysfs_u64(path: &std::path::Path) -> Option<u64> {
|
||||||
std::fs::read_to_string(path).ok()?.trim().parse().ok()
|
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> {
|
fn find_hwmon_temp(device_path: &std::path::Path) -> Option<u64> {
|
||||||
let hwmon_dir = device_path.join("hwmon");
|
let hwmon_dir = device_path.join("hwmon");
|
||||||
let entries = std::fs::read_dir(&hwmon_dir).ok()?;
|
let entries = std::fs::read_dir(&hwmon_dir).ok()?;
|
||||||
@@ -166,8 +173,8 @@ fn find_hwmon_temp(device_path: &std::path::Path) -> Option<u64> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "gpu-detect")]
|
||||||
/// Apple GPU-tiedot — wgpu/Metal antaa nimen, tarkempaa dataa ei saa ilman IOKit:ia
|
/// 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]) {
|
fn enrich_apple_gpus(gpus: &mut [GpuInfo]) {
|
||||||
// Apple Silicon -koneiden unified memory: koko RAM on GPU:n käytettävissä
|
// Apple Silicon -koneiden unified memory: koko RAM on GPU:n käytettävissä
|
||||||
// Arvioidaan system RAM:sta
|
// Arvioidaan system RAM:sta
|
||||||
@@ -187,13 +194,18 @@ fn enrich_apple_gpus(gpus: &mut [GpuInfo]) {
|
|||||||
|
|
||||||
/// Kerää kaikki GPU:t ja täydentää valmistajakohtaiset tiedot
|
/// Kerää kaikki GPU:t ja täydentää valmistajakohtaiset tiedot
|
||||||
fn collect_all_gpus() -> Vec<GpuInfo> {
|
fn collect_all_gpus() -> Vec<GpuInfo> {
|
||||||
let mut gpus = collect_gpus_wgpu();
|
#[cfg(feature = "gpu-detect")]
|
||||||
|
{
|
||||||
enrich_nvidia_gpus(&mut gpus);
|
let mut gpus = collect_gpus_wgpu();
|
||||||
enrich_amd_gpus(&mut gpus);
|
enrich_nvidia_gpus(&mut gpus);
|
||||||
enrich_apple_gpus(&mut gpus);
|
enrich_amd_gpus(&mut gpus);
|
||||||
|
enrich_apple_gpus(&mut gpus);
|
||||||
gpus
|
return gpus;
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "gpu-detect"))]
|
||||||
|
{
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Kerää järjestelmätiedot (CPU, RAM, OS)
|
/// Kerää järjestelmätiedot (CPU, RAM, OS)
|
||||||
@@ -212,7 +224,7 @@ fn collect_system_info() -> serde_json::Value {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Koko auth-viesti hubille
|
/// Koko auth-viesti hubille
|
||||||
fn build_auth_message(allocated_gb: u32) -> String {
|
fn build_auth_message(allocated_gb: u32, model_name: &str, models_data: Option<serde_json::Value>) -> String {
|
||||||
let sys = collect_system_info();
|
let sys = collect_system_info();
|
||||||
let gpus = collect_all_gpus();
|
let gpus = collect_all_gpus();
|
||||||
|
|
||||||
@@ -222,19 +234,29 @@ fn build_auth_message(allocated_gb: u32) -> String {
|
|||||||
v
|
v
|
||||||
}).collect();
|
}).collect();
|
||||||
|
|
||||||
|
let api_key = std::env::var("NODE_API_KEY").unwrap_or_default();
|
||||||
|
|
||||||
let mut msg = json!({
|
let mut msg = json!({
|
||||||
"type": "auth",
|
"type": "auth",
|
||||||
"status": "agent_ready",
|
"status": "agent_ready",
|
||||||
"node_type": "native",
|
"node_type": "native",
|
||||||
"allocated_gb": allocated_gb,
|
"allocated_gb": allocated_gb,
|
||||||
"selected_task": "qwen-coder-05b",
|
"selected_task": model_name,
|
||||||
"system": sys,
|
"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() {
|
if !gpu_json.is_empty() {
|
||||||
msg.as_object_mut().unwrap().insert("gpus".to_string(), json!(gpu_json));
|
msg.as_object_mut().unwrap().insert("gpus".to_string(), json!(gpu_json));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(models) = models_data {
|
||||||
|
msg.as_object_mut().unwrap().insert("models".to_string(), models);
|
||||||
|
}
|
||||||
|
|
||||||
msg.to_string()
|
msg.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,10 +269,24 @@ fn format_optional<T: std::fmt::Display>(val: Option<T>, suffix: &str) -> String
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
|
let file_appender = tracing_appender::rolling::never(".", "native-node.log");
|
||||||
|
let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
|
||||||
|
|
||||||
tracing_subscriber::fmt()
|
tracing_subscriber::fmt()
|
||||||
.with_env_filter("native_node=debug")
|
.with_env_filter("native_node=debug")
|
||||||
|
.with_writer(non_blocking)
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
|
// Hookataan paniikkitilanteet palauttamaan terminaalin raw-moodista
|
||||||
|
let original_hook = std::panic::take_hook();
|
||||||
|
std::panic::set_hook(Box::new(move |panic_info| {
|
||||||
|
tui_dashboard::restore_terminal();
|
||||||
|
original_hook(panic_info);
|
||||||
|
}));
|
||||||
|
|
||||||
|
let tui_state = std::sync::Arc::new(tokio::sync::RwLock::new(tui_dashboard::DashboardState::new()));
|
||||||
|
let (cmd_tx, mut cmd_rx) = tokio::sync::mpsc::unbounded_channel::<String>();
|
||||||
|
|
||||||
let hub_url = std::env::var("HUB_URL").unwrap_or_else(|_| "ws://hub:3000/ws".to_string());
|
let hub_url = std::env::var("HUB_URL").unwrap_or_else(|_| "ws://hub:3000/ws".to_string());
|
||||||
let allocated_gb: u32 = std::env::var("ALLOCATED_GB")
|
let allocated_gb: u32 = std::env::var("ALLOCATED_GB")
|
||||||
.ok()
|
.ok()
|
||||||
@@ -266,9 +302,24 @@ async fn main() {
|
|||||||
sys["cpu_cores"],
|
sys["cpu_cores"],
|
||||||
sys["ram_total_mb"]
|
sys["ram_total_mb"]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut st = tui_state.write().await;
|
||||||
|
st.sys_info = format!("{} | {} | {} ydintä | {} MB RAM",
|
||||||
|
sys["hostname"].as_str().unwrap_or("?"),
|
||||||
|
sys["os"].as_str().unwrap_or("?"),
|
||||||
|
sys["cpu_cores"],
|
||||||
|
sys["ram_total_mb"]
|
||||||
|
);
|
||||||
|
let i = st.sys_info.clone();
|
||||||
|
st.push_log("System", format!("Järjestelmä: {}", i), None);
|
||||||
|
}
|
||||||
|
|
||||||
let gpus = collect_all_gpus();
|
let gpus = collect_all_gpus();
|
||||||
if gpus.is_empty() {
|
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");
|
tracing::info!("GPU:ta ei havaittu — toimitaan CPU-moodissa");
|
||||||
} else {
|
} else {
|
||||||
for (i, gpu) in gpus.iter().enumerate() {
|
for (i, gpu) in gpus.iter().enumerate() {
|
||||||
@@ -302,6 +353,36 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let active_model = llm.as_ref().map(|e| e.model_name()).unwrap_or_else(|| "unknown".to_string());
|
||||||
|
tracing::info!("Käytettävä kielimalli konfiguroitu (selected_task): {}", active_model);
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut st = tui_state.write().await;
|
||||||
|
st.model_name = active_model.clone();
|
||||||
|
st.push_log("System", format!("Malli valmis: {}", active_model), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Käynnistetään graafinen TUI vasta kun TUI:n Prompt (LlmEngine::load) on ohitettu!
|
||||||
|
let ui_state = tui_state.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(e) = tui_dashboard::run_dashboard(ui_state, cmd_tx).await {
|
||||||
|
tracing::error!("Pääluupin TUI kaatui: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Haetaan paikalliset mallit hubille lähetettäväksi
|
||||||
|
let mut available_models = None;
|
||||||
|
if let Some(ref engine) = llm {
|
||||||
|
match engine.fetch_models().await {
|
||||||
|
Ok(models) => {
|
||||||
|
available_models = Some(models);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("Mallilistauksen haku epäonnistui: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Yhdistetään hubiin
|
// Yhdistetään hubiin
|
||||||
loop {
|
loop {
|
||||||
match connect_async(&hub_url).await {
|
match connect_async(&hub_url).await {
|
||||||
@@ -309,80 +390,184 @@ async fn main() {
|
|||||||
tracing::info!("Yhdistetty hubiin!");
|
tracing::info!("Yhdistetty hubiin!");
|
||||||
let (mut write, mut read) = ws_stream.split();
|
let (mut write, mut read) = ws_stream.split();
|
||||||
|
|
||||||
let auth = build_auth_message(allocated_gb);
|
let auth = build_auth_message(allocated_gb, &active_model, available_models.clone());
|
||||||
if write.send(Message::Text(auth)).await.is_err() {
|
if write.send(Message::Text(auth)).await.is_err() {
|
||||||
tracing::error!("Auth-viestin lähetys epäonnistui");
|
tracing::error!("Auth-viestin lähetys epäonnistui");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut busy = false;
|
loop {
|
||||||
|
tokio::select! {
|
||||||
while let Some(Ok(msg)) = read.next().await {
|
cmd = cmd_rx.recv() => {
|
||||||
if let Message::Text(text) = msg {
|
if let Some(cmd_str) = cmd {
|
||||||
// LLM-promptit
|
if cmd_str == "pause" {
|
||||||
if text.contains("llm_prompt") && !busy {
|
tracing::info!("Tauotetaan solmun suoritus (Hub ei lähetä tehtäviä)...");
|
||||||
if let Ok(task) = serde_json::from_str::<serde_json::Value>(&text) {
|
let req = json!({"type": "status_update", "status": "paused"});
|
||||||
let prompt = task.get("prompt").and_then(|v| v.as_str()).unwrap_or("");
|
let _ = write.send(Message::Text(req.to_string())).await;
|
||||||
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("");
|
let mut st = tui_state.write().await;
|
||||||
|
st.status = "PAUSED".to_string();
|
||||||
if !prompt.is_empty() && msg_model.starts_with("qwen-coder") {
|
st.push_log("Network", "Solmu siirretty taukotilaan".to_string(), None);
|
||||||
|
}
|
||||||
if let Some(ref engine) = llm {
|
} else if cmd_str == "resume" {
|
||||||
busy = true;
|
tracing::info!("Jatketaan solmun suoritusta...");
|
||||||
let max_tokens = task.get("max_tokens").and_then(|v| v.as_u64()).unwrap_or(512) as usize;
|
let req = json!({"type": "status_update", "status": "active"});
|
||||||
tracing::info!("Generoidaan (task_id: {}, max_tokens: {}): \"{}\"", task_id, max_tokens, &prompt[..prompt.len().min(100)]);
|
let _ = write.send(Message::Text(req.to_string())).await;
|
||||||
|
{
|
||||||
let model_name = engine.model_name();
|
let mut st = tui_state.write().await;
|
||||||
match engine.generate(prompt, max_tokens).await {
|
st.status = "ACTIVE".to_string();
|
||||||
Ok(result) => {
|
st.push_log("System", "Suoritus jatkuu...".to_string(), None);
|
||||||
tracing::info!(
|
|
||||||
"Tulos: {} tokenia | {:.0}ms | {:.1} tok/s | \"{}\"",
|
|
||||||
result.tokens_generated,
|
|
||||||
result.duration_ms,
|
|
||||||
result.tokens_per_sec,
|
|
||||||
&result.text[..result.text.len().min(80)]
|
|
||||||
);
|
|
||||||
|
|
||||||
let done = json!({
|
|
||||||
"type": "llm_done",
|
|
||||||
"prompt": prompt,
|
|
||||||
"model": format!("{} (Ollama)", model_name),
|
|
||||||
"response": result.text,
|
|
||||||
"tokens_generated": result.tokens_generated,
|
|
||||||
"duration_ms": result.duration_ms,
|
|
||||||
"tokens_per_sec": (result.tokens_per_sec * 10.0).round() / 10.0,
|
|
||||||
"load_time_ms": 0,
|
|
||||||
"task_id": task_id,
|
|
||||||
});
|
|
||||||
let _ = write.send(Message::Text(done.to_string())).await;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("Inferenssivirhe: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
busy = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Mallin vaihto lennossa
|
ws_msg = read.next() => {
|
||||||
if text.contains("change_model") {
|
match ws_msg {
|
||||||
if let Ok(task) = serde_json::from_str::<serde_json::Value>(&text) {
|
Some(Ok(Message::Text(text))) => {
|
||||||
if let Some(new_model) = task.get("model").and_then(|v| v.as_str()) {
|
// Hubin control-viestit
|
||||||
if let Some(ref engine) = llm {
|
if text.contains(r#""type":"control""#) {
|
||||||
tracing::info!("Vaihdetaan malli: {}", new_model);
|
if let Ok(task) = serde_json::from_str::<serde_json::Value>(&text) {
|
||||||
engine.set_model(new_model.to_string());
|
if let Some(action) = task.get("action").and_then(|v| v.as_str()) {
|
||||||
match engine.ensure_model().await {
|
if action == "pause" {
|
||||||
Ok(()) => tracing::info!("Malli {} valmis!", new_model),
|
tracing::info!("Hub pakotti solmun tauolle (Pause)");
|
||||||
Err(e) => tracing::error!("Mallin lataus epäonnistui: {}", e),
|
let req = json!({"type": "status_update", "status": "paused"});
|
||||||
|
let _ = write.send(Message::Text(req.to_string())).await;
|
||||||
|
{
|
||||||
|
let mut st = tui_state.write().await;
|
||||||
|
st.status = "PAUSED".to_string();
|
||||||
|
st.push_log("Network", "Hub kytki solmun tauolle".to_string(), None);
|
||||||
|
}
|
||||||
|
} else if action == "resume" {
|
||||||
|
tracing::info!("Hub aktivoi solmun suorituksen (Resume)");
|
||||||
|
let req = json!({"type": "status_update", "status": "active"});
|
||||||
|
let _ = write.send(Message::Text(req.to_string())).await;
|
||||||
|
{
|
||||||
|
let mut st = tui_state.write().await;
|
||||||
|
st.status = "ACTIVE".to_string();
|
||||||
|
st.push_log("Network", "Hub palautti solmun töihin".to_string(), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Verkon globaali tila
|
||||||
|
if text.contains(r#""type":"network_status""#) {
|
||||||
|
if let Ok(status) = serde_json::from_str::<serde_json::Value>(&text) {
|
||||||
|
if let Some(nodes) = status.get("active_nodes").and_then(|v| v.as_u64()) {
|
||||||
|
if let Some(tasks) = status.get("tasks").and_then(|v| v.as_u64()) {
|
||||||
|
let mut st = tui_state.write().await;
|
||||||
|
st.network_active_nodes = nodes as usize;
|
||||||
|
st.network_total_tasks = tasks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LLM-promptit
|
||||||
|
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") || msg_model.starts_with("qwen2.5-coder") || msg_model.starts_with("phi")) {
|
||||||
|
if let Some(ref engine) = llm {
|
||||||
|
let gen_opts = inference::GenerateOptions {
|
||||||
|
max_tokens: task.get("max_tokens").and_then(|v| v.as_u64()).unwrap_or(1024) as usize,
|
||||||
|
system_prompt: task.get("system_prompt").and_then(|v| v.as_str()).map(|s| s.to_string()),
|
||||||
|
temperature: task.get("temperature").and_then(|v| v.as_f64()),
|
||||||
|
top_k: task.get("top_k").and_then(|v| v.as_u64()),
|
||||||
|
repeat_penalty: task.get("repeat_penalty").and_then(|v| v.as_f64()),
|
||||||
|
stop: task.get("stop").and_then(|v| v.as_array()).map(|a| a.iter().filter_map(|s| s.as_str().map(|s| s.to_string())).collect()),
|
||||||
|
};
|
||||||
|
let prompt_lines = prompt.lines().count();
|
||||||
|
let prompt_last: String = prompt.lines().last().unwrap_or("").chars().take(60).collect();
|
||||||
|
tracing::info!("→ task_id:{} | {}r prompti | \"{}...\"", task_id, prompt_lines, prompt_last);
|
||||||
|
{
|
||||||
|
let mut st = tui_state.write().await;
|
||||||
|
st.cur_task_id = Some(task_id.to_string());
|
||||||
|
st.cur_prompt = Some(format!("→ {} riviä | \"{}...\"", prompt_lines, prompt_last));
|
||||||
|
}
|
||||||
|
|
||||||
|
let model_name = engine.model_name();
|
||||||
|
match engine.generate(prompt, &gen_opts).await {
|
||||||
|
Ok(result) => {
|
||||||
|
let tokens_sec = (result.tokens_per_sec * 10.0).round() / 10.0;
|
||||||
|
tracing::info!(
|
||||||
|
"✓ {} | {} tok | {:.0}ms | {:.1} tok/s",
|
||||||
|
model_name,
|
||||||
|
result.tokens_generated,
|
||||||
|
result.duration_ms,
|
||||||
|
tokens_sec,
|
||||||
|
);
|
||||||
|
{
|
||||||
|
let mut st = tui_state.write().await;
|
||||||
|
st.tasks_completed += 1;
|
||||||
|
st.last_tokens_sec = tokens_sec as f64;
|
||||||
|
st.cur_task_id = None;
|
||||||
|
st.cur_prompt = None;
|
||||||
|
|
||||||
|
let msg_type = if task_id == "status-check" { "Ping" } else { "Task" };
|
||||||
|
let msg_text = format!("{} ({} tok)", task_id, result.tokens_generated);
|
||||||
|
st.push_log(msg_type, msg_text, Some(tokens_sec as f64));
|
||||||
|
}
|
||||||
|
let prompt_short: String = prompt.lines().last().unwrap_or("").chars().take(100).collect();
|
||||||
|
let done = json!({
|
||||||
|
"type": "llm_done",
|
||||||
|
"prompt": prompt_short,
|
||||||
|
"model": format!("{} (Ollama)", model_name),
|
||||||
|
"response": result.text,
|
||||||
|
"tokens_generated": result.tokens_generated,
|
||||||
|
"duration_ms": result.duration_ms,
|
||||||
|
"tokens_per_sec": tokens_sec,
|
||||||
|
"load_time_ms": 0,
|
||||||
|
"task_id": task_id,
|
||||||
|
});
|
||||||
|
let _ = write.send(Message::Text(done.to_string())).await;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Inferenssivirhe: {}", e);
|
||||||
|
{
|
||||||
|
let mut st = tui_state.write().await;
|
||||||
|
st.cur_task_id = None;
|
||||||
|
st.cur_prompt = None;
|
||||||
|
st.push_log("System", format!("Virhe inferenssissä: {}", e), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mallin vaihto lennossa
|
||||||
|
if text.contains("change_model") {
|
||||||
|
if let Ok(task) = serde_json::from_str::<serde_json::Value>(&text) {
|
||||||
|
if let Some(new_model) = task.get("model").and_then(|v| v.as_str()) {
|
||||||
|
if let Some(ref engine) = llm {
|
||||||
|
tracing::info!("Vaihdetaan malli: {}", new_model);
|
||||||
|
engine.set_model(new_model.to_string());
|
||||||
|
match engine.ensure_model().await {
|
||||||
|
Ok(()) => {
|
||||||
|
tracing::info!("Malli {} valmis!", new_model);
|
||||||
|
let mut st = tui_state.write().await;
|
||||||
|
st.model_name = new_model.to_string();
|
||||||
|
st.push_log("System", format!("Malli {} ladattu & valmis!", new_model), None);
|
||||||
|
}
|
||||||
|
Err(e) => tracing::error!("Mallin lataus epäonnistui: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some(Ok(_)) => {} // Muut viestityypit (binary/ping)
|
||||||
|
Some(Err(_)) | None => break, // Yhteys poikki
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tracing::warn!("Yhteys hubiin katkesi — yritetään uudelleen 5s...");
|
tracing::warn!("Yhteys hubiin katkesi — yritetään uudelleen 5s...");
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|||||||
67
network-poc/native-node/src/tui.rs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
use dialoguer::{Select, Input, theme::ColorfulTheme};
|
||||||
|
use reqwest::Client;
|
||||||
|
|
||||||
|
pub async fn select_model(ollama_url: &str, client: &Client) -> Result<String, String> {
|
||||||
|
// 1. Hae tagit
|
||||||
|
let mut models = vec![];
|
||||||
|
println!(" Haetaan asennettuja malleja osoitteesta {}...", ollama_url);
|
||||||
|
if let Ok(resp) = client.get(&format!("{}/api/tags", ollama_url)).send().await {
|
||||||
|
if resp.status().is_success() {
|
||||||
|
if let Ok(json) = resp.json::<serde_json::Value>().await {
|
||||||
|
if let Some(arr) = json.get("models").and_then(|v| v.as_array()) {
|
||||||
|
for m in arr {
|
||||||
|
if let Some(name) = m.get("name").and_then(|v| v.as_str()) {
|
||||||
|
models.push(name.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let download_opt = "[➕ Lataa uusi malli internetistä]";
|
||||||
|
let mut options = vec![download_opt.to_string()];
|
||||||
|
options.extend(models);
|
||||||
|
|
||||||
|
// 2. Kysy käyttäjältä Selectillä
|
||||||
|
let theme = ColorfulTheme::default();
|
||||||
|
let selection = Select::with_theme(&theme)
|
||||||
|
.with_prompt("Valitse Ollama-malli Kipinä-verkkoa varten:")
|
||||||
|
.default(if options.len() > 1 { 1 } else { 0 })
|
||||||
|
.items(&options)
|
||||||
|
.interact()
|
||||||
|
.map_err(|e| format!("TUI virhe: {}", e))?;
|
||||||
|
|
||||||
|
let selected = &options[selection];
|
||||||
|
|
||||||
|
// 3. Jos käyttäjä haluaa uuden, kysy nimeä
|
||||||
|
if selected == download_opt {
|
||||||
|
let new_model: String = Input::with_theme(&theme)
|
||||||
|
.with_prompt("Syötä ladattavan mallin nimi (esim. llama3 tai qwen2.5-coder:3b)")
|
||||||
|
.interact_text()
|
||||||
|
.map_err(|e| format!("TUI virhe: {}", e))?;
|
||||||
|
|
||||||
|
let new_model = new_model.trim().to_string();
|
||||||
|
if new_model.is_empty() {
|
||||||
|
return Err("Mallin nimi ei voi olla tyhjä".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(" Ladataan malleja taustalla... Tämä voi kestää hetken ({})", new_model);
|
||||||
|
// Odotetaan että pull on valmis
|
||||||
|
let pull_body = serde_json::json!({ "name": &new_model });
|
||||||
|
let resp = client.post(&format!("{}/api/pull", ollama_url))
|
||||||
|
.json(&pull_body)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Pull req virhe: {}", e))?;
|
||||||
|
|
||||||
|
if resp.status().is_success() {
|
||||||
|
println!(" ✓ Malli {} ladattu onnistuneesti!", new_model);
|
||||||
|
return Ok(new_model);
|
||||||
|
} else {
|
||||||
|
return Err(format!("Ollama pull epäonnistui: {}", resp.status()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(selected.clone())
|
||||||
|
}
|
||||||
223
network-poc/native-node/src/tui_dashboard.rs
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
use crossterm::{
|
||||||
|
event::{self, Event, EventStream, KeyCode},
|
||||||
|
execute,
|
||||||
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
|
};
|
||||||
|
use ratatui::{
|
||||||
|
backend::CrosstermBackend,
|
||||||
|
layout::{Constraint, Direction, Layout, Alignment},
|
||||||
|
style::{Color, Modifier, Style},
|
||||||
|
widgets::{Block, Borders, Paragraph, Wrap},
|
||||||
|
Terminal,
|
||||||
|
};
|
||||||
|
use std::io;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use futures_util::StreamExt;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct LogEntry {
|
||||||
|
pub ty: String,
|
||||||
|
pub msg: String,
|
||||||
|
pub speed: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DashboardState {
|
||||||
|
pub logs: Vec<LogEntry>,
|
||||||
|
pub status: String,
|
||||||
|
pub node_id: Option<u64>,
|
||||||
|
pub sys_info: String,
|
||||||
|
pub model_name: String,
|
||||||
|
pub cur_task_id: Option<String>,
|
||||||
|
pub cur_prompt: Option<String>,
|
||||||
|
pub tasks_completed: u32,
|
||||||
|
pub last_tokens_sec: f64,
|
||||||
|
pub network_active_nodes: usize,
|
||||||
|
pub network_total_tasks: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DashboardState {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
logs: Vec::new(),
|
||||||
|
status: "ACTIVE".to_string(),
|
||||||
|
node_id: None,
|
||||||
|
sys_info: "".to_string(),
|
||||||
|
model_name: "Yhdistetään...".to_string(),
|
||||||
|
cur_task_id: None,
|
||||||
|
cur_prompt: None,
|
||||||
|
tasks_completed: 0,
|
||||||
|
last_tokens_sec: 0.0,
|
||||||
|
network_active_nodes: 1, // oletetaan itsemme
|
||||||
|
network_total_tasks: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_log(&mut self, ty: &str, msg: String, speed: Option<f64>) {
|
||||||
|
self.logs.push(LogEntry {
|
||||||
|
ty: ty.to_string(),
|
||||||
|
msg,
|
||||||
|
speed,
|
||||||
|
});
|
||||||
|
if self.logs.len() > 100 {
|
||||||
|
self.logs.remove(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run_dashboard(
|
||||||
|
state: Arc<RwLock<DashboardState>>,
|
||||||
|
cmd_tx: tokio::sync::mpsc::UnboundedSender<String>,
|
||||||
|
) -> Result<(), io::Error> {
|
||||||
|
enable_raw_mode()?;
|
||||||
|
let mut stdout = io::stdout();
|
||||||
|
execute!(stdout, EnterAlternateScreen)?;
|
||||||
|
let backend = CrosstermBackend::new(stdout);
|
||||||
|
let mut terminal = Terminal::new(backend)?;
|
||||||
|
terminal.clear()?;
|
||||||
|
|
||||||
|
let mut reader = EventStream::new();
|
||||||
|
let mut interval = tokio::time::interval(Duration::from_millis(100));
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
_ = interval.tick() => {
|
||||||
|
let st = state.read().await;
|
||||||
|
terminal.draw(|f| ui(f, &st))?;
|
||||||
|
}
|
||||||
|
ev = reader.next() => {
|
||||||
|
if let Some(Ok(Event::Key(key))) = ev {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Char('q') | KeyCode::Esc => {
|
||||||
|
// Palautetaan näyttö ja suljetaan ohjelma
|
||||||
|
disable_raw_mode()?;
|
||||||
|
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
KeyCode::Char('p') | KeyCode::Char('P') => {
|
||||||
|
let _ = cmd_tx.send("pause".to_string());
|
||||||
|
}
|
||||||
|
KeyCode::Char('r') | KeyCode::Char('R') | KeyCode::Char('s') => {
|
||||||
|
let _ = cmd_tx.send("resume".to_string());
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn restore_terminal() {
|
||||||
|
let _ = disable_raw_mode();
|
||||||
|
let _ = execute!(io::stdout(), LeaveAlternateScreen);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ui(f: &mut ratatui::Frame, st: &DashboardState) {
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Length(3), // Header
|
||||||
|
Constraint::Min(0), // Body
|
||||||
|
Constraint::Length(3), // Footer / Status
|
||||||
|
].as_ref())
|
||||||
|
.split(f.area());
|
||||||
|
|
||||||
|
// --- Header ---
|
||||||
|
let header_text = match st.node_id {
|
||||||
|
Some(id) => format!(" Kipinä Agentic Node #{} ", id),
|
||||||
|
None => " Kipinä Agentic Node (Yhdistää...) ".to_string(),
|
||||||
|
};
|
||||||
|
let header = Paragraph::new(header_text)
|
||||||
|
.style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD))
|
||||||
|
.alignment(Alignment::Center)
|
||||||
|
.block(Block::default().borders(Borders::ALL).style(Style::default().fg(Color::DarkGray)));
|
||||||
|
f.render_widget(header, chunks[0]);
|
||||||
|
|
||||||
|
// --- Body ---
|
||||||
|
let body_chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Length(7), // Yläosan info ja tehtävä
|
||||||
|
Constraint::Min(0), // Lokit / Chat alas
|
||||||
|
].as_ref())
|
||||||
|
.split(chunks[1]);
|
||||||
|
|
||||||
|
let top_panels = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Percentage(40), // Vasen paneeli (Info)
|
||||||
|
Constraint::Percentage(60), // Oikea paneeli (Tehtävä)
|
||||||
|
].as_ref())
|
||||||
|
.split(body_chunks[0]);
|
||||||
|
|
||||||
|
// Vasen paneeli: Laitteisto, Malli & Verkosto
|
||||||
|
let info_text = format!(
|
||||||
|
"🚀 Malli: {}\n💻 Järjestelmä: {}\n📊 Tehdyt: {} | Nopeus: {} t/s\n🌐 Verkosto: {} solmua | {} tehtävää",
|
||||||
|
st.model_name, st.sys_info, st.tasks_completed, st.last_tokens_sec, st.network_active_nodes, st.network_total_tasks
|
||||||
|
);
|
||||||
|
let left_panel = Paragraph::new(info_text)
|
||||||
|
.block(Block::default().title(" Laitteisto ja AI ").borders(Borders::ALL))
|
||||||
|
.style(Style::default().fg(Color::White))
|
||||||
|
.wrap(Wrap { trim: true });
|
||||||
|
f.render_widget(left_panel, top_panels[0]);
|
||||||
|
|
||||||
|
// Oikea paneeli: Käynnissä oleva tehtävä
|
||||||
|
let task_title = match &st.cur_task_id {
|
||||||
|
Some(id) => format!(" Työn alla: {} ", id),
|
||||||
|
None => " Vapaana ".to_string(),
|
||||||
|
};
|
||||||
|
let task_content = st.cur_prompt.clone().unwrap_or_else(|| "Odotetaan tehtäviä Hubilta...".to_string());
|
||||||
|
|
||||||
|
let task_style = if st.cur_task_id.is_some() {
|
||||||
|
Style::default().fg(Color::Magenta)
|
||||||
|
} else {
|
||||||
|
Style::default().fg(Color::DarkGray)
|
||||||
|
};
|
||||||
|
|
||||||
|
let task_panel = Paragraph::new(task_content)
|
||||||
|
.wrap(Wrap { trim: true })
|
||||||
|
.block(Block::default().title(task_title).borders(Borders::ALL).style(task_style));
|
||||||
|
f.render_widget(task_panel, top_panels[1]);
|
||||||
|
|
||||||
|
// Alaosan paneeli: Tapahtumaloki koko leveydeltä
|
||||||
|
let area_height = body_chunks[1].height.saturating_sub(2) as usize;
|
||||||
|
let skip_count = if st.logs.len() > area_height { st.logs.len() - area_height } else { 0 };
|
||||||
|
|
||||||
|
let visible_logs: Vec<ratatui::text::Line> = st.logs.iter().skip(skip_count).map(|log| {
|
||||||
|
let ty_color = match log.ty.as_str() {
|
||||||
|
"System" => Color::Yellow,
|
||||||
|
"Network" => Color::Blue,
|
||||||
|
"Task" => Color::Magenta,
|
||||||
|
"Ping" => Color::DarkGray,
|
||||||
|
_ => Color::White,
|
||||||
|
};
|
||||||
|
|
||||||
|
let speed_str = if let Some(s) = log.speed {
|
||||||
|
format!(" | {:.1} tok/s", s)
|
||||||
|
} else {
|
||||||
|
"".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
ratatui::text::Line::from(vec![
|
||||||
|
ratatui::text::Span::styled(format!("{: <8}", log.ty), Style::default().fg(ty_color).add_modifier(Modifier::BOLD)),
|
||||||
|
ratatui::text::Span::raw(" | "),
|
||||||
|
ratatui::text::Span::styled(log.msg.clone(), Style::default().fg(Color::White)),
|
||||||
|
ratatui::text::Span::styled(speed_str, Style::default().fg(ty_color)),
|
||||||
|
])
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
let logs_panel = Paragraph::new(visible_logs)
|
||||||
|
.block(Block::default().title(" Tapahtumaloki ").borders(Borders::ALL).style(Style::default().fg(Color::Cyan)));
|
||||||
|
f.render_widget(logs_panel, body_chunks[1]);
|
||||||
|
|
||||||
|
// --- Footer / Status ---
|
||||||
|
let status_color = if st.status == "ACTIVE" { Color::Green } else { Color::Yellow };
|
||||||
|
let status_text = format!(" Tila: {} | Komennot: [P] Pause / [R] Työhön / [Q] Sulje ", st.status);
|
||||||
|
let footer = Paragraph::new(status_text)
|
||||||
|
.style(Style::default().fg(status_color).add_modifier(Modifier::BOLD))
|
||||||
|
.alignment(Alignment::Center)
|
||||||
|
.block(Block::default().borders(Borders::ALL));
|
||||||
|
f.render_widget(footer, chunks[2]);
|
||||||
|
}
|
||||||
@@ -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
|
// Qwen2.5-Coder: 4 = 0.5B, 5 = 3B
|
||||||
if let Ok(task) = serde_json::from_str::<serde_json::Value>(&msg) {
|
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();
|
let prompt = task.get("prompt").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||||
@@ -366,16 +368,23 @@ pub async fn start_agent_node(hub_url: String, has_webgpu: bool, device_info_jso
|
|||||||
let _ = ws_clone.borrow().send_with_str(&err_msg.to_string());
|
let _ = ws_clone.borrow().send_with_str(&err_msg.to_string());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Välitetään parametrit JSON-promptina coderille
|
||||||
|
let coder_prompt = serde_json::json!({
|
||||||
|
"prompt": prompt,
|
||||||
|
"system": task.get("system_prompt").and_then(|v| v.as_str()).unwrap_or(""),
|
||||||
|
"max_tokens": task.get("max_tokens").and_then(|v| v.as_u64()).unwrap_or(512),
|
||||||
|
}).to_string();
|
||||||
let use_3b = current_task == 5;
|
let use_3b = current_task == 5;
|
||||||
LLM_BUSY.store(true, Ordering::SeqCst);
|
LLM_BUSY.store(true, Ordering::SeqCst);
|
||||||
let ws_for_async = ws_clone.clone();
|
let ws_for_async = ws_clone.clone();
|
||||||
wasm_bindgen_futures::spawn_local(async move {
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
qwen_coder::run_coder_inference(prompt, ws_for_async, use_3b, task_id).await;
|
qwen_coder::run_coder_inference(coder_prompt, ws_for_async, use_3b, task_id).await;
|
||||||
LLM_BUSY.store(false, Ordering::SeqCst);
|
LLM_BUSY.store(false, Ordering::SeqCst);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} // current_task == 4 || 5
|
||||||
} else if msg.contains("ai_task") {
|
} else if msg.contains("ai_task") {
|
||||||
console_log!("Hub task vastaanotettu, ajetaan GPU:lla...");
|
console_log!("Hub task vastaanotettu, ajetaan GPU:lla...");
|
||||||
let ws_for_async = ws_clone.clone();
|
let ws_for_async = ws_clone.clone();
|
||||||
|
|||||||
@@ -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)
|
/// 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>) {
|
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 size_label = if use_3b { "3B" } else { "0.5B" };
|
||||||
|
|
||||||
let start_load = crate::perf_now();
|
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 {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
console_log!("[Coder] Malli valmis, aloitetaan inferenssi");
|
||||||
|
|
||||||
let load_time = crate::perf_now() - start_load;
|
let load_time = crate::perf_now() - start_load;
|
||||||
if load_time > 100.0 {
|
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) {
|
if let Ok(text) = cached.tokenizer.decode(&[next_token], true) {
|
||||||
generated_text.push_str(&text);
|
generated_text.push_str(&text);
|
||||||
let mut chunk = serde_json::json!({ "type": "llm_chunk", "token": text, "prompt": prompt, "model": "Qwen2.5-Coder" });
|
let mut chunk = serde_json::json!({ "type": "llm_chunk", "token": text, "prompt": prompt, "model": "Qwen2.5-Coder" });
|
||||||
if let Some(ref tid) = task_id { chunk.as_object_mut().unwrap().insert("task_id".to_string(), serde_json::json!(tid)); }
|
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());
|
let _ = ws.borrow().send_with_str(&chunk.to_string());
|
||||||
}
|
}
|
||||||
all_generated.push(next_token);
|
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" });
|
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());
|
let _ = ws.borrow().send_with_str(&chunk.to_string());
|
||||||
}
|
}
|
||||||
all_generated.push(next_token);
|
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,
|
"load_time_ms": (load_time * 100.0).round() / 100.0,
|
||||||
});
|
});
|
||||||
if let Some(tid) = task_id {
|
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());
|
let _ = ws.borrow().send_with_str(&done.to_string());
|
||||||
}
|
}
|
||||||
|
|||||||
33
network-poc/start-local.sh
Executable file
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Kipinä Agentic Network lokaali pikatesti
|
||||||
|
# Tämä ohjelma käynnistää lokaalin Kipinä Hubin taustalle, ja heittää sen jälkeen näkyviin
|
||||||
|
# visuaalisen Natiivisolmun Ratatui-ruudun yhdistäen sen automaattisesti siihen.
|
||||||
|
# Kun poistut Ratatui-näytöstä (esim painamalla Q), niin skripti sammuttaa siististi
|
||||||
|
# myös taustalla pyörivän lokaalin Hubin!
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
echo "========================================="
|
||||||
|
echo " 🔥 Kipinä Agentic Network - Local Run 🔥"
|
||||||
|
echo "========================================="
|
||||||
|
echo "Varmistetaan portin 3000 vapaus..."
|
||||||
|
lsof -ti :3000 | xargs kill -9 2>/dev/null || true
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Käynnistetään Kipinä Hub taustalle..."
|
||||||
|
# Ohjataan Hubin logit erilliseen tiedostoon jottei se sotke näkymää!
|
||||||
|
env STATIC_DIR=frontend/dist cargo run -p hub > hub-local.log 2>&1 &
|
||||||
|
HUB_PID=$!
|
||||||
|
|
||||||
|
# Odotellaan, että Hub saa portit kuunteluun
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
echo "Käynnistetään Natiivisolmu ja Ratatui-dashboard..."
|
||||||
|
# Käynnistetään TUI ja pakotetaan yhdistämään lokaaliin Hubiin.
|
||||||
|
env -u OLLAMA_MODEL HUB_URL=ws://127.0.0.1:3000/ws cargo run -p native-node
|
||||||
|
|
||||||
|
# Kun TUI ohjelmasta on poistuttu
|
||||||
|
echo ""
|
||||||
|
echo "Dashboard suljettu! Ajetaan lokaali Hub (#$HUB_PID) siististi alas..."
|
||||||
|
kill $HUB_PID
|
||||||
|
echo "Kaikki sammutettu. Kiitos!"
|
||||||
1
network-poc/target-check/.rustc_info.json
Normal 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":{}}
|
||||||
3
network-poc/target-check/CACHEDIR.TAG
Normal 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/
|
||||||
24
network-poc/temp/frontend-old/.gitignore
vendored
Normal 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/
|
||||||
4
network-poc/temp/frontend-old/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["astro-build.astro-vscode"],
|
||||||
|
"unwantedRecommendations": []
|
||||||
|
}
|
||||||
11
network-poc/temp/frontend-old/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"command": "./node_modules/.bin/astro dev",
|
||||||
|
"name": "Development server",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "node-terminal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
43
network-poc/temp/frontend-old/README.md
Normal 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).
|
||||||
5
network-poc/temp/frontend-old/astro.config.mjs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// @ts-check
|
||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({});
|
||||||
4731
network-poc/temp/frontend-old/package-lock.json
generated
Normal file
18
network-poc/temp/frontend-old/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
34
network-poc/temp/frontend-old/public/avatars/README.md
Normal 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ä.
|
||||||
|
Before Width: | Height: | Size: 696 KiB After Width: | Height: | Size: 696 KiB |
BIN
network-poc/temp/frontend-old/public/avatars/bear.png
Normal file
|
After Width: | Height: | Size: 757 KiB |
BIN
network-poc/temp/frontend-old/public/avatars/beaver.png
Normal file
|
After Width: | Height: | Size: 700 KiB |
BIN
network-poc/temp/frontend-old/public/avatars/chameleon.png
Normal file
|
After Width: | Height: | Size: 731 KiB |
BIN
network-poc/temp/frontend-old/public/avatars/elephant.png
Normal file
|
After Width: | Height: | Size: 711 KiB |
BIN
network-poc/temp/frontend-old/public/avatars/gecko.png
Normal file
|
After Width: | Height: | Size: 695 KiB |
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 432 KiB After Width: | Height: | Size: 432 KiB |
|
Before Width: | Height: | Size: 650 KiB After Width: | Height: | Size: 650 KiB |
|
Before Width: | Height: | Size: 389 KiB After Width: | Height: | Size: 389 KiB |
|
Before Width: | Height: | Size: 596 KiB After Width: | Height: | Size: 596 KiB |
|
Before Width: | Height: | Size: 496 KiB After Width: | Height: | Size: 496 KiB |
BIN
network-poc/temp/frontend-old/public/avatars/lion.png
Normal file
|
After Width: | Height: | Size: 872 KiB |