"""Terminal-Aufgaben. Jede Aufgabe liefert einen Shell-Einzeiler, der in einer frisch aufgebauten Sandbox laeuft; bewertet wird die tatsaechliche Ausgabe. Kein LLM benotet hier irgendwas — entweder die Ausgabe stimmt oder nicht. Die Sandbox wird pro Aufgabe neu erzeugt, damit ein zerstoerender Befehl nur seine eigene Kopie trifft. """ # Dateien, die in jeder Sandbox liegen. Bewusst mit Stolpersteinen: # Leerzeilen, doppelte Eintraege, gemischte Gross-/Kleinschreibung. SANDBOX = { "messwerte.csv": ( # Die 6 taucht absichtlich auch ausserhalb der kanal-Spalte auf # (16.1, 231.5 hat keine, aber 16.1 und 60.4 schon). Sonst besteht # ein blindes "grep -c 6" die Spaltenaufgabe zufaellig. "geraet;kanal;spannung;status\n" "pumpe;6;47.3;ok\n" "luefter;3;16.1;warn\n" "motor;6;231.5;ok\n" "ventil;5;60.4;fehler\n" "presse;3;400.2;ok\n" "kompressor;6;18.7;warn\n" ), "log.txt": ( "2026-08-15 10:01 INFO start\n" "2026-08-15 10:02 WARN kanal 6 instabil\n" "2026-08-15 10:02 INFO laeuft\n" "\n" "2026-08-15 10:05 ERROR kanal 5 ausgefallen\n" "2026-08-15 10:06 WARN kanal 6 instabil\n" "2026-08-15 10:09 INFO ende\n" ), "namen.txt": "anna\nbernd\nanna\nclara\nbernd\nanna\ndora\n", "config.json": '{"host":"11.0.0.12","port":8210,"modell":"qwen3.8-27b","sessions":2}\n', } # Zusaetzliche Dateien mit definierter Groesse fuer die "groesste Datei"-Aufgabe. SANDBOX_SIZED = {"klein.bin": 100, "mittel.bin": 5000, "gross.bin": 90000} TASKS = [ dict( id="term01", titel="Zeilen zaehlen", prompt="Gib genau einen Shell-Einzeiler aus, der die Anzahl der Zeilen " "in der Datei log.txt ausgibt — nur die nackte Zahl, kein Dateiname. " "Antworte ausschliesslich mit dem Befehl, ohne Erklaerung und ohne Code-Block.", check=lambda out: out.strip() == "7", erwartet="7", ), dict( id="term02", titel="Muster mit Zeilennummer", prompt="Gib genau einen Shell-Einzeiler aus, der aus log.txt alle Zeilen mit " "dem Wort WARN samt ihrer Zeilennummer ausgibt. " "Antworte ausschliesslich mit dem Befehl, ohne Erklaerung und ohne Code-Block.", # Zeilen 2 und 6 enthalten WARN. check=lambda out: ("2:" in out and "6:" in out and out.count("WARN") == 2 and "ERROR" not in out), erwartet="Zeilen 2 und 6 mit Praefix", ), dict( id="term03", titel="Groesste Datei", prompt="Gib genau einen Shell-Einzeiler aus, der den Namen der groessten " "Datei im aktuellen Verzeichnis ausgibt — nur den Namen, sonst nichts. " "Antworte ausschliesslich mit dem Befehl, ohne Erklaerung und ohne Code-Block.", check=lambda out: out.strip().split("/")[-1] == "gross.bin", erwartet="gross.bin", ), dict( id="term04", titel="Spalte aus CSV", prompt="Die Datei messwerte.csv ist mit Semikolon getrennt und hat eine " "Kopfzeile. Gib genau einen Shell-Einzeiler aus, der nur die Werte " "der Spalte 'geraet' ohne die Kopfzeile ausgibt. " "Antworte ausschliesslich mit dem Befehl, ohne Erklaerung und ohne Code-Block.", check=lambda out: [z for z in out.strip().splitlines() if z.strip()] == [ "pumpe", "luefter", "motor", "ventil", "presse", "kompressor"], erwartet="6 Geraetenamen", ), dict( id="term05", titel="Doppelte Zeilen", prompt="Gib genau einen Shell-Einzeiler aus, der aus namen.txt jeden Namen " "mit seiner Anzahl ausgibt, absteigend nach Anzahl sortiert. " "Antworte ausschliesslich mit dem Befehl, ohne Erklaerung und ohne Code-Block.", # anna 3x, bernd 2x, clara 1x, dora 1x — anna muss zuerst kommen. check=lambda out: (out.strip().splitlines()[0].strip().split()[-1] == "anna" or out.strip().splitlines()[0].strip().split()[0] == "anna") and "3" in out.strip().splitlines()[0], erwartet="anna mit 3 zuerst", ), dict( id="term06", titel="Summe einer Spalte", prompt="Die Datei messwerte.csv ist mit Semikolon getrennt und hat eine " "Kopfzeile. Gib genau einen Shell-Einzeiler aus, der die Summe der " "Spalte 'spannung' ausgibt. " "Antworte ausschliesslich mit dem Befehl, ohne Erklaerung und ohne Code-Block.", # 47.3+16.1+231.5+60.4+400.2+18.7 = 774.2 check=lambda out: any( abs(float(t) - 774.2) < 0.05 for t in out.replace(",", ".").split() if _istzahl(t)), erwartet="774.2", ), dict( id="term07", titel="Numerisch sortieren", prompt="Die Datei messwerte.csv ist mit Semikolon getrennt und hat eine " "Kopfzeile. Gib genau einen Shell-Einzeiler aus, der die Datenzeilen " "ohne Kopfzeile absteigend nach der Spalte 'spannung' sortiert ausgibt. " "Antworte ausschliesslich mit dem Befehl, ohne Erklaerung und ohne Code-Block.", check=lambda out: _erste_datenzeile(out).startswith("presse") and "geraet;kanal" not in out, erwartet="presse zuerst (400.2)", ), dict( id="term08", titel="JSON-Feld lesen", prompt="Gib genau einen Shell-Einzeiler aus, der aus config.json nur den " "Wert des Feldes 'port' ausgibt. " "Antworte ausschliesslich mit dem Befehl, ohne Erklaerung und ohne Code-Block.", check=lambda out: out.strip().strip('"') == "8210", erwartet="8210", ), dict( id="term09", titel="Ersetzen in Datei", prompt="Gib genau einen Shell-Einzeiler aus, der in der Datei log.txt jedes " "Vorkommen von WARN durch ACHTUNG ersetzt und die Datei dabei " "tatsaechlich veraendert. Danach soll nichts weiter ausgegeben werden. " "Antworte ausschliesslich mit dem Befehl, ohne Erklaerung und ohne Code-Block.", # Geprueft wird der Dateiinhalt nach dem Lauf, nicht die Ausgabe. check=None, datei_check=lambda dateien: ( dateien.get("log.txt", "").count("ACHTUNG") == 2 and "WARN" not in dateien.get("log.txt", "")), erwartet="log.txt enthaelt 2x ACHTUNG, kein WARN", ), dict( id="term10", titel="Gefiltert zaehlen", prompt="Die Datei messwerte.csv ist mit Semikolon getrennt und hat eine " "Kopfzeile. Gib genau einen Shell-Einzeiler aus, der zaehlt, wie viele " "Zeilen in der Spalte 'kanal' den Wert 6 haben — nur die nackte Zahl. " "Antworte ausschliesslich mit dem Befehl, ohne Erklaerung und ohne Code-Block.", check=lambda out: out.strip() == "3", erwartet="3", ), ] def _istzahl(t): try: float(t) return True except ValueError: return False def _erste_datenzeile(out): for z in out.strip().splitlines(): if z.strip(): return z.strip() return ""