@@ -19,8 +19,8 @@ import Settings from "../components/Settings.astro";
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
<script is:inline type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11.14.0 /dist/mermaid.esm.min.mjs';
mermaid.initialize({ startOnLoad: false, theme: 'dark' });
window.mermaid = mermaid;
</script>
@@ -40,10 +40,10 @@ import Settings from "../components/Settings.astro";
<h1 class="hero-title">Näe miten AI-agenttitiimi rakentaa projektin.</h1>
<div class="hero-divider"></div>
<p class="hero-desc">
Seuraa reaaliajassa miten kuusi erikoistunutta AI-agenttia suunnittelee , koodaa, testaa ja katselmoi ohjelmistoprojektin — askel askeleelta.
Seuraa reaaliajassa miten ohjelmistokehitykseen erikoistuneet AI-agenti t suunnittelevat , koodaavat , testaavat ja katselmoivat ohjelmistoprojektin askel askeleelta. Nämä veijarit ovat erityisen hyviä Python-ohjelmoinnissa.
</p>
<p class="hero-notice" style="border-left-color:#ff6b00;color:#ff6b00">
Jokaisen agentin prompti, syöte ja tulos tallennetaan. Lopuksi saat toistettavan CrewAI-projekt in.
Jokaisen agentin prompti, syöte ja tulos tallennetaan. Lopputuloksena syntyy CrewAI-projekti, jonka parissa voit jatkaa eteenpä in.
</p>
<div class="hero-input-group">
@@ -153,14 +153,100 @@ import Settings from "../components/Settings.astro";
return esc(code);
}
// === Mekaaninen koodivalidointi (QA-stepin tueksi) ===
function validateProjectCode(files) {
const issues = [];
const fileNames = Object.keys(files);
for (const [fname, code] of Object.entries(files)) {
if (!fname.endsWith('.py')) continue;
const lines = code.split('\n');
// 1. Relatiiviset importit
for (const line of lines) {
const m = line.match(/^from\s+\.(\w*)\s+import/);
if (m) issues.push(`ISSUE: ${fname}: relatiivinen import "from .${m[1]}" — käytä absoluuttista: from ${m[1]} import ...`);
}
// 2. Projektin sisäiset importit — tarkista että importatut nimet löytyvät
for (const line of lines) {
const m = line.match(/^from\s+(models|schemas|main)\s+import\s+(.+)/);
if (!m) continue;
const srcFile = m[1] + '.py';
const srcCode = files[srcFile];
if (!srcCode) { issues.push(`ISSUE: ${fname}: importtaa "${m[1]}" mutta ${srcFile} puuttuu`); continue; }
const names = m[2].split(',').map(n => n.trim().split(/\s+as\s+/)[0].trim());
for (const name of names) {
if (name && !srcCode.includes(name)) {
issues.push(`ISSUE: ${fname}: importtaa "${name}" moduulista ${m[1]} mutta sitä ei löydy ${srcFile}:stä`);
}
}
}
// 3. models.py: SQLite connect_args
if (fname === 'models.py') {
if (/sqlite/i.test(code) && !code.includes('check_same_thread'))
issues.push('ISSUE: models.py: SQLite create_engine puuttuu connect_args={"check_same_thread": False}');
}
// 4. schemas.py: date/datetime importit
if (fname === 'schemas.py') {
if (/:\s*date\b/.test(code) && !/from datetime import/.test(code))
issues.push('ISSUE: schemas.py: käyttää date-tyyppiä mutta "from datetime import date" puuttuu');
}
// 5. test_main.py: ei saa uudelleenmääritellä appia tai modeleita
if (fname === 'test_main.py') {
if (/^app\s*=\s*FastAPI\s*\(/m.test(code))
issues.push('ISSUE: test_main.py: luo oman FastAPI()-instanssin — pitäisi importata main.py:stä');
if (/^class\s+(Todo|User|Base)\b/m.test(code))
issues.push('ISSUE: test_main.py: uudelleenmäärittelee model-luokan — pitäisi importata models.py:stä');
}
// 6. main.py: FastAPI query param reitissä
if (fname === 'main.py') {
const routeMatches = code.matchAll(/@app\.\w+\(\s*["']([^"']+)["']/g);
for (const rm of routeMatches) {
if (rm[1].includes('?') && rm[1].includes('{'))
issues.push(`ISSUE: main.py: reitti "${rm[1]}" sisältää query parametrin — FastAPI:ssa query params tulevat funktion parametreina`);
}
}
}
// 7. pyproject.toml: poetry
if (files['pyproject.toml']) {
const toml = files['pyproject.toml'];
if (/\[tool\.poetry\]/.test(toml)) issues.push('ISSUE: pyproject.toml: sisältää [tool.poetry] — käytä vain [project] (PEP 621)');
if (!/fastapi/i.test(toml)) issues.push('ISSUE: pyproject.toml: puuttuu fastapi riippuvuuksista');
if (!/sqlalchemy/i.test(toml)) issues.push('ISSUE: pyproject.toml: puuttuu sqlalchemy riippuvuuksista');
}
// 8. Dockerfile: poetry
if (files['Dockerfile']) {
const df = files['Dockerfile'];
if (/poetry/i.test(df)) issues.push('ISSUE: Dockerfile: käyttää Poetryä — pitäisi käyttää uv:tä');
}
return issues;
}
// === Landing → App siirtymä ===
function startProject(task) {
function startProject(task, skipValidation ) {
if (!task || !task.trim()) return;
const trimmed = task.trim();
if (!skipValidation && (trimmed.length < 10 || trimmed.split(/\s+/).length < 2)) {
const input = document.getElementById('landing-input');
if (input) {
input.classList.add('shake');
input.setAttribute('placeholder', 'Kuvaile tarkemmin, esim. "REST API käyttäjähallinnalle"');
setTimeout(() => input.classList.remove('shake'), 500);
}
return;
}
document.getElementById('landing').classList.add('hidden');
document.getElementById('app').classList.add('active');
// Käynnistä pipeline suoraan termExec:n kautta
setTimeout(() => {
if (typeof termExec === 'function') termExec(`kpn project "${task.trim() }"`);
if (typeof termExec === 'function') termExec(`kpn project "${trimmed }"`);
}, 200);
}
@@ -177,7 +263,7 @@ import Settings from "../components/Settings.astro";
if (e.key === 'Enter') startProject(e.target.value);
});
document.querySelectorAll('.example-btn').forEach(btn => {
btn.addEventListener('click', () => startProject(btn.dataset.prompt));
btn.addEventListener('click', () => startProject(btn.dataset.prompt, true ));
});
// === Oppimispolku — renderöinti ===
@@ -251,20 +337,22 @@ pyproject.toml: project dependencies` },
prompt: `You are an expert Python developer. Write complete, production-ready code.
CRITICAL RULES:
1. Include ALL imports at the top of every file
2. Import from other project files: from models import User , SessionLocal
3. Pydantic schemas use different names than SQLAlchemy models: UserCreate, UserResponse (not User)
4. SQLAlchemy engine: create_engine(url, connect_args={"check_same_thread": False} )
5. SessionLocal: sessionmaker(autocommit=False, autoflush=False, bind=engine )
6. FastAPI dependencies: def get_db(): db = SessionLocal(); try: yield db; finally: db.close( )
7. Pydantic v2: use model_dump() not dict(), class Config: from_attributes = True
8. All CRUD endpoints: POST (201), GET list, GET by id, PUT, DELETE (204)
1. Include ALL imports at the top of every file — including stdlib (from datetime import date, etc.)
2. Import from other project files: from models import Todo , SessionLocal
3. NEVER use relative imports (from .models) — ALWAYS absolute: from models import ...
4. Pydantic schemas use different names than SQLAlchemy models: TodoCreate, TodoResponse (not Todo )
5. SQLAlchemy engine: create_engine(url, connect_args={"check_same_thread": False} )
6. SessionLocal: sessionmaker(autocommit=False, autoflush=False, bind=engine )
7. FastAPI dependencies: def get_db(): db = SessionLocal(); try: yield db; finally: db.close()
8. Pydantic v2: use model_dump() not dict(), class Config: from_attributes = True
9. All CRUD endpoints: POST (201), GET list, GET by id, PUT, DELETE (204)
NEVER:
- Add explanations or comments like "# Add routes here"
- Leave out any import (EVERY type you use must be imported)
- Use relative imports (from .models)
- Add explanations or comments
- Leave placeholder code or TODO comments
- Use Flask syntax (app.run) in FastAPI projects
- 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 Engineer', avatar: '/avatars/pesukarhu_notext.webp', model: 'qwen-coder', order: 3,
@@ -274,16 +362,23 @@ NEVER:
YOUR RESPONSIBILITIES:
1. Design normalized database schemas with proper column types and constraints
2. Define SQLAlchemy models with __tablename__, primary keys, indexes, and relationships
3. Set up engine, SessionLocal, and Base in the same file (models.py or database.py )
3. Set up engine, SessionLocal, and Base in the same file (models.py)
4. Use String(length) not bare String for SQLite compatibility
5. Add nullable=False for required fields, unique=True where appropriate
6. Use Column(Integer, primary_key=True, index=True) for IDs
7. SQLite: create_engine(url, connect_args={"check_same_thread": False})
ENUM HANDLING (IMPORTANT):
- For status fields, use Column(String(20)) with a default value — simpler and SQLite-compatible
- Do NOT define Python Enum classes — use plain strings instead
- Example: status = Column(String(20), default="pending")
ALWAYS INCLUDE:
- from sqlalchemy import create_engine, Column, Integer, String
- from sqlalchemy.ext.declarative import declarative_base
- from sqlalchemy.orm import sessionmaker
- DATABASE_URL, engine, SessionLocal, Base` },
- DATABASE_URL, engine, SessionLocal, Base
- create_engine with connect_args={"check_same_thread": False}` },
qa: { name: 'QA', avatar: '/avatars/susi_notext.webp', model: 'qwen-coder', order: 4,
temperature: 0.4, topK: 40, repeatPenalty: 1.15, maxTokens: 1024,
prompt: `You are a QA engineer responsible for code review and automated testing.
@@ -305,29 +400,30 @@ WHEN REVIEWING:
- Be specific and actionable, not vague
WHEN WRITING TESTS:
- pytest as the test framework
- FastAPI TestClient for API endpoint testing
- SQLAlchemy in-memory SQLite for test database isolation
- ALWAYS import app from main.py: from main import app, get_db
- ALWAYS import Base from models.py: from models import Base
- NEVER redefine the app, models, or routes in the test file
- Use file-based SQLite for test isolation: sqlite:///./test.db
- Override the get_db dependency to use test database
- Use TestClient from fastapi.testclient
- Test all CRUD: create (201), list (200), get by id (200/404), update (200), delete (204)
- ALWAYS: from fastapi.testclient import TestClient ` },
- Each test should create its own data, not depend on other tests ` },
tester: { name: 'DevOps', avatar: '/avatars/laiskiainen_notext.webp', model: 'qwen-coder', order: 5,
temperature: 0.3, topK: 40, repeatPenalty: 1.1, maxTokens: 1024,
prompt: `You are a DevOps engineer specializing in containerization and deployment.
YOUR RESPONSIBILITIES:
1. Write production-ready Dockerfiles
2. Use multi-stage builds when appropriate
3. Follow security best practices (non-root user, minimal base image)
4. Configure health checks and proper signal handling
DOCKERFILE RULES:
- Use python:3.12-slim as base
- Install uv: COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
- Copy pyproject.toml first, then uv sync, then copy source
- Expose appropriate port s
- Use uv run for CMD
- ENV UV_CACHE_DIR=/tmp/uv-cache (MUST set before uv sync)
- Copy pyproject.toml first, then RUN uv sync, then COPY source file s
- Set USER AFTER installing dependencies (uv sync needs write access)
- RUN useradd -m appuser && chown -R appuser:appuser /app /tmp/uv-cache
- NEVER use pip, poetry, or requirements.txt
- Expose port 8000
- CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Write ONLY the requested files , no explanations.` },
Write ONLY the Docker file, no explanations.` },
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.
@@ -1023,6 +1119,171 @@ OUTPUT FORMAT:
return crewFiles;
}
// === Template Pipeline — rakennuspalaset ===
const SPEC_SYSTEM = `You are a software architect. Given a project description, output a JSON specification.
Output ONLY valid JSON, no explanations. Follow this exact schema:
{
"project_name": "short-name",
"description": "One sentence",
"entities": [
{
"name": "Todo",
"table_name": "todos",
"fields": [
{"name": "title", "sa_type": "String(255)", "py_type": "str", "nullable": false, "default": null},
{"name": "description", "sa_type": "Text", "py_type": "str | None", "nullable": true, "default": null},
{"name": "status", "sa_type": "String(20)", "py_type": "str", "nullable": false, "default": "pending"}
]
}
],
"extra_imports": ["from datetime import date"]
}
RULES:
- sa_type: SQLAlchemy column type (String(N), Text, Integer, Date, DateTime, Boolean, Float)
- py_type: Python type hint (str, int, float, bool, date, datetime, str | None, etc.)
- Do NOT use Enum — use String(20) with a default value for status fields
- nullable: true = optional field
- default: null = no default, otherwise a string/number value
- extra_imports: stdlib imports needed in schemas.py (e.g. "from datetime import date")
- entity name: PascalCase singular, table_name: snake_case plural
- Keep it simple: 1-3 entities, 3-7 fields each`;
function extractJson(text) {
const m = text.match(/```(?:json)?\s*\n([\s\S]*?)```/);
if (m) text = m[1].trim();
let depth = 0, start = null;
for (let i = 0; i < text.length; i++) {
if (text[i] === '{') { if (depth === 0) start = i; depth++; }
else if (text[i] === '}') { depth--; if (depth === 0 && start !== null) { try { return JSON.parse(text.slice(start, i+1)); } catch(e) { continue; } } }
}
return null;
}
function tmplModels(spec) {
const saTypes = new Set(['Integer']);
for (const e of spec.entities) for (const f of e.fields) saTypes.add(f.sa_type.match(/^(\w+)/)[1]);
const imports = [...saTypes].sort().join(', ');
let code = `from sqlalchemy import create_engine, Column, ${imports}\n`;
code += `from sqlalchemy.ext.declarative import declarative_base\nfrom sqlalchemy.orm import sessionmaker\n\n`;
code += `DATABASE_URL = "sqlite:///./app.db"\n`;
code += `engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})\n`;
code += `SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)\nBase = declarative_base()\n\n`;
for (const e of spec.entities) {
code += `class ${e.name}(Base):\n __tablename__ = "${e.table_name}"\n id = Column(Integer, primary_key=True, index=True)\n`;
for (const f of e.fields) {
let parts = [`Column(${f.sa_type}`];
if (!f.nullable) parts.push('nullable=False');
if (f.default !== null && f.default !== undefined) parts.push(typeof f.default === 'string' ? `default="${f.default}"` : `default=${f.default}`);
code += ` ${f.name} = ${parts.join(', ')})\n`;
}
code += '\n';
}
code += 'Base.metadata.create_all(bind=engine)\n';
return code;
}
function tmplSchemas(spec) {
let code = 'from pydantic import BaseModel\n';
for (const imp of (spec.extra_imports || [])) code += imp + '\n';
code += '\n';
for (const e of spec.entities) {
code += `class ${e.name}Create(BaseModel):\n`;
for (const f of e.fields) {
if (f.default !== null && f.default !== undefined) code += ` ${f.name}: ${f.py_type} = ${typeof f.default === 'string' ? '"'+f.default+'"' : f.default}\n`;
else if (f.nullable && f.py_type.includes('None')) code += ` ${f.name}: ${f.py_type} = None\n`;
else code += ` ${f.name}: ${f.py_type}\n`;
}
code += `\nclass ${e.name}Response(${e.name}Create):\n id: int\n\n class Config:\n from_attributes = True\n\n`;
}
return code;
}
function tmplMain(spec) {
const modelNames = spec.entities.map(e => e.name).join(', ');
const createNames = spec.entities.map(e => e.name+'Create').join(', ');
const responseNames = spec.entities.map(e => e.name+'Response').join(', ');
let code = `from fastapi import FastAPI, Depends, HTTPException\nfrom sqlalchemy.orm import Session\n`;
code += `from models import Base, engine, SessionLocal, ${modelNames}\nfrom schemas import ${createNames}, ${responseNames}\n\n`;
code += `app = FastAPI()\n\ndef get_db():\n db = SessionLocal()\n try:\n yield db\n finally:\n db.close()\n\n`;
for (const e of spec.entities) {
const lo = e.name.toLowerCase(), tb = e.table_name;
code += `@app.post("/${tb}/", response_model=${e.name}Response, status_code=201)\n`;
code += `def create_${lo}(item: ${e.name}Create, db: Session = Depends(get_db)):\n`;
code += ` db_item = ${e.name}(**item.model_dump())\n db.add(db_item)\n db.commit()\n db.refresh(db_item)\n return db_item\n\n`;
code += `@app.get("/${tb}/", response_model=list[${e.name}Response])\n`;
code += `def list_${lo}s(db: Session = Depends(get_db)):\n return db.query(${e.name}).all()\n\n`;
code += `@app.get("/${tb}/{item_id}", response_model=${e.name}Response)\n`;
code += `def get_${lo}(item_id: int, db: Session = Depends(get_db)):\n`;
code += ` item = db.query(${e.name}).filter(${e.name}.id == item_id).first()\n`;
code += ` if not item:\n raise HTTPException(status_code=404, detail="${e.name} not found")\n return item\n\n`;
code += `@app.put("/${tb}/{item_id}", response_model=${e.name}Response)\n`;
code += `def update_${lo}(item_id: int, item: ${e.name}Create, db: Session = Depends(get_db)):\n`;
code += ` db_item = db.query(${e.name}).filter(${e.name}.id == item_id).first()\n`;
code += ` if not db_item:\n raise HTTPException(status_code=404, detail="${e.name} not found")\n`;
code += ` for key, value in item.model_dump().items():\n setattr(db_item, key, value)\n db.commit()\n db.refresh(db_item)\n return db_item\n\n`;
code += `@app.delete("/${tb}/{item_id}", status_code=204)\n`;
code += `def delete_${lo}(item_id: int, db: Session = Depends(get_db)):\n`;
code += ` db_item = db.query(${e.name}).filter(${e.name}.id == item_id).first()\n`;
code += ` if not db_item:\n raise HTTPException(status_code=404, detail="${e.name} not found")\n db.delete(db_item)\n db.commit()\n\n`;
}
return code;
}
function tmplTests(spec) {
let code = `from fastapi.testclient import TestClient\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n`;
code += `from main import app, get_db\nfrom models import Base\n\n`;
code += `TEST_DB = "sqlite:///./test.db"\ntest_engine = create_engine(TEST_DB, connect_args={"check_same_thread": False})\n`;
code += `TestSession = sessionmaker(autocommit=False, autoflush=False, bind=test_engine)\nBase.metadata.create_all(bind=test_engine)\n\n`;
code += `def override_get_db():\n db = TestSession()\n try:\n yield db\n finally:\n db.close()\n\n`;
code += `app.dependency_overrides[get_db] = override_get_db\nclient = TestClient(app)\n\n`;
for (const e of spec.entities) {
const lo = e.name.toLowerCase(), tb = e.table_name;
const testData = {};
for (const f of e.fields) {
if (f.default !== null && f.default !== undefined) { testData[f.name] = f.default; continue; }
if (f.py_type.includes('str')) testData[f.name] = `Test ${f.name}`;
else if (f.py_type.includes('int')) testData[f.name] = 1;
else if (f.py_type.includes('float')) testData[f.name] = 1.0;
else if (f.py_type.includes('bool')) testData[f.name] = true;
else if (f.py_type.includes('date')) testData[f.name] = '2024-01-15';
}
const td = JSON.stringify(testData);
const firstStr = e.fields.find(f => f.py_type.includes('str') && f.name !== 'status');
const updateData = {...testData};
if (firstStr) updateData[firstStr.name] = `Updated ${firstStr.name}`;
const ud = JSON.stringify(updateData);
code += `def test_create_${lo}():\n response = client.post('/${tb}/', json=${td})\n assert response.status_code == 201\n assert 'id' in response.json()\n\n`;
code += `def test_list_${lo}s():\n client.post('/${tb}/', json=${td})\n response = client.get('/${tb}/')\n assert response.status_code == 200\n assert len(response.json()) >= 1\n\n`;
code += `def test_get_${lo}_by_id():\n created = client.post('/${tb}/', json=${td}).json()\n item_id = created['id']\n response = client.get(f'/${tb}/{item_id}')\n assert response.status_code == 200\n assert response.json()['id'] == item_id\n\n`;
code += `def test_get_${lo}_not_found():\n response = client.get('/${tb}/99999')\n assert response.status_code == 404\n\n`;
code += `def test_update_${lo}():\n created = client.post('/${tb}/', json=${td}).json()\n item_id = created['id']\n response = client.put(f'/${tb}/{item_id}', json=${ud})\n assert response.status_code == 200\n\n`;
code += `def test_delete_${lo}():\n created = client.post('/${tb}/', json=${td}).json()\n item_id = created['id']\n response = client.delete(f'/${tb}/{item_id}')\n assert response.status_code == 204\n response = client.get(f'/${tb}/{item_id}')\n assert response.status_code == 404\n\n`;
}
return code;
}
function tmplPyproject(spec) {
const name = (spec.project_name || 'app').toLowerCase().replace(/\s+/g, '-');
return `[project]\nname = "${name}"\nversion = "0.1.0"\nrequires-python = ">=3.11"\ndependencies = [\n "fastapi",\n "uvicorn[standard]",\n "sqlalchemy",\n "pytest",\n "httpx",\n]\n`;
}
function tmplDockerfile() {
return `FROM python:3.12-slim\nCOPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv\nENV UV_CACHE_DIR=/tmp/uv-cache\nWORKDIR /app\nCOPY pyproject.toml .\nRUN uv sync\nCOPY *.py .\nRUN useradd -m appuser && chown -R appuser:appuser /app /tmp/uv-cache\nUSER appuser\nEXPOSE 8000\nCMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]\n`;
}
function tmplGenerate(spec) {
return {
'models.py': tmplModels(spec),
'schemas.py': tmplSchemas(spec),
'main.py': tmplMain(spec),
'test_main.py': tmplTests(spec),
'pyproject.toml': tmplPyproject(spec),
'Dockerfile': tmplDockerfile(),
};
}
async function kpnProject(task) {
pipelineAbort = new AbortController();
const promptLog = [];
@@ -1040,188 +1301,62 @@ OUTPUT FORMAT:
promptLog.push({ step: 0, agentKey: 'client', agentName: cli.name, model: cli.model, label: 'requirements', systemPrompt: cli.prompt || '', userPrompt: task, response: brief });
termLog(` <span style="color:#8b949e">Requirements ready → Manager</span>`);
// Valitaan mallipohja automaattisesti briefin perusteella
const template = selectTemplate(brief );
// === Vaihe 2: JSON-speksi vaatimuksista ===
termLog(`\n<span style="color:#d29922;font-weight:bold">[1] ${esc(mgr.name)}</span> — JSON-speksi` );
highlightAgent('manager');
explainStep('Arkkitehtuuri', `${mgr.name} analysoi vaatimukset ja tuottaa JSON-speksin: entiteetit, kentät, tyypit.`);
// Tiedostolista: mallipohjasta tai managerin dynaamisesta suunnitelmasta
let fileOrder = [] ;
let fileDefs = {} ;
const specRaw = await kpnRun(mgr.model, `${brief}\n\nOutput a JSON spec for this project.`, false, { ...mgr, prompt: SPEC_SYSTEM });
const spec = specRaw ? extractJson(specRaw) : null ;
promptLog.push({ step: 1, agentKey: 'manager', agentName: mgr.name, model: mgr.model, label: 'JSON-speksi', systemPrompt: SPEC_SYSTEM, userPrompt: brief, response: specRaw || '' }) ;
if (template ) {
// Mallipohja löytyi — käytetään sen rakennetta
fileOrder = template.order;
fileDefs = template.files ;
explainStep('Template', `Detected "${template.name}" — ${fileOrder.length} files: ${fileOrder.join(', ')}.`);
} else {
// Vapaa tila — Manageri päättää tiedostorakenteen
termLog(`\n<span style="color:#d29922;font-weight:bold">[1] ${esc(mgr.name)}</span> — File structure`);
highlightAgent('manager');
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(' ✗ Planning failed', '#f85149'); return; }
// Parsitaan managerin tuottama tiedostolista
for (const line of plan.split('\n')) {
const m = line.match(/^\s*[-*]?\s*(\S+\.\w+)\s*[:\-– ]\s*(.+)/);
if (m) {
const fname = m[1].replace(/^`|`$/g, '');
fileOrder.push(fname);
fileDefs[fname] = { description: m[2].trim(), instructions: m[2].trim() };
}
}
if (fileOrder.length === 0) {
termLog(' ✗ Manager produced no file list', '#f85149');
return;
}
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 });
if (!spec || !spec.entities || spec.entities.length === 0 ) {
termLog(' ✗ JSON-speksi epäonnistui — fallback vapaaseen generointiin', '#f85149');
// TODO: fallback vanhaan pipeline-logiikkaan
return ;
}
const files = {} ;
termLog(` <span style="color:#3fb950">✓ ${spec.entities.length} entiteettiä: ${spec.entities.map(e => e.name).join(', ')}</span>`) ;
// === Vaihe 3: Koodigenerointi templateista ===
const files = tmplGenerate(spec);
const fileOrder = Object.keys(files);
const agentMap = { 'models.py': 'data', 'schemas.py': 'coder', 'main.py': 'coder', 'test_main.py': 'qa', 'pyproject.toml': 'coder', 'Dockerfile': 'tester' };
const agentNames = { data: 'Data Engineer', coder: 'Coder', qa: 'QA', tester: 'DevOps' };
for (let i = 0; i < fileOrder.length; i++) {
const fileName = fileOrder[i];
const fileDef = fileDefs[fileName] ;
if (!fileDef) continue ;
const agentKey = agentMap[fileName] || 'coder' ;
const agentName = agentNames[agentKey] || 'Coder' ;
const agent = agents[agentKey] || cdr;
const step = i + 1 ;
// Valitaan oikea agentti tiedostotyypin mukaan
const isDbFile = fileName === 'models.py' || fileName === 'database.py' || fileName === 'etl.py' ;
const dataAgent = agents.data || Object.values(agents)[3];
const fileAgent = isDbFile && dataAgent ? dataAgent : cdr;
const fileAgentKey = isDbFile && dataAgent ? 'data' : 'coder';
termLog(`\n<span style="color:#3fb950;font-weight:bold">[${i+2}/${fileOrder.length+1}] ${esc(agent.name)}</span> — ${esc(fileName)}`) ;
highlightAgent(agentKey);
explainStep(fileName, `${agent.name} generoi ${fileName} rakennuspalasista (template pipeline).`) ;
termLog(`\n<span style="color:#3fb950;font-weight:bold">[${step}/${fileOrder.length}] ${esc(fileAgent.name)}</span> — ${esc(fileName)}`);
highlightAgent(fileAgentKey );
// Pieni viive UX:ää varten — näyttää agentin "työskentelevän"
await new Promise(r => setTimeout(r, 300) );
explainStep(fileName, fileDef.instructions || fileDef.description );
// Rakennetaan prompti
let prompt = '';
if (fileAgent.prompt) prompt += fileAgent.prompt + '\n\n';
// Esimerkki (vain mallipohjatilassa)
if (fileDef.example) {
prompt += `EXAMPLE of ${fileName} (for a different project, adapt to this one):\n`;
prompt += '```\n' + fileDef.example + '\n```\n\n';
}
// Aiemmin generoidut tiedostot (konteksti)
const prevFiles = Object.entries(files);
if (prevFiles.length > 0) {
prompt += 'Already written files in THIS project:\n';
for (const [name, code] of prevFiles) {
prompt += `--- ${name} ---\n${code}\n\n`;
}
}
// Asiakkaan vaatimusmäärittely
prompt += `PROJECT REQUIREMENTS (from product owner):\n${brief}\n\n`;
// Tehtävä
prompt += `NOW write "${fileName}" for THIS project: ${task}\n`;
if (fileDef.instructions) prompt += fileDef.instructions + '\n';
prompt += 'Adapt to the project requirements. Import from already written files. Write ONLY the code, no explanations.';
const code = await kpnRun(fileAgent.model, prompt, false, fileAgent);
if (!code) {
termLog(` ✗ Keskeytyi (${fileName})`, '#f85149');
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 });
promptLog.push({ step: promptLog.length, agentKey, agentName: agent.name, model: 'template', label: fileName, systemPrompt: '(template pipeline — ei LLM-promptia)', userPrompt: `Generated from spec: ${JSON.stringify(spec.entities.map(e => e.name))}`, response: files[fileName] } );
termLog(` <span style="color:#3fb950">✓ ${files[fileName].split('\\n').length} riviä</span>`);
}
const allCode = Object.entries(files).map(([n,c]) => `--- ${n} ---\n${c}`).join('\n\n') ;
let stepN = fileOrder.length + 1;
let stepN = fileOrder.length + 2 ;
// QA-katselmointi → Coder-korjaus -luuppi (max N kierrosta)
// === Vaihe 4: Mekaaninen QA-validointi ===
const qaAgent = agents.qa || Object.values(agents)[4];
const MAX_REVIEW_ROUNDS = pipelineConfig.maxReviewRounds ;
termLog(`\n<span style="color:#d2a8ff;font-weight:bold">[${stepN}] ${esc(qaAgent.name)}</span> — validointi`) ;
highlightAgent('qa');
explainStep('Validointi', `${qaAgent.name} ajaa mekaanisen koodivalidoinnin.`);
for (let round = 0; round < MAX_REVIEW_ROUNDS; round++) {
const currentCode = Object.entries(files).map(([n,c]) => `--- ${n} ---\n${c}`).join('\n\n');
// QA katselmoi
termLog(`\n<span style="color:#d2a8ff;font-weight:bold">[${stepN}] ${esc(qaAgent.name)}</span> — katselmointi${round > 0 ? ' (kierros '+(round+1)+')' : ''}`);
highlightAgent('qa' );
if (round === 0) explainStep('Katselmointi', `${qaAgent.name} analysoi koodin: importit, nimeämiset, virheenkäsittely, tietoturva.`);
else explainStep('Uudelleentarkistus', `${qaAgent.name} tarkistaa korjaukset.`);
const reviewPrompt = (qaAgent.prompt ? qaAgent.prompt+'\n\n' : '') + `Review this project code for issues. If everything is correct, respond with "LGTM". Otherwise list issues as "ISSUE: filename.py: description".\n\n${currentCode}`;
const review = await kpnRun(qaAgent.model, reviewPrompt, false, qaAgent);
promptLog.push({ step: promptLog.length, agentKey: 'qa', agentName: qaAgent.name, model: qaAgent.model, label: 'review' + (round > 0 ? '_r'+(round+1) : ''), systemPrompt: qaAgent.prompt || '', userPrompt: reviewPrompt, response: review || '' });
stepN++;
// LGTM → ei korjauksia tarvita
if (!review || review.toLowerCase().includes('lgtm')) {
termLog(` <span style="color:#3fb950">✓ ${esc(qaAgent.name)}: LGTM</span>`);
break;
}
// Korjaukset → Coder
termLog(`\n<span style="color:#d29922;font-weight:bold">[${stepN}] ${esc(cdr.name)}</span> — korjaukset${round > 0 ? ' (kierros '+(round+1)+')' : ''}`);
highlightAgent('coder');
explainStep('Korjaus', `${qaAgent.name} löysi ongelmia. ${cdr.name} saa palautteen ja korjaa.`);
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 });
const fixedParts = fixedCode.split(/^---\s*(\S+)\s*---$/m);
for (let j = 1; j < fixedParts.length; j += 2) {
const fname = fixedParts[j].trim();
const fcode = (fixedParts[j+1] || '').trim();
if (fname && fcode && files[fname] !== undefined) {
files[fname] = fcode;
}
}
}
stepN++;
} // for review round
// Päivitetään allCode korjausten jälkeen
const updatedCode = Object.entries(files).map(([n,c]) => `--- ${n} ---\n${c}`).join('\n\n');
// QA: testit (saa katselmoidut ja korjatut tiedostot)
if (qaAgent) {
termLog(`\n<span style="color:#d2a8ff;font-weight:bold">[${stepN}] ${esc(qaAgent.name)}</span> — testit`);
highlightAgent('qa');
explainStep('Testit', `${qaAgent.name} kirjoittaa pytest-testit katselmoidulle koodille.`);
const qaTestPrompt = (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, qaTestPrompt, false, qaAgent);
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: qaTestPrompt, response: tests });
}
stepN++;
}
// DevOps: Dockerfile + deployment (saa kaikki tiedostot mukaan lukien testit)
const tst = agents.tester || Object.values(agents)[5];
const allFilesNow = Object.keys(files).join(', ');
termLog(`\n<span style="color:var(--accent);font-weight:bold">[${stepN}] ${esc(tst.name)}</span> — Dockerfile`);
highlightAgent('tester');
explainStep('Dockerfile', `${tst.name} generoi Docker-kontin kaikista ${Object.keys(files).length} tiedostosta: ${allFilesNow}`);
const dockerPrompt = (tst.prompt ? tst.prompt+'\n\n' : '') +
`Write a Dockerfile for this Python FastAPI project.\n\n` +
`Project files: ${allFilesNow}\n\n` +
`Requirements:\n` +
`- Use python:3.12-slim as base\n` +
`- Install uv: COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv\n` +
`- Copy pyproject.toml first, then uv sync, then copy source\n` +
`- Expose port 8000\n` +
`- 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;
promptLog.push({ step: promptLog.length, agentKey: 'tester', agentName: tst.name, model: tst.model, label: 'Dockerfile', systemPrompt: tst.prompt || '', userPrompt: dockerPrompt, response: dockerfile });
const mechIssues = validateProjectCode(files);
if (mechIssues.length > 0) {
termLog(` <span style="color:#d29922">⚠ ${mechIssues.length} ongelmaa (template-bugeja — korjattava):</span>`);
for (const issue of mechIssues) termLog(` <span style="color:#d29922">${esc(issue)}</span>`);
} else {
termLog(` <span style="color:#3fb950">✓ Kaikki tiedostot validoitu — 0 ongelmaa</span>` );
}
promptLog.push({ step: promptLog.length, agentKey: 'qa', agentName: qaAgent.name, model: 'mekaaninen', label: 'validointi', systemPrompt: '(mekaaninen validointi — ei LLM:ää)', userPrompt: 'validateProjectCode(files)', response: mechIssues.length === 0 ? 'OK — 0 issues' : mechIssues.join('\n') });
stepN++;
// Tarkkailija: yhteenveto + raportti + arvosana
@@ -1380,7 +1515,8 @@ OUTPUT FORMAT:
const lv = new DataView(local.buffer);
lv.setUint32(0, 0x04034b50, true); // signature
lv.setUint16(4, 20, true); // version needed
lv.setUint16(8 , 8 , true); // UTF-8 flag
lv.setUint16(6 , 0x0800 , true); // UTF-8 flag (bit 11)
lv.setUint16(8, 0, true); // compression: stored
lv.setUint32(14, crc, true); // CRC-32
lv.setUint32(18, dataBytes.length, true); // compressed size
lv.setUint32(22, dataBytes.length, true); // uncompressed size
@@ -1395,7 +1531,8 @@ OUTPUT FORMAT:
cv.setUint32(0, 0x02014b50, true); // signature
cv.setUint16(4, 20, true); // version made by
cv.setUint16(6, 20, true); // version needed
cv.setUint16(8, 8 , true); // UTF-8 flag
cv.setUint16(8, 0x0800 , true); // UTF-8 flag (bit 11)
cv.setUint16(10, 0, true); // compression: stored
cv.setUint32(16, crc, true);
cv.setUint32(20, dataBytes.length, true);
cv.setUint32(24, dataBytes.length, true);