diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index c931ba7..8041541 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: run: pip install -e ".[dev]" - name: Ruff (lint) - run: ruff check src/ tests/ build_archive.py scripts/eval_prompt_tests.py + run: ruff check src/ tests/ build_archive.py - name: Mypy (type check) run: mypy diff --git a/CHANGELOG.md b/CHANGELOG.md index ca37c9d..1463e60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,36 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Per-profile chat streaming and chat-request timeouts: `stream`, `read_timeout` and `connect_timeout` are now resolvable in `[default]`/`[model.]` (CLI `--stream`/`--read-timeout`/`--connect-timeout` still override). -- Multimodal support: `mmproj` and `mmproj_offload` config keys (CLI `--mmproj`, - `--no-mmproj-offload`) pass a vision projector to the server. The path resolves - under `hf_home` like `model_path`, and a configured-but-missing projector now - fails with exit code 3 — for `--change` before the running container is removed. -- `docs/KI_TOOLS_PROFILES.md`: which local model suits which task, plus mmproj - compatibility per base model. -- `docs/EVAL_RUBRIC.md` and `scripts/eval_prompt_tests.py`: scoring rubric and an - evaluator for the manual prompt-test archive that measures length compliance and - **executes** the model-generated code against its own pytest suite. -- `scripts/run_prompt_suite.sh`: runs a prompt domain against a running profile and - stores the outputs under the archive's naming convention. -- `[model.qwen35base]` profile: the non-abliterated Qwen3.6-35B-A3B (bartowski - imatrix Q4_K_M) as a control for the abliteration comparison. Inherits sampling, - `ctx_size` and cache types from `[default]`; only model, container, port and GPU - differ, so it runs alongside the production server. ### Changed - The chat request now honors the configured `(connect, read)` timeout instead of a hard-coded 30 s; the default `read_timeout` is 600 s so slow reasoning models are no longer cut off mid-generation. -- **Breaking:** `--print-effective-config` is now an action in the mutually - exclusive action group instead of a flag, and it skips the `docker_available()` - check. `--print-effective-config --start` is therefore an argparse error - (exit 2) rather than a config dump followed by a real container start. - -### Fixed -- `--print-effective-config` combined with an action silently performed that - action. Documented as a diagnostic command (README, installation guide, manual), - `--print-effective-config --config … --start` printed the resolved config and - then ran `do_start()`, replacing a running container with the `[default]` model. ## [0.1.0] - 2026-07-07 diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index cb1feb2..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,104 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Commands - -```bash -pip install -e ".[dev]" # dev install -scripts/check.sh # full local CI gate (ruff + mypy + pytest) — mirrors .forgejo/workflows/ci.yml -python -m pytest -q # tests -python -m pytest tests/test_prompt_urls.py -q # one file -python -m pytest tests/test_actions.py::test_do_start -q # one test -ruff check src/ tests/ build_archive.py -mypy # config in pyproject.toml (files = src/llamacppctl) -``` - -Always use `python -m pytest`, never the `pytest` console script: the test modules do -`from tests.test_docker_ops import ...`, which needs the repo root on `sys.path`. - -`git config core.hooksPath .githooks` (once per clone) enables the pre-push hook that runs -`scripts/check.sh`. CI runs on Python 3.10 and 3.12; `requires-python = ">=3.10"`. - -Other entry points: -- `python3 build_archive.py` — builds and self-verifies `llamacppctl-installable.tar.gz` - (has its own required-file manifest; adding a top-level file that must ship means updating it). -- `SMOKE_GPU=1 scripts/smoke.sh` — opt-in end-to-end test against a real Docker + GPU + GGUF model. - Not part of the pytest suite. - -## Architecture - -A single CLI (`llamacppctl`) that starts/checks/stops/switches llama.cpp servers running as Docker -containers, plus a security-hardened input layer for system/user prompts from text, file, or URL. - -`main.py` is a thin pipeline: parse (`cli.build_parser`) → validate (`cli.validate_args`) → -`docker_available()` → resolve prompt sources (`prompt_io`) → resolve config -(`config.resolve_effective_config`) → dispatch to `actions.do_*`. - -Layering is deliberate and must be preserved — it keeps the security-critical input validation -independently testable from orchestration: -- `prompt_io.py` knows nothing about argparse; it works only with `PromptSource` + `InputPolicy`. -- `config.py` knows nothing about Docker or HTTP. -- `docker_ops.py` / `http_ops.py` know nothing about CLI semantics. -- `actions.py` orchestrates; `schema.py` holds the shared dataclasses (`ServerConfig`, - `PromptConfig`, `ChatReply`, `CheckResult`). - -Two cross-module couplings to know about: -- `main._resolve_prompts()` stashes loaded prompts on the argparse namespace as - `args._resolved_system_prompt` / `args._resolved_user_prompt`; `config.py` picks them up there. -- `main.run()` calls `docker_available()` *before* config resolution, so `--dry-run` still - needs a reachable Docker daemon. Only `--print-effective-config` skips that check — it is a - standalone action in the mutually-exclusive group, never a modifier, so it cannot start - anything. - -### Config resolution - -INI file (`llama.cpp.config`, template in `llama.cpp.config.example`), lowest to highest priority: -builtin defaults (`config.builtin_defaults()`) → `[default]` → `[model.]` (via `--profile`) -→ CLI overrides. `[prompt.]` sections resolve separately and only supply a *fallback* system -prompt — an explicit `-s`/`--system-file`/`--system-url` always wins. - -`container_name` is mandatory (empty → `ConfigError`). It is the only identity anchor: Docker -`--name`, the `--stop`/`--check` target, and the derived lock path -`/tmp/llamacppctl..lock`. Profiles meant to run in parallel need distinct -`container_name` *and* `host_port`, otherwise a second `--start` silently replaces the first -container. - -`max_tokens` / `chat_temperature` / `connect_timeout` / `read_timeout` affect only the chat request -(`--chat` and the reply printed after `--start`), never the container. - -### Exit codes (`actions.py`) - -`0` ok · `1` general/config/prompt error · `2` argparse validation · `3` model path missing · -`4` docker error · `5` HTTP not ready (also `--check` failing) · `6` lock busy. -`--check` is meant to be scriptable; keep those codes stable. - -## Invariants worth not breaking - -- All Docker calls go through `subprocess.run()` with **argument lists** — never `shell=True`, - never composed shell strings. -- The port publishes to `127.0.0.1` by default (`docker_ops._port_publish()`); the llama.cpp - OpenAI endpoint is unauthenticated unless `api_key` is set. `--expose` binds all interfaces. -- Every HTTP request in `prompt_io` — including each redirect hop — passes `validate_url_target()`: - HTTPS-only, no IP literals, no embedded credentials, no `localhost`, all resolved addresses - classified (private/loopback/link-local/multicast/reserved/unspecified → reject), metadata IP - `169.254.169.254` blocked. `_pin_dns()` then restricts `getaddrinfo` to the already-validated IPs - for the duration of the request, closing the TOCTOU/DNS-rebinding window. Size limits are enforced - both from `Content-Length` and while streaming. -- Escape hatches (`--allow-private-url`, `--allow-insecure-http`, `--allow-ip-host`, - `--allow-symlinks`, `--follow-redirects`, `--allow-html-input`) are opt-in and off by default. - `--allow-private-url` and `--url-allow-host` are mutually exclusive. -- `--start` and `--change` hold an exclusive non-blocking `fcntl.flock`; `--change` validates the - model path *before* removing the running container. `--dry-run` takes no lock and touches nothing. - -Tests mock `subprocess` and `requests` (and DNS resolution for the SSRF cases); nothing in the -pytest suite requires Docker or the network. - -## Conventions - -- Docs, comments in `docs/`, and the example prompts are in German; source code, docstrings, and - commit subjects are in English. Commits follow Conventional Commits (`feat(chat):`, `docs(prompts):`). -- Line length 100, `from __future__ import annotations` in every module, `py.typed` is shipped — - keep annotations complete enough for `mypy` to pass. -- Behavior changes should be reflected in `man/llamacppctl.1`, `CHANGELOG.md`, and — for security- - relevant ones — `docs/SECURITY_AND_OPERATIONS.md`. diff --git a/README.md b/README.md index 197c931..a780db4 100644 --- a/README.md +++ b/README.md @@ -44,10 +44,9 @@ Voraussetzungen auf dem Zielsystem: - Docker (CLI + laufender Daemon), für `--start`/--check`/--stop`/--change` - Netzwerkzugriff auf den Container-Host-Port für `--chat`/--check` -`--print-effective-config` ist eine eigenständige, nebenwirkungsfreie Aktion und -benötigt keinen laufenden Docker-Daemon. `--start`/--check`/--stop`/--change` -erfordern einen erreichbaren Docker-Daemon — auch zusammen mit `--dry-run`, weil -die Verfügbarkeit geprüft wird, bevor der Trockenlauf greift. +`--print-effective-config` und `--dry-run` benötigen keinen laufenden Docker-Daemon +für die reine Konfigurationsprüfung; `--start`/--check`/--stop`/--change` erfordern +einen erreichbaren Docker-Daemon. ## Konfiguration @@ -94,8 +93,8 @@ llamacppctl --start --config llama.cpp.config # Nur den geplanten docker-run-Befehl anzeigen, nichts ausführen llamacppctl --start --config llama.cpp.config --dry-run -# Effektive Konfiguration als JSON ausgeben (startet nichts, braucht kein Docker) -llamacppctl --print-effective-config --config llama.cpp.config +# Effektive Konfiguration als JSON ausgeben +llamacppctl --print-effective-config --config llama.cpp.config --start # Status prüfen llamacppctl --check --config llama.cpp.config diff --git a/docs/BEDIENUNGSANLEITUNG.md b/docs/BEDIENUNGSANLEITUNG.md index 7b8a91c..87b2605 100644 --- a/docs/BEDIENUNGSANLEITUNG.md +++ b/docs/BEDIENUNGSANLEITUNG.md @@ -49,7 +49,7 @@ Siehe [`INSTALL_FROM_ARCHIVE.md`](INSTALL_FROM_ARCHIVE.md). ```bash llamacppctl --help -llamacppctl --print-effective-config --config llama.cpp.config +llamacppctl --print-effective-config --config llama.cpp.config --start --dry-run ``` --- diff --git a/docs/EVAL_RUBRIC.md b/docs/EVAL_RUBRIC.md deleted file mode 100644 index 4b0c9ef..0000000 --- a/docs/EVAL_RUBRIC.md +++ /dev/null @@ -1,108 +0,0 @@ -# Bewertungsrubrik für Modell-Prompt-Tests - -Zweck: Modellwechsel (anderes Modell, andere Quantisierung, `Balanced` statt -`Aggressive`, MTP, KV-Cache-Einstellungen) sollen an **Zahlen** entschieden -werden, nicht an Bauchgefühl. Ohne Vorher-Nachher-Messung ist nicht feststellbar, -ob eine Änderung verbessert oder verschlechtert hat. - -Grundlage sind die Läufe unter `~/llamacppctl_prompt_tests/` gegen die Prompts in -`example_user_prompts/`. Modelleignung siehe [`KI_TOOLS_PROFILES.md`](KI_TOOLS_PROFILES.md). - -## Zwei Ebenen - -**Objektiv** — von `scripts/eval_prompt_tests.py` automatisch gemessen, keine -Meinung nötig: - -| Metrik | Domäne | Bedeutung | -|---|---|---| -| Wortzahl, Abweichung vom Ziel | prosa, reden | Der Prompt nennt ein Ziel („etwa 1200 Wörtern"). | -| Trunkierungsverdacht | alle | Text endet ohne Satzzeichen → Budget zu klein. | -| Tests laufen durch | coding | Der generierte Code wird **wirklich ausgeführt**. | -| Anteil bestandener Tests | coding | `passed / (passed + failed)`. | - -**Subjektiv** — von Hand oder per LLM-Judge, Skala 1–5. Die Anker unten sind -bewusst so formuliert, dass 3 „brauchbar, aber mit Mängeln" bedeutet; ein -durchschnittlich guter Text landet nicht automatisch bei 4. - -## Skala - -| Punkte | Bedeutung | -|---|---| -| 5 | Kriterium durchgängig erfüllt; kein Eingriff nötig. | -| 4 | Erfüllt, mit einzelnen Schwächen, die den Gesamteindruck nicht tragen. | -| 3 | Brauchbar, aber erkennbare Mängel; Überarbeitung nötig. | -| 2 | Kriterium überwiegend verfehlt; Grundgerüst vorhanden. | -| 1 | Verfehlt. | - -## Prosa - -1. **Show, don't tell** — Innenleben ausschließlich über Handlung, Gegenstand, - Wahrnehmung. *1 = benannte Gefühle („sie war traurig"); 5 = kein einziges - benanntes Gefühl, Zustand trotzdem eindeutig.* -2. **Schlussbild** — konkretes, bedeutungstragendes Bild statt Moral oder - Zusammenfassung. *1 = explizite Lehre; 5 = Bild, das die Geschichte trägt.* -3. **Ton- und Registertreue** — hält die geforderte Tonlage (z. B. „nüchtern, - ohne Pathos") über den gesamten Text. -4. **Sprachliche Präzision** — konkrete Substantive, keine Füllattribute, keine - Klischees. -5. **Komposition** — Aufbau trägt; kein Leerlauf, kein abrupter Abbruch. -6. **Sprachrichtigkeit (Deutsch)** — Grammatik, Kasus, Idiomatik. *Eigener - Punkt, weil abliterierte, primär englisch trainierte Modelle hier auffällig - sind.* - -## Reden - -1. **Argumentative Substanz** — trägt der Gedankengang, oder reiht er Behauptungen? -2. **Vorweggenommene Einwände** — wird der stärkste Gegeneinwand benannt und - beantwortet, nicht der schwächste? -3. **Benannte Zielkonflikte** — werden echte Interessengegensätze offen - ausgesprochen statt harmonisiert? -4. **Rhetorische Mittel** — bewusst und sparsam eingesetzt (Trikolon, Anapher, - Antithese), nicht dekorativ. -5. **Adressatenbezug** — Sprache, Beispiele und Anrede passen zum Publikum. -6. **Sprachrichtigkeit (Deutsch)** — siehe oben. - -## Coding - -1. **Korrektheit** — *primär aus dem automatischen Testlauf.* Ein Modul, dessen - eigene Tests durchfallen, kann in diesem Kriterium nicht über 2 kommen. -2. **Anforderungsabdeckung** — sind alle Punkte des Prompts umgesetzt? -3. **Fehlerbehandlung** — die im Prompt geforderten Fehlerfälle, sauber getrennt. -4. **Testqualität** — decken die Tests die genannten Fälle ab, und sind sie - konsistent zum eigenen Code (richtiger Exception-Typ, Fixtures erfüllen das - Schema)? -5. **Keine Halluzinationen** — keine erfundenen APIs, Signaturen, - Framework-Aussagen oder Benchmark-Messwerte. *Automatisch nicht prüfbar; - erfundene Messwerte sind in den bisherigen Läufen wiederholt aufgetreten.* -6. **Lesbarkeit** — Benennung, Struktur, Kommentardichte. - -## Durchführung - -- **Blind bewerten.** Die Modellzuordnung steckt im Dateinamen; beim Scoren - ausblenden (`scripts/eval_prompt_tests.py --anonymize` schreibt anonymisierte - Kopien mit Zufalls-IDs und eine Auflösungstabelle). -- **Ein Kriterium über alle Texte**, nicht ein Text über alle Kriterien. Das - hält den Maßstab konstant. -- **LLM-Judge:** möglich gegen den lokalen Server, aber der Judge darf **nicht** - das bewertete Modell sein. Ein Modell bewertet seine eigenen Texte zu gut. - Der Judge ersetzt die menschliche Stichprobe nicht — mindestens 20 % der Texte - gegenlesen und die Übereinstimmung prüfen. -- **Mindestens zwei Läufe pro Zelle**, weil bei `temp > 0` ein Einzellauf wenig - aussagt. Für Coding-Vergleiche `--seed` fixieren. - -## Was die Zahlen nicht sagen - -Ein bestandener Testlauf beweist, dass der Code *seine eigenen* Tests besteht — -nicht, dass er korrekt ist. Wenn Modell und Test aus derselben Halluzination -stammen, sind beide konsistent falsch. Kriterium 4 (Testqualität) ist deshalb -nicht redundant zu Kriterium 1, sondern dessen Korrektiv. - -## Sicherheitshinweis - -`scripts/eval_prompt_tests.py` führt **modellgenerierten Code aus**. Das ist der -Sinn der Übung, aber es ist Codeausführung aus einer nicht vertrauenswürdigen -Quelle. Der Runner arbeitet in einem temporären Verzeichnis, erzwingt ein -Zeitlimit und verweigert den Start als `root`. Er bietet **keine** Netz- oder -Dateisystem-Isolation. Wer die Läufe auf einem Rechner mit Produktionsdaten -auswertet, sollte `--no-exec` verwenden oder das Skript in einem Container -starten. diff --git a/docs/INSTALL_FROM_ARCHIVE.md b/docs/INSTALL_FROM_ARCHIVE.md index ab0e641..2c7c2ef 100644 --- a/docs/INSTALL_FROM_ARCHIVE.md +++ b/docs/INSTALL_FROM_ARCHIVE.md @@ -64,7 +64,7 @@ cp llama.cpp.config.example llama.cpp.config Auflösung prüfen, ohne etwas zu starten: ```bash -llamacppctl --print-effective-config --config llama.cpp.config +llamacppctl --print-effective-config --config llama.cpp.config --start --dry-run ``` Das zeigt die vollständig aufgelöste Konfiguration als JSON **und** das diff --git a/docs/KI_TOOLS_PROFILES.md b/docs/KI_TOOLS_PROFILES.md deleted file mode 100644 index 674a580..0000000 --- a/docs/KI_TOOLS_PROFILES.md +++ /dev/null @@ -1,169 +0,0 @@ -# Modellprofile — Eignung der lokalen GGUF-Modelle - -Dieses Dokument beschreibt, **wofür** sich die Modelle unter -`$HF_HOME/models/qwen3/` jeweils eignen, unabhängig von den Beispiel-Prompts in -`example_system_prompts/`. Grundlage sind die Modellkarten auf Hugging Face -(Stand Juli 2026) sowie die eigenen Testläufe unter `~/llamacppctl_prompt_tests/`. - -> Die Angaben zu Fähigkeiten und Benchmarks stammen, wo nicht anders vermerkt, -> **von den Modellautoren selbst** und sind nicht unabhängig verifiziert. - -## Übersicht - -| Datei | Profil | Basis | Primäre Eignung | -|---|---|---|---| -| `ornith-1.0-35b-Q4_K_M.gguf` | `ornith35b` | Qwen3.5-MoE | Agentisches Coding | -| `Carnice-Qwen3.6-MoE-35B-A3B-Q4_K_M.gguf` | `carnice` | Qwen3.6-35B-A3B | Agenten-/Tool-Calling-Workflows | -| `Qwopus3.6-35B-A3B-v1-Q4_K_M.gguf` | `qwopus` | Qwen3.6-35B-A3B | Reasoning, Vision, Tool-Calling | -| `Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-Q4_K_M.gguf` | `[default]` | Qwen3.6-35B-A3B | Kreatives Schreiben ohne Verweigerungen | -| `Qwen3.6-27B-Uncensored-HauhauCS-Aggressive-IQ4_XS.gguf` | `qwen27b` | Qwen3.6-27B (dense) | Wie oben, kleinerer VRAM-Bedarf | -| `mmproj.gguf` | — | Qwen3.6-35B-A3B | Vision-Projektor (kein eigenständiges Modell) | - -Alle 35B-A3B-Modelle sind MoE mit **3 B aktiven Parametern** pro Token: hoher -Durchsatz bei 21 GB VRAM-Bedarf. Das 27B ist dense — langsamer pro Token, aber -nur ~15 GB. - -## Ornith-1.0-35B — agentisches Coding - -Quelle: [`deepreinforce-ai/Ornith-1.0-35B-GGUF`](https://huggingface.co/deepreinforce-ai/Ornith-1.0-35B-GGUF) (MIT) - -Ein dediziertes Coding-Modell aus einer selbstverbessernden Modellfamilie. Das -Trainings-Framework erzeugt per RL nicht nur Lösungs-Rollouts, sondern optimiert -auch das treibende Scaffold gemeinsam mit ihnen. Beworben mit SOTA unter -vergleichbar großen Open-Source-Modellen auf Terminal-Bench 2.1, SWE-Bench, -NL2Repo und OpenClaw. Reasoning-Modell: die Assistant-Antwort beginnt mit einem -``-Block. - -**Einsetzen für:** Code-Generierung, Refactoring, Repo-weite Aufgaben, -Agenten-Loops mit Tool-Calls. -**Nicht einsetzen für:** Prosa und Reden — dafür ist das Sampling im Profil -(`temp = 0.20`) bewusst deterministisch gewählt. - -Beachte: Basis ist **Qwen3.5**-MoE (`qwen3_5_moe`), nicht 3.6. Die lokale -`mmproj.gguf` passt deshalb *nicht* zu diesem Modell. - -## Carnice-Qwen3.6-MoE-35B-A3B — Agenten-Workflows - -Quelle: [`samuelcardillo/Carnice-Qwen3.6-MoE-35B-A3B`](https://huggingface.co/samuelcardillo/Carnice-Qwen3.6-MoE-35B-A3B) - -QLoRA-Finetune von Qwen3.6-35B-A3B, trainiert auf echten Execution-Traces der -Hermes-Agent-Runtime. Das erklärte Ziel ist, dem Modell die konkreten -Konversationsmuster beizubringen, die ein Agenten-Runtime erwartet — nicht -generisches Reasoning. - -**Einsetzen für:** Tool-Calling-Pipelines, Agenten-Orchestrierung (z. B. n8n), -strukturierte Mehrschritt-Abläufe. - -## Qwopus3.6-35B-A3B-v1 — Reasoning + Vision - -Quelle: [`Jackrong/Qwopus3.6-35B-A3B-v1`](https://huggingface.co/Jackrong/Qwopus3.6-35B-A3B-v1) - -Reasoning-verstärkter Finetune, dreistufiges SFT mit progressiv steigender -Reasoning-Komplexität. Unterstützt Vision und Tool-Calling. - -**Einsetzen für:** Aufgaben mit langer Gedankenkette, multimodale Eingaben. -**Vorbehalt:** Der Autor kennzeichnet das Modell ausdrücklich als experimentell, -ohne vollständige Performance- oder Sicherheitsevaluation. - -Für reines Coding gibt es die separate Variante -[`Qwopus3.6-35B-A3B-Coder-MTP`](https://huggingface.co/Jackrong/Qwopus3.6-35B-A3B-Coder-MTP-GGUF) -(thinking-off, 62,4 % auf einem 300-Fall-SWE-Bench-Lauf) — lokal nicht vorhanden. - -## HauhauCS Uncensored (35B-A3B und 27B) — kreatives Schreiben - -Quellen: [35B-A3B](https://huggingface.co/HauhauCS/Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive), -[27B](https://huggingface.co/HauhauCS/Qwen3.6-27B-Uncensored-HauhauCS-Aggressive) - -**Abliterierte** Modelle: keine neuen Fähigkeiten, entfernte Verweigerungen -(Modellkarte: „0/465 Refusals"). Alle Quants mit imatrix erzeugt. Beide sind -multimodal. - -**Einsetzen für:** Prosa, Reden, Dialog, Rollenspiel — Themen, bei denen ein -aligntes Modell abbricht. -**Nicht einsetzen für:** Coding. Eigene Testläufe zeigen wiederkehrend Tests, die -inkonsistent zum selbst erzeugten Schema sind, erfundene Framework-Aussagen und -Benchmarks mit erfundenen Messwerten. - -Zwei Punkte, die in der aktuellen Konfiguration relevant sind: - -1. Die Karte des 27B empfiehlt selbst, dass **99,9 % der Nutzer die - `Balanced`-Variante** nehmen sollten statt `Aggressive`. Beide erreichen - dieselbe Refusal-Rate; `Balanced` bietet zusätzlich stabileres Sampling für - agentic coding, Tool-Use und Reasoning. `Aggressive` spart lediglich - Vorreden. Aktuell sind beide lokalen HauhauCS-Modelle `Aggressive`. -2. Die Behauptungen „lossless" und „zero capability loss" stammen vom Autor der - Modellkarte. Abliteration kostet in aller Regel *etwas* Fähigkeit. - -## Empirische Befunde (eigene Läufe) - -Getestet gegen das Default-Modell (HauhauCS 35B-A3B), Reasoning aktiv, ctx 262144: - -- **Längenvorgaben werden nicht präzise getroffen** (−27 % bis +26 % über sechs - Läufe). Länge als Spanne vorgeben oder im zweiten Turn nachjustieren. -- **Reasoning frisst das Antwortbudget.** Bei `--max-tokens 10000` endeten - schwere Aufgaben in `finish_reason=length`. Für Prosa/Reden/Dialog - `--max-tokens` ≥ 16000 wählen. -- **`--chat` ohne `--stream`** läuft bei langem Output in den Read-Timeout. - Immer `--stream` verwenden (jeder Token setzt den Timeout zurück). -- **Coding: Struktur gut, Korrektheit unzuverlässig.** Generierten Code und - generierte Tests immer wirklich ausführen. - -Rohdaten: `~/llamacppctl_prompt_tests/`. - -## Vision (mmproj) - -Die lokale `mmproj.gguf` (902 MB) ist der Vision-Encoder des **Basismodells -Qwen3.6-35B-A3B** (aus dem unsloth-Repo). GGUF-Metadaten: - -``` -general.architecture = clip -general.name = Qwen3.6 35B A3B -clip.projector_type = qwen3vl_merger -clip.has_vision_encoder -``` - -Ein mmproj ist kein eigenständiges Modell, sondern projiziert Bild-Embeddings in -den Textmodell-Raum. Er muss zum Vision-Tower der jeweiligen Basis passen. - -| Modell | mmproj.gguf passt? | -|---|---| -| HauhauCS 35B-A3B | ja (Basis Qwen3.6-35B-A3B) | -| Qwopus3.6-35B-A3B | ja (dieselbe Basis) | -| Carnice-Qwen3.6-MoE-35B-A3B | ja (dieselbe Basis) | -| `ornith-1.0-35b` | **nein** — Basis Qwen3.5-MoE, eigener Projektor nötig | -| HauhauCS 27B (dense) | **nein** — anderer Vision-Encoder | - -Verwendung über `llamacppctl` (siehe `[model.vision]` in `llama.cpp.config`): - -```bash -llamacppctl --start --config llama.cpp.config --profile vision -``` - -Das Profil setzt `mmproj = models/qwen3/mmproj.gguf`; der Pfad wird — wie -`model_path` — relativ zu `hf_home` aufgelöst und als `--mmproj` an den -llama.cpp-Server durchgereicht. Der Projektor wird standardmäßig auf die GPU -ausgelagert; `mmproj_offload = false` bzw. `--no-mmproj-offload` verhindert das, -wenn der VRAM knapp ist. - -Bilder gehen anschließend über den OpenAI-kompatiblen Endpunkt -`/v1/chat/completions` als `image_url`-Content-Part. `llamacppctl --chat` sendet -derzeit nur Text; für Bild-Requests den Endpunkt direkt ansprechen. - -## MTP / Speculative Decoding (nicht lokal vorhanden) - -Für Ornith, Carnice und Qwopus existieren **MTP-Quants** mit eingebettetem -Multi-Token-Prediction-Head. Mit hinreichend aktuellem llama.cpp erlauben sie -self-speculative Decoding ohne separates Draft-Modell (`--draft-mtp`); -Community-Berichte nennen etwa doppelten Decode-Durchsatz bei ~2,5 % größerer -Datei. Die im pyproject gepinnte Image-Version muss das unterstützen. - -## Quellen - -- -- -- -- -- -- -- -- diff --git a/docs/SECURITY_AND_OPERATIONS.md b/docs/SECURITY_AND_OPERATIONS.md index 254d7d6..4eaa144 100644 --- a/docs/SECURITY_AND_OPERATIONS.md +++ b/docs/SECURITY_AND_OPERATIONS.md @@ -258,29 +258,22 @@ keinen vermeidbaren Ausfall verursacht. ## 6. `--dry-run` und `--print-effective-config` als Sicherheitswerkzeuge -- `--print-effective-config` ist eine **eigene Aktion** in der - Mutually-Exclusive-Gruppe: sie gibt die vollständig aufgelöste Konfiguration - (Server- und Prompt-Konfiguration) als JSON aus und beendet sich. Damit lässt - sich prüfen, welche Werte aus welcher Quelle (Defaults/[default]/[model.*]/CLI) - tatsächlich gewonnen haben, ohne einen Container anzufassen. - - Bis einschließlich 0.1.0 war dies ein *Flag*. Da argparse immer eine Aktion - verlangt, führte `--print-effective-config --start` die Konfigurationsausgabe - **und anschließend einen echten `do_start()`** aus — der einen laufenden - Container stillschweigend ersetzte. Die Kombination ist jetzt ein - argparse-Fehler (Exit-Code 2). +- `--print-effective-config` gibt die vollständig aufgelöste Konfiguration + (Server- und Prompt-Konfiguration) als JSON aus, **bevor** irgendeine + Docker- oder HTTP-Aktion ausgeführt wird. Damit lässt sich prüfen, welche + Werte aus welcher Quelle (Defaults/[default]/[model.*]/CLI) tatsächlich + gewonnen haben, ohne einen Container anzufassen. - `--dry-run` (in Kombination mit `--start`/--change`) zeigt den vollständig zusammengesetzten `docker run`-Befehl (Shell-quotiert zur Anzeige) an, **ohne** ihn auszuführen. Empfohlen vor jeder Änderung an einer Produktionskonfiguration, insbesondere nach Anpassungen an `llama.cpp.config`. -`--print-effective-config` erfordert **keinen** erreichbaren Docker-Daemon; -`main.run()` überspringt die `docker_available()`-Prüfung für diese Aktion. Für -`--dry-run` gilt das **nicht**: es ist ein Modifikator von `--start`/`--change`, -und die Docker-Prüfung läuft vor der Konfigurationsauflösung. Auf einem Host ganz -ohne Docker schlägt `--start --dry-run` daher mit einer expliziten Fehlermeldung -fehl, statt still falsche Annahmen zu treffen. +Beide Flags erfordern **keinen** erreichbaren Docker-Daemon für ihre reine +Ausgabe — `main.run()` prüft `docker_available()` derzeit vor der +Konfigurationsauflösung; auf einem Host ganz ohne Docker (z. B. zur reinen +Konfigurationsvalidierung) schlägt der Aufruf entsprechend mit einer +expliziten Fehlermeldung fehl statt still falsche Annahmen zu treffen. ## 7. Betriebsmodell auf einem dedizierten GPU-Host diff --git a/llama.cpp.config.example b/llama.cpp.config.example index 4656f45..b411535 100644 --- a/llama.cpp.config.example +++ b/llama.cpp.config.example @@ -88,24 +88,6 @@ host_port = 8003 gpu_device = 1 ctx_size = 131072 -# Vision profile: `mmproj` points at the multimodal projector (vision encoder) -# GGUF. Like `model_path` it is resolved relative to `hf_home` unless absolute. -# The projector MUST match the base model's vision tower -- a projector built for -# a different base will not load. A missing projector aborts with exit code 3 -# (for --change: before the running container is removed). -# -# llama.cpp offloads the projector to the GPU by default; set mmproj_offload to -# false to keep it on the CPU when VRAM is tight. Images are then sent to -# /v1/chat/completions as an image_url content part -- `llamacppctl --chat` -# itself only sends text. -[model.vision] -model_path = qwen3/Qwen3-35B-A3B-Q4_K_M.gguf -mmproj = qwen3/mmproj.gguf -mmproj_offload = true -container_name = llama_cpp_vision -host_port = 8004 -gpu_device = 1 - [prompt.concise] system_prompt = Du antwortest kurz, präzise und technisch. diff --git a/scripts/check.sh b/scripts/check.sh index 9773ca7..a7e59c7 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -27,7 +27,7 @@ run() { fi } -run "ruff (lint)" "${BIN}ruff" check src/ tests/ build_archive.py scripts/eval_prompt_tests.py +run "ruff (lint)" "${BIN}ruff" check src/ tests/ build_archive.py run "mypy (types)" "${BIN}mypy" # Use `python -m pytest` (not the pytest console script) so the repo root is on # sys.path — the test modules import `from tests.test_docker_ops import ...`. diff --git a/scripts/eval_prompt_tests.py b/scripts/eval_prompt_tests.py deleted file mode 100755 index 290db3d..0000000 --- a/scripts/eval_prompt_tests.py +++ /dev/null @@ -1,457 +0,0 @@ -#!/usr/bin/env python3 -"""eval_prompt_tests.py - -Auswertung der manuellen Modell-Prompt-Testlaeufe unter -``~/llamacppctl_prompt_tests/`` gegen die Prompts in ``example_user_prompts/``. - -Misst die objektive Ebene der Rubrik (docs/EVAL_RUBRIC.md): - - * prosa/reden: Wortzahl und Abweichung vom im Prompt genannten Zielwert, - Trunkierungsverdacht. - * coding: extrahiert die Python-Codebloecke, schreibt Modul und Tests in - ein temporaeres Verzeichnis und **fuehrt pytest tatsaechlich aus**. - -WARNUNG: ``--exec`` fuehrt modellgenerierten Code aus. Der Runner nutzt ein -temporaeres Verzeichnis, ein Zeitlimit und verweigert den Start als root, bietet -aber keine Netz- oder Dateisystem-Isolation. Siehe docs/EVAL_RUBRIC.md. - -Beispiele ---------- - scripts/eval_prompt_tests.py # messen + Tests ausfuehren - scripts/eval_prompt_tests.py --no-exec # nur statisch messen - scripts/eval_prompt_tests.py --json report.json # maschinenlesbar - scripts/eval_prompt_tests.py --anonymize blind/ # blinde Bewertung vorbereiten - -Exit-Codes ----------- - 0 Auswertung durchgelaufen (auch wenn einzelne Testlaeufe rot sind) - 1 Eingabeverzeichnis fehlt oder unsichere Ausfuehrungsumgebung -""" - -from __future__ import annotations - -import argparse -import json -import os -import random -import re -import shutil -import subprocess -import sys -import tempfile -from dataclasses import asdict, dataclass, field -from pathlib import Path -from typing import Optional - -DEFAULT_RESULTS_DIR = Path.home() / "llamacppctl_prompt_tests" -DEFAULT_PROMPTS_DIR = Path(__file__).resolve().parents[1] / "example_user_prompts" - -DOMAINS = ("prosa", "reden", "coding") -# Dateien ohne Modellpraefix stammen vom Default-Modell aus llama.cpp.config. -KNOWN_MODELS = ("carnice", "qwen27b", "qwopus", "ornith", "qwen35base") -DEFAULT_MODEL_LABEL = "default" -VARIANT_MARKERS = frozenset({"r2", "highbudget", "explizit"}) - -# "etwa 1200 Wörtern", "ca. 700 Wörter", "rund 900 Worten" -LENGTH_TARGET_RE = re.compile( - r"(?:etwa|ca\.|circa|rund)\s*(\d{2,5})\s*(?:Wörter|Wörtern|Worte|Worten)", - re.IGNORECASE, -) -FENCE_RE = re.compile(r"^```([A-Za-z+#]*)\s*$(.*?)^```\s*$", re.MULTILINE | re.DOTALL) -PY_FILENAME_RE = re.compile(r"`([A-Za-z_][A-Za-z0-9_]*)\.py`") -FROM_IMPORT_RE = re.compile(r"^\s*from\s+([A-Za-z_][A-Za-z0-9_]*)\s+import", re.MULTILINE) -PLAIN_IMPORT_RE = re.compile(r"^\s*import\s+([A-Za-z_][A-Za-z0-9_]*)", re.MULTILINE) -PYTEST_COUNT_RE = re.compile(r"(\d+)\s+(passed|failed|error|errors)") - -# Endet der Text sauber? Fehlendes Satzzeichen deutet auf finish_reason=length. -SENTENCE_END = tuple('.!?"»“’—-)') - -# Ein Modul darf niemals nach einem Stdlib-Modul benannt werden: eine Datei -# sqlite3.py im Arbeitsverzeichnis ueberschattet die echte Stdlib und laesst den -# Testlauf mit einem Importfehler scheitern, der faelschlich dem Modell -# angelastet wuerde. sys.stdlib_module_names ist exakt (Python >= 3.10). -RESERVED_MODULE_NAMES = frozenset(sys.stdlib_module_names) | {"pytest", "conftest"} - - -@dataclass -class Run: - """Ein einzelner Testlauf, abgeleitet aus dem Dateinamen.""" - - path: Path - model: str - domain: str - case: str - variant: str = "" - - @property - def label(self) -> str: - base = f"{self.model}/{self.domain}_{self.case}" - return f"{base}[{self.variant}]" if self.variant else base - - -@dataclass -class Metrics: - label: str - model: str - domain: str - case: str - variant: str - words: int = 0 - target_words: Optional[int] = None - deviation_pct: Optional[float] = None - looks_truncated: bool = False - # coding - executed: bool = False - exec_status: str = "" # ok | failed | timeout | no-tests | skipped- - tests_passed: int = 0 - tests_failed: int = 0 - detail: str = "" - code_langs: list = field(default_factory=list) - - -def parse_run(path: Path) -> Optional[Run]: - """Zerlegt ``[_]_[_].out.txt``. - - Die Namenskonvention ist historisch uneinheitlich (mal steht der Fallname - hinter der Nummer, mal eine Variante wie ``r2``), deshalb wird tolerant - geparst statt streng validiert. - """ - stem = path.name - for suffix in (".out.txt", ".txt", ".md"): - if stem.endswith(suffix): - stem = stem[: -len(suffix)] - break - - tokens = stem.split("_") - if not tokens: - return None - - model = DEFAULT_MODEL_LABEL - if tokens[0] in KNOWN_MODELS: - model = tokens[0] - tokens = tokens[1:] - - domain = next((t for t in tokens if t in DOMAINS), None) - if domain is None: - return None - rest = tokens[tokens.index(domain) + 1 :] - - case = next((t for t in rest if t.isdigit()), None) - if case is None: - return None - - # Der Fallname (z. B. "jsonl_validator") ist keine Variante. Nur bekannte - # Marker zaehlen -- sie muessen erhalten bleiben, sonst kollidieren zwei - # verschiedene Laeufe (prosa_04_dialog vs. prosa_04_dialog_highbudget) unter - # demselben Label. - variant = "_".join(t for t in rest[rest.index(case) + 1 :] if t in VARIANT_MARKERS) - - return Run(path=path, model=model, domain=domain, case=case, variant=variant) - - -def load_length_targets(prompts_dir: Path) -> dict: - """Mappt (domaene, nr) auf die im Prompt genannte Ziel-Wortzahl.""" - targets: dict = {} - if not prompts_dir.is_dir(): - return targets - for prompt in sorted(prompts_dir.glob("*.md")): - tokens = prompt.stem.split("_") - if len(tokens) < 2 or tokens[0] not in DOMAINS or not tokens[1].isdigit(): - continue - match = LENGTH_TARGET_RE.search(prompt.read_text(encoding="utf-8")) - if match: - targets[(tokens[0], tokens[1])] = int(match.group(1)) - return targets - - -def count_words(text: str) -> int: - """Woerter ohne Markdown-Auszeichnung und ohne Codebloecke.""" - without_code = FENCE_RE.sub("", text) - cleaned = re.sub(r"[#*_>`]", " ", without_code) - return len(cleaned.split()) - - -def looks_truncated(text: str) -> bool: - stripped = text.strip() - if not stripped: - return True - return not stripped.endswith(SENTENCE_END) - - -def extract_code_blocks(text: str) -> list: - """Liefert (sprache, code, start_offset) je Fence-Block. - - Der Offset wird gebraucht, um den im Fliesstext *vor* einem Block genannten - Dateinamen zu finden; ein Textvergleich waere mehrdeutig, wenn derselbe Code - mehrfach vorkommt. - """ - return [ - (match.group(1).lower(), match.group(2), match.start()) - for match in FENCE_RE.finditer(text) - ] - - -def is_test_block(code: str) -> bool: - return "def test_" in code or "import pytest" in code - - -def _usable(name: str) -> bool: - return bool(name) and name not in RESERVED_MODULE_NAMES and not name.startswith("test_") - - -def infer_module_name(markdown: str, block_start: int, tests: list) -> str: - """Ermittelt den Dateinamen des Moduls. - - Reihenfolge: (1) letzte im Fliesstext *vor* dem Block genannte ``*.py``-Datei, - (2) ``from X import`` in den Tests -- die Tests importieren das Modul unter - genau diesem Namen, (3) ``import X``. Stdlib-Namen und ``test_*`` werden in - jeder Stufe verworfen. - """ - for name in reversed(PY_FILENAME_RE.findall(markdown[:block_start])): - if _usable(name): - return name - - for pattern in (FROM_IMPORT_RE, PLAIN_IMPORT_RE): - for test_code in tests: - for candidate in pattern.findall(test_code): - if _usable(candidate): - return candidate - - return "generated_module" - - -def _parse_pytest_counts(output: str) -> tuple: - passed = failed = 0 - for count, kind in PYTEST_COUNT_RE.findall(output): - if kind == "passed": - passed = int(count) - else: - failed += int(count) - return passed, failed - - -def run_python_case(markdown: str, timeout: int, keep_dir: Optional[Path]) -> dict: - """Schreibt Modul + Tests in ein Temp-Verzeichnis und ruft pytest auf.""" - blocks = extract_code_blocks(markdown) - py_blocks = [(body, start) for lang, body, start in blocks if lang in ("python", "py")] - if not py_blocks: - return {"exec_status": "no-code", "detail": "kein Python-Block gefunden"} - - tests = [body for body, _ in py_blocks if is_test_block(body)] - modules = [(body, start) for body, start in py_blocks if not is_test_block(body)] - if not tests: - return {"exec_status": "no-tests", "detail": f"{len(modules)} Modul-Block(loecke), keine Tests"} - - workdir = Path(tempfile.mkdtemp(prefix="llamaeval_")) - warnings = [] - try: - used: set = set() - for module_body, block_start in modules: - name = infer_module_name(markdown, block_start, tests) - if name == "generated_module": - # Weder der Fliesstext nennt einen Dateinamen noch importieren die - # Tests das Modul: ein echter Mangel der Testqualitaet, kein - # Ratefehler des Runners. - warnings.append("Tests importieren das Modul nicht") - while name in used: # zwei Modul-Bloecke, gleicher geratener Name - name += "_x" - used.add(name) - (workdir / f"{name}.py").write_text(module_body, encoding="utf-8") - for index, test_body in enumerate(tests): - suffix = "" if index == 0 else f"_{index}" - (workdir / f"test_generated{suffix}.py").write_text(test_body, encoding="utf-8") - - try: - proc = subprocess.run( - [sys.executable, "-m", "pytest", "-q", "--tb=no", "-p", "no:cacheprovider"], - cwd=workdir, - capture_output=True, - text=True, - timeout=timeout, - ) - except subprocess.TimeoutExpired: - return {"exec_status": "timeout", "detail": f"pytest > {timeout}s"} - except FileNotFoundError: - return {"exec_status": "skipped-no-pytest", "detail": "pytest nicht gefunden"} - - output = proc.stdout + proc.stderr - passed, failed = _parse_pytest_counts(output) - status = "ok" if proc.returncode == 0 else "failed" - last = [ln for ln in output.strip().splitlines() if ln.strip()] - detail = "; ".join(warnings + ([last[-1]] if last else [])) - return { - "exec_status": status, - "tests_passed": passed, - "tests_failed": failed, - "detail": detail[:120], - } - finally: - if keep_dir is not None: - shutil.move(str(workdir), str(keep_dir / workdir.name)) - else: - shutil.rmtree(workdir, ignore_errors=True) - - -def evaluate(run: Run, targets: dict, do_exec: bool, timeout: int, keep_dir) -> Metrics: - text = run.path.read_text(encoding="utf-8", errors="replace") - metrics = Metrics( - label=run.label, - model=run.model, - domain=run.domain, - case=run.case, - variant=run.variant, - words=count_words(text), - looks_truncated=looks_truncated(text), - code_langs=sorted({lang for lang, _, _ in extract_code_blocks(text) if lang}), - ) - - target = targets.get((run.domain, run.case)) - if target: - metrics.target_words = target - metrics.deviation_pct = round((metrics.words - target) / target * 100, 1) - - if run.domain != "coding": - return metrics - - if not do_exec: - metrics.exec_status = "skipped-no-exec" - return metrics - if "typescript" in metrics.code_langs and "python" not in metrics.code_langs: - metrics.exec_status = "skipped-typescript" - metrics.detail = "kein tsc im PATH; TypeScript-Lauf nicht implementiert" - return metrics - - result = run_python_case(text, timeout, keep_dir) - metrics.executed = result["exec_status"] in ("ok", "failed") - metrics.exec_status = result["exec_status"] - metrics.tests_passed = result.get("tests_passed", 0) - metrics.tests_failed = result.get("tests_failed", 0) - metrics.detail = result.get("detail", "") - return metrics - - -def anonymize(runs: list, out_dir: Path) -> Path: - """Kopiert die Laeufe unter Zufalls-IDs und schreibt die Aufloesungstabelle. - - Fuer blinde Bewertung: der Bewertende darf das Modell nicht kennen. - """ - out_dir.mkdir(parents=True, exist_ok=True) - ids = [f"text_{i:03d}" for i in range(len(runs))] - random.shuffle(ids) - mapping = {} - for run, blind_id in zip(runs, ids): - shutil.copyfile(run.path, out_dir / f"{blind_id}.txt") - mapping[blind_id] = {"model": run.model, "domain": run.domain, "case": run.case, - "variant": run.variant, "source": run.path.name} - key_path = out_dir / "AUFLOESUNG.json" - key_path.write_text(json.dumps(mapping, indent=2, ensure_ascii=False), encoding="utf-8") - return key_path - - -def _fmt_dev(metrics: Metrics) -> str: - if metrics.deviation_pct is None: - return "—" - return f"{metrics.deviation_pct:+.1f}%" - - -def print_report(all_metrics: list) -> None: - for domain in DOMAINS: - rows = [m for m in all_metrics if m.domain == domain] - if not rows: - continue - print(f"\n=== {domain} ===") - if domain == "coding": - print(f"{'Lauf':<28} {'Status':<18} {'passed':>6} {'failed':>6} Detail") - for m in sorted(rows, key=lambda r: (r.model, r.case)): - print(f"{m.label:<28} {m.exec_status:<18} {m.tests_passed:>6} " - f"{m.tests_failed:>6} {m.detail[:60]}") - else: - print(f"{'Lauf':<28} {'Wörter':>7} {'Ziel':>6} {'Abw.':>8} {'trunkiert':<9}") - for m in sorted(rows, key=lambda r: (r.model, r.case)): - target = str(m.target_words) if m.target_words else "—" - trunc = "ja" if m.looks_truncated else "" - print(f"{m.label:<28} {m.words:>7} {target:>6} {_fmt_dev(m):>8} {trunc:<9}") - - coding = [m for m in all_metrics if m.domain == "coding" and m.executed] - if coding: - green = sum(1 for m in coding if m.exec_status == "ok") - total_p = sum(m.tests_passed for m in coding) - total_f = sum(m.tests_failed for m in coding) - print(f"\nCoding gesamt: {green}/{len(coding)} Läufe grün, " - f"{total_p} Tests bestanden, {total_f} durchgefallen.") - - lengths = [m for m in all_metrics if m.deviation_pct is not None] - if lengths: - within5 = sum(1 for m in lengths if abs(m.deviation_pct) <= 5) - worst = max(lengths, key=lambda m: abs(m.deviation_pct)) - print(f"Längentreue: {within5}/{len(lengths)} Läufe im ±5%-Fenster; " - f"größte Abweichung {_fmt_dev(worst)} ({worst.label}).") - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__.splitlines()[1] if __doc__ else "") - parser.add_argument("--results-dir", type=Path, default=DEFAULT_RESULTS_DIR) - parser.add_argument("--prompts-dir", type=Path, default=DEFAULT_PROMPTS_DIR) - parser.add_argument("--no-exec", dest="exec_code", action="store_false", default=True, - help="Generierten Code NICHT ausführen, nur statisch messen") - parser.add_argument("--timeout", type=int, default=60, help="Sekunden pro pytest-Lauf") - parser.add_argument("--json", type=Path, help="Report zusätzlich als JSON schreiben") - parser.add_argument("--keep-workdirs", type=Path, - help="Temp-Verzeichnisse der Testläufe hierhin retten (Debugging)") - parser.add_argument("--anonymize", type=Path, - help="Anonymisierte Kopien + Auflösungstabelle für blinde Bewertung") - args = parser.parse_args() - - if not args.results_dir.is_dir(): - print(f"Ergebnisverzeichnis nicht gefunden: {args.results_dir}", file=sys.stderr) - return 1 - - if args.exec_code and hasattr(os, "geteuid") and os.geteuid() == 0: - print("Verweigert: modellgenerierten Code nicht als root ausführen " - "(--no-exec erzwingt die statische Auswertung).", file=sys.stderr) - return 1 - - runs = [] - skipped = [] - for path in sorted(args.results_dir.iterdir()): - if not path.is_file(): - continue - run = parse_run(path) - (runs.append(run) if run else skipped.append(path.name)) - - if not runs: - print(f"Keine auswertbaren Läufe in {args.results_dir}", file=sys.stderr) - return 1 - - if args.anonymize: - key = anonymize(runs, args.anonymize) - print(f"{len(runs)} Texte anonymisiert nach {args.anonymize}; Auflösung: {key}") - - if args.keep_workdirs: - args.keep_workdirs.mkdir(parents=True, exist_ok=True) - - targets = load_length_targets(args.prompts_dir) - if not targets: - print(f"Warnung: keine Ziel-Wortzahlen aus {args.prompts_dir} gelesen", file=sys.stderr) - - all_metrics = [ - evaluate(run, targets, args.exec_code, args.timeout, args.keep_workdirs) - for run in runs - ] - - print_report(all_metrics) - if skipped: - print(f"\nÜbersprungen ({len(skipped)}): {', '.join(skipped)}") - - if args.json: - args.json.write_text( - json.dumps([asdict(m) for m in all_metrics], indent=2, ensure_ascii=False), - encoding="utf-8", - ) - print(f"JSON-Report: {args.json}") - - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/scripts/run_prompt_suite.sh b/scripts/run_prompt_suite.sh deleted file mode 100755 index bc73570..0000000 --- a/scripts/run_prompt_suite.sh +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env bash -# -# Faehrt eine Domaene der Beispiel-Prompts gegen ein laufendes Modell-Profil und -# legt die Ausgaben unter der etablierten Namenskonvention im Test-Archiv ab: -# -#