From 1f85c0362429d55061025f3c48ce93dd57b44603 Mon Sep 17 00:00:00 2001 From: Jaakko Vanhala Date: Sun, 12 Apr 2026 15:47:46 +0300 Subject: [PATCH] =?UTF-8?q?Pipeline-rajoitteet=20kevennetty=20ja=20n=C3=A4?= =?UTF-8?q?kyville=20Asetukset-sivulle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - maxTokens: client/manager/devops/observer 512→1024 - Client: 200→400 sanaa, 3-5→3-8 ominaisuutta, MVP-rajoitus poistettu - Manager: 4-5→8 tiedostoa, vapaa tila 6→8 - Terminaali: 100→300 riviä, CrewAI prompt truncation 20→50 riviä - Uusi pipelineConfig-objekti (localStorage-persistenssi) - Asetukset-sivulle Pipeline-rajoitteet -osio sliderien kanssa - AGENTS_VERSION 3→4 --- .../frontend/src/components/Settings.astro | 43 +++++ network-poc/frontend/src/pages/index.astro | 157 ++++++++++-------- 2 files changed, 132 insertions(+), 68 deletions(-) diff --git a/network-poc/frontend/src/components/Settings.astro b/network-poc/frontend/src/components/Settings.astro index 9d7008e..747c2b1 100644 --- a/network-poc/frontend/src/components/Settings.astro +++ b/network-poc/frontend/src/components/Settings.astro @@ -58,6 +58,49 @@ + +
+

Pipeline-rajoitteet

+

Projektin generoinnin rajat. Suuremmat arvot = rikkaampi output, hitaampi suoritus.

+
+
+ + +
Vaatimusmäärittelyn maksimipituus sanoina
+
+
+ + +
Montako ominaisuutta vaatimuksiin
+
+
+ + +
Managerin suunnittelemien tiedostojen yläraja
+
+
+ + +
Tiedostoraja kun ei mallipohjaa
+
+
+ + +
Katselmointi-korjaus-syklien max määrä
+
+
+ + +
Terminaalin näyttämien rivien yläraja
+
+
+ + +
tasks.yaml:n promptin max rivimäärä
+
+
+
+
diff --git a/network-poc/frontend/src/pages/index.astro b/network-poc/frontend/src/pages/index.astro index ab54cf4..32f595d 100644 --- a/network-poc/frontend/src/pages/index.astro +++ b/network-poc/frontend/src/pages/index.astro @@ -42,7 +42,7 @@ import Settings from "../components/Settings.astro";

Seuraa reaaliajassa miten kuusi erikoistunutta AI-agenttia suunnittelee, koodaa, testaa ja katselmoi ohjelmistoprojektin — askel askeleelta.

-

+

Jokaisen agentin prompti, syöte ja tulos tallennetaan. Lopuksi saat toistettavan CrewAI-projektin.

@@ -212,33 +212,32 @@ import Settings from "../components/Settings.astro"; // === Globaalit tilat === const defaultAgents = { - client: { name: 'Asiakas', avatar: '/avatars/kettu_notext.webp', model: 'qwen-coder', order: 0, - temperature: 0.6, topK: 40, repeatPenalty: 1.15, maxTokens: 512, + client: { name: 'Client', avatar: '/avatars/kettu_notext.webp', model: 'qwen-coder', order: 0, + temperature: 0.6, topK: 40, repeatPenalty: 1.15, maxTokens: 1024, prompt: `You are a product owner who turns vague ideas into clear, actionable software requirements. GIVEN a short project description from the user, produce a structured brief: 1. PROJECT NAME: a short, descriptive name 2. GOAL: one sentence explaining what the software does and who it's for -3. CORE FEATURES: numbered list of 3-5 concrete features (not vague wishes) -4. DATA MODEL: list the main entities and their key fields -5. API ENDPOINTS: list the essential REST endpoints (method + path + purpose) -6. CONSTRAINTS: any technical constraints (e.g. "must use SQLite", "no auth needed for MVP") +3. CORE FEATURES: numbered list of 3-8 concrete features (not vague wishes) +4. DATA MODEL: list the main entities and their key fields (include field types) +5. API ENDPOINTS: list the REST endpoints (method + path + purpose) +6. CONSTRAINTS: any technical constraints (e.g. "must use SQLite", "no auth needed") RULES: - Be specific: "User can filter todos by status" not "todo management" -- Keep scope small — MVP only, no nice-to-haves - Use plain English, no code -- Maximum 200 words total` }, - manager: { name: 'Manageri', avatar: '/avatars/karhunpentu.webp', model: 'qwen-coder', order: 1, - temperature: 0.5, topK: 40, repeatPenalty: 1.15, maxTokens: 512, +- Maximum 400 words total` }, + manager: { name: 'Manager', avatar: '/avatars/karhunpentu.webp', model: 'qwen-coder', order: 1, + temperature: 0.5, topK: 40, repeatPenalty: 1.15, maxTokens: 1024, prompt: `You are a senior project manager and software architect. Your job is to plan the file structure of a software project. RULES: - List each source file on its own line in format: filename.py: one-line description - List dependency files FIRST (models.py, schemas.py before main.py) - Use pyproject.toml for Python dependencies (never requirements.txt) -- Maximum 4-5 files per project +- Maximum 8 files per project - Only filenames, no directory paths - Common patterns: models.py → schemas.py → main.py → pyproject.toml @@ -247,7 +246,7 @@ models.py: SQLAlchemy database models and engine setup schemas.py: Pydantic request/response schemas main.py: FastAPI application with CRUD endpoints pyproject.toml: project dependencies` }, - coder: { name: 'Koodari', avatar: '/avatars/kipina_notext.webp', model: 'qwen-coder', order: 2, + coder: { name: 'Coder', avatar: '/avatars/kipina_notext.webp', model: 'qwen-coder', order: 2, temperature: 0.7, topK: 40, repeatPenalty: 1.15, maxTokens: 1024, prompt: `You are an expert Python developer. Write complete, production-ready code. @@ -268,7 +267,7 @@ NEVER: - Forget to import from other project files - Use requirements.txt or Poetry — always use pyproject.toml with [project] format (PEP 621) - Use pip install — use uv (e.g. uv run uvicorn main:app --reload)` }, - data: { name: 'Data', avatar: '/avatars/pesukarhu_notext.webp', model: 'qwen-coder', order: 3, + data: { name: 'Data Engineer', avatar: '/avatars/pesukarhu_notext.webp', model: 'qwen-coder', order: 3, temperature: 0.5, topK: 40, repeatPenalty: 1.15, maxTokens: 1024, prompt: `You are a database architect specializing in SQLAlchemy and relational databases. @@ -303,7 +302,7 @@ TEST STRUCTURE: ALWAYS: from fastapi.testclient import TestClient` }, tester: { name: 'DevOps', avatar: '/avatars/laiskiainen_notext.webp', model: 'qwen-coder', order: 5, - temperature: 0.3, topK: 40, repeatPenalty: 1.1, maxTokens: 512, + temperature: 0.3, topK: 40, repeatPenalty: 1.1, maxTokens: 1024, prompt: `You are a strict code reviewer and static analysis expert. Analyze the code line by line. STATIC ANALYSIS CHECKLIST: @@ -321,8 +320,8 @@ RESPOND: - If all checks pass: "LGTM" - If issues found: list each as "ISSUE: filename.py: description" - Be specific and actionable, not vague` }, - observer: { name: 'Tarkkailija', avatar: '/avatars/aikuinen_susi.webp', model: 'qwen-coder', order: 6, - temperature: 0.6, topK: 40, repeatPenalty: 1.15, maxTokens: 512, + observer: { name: 'Observer', avatar: '/avatars/aikuinen_susi.webp', model: 'qwen-coder', order: 6, + temperature: 0.6, topK: 40, repeatPenalty: 1.15, maxTokens: 1024, prompt: `You are an independent technical observer and risk analyst. EVALUATE THE PROJECT FOR: @@ -337,7 +336,7 @@ OUTPUT FORMAT: - End with overall assessment: "SHIP IT" or "NEEDS WORK: reason"` }, }; // Versio: kasvata kun oletuspromptit muuttuvat - const AGENTS_VERSION = 3; + const AGENTS_VERSION = 4; let agents; const savedVersion = parseInt(localStorage.getItem('kpn-agents-version') || '0'); if (savedVersion < AGENTS_VERSION && localStorage.getItem('kpn-agents')) { @@ -369,6 +368,19 @@ OUTPUT FORMAT: let settings = JSON.parse(localStorage.getItem('kpn-settings') || 'null') || { ...defaultSettings }; function saveSettings() { localStorage.setItem('kpn-settings', JSON.stringify(settings)); } + // Pipeline-rajoitteet (näkyvillä Asetukset-sivulla) + const defaultPipelineConfig = { + clientMaxWords: 400, + clientMaxFeatures: 8, + managerMaxFiles: 8, + freeMaxFiles: 8, + maxReviewRounds: 3, + terminalMaxLines: 300, + crewaiPromptLines: 50, + }; + let pipelineConfig = JSON.parse(localStorage.getItem('kpn-pipeline') || 'null') || { ...defaultPipelineConfig }; + function savePipelineConfig() { localStorage.setItem('kpn-pipeline', JSON.stringify(pipelineConfig)); } + // === Tab switching === window.switchTab = function(tab) { document.querySelectorAll('.panel').forEach(p => p.classList.remove('active')); @@ -561,7 +573,7 @@ OUTPUT FORMAT: if (color) div.style.color = color; div.innerHTML = html; termPanel.appendChild(div); - while (termPanel.children.length > 100 && !termPanel.firstChild.querySelector('.stream-content')) termPanel.removeChild(termPanel.firstChild); + while (termPanel.children.length > pipelineConfig.terminalMaxLines && !termPanel.firstChild.querySelector('.stream-content')) termPanel.removeChild(termPanel.firstChild); termPanel.scrollTop = termPanel.scrollHeight; } @@ -658,21 +670,12 @@ OUTPUT FORMAT: const statusDiv = document.createElement('div'); statusDiv.className = 'terminal-line'; statusDiv.id = 'status-' + taskId; - statusDiv.innerHTML = ` ${model} käsittelee...`; + statusDiv.innerHTML = ` ${model} processing...`; termPanel.appendChild(statusDiv); termPanel.scrollTop = termPanel.scrollHeight; try { - if (!silent) { - const streamDiv = document.createElement('div'); - streamDiv.className = 'terminal-line'; - streamDiv.innerHTML = ' '; - termPanel.appendChild(streamDiv); - termPanel.scrollTop = termPanel.scrollHeight; - activeStreams[taskId] = streamDiv; - } - - statusDiv.innerHTML = ` ${model} käsittelee...`; + statusDiv.innerHTML = ` ${model} processing...`; // Rakennetaan pyyntö: agentin asetukset tai globaalit oletukset const opts = agentOpts || {}; @@ -716,14 +719,7 @@ OUTPUT FORMAT: statusDiv.innerHTML = ` ${esc(data.model || model)} ${tokGen} tok · ${durS} · ${tokS}`; if (!silent && response) { - const firstLine = response.split('\n').find(l => l.trim()) || response; - const lineCount = response.split('\n').filter(l => l.trim()).length; - const uid = 'code-' + Date.now(); - termLog( - ` ` - + ` ${esc(firstLine.trim())} ${lineCount > 1 ? '(+' + (lineCount-1) + ' riviä)' : ''}` - + `` - ); + } return response; } catch(e) { @@ -975,11 +971,11 @@ OUTPUT FORMAT: const name = `step_${i}_${e.label.replace(/[^a-z0-9]/gi, '_').toLowerCase()}`; ty += `\n${name}:\n`; ty += ` description: |\n`; - const descLines = e.userPrompt.split('\n').slice(0, 20); + const descLines = e.userPrompt.split('\n').slice(0, pipelineConfig.crewaiPromptLines); for (const line of descLines) { ty += ` ${line}\n`; } - if (e.userPrompt.split('\n').length > 20) ty += ` # ... (truncated)\n`; + if (e.userPrompt.split('\n').length > pipelineConfig.crewaiPromptLines) ty += ` # ... (truncated)\n`; ty += ` expected_output: >-\n ${e.label}\n`; ty += ` agent: ${e.agentKey}\n`; } @@ -1027,14 +1023,14 @@ OUTPUT FORMAT: const cdr = agents.coder || Object.values(agents)[2]; // Asiakas: jalostaa vaatimukset - termLog(`━━━ Projekti — ${esc(task)} ━━━`); - termLog(`\n[0] ${esc(cli.name)} — vaatimusmäärittely`); + termLog(`━━━ Project — ${esc(task)} ━━━`); + termLog(`\n[0] ${esc(cli.name)} — Requirements Definition`); highlightAgent('client'); - explainStep('Vaatimusmäärittely', `${cli.name} muotoilee idean selkeiksi vaatimuksiksi: ominaisuudet, datamallit, rajapinnat.`); + explainStep('Requirements Definition', `${cli.name} forms clear requirements: features, models, endpoints.`); const brief = await kpnRun(cli.model, `${task}`, false, cli); - if (!brief) { termLog(' ✗ Vaatimusmäärittely epäonnistui', '#f85149'); return; } - promptLog.push({ step: 0, agentKey: 'client', agentName: cli.name, model: cli.model, label: 'vaatimusmäärittely', systemPrompt: cli.prompt || '', userPrompt: task, response: brief }); - termLog(` Vaatimukset valmiit → Manageri`); + if (!brief) { termLog(' ✗ Requirements definition failed', '#f85149'); return; } + promptLog.push({ step: 0, agentKey: 'client', agentName: cli.name, model: cli.model, label: 'requirements', systemPrompt: cli.prompt || '', userPrompt: task, response: brief }); + termLog(` Requirements ready → Manager`); // Valitaan mallipohja automaattisesti briefin perusteella const template = selectTemplate(brief); @@ -1047,15 +1043,15 @@ OUTPUT FORMAT: // Mallipohja löytyi — käytetään sen rakennetta fileOrder = template.order; fileDefs = template.files; - explainStep('Mallipohja', `Tunnistettiin "${template.name}" — ${fileOrder.length} tiedostoa: ${fileOrder.join(', ')}.`); + explainStep('Template', `Detected "${template.name}" — ${fileOrder.length} files: ${fileOrder.join(', ')}.`); } else { // Vapaa tila — Manageri päättää tiedostorakenteen - termLog(`\n[1] ${esc(mgr.name)} — tiedostorakenne`); + termLog(`\n[1] ${esc(mgr.name)} — File structure`); 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.`; + explainStep('Free mode', 'No suitable template found. Manager plans the architecture.'); + 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 ${pipelineConfig.freeMaxFiles} files. List dependency files first.`; const plan = await kpnRun(mgr.model, planPrompt, false, mgr); - if (!plan) { termLog(' ✗ Suunnittelu epäonnistui', '#f85149'); return; } + if (!plan) { termLog(' ✗ Planning failed', '#f85149'); return; } // Parsitaan managerin tuottama tiedostolista for (const line of plan.split('\n')) { @@ -1067,11 +1063,11 @@ OUTPUT FORMAT: } } if (fileOrder.length === 0) { - termLog(' ✗ Manageri ei tuottanut tiedostolistaa', '#f85149'); + termLog(' ✗ Manager produced no file list', '#f85149'); return; } - explainStep('Suunnitelma', `${fileOrder.length} tiedostoa: ${fileOrder.join(', ')}`); - promptLog.push({ step: 1, agentKey: 'manager', agentName: mgr.name, model: mgr.model, label: 'tiedostorakenne', systemPrompt: mgr.prompt || '', userPrompt: planPrompt, response: plan }); + explainStep('Plan', `${fileOrder.length} files: ${fileOrder.join(', ')}`); + promptLog.push({ step: 1, agentKey: 'manager', agentName: mgr.name, model: mgr.model, label: 'file structure', systemPrompt: mgr.prompt || '', userPrompt: planPrompt, response: plan }); } const files = {}; @@ -1135,16 +1131,16 @@ OUTPUT FORMAT: // Review-korjausluuppi: max 2 kierrosta const tst = agents.tester || Object.values(agents)[5]; - const MAX_REVIEW_ROUNDS = 3; + const MAX_REVIEW_ROUNDS = pipelineConfig.maxReviewRounds; for (let round = 0; round < MAX_REVIEW_ROUNDS; round++) { const currentCode = Object.entries(files).map(([n,c]) => `--- ${n} ---\n${c}`).join('\n\n'); // DevOps review - termLog(`\n[${stepN}] ${esc(tst.name)} — koodikatselmointi${round > 0 ? ' (kierros '+(round+1)+')' : ''}`); + termLog(`\n[${stepN}] ${esc(tst.name)} — code review${round > 0 ? ' (round '+(round+1)+')' : ''}`); highlightAgent('tester'); - if (round === 0) explainStep('Koodikatselmointi', `${tst.name} analysoi koodin rivi riviltä: importit, nimeämiset, virheenkäsittely, tietoturva.`); - else explainStep('Uudelleentarkistus', `${tst.name} tarkistaa korjaukset.`); + if (round === 0) explainStep('Code Review', `${tst.name} reviews code line by line: imports, naming, security.`); + else explainStep('Re-evaluation', `${tst.name} checks the applied fixes.`); const reviewPrompt = (tst.prompt ? tst.prompt+'\n\n' : '') + `Review this project:\n\n${currentCode}`; const review = await kpnRun(tst.model, reviewPrompt, false, tst); @@ -1158,16 +1154,16 @@ OUTPUT FORMAT: } // Korjaukset - termLog(`\n[${stepN}] ${esc(cdr.name)} — korjaukset${round > 0 ? ' (kierros '+(round+1)+')' : ''}`); + termLog(`\n[${stepN}] ${esc(cdr.name)} — fixes${round > 0 ? ' (round '+(round+1)+')' : ''}`); highlightAgent('coder'); - explainStep('Korjaus', `${tst.name} löysi ongelmia. ${cdr.name} saa palautteen ja korjaa.`); + explainStep('Fixing', `${tst.name} found issues. ${cdr.name} will attempt to fix them.`); const fixPrompt = `${cdr.prompt ? cdr.prompt+'\n\n' : ''}Fix these issues:\n${review}\n\nCurrent code:\n${currentCode}\n\nWrite ALL corrected files. Start each file with: --- filename.py ---`; const fixedCode = await kpnRun(cdr.model, fixPrompt, false, cdr); // Parsitaan korjatut tiedostot takaisin files-objektiin if (fixedCode) { - promptLog.push({ step: promptLog.length, agentKey: 'coder', agentName: cdr.name, model: cdr.model, label: 'korjaus' + (round > 0 ? '_r'+(round+1) : ''), systemPrompt: cdr.prompt || '', userPrompt: fixPrompt, response: fixedCode }); + promptLog.push({ step: promptLog.length, agentKey: 'coder', agentName: cdr.name, model: cdr.model, label: 'fix' + (round > 0 ? '_r'+(round+1) : ''), systemPrompt: cdr.prompt || '', userPrompt: fixPrompt, response: fixedCode }); const fixedParts = fixedCode.split(/^---\s*(\S+)\s*---$/m); for (let j = 1; j < fixedParts.length; j += 2) { const fname = fixedParts[j].trim(); @@ -1293,23 +1289,23 @@ OUTPUT FORMAT: pipelineAbort = new AbortController(); const cli = agents.client || Object.values(agents)[0]; termLog(`━━━ Pipeline ━━━`); - termLog(`\n[1/4] ${esc(cli.name)} — vaatimukset`); + termLog(`\n[1/4] ${esc(cli.name)} — Requirements`); highlightAgent('client'); const brief = await kpnRun(cli.model, `${task}`, false, cli); if (!brief) return; - termLog(`\n[2/4] Manageri`); + termLog(`\n[2/4] Manager`); highlightAgent('manager'); const plan = await kpnRun('qwen-coder', `Requirements:\n${brief}\n\nAnalyse briefly, write a spec:\n${task}`); if (!plan) return; - termLog(`\n[3/4] Koodari`); + termLog(`\n[3/4] Coder`); highlightAgent('coder'); const code = await kpnRun('qwen-coder', `${plan}\n\nWrite the code.`); if (!code) return; - termLog(`\n[4/4] Testaaja`); + termLog(`\n[4/4] DevOps`); highlightAgent('tester'); await kpnRun('qwen-coder', `Review briefly:\n${code}`); highlightAgent(null); - termLog(`\n━━━ Valmis ━━━`); + termLog(`\n━━━ Done ━━━`); } // === Project card === @@ -1403,14 +1399,14 @@ OUTPUT FORMAT: else window._editorModels[name] = m.editor.createModel(code, langMap[ext]||'plaintext'); } document.getElementById('editor-file-list').innerHTML = Object.keys(files).map(n => `
${n}
`).join(''); - document.getElementById('editor-tabs').innerHTML = Object.keys(files).map(n => `
${n}
`).join(''); + openFile(Object.keys(files)[0]); }; window.openFile = function(name) { if (!window._editorModels[name] || !window._monaco) return; window._monaco.setModel(window._editorModels[name]); document.querySelectorAll('#editor-file-list .dd-item').forEach(el => el.style.background = el.textContent===name ? 'var(--border)' : ''); - document.querySelectorAll('#editor-tabs .project-tab').forEach(el => el.classList.toggle('active', el.textContent===name)); + document.getElementById('editor-tabs').innerHTML = `
${name}
`; }; // === Guide loader === @@ -1543,6 +1539,29 @@ OUTPUT FORMAT: els.maxTokens.oninput = () => { settings.maxTokens = +els.maxTokens.value; els.maxtokVal.textContent = settings.maxTokens; saveSettings(); }; els.stopSeq.oninput = () => { settings.stopSequences = els.stopSeq.value.replace(/\n/g, '\\n'); saveSettings(); }; els.model.onchange = () => { settings.model = els.model.value; saveSettings(); }; + + // Pipeline-rajoitteet + const plcMap = [ + ['plc-words', 'clientMaxWords'], + ['plc-feats', 'clientMaxFeatures'], + ['plc-mfiles', 'managerMaxFiles'], + ['plc-ffiles', 'freeMaxFiles'], + ['plc-review', 'maxReviewRounds'], + ['plc-term', 'terminalMaxLines'], + ['plc-crew', 'crewaiPromptLines'], + ]; + for (const [id, key] of plcMap) { + const slider = document.getElementById('set-' + id); + const valEl = document.getElementById('set-' + id + '-val'); + if (!slider || !valEl) continue; + slider.value = pipelineConfig[key]; + valEl.textContent = pipelineConfig[key]; + slider.oninput = () => { + pipelineConfig[key] = +slider.value; + valEl.textContent = pipelineConfig[key]; + savePipelineConfig(); + }; + } } // Alustetaan kun settings-tab avataan const origSwitchTab = window.switchTab; @@ -1554,7 +1573,9 @@ OUTPUT FORMAT: window.resetSettings = function() { if (!confirm('Palautetaanko kaikki asetukset oletuksiin?')) return; settings = { ...defaultSettings }; + pipelineConfig = { ...defaultPipelineConfig }; saveSettings(); + savePipelineConfig(); initSettings(); }; // === Raportti-modal ===