Template-pohjainen projektipipeline + opettavat selitykset

Uusi lähestymistapa: sen sijaan, että malli keksii rakenteen tyhjästä,
sille annetaan mallipohja (template) joka sisältää:
- Tiedostojärjestys (models → schemas → main → pyproject.toml)
- Esimerkkikoodi jokaiselle tiedostolle (few-shot)
- Yksityiskohtaiset ohjeet (importit, nimeämiskäytännöt, patternit)

Jokainen vaihe selitetään terminaalissa:
💡 models.py — "Define the SQLAlchemy model. Always include engine
   with check_same_thread=False for SQLite..."
💡 Koodikatselmointi — "Testaaja tarkistaa importit, nimeämiset..."
💡 Tulos — "Aja: uv run uvicorn main:app --reload"

templates/fastapi-crud.json sisältää täydellisen esimerkkiprojektin
jota malli adaptoi käyttäjän kuvaukseen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jaakko Vanhala
2026-04-09 22:32:03 +03:00
parent d85cab4bc0
commit 1216e016c2
2 changed files with 101 additions and 42 deletions

View File

@@ -0,0 +1,27 @@
{
"name": "FastAPI CRUD",
"description": "REST API with SQLite database",
"files": {
"models.py": {
"description": "SQLAlchemy models, engine, and session",
"example": "from sqlalchemy import create_engine, Column, Integer, String\nfrom sqlalchemy.ext.declarative import declarative_base\nfrom sqlalchemy.orm import sessionmaker\n\nDATABASE_URL = \"sqlite:///./app.db\"\nengine = create_engine(DATABASE_URL, connect_args={\"check_same_thread\": False})\nSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)\nBase = declarative_base()\n\nclass Item(Base):\n __tablename__ = \"items\"\n id = Column(Integer, primary_key=True, index=True)\n name = Column(String(100), nullable=False)\n description = Column(String(500))",
"instructions": "Define the SQLAlchemy model based on the project description. Always include:\n- engine with check_same_thread=False for SQLite\n- SessionLocal with autocommit=False\n- Base = declarative_base()\n- Model class with __tablename__, primary key, and fields"
},
"schemas.py": {
"description": "Pydantic request/response schemas",
"example": "from pydantic import BaseModel\n\nclass ItemCreate(BaseModel):\n name: str\n description: str | None = None\n\nclass ItemResponse(ItemCreate):\n id: int\n\n class Config:\n from_attributes = True",
"instructions": "Create Pydantic schemas that match the SQLAlchemy model:\n- Create schema: fields without id (user provides these)\n- Response schema: inherits from Create, adds id\n- Add class Config with from_attributes = True (required for SQLAlchemy ORM)"
},
"main.py": {
"description": "FastAPI app with CRUD endpoints",
"example": "from fastapi import FastAPI, Depends, HTTPException\nfrom sqlalchemy.orm import Session\nfrom models import Base, engine, SessionLocal, Item\nfrom schemas import ItemCreate, ItemResponse\n\nBase.metadata.create_all(bind=engine)\napp = FastAPI()\n\ndef get_db():\n db = SessionLocal()\n try:\n yield db\n finally:\n db.close()\n\n@app.post(\"/items/\", response_model=ItemResponse, status_code=201)\ndef create_item(item: ItemCreate, db: Session = Depends(get_db)):\n db_item = Item(**item.model_dump())\n db.add(db_item)\n db.commit()\n db.refresh(db_item)\n return db_item\n\n@app.get(\"/items/\", response_model=list[ItemResponse])\ndef list_items(db: Session = Depends(get_db)):\n return db.query(Item).all()\n\n@app.get(\"/items/{item_id}\", response_model=ItemResponse)\ndef get_item(item_id: int, db: Session = Depends(get_db)):\n item = db.query(Item).filter(Item.id == item_id).first()\n if not item:\n raise HTTPException(status_code=404, detail=\"Not found\")\n return item\n\n@app.put(\"/items/{item_id}\", response_model=ItemResponse)\ndef update_item(item_id: int, item: ItemCreate, db: Session = Depends(get_db)):\n db_item = db.query(Item).filter(Item.id == item_id).first()\n if not db_item:\n raise HTTPException(status_code=404, detail=\"Not found\")\n 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@app.delete(\"/items/{item_id}\", status_code=204)\ndef delete_item(item_id: int, db: Session = Depends(get_db)):\n db_item = db.query(Item).filter(Item.id == item_id).first()\n if not db_item:\n raise HTTPException(status_code=404, detail=\"Not found\")\n db.delete(db_item)\n db.commit()",
"instructions": "Create the FastAPI app with all CRUD endpoints:\n- Import from models.py and schemas.py (use exact class names)\n- create_all(bind=engine) at module level\n- get_db dependency with yield pattern\n- POST (201), GET list, GET by id, PUT, DELETE (204)\n- Use response_model for type safety\n- Use model_dump() not dict() (Pydantic v2)"
},
"pyproject.toml": {
"description": "Project dependencies",
"example": "[project]\nname = \"myapp\"\nversion = \"0.1.0\"\nrequires-python = \">=3.11\"\ndependencies = [\n \"fastapi\",\n \"uvicorn[standard]\",\n \"sqlalchemy\",\n]\n\n[project.scripts]\ndev = \"uvicorn main:app --reload\"",
"instructions": "List the exact dependencies needed. Use [project.scripts] for run commands."
}
},
"order": ["models.py", "schemas.py", "main.py", "pyproject.toml"]
}

View File

@@ -566,66 +566,98 @@ Provide a brief risk assessment with severity (low/medium/high/critical).` },
termPanel?.addEventListener('click', () => termInput?.focus());
document.addEventListener('click', (e) => { if (!termInput?.contains(e.target) && !dropdown?.contains(e.target)) hideDD(); });
// === Project pipeline ===
// === Template-pohjainen projektipipeline ===
let templates = {};
// Ladataan mallipohjat
(async () => {
try {
const res = await fetch('/templates/fastapi-crud.json');
if (res.ok) { const t = await res.json(); templates[t.name] = t; }
} catch(e) {}
})();
function explainStep(title, explanation) {
termLog(`\n <span style="color:#a371f7;font-size:12px">💡 ${esc(title)}</span>`);
termLog(` <span style="color:#8b949e;font-size:12px">${esc(explanation)}</span>`);
}
async function kpnProject(task) {
const mgr = agents.manager || Object.values(agents)[0];
const cdr = agents.coder || Object.values(agents)[1];
const tst = agents.tester || Object.values(agents)[2];
termLog(`<span style="color:var(--purple);font-weight:bold">━━━ Projekti käynnistyy ━━━</span>`);
termLog(`\n<span style="color:#d29922;font-weight:bold">[1] ${esc(mgr.name)}</span> — suunnittelu`);
const mgrPrompt = (mgr.prompt ? mgr.prompt + '\n\n' : '') + `List the source files needed for this project. One file per line, format:\nfilename.py: what this file contains\n\nRules:\n- Max 4 files\n- Only .py, .toml, .json, .html files\n- No directories, just filenames\n- Dependencies first (models.py before main.py)\n- Use pyproject.toml for deps\n\nProject: ${task}`;
const plan = await kpnRun(mgr.model, mgrPrompt);
if (!plan) { termLog(' ✗ Keskeytyi', '#f85149'); return; }
const fileList = plan.split('\n').map(l => l.trim().replace(/^[\d\.\-\*\s]+/,'').replace(/\*+/g,'').replace(/`/g,'')).map(l => {
if (l.includes(':')) { const [n,...d] = l.split(':'); return { name: n.trim(), desc: d.join(':').trim() }; }
return { name: l.trim(), desc: '' };
}).filter(f => f.name.length > 0 && f.name.length < 40 && !f.name.includes('/') && !f.name.includes(' ') && /\.\w{1,5}$/.test(f.name));
if (!fileList.length) {
termLog(' Ei tiedostojakoa — generoidaan yhtenä', '#8b949e');
await kpnRun(cdr.model, `${cdr.prompt ? cdr.prompt+'\n\n' : ''}Project: ${task}\n\nWrite all the code.`);
termLog(`\n<span style="color:var(--purple);font-weight:bold">━━━ Valmis ━━━</span>`);
// Etsitään sopivin mallipohja
const template = Object.values(templates)[0]; // Toistaiseksi vain FastAPI CRUD
if (!template) {
termLog(' ✗ Mallipohjia ei ladattu', '#f85149');
return;
}
termLog(` <span style="color:#8b949e">${fileList.length} tiedostoa: ${fileList.map(f=>f.name).join(', ')}</span>`);
termLog(`<span style="color:var(--purple);font-weight:bold">━━━ ${esc(template.name)} — ${esc(task)} ━━━</span>`);
explainStep('Mallipohja', `Käytetään "${template.name}" -mallipohjaa jossa ${template.order.length} tiedostoa: ${template.order.join(', ')}. Jokainen tiedosto generoidaan järjestyksessä, ja aiemmat tiedostot annetaan kontekstina seuraavalle.`);
const files = {};
for (let i = 0; i < fileList.length; i++) {
const f = fileList[i];
termLog(`\n<span style="color:#3fb950;font-weight:bold">[${i+2}] ${esc(cdr.name)}</span> — ${esc(f.name)}`);
let ctx = '';
const prev = Object.entries(files);
if (prev.length) ctx = 'Already written:\n' + prev.map(([n,c]) => `--- ${n} ---\n${c}`).join('\n\n') + '\n\n';
let extra = '';
if (f.name === 'pyproject.toml') extra = '\nUse format: [project]\\nname="proj"\\nversion="0.1.0"\\nrequires-python=">=3.11"\\ndependencies=["fastapi","uvicorn"]';
// Varmistetaan oikeat importit: listaa aiempien tiedostojen exportit
let importHint = '';
const prevNames = Object.keys(files);
if (prevNames.length > 0) {
importHint = '\n\nIMPORTANT: Import from already written files. ';
importHint += prevNames.map(n => `from ${n.replace('.py','')} import ...`).join(', ');
importHint += '. Use correct imports for all dependencies.';
importHint += '\nUse separate names for Pydantic schemas (e.g. UserCreate, UserResponse) vs SQLAlchemy models (e.g. User).';
for (let i = 0; i < template.order.length; i++) {
const fileName = template.order[i];
const fileDef = template.files[fileName];
if (!fileDef) continue;
const step = i + 1;
termLog(`\n<span style="color:#3fb950;font-weight:bold">[${step}/${template.order.length}] ${esc(cdr.name)}</span> — ${esc(fileName)}`);
// Opettava selitys: miksi tämä tiedosto, mitä se sisältää
explainStep(fileName, fileDef.instructions);
// Rakennetaan prompti: esimerkki + konteksti + ohje
let prompt = '';
// Agentin system prompt
if (cdr.prompt) prompt += cdr.prompt + '\n\n';
// Esimerkki (few-shot)
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`;
}
const coderPrompt = (cdr.prompt ? cdr.prompt+'\n\n' : '') + `${ctx}Project: ${task}\nWrite ONLY "${f.name}"${f.desc ? ': '+f.desc : ''}.${extra}${importHint}\nInclude all necessary imports. Write complete, working code.`;
const code = await kpnRun(cdr.model, coderPrompt);
if (!code) { termLog(` ✗ Keskeytyi (${f.name})`, '#f85149'); return; }
files[f.name] = code;
}
// Tehtävä
prompt += `NOW write "${fileName}" for THIS project: ${task}\n`;
prompt += fileDef.instructions + '\n';
prompt += 'Adapt the example to match the project description. Import from already written files. Write ONLY the code, no explanations.';
const code = await kpnRun(cdr.model, prompt);
if (!code) {
termLog(` ✗ Keskeytyi (${fileName})`, '#f85149');
return;
}
files[fileName] = code;
}
// Review
const allCode = Object.entries(files).map(([n,c]) => `--- ${n} ---\n${c}`).join('\n\n');
termLog(`\n<span style="color:var(--accent);font-weight:bold">[${fileList.length+2}] ${esc(tst.name)}</span> — review`);
const tstPrompt = (tst.prompt ? tst.prompt+'\n\n' : '') + `Review briefly. Say LGTM if ok.\n${allCode}`;
termLog(`\n<span style="color:var(--accent);font-weight:bold">[${template.order.length + 1}] ${esc(tst.name)}</span> — review`);
explainStep('Koodikatselmointi', 'Testaaja tarkistaa importit, nimeämiset, puuttuvat virheenkäsittelyt ja tiedostojen yhteensopivuuden.');
const tstPrompt = (tst.prompt ? tst.prompt+'\n\n' : '') +
`Review this project. Check:\n1. All imports are correct (files import from each other)\n2. Pydantic schema names don't conflict with SQLAlchemy models\n3. All CRUD endpoints exist\n4. Error handling is present\nIf everything is correct, say "LGTM". Otherwise list specific issues.\n\n${allCode}`;
const review = await kpnRun(tst.model, tstPrompt);
if (review && !review.toLowerCase().includes('lgtm')) {
termLog(`\n<span style="color:#d29922;font-weight:bold">[${fileList.length+3}] ${esc(cdr.name)}</span> — korjaukset`);
await kpnRun(cdr.model, `${cdr.prompt ? cdr.prompt+'\n\n' : ''}Fix issues:\n${review}\n\nCode:\n${allCode}`);
termLog(`\n<span style="color:#d29922;font-weight:bold">[${template.order.length + 2}] ${esc(cdr.name)}</span> — korjaukset`);
explainStep('Korjausluuppi', 'Testaaja löysi ongelmia. Koodari saa palautteen ja korjaa koodin.');
await kpnRun(cdr.model, `${cdr.prompt ? cdr.prompt+'\n\n' : ''}Fix these issues:\n${review}\n\nCurrent code:\n${allCode}\n\nWrite the corrected files.`);
}
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. Aja: uv run uvicorn main:app --reload`);
renderProjectCard(files, task);
}