uusi projekti

This commit is contained in:
Jaakko Vanhala
2026-04-12 10:28:57 +03:00
parent 094b183c17
commit 2f140c8a15
16 changed files with 521 additions and 102 deletions

131
kipina-node Executable file
View 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" -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"

BIN
kipina-node-bin Executable file

Binary file not shown.

View File

@@ -1 +1 @@
5f005820535910a5052a33cfcfc0bd6909d11c25
dirty-3e9cdd70c60dadfb970cee47ebbd912c

View File

@@ -0,0 +1,33 @@
{
"name": "Data Analytics Pipeline",
"description": "ETL, analysis, and visualization with Docker (MariaDB + Jupyter)",
"keywords": ["data", "analytics", "csv", "etl", "visualization", "statistics", "dashboard", "jupyter", "pandas", "matplotlib"],
"files": {
"etl.py": {
"description": "Data loading, cleaning, and transformation",
"example": "import pandas as pd\nfrom pathlib import Path\nfrom sqlalchemy import create_engine\n\nDB_URL = \"mysql+pymysql://root:secret@localhost:3306/analytics\"\nengine = create_engine(DB_URL)\n\ndef load_csv(path: str) -> pd.DataFrame:\n df = pd.read_csv(path)\n print(f\"Loaded {len(df)} rows from {path}\")\n return df\n\ndef clean(df: pd.DataFrame) -> pd.DataFrame:\n df = df.dropna(subset=[\"x\", \"y\"])\n df = df[(df[\"x\"] >= 0) & (df[\"y\"] >= 0)] # Remove outliers\n df[\"timestamp\"] = pd.to_datetime(df[\"timestamp\"])\n return df.sort_values(\"timestamp\").reset_index(drop=True)\n\ndef to_database(df: pd.DataFrame, table: str):\n df.to_sql(table, engine, if_exists=\"replace\", index=False)\n print(f\"Wrote {len(df)} rows to {table}\")\n\nif __name__ == \"__main__\":\n for csv_file in sorted(Path(\"data\").glob(\"*.csv\")):\n df = load_csv(str(csv_file))\n df = clean(df)\n to_database(df, \"measurements\")",
"instructions": "Write the ETL pipeline:\n- Load CSV files from data/ directory using pandas\n- Clean: remove nulls, filter outliers, parse timestamps\n- Transform: convert units, compute derived columns\n- Load into MariaDB via SQLAlchemy\n- Make it runnable as a standalone script"
},
"analysis.py": {
"description": "Statistical analysis and metrics computation",
"example": "import pandas as pd\nfrom sqlalchemy import create_engine\n\nDB_URL = \"mysql+pymysql://root:secret@localhost:3306/analytics\"\nengine = create_engine(DB_URL)\n\ndef load_data() -> pd.DataFrame:\n return pd.read_sql(\"SELECT * FROM measurements\", engine)\n\ndef summary_stats(df: pd.DataFrame) -> dict:\n return {\n \"total_rows\": len(df),\n \"date_range\": f\"{df['timestamp'].min()} to {df['timestamp'].max()}\",\n \"unique_entities\": df[\"entity_id\"].nunique(),\n }\n\ndef hourly_distribution(df: pd.DataFrame) -> pd.DataFrame:\n df[\"hour\"] = df[\"timestamp\"].dt.hour\n return df.groupby(\"hour\").size().reset_index(name=\"count\")\n\nif __name__ == \"__main__\":\n df = load_data()\n stats = summary_stats(df)\n for k, v in stats.items():\n print(f\"{k}: {v}\")",
"instructions": "Write analysis functions:\n- Load cleaned data from MariaDB\n- Compute summary statistics (counts, date ranges, distributions)\n- Time-based analysis (hourly, daily, weekly patterns)\n- Group-level metrics (per entity, per zone)\n- Return DataFrames and dicts suitable for visualization"
},
"visualize.py": {
"description": "Charts and visualizations with matplotlib",
"example": "import matplotlib.pyplot as plt\nimport pandas as pd\nfrom analysis import load_data, hourly_distribution\n\ndef plot_heatmap(df: pd.DataFrame, title: str, output: str):\n fig, ax = plt.subplots(figsize=(12, 8))\n scatter = ax.scatter(df[\"x\"], df[\"y\"], c=df[\"density\"], cmap=\"hot\", alpha=0.5, s=2)\n ax.set_title(title)\n ax.set_xlabel(\"x\")\n ax.set_ylabel(\"y\")\n ax.invert_yaxis()\n plt.colorbar(scatter, label=\"Density\")\n plt.tight_layout()\n plt.savefig(output, dpi=150)\n print(f\"Saved {output}\")\n\ndef plot_bar(df: pd.DataFrame, x: str, y: str, title: str, output: str):\n fig, ax = plt.subplots(figsize=(10, 5))\n ax.bar(df[x], df[y], color=\"steelblue\")\n ax.set_title(title)\n ax.set_xlabel(x)\n ax.set_ylabel(y)\n plt.tight_layout()\n plt.savefig(output, dpi=150)\n\nif __name__ == \"__main__\":\n df = load_data()\n hourly = hourly_distribution(df)\n plot_bar(hourly, \"hour\", \"count\", \"Hourly Distribution\", \"output/hourly.png\")",
"instructions": "Write visualization functions:\n- Import analysis functions for data\n- Heatmaps, bar charts, line charts as appropriate\n- Save figures to output/ directory (PNG, 150 DPI)\n- Use matplotlib with clear titles, labels, colorbars\n- Make it runnable as standalone to generate all charts"
},
"docker-compose.yml": {
"description": "Docker Compose stack for database and Jupyter",
"example": "services:\n db:\n image: mariadb:11\n environment:\n MYSQL_ROOT_PASSWORD: secret\n MYSQL_DATABASE: analytics\n ports:\n - \"3306:3306\"\n volumes:\n - db_data:/var/lib/mysql\n\n jupyter:\n image: jupyter/scipy-notebook:latest\n ports:\n - \"8888:8888\"\n volumes:\n - .:/home/jovyan/work\n environment:\n JUPYTER_TOKEN: kipina\n depends_on:\n - db\n\nvolumes:\n db_data:",
"instructions": "Write docker-compose.yml:\n- MariaDB service with persistent volume\n- JupyterLab service with project mounted\n- Correct environment variables\n- Port mappings for local development\n- Write ONLY the YAML, no explanations"
},
"pyproject.toml": {
"description": "Project dependencies",
"example": "[project]\nname = \"analytics\"\nversion = \"0.1.0\"\nrequires-python = \">=3.11\"\ndependencies = [\n \"pandas\",\n \"matplotlib\",\n \"sqlalchemy\",\n \"pymysql\",\n]\n\n[project.scripts]\netl = \"python etl.py\"\nanalyze = \"python analysis.py\"\nvisualize = \"python visualize.py\"",
"instructions": "Use [project] format (PEP 621). List all data science dependencies. Add scripts for ETL, analysis, and visualization."
}
},
"order": ["etl.py", "analysis.py", "visualize.py", "docker-compose.yml", "pyproject.toml"]
}

