Pipelinen parannuksia building blockeilla
This commit is contained in:
@@ -230,6 +230,144 @@ mitä luokkia importata.
|
||||
|
||||
---
|
||||
|
||||
## Rakennuspalaset vs. vapaa generointi
|
||||
|
||||
Kielimalli voi generoida koodia kahdella perustavanlaatuisesti eri tavalla.
|
||||
Ymmärtäminen milloin kumpikin toimii on avain luotettavaan koodigenerointi-pipelineen.
|
||||
|
||||
### Tapa 1: Vapaa generointi (naivi)
|
||||
|
||||
LLM generoi jokaisen tiedoston tyhjästä. Prompti kuvaa mitä halutaan,
|
||||
malli tuottaa koko tiedoston — importeista lähtien.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
P["Prompti"] --> LLM1["LLM: models.py"]
|
||||
LLM1 --> V1{"Validointi"}
|
||||
V1 -->|virhe| LLM1
|
||||
V1 -->|ok| LLM2["LLM: schemas.py"]
|
||||
LLM2 --> V2{"Validointi"}
|
||||
V2 -->|virhe| LLM2
|
||||
V2 -->|ok| LLM3["LLM: main.py"]
|
||||
LLM3 --> V3{"..."}
|
||||
|
||||
style V1 fill:#1a1e2e,stroke:#f85149,color:#c9d1d9
|
||||
style V2 fill:#1a1e2e,stroke:#f85149,color:#c9d1d9
|
||||
style V3 fill:#1a1e2e,stroke:#f85149,color:#c9d1d9
|
||||
```
|
||||
|
||||
**Ongelma:** Pieni malli (0.5B–7B) tekee toistuvia rakenteellisia virheitä:
|
||||
|
||||
| Virhe | Esiintymistiheys | Selitys |
|
||||
|-------|:---:|------|
|
||||
| Puuttuva import | ~60% | `from datetime import date` unohtuu |
|
||||
| SQLite `connect_args` | ~80% | Malli ei muista SQLite-erityisyyttä |
|
||||
| Väärä Enum-käyttö | ~50% | Sekoittaa `sqlalchemy.Enum` ja `enum.Enum` |
|
||||
| Poetry pyproject.toml:ssa | ~40% | Malli suosii Poetryä vaikka ohje sanoo uv |
|
||||
| Testit kopioivat koko appin | ~70% | Malli ei osaa importata, luo uudet reitit |
|
||||
|
||||
Retry-loopilla (virhe → uusi yritys virheviestin kanssa) osa korjautuu,
|
||||
mutta **sama malli toistaa samoja virheitä** koska ne johtuvat harjoitusdatasta.
|
||||
7 tiedoston projekti vaatii 7–14 LLM-kutsua ja 80–120 sekuntia.
|
||||
|
||||
### Tapa 2: Rakennuspalaset (template pipeline)
|
||||
|
||||
LLM:ltä pyydetään **vain JSON-speksi** — entiteetit, kentät ja tyypit.
|
||||
Koodi kootaan mekaanisesti valmiista pohjista joiden rakenne on todistettavasti
|
||||
oikein.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
P["Projektin kuvaus"] --> LLM["LLM: JSON-speksi"]
|
||||
LLM --> S["{ entities: [...] }"]
|
||||
S --> T1["Template: models.py"]
|
||||
S --> T2["Template: schemas.py"]
|
||||
S --> T3["Template: main.py"]
|
||||
S --> T4["Template: test_main.py"]
|
||||
S --> T5["Template: Dockerfile"]
|
||||
T1 & T2 & T3 & T4 & T5 --> D["Docker build + pytest"]
|
||||
|
||||
style LLM fill:#1a1e2e,stroke:#d29922,color:#c9d1d9
|
||||
style S fill:#1a1e2e,stroke:#3fb950,color:#c9d1d9
|
||||
style D fill:#1a1e2e,stroke:#58a6ff,color:#c9d1d9
|
||||
```
|
||||
|
||||
**Idea:** Malli on hyvä päättämään *mitä* (entiteetit, kentät), mutta huono
|
||||
muistamaan *miten* (importit, engine setup, testikonfiguraatio). Annetaan
|
||||
mallin tehdä se missä se on hyvä, ja hoidetaan loput mekaanisesti.
|
||||
|
||||
### LLM:n ainoa tehtävä
|
||||
|
||||
Malli tuottaa JSON-rakenteen kuten:
|
||||
|
||||
```json
|
||||
{
|
||||
"project_name": "todo-app",
|
||||
"entities": [
|
||||
{
|
||||
"name": "Todo",
|
||||
"table_name": "todos",
|
||||
"fields": [
|
||||
{"name": "title", "sa_type": "String(255)", "py_type": "str", "nullable": false},
|
||||
{"name": "due_date", "sa_type": "Date", "py_type": "date | None", "nullable": true},
|
||||
{"name": "status", "sa_type": "String(20)", "py_type": "str", "default": "pending"}
|
||||
]
|
||||
}
|
||||
],
|
||||
"extra_imports": ["from datetime import date"]
|
||||
}
|
||||
```
|
||||
|
||||
Tämä on yksinkertainen tehtävä jossa pienikin malli onnistuu luotettavasti:
|
||||
entiteettien tunnistus projektin kuvauksesta ja kenttätyyppien valinta.
|
||||
|
||||
### Template täyttää loput
|
||||
|
||||
Jokainen template on kuin madlib — aukot täytetään speksin datalla:
|
||||
|
||||
**models.py template (yksinkertaistettu):**
|
||||
```python
|
||||
from sqlalchemy import create_engine, Column, Integer, {sa_types}
|
||||
# ... aina samat importit, engine setup, SessionLocal ...
|
||||
|
||||
class {entity.name}(Base):
|
||||
__tablename__ = "{entity.table_name}"
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
{field.name} = Column({field.sa_type}, nullable={field.nullable})
|
||||
# ... jokainen kenttä speksistä ...
|
||||
```
|
||||
|
||||
Tulos: importit ovat aina oikein, `connect_args` on aina mukana,
|
||||
testit importoivat aina `main.py`:stä eivätkä kopioi sitä.
|
||||
|
||||
### Vertailu: mittaustulokset
|
||||
|
||||
| | Vapaa generointi | Rakennuspalaset |
|
||||
|---|:---:|:---:|
|
||||
| LLM-kutsuja | 7–14 | **1** |
|
||||
| Aika | 80–120s | **~20s** |
|
||||
| Syntaksi OK | ~70% | **100%** |
|
||||
| Docker build | vaihteleva | **100%** |
|
||||
| Pytest läpi | 0% | **100%** |
|
||||
| API toimii | ~30% | **100%** |
|
||||
|
||||
### Milloin kumpikin toimii
|
||||
|
||||
**Rakennuspalaset** kun:
|
||||
- Projektin rakenne on tunnettu (FastAPI + SQLAlchemy CRUD)
|
||||
- Laatu ja luotettavuus ovat tärkeitä
|
||||
- Malli on pieni (0.5B–7B)
|
||||
|
||||
**Vapaa generointi** kun:
|
||||
- Projektin rakenne on epätavallinen
|
||||
- Tarvitaan custom-logiikkaa jota template ei kata
|
||||
- Malli on riittävän iso (>70B tai pilvi-API)
|
||||
|
||||
Paras lopputulos syntyy yhdistelmällä: **rakennuspalaset perusrakenteelle,
|
||||
vapaa generointi business-logiikalle**.
|
||||
|
||||
---
|
||||
|
||||
## Laadun parantaminen
|
||||
|
||||
### 1. Isompi malli (suurin vaikutus)
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
<!-- Agenttigalleria + konfigurointipaneeli -->
|
||||
<!-- Agent gallery + configuration panel -->
|
||||
<div style="display:flex;gap:16px;padding:10px 0;align-items:flex-start">
|
||||
<!-- Agenttilista (drag & drop) -->
|
||||
<div id="agent-bar" style="display:flex;gap:6px;align-items:flex-end;flex-wrap:wrap">
|
||||
<!-- Renderöidään JS:stä -->
|
||||
</div>
|
||||
<!-- + Lisää agentti -->
|
||||
<div id="add-agent-btn" class="agent-avatar" onclick="addCustomAgent()" title="Lisää oma agentti" style="opacity:0.4">
|
||||
<!-- + Add agent -->
|
||||
<div id="add-agent-btn" class="agent-avatar" onclick="addCustomAgent()" title="Add custom agent" style="opacity:0.4">
|
||||
<div style="width:48px;height:48px;border-radius:50%;border:2px dashed var(--border);display:flex;align-items:center;justify-content:center;font-size:24px;color:var(--border)">+</div>
|
||||
<span style="font-size:10px;color:#8b949e;text-align:center;display:block">Lisää</span>
|
||||
<span style="font-size:10px;color:#8b949e;text-align:center;display:block">Add</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Agentin konfigurointipaneeli (avautuu klikkaamalla avataria) -->
|
||||
<!-- Agent configuration panel (opens clicking avatar) -->
|
||||
<div id="agent-config" style="display:none;background:var(--panel);border:1px solid var(--border);border-radius:6px;padding:16px;margin-bottom:10px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
|
||||
<div style="display:flex;align-items:center;gap:10px">
|
||||
<img id="config-avatar" src="" style="width:40px;height:40px;border-radius:50%">
|
||||
<div>
|
||||
<input id="config-name" style="background:transparent;border:none;color:var(--text);font-size:16px;font-weight:600;outline:none;width:200px" placeholder="Agentin nimi">
|
||||
<input id="config-name" style="background:transparent;border:none;color:var(--text);font-size:16px;font-weight:600;outline:none;width:200px" placeholder="Agent Name">
|
||||
<div id="config-role" style="font-size:11px;color:#8b949e"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:6px">
|
||||
<button class="btn btn-red" onclick="deleteAgent()" title="Poista agentti">Poista</button>
|
||||
<button class="btn btn-muted" onclick="closeAgentConfig()">Sulje</button>
|
||||
<button class="btn btn-red" onclick="deleteAgent()" title="Delete agent">Delete</button>
|
||||
<button class="btn btn-muted" onclick="closeAgentConfig()">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Malli -->
|
||||
<!-- Model -->
|
||||
<div style="margin-bottom:10px">
|
||||
<label style="font-size:12px;color:#8b949e;display:block;margin-bottom:4px">Kielimalli</label>
|
||||
<label style="font-size:12px;color:#8b949e;display:block;margin-bottom:4px">Model</label>
|
||||
<select id="config-model" style="background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:4px;padding:6px 10px;font-size:13px;width:100%">
|
||||
<option value="qwen-coder">Qwen2.5-Coder:0.5B (selain)</option>
|
||||
<option value="qwen-coder">Qwen2.5-Coder:0.5B (browser)</option>
|
||||
<option value="qwen-coder-3b">Qwen2.5-Coder:3B (Ollama)</option>
|
||||
<option value="qwen2.5-coder:7b">Qwen2.5-Coder:7B (Ollama)</option>
|
||||
<option value="qwen2.5-coder:1.5b">Qwen2.5-Coder:1.5B (Ollama)</option>
|
||||
@@ -39,41 +39,41 @@
|
||||
</div>
|
||||
|
||||
<!-- System prompt -->
|
||||
<div style="margin-bottom:10px" title="Agentin perusohje joka lähetetään kielimallille jokaisessa pyynnössä. Hyvän promptin rakenne: 1. Rooli: 'You are an expert...' 2. Säännöt: RULES/CRITICAL RULES listana 3. Esimerkit: EXAMPLE OUTPUT 4. Kiellot: NEVER-lista Vinkki: käytä englantia — malli ymmärtää sen paremmin ja se kuluttaa vähemmän tokeneita.">
|
||||
<div style="margin-bottom:10px" title="System prompt sent to the LLM on every request. Good prompt structure: 1. Role: 'You are an expert...' 2. Rules: RULES/CRITICAL RULES as list 3. Examples: EXAMPLE OUTPUT 4. Restrictions: NEVER-list ">
|
||||
<label style="font-size:12px;color:#8b949e;display:block;margin-bottom:4px;cursor:help">System prompt 💡</label>
|
||||
<textarea id="config-prompt" style="width:100%;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:4px;padding:8px;font-size:13px;font-family:'Courier New',monospace;resize:vertical;overflow:hidden;min-height:60px" placeholder="Kuvaa agentin rooli ja käyttäytyminen..."></textarea>
|
||||
<textarea id="config-prompt" style="width:100%;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:4px;padding:8px;font-size:13px;font-family:'Courier New',monospace;resize:vertical;overflow:hidden;min-height:60px" placeholder="Describe the agent's role and behavior..."></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Sampling-parametrit -->
|
||||
<!-- Sampling Parameters -->
|
||||
<div style="margin-bottom:10px">
|
||||
<label style="font-size:12px;color:#8b949e;display:block;margin-bottom:8px">Sampling-parametrit</label>
|
||||
<label style="font-size:12px;color:#8b949e;display:block;margin-bottom:8px">Sampling Parameters</label>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
|
||||
<div title="Kontrolloi 'luovuutta'. Matala arvo (0.2-0.4) tuottaa ennustettavaa, toistettavaa koodia — hyvä testaajille ja reviewereille. Keskiarvo (0.6-0.8) on paras koodin generointiin. Korkea arvo (1.0+) lisää vaihtelua mutta myös virheitä. Suositus: • Manageri: 0.5 (tarkat tiedostolistat) • Koodari: 0.7 (toimiva koodi + vaihtelu) • Testaaja: 0.3 (deterministinen arviointi)">
|
||||
<div title="Controls 'creativity'. Low value (0.2-0.4) produces predictable, repeatable code — good for testers and reviewers. Medium value (0.6-0.8) is best for generating code. High value (1.0+) adds variation but also errors. Recommendation: • Manager: 0.5 (precise file lists) • Coder: 0.7 (working code + variation) • Tester: 0.3 (deterministic evaluation)">
|
||||
<label style="font-size:11px;color:#8b949e;cursor:help">Temperature 💡 <span id="config-temp-val" style="color:var(--accent);float:right">0.7</span></label>
|
||||
<input type="range" id="config-temperature" min="0" max="1.5" step="0.1" value="0.7" style="width:100%;accent-color:var(--accent)">
|
||||
<div style="font-size:10px;color:#30363d">0=tarkka · 0.7=oletus · 1.5=luova</div>
|
||||
<div style="font-size:10px;color:#30363d">0=strict · 0.7=default · 1.5=creative</div>
|
||||
</div>
|
||||
<div title="Vastauksen maksimipituus tokeneina (~1 token ≈ 4 merkkiä). Suositus: • Manageri: 256-512 (lyhyet tiedostolistat) • Koodari: 1024-2048 (täydet tiedostot, CRUD-endpointit) • Testaaja: 256-512 (lyhyet arvioinnit) Jos koodi katkeaa kesken, nosta tätä. Jos malli tuottaa turhaa toistoa, laske.">
|
||||
<div title="Maximum response length in tokens (~1 token ≈ 4 chars). Recommendation: • Manager: 256-512 (short lists) • Coder: 1024-2048 (full files, CRUD endpoints) • Tester: 256-512 (short evaluations) If code cuts off early, increase this.">
|
||||
<label style="font-size:11px;color:#8b949e;cursor:help">Max tokens 💡 <span id="config-maxtok-val" style="color:var(--accent);float:right">1024</span></label>
|
||||
<input type="range" id="config-maxtokens" min="64" max="4096" step="64" value="1024" style="width:100%;accent-color:var(--accent)">
|
||||
<div style="font-size:10px;color:#30363d">Vastauksen maksimipituus</div>
|
||||
<div style="font-size:10px;color:#30363d">Maximum response length</div>
|
||||
</div>
|
||||
<div title="Montako todennäköisintä tokenia huomioidaan valinnassa. Pieni arvo (1-10) tekee vastauksesta deterministisen. Suuri arvo (50-100) sallii harvinaisempia sanoja. Suositus: • Boilerplate-koodi: 20-30 (tutut patternit) • Yleiskoodi: 40 (hyvä oletus) • Luova teksti: 60-80 Yleensä ei tarvitse muuttaa oletuksesta.">
|
||||
<div title="How many most probable tokens are considered. Low value (1-10) makes response deterministic. High value (50-100) allows rarer words. Recommendation: • Boilerplate code: 20-30 (familiar patterns) • General code: 40 (good default) • Creative text: 60-80">
|
||||
<label style="font-size:11px;color:#8b949e;cursor:help">Top-K 💡 <span id="config-topk-val" style="color:var(--accent);float:right">40</span></label>
|
||||
<input type="range" id="config-topk" min="1" max="100" step="1" value="40" style="width:100%;accent-color:var(--accent)">
|
||||
<div style="font-size:10px;color:#30363d">1=greedy · 40=oletus · 100=laaja</div>
|
||||
<div style="font-size:10px;color:#30363d">1=greedy · 40=default · 100=wide</div>
|
||||
</div>
|
||||
<div title="Vähentää jo tuotettujen sanojen todennäköisyyttä. Estää mallia toistamasta samaa lausetta. Liian korkea arvo (>1.5) voi rikkoa koodin koska samat avainsanat (return, if, def) ovat tarpeellisia. Suositus: • Koodi: 1.1-1.2 (lievä, sallii toiston) • Teksti: 1.15-1.3 (vahvempi) • Review: 1.0-1.1 (ei rangaistusta, lyhyet vastaukset)">
|
||||
<div title="Reduces the probability of already generated words. Prevents model from repeating same sentences. Too high value (>1.5) can break code because common keywords (return, if, def) are necessary. Recommendation: • Code: 1.1-1.2 (mild, allows repetition) • Text: 1.15-1.3 (stronger penalty) • Review: 1.0-1.1 (no penalty, short answers)">
|
||||
<label style="font-size:11px;color:#8b949e;cursor:help">Repetition penalty 💡 <span id="config-rep-val" style="color:var(--accent);float:right">1.15</span></label>
|
||||
<input type="range" id="config-repeat" min="1.0" max="2.0" step="0.05" value="1.15" style="width:100%;accent-color:var(--accent)">
|
||||
<div style="font-size:10px;color:#30363d">1.0=ei · 1.15=oletus · 2.0=vahva</div>
|
||||
<div style="font-size:10px;color:#30363d">1.0=none · 1.15=default · 2.0=strong</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pipeline-järjestys -->
|
||||
<!-- Pipeline order -->
|
||||
<div>
|
||||
<label style="font-size:12px;color:#8b949e;display:block;margin-bottom:4px">Pipeline-järjestys <span style="color:var(--border)">(vedä järjestääksesi)</span></label>
|
||||
<label style="font-size:12px;color:#8b949e;display:block;margin-bottom:4px">Pipeline Order <span style="color:var(--border)">(drag to sort)</span></label>
|
||||
<div id="config-pipeline" style="display:flex;gap:4px;flex-wrap:wrap"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!-- Monaco Editor paneeli -->
|
||||
<div id="panel-editor" class="panel">
|
||||
<div style="display:flex;height:calc(100vh - 200px);gap:0;border:1px solid var(--border);border-radius:6px;overflow:hidden">
|
||||
<div style="display:flex;flex:1;min-height:0;gap:0;border:1px solid var(--border);border-radius:6px;overflow:hidden">
|
||||
<div id="editor-filetree" style="width:200px;min-width:150px;background:var(--bg);border-right:1px solid var(--border);overflow:auto;resize:horizontal;font-family:'Courier New',monospace;font-size:13px">
|
||||
<div style="padding:10px 12px;color:#8b949e;font-size:11px;text-transform:uppercase;letter-spacing:0.5px;border-bottom:1px solid var(--border)">Tiedostot</div>
|
||||
<div id="editor-file-list" style="padding:4px 0">
|
||||
|
||||
@@ -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-agentit 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-projektin.
|
||||
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 ports
|
||||
- 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 files
|
||||
- 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 Dockerfile, 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);
|
||||
|
||||
@@ -17,13 +17,24 @@ body {
|
||||
font-size: 16px;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.container { max-width: 1600px; margin: 0 auto; padding: 20px 40px; }
|
||||
.container {
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px 40px;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#app:not(.active) { display: none; }
|
||||
#landing.hidden { display: none; }
|
||||
|
||||
/* Tabs */
|
||||
.tabs { display: flex; gap: 4px; margin-bottom: 16px; }
|
||||
.tabs { display: flex; gap: 4px; margin-bottom: 16px; flex-shrink: 0; }
|
||||
.tab {
|
||||
padding: 10px 20px; border-radius: 6px 6px 0 0; cursor: pointer;
|
||||
border: 1px solid var(--border); border-bottom: none;
|
||||
@@ -33,7 +44,7 @@ body {
|
||||
|
||||
/* Panels */
|
||||
.panel { display: none; }
|
||||
.panel.active { display: block; }
|
||||
.panel.active { display: flex; flex-direction: column; flex: 1; min-height: 0; }
|
||||
|
||||
/* Status bar */
|
||||
.status-bar {
|
||||
@@ -52,7 +63,7 @@ body {
|
||||
.terminal {
|
||||
background: #010409; border: 1px solid var(--border); border-top: none;
|
||||
font-family: 'Courier New', monospace; font-size: 16px;
|
||||
min-height: 400px; max-height: 70vh; overflow-y: auto;
|
||||
flex: 1; min-height: 0; max-height: none; overflow-y: auto;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
.terminal-line { padding: 1px 0; white-space: pre-wrap; word-break: break-word; }
|
||||
@@ -172,6 +183,13 @@ body {
|
||||
.agent-avatar.active img {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 25px rgba(88,166,255,0.8);
|
||||
animation: agentBlink 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes agentBlink {
|
||||
0% { opacity: 0.8; box-shadow: 0 0 15px rgba(88,166,255,0.5); }
|
||||
50% { opacity: 1.0; box-shadow: 0 0 35px rgba(88,166,255,1.0); }
|
||||
100% { opacity: 0.8; box-shadow: 0 0 15px rgba(88,166,255,0.5); }
|
||||
}
|
||||
|
||||
/* Settings */
|
||||
@@ -265,6 +283,16 @@ body {
|
||||
border-color: #ff6b00; box-shadow: 0 0 0 3px rgba(255,107,0,0.15);
|
||||
}
|
||||
.hero-input::placeholder { color: #484f58; }
|
||||
.hero-input.shake {
|
||||
animation: shake 0.4s ease;
|
||||
border-color: #f85149;
|
||||
box-shadow: 0 0 0 3px rgba(248,81,73,0.2);
|
||||
}
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
20%, 60% { transform: translateX(-6px); }
|
||||
40%, 80% { transform: translateX(6px); }
|
||||
}
|
||||
.hero-btn {
|
||||
padding: 14px 28px; font-size: 16px; font-weight: 600;
|
||||
font-family: 'Inter', sans-serif;
|
||||
|
||||
Reference in New Issue
Block a user