diff --git a/network-poc/frontend/src/pages/index.astro b/network-poc/frontend/src/pages/index.astro index 0206d2c..241b621 100644 --- a/network-poc/frontend/src/pages/index.astro +++ b/network-poc/frontend/src/pages/index.astro @@ -793,8 +793,92 @@ OUTPUT FORMAT: termLog(` ${esc(explanation)}`); } + // --- CrewAI-yhteensopiva output --- + function generateCrewAIFiles(promptLog, task) { + const crewFiles = {}; + + // Kerätään uniikit agentit + const agentMap = {}; + for (const entry of promptLog) { + if (!agentMap[entry.agentKey]) { + const firstSentence = (entry.systemPrompt || '').split(/\.\s/)[0].trim(); + agentMap[entry.agentKey] = { + name: entry.agentName, + model: entry.model, + systemPrompt: entry.systemPrompt || '', + goal: firstSentence.replace(/^You are (a |an )?/i, '').trim() || entry.label, + }; + } + } + + // agents.yaml + let ay = '# Agents — Kipinä Agentic Studio → CrewAI\n'; + for (const [key, a] of Object.entries(agentMap)) { + ay += `\n${key}:\n`; + ay += ` role: >-\n ${a.name}\n`; + ay += ` goal: >-\n ${a.goal}\n`; + ay += ` backstory: |\n`; + for (const line of a.systemPrompt.split('\n')) { + ay += ` ${line}\n`; + } + ay += ` llm: ${a.model}\n`; + } + crewFiles['agents.yaml'] = ay; + + // tasks.yaml + let ty = '# Tasks — Kipinä Agentic Studio → CrewAI\n'; + for (let i = 0; i < promptLog.length; i++) { + const e = promptLog[i]; + 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); + for (const line of descLines) { + ty += ` ${line}\n`; + } + if (e.userPrompt.split('\n').length > 20) ty += ` # ... (truncated)\n`; + ty += ` expected_output: >-\n ${e.label}\n`; + ty += ` agent: ${e.agentKey}\n`; + } + crewFiles['tasks.yaml'] = ty; + + // crew.py + const agentKeys = Object.keys(agentMap); + const taskNames = promptLog.map((e, i) => `step_${i}_${e.label.replace(/[^a-z0-9]/gi, '_').toLowerCase()}`); + let py = `"""${task}\n\nCrewAI crew — generated by Kipinä Agentic Studio.\nRun: crewai run\n"""\n\n`; + py += `from crewai import Agent, Crew, Process, Task\n`; + py += `from crewai.project import CrewBase, agent, crew, task\n\n\n`; + py += `@CrewBase\nclass ProjectCrew:\n """${task}"""\n\n`; + py += ` agents_config = "agents.yaml"\n tasks_config = "tasks.yaml"\n\n`; + for (const key of agentKeys) { + py += ` @agent\n def ${key}(self) -> Agent:\n return Agent(config=self.agents_config["${key}"])\n\n`; + } + for (const name of taskNames) { + py += ` @task\n def ${name}(self) -> Task:\n return Task(config=self.tasks_config["${name}"])\n\n`; + } + py += ` @crew\n def crew(self) -> Crew:\n return Crew(\n`; + py += ` agents=self.agents,\n tasks=self.tasks,\n`; + py += ` process=Process.sequential,\n verbose=True,\n )\n`; + crewFiles['crew.py'] = py; + + // prompts/*.md — jokaisen vaiheen system prompt + syöte + tulos + for (let i = 0; i < promptLog.length; i++) { + const e = promptLog[i]; + let md = `# ${i} — ${e.agentName} (${e.agentKey}) — ${e.label}\n\n`; + md += `**Malli:** \`${e.model}\`\n\n`; + md += `## System Prompt\n\n\`\`\`\n${e.systemPrompt || '(ei system promptia)'}\n\`\`\`\n\n`; + md += `## Syöte\n\n\`\`\`\n${e.userPrompt}\n\`\`\`\n\n`; + md += `## Tulos\n\n\`\`\`\n${e.response || '(ei tulosta)'}\n\`\`\`\n`; + const safe = e.label.replace(/[^a-z0-9._-]/gi, '_').toLowerCase(); + crewFiles[`prompts/${i}_${e.agentKey}_${safe}.md`] = md; + } + + return crewFiles; + } + async function kpnProject(task) { pipelineAbort = new AbortController(); + const promptLog = []; const cli = agents.client || Object.values(agents)[0]; const mgr = agents.manager || Object.values(agents)[1]; const cdr = agents.coder || Object.values(agents)[2]; @@ -806,6 +890,7 @@ OUTPUT FORMAT: explainStep('Vaatimusmäärittely', `${cli.name} muotoilee idean selkeiksi vaatimuksiksi: ominaisuudet, datamallit, rajapinnat.`); 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`); // Valitaan mallipohja automaattisesti briefin perusteella @@ -843,6 +928,7 @@ OUTPUT FORMAT: 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 }); } const files = {}; @@ -898,10 +984,11 @@ OUTPUT FORMAT: return; } files[fileName] = code; + promptLog.push({ step: promptLog.length, agentKey: fileAgentKey, agentName: fileAgent.name, model: fileAgent.model, label: fileName, systemPrompt: fileAgent.prompt || '', userPrompt: prompt, response: code }); } const allCode = Object.entries(files).map(([n,c]) => `--- ${n} ---\n${c}`).join('\n\n'); - let stepN = template.order.length + 1; + let stepN = fileOrder.length + 1; // Review-korjausluuppi: max 2 kierrosta const tst = agents.tester || Object.values(agents)[5]; @@ -918,6 +1005,7 @@ OUTPUT FORMAT: const reviewPrompt = (tst.prompt ? tst.prompt+'\n\n' : '') + `Review this project:\n\n${currentCode}`; const review = await kpnRun(tst.model, reviewPrompt, false, tst); + promptLog.push({ step: promptLog.length, agentKey: 'tester', agentName: tst.name, model: tst.model, label: 'review' + (round > 0 ? '_r'+(round+1) : ''), systemPrompt: tst.prompt || '', userPrompt: reviewPrompt, response: review || '' }); stepN++; // LGTM → ei korjauksia tarvita @@ -936,6 +1024,7 @@ OUTPUT FORMAT: // 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 }); const fixedParts = fixedCode.split(/^---\s*(\S+)\s*---$/m); for (let j = 1; j < fixedParts.length; j += 2) { const fname = fixedParts[j].trim(); @@ -959,7 +1048,10 @@ OUTPUT FORMAT: explainStep('Testit', `${qaAgent.name} kirjoittaa pytest-testit korjatulle koodille.`); const qaPrompt = (qaAgent.prompt ? qaAgent.prompt+'\n\n' : '') + `Write pytest tests for this project:\n\n${updatedCode}\n\nWrite a complete test_main.py file with TestClient.`; const tests = await kpnRun(qaAgent.model, qaPrompt, false, qaAgent); - if (tests) files['test_main.py'] = tests; + if (tests) { + files['test_main.py'] = tests; + promptLog.push({ step: promptLog.length, agentKey: 'qa', agentName: qaAgent.name, model: qaAgent.model, label: 'test_main.py', systemPrompt: qaAgent.prompt || '', userPrompt: qaPrompt, response: tests }); + } stepN++; } @@ -979,7 +1071,10 @@ OUTPUT FORMAT: `- CMD: uv run uvicorn main:app --host 0.0.0.0 --port 8000\n` + `\nWrite ONLY the Dockerfile, no explanations.`; const dockerfile = await kpnRun(tst.model, dockerPrompt, false, tst); - if (dockerfile) files['Dockerfile'] = dockerfile; + if (dockerfile) { + files['Dockerfile'] = dockerfile; + promptLog.push({ step: promptLog.length, agentKey: 'tester', agentName: tst.name, model: tst.model, label: 'Dockerfile', systemPrompt: tst.prompt || '', userPrompt: dockerPrompt, response: dockerfile }); + } stepN++; // Tarkkailija: yhteenveto + raportti + arvosana @@ -1018,6 +1113,7 @@ OUTPUT FORMAT: files['README.md'] = readme; // Tallennetaan raportti globaalisti jotta tarkkailija-klikkaus avaa sen window._lastReport = readme; + promptLog.push({ step: promptLog.length, agentKey: 'observer', agentName: obs.name, model: obs.model, label: 'README.md', systemPrompt: obs.prompt || '', userPrompt: obsPrompt, response: readme }); // Parsitaan arvosana → tarkkailijan kehäväri const firstLine = readme.split('\n')[0].toUpperCase(); let verdictColor = '#3fb950'; // oletus: vihreä @@ -1040,6 +1136,10 @@ OUTPUT FORMAT: termLog(`\n━━━ Valmis (${Object.keys(files).length} tiedostoa) ━━━`); explainStep('Tulos', `Projekti "${task}" generoitu ${Object.keys(files).length} tiedostoon. Klikkaa "Avaa editorissa" tutkiaksesi koodia ja README:tä.`); + // CrewAI-yhteensopiva output + const crewFiles = generateCrewAIFiles(promptLog, task); + Object.assign(files, crewFiles); + renderProjectCard(files, task); }