View File

@@ -1,6 +1,7 @@
{
"name": "FastAPI CRUD",
"description": "REST API with SQLite database",
"keywords": ["api", "rest", "crud", "endpoint", "fastapi", "web", "backend", "server", "database", "sqlite"],
"files": {
"models.py": {
"description": "SQLAlchemy models, engine, and session",

View File

@@ -501,10 +501,16 @@ OUTPUT FORMAT:
// Wasm-autostart vain jos natiivisolmua ei löydy (tarkistetaan onopen:ssa)
// === Pipeline-keskeytys ===
let pipelineAbort = null; // AbortController tai null
// === kpnRun: lähettää promptin mallille ===
const activeStreams = {};
async function kpnRun(model, prompt, silent, agentOpts) {
// Tarkistetaan keskeytys
if (pipelineAbort?.signal?.aborted) return null;
const taskId = crypto.randomUUID();
const statusDiv = document.createElement('div');
statusDiv.className = 'terminal-line';
@@ -514,10 +520,6 @@ OUTPUT FORMAT:
termPanel.scrollTop = termPanel.scrollHeight;
try {
// Ei odotetaan Wasmia — lähetetään suoraan hubille.
// Jos hub löytää natiivisolmun, vastaus tulee nopeasti.
// Jos 503, käynnistetään Wasm-fallback.
if (!silent) {
const streamDiv = document.createElement('div');
streamDiv.className = 'terminal-line';
@@ -535,18 +537,18 @@ OUTPUT FORMAT:
model,
prompt,
task_id: taskId,
system_prompt: opts.systemPrompt || settings.systemPrompt || undefined,
system_prompt: opts.prompt || settings.systemPrompt || undefined,
temperature: opts.temperature ?? settings.temperature ?? undefined,
top_k: opts.topK ?? settings.topK ?? undefined,
max_tokens: opts.maxTokens ?? settings.maxTokens ?? undefined,
repeat_penalty: opts.repeatPenalty ?? settings.repeatPenalty ?? undefined,
stop: settings.stopSequences ? settings.stopSequences.split('\\n').filter(Boolean) : undefined,
};
const res = await fetch('/api/v1/chat/completions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
signal: pipelineAbort?.signal,
});
if (res.status === 503 && !wasmNodeStarted) {
@@ -619,7 +621,7 @@ OUTPUT FORMAT:
const kpnExamples = {
'kpn run coder': ['"hello world in python"','"fibonacci in rust"','"quicksort in javascript"'],
'kpn run coder-3b': ['"REST API with Flask"','"binary search tree"'],
'kpn project': ['"FastAPI + SQLite REST API"','"CLI tool for CSV processing"'],
'kpn project': ['"FastAPI + SQLite REST API"','"UWB indoor positioning analytics — CSV cart tracking data, heatmaps, statistics, Docker (MariaDB + Jupyter)"','"CLI tool for CSV processing"'],
'kpn pipeline': ['"todo-sovellus"','"laskin pythonilla"'],
};
@@ -753,15 +755,30 @@ OUTPUT FORMAT:
// === Template-pohjainen projektipipeline ===
let templates = {};
const TEMPLATE_FILES = ['fastapi-crud.json', 'data-analytics.json'];
// Ladataan mallipohjat
(async () => {
for (const file of TEMPLATE_FILES) {
try {
const res = await fetch('/templates/fastapi-crud.json');
const res = await fetch(`/templates/${file}`);
if (res.ok) { const t = await res.json(); templates[t.name] = t; }
} catch(e) {}
}
})();
// Valitaan mallipohja Asiakkaan briefin perusteella (keywords-match)
function selectTemplate(brief) {
const lower = brief.toLowerCase();
let best = null, bestScore = 0;
for (const t of Object.values(templates)) {
const keywords = t.keywords || [];
const score = keywords.filter(k => lower.includes(k)).length;
if (score > bestScore) { bestScore = score; best = t; }
}
return best; // null = vapaa tila
}
function explainStep(title, explanation) {
termLog(`\n <span style="color:#a371f7;font-size:12px">💡 ${esc(title)}</span>`);
termLog(` <span style="color:#8b949e;font-size:12px">${esc(explanation)}</span>`);
@@ -769,18 +786,11 @@ OUTPUT FORMAT:
async function kpnProject(task) {
const cli = agents.client || Object.values(agents)[0];
const mgr = agents.manager || Object.values(agents)[1];
const cdr = agents.coder || Object.values(agents)[2];
// Etsitään sopivin mallipohja
const template = Object.values(templates)[0]; // Toistaiseksi vain FastAPI CRUD
if (!template) {
termLog(' ✗ Mallipohjia ei ladattu', '#f85149');
return;
}
termLog(`<span style="color:var(--purple);font-weight:bold">━━━ ${esc(template.name)} — ${esc(task)} ━━━</span>`);
// Asiakas: jalostaa vaatimukset
termLog(`<span style="color:var(--purple);font-weight:bold">━━━ Projekti — ${esc(task)} ━━━</span>`);
termLog(`\n<span style="color:#f0883e;font-weight:bold">[0] ${esc(cli.name)}</span> — vaatimusmäärittely`);
highlightAgent('client');
explainStep('Vaatimusmäärittely', `${cli.name} muotoilee idean selkeiksi vaatimuksiksi: ominaisuudet, datamallit, rajapinnat.`);
@@ -788,37 +798,72 @@ OUTPUT FORMAT:
if (!brief) { termLog(' ✗ Vaatimusmäärittely epäonnistui', '#f85149'); return; }
termLog(` <span style="color:#8b949e">Vaatimukset valmiit → Manageri</span>`);
explainStep('Mallipohja', `Käytetään "${template.name}" -mallipohjaa jossa ${template.order.length} tiedostoa: ${template.order.join(', ')}. Jokainen tiedosto generoidaan järjestyksessä, ja aiemmat tiedostot annetaan kontekstina seuraavalle.`);
// Valitaan mallipohja automaattisesti briefin perusteella
const template = selectTemplate(brief);
// Tiedostolista: mallipohjasta tai managerin dynaamisesta suunnitelmasta
let fileOrder = [];
let fileDefs = {};
if (template) {
// Mallipohja löytyi — käytetään sen rakennetta
fileOrder = template.order;
fileDefs = template.files;
explainStep('Mallipohja', `Tunnistettiin "${template.name}" — ${fileOrder.length} tiedostoa: ${fileOrder.join(', ')}.`);
} else {
// Vapaa tila — Manageri päättää tiedostorakenteen
termLog(`\n<span style="color:#d29922;font-weight:bold">[1] ${esc(mgr.name)}</span> — tiedostorakenne`);
highlightAgent('manager');
explainStep('Vapaa tila', 'Sopivaa mallipohjaa ei löytynyt. Manageri suunnittelee tiedostorakenteen vaatimusten perusteella.');
const planPrompt = `PROJECT REQUIREMENTS:\n${brief}\n\nPlan the file structure for this project. List each file on its own line:\nfilename.ext: one-line description\n\nMaximum 6 files. List dependency files first.`;
const plan = await kpnRun(mgr.model, planPrompt, false, mgr);
if (!plan) { termLog(' ✗ Suunnittelu epäonnistui', '#f85149'); return; }
// Parsitaan managerin tuottama tiedostolista
for (const line of plan.split('\n')) {
const m = line.match(/^\s*[-*]?\s*(\S+\.\w+)\s*[:\-]\s*(.+)/);
if (m) {
const fname = m[1].replace(/^`|`$/g, '');
fileOrder.push(fname);
fileDefs[fname] = { description: m[2].trim(), instructions: m[2].trim() };
}
}
if (fileOrder.length === 0) {
termLog(' ✗ Manageri ei tuottanut tiedostolistaa', '#f85149');
return;
}
explainStep('Suunnitelma', `${fileOrder.length} tiedostoa: ${fileOrder.join(', ')}`);
}
const files = {};
for (let i = 0; i < template.order.length; i++) {
const fileName = template.order[i];
const fileDef = template.files[fileName];
for (let i = 0; i < fileOrder.length; i++) {
const fileName = fileOrder[i];
const fileDef = fileDefs[fileName];
if (!fileDef) continue;
const step = i + 1;
// Valitaan oikea agentti tiedostotyypin mukaan
const isDbFile = fileName === 'models.py' || fileName === 'database.py';
const isDbFile = fileName === 'models.py' || fileName === 'database.py' || fileName === 'etl.py';
const dataAgent = agents.data || Object.values(agents)[3];
const fileAgent = isDbFile && dataAgent ? dataAgent : cdr;
const fileAgentKey = isDbFile && dataAgent ? 'data' : 'coder';
termLog(`\n<span style="color:#3fb950;font-weight:bold">[${step}/${template.order.length}] ${esc(fileAgent.name)}</span> — ${esc(fileName)}`);
termLog(`\n<span style="color:#3fb950;font-weight:bold">[${step}/${fileOrder.length}] ${esc(fileAgent.name)}</span> — ${esc(fileName)}`);
highlightAgent(fileAgentKey);
// Opettava selitys: miksi tämä tiedosto, mitä se sisältää
explainStep(fileName, fileDef.instructions);
explainStep(fileName, fileDef.instructions || fileDef.description);
// Rakennetaan prompti: esimerkki + konteksti + ohje
// Rakennetaan prompti
let prompt = '';
// Agentin system prompt (data-agentti models.py:lle, koodari muille)
if (fileAgent.prompt) prompt += fileAgent.prompt + '\n\n';
// Esimerkki (few-shot)
// Esimerkki (vain mallipohjatilassa)
if (fileDef.example) {
prompt += `EXAMPLE of ${fileName} (for a different project, adapt to this one):\n`;
prompt += '```\n' + fileDef.example + '\n```\n\n';
}
// Aiemmin generoidut tiedostot (konteksti)
const prevFiles = Object.entries(files);
@@ -834,8 +879,8 @@ OUTPUT FORMAT:
// Tehtävä
prompt += `NOW write "${fileName}" for THIS project: ${task}\n`;
prompt += fileDef.instructions + '\n';
prompt += 'Adapt the example to match the project description. Import from already written files. Write ONLY the code, no explanations.';
if (fileDef.instructions) prompt += fileDef.instructions + '\n';
prompt += 'Adapt to the project requirements. Import from already written files. Write ONLY the code, no explanations.';
const code = await kpnRun(fileAgent.model, prompt, false, fileAgent);
if (!code) {

View File

@@ -4,6 +4,13 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
echo "=== Kipinä Studio Local Development ==="
# Tapetaan vanhat prosessit portissa 3000
if lsof -ti:3000 >/dev/null 2>&1; then
echo "[0] Tapetaan vanhat prosessit portissa 3000..."
lsof -ti:3000 | xargs kill -9 2>/dev/null || true
sleep 1
fi
# Frontend
echo "[1/3] Rakennetaan frontend..."
cd "$SCRIPT_DIR/frontend"
@@ -12,18 +19,32 @@ npm run build --silent 2>&1 | tail -1
# Hub
echo "[2/3] Käynnistetään hub..."
cd "$SCRIPT_DIR/hub"
cargo run &
cd "$SCRIPT_DIR"
STATIC_DIR="$SCRIPT_DIR/frontend/dist" cargo run -p hub &
HUB_PID=$!
sleep 3
sleep 2
# 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 &
# Valitaan automaattisesti ensimmäinen qwen-coder -malli
MODEL=$(curl -s http://localhost:11434/api/tags | python3 -c "
import sys, json
models = json.load(sys.stdin).get('models', [])
for m in models:
if 'coder' in m['name']:
print(m['name']); break
else:
if models: print(models[0]['name'])
" 2>/dev/null)
if [ -n "$MODEL" ]; then
echo "[3/3] Ollama löytyi — käynnistetään native-node (malli: $MODEL)..."
HUB_URL=ws://localhost:3000/ws OLLAMA_MODEL="$MODEL" cargo run -p native-node --no-default-features &
NODE_PID=$!
echo " Native-node PID: $NODE_PID"
else
echo "[3/3] Ollama käynnissä mutta ei malleja — asenna: ollama pull qwen2.5-coder:7b"
fi
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"
@@ -33,5 +54,11 @@ echo ""
echo "=== http://localhost:3000 ==="
echo " Ctrl+C pysäyttää"
# Avataan selain
open http://localhost:3000 2>/dev/null || xdg-open http://localhost:3000 2>/dev/null || true
# Siivotaan lapset Ctrl+C:llä
trap 'echo ""; echo "Pysäytetään..."; kill $HUB_PID $NODE_PID 2>/dev/null; exit 0' INT TERM
# Odotetaan hub-prosessia
wait $HUB_PID

View File

@@ -109,14 +109,21 @@ impl LlmEngine {
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(),
"<|im_end|>".into(),
];
let mut body = serde_json::json!({
// Rakennetaan messages-lista (chat API)
let mut messages = Vec::new();
if let Some(ref sp) = opts.system_prompt {
if !sp.is_empty() {
messages.push(serde_json::json!({"role": "system", "content": sp}));
}
}
messages.push(serde_json::json!({"role": "user", "content": prompt}));
let body = serde_json::json!({
"model": model,
"prompt": prompt,
"messages": messages,
"stream": false,
"options": {
"num_predict": opts.max_tokens,
@@ -126,16 +133,13 @@ impl LlmEngine {
"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 resp = self.client.post(format!("{}/api/generate", self.ollama_url))
let resp = self.client.post(format!("{}/api/chat", self.ollama_url))
.json(&body)
.send()
.await
.map_err(|e| format!("Ollama generate: {}", e))?;
.map_err(|e| format!("Ollama chat: {}", e))?;
if !resp.status().is_success() {
return Err(format!("Ollama HTTP {}", resp.status()));
@@ -144,7 +148,7 @@ impl LlmEngine {
let body: serde_json::Value = resp.json().await
.map_err(|e| format!("Ollama JSON: {}", e))?;
let text = body["response"].as_str().unwrap_or("").to_string();
let text = body["message"]["content"].as_str().unwrap_or("").to_string();
let _total_duration_ns = body["total_duration"].as_u64().unwrap_or(0);
let eval_count = body["eval_count"].as_u64().unwrap_or(0) as usize;
let eval_duration_ns = body["eval_duration"].as_u64().unwrap_or(1);
@@ -163,40 +167,15 @@ impl LlmEngine {
}
}
/// Siivoa markdown-koodiblokki-merkit ja selitystekstit
/// Siivoa markdown-koodiblokki-merkit vastauksesta
fn strip_code_fences(text: &str) -> 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
trimmed != "```" && !(trimmed.starts_with("```") && !trimmed[3..].contains('`'))
}).collect();
let mut result = filtered.join("\n").trim().to_string();
// Poista selitysteksti lopusta (kaikki rivin "\nPlease note" jälkeen jne.)
let lower = result.to_lowercase();
for stop in &["\nplease note", "\nthis is a basic", "\nthis code", "\nnote that", "\nremember to", "\nyou can", "\nto run"] {
if let Some(pos) = lower.find(stop) {
result = result[..pos].trim_end().to_string();
}
}
// Poista johdantolauseet alusta
let lower = result.to_lowercase();
for prefix in &["sure!", "here is", "here's", "certainly!", "below is"] {
if lower.starts_with(prefix) {
if let Some(nl) = result.find('\n') {
result = result[nl + 1..].to_string();
}
break;
}
}
result.trim().to_string()
filtered.join("\n").trim().to_string()
}
pub struct GenerateResult {

View File

@@ -1,5 +1,6 @@
use futures_util::{SinkExt, StreamExt};
use serde_json::json;
use std::io::IsTerminal;
use sysinfo::System;
use tokio_tungstenite::connect_async;
use tokio_tungstenite::tungstenite::Message;
@@ -362,13 +363,17 @@ async fn main() {
st.push_log("System", format!("Malli valmis: {}", active_model), None);
}
// Käynnistetään graafinen TUI vasta kun TUI:n Prompt (LlmEngine::load) on ohitettu!
// Käynnistetään graafinen TUI vain jos stdin on terminaali (ei taustaprosessina)
let ui_state = tui_state.clone();
if std::io::stdin().is_terminal() {
tokio::spawn(async move {
if let Err(e) = tui_dashboard::run_dashboard(ui_state, cmd_tx).await {
tracing::error!("Pääluupin TUI kaatui: {}", e);
}
});
} else {
tracing::info!("Ei terminaalia — TUI ohitettu, lokitetaan stdoutiin");
};
// Haetaan paikalliset mallit hubille lähetettäväksi
let mut available_models = None;
@@ -418,6 +423,48 @@ async fn main() {
st.status = "ACTIVE".to_string();
st.push_log("System", "Suoritus jatkuu...".to_string(), None);
}
} else if cmd_str == "fetch_models" {
// Haetaan mallit Ollamasta ja avataan valikkö
if let Some(ref engine) = llm {
match engine.fetch_models().await {
Ok(tags) => {
let models: Vec<String> = tags.get("models")
.and_then(|v| v.as_array())
.map(|arr| arr.iter()
.filter_map(|m| m.get("name").and_then(|n| n.as_str()).map(|s| s.to_string()))
.collect())
.unwrap_or_default();
let mut st = tui_state.write().await;
st.model_picker_items = models;
st.model_picker_idx = 0;
st.model_picker_open = true;
}
Err(e) => {
let mut st = tui_state.write().await;
st.push_log("System", format!("Mallilistan haku epäonnistui: {}", e), None);
}
}
}
} else if let Some(model) = cmd_str.strip_prefix("change_model:") {
// TUI:sta valittu malli — vaihdetaan
if let Some(ref engine) = llm {
engine.set_model(model.to_string());
match engine.ensure_model().await {
Ok(()) => {
tracing::info!("Malli vaihdettu: {}", model);
let mut st = tui_state.write().await;
st.model_name = model.to_string();
st.push_log("System", format!("Malli vaihdettu: {}", model), None);
// Ilmoitetaan hubille
let auth = build_auth_message(allocated_gb, model, available_models.clone());
let _ = write.send(Message::Text(auth)).await;
}
Err(e) => {
let mut st = tui_state.write().await;
st.push_log("System", format!("Mallin vaihto epäonnistui: {}", e), None);
}
}
}
}
}
}

View File

@@ -35,6 +35,10 @@ pub struct DashboardState {
pub last_tokens_sec: f64,
pub network_active_nodes: usize,
pub network_total_tasks: u64,
// Mallivalikko
pub model_picker_open: bool,
pub model_picker_items: Vec<String>,
pub model_picker_idx: usize,
}
impl DashboardState {
@@ -51,6 +55,9 @@ impl DashboardState {
last_tokens_sec: 0.0,
network_active_nodes: 1, // oletetaan itsemme
network_total_tasks: 0,
model_picker_open: false,
model_picker_items: Vec::new(),
model_picker_idx: 0,
}
}
@@ -88,9 +95,38 @@ pub async fn run_dashboard(
}
ev = reader.next() => {
if let Some(Ok(Event::Key(key))) = ev {
let picker_open = state.read().await.model_picker_open;
if picker_open {
// Mallivalikko auki — navigointi
match key.code {
KeyCode::Up | KeyCode::Char('k') => {
let mut st = state.write().await;
if st.model_picker_idx > 0 { st.model_picker_idx -= 1; }
}
KeyCode::Down | KeyCode::Char('j') => {
let mut st = state.write().await;
let max = st.model_picker_items.len().saturating_sub(1);
if st.model_picker_idx < max { st.model_picker_idx += 1; }
}
KeyCode::Enter => {
let mut st = state.write().await;
let idx = st.model_picker_idx;
if let Some(model) = st.model_picker_items.get(idx).cloned() {
st.model_picker_open = false;
st.push_log("System", format!("Vaihdetaan malliin: {}...", model), None);
let _ = cmd_tx.send(format!("change_model:{}", model));
}
}
KeyCode::Esc | KeyCode::Char('m') | KeyCode::Char('M') => {
state.write().await.model_picker_open = false;
}
_ => {}
}
} else {
// Normaali tila
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);
@@ -101,12 +137,16 @@ pub async fn run_dashboard(
KeyCode::Char('r') | KeyCode::Char('R') | KeyCode::Char('s') => {
let _ = cmd_tx.send("resume".to_string());
}
KeyCode::Char('m') | KeyCode::Char('M') => {
let _ = cmd_tx.send("fetch_models".to_string());
}
_ => {}
}
}
}
}
}
}
}
pub fn restore_terminal() {
@@ -214,10 +254,43 @@ fn ui(f: &mut ratatui::Frame, st: &DashboardState) {
// --- 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 status_text = format!(" Tila: {} | [P] Pause [R] Työhön [M] Malli [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]);
// --- Mallivalikko-overlay ---
if st.model_picker_open && !st.model_picker_items.is_empty() {
let area = f.area();
let popup_h = (st.model_picker_items.len() as u16 + 4).min(area.height - 4);
let popup_w = 50.min(area.width - 4);
let popup = ratatui::layout::Rect::new(
(area.width - popup_w) / 2,
(area.height - popup_h) / 2,
popup_w,
popup_h,
);
// Tausta
f.render_widget(ratatui::widgets::Clear, popup);
let items: Vec<ratatui::text::Line> = st.model_picker_items.iter().enumerate().map(|(i, name)| {
if i == st.model_picker_idx {
ratatui::text::Line::from(format!("{} ", name))
.style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD))
} else {
ratatui::text::Line::from(format!(" {} ", name))
.style(Style::default().fg(Color::White))
}
}).collect();
let picker = Paragraph::new(items)
.block(Block::default()
.title(" Vaihda malli [↑↓] Enter=valitse Esc=peruuta ")
.borders(Borders::ALL)
.style(Style::default().fg(Color::Cyan)));
f.render_widget(picker, popup);
}
}

Binary file not shown.

83
projektit/projekti1.md Normal file
View File

@@ -0,0 +1,83 @@
---
title: UWB-paikannus sisätiloihin (2024)
tags: project
slideOptions:
text-align: left,
transition: slide,
theme: white,
hideAddressBar: true,
touch: true,
slideNumber: true,
controls: true,
controlsLayout: 'bottom-right'
spotlight:
enabled: true
---
# UWB-paikannus sisätiloihin (2026)
---
![](https://gitlab.dclabra.fi/wiki/uploads/upload_dbe5eca9539b590febdb95942eaa16a7.jpg)
----
Ennakkotietona annettakoon pisteiden kohdistaminen yllä olevaan kuvatiedostoon:
- y=0 yläreunan seinän sisäpinta, x=0 kassakoneiden keskilinja
- y=5220 alareunan seinän sisäpinta, x=10406 oikealla seinän sisäpinta
Paikassa n. 100, 2500 on yksi latausasema kärryille, siinä on sisäänkäynti kauppaan (turvaportit)
Paikassa n. 900, 3600 on varsinainen latausasema joka on alakerrassa. Sieltä on liukuportaat vasemmalle kuvassa. Siellä ei ole kunnollista paikannusta joka aiheuttaa paikan hyppimistä. Nämä latausasemat eivät ole kiinnostavia tietoja.
Eli yhden yksikön muutos koordinaateissa vastaa noin yhden senttimetrin muutosta "kartalla"?
---
# Dataformaatti
Datan formaatti on esitetty alla. Data itsessään on taltioituna csv-tiedostoihin. CSV-tiedostoja on paljon ja niissä on miljoonia rivejä, joten raakadatan käsittely voi olla raskasta.
![](https://gitlab.dclabra.fi/wiki/uploads/upload_b7a59c88b50fda806ec103ad2bbeeb6b.png)
Varsinaisen ETL-/ELT-prosessin jälkeen data pitäisi olla esikäsitelty ja siivottu. Tämä prosessi on kuitenkin syytä tehdä heti alkuun, jotta myöhemmät dataan liittyvät operaatiot olisivat nopeampia.
---
Tehtävälistaa:
- Data platform
- MariaDB-tietokantakontti
- Jupyterlab-kontti ETL-prosessia ja data-analyysia varten
- Visualisointi historiadatan perusteella
- Kärryjen liikkeet kaupan layoutissa
- Outlierit pois datasta (x,y-pisteet, jotka ylittävät rajat)
- Läpimenoaika, "kuumat alueet" (eli missä on vietetty aikaa)
- Tilastoja
- Datan ajallinen täsmällisyys (näytevälin dt keskiarvo ja keskihajonta)
- Datan paikannustäsmällisyys (outlayreiden esiintymistaajuus, paikannuksen kohina eli x,y-koordinaatin heittelehtiminen luonnottomasti)
- Raportteja päivä-, viikko-, kuukausitason "liikennöinnistä"
- Läpimenoaikojen tilastointi (eri aukioloajan tunteina, eri päivinä, ruuhkahuippujen / hiljaisimpien aikojen löytäminen)
- Kuinka monta kassaa on käytössä eri aukioloajan tunteina, eri päivinä
- Kassajonojen kertyminen (kuinka monta asiakasta jonottaa kuinka monessa jonossa)
- (x,y)-koordinaattien skaalaus mittayksikköön [m]
- Keskimääräinen kärryjen kulkema matka
- Kärryjen nopeus [km/h], nopeuden liukuva keskiarvoistus (valon nopeudella / mach-nopeuksilla tapahtuvien liikkeiden karsiminen pois)
- Ostoskärryjen tasainen kierto, onko kärryjä, jotka ovat erittäin paljon/vähän käytössä
- Visualisointeja ja tilastoja
- kuumat alueet visualisoituna pohjakuvaan
- eri aikaväleinä: 9.00-11.00; 11.00-13.00; 13.00-15.00; 15.00-17.00; 17.00-19.00; 19.00-21.00
- eri viikonpäivinä
- Tilastot ja kuvaajat yllä mainitusta (esim histogrammit)
- Useamman datalähteen yhdistäminen
- Esim. avoimen säätietohistorian yhdistäminen eri tuntien kävijämääriin
- Jotain muuta, asiakkaalle mahdollisesti lisäarvoa tuottavaa - keksikää jotain jännää!
---