QA katselmoi, DevOps keskittyy deploymenttiin
- Review-luuppi siirretty DevOps→QA: QA katselmoi koodin ja lähettää korjausvaatimukset Coderille (max 3 kierrosta) - QA:n prompt laajennettu: review-checklist + testien kirjoitus - DevOps:n prompt uusittu: Dockerfile + deployment -fokus - Pipeline: Client→Manager→Coder→QA review↔Coder fix→QA testit→DevOps Dockerfile→Observer - AGENTS_VERSION 4→5
This commit is contained in:
@@ -286,26 +286,9 @@ ALWAYS INCLUDE:
|
|||||||
- DATABASE_URL, engine, SessionLocal, Base` },
|
- DATABASE_URL, engine, SessionLocal, Base` },
|
||||||
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 writing automated tests.
|
prompt: `You are a QA engineer responsible for code review and automated testing.
|
||||||
|
|
||||||
WRITE TESTS USING:
|
CODE REVIEW CHECKLIST:
|
||||||
- pytest as the test framework
|
|
||||||
- FastAPI TestClient for API endpoint testing
|
|
||||||
- SQLAlchemy in-memory SQLite for test database isolation
|
|
||||||
|
|
||||||
TEST STRUCTURE:
|
|
||||||
1. test_create: POST valid data → 201, verify response matches input
|
|
||||||
2. test_list: GET collection → 200, verify array response
|
|
||||||
3. test_get_by_id: GET with valid/invalid ID → 200/404
|
|
||||||
4. test_update: PUT with valid data → 200, verify changes persisted
|
|
||||||
5. test_delete: DELETE → 204, verify GET returns 404 after
|
|
||||||
|
|
||||||
ALWAYS: from fastapi.testclient import TestClient` },
|
|
||||||
tester: { name: 'DevOps', avatar: '/avatars/laiskiainen_notext.webp', model: 'qwen-coder', order: 5,
|
|
||||||
temperature: 0.3, topK: 40, repeatPenalty: 1.1, maxTokens: 1024,
|
|
||||||
prompt: `You are a strict code reviewer and static analysis expert. Analyze the code line by line.
|
|
||||||
|
|
||||||
STATIC ANALYSIS CHECKLIST:
|
|
||||||
1. IMPORTS: Every "from X import Y" must match an actual export in file X
|
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)
|
2. NAMES: Pydantic schemas (UserCreate) must not shadow SQLAlchemy models (User)
|
||||||
3. TYPES: All function parameters have type hints, return types specified
|
3. TYPES: All function parameters have type hints, return types specified
|
||||||
@@ -316,10 +299,35 @@ STATIC ANALYSIS CHECKLIST:
|
|||||||
8. MODELS: Pydantic Config has from_attributes=True, uses model_dump() not dict()
|
8. MODELS: Pydantic Config has from_attributes=True, uses model_dump() not dict()
|
||||||
9. COMPLETENESS: No placeholder comments, no "TODO", no "pass" in handlers
|
9. COMPLETENESS: No placeholder comments, no "TODO", no "pass" in handlers
|
||||||
|
|
||||||
RESPOND:
|
WHEN REVIEWING:
|
||||||
- If all checks pass: "LGTM"
|
- If all checks pass: respond "LGTM"
|
||||||
- If issues found: list each as "ISSUE: filename.py: description"
|
- If issues found: list each as "ISSUE: filename.py: description"
|
||||||
- Be specific and actionable, not vague` },
|
- 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` },
|
||||||
|
tester: { name: 'DevOps', avatar: '/avatars/laiskiainen_notext.webp', model: 'qwen-coder', order: 5,
|
||||||
|
temperature: 0.3, topK: 40, repeatPenalty: 1.1, maxTokens: 1024,
|
||||||
|
prompt: `You are a DevOps engineer specializing in containerization and deployment.
|
||||||
|
|
||||||
|
YOUR RESPONSIBILITIES:
|
||||||
|
1. Write production-ready Dockerfiles
|
||||||
|
2. Use multi-stage builds when appropriate
|
||||||
|
3. Follow security best practices (non-root user, minimal base image)
|
||||||
|
4. Configure health checks and proper signal handling
|
||||||
|
|
||||||
|
DOCKERFILE RULES:
|
||||||
|
- Use python:3.12-slim as base
|
||||||
|
- Install uv: COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
|
||||||
|
- Copy pyproject.toml first, then uv sync, then copy source
|
||||||
|
- Expose appropriate ports
|
||||||
|
- Use uv run for CMD
|
||||||
|
|
||||||
|
Write ONLY the requested files, 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.
|
||||||
@@ -336,7 +344,7 @@ OUTPUT FORMAT:
|
|||||||
- End with overall assessment: "SHIP IT" or "NEEDS WORK: reason"` },
|
- End with overall assessment: "SHIP IT" or "NEEDS WORK: reason"` },
|
||||||
};
|
};
|
||||||
// Versio: kasvata kun oletuspromptit muuttuvat
|
// Versio: kasvata kun oletuspromptit muuttuvat
|
||||||
const AGENTS_VERSION = 4;
|
const AGENTS_VERSION = 5;
|
||||||
let agents;
|
let agents;
|
||||||
const savedVersion = parseInt(localStorage.getItem('kpn-agents-version') || '0');
|
const savedVersion = parseInt(localStorage.getItem('kpn-agents-version') || '0');
|
||||||
if (savedVersion < AGENTS_VERSION && localStorage.getItem('kpn-agents')) {
|
if (savedVersion < AGENTS_VERSION && localStorage.getItem('kpn-agents')) {
|
||||||
@@ -1129,41 +1137,41 @@ OUTPUT FORMAT:
|
|||||||
const allCode = Object.entries(files).map(([n,c]) => `--- ${n} ---\n${c}`).join('\n\n');
|
const allCode = Object.entries(files).map(([n,c]) => `--- ${n} ---\n${c}`).join('\n\n');
|
||||||
let stepN = fileOrder.length + 1;
|
let stepN = fileOrder.length + 1;
|
||||||
|
|
||||||
// Review-korjausluuppi: max 2 kierrosta
|
// QA-katselmointi → Coder-korjaus -luuppi (max N kierrosta)
|
||||||
const tst = agents.tester || Object.values(agents)[5];
|
const qaAgent = agents.qa || Object.values(agents)[4];
|
||||||
const MAX_REVIEW_ROUNDS = pipelineConfig.maxReviewRounds;
|
const MAX_REVIEW_ROUNDS = pipelineConfig.maxReviewRounds;
|
||||||
|
|
||||||
for (let round = 0; round < MAX_REVIEW_ROUNDS; round++) {
|
for (let round = 0; round < MAX_REVIEW_ROUNDS; round++) {
|
||||||
const currentCode = Object.entries(files).map(([n,c]) => `--- ${n} ---\n${c}`).join('\n\n');
|
const currentCode = Object.entries(files).map(([n,c]) => `--- ${n} ---\n${c}`).join('\n\n');
|
||||||
|
|
||||||
// DevOps review
|
// QA katselmoi
|
||||||
termLog(`\n<span style="color:var(--accent);font-weight:bold">[${stepN}] ${esc(tst.name)}</span> — code review${round > 0 ? ' (round '+(round+1)+')' : ''}`);
|
termLog(`\n<span style="color:#d2a8ff;font-weight:bold">[${stepN}] ${esc(qaAgent.name)}</span> — katselmointi${round > 0 ? ' (kierros '+(round+1)+')' : ''}`);
|
||||||
highlightAgent('tester');
|
highlightAgent('qa');
|
||||||
if (round === 0) explainStep('Code Review', `${tst.name} reviews code line by line: imports, naming, security.`);
|
if (round === 0) explainStep('Katselmointi', `${qaAgent.name} analysoi koodin: importit, nimeämiset, virheenkäsittely, tietoturva.`);
|
||||||
else explainStep('Re-evaluation', `${tst.name} checks the applied fixes.`);
|
else explainStep('Uudelleentarkistus', `${qaAgent.name} tarkistaa korjaukset.`);
|
||||||
|
|
||||||
const reviewPrompt = (tst.prompt ? tst.prompt+'\n\n' : '') + `Review this project:\n\n${currentCode}`;
|
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(tst.model, reviewPrompt, false, tst);
|
const review = await kpnRun(qaAgent.model, reviewPrompt, false, qaAgent);
|
||||||
promptLog.push({ step: promptLog.length, agentKey: 'tester', agentName: tst.name, model: tst.model, label: 'review' + (round > 0 ? '_r'+(round+1) : ''), systemPrompt: tst.prompt || '', userPrompt: reviewPrompt, response: review || '' });
|
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++;
|
stepN++;
|
||||||
|
|
||||||
// LGTM → ei korjauksia tarvita
|
// LGTM → ei korjauksia tarvita
|
||||||
if (!review || review.toLowerCase().includes('lgtm')) {
|
if (!review || review.toLowerCase().includes('lgtm')) {
|
||||||
termLog(` <span style="color:#3fb950">✓ ${esc(tst.name)}: LGTM</span>`);
|
termLog(` <span style="color:#3fb950">✓ ${esc(qaAgent.name)}: LGTM</span>`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Korjaukset
|
// Korjaukset → Coder
|
||||||
termLog(`\n<span style="color:#d29922;font-weight:bold">[${stepN}] ${esc(cdr.name)}</span> — fixes${round > 0 ? ' (round '+(round+1)+')' : ''}`);
|
termLog(`\n<span style="color:#d29922;font-weight:bold">[${stepN}] ${esc(cdr.name)}</span> — korjaukset${round > 0 ? ' (kierros '+(round+1)+')' : ''}`);
|
||||||
highlightAgent('coder');
|
highlightAgent('coder');
|
||||||
explainStep('Fixing', `${tst.name} found issues. ${cdr.name} will attempt to fix them.`);
|
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 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);
|
const fixedCode = await kpnRun(cdr.model, fixPrompt, false, cdr);
|
||||||
|
|
||||||
// Parsitaan korjatut tiedostot takaisin files-objektiin
|
// Parsitaan korjatut tiedostot takaisin files-objektiin
|
||||||
if (fixedCode) {
|
if (fixedCode) {
|
||||||
promptLog.push({ step: promptLog.length, agentKey: 'coder', agentName: cdr.name, model: cdr.model, label: 'fix' + (round > 0 ? '_r'+(round+1) : ''), systemPrompt: cdr.prompt || '', userPrompt: fixPrompt, response: 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);
|
const fixedParts = fixedCode.split(/^---\s*(\S+)\s*---$/m);
|
||||||
for (let j = 1; j < fixedParts.length; j += 2) {
|
for (let j = 1; j < fixedParts.length; j += 2) {
|
||||||
const fname = fixedParts[j].trim();
|
const fname = fixedParts[j].trim();
|
||||||
@@ -1179,22 +1187,22 @@ OUTPUT FORMAT:
|
|||||||
// Päivitetään allCode korjausten jälkeen
|
// Päivitetään allCode korjausten jälkeen
|
||||||
const updatedCode = Object.entries(files).map(([n,c]) => `--- ${n} ---\n${c}`).join('\n\n');
|
const updatedCode = Object.entries(files).map(([n,c]) => `--- ${n} ---\n${c}`).join('\n\n');
|
||||||
|
|
||||||
// QA: testit (saa korjatut tiedostot)
|
// QA: testit (saa katselmoidut ja korjatut tiedostot)
|
||||||
const qaAgent = agents.qa || Object.values(agents)[4];
|
|
||||||
if (qaAgent) {
|
if (qaAgent) {
|
||||||
termLog(`\n<span style="color:#d2a8ff;font-weight:bold">[${stepN}] ${esc(qaAgent.name)}</span> — testit`);
|
termLog(`\n<span style="color:#d2a8ff;font-weight:bold">[${stepN}] ${esc(qaAgent.name)}</span> — testit`);
|
||||||
highlightAgent('qa');
|
highlightAgent('qa');
|
||||||
explainStep('Testit', `${qaAgent.name} kirjoittaa pytest-testit korjatulle koodille.`);
|
explainStep('Testit', `${qaAgent.name} kirjoittaa pytest-testit katselmoidulle koodille.`);
|
||||||
const qaPrompt = (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 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, qaPrompt, false, qaAgent);
|
const tests = await kpnRun(qaAgent.model, qaTestPrompt, false, qaAgent);
|
||||||
if (tests) {
|
if (tests) {
|
||||||
files['test_main.py'] = 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: qaPrompt, response: 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++;
|
stepN++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// DevOps: Dockerfile (saa kaikki tiedostot mukaan lukien testit)
|
// DevOps: Dockerfile + deployment (saa kaikki tiedostot mukaan lukien testit)
|
||||||
|
const tst = agents.tester || Object.values(agents)[5];
|
||||||
const allFilesNow = Object.keys(files).join(', ');
|
const allFilesNow = Object.keys(files).join(', ');
|
||||||
termLog(`\n<span style="color:var(--accent);font-weight:bold">[${stepN}] ${esc(tst.name)}</span> — Dockerfile`);
|
termLog(`\n<span style="color:var(--accent);font-weight:bold">[${stepN}] ${esc(tst.name)}</span> — Dockerfile`);
|
||||||
highlightAgent('tester');
|
highlightAgent('tester');
|
||||||
|
|||||||
Reference in New Issue
Block a user