diff --git a/kipina-node b/kipina-node new file mode 100755 index 0000000..d48978f --- /dev/null +++ b/kipina-node @@ -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" diff --git a/kipina-node-bin b/kipina-node-bin new file mode 100755 index 0000000..1adbef9 Binary files /dev/null and b/kipina-node-bin differ diff --git a/network-poc/frontend/public/download/.build-hash b/network-poc/frontend/public/download/.build-hash index ffb8767..e103fed 100644 --- a/network-poc/frontend/public/download/.build-hash +++ b/network-poc/frontend/public/download/.build-hash @@ -1 +1 @@ -5f005820535910a5052a33cfcfc0bd6909d11c25 +dirty-3e9cdd70c60dadfb970cee47ebbd912c diff --git a/network-poc/frontend/public/download/kipina-node-linux-arm64 b/network-poc/frontend/public/download/kipina-node-linux-arm64 index 1e48460..83abfed 100755 Binary files a/network-poc/frontend/public/download/kipina-node-linux-arm64 and b/network-poc/frontend/public/download/kipina-node-linux-arm64 differ diff --git a/network-poc/frontend/public/download/kipina-node-linux-x86_64 b/network-poc/frontend/public/download/kipina-node-linux-x86_64 index 2f7cf56..377a1f0 100755 Binary files a/network-poc/frontend/public/download/kipina-node-linux-x86_64 and b/network-poc/frontend/public/download/kipina-node-linux-x86_64 differ diff --git a/network-poc/frontend/public/download/kipina-node-macos-arm64 b/network-poc/frontend/public/download/kipina-node-macos-arm64 index 6e9cd66..1b2c9ed 100755 Binary files a/network-poc/frontend/public/download/kipina-node-macos-arm64 and b/network-poc/frontend/public/download/kipina-node-macos-arm64 differ diff --git a/network-poc/frontend/public/download/kipina-node-windows-x86_64.exe b/network-poc/frontend/public/download/kipina-node-windows-x86_64.exe index 5b3c2a5..3a303db 100755 Binary files a/network-poc/frontend/public/download/kipina-node-windows-x86_64.exe and b/network-poc/frontend/public/download/kipina-node-windows-x86_64.exe differ diff --git a/network-poc/frontend/public/templates/data-analytics.json b/network-poc/frontend/public/templates/data-analytics.json new file mode 100644 index 0000000..598801e --- /dev/null +++ b/network-poc/frontend/public/templates/data-analytics.json @@ -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"] +} diff --git a/network-poc/frontend/public/templates/fastapi-crud.json b/network-poc/frontend/public/templates/fastapi-crud.json index cc6c988..7a7a72c 100644 --- a/network-poc/frontend/public/templates/fastapi-crud.json +++ b/network-poc/frontend/public/templates/fastapi-crud.json @@ -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", diff --git a/network-poc/frontend/src/pages/index.astro b/network-poc/frontend/src/pages/index.astro index 52226ad..0868d06 100644 --- a/network-poc/frontend/src/pages/index.astro +++ b/network-poc/frontend/src/pages/index.astro @@ -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 () => { - try { - const res = await fetch('/templates/fastapi-crud.json'); - if (res.ok) { const t = await res.json(); templates[t.name] = t; } - } catch(e) {} + for (const file of TEMPLATE_FILES) { + try { + 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 💡 ${esc(title)}`); termLog(` ${esc(explanation)}`); @@ -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(`━━━ ${esc(template.name)} — ${esc(task)} ━━━`); - // Asiakas: jalostaa vaatimukset + termLog(`━━━ Projekti — ${esc(task)} ━━━`); termLog(`\n[0] ${esc(cli.name)} — 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(` Vaatimukset valmiit → Manageri`); - 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[1] ${esc(mgr.name)} — 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[${step}/${template.order.length}] ${esc(fileAgent.name)} — ${esc(fileName)}`); + termLog(`\n[${step}/${fileOrder.length}] ${esc(fileAgent.name)} — ${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) - prompt += `EXAMPLE of ${fileName} (for a different project, adapt to this one):\n`; - prompt += '```\n' + fileDef.example + '\n```\n\n'; + // 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) { diff --git a/network-poc/local.sh b/network-poc/local.sh index 1e8f4fe..76cceae 100755 --- a/network-poc/local.sh +++ b/network-poc/local.sh @@ -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 & - NODE_PID=$! - echo " Native-node PID: $NODE_PID" + # 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 diff --git a/network-poc/native-node/src/inference.rs b/network-poc/native-node/src/inference.rs index 8f0657e..0bbcdb5 100644 --- a/network-poc/native-node/src/inference.rs +++ b/network-poc/native-node/src/inference.rs @@ -109,14 +109,21 @@ impl LlmEngine { let model = self.model.borrow().clone(); let default_stop: Vec = 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 { diff --git a/network-poc/native-node/src/main.rs b/network-poc/native-node/src/main.rs index 4393ad2..b7d05a2 100644 --- a/network-poc/native-node/src/main.rs +++ b/network-poc/native-node/src/main.rs @@ -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(); - tokio::spawn(async move { - if let Err(e) = tui_dashboard::run_dashboard(ui_state, cmd_tx).await { - tracing::error!("Pääluupin TUI kaatui: {}", e); - } - }); + 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 = 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); + } + } + } } } } diff --git a/network-poc/native-node/src/tui_dashboard.rs b/network-poc/native-node/src/tui_dashboard.rs index a9b251d..9442e43 100644 --- a/network-poc/native-node/src/tui_dashboard.rs +++ b/network-poc/native-node/src/tui_dashboard.rs @@ -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, + 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,20 +95,53 @@ pub async fn run_dashboard( } 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); + 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; + } + _ => {} } - KeyCode::Char('p') | KeyCode::Char('P') => { - let _ = cmd_tx.send("pause".to_string()); + } else { + // Normaali tila + match key.code { + KeyCode::Char('q') | KeyCode::Esc => { + 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()); + } + KeyCode::Char('m') | KeyCode::Char('M') => { + let _ = cmd_tx.send("fetch_models".to_string()); + } + _ => {} } - KeyCode::Char('r') | KeyCode::Char('R') | KeyCode::Char('s') => { - let _ = cmd_tx.send("resume".to_string()); - } - _ => {} } } } @@ -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 = 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); + } } diff --git a/network-poc/nodes.db b/network-poc/nodes.db index 4739a4a..19e9928 100644 Binary files a/network-poc/nodes.db and b/network-poc/nodes.db differ diff --git a/projektit/projekti1.md b/projektit/projekti1.md new file mode 100644 index 0000000..954140a --- /dev/null +++ b/projektit/projekti1.md @@ -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ää! + +--- +