diff --git a/network-poc/static/index.html b/network-poc/static/index.html
index 8c5a80d..0e9f115 100644
--- a/network-poc/static/index.html
+++ b/network-poc/static/index.html
@@ -1247,8 +1247,16 @@ Max ~100 sanaa — pidempi vie konteksti-ikkunaa." style="color:#484f58;cursor:h
-
-
+
+
+
+
+
@@ -4780,11 +4788,255 @@ ${filesHtml}
builderEditing = null;
}
+ function builderExportCrewAI() {
+ if (builderAgents.length === 0) { alert('Ei agentteja — luo ensin agentteja.'); return; }
+
+ // Rooli-kuvaukset CrewAI:lle
+ const roleGoals = {
+ coder: 'Write clean, tested, production-ready code',
+ qa: 'Find bugs, write tests, ensure quality',
+ devops: 'Create deployment configs, CI/CD, Docker',
+ devsecops: 'Security auditing, OWASP compliance, vulnerability scanning',
+ architect: 'System design, API contracts, architecture decisions',
+ iac: 'Write infrastructure-as-code (OpenTofu/Terraform HCL)',
+ data: 'Database design, SQL optimization, data modeling',
+ manager: 'Break down tasks, coordinate team, prioritize work',
+ writer: 'Write documentation, READMEs, guides',
+ custom: 'Complete assigned tasks effectively',
+ };
+
+ // crew.py
+ const agentDefs = builderAgents.map(a => {
+ const varName = a.id.replace(/[^a-z0-9_]/g, '_');
+ const goal = roleGoals[a.role] || roleGoals.custom;
+ return `${varName} = Agent(
+ role="${a.name}",
+ goal="${goal}",
+ backstory="""${a.prompt.replace(/"/g, '\\"')}""",
+ llm="ollama/${a.model}",
+ verbose=True,
+)`;
+ }).join('\n\n');
+
+ const taskDefs = builderAgents.map((a, i) => {
+ const varName = a.id.replace(/[^a-z0-9_]/g, '_');
+ const goal = roleGoals[a.role] || roleGoals.custom;
+ return `task_${varName} = Task(
+ description="${goal} for the project: {project_description}",
+ expected_output="Completed deliverable from ${a.name}",
+ agent=${varName},
+)`;
+ }).join('\n\n');
+
+ const agentVars = builderAgents.map(a => a.id.replace(/[^a-z0-9_]/g, '_'));
+ const taskVars = builderAgents.map(a => 'task_' + a.id.replace(/[^a-z0-9_]/g, '_'));
+
+ const crewPy = `"""
+Kipinä Agent Crew — generoitu Agent Builderista
+https://kipina.studio/#builder
+"""
+from crewai import Agent, Task, Crew, Process
+
+# ── Agentit ──
+
+${agentDefs}
+
+# ── Tehtävät ──
+
+${taskDefs}
+
+# ── Tiimi ──
+
+crew = Crew(
+ agents=[${agentVars.join(', ')}],
+ tasks=[${taskVars.join(', ')}],
+ process=Process.sequential,
+ verbose=True,
+)
+
+if __name__ == "__main__":
+ import sys
+ project = sys.argv[1] if len(sys.argv) > 1 else "FastAPI + SQLite CRUD API"
+ result = crew.kickoff(inputs={"project_description": project})
+ print(result)
+`;
+
+ // pyproject.toml
+ const pyproject = `[project]
+name = "kipina-crew"
+version = "0.1.0"
+requires-python = ">=3.11"
+dependencies = [
+ "crewai[tools]>=0.100.0",
+]
+
+[project.scripts]
+crew = "crew:crew.kickoff"
+`;
+
+ // .env
+ const models = [...new Set(builderAgents.map(a => a.model))];
+ const dotenv = `# Ollama-asetukset
+OLLAMA_BASE_URL=http://ollama:11434
+# Mallit jotka tarvitaan: ${models.join(', ')}
+`;
+
+ // Dockerfile
+ const dockerfile = `FROM python:3.12-slim
+COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
+WORKDIR /app
+COPY pyproject.toml ./
+RUN uv sync --no-dev
+COPY crew.py .env ./
+CMD ["uv", "run", "python", "crew.py"]
+`;
+
+ // docker-compose.yml
+ const ollamaPulls = models.map(m => `ollama pull ${m}`).join(' && ');
+ const compose = `services:
+ ollama:
+ image: ollama/ollama:latest
+ container_name: kipina-crew-ollama
+ ports:
+ - "11434:11434"
+ volumes:
+ - ollama-models:/root/.ollama
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:11434/api/version"]
+ interval: 5s
+ timeout: 3s
+ retries: 10
+
+ # Ladataan mallit Ollamaan ennen crewn käynnistystä
+ ollama-init:
+ image: ollama/ollama:latest
+ depends_on:
+ ollama:
+ condition: service_healthy
+ entrypoint: ["sh", "-c", "${ollamaPulls}"]
+ environment:
+ - OLLAMA_HOST=http://ollama:11434
+
+ crew:
+ build: .
+ container_name: kipina-crew
+ depends_on:
+ ollama-init:
+ condition: service_completed_successfully
+ environment:
+ - OLLAMA_BASE_URL=http://ollama:11434
+ # Projektin kuvaus argumenttina:
+ # docker compose run crew uv run python crew.py "FastAPI todo app"
+
+volumes:
+ ollama-models:
+`;
+
+ // README.md
+ const readme = `# Kipinä Agent Crew
+
+Generoitu [Kipinä Agent Builderista](https://kipina.studio/#builder).
+
+## Agentit
+
+${builderAgents.map(a => `- **${a.name}** — ${a.role} (${a.model})`).join('\\n')}
+
+## Käynnistys
+
+\`\`\`bash
+# Docker Compose (suositeltu) — käynnistää Ollaman + lataa mallit + ajaa crewn
+docker compose run crew uv run python crew.py "FastAPI + SQLite CRUD API"
+
+# Tai lokaalisti (vaatii Ollaman käynnissä)
+uv sync
+uv run python crew.py "FastAPI + SQLite CRUD API"
+\`\`\`
+`;
+
+ // Genero ZIP käyttäen samaa logiikkaa kuin downloadZip
+ const files = {
+ 'crew.py': crewPy,
+ 'pyproject.toml': pyproject,
+ '.env': dotenv,
+ 'Dockerfile': dockerfile,
+ 'docker-compose.yml': compose,
+ 'README.md': readme,
+ };
+
+ function crc32(bytes) {
+ let crc = 0xFFFFFFFF;
+ for (let i = 0; i < bytes.length; i++) {
+ crc ^= bytes[i];
+ for (let j = 0; j < 8; j++) crc = (crc >>> 1) ^ (crc & 1 ? 0xEDB88320 : 0);
+ }
+ return (crc ^ 0xFFFFFFFF) >>> 0;
+ }
+
+ const entries = Object.entries(files);
+ const parts = [];
+ const centralDir = [];
+ let offset = 0;
+
+ for (const [name, content] of entries) {
+ const nameBytes = new TextEncoder().encode(name);
+ const contentBytes = new TextEncoder().encode(content || '');
+ const crc = crc32(contentBytes);
+
+ const header = new Uint8Array(30 + nameBytes.length);
+ const hv = new DataView(header.buffer);
+ hv.setUint32(0, 0x04034b50, true);
+ hv.setUint16(4, 20, true);
+ hv.setUint16(8, 0, true);
+ hv.setUint32(14, crc, true);
+ hv.setUint32(18, contentBytes.length, true);
+ hv.setUint32(22, contentBytes.length, true);
+ hv.setUint16(26, nameBytes.length, true);
+ header.set(nameBytes, 30);
+ parts.push(header, contentBytes);
+
+ const cd = new Uint8Array(46 + nameBytes.length);
+ const cv = new DataView(cd.buffer);
+ cv.setUint32(0, 0x02014b50, true);
+ cv.setUint16(4, 20, true);
+ cv.setUint16(6, 20, true);
+ cv.setUint16(10, 0, true);
+ cv.setUint32(16, crc, true);
+ cv.setUint32(20, contentBytes.length, true);
+ cv.setUint32(24, contentBytes.length, true);
+ cv.setUint16(28, nameBytes.length, true);
+ cv.setUint32(42, offset, true);
+ cd.set(nameBytes, 46);
+ centralDir.push(cd);
+ offset += header.length + contentBytes.length;
+ }
+
+ const cdOffset = offset;
+ let cdSize = 0;
+ for (const cd of centralDir) { parts.push(cd); cdSize += cd.length; }
+ const eocd = new Uint8Array(22);
+ const ev = new DataView(eocd.buffer);
+ ev.setUint32(0, 0x06054b50, true);
+ ev.setUint16(8, entries.length, true);
+ ev.setUint16(10, entries.length, true);
+ ev.setUint32(12, cdSize, true);
+ ev.setUint32(16, cdOffset, true);
+ parts.push(eocd);
+
+ const blob = new Blob(parts, { type: 'application/zip' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = 'kipina-crew.zip';
+ a.click();
+ URL.revokeObjectURL(url);
+ }
+
// Globaalit Builder-funktiot (onclick tarvitsee)
window.builderNew = builderNew;
window.builderEdit = builderEdit;
window.builderSave = builderSave;
window.builderDelete = builderDelete;
+ window.builderExportCrewAI = builderExportCrewAI;
window.builderCancel = builderCancel;
// Ladataan agentit kun builder-tabi avataan