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:
27
network-poc/frontend/public/templates/fastapi-crud.json
Normal file
27
network-poc/frontend/public/templates/fastapi-crud.json
Normal 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"]
|
||||||
|
}
|
||||||
@@ -566,66 +566,98 @@ Provide a brief risk assessment with severity (low/medium/high/critical).` },
|
|||||||
termPanel?.addEventListener('click', () => termInput?.focus());
|
termPanel?.addEventListener('click', () => termInput?.focus());
|
||||||
document.addEventListener('click', (e) => { if (!termInput?.contains(e.target) && !dropdown?.contains(e.target)) hideDD(); });
|
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) {
|
async function kpnProject(task) {
|
||||||
const mgr = agents.manager || Object.values(agents)[0];
|
|
||||||
const cdr = agents.coder || Object.values(agents)[1];
|
const cdr = agents.coder || Object.values(agents)[1];
|
||||||
const tst = agents.tester || Object.values(agents)[2];
|
const tst = agents.tester || Object.values(agents)[2];
|
||||||
|
|
||||||
termLog(`<span style="color:var(--purple);font-weight:bold">━━━ Projekti käynnistyy ━━━</span>`);
|
// Etsitään sopivin mallipohja
|
||||||
termLog(`\n<span style="color:#d29922;font-weight:bold">[1] ${esc(mgr.name)}</span> — suunnittelu`);
|
const template = Object.values(templates)[0]; // Toistaiseksi vain FastAPI CRUD
|
||||||
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}`;
|
if (!template) {
|
||||||
const plan = await kpnRun(mgr.model, mgrPrompt);
|
termLog(' ✗ Mallipohjia ei ladattu', '#f85149');
|
||||||
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>`);
|
|
||||||
return;
|
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 = {};
|
const files = {};
|
||||||
for (let i = 0; i < fileList.length; i++) {
|
|
||||||
const f = fileList[i];
|
for (let i = 0; i < template.order.length; i++) {
|
||||||
termLog(`\n<span style="color:#3fb950;font-weight:bold">[${i+2}] ${esc(cdr.name)}</span> — ${esc(f.name)}`);
|
const fileName = template.order[i];
|
||||||
let ctx = '';
|
const fileDef = template.files[fileName];
|
||||||
const prev = Object.entries(files);
|
if (!fileDef) continue;
|
||||||
if (prev.length) ctx = 'Already written:\n' + prev.map(([n,c]) => `--- ${n} ---\n${c}`).join('\n\n') + '\n\n';
|
|
||||||
let extra = '';
|
const step = i + 1;
|
||||||
if (f.name === 'pyproject.toml') extra = '\nUse format: [project]\\nname="proj"\\nversion="0.1.0"\\nrequires-python=">=3.11"\\ndependencies=["fastapi","uvicorn"]';
|
termLog(`\n<span style="color:#3fb950;font-weight:bold">[${step}/${template.order.length}] ${esc(cdr.name)}</span> — ${esc(fileName)}`);
|
||||||
// Varmistetaan oikeat importit: listaa aiempien tiedostojen exportit
|
|
||||||
let importHint = '';
|
// Opettava selitys: miksi tämä tiedosto, mitä se sisältää
|
||||||
const prevNames = Object.keys(files);
|
explainStep(fileName, fileDef.instructions);
|
||||||
if (prevNames.length > 0) {
|
|
||||||
importHint = '\n\nIMPORTANT: Import from already written files. ';
|
// Rakennetaan prompti: esimerkki + konteksti + ohje
|
||||||
importHint += prevNames.map(n => `from ${n.replace('.py','')} import ...`).join(', ');
|
let prompt = '';
|
||||||
importHint += '. Use correct imports for all dependencies.';
|
|
||||||
importHint += '\nUse separate names for Pydantic schemas (e.g. UserCreate, UserResponse) vs SQLAlchemy models (e.g. User).';
|
// 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; }
|
// Tehtävä
|
||||||
files[f.name] = code;
|
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
|
// Review
|
||||||
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');
|
||||||
termLog(`\n<span style="color:var(--accent);font-weight:bold">[${fileList.length+2}] ${esc(tst.name)}</span> — review`);
|
termLog(`\n<span style="color:var(--accent);font-weight:bold">[${template.order.length + 1}] ${esc(tst.name)}</span> — review`);
|
||||||
const tstPrompt = (tst.prompt ? tst.prompt+'\n\n' : '') + `Review briefly. Say LGTM if ok.\n${allCode}`;
|
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);
|
const review = await kpnRun(tst.model, tstPrompt);
|
||||||
|
|
||||||
if (review && !review.toLowerCase().includes('lgtm')) {
|
if (review && !review.toLowerCase().includes('lgtm')) {
|
||||||
termLog(`\n<span style="color:#d29922;font-weight:bold">[${fileList.length+3}] ${esc(cdr.name)}</span> — korjaukset`);
|
termLog(`\n<span style="color:#d29922;font-weight:bold">[${template.order.length + 2}] ${esc(cdr.name)}</span> — korjaukset`);
|
||||||
await kpnRun(cdr.model, `${cdr.prompt ? cdr.prompt+'\n\n' : ''}Fix issues:\n${review}\n\nCode:\n${allCode}`);
|
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>`);
|
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);
|
renderProjectCard(files, task);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user