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
|
## Laadun parantaminen
|
||||||
|
|
||||||
### 1. Isompi malli (suurin vaikutus)
|
### 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">
|
<div style="display:flex;gap:16px;padding:10px 0;align-items:flex-start">
|
||||||
<!-- Agenttilista (drag & drop) -->
|
<!-- Agenttilista (drag & drop) -->
|
||||||
<div id="agent-bar" style="display:flex;gap:6px;align-items:flex-end;flex-wrap:wrap">
|
<div id="agent-bar" style="display:flex;gap:6px;align-items:flex-end;flex-wrap:wrap">
|
||||||
<!-- Renderöidään JS:stä -->
|
<!-- Renderöidään JS:stä -->
|
||||||
</div>
|
</div>
|
||||||
<!-- + Lisää agentti -->
|
<!-- + Add agent -->
|
||||||
<div id="add-agent-btn" class="agent-avatar" onclick="addCustomAgent()" title="Lisää oma agentti" style="opacity:0.4">
|
<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>
|
<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>
|
||||||
</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 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;justify-content:space-between;align-items:center;margin-bottom:12px">
|
||||||
<div style="display:flex;align-items:center;gap:10px">
|
<div style="display:flex;align-items:center;gap:10px">
|
||||||
<img id="config-avatar" src="" style="width:40px;height:40px;border-radius:50%">
|
<img id="config-avatar" src="" style="width:40px;height:40px;border-radius:50%">
|
||||||
<div>
|
<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 id="config-role" style="font-size:11px;color:#8b949e"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;gap:6px">
|
<div style="display:flex;gap:6px">
|
||||||
<button class="btn btn-red" onclick="deleteAgent()" title="Poista agentti">Poista</button>
|
<button class="btn btn-red" onclick="deleteAgent()" title="Delete agent">Delete</button>
|
||||||
<button class="btn btn-muted" onclick="closeAgentConfig()">Sulje</button>
|
<button class="btn btn-muted" onclick="closeAgentConfig()">Close</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Malli -->
|
<!-- Model -->
|
||||||
<div style="margin-bottom:10px">
|
<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%">
|
<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="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:7b">Qwen2.5-Coder:7B (Ollama)</option>
|
||||||
<option value="qwen2.5-coder:1.5b">Qwen2.5-Coder:1.5B (Ollama)</option>
|
<option value="qwen2.5-coder:1.5b">Qwen2.5-Coder:1.5B (Ollama)</option>
|
||||||
@@ -39,41 +39,41 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- System prompt -->
|
<!-- 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>
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Sampling-parametrit -->
|
<!-- Sampling Parameters -->
|
||||||
<div style="margin-bottom:10px">
|
<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 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>
|
<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)">
|
<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>
|
||||||
<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>
|
<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)">
|
<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>
|
||||||
<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>
|
<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)">
|
<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>
|
||||||
<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>
|
<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)">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pipeline-järjestys -->
|
<!-- Pipeline order -->
|
||||||
<div>
|
<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 id="config-pipeline" style="display:flex;gap:4px;flex-wrap:wrap"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<!-- Monaco Editor paneeli -->
|
<!-- Monaco Editor paneeli -->
|
||||||
<div id="panel-editor" class="panel">
|
<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 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 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">
|
<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 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">
|
<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 src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
|
||||||
<script type="module">
|
<script is:inline type="module">
|
||||||
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11.14.0/dist/mermaid.esm.min.mjs';
|
||||||
mermaid.initialize({ startOnLoad: false, theme: 'dark' });
|
mermaid.initialize({ startOnLoad: false, theme: 'dark' });
|
||||||
window.mermaid = mermaid;
|
window.mermaid = mermaid;
|
||||||
</script>
|
</script>
|
||||||
@@ -40,10 +40,10 @@ import Settings from "../components/Settings.astro";
|
|||||||
<h1 class="hero-title">Näe miten AI-agenttitiimi rakentaa projektin.</h1>
|
<h1 class="hero-title">Näe miten AI-agenttitiimi rakentaa projektin.</h1>
|
||||||
<div class="hero-divider"></div>
|
<div class="hero-divider"></div>
|
||||||
<p class="hero-desc">
|
<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>
|
||||||
<p class="hero-notice" style="border-left-color:#ff6b00;color:#ff6b00">
|
<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>
|
</p>
|
||||||
|
|
||||||
<div class="hero-input-group">
|
<div class="hero-input-group">
|
||||||
@@ -153,14 +153,100 @@ import Settings from "../components/Settings.astro";
|
|||||||
return esc(code);
|
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ä ===
|
// === Landing → App siirtymä ===
|
||||||
function startProject(task) {
|
function startProject(task, skipValidation) {
|
||||||
if (!task || !task.trim()) return;
|
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('landing').classList.add('hidden');
|
||||||
document.getElementById('app').classList.add('active');
|
document.getElementById('app').classList.add('active');
|
||||||
// Käynnistä pipeline suoraan termExec:n kautta
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (typeof termExec === 'function') termExec(`kpn project "${task.trim()}"`);
|
if (typeof termExec === 'function') termExec(`kpn project "${trimmed}"`);
|
||||||
}, 200);
|
}, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +263,7 @@ import Settings from "../components/Settings.astro";
|
|||||||
if (e.key === 'Enter') startProject(e.target.value);
|
if (e.key === 'Enter') startProject(e.target.value);
|
||||||
});
|
});
|
||||||
document.querySelectorAll('.example-btn').forEach(btn => {
|
document.querySelectorAll('.example-btn').forEach(btn => {
|
||||||
btn.addEventListener('click', () => startProject(btn.dataset.prompt));
|
btn.addEventListener('click', () => startProject(btn.dataset.prompt, true));
|
||||||
});
|
});
|
||||||
|
|
||||||
// === Oppimispolku — renderöinti ===
|
// === Oppimispolku — renderöinti ===
|
||||||
@@ -251,20 +337,22 @@ pyproject.toml: project dependencies` },
|
|||||||
prompt: `You are an expert Python developer. Write complete, production-ready code.
|
prompt: `You are an expert Python developer. Write complete, production-ready code.
|
||||||
|
|
||||||
CRITICAL RULES:
|
CRITICAL RULES:
|
||||||
1. Include ALL imports at the top of every file
|
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 User, SessionLocal
|
2. Import from other project files: from models import Todo, SessionLocal
|
||||||
3. Pydantic schemas use different names than SQLAlchemy models: UserCreate, UserResponse (not User)
|
3. NEVER use relative imports (from .models) — ALWAYS absolute: from models import ...
|
||||||
4. SQLAlchemy engine: create_engine(url, connect_args={"check_same_thread": False})
|
4. Pydantic schemas use different names than SQLAlchemy models: TodoCreate, TodoResponse (not Todo)
|
||||||
5. SessionLocal: sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
5. SQLAlchemy engine: create_engine(url, connect_args={"check_same_thread": False})
|
||||||
6. FastAPI dependencies: def get_db(): db = SessionLocal(); try: yield db; finally: db.close()
|
6. SessionLocal: sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
7. Pydantic v2: use model_dump() not dict(), class Config: from_attributes = True
|
7. FastAPI dependencies: def get_db(): db = SessionLocal(); try: yield db; finally: db.close()
|
||||||
8. All CRUD endpoints: POST (201), GET list, GET by id, PUT, DELETE (204)
|
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:
|
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
|
- Leave placeholder code or TODO comments
|
||||||
- Use Flask syntax (app.run) in FastAPI projects
|
- 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 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)` },
|
- 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,
|
data: { name: 'Data Engineer', avatar: '/avatars/pesukarhu_notext.webp', model: 'qwen-coder', order: 3,
|
||||||
@@ -274,16 +362,23 @@ NEVER:
|
|||||||
YOUR RESPONSIBILITIES:
|
YOUR RESPONSIBILITIES:
|
||||||
1. Design normalized database schemas with proper column types and constraints
|
1. Design normalized database schemas with proper column types and constraints
|
||||||
2. Define SQLAlchemy models with __tablename__, primary keys, indexes, and relationships
|
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
|
4. Use String(length) not bare String for SQLite compatibility
|
||||||
5. Add nullable=False for required fields, unique=True where appropriate
|
5. Add nullable=False for required fields, unique=True where appropriate
|
||||||
6. Use Column(Integer, primary_key=True, index=True) for IDs
|
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:
|
ALWAYS INCLUDE:
|
||||||
- from sqlalchemy import create_engine, Column, Integer, String
|
- from sqlalchemy import create_engine, Column, Integer, String
|
||||||
- from sqlalchemy.ext.declarative import declarative_base
|
- from sqlalchemy.ext.declarative import declarative_base
|
||||||
- from sqlalchemy.orm import sessionmaker
|
- 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,
|
qa: { name: 'QA', avatar: '/avatars/susi_notext.webp', model: 'qwen-coder', order: 4,
|
||||||
temperature: 0.4, topK: 40, repeatPenalty: 1.15, maxTokens: 1024,
|
temperature: 0.4, topK: 40, repeatPenalty: 1.15, maxTokens: 1024,
|
||||||
prompt: `You are a QA engineer responsible for code review and automated testing.
|
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
|
- Be specific and actionable, not vague
|
||||||
|
|
||||||
WHEN WRITING TESTS:
|
WHEN WRITING TESTS:
|
||||||
- pytest as the test framework
|
- ALWAYS import app from main.py: from main import app, get_db
|
||||||
- FastAPI TestClient for API endpoint testing
|
- ALWAYS import Base from models.py: from models import Base
|
||||||
- SQLAlchemy in-memory SQLite for test database isolation
|
- 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)
|
- 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,
|
tester: { name: 'DevOps', avatar: '/avatars/laiskiainen_notext.webp', model: 'qwen-coder', order: 5,
|
||||||
temperature: 0.3, topK: 40, repeatPenalty: 1.1, maxTokens: 1024,
|
temperature: 0.3, topK: 40, repeatPenalty: 1.1, maxTokens: 1024,
|
||||||
prompt: `You are a DevOps engineer specializing in containerization and deployment.
|
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:
|
DOCKERFILE RULES:
|
||||||
- Use python:3.12-slim as base
|
- Use python:3.12-slim as base
|
||||||
- Install uv: COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
|
- Install uv: COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
|
||||||
- Copy pyproject.toml first, then uv sync, then copy source
|
- ENV UV_CACHE_DIR=/tmp/uv-cache (MUST set before uv sync)
|
||||||
- Expose appropriate ports
|
- Copy pyproject.toml first, then RUN uv sync, then COPY source files
|
||||||
- Use uv run for CMD
|
- 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,
|
observer: { name: 'Observer', avatar: '/avatars/aikuinen_susi.webp', model: 'qwen-coder', order: 6,
|
||||||
temperature: 0.6, topK: 40, repeatPenalty: 1.15, maxTokens: 1024,
|
temperature: 0.6, topK: 40, repeatPenalty: 1.15, maxTokens: 1024,
|
||||||
prompt: `You are an independent technical observer and risk analyst.
|
prompt: `You are an independent technical observer and risk analyst.
|
||||||
@@ -1023,6 +1119,171 @@ OUTPUT FORMAT:
|
|||||||
return crewFiles;
|
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) {
|
async function kpnProject(task) {
|
||||||
pipelineAbort = new AbortController();
|
pipelineAbort = new AbortController();
|
||||||
const promptLog = [];
|
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 });
|
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>`);
|
termLog(` <span style="color:#8b949e">Requirements ready → Manager</span>`);
|
||||||
|
|
||||||
// Valitaan mallipohja automaattisesti briefin perusteella
|
// === Vaihe 2: JSON-speksi vaatimuksista ===
|
||||||
const template = selectTemplate(brief);
|
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
|
const specRaw = await kpnRun(mgr.model, `${brief}\n\nOutput a JSON spec for this project.`, false, { ...mgr, prompt: SPEC_SYSTEM });
|
||||||
let fileOrder = [];
|
const spec = specRaw ? extractJson(specRaw) : null;
|
||||||
let fileDefs = {};
|
promptLog.push({ step: 1, agentKey: 'manager', agentName: mgr.name, model: mgr.model, label: 'JSON-speksi', systemPrompt: SPEC_SYSTEM, userPrompt: brief, response: specRaw || '' });
|
||||||
|
|
||||||
if (template) {
|
if (!spec || !spec.entities || spec.entities.length === 0) {
|
||||||
// Mallipohja löytyi — käytetään sen rakennetta
|
termLog(' ✗ JSON-speksi epäonnistui — fallback vapaaseen generointiin', '#f85149');
|
||||||
fileOrder = template.order;
|
// TODO: fallback vanhaan pipeline-logiikkaan
|
||||||
fileDefs = template.files;
|
return;
|
||||||
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 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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++) {
|
for (let i = 0; i < fileOrder.length; i++) {
|
||||||
const fileName = fileOrder[i];
|
const fileName = fileOrder[i];
|
||||||
const fileDef = fileDefs[fileName];
|
const agentKey = agentMap[fileName] || 'coder';
|
||||||
if (!fileDef) continue;
|
const agentName = agentNames[agentKey] || 'Coder';
|
||||||
|
const agent = agents[agentKey] || cdr;
|
||||||
|
|
||||||
const step = i + 1;
|
termLog(`\n<span style="color:#3fb950;font-weight:bold">[${i+2}/${fileOrder.length+1}] ${esc(agent.name)}</span> — ${esc(fileName)}`);
|
||||||
// Valitaan oikea agentti tiedostotyypin mukaan
|
highlightAgent(agentKey);
|
||||||
const isDbFile = fileName === 'models.py' || fileName === 'database.py' || fileName === 'etl.py';
|
explainStep(fileName, `${agent.name} generoi ${fileName} rakennuspalasista (template pipeline).`);
|
||||||
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">[${step}/${fileOrder.length}] ${esc(fileAgent.name)}</span> — ${esc(fileName)}`);
|
// Pieni viive UX:ää varten — näyttää agentin "työskentelevän"
|
||||||
highlightAgent(fileAgentKey);
|
await new Promise(r => setTimeout(r, 300));
|
||||||
|
|
||||||
explainStep(fileName, fileDef.instructions || fileDef.description);
|
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>`);
|
||||||
// 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 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const allCode = Object.entries(files).map(([n,c]) => `--- ${n} ---\n${c}`).join('\n\n');
|
let stepN = fileOrder.length + 2;
|
||||||
let stepN = fileOrder.length + 1;
|
|
||||||
|
|
||||||
// QA-katselmointi → Coder-korjaus -luuppi (max N kierrosta)
|
// === Vaihe 4: Mekaaninen QA-validointi ===
|
||||||
const qaAgent = agents.qa || Object.values(agents)[4];
|
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 mechIssues = validateProjectCode(files);
|
||||||
const currentCode = Object.entries(files).map(([n,c]) => `--- ${n} ---\n${c}`).join('\n\n');
|
if (mechIssues.length > 0) {
|
||||||
|
termLog(` <span style="color:#d29922">⚠ ${mechIssues.length} ongelmaa (template-bugeja — korjattava):</span>`);
|
||||||
// QA katselmoi
|
for (const issue of mechIssues) termLog(` <span style="color:#d29922">${esc(issue)}</span>`);
|
||||||
termLog(`\n<span style="color:#d2a8ff;font-weight:bold">[${stepN}] ${esc(qaAgent.name)}</span> — katselmointi${round > 0 ? ' (kierros '+(round+1)+')' : ''}`);
|
} else {
|
||||||
highlightAgent('qa');
|
termLog(` <span style="color:#3fb950">✓ Kaikki tiedostot validoitu — 0 ongelmaa</span>`);
|
||||||
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 });
|
|
||||||
}
|
}
|
||||||
|
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++;
|
stepN++;
|
||||||
|
|
||||||
// Tarkkailija: yhteenveto + raportti + arvosana
|
// Tarkkailija: yhteenveto + raportti + arvosana
|
||||||
@@ -1380,7 +1515,8 @@ OUTPUT FORMAT:
|
|||||||
const lv = new DataView(local.buffer);
|
const lv = new DataView(local.buffer);
|
||||||
lv.setUint32(0, 0x04034b50, true); // signature
|
lv.setUint32(0, 0x04034b50, true); // signature
|
||||||
lv.setUint16(4, 20, true); // version needed
|
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(14, crc, true); // CRC-32
|
||||||
lv.setUint32(18, dataBytes.length, true); // compressed size
|
lv.setUint32(18, dataBytes.length, true); // compressed size
|
||||||
lv.setUint32(22, dataBytes.length, true); // uncompressed size
|
lv.setUint32(22, dataBytes.length, true); // uncompressed size
|
||||||
@@ -1395,7 +1531,8 @@ OUTPUT FORMAT:
|
|||||||
cv.setUint32(0, 0x02014b50, true); // signature
|
cv.setUint32(0, 0x02014b50, true); // signature
|
||||||
cv.setUint16(4, 20, true); // version made by
|
cv.setUint16(4, 20, true); // version made by
|
||||||
cv.setUint16(6, 20, true); // version needed
|
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(16, crc, true);
|
||||||
cv.setUint32(20, dataBytes.length, true);
|
cv.setUint32(20, dataBytes.length, true);
|
||||||
cv.setUint32(24, dataBytes.length, true);
|
cv.setUint32(24, dataBytes.length, true);
|
||||||
|
|||||||
@@ -17,13 +17,24 @@ body {
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
color: var(--text);
|
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 */
|
||||||
.tabs { display: flex; gap: 4px; margin-bottom: 16px; }
|
.tabs { display: flex; gap: 4px; margin-bottom: 16px; flex-shrink: 0; }
|
||||||
.tab {
|
.tab {
|
||||||
padding: 10px 20px; border-radius: 6px 6px 0 0; cursor: pointer;
|
padding: 10px 20px; border-radius: 6px 6px 0 0; cursor: pointer;
|
||||||
border: 1px solid var(--border); border-bottom: none;
|
border: 1px solid var(--border); border-bottom: none;
|
||||||
@@ -33,7 +44,7 @@ body {
|
|||||||
|
|
||||||
/* Panels */
|
/* Panels */
|
||||||
.panel { display: none; }
|
.panel { display: none; }
|
||||||
.panel.active { display: block; }
|
.panel.active { display: flex; flex-direction: column; flex: 1; min-height: 0; }
|
||||||
|
|
||||||
/* Status bar */
|
/* Status bar */
|
||||||
.status-bar {
|
.status-bar {
|
||||||
@@ -52,7 +63,7 @@ body {
|
|||||||
.terminal {
|
.terminal {
|
||||||
background: #010409; border: 1px solid var(--border); border-top: none;
|
background: #010409; border: 1px solid var(--border); border-top: none;
|
||||||
font-family: 'Courier New', monospace; font-size: 16px;
|
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;
|
padding: 12px 16px;
|
||||||
}
|
}
|
||||||
.terminal-line { padding: 1px 0; white-space: pre-wrap; word-break: break-word; }
|
.terminal-line { padding: 1px 0; white-space: pre-wrap; word-break: break-word; }
|
||||||
@@ -172,6 +183,13 @@ body {
|
|||||||
.agent-avatar.active img {
|
.agent-avatar.active img {
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
box-shadow: 0 0 25px rgba(88,166,255,0.8);
|
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 */
|
/* Settings */
|
||||||
@@ -265,6 +283,16 @@ body {
|
|||||||
border-color: #ff6b00; box-shadow: 0 0 0 3px rgba(255,107,0,0.15);
|
border-color: #ff6b00; box-shadow: 0 0 0 3px rgba(255,107,0,0.15);
|
||||||
}
|
}
|
||||||
.hero-input::placeholder { color: #484f58; }
|
.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 {
|
.hero-btn {
|
||||||
padding: 14px 28px; font-size: 16px; font-weight: 600;
|
padding: 14px 28px; font-size: 16px; font-weight: 600;
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
|
|||||||
@@ -1011,15 +1011,15 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
|
|||||||
if let Some(obj) = json.as_object_mut() {
|
if let Some(obj) = json.as_object_mut() {
|
||||||
let model = obj.get("model").and_then(|v| v.as_str()).unwrap_or("?");
|
let model = obj.get("model").and_then(|v| v.as_str()).unwrap_or("?");
|
||||||
let prompt = obj.get("prompt").and_then(|v| v.as_str()).unwrap_or("");
|
let prompt = obj.get("prompt").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
let response = obj.get("response").and_then(|v| v.as_str()).unwrap_or("");
|
let _response = obj.get("response").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
let tok_gen = obj.get("tokens_generated").and_then(|v| v.as_u64()).unwrap_or(0);
|
let tok_gen = obj.get("tokens_generated").and_then(|v| v.as_u64()).unwrap_or(0);
|
||||||
let duration = obj.get("duration_ms").and_then(|v| v.as_f64()).unwrap_or(0.0);
|
let duration = obj.get("duration_ms").and_then(|v| v.as_f64()).unwrap_or(0.0);
|
||||||
let tok_s = obj.get("tokens_per_sec").and_then(|v| v.as_f64()).unwrap_or(0.0);
|
let tok_s = obj.get("tokens_per_sec").and_then(|v| v.as_f64()).unwrap_or(0.0);
|
||||||
|
|
||||||
println!();
|
println!();
|
||||||
println!("\x1b[35m━━━ Solmu {} ━━━ {} ━━━\x1b[0m", node_id, model);
|
println!("\x1b[35m━━━ Solmu {} ━━━ {} ━━━\x1b[0m", node_id, model);
|
||||||
println!(" Prompt: \x1b[33m\"{}\"\x1b[0m", prompt);
|
let prompt_preview: String = prompt.chars().take(80).collect();
|
||||||
println!(" Vastaus: \x1b[32m{}\x1b[0m", response);
|
println!(" Prompt: \x1b[33m\"{}...\"\x1b[0m", prompt_preview);
|
||||||
println!(" {} tokenia | {:.0}ms | \x1b[36m{:.1} tok/s\x1b[0m", tok_gen, duration, tok_s);
|
println!(" {} tokenia | {:.0}ms | \x1b[36m{:.1} tok/s\x1b[0m", tok_gen, duration, tok_s);
|
||||||
|
|
||||||
state.db.increment_tasks(node_id);
|
state.db.increment_tasks(node_id);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crossterm::{
|
use crossterm::{
|
||||||
event::{self, Event, EventStream, KeyCode},
|
event::{Event, EventStream, KeyCode},
|
||||||
execute,
|
execute,
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
};
|
};
|
||||||
|
|||||||
Binary file not shown.
16
zipit/loop_runs/baseline_v1/Dockerfile
Normal file
16
zipit/loop_runs/baseline_v1/Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
FROM python:3.12-slim as builder
|
||||||
|
|
||||||
|
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY pyproject.toml .
|
||||||
|
RUN uv sync
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
USER 1000
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
76
zipit/loop_runs/baseline_v1/main.py
Normal file
76
zipit/loop_runs/baseline_v1/main.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
from fastapi import FastAPI, HTTPException, Depends, Query
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from typing import List, Optional
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from models import Todo, SessionLocal
|
||||||
|
from schemas import TodoCreate, TodoResponse
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
# Dependency to get database session
|
||||||
|
def get_db():
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
@app.post("/todos", response_model=TodoResponse, status_code=201)
|
||||||
|
async def create_todo(todo: TodoCreate, db: Session = Depends(get_db)):
|
||||||
|
db_todo = Todo(**todo.model_dump())
|
||||||
|
db.add(db_todo)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_todo)
|
||||||
|
return db_todo
|
||||||
|
|
||||||
|
@app.get("/todos", response_model=List[TodoResponse])
|
||||||
|
async def read_todos(status: Optional[str] = Query(None), db: Session = Depends(get_db)):
|
||||||
|
if status:
|
||||||
|
todos = db.query(Todo).filter(Todo.status == status).all()
|
||||||
|
else:
|
||||||
|
todos = db.query(Todo).all()
|
||||||
|
return todos
|
||||||
|
|
||||||
|
@app.get("/todos/{id}", response_model=TodoResponse)
|
||||||
|
async def read_todo(id: int, db: Session = Depends(get_db)):
|
||||||
|
todo = db.query(Todo).filter(Todo.id == id).first()
|
||||||
|
if not todo:
|
||||||
|
raise HTTPException(status_code=404, detail="Todo not found")
|
||||||
|
return todo
|
||||||
|
|
||||||
|
@app.put("/todos/{id}", response_model=TodoResponse)
|
||||||
|
async def update_todo(id: int, todo: TodoCreate, db: Session = Depends(get_db)):
|
||||||
|
db_todo = db.query(Todo).filter(Todo.id == id).first()
|
||||||
|
if not db_todo:
|
||||||
|
raise HTTPException(status_code=404, detail="Todo not found")
|
||||||
|
for key, value in todo.model_dump().items():
|
||||||
|
setattr(db_todo, key, value)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_todo)
|
||||||
|
return db_todo
|
||||||
|
|
||||||
|
@app.delete("/todos/{id}", status_code=204)
|
||||||
|
async def delete_todo(id: int, db: Session = Depends(get_db)):
|
||||||
|
db_todo = db.query(Todo).filter(Todo.id == id).first()
|
||||||
|
if not db_todo:
|
||||||
|
raise HTTPException(status_code=404, detail="Todo not found")
|
||||||
|
db.delete(db_todo)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
@app.patch("/todos/{id}/complete", status_code=204)
|
||||||
|
async def complete_todo(id: int, db: Session = Depends(get_db)):
|
||||||
|
db_todo = db.query(Todo).filter(Todo.id == id).first()
|
||||||
|
if not db_todo:
|
||||||
|
raise HTTPException(status_code=404, detail="Todo not found")
|
||||||
|
db_todo.status = 'completed'
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
@app.get("/todos/count", response_model=int)
|
||||||
|
async def get_todo_count(db: Session = Depends(get_db)):
|
||||||
|
return db.query(Todo).count()
|
||||||
|
|
||||||
|
@app.get("/todos/search?q={query}", response_model=List[TodoResponse])
|
||||||
|
async def search_todos(query: str, db: Session = Depends(get_db)):
|
||||||
|
todos = db.query(Todo).filter(Todo.title.contains(query)).all()
|
||||||
|
return todos
|
||||||
23
zipit/loop_runs/baseline_v1/models.py
Normal file
23
zipit/loop_runs/baseline_v1/models.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
from sqlalchemy import create_engine, Column, Integer, String, Text, Enum, DateTime
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
DATABASE_URL = "sqlite:///./todos.db"
|
||||||
|
engine = create_engine(DATABASE_URL)
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
class TodoStatus(Enum):
|
||||||
|
completed = 'completed'
|
||||||
|
pending = 'pending'
|
||||||
|
|
||||||
|
class Todo(Base):
|
||||||
|
__tablename__ = "todos"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
title = Column(String(255), nullable=False)
|
||||||
|
description = Column(Text, nullable=True)
|
||||||
|
status = Column(Enum(TodoStatus), default=TodoStatus.pending)
|
||||||
|
created_at = Column(DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
32
zipit/loop_runs/baseline_v1/pyproject.toml
Normal file
32
zipit/loop_runs/baseline_v1/pyproject.toml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
[project]
|
||||||
|
name = "todo-app"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "A simple todo application using FastAPI and SQLAlchemy"
|
||||||
|
authors = [
|
||||||
|
{ name="Your Name", email="your.email@example.com" }
|
||||||
|
]
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
"fastapi",
|
||||||
|
"uvicorn[standard]",
|
||||||
|
"sqlalchemy",
|
||||||
|
"python-dotenv"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.poetry]
|
||||||
|
name = "todo-app"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "A simple todo application using FastAPI and SQLAlchemy"
|
||||||
|
authors = [
|
||||||
|
{ name="Your Name", email="your.email@example.com" }
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
fastapi = "^0.85.0"
|
||||||
|
uvicorn = { version = "^0.20.0", extras = ["standard"] }
|
||||||
|
sqlalchemy = "^1.4.36"
|
||||||
|
python-dotenv = "^0.21.0"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core>=1.0.0"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
105
zipit/loop_runs/baseline_v1/report.json
Normal file
105
zipit/loop_runs/baseline_v1/report.json
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
{
|
||||||
|
"run_id": "baseline_v1",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step": "requirements",
|
||||||
|
"agent": "client",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 7.5,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 1196
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "models.py",
|
||||||
|
"agent": "data",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 5.8,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 824
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "schemas.py",
|
||||||
|
"agent": "coder",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 4.0,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 361
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "main.py",
|
||||||
|
"agent": "coder",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 18.9,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 2617
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "pyproject.toml",
|
||||||
|
"agent": "coder",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 7.9,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 724
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "test_main.py",
|
||||||
|
"agent": "qa",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 15.3,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 1689
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "Dockerfile",
|
||||||
|
"agent": "tester",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 3.3,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 249
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"model": "qwen2.5-coder:7b-instruct-q4_K_M",
|
||||||
|
"summary": "7/7 passed"
|
||||||
|
}
|
||||||
34
zipit/loop_runs/baseline_v1/requirements
Normal file
34
zipit/loop_runs/baseline_v1/requirements
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
**PROJECT NAME:** Todo Manager
|
||||||
|
|
||||||
|
**GOAL:** A simple todo application for individuals to manage their tasks efficiently.
|
||||||
|
|
||||||
|
**CORE FEATURES:**
|
||||||
|
1. Create a new todo item with title and description.
|
||||||
|
2. Read (view) all todos or filter by status (e.g., completed, pending).
|
||||||
|
3. Update an existing todo item's details.
|
||||||
|
4. Delete a todo item.
|
||||||
|
5. Mark a todo as completed.
|
||||||
|
6. View the total number of todos.
|
||||||
|
7. Search for todos by partial title.
|
||||||
|
|
||||||
|
**DATA MODEL:**
|
||||||
|
1. **Todo**
|
||||||
|
- id (integer, primary key)
|
||||||
|
- title (string, required)
|
||||||
|
- description (text, optional)
|
||||||
|
- status (enum [completed, pending], default: pending)
|
||||||
|
- created_at (timestamp, auto-generated)
|
||||||
|
|
||||||
|
**API ENDPOINTS:**
|
||||||
|
1. **POST /todos**: Create a new todo item.
|
||||||
|
2. **GET /todos**: Retrieve all todos.
|
||||||
|
3. **GET /todos/status/{status}**: Filter todos by status.
|
||||||
|
4. **PUT /todos/{id}**: Update an existing todo item.
|
||||||
|
5. **DELETE /todos/{id}**: Delete a todo item.
|
||||||
|
6. **PATCH /todos/{id}/complete**: Mark a todo as completed.
|
||||||
|
7. **GET /todos/count**: Get the total number of todos.
|
||||||
|
8. **GET /todos/search?q={query}**: Search for todos by partial title.
|
||||||
|
|
||||||
|
**CONSTRAINTS:**
|
||||||
|
- Must use SQLite as the database.
|
||||||
|
- No authentication is required.
|
||||||
16
zipit/loop_runs/baseline_v1/schemas.py
Normal file
16
zipit/loop_runs/baseline_v1/schemas.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
class TodoCreate(BaseModel):
|
||||||
|
title: str = Field(..., max_length=255)
|
||||||
|
description: str | None = Field(None)
|
||||||
|
|
||||||
|
class TodoResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
title: str
|
||||||
|
description: str | None
|
||||||
|
status: str
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
59
zipit/loop_runs/baseline_v1/test_main.py
Normal file
59
zipit/loop_runs/baseline_v1/test_main.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from main import app
|
||||||
|
from models import Base
|
||||||
|
from main import get_db
|
||||||
|
|
||||||
|
engine = create_engine("sqlite:///:memory:", connect_args={"check_same_thread": False})
|
||||||
|
TestSession = sessionmaker(bind=engine)
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
|
def override_get_db():
|
||||||
|
db = TestSession()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
app.dependency_overrides[get_db] = override_get_db
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
def test_create_todo():
|
||||||
|
response = client.post("/todos", json={"title": "Test Todo"})
|
||||||
|
assert response.status_code == 201
|
||||||
|
todo = response.json()
|
||||||
|
assert todo["title"] == "Test Todo"
|
||||||
|
assert todo["status"] == "pending"
|
||||||
|
|
||||||
|
def test_read_todos():
|
||||||
|
response = client.get("/todos")
|
||||||
|
assert response.status_code == 200
|
||||||
|
todos = response.json()
|
||||||
|
assert len(todos) == 1
|
||||||
|
|
||||||
|
def test_get_todo_by_id():
|
||||||
|
response = client.get("/todos/1")
|
||||||
|
assert response.status_code == 200
|
||||||
|
todo = response.json()
|
||||||
|
assert todo["id"] == 1
|
||||||
|
assert todo["title"] == "Test Todo"
|
||||||
|
|
||||||
|
def test_get_nonexistent_todo():
|
||||||
|
response = client.get("/todos/999")
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
def test_update_todo():
|
||||||
|
response = client.put("/todos/1", json={"title": "Updated Todo"})
|
||||||
|
assert response.status_code == 200
|
||||||
|
todo = response.json()
|
||||||
|
assert todo["id"] == 1
|
||||||
|
assert todo["title"] == "Updated Todo"
|
||||||
|
|
||||||
|
def test_delete_todo():
|
||||||
|
response = client.delete("/todos/1")
|
||||||
|
assert response.status_code == 204
|
||||||
|
|
||||||
|
def test_complete_todo():
|
||||||
|
response = client.patch("/todos/1/complete")
|
||||||
|
assert response.status_code == 204
|
||||||
18
zipit/loop_runs/v2_strict/Dockerfile
Normal file
18
zipit/loop_runs/v2_strict/Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
```Dockerfile
|
||||||
|
FROM python:3.12-slim as builder
|
||||||
|
|
||||||
|
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY pyproject.toml .
|
||||||
|
RUN uv sync
|
||||||
|
|
||||||
|
COPY models.py schemas.py main.py test_main.py .
|
||||||
|
|
||||||
|
USER nonrootuser
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
|
```
|
||||||
64
zipit/loop_runs/v2_strict/main.py
Normal file
64
zipit/loop_runs/v2_strict/main.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
from fastapi import FastAPI, HTTPException, Depends, Query
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from typing import List, Optional
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from models import Task, engine
|
||||||
|
from schemas import TaskCreate, TaskResponse
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
@app.post("/tasks/", response_model=TaskResponse, status_code=201)
|
||||||
|
async def create_task(task: TaskCreate, db: Session = Depends(get_db)):
|
||||||
|
db_task = Task(**task.model_dump())
|
||||||
|
db.add(db_task)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_task)
|
||||||
|
return db_task
|
||||||
|
|
||||||
|
@app.get("/tasks/", response_model=List[TaskResponse])
|
||||||
|
async def get_tasks(status: Optional[str] = Query(None), db: Session = Depends(get_db)):
|
||||||
|
if status:
|
||||||
|
tasks = db.query(Task).filter_by(status=status).all()
|
||||||
|
else:
|
||||||
|
tasks = db.query(Task).all()
|
||||||
|
return tasks
|
||||||
|
|
||||||
|
@app.get("/tasks/{task_id}", response_model=TaskResponse)
|
||||||
|
async def get_task(task_id: int, db: Session = Depends(get_db)):
|
||||||
|
task = db.query(Task).filter(Task.id == task_id).first()
|
||||||
|
if not task:
|
||||||
|
raise HTTPException(status_code=404, detail="Task not found")
|
||||||
|
return task
|
||||||
|
|
||||||
|
@app.put("/tasks/{task_id}", response_model=TaskResponse)
|
||||||
|
async def update_task(task_id: int, task: TaskCreate, db: Session = Depends(get_db)):
|
||||||
|
db_task = db.query(Task).filter(Task.id == task_id).first()
|
||||||
|
if not db_task:
|
||||||
|
raise HTTPException(status_code=404, detail="Task not found")
|
||||||
|
|
||||||
|
for key, value in task.model_dump().items():
|
||||||
|
setattr(db_task, key, value)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_task)
|
||||||
|
return db_task
|
||||||
|
|
||||||
|
@app.delete("/tasks/{task_id}", status_code=204)
|
||||||
|
async def delete_task(task_id: int, db: Session = Depends(get_db)):
|
||||||
|
db_task = db.query(Task).filter(Task.id == task_id).first()
|
||||||
|
if not db_task:
|
||||||
|
raise HTTPException(status_code=404, detail="Task not found")
|
||||||
|
|
||||||
|
db.delete(db_task)
|
||||||
|
db.commit()
|
||||||
|
return
|
||||||
18
zipit/loop_runs/v2_strict/models.py
Normal file
18
zipit/loop_runs/v2_strict/models.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from sqlalchemy import create_engine, Column, Integer, String, Text, Date, Enum
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
DATABASE_URL = "sqlite:///./todo.db"
|
||||||
|
engine = create_engine(DATABASE_URL, connect_args={'check_same_thread': False})
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
class Task(Base):
|
||||||
|
__tablename__ = "tasks"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
title = Column(String(255), nullable=False)
|
||||||
|
description = Column(Text)
|
||||||
|
due_date = Column(Date)
|
||||||
|
status = Column(Enum('pending', 'completed'), default='pending')
|
||||||
|
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
13
zipit/loop_runs/v2_strict/pyproject.toml
Normal file
13
zipit/loop_runs/v2_strict/pyproject.toml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[project]
|
||||||
|
name = "todo-app"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "A simple task management API using FastAPI and SQLAlchemy"
|
||||||
|
authors = [
|
||||||
|
{ name="Your Name", email="your.email@example.com" }
|
||||||
|
]
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
"fastapi",
|
||||||
|
"uvicorn[standard]",
|
||||||
|
"sqlalchemy"
|
||||||
|
]
|
||||||
113
zipit/loop_runs/v2_strict/report.json
Normal file
113
zipit/loop_runs/v2_strict/report.json
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
{
|
||||||
|
"run_id": "v2_strict",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step": "requirements",
|
||||||
|
"agent": "client",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 6.6,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 1190
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "models.py",
|
||||||
|
"agent": "data",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 5.3,
|
||||||
|
"errors": [
|
||||||
|
"SQLite create_engine puuttuu connect_args={'check_same_thread': False}"
|
||||||
|
],
|
||||||
|
"code_length": 699
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attempt": 2,
|
||||||
|
"elapsed": 4.4,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 712
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "schemas.py",
|
||||||
|
"agent": "coder",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 5.2,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 494
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "main.py",
|
||||||
|
"agent": "coder",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 14.4,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 2036
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "pyproject.toml",
|
||||||
|
"agent": "coder",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 3.3,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 267
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "test_main.py",
|
||||||
|
"agent": "qa",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 16.4,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 2071
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "Dockerfile",
|
||||||
|
"agent": "tester",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 3.5,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 314
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"model": "qwen2.5-coder:7b-instruct-q4_K_M",
|
||||||
|
"summary": "7/7 passed"
|
||||||
|
}
|
||||||
32
zipit/loop_runs/v2_strict/requirements
Normal file
32
zipit/loop_runs/v2_strict/requirements
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
**PROJECT NAME:** Todo-sovellus
|
||||||
|
|
||||||
|
**GOAL:** Create a simple task management application for individuals to keep track of their daily tasks and manage them efficiently.
|
||||||
|
|
||||||
|
**CORE FEATURES:**
|
||||||
|
1. Add new tasks with title, description, due date, and status.
|
||||||
|
2. View all tasks in a list format.
|
||||||
|
3. Filter tasks by status (e.g., pending, completed).
|
||||||
|
4. Update task details such as title, description, due date, and status.
|
||||||
|
5. Delete individual tasks.
|
||||||
|
6. Mark tasks as complete or incomplete.
|
||||||
|
7. Search for tasks by keywords in the title or description.
|
||||||
|
|
||||||
|
**DATA MODEL:**
|
||||||
|
1. **Task**
|
||||||
|
- id (integer, primary key)
|
||||||
|
- title (string, required)
|
||||||
|
- description (text, optional)
|
||||||
|
- due_date (date, optional)
|
||||||
|
- status (enum ['pending', 'completed'], default='pending')
|
||||||
|
|
||||||
|
**API ENDPOINTS:**
|
||||||
|
1. **GET /tasks** - Retrieve all tasks.
|
||||||
|
2. **POST /tasks** - Create a new task.
|
||||||
|
3. **GET /tasks/{id}** - Retrieve a specific task by ID.
|
||||||
|
4. **PUT /tasks/{id}** - Update an existing task.
|
||||||
|
5. **DELETE /tasks/{id}** - Delete a specific task.
|
||||||
|
6. **GET /tasks/status/{status}** - Filter tasks by status.
|
||||||
|
|
||||||
|
**CONSTRAINTS:**
|
||||||
|
- Must use SQLite as the database.
|
||||||
|
- No authentication is required for accessing endpoints.
|
||||||
18
zipit/loop_runs/v2_strict/schemas.py
Normal file
18
zipit/loop_runs/v2_strict/schemas.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
class TaskCreate(BaseModel):
|
||||||
|
title: str = Field(..., min_length=1, max_length=255)
|
||||||
|
description: str | None = Field(None, max_length=255)
|
||||||
|
due_date: date | None = Field(None)
|
||||||
|
status: str = Field('pending', regex='^(pending|completed)$')
|
||||||
|
|
||||||
|
class TaskResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
title: str
|
||||||
|
description: str | None
|
||||||
|
due_date: date | None
|
||||||
|
status: str
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
57
zipit/loop_runs/v2_strict/test_main.py
Normal file
57
zipit/loop_runs/v2_strict/test_main.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from main import app
|
||||||
|
from models import Base
|
||||||
|
|
||||||
|
engine = create_engine("sqlite:///:memory:", connect_args={"check_same_thread": False})
|
||||||
|
TestSession = sessionmaker(bind=engine)
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
|
def override_get_db():
|
||||||
|
db = TestSession()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
app.dependency_overrides[get_db] = override_get_db
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
def test_create_task():
|
||||||
|
response = client.post("/tasks/", json={"title": "Test Task", "description": "This is a test task"})
|
||||||
|
assert response.status_code == 201
|
||||||
|
assert response.json()["title"] == "Test Task"
|
||||||
|
assert response.json()["description"] == "This is a test task"
|
||||||
|
|
||||||
|
def test_get_tasks():
|
||||||
|
response = client.get("/tasks/")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert len(response.json()) > 0
|
||||||
|
|
||||||
|
def test_get_task_by_id():
|
||||||
|
response = client.post("/tasks/", json={"title": "Test Task", "description": "This is a test task"})
|
||||||
|
task_id = response.json()["id"]
|
||||||
|
response = client.get(f"/tasks/{task_id}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["id"] == task_id
|
||||||
|
|
||||||
|
def test_get_task_by_id_not_found():
|
||||||
|
response = client.get("/tasks/999")
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
def test_update_task():
|
||||||
|
response = client.post("/tasks/", json={"title": "Test Task", "description": "This is a test task"})
|
||||||
|
task_id = response.json()["id"]
|
||||||
|
updated_data = {"title": "Updated Test Task"}
|
||||||
|
response = client.put(f"/tasks/{task_id}", json=updated_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["title"] == "Updated Test Task"
|
||||||
|
|
||||||
|
def test_delete_task():
|
||||||
|
response = client.post("/tasks/", json={"title": "Test Task", "description": "This is a test task"})
|
||||||
|
task_id = response.json()["id"]
|
||||||
|
response = client.delete(f"/tasks/{task_id}")
|
||||||
|
assert response.status_code == 204
|
||||||
|
response = client.get(f"/tasks/{task_id}")
|
||||||
|
assert response.status_code == 404
|
||||||
16
zipit/loop_runs/v3_docker/Dockerfile
Normal file
16
zipit/loop_runs/v3_docker/Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
FROM python:3.12-slim as builder
|
||||||
|
|
||||||
|
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY pyproject.toml .
|
||||||
|
RUN uv sync
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
USER 1000
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
10
zipit/loop_runs/v3_docker/docker-compose.yml
Normal file
10
zipit/loop_runs/v3_docker/docker-compose.yml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "18765:8000"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/docs')"]
|
||||||
|
interval: 3s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
64
zipit/loop_runs/v3_docker/main.py
Normal file
64
zipit/loop_runs/v3_docker/main.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
from fastapi import FastAPI, HTTPException, Depends, Query
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from typing import List, Optional
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from .models import Todo, SessionLocal
|
||||||
|
from .schemas import TodoCreate, TodoResponse
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
# Dependency to get database session
|
||||||
|
def get_db():
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
@app.post("/todos", response_model=TodoResponse, status_code=201)
|
||||||
|
async def create_todo(todo: TodoCreate, db: Session = Depends(get_db)):
|
||||||
|
db_todo = Todo(**todo.model_dump())
|
||||||
|
db.add(db_todo)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_todo)
|
||||||
|
return db_todo
|
||||||
|
|
||||||
|
@app.get("/todos", response_model=List[TodoResponse])
|
||||||
|
async def read_todos(status: Optional[str] = Query(None), db: Session = Depends(get_db)):
|
||||||
|
if status:
|
||||||
|
todos = db.query(Todo).filter(Todo.status == status).all()
|
||||||
|
else:
|
||||||
|
todos = db.query(Todo).all()
|
||||||
|
return todos
|
||||||
|
|
||||||
|
@app.get("/todos/{todo_id}", response_model=TodoResponse)
|
||||||
|
async def read_todo(todo_id: int, db: Session = Depends(get_db)):
|
||||||
|
todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
||||||
|
if not todo:
|
||||||
|
raise HTTPException(status_code=404, detail="Todo not found")
|
||||||
|
return todo
|
||||||
|
|
||||||
|
@app.put("/todos/{todo_id}", response_model=TodoResponse)
|
||||||
|
async def update_todo(todo_id: int, todo_update: TodoCreate, db: Session = Depends(get_db)):
|
||||||
|
todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
||||||
|
if not todo:
|
||||||
|
raise HTTPException(status_code=404, detail="Todo not found")
|
||||||
|
|
||||||
|
todo.title = todo_update.title
|
||||||
|
todo.description = todo_update.description
|
||||||
|
todo.due_date = todo_update.due_date
|
||||||
|
todo.status = todo_update.status
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
db.refresh(todo)
|
||||||
|
return todo
|
||||||
|
|
||||||
|
@app.delete("/todos/{todo_id}", status_code=204)
|
||||||
|
async def delete_todo(todo_id: int, db: Session = Depends(get_db)):
|
||||||
|
todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
||||||
|
if not todo:
|
||||||
|
raise HTTPException(status_code=404, detail="Todo not found")
|
||||||
|
|
||||||
|
db.delete(todo)
|
||||||
|
db.commit()
|
||||||
18
zipit/loop_runs/v3_docker/models.py
Normal file
18
zipit/loop_runs/v3_docker/models.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from sqlalchemy import create_engine, Column, Integer, String, Text, Date, Enum
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
DATABASE_URL = "sqlite:///./todos.db"
|
||||||
|
engine = create_engine(DATABASE_URL, connect_args={'check_same_thread': False})
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
class Todo(Base):
|
||||||
|
__tablename__ = "todos"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
title = Column(String(255), nullable=False)
|
||||||
|
description = Column(Text, nullable=False)
|
||||||
|
due_date = Column(Date, nullable=False)
|
||||||
|
status = Column(Enum("pending", "completed"), nullable=False)
|
||||||
|
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
16
zipit/loop_runs/v3_docker/pyproject.toml
Normal file
16
zipit/loop_runs/v3_docker/pyproject.toml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[project]
|
||||||
|
name = "todo-app"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "A simple todo app using FastAPI and SQLAlchemy"
|
||||||
|
authors = [
|
||||||
|
{ name="Your Name", email="your.email@example.com" }
|
||||||
|
]
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
"fastapi",
|
||||||
|
"uvicorn[standard]",
|
||||||
|
"sqlalchemy",
|
||||||
|
"pytest",
|
||||||
|
"httpx",
|
||||||
|
"python-dateutil"
|
||||||
|
]
|
||||||
134
zipit/loop_runs/v3_docker/report.json
Normal file
134
zipit/loop_runs/v3_docker/report.json
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
{
|
||||||
|
"run_id": "v3_docker",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step": "requirements",
|
||||||
|
"agent": "client",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 6.2,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 1105
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "models.py",
|
||||||
|
"agent": "data",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 5.4,
|
||||||
|
"errors": [
|
||||||
|
"SQLite create_engine puuttuu connect_args={'check_same_thread': False}"
|
||||||
|
],
|
||||||
|
"code_length": 672
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attempt": 2,
|
||||||
|
"elapsed": 4.6,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 742
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "schemas.py",
|
||||||
|
"agent": "coder",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 4.2,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 370
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "main.py",
|
||||||
|
"agent": "coder",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 14.9,
|
||||||
|
"errors": [
|
||||||
|
"Kutsutaan 'Depends()' mutta sitä ei ole importattu eikä määritelty",
|
||||||
|
"Kutsutaan 'Depends()' mutta sitä ei ole importattu eikä määritelty",
|
||||||
|
"Kutsutaan 'Depends()' mutta sitä ei ole importattu eikä määritelty",
|
||||||
|
"Kutsutaan 'Depends()' mutta sitä ei ole importattu eikä määritelty",
|
||||||
|
"Kutsutaan 'Depends()' mutta sitä ei ole importattu eikä määritelty"
|
||||||
|
],
|
||||||
|
"code_length": 1975
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attempt": 2,
|
||||||
|
"elapsed": 14.4,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 2058
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "pyproject.toml",
|
||||||
|
"agent": "coder",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 3.6,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 279
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "test_main.py",
|
||||||
|
"agent": "qa",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 21.1,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 2415
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "Dockerfile",
|
||||||
|
"agent": "tester",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 3.3,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 249
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"model": "qwen2.5-coder:7b-instruct-q4_K_M",
|
||||||
|
"summary": "7/7 passed",
|
||||||
|
"docker": {
|
||||||
|
"build": true,
|
||||||
|
"pytest": false,
|
||||||
|
"api": false,
|
||||||
|
"errors": [
|
||||||
|
"pytest failed (exit 2):\n Network v3_docker_default Creating\n Network v3_docker_default Created\nerror: Failed to initialize cache at `/.cache/uv`\n Caused by: failed to create directory `/.cache/uv`: Permission denied (os error 13)\n\n",
|
||||||
|
"Kontti ei käynnistynyt 15s:ssa"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
30
zipit/loop_runs/v3_docker/requirements
Normal file
30
zipit/loop_runs/v3_docker/requirements
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
**PROJECT NAME:** TodoSovellus
|
||||||
|
|
||||||
|
**GOAL:** To create a simple task management application for individuals to keep track of their daily tasks and manage them efficiently.
|
||||||
|
|
||||||
|
**CORE FEATURES:**
|
||||||
|
1. Create new todos with title, description, due date, and status.
|
||||||
|
2. Read (view) all todos and filter by status (e.g., pending, completed).
|
||||||
|
3. Update existing todos to change details like title, description, due date, or status.
|
||||||
|
4. Delete todos that are no longer needed.
|
||||||
|
5. Mark todos as completed or pending.
|
||||||
|
6. View the total number of todos and count by status.
|
||||||
|
|
||||||
|
**DATA MODEL:**
|
||||||
|
1. **Todo**
|
||||||
|
- id (integer, primary key)
|
||||||
|
- title (string)
|
||||||
|
- description (text)
|
||||||
|
- due_date (date)
|
||||||
|
- status (enum: "pending", "completed")
|
||||||
|
|
||||||
|
**API ENDPOINTS:**
|
||||||
|
1. **POST /todos** - Create a new todo
|
||||||
|
2. **GET /todos** - Retrieve all todos, optionally filter by status
|
||||||
|
3. **GET /todos/{id}** - Retrieve a single todo by ID
|
||||||
|
4. **PUT /todos/{id}** - Update an existing todo
|
||||||
|
5. **DELETE /todos/{id}** - Delete a todo
|
||||||
|
|
||||||
|
**CONSTRAINTS:**
|
||||||
|
- Must use SQLite as the database.
|
||||||
|
- No authentication is required for accessing endpoints.
|
||||||
18
zipit/loop_runs/v3_docker/schemas.py
Normal file
18
zipit/loop_runs/v3_docker/schemas.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
class TodoCreate(BaseModel):
|
||||||
|
title: str = Field(..., max_length=255)
|
||||||
|
description: str
|
||||||
|
due_date: date
|
||||||
|
status: str = "pending"
|
||||||
|
|
||||||
|
class TodoResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
title: str
|
||||||
|
description: str
|
||||||
|
due_date: date
|
||||||
|
status: str
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
57
zipit/loop_runs/v3_docker/test_main.py
Normal file
57
zipit/loop_runs/v3_docker/test_main.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from main import app
|
||||||
|
from models import Base
|
||||||
|
from main import get_db
|
||||||
|
|
||||||
|
engine = create_engine("sqlite:///:memory:", connect_args={"check_same_thread": False})
|
||||||
|
TestSession = sessionmaker(bind=engine)
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
|
def override_get_db():
|
||||||
|
db = TestSession()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
app.dependency_overrides[get_db] = override_get_db
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
def test_create_todo():
|
||||||
|
response = client.post("/todos", json={"title": "Test Todo", "description": "This is a test todo.", "due_date": "2023-12-31"})
|
||||||
|
assert response.status_code == 201
|
||||||
|
assert response.json()["title"] == "Test Todo"
|
||||||
|
assert response.json()["status"] == "pending"
|
||||||
|
|
||||||
|
def test_read_todos():
|
||||||
|
client.post("/todos", json={"title": "Todo 1", "description": "First todo.", "due_date": "2023-12-31"})
|
||||||
|
client.post("/todos", json={"title": "Todo 2", "description": "Second todo.", "due_date": "2024-01-01"})
|
||||||
|
response = client.get("/todos")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert len(response.json()) == 2
|
||||||
|
|
||||||
|
def test_read_todo():
|
||||||
|
todo = client.post("/todos", json={"title": "Test Todo", "description": "This is a test todo.", "due_date": "2023-12-31"})
|
||||||
|
response = client.get(f"/todos/{todo.json()['id']}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["title"] == "Test Todo"
|
||||||
|
|
||||||
|
def test_read_todo_not_found():
|
||||||
|
response = client.get("/todos/999")
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
def test_update_todo():
|
||||||
|
todo = client.post("/todos", json={"title": "Test Todo", "description": "This is a test todo.", "due_date": "2023-12-31"})
|
||||||
|
response = client.put(f"/todos/{todo.json()['id']}", json={"title": "Updated Todo", "description": "Updated description.", "due_date": "2024-01-01", "status": "completed"})
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["title"] == "Updated Todo"
|
||||||
|
assert response.json()["status"] == "completed"
|
||||||
|
|
||||||
|
def test_delete_todo():
|
||||||
|
todo = client.post("/todos", json={"title": "Test Todo", "description": "This is a test todo.", "due_date": "2023-12-31"})
|
||||||
|
response = client.delete(f"/todos/{todo.json()['id']}")
|
||||||
|
assert response.status_code == 204
|
||||||
|
response = client.get(f"/todos/{todo.json()['id']}")
|
||||||
|
assert response.status_code == 404
|
||||||
11
zipit/loop_runs/v4_docker/Dockerfile
Normal file
11
zipit/loop_runs/v4_docker/Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
FROM python:3.12-slim
|
||||||
|
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
|
||||||
|
ENV UV_CACHE_DIR=/tmp/uv-cache
|
||||||
|
WORKDIR /app
|
||||||
|
COPY pyproject.toml .
|
||||||
|
RUN uv sync
|
||||||
|
COPY *.py .
|
||||||
|
RUN useradd -m appuser
|
||||||
|
USER appuser
|
||||||
|
EXPOSE 8000
|
||||||
|
CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
58
zipit/loop_runs/v4_docker/main.py
Normal file
58
zipit/loop_runs/v4_docker/main.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
from fastapi import FastAPI, Depends, HTTPException
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from models import Todo, SessionLocal
|
||||||
|
from schemas import TodoCreate, TodoResponse
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
# Dependency to get database session
|
||||||
|
def get_db():
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
@app.post("/todos", response_model=TodoResponse, status_code=201)
|
||||||
|
def create_todo(todo: TodoCreate, db: Session = Depends(get_db)):
|
||||||
|
db_todo = Todo(**todo.model_dump())
|
||||||
|
db.add(db_todo)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_todo)
|
||||||
|
return db_todo
|
||||||
|
|
||||||
|
@app.get("/todos", response_model=List[TodoResponse])
|
||||||
|
def read_todos(status: Optional[str] = None, db: Session = Depends(get_db)):
|
||||||
|
if status:
|
||||||
|
todos = db.query(Todo).filter(Todo.status == status).all()
|
||||||
|
else:
|
||||||
|
todos = db.query(Todo).all()
|
||||||
|
return todos
|
||||||
|
|
||||||
|
@app.get("/todos/{todo_id}", response_model=TodoResponse)
|
||||||
|
def read_todo(todo_id: int, db: Session = Depends(get_db)):
|
||||||
|
todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
||||||
|
if not todo:
|
||||||
|
raise HTTPException(status_code=404, detail="Todo not found")
|
||||||
|
return todo
|
||||||
|
|
||||||
|
@app.put("/todos/{todo_id}", response_model=TodoResponse)
|
||||||
|
def update_todo(todo_id: int, todo: TodoCreate, db: Session = Depends(get_db)):
|
||||||
|
db_todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
||||||
|
if not db_todo:
|
||||||
|
raise HTTPException(status_code=404, detail="Todo not found")
|
||||||
|
for key, value in todo.model_dump().items():
|
||||||
|
setattr(db_todo, key, value)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_todo)
|
||||||
|
return db_todo
|
||||||
|
|
||||||
|
@app.delete("/todos/{todo_id}", status_code=204)
|
||||||
|
def delete_todo(todo_id: int, db: Session = Depends(get_db)):
|
||||||
|
db_todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
||||||
|
if not db_todo:
|
||||||
|
raise HTTPException(status_code=404, detail="Todo not found")
|
||||||
|
db.delete(db_todo)
|
||||||
|
db.commit()
|
||||||
18
zipit/loop_runs/v4_docker/models.py
Normal file
18
zipit/loop_runs/v4_docker/models.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from sqlalchemy import create_engine, Column, Integer, String, Text, Date, Enum
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
DATABASE_URL = "sqlite:///./todos.db"
|
||||||
|
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
class Todo(Base):
|
||||||
|
__tablename__ = "todos"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
title = Column(String(255), nullable=False)
|
||||||
|
description = Column(Text, nullable=True)
|
||||||
|
due_date = Column(Date, nullable=True)
|
||||||
|
status = Column(Enum('pending', 'completed'), nullable=False)
|
||||||
|
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
13
zipit/loop_runs/v4_docker/pyproject.toml
Normal file
13
zipit/loop_runs/v4_docker/pyproject.toml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[project]
|
||||||
|
name = "todo-app"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "A simple todo application using FastAPI and SQLAlchemy"
|
||||||
|
authors = [
|
||||||
|
{ name="Your Name", email="your.email@example.com" }
|
||||||
|
]
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
"fastapi",
|
||||||
|
"uvicorn[standard]",
|
||||||
|
"sqlalchemy"
|
||||||
|
]
|
||||||
145
zipit/loop_runs/v4_docker/report.json
Normal file
145
zipit/loop_runs/v4_docker/report.json
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
{
|
||||||
|
"run_id": "v4_docker",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step": "requirements",
|
||||||
|
"agent": "client",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 6.6,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 1106
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "models.py",
|
||||||
|
"agent": "data",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 5.4,
|
||||||
|
"errors": [
|
||||||
|
"SQLite create_engine puuttuu connect_args={'check_same_thread': False}"
|
||||||
|
],
|
||||||
|
"code_length": 696
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attempt": 2,
|
||||||
|
"elapsed": 4.7,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 740
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "schemas.py",
|
||||||
|
"agent": "coder",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 5.1,
|
||||||
|
"errors": [
|
||||||
|
"SyntaxError: invalid syntax (rivi 8)"
|
||||||
|
],
|
||||||
|
"code_length": 472
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attempt": 2,
|
||||||
|
"elapsed": 2.9,
|
||||||
|
"errors": [
|
||||||
|
"SyntaxError: invalid syntax (rivi 8)"
|
||||||
|
],
|
||||||
|
"code_length": 372
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attempt": 3,
|
||||||
|
"elapsed": 3.5,
|
||||||
|
"errors": [
|
||||||
|
"SyntaxError: invalid syntax (rivi 8)"
|
||||||
|
],
|
||||||
|
"code_length": 472
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [
|
||||||
|
"SyntaxError: invalid syntax (rivi 8)"
|
||||||
|
],
|
||||||
|
"passed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "main.py",
|
||||||
|
"agent": "coder",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 14.6,
|
||||||
|
"errors": [
|
||||||
|
"Kutsutaan 'Depends()' mutta sitä ei ole importattu eikä määritelty",
|
||||||
|
"Kutsutaan 'Depends()' mutta sitä ei ole importattu eikä määritelty",
|
||||||
|
"Kutsutaan 'Depends()' mutta sitä ei ole importattu eikä määritelty",
|
||||||
|
"Kutsutaan 'Depends()' mutta sitä ei ole importattu eikä määritelty",
|
||||||
|
"Kutsutaan 'Depends()' mutta sitä ei ole importattu eikä määritelty"
|
||||||
|
],
|
||||||
|
"code_length": 1984
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attempt": 2,
|
||||||
|
"elapsed": 14.0,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 1911
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "pyproject.toml",
|
||||||
|
"agent": "coder",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 3.6,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 264
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "test_main.py",
|
||||||
|
"agent": "qa",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 15.9,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 1779
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "Dockerfile",
|
||||||
|
"agent": "tester",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 4.0,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 291
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"model": "qwen2.5-coder:7b-instruct-q4_K_M",
|
||||||
|
"summary": "6/7 passed"
|
||||||
|
}
|
||||||
30
zipit/loop_runs/v4_docker/requirements
Normal file
30
zipit/loop_runs/v4_docker/requirements
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
**PROJECT NAME:** Todo-sovellus
|
||||||
|
|
||||||
|
**GOAL:** The software is a simple task manager for individuals to keep track of their daily tasks and manage them efficiently.
|
||||||
|
|
||||||
|
**CORE FEATURES:**
|
||||||
|
1. Create new todos with title, description, due date, and status.
|
||||||
|
2. Read (view) all todos or filter by status (e.g., pending, completed).
|
||||||
|
3. Update existing todos (change title, description, due date, status).
|
||||||
|
4. Delete todos that are no longer needed.
|
||||||
|
5. Mark a todo as complete.
|
||||||
|
6. View the total number of todos and count by status.
|
||||||
|
|
||||||
|
**DATA MODEL:**
|
||||||
|
1. **Todo**
|
||||||
|
- id (integer, primary key)
|
||||||
|
- title (string, required)
|
||||||
|
- description (text, optional)
|
||||||
|
- due_date (date, optional)
|
||||||
|
- status (enum: 'pending', 'completed')
|
||||||
|
|
||||||
|
**API ENDPOINTS:**
|
||||||
|
1. **GET /todos** - Retrieve all todos.
|
||||||
|
2. **GET /todos?status=...** - Filter todos by status (e.g., `/todos?status=pending`).
|
||||||
|
3. **POST /todos** - Create a new todo.
|
||||||
|
4. **PUT /todos/{id}** - Update an existing todo.
|
||||||
|
5. **DELETE /todos/{id}** - Delete a todo.
|
||||||
|
|
||||||
|
**CONSTRAINTS:**
|
||||||
|
- Must use SQLite as the database.
|
||||||
|
- No authentication is required for accessing endpoints.
|
||||||
18
zipit/loop_runs/v4_docker/schemas.py
Normal file
18
zipit/loop_runs/v4_docker/schemas.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
class TodoCreate(BaseModel):
|
||||||
|
title: str = Field(..., max_length=255)
|
||||||
|
description: str | None = Field(None, max_length=255)
|
||||||
|
due_date: date | None = Field(None)
|
||||||
|
status: str = Field(..., in=["pending", "completed"])
|
||||||
|
|
||||||
|
class TodoResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
title: str
|
||||||
|
description: str | None
|
||||||
|
due_date: date | None
|
||||||
|
status: str
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
59
zipit/loop_runs/v4_docker/test_main.py
Normal file
59
zipit/loop_runs/v4_docker/test_main.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from main import app
|
||||||
|
from models import Base
|
||||||
|
from main import get_db
|
||||||
|
|
||||||
|
engine = create_engine("sqlite:///:memory:", connect_args={"check_same_thread": False})
|
||||||
|
TestSession = sessionmaker(bind=engine)
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
|
def override_get_db():
|
||||||
|
db = TestSession()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
app.dependency_overrides[get_db] = override_get_db
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
def test_create_todo():
|
||||||
|
response = client.post("/todos", json={"title": "Test Todo", "status": "pending"})
|
||||||
|
assert response.status_code == 201
|
||||||
|
todo = response.json()
|
||||||
|
assert todo["title"] == "Test Todo"
|
||||||
|
assert todo["status"] == "pending"
|
||||||
|
|
||||||
|
def test_read_todos():
|
||||||
|
response = client.get("/todos")
|
||||||
|
assert response.status_code == 200
|
||||||
|
todos = response.json()
|
||||||
|
assert len(todos) == 1
|
||||||
|
|
||||||
|
def test_get_todo_by_id():
|
||||||
|
response = client.get("/todos/1")
|
||||||
|
assert response.status_code == 200
|
||||||
|
todo = response.json()
|
||||||
|
assert todo["id"] == 1
|
||||||
|
assert todo["title"] == "Test Todo"
|
||||||
|
assert todo["status"] == "pending"
|
||||||
|
|
||||||
|
def test_get_todo_by_id_not_found():
|
||||||
|
response = client.get("/todos/999")
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
def test_update_todo():
|
||||||
|
response = client.put("/todos/1", json={"title": "Updated Todo", "status": "completed"})
|
||||||
|
assert response.status_code == 200
|
||||||
|
todo = response.json()
|
||||||
|
assert todo["id"] == 1
|
||||||
|
assert todo["title"] == "Updated Todo"
|
||||||
|
assert todo["status"] == "completed"
|
||||||
|
|
||||||
|
def test_delete_todo():
|
||||||
|
response = client.delete("/todos/1")
|
||||||
|
assert response.status_code == 204
|
||||||
|
response = client.get("/todos/1")
|
||||||
|
assert response.status_code == 404
|
||||||
11
zipit/loop_runs/v5_feedback/Dockerfile
Normal file
11
zipit/loop_runs/v5_feedback/Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
FROM python:3.12-slim
|
||||||
|
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
|
||||||
|
ENV UV_CACHE_DIR=/tmp/uv-cache
|
||||||
|
WORKDIR /app
|
||||||
|
COPY pyproject.toml .
|
||||||
|
RUN uv sync
|
||||||
|
COPY *.py .
|
||||||
|
RUN useradd -m appuser
|
||||||
|
USER appuser
|
||||||
|
EXPOSE 8000
|
||||||
|
CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
74
zipit/loop_runs/v5_feedback/main.py
Normal file
74
zipit/loop_runs/v5_feedback/main.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
from fastapi import FastAPI, HTTPException, Depends, Query
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from typing import List, Optional
|
||||||
|
from pydantic.v1 import model_dump
|
||||||
|
|
||||||
|
from models import Task, User, Base, engine
|
||||||
|
from schemas import TaskCreate, TaskResponse
|
||||||
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
@app.post("/tasks/", response_model=TaskResponse, status_code=201)
|
||||||
|
async def create_task(task: TaskCreate, db: Session = Depends(get_db)):
|
||||||
|
db_task = Task(**model_dump(task))
|
||||||
|
db.add(db_task)
|
||||||
|
try:
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_task)
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
return db_task
|
||||||
|
|
||||||
|
@app.get("/tasks/", response_model=List[TaskResponse])
|
||||||
|
async def read_tasks(
|
||||||
|
status: Optional[str] = Query(None),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
query = db.query(Task)
|
||||||
|
if status:
|
||||||
|
query = query.filter(Task.status == status)
|
||||||
|
tasks = query.all()
|
||||||
|
return tasks
|
||||||
|
|
||||||
|
@app.get("/tasks/{task_id}", response_model=TaskResponse)
|
||||||
|
async def read_task(task_id: int, db: Session = Depends(get_db)):
|
||||||
|
task = db.query(Task).filter(Task.id == task_id).first()
|
||||||
|
if not task:
|
||||||
|
raise HTTPException(status_code=404, detail="Task not found")
|
||||||
|
return task
|
||||||
|
|
||||||
|
@app.put("/tasks/{task_id}", response_model=TaskResponse)
|
||||||
|
async def update_task(task_id: int, task: TaskCreate, db: Session = Depends(get_db)):
|
||||||
|
db_task = db.query(Task).filter(Task.id == task_id).first()
|
||||||
|
if not db_task:
|
||||||
|
raise HTTPException(status_code=404, detail="Task not found")
|
||||||
|
|
||||||
|
for key, value in model_dump(task).items():
|
||||||
|
setattr(db_task, key, value)
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_task)
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
return db_task
|
||||||
|
|
||||||
|
@app.delete("/tasks/{task_id}", status_code=204)
|
||||||
|
async def delete_task(task_id: int, db: Session = Depends(get_db)):
|
||||||
|
task = db.query(Task).filter(Task.id == task_id).first()
|
||||||
|
if not task:
|
||||||
|
raise HTTPException(status_code=404, detail="Task not found")
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.delete(task)
|
||||||
|
db.commit()
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
25
zipit/loop_runs/v5_feedback/models.py
Normal file
25
zipit/loop_runs/v5_feedback/models.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from sqlalchemy import create_engine, Column, Integer, String, Text, Date, Enum
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
DATABASE_URL = "sqlite:///./todo.db"
|
||||||
|
engine = create_engine(DATABASE_URL, connect_args={'check_same_thread': False})
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
class Task(Base):
|
||||||
|
__tablename__ = "tasks"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
title = Column(String(255), nullable=False)
|
||||||
|
description = Column(Text, nullable=True)
|
||||||
|
due_date = Column(Date, nullable=True)
|
||||||
|
status = Column(Enum('pending', 'completed'), default='pending')
|
||||||
|
|
||||||
|
class User(Base):
|
||||||
|
__tablename__ = "users"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
username = Column(String(255), unique=True, nullable=False)
|
||||||
|
email = Column(String(255), unique=True, nullable=False)
|
||||||
|
password_hash = Column(String(255), nullable=False)
|
||||||
|
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
13
zipit/loop_runs/v5_feedback/pyproject.toml
Normal file
13
zipit/loop_runs/v5_feedback/pyproject.toml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[project]
|
||||||
|
name = "todo-app"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "A simple task management API"
|
||||||
|
authors = [
|
||||||
|
{ name="Your Name", email="your.email@example.com" }
|
||||||
|
]
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
"fastapi",
|
||||||
|
"uvicorn[standard]",
|
||||||
|
"sqlalchemy"
|
||||||
|
]
|
||||||
163
zipit/loop_runs/v5_feedback/report.json
Normal file
163
zipit/loop_runs/v5_feedback/report.json
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
{
|
||||||
|
"run_id": "v5_feedback",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step": "requirements",
|
||||||
|
"agent": "client",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 9.1,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 1560
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "models.py",
|
||||||
|
"agent": "data",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 7.5,
|
||||||
|
"errors": [
|
||||||
|
"SQLite create_engine puuttuu connect_args={'check_same_thread': False}"
|
||||||
|
],
|
||||||
|
"code_length": 982
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attempt": 2,
|
||||||
|
"elapsed": 7.6,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 1025
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "schemas.py",
|
||||||
|
"agent": "coder",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 6.2,
|
||||||
|
"errors": [
|
||||||
|
"Schema puuttuu kenttiä jotka ovat modelissa: {'password_hash', 'email', 'username'}"
|
||||||
|
],
|
||||||
|
"code_length": 494
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attempt": 2,
|
||||||
|
"elapsed": 4.6,
|
||||||
|
"errors": [
|
||||||
|
"Schema puuttuu kenttiä jotka ovat modelissa: {'password_hash', 'email', 'username'}"
|
||||||
|
],
|
||||||
|
"code_length": 494
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attempt": 3,
|
||||||
|
"elapsed": 4.0,
|
||||||
|
"errors": [
|
||||||
|
"Schema puuttuu kenttiä jotka ovat modelissa: {'password_hash', 'email', 'username'}"
|
||||||
|
],
|
||||||
|
"code_length": 494
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [
|
||||||
|
"Schema puuttuu kenttiä jotka ovat modelissa: {'password_hash', 'email', 'username'}"
|
||||||
|
],
|
||||||
|
"passed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "main.py",
|
||||||
|
"agent": "coder",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 18.9,
|
||||||
|
"errors": [
|
||||||
|
"Kutsutaan 'create_engine()' mutta sitä ei ole importattu eikä määritelty",
|
||||||
|
"Kutsutaan 'sessionmaker()' mutta sitä ei ole importattu eikä määritelty"
|
||||||
|
],
|
||||||
|
"code_length": 2531
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attempt": 2,
|
||||||
|
"elapsed": 18.7,
|
||||||
|
"errors": [
|
||||||
|
"Kutsutaan 'SessionLocal()' mutta sitä ei ole importattu eikä määritelty"
|
||||||
|
],
|
||||||
|
"code_length": 2337
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attempt": 3,
|
||||||
|
"elapsed": 18.4,
|
||||||
|
"errors": [
|
||||||
|
"Kutsutaan 'SessionLocal()' mutta sitä ei ole importattu eikä määritelty"
|
||||||
|
],
|
||||||
|
"code_length": 2337
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [
|
||||||
|
"Kutsutaan 'SessionLocal()' mutta sitä ei ole importattu eikä määritelty"
|
||||||
|
],
|
||||||
|
"passed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "pyproject.toml",
|
||||||
|
"agent": "coder",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 7.4,
|
||||||
|
"errors": [
|
||||||
|
"Sisältää poetry-konfiguraation — käytä VAIN [project] (PEP 621) + uv",
|
||||||
|
"build-backend käyttää poetryä — poista tai vaihda"
|
||||||
|
],
|
||||||
|
"code_length": 647
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attempt": 2,
|
||||||
|
"elapsed": 2.9,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 238
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "test_main.py",
|
||||||
|
"agent": "qa",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 14.1,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 1567
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "Dockerfile",
|
||||||
|
"agent": "tester",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 4.1,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 291
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"model": "qwen2.5-coder:7b-instruct-q4_K_M",
|
||||||
|
"summary": "5/7 passed"
|
||||||
|
}
|
||||||
40
zipit/loop_runs/v5_feedback/requirements
Normal file
40
zipit/loop_runs/v5_feedback/requirements
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
**PROJECT NAME:** Todo-sovellus
|
||||||
|
|
||||||
|
**GOAL:** Create a simple task management application for individuals to keep track of their daily tasks and manage them efficiently.
|
||||||
|
|
||||||
|
**CORE FEATURES:**
|
||||||
|
1. Add new tasks with title, description, due date, and status.
|
||||||
|
2. View all tasks with filtering by status (e.g., pending, completed).
|
||||||
|
3. Update existing tasks including changing the title, description, due date, and status.
|
||||||
|
4. Delete tasks that are no longer needed.
|
||||||
|
5. Mark tasks as completed or pending.
|
||||||
|
6. Search for tasks by keywords in the title or description.
|
||||||
|
7. Export task list to a CSV file.
|
||||||
|
|
||||||
|
**DATA MODEL:**
|
||||||
|
1. **Task**
|
||||||
|
- id (integer, primary key)
|
||||||
|
- title (string, required)
|
||||||
|
- description (text, optional)
|
||||||
|
- due_date (date, optional)
|
||||||
|
- status (enum ['pending', 'completed'], default 'pending')
|
||||||
|
|
||||||
|
2. **User**
|
||||||
|
- id (integer, primary key)
|
||||||
|
- username (string, unique, required)
|
||||||
|
- email (string, unique, required)
|
||||||
|
- password_hash (string, required)
|
||||||
|
|
||||||
|
**API ENDPOINTS:**
|
||||||
|
1. **GET /tasks** - Retrieve all tasks with optional filters by status.
|
||||||
|
2. **POST /tasks** - Create a new task.
|
||||||
|
3. **GET /tasks/{task_id}** - Retrieve a specific task by ID.
|
||||||
|
4. **PUT /tasks/{task_id}** - Update an existing task.
|
||||||
|
5. **DELETE /tasks/{task_id}** - Delete a task.
|
||||||
|
6. **POST /users/register** - Register a new user.
|
||||||
|
7. **POST /users/login** - Authenticate and return a token for authorized access.
|
||||||
|
|
||||||
|
**CONSTRAINTS:**
|
||||||
|
- Must use SQLite as the database.
|
||||||
|
- No authentication is required for accessing CRUD operations.
|
||||||
|
- Implement unit tests for all API endpoints.
|
||||||
18
zipit/loop_runs/v5_feedback/schemas.py
Normal file
18
zipit/loop_runs/v5_feedback/schemas.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
class TaskCreate(BaseModel):
|
||||||
|
title: str = Field(..., min_length=1, max_length=255)
|
||||||
|
description: str | None = Field(None, max_length=255)
|
||||||
|
due_date: date | None = Field(None)
|
||||||
|
status: str = Field('pending', regex='^(pending|completed)$')
|
||||||
|
|
||||||
|
class TaskResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
title: str
|
||||||
|
description: str | None
|
||||||
|
due_date: date | None
|
||||||
|
status: str
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
51
zipit/loop_runs/v5_feedback/test_main.py
Normal file
51
zipit/loop_runs/v5_feedback/test_main.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from main import app
|
||||||
|
from models import Base
|
||||||
|
|
||||||
|
engine = create_engine("sqlite:///:memory:", connect_args={"check_same_thread": False})
|
||||||
|
TestSession = sessionmaker(bind=engine)
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
|
def override_get_db():
|
||||||
|
db = TestSession()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
app.dependency_overrides[get_db] = override_get_db
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
def test_create_task():
|
||||||
|
response = client.post("/tasks/", json={"title": "Test Task", "description": "This is a test task"})
|
||||||
|
assert response.status_code == 201
|
||||||
|
assert response.json()["title"] == "Test Task"
|
||||||
|
assert response.json()["description"] == "This is a test task"
|
||||||
|
|
||||||
|
def test_read_tasks():
|
||||||
|
response = client.get("/tasks/")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert len(response.json()) == 1
|
||||||
|
|
||||||
|
def test_get_task_by_id():
|
||||||
|
response = client.get("/tasks/1")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["id"] == 1
|
||||||
|
|
||||||
|
def test_get_task_by_id_not_found():
|
||||||
|
response = client.get("/tasks/999")
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
def test_update_task():
|
||||||
|
response = client.put("/tasks/1", json={"title": "Updated Task"})
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["title"] == "Updated Task"
|
||||||
|
|
||||||
|
def test_delete_task():
|
||||||
|
response = client.delete("/tasks/1")
|
||||||
|
assert response.status_code == 204
|
||||||
|
|
||||||
|
response = client.get("/tasks/1")
|
||||||
|
assert response.status_code == 404
|
||||||
11
zipit/loop_runs/v6_stable/Dockerfile
Normal file
11
zipit/loop_runs/v6_stable/Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
FROM python:3.12-slim
|
||||||
|
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
|
||||||
|
ENV UV_CACHE_DIR=/tmp/uv-cache
|
||||||
|
WORKDIR /app
|
||||||
|
COPY pyproject.toml .
|
||||||
|
RUN uv sync
|
||||||
|
COPY *.py .
|
||||||
|
RUN useradd -m appuser
|
||||||
|
USER appuser
|
||||||
|
EXPOSE 8000
|
||||||
|
CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
62
zipit/loop_runs/v6_stable/main.py
Normal file
62
zipit/loop_runs/v6_stable/main.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
from fastapi import FastAPI, HTTPException, Depends
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from models import Task, SessionLocal
|
||||||
|
from schemas import TaskCreate, TaskResponse
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
# Dependency to get database session
|
||||||
|
def get_db():
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
@app.post("/tasks/", response_model=TaskResponse, status_code=201)
|
||||||
|
def create_task(task: TaskCreate, db: Session = Depends(get_db)):
|
||||||
|
db_task = Task(**task.model_dump())
|
||||||
|
db.add(db_task)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_task)
|
||||||
|
return db_task
|
||||||
|
|
||||||
|
@app.get("/tasks/", response_model=List[TaskResponse])
|
||||||
|
def read_tasks(status: str = None, db: Session = Depends(get_db)):
|
||||||
|
if status:
|
||||||
|
tasks = db.query(Task).filter_by(status=status).all()
|
||||||
|
else:
|
||||||
|
tasks = db.query(Task).all()
|
||||||
|
return tasks
|
||||||
|
|
||||||
|
@app.get("/tasks/{task_id}", response_model=TaskResponse)
|
||||||
|
def read_task(task_id: int, db: Session = Depends(get_db)):
|
||||||
|
task = db.query(Task).filter(Task.id == task_id).first()
|
||||||
|
if not task:
|
||||||
|
raise HTTPException(status_code=404, detail="Task not found")
|
||||||
|
return task
|
||||||
|
|
||||||
|
@app.put("/tasks/{task_id}", response_model=TaskResponse)
|
||||||
|
def update_task(task_id: int, task: TaskCreate, db: Session = Depends(get_db)):
|
||||||
|
db_task = db.query(Task).filter(Task.id == task_id).first()
|
||||||
|
if not db_task:
|
||||||
|
raise HTTPException(status_code=404, detail="Task not found")
|
||||||
|
|
||||||
|
for key, value in task.model_dump().items():
|
||||||
|
setattr(db_task, key, value)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_task)
|
||||||
|
return db_task
|
||||||
|
|
||||||
|
@app.delete("/tasks/{task_id}", status_code=204)
|
||||||
|
def delete_task(task_id: int, db: Session = Depends(get_db)):
|
||||||
|
db_task = db.query(Task).filter(Task.id == task_id).first()
|
||||||
|
if not db_task:
|
||||||
|
raise HTTPException(status_code=404, detail="Task not found")
|
||||||
|
|
||||||
|
db.delete(db_task)
|
||||||
|
db.commit()
|
||||||
|
return
|
||||||
23
zipit/loop_runs/v6_stable/models.py
Normal file
23
zipit/loop_runs/v6_stable/models.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
from sqlalchemy import create_engine, Column, Integer, String, Date, Enum
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
DATABASE_URL = "sqlite:///./todos.db"
|
||||||
|
engine = create_engine(DATABASE_URL, connect_args={'check_same_thread': False})
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
class TaskStatus(Enum):
|
||||||
|
pending = 'pending'
|
||||||
|
completed = 'completed'
|
||||||
|
|
||||||
|
class Task(Base):
|
||||||
|
__tablename__ = "tasks"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
title = Column(String(255), nullable=False)
|
||||||
|
description = Column(String(1000))
|
||||||
|
due_date = Column(Date)
|
||||||
|
status = Column(Enum(TaskStatus), default=TaskStatus.pending)
|
||||||
|
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
15
zipit/loop_runs/v6_stable/pyproject.toml
Normal file
15
zipit/loop_runs/v6_stable/pyproject.toml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[project]
|
||||||
|
name = "todo-app"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "A simple task management API using FastAPI and SQLAlchemy"
|
||||||
|
authors = [
|
||||||
|
{ name="Your Name", email="your.email@example.com" }
|
||||||
|
]
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
"fastapi",
|
||||||
|
"uvicorn[standard]",
|
||||||
|
"sqlalchemy",
|
||||||
|
"python-dotenv",
|
||||||
|
"pydantic"
|
||||||
|
]
|
||||||
126
zipit/loop_runs/v6_stable/report.json
Normal file
126
zipit/loop_runs/v6_stable/report.json
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
{
|
||||||
|
"run_id": "v6_stable",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step": "requirements",
|
||||||
|
"agent": "client",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 7.7,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 1361
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "models.py",
|
||||||
|
"agent": "data",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 5.9,
|
||||||
|
"errors": [
|
||||||
|
"SQLite create_engine puuttuu connect_args={'check_same_thread': False}",
|
||||||
|
"Luokka 'TaskStatus' perii Enum:in mutta 'from enum import Enum' puuttuu (SQLAlchemy Enum ei ole Python Enum)"
|
||||||
|
],
|
||||||
|
"code_length": 751
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attempt": 2,
|
||||||
|
"elapsed": 5.6,
|
||||||
|
"errors": [
|
||||||
|
"Luokka 'TaskStatus' perii Enum:in mutta 'from enum import Enum' puuttuu (SQLAlchemy Enum ei ole Python Enum)"
|
||||||
|
],
|
||||||
|
"code_length": 794
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attempt": 3,
|
||||||
|
"elapsed": 5.5,
|
||||||
|
"errors": [
|
||||||
|
"Luokka 'TaskStatus' perii Enum:in mutta 'from enum import Enum' puuttuu (SQLAlchemy Enum ei ole Python Enum)"
|
||||||
|
],
|
||||||
|
"code_length": 794
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [
|
||||||
|
"Luokka 'TaskStatus' perii Enum:in mutta 'from enum import Enum' puuttuu (SQLAlchemy Enum ei ole Python Enum)"
|
||||||
|
],
|
||||||
|
"passed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "schemas.py",
|
||||||
|
"agent": "coder",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 5.4,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 481
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "main.py",
|
||||||
|
"agent": "coder",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 14.1,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 1914
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "pyproject.toml",
|
||||||
|
"agent": "coder",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 3.6,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 304
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "test_main.py",
|
||||||
|
"agent": "qa",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 16.9,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 2108
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "Dockerfile",
|
||||||
|
"agent": "tester",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 4.0,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 291
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"model": "qwen2.5-coder:7b-instruct-q4_K_M",
|
||||||
|
"summary": "6/7 passed"
|
||||||
|
}
|
||||||
32
zipit/loop_runs/v6_stable/requirements
Normal file
32
zipit/loop_runs/v6_stable/requirements
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
**PROJECT NAME:** TodoSovellus
|
||||||
|
|
||||||
|
**GOAL:** Create a simple task management application for individuals to keep track of their daily tasks and manage them efficiently.
|
||||||
|
|
||||||
|
**CORE FEATURES:**
|
||||||
|
1. **Add Task**: Users can add new tasks with a title, description, due date, and status (e.g., pending, completed).
|
||||||
|
2. **View Tasks**: Users can view all tasks or filter by status (pending, completed).
|
||||||
|
3. **Edit Task**: Users can update the details of an existing task.
|
||||||
|
4. **Delete Task**: Users can remove tasks that are no longer needed.
|
||||||
|
5. **Mark as Completed**: Users can mark a task as completed.
|
||||||
|
6. **Filter Tasks**: Users can filter tasks based on their status (pending, completed).
|
||||||
|
7. **Task Details**: Users can view detailed information about each task.
|
||||||
|
|
||||||
|
**DATA MODEL:**
|
||||||
|
1. **Tasks**
|
||||||
|
- id (integer, primary key)
|
||||||
|
- title (string)
|
||||||
|
- description (text)
|
||||||
|
- due_date (date)
|
||||||
|
- status (enum: pending, completed)
|
||||||
|
|
||||||
|
**API ENDPOINTS:**
|
||||||
|
1. **GET /tasks**: Retrieve all tasks.
|
||||||
|
2. **POST /tasks**: Create a new task.
|
||||||
|
3. **GET /tasks/{id}**: Retrieve details of a specific task by ID.
|
||||||
|
4. **PUT /tasks/{id}**: Update an existing task.
|
||||||
|
5. **DELETE /tasks/{id}**: Delete a task.
|
||||||
|
6. **GET /tasks/status/{status}**: Filter tasks by status (pending, completed).
|
||||||
|
|
||||||
|
**CONSTRAINTS:**
|
||||||
|
- Must use SQLite as the database.
|
||||||
|
- No authentication is required for accessing endpoints.
|
||||||
18
zipit/loop_runs/v6_stable/schemas.py
Normal file
18
zipit/loop_runs/v6_stable/schemas.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
class TaskCreate(BaseModel):
|
||||||
|
title: str = Field(..., min_length=1, max_length=255)
|
||||||
|
description: str = Field(None, min_length=0, max_length=1000)
|
||||||
|
due_date: date = Field(None)
|
||||||
|
status: str = Field('pending', regex='^(pending|completed)$')
|
||||||
|
|
||||||
|
class TaskResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
title: str
|
||||||
|
description: str
|
||||||
|
due_date: date
|
||||||
|
status: str
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
58
zipit/loop_runs/v6_stable/test_main.py
Normal file
58
zipit/loop_runs/v6_stable/test_main.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from main import app
|
||||||
|
from models import Base
|
||||||
|
from main import get_db
|
||||||
|
|
||||||
|
engine = create_engine("sqlite:///:memory:", connect_args={"check_same_thread": False})
|
||||||
|
TestSession = sessionmaker(bind=engine)
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
|
def override_get_db():
|
||||||
|
db = TestSession()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
app.dependency_overrides[get_db] = override_get_db
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
def test_create_task():
|
||||||
|
response = client.post("/tasks/", json={"title": "Test Task", "description": "This is a test task"})
|
||||||
|
assert response.status_code == 201
|
||||||
|
assert response.json()["title"] == "Test Task"
|
||||||
|
assert response.json()["status"] == "pending"
|
||||||
|
|
||||||
|
def test_read_tasks():
|
||||||
|
response = client.get("/tasks/")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert len(response.json()) == 1
|
||||||
|
|
||||||
|
def test_get_task_by_id():
|
||||||
|
task_response = client.post("/tasks/", json={"title": "Test Task", "description": "This is a test task"})
|
||||||
|
task_id = task_response.json()["id"]
|
||||||
|
response = client.get(f"/tasks/{task_id}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["id"] == task_id
|
||||||
|
|
||||||
|
def test_get_task_by_id_not_found():
|
||||||
|
response = client.get("/tasks/999")
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
def test_update_task():
|
||||||
|
task_response = client.post("/tasks/", json={"title": "Test Task", "description": "This is a test task"})
|
||||||
|
task_id = task_response.json()["id"]
|
||||||
|
update_data = {"title": "Updated Test Task"}
|
||||||
|
response = client.put(f"/tasks/{task_id}", json=update_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["title"] == "Updated Test Task"
|
||||||
|
|
||||||
|
def test_delete_task():
|
||||||
|
task_response = client.post("/tasks/", json={"title": "Test Task", "description": "This is a test task"})
|
||||||
|
task_id = task_response.json()["id"]
|
||||||
|
response = client.delete(f"/tasks/{task_id}")
|
||||||
|
assert response.status_code == 204
|
||||||
|
response = client.get(f"/tasks/{task_id}")
|
||||||
|
assert response.status_code == 404
|
||||||
11
zipit/loop_runs/v7_enum_fix/Dockerfile
Normal file
11
zipit/loop_runs/v7_enum_fix/Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
FROM python:3.12-slim
|
||||||
|
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
|
||||||
|
ENV UV_CACHE_DIR=/tmp/uv-cache
|
||||||
|
WORKDIR /app
|
||||||
|
COPY pyproject.toml .
|
||||||
|
RUN uv sync
|
||||||
|
COPY *.py .
|
||||||
|
RUN useradd -m appuser
|
||||||
|
USER appuser
|
||||||
|
EXPOSE 8000
|
||||||
|
CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
10
zipit/loop_runs/v7_enum_fix/docker-compose.yml
Normal file
10
zipit/loop_runs/v7_enum_fix/docker-compose.yml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "18765:8000"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/docs')"]
|
||||||
|
interval: 3s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
63
zipit/loop_runs/v7_enum_fix/main.py
Normal file
63
zipit/loop_runs/v7_enum_fix/main.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
from fastapi import FastAPI, HTTPException, Depends
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from typing import List
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from models import Task, SessionLocal
|
||||||
|
from schemas import TaskCreate, TaskResponse
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
# Dependency to get database session
|
||||||
|
def get_db():
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
@app.post("/tasks/", response_model=TaskResponse, status_code=201)
|
||||||
|
async def create_task(task: TaskCreate, db: Session = Depends(get_db)):
|
||||||
|
db_task = Task(**task.model_dump())
|
||||||
|
db.add(db_task)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_task)
|
||||||
|
return db_task
|
||||||
|
|
||||||
|
@app.get("/tasks/", response_model=List[TaskResponse])
|
||||||
|
async def read_tasks(status: str = None, db: Session = Depends(get_db)):
|
||||||
|
if status:
|
||||||
|
tasks = db.query(Task).filter_by(status=status).all()
|
||||||
|
else:
|
||||||
|
tasks = db.query(Task).all()
|
||||||
|
return tasks
|
||||||
|
|
||||||
|
@app.get("/tasks/{task_id}", response_model=TaskResponse)
|
||||||
|
async def read_task(task_id: int, db: Session = Depends(get_db)):
|
||||||
|
task = db.query(Task).filter(Task.id == task_id).first()
|
||||||
|
if not task:
|
||||||
|
raise HTTPException(status_code=404, detail="Task not found")
|
||||||
|
return task
|
||||||
|
|
||||||
|
@app.put("/tasks/{task_id}", response_model=TaskResponse)
|
||||||
|
async def update_task(task_id: int, task_update: TaskCreate, db: Session = Depends(get_db)):
|
||||||
|
task = db.query(Task).filter(Task.id == task_id).first()
|
||||||
|
if not task:
|
||||||
|
raise HTTPException(status_code=404, detail="Task not found")
|
||||||
|
|
||||||
|
for key, value in task_update.model_dump().items():
|
||||||
|
setattr(task, key, value)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
db.refresh(task)
|
||||||
|
return task
|
||||||
|
|
||||||
|
@app.delete("/tasks/{task_id}", status_code=204)
|
||||||
|
async def delete_task(task_id: int, db: Session = Depends(get_db)):
|
||||||
|
task = db.query(Task).filter(Task.id == task_id).first()
|
||||||
|
if not task:
|
||||||
|
raise HTTPException(status_code=404, detail="Task not found")
|
||||||
|
|
||||||
|
db.delete(task)
|
||||||
|
db.commit()
|
||||||
|
return
|
||||||
18
zipit/loop_runs/v7_enum_fix/models.py
Normal file
18
zipit/loop_runs/v7_enum_fix/models.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from sqlalchemy import create_engine, Column, Integer, String, Date
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
DATABASE_URL = "sqlite:///./todos.db"
|
||||||
|
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
class Task(Base):
|
||||||
|
__tablename__ = "tasks"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
title = Column(String(255), nullable=False)
|
||||||
|
description = Column(String(1000))
|
||||||
|
due_date = Column(Date, nullable=False)
|
||||||
|
status = Column(String(20), default="pending")
|
||||||
|
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
17
zipit/loop_runs/v7_enum_fix/pyproject.toml
Normal file
17
zipit/loop_runs/v7_enum_fix/pyproject.toml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
[project]
|
||||||
|
name = "todo-app"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "A simple task management API using FastAPI and SQLAlchemy"
|
||||||
|
authors = [
|
||||||
|
{ name="Your Name", email="your.email@example.com" }
|
||||||
|
]
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
"fastapi",
|
||||||
|
"uvicorn[standard]",
|
||||||
|
"sqlalchemy",
|
||||||
|
"pytest",
|
||||||
|
"httpx",
|
||||||
|
"python-dotenv",
|
||||||
|
"pydantic>=2.0.0"
|
||||||
|
]
|
||||||
123
zipit/loop_runs/v7_enum_fix/report.json
Normal file
123
zipit/loop_runs/v7_enum_fix/report.json
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
{
|
||||||
|
"run_id": "v7_enum_fix",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step": "requirements",
|
||||||
|
"agent": "client",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 7.6,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 1361
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "models.py",
|
||||||
|
"agent": "data",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 5.9,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 707
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "schemas.py",
|
||||||
|
"agent": "coder",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 5.4,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 464
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "main.py",
|
||||||
|
"agent": "coder",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 14.1,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 1960
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "pyproject.toml",
|
||||||
|
"agent": "coder",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 8.6,
|
||||||
|
"errors": [
|
||||||
|
"Sisältää poetry-konfiguraation — käytä VAIN [project] (PEP 621) + uv",
|
||||||
|
"build-backend käyttää poetryä — poista tai vaihda"
|
||||||
|
],
|
||||||
|
"code_length": 815
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attempt": 2,
|
||||||
|
"elapsed": 3.6,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 311
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "test_main.py",
|
||||||
|
"agent": "qa",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 20.3,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 2050
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "Dockerfile",
|
||||||
|
"agent": "tester",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 4.5,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 291
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"model": "qwen2.5-coder:7b-instruct-q4_K_M",
|
||||||
|
"summary": "7/7 passed",
|
||||||
|
"docker": {
|
||||||
|
"build": true,
|
||||||
|
"pytest": false,
|
||||||
|
"api": false,
|
||||||
|
"errors": [
|
||||||
|
"pytest failed (exit 2):\n Network v7_enum_fix_default Creating\n Network v7_enum_fix_default Created\nerror: Failed to initialize cache at `/tmp/uv-cache`\n Caused by: failed to open file `/tmp/uv-cache/sdists-v9/.git`: Permission denied (os error 13)\n\n",
|
||||||
|
"Kontti ei käynnistynyt 15s:ssa"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
32
zipit/loop_runs/v7_enum_fix/requirements
Normal file
32
zipit/loop_runs/v7_enum_fix/requirements
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
**PROJECT NAME:** TodoSovellus
|
||||||
|
|
||||||
|
**GOAL:** Create a simple task management application for individuals to keep track of their daily tasks and manage them efficiently.
|
||||||
|
|
||||||
|
**CORE FEATURES:**
|
||||||
|
1. **Add Task**: Users can add new tasks with a title, description, due date, and status (e.g., pending, completed).
|
||||||
|
2. **View Tasks**: Users can view all tasks or filter by status (pending, completed).
|
||||||
|
3. **Edit Task**: Users can update the details of an existing task.
|
||||||
|
4. **Delete Task**: Users can remove tasks that are no longer needed.
|
||||||
|
5. **Mark as Completed**: Users can mark a task as completed.
|
||||||
|
6. **Filter Tasks**: Users can filter tasks based on their status (pending, completed).
|
||||||
|
7. **Task Details**: Users can view detailed information about each task.
|
||||||
|
|
||||||
|
**DATA MODEL:**
|
||||||
|
1. **Tasks**
|
||||||
|
- id (integer, primary key)
|
||||||
|
- title (string)
|
||||||
|
- description (text)
|
||||||
|
- due_date (date)
|
||||||
|
- status (enum: pending, completed)
|
||||||
|
|
||||||
|
**API ENDPOINTS:**
|
||||||
|
1. **GET /tasks**: Retrieve all tasks.
|
||||||
|
2. **POST /tasks**: Create a new task.
|
||||||
|
3. **GET /tasks/{id}**: Retrieve details of a specific task by ID.
|
||||||
|
4. **PUT /tasks/{id}**: Update an existing task.
|
||||||
|
5. **DELETE /tasks/{id}**: Delete a task.
|
||||||
|
6. **GET /tasks/status/{status}**: Filter tasks by status (pending, completed).
|
||||||
|
|
||||||
|
**CONSTRAINTS:**
|
||||||
|
- Must use SQLite as the database.
|
||||||
|
- No authentication is required for accessing endpoints.
|
||||||
18
zipit/loop_runs/v7_enum_fix/schemas.py
Normal file
18
zipit/loop_runs/v7_enum_fix/schemas.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
class TaskCreate(BaseModel):
|
||||||
|
title: str = Field(..., min_length=1, max_length=255)
|
||||||
|
description: str = Field(None, max_length=1000)
|
||||||
|
due_date: date = Field(...)
|
||||||
|
status: str = Field("pending", min_length=1, max_length=20)
|
||||||
|
|
||||||
|
class TaskResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
title: str
|
||||||
|
description: str
|
||||||
|
due_date: date
|
||||||
|
status: str
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
56
zipit/loop_runs/v7_enum_fix/test_main.py
Normal file
56
zipit/loop_runs/v7_enum_fix/test_main.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from main import app
|
||||||
|
from models import Base
|
||||||
|
from main import get_db
|
||||||
|
|
||||||
|
engine = create_engine("sqlite:///:memory:", connect_args={"check_same_thread": False})
|
||||||
|
TestSession = sessionmaker(bind=engine)
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
|
def override_get_db():
|
||||||
|
db = TestSession()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
app.dependency_overrides[get_db] = override_get_db
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
def test_create_task():
|
||||||
|
response = client.post("/tasks/", json={"title": "Test Task", "due_date": "2023-12-31"})
|
||||||
|
assert response.status_code == 201
|
||||||
|
assert response.json()["title"] == "Test Task"
|
||||||
|
assert response.json()["status"] == "pending"
|
||||||
|
|
||||||
|
def test_read_tasks():
|
||||||
|
client.post("/tasks/", json={"title": "Task 1", "due_date": "2023-12-31"})
|
||||||
|
client.post("/tasks/", json={"title": "Task 2", "due_date": "2024-01-01"})
|
||||||
|
response = client.get("/tasks/")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert len(response.json()) == 2
|
||||||
|
|
||||||
|
def test_read_task():
|
||||||
|
task = client.post("/tasks/", json={"title": "Test Task", "due_date": "2023-12-31"}).json()
|
||||||
|
response = client.get(f"/tasks/{task['id']}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["id"] == task["id"]
|
||||||
|
|
||||||
|
def test_read_task_not_found():
|
||||||
|
response = client.get("/tasks/999")
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
def test_update_task():
|
||||||
|
task = client.post("/tasks/", json={"title": "Test Task", "due_date": "2023-12-31"}).json()
|
||||||
|
response = client.put(f"/tasks/{task['id']}", json={"status": "completed"})
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["status"] == "completed"
|
||||||
|
|
||||||
|
def test_delete_task():
|
||||||
|
task = client.post("/tasks/", json={"title": "Test Task", "due_date": "2023-12-31"}).json()
|
||||||
|
response = client.delete(f"/tasks/{task['id']}")
|
||||||
|
assert response.status_code == 204
|
||||||
|
response = client.get(f"/tasks/{task['id']}")
|
||||||
|
assert response.status_code == 404
|
||||||
11
zipit/loop_runs/v8_cache_fix/Dockerfile
Normal file
11
zipit/loop_runs/v8_cache_fix/Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
FROM python:3.12-slim
|
||||||
|
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
|
||||||
|
ENV UV_CACHE_DIR=/tmp/uv-cache
|
||||||
|
WORKDIR /app
|
||||||
|
COPY pyproject.toml .
|
||||||
|
RUN uv sync
|
||||||
|
COPY *.py .
|
||||||
|
RUN useradd -m appuser && chown -R appuser:appuser /app /tmp/uv-cache
|
||||||
|
USER appuser
|
||||||
|
EXPOSE 8000
|
||||||
|
CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
10
zipit/loop_runs/v8_cache_fix/docker-compose.yml
Normal file
10
zipit/loop_runs/v8_cache_fix/docker-compose.yml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "18765:8000"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/docs')"]
|
||||||
|
interval: 3s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
63
zipit/loop_runs/v8_cache_fix/main.py
Normal file
63
zipit/loop_runs/v8_cache_fix/main.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
from fastapi import FastAPI, HTTPException, Depends
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from typing import List
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from models import Task, SessionLocal
|
||||||
|
from schemas import TaskCreate, TaskResponse
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
# Dependency to get database session
|
||||||
|
def get_db():
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
@app.post("/tasks/", response_model=TaskResponse, status_code=201)
|
||||||
|
async def create_task(task: TaskCreate, db: Session = Depends(get_db)):
|
||||||
|
db_task = Task(**task.model_dump())
|
||||||
|
db.add(db_task)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_task)
|
||||||
|
return db_task
|
||||||
|
|
||||||
|
@app.get("/tasks/", response_model=List[TaskResponse])
|
||||||
|
async def read_tasks(status: str = None, db: Session = Depends(get_db)):
|
||||||
|
if status:
|
||||||
|
tasks = db.query(Task).filter_by(status=status).all()
|
||||||
|
else:
|
||||||
|
tasks = db.query(Task).all()
|
||||||
|
return tasks
|
||||||
|
|
||||||
|
@app.get("/tasks/{task_id}", response_model=TaskResponse)
|
||||||
|
async def read_task(task_id: int, db: Session = Depends(get_db)):
|
||||||
|
task = db.query(Task).filter(Task.id == task_id).first()
|
||||||
|
if not task:
|
||||||
|
raise HTTPException(status_code=404, detail="Task not found")
|
||||||
|
return task
|
||||||
|
|
||||||
|
@app.put("/tasks/{task_id}", response_model=TaskResponse)
|
||||||
|
async def update_task(task_id: int, task_update: TaskCreate, db: Session = Depends(get_db)):
|
||||||
|
task = db.query(Task).filter(Task.id == task_id).first()
|
||||||
|
if not task:
|
||||||
|
raise HTTPException(status_code=404, detail="Task not found")
|
||||||
|
|
||||||
|
for key, value in task_update.model_dump().items():
|
||||||
|
setattr(task, key, value)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
db.refresh(task)
|
||||||
|
return task
|
||||||
|
|
||||||
|
@app.delete("/tasks/{task_id}", status_code=204)
|
||||||
|
async def delete_task(task_id: int, db: Session = Depends(get_db)):
|
||||||
|
task = db.query(Task).filter(Task.id == task_id).first()
|
||||||
|
if not task:
|
||||||
|
raise HTTPException(status_code=404, detail="Task not found")
|
||||||
|
|
||||||
|
db.delete(task)
|
||||||
|
db.commit()
|
||||||
|
return
|
||||||
18
zipit/loop_runs/v8_cache_fix/models.py
Normal file
18
zipit/loop_runs/v8_cache_fix/models.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from sqlalchemy import create_engine, Column, Integer, String, Date
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
DATABASE_URL = "sqlite:///./todos.db"
|
||||||
|
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
class Task(Base):
|
||||||
|
__tablename__ = "tasks"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
title = Column(String(255), nullable=False)
|
||||||
|
description = Column(String(1000))
|
||||||
|
due_date = Column(Date, nullable=False)
|
||||||
|
status = Column(String(20), default="pending")
|
||||||
|
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
17
zipit/loop_runs/v8_cache_fix/pyproject.toml
Normal file
17
zipit/loop_runs/v8_cache_fix/pyproject.toml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
[project]
|
||||||
|
name = "todo-app"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "A simple task management API using FastAPI and SQLAlchemy"
|
||||||
|
authors = [
|
||||||
|
{ name="Your Name", email="your.email@example.com" }
|
||||||
|
]
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
"fastapi",
|
||||||
|
"uvicorn[standard]",
|
||||||
|
"sqlalchemy",
|
||||||
|
"pytest",
|
||||||
|
"httpx",
|
||||||
|
"python-dotenv",
|
||||||
|
"pydantic>=2.0.0"
|
||||||
|
]
|
||||||
123
zipit/loop_runs/v8_cache_fix/report.json
Normal file
123
zipit/loop_runs/v8_cache_fix/report.json
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
{
|
||||||
|
"run_id": "v8_cache_fix",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step": "requirements",
|
||||||
|
"agent": "client",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 7.7,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 1361
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "models.py",
|
||||||
|
"agent": "data",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 5.8,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 707
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "schemas.py",
|
||||||
|
"agent": "coder",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 5.2,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 464
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "main.py",
|
||||||
|
"agent": "coder",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 14.1,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 1960
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "pyproject.toml",
|
||||||
|
"agent": "coder",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 8.8,
|
||||||
|
"errors": [
|
||||||
|
"Sisältää poetry-konfiguraation — käytä VAIN [project] (PEP 621) + uv",
|
||||||
|
"build-backend käyttää poetryä — poista tai vaihda"
|
||||||
|
],
|
||||||
|
"code_length": 815
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attempt": 2,
|
||||||
|
"elapsed": 3.6,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 311
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "test_main.py",
|
||||||
|
"agent": "qa",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 19.8,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 2050
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "Dockerfile",
|
||||||
|
"agent": "tester",
|
||||||
|
"attempts": [
|
||||||
|
{
|
||||||
|
"attempt": 1,
|
||||||
|
"elapsed": 4.6,
|
||||||
|
"errors": [],
|
||||||
|
"code_length": 338
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final_errors": [],
|
||||||
|
"passed": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"model": "qwen2.5-coder:7b-instruct-q4_K_M",
|
||||||
|
"summary": "7/7 passed",
|
||||||
|
"docker": {
|
||||||
|
"build": true,
|
||||||
|
"pytest": false,
|
||||||
|
"api": false,
|
||||||
|
"errors": [
|
||||||
|
"pytest failed (exit 1):\n=============== short test summary info ============================\nFAILED test_main.py::test_create_task - sqlalchemy.exc.OperationalError: (sql...\nFAILED test_main.py::test_read_tasks - sqlalchemy.exc.OperationalError: (sqli...\nFAILED test_main.py::test_read_task - sqlalchemy.exc.OperationalError: (sqlit...\nFAILED test_main.py::test_read_task_not_found - sqlalchemy.exc.OperationalErr...\nFAILED test_main.py::test_update_task - sqlalchemy.exc.OperationalError: (sql...\nFAILED test_main.py::test_delete_task - sqlalchemy.exc.OperationalError: (sql...\n======================== 6 failed, 2 warnings in 2.50s =========================\n Network v8_cache_fix_default Creating\n Network v8_cache_fix_default Created\nwarning: No `requires-python` value found in the workspace. Defaulting to `>=3.12`.\n\n",
|
||||||
|
"POST /todos/ ja /tasks/ molemmat failasivat: HTTP Error 422: Unprocessable Entity"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
32
zipit/loop_runs/v8_cache_fix/requirements
Normal file
32
zipit/loop_runs/v8_cache_fix/requirements
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
**PROJECT NAME:** TodoSovellus
|
||||||
|
|
||||||
|
**GOAL:** Create a simple task management application for individuals to keep track of their daily tasks and manage them efficiently.
|
||||||
|
|
||||||
|
**CORE FEATURES:**
|
||||||
|
1. **Add Task**: Users can add new tasks with a title, description, due date, and status (e.g., pending, completed).
|
||||||
|
2. **View Tasks**: Users can view all tasks or filter by status (pending, completed).
|
||||||
|
3. **Edit Task**: Users can update the details of an existing task.
|
||||||
|
4. **Delete Task**: Users can remove tasks that are no longer needed.
|
||||||
|
5. **Mark as Completed**: Users can mark a task as completed.
|
||||||
|
6. **Filter Tasks**: Users can filter tasks based on their status (pending, completed).
|
||||||
|
7. **Task Details**: Users can view detailed information about each task.
|
||||||
|
|
||||||
|
**DATA MODEL:**
|
||||||
|
1. **Tasks**
|
||||||
|
- id (integer, primary key)
|
||||||
|
- title (string)
|
||||||
|
- description (text)
|
||||||
|
- due_date (date)
|
||||||
|
- status (enum: pending, completed)
|
||||||
|
|
||||||
|
**API ENDPOINTS:**
|
||||||
|
1. **GET /tasks**: Retrieve all tasks.
|
||||||
|
2. **POST /tasks**: Create a new task.
|
||||||
|
3. **GET /tasks/{id}**: Retrieve details of a specific task by ID.
|
||||||
|
4. **PUT /tasks/{id}**: Update an existing task.
|
||||||
|
5. **DELETE /tasks/{id}**: Delete a task.
|
||||||
|
6. **GET /tasks/status/{status}**: Filter tasks by status (pending, completed).
|
||||||
|
|
||||||
|
**CONSTRAINTS:**
|
||||||
|
- Must use SQLite as the database.
|
||||||
|
- No authentication is required for accessing endpoints.
|
||||||
18
zipit/loop_runs/v8_cache_fix/schemas.py
Normal file
18
zipit/loop_runs/v8_cache_fix/schemas.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
class TaskCreate(BaseModel):
|
||||||
|
title: str = Field(..., min_length=1, max_length=255)
|
||||||
|
description: str = Field(None, max_length=1000)
|
||||||
|
due_date: date = Field(...)
|
||||||
|
status: str = Field("pending", min_length=1, max_length=20)
|
||||||
|
|
||||||
|
class TaskResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
title: str
|
||||||
|
description: str
|
||||||
|
due_date: date
|
||||||
|
status: str
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
56
zipit/loop_runs/v8_cache_fix/test_main.py
Normal file
56
zipit/loop_runs/v8_cache_fix/test_main.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from main import app
|
||||||
|
from models import Base
|
||||||
|
from main import get_db
|
||||||
|
|
||||||
|
engine = create_engine("sqlite:///:memory:", connect_args={"check_same_thread": False})
|
||||||
|
TestSession = sessionmaker(bind=engine)
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
|
def override_get_db():
|
||||||
|
db = TestSession()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
app.dependency_overrides[get_db] = override_get_db
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
def test_create_task():
|
||||||
|
response = client.post("/tasks/", json={"title": "Test Task", "due_date": "2023-12-31"})
|
||||||
|
assert response.status_code == 201
|
||||||
|
assert response.json()["title"] == "Test Task"
|
||||||
|
assert response.json()["status"] == "pending"
|
||||||
|
|
||||||
|
def test_read_tasks():
|
||||||
|
client.post("/tasks/", json={"title": "Task 1", "due_date": "2023-12-31"})
|
||||||
|
client.post("/tasks/", json={"title": "Task 2", "due_date": "2024-01-01"})
|
||||||
|
response = client.get("/tasks/")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert len(response.json()) == 2
|
||||||
|
|
||||||
|
def test_read_task():
|
||||||
|
task = client.post("/tasks/", json={"title": "Test Task", "due_date": "2023-12-31"}).json()
|
||||||
|
response = client.get(f"/tasks/{task['id']}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["id"] == task["id"]
|
||||||
|
|
||||||
|
def test_read_task_not_found():
|
||||||
|
response = client.get("/tasks/999")
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
def test_update_task():
|
||||||
|
task = client.post("/tasks/", json={"title": "Test Task", "due_date": "2023-12-31"}).json()
|
||||||
|
response = client.put(f"/tasks/{task['id']}", json={"status": "completed"})
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["status"] == "completed"
|
||||||
|
|
||||||
|
def test_delete_task():
|
||||||
|
task = client.post("/tasks/", json={"title": "Test Task", "due_date": "2023-12-31"}).json()
|
||||||
|
response = client.delete(f"/tasks/{task['id']}")
|
||||||
|
assert response.status_code == 204
|
||||||
|
response = client.get(f"/tasks/{task['id']}")
|
||||||
|
assert response.status_code == 404
|
||||||
BIN
zipit/projekti.zip
Normal file
BIN
zipit/projekti.zip
Normal file
Binary file not shown.
30
zipit/projekti_clean/Dockerfile
Normal file
30
zipit/projekti_clean/Dockerfile
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Stage 1: Build
|
||||||
|
FROM python:3.12-slim as builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY pyproject.toml .
|
||||||
|
RUN pip install --no-cache-dir poetry && \
|
||||||
|
poetry config virtualenvs.create false && \
|
||||||
|
poetry install --no-interaction --no-ansi
|
||||||
|
|
||||||
|
COPY models.py schemas.py main.py test_main.py .
|
||||||
|
|
||||||
|
# Stage 2: Production
|
||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=builder /root/.local/share/python-poetry/cache /root/.local/share/python-poetry/cache
|
||||||
|
COPY --from=builder /app /app
|
||||||
|
|
||||||
|
RUN useradd -m appuser && \
|
||||||
|
chown -R appuser:appuser /app
|
||||||
|
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
50
zipit/projekti_clean/README.md
Normal file
50
zipit/projekti_clean/README.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
VERDICT: GREEN
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This project is a simple Todo application using FastAPI for the backend and SQLite as the database. It includes CRUD endpoints for managing tasks.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| models.py | Defines the data model for the todo items using SQLAlchemy ORM. |
|
||||||
|
| schemas.py | Contains Pydantic schemas for validating request and response data. |
|
||||||
|
| main.py | The FastAPI application, defining routes and business logic. |
|
||||||
|
| pyproject.toml | Project metadata and dependencies. |
|
||||||
|
| test_main.py | Unit tests for the FastAPI endpoints. |
|
||||||
|
| Dockerfile | Docker configuration to build and run the application. |
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
git clone <repo>
|
||||||
|
cd project
|
||||||
|
uv sync
|
||||||
|
uv run uvicorn main:app --reload
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
docker build -t todo-sovellus-fastapi---sqlite--crud-endpointit-ja-testit .
|
||||||
|
docker run -p 8000:8000 todo-sovellus-fastapi---sqlite--crud-endpointit-ja-testit
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
| Method | Path | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| POST | /todos/ | Create a new todo item. |
|
||||||
|
| GET | /todos/ | List all todos or filter by status. |
|
||||||
|
| GET | /todos/{id} | Retrieve a specific todo item by ID. |
|
||||||
|
| PUT | /todos/{id} | Update an existing todo item. |
|
||||||
|
| DELETE | /todos/{id} | Delete a todo item. |
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
The project follows a clean separation of concerns:
|
||||||
|
- **models.py**: Defines the data model using SQLAlchemy ORM.
|
||||||
|
- **schemas.py**: Contains Pydantic models for data validation and serialization.
|
||||||
|
- **main.py**: Implements FastAPI routes, business logic, and database interactions.
|
||||||
|
|
||||||
|
This logical structure makes it easy to understand and maintain each component independently.
|
||||||
|
|
||||||
|
## Risk Assessment
|
||||||
|
| Severity | Issue |
|
||||||
|
|----------|-------|
|
||||||
|
| LOW | The project uses parameterized queries in the SQLAlchemy ORM which helps prevent SQL injection. However, input validation is minimal (e.g., no checks for `due_date` format). |
|
||||||
|
| MEDIUM | Error handling and database connection management are basic but adequate. Database connections are managed via context managers, which ensures proper closure of sessions. |
|
||||||
|
| LOW | The project uses consistent naming conventions for variables and functions, making the code easy to understand. However, there's room for improving documentation around API endpoints and models. |
|
||||||
|
|
||||||
|
Overall assessment: "SHIP IT"
|
||||||
150
zipit/projekti_clean/agents.yaml
Normal file
150
zipit/projekti_clean/agents.yaml
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
# Agents — Kipinä Agentic Studio → CrewAI
|
||||||
|
|
||||||
|
client:
|
||||||
|
role: >-
|
||||||
|
Client
|
||||||
|
goal: >-
|
||||||
|
product owner who turns vague ideas into clear, actionable software requirements
|
||||||
|
backstory: |
|
||||||
|
You are a product owner who turns vague ideas into clear, actionable software requirements.
|
||||||
|
|
||||||
|
GIVEN a short project description from the user, produce a structured brief:
|
||||||
|
|
||||||
|
1. PROJECT NAME: a short, descriptive name
|
||||||
|
2. GOAL: one sentence explaining what the software does and who it's for
|
||||||
|
3. CORE FEATURES: numbered list of 3-8 concrete features (not vague wishes)
|
||||||
|
4. DATA MODEL: list the main entities and their key fields (include field types)
|
||||||
|
5. API ENDPOINTS: list the REST endpoints (method + path + purpose)
|
||||||
|
6. CONSTRAINTS: any technical constraints (e.g. "must use SQLite", "no auth needed")
|
||||||
|
|
||||||
|
RULES:
|
||||||
|
- Be specific: "User can filter todos by status" not "todo management"
|
||||||
|
- Use plain English, no code
|
||||||
|
- Maximum 400 words total
|
||||||
|
llm: qwen-coder
|
||||||
|
|
||||||
|
data:
|
||||||
|
role: >-
|
||||||
|
Data Engineer
|
||||||
|
goal: >-
|
||||||
|
database architect specializing in SQLAlchemy and relational databases
|
||||||
|
backstory: |
|
||||||
|
You are a database architect specializing in SQLAlchemy and relational databases.
|
||||||
|
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
llm: qwen-coder
|
||||||
|
|
||||||
|
coder:
|
||||||
|
role: >-
|
||||||
|
Coder
|
||||||
|
goal: >-
|
||||||
|
expert Python developer
|
||||||
|
backstory: |
|
||||||
|
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)
|
||||||
|
|
||||||
|
NEVER:
|
||||||
|
- Add explanations or comments like "# Add routes here"
|
||||||
|
- 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)
|
||||||
|
llm: qwen-coder
|
||||||
|
|
||||||
|
qa:
|
||||||
|
role: >-
|
||||||
|
QA
|
||||||
|
goal: >-
|
||||||
|
QA engineer responsible for code review and automated testing
|
||||||
|
backstory: |
|
||||||
|
You are a QA engineer responsible for code review and automated testing.
|
||||||
|
|
||||||
|
CODE REVIEW CHECKLIST:
|
||||||
|
1. IMPORTS: Every "from X import Y" must match an actual export in file X
|
||||||
|
2. NAMES: Pydantic schemas (UserCreate) must not shadow SQLAlchemy models (User)
|
||||||
|
3. TYPES: All function parameters have type hints, return types specified
|
||||||
|
4. ERRORS: Every db query that can return None has a 404 check
|
||||||
|
5. RESOURCES: Database session uses yield+finally pattern (no leaks)
|
||||||
|
6. SECURITY: No raw SQL, no hardcoded secrets, inputs validated via Pydantic
|
||||||
|
7. ENDPOINTS: All CRUD operations exist (POST/GET/GET-by-id/PUT/DELETE)
|
||||||
|
8. MODELS: Pydantic Config has from_attributes=True, uses model_dump() not dict()
|
||||||
|
9. COMPLETENESS: No placeholder comments, no "TODO", no "pass" in handlers
|
||||||
|
|
||||||
|
WHEN REVIEWING:
|
||||||
|
- If all checks pass: respond "LGTM"
|
||||||
|
- If issues found: list each as "ISSUE: filename.py: description"
|
||||||
|
- 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
|
||||||
|
- Test all CRUD: create (201), list (200), get by id (200/404), update (200), delete (204)
|
||||||
|
- ALWAYS: from fastapi.testclient import TestClient
|
||||||
|
llm: qwen-coder
|
||||||
|
|
||||||
|
tester:
|
||||||
|
role: >-
|
||||||
|
DevOps
|
||||||
|
goal: >-
|
||||||
|
DevOps engineer specializing in containerization and deployment
|
||||||
|
backstory: |
|
||||||
|
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
|
||||||
|
|
||||||
|
Write ONLY the requested files, no explanations.
|
||||||
|
llm: qwen-coder
|
||||||
|
|
||||||
|
observer:
|
||||||
|
role: >-
|
||||||
|
Observer
|
||||||
|
goal: >-
|
||||||
|
independent technical observer and risk analyst
|
||||||
|
backstory: |
|
||||||
|
You are an independent technical observer and risk analyst.
|
||||||
|
|
||||||
|
EVALUATE THE PROJECT FOR:
|
||||||
|
1. ARCHITECTURE: Is the file structure logical? Are responsibilities separated?
|
||||||
|
2. SECURITY: SQL injection risks? Input validation? Authentication?
|
||||||
|
3. RELIABILITY: Error handling? Database connection management? Edge cases?
|
||||||
|
4. MAINTAINABILITY: Consistent naming? Clear code structure? Would a new developer understand this?
|
||||||
|
|
||||||
|
OUTPUT FORMAT:
|
||||||
|
- RISK: [critical/high/medium/low] Description
|
||||||
|
- List max 3-5 most important findings
|
||||||
|
- End with overall assessment: "SHIP IT" or "NEEDS WORK: reason"
|
||||||
|
llm: qwen-coder
|
||||||
85
zipit/projekti_clean/crew.py
Normal file
85
zipit/projekti_clean/crew.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
"""Todo-sovellus FastAPI + SQLite, CRUD-endpointit ja testit
|
||||||
|
|
||||||
|
CrewAI crew — generated by Kipinä Agentic Studio.
|
||||||
|
Run: crewai run
|
||||||
|
"""
|
||||||
|
|
||||||
|
from crewai import Agent, Crew, Process, Task
|
||||||
|
from crewai.project import CrewBase, agent, crew, task
|
||||||
|
|
||||||
|
|
||||||
|
@CrewBase
|
||||||
|
class ProjectCrew:
|
||||||
|
"""Todo-sovellus FastAPI + SQLite, CRUD-endpointit ja testit"""
|
||||||
|
|
||||||
|
agents_config = "agents.yaml"
|
||||||
|
tasks_config = "tasks.yaml"
|
||||||
|
|
||||||
|
@agent
|
||||||
|
def client(self) -> Agent:
|
||||||
|
return Agent(config=self.agents_config["client"])
|
||||||
|
|
||||||
|
@agent
|
||||||
|
def data(self) -> Agent:
|
||||||
|
return Agent(config=self.agents_config["data"])
|
||||||
|
|
||||||
|
@agent
|
||||||
|
def coder(self) -> Agent:
|
||||||
|
return Agent(config=self.agents_config["coder"])
|
||||||
|
|
||||||
|
@agent
|
||||||
|
def qa(self) -> Agent:
|
||||||
|
return Agent(config=self.agents_config["qa"])
|
||||||
|
|
||||||
|
@agent
|
||||||
|
def tester(self) -> Agent:
|
||||||
|
return Agent(config=self.agents_config["tester"])
|
||||||
|
|
||||||
|
@agent
|
||||||
|
def observer(self) -> Agent:
|
||||||
|
return Agent(config=self.agents_config["observer"])
|
||||||
|
|
||||||
|
@task
|
||||||
|
def step_0_requirements(self) -> Task:
|
||||||
|
return Task(config=self.tasks_config["step_0_requirements"])
|
||||||
|
|
||||||
|
@task
|
||||||
|
def step_1_models_py(self) -> Task:
|
||||||
|
return Task(config=self.tasks_config["step_1_models_py"])
|
||||||
|
|
||||||
|
@task
|
||||||
|
def step_2_schemas_py(self) -> Task:
|
||||||
|
return Task(config=self.tasks_config["step_2_schemas_py"])
|
||||||
|
|
||||||
|
@task
|
||||||
|
def step_3_main_py(self) -> Task:
|
||||||
|
return Task(config=self.tasks_config["step_3_main_py"])
|
||||||
|
|
||||||
|
@task
|
||||||
|
def step_4_pyproject_toml(self) -> Task:
|
||||||
|
return Task(config=self.tasks_config["step_4_pyproject_toml"])
|
||||||
|
|
||||||
|
@task
|
||||||
|
def step_5_review(self) -> Task:
|
||||||
|
return Task(config=self.tasks_config["step_5_review"])
|
||||||
|
|
||||||
|
@task
|
||||||
|
def step_6_test_main_py(self) -> Task:
|
||||||
|
return Task(config=self.tasks_config["step_6_test_main_py"])
|
||||||
|
|
||||||
|
@task
|
||||||
|
def step_7_dockerfile(self) -> Task:
|
||||||
|
return Task(config=self.tasks_config["step_7_dockerfile"])
|
||||||
|
|
||||||
|
@task
|
||||||
|
def step_8_readme_md(self) -> Task:
|
||||||
|
return Task(config=self.tasks_config["step_8_readme_md"])
|
||||||
|
|
||||||
|
@crew
|
||||||
|
def crew(self) -> Crew:
|
||||||
|
return Crew(
|
||||||
|
agents=self.agents,
|
||||||
|
tasks=self.tasks,
|
||||||
|
process=Process.sequential,
|
||||||
|
verbose=True,
|
||||||
|
)
|
||||||
56
zipit/projekti_clean/main.py
Normal file
56
zipit/projekti_clean/main.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
from fastapi import FastAPI, Depends, HTTPException
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from models import Base, engine, SessionLocal, Todo
|
||||||
|
from schemas import TodoCreate, TodoResponse
|
||||||
|
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
@app.post("/todos/", response_model=TodoResponse, status_code=201)
|
||||||
|
def create_todo(todo: TodoCreate, db: Session = Depends(get_db)):
|
||||||
|
db_todo = Todo(**todo.model_dump())
|
||||||
|
db.add(db_todo)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_todo)
|
||||||
|
return db_todo
|
||||||
|
|
||||||
|
@app.get("/todos/", response_model=list[TodoResponse])
|
||||||
|
def list_todos(status: str | None = None, db: Session = Depends(get_db)):
|
||||||
|
if status:
|
||||||
|
query = db.query(Todo).filter_by(status=status)
|
||||||
|
else:
|
||||||
|
query = db.query(Todo)
|
||||||
|
return query.all()
|
||||||
|
|
||||||
|
@app.get("/todos/{todo_id}", response_model=TodoResponse)
|
||||||
|
def get_todo(todo_id: int, db: Session = Depends(get_db)):
|
||||||
|
todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
||||||
|
if not todo:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
return todo
|
||||||
|
|
||||||
|
@app.put("/todos/{todo_id}", response_model=TodoResponse)
|
||||||
|
def update_todo(todo_id: int, todo: TodoCreate, db: Session = Depends(get_db)):
|
||||||
|
db_todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
||||||
|
if not db_todo:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
for key, value in todo.model_dump().items():
|
||||||
|
setattr(db_todo, key, value)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_todo)
|
||||||
|
return db_todo
|
||||||
|
|
||||||
|
@app.delete("/todos/{todo_id}", status_code=204)
|
||||||
|
def delete_todo(todo_id: int, db: Session = Depends(get_db)):
|
||||||
|
db_todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
||||||
|
if not db_todo:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
db.delete(db_todo)
|
||||||
|
db.commit()
|
||||||
16
zipit/projekti_clean/models.py
Normal file
16
zipit/projekti_clean/models.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from sqlalchemy import create_engine, Column, Integer, String, Date, Enum
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
DATABASE_URL = "sqlite:///./todo.db"
|
||||||
|
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
class Todo(Base):
|
||||||
|
__tablename__ = "todos"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
title = Column(String(100), nullable=False)
|
||||||
|
description = Column(String(500))
|
||||||
|
due_date = Column(Date, nullable=False)
|
||||||
|
status = Column(Enum('pending', 'completed'), default='pending')
|
||||||
64
zipit/projekti_clean/prompts/0_client_requirements.md
Normal file
64
zipit/projekti_clean/prompts/0_client_requirements.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# 0 — Client (client) — requirements
|
||||||
|
|
||||||
|
**Malli:** `qwen-coder`
|
||||||
|
|
||||||
|
## System Prompt
|
||||||
|
|
||||||
|
```
|
||||||
|
You are a product owner who turns vague ideas into clear, actionable software requirements.
|
||||||
|
|
||||||
|
GIVEN a short project description from the user, produce a structured brief:
|
||||||
|
|
||||||
|
1. PROJECT NAME: a short, descriptive name
|
||||||
|
2. GOAL: one sentence explaining what the software does and who it's for
|
||||||
|
3. CORE FEATURES: numbered list of 3-8 concrete features (not vague wishes)
|
||||||
|
4. DATA MODEL: list the main entities and their key fields (include field types)
|
||||||
|
5. API ENDPOINTS: list the REST endpoints (method + path + purpose)
|
||||||
|
6. CONSTRAINTS: any technical constraints (e.g. "must use SQLite", "no auth needed")
|
||||||
|
|
||||||
|
RULES:
|
||||||
|
- Be specific: "User can filter todos by status" not "todo management"
|
||||||
|
- Use plain English, no code
|
||||||
|
- Maximum 400 words total
|
||||||
|
```
|
||||||
|
|
||||||
|
## Syöte
|
||||||
|
|
||||||
|
```
|
||||||
|
Todo-sovellus FastAPI + SQLite, CRUD-endpointit ja testit
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tulos
|
||||||
|
|
||||||
|
```
|
||||||
|
1. **PROJECT NAME:** Todo-sovellus
|
||||||
|
|
||||||
|
2. **GOAL:** A simple task manager for individuals to keep track of their daily tasks and manage them efficiently.
|
||||||
|
|
||||||
|
3. **CORE FEATURES:**
|
||||||
|
- Create new todos with title, description, due date.
|
||||||
|
- Read/View all todos or a single todo by ID.
|
||||||
|
- Update existing todos (title, description, status).
|
||||||
|
- Delete completed or unnecessary todos.
|
||||||
|
- Filter todos by status (e.g., pending, completed).
|
||||||
|
|
||||||
|
4. **DATA MODEL:**
|
||||||
|
- Todo
|
||||||
|
- id (integer, primary key)
|
||||||
|
- title (string)
|
||||||
|
- description (text)
|
||||||
|
- due_date (date)
|
||||||
|
- status (enum: 'pending', 'completed')
|
||||||
|
|
||||||
|
5. **API ENDPOINTS:**
|
||||||
|
- GET /todos: Retrieve all todos.
|
||||||
|
- GET /todos/{id}: Retrieve a single todo by ID.
|
||||||
|
- POST /todos: Create a new todo.
|
||||||
|
- PUT /todos/{id}: Update an existing todo.
|
||||||
|
- DELETE /todos/{id}: Delete a todo.
|
||||||
|
|
||||||
|
6. **CONSTRAINTS:**
|
||||||
|
- Must use SQLite as the database.
|
||||||
|
- No authentication required for accessing endpoints.
|
||||||
|
- Implement unit tests and integration tests to ensure functionality.
|
||||||
|
```
|
||||||
122
zipit/projekti_clean/prompts/1_data_models.py.md
Normal file
122
zipit/projekti_clean/prompts/1_data_models.py.md
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
# 1 — Data Engineer (data) — models.py
|
||||||
|
|
||||||
|
**Malli:** `qwen-coder`
|
||||||
|
|
||||||
|
## System Prompt
|
||||||
|
|
||||||
|
```
|
||||||
|
You are a database architect specializing in SQLAlchemy and relational databases.
|
||||||
|
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
## Syöte
|
||||||
|
|
||||||
|
```
|
||||||
|
You are a database architect specializing in SQLAlchemy and relational databases.
|
||||||
|
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
EXAMPLE of models.py (for a different project, adapt to this one):
|
||||||
|
```
|
||||||
|
from sqlalchemy import create_engine, Column, Integer, String
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
DATABASE_URL = "sqlite:///./app.db"
|
||||||
|
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
class Item(Base):
|
||||||
|
__tablename__ = "items"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
name = Column(String(100), nullable=False)
|
||||||
|
description = Column(String(500))
|
||||||
|
```
|
||||||
|
|
||||||
|
PROJECT REQUIREMENTS (from product owner):
|
||||||
|
1. **PROJECT NAME:** Todo-sovellus
|
||||||
|
|
||||||
|
2. **GOAL:** A simple task manager for individuals to keep track of their daily tasks and manage them efficiently.
|
||||||
|
|
||||||
|
3. **CORE FEATURES:**
|
||||||
|
- Create new todos with title, description, due date.
|
||||||
|
- Read/View all todos or a single todo by ID.
|
||||||
|
- Update existing todos (title, description, status).
|
||||||
|
- Delete completed or unnecessary todos.
|
||||||
|
- Filter todos by status (e.g., pending, completed).
|
||||||
|
|
||||||
|
4. **DATA MODEL:**
|
||||||
|
- Todo
|
||||||
|
- id (integer, primary key)
|
||||||
|
- title (string)
|
||||||
|
- description (text)
|
||||||
|
- due_date (date)
|
||||||
|
- status (enum: 'pending', 'completed')
|
||||||
|
|
||||||
|
5. **API ENDPOINTS:**
|
||||||
|
- GET /todos: Retrieve all todos.
|
||||||
|
- GET /todos/{id}: Retrieve a single todo by ID.
|
||||||
|
- POST /todos: Create a new todo.
|
||||||
|
- PUT /todos/{id}: Update an existing todo.
|
||||||
|
- DELETE /todos/{id}: Delete a todo.
|
||||||
|
|
||||||
|
6. **CONSTRAINTS:**
|
||||||
|
- Must use SQLite as the database.
|
||||||
|
- No authentication required for accessing endpoints.
|
||||||
|
- Implement unit tests and integration tests to ensure functionality.
|
||||||
|
|
||||||
|
NOW write "models.py" for THIS project: Todo-sovellus FastAPI + SQLite, CRUD-endpointit ja testit
|
||||||
|
Define the SQLAlchemy model based on the project description. Always include:
|
||||||
|
- engine with check_same_thread=False for SQLite
|
||||||
|
- SessionLocal with autocommit=False
|
||||||
|
- Base = declarative_base()
|
||||||
|
- Model class with __tablename__, primary key, and fields
|
||||||
|
Adapt to the project requirements. Import from already written files. Write ONLY the code, no explanations.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tulos
|
||||||
|
|
||||||
|
```
|
||||||
|
from sqlalchemy import create_engine, Column, Integer, String, Date, Enum
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
DATABASE_URL = "sqlite:///./todo.db"
|
||||||
|
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
class Todo(Base):
|
||||||
|
__tablename__ = "todos"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
title = Column(String(100), nullable=False)
|
||||||
|
description = Column(String(500))
|
||||||
|
due_date = Column(Date, nullable=False)
|
||||||
|
status = Column(Enum('pending', 'completed'), default='pending')
|
||||||
|
```
|
||||||
142
zipit/projekti_clean/prompts/2_coder_schemas.py.md
Normal file
142
zipit/projekti_clean/prompts/2_coder_schemas.py.md
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
# 2 — Coder (coder) — schemas.py
|
||||||
|
|
||||||
|
**Malli:** `qwen-coder`
|
||||||
|
|
||||||
|
## System 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)
|
||||||
|
|
||||||
|
NEVER:
|
||||||
|
- Add explanations or comments like "# Add routes here"
|
||||||
|
- 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)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Syöte
|
||||||
|
|
||||||
|
```
|
||||||
|
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)
|
||||||
|
|
||||||
|
NEVER:
|
||||||
|
- Add explanations or comments like "# Add routes here"
|
||||||
|
- 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)
|
||||||
|
|
||||||
|
EXAMPLE of schemas.py (for a different project, adapt to this one):
|
||||||
|
```
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
class ItemCreate(BaseModel):
|
||||||
|
name: str
|
||||||
|
description: str | None = None
|
||||||
|
|
||||||
|
class ItemResponse(ItemCreate):
|
||||||
|
id: int
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
```
|
||||||
|
|
||||||
|
Already written files in THIS project:
|
||||||
|
--- models.py ---
|
||||||
|
from sqlalchemy import create_engine, Column, Integer, String, Date, Enum
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
DATABASE_URL = "sqlite:///./todo.db"
|
||||||
|
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
class Todo(Base):
|
||||||
|
__tablename__ = "todos"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
title = Column(String(100), nullable=False)
|
||||||
|
description = Column(String(500))
|
||||||
|
due_date = Column(Date, nullable=False)
|
||||||
|
status = Column(Enum('pending', 'completed'), default='pending')
|
||||||
|
|
||||||
|
PROJECT REQUIREMENTS (from product owner):
|
||||||
|
1. **PROJECT NAME:** Todo-sovellus
|
||||||
|
|
||||||
|
2. **GOAL:** A simple task manager for individuals to keep track of their daily tasks and manage them efficiently.
|
||||||
|
|
||||||
|
3. **CORE FEATURES:**
|
||||||
|
- Create new todos with title, description, due date.
|
||||||
|
- Read/View all todos or a single todo by ID.
|
||||||
|
- Update existing todos (title, description, status).
|
||||||
|
- Delete completed or unnecessary todos.
|
||||||
|
- Filter todos by status (e.g., pending, completed).
|
||||||
|
|
||||||
|
4. **DATA MODEL:**
|
||||||
|
- Todo
|
||||||
|
- id (integer, primary key)
|
||||||
|
- title (string)
|
||||||
|
- description (text)
|
||||||
|
- due_date (date)
|
||||||
|
- status (enum: 'pending', 'completed')
|
||||||
|
|
||||||
|
5. **API ENDPOINTS:**
|
||||||
|
- GET /todos: Retrieve all todos.
|
||||||
|
- GET /todos/{id}: Retrieve a single todo by ID.
|
||||||
|
- POST /todos: Create a new todo.
|
||||||
|
- PUT /todos/{id}: Update an existing todo.
|
||||||
|
- DELETE /todos/{id}: Delete a todo.
|
||||||
|
|
||||||
|
6. **CONSTRAINTS:**
|
||||||
|
- Must use SQLite as the database.
|
||||||
|
- No authentication required for accessing endpoints.
|
||||||
|
- Implement unit tests and integration tests to ensure functionality.
|
||||||
|
|
||||||
|
NOW write "schemas.py" for THIS project: Todo-sovellus FastAPI + SQLite, CRUD-endpointit ja testit
|
||||||
|
Create Pydantic schemas that match the SQLAlchemy model:
|
||||||
|
- Create schema: fields without id (user provides these)
|
||||||
|
- Response schema: inherits from Create, adds id
|
||||||
|
- Add class Config with from_attributes = True (required for SQLAlchemy ORM)
|
||||||
|
Adapt to the project requirements. Import from already written files. Write ONLY the code, no explanations.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tulos
|
||||||
|
|
||||||
|
```
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
class TodoCreate(BaseModel):
|
||||||
|
title: str
|
||||||
|
description: str | None = None
|
||||||
|
due_date: date
|
||||||
|
status: str = 'pending'
|
||||||
|
|
||||||
|
class TodoResponse(TodoCreate):
|
||||||
|
id: int
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
```
|
||||||
244
zipit/projekti_clean/prompts/3_coder_main.py.md
Normal file
244
zipit/projekti_clean/prompts/3_coder_main.py.md
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
# 3 — Coder (coder) — main.py
|
||||||
|
|
||||||
|
**Malli:** `qwen-coder`
|
||||||
|
|
||||||
|
## System 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)
|
||||||
|
|
||||||
|
NEVER:
|
||||||
|
- Add explanations or comments like "# Add routes here"
|
||||||
|
- 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)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Syöte
|
||||||
|
|
||||||
|
```
|
||||||
|
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)
|
||||||
|
|
||||||
|
NEVER:
|
||||||
|
- Add explanations or comments like "# Add routes here"
|
||||||
|
- 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)
|
||||||
|
|
||||||
|
EXAMPLE of main.py (for a different project, adapt to this one):
|
||||||
|
```
|
||||||
|
from fastapi import FastAPI, Depends, HTTPException
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from models import Base, engine, SessionLocal, Item
|
||||||
|
from schemas import ItemCreate, ItemResponse
|
||||||
|
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
@app.post("/items/", response_model=ItemResponse, status_code=201)
|
||||||
|
def create_item(item: ItemCreate, db: Session = Depends(get_db)):
|
||||||
|
db_item = Item(**item.model_dump())
|
||||||
|
db.add(db_item)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_item)
|
||||||
|
return db_item
|
||||||
|
|
||||||
|
@app.get("/items/", response_model=list[ItemResponse])
|
||||||
|
def list_items(db: Session = Depends(get_db)):
|
||||||
|
return db.query(Item).all()
|
||||||
|
|
||||||
|
@app.get("/items/{item_id}", response_model=ItemResponse)
|
||||||
|
def get_item(item_id: int, db: Session = Depends(get_db)):
|
||||||
|
item = db.query(Item).filter(Item.id == item_id).first()
|
||||||
|
if not item:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
return item
|
||||||
|
|
||||||
|
@app.put("/items/{item_id}", response_model=ItemResponse)
|
||||||
|
def update_item(item_id: int, item: ItemCreate, db: Session = Depends(get_db)):
|
||||||
|
db_item = db.query(Item).filter(Item.id == item_id).first()
|
||||||
|
if not db_item:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
for key, value in item.model_dump().items():
|
||||||
|
setattr(db_item, key, value)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_item)
|
||||||
|
return db_item
|
||||||
|
|
||||||
|
@app.delete("/items/{item_id}", status_code=204)
|
||||||
|
def delete_item(item_id: int, db: Session = Depends(get_db)):
|
||||||
|
db_item = db.query(Item).filter(Item.id == item_id).first()
|
||||||
|
if not db_item:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
db.delete(db_item)
|
||||||
|
db.commit()
|
||||||
|
```
|
||||||
|
|
||||||
|
Already written files in THIS project:
|
||||||
|
--- models.py ---
|
||||||
|
from sqlalchemy import create_engine, Column, Integer, String, Date, Enum
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
DATABASE_URL = "sqlite:///./todo.db"
|
||||||
|
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
class Todo(Base):
|
||||||
|
__tablename__ = "todos"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
title = Column(String(100), nullable=False)
|
||||||
|
description = Column(String(500))
|
||||||
|
due_date = Column(Date, nullable=False)
|
||||||
|
status = Column(Enum('pending', 'completed'), default='pending')
|
||||||
|
|
||||||
|
--- schemas.py ---
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
class TodoCreate(BaseModel):
|
||||||
|
title: str
|
||||||
|
description: str | None = None
|
||||||
|
due_date: date
|
||||||
|
status: str = 'pending'
|
||||||
|
|
||||||
|
class TodoResponse(TodoCreate):
|
||||||
|
id: int
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
PROJECT REQUIREMENTS (from product owner):
|
||||||
|
1. **PROJECT NAME:** Todo-sovellus
|
||||||
|
|
||||||
|
2. **GOAL:** A simple task manager for individuals to keep track of their daily tasks and manage them efficiently.
|
||||||
|
|
||||||
|
3. **CORE FEATURES:**
|
||||||
|
- Create new todos with title, description, due date.
|
||||||
|
- Read/View all todos or a single todo by ID.
|
||||||
|
- Update existing todos (title, description, status).
|
||||||
|
- Delete completed or unnecessary todos.
|
||||||
|
- Filter todos by status (e.g., pending, completed).
|
||||||
|
|
||||||
|
4. **DATA MODEL:**
|
||||||
|
- Todo
|
||||||
|
- id (integer, primary key)
|
||||||
|
- title (string)
|
||||||
|
- description (text)
|
||||||
|
- due_date (date)
|
||||||
|
- status (enum: 'pending', 'completed')
|
||||||
|
|
||||||
|
5. **API ENDPOINTS:**
|
||||||
|
- GET /todos: Retrieve all todos.
|
||||||
|
- GET /todos/{id}: Retrieve a single todo by ID.
|
||||||
|
- POST /todos: Create a new todo.
|
||||||
|
- PUT /todos/{id}: Update an existing todo.
|
||||||
|
- DELETE /todos/{id}: Delete a todo.
|
||||||
|
|
||||||
|
6. **CONSTRAINTS:**
|
||||||
|
- Must use SQLite as the database.
|
||||||
|
- No authentication required for accessing endpoints.
|
||||||
|
- Implement unit tests and integration tests to ensure functionality.
|
||||||
|
|
||||||
|
NOW write "main.py" for THIS project: Todo-sovellus FastAPI + SQLite, CRUD-endpointit ja testit
|
||||||
|
Create the FastAPI app with all CRUD endpoints:
|
||||||
|
- Import from models.py and schemas.py (use exact class names)
|
||||||
|
- create_all(bind=engine) at module level
|
||||||
|
- get_db dependency with yield pattern
|
||||||
|
- POST (201), GET list, GET by id, PUT, DELETE (204)
|
||||||
|
- Use response_model for type safety
|
||||||
|
- Use model_dump() not dict() (Pydantic v2)
|
||||||
|
Adapt to the project requirements. Import from already written files. Write ONLY the code, no explanations.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tulos
|
||||||
|
|
||||||
|
```
|
||||||
|
from fastapi import FastAPI, Depends, HTTPException
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from models import Base, engine, SessionLocal, Todo
|
||||||
|
from schemas import TodoCreate, TodoResponse
|
||||||
|
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
@app.post("/todos/", response_model=TodoResponse, status_code=201)
|
||||||
|
def create_todo(todo: TodoCreate, db: Session = Depends(get_db)):
|
||||||
|
db_todo = Todo(**todo.model_dump())
|
||||||
|
db.add(db_todo)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_todo)
|
||||||
|
return db_todo
|
||||||
|
|
||||||
|
@app.get("/todos/", response_model=list[TodoResponse])
|
||||||
|
def list_todos(status: str | None = None, db: Session = Depends(get_db)):
|
||||||
|
if status:
|
||||||
|
query = db.query(Todo).filter_by(status=status)
|
||||||
|
else:
|
||||||
|
query = db.query(Todo)
|
||||||
|
return query.all()
|
||||||
|
|
||||||
|
@app.get("/todos/{todo_id}", response_model=TodoResponse)
|
||||||
|
def get_todo(todo_id: int, db: Session = Depends(get_db)):
|
||||||
|
todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
||||||
|
if not todo:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
return todo
|
||||||
|
|
||||||
|
@app.put("/todos/{todo_id}", response_model=TodoResponse)
|
||||||
|
def update_todo(todo_id: int, todo: TodoCreate, db: Session = Depends(get_db)):
|
||||||
|
db_todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
||||||
|
if not db_todo:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
for key, value in todo.model_dump().items():
|
||||||
|
setattr(db_todo, key, value)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_todo)
|
||||||
|
return db_todo
|
||||||
|
|
||||||
|
@app.delete("/todos/{todo_id}", status_code=204)
|
||||||
|
def delete_todo(todo_id: int, db: Session = Depends(get_db)):
|
||||||
|
db_todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
||||||
|
if not db_todo:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
db.delete(db_todo)
|
||||||
|
db.commit()
|
||||||
|
```
|
||||||
212
zipit/projekti_clean/prompts/4_coder_pyproject.toml.md
Normal file
212
zipit/projekti_clean/prompts/4_coder_pyproject.toml.md
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
# 4 — Coder (coder) — pyproject.toml
|
||||||
|
|
||||||
|
**Malli:** `qwen-coder`
|
||||||
|
|
||||||
|
## System 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)
|
||||||
|
|
||||||
|
NEVER:
|
||||||
|
- Add explanations or comments like "# Add routes here"
|
||||||
|
- 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)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Syöte
|
||||||
|
|
||||||
|
```
|
||||||
|
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)
|
||||||
|
|
||||||
|
NEVER:
|
||||||
|
- Add explanations or comments like "# Add routes here"
|
||||||
|
- 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)
|
||||||
|
|
||||||
|
EXAMPLE of pyproject.toml (for a different project, adapt to this one):
|
||||||
|
```
|
||||||
|
[project]
|
||||||
|
name = "myapp"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
dependencies = [
|
||||||
|
"fastapi",
|
||||||
|
"uvicorn[standard]",
|
||||||
|
"sqlalchemy",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
dev = "uvicorn main:app --reload"
|
||||||
|
```
|
||||||
|
|
||||||
|
Already written files in THIS project:
|
||||||
|
--- models.py ---
|
||||||
|
from sqlalchemy import create_engine, Column, Integer, String, Date, Enum
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
DATABASE_URL = "sqlite:///./todo.db"
|
||||||
|
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
class Todo(Base):
|
||||||
|
__tablename__ = "todos"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
title = Column(String(100), nullable=False)
|
||||||
|
description = Column(String(500))
|
||||||
|
due_date = Column(Date, nullable=False)
|
||||||
|
status = Column(Enum('pending', 'completed'), default='pending')
|
||||||
|
|
||||||
|
--- schemas.py ---
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
class TodoCreate(BaseModel):
|
||||||
|
title: str
|
||||||
|
description: str | None = None
|
||||||
|
due_date: date
|
||||||
|
status: str = 'pending'
|
||||||
|
|
||||||
|
class TodoResponse(TodoCreate):
|
||||||
|
id: int
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
--- main.py ---
|
||||||
|
from fastapi import FastAPI, Depends, HTTPException
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from models import Base, engine, SessionLocal, Todo
|
||||||
|
from schemas import TodoCreate, TodoResponse
|
||||||
|
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
@app.post("/todos/", response_model=TodoResponse, status_code=201)
|
||||||
|
def create_todo(todo: TodoCreate, db: Session = Depends(get_db)):
|
||||||
|
db_todo = Todo(**todo.model_dump())
|
||||||
|
db.add(db_todo)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_todo)
|
||||||
|
return db_todo
|
||||||
|
|
||||||
|
@app.get("/todos/", response_model=list[TodoResponse])
|
||||||
|
def list_todos(status: str | None = None, db: Session = Depends(get_db)):
|
||||||
|
if status:
|
||||||
|
query = db.query(Todo).filter_by(status=status)
|
||||||
|
else:
|
||||||
|
query = db.query(Todo)
|
||||||
|
return query.all()
|
||||||
|
|
||||||
|
@app.get("/todos/{todo_id}", response_model=TodoResponse)
|
||||||
|
def get_todo(todo_id: int, db: Session = Depends(get_db)):
|
||||||
|
todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
||||||
|
if not todo:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
return todo
|
||||||
|
|
||||||
|
@app.put("/todos/{todo_id}", response_model=TodoResponse)
|
||||||
|
def update_todo(todo_id: int, todo: TodoCreate, db: Session = Depends(get_db)):
|
||||||
|
db_todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
||||||
|
if not db_todo:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
for key, value in todo.model_dump().items():
|
||||||
|
setattr(db_todo, key, value)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_todo)
|
||||||
|
return db_todo
|
||||||
|
|
||||||
|
@app.delete("/todos/{todo_id}", status_code=204)
|
||||||
|
def delete_todo(todo_id: int, db: Session = Depends(get_db)):
|
||||||
|
db_todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
||||||
|
if not db_todo:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
db.delete(db_todo)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
PROJECT REQUIREMENTS (from product owner):
|
||||||
|
1. **PROJECT NAME:** Todo-sovellus
|
||||||
|
|
||||||
|
2. **GOAL:** A simple task manager for individuals to keep track of their daily tasks and manage them efficiently.
|
||||||
|
|
||||||
|
3. **CORE FEATURES:**
|
||||||
|
- Create new todos with title, description, due date.
|
||||||
|
- Read/View all todos or a single todo by ID.
|
||||||
|
- Update existing todos (title, description, status).
|
||||||
|
- Delete completed or unnecessary todos.
|
||||||
|
- Filter todos by status (e.g., pending, completed).
|
||||||
|
|
||||||
|
4. **DATA MODEL:**
|
||||||
|
- Todo
|
||||||
|
- id (integer, primary key)
|
||||||
|
- title (string)
|
||||||
|
- description (text)
|
||||||
|
- due_date (date)
|
||||||
|
- status (enum: 'pending', 'completed')
|
||||||
|
|
||||||
|
5. **API ENDPOINTS:**
|
||||||
|
- GET /todos: Retrieve all todos.
|
||||||
|
- GET /todos/{id}: Retrieve a single todo by ID.
|
||||||
|
- POST /todos: Create a new todo.
|
||||||
|
- PUT /todos/{id}: Update an existing todo.
|
||||||
|
- DELETE /todos/{id}: Delete a todo.
|
||||||
|
|
||||||
|
6. **CONSTRAINTS:**
|
||||||
|
- Must use SQLite as the database.
|
||||||
|
- No authentication required for accessing endpoints.
|
||||||
|
- Implement unit tests and integration tests to ensure functionality.
|
||||||
|
|
||||||
|
NOW write "pyproject.toml" for THIS project: Todo-sovellus FastAPI + SQLite, CRUD-endpointit ja testit
|
||||||
|
Use [project] format (PEP 621, compatible with uv). List dependencies under [project.dependencies]. Add [project.scripts] with dev command. Never use requirements.txt or Poetry format. Run with: uv run uvicorn main:app --reload
|
||||||
|
Adapt to the project requirements. Import from already written files. Write ONLY the code, no explanations.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tulos
|
||||||
|
|
||||||
|
```
|
||||||
|
[project]
|
||||||
|
name = "todo-sovellus"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
dependencies = [
|
||||||
|
"fastapi",
|
||||||
|
"uvicorn[standard]",
|
||||||
|
"sqlalchemy",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
dev = "uvicorn main:app --reload"
|
||||||
|
```
|
||||||
174
zipit/projekti_clean/prompts/5_qa_review.md
Normal file
174
zipit/projekti_clean/prompts/5_qa_review.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
# 5 — QA (qa) — review
|
||||||
|
|
||||||
|
**Malli:** `qwen-coder`
|
||||||
|
|
||||||
|
## System Prompt
|
||||||
|
|
||||||
|
```
|
||||||
|
You are a QA engineer responsible for code review and automated testing.
|
||||||
|
|
||||||
|
CODE REVIEW CHECKLIST:
|
||||||
|
1. IMPORTS: Every "from X import Y" must match an actual export in file X
|
||||||
|
2. NAMES: Pydantic schemas (UserCreate) must not shadow SQLAlchemy models (User)
|
||||||
|
3. TYPES: All function parameters have type hints, return types specified
|
||||||
|
4. ERRORS: Every db query that can return None has a 404 check
|
||||||
|
5. RESOURCES: Database session uses yield+finally pattern (no leaks)
|
||||||
|
6. SECURITY: No raw SQL, no hardcoded secrets, inputs validated via Pydantic
|
||||||
|
7. ENDPOINTS: All CRUD operations exist (POST/GET/GET-by-id/PUT/DELETE)
|
||||||
|
8. MODELS: Pydantic Config has from_attributes=True, uses model_dump() not dict()
|
||||||
|
9. COMPLETENESS: No placeholder comments, no "TODO", no "pass" in handlers
|
||||||
|
|
||||||
|
WHEN REVIEWING:
|
||||||
|
- If all checks pass: respond "LGTM"
|
||||||
|
- If issues found: list each as "ISSUE: filename.py: description"
|
||||||
|
- 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
|
||||||
|
- Test all CRUD: create (201), list (200), get by id (200/404), update (200), delete (204)
|
||||||
|
- ALWAYS: from fastapi.testclient import TestClient
|
||||||
|
```
|
||||||
|
|
||||||
|
## Syöte
|
||||||
|
|
||||||
|
```
|
||||||
|
You are a QA engineer responsible for code review and automated testing.
|
||||||
|
|
||||||
|
CODE REVIEW CHECKLIST:
|
||||||
|
1. IMPORTS: Every "from X import Y" must match an actual export in file X
|
||||||
|
2. NAMES: Pydantic schemas (UserCreate) must not shadow SQLAlchemy models (User)
|
||||||
|
3. TYPES: All function parameters have type hints, return types specified
|
||||||
|
4. ERRORS: Every db query that can return None has a 404 check
|
||||||
|
5. RESOURCES: Database session uses yield+finally pattern (no leaks)
|
||||||
|
6. SECURITY: No raw SQL, no hardcoded secrets, inputs validated via Pydantic
|
||||||
|
7. ENDPOINTS: All CRUD operations exist (POST/GET/GET-by-id/PUT/DELETE)
|
||||||
|
8. MODELS: Pydantic Config has from_attributes=True, uses model_dump() not dict()
|
||||||
|
9. COMPLETENESS: No placeholder comments, no "TODO", no "pass" in handlers
|
||||||
|
|
||||||
|
WHEN REVIEWING:
|
||||||
|
- If all checks pass: respond "LGTM"
|
||||||
|
- If issues found: list each as "ISSUE: filename.py: description"
|
||||||
|
- 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
|
||||||
|
- Test all CRUD: create (201), list (200), get by id (200/404), update (200), delete (204)
|
||||||
|
- ALWAYS: from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
Review this project code for issues. If everything is correct, respond with "LGTM". Otherwise list issues as "ISSUE: filename.py: description".
|
||||||
|
|
||||||
|
--- models.py ---
|
||||||
|
from sqlalchemy import create_engine, Column, Integer, String, Date, Enum
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
DATABASE_URL = "sqlite:///./todo.db"
|
||||||
|
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
class Todo(Base):
|
||||||
|
__tablename__ = "todos"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
title = Column(String(100), nullable=False)
|
||||||
|
description = Column(String(500))
|
||||||
|
due_date = Column(Date, nullable=False)
|
||||||
|
status = Column(Enum('pending', 'completed'), default='pending')
|
||||||
|
|
||||||
|
--- schemas.py ---
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
class TodoCreate(BaseModel):
|
||||||
|
title: str
|
||||||
|
description: str | None = None
|
||||||
|
due_date: date
|
||||||
|
status: str = 'pending'
|
||||||
|
|
||||||
|
class TodoResponse(TodoCreate):
|
||||||
|
id: int
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
--- main.py ---
|
||||||
|
from fastapi import FastAPI, Depends, HTTPException
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from models import Base, engine, SessionLocal, Todo
|
||||||
|
from schemas import TodoCreate, TodoResponse
|
||||||
|
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
@app.post("/todos/", response_model=TodoResponse, status_code=201)
|
||||||
|
def create_todo(todo: TodoCreate, db: Session = Depends(get_db)):
|
||||||
|
db_todo = Todo(**todo.model_dump())
|
||||||
|
db.add(db_todo)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_todo)
|
||||||
|
return db_todo
|
||||||
|
|
||||||
|
@app.get("/todos/", response_model=list[TodoResponse])
|
||||||
|
def list_todos(status: str | None = None, db: Session = Depends(get_db)):
|
||||||
|
if status:
|
||||||
|
query = db.query(Todo).filter_by(status=status)
|
||||||
|
else:
|
||||||
|
query = db.query(Todo)
|
||||||
|
return query.all()
|
||||||
|
|
||||||
|
@app.get("/todos/{todo_id}", response_model=TodoResponse)
|
||||||
|
def get_todo(todo_id: int, db: Session = Depends(get_db)):
|
||||||
|
todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
||||||
|
if not todo:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
return todo
|
||||||
|
|
||||||
|
@app.put("/todos/{todo_id}", response_model=TodoResponse)
|
||||||
|
def update_todo(todo_id: int, todo: TodoCreate, db: Session = Depends(get_db)):
|
||||||
|
db_todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
||||||
|
if not db_todo:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
for key, value in todo.model_dump().items():
|
||||||
|
setattr(db_todo, key, value)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_todo)
|
||||||
|
return db_todo
|
||||||
|
|
||||||
|
@app.delete("/todos/{todo_id}", status_code=204)
|
||||||
|
def delete_todo(todo_id: int, db: Session = Depends(get_db)):
|
||||||
|
db_todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
||||||
|
if not db_todo:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
db.delete(db_todo)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
--- pyproject.toml ---
|
||||||
|
[project]
|
||||||
|
name = "todo-sovellus"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
dependencies = [
|
||||||
|
"fastapi",
|
||||||
|
"uvicorn[standard]",
|
||||||
|
"sqlalchemy",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
dev = "uvicorn main:app --reload"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tulos
|
||||||
|
|
||||||
|
```
|
||||||
|
LGTM
|
||||||
|
```
|
||||||
291
zipit/projekti_clean/prompts/6_qa_test_main.py.md
Normal file
291
zipit/projekti_clean/prompts/6_qa_test_main.py.md
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
# 6 — QA (qa) — test_main.py
|
||||||
|
|
||||||
|
**Malli:** `qwen-coder`
|
||||||
|
|
||||||
|
## System Prompt
|
||||||
|
|
||||||
|
```
|
||||||
|
You are a QA engineer responsible for code review and automated testing.
|
||||||
|
|
||||||
|
CODE REVIEW CHECKLIST:
|
||||||
|
1. IMPORTS: Every "from X import Y" must match an actual export in file X
|
||||||
|
2. NAMES: Pydantic schemas (UserCreate) must not shadow SQLAlchemy models (User)
|
||||||
|
3. TYPES: All function parameters have type hints, return types specified
|
||||||
|
4. ERRORS: Every db query that can return None has a 404 check
|
||||||
|
5. RESOURCES: Database session uses yield+finally pattern (no leaks)
|
||||||
|
6. SECURITY: No raw SQL, no hardcoded secrets, inputs validated via Pydantic
|
||||||
|
7. ENDPOINTS: All CRUD operations exist (POST/GET/GET-by-id/PUT/DELETE)
|
||||||
|
8. MODELS: Pydantic Config has from_attributes=True, uses model_dump() not dict()
|
||||||
|
9. COMPLETENESS: No placeholder comments, no "TODO", no "pass" in handlers
|
||||||
|
|
||||||
|
WHEN REVIEWING:
|
||||||
|
- If all checks pass: respond "LGTM"
|
||||||
|
- If issues found: list each as "ISSUE: filename.py: description"
|
||||||
|
- 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
|
||||||
|
- Test all CRUD: create (201), list (200), get by id (200/404), update (200), delete (204)
|
||||||
|
- ALWAYS: from fastapi.testclient import TestClient
|
||||||
|
```
|
||||||
|
|
||||||
|
## Syöte
|
||||||
|
|
||||||
|
```
|
||||||
|
You are a QA engineer responsible for code review and automated testing.
|
||||||
|
|
||||||
|
CODE REVIEW CHECKLIST:
|
||||||
|
1. IMPORTS: Every "from X import Y" must match an actual export in file X
|
||||||
|
2. NAMES: Pydantic schemas (UserCreate) must not shadow SQLAlchemy models (User)
|
||||||
|
3. TYPES: All function parameters have type hints, return types specified
|
||||||
|
4. ERRORS: Every db query that can return None has a 404 check
|
||||||
|
5. RESOURCES: Database session uses yield+finally pattern (no leaks)
|
||||||
|
6. SECURITY: No raw SQL, no hardcoded secrets, inputs validated via Pydantic
|
||||||
|
7. ENDPOINTS: All CRUD operations exist (POST/GET/GET-by-id/PUT/DELETE)
|
||||||
|
8. MODELS: Pydantic Config has from_attributes=True, uses model_dump() not dict()
|
||||||
|
9. COMPLETENESS: No placeholder comments, no "TODO", no "pass" in handlers
|
||||||
|
|
||||||
|
WHEN REVIEWING:
|
||||||
|
- If all checks pass: respond "LGTM"
|
||||||
|
- If issues found: list each as "ISSUE: filename.py: description"
|
||||||
|
- 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
|
||||||
|
- Test all CRUD: create (201), list (200), get by id (200/404), update (200), delete (204)
|
||||||
|
- ALWAYS: from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
Write pytest tests for this project:
|
||||||
|
|
||||||
|
--- models.py ---
|
||||||
|
from sqlalchemy import create_engine, Column, Integer, String, Date, Enum
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
DATABASE_URL = "sqlite:///./todo.db"
|
||||||
|
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
class Todo(Base):
|
||||||
|
__tablename__ = "todos"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
title = Column(String(100), nullable=False)
|
||||||
|
description = Column(String(500))
|
||||||
|
due_date = Column(Date, nullable=False)
|
||||||
|
status = Column(Enum('pending', 'completed'), default='pending')
|
||||||
|
|
||||||
|
--- schemas.py ---
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
class TodoCreate(BaseModel):
|
||||||
|
title: str
|
||||||
|
description: str | None = None
|
||||||
|
due_date: date
|
||||||
|
status: str = 'pending'
|
||||||
|
|
||||||
|
class TodoResponse(TodoCreate):
|
||||||
|
id: int
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
--- main.py ---
|
||||||
|
from fastapi import FastAPI, Depends, HTTPException
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from models import Base, engine, SessionLocal, Todo
|
||||||
|
from schemas import TodoCreate, TodoResponse
|
||||||
|
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
@app.post("/todos/", response_model=TodoResponse, status_code=201)
|
||||||
|
def create_todo(todo: TodoCreate, db: Session = Depends(get_db)):
|
||||||
|
db_todo = Todo(**todo.model_dump())
|
||||||
|
db.add(db_todo)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_todo)
|
||||||
|
return db_todo
|
||||||
|
|
||||||
|
@app.get("/todos/", response_model=list[TodoResponse])
|
||||||
|
def list_todos(status: str | None = None, db: Session = Depends(get_db)):
|
||||||
|
if status:
|
||||||
|
query = db.query(Todo).filter_by(status=status)
|
||||||
|
else:
|
||||||
|
query = db.query(Todo)
|
||||||
|
return query.all()
|
||||||
|
|
||||||
|
@app.get("/todos/{todo_id}", response_model=TodoResponse)
|
||||||
|
def get_todo(todo_id: int, db: Session = Depends(get_db)):
|
||||||
|
todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
||||||
|
if not todo:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
return todo
|
||||||
|
|
||||||
|
@app.put("/todos/{todo_id}", response_model=TodoResponse)
|
||||||
|
def update_todo(todo_id: int, todo: TodoCreate, db: Session = Depends(get_db)):
|
||||||
|
db_todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
||||||
|
if not db_todo:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
for key, value in todo.model_dump().items():
|
||||||
|
setattr(db_todo, key, value)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_todo)
|
||||||
|
return db_todo
|
||||||
|
|
||||||
|
@app.delete("/todos/{todo_id}", status_code=204)
|
||||||
|
def delete_todo(todo_id: int, db: Session = Depends(get_db)):
|
||||||
|
db_todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
||||||
|
if not db_todo:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
db.delete(db_todo)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
--- pyproject.toml ---
|
||||||
|
[project]
|
||||||
|
name = "todo-sovellus"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
dependencies = [
|
||||||
|
"fastapi",
|
||||||
|
"uvicorn[standard]",
|
||||||
|
"sqlalchemy",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
dev = "uvicorn main:app --reload"
|
||||||
|
|
||||||
|
Write a complete test_main.py file with TestClient.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tulos
|
||||||
|
|
||||||
|
```
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from sqlalchemy import create_engine, Column, Integer, String, Date, Enum
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
DATABASE_URL = "sqlite:///./test_todo.db"
|
||||||
|
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
class Todo(Base):
|
||||||
|
__tablename__ = "todos"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
title = Column(String(100), nullable=False)
|
||||||
|
description = Column(String(500))
|
||||||
|
due_date = Column(Date, nullable=False)
|
||||||
|
status = Column(Enum('pending', 'completed'), default='pending')
|
||||||
|
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
@app.post("/todos/", response_model=Todo, status_code=201)
|
||||||
|
def create_todo(todo: TodoCreate, db: Session = Depends(get_db)):
|
||||||
|
db_todo = Todo(**todo.model_dump())
|
||||||
|
db.add(db_todo)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_todo)
|
||||||
|
return db_todo
|
||||||
|
|
||||||
|
@app.get("/todos/", response_model=list[Todo])
|
||||||
|
def list_todos(status: str | None = None, db: Session = Depends(get_db)):
|
||||||
|
if status:
|
||||||
|
query = db.query(Todo).filter_by(status=status)
|
||||||
|
else:
|
||||||
|
query = db.query(Todo)
|
||||||
|
return query.all()
|
||||||
|
|
||||||
|
@app.get("/todos/{todo_id}", response_model=Todo)
|
||||||
|
def get_todo(todo_id: int, db: Session = Depends(get_db)):
|
||||||
|
todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
||||||
|
if not todo:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
return todo
|
||||||
|
|
||||||
|
@app.put("/todos/{todo_id}", response_model=Todo)
|
||||||
|
def update_todo(todo_id: int, todo: TodoCreate, db: Session = Depends(get_db)):
|
||||||
|
db_todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
||||||
|
if not db_todo:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
for key, value in todo.model_dump().items():
|
||||||
|
setattr(db_todo, key, value)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_todo)
|
||||||
|
return db_todo
|
||||||
|
|
||||||
|
@app.delete("/todos/{todo_id}", status_code=204)
|
||||||
|
def delete_todo(todo_id: int, db: Session = Depends(get_db)):
|
||||||
|
db_todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
||||||
|
if not db_todo:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
db.delete(db_todo)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
def test_create_todo():
|
||||||
|
response = client.post("/todos/", json={"title": "Test Todo", "description": "This is a test todo.", "due_date": "2023-12-31"})
|
||||||
|
assert response.status_code == 201
|
||||||
|
data = response.json()
|
||||||
|
assert "id" in data
|
||||||
|
assert data["title"] == "Test Todo"
|
||||||
|
assert data["description"] == "This is a test todo."
|
||||||
|
assert data["due_date"] == "2023-12-31"
|
||||||
|
assert data["status"] == "pending"
|
||||||
|
|
||||||
|
def test_list_todos():
|
||||||
|
response = client.get("/todos/")
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert len(data) > 0
|
||||||
|
|
||||||
|
def test_get_todo_by_id():
|
||||||
|
todo_id = create_test_todo().json()["id"]
|
||||||
|
response = client.get(f"/todos/{todo_id}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert "id" in data
|
||||||
|
assert data["title"] == "Test Todo"
|
||||||
|
|
||||||
|
def test_get_nonexistent_todo():
|
||||||
|
response = client.get("/todos/999")
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
def test_update_todo():
|
||||||
|
todo_id = create_test_todo().json()["id"]
|
||||||
|
response = client.put(f"/todos/{todo_id}", json={"title": "Updated Todo"})
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert "id" in data
|
||||||
|
assert data["title"] == "Updated Todo"
|
||||||
|
|
||||||
|
def test_delete_todo():
|
||||||
|
todo_id = create_test_todo().json()["id"]
|
||||||
|
response = client.delete(f"/todos/{todo_id}")
|
||||||
|
assert response.status_code == 204
|
||||||
|
|
||||||
|
def create_test_todo():
|
||||||
|
return client.post("/todos/", json={"title": "Test Todo", "description": "This is a test todo.", "due_date": "2023
|
||||||
|
```
|
||||||
93
zipit/projekti_clean/prompts/7_tester_dockerfile.md
Normal file
93
zipit/projekti_clean/prompts/7_tester_dockerfile.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# 7 — DevOps (tester) — Dockerfile
|
||||||
|
|
||||||
|
**Malli:** `qwen-coder`
|
||||||
|
|
||||||
|
## System 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
|
||||||
|
|
||||||
|
Write ONLY the requested files, no explanations.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Syöte
|
||||||
|
|
||||||
|
```
|
||||||
|
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
|
||||||
|
|
||||||
|
Write ONLY the requested files, no explanations.
|
||||||
|
|
||||||
|
Write a Dockerfile for this Python FastAPI project.
|
||||||
|
|
||||||
|
Project files: models.py, schemas.py, main.py, pyproject.toml, test_main.py
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
- 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 port 8000
|
||||||
|
- CMD: uv run uvicorn main:app --host 0.0.0.0 --port 8000
|
||||||
|
|
||||||
|
Write ONLY the Dockerfile, no explanations.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tulos
|
||||||
|
|
||||||
|
```
|
||||||
|
# Stage 1: Build
|
||||||
|
FROM python:3.12-slim as builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY pyproject.toml .
|
||||||
|
RUN pip install --no-cache-dir poetry && \
|
||||||
|
poetry config virtualenvs.create false && \
|
||||||
|
poetry install --no-interaction --no-ansi
|
||||||
|
|
||||||
|
COPY models.py schemas.py main.py test_main.py .
|
||||||
|
|
||||||
|
# Stage 2: Production
|
||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=builder /root/.local/share/python-poetry/cache /root/.local/share/python-poetry/cache
|
||||||
|
COPY --from=builder /app /app
|
||||||
|
|
||||||
|
RUN useradd -m appuser && \
|
||||||
|
chown -R appuser:appuser /app
|
||||||
|
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
|
```
|
||||||
404
zipit/projekti_clean/prompts/8_observer_readme.md.md
Normal file
404
zipit/projekti_clean/prompts/8_observer_readme.md.md
Normal file
@@ -0,0 +1,404 @@
|
|||||||
|
# 8 — Observer (observer) — README.md
|
||||||
|
|
||||||
|
**Malli:** `qwen-coder`
|
||||||
|
|
||||||
|
## System Prompt
|
||||||
|
|
||||||
|
```
|
||||||
|
You are an independent technical observer and risk analyst.
|
||||||
|
|
||||||
|
EVALUATE THE PROJECT FOR:
|
||||||
|
1. ARCHITECTURE: Is the file structure logical? Are responsibilities separated?
|
||||||
|
2. SECURITY: SQL injection risks? Input validation? Authentication?
|
||||||
|
3. RELIABILITY: Error handling? Database connection management? Edge cases?
|
||||||
|
4. MAINTAINABILITY: Consistent naming? Clear code structure? Would a new developer understand this?
|
||||||
|
|
||||||
|
OUTPUT FORMAT:
|
||||||
|
- RISK: [critical/high/medium/low] Description
|
||||||
|
- List max 3-5 most important findings
|
||||||
|
- End with overall assessment: "SHIP IT" or "NEEDS WORK: reason"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Syöte
|
||||||
|
|
||||||
|
```
|
||||||
|
You are an independent technical observer and risk analyst.
|
||||||
|
|
||||||
|
EVALUATE THE PROJECT FOR:
|
||||||
|
1. ARCHITECTURE: Is the file structure logical? Are responsibilities separated?
|
||||||
|
2. SECURITY: SQL injection risks? Input validation? Authentication?
|
||||||
|
3. RELIABILITY: Error handling? Database connection management? Edge cases?
|
||||||
|
4. MAINTAINABILITY: Consistent naming? Clear code structure? Would a new developer understand this?
|
||||||
|
|
||||||
|
OUTPUT FORMAT:
|
||||||
|
- RISK: [critical/high/medium/low] Description
|
||||||
|
- List max 3-5 most important findings
|
||||||
|
- End with overall assessment: "SHIP IT" or "NEEDS WORK: reason"
|
||||||
|
|
||||||
|
Write a project report in clean markdown for: Todo-sovellus FastAPI + SQLite, CRUD-endpointit ja testit
|
||||||
|
|
||||||
|
FIRST LINE must be exactly one of:
|
||||||
|
VERDICT: GREEN
|
||||||
|
VERDICT: ORANGE
|
||||||
|
VERDICT: RED
|
||||||
|
|
||||||
|
Then write this report:
|
||||||
|
|
||||||
|
# Todo-sovellus FastAPI + SQLite, CRUD-endpointit ja testit
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
One paragraph describing what this project does.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| models.py | ... |
|
||||||
|
| schemas.py | ... |
|
||||||
|
| main.py | ... |
|
||||||
|
| pyproject.toml | ... |
|
||||||
|
| test_main.py | ... |
|
||||||
|
| Dockerfile | ... |
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
```bash
|
||||||
|
git clone <repo>
|
||||||
|
cd project
|
||||||
|
uv sync
|
||||||
|
uv run uvicorn main:app --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
```bash
|
||||||
|
docker build -t todo-sovellus-fastapi---sqlite--crud-endpointit-ja-testit .
|
||||||
|
docker run -p 8000:8000 todo-sovellus-fastapi---sqlite--crud-endpointit-ja-testit
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
| Method | Path | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| POST | /items/ | Create |
|
||||||
|
| GET | /items/ | List all |
|
||||||
|
| GET | /items/{id} | Get by ID |
|
||||||
|
| PUT | /items/{id} | Update |
|
||||||
|
| DELETE | /items/{id} | Delete |
|
||||||
|
(Adapt paths and descriptions to match the actual code)
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
Describe the project structure and design decisions.
|
||||||
|
|
||||||
|
## Risk Assessment
|
||||||
|
| Severity | Issue |
|
||||||
|
|----------|-------|
|
||||||
|
| ... | ... |
|
||||||
|
|
||||||
|
Project code:
|
||||||
|
--- models.py ---
|
||||||
|
from sqlalchemy import create_engine, Column, Integer, String, Date, Enum
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
DATABASE_URL = "sqlite:///./todo.db"
|
||||||
|
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
class Todo(Base):
|
||||||
|
__tablename__ = "todos"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
title = Column(String(100), nullable=False)
|
||||||
|
description = Column(String(500))
|
||||||
|
due_date = Column(Date, nullable=False)
|
||||||
|
status = Column(Enum('pending', 'completed'), default='pending')
|
||||||
|
|
||||||
|
--- schemas.py ---
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
class TodoCreate(BaseModel):
|
||||||
|
title: str
|
||||||
|
description: str | None = None
|
||||||
|
due_date: date
|
||||||
|
status: str = 'pending'
|
||||||
|
|
||||||
|
class TodoResponse(TodoCreate):
|
||||||
|
id: int
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
--- main.py ---
|
||||||
|
from fastapi import FastAPI, Depends, HTTPException
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from models import Base, engine, SessionLocal, Todo
|
||||||
|
from schemas import TodoCreate, TodoResponse
|
||||||
|
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
@app.post("/todos/", response_model=TodoResponse, status_code=201)
|
||||||
|
def create_todo(todo: TodoCreate, db: Session = Depends(get_db)):
|
||||||
|
db_todo = Todo(**todo.model_dump())
|
||||||
|
db.add(db_todo)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_todo)
|
||||||
|
return db_todo
|
||||||
|
|
||||||
|
@app.get("/todos/", response_model=list[TodoResponse])
|
||||||
|
def list_todos(status: str | None = None, db: Session = Depends(get_db)):
|
||||||
|
if status:
|
||||||
|
query = db.query(Todo).filter_by(status=status)
|
||||||
|
else:
|
||||||
|
query = db.query(Todo)
|
||||||
|
return query.all()
|
||||||
|
|
||||||
|
@app.get("/todos/{todo_id}", response_model=TodoResponse)
|
||||||
|
def get_todo(todo_id: int, db: Session = Depends(get_db)):
|
||||||
|
todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
||||||
|
if not todo:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
return todo
|
||||||
|
|
||||||
|
@app.put("/todos/{todo_id}", response_model=TodoResponse)
|
||||||
|
def update_todo(todo_id: int, todo: TodoCreate, db: Session = Depends(get_db)):
|
||||||
|
db_todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
||||||
|
if not db_todo:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
for key, value in todo.model_dump().items():
|
||||||
|
setattr(db_todo, key, value)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_todo)
|
||||||
|
return db_todo
|
||||||
|
|
||||||
|
@app.delete("/todos/{todo_id}", status_code=204)
|
||||||
|
def delete_todo(todo_id: int, db: Session = Depends(get_db)):
|
||||||
|
db_todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
||||||
|
if not db_todo:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
db.delete(db_todo)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
--- pyproject.toml ---
|
||||||
|
[project]
|
||||||
|
name = "todo-sovellus"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
dependencies = [
|
||||||
|
"fastapi",
|
||||||
|
"uvicorn[standard]",
|
||||||
|
"sqlalchemy",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
dev = "uvicorn main:app --reload"
|
||||||
|
|
||||||
|
--- test_main.py ---
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from sqlalchemy import create_engine, Column, Integer, String, Date, Enum
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
DATABASE_URL = "sqlite:///./test_todo.db"
|
||||||
|
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
class Todo(Base):
|
||||||
|
__tablename__ = "todos"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
title = Column(String(100), nullable=False)
|
||||||
|
description = Column(String(500))
|
||||||
|
due_date = Column(Date, nullable=False)
|
||||||
|
status = Column(Enum('pending', 'completed'), default='pending')
|
||||||
|
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
@app.post("/todos/", response_model=Todo, status_code=201)
|
||||||
|
def create_todo(todo: TodoCreate, db: Session = Depends(get_db)):
|
||||||
|
db_todo = Todo(**todo.model_dump())
|
||||||
|
db.add(db_todo)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_todo)
|
||||||
|
return db_todo
|
||||||
|
|
||||||
|
@app.get("/todos/", response_model=list[Todo])
|
||||||
|
def list_todos(status: str | None = None, db: Session = Depends(get_db)):
|
||||||
|
if status:
|
||||||
|
query = db.query(Todo).filter_by(status=status)
|
||||||
|
else:
|
||||||
|
query = db.query(Todo)
|
||||||
|
return query.all()
|
||||||
|
|
||||||
|
@app.get("/todos/{todo_id}", response_model=Todo)
|
||||||
|
def get_todo(todo_id: int, db: Session = Depends(get_db)):
|
||||||
|
todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
||||||
|
if not todo:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
return todo
|
||||||
|
|
||||||
|
@app.put("/todos/{todo_id}", response_model=Todo)
|
||||||
|
def update_todo(todo_id: int, todo: TodoCreate, db: Session = Depends(get_db)):
|
||||||
|
db_todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
||||||
|
if not db_todo:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
for key, value in todo.model_dump().items():
|
||||||
|
setattr(db_todo, key, value)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_todo)
|
||||||
|
return db_todo
|
||||||
|
|
||||||
|
@app.delete("/todos/{todo_id}", status_code=204)
|
||||||
|
def delete_todo(todo_id: int, db: Session = Depends(get_db)):
|
||||||
|
db_todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
||||||
|
if not db_todo:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
db.delete(db_todo)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
def test_create_todo():
|
||||||
|
response = client.post("/todos/", json={"title": "Test Todo", "description": "This is a test todo.", "due_date": "2023-12-31"})
|
||||||
|
assert response.status_code == 201
|
||||||
|
data = response.json()
|
||||||
|
assert "id" in data
|
||||||
|
assert data["title"] == "Test Todo"
|
||||||
|
assert data["description"] == "This is a test todo."
|
||||||
|
assert data["due_date"] == "2023-12-31"
|
||||||
|
assert data["status"] == "pending"
|
||||||
|
|
||||||
|
def test_list_todos():
|
||||||
|
response = client.get("/todos/")
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert len(data) > 0
|
||||||
|
|
||||||
|
def test_get_todo_by_id():
|
||||||
|
todo_id = create_test_todo().json()["id"]
|
||||||
|
response = client.get(f"/todos/{todo_id}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert "id" in data
|
||||||
|
assert data["title"] == "Test Todo"
|
||||||
|
|
||||||
|
def test_get_nonexistent_todo():
|
||||||
|
response = client.get("/todos/999")
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
def test_update_todo():
|
||||||
|
todo_id = create_test_todo().json()["id"]
|
||||||
|
response = client.put(f"/todos/{todo_id}", json={"title": "Updated Todo"})
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert "id" in data
|
||||||
|
assert data["title"] == "Updated Todo"
|
||||||
|
|
||||||
|
def test_delete_todo():
|
||||||
|
todo_id = create_test_todo().json()["id"]
|
||||||
|
response = client.delete(f"/todos/{todo_id}")
|
||||||
|
assert response.status_code == 204
|
||||||
|
|
||||||
|
def create_test_todo():
|
||||||
|
return client.post("/todos/", json={"title": "Test Todo", "description": "This is a test todo.", "due_date": "2023
|
||||||
|
|
||||||
|
--- Dockerfile ---
|
||||||
|
# Stage 1: Build
|
||||||
|
FROM python:3.12-slim as builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY pyproject.toml .
|
||||||
|
RUN pip install --no-cache-dir poetry && \
|
||||||
|
poetry config virtualenvs.create false && \
|
||||||
|
poetry install --no-interaction --no-ansi
|
||||||
|
|
||||||
|
COPY models.py schemas.py main.py test_main.py .
|
||||||
|
|
||||||
|
# Stage 2: Production
|
||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=builder /root/.local/share/python-poetry/cache /root/.local/share/python-poetry/cache
|
||||||
|
COPY --from=builder /app /app
|
||||||
|
|
||||||
|
RUN useradd -m appuser && \
|
||||||
|
chown -R appuser:appuser /app
|
||||||
|
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tulos
|
||||||
|
|
||||||
|
```
|
||||||
|
VERDICT: GREEN
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This project is a simple Todo application using FastAPI for the backend and SQLite as the database. It includes CRUD endpoints for managing tasks.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| models.py | Defines the data model for the todo items using SQLAlchemy ORM. |
|
||||||
|
| schemas.py | Contains Pydantic schemas for validating request and response data. |
|
||||||
|
| main.py | The FastAPI application, defining routes and business logic. |
|
||||||
|
| pyproject.toml | Project metadata and dependencies. |
|
||||||
|
| test_main.py | Unit tests for the FastAPI endpoints. |
|
||||||
|
| Dockerfile | Docker configuration to build and run the application. |
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
git clone <repo>
|
||||||
|
cd project
|
||||||
|
uv sync
|
||||||
|
uv run uvicorn main:app --reload
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
docker build -t todo-sovellus-fastapi---sqlite--crud-endpointit-ja-testit .
|
||||||
|
docker run -p 8000:8000 todo-sovellus-fastapi---sqlite--crud-endpointit-ja-testit
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
| Method | Path | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| POST | /todos/ | Create a new todo item. |
|
||||||
|
| GET | /todos/ | List all todos or filter by status. |
|
||||||
|
| GET | /todos/{id} | Retrieve a specific todo item by ID. |
|
||||||
|
| PUT | /todos/{id} | Update an existing todo item. |
|
||||||
|
| DELETE | /todos/{id} | Delete a todo item. |
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
The project follows a clean separation of concerns:
|
||||||
|
- **models.py**: Defines the data model using SQLAlchemy ORM.
|
||||||
|
- **schemas.py**: Contains Pydantic models for data validation and serialization.
|
||||||
|
- **main.py**: Implements FastAPI routes, business logic, and database interactions.
|
||||||
|
|
||||||
|
This logical structure makes it easy to understand and maintain each component independently.
|
||||||
|
|
||||||
|
## Risk Assessment
|
||||||
|
| Severity | Issue |
|
||||||
|
|----------|-------|
|
||||||
|
| LOW | The project uses parameterized queries in the SQLAlchemy ORM which helps prevent SQL injection. However, input validation is minimal (e.g., no checks for `due_date` format). |
|
||||||
|
| MEDIUM | Error handling and database connection management are basic but adequate. Database connections are managed via context managers, which ensures proper closure of sessions. |
|
||||||
|
| LOW | The project uses consistent naming conventions for variables and functions, making the code easy to understand. However, there's room for improving documentation around API endpoints and models. |
|
||||||
|
|
||||||
|
Overall assessment: "SHIP IT"
|
||||||
|
```
|
||||||
12
zipit/projekti_clean/pyproject.toml
Normal file
12
zipit/projekti_clean/pyproject.toml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[project]
|
||||||
|
name = "todo-sovellus"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
dependencies = [
|
||||||
|
"fastapi",
|
||||||
|
"uvicorn[standard]",
|
||||||
|
"sqlalchemy",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
dev = "uvicorn main:app --reload"
|
||||||
13
zipit/projekti_clean/schemas.py
Normal file
13
zipit/projekti_clean/schemas.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
class TodoCreate(BaseModel):
|
||||||
|
title: str
|
||||||
|
description: str | None = None
|
||||||
|
due_date: date
|
||||||
|
status: str = 'pending'
|
||||||
|
|
||||||
|
class TodoResponse(TodoCreate):
|
||||||
|
id: int
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
442
zipit/projekti_clean/tasks.yaml
Normal file
442
zipit/projekti_clean/tasks.yaml
Normal file
@@ -0,0 +1,442 @@
|
|||||||
|
# Tasks — Kipinä Agentic Studio → CrewAI
|
||||||
|
|
||||||
|
step_0_requirements:
|
||||||
|
description: |
|
||||||
|
Todo-sovellus FastAPI + SQLite, CRUD-endpointit ja testit
|
||||||
|
expected_output: >-
|
||||||
|
requirements
|
||||||
|
agent: client
|
||||||
|
|
||||||
|
step_1_models_py:
|
||||||
|
description: |
|
||||||
|
You are a database architect specializing in SQLAlchemy and relational databases.
|
||||||
|
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
EXAMPLE of models.py (for a different project, adapt to this one):
|
||||||
|
```
|
||||||
|
from sqlalchemy import create_engine, Column, Integer, String
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
DATABASE_URL = "sqlite:///./app.db"
|
||||||
|
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
class Item(Base):
|
||||||
|
__tablename__ = "items"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
name = Column(String(100), nullable=False)
|
||||||
|
description = Column(String(500))
|
||||||
|
```
|
||||||
|
|
||||||
|
PROJECT REQUIREMENTS (from product owner):
|
||||||
|
1. **PROJECT NAME:** Todo-sovellus
|
||||||
|
|
||||||
|
2. **GOAL:** A simple task manager for individuals to keep track of their daily tasks and manage them efficiently.
|
||||||
|
|
||||||
|
3. **CORE FEATURES:**
|
||||||
|
- Create new todos with title, description, due date.
|
||||||
|
- Read/View all todos or a single todo by ID.
|
||||||
|
- Update existing todos (title, description, status).
|
||||||
|
- Delete completed or unnecessary todos.
|
||||||
|
- Filter todos by status (e.g., pending, completed).
|
||||||
|
|
||||||
|
4. **DATA MODEL:**
|
||||||
|
- Todo
|
||||||
|
- id (integer, primary key)
|
||||||
|
- title (string)
|
||||||
|
# ... (truncated)
|
||||||
|
expected_output: >-
|
||||||
|
models.py
|
||||||
|
agent: data
|
||||||
|
|
||||||
|
step_2_schemas_py:
|
||||||
|
description: |
|
||||||
|
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)
|
||||||
|
|
||||||
|
NEVER:
|
||||||
|
- Add explanations or comments like "# Add routes here"
|
||||||
|
- 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)
|
||||||
|
|
||||||
|
EXAMPLE of schemas.py (for a different project, adapt to this one):
|
||||||
|
```
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
class ItemCreate(BaseModel):
|
||||||
|
name: str
|
||||||
|
description: str | None = None
|
||||||
|
|
||||||
|
class ItemResponse(ItemCreate):
|
||||||
|
id: int
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
```
|
||||||
|
|
||||||
|
Already written files in THIS project:
|
||||||
|
--- models.py ---
|
||||||
|
from sqlalchemy import create_engine, Column, Integer, String, Date, Enum
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
DATABASE_URL = "sqlite:///./todo.db"
|
||||||
|
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
class Todo(Base):
|
||||||
|
__tablename__ = "todos"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
title = Column(String(100), nullable=False)
|
||||||
|
# ... (truncated)
|
||||||
|
expected_output: >-
|
||||||
|
schemas.py
|
||||||
|
agent: coder
|
||||||
|
|
||||||
|
step_3_main_py:
|
||||||
|
description: |
|
||||||
|
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)
|
||||||
|
|
||||||
|
NEVER:
|
||||||
|
- Add explanations or comments like "# Add routes here"
|
||||||
|
- 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)
|
||||||
|
|
||||||
|
EXAMPLE of main.py (for a different project, adapt to this one):
|
||||||
|
```
|
||||||
|
from fastapi import FastAPI, Depends, HTTPException
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from models import Base, engine, SessionLocal, Item
|
||||||
|
from schemas import ItemCreate, ItemResponse
|
||||||
|
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
@app.post("/items/", response_model=ItemResponse, status_code=201)
|
||||||
|
def create_item(item: ItemCreate, db: Session = Depends(get_db)):
|
||||||
|
db_item = Item(**item.model_dump())
|
||||||
|
db.add(db_item)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_item)
|
||||||
|
return db_item
|
||||||
|
|
||||||
|
@app.get("/items/", response_model=list[ItemResponse])
|
||||||
|
def list_items(db: Session = Depends(get_db)):
|
||||||
|
return db.query(Item).all()
|
||||||
|
|
||||||
|
@app.get("/items/{item_id}", response_model=ItemResponse)
|
||||||
|
# ... (truncated)
|
||||||
|
expected_output: >-
|
||||||
|
main.py
|
||||||
|
agent: coder
|
||||||
|
|
||||||
|
step_4_pyproject_toml:
|
||||||
|
description: |
|
||||||
|
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)
|
||||||
|
|
||||||
|
NEVER:
|
||||||
|
- Add explanations or comments like "# Add routes here"
|
||||||
|
- 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)
|
||||||
|
|
||||||
|
EXAMPLE of pyproject.toml (for a different project, adapt to this one):
|
||||||
|
```
|
||||||
|
[project]
|
||||||
|
name = "myapp"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
dependencies = [
|
||||||
|
"fastapi",
|
||||||
|
"uvicorn[standard]",
|
||||||
|
"sqlalchemy",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
dev = "uvicorn main:app --reload"
|
||||||
|
```
|
||||||
|
|
||||||
|
Already written files in THIS project:
|
||||||
|
--- models.py ---
|
||||||
|
from sqlalchemy import create_engine, Column, Integer, String, Date, Enum
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
DATABASE_URL = "sqlite:///./todo.db"
|
||||||
|
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
class Todo(Base):
|
||||||
|
__tablename__ = "todos"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
# ... (truncated)
|
||||||
|
expected_output: >-
|
||||||
|
pyproject.toml
|
||||||
|
agent: coder
|
||||||
|
|
||||||
|
step_5_review:
|
||||||
|
description: |
|
||||||
|
You are a QA engineer responsible for code review and automated testing.
|
||||||
|
|
||||||
|
CODE REVIEW CHECKLIST:
|
||||||
|
1. IMPORTS: Every "from X import Y" must match an actual export in file X
|
||||||
|
2. NAMES: Pydantic schemas (UserCreate) must not shadow SQLAlchemy models (User)
|
||||||
|
3. TYPES: All function parameters have type hints, return types specified
|
||||||
|
4. ERRORS: Every db query that can return None has a 404 check
|
||||||
|
5. RESOURCES: Database session uses yield+finally pattern (no leaks)
|
||||||
|
6. SECURITY: No raw SQL, no hardcoded secrets, inputs validated via Pydantic
|
||||||
|
7. ENDPOINTS: All CRUD operations exist (POST/GET/GET-by-id/PUT/DELETE)
|
||||||
|
8. MODELS: Pydantic Config has from_attributes=True, uses model_dump() not dict()
|
||||||
|
9. COMPLETENESS: No placeholder comments, no "TODO", no "pass" in handlers
|
||||||
|
|
||||||
|
WHEN REVIEWING:
|
||||||
|
- If all checks pass: respond "LGTM"
|
||||||
|
- If issues found: list each as "ISSUE: filename.py: description"
|
||||||
|
- 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
|
||||||
|
- Test all CRUD: create (201), list (200), get by id (200/404), update (200), delete (204)
|
||||||
|
- ALWAYS: from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
Review this project code for issues. If everything is correct, respond with "LGTM". Otherwise list issues as "ISSUE: filename.py: description".
|
||||||
|
|
||||||
|
--- models.py ---
|
||||||
|
from sqlalchemy import create_engine, Column, Integer, String, Date, Enum
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
DATABASE_URL = "sqlite:///./todo.db"
|
||||||
|
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
class Todo(Base):
|
||||||
|
__tablename__ = "todos"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
title = Column(String(100), nullable=False)
|
||||||
|
description = Column(String(500))
|
||||||
|
due_date = Column(Date, nullable=False)
|
||||||
|
status = Column(Enum('pending', 'completed'), default='pending')
|
||||||
|
|
||||||
|
--- schemas.py ---
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
class TodoCreate(BaseModel):
|
||||||
|
title: str
|
||||||
|
# ... (truncated)
|
||||||
|
expected_output: >-
|
||||||
|
review
|
||||||
|
agent: qa
|
||||||
|
|
||||||
|
step_6_test_main_py:
|
||||||
|
description: |
|
||||||
|
You are a QA engineer responsible for code review and automated testing.
|
||||||
|
|
||||||
|
CODE REVIEW CHECKLIST:
|
||||||
|
1. IMPORTS: Every "from X import Y" must match an actual export in file X
|
||||||
|
2. NAMES: Pydantic schemas (UserCreate) must not shadow SQLAlchemy models (User)
|
||||||
|
3. TYPES: All function parameters have type hints, return types specified
|
||||||
|
4. ERRORS: Every db query that can return None has a 404 check
|
||||||
|
5. RESOURCES: Database session uses yield+finally pattern (no leaks)
|
||||||
|
6. SECURITY: No raw SQL, no hardcoded secrets, inputs validated via Pydantic
|
||||||
|
7. ENDPOINTS: All CRUD operations exist (POST/GET/GET-by-id/PUT/DELETE)
|
||||||
|
8. MODELS: Pydantic Config has from_attributes=True, uses model_dump() not dict()
|
||||||
|
9. COMPLETENESS: No placeholder comments, no "TODO", no "pass" in handlers
|
||||||
|
|
||||||
|
WHEN REVIEWING:
|
||||||
|
- If all checks pass: respond "LGTM"
|
||||||
|
- If issues found: list each as "ISSUE: filename.py: description"
|
||||||
|
- 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
|
||||||
|
- Test all CRUD: create (201), list (200), get by id (200/404), update (200), delete (204)
|
||||||
|
- ALWAYS: from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
Write pytest tests for this project:
|
||||||
|
|
||||||
|
--- models.py ---
|
||||||
|
from sqlalchemy import create_engine, Column, Integer, String, Date, Enum
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
DATABASE_URL = "sqlite:///./todo.db"
|
||||||
|
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
class Todo(Base):
|
||||||
|
__tablename__ = "todos"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
title = Column(String(100), nullable=False)
|
||||||
|
description = Column(String(500))
|
||||||
|
due_date = Column(Date, nullable=False)
|
||||||
|
status = Column(Enum('pending', 'completed'), default='pending')
|
||||||
|
|
||||||
|
--- schemas.py ---
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
class TodoCreate(BaseModel):
|
||||||
|
title: str
|
||||||
|
# ... (truncated)
|
||||||
|
expected_output: >-
|
||||||
|
test_main.py
|
||||||
|
agent: qa
|
||||||
|
|
||||||
|
step_7_dockerfile:
|
||||||
|
description: |
|
||||||
|
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
|
||||||
|
|
||||||
|
Write ONLY the requested files, no explanations.
|
||||||
|
|
||||||
|
Write a Dockerfile for this Python FastAPI project.
|
||||||
|
|
||||||
|
Project files: models.py, schemas.py, main.py, pyproject.toml, test_main.py
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
- 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 port 8000
|
||||||
|
- CMD: uv run uvicorn main:app --host 0.0.0.0 --port 8000
|
||||||
|
|
||||||
|
Write ONLY the Dockerfile, no explanations.
|
||||||
|
expected_output: >-
|
||||||
|
Dockerfile
|
||||||
|
agent: tester
|
||||||
|
|
||||||
|
step_8_readme_md:
|
||||||
|
description: |
|
||||||
|
You are an independent technical observer and risk analyst.
|
||||||
|
|
||||||
|
EVALUATE THE PROJECT FOR:
|
||||||
|
1. ARCHITECTURE: Is the file structure logical? Are responsibilities separated?
|
||||||
|
2. SECURITY: SQL injection risks? Input validation? Authentication?
|
||||||
|
3. RELIABILITY: Error handling? Database connection management? Edge cases?
|
||||||
|
4. MAINTAINABILITY: Consistent naming? Clear code structure? Would a new developer understand this?
|
||||||
|
|
||||||
|
OUTPUT FORMAT:
|
||||||
|
- RISK: [critical/high/medium/low] Description
|
||||||
|
- List max 3-5 most important findings
|
||||||
|
- End with overall assessment: "SHIP IT" or "NEEDS WORK: reason"
|
||||||
|
|
||||||
|
Write a project report in clean markdown for: Todo-sovellus FastAPI + SQLite, CRUD-endpointit ja testit
|
||||||
|
|
||||||
|
FIRST LINE must be exactly one of:
|
||||||
|
VERDICT: GREEN
|
||||||
|
VERDICT: ORANGE
|
||||||
|
VERDICT: RED
|
||||||
|
|
||||||
|
Then write this report:
|
||||||
|
|
||||||
|
# Todo-sovellus FastAPI + SQLite, CRUD-endpointit ja testit
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
One paragraph describing what this project does.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| models.py | ... |
|
||||||
|
| schemas.py | ... |
|
||||||
|
| main.py | ... |
|
||||||
|
| pyproject.toml | ... |
|
||||||
|
| test_main.py | ... |
|
||||||
|
| Dockerfile | ... |
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
```bash
|
||||||
|
git clone <repo>
|
||||||
|
cd project
|
||||||
|
uv sync
|
||||||
|
uv run uvicorn main:app --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
```bash
|
||||||
|
docker build -t todo-sovellus-fastapi---sqlite--crud-endpointit-ja-testit .
|
||||||
|
docker run -p 8000:8000 todo-sovellus-fastapi---sqlite--crud-endpointit-ja-testit
|
||||||
|
```
|
||||||
|
# ... (truncated)
|
||||||
|
expected_output: >-
|
||||||
|
README.md
|
||||||
|
agent: observer
|
||||||
116
zipit/projekti_clean/test_main.py
Normal file
116
zipit/projekti_clean/test_main.py
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from sqlalchemy import create_engine, Column, Integer, String, Date, Enum
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
DATABASE_URL = "sqlite:///./test_todo.db"
|
||||||
|
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
class Todo(Base):
|
||||||
|
__tablename__ = "todos"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
title = Column(String(100), nullable=False)
|
||||||
|
description = Column(String(500))
|
||||||
|
due_date = Column(Date, nullable=False)
|
||||||
|
status = Column(Enum('pending', 'completed'), default='pending')
|
||||||
|
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
@app.post("/todos/", response_model=Todo, status_code=201)
|
||||||
|
def create_todo(todo: TodoCreate, db: Session = Depends(get_db)):
|
||||||
|
db_todo = Todo(**todo.model_dump())
|
||||||
|
db.add(db_todo)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_todo)
|
||||||
|
return db_todo
|
||||||
|
|
||||||
|
@app.get("/todos/", response_model=list[Todo])
|
||||||
|
def list_todos(status: str | None = None, db: Session = Depends(get_db)):
|
||||||
|
if status:
|
||||||
|
query = db.query(Todo).filter_by(status=status)
|
||||||
|
else:
|
||||||
|
query = db.query(Todo)
|
||||||
|
return query.all()
|
||||||
|
|
||||||
|
@app.get("/todos/{todo_id}", response_model=Todo)
|
||||||
|
def get_todo(todo_id: int, db: Session = Depends(get_db)):
|
||||||
|
todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
||||||
|
if not todo:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
return todo
|
||||||
|
|
||||||
|
@app.put("/todos/{todo_id}", response_model=Todo)
|
||||||
|
def update_todo(todo_id: int, todo: TodoCreate, db: Session = Depends(get_db)):
|
||||||
|
db_todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
||||||
|
if not db_todo:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
for key, value in todo.model_dump().items():
|
||||||
|
setattr(db_todo, key, value)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_todo)
|
||||||
|
return db_todo
|
||||||
|
|
||||||
|
@app.delete("/todos/{todo_id}", status_code=204)
|
||||||
|
def delete_todo(todo_id: int, db: Session = Depends(get_db)):
|
||||||
|
db_todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
||||||
|
if not db_todo:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
db.delete(db_todo)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
def test_create_todo():
|
||||||
|
response = client.post("/todos/", json={"title": "Test Todo", "description": "This is a test todo.", "due_date": "2023-12-31"})
|
||||||
|
assert response.status_code == 201
|
||||||
|
data = response.json()
|
||||||
|
assert "id" in data
|
||||||
|
assert data["title"] == "Test Todo"
|
||||||
|
assert data["description"] == "This is a test todo."
|
||||||
|
assert data["due_date"] == "2023-12-31"
|
||||||
|
assert data["status"] == "pending"
|
||||||
|
|
||||||
|
def test_list_todos():
|
||||||
|
response = client.get("/todos/")
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert len(data) > 0
|
||||||
|
|
||||||
|
def test_get_todo_by_id():
|
||||||
|
todo_id = create_test_todo().json()["id"]
|
||||||
|
response = client.get(f"/todos/{todo_id}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert "id" in data
|
||||||
|
assert data["title"] == "Test Todo"
|
||||||
|
|
||||||
|
def test_get_nonexistent_todo():
|
||||||
|
response = client.get("/todos/999")
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
def test_update_todo():
|
||||||
|
todo_id = create_test_todo().json()["id"]
|
||||||
|
response = client.put(f"/todos/{todo_id}", json={"title": "Updated Todo"})
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert "id" in data
|
||||||
|
assert data["title"] == "Updated Todo"
|
||||||
|
|
||||||
|
def test_delete_todo():
|
||||||
|
todo_id = create_test_todo().json()["id"]
|
||||||
|
response = client.delete(f"/todos/{todo_id}")
|
||||||
|
assert response.status_code == 204
|
||||||
|
|
||||||
|
def create_test_todo():
|
||||||
|
return client.post("/todos/", json={"title": "Test Todo", "description": "This is a test todo.", "due_date": "2023
|
||||||
BIN
zipit/rest-api-kyttjhallinnalle.zip
Normal file
BIN
zipit/rest-api-kyttjhallinnalle.zip
Normal file
Binary file not shown.
11
zipit/rest_api_check/Dockerfile
Normal file
11
zipit/rest_api_check/Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
FROM python:3.12-slim
|
||||||
|
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
|
||||||
|
ENV UV_CACHE_DIR=/tmp/uv-cache
|
||||||
|
WORKDIR /app
|
||||||
|
COPY pyproject.toml .
|
||||||
|
RUN uv sync
|
||||||
|
COPY *.py .
|
||||||
|
RUN useradd -m appuser && chown -R appuser:appuser /app /tmp/uv-cache
|
||||||
|
USER appuser
|
||||||
|
EXPOSE 8000
|
||||||
|
CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
56
zipit/rest_api_check/README.md
Normal file
56
zipit/rest_api_check/README.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
**VERDICT: ORANGE**
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This project is a REST API for user management using SQLite as the database backend, built with FastAPI.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| models.py | Defines the data model for users. |
|
||||||
|
| schemas.py | Contains Pydantic schema definitions for input and output validation. |
|
||||||
|
| main.py | Implements the FastAPI application with endpoints for user operations. |
|
||||||
|
| test_main.py | Includes unit tests for the API endpoints using TestClient from fastapi.testclient. |
|
||||||
|
| pyproject.toml | Project metadata, dependencies, and build configuration. |
|
||||||
|
| Dockerfile | Defines a Docker image for running the API, including environment setup and dependency installation. |
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
git clone <repo>
|
||||||
|
cd project
|
||||||
|
uv sync
|
||||||
|
uv run uvicorn main:app --reload
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
docker build -t rest-api-k-ytt-j-hallinnalle-sqlite-tietokannalla .
|
||||||
|
docker run -p 8000:8000 rest-api-k-ytt-j-hallinnalle-sqlite-tietokannalla
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
| Method | Path | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| POST | /users/ | Create a new user. |
|
||||||
|
| GET | /users/ | List all users. |
|
||||||
|
| GET | /users/{id} | Retrieve a specific user by ID. |
|
||||||
|
| PUT | /users/{id} | Update an existing user. |
|
||||||
|
| DELETE | /users/{id} | Delete a user. |
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
The project uses FastAPI for the web framework, SQLAlchemy as the ORM, and SQLite as the database. The code is organized into `models.py` (data models), `schemas.py` (Pydantic schemas), and `main.py` (FastAPI application). Testing is done with `test_main.py`. A Dockerfile is provided to containerize the application.
|
||||||
|
|
||||||
|
## Risk Assessment
|
||||||
|
| Severity | Issue |
|
||||||
|
|----------|-------|
|
||||||
|
| **HIGH** | SQL injection risks: The API directly uses user input in SQL queries without validation or parameterized statements. This can lead to SQL injection vulnerabilities if an attacker manages to inject malicious SQL code through the input fields. |
|
||||||
|
| **MEDIUM** | Input validation is minimal: While Pydantic schemas are used for data validation, they only validate types and lengths but do not sanitize inputs (e.g., no escaping of special characters). This could potentially lead to security issues if an attacker bypasses these checks. |
|
||||||
|
| **MEDIUM** | Authentication and authorization: The API does not implement any authentication or authorization mechanisms. Any user can create, read, update, or delete users without proper credentials. |
|
||||||
|
| **LOW** | Error handling: Basic error handling is implemented using HTTPException for specific cases (e.g., resource not found). However, more comprehensive logging and exception handling could improve the robustness of the application. |
|
||||||
|
| **MEDIUM** | Database connection management: The database session is created once per request in `get_db` but is not explicitly closed or committed/rolled back in all cases, which can lead to potential memory leaks or data inconsistencies if an error occurs during a transaction. |
|
||||||
|
|
||||||
|
### Most Important Findings:
|
||||||
|
1. **SQL Injection Risks**: Direct user input usage in SQL queries without parameterized statements.
|
||||||
|
2. **Minimal Input Validation**: Pydantic schemas only validate types and lengths but do not sanitize inputs.
|
||||||
|
3. **Lack of Authentication and Authorization**: Anybody can perform CRUD operations on users.
|
||||||
|
4. **Error Handling Improvements Needed**: More comprehensive error handling, logging, and transaction management are required.
|
||||||
|
|
||||||
|
### Overall Assessment:
|
||||||
|
**NEEDS WORK: SQL injection risks, minimal input validation, lack of authentication/authorization, and basic error handling improvements are critical issues that need to be addressed before the project can be considered secure and robust.**
|
||||||
|
|
||||||
|
The architecture is logical but lacks necessary security features, and some parts of the code could benefit from better practices in terms of error management and database connection handling.
|
||||||
115
zipit/rest_api_check/agents.yaml
Normal file
115
zipit/rest_api_check/agents.yaml
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
# Agents — Kipinä Agentic Studio → CrewAI
|
||||||
|
|
||||||
|
client:
|
||||||
|
role: >-
|
||||||
|
Client
|
||||||
|
goal: >-
|
||||||
|
product owner who turns vague ideas into clear, actionable software requirements
|
||||||
|
backstory: |
|
||||||
|
You are a product owner who turns vague ideas into clear, actionable software requirements.
|
||||||
|
|
||||||
|
GIVEN a short project description from the user, produce a structured brief:
|
||||||
|
|
||||||
|
1. PROJECT NAME: a short, descriptive name
|
||||||
|
2. GOAL: one sentence explaining what the software does and who it's for
|
||||||
|
3. CORE FEATURES: numbered list of 3-8 concrete features (not vague wishes)
|
||||||
|
4. DATA MODEL: list the main entities and their key fields (include field types)
|
||||||
|
5. API ENDPOINTS: list the REST endpoints (method + path + purpose)
|
||||||
|
6. CONSTRAINTS: any technical constraints (e.g. "must use SQLite", "no auth needed")
|
||||||
|
|
||||||
|
RULES:
|
||||||
|
- Be specific: "User can filter todos by status" not "todo management"
|
||||||
|
- Use plain English, no code
|
||||||
|
- Maximum 400 words total
|
||||||
|
llm: qwen-coder
|
||||||
|
|
||||||
|
manager:
|
||||||
|
role: >-
|
||||||
|
Manager
|
||||||
|
goal: >-
|
||||||
|
software architect
|
||||||
|
backstory: |
|
||||||
|
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
|
||||||
|
llm: qwen-coder
|
||||||
|
|
||||||
|
data:
|
||||||
|
role: >-
|
||||||
|
Data Engineer
|
||||||
|
goal: >-
|
||||||
|
(template pipeline — ei LLM-promptia)
|
||||||
|
backstory: |
|
||||||
|
(template pipeline — ei LLM-promptia)
|
||||||
|
llm: template
|
||||||
|
|
||||||
|
coder:
|
||||||
|
role: >-
|
||||||
|
Coder
|
||||||
|
goal: >-
|
||||||
|
(template pipeline — ei LLM-promptia)
|
||||||
|
backstory: |
|
||||||
|
(template pipeline — ei LLM-promptia)
|
||||||
|
llm: template
|
||||||
|
|
||||||
|
qa:
|
||||||
|
role: >-
|
||||||
|
QA
|
||||||
|
goal: >-
|
||||||
|
(template pipeline — ei LLM-promptia)
|
||||||
|
backstory: |
|
||||||
|
(template pipeline — ei LLM-promptia)
|
||||||
|
llm: template
|
||||||
|
|
||||||
|
tester:
|
||||||
|
role: >-
|
||||||
|
DevOps
|
||||||
|
goal: >-
|
||||||
|
(template pipeline — ei LLM-promptia)
|
||||||
|
backstory: |
|
||||||
|
(template pipeline — ei LLM-promptia)
|
||||||
|
llm: template
|
||||||
|
|
||||||
|
observer:
|
||||||
|
role: >-
|
||||||
|
Observer
|
||||||
|
goal: >-
|
||||||
|
independent technical observer and risk analyst
|
||||||
|
backstory: |
|
||||||
|
You are an independent technical observer and risk analyst.
|
||||||
|
|
||||||
|
EVALUATE THE PROJECT FOR:
|
||||||
|
1. ARCHITECTURE: Is the file structure logical? Are responsibilities separated?
|
||||||
|
2. SECURITY: SQL injection risks? Input validation? Authentication?
|
||||||
|
3. RELIABILITY: Error handling? Database connection management? Edge cases?
|
||||||
|
4. MAINTAINABILITY: Consistent naming? Clear code structure? Would a new developer understand this?
|
||||||
|
|
||||||
|
OUTPUT FORMAT:
|
||||||
|
- RISK: [critical/high/medium/low] Description
|
||||||
|
- List max 3-5 most important findings
|
||||||
|
- End with overall assessment: "SHIP IT" or "NEEDS WORK: reason"
|
||||||
|
llm: qwen-coder
|
||||||
93
zipit/rest_api_check/crew.py
Normal file
93
zipit/rest_api_check/crew.py
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
"""REST API käyttäjähallinnalle SQLite-tietokannalla
|
||||||
|
|
||||||
|
CrewAI crew — generated by Kipinä Agentic Studio.
|
||||||
|
Run: crewai run
|
||||||
|
"""
|
||||||
|
|
||||||
|
from crewai import Agent, Crew, Process, Task
|
||||||
|
from crewai.project import CrewBase, agent, crew, task
|
||||||
|
|
||||||
|
|
||||||
|
@CrewBase
|
||||||
|
class ProjectCrew:
|
||||||
|
"""REST API käyttäjähallinnalle SQLite-tietokannalla"""
|
||||||
|
|
||||||
|
agents_config = "agents.yaml"
|
||||||
|
tasks_config = "tasks.yaml"
|
||||||
|
|
||||||
|
@agent
|
||||||
|
def client(self) -> Agent:
|
||||||
|
return Agent(config=self.agents_config["client"])
|
||||||
|
|
||||||
|
@agent
|
||||||
|
def manager(self) -> Agent:
|
||||||
|
return Agent(config=self.agents_config["manager"])
|
||||||
|
|
||||||
|
@agent
|
||||||
|
def data(self) -> Agent:
|
||||||
|
return Agent(config=self.agents_config["data"])
|
||||||
|
|
||||||
|
@agent
|
||||||
|
def coder(self) -> Agent:
|
||||||
|
return Agent(config=self.agents_config["coder"])
|
||||||
|
|
||||||
|
@agent
|
||||||
|
def qa(self) -> Agent:
|
||||||
|
return Agent(config=self.agents_config["qa"])
|
||||||
|
|
||||||
|
@agent
|
||||||
|
def tester(self) -> Agent:
|
||||||
|
return Agent(config=self.agents_config["tester"])
|
||||||
|
|
||||||
|
@agent
|
||||||
|
def observer(self) -> Agent:
|
||||||
|
return Agent(config=self.agents_config["observer"])
|
||||||
|
|
||||||
|
@task
|
||||||
|
def step_0_requirements(self) -> Task:
|
||||||
|
return Task(config=self.tasks_config["step_0_requirements"])
|
||||||
|
|
||||||
|
@task
|
||||||
|
def step_1_json_speksi(self) -> Task:
|
||||||
|
return Task(config=self.tasks_config["step_1_json_speksi"])
|
||||||
|
|
||||||
|
@task
|
||||||
|
def step_2_models_py(self) -> Task:
|
||||||
|
return Task(config=self.tasks_config["step_2_models_py"])
|
||||||
|
|
||||||
|
@task
|
||||||
|
def step_3_schemas_py(self) -> Task:
|
||||||
|
return Task(config=self.tasks_config["step_3_schemas_py"])
|
||||||
|
|
||||||
|
@task
|
||||||
|
def step_4_main_py(self) -> Task:
|
||||||
|
return Task(config=self.tasks_config["step_4_main_py"])
|
||||||
|
|
||||||
|
@task
|
||||||
|
def step_5_test_main_py(self) -> Task:
|
||||||
|
return Task(config=self.tasks_config["step_5_test_main_py"])
|
||||||
|
|
||||||
|
@task
|
||||||
|
def step_6_pyproject_toml(self) -> Task:
|
||||||
|
return Task(config=self.tasks_config["step_6_pyproject_toml"])
|
||||||
|
|
||||||
|
@task
|
||||||
|
def step_7_dockerfile(self) -> Task:
|
||||||
|
return Task(config=self.tasks_config["step_7_dockerfile"])
|
||||||
|
|
||||||
|
@task
|
||||||
|
def step_8_validointi(self) -> Task:
|
||||||
|
return Task(config=self.tasks_config["step_8_validointi"])
|
||||||
|
|
||||||
|
@task
|
||||||
|
def step_9_readme_md(self) -> Task:
|
||||||
|
return Task(config=self.tasks_config["step_9_readme_md"])
|
||||||
|
|
||||||
|
@crew
|
||||||
|
def crew(self) -> Crew:
|
||||||
|
return Crew(
|
||||||
|
agents=self.agents,
|
||||||
|
tasks=self.tasks,
|
||||||
|
process=Process.sequential,
|
||||||
|
verbose=True,
|
||||||
|
)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user