diff --git a/network-poc/frontend/public/templates/fastapi-crud.json b/network-poc/frontend/public/templates/fastapi-crud.json new file mode 100644 index 0000000..1f642d0 --- /dev/null +++ b/network-poc/frontend/public/templates/fastapi-crud.json @@ -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"] +} diff --git a/network-poc/frontend/src/pages/index.astro b/network-poc/frontend/src/pages/index.astro index 6d2a69f..2f7f391 100644 --- a/network-poc/frontend/src/pages/index.astro +++ b/network-poc/frontend/src/pages/index.astro @@ -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 💡 ${esc(title)}`); + termLog(` ${esc(explanation)}`); + } + 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(`━━━ Projekti käynnistyy ━━━`); - termLog(`\n[1] ${esc(mgr.name)} — 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━━━ Valmis ━━━`); + // Etsitään sopivin mallipohja + const template = Object.values(templates)[0]; // Toistaiseksi vain FastAPI CRUD + if (!template) { + termLog(' ✗ Mallipohjia ei ladattu', '#f85149'); return; } - termLog(` ${fileList.length} tiedostoa: ${fileList.map(f=>f.name).join(', ')}`); + termLog(`━━━ ${esc(template.name)} — ${esc(task)} ━━━`); + 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[${i+2}] ${esc(cdr.name)} — ${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[${step}/${template.order.length}] ${esc(cdr.name)} — ${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[${fileList.length+2}] ${esc(tst.name)} — review`); - const tstPrompt = (tst.prompt ? tst.prompt+'\n\n' : '') + `Review briefly. Say LGTM if ok.\n${allCode}`; + termLog(`\n[${template.order.length + 1}] ${esc(tst.name)} — 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[${fileList.length+3}] ${esc(cdr.name)} — korjaukset`); - await kpnRun(cdr.model, `${cdr.prompt ? cdr.prompt+'\n\n' : ''}Fix issues:\n${review}\n\nCode:\n${allCode}`); + termLog(`\n[${template.order.length + 2}] ${esc(cdr.name)} — 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━━━ Valmis (${Object.keys(files).length} tiedostoa) ━━━`); + 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); }