CrewAI-yhteensopiva projektioutput: agents.yaml, tasks.yaml, crew.py, prompts/

Pipeline kerää promptLog-listan jokaisesta agenttikutsusta (system prompt +
syöte + tulos) ja generoi lopuksi CrewAI-rakenteen files-objektiin.
Korjattu myös template.order.length-kaatuminen vapaassa tilassa.
This commit is contained in:
Jaakko Vanhala
2026-04-12 13:41:04 +03:00
parent 7fcc97f525
commit 1718805978

View File

@@ -793,8 +793,92 @@ OUTPUT FORMAT:
termLog(` <span style="color:#8b949e;font-size:12px">${esc(explanation)}</span>`); termLog(` <span style="color:#8b949e;font-size:12px">${esc(explanation)}</span>`);
} }
// --- 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) { async function kpnProject(task) {
pipelineAbort = new AbortController(); pipelineAbort = new AbortController();
const promptLog = [];
const cli = agents.client || Object.values(agents)[0]; const cli = agents.client || Object.values(agents)[0];
const mgr = agents.manager || Object.values(agents)[1]; const mgr = agents.manager || Object.values(agents)[1];
const cdr = agents.coder || Object.values(agents)[2]; 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.`); explainStep('Vaatimusmäärittely', `${cli.name} muotoilee idean selkeiksi vaatimuksiksi: ominaisuudet, datamallit, rajapinnat.`);
const brief = await kpnRun(cli.model, `${task}`, false, cli); const brief = await kpnRun(cli.model, `${task}`, false, cli);
if (!brief) { termLog(' ✗ Vaatimusmäärittely epäonnistui', '#f85149'); return; } 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(` <span style="color:#8b949e">Vaatimukset valmiit → Manageri</span>`); termLog(` <span style="color:#8b949e">Vaatimukset valmiit → Manageri</span>`);
// Valitaan mallipohja automaattisesti briefin perusteella // Valitaan mallipohja automaattisesti briefin perusteella
@@ -843,6 +928,7 @@ OUTPUT FORMAT:
return; return;
} }
explainStep('Suunnitelma', `${fileOrder.length} tiedostoa: ${fileOrder.join(', ')}`); 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 = {}; const files = {};
@@ -898,10 +984,11 @@ OUTPUT FORMAT:
return; return;
} }
files[fileName] = code; 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'); 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 // Review-korjausluuppi: max 2 kierrosta
const tst = agents.tester || Object.values(agents)[5]; 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 reviewPrompt = (tst.prompt ? tst.prompt+'\n\n' : '') + `Review this project:\n\n${currentCode}`;
const review = await kpnRun(tst.model, reviewPrompt, false, tst); 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++; stepN++;
// LGTM → ei korjauksia tarvita // LGTM → ei korjauksia tarvita
@@ -936,6 +1024,7 @@ OUTPUT FORMAT:
// Parsitaan korjatut tiedostot takaisin files-objektiin // Parsitaan korjatut tiedostot takaisin files-objektiin
if (fixedCode) { 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); const fixedParts = fixedCode.split(/^---\s*(\S+)\s*---$/m);
for (let j = 1; j < fixedParts.length; j += 2) { for (let j = 1; j < fixedParts.length; j += 2) {
const fname = fixedParts[j].trim(); const fname = fixedParts[j].trim();
@@ -959,7 +1048,10 @@ OUTPUT FORMAT:
explainStep('Testit', `${qaAgent.name} kirjoittaa pytest-testit korjatulle koodille.`); 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 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); 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++; stepN++;
} }
@@ -979,7 +1071,10 @@ OUTPUT FORMAT:
`- CMD: uv run uvicorn main:app --host 0.0.0.0 --port 8000\n` + `- CMD: uv run uvicorn main:app --host 0.0.0.0 --port 8000\n` +
`\nWrite ONLY the Dockerfile, no explanations.`; `\nWrite ONLY the Dockerfile, no explanations.`;
const dockerfile = await kpnRun(tst.model, dockerPrompt, false, tst); 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++; stepN++;
// Tarkkailija: yhteenveto + raportti + arvosana // Tarkkailija: yhteenveto + raportti + arvosana
@@ -1018,6 +1113,7 @@ OUTPUT FORMAT:
files['README.md'] = readme; files['README.md'] = readme;
// Tallennetaan raportti globaalisti jotta tarkkailija-klikkaus avaa sen // Tallennetaan raportti globaalisti jotta tarkkailija-klikkaus avaa sen
window._lastReport = readme; 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 // Parsitaan arvosana → tarkkailijan kehäväri
const firstLine = readme.split('\n')[0].toUpperCase(); const firstLine = readme.split('\n')[0].toUpperCase();
let verdictColor = '#3fb950'; // oletus: vihreä let verdictColor = '#3fb950'; // oletus: vihreä
@@ -1040,6 +1136,10 @@ OUTPUT FORMAT:
termLog(`\n<span style="color:var(--purple);font-weight:bold">━━━ Valmis (${Object.keys(files).length} tiedostoa) ━━━</span>`); termLog(`\n<span style="color:var(--purple);font-weight:bold">━━━ Valmis (${Object.keys(files).length} tiedostoa) ━━━</span>`);
explainStep('Tulos', `Projekti "${task}" generoitu ${Object.keys(files).length} tiedostoon. Klikkaa "Avaa editorissa" tutkiaksesi koodia ja README:tä.`); 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); renderProjectCard(files, task);
} }