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);
}