Compare commits

98 Commits

Author SHA1 Message Date
team3
9e888c216a update 2026-06-23 21:44:28 +02:00
team3
6a5a1cccd9 update 2026-06-23 19:26:38 +02:00
team3
cec8d1ee5f update 2026-06-23 16:23:12 +02:00
team3
cd1c926599 update 2026-06-23 12:04:17 +02:00
team3
9a02e8dc76 update 2026-06-23 11:44:56 +02:00
team3
0eaa72c536 update 2026-06-23 11:20:44 +02:00
team3
c07e1e9e30 update 2026-06-23 11:12:31 +02:00
team3
8fe0e8581f update 2026-06-23 09:58:43 +02:00
team3
28d0b494cd update 2026-06-22 22:21:23 +02:00
team3
b3b5dbf37d update 2026-06-22 17:43:31 +02:00
team3
c5543c0529 update 2026-06-22 16:30:39 +02:00
team3
36b7aabdb0 update 2026-06-22 15:23:39 +02:00
team3
0951337a29 update 2026-06-22 14:26:07 +02:00
team3
bd1de0770a update 2026-06-22 14:13:12 +02:00
team3
613a4a2b1f update 2026-06-22 13:44:33 +02:00
team3
0adc223c0a update 2026-06-22 12:55:52 +02:00
team3
04900eef55 update 2026-06-22 12:24:44 +02:00
team3
2d1a3d3e37 update 2026-06-22 11:36:42 +02:00
team3
4f1f8d18d0 update 2026-06-22 10:13:30 +02:00
team3
b54f5e23f3 update 2026-06-22 06:07:22 +02:00
team3
bf9eea967b update 2026-06-21 23:01:14 +02:00
team3
2e33fa5c47 update 2026-06-21 22:53:54 +02:00
team3
b4c686f74f update 2026-06-21 22:06:02 +02:00
team3
50b1c81bca update 2026-06-21 20:02:06 +02:00
team3
8ba4b94498 update 2026-06-21 20:00:13 +02:00
team3
885c4811d0 update 2026-06-21 19:26:01 +02:00
team3
befd509c08 update 2026-06-21 18:32:55 +02:00
team3
3965bc5bf9 update 2026-06-21 17:27:10 +02:00
team3
920046b774 update 2026-06-21 16:30:54 +02:00
team3
89a853981d update 2026-06-21 16:22:13 +02:00
team3
76ae2161e1 update 2026-06-21 15:46:23 +02:00
team3
1910f730d2 update 2026-06-21 15:38:10 +02:00
team3
20831e1399 update 2026-06-21 13:27:19 +02:00
team3
ba529e61a8 update 2026-06-19 14:13:22 +02:00
team3
0868a768a2 update 2026-06-19 14:04:19 +02:00
team3
26e097dc5a update 2026-06-19 12:59:13 +02:00
team3
3168ec9094 update 2026-06-19 09:51:47 +02:00
team3
1fba9ba898 update 2026-06-19 09:06:45 +02:00
team3
bf6ab18ffd update 2026-06-18 20:24:24 +02:00
root
d80b22d53e update 2026-06-18 15:28:14 +00:00
team3
f24054ba56 update 2026-06-18 17:22:03 +02:00
team3
30b28290ca update 2026-06-18 17:19:08 +02:00
team3
a17ea155b8 update 2026-06-18 17:11:24 +02:00
team3
ed006976d9 update 2026-06-18 16:51:38 +02:00
team3
d067cb8b54 update 2026-06-18 16:45:33 +02:00
team3
9c0f622e0c update 2026-06-18 14:30:21 +02:00
team3
d8932c90d6 update 2026-06-18 12:50:01 +02:00
team3
3df5976d3d update 2026-06-18 12:22:34 +02:00
team3
b8f08ed71b update 2026-06-18 12:06:22 +02:00
team3
af6fb7a9c2 update 2026-06-18 11:54:49 +02:00
team3
59d108a13a update 2026-06-18 11:46:09 +02:00
team3
993552efe5 update 2026-06-18 11:37:17 +02:00
team3
9f7553edbc update 2026-06-18 11:25:48 +02:00
team3
4b0cf1ef29 update 2026-06-18 09:58:45 +02:00
team3
1aba312242 update 2026-06-18 09:42:29 +02:00
team3
229880b7e6 update 2026-06-18 00:00:24 +02:00
team3
3c839861e7 update 2026-06-17 23:17:52 +02:00
team3
dce9156ac8 update 2026-06-17 22:56:24 +02:00
team3
0ff33271a0 update 2026-06-17 19:59:06 +02:00
team3
c487e9cfcd update 2026-06-17 18:05:25 +02:00
team3
2f702477c7 update 2026-06-17 17:40:05 +02:00
team3
18b0a32f1e update 2026-06-17 17:02:44 +02:00
team3
d81bba9644 update 2026-06-17 17:01:41 +02:00
team3
cc68bdd242 update 2026-06-17 17:01:11 +02:00
team3
229ed93c5d update 2026-06-17 16:50:28 +02:00
team3
61a22a4d9f update 2026-06-17 16:41:04 +02:00
team3
76a405bab9 update 2026-06-17 15:31:56 +02:00
team3
7a524ddb99 update 2026-06-17 15:25:24 +02:00
team3
b3097a6143 update 2026-06-17 15:08:44 +02:00
team3
a503285b6f update 2026-06-17 14:25:05 +02:00
team3
5268c77fde update 2026-06-17 13:38:38 +02:00
team3
f30c7a6456 update 2026-06-17 13:30:09 +02:00
team3
0a96705453 update 2026-06-17 12:55:00 +02:00
team3
5fea458d66 update 2026-06-17 12:32:47 +02:00
team3
390ec12bbf update 2026-06-17 11:54:45 +02:00
team3
e8214598ea update 2026-06-17 11:21:12 +02:00
team3
d6b748cb94 update 2026-06-17 11:09:28 +02:00
team3
c00580c261 update 2026-06-15 18:51:50 +02:00
team3
b592944497 update 2026-06-15 18:27:08 +02:00
team3
466818c47c update 2026-06-15 17:02:53 +02:00
team3
19b520a3b1 update 2026-06-15 16:09:38 +02:00
team3
44e04315cc update 2026-06-15 15:43:32 +02:00
team3
47be019f02 update 2026-06-15 15:19:40 +02:00
team3
a6d249acf6 update 2026-06-15 12:35:09 +02:00
team3
003f6d3b8e update 2026-06-15 08:59:06 +02:00
team3
33a4440404 update 2026-06-15 08:46:28 +02:00
team3
25a07ede4d update 2026-06-14 22:53:10 +02:00
team3
08dd0ccd69 update 2026-06-14 22:43:54 +02:00
team3
6e5d673ca7 update 2026-06-14 22:09:48 +02:00
team3
54adcdc50c update 2026-06-14 17:57:40 +02:00
team3
77fd6156f6 update 2026-06-14 15:18:56 +02:00
team3
143e6d6f7c update 2026-06-14 14:55:44 +02:00
team3
2b89e21cd3 update 2026-06-14 14:02:27 +02:00
team3
822f6ee3e9 update 2026-06-14 12:24:49 +02:00
team3
8382d6f27a update 2026-06-12 17:53:49 +02:00
team3
a7fd345bb6 update 2026-06-12 17:46:30 +02:00
team3
0ba708dc54 update 2026-06-12 17:22:12 +02:00
team3
78d5833fe4 update 2026-06-12 17:18:42 +02:00
97 changed files with 30222 additions and 1518 deletions

View File

@@ -23,7 +23,15 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
RUN useradd -m -u 1000 app
COPY backend/requirements.txt /app/backend/requirements.txt
RUN pip install --no-cache-dir -r /app/backend/requirements.txt
# torch als CPU-Build (~190 MB) — eigener Index, sonst zieht pip die CUDA-Variante (~2,5 GB).
# Fürs Lesbarkeits-Gate (transformers nutzt das vorhandene torch).
RUN pip install --no-cache-dir torch --index-url https://download.pytorch.org/whl/cpu \
&& pip install --no-cache-dir -r /app/backend/requirements.txt
# Chromium + OS-Libs für Playwright (als root) in ein gemeinsames, welt-lesbares Verzeichnis.
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
RUN python3 -m playwright install --with-deps chromium \
&& chmod -R a+rX /ms-playwright
COPY --chown=app:app backend/ /app/backend/
COPY --chown=app:app templates/ /app/templates/

View File

@@ -1,10 +1,10 @@
.PHONY: install dev prod stop logs remove auth sync sync-projects projects searxng ollama
.PHONY: install dev prod stop logs remove auth sync sync-projects sync-all projects searxng ollama
COMPOSE = docker compose
auth:
@mkdir -p .claude-data storage projects
@chown -R 1000:1000 .claude-data storage projects
@mkdir -p .claude-data storage projects uni
@chown -R 1000:1000 .claude-data storage projects uni
@grep -q "^CLAUDE_CODE_OAUTH_TOKEN=.\+" .env 2>/dev/null \
|| echo "WARNUNG: CLAUDE_CODE_OAUTH_TOKEN fehlt in .env (Claude-Provider inaktiv)."
@grep -q "^MINIMAX_API_KEY=.\+" .env 2>/dev/null \
@@ -12,7 +12,10 @@ auth:
@echo "Verzeichnisse angelegt und auf uid 1000 chowned."
install:
pip install --break-system-packages fastapi uvicorn[standard] aiosqlite uv
pip install --break-system-packages fastapi uvicorn[standard] aiosqlite uv playwright transformers
pip install --break-system-packages torch --index-url https://download.pytorch.org/whl/cpu
python3 -m playwright install chromium
@echo "Falls Chromium OS-Libs fehlen: 'sudo python3 -m playwright install-deps chromium' einmalig ausführen."
@which pdftotext >/dev/null 2>&1 || sudo apt-get install -y poppler-utils
cd frontend && npm install
npm install -g opencode-ai
@@ -58,17 +61,22 @@ ollama:
ollama pull qwen3.5:9b
@echo "Ollama bereit — Provider 'Lokal' ist aktiv (Modelle anpassen: backend/config.py + dev-ops/opencode.json)."
sync:
sync: stop
@mkdir -p storage/themen
@rm -f storage/creator.db-shm storage/creator.db-wal
rsync -avz --progress root@178.104.67.87:/var/www/creator/storage/creator.db storage/
rsync -avz --progress root@178.104.67.87:/var/www/creator/storage/creator.db-wal storage/
rsync -avz --progress --delete root@178.104.67.87:/var/www/creator/storage/themen/ storage/themen/
@echo "Sync abgeschlossen."
# Projekte vom Server holen: `make sync-projects` (auch: `make sync projects`).
# Ohne --delete — lokale Projekte, die es auf dem Server nicht gibt, bleiben.
# Mit --delete — exakter Spiegel: lokale Projekte ohne Server-Pendant werden gelöscht.
sync-projects:
@mkdir -p projects
rsync -avz --progress root@178.104.67.87:/var/www/creator/projects/ projects/
rsync -avz --progress --delete root@178.104.67.87:/var/www/creator/projects/ projects/
@echo "Projekt-Sync abgeschlossen."
sync-all: sync sync-projects
@echo "Voller Sync (Themen + Projekte) abgeschlossen."
projects: sync-projects

View File

@@ -9,6 +9,7 @@ import logging
import os
import re
import shutil
import signal
import tempfile
import time
import urllib.request
@@ -20,12 +21,35 @@ log = logging.getLogger("creator.agents")
_active_processes: dict[str, asyncio.subprocess.Process] = {}
# Abgebrochene Scopes (Schlüssel-Präfixe, symmetrisch zu kill_process). Ein Agent, dessen
# Key mit einem dieser Präfixe beginnt, bricht VOR dem Spawn ab — so werden auch in der
# Semaphore-Schlange WARTENDE Agenten beim Abbruch sofort gestoppt, statt noch zu starten.
_cancelled_prefixes: set[str] = set()
def cancel_scope(prefix: str) -> None:
_cancelled_prefixes.add(prefix)
def clear_scope(prefix: str) -> None:
_cancelled_prefixes.discard(prefix)
def _scope_cancelled(agent_key: str) -> bool:
return any(agent_key.startswith(p) for p in _cancelled_prefixes)
# Deckelt die realen CLI-Prozesse — unabhängig von der Pipeline-Semaphore in
# generator.py. Acquire passiert VOR dem Spawn, damit Wartezeit in der Queue
# nicht gegen den Agent-Timeout zählt.
_batch_sem = asyncio.Semaphore(MAX_CONCURRENT_AGENTS)
_interactive_sem = asyncio.Semaphore(MAX_CONCURRENT_INTERACTIVE)
# OpenCode-Starts serialisieren: gleichzeitig startende Prozesse kollidieren an
# der internen Session-DB ("database is locked", Exit nach <1s). Der kurze
# Versatz entzerrt die Starts; danach laufen die Prozesse normal parallel.
_opencode_start_lock = asyncio.Lock()
_OPENCODE_START_DELAY = 1.0
# Capability → Claude --allowedTools
_CLAUDE_TOOLS = {
"full": "Write,Bash,Read,WebSearch,WebFetch",
@@ -61,6 +85,18 @@ def provider_available(provider: str) -> bool:
return True
def _kill(process) -> None:
"""Killt den Agenten samt Kindprozessen über die Prozess-Gruppe (sonst überleben die
von der CLI gestarteten Kinder, halten die Pipes offen und blockieren communicate())."""
try:
os.killpg(os.getpgid(process.pid), signal.SIGKILL)
except (ProcessLookupError, PermissionError):
try:
process.kill()
except ProcessLookupError:
pass
def kill_process(agent_key_prefix: str) -> None:
"""Killt alle aktiven Prozesse, deren Key mit dem Prefix beginnt (deckt -plan/-w1… ab)."""
for key, process in list(_active_processes.items()):
@@ -69,7 +105,7 @@ def kill_process(agent_key_prefix: str) -> None:
continue
if key.startswith(agent_key_prefix):
log.debug("kill agent %s", key)
process.kill()
_kill(process)
async def run_agent(
@@ -81,25 +117,39 @@ async def run_agent(
capabilities: str = "none",
lane: str = "batch",
) -> tuple[int, str, str]:
if _scope_cancelled(agent_key): # vor dem Anstehen: gar nicht erst in die Schlange
return 1, "", "abgebrochen"
if provider not in PROVIDERS:
return 1, "", f"Unbekannter Provider: {provider}"
if shutil.which(PROVIDERS[provider]["cli"]) is None:
return 1, "", f"CLI '{PROVIDERS[provider]['cli']}' nicht installiert (Provider: {provider})"
sem = _interactive_sem if lane == "interactive" else _batch_sem
async with sem:
if _scope_cancelled(agent_key): # nach dem Acquire: in der Schlange abgebrochen → kein Spawn
return 1, "", "abgebrochen"
if PROVIDERS[provider]["cli"] == "opencode":
return await _run_opencode(agent_key, prompt, timeout, provider, role, capabilities)
return await _run_claude_cli(agent_key, prompt, timeout, role, capabilities)
async def _communicate(agent_key: str, cmd: list[str], stdin_data: bytes | None, timeout: int) -> tuple[int, str, str]:
async def _communicate(agent_key: str, cmd: list[str], stdin_data: bytes | None, timeout: int, stagger: bool = False) -> tuple[int, str, str]:
start = time.monotonic()
process = await asyncio.create_subprocess_exec(
*cmd,
stdin=asyncio.subprocess.PIPE if stdin_data is not None else asyncio.subprocess.DEVNULL,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
async def spawn():
return await asyncio.create_subprocess_exec(
*cmd,
stdin=asyncio.subprocess.PIPE if stdin_data is not None else asyncio.subprocess.DEVNULL,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
start_new_session=True, # eigene Prozess-Gruppe → killpg killt auch Kindprozesse
)
if stagger:
async with _opencode_start_lock:
process = await spawn()
await asyncio.sleep(_OPENCODE_START_DELAY)
else:
process = await spawn()
_active_processes[agent_key] = process
try:
try:
@@ -108,7 +158,7 @@ async def _communicate(agent_key: str, cmd: list[str], stdin_data: bytes | None,
timeout=timeout,
)
except asyncio.TimeoutError:
process.kill()
_kill(process)
try:
await asyncio.wait_for(process.wait(), timeout=5)
except asyncio.TimeoutError:
@@ -154,7 +204,7 @@ async def _run_opencode(agent_key: str, prompt: str, timeout: int, provider: str
"-f", str(prompt_path),
]
try:
rc, stdout, stderr = await _communicate(agent_key, cmd, None, timeout)
rc, stdout, stderr = await _communicate(agent_key, cmd, None, timeout, stagger=True)
return rc, _clean_opencode_output(stdout), stderr
finally:
prompt_path.unlink(missing_ok=True)

File diff suppressed because it is too large Load Diff

View File

@@ -6,66 +6,105 @@ STORAGE_DIR = PROJECT_ROOT / "storage"
FRONTEND_DIST = PROJECT_ROOT / "frontend" / "dist"
DB_PATH = STORAGE_DIR / "creator.db"
PROJECTS_DIR = PROJECT_ROOT / "projects"
UNI_DIR = PROJECT_ROOT / "uni"
MAX_CONCURRENT_GENERATIONS = 10
# Lesbarkeits-Gate: deterministischer Prüfer (kleines deutsches Komplexitäts-Modell,
# Skala 17). Zu schwere Sections gehen in die Lese-Prüfungs-Überarbeitung.
# Fehlen transformers/torch oder das Modell → Gate stumm aus.
LESBARKEIT_AKTIV = True
LESBARKEIT_MODELL = "MiriUll/distilbert-german-text-complexity"
# Anker auf der 17-Skala (TextComplexityDE): Leichte Sprache ~1,2; Wikipedia-Schnitt
# ~3,22; ab MOS > 4 gilt ein Satz als „echt komplex" (Vereinfachungs-Grenze des Papers).
LESBARKEIT_MAX = 3.5 # Section zu schwer, wenn der Satz-Schnitt darüber liegt
LESBARKEIT_HART = 4.0 # Einzelsatz ab hier „hart"
LESBARKEIT_HART_ANTEIL = 0.30 # … ODER wenn dieser Anteil der Sätze hart ist
# Deckel für gleichzeitige CLI-Agenten-Prozesse (über alle Generierungen hinweg).
# Eigene Spur für interaktive Aufrufe (Chat, Elemente), damit sie nicht hinter
# laufenden Writern in der Warteschlange hängen.
MAX_CONCURRENT_AGENTS = 12
MAX_CONCURRENT_AGENTS = 10
MAX_CONCURRENT_INTERACTIVE = 4
# Grace-Fenster der Konsens-Races (Bausteine, Guide, OnePager): Nach dem ersten
# gültigen Ergebnis dürfen die übrigen Agenten noch so viele Sekunden fertig
# werden (Kill nur, wenn das Minimum schon steht).
KONSENS_GRACE = 300
# Recherche-Race: längeres Grace-Fenster. Recherche treibt die ganze Bausteine-Anzahl;
# bei langsamen Providern (z.B. MiniMax) sollen ALLE 5 Agenten fertig werden, nicht nur
# das Quorum von 3. Pro-Agent-Timeout (TIMEOUTS["recherche"]=1800s) deckelt echte Hänger.
RECHERCHE_GRACE = 900
# Cap der Klärungs- und Prüf-Loops: maximale Runden, bis alles entschieden sein
# muss. In der letzten Runde MUSS der Mapping-Agent jeden Eintrag entscheiden;
# Prüf-Loops lassen Rest-Beanstandungen danach stehen.
KONSENS_MAX_RUNDEN = 3
# Crawler-Sichtung (Content/Noise) — deterministischer Regel-Filter statt LLM.
# Match: Substring (klein) gegen URL UND Dateiname. Reihenfolge: keep > noise > min_chars > behalten.
# Sonderregeln einfach hier ergänzen.
CRAWL_KEEP_PATTERNS = ["learn-unit", "learn-course"] # immer Content
CRAWL_NOISE_PATTERNS = [ # eindeutig themenfremd → raus
"clubs", "events", "podcasts", "resources", "-u-",
"academy", "pricing", "/plans", "career", "newsletter", "impressum", "login",
]
CRAWL_MIN_CHARS = 400 # zu wenig Text → raus
# Timeouts pro Agenten-Schritt: (Basis-Sekunden, Sekunden pro Baustein/Section).
# Gilt für alle Provider gleich — wer zu langsam ist, wird neu gestartet bzw. überholt.
TIMEOUTS = {
"recherche": (1800, 0), # fix 30 min
"auswahl": (600, 10),
"auswahl_check": (300, 2),
"recherche_mapping": (600, 3), # n = vorgemergte Einträge
"auswahl_mapping": (600, 2), # n = Rest-Einträge (Bausteine-Inventar)
"ergaenzung": (900, 0), # Themenfeld-Ergänzung bei Projekten (Web-Recherche)
"guide_auswahl": (300, 5), # pro Baustein im Inventar
"guide_check": (300, 2), # Auswahl-/Gliederungs-Prüfung (nur Titellisten)
"plan": (300, 5),
"plan_judge": (600, 5), # Judge liest bis zu 5 Gliederungen, n = Sections
"inhalt": (600, 90), # Inhalte je Baustein im Chunk identifizieren (Websuche)
"inhalt_check": (300, 10), # Inhalts-Prüfung je Baustein im Paket
"subbaustein": (900, 45), # Subbausteine je Baustein im Chunk finden (Websuche)
"subbaustein_check": (300, 15), # Judge entscheidet strittige Subbausteine im Chunk
"stufe": (300, 10), # Subbausteine einstufen je Chunk
"stufe_check": (300, 10), # Judge entscheidet strittige Stufen im Chunk
"relevanz": (300, 10), # Subbausteine relevant/rand je Chunk
"relevanz_check": (300, 10), # Judge entscheidet strittige Relevanz im Chunk
"frage_muster": (300, 15), # Frage-Muster je Baustein (Subbausteine × Typen)
"frage_muster_check": (300, 10), # Kritiker bereinigt die Muster-Tabelle je Baustein
"writer": (600, 120), # pro Section im Chunk
"lese_check": (300, 10), # pro Section im Paket
"onepager_recherche": (900, 0),
"onepager_bauen": (300, 0),
"onepager_verify": (300, 0),
}
# Auswahl-Auftrag je Format: (Mindest-Anteil, Maximal-Anteil, Mindestanzahl, Zweck).
FORMAT_ANTEIL = {
"MiniGuide": (0.05, 0.10, 8, "einen kompakten Anfänger-Guide — der schnelle Einstieg ins Thema"),
"Guide": (0.25, 0.35, 20, "einen ausführlichen Anfänger-Guide — ein solides Fundament im Thema"),
"FullGuide": (0.90, 1.00, 0, "einen Komplett-Guide — das ganze Thema"),
# Zweck je Format — fließt in den Gliederungs-Judge (was der Guide leisten soll).
FORMAT_ZWECK = {
"Guide": "einen fokussierten Guide — alles Relevante ohne Randthemen",
"FullGuide": "einen Komplett-Guide — das ganze Thema inkl. Randthemen",
"Rest": "einen Ergänzungs-Guide — nur die Randthemen",
}
# Provider-Stacks: komplett unabhängig, einer kann jederzeit entfernt werden.
# Rollen: "quick" = Massenarbeit (Recherche, Einordnung),
# "fast" = Urteilsaufgaben mit kleinem Output (Auswahl, Final, OnePager, Chat),
# "guide" = große Generierung (Plan, Writer).
# "fast" = Interaktion + Voten (Chat, Prüfung, Klärung, Elemente),
# "judge" = Mapping-/Judge-/Prüf-Agenten — kalt (niedrige Temperature,
# ohne Thinking) für stabile Urteile; Claude/Lokal mappen auf "fast",
# "guide" = große Generierung (Vorschläge, Writer).
DEFAULT_PROVIDER = "claude"
PROVIDERS = {
"claude": {
"cli": "claude",
"guide": "claude-opus-4-8[1m]",
"fast": "claude-sonnet-4-6",
"judge": "claude-sonnet-4-6", # CLI kennt keine Temperature
"quick": "claude-sonnet-4-6",
"env_key": None, # Auth via CLAUDE_CODE_OAUTH_TOKEN oder ~/.claude
},
# "minimax-kalt/…" ist KEIN eigener Stack, nur ein opencode-Provider-Eintrag
# (dev-ops/opencode.json) mit niedriger Temperature; M3 dort ohne Thinking.
"minimax": {
"cli": "opencode",
"guide": "minimax/MiniMax-M3",
"fast": "minimax/MiniMax-M2.7-highspeed",
"quick": "minimax/MiniMax-M2.7-highspeed",
"env_key": "MINIMAX_API_KEY",
},
# Wie "minimax", aber Chat/Elemente (Rolle "fast") laufen auf M3 OHNE Thinking.
# M2.x kann Thinking nicht abschalten — nur M3 respektiert thinking:disabled.
# guide/quick bleiben identisch zur Thinking-Variante.
"minimax-direkt": {
"cli": "opencode",
"guide": "minimax/MiniMax-M3",
"fast": "minimax-direkt/MiniMax-M3",
"fast": "minimax-kalt/MiniMax-M2.7-highspeed",
"judge": "minimax-kalt/MiniMax-M3",
"quick": "minimax/MiniMax-M2.7-highspeed",
"env_key": "MINIMAX_API_KEY",
},
@@ -73,6 +112,7 @@ PROVIDERS = {
"cli": "opencode",
"guide": "ollama/qwen3.6:27b",
"fast": "ollama/qwen3.5:9b",
"judge": "ollama/qwen3.5:9b",
"quick": "ollama/qwen3.5:9b",
"env_key": None,
"check_url": "http://localhost:11434/api/tags", # Ollama erreichbar?

113
backend/crawl.py Normal file
View File

@@ -0,0 +1,113 @@
"""Geboundeter Domain-Crawler für Link-Quellen — rendert JS via Playwright (Chromium).
Lädt ab einer Start-URL Seiten + PDFs — NUR dieselbe Domain, begrenzte Tiefe und
Seitenzahl. HTML-Seiten werden im Headless-Browser gerendert (für SPAs nötig), dann
Links + Text aus dem fertigen DOM gezogen. PDFs werden direkt als Bytes geladen.
Deterministisch, gebounded; läuft via asyncio.to_thread (Sync-API, kein Event-Loop).
"""
import hashlib
import logging
import re
from pathlib import Path
from urllib.parse import urldefrag, urlparse
from urllib.request import Request, urlopen
from fsutil import atomic_write_text
log = logging.getLogger("creator.crawl")
MAX_TIEFE = 3
MAX_SEITEN = 500
SEITE_TIMEOUT = 30 # Sekunden pro Seite (Render bzw. PDF-Download)
MAX_BYTES = 10_000_000 # 10 MB Deckel pro PDF
_UA = "Mozilla/5.0 (creator-lernbot)"
def _fetch_bytes(url: str) -> bytes | None:
"""PDF-Bytes per urllib laden (kein Rendering nötig). None bei Fehler/zu groß."""
try:
req = Request(url, headers={"User-Agent": _UA})
with urlopen(req, timeout=SEITE_TIMEOUT) as resp:
data = resp.read(MAX_BYTES + 1)
return None if len(data) > MAX_BYTES else data
except Exception as e:
log.debug("crawl: PDF-Fetch fehlgeschlagen %s: %s", url, e)
return None
def _name(url: str, ext: str) -> str:
h = hashlib.md5(url.encode("utf-8")).hexdigest()[:8]
slug = re.sub(r"[^a-zA-Z0-9]+", "-", urlparse(url).path).strip("-")[:60] or "index"
return f"{h}-{slug}{ext}"
def _is_pdf(url: str) -> bool:
return url.lower().split("?")[0].rstrip("/").endswith(".pdf")
def crawl(start_url: str, ziel: Path, *, max_tiefe: int = MAX_TIEFE, max_seiten: int = MAX_SEITEN, cancelled=None) -> int:
"""Crawlt ab start_url (nur gleiche Domain), rendert JS und legt Seiten/PDFs in `ziel` ab.
BFS bis `max_tiefe` / `max_seiten`. Fehler einzelner Seiten werden übersprungen.
Schreibt am ENDE einen `.done`-Marker; ein Abbruch (`cancelled()` → True) lässt ihn weg,
sodass ein Neustart neu crawlt. Gibt die Zahl gespeicherter Quellen zurück.
"""
# Lazy: so startet das Backend auch ohne installiertes Playwright; nur das Crawlen schlägt dann fehl.
from playwright.sync_api import sync_playwright
ziel.mkdir(parents=True, exist_ok=True)
domain = urlparse(start_url).netloc
gesehen: set[str] = set()
queue: list[tuple[str, int]] = [(urldefrag(start_url)[0], 0)]
gespeichert = 0
with sync_playwright() as pw:
browser = pw.chromium.launch(args=["--no-sandbox"]) # non-root (Docker user app)
page = browser.new_page(user_agent=_UA)
try:
while queue and gespeichert < max_seiten:
if cancelled and cancelled():
return gespeichert # Abbruch → KEIN .done-Marker → Neustart crawlt neu
url, tiefe = queue.pop(0)
if url in gesehen:
continue
gesehen.add(url)
# PDFs brauchen kein Rendering — direkt laden.
if _is_pdf(url):
data = _fetch_bytes(url)
if data:
p = ziel / _name(url, ".pdf")
if not p.exists():
p.write_bytes(data)
gespeichert += 1
continue
try:
page.goto(url, wait_until="networkidle", timeout=SEITE_TIMEOUT * 1000)
except Exception as e:
log.debug("crawl: goto unvollständig %s: %s", url, e) # trotzdem versuchen, Inhalt zu lesen
try:
text = page.inner_text("body").strip()
except Exception:
text = ""
if text:
atomic_write_text(ziel / _name(url, ".txt"), f"QUELLE: {url}\n\n{text}")
gespeichert += 1
if tiefe < max_tiefe:
try:
hrefs = page.eval_on_selector_all("a[href]", "els => els.map(e => e.href)")
except Exception:
hrefs = []
for href in hrefs:
nxt = urldefrag(href)[0]
if (nxt.startswith(("http://", "https://"))
and urlparse(nxt).netloc == domain and nxt not in gesehen):
queue.append((nxt, tiefe + 1))
finally:
browser.close()
(ziel / ".done").write_text("ok", encoding="utf-8") # sauber durchgelaufen
log.info("crawl %s%d Quellen in %s", start_url, gespeichert, ziel)
return gespeichert

View File

@@ -47,6 +47,129 @@ CREATE TABLE IF NOT EXISTS elements (
)
"""
CREATE_BAUSTEIN_TEXTE = """
CREATE TABLE IF NOT EXISTS baustein_texte (
topic TEXT NOT NULL,
baustein TEXT NOT NULL,
art TEXT NOT NULL,
md TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
PRIMARY KEY (topic, baustein, art)
)
"""
CREATE_BAUSTEIN_PROGRESS = """
CREATE TABLE IF NOT EXISTS baustein_progress (
topic TEXT NOT NULL,
baustein TEXT NOT NULL,
gute_antworten INTEGER NOT NULL DEFAULT 0,
streak INTEGER NOT NULL DEFAULT 0,
absolviert TEXT,
verstanden TEXT,
gemeistert TEXT,
updated_at TEXT NOT NULL,
PRIMARY KEY (topic, baustein)
)
"""
# --- Bausteine-Pipeline-Inhalt (ersetzt Datei-Sidecars) ---
# Inventar: ein Baustein je (topic, titel_norm). nennungen = Anzahl Agenten/Runden,
# die ihn nannten (≥2 = Konsens). status: kandidat/konsens/rest/verworfen.
CREATE_BAUSTEINE = """
CREATE TABLE IF NOT EXISTS bausteine (
topic TEXT NOT NULL,
titel_norm TEXT NOT NULL,
titel TEXT NOT NULL,
beschreibung TEXT NOT NULL DEFAULT '',
nennungen INTEGER NOT NULL DEFAULT 1,
status TEXT NOT NULL DEFAULT 'kandidat',
quellen TEXT NOT NULL DEFAULT '[]',
updated_at TEXT NOT NULL,
PRIMARY KEY (topic, titel_norm)
)
"""
# Subbausteine je Baustein. stufe (einfach/mittel/schwer) + relevanz (relevant/rand)
# werden später gesetzt. nennungen analog zum Inventar.
CREATE_SUBBAUSTEINE = """
CREATE TABLE IF NOT EXISTS subbausteine (
topic TEXT NOT NULL,
baustein_norm TEXT NOT NULL,
sub_norm TEXT NOT NULL,
baustein TEXT NOT NULL,
sub_titel TEXT NOT NULL,
nennungen INTEGER NOT NULL DEFAULT 1,
stufe TEXT,
relevanz TEXT,
status TEXT NOT NULL DEFAULT 'kandidat',
updated_at TEXT NOT NULL,
PRIMARY KEY (topic, baustein_norm, sub_norm)
)
"""
# Ein Frage-Muster je (Baustein, Subbaustein, Typ).
CREATE_FRAGE_MUSTER = """
CREATE TABLE IF NOT EXISTS frage_muster (
topic TEXT NOT NULL,
baustein_norm TEXT NOT NULL,
sub_norm TEXT NOT NULL,
baustein TEXT NOT NULL,
sub_titel TEXT NOT NULL,
typ TEXT NOT NULL,
frage TEXT NOT NULL,
updated_at TEXT NOT NULL,
PRIMARY KEY (topic, baustein_norm, sub_norm, typ)
)
"""
# Crawl-Seiten je Thema: inhalt (1=Content/0=Noise, von der Sichtung) + gelesen (von der Recherche).
CREATE_RECHERCHE_COVERAGE = """
CREATE TABLE IF NOT EXISTS recherche_coverage (
topic TEXT NOT NULL,
quelle TEXT NOT NULL,
gelesen INTEGER NOT NULL DEFAULT 0,
inhalt INTEGER,
updated_at TEXT NOT NULL,
PRIMARY KEY (topic, quelle)
)
"""
# Schritt-Status der Pipeline (ersetzt Datei-Existenz-Resume + Reset-Globs).
CREATE_PIPELINE_STATE = """
CREATE TABLE IF NOT EXISTS pipeline_state (
topic TEXT NOT NULL,
schritt TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'offen',
updated_at TEXT NOT NULL,
PRIMARY KEY (topic, schritt)
)
"""
# Fertiger Guide-Inhalt je (Thema, Format) als JSON-Blob (ersetzt die Guide-JSON-Datei).
# Geteilt über alle Guide-Läufe desselben Thema+Formats (wie zuvor die Content-Datei).
CREATE_GUIDE_CONTENT = """
CREATE TABLE IF NOT EXISTS guide_content (
topic TEXT NOT NULL,
format TEXT NOT NULL,
json TEXT NOT NULL,
updated_at TEXT NOT NULL,
PRIMARY KEY (topic, format)
)
"""
# Quellen-Wahl je Thema (ersetzt quelle.json).
CREATE_QUELLE = """
CREATE TABLE IF NOT EXISTS quelle (
topic TEXT PRIMARY KEY,
type TEXT NOT NULL,
ort TEXT NOT NULL DEFAULT '',
spec TEXT NOT NULL DEFAULT '',
updated_at TEXT NOT NULL
)
"""
_db: aiosqlite.Connection | None = None
@@ -67,14 +190,64 @@ async def init_db():
await db.execute(CREATE_PROGRESS)
await db.execute(CREATE_TOPICS)
await db.execute(CREATE_ELEMENTS)
await db.execute(CREATE_BAUSTEIN_TEXTE)
await db.execute(CREATE_BAUSTEIN_PROGRESS)
await db.execute(CREATE_BAUSTEINE)
await db.execute(CREATE_SUBBAUSTEINE)
await db.execute(CREATE_FRAGE_MUSTER)
await db.execute(CREATE_RECHERCHE_COVERAGE)
await db.execute(CREATE_PIPELINE_STATE)
await db.execute(CREATE_GUIDE_CONTENT)
await db.execute(CREATE_QUELLE)
try: # Migration für Bestands-DBs ohne step-Spalte
await db.execute("ALTER TABLE guides ADD COLUMN step INTEGER")
except aiosqlite.OperationalError:
pass
try: # Migration: recherche_coverage.inhalt (Content/Noise aus der Sichtung)
await db.execute("ALTER TABLE recherche_coverage ADD COLUMN inhalt INTEGER")
except aiosqlite.OperationalError:
pass
try: # Migration für Bestands-DBs ohne verstanden-Spalte (Mastery-Stufe)
await db.execute("ALTER TABLE baustein_progress ADD COLUMN verstanden TEXT")
except aiosqlite.OperationalError:
pass
try: # Migration für Bestands-DBs ohne gemeistert-Spalte (Meisterpfad 25)
await db.execute("ALTER TABLE baustein_progress ADD COLUMN gemeistert TEXT")
except aiosqlite.OperationalError:
pass
try: # Migration für Bestands-DBs ohne streak-Spalte (persistente Streak-Bonus-Folge)
await db.execute("ALTER TABLE baustein_progress ADD COLUMN streak INTEGER NOT NULL DEFAULT 0")
except aiosqlite.OperationalError:
pass
# Offene-Frage-Anker: Basis/Streak VOR der aktuell offenen Frage — macht die Bewertung
# serverseitig idempotent (Re-Bewertung) und driftfrei über Fragen hinweg.
for _spalte, _typ in (("offene_frage", "TEXT"), ("offene_basis", "INTEGER"), ("offene_streak", "INTEGER")):
try:
await db.execute(f"ALTER TABLE baustein_progress ADD COLUMN {_spalte} {_typ}")
except aiosqlite.OperationalError:
pass
# Migration: alte vertiefungen-Tabelle → baustein_texte (Bestand = lange Form, art 'deepdive')
cursor = await db.execute("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'vertiefungen'")
if await cursor.fetchone():
await db.execute(
"INSERT OR IGNORE INTO baustein_texte (topic, baustein, art, md, created_at, updated_at) "
"SELECT topic, baustein, 'deepdive', md, created_at, updated_at FROM vertiefungen"
)
await db.execute("DROP TABLE vertiefungen")
await db.execute(
"UPDATE guides SET status = 'error', progress = NULL, error_msg = 'Server-Neustart' "
"WHERE status IN ('queued', 'generating')"
)
# Milestone-Migration auf Score-Skala 030: absolviert≥6, verstanden≥12, gemeistert≥18.
# Leitet die Meilensteine aus dem aktuellen Score ab (bestehende Timestamps bleiben),
# idempotent — nach einem Lauf stabil, im Normalbetrieb deckungsgleich mit der Prüf-Route.
await db.execute("UPDATE baustein_progress SET gute_antworten = 30 WHERE gute_antworten > 30") # Alt-Scores deckeln
for _spalte, _schwelle in (("absolviert", 6), ("verstanden", 12), ("gemeistert", 18)):
await db.execute(
f"UPDATE baustein_progress SET {_spalte} = COALESCE({_spalte}, datetime('now')) "
f"WHERE gute_antworten >= {_schwelle}"
)
await db.execute(f"UPDATE baustein_progress SET {_spalte} = NULL WHERE gute_antworten < {_schwelle}")
await db.commit()
@@ -255,3 +428,445 @@ async def delete_progress(guide_id: str) -> None:
db = await get_db()
await db.execute("DELETE FROM guide_progress WHERE guide_id = ?", (guide_id,))
await db.commit()
# --- Baustein-Lernen: Vertiefungen + Prüfungs-Fortschritt ---
def _now() -> str:
from datetime import datetime, timezone
return datetime.now(timezone.utc).isoformat()
async def list_baustein_progress(topic: str) -> list[dict]:
db = await get_db()
cursor = await db.execute(
"SELECT baustein, gute_antworten, streak, absolviert, verstanden, gemeistert FROM baustein_progress WHERE topic = ?", (topic,)
)
rows = await cursor.fetchall()
return [{"baustein": b, "gute_antworten": n, "streak": s, "absolviert": a, "verstanden": v, "gemeistert": m} for b, n, s, a, v, m in rows]
async def get_baustein_progress(topic: str, baustein: str) -> dict:
"""Eine Baustein-Zeile inkl. Offene-Frage-Anker. Defaults, falls noch keine existiert."""
db = await get_db()
cursor = await db.execute(
"SELECT gute_antworten, streak, absolviert, verstanden, gemeistert, "
"offene_frage, offene_basis, offene_streak FROM baustein_progress "
"WHERE topic = ? AND baustein = ?",
(topic, baustein),
)
row = await cursor.fetchone()
if row is None:
return {"gute_antworten": 0, "streak": 0, "absolviert": None, "verstanden": None,
"gemeistert": None, "offene_frage": None, "offene_basis": None, "offene_streak": None}
return {"gute_antworten": row[0], "streak": row[1], "absolviert": row[2], "verstanden": row[3],
"gemeistert": row[4], "offene_frage": row[5], "offene_basis": row[6], "offene_streak": row[7]}
async def set_offene_frage(topic: str, baustein: str, frage: str, basis: int, streak: int) -> None:
"""Friert Basis + Streak VOR der jetzt offenen Frage ein (Anker für idempotente Re-Bewertung)."""
db = await get_db()
now = _now()
await db.execute(
"""INSERT INTO baustein_progress (topic, baustein, offene_frage, offene_basis, offene_streak, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(topic, baustein) DO UPDATE SET
offene_frage = excluded.offene_frage, offene_basis = excluded.offene_basis,
offene_streak = excluded.offene_streak, updated_at = excluded.updated_at""",
(topic, baustein, frage, basis, streak, now),
)
await db.commit()
async def set_baustein_score(topic: str, baustein: str, score: int) -> int:
"""Setzt den Score absolut (vom Aufrufer geclampt) und liefert ihn zurück."""
db = await get_db()
await db.execute(
"""INSERT INTO baustein_progress (topic, baustein, gute_antworten, updated_at)
VALUES (?, ?, ?, ?)
ON CONFLICT(topic, baustein) DO UPDATE SET
gute_antworten = excluded.gute_antworten, updated_at = excluded.updated_at""",
(topic, baustein, score, _now()),
)
await db.commit()
return score
async def set_baustein_streak(topic: str, baustein: str, streak: int) -> None:
"""Persistiert die Streak-Folge (Bonus) absolut — überlebt Reload/Session-Wechsel."""
db = await get_db()
await db.execute(
"""INSERT INTO baustein_progress (topic, baustein, streak, updated_at)
VALUES (?, ?, ?, ?)
ON CONFLICT(topic, baustein) DO UPDATE SET
streak = excluded.streak, updated_at = excluded.updated_at""",
(topic, baustein, streak, _now()),
)
await db.commit()
async def set_baustein_verstanden(topic: str, baustein: str) -> bool:
"""Markiert verstanden (Mastery); True nur beim ersten Mal. Sticky wie absolviert."""
db = await get_db()
now = _now()
await db.execute(
"INSERT OR IGNORE INTO baustein_progress (topic, baustein, gute_antworten, updated_at) VALUES (?, ?, 0, ?)",
(topic, baustein, now),
)
cursor = await db.execute(
"UPDATE baustein_progress SET verstanden = ?, updated_at = ? "
"WHERE topic = ? AND baustein = ? AND verstanden IS NULL",
(now, now, topic, baustein),
)
await db.commit()
return cursor.rowcount > 0
async def set_baustein_gemeistert(topic: str, baustein: str) -> bool:
"""Markiert gemeistert (Meisterpfad, Score 25); True nur beim ersten Mal. Sticky."""
db = await get_db()
now = _now()
await db.execute(
"INSERT OR IGNORE INTO baustein_progress (topic, baustein, gute_antworten, updated_at) VALUES (?, ?, 0, ?)",
(topic, baustein, now),
)
cursor = await db.execute(
"UPDATE baustein_progress SET gemeistert = ?, updated_at = ? "
"WHERE topic = ? AND baustein = ? AND gemeistert IS NULL",
(now, now, topic, baustein),
)
await db.commit()
return cursor.rowcount > 0
async def set_baustein_absolviert(topic: str, baustein: str) -> bool:
"""Markiert absolviert; True nur beim ersten Mal (steuert den Element-Task)."""
db = await get_db()
now = _now()
await db.execute(
"INSERT OR IGNORE INTO baustein_progress (topic, baustein, gute_antworten, updated_at) VALUES (?, ?, 0, ?)",
(topic, baustein, now),
)
cursor = await db.execute(
"UPDATE baustein_progress SET absolviert = ?, updated_at = ? "
"WHERE topic = ? AND baustein = ? AND absolviert IS NULL",
(now, now, topic, baustein),
)
await db.commit()
return cursor.rowcount > 0
async def list_baustein_levels_all() -> dict[str, dict[str, set[str]]]:
"""Bausteine je Meilenstein in EINER Query: {"absolviert"/"verstanden"/"gemeistert": {topic → {Titel}}}."""
db = await get_db()
cursor = await db.execute(
"SELECT topic, baustein, absolviert, verstanden, gemeistert FROM baustein_progress"
)
rows = await cursor.fetchall()
levels: dict[str, dict[str, set[str]]] = {"absolviert": {}, "verstanden": {}, "gemeistert": {}}
for topic, baustein, absolviert, verstanden, gemeistert in rows:
if absolviert is not None:
levels["absolviert"].setdefault(topic, set()).add(baustein)
if verstanden is not None:
levels["verstanden"].setdefault(topic, set()).add(baustein)
if gemeistert is not None:
levels["gemeistert"].setdefault(topic, set()).add(baustein)
return levels
async def delete_baustein_daten(topic: str) -> None:
db = await get_db()
await db.execute("DELETE FROM baustein_texte WHERE topic = ?", (topic,))
await db.execute("DELETE FROM baustein_progress WHERE topic = ?", (topic,))
await db.commit()
# --- Bausteine-Pipeline-Inhalt: Inventar / Subbausteine / Frage-Muster / Coverage / State / Quelle ---
async def upsert_baustein(topic: str, titel_norm: str, titel: str, beschreibung: str = "", quellen: list | None = None) -> None:
"""Kandidat einfügen oder Nennungszähler erhöhen. Erst-Beschreibung bleibt erhalten."""
db = await get_db()
await db.execute(
"""INSERT INTO bausteine (topic, titel_norm, titel, beschreibung, nennungen, status, quellen, updated_at)
VALUES (?, ?, ?, ?, 1, 'kandidat', ?, ?)
ON CONFLICT(topic, titel_norm) DO UPDATE SET
nennungen = nennungen + 1, quellen = excluded.quellen, updated_at = excluded.updated_at""",
(topic, titel_norm, titel, beschreibung, json.dumps(quellen or [], ensure_ascii=False), _now()),
)
await db.commit()
async def list_bausteine(topic: str, status: str | None = None) -> list[dict]:
db = await get_db()
if status is None:
cursor = await db.execute("SELECT * FROM bausteine WHERE topic = ? ORDER BY rowid", (topic,))
else:
cursor = await db.execute("SELECT * FROM bausteine WHERE topic = ? AND status = ? ORDER BY rowid", (topic, status))
rows = await cursor.fetchall()
out = []
for row in rows:
d = _row_to_dict(row, cursor)
d["quellen"] = json.loads(d.get("quellen") or "[]")
out.append(d)
return out
async def set_baustein_status(topic: str, titel_norm: str, status: str, titel: str | None = None, beschreibung: str | None = None) -> None:
"""Status setzen; optional Titel/Beschreibung aktualisieren (z.B. nach semantischem Merge)."""
db = await get_db()
fields = {"status": status, "updated_at": _now()}
if titel is not None:
fields["titel"] = titel
if beschreibung is not None:
fields["beschreibung"] = beschreibung
sets = ", ".join(f"{k} = :{k}" for k in fields)
await db.execute(
f"UPDATE bausteine SET {sets} WHERE topic = :topic AND titel_norm = :titel_norm",
{**fields, "topic": topic, "titel_norm": titel_norm},
)
await db.commit()
async def delete_bausteine(topic: str) -> None:
db = await get_db()
await db.execute("DELETE FROM bausteine WHERE topic = ?", (topic,))
await db.commit()
async def upsert_subbaustein(topic: str, baustein_norm: str, sub_norm: str, baustein: str, sub_titel: str) -> None:
db = await get_db()
await db.execute(
"""INSERT INTO subbausteine (topic, baustein_norm, sub_norm, baustein, sub_titel, nennungen, status, updated_at)
VALUES (?, ?, ?, ?, ?, 1, 'kandidat', ?)
ON CONFLICT(topic, baustein_norm, sub_norm) DO UPDATE SET
nennungen = nennungen + 1, updated_at = excluded.updated_at""",
(topic, baustein_norm, sub_norm, baustein, sub_titel, _now()),
)
await db.commit()
async def put_subbaustein(topic: str, baustein_norm: str, sub_norm: str, baustein: str, sub_titel: str,
stufe: str | None = None, relevanz: str | None = None, status: str = "konsens") -> None:
"""Insert/Update OHNE Nennungszähler (Spiegel aus dem Sidecar). stufe/relevanz nur überschreiben,
wenn ein neuer Wert übergeben wird (COALESCE schützt Bestehendes)."""
db = await get_db()
await db.execute(
"""INSERT INTO subbausteine (topic, baustein_norm, sub_norm, baustein, sub_titel, nennungen, stufe, relevanz, status, updated_at)
VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?, ?)
ON CONFLICT(topic, baustein_norm, sub_norm) DO UPDATE SET
baustein = excluded.baustein, sub_titel = excluded.sub_titel,
stufe = COALESCE(excluded.stufe, subbausteine.stufe),
relevanz = COALESCE(excluded.relevanz, subbausteine.relevanz),
status = excluded.status, updated_at = excluded.updated_at""",
(topic, baustein_norm, sub_norm, baustein, sub_titel, stufe, relevanz, status, _now()),
)
await db.commit()
async def list_subbausteine(topic: str, baustein_norm: str | None = None) -> list[dict]:
db = await get_db()
if baustein_norm is None:
cursor = await db.execute("SELECT * FROM subbausteine WHERE topic = ? ORDER BY rowid", (topic,))
else:
cursor = await db.execute(
"SELECT * FROM subbausteine WHERE topic = ? AND baustein_norm = ? ORDER BY rowid", (topic, baustein_norm)
)
rows = await cursor.fetchall()
return [_row_to_dict(row, cursor) for row in rows]
async def set_subbaustein_felder(topic: str, baustein_norm: str, sub_norm: str, **fields) -> None:
"""Setzt Felder (stufe/relevanz/status/sub_titel) einer Subbaustein-Zeile."""
fields["updated_at"] = _now()
sets = ", ".join(f"{k} = :{k}" for k in fields)
db = await get_db()
await db.execute(
f"UPDATE subbausteine SET {sets} WHERE topic = :topic AND baustein_norm = :baustein_norm AND sub_norm = :sub_norm",
{**fields, "topic": topic, "baustein_norm": baustein_norm, "sub_norm": sub_norm},
)
await db.commit()
async def delete_subbausteine(topic: str) -> None:
db = await get_db()
await db.execute("DELETE FROM subbausteine WHERE topic = ?", (topic,))
await db.commit()
async def upsert_frage_muster(topic: str, baustein_norm: str, sub_norm: str, baustein: str, sub_titel: str, typ: str, frage: str) -> None:
db = await get_db()
await db.execute(
"""INSERT INTO frage_muster (topic, baustein_norm, sub_norm, baustein, sub_titel, typ, frage, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(topic, baustein_norm, sub_norm, typ) DO UPDATE SET
frage = excluded.frage, updated_at = excluded.updated_at""",
(topic, baustein_norm, sub_norm, baustein, sub_titel, typ, frage, _now()),
)
await db.commit()
async def list_frage_muster(topic: str, baustein_norm: str | None = None) -> list[dict]:
db = await get_db()
if baustein_norm is None:
cursor = await db.execute("SELECT * FROM frage_muster WHERE topic = ? ORDER BY rowid", (topic,))
else:
cursor = await db.execute(
"SELECT * FROM frage_muster WHERE topic = ? AND baustein_norm = ? ORDER BY rowid", (topic, baustein_norm)
)
rows = await cursor.fetchall()
return [_row_to_dict(row, cursor) for row in rows]
async def delete_frage_muster(topic: str) -> None:
db = await get_db()
await db.execute("DELETE FROM frage_muster WHERE topic = ?", (topic,))
await db.commit()
async def mark_quellen_gelesen(topic: str, quellen: list[str]) -> None:
"""Markiert die zitierten Crawl-Seiten als gelesen (Recherche-Loop-Abdeckung)."""
if not quellen:
return
db = await get_db()
now = _now()
await db.executemany(
"""INSERT INTO recherche_coverage (topic, quelle, gelesen, updated_at) VALUES (?, ?, 1, ?)
ON CONFLICT(topic, quelle) DO UPDATE SET gelesen = 1, updated_at = excluded.updated_at""",
[(topic, q, now) for q in quellen],
)
await db.commit()
async def list_coverage(topic: str) -> dict[str, int]:
db = await get_db()
cursor = await db.execute("SELECT quelle, gelesen FROM recherche_coverage WHERE topic = ?", (topic,))
rows = await cursor.fetchall()
return {q: g for q, g in rows}
async def mark_inhalt(topic: str, content: list[str], noise: list[str]) -> None:
"""Sichtungs-Ergebnis je Crawl-Seite ablegen: inhalt=1 (Content) bzw. 0 (Noise)."""
db = await get_db()
now = _now()
rows = [(topic, q, 1, now) for q in content] + [(topic, q, 0, now) for q in noise]
if not rows:
return
await db.executemany(
"""INSERT INTO recherche_coverage (topic, quelle, inhalt, updated_at) VALUES (?, ?, ?, ?)
ON CONFLICT(topic, quelle) DO UPDATE SET inhalt = excluded.inhalt, updated_at = excluded.updated_at""",
rows,
)
await db.commit()
async def list_content(topic: str) -> list[str]:
"""Crawl-Seiten, die die Sichtung als Content markiert hat (inhalt=1)."""
db = await get_db()
cursor = await db.execute(
"SELECT quelle FROM recherche_coverage WHERE topic = ? AND inhalt = 1 ORDER BY quelle", (topic,)
)
return [r[0] for r in await cursor.fetchall()]
async def delete_coverage(topic: str) -> None:
db = await get_db()
await db.execute("DELETE FROM recherche_coverage WHERE topic = ?", (topic,))
await db.commit()
async def set_step_status(topic: str, schritt: str, status: str) -> None:
db = await get_db()
await db.execute(
"""INSERT INTO pipeline_state (topic, schritt, status, updated_at) VALUES (?, ?, ?, ?)
ON CONFLICT(topic, schritt) DO UPDATE SET status = excluded.status, updated_at = excluded.updated_at""",
(topic, schritt, status, _now()),
)
await db.commit()
async def get_step_status(topic: str, schritt: str) -> str:
db = await get_db()
cursor = await db.execute(
"SELECT status FROM pipeline_state WHERE topic = ? AND schritt = ?", (topic, schritt)
)
row = await cursor.fetchone()
return row[0] if row else "offen"
async def list_pipeline_state(topic: str) -> dict[str, str]:
db = await get_db()
cursor = await db.execute("SELECT schritt, status FROM pipeline_state WHERE topic = ?", (topic,))
rows = await cursor.fetchall()
return {s: st for s, st in rows}
async def delete_pipeline_state(topic: str, schritte: list[str] | None = None) -> None:
db = await get_db()
if schritte is None:
await db.execute("DELETE FROM pipeline_state WHERE topic = ?", (topic,))
elif schritte:
marks = ",".join("?" for _ in schritte)
await db.execute(f"DELETE FROM pipeline_state WHERE topic = ? AND schritt IN ({marks})", (topic, *schritte))
await db.commit()
async def set_quelle(topic: str, type: str, ort: str = "", spec: str = "") -> None:
db = await get_db()
await db.execute(
"""INSERT INTO quelle (topic, type, ort, spec, updated_at) VALUES (?, ?, ?, ?, ?)
ON CONFLICT(topic) DO UPDATE SET
type = excluded.type, ort = excluded.ort, spec = excluded.spec, updated_at = excluded.updated_at""",
(topic, type, ort, spec, _now()),
)
await db.commit()
async def get_quelle(topic: str) -> dict | None:
db = await get_db()
cursor = await db.execute("SELECT type, ort, spec FROM quelle WHERE topic = ?", (topic,))
row = await cursor.fetchone()
if row is None:
return None
return {"type": row[0], "ort": row[1], "spec": row[2]}
async def delete_quelle(topic: str) -> None:
db = await get_db()
await db.execute("DELETE FROM quelle WHERE topic = ?", (topic,))
await db.commit()
async def set_guide_content(topic: str, format: str, content_json: str) -> None:
"""Fertigen Guide-Inhalt (JSON-Blob) je Thema+Format speichern."""
db = await get_db()
await db.execute(
"""INSERT INTO guide_content (topic, format, json, updated_at) VALUES (?, ?, ?, ?)
ON CONFLICT(topic, format) DO UPDATE SET json = excluded.json, updated_at = excluded.updated_at""",
(topic, format, content_json, _now()),
)
await db.commit()
async def get_guide_content(topic: str, format: str) -> str | None:
db = await get_db()
cursor = await db.execute("SELECT json FROM guide_content WHERE topic = ? AND format = ?", (topic, format))
row = await cursor.fetchone()
return row[0] if row else None
async def delete_guide_content(topic: str, format: str | None = None) -> None:
db = await get_db()
if format is None:
await db.execute("DELETE FROM guide_content WHERE topic = ?", (topic,))
else:
await db.execute("DELETE FROM guide_content WHERE topic = ? AND format = ?", (topic, format))
await db.commit()
async def delete_topic_pipeline(topic: str) -> None:
"""Bausteine-Bereich eines Themas verwerfen (Inventar/Subs/Muster/Coverage/State).
NICHT die Themen-Config `quelle` — die wird separat verwaltet (delete_quelle)."""
db = await get_db()
for tab in ("bausteine", "subbausteine", "frage_muster", "recherche_coverage", "pipeline_state"):
await db.execute(f"DELETE FROM {tab} WHERE topic = ?", (topic,))
await db.commit()

View File

@@ -71,7 +71,7 @@ def _topic_context(topic: str, limit: int = 12000) -> str:
bp = bausteine_path(topic)
if bp.exists():
parts.append(bp.read_text(encoding="utf-8"))
for fmt in ("FullGuide", "Guide", "MiniGuide", "OnePager"):
for fmt in ("Guide", "FullGuide"): # bester verfügbarer Prosa-Guide als Chat-Kontext
content = _json_datei(guide_content_path(topic, fmt))
if content:
for ch in content.get("chapters", []):
@@ -82,14 +82,17 @@ def _topic_context(topic: str, limit: int = 12000) -> str:
return text[:limit] if text else "(kein Material vorhanden)"
async def generate_element(topic: str, hint: str, provider: str = DEFAULT_PROVIDER) -> dict:
async def generate_element(topic: str, hint: str, provider: str = DEFAULT_PROVIDER, extra_context: str = "") -> dict:
"""Erstellt Element-Felder per KI. Fallback: nur Titel aus dem Stichwort."""
fallback = {"title": hint.strip() or "Neues Element", "description": "", "examples": [], "hints": []}
try:
context = _topic_context(topic)
if extra_context.strip():
context = (extra_context.strip() + "\n\n" + context)[:12000]
prompt = _prompt(
"Element-Create",
topic=topic, hint=hint.strip() or "(keins — wähle selbst ein Kernkonzept)",
context=_topic_context(topic),
context=context,
)
returncode, stdout, _ = await run_agent(
"element-" + str(uuid.uuid4()), prompt, 240, provider=provider, role="fast", capabilities="none", lane="interactive"

View File

@@ -1,6 +1,10 @@
"""Guide-Generierung: 6 Schritte mit Prüfung nach jeder Phase (OnePager hat einen eigenen Weg).
"""Guide-Generierung als Konsens-Pipeline.
Prüf-Agenten notieren nur Probleme; das Anpassen übernimmt der jeweilige Erzeuger-Typ.
Gliederung: Auswahl der Bausteine (deterministisch je Format) → 3 Vorschläge
(Grace), die Bausteine NUMMERN-basiert in Kapitel ordnen → ein Judge merged die
Vorschläge zu einer kohärenten Reihenfolge.
Schreiben: Writer je Baustein. Lese-Prüfung: Check→Fix (eine Runde),
Folgerunden prüfen nur ersetzte Sections; danach bleiben Beanstandungen stehen.
Schritt-Dateien bleiben liegen → Abbruch erhält Fortschritt, ▶ setzt am offenen Schritt fort.
"""
@@ -12,43 +16,87 @@ from datetime import datetime, timezone
from pathlib import Path
from agents import run_agent
from bausteine import _pdfs_konvertieren
from config import DEFAULT_PROVIDER, FORMAT_ANTEIL, TEMPLATES_DIR
from database import list_guides, update_guide
from bausteine import _pdfs_konvertieren, quelle_ordner
from config import (
DEFAULT_PROVIDER, FORMAT_ZWECK, KONSENS_GRACE,
LESBARKEIT_AKTIV, TEMPLATES_DIR,
)
import lesbarkeit
from database import list_guides, update_guide, list_bausteine, list_subbausteine, set_guide_content
from fsutil import atomic_write_json, atomic_write_text
from jsonio import read_json_file as _json_datei
from onepager import _generate_onepager
from paths import bausteine_path, guide_content_path, project_dir
from paths import bausteine_path, guide_content_path, project_dir, subbausteine_path
from pipeline import (
CANCELLED, FAILED, GenContext, _check_then_fix, _claude_error, _extra,
_fail, _gather_error, _log, _prompt, _race, _semaphore, _set_progress,
_set_step, _timeout, clear_guide_cancelled, is_guide_cancelled,
run_single_slot,
CANCELLED, FAILED, GenContext, _claude_error, _extra,
_fail, _gather_error, _gather_fortschritt, _log, _prompt, _race,
_semaphore, _set_progress, _set_step, _timeout, clear_guide_cancelled,
is_guide_cancelled, run_single_slot,
)
from textkit import (
_eindeutige_titel, _lade_bausteine, _parse_fragment, _split_chunks,
_titel, _titel_aufloesen, _titel_index, _zuteilung_text,
_titel, _titel_aufloesen, _titel_index,
)
log = logging.getLogger("creator.guide")
GUIDE_STEPS = ("Auswahl", "Auswahl-Prüfung", "Gliederung", "Gliederungs-Prüfung", "Schreiben", "Lese-Prüfung")
GUIDE_STEPS = ("Gliederung", "Inhalte", "Inhalts-Check", "Schreiben", "Lese-Prüfung")
# Writer skalieren mit der Section-Zahl: 1 Writer je ~30 Sections (gedeckelt).
# Kleine Pakete vermeiden Lazy-Output bei langen Listen und begrenzen den Schaden
# eines fehlgeschlagenen Writers.
WRITER_SECTIONS = 30
WRITER_MAX = 20
# Inhalte/Inhalts-Check/Lese-Prüfung laufen in Paketen von ~GUIDE_CHUNK Bausteinen je Agent.
# Nur der Writer (Schreiben) bleibt 1 Agent je Baustein (variable Längen, kein Kürzen, keine
# Längen-Angleichung zwischen Bausteinen).
GUIDE_CHUNK = 10
# Prüf-Schritte als Panel: CHECK_PANEL Judges je Chunk, Section beanstandet bei Mehrheit.
# Ein einzelner Judge ist bias-/sampling-anfällig; ein kleines Panel ist stabiler.
CHECK_PANEL = 3
# Lese-Prüfung: nur EINE Runde (Check + Fix). Folgerunden brachten kaum Mehrwert
# (1 Agent je Baustein prüft ohnehin fein), kosten aber extra Agenten.
LESE_RUNDEN = 1
async def _load_subbausteine(topic: str) -> dict[str, list[dict]]:
"""Subbausteine je Baustein — DB-first ({titel, stufe, relevanz}), Fallback Sidecar-Datei.
Fehlt beides → {} (Guide nimmt alles)."""
out: dict[str, list[dict]] = {}
for r in await list_subbausteine(topic):
if r["status"] == "konsens" and r["sub_titel"] and r["stufe"] in ("einfach", "mittel", "schwer"):
out.setdefault(r["baustein"], []).append(
{"titel": r["sub_titel"], "stufe": r["stufe"], "relevanz": r["relevanz"]})
if out:
return out
data = _json_datei(subbausteine_path(topic))
if not isinstance(data, dict):
return {}
for titel, subs in data.items():
if not isinstance(subs, list):
continue
gut = [s for s in subs if isinstance(s, dict) and str(s.get("titel", "")).strip()
and s.get("stufe") in ("einfach", "mittel", "schwer")]
if gut:
out[titel] = gut
return out
def _zuteilung_subs(chunk: list[dict], entries: dict[int, str], subs_by_titel: dict[str, list[dict]]) -> str:
"""Wie _zuteilung_text, aber listet pro Baustein seine Subbausteine mit Stufe."""
lines: list[str] = []
for ch in chunk:
lines.append(f"KAPITEL: {ch['title']}")
for num in ch["nums"]:
lines.append(f"- {entries[num]}")
for s in subs_by_titel.get(_titel(entries[num]), []):
lines.append(f" [{s['stufe']}] {s['titel']}")
return "\n".join(lines)
def _guide_files(content_path: Path) -> dict:
d, stem = content_path.parent, content_path.stem
return {
"auswahl": d / f"{stem}.auswahl.json",
"auswahl_check": d / f"{stem}.auswahl-check.json",
"gliederung": d / f"{stem}.gliederung.json",
"gliederung_check": d / f"{stem}.gliederung-check.json",
# chunk-/lese-check-/fix-Dateien sind dynamisch: {stem}.chunk-i.md usw.
"gliederung_slots": [d / f"{stem}.gliederung-{i}.json" for i in (1, 2, 3)],
"gliederung": d / f"{stem}.gliederung.json", # Judge-Ausgabe
# chunk-/lese-check-/fix-Dateien sind dynamisch:
# {stem}.chunk-i.md, {stem}.lese-check-r{n}-{i}.json, {stem}.fix-r{n}-{i}.md
}
@@ -57,27 +105,55 @@ def guide_slot_dateien(content_path: Path) -> list[Path]:
return [p for p in content_path.parent.glob(f"{content_path.stem}.*") if p != content_path]
def _resolve_auswahl(data, entries: dict[int, str], k_min: int, k_max: int) -> list[int] | None:
"""{"bausteine": [Titel]} → Nummern; None bei Schema-Verstoß/Drift/falschem Umfang."""
if not isinstance(data, dict) or not isinstance(data.get("bausteine"), list):
return None
idx = _titel_index(entries)
nums: list[int] = []
seen: set[int] = set()
total = unknown = 0
for t in data["bausteine"]:
total += 1
num = _titel_aufloesen(idx, t) if isinstance(t, str) else None
if num is None:
unknown += 1
elif num not in seen:
seen.add(num)
nums.append(num)
if total == 0 or (total - unknown) / total < 0.85:
return None
if len(nums) < 0.9 * k_min or len(nums) > 1.1 * k_max:
return None
return nums
def _fertig_path(content_path: Path) -> Path:
return content_path.parent / f"{content_path.stem}.fertig"
def guide_fertig_step(content_path: Path) -> int:
"""Höchster VOLL abgeschlossener Schritt-Index (Marker je Thema+Format). -1 = keiner.
Existiert die Content-Datei, sind alle Schritte fertig."""
if content_path.exists():
return len(GUIDE_STEPS) - 1
try:
return int(_fertig_path(content_path).read_text(encoding="utf-8").strip())
except (OSError, ValueError):
return -1
def _set_fertig(content_path: Path, step: int) -> None:
"""Marker auf `step` setzen — monoton (nur erhöhen), außer beim Re-Run-Reset (force)."""
if step > guide_fertig_step(content_path):
atomic_write_text(_fertig_path(content_path), str(step))
def _reset_fertig(content_path: Path, step: int) -> None:
"""Marker hart auf `step` setzen (für Re-Run ab Schritt; step kann sinken)."""
if step < 0:
_fertig_path(content_path).unlink(missing_ok=True)
else:
atomic_write_text(_fertig_path(content_path), str(step))
# Slot-Datei-Globs je Schritt (Index = GUIDE_STEPS). Stem-verankert, kollisionsfrei.
_STEP_GLOBS = (
("gliederung*",), # 0 Gliederung (inkl. Auswahl-Filter)
("inhalt-chunk-*", "inhalt-nach-*"), # 1 Inhalte (inkl. Nachrunde)
("inhalt-check-*", "inhalt-fix-*"), # 2 Inhalts-Check
("chunk-*",), # 3 Schreiben (chunk-* matcht auch chunk-nach-*)
("lese-check-*", "fix-r*"), # 4 Lese-Prüfung
)
def _reset_guide_ab_step(content_path: Path, step: int) -> None:
"""Re-Run ab Schritt: Content + alle Slot-Dateien der Schritte ≥ step löschen.
Frühere Schritte bleiben → der Resume baut ab `step` neu (alles darunter wiederverwendet)."""
content_path.unlink(missing_ok=True) # nicht mehr „done" → kein Frischstart-Wipe
d, stem = content_path.parent, content_path.stem
for globs in _STEP_GLOBS[step:]:
for pat in globs:
for p in d.glob(f"{stem}.{pat}"):
p.unlink(missing_ok=True)
_reset_fertig(content_path, step - 1) # Schritte < step gelten als fertig
def _lese_probleme_schema(data):
@@ -97,25 +173,51 @@ def _lese_probleme_schema(data):
return out or None
def _resolve_gliederung(data, entries: dict[int, str], soll_min: int, soll_max: int) -> list[dict] | None:
"""{"kapitel": [{"titel", "bausteine": [Titel]}]} → [{"title", "nums"}].
def _panel_probleme(judge_paths: list[Path], geltung: set[int], idx: dict[str, int]) -> dict[int, str]:
"""Panel-Aggregation: mehrere Judge-Outputs eines Chunks → beanstandete {num: problem}.
Eine Stimme je Judge, der eine Section nennt. Beanstandet, wenn > Hälfte der
GELIEFERTEN (valid geparsten) Judges sie nennt (3→≥2, 2→≥2, 1→≥1). Robust gegen
Einzel-Ausfall: fehlende Dateien zählen nicht mit. Problem-Text vom erstnennenden Judge.
"""
outputs = [p for p in (_lese_probleme_schema(_json_datei(j)) for j in judge_paths) if p is not None]
if not outputs:
return {}
votes: dict[int, int] = {}
problem: dict[int, str] = {}
for out in outputs:
gesehen: set[int] = set()
for item in out:
num = _titel_aufloesen(idx, item["section"])
if num is None or num not in geltung or num in gesehen:
continue
gesehen.add(num)
votes[num] = votes.get(num, 0) + 1
problem.setdefault(num, item["problem"])
schwelle = len(outputs) / 2
return {num: problem[num] for num, v in votes.items() if v > schwelle}
def _resolve_gliederung(data, entries: dict[int, str], soll_min: int, soll_max: int) -> list[dict] | None:
"""{"kapitel": [{"titel", "nummern": [1, 3, 7]}]} → [{"title", "nums"}].
Nummern sind die IDs aus `entries` (1-basiert, wie dem Agenten präsentiert).
`soll_min`/`soll_max` = erlaubte Spanne gewählter Bausteine (mit kleiner Toleranz).
"""
if not isinstance(data, dict) or not isinstance(data.get("kapitel"), list):
return None
idx = _titel_index(entries)
gueltig = set(entries)
chapters: list[dict] = []
seen: set[int] = set()
total = unknown = 0
for ch in data["kapitel"]:
if not isinstance(ch, dict) or not isinstance(ch.get("bausteine"), list):
if not isinstance(ch, dict) or not isinstance(ch.get("nummern"), list):
return None
nums = []
for t in ch["bausteine"]:
for t in ch["nummern"]:
total += 1
num = _titel_aufloesen(idx, t) if isinstance(t, str) else None
if num is None:
num = t if isinstance(t, int) and not isinstance(t, bool) else None
if num is None or num not in gueltig:
unknown += 1
elif num not in seen:
nums.append(num)
@@ -131,6 +233,20 @@ def _resolve_gliederung(data, entries: dict[int, str], soll_min: int, soll_max:
return chapters
def _fallback_gliederung(entries: dict[int, str]) -> list[dict]:
"""Deterministische Gliederung, wenn die Agenten keine liefern: ein Kapitel mit
allen gewählten Bausteinen in Reihenfolge. Garantiert vollständige Abdeckung."""
return [{"title": "Inhalte", "nums": list(entries)}]
def _mit_resten(plan: list[dict], entries: dict[int, str]) -> list[dict]:
"""Stellt sicher, dass JEDER gewählte Baustein im Plan steht — fehlende landen in
einem Kapitel „Weitere" (gegen Agenten/Judge, die Bausteine weglassen)."""
drin = {num for ch in plan for num in ch.get("nums", [])}
fehlen = [num for num in entries if num not in drin]
return [*plan, {"title": "Weitere", "nums": fehlen}] if fehlen else plan
async def _generate_sections(
guide_id: str, topic: str, format_name: str, entries: dict[int, str],
facts: str, instructions: str, provider: str,
@@ -142,165 +258,294 @@ async def _generate_sections(
ctx = GenContext(topic=topic, provider=provider, is_cancelled=is_cancelled, guide_id=guide_id)
spec = (TEMPLATES_DIR / "Format" / "Section.md").read_text(encoding="utf-8")
files = _guide_files(content_path)
bausteine_liste = "\n".join(f"- {t}" for t in entries.values())
n = len(entries)
anteil_min, anteil_max, minimum, zweck = FORMAT_ANTEIL[format_name]
k_min = min(n, max(minimum, math.ceil(anteil_min * n)))
k_max = min(n, max(k_min, math.floor(anteil_max * n)))
auswahl_auftrag = (
f"Wähle MINDESTENS {k_min} und HÖCHSTENS {k_max} der Bausteine und baue daraus {zweck}. "
"Wähle, was diesem Zweck dient — lass weg, was dafür nicht nötig ist."
)
zweck = FORMAT_ZWECK[format_name]
# Schritt 1: Auswahl — vorhandene gültige Datei wird übernommen (Resume)
auswahl = _resolve_auswahl(_json_datei(files["auswahl"]), entries, k_min, k_max)
if auswahl is None:
await _set_step(guide_id, 0, "Wähle Bausteine…")
files["auswahl"].unlink(missing_ok=True)
status, auswahl = await run_single_slot(
ctx, "Guide-Auswahl",
key=f"{guide_id}-auswahl",
prompt=_prompt(
"Guide-Auswahl",
topic=topic, format_name=format_name, bausteine=bausteine_liste,
auswahl_auftrag=auswahl_auftrag, out_path=files["auswahl"], extra=_extra(instructions),
),
role="guide", capabilities="files",
payload=lambda result: _resolve_auswahl(_json_datei(files["auswahl"]), entries, k_min, k_max),
timeout=_timeout("guide_auswahl", n),
)
if status == CANCELLED:
return None
if status == FAILED:
await _fail(guide_id, "Auswahl fehlgeschlagen")
return None
# Subbausteine je Baustein (DB-first) — früh geladen: steuert Auswahl + Sub-Filter je Format.
# Fehlt sie → {} (Fallback: Guide nimmt alles).
subs_raw = await _load_subbausteine(topic)
def auswahl_titel() -> str:
return "\n".join(f"- {_titel(entries[num])}" for num in auswahl)
def _hat_relevanz(num, art):
return any(isinstance(s, dict) and s.get("relevanz") == art for s in subs_raw.get(_titel(entries[num]), []))
def auswahl_json() -> str:
return json.dumps({"bausteine": [_titel(entries[num]) for num in auswahl]}, ensure_ascii=False)
# Schritt 2: Auswahl-Prüfung — notiert Probleme; Anpassung macht ein Auswahl-Agent
status, fixed = await _check_then_fix(
ctx, name="Auswahl", step=1,
check_key=f"{guide_id}-auswahl-check",
check_prompt=_prompt(
"Guide-Auswahl-Check",
topic=topic, format_name=format_name, auswahl_auftrag=auswahl_auftrag,
bausteine=bausteine_liste, auswahl=auswahl_titel(),
out_path=files["auswahl_check"], extra=_extra(instructions),
),
check_path=files["auswahl_check"], check_timeout=_timeout("guide_check", len(auswahl)),
fix_key=f"{guide_id}-auswahl-fix",
build_fix_prompt=lambda probleme: _prompt(
"Guide-Auswahl-Fix",
topic=topic, format_name=format_name, auswahl_auftrag=auswahl_auftrag,
bausteine=bausteine_liste, auswahl=auswahl_titel(),
probleme="\n".join(f"- {p}" for p in probleme),
out_path=files["auswahl"], extra=_extra(instructions),
),
fix_payload=lambda result: _resolve_auswahl(_json_datei(files["auswahl"]), entries, k_min, k_max),
fix_timeout=_timeout("guide_auswahl", n), fix_role="guide",
on_fix_invalid=lambda: atomic_write_text(files["auswahl"], auswahl_json()),
)
if status == CANCELLED:
# Auswahl (Teil von Gliederung). Deterministisch je Format:
# Guide=relevante Bausteine · FullGuide=alle · Rest=reine Rand-Bausteine (keine im Guide).
if format_name == "FullGuide":
auswahl = list(entries)
elif format_name == "Rest":
auswahl = [num for num in entries if not _hat_relevanz(num, "relevant")]
else: # Guide
auswahl = [num for num in entries if _hat_relevanz(num, "relevant")]
if not auswahl:
_log(topic, "Guide: keine Relevanz-Daten — alle Bausteine genommen")
auswahl = list(entries)
if not auswahl:
await _fail(guide_id, "Keine passenden Bausteine für dieses Format")
return None
if status == FAILED:
await _fail(guide_id, "Auswahl-Prüfung fehlgeschlagen")
return None
if fixed is not None:
auswahl = fixed
sel_entries = {num: entries[num] for num in auswahl}
soll = len(sel_entries)
sel_liste = "\n".join(f"- {t}" for t in sel_entries.values())
# Nummerierte Liste (ID = Baustein-Nummer aus entries) — Agenten/Judge ordnen per Nummer.
sel_liste = "\n".join(f"{num}. {t}" for num, t in sel_entries.items())
# Schritt 3: Gliederung der festen Auswahl
# Schritt 0: Gliederung — bis zu 3 Vorschläge (Grace), ein Judge merged sie. Bricht NIE ab:
# 0 gültige → Code-Fallback, 1 → direkt, ≥2 → Judge (mit Vorschlag als Rückfall).
# Gültiges gliederung.json überspringt den Schritt.
plan = _resolve_gliederung(_json_datei(files["gliederung"]), sel_entries, soll, soll)
if plan is None:
await _set_step(guide_id, 2, "Plane Gliederung…")
await _set_step(guide_id, 0, "Gliederungs-Vorschläge (3 Agenten)")
files["gliederung"].unlink(missing_ok=True)
status, plan = await run_single_slot(
ctx, "Gliederung",
key=f"{guide_id}-gliederung",
prompt=_prompt(
"Guide-Gliederung",
topic=topic, format_name=format_name, bausteine=sel_liste,
out_path=files["gliederung"], extra=_extra(instructions),
),
role="guide", capabilities="files",
payload=lambda result: _resolve_gliederung(_json_datei(files["gliederung"]), sel_entries, soll, soll),
timeout=_timeout("plan", soll),
)
if status == CANCELLED:
return None
if status == FAILED:
await _fail(guide_id, "Gliederung fehlgeschlagen")
return None
vorschlaege: list[list[dict]] = []
offen = []
for i, path in enumerate(files["gliederung_slots"], 1):
res = _resolve_gliederung(_json_datei(path), sel_entries, soll, soll)
if res is not None:
vorschlaege.append(res)
else:
offen.append((i, path))
if len(vorschlaege) < 3 and offen:
slots = [
{
"key": f"{guide_id}-gliederung-{i}",
"prompt": _prompt(
"Guide-Gliederung",
topic=topic, format_name=format_name, bausteine=sel_liste,
out_path=path, extra=_extra(instructions),
),
"role": "guide", "capabilities": "files",
"payload": (lambda result, p=path: _resolve_gliederung(_json_datei(p), sel_entries, soll, soll)),
}
for i, path in offen
]
# Quorum 1: nimm, was kommt — kein Mindestzwang, kein Abbruch.
neue = await _race(
topic, "Gliederung", slots, 1, _timeout("plan", soll),
provider, cancelled=is_cancelled, grace=KONSENS_GRACE,
)
if is_cancelled():
return None
vorschlaege += neue or []
def gliederung_text() -> str:
return "\n".join(_zuteilung_text([ch], {num: _titel(entries[num]) for num in ch["nums"]}) for ch in plan)
if not vorschlaege:
_log(topic, "Gliederung: kein gültiger Vorschlag — deterministischer Fallback")
plan = _fallback_gliederung(sel_entries)
elif len(vorschlaege) == 1:
plan = vorschlaege[0] # ein Vorschlag → kein Judge nötig
else:
await _set_step(guide_id, 0, "Gliederungen zusammenführen…")
bloecke = "\n\n".join(
f"### Vorschlag {i}\n"
+ "\n".join(f"KAPITEL: {ch['title']}\n Nummern: {', '.join(str(num) for num in ch['nums'])}" for ch in v)
for i, v in enumerate(vorschlaege, 1)
)
status, plan = await run_single_slot(
ctx, "Gliederungs-Judge",
key=f"{guide_id}-gliederung-judge",
prompt=_prompt(
"Guide-Gliederung-Judge",
topic=topic, format_name=format_name, zweck=zweck, n=len(vorschlaege),
bausteine=sel_liste, gliederungen=bloecke,
out_path=files["gliederung"], extra=_extra(instructions),
),
role="judge", capabilities="files",
payload=lambda result: _resolve_gliederung(_json_datei(files["gliederung"]), sel_entries, soll, soll),
timeout=_timeout("plan_judge", soll),
)
if status == CANCELLED:
return None
if status == FAILED or plan is None:
_log(topic, "Gliederung-Judge ohne Ergebnis — bester Vorschlag bleibt")
plan = vorschlaege[0]
def gliederung_json() -> str:
return json.dumps(
{"kapitel": [{"titel": ch["title"], "bausteine": [_titel(entries[num]) for num in ch["nums"]]} for ch in plan]},
ensure_ascii=False,
)
# Garantie: jeder gewählte Baustein steht im Plan (gegen weglassende Agenten/Judges).
plan = _mit_resten(plan, sel_entries)
_set_fertig(content_path, 0) # Gliederung steht
# Schritt 4: Gliederungs-Prüfung
status, fixed = await _check_then_fix(
ctx, name="Gliederung", step=3,
check_key=f"{guide_id}-gliederung-check",
check_prompt=_prompt(
"Guide-Gliederung-Check",
topic=topic, format_name=format_name, zweck=zweck,
auswahl=auswahl_titel(), gliederung=gliederung_text(),
out_path=files["gliederung_check"], extra=_extra(instructions),
),
check_path=files["gliederung_check"], check_timeout=_timeout("guide_check", soll),
fix_key=f"{guide_id}-gliederung-fix",
build_fix_prompt=lambda probleme: _prompt(
"Guide-Gliederung-Fix",
topic=topic, format_name=format_name,
auswahl=auswahl_titel(), gliederung=gliederung_text(),
probleme="\n".join(f"- {p}" for p in probleme),
out_path=files["gliederung"], extra=_extra(instructions),
),
fix_payload=lambda result: _resolve_gliederung(_json_datei(files["gliederung"]), sel_entries, soll, soll),
fix_timeout=_timeout("plan", soll), fix_role="guide",
on_fix_invalid=lambda: atomic_write_text(files["gliederung"], gliederung_json()),
)
if status == CANCELLED:
return None
if status == FAILED:
await _fail(guide_id, "Gliederungs-Prüfung fehlgeschlagen")
return None
if fixed is not None:
plan = fixed
# Schritt 5: Schreiben — vorhandene Chunk-Dateien werden übernommen (Resume)
# Grobe Chunks (~GUIDE_CHUNK Bausteine je Agent) für Inhalte, Inhalts-Check und Lese-Prüfung.
# Der Writer baut darunter pro Baustein (eigene feine Chunks, s.u.) → variable Längen.
total_sections = sum(len(c["nums"]) for c in plan)
chunks = _split_chunks(plan, min(WRITER_MAX, max(1, math.ceil(total_sections / WRITER_SECTIONS))))
zuteilungen = [_zuteilung_text(chunk, entries) for chunk in chunks]
chunks = _split_chunks(plan, max(1, math.ceil(total_sections / GUIDE_CHUNK)))
# Subbausteine je Baustein, je Format gefiltert: Guide=relevant · FullGuide=alle · Rest=Rand.
# Subs ohne Relevanz-Feld (Alt-Themen) zählen als „nicht Rand" (→ Guide/FullGuide).
if format_name == "FullGuide":
subs_by_titel = {t: list(subs) for t, subs in subs_raw.items()}
elif format_name == "Rest":
subs_by_titel = {t: [s for s in subs if s.get("relevanz") == "rand"] for t, subs in subs_raw.items()}
else: # Guide
subs_by_titel = {t: [s for s in subs if s.get("relevanz") != "rand"] for t, subs in subs_raw.items()}
subs_by_titel = {t: subs for t, subs in subs_by_titel.items() if subs}
zuteilungen = [_zuteilung_subs(chunk, entries, subs_by_titel) for chunk in chunks]
chunk_sizes = [sum(len(c["nums"]) for c in chunk) for chunk in chunks]
writer_count = len(zuteilungen)
paths = [content_path.parent / f"{content_path.stem}.chunk-{i}.md" for i in range(1, writer_count + 1)]
writer_count = len(chunks)
idx = _titel_index(entries)
# Schritt 2: Inhalte je Baustein identifizieren — pro Chunk ein Agent (Marker-Output, Resume).
inhalt_paths = [content_path.parent / f"{content_path.stem}.inhalt-chunk-{i}.md" for i in range(1, writer_count + 1)]
offen = [i for i, p in enumerate(inhalt_paths) if not p.exists()]
if offen:
async def melde(d, t): await _set_step(guide_id, 1, f"Sammle Inhalte {d}/{t}")
results = await _gather_fortschritt([
run_agent(
f"{guide_id}-inhalt-{i + 1}",
_prompt(
"Guide-Inhalt",
topic=topic, zuteilung=zuteilungen[i], facts=facts,
out_path=inhalt_paths[i], extra=_extra(instructions),
),
_timeout("inhalt", chunk_sizes[i]), provider=provider, role="guide", capabilities="full",
)
for i in offen
], writer_count, melde, start=writer_count - len(offen))
if is_cancelled():
return None
if not any(p.exists() for p in inhalt_paths):
await _fail(guide_id, _gather_error("Inhalts-Fehler", list(results)))
return None
inhalt_by_num: dict[int, str] = {}
for p in inhalt_paths:
if not p.exists():
continue
for sec in _parse_fragment(p.read_text(encoding="utf-8")):
num = _titel_aufloesen(idx, sec["titel"])
if num is not None and num not in inhalt_by_num and sec["md"].strip():
inhalt_by_num[num] = sec["md"]
if not inhalt_by_num:
await _fail(guide_id, "Keine Inhalte identifiziert")
return None
# Nachrunde: fehlende Bausteine (Chunk-Ausfall oder Lazy-Output) gezielt nachziehen — eine Runde.
geplant_nums = [num for ch in plan for num in ch["nums"]]
fehlend = [num for num in geplant_nums if num not in inhalt_by_num]
if fehlend:
_log(topic, f"Inhalte: {len(fehlend)} Baustein(e) fehlen — Nachrunde…")
nach_chunks = [[{"title": "Weitere", "nums": fehlend[k:k + GUIDE_CHUNK]}] for k in range(0, len(fehlend), GUIDE_CHUNK)]
nach_paths = [content_path.parent / f"{content_path.stem}.inhalt-nach-{k}.md" for k in range(1, len(nach_chunks) + 1)]
nach_offen = [k for k, p in enumerate(nach_paths) if not p.exists()]
if nach_offen:
async def melde_n(d, t): await _set_step(guide_id, 1, f"Sammle fehlende Inhalte {d}/{t}")
await _gather_fortschritt([
run_agent(
f"{guide_id}-inhalt-nach-{k + 1}",
_prompt(
"Guide-Inhalt",
topic=topic, zuteilung=_zuteilung_subs(nach_chunks[k], entries, subs_by_titel),
facts=facts, out_path=nach_paths[k], extra=_extra(instructions),
),
_timeout("inhalt", len(nach_chunks[k][0]["nums"])), provider=provider, role="guide", capabilities="full",
)
for k in nach_offen
], len(nach_chunks), melde_n, start=len(nach_chunks) - len(nach_offen))
if is_cancelled():
return None
for p in nach_paths:
if not p.exists():
continue
for sec in _parse_fragment(p.read_text(encoding="utf-8")):
num = _titel_aufloesen(idx, sec["titel"])
if num is not None and num not in inhalt_by_num and sec["md"].strip():
inhalt_by_num[num] = sec["md"]
if all(p.exists() for p in inhalt_paths):
_set_fertig(content_path, 1) # Inhalte vollständig
inhalt_chunk_nums = [[num for ch in chunk for num in ch["nums"] if num in inhalt_by_num] for chunk in chunks]
# Schritt 3: Inhalte prüfen — CHECK_PANEL Judges je Chunk, Mehrheit beanstandet.
# + beanstandete einmal überarbeiten. Resume: nur fehlende Judge-Dateien neu starten.
check_judge_paths = [
[content_path.parent / f"{content_path.stem}.inhalt-check-{i}-j{j}.json" for j in range(1, CHECK_PANEL + 1)]
for i in range(1, writer_count + 1)
]
offen_slots = [
(i, j) for i in range(writer_count) if inhalt_chunk_nums[i]
for j in range(CHECK_PANEL) if _lese_probleme_schema(_json_datei(check_judge_paths[i][j])) is None
]
if offen_slots:
await _set_step(guide_id, 2, "Prüfe Inhalte…")
sections_je_chunk = {
i: "\n\n".join(f"SECTION: {_titel(entries[num])}\n{inhalt_by_num[num]}" for num in inhalt_chunk_nums[i])
for i, _ in offen_slots
}
slots = [{
"key": f"{guide_id}-inhalt-check-{i + 1}-j{j + 1}",
"prompt": _prompt(
"Guide-Inhalt-Check",
topic=topic, format_name=format_name, sections=sections_je_chunk[i],
out_path=check_judge_paths[i][j], extra=_extra(instructions),
),
"role": "judge", "capabilities": "files",
"payload": (lambda result, p=check_judge_paths[i][j]: _lese_probleme_schema(_json_datei(p))),
} for i, j in offen_slots]
n_checks = len(slots)
upd = lambda n: asyncio.create_task(_set_step(guide_id, 2, f"Prüfe Inhalte {n}/{n_checks}"))
await _race(topic, "Inhalts-Prüfung", slots, len(slots), _timeout("inhalt_check", max(chunk_sizes)), provider, on_update=upd, cancelled=is_cancelled, grace=KONSENS_GRACE)
if is_cancelled():
return None
probleme_by_num: dict[int, str] = {}
for i in range(writer_count):
if inhalt_chunk_nums[i]:
probleme_by_num.update(_panel_probleme(check_judge_paths[i], set(inhalt_chunk_nums[i]), idx))
if probleme_by_num:
_log(topic, f"Inhalts-Prüfung: {len(probleme_by_num)} Baustein(e) beanstandet")
await _set_step(guide_id, 2, f"Überarbeite {len(probleme_by_num)} Inhalt(e)…")
fix_chunks = [[num for num in nums if num in probleme_by_num] for nums in inhalt_chunk_nums]
fix_paths = [content_path.parent / f"{content_path.stem}.inhalt-fix-{i + 1}.md" for i in range(writer_count)]
fix_offen = [i for i, nums in enumerate(fix_chunks) if nums and not fix_paths[i].exists()]
results = await asyncio.gather(*[
run_agent(
f"{guide_id}-inhalt-fix-{i + 1}",
_prompt(
"Guide-Inhalt-Fix",
topic=topic, facts=facts,
auftraege="\n\n".join(
f"SECTION: {_titel(entries[num])}\nPROBLEM: {probleme_by_num[num]}\nAKTUELL:\n{inhalt_by_num[num]}"
for num in fix_chunks[i]
),
out_path=fix_paths[i], extra=_extra(instructions),
),
_timeout("inhalt", len(fix_chunks[i])), provider=provider, role="guide", capabilities="full",
)
for i in fix_offen
], return_exceptions=True)
if is_cancelled():
return None
for p in fix_paths:
if not p.exists():
continue
for sec in _parse_fragment(p.read_text(encoding="utf-8")):
num = _titel_aufloesen(idx, sec["titel"])
if num in probleme_by_num and sec["md"].strip():
inhalt_by_num[num] = sec["md"]
_set_fertig(content_path, 2) # Inhalts-Check durch
# Schritt 4: Schreiben — Writer formuliert die geprüften Inhalte aus (Resume).
# FEINE Chunks: genau 1 Baustein je Writer → variable Längen, kein Budget-Rationieren.
def inhalte_text(chunk) -> str:
nums = [num for ch in chunk for num in ch["nums"] if num in inhalt_by_num]
return "\n\n".join(f"<!-- section: {_titel(entries[num])} -->\n{inhalt_by_num[num]}" for num in nums)
w_chunks = [[{"title": ch["title"], "nums": [num]}] for ch in plan for num in ch["nums"]]
w_zuteil = [_zuteilung_subs(c, entries, subs_by_titel) for c in w_chunks]
paths = [content_path.parent / f"{content_path.stem}.chunk-{i}.md" for i in range(1, len(w_chunks) + 1)]
offen = [i for i, p in enumerate(paths) if not p.exists()]
if offen:
await _set_step(guide_id, 4, f"Schreibe Sections ({writer_count} Writer)…" if writer_count > 1 else "Schreibe Sections")
results = await asyncio.gather(*[
async def melde(d, t): await _set_step(guide_id, 3, f"Schreibe Sections {d}/{t}")
results = await _gather_fortschritt([
run_agent(
f"{guide_id}-w{i + 1}",
_prompt(
"Guide-Writer",
topic=topic, format_name=format_name, zuteilung=zuteilungen[i],
facts=facts, spec=spec, out_path=paths[i], extra=_extra(instructions),
topic=topic, format_name=format_name, zuteilung=w_zuteil[i],
inhalte=inhalte_text(w_chunks[i]),
spec=spec, out_path=paths[i], extra=_extra(instructions),
),
_timeout("writer", chunk_sizes[i]), provider=provider, role="guide", capabilities="full",
_timeout("writer", 1), provider=provider, role="guide", capabilities="files",
)
for i in offen
], return_exceptions=True)
], len(w_chunks), melde, start=len(w_chunks) - len(offen))
if is_cancelled():
return None
for i, r in zip(offen, results):
@@ -314,7 +559,6 @@ async def _generate_sections(
await _fail(guide_id, _gather_error("Writer-Fehler", list(results)))
return None
idx = _titel_index(entries)
by_num: dict[int, dict] = {}
for p in paths:
if not p.exists():
@@ -329,61 +573,117 @@ async def _generate_sections(
await _fail(guide_id, "Keine Sections in der Writer-Ausgabe gefunden")
return None
# Schritt 6: Lese-Prüfung pro Writer-Paket — Fix beauftragt Writer nur mit beanstandeten Sections
# Nachrunde: fehlende Sections (Writer-Ausfall) gezielt nachschreiben — eine Runde.
nach_fehlend = [num for num in geplant_nums if num not in by_num]
if nach_fehlend:
_log(topic, f"Schreiben: {len(nach_fehlend)} Section(s) fehlen — Nachrunde…")
nw_chunks = [[{"title": "Weitere", "nums": [num]}] for num in nach_fehlend]
nw_paths = [content_path.parent / f"{content_path.stem}.chunk-nach-{k}.md" for k in range(1, len(nw_chunks) + 1)]
nw_offen = [k for k, p in enumerate(nw_paths) if not p.exists()]
if nw_offen:
async def melde_nw(d, t): await _set_step(guide_id, 3, f"Schreibe fehlende Sections {d}/{t}")
await _gather_fortschritt([
run_agent(
f"{guide_id}-w-nach-{k + 1}",
_prompt(
"Guide-Writer",
topic=topic, format_name=format_name, zuteilung=_zuteilung_subs(nw_chunks[k], entries, subs_by_titel),
inhalte=inhalte_text(nw_chunks[k]), spec=spec, out_path=nw_paths[k], extra=_extra(instructions),
),
_timeout("writer", 1), provider=provider, role="guide", capabilities="files",
)
for k in nw_offen
], len(nw_chunks), melde_nw, start=len(nw_chunks) - len(nw_offen))
if is_cancelled():
return None
for p in nw_paths:
if not p.exists():
continue
for sec in _parse_fragment(p.read_text(encoding="utf-8")):
num = _titel_aufloesen(idx, sec["titel"])
if num is not None and num not in by_num and sec["md"].strip():
by_num[num] = sec
if all(p.exists() for p in paths):
_set_fertig(content_path, 3) # Schreiben vollständig
# Schritt 3: Lese-Prüfungs-Loop — Check pro Writer-Paket, Fix nur für
# beanstandete Sections; Folgerunden prüfen NUR die ersetzten Sections.
# Nach dem Runden-Cap bleiben offene Beanstandungen stehen.
chunk_nums = [[num for ch in chunk for num in ch["nums"] if num in by_num] for chunk in chunks]
check_paths = [content_path.parent / f"{content_path.stem}.lese-check-{i}.json" for i in range(1, writer_count + 1)]
offen_checks = [i for i, p in enumerate(check_paths) if _lese_probleme_schema(_json_datei(p)) is None and chunk_nums[i]]
if offen_checks:
await _set_step(guide_id, 5, f"Prüfe Lesbarkeit ({len(offen_checks)} Prüfer)…" if len(offen_checks) > 1 else "Prüfe Lesbarkeit…")
def sections_text(nums: list[int]) -> str:
return "\n\n".join(f"SECTION: {_titel(entries[num])}\n{by_num[num]['md']}" for num in nums)
def sections_text(nums: list[int]) -> str:
return "\n\n".join(f"SECTION: {_titel(entries[num])}\n{by_num[num]['md']}" for num in nums)
slots = [{
"key": f"{guide_id}-lese-check-{i + 1}",
"prompt": _prompt(
"Guide-Lese-Check",
topic=topic, format_name=format_name, spec=spec,
sections=sections_text(chunk_nums[i]),
out_path=check_paths[i], extra=_extra(instructions),
),
"role": "fast", "capabilities": "files",
"payload": (lambda result, p=check_paths[i]: _lese_probleme_schema(_json_datei(p))),
} for i in offen_checks]
res = await _race(topic, "Lese-Prüfung", slots, len(slots), _timeout("lese_check", max(chunk_sizes)), provider, cancelled=is_cancelled)
if is_cancelled():
return None
if res is None:
await _fail(guide_id, "Lese-Prüfung fehlgeschlagen")
return None
def auftraege_text(nums: list[int], probleme: dict[int, str]) -> str:
return "\n\n".join(
f"SECTION: {_titel(entries[num])}\nPROBLEM: {probleme[num]}\nAKTUELLER INHALT:\n{by_num[num]['md']}"
for num in nums
)
probleme_by_num: dict[int, str] = {}
for p in check_paths:
for item in (_lese_probleme_schema(_json_datei(p)) or []):
num = _titel_aufloesen(idx, item["section"])
if num in by_num and num not in probleme_by_num:
probleme_by_num[num] = item["problem"]
scope = chunk_nums
for runde in range(1, LESE_RUNDEN + 1):
# CHECK_PANEL Judges je Paket; Mehrheit beanstandet. Aggregation robust gegen Einzel-Ausfall.
check_judge_paths = [
[content_path.parent / f"{content_path.stem}.lese-check-r{runde}-{i}-j{j}.json" for j in range(1, CHECK_PANEL + 1)]
for i in range(1, writer_count + 1)
]
offen_slots = [
(i, j) for i in range(writer_count) if scope[i]
for j in range(CHECK_PANEL) if _lese_probleme_schema(_json_datei(check_judge_paths[i][j])) is None
]
if offen_slots:
await _set_step(guide_id, 4, "Prüfe Lesbarkeit…")
sections_je_chunk = {i: sections_text(scope[i]) for i, _ in offen_slots}
slots = [{
"key": f"{guide_id}-lese-check-r{runde}-{i + 1}-j{j + 1}",
"prompt": _prompt(
"Guide-Lese-Check",
topic=topic, format_name=format_name, spec=spec,
sections=sections_je_chunk[i],
out_path=check_judge_paths[i][j], extra=_extra(instructions),
),
"role": "judge", "capabilities": "files",
"payload": (lambda result, p=check_judge_paths[i][j]: _lese_probleme_schema(_json_datei(p))),
} for i, j in offen_slots]
n_checks = len(slots)
upd = lambda n: asyncio.create_task(_set_step(guide_id, 4, f"Prüfe Lesbarkeit {n}/{n_checks}"))
res = await _race(topic, f"Lese-Prüfung r{runde}", slots, len(slots), _timeout("lese_check", max(chunk_sizes)), provider, on_update=upd, cancelled=is_cancelled, grace=KONSENS_GRACE)
if is_cancelled():
return None
if res is None:
_log(topic, f"Lese-Prüfung Runde {runde}: kein volles Quorum — vorhandene Judges aggregiert")
if probleme_by_num:
_log(topic, f"Lese-Prüfung: {len(probleme_by_num)} Section(s) beanstandet")
await _set_step(guide_id, 5, f"Überarbeite {len(probleme_by_num)} Section(s)…")
probleme_by_num: dict[int, str] = {}
for i in range(writer_count):
if scope[i]:
probleme_by_num.update(_panel_probleme(check_judge_paths[i], set(scope[i]), idx))
# Deterministisches Lesbarkeits-Gate: zu schwere Sections in dieselbe
# Überarbeitung einreihen (LLM-Beanstandung hat Vorrang). Gate aus → no-op.
if LESBARKEIT_AKTIV:
md_by_num = {num: by_num[num]["md"] for nums in scope for num in nums if num in by_num}
hinweise = await asyncio.to_thread(lesbarkeit.bewerte_sections, md_by_num)
if hinweise:
_log(topic, f"Lesbarkeit: {len(hinweise)} Section(s) zu schwer")
for num, hinweis in hinweise.items():
probleme_by_num.setdefault(num, hinweis)
if not probleme_by_num:
break
_log(topic, f"Lese-Prüfung Runde {runde}: {len(probleme_by_num)} Section(s) beanstandet")
await _set_step(guide_id, 4, f"Überarbeite {len(probleme_by_num)} Section(s) (Runde {runde})…")
fix_chunks = [[num for num in nums if num in probleme_by_num] for nums in chunk_nums]
fix_offen = [i for i, nums in enumerate(fix_chunks) if nums]
fix_paths = [content_path.parent / f"{content_path.stem}.fix-{i + 1}.md" for i in range(writer_count)]
def auftraege_text(nums: list[int]) -> str:
return "\n\n".join(
f"SECTION: {_titel(entries[num])}\nPROBLEM: {probleme_by_num[num]}\nAKTUELLER INHALT:\n{by_num[num]['md']}"
for num in nums
)
fix_paths = [content_path.parent / f"{content_path.stem}.fix-r{runde}-{i + 1}.md" for i in range(writer_count)]
fix_offen = [i for i, nums in enumerate(fix_chunks) if nums and not fix_paths[i].exists()]
results = await asyncio.gather(*[
run_agent(
f"{guide_id}-fix-w{i + 1}",
f"{guide_id}-fix-r{runde}-w{i + 1}",
_prompt(
"Guide-Sections-Fix",
topic=topic, format_name=format_name, facts=facts, spec=spec,
auftraege=auftraege_text(fix_chunks[i]),
auftraege=auftraege_text(fix_chunks[i], probleme_by_num),
out_path=fix_paths[i], extra=_extra(instructions),
),
_timeout("writer", len(fix_chunks[i])), provider=provider, role="guide", capabilities="full",
@@ -394,23 +694,41 @@ async def _generate_sections(
return None
for i, r in zip(fix_offen, results):
if isinstance(r, BaseException) or (not isinstance(r, BaseException) and r[0] != 0):
_log(topic, f"Sections-Fix {i + 1} fehlgeschlagen — Original bleibt")
ersetzt = 0
for i in fix_offen:
if not fix_paths[i].exists():
_log(topic, f"Sections-Fix {i + 1} (Runde {runde}) fehlgeschlagen — Original bleibt")
ersetzt: set[int] = set()
for p in fix_paths:
if not p.exists():
continue
for sec in _parse_fragment(fix_paths[i].read_text(encoding="utf-8")):
for sec in _parse_fragment(p.read_text(encoding="utf-8")):
num = _titel_aufloesen(idx, sec["titel"])
if num in probleme_by_num and sec["md"].strip():
by_num[num] = sec
ersetzt += 1
_log(topic, f"Lese-Prüfung: {ersetzt} Section(s) überarbeitet")
ersetzt.add(num)
_log(topic, f"Lese-Prüfung Runde {runde}: {len(ersetzt)} Section(s) überarbeitet")
if not ersetzt:
break
if runde == LESE_RUNDEN:
_log(topic, f"Lese-Prüfung: 1 Runde — Überarbeitung bleibt ungeprüft")
break
scope = [[num for num in nums if num in ersetzt] for nums in chunk_nums]
_set_fertig(content_path, 4) # Lese-Prüfung durch
# Prüfbar = Format hat Prüfung UND Baustein hat ≥1 relevanten Subbaustein.
# Guide ist immer prüfbar (auch ohne Relevanz-Daten, Fallback = alles).
def _pruefbar(num):
if format_name == "Guide":
return True
if format_name == "FullGuide":
return any(isinstance(s, dict) and s.get("relevanz") == "relevant"
for s in subs_raw.get(_titel(entries[num]), []))
return False # Rest u.a. → reine Lese-Sections
await _set_progress(guide_id, "Setze zusammen…")
chapters: list[dict] = []
for ch in plan:
sections = [
{"num": num, "title": _titel(entries[num]), "md": by_num[num]["md"]}
{"num": num, "title": _titel(entries[num]), "md": by_num[num]["md"],
"kompakt": by_num[num].get("kompakt", ""), "pruefbar": _pruefbar(num)}
for num in ch["nums"] if num in by_num
]
if sections:
@@ -438,14 +756,14 @@ async def reconcile_guides() -> None:
await update_guide(g["id"], status="error", error_msg="Inhalt fehlt — neu generieren", updated_at=now)
async def generate_guide(guide_id: str, topic: str, format_name: str, instructions: str = "", provider: str = DEFAULT_PROVIDER) -> None:
async def generate_guide(guide_id: str, topic: str, format_name: str, instructions: str = "", provider: str = DEFAULT_PROVIDER, ab_step: int | None = None) -> None:
async with _semaphore:
now = datetime.now(timezone.utc).isoformat()
await update_guide(guide_id, status="generating", progress="Starte…", updated_at=now)
content_path = guide_content_path(topic, format_name)
content_path.parent.mkdir(parents=True, exist_ok=True)
project = project_dir(topic) if project_dir(topic).is_dir() else None
project = quelle_ordner(topic) # Ordner-Quelle (projekt/uni/link) → Pfad, sonst None
try:
if is_guide_cancelled(guide_id):
@@ -454,29 +772,37 @@ async def generate_guide(guide_id: str, topic: str, format_name: str, instructio
if project:
await asyncio.to_thread(_pdfs_konvertieren, project)
# „Neu erstellen": fertiger Guide → kompletter Frischstart.
# Re-Run ab Schritt: Content + Slots ab `ab_step` löschen, Rest bleibt → Resume baut ab dort.
# Sonst „Neu erstellen": fertiger Guide → kompletter Frischstart.
# Sonst sind Schritt-Dateien Reste eines Abbruchs/Fehlers → Resume.
if content_path.exists():
if ab_step is not None:
_reset_guide_ab_step(content_path, ab_step)
elif content_path.exists():
for p_alt in guide_slot_dateien(content_path):
p_alt.unlink(missing_ok=True)
if format_name == "OnePager":
chapters = await _generate_onepager(guide_id, topic, instructions, provider, project, content_path)
else:
alle = _lade_bausteine(bausteine_path(topic).read_text(encoding="utf-8"))
if not alle:
await _fail(guide_id, "Keine Bausteine gefunden")
return
entries = _eindeutige_titel(alle)
facts = _prompt("Guide-Fakten-Projekt", project=project) if project else _prompt("Guide-Fakten-Thema")
chapters = await _generate_sections(
guide_id, topic, format_name, entries,
facts, instructions, provider, content_path,
)
bs = await list_bausteine(topic, status="konsens")
if bs:
alle = {i: (f"{b['titel']}{b['beschreibung']}" if b["beschreibung"] else b["titel"])
for i, b in enumerate(bs, 1)}
else: # Fallback: bausteine.md (Alt-Themen)
bp = bausteine_path(topic)
alle = _lade_bausteine(bp.read_text(encoding="utf-8")) if bp.exists() else {}
if not alle:
await _fail(guide_id, "Keine Bausteine gefunden")
return
entries = _eindeutige_titel(alle)
facts = _prompt("Guide-Fakten-Projekt", project=project) if project else _prompt("Guide-Fakten-Thema")
chapters = await _generate_sections(
guide_id, topic, format_name, entries,
facts, instructions, provider, content_path,
)
if chapters is None or is_guide_cancelled(guide_id):
return
content = {"topic": topic, "format": format_name, "chapters": chapters}
atomic_write_json(content_path, {"topic": topic, "format": format_name, "chapters": chapters}, indent=1)
atomic_write_json(content_path, content, indent=1) # Brücke (Resume/Fallback)
await set_guide_content(topic, format_name, json.dumps(content, ensure_ascii=False))
now = datetime.now(timezone.utc).isoformat()
await update_guide(guide_id, status="done", progress=None, step=None, updated_at=now)

537
backend/lernen.py Normal file
View File

@@ -0,0 +1,537 @@
"""Baustein-Lernen: Vertiefung, Bausteinchat und Prüfung zu einzelnen Guide-Sections.
Alle Aufrufe sind interaktiv (stdout-Antwort, lane "interactive") und stateless —
der Chat-/Prüfungs-Verlauf kommt vom Frontend, persistiert wird nur der
Prüfungs-Zähler (DB) und die Vertiefung (DB).
"""
import logging
import random
import re
import uuid
from datetime import datetime, timezone
from agents import run_agent
from config import DEFAULT_PROVIDER
from database import create_element, list_elements
from elements import generate_element
from jsonio import parse_json_text as _parse_json_text
from pipeline import _prompt, _probleme_schema
from textkit import _norm_titel
log = logging.getLogger("creator.lernen")
# Meilensteine je Baustein (Score 030). Form ergibt sich aus dem Score-Band:
# 06 Quiz, 712 Lückentext, 1318 Erklären, 1930 frei.
NOETIG = 6 # MiniGuide-Meilenstein → „absolviert" (Quiz-Band durch)
MASTERY = 12 # Guide-Meilenstein → „verstanden" (Lückentext-Band durch)
MEISTERN = 18 # ProGuide-Meilenstein → „gemeistert" (Erklären-Band durch, Thema fertig)
MAX_VOLL = 30 # FullGuide-Maximum (freie Formwahl ab 18)
CHAT_TIMEOUT = 240
PRUEFUNG_TIMEOUT = 120 # kurze JSON-Turns; deckelt die Serien-Latenz pro Prüfungs-Schritt
GRUENDLICH_TIMEOUT = 600 # „Gründlich prüfen": starkes Modell (role guide) braucht länger
KRITIK_MAX_RUNDEN = 2 # Generator → Kritiker → ggf. Neu, höchstens so oft
# Fragetypen für Active Recall — pro Frage einer, zufällig gewählt. Schafft Vielfalt.
FRAGETYPEN = {
"abruf": "Free Recall: Lass den Lerner die Kernidee frei aus dem Kopf erklären (offene Verständnisfrage).",
"punkt": "Cued Recall: Frag gezielt EIN konkretes Detail oder eine Abgrenzung ab.",
"warum": "Warum-Frage: Frag nach dem Grund/Mechanismus — warum funktioniert oder gilt das so?",
"anwendung": "Anwendung: Lass das Konzept auf EIN kurzes, neues Beispiel/Szenario anwenden.",
"pruefen": "Bei Code-/Tool-Themen: kleinen Schnipsel zeigen — Output vorhersagen ODER den Fehler finden. Kein Code-Thema → stattdessen eine Anwendungsfrage.",
}
# Antwort-Niveau (wie sehr der Kern getroffen ist) → Punkt-Delta.
NIVEAUS = {
"kaum": -1, # < 25 % richtig
"teilweise": 0, # 2549 %
"solide": 1, # 5074 %
"stark": 2, # 7599 %
"komplett": 3, # 100 %
}
def score_berechnen(basis: int, delta: int, floor: int, cap: int) -> int:
"""Neuer Score · driftfrei aus der Basis. `delta` liefert die Route je Prüfungsform
(Quiz +1/+3, Lückentext +1/+3, Erklären 1…+3; falsch 1). Auf [floor, cap] geklemmt.
floor = höchster erreichter Meilenstein (6/12/18); cap = Max des höchsten Formats.
Am Format-Deckel (basis ≥ cap) eingefroren: „Max" — weder + noch , bis das nächste
Format freischaltet. Bei cap 30 (FullGuide) bleibt 30 endgültig."""
if basis >= cap:
return basis
return max(floor, min(cap, basis + delta))
def _zufall_malus(basis: int, delta: int) -> int:
"""Im Zufallband (Score ≥ MEISTERN=18, FullGuide) kostet jeder Fehler 3 statt 1 —
die 30 erreicht man nur mit Sicherheit. Richtige Antworten bleiben unverändert."""
return -3 if (basis >= MEISTERN and delta < 0) else delta
def floor_aus_meilensteinen(absolviert: bool, verstanden: bool, gemeistert: bool) -> int:
"""Untergrenze des Scores: ein erreichter Meilenstein hält (kein Rückfall ins Band davor)."""
if gemeistert:
return MEISTERN
if verstanden:
return MASTERY
if absolviert:
return NOETIG
return 0
def _transcript(messages: list[dict]) -> str:
return "\n".join(
f"{'Nutzer' if m.get('role') == 'user' else 'Assistent'}: {m.get('content', '')}"
for m in messages
) or "(leer)"
async def baustein_chat(topic: str, baustein: str, section: str, kompakt: str | None, messages: list[dict], provider: str = DEFAULT_PROVIDER) -> str:
try:
prompt = _prompt(
"Baustein-Chat",
topic=topic, baustein=baustein,
section_block=section.strip() or "(keine Guide-Fassung übergeben)",
kompakt_block=(kompakt or "").strip() or "(keine)",
transcript=_transcript(messages),
)
returncode, stdout, _ = await run_agent(
"bausteinchat-" + str(uuid.uuid4()), prompt, CHAT_TIMEOUT,
provider=provider, role="fast", capabilities="none", lane="interactive",
)
if returncode != 0:
return "Entschuldigung, das hat nicht geklappt. Bitte versuche es erneut."
reply = stdout.strip()
return reply or "Entschuldigung, ich habe keine Antwort erhalten."
except Exception:
log.warning("[%s] Baustein-Chat fehlgeschlagen (%s)", topic, baustein, exc_info=True)
return "Entschuldigung, das hat nicht geklappt. Bitte versuche es erneut."
def _frage_schema(data) -> dict | None:
"""{"frage": str} · sonst None."""
if not isinstance(data, dict):
return None
frage = str(data.get("frage", "")).strip()
return {"frage": frage} if frage else None
def _bewertung_schema(data) -> dict | None:
"""{"feedback": str, "niveau": ∈ NIVEAUS} · sonst None."""
if not isinstance(data, dict):
return None
feedback = str(data.get("feedback", "")).strip()
niveau = data.get("niveau")
if not feedback or niveau not in NIVEAUS:
return None
return {"feedback": feedback, "niveau": niveau}
async def _gen_call(name: str, role: str, schema, provider: str, timeout: int = PRUEFUNG_TIMEOUT, **kwargs) -> dict | None:
"""Generator-Agent: Template füllen, laufen lassen, per schema parsen · None bei Fehler."""
returncode, stdout, _ = await run_agent(
name.lower() + "-" + str(uuid.uuid4()), _prompt(name, **kwargs), timeout,
provider=provider, role=role, capabilities="none", lane="interactive",
)
return schema(_parse_json_text(stdout)) if returncode == 0 else None
async def _kritik_call(name: str, provider: str, role: str = "judge", timeout: int = PRUEFUNG_TIMEOUT, **kwargs) -> list[str]:
"""Kritiker-Agent (Default role judge): leere Liste = in Ordnung. Fail-open: Ausfall des
Kritikers darf den Turn nicht blockieren, also dann ebenfalls leere Liste."""
returncode, stdout, _ = await run_agent(
name.lower() + "-" + str(uuid.uuid4()), _prompt(name, **kwargs), timeout,
provider=provider, role=role, capabilities="none", lane="interactive",
)
if returncode != 0:
return []
return _probleme_schema(_parse_json_text(stdout)) or []
def _kritik_block(vorversion: str, probleme: list[str]) -> str:
punkte = "\n".join(f"- {p}" for p in probleme)
return (
f"Deine vorige Fassung war:\n«{vorversion}»\n\n"
f"Der Prüfer bemängelt:\n{punkte}\n\nBehebe diese Punkte."
)
def _bewertung_text(bew: dict) -> str:
return f"Niveau: {bew['niveau']}\nFeedback: {bew['feedback']}"
# Deterministischer Guard gegen Doppelfragen — der KI-Kritiker übersieht „…, und welchen…".
_FRAGEWORT = r"(was|welche[rsnm]?|wie|wieso|warum|wofür|wozu|wann|wo|wer|wem|wen|nenne)"
_DOPPEL_RE = re.compile(r"[,;]?\s+(und|sowie|außerdem|bzw\.?)\s+" + _FRAGEWORT + r"\b", re.IGNORECASE)
def _doppelfrage_mangel(frage: str) -> str | None:
"""Erkennt zwei verkettete Fragen. None = ok. Flaggt NUR 'und/sowie' + Fragewort."""
if frage.count("?") > 1:
return "Mehr als ein Fragezeichen — stelle GENAU EINE Frage."
if _DOPPEL_RE.search(frage):
return "Zwei Fragen mit 'und'/'sowie' verkettet — stelle GENAU EINE Frage, eine Sache."
return None
async def _frage_mit_kritik(
topic: str, baustein: str, section_block: str, kompakt_block: str,
transcript: str, vermeide_block: str, typ_block: str, fokus_block: str, provider: str,
) -> str | None:
"""Frage generieren, vom Kritiker prüfen lassen, bei Mängeln neu (max KRITIK_MAX_RUNDEN)."""
kritik_block = "(keine)"
frage = None
for _ in range(KRITIK_MAX_RUNDEN):
data = await _gen_call(
"Baustein-Frage", "fast", _frage_schema, provider,
topic=topic, baustein=baustein, section_block=section_block,
kompakt_block=kompakt_block, transcript=transcript, vermeide_block=vermeide_block,
typ_block=typ_block, fokus_block=fokus_block, kritik_block=kritik_block,
)
if data is None:
return None
frage = data["frage"]
probleme = await _kritik_call(
"Baustein-Frage-Kritik", provider, role="guide", # starke KI prüft die Regeln
topic=topic, baustein=baustein, section_block=section_block,
kompakt_block=kompakt_block, transcript=transcript, vermeide_block=vermeide_block,
typ_block=typ_block, fokus_block=fokus_block, frage=frage,
)
hart = _doppelfrage_mangel(frage) # erzwingt Neugenerierung, auch wenn der KI-Kritiker es übersah
if hart:
probleme = [hart, *(probleme or [])]
if not probleme:
return frage
kritik_block = _kritik_block(frage, probleme)
return frage # best-effort nach der letzten Runde
async def _bewertung_mit_kritik(
topic: str, baustein: str, section_block: str, kompakt_block: str,
frage: str, transcript: str, begruendung_block: str, provider: str, role: str = "judge",
) -> dict | None:
"""Antwort bewerten (Niveau), vom Kritiker prüfen lassen, bei Fehlurteil neu.
`frage` ankert die geprüfte Frage; der Dialog (transcript) liefert Antwort + Diskussion.
`begruendung_block` = optionale Unzufriedenheit des Lerners (nur bei „Gründlich prüfen").
`role` = "judge" (schnell) oder "guide" (gründlich, starkes Modell mit Thinking).
"""
timeout = GRUENDLICH_TIMEOUT if role == "guide" else PRUEFUNG_TIMEOUT
kritik_block = "(keine)"
bew = None
for _ in range(KRITIK_MAX_RUNDEN):
bew = await _gen_call(
"Baustein-Bewertung", role, _bewertung_schema, provider, timeout,
topic=topic, baustein=baustein, section_block=section_block,
kompakt_block=kompakt_block, frage=frage, transcript=transcript,
begruendung_block=begruendung_block, kritik_block=kritik_block,
)
if bew is None:
return None
probleme = await _kritik_call(
"Baustein-Bewertung-Kritik", provider, role=role, timeout=timeout,
topic=topic, baustein=baustein, section_block=section_block,
kompakt_block=kompakt_block, frage=frage, transcript=transcript,
bewertung_block=_bewertung_text(bew),
)
if not probleme:
return bew
kritik_block = _kritik_block(_bewertung_text(bew), probleme)
return bew # best-effort nach der letzten Runde
def _bloecke(section: str, kompakt: str | None) -> tuple[str, str]:
return (
section.strip() or "(keine Guide-Fassung übergeben)",
(kompakt or "").strip() or "(keine)",
)
def _vermeide_block(vermeide: list[str] | None) -> str:
eintraege = [f.strip() for f in (vermeide or []) if f and f.strip()]
return "\n".join(f"- {f}" for f in eintraege) or "(keine)"
async def pruefung_frage(
topic: str, baustein: str, section: str, kompakt: str | None,
messages: list[dict], subbausteine: list[str] | None = None,
vermeide: list[str] | None = None, provider: str = DEFAULT_PROVIDER,
) -> str | None:
"""Aktion 'frage': eine Frage generieren — zufälliger Typ zu einem zufälligen Subbaustein,
dann Kritiker (Generator → Kritiker sequenziell) · None bei Fehler."""
try:
section_block, kompakt_block = _bloecke(section, kompakt)
transcript = _transcript(messages) if messages else "(leer)"
typ_block = FRAGETYPEN[random.choice(list(FRAGETYPEN))]
subs = [s for s in (subbausteine or []) if s and s.strip()]
fokus = random.choice(subs) if subs else ""
fokus_block = (
f"Konzentriere die Frage auf diesen Subbaustein: „{fokus}\"" if fokus
else "(ganzer Baustein — kein bestimmter Subbaustein)"
)
return await _frage_mit_kritik(
topic, baustein, section_block, kompakt_block, transcript,
_vermeide_block(vermeide), typ_block, fokus_block, provider,
)
except Exception:
log.warning("[%s] Frage fehlgeschlagen (%s)", topic, baustein, exc_info=True)
return None
async def pruefung_frage_variante(
topic: str, baustein: str, section: str, kompakt: str | None,
muster: str, provider: str = DEFAULT_PROVIDER,
) -> str | None:
"""Aktion 'frage' mit Muster: aus einem vordefinierten Muster eine konkrete, im
Wortlaut leicht andere Frage formulieren. Kein Kritiker (Muster ist build-geprüft).
Stil-Guard bleibt als billiger Schutz gegen Doppelfragen · None bei Fehler."""
try:
section_block, kompakt_block = _bloecke(section, kompakt)
data = await _gen_call(
"Baustein-Frage-Variante", "fast", _frage_schema, provider,
topic=topic, baustein=baustein, section_block=section_block,
kompakt_block=kompakt_block, muster=muster,
)
if data is None:
return None
return data["frage"]
except Exception:
log.warning("[%s] Frage-Variante fehlgeschlagen (%s)", topic, baustein, exc_info=True)
return None
def _optionen_schema(opts) -> list[dict] | None:
"""[{text, korrekt}]×4 → validierte Liste · sonst None."""
if not isinstance(opts, list) or len(opts) != 4:
return None
out = []
for o in opts:
if not isinstance(o, dict):
return None
text = str(o.get("text", "")).strip()
korrekt = o.get("korrekt")
if not text or not isinstance(korrekt, bool):
return None
out.append({"text": text, "korrekt": korrekt})
return out
def _quiz_schema(data, schwer: bool = False) -> dict | None:
"""{"frage": str, "optionen": [{text, korrekt}]×4} → validiert · sonst None.
leicht: genau 1 richtig (Single) · schwer: mindestens 2 richtig (mehrdeutig)."""
if not isinstance(data, dict):
return None
frage = str(data.get("frage", "")).strip()
out = _optionen_schema(data.get("optionen"))
if not frage or out is None:
return None
n_korrekt = sum(o["korrekt"] for o in out)
if (n_korrekt < 2 if schwer else n_korrekt != 1):
return None
return {"frage": frage, "optionen": out}
def _lueckwahl_schema(data) -> dict | None:
"""{"satz": str (mit ___), "optionen": [{text, korrekt}]×4} → genau 1 korrekt · sonst None."""
if not isinstance(data, dict):
return None
satz = str(data.get("satz", "")).strip()
out = _optionen_schema(data.get("optionen"))
if not satz or "___" not in satz or out is None or sum(o["korrekt"] for o in out) != 1:
return None
return {"satz": satz, "optionen": out}
async def quiz_generieren(
topic: str, baustein: str, section: str, kompakt: str | None,
muster: str, schwer: bool = False, provider: str = DEFAULT_PROVIDER,
) -> dict | None:
"""Aus einem Muster eine Multiple-Choice-Frage. schwer: 04 richtig (Multi).
leicht: genau 1 richtig (Single). Starkes Modell (role guide) für korrekte Flags.
{frage, optionen:[{text,korrekt}]} · None bei Fehler."""
try:
section_block, kompakt_block = _bloecke(section, kompakt)
anzahl_block = (
"MINDESTENS ZWEI Optionen sind richtig (gern auch drei) — NIEMALS nur eine. Mische bewusst, welche."
if schwer else
"GENAU EINE Option ist richtig, die anderen drei sind klar falsch."
)
return await _gen_call(
"Baustein-Quiz", "guide", (lambda d: _quiz_schema(d, schwer=schwer)), provider,
topic=topic, baustein=baustein, section_block=section_block,
kompakt_block=kompakt_block, muster=muster, anzahl_block=anzahl_block,
)
except Exception:
log.warning("[%s] Quiz-Frage fehlgeschlagen (%s)", topic, baustein, exc_info=True)
return None
async def lueckwahl_generieren(
topic: str, baustein: str, section: str, kompakt: str | None,
muster: str, provider: str = DEFAULT_PROVIDER,
) -> dict | None:
"""Lückentext mit Auswahl (leichte Variante): Satz mit ___ + 4 Begriffe, genau 1 richtig.
{satz, optionen:[{text,korrekt}]} · None bei Fehler."""
try:
section_block, kompakt_block = _bloecke(section, kompakt)
return await _gen_call(
"Baustein-Lueckwahl", "guide", _lueckwahl_schema, provider,
topic=topic, baustein=baustein, section_block=section_block,
kompakt_block=kompakt_block, muster=muster,
)
except Exception:
log.warning("[%s] Lückentext-Auswahl fehlgeschlagen (%s)", topic, baustein, exc_info=True)
return None
def _lueck_schema(data) -> dict | None:
"""{"satz": str (mit ___), "loesung": str, "alternativen": [str]} → validiert · sonst None."""
if not isinstance(data, dict):
return None
satz = str(data.get("satz", "")).strip()
loesung = str(data.get("loesung", "")).strip()
alt = data.get("alternativen", [])
if not satz or "___" not in satz or not loesung:
return None
alternativen = [str(a).strip() for a in alt if isinstance(a, str) and str(a).strip()] if isinstance(alt, list) else []
return {"satz": satz, "loesung": loesung, "alternativen": alternativen}
async def lueckentext_generieren(
topic: str, baustein: str, section: str, kompakt: str | None,
muster: str, provider: str = DEFAULT_PROVIDER,
) -> dict | None:
"""Aus einem Muster eine Lückentext-Aufgabe (Satz mit ___, Lösung, Synonyme).
{satz, loesung, alternativen} · None bei Fehler."""
try:
section_block, kompakt_block = _bloecke(section, kompakt)
return await _gen_call(
"Baustein-Lueckentext", "fast", _lueck_schema, provider,
topic=topic, baustein=baustein, section_block=section_block,
kompakt_block=kompakt_block, muster=muster,
)
except Exception:
log.warning("[%s] Lückentext-Frage fehlgeschlagen (%s)", topic, baustein, exc_info=True)
return None
def _norm_begriff(t: str) -> str:
return re.sub(r"[^\wäöüß]", "", str(t or "").lower())
def _richtig_schema(data) -> dict | None:
if not isinstance(data, dict) or not isinstance(data.get("richtig"), bool):
return None
return {"richtig": data["richtig"]}
async def lueckentext_pruefen(
topic: str, baustein: str, satz: str, loesung: str, alternativen: list[str],
eingabe: str, provider: str = DEFAULT_PROVIDER,
) -> bool:
"""Lückentext-Antwort prüfen: erst normalisierter Vergleich (Lösung + Synonyme),
sonst 1 KI-Call für Synonym-Toleranz. Fail-open zu RICHTIG nur bei exaktem Match."""
if not eingabe.strip():
return False
norm = _norm_begriff(eingabe)
if norm and norm in {_norm_begriff(loesung), *(_norm_begriff(a) for a in alternativen)}:
return True
data = await _gen_call(
"Baustein-Lueckentext-Pruefung", "fast", _richtig_schema, provider,
topic=topic, baustein=baustein, satz=satz, loesung=loesung,
alternativen=", ".join(alternativen) or "(keine)", eingabe=eingabe,
)
return bool(data and data["richtig"])
async def pruefung_bewertung_schnell(
topic: str, baustein: str, section: str, kompakt: str | None,
frage: str, messages: list[dict], provider: str = DEFAULT_PROVIDER,
) -> dict | None:
"""Aktion 'antwort' (Agent 1, schnell): nur Evaluator, kein Kritiker. → {feedback, niveau}."""
try:
section_block, kompakt_block = _bloecke(section, kompakt)
transcript = _transcript(messages) if messages else "(leer)"
return await _gen_call(
"Baustein-Bewertung", "judge", _bewertung_schema, provider,
topic=topic, baustein=baustein, section_block=section_block, kompakt_block=kompakt_block,
frage=frage.strip() or "(keine Frage übergeben)", transcript=transcript,
begruendung_block="(keine)", kritik_block="(keine)",
)
except Exception:
log.warning("[%s] Schnell-Bewertung fehlgeschlagen (%s)", topic, baustein, exc_info=True)
return None
async def pruefung_bewertung(
topic: str, baustein: str, section: str, kompakt: str | None,
frage: str, messages: list[dict], provider: str = DEFAULT_PROVIDER,
role: str = "judge", begruendung: str = "",
) -> dict | None:
"""Aktion 'antwort_pruefen' (Agent 2, genau): Evaluator + Kritiker. → {feedback, niveau}.
`role` = "guide" für „Gründlich prüfen" (starkes Modell). `begruendung` = optionale
Unzufriedenheit des Lerners mit einer früheren Bewertung.
"""
try:
section_block, kompakt_block = _bloecke(section, kompakt)
transcript = _transcript(messages) if messages else "(leer)"
begruendung_block = begruendung.strip() or "(keine)"
return await _bewertung_mit_kritik(
topic, baustein, section_block, kompakt_block,
frage.strip() or "(keine Frage übergeben)", transcript, begruendung_block, provider, role,
)
except Exception:
log.warning("[%s] Bewertung fehlgeschlagen (%s)", topic, baustein, exc_info=True)
return None
async def baustein_diskussion(
topic: str, baustein: str, section: str, kompakt: str | None,
frage: str, letzte_bewertung: str | None, messages: list[dict], provider: str = DEFAULT_PROVIDER,
) -> str | None:
"""Aktion 'diskussion': Tutor erklärt/diskutiert die Frage oder eine Bewertung.
Kein Bewerten, kein Kritiker — hier ist der Mensch der Prüfer. None bei Fehler.
"""
try:
section_block, kompakt_block = _bloecke(section, kompakt)
prompt = _prompt(
"Baustein-Pruefung-Diskussion",
topic=topic, baustein=baustein,
section_block=section_block, kompakt_block=kompakt_block,
frage=frage.strip() or "(keine Frage übergeben)",
letzte_bewertung_block=(letzte_bewertung or "").strip() or "(noch keine)",
transcript=_transcript(messages) if messages else "(leer)",
)
returncode, stdout, _ = await run_agent(
"pruefungdiskussion-" + str(uuid.uuid4()), prompt, CHAT_TIMEOUT,
provider=provider, role="fast", capabilities="none", lane="interactive",
)
if returncode != 0:
return None
return stdout.strip() or None
except Exception:
log.warning("[%s] Prüfungs-Diskussion fehlgeschlagen (%s)", topic, baustein, exc_info=True)
return None
async def baustein_element_anlegen(topic: str, baustein: str, section: str, provider: str = DEFAULT_PROVIDER) -> None:
"""Hintergrund-Task nach dem Absolvieren: Baustein als Element anlegen.
Dedup über normalisierte Titel — existiert schon ein Element zum Baustein,
passiert nichts. Darf nie eine Exception nach außen werfen.
"""
try:
vorhanden = {_norm_titel(e["title"]) for e in await list_elements(topic)}
if _norm_titel(baustein) in vorhanden:
return
fields = await generate_element(topic, hint=baustein, provider=provider, extra_context=section)
if _norm_titel(fields["title"]) in vorhanden:
return
now = datetime.now(timezone.utc).isoformat()
await create_element({"id": str(uuid.uuid4()), "topic": topic, **fields, "created_at": now, "updated_at": now})
log.info("[%s] Baustein als Element angelegt: %s", topic, fields["title"])
except Exception:
log.warning("[%s] Element-Anlage nach Prüfung fehlgeschlagen (%s)", topic, baustein, exc_info=True)

115
backend/lesbarkeit.py Normal file
View File

@@ -0,0 +1,115 @@
"""Deterministisches Lesbarkeits-Gate für Guide-Sections.
Ein kleines deutsches Komplexitäts-Modell (DistilBERT, GermEval 2022, Skala 17)
bewertet die Verständlichkeit der Fließtext-Prosa. guide.py meldet zu schwere
Sections in die bestehende Lese-Prüfungs-/Überarbeitungs-Schleife — kein Prompt,
kein Raten.
Optional: fehlen `transformers`/`torch` oder lädt das Modell nicht, ist das Gate
stumm deaktiviert (das Backend läuft unverändert weiter). CPU genügt; der Aufrufer
wrappt die Bewertung in `asyncio.to_thread` (blockierende Modell-Inferenz).
"""
import logging
import re
from config import (
LESBARKEIT_AKTIV, LESBARKEIT_HART, LESBARKEIT_HART_ANTEIL, LESBARKEIT_MAX, LESBARKEIT_MODELL,
)
log = logging.getLogger("creator.lesbarkeit")
_modell_cache = None # (tokenizer, model, torch) — Singleton
_ladeversuch = False # schon versucht zu laden?
# Markup raus → reiner Fließtext (Code zählt nicht zur Lesbarkeit).
_CODE_FENCE = re.compile(r"```.*?```", re.DOTALL)
_COMMENT = re.compile(r"<!--.*?-->", re.DOTALL)
_INLINE_CODE = re.compile(r"`[^`]*`")
_LINK = re.compile(r"\[([^\]]*)\]\([^)]*\)")
_MD_MARK = re.compile(r"^[ \t]*([#>]+|[-*+]\s)|[*_~|]", re.MULTILINE)
_WS = re.compile(r"\s+")
_SATZ = re.compile(r"(?<=[.!?])\s+")
def _modell():
"""Lädt das Modell einmalig. None = Gate aus (deaktiviert oder Lade-Fehler)."""
global _modell_cache, _ladeversuch
if _ladeversuch:
return _modell_cache
_ladeversuch = True
if not LESBARKEIT_AKTIV:
return None
try:
import torch
from transformers import AutoModelForSequenceClassification, AutoTokenizer
tok = AutoTokenizer.from_pretrained(LESBARKEIT_MODELL)
model = AutoModelForSequenceClassification.from_pretrained(LESBARKEIT_MODELL)
model.eval()
_modell_cache = (tok, model, torch)
log.info("Lesbarkeits-Modell geladen: %s (num_labels=%d)", LESBARKEIT_MODELL, model.config.num_labels)
except Exception as e:
log.warning("Lesbarkeits-Gate deaktiviert (Modell nicht ladbar): %s", e)
_modell_cache = None
return _modell_cache
def _prosa(md: str) -> str:
"""Markdown/Code strippen → reiner Fließtext für die Bewertung."""
t = _CODE_FENCE.sub(" ", md)
t = _COMMENT.sub(" ", t)
t = _INLINE_CODE.sub(" ", t)
t = _LINK.sub(r"\1", t)
t = _MD_MARK.sub(" ", t)
return _WS.sub(" ", t).strip()
def _saetze(text: str) -> list[str]:
"""Fließtext in Sätze splitten; sehr kurze Fragmente verwerfen."""
return [s.strip() for s in _SATZ.split(text) if len(s.strip()) >= 15]
def _scores(saetze: list[str]) -> list[float]:
"""Komplexität je Satz (17). Regression (num_labels=1) oder Erwartungswert über Klassen."""
tok, model, torch = _modell_cache
werte: list[float] = []
n = model.config.num_labels
for i in range(0, len(saetze), 16):
batch = saetze[i:i + 16]
enc = tok(batch, return_tensors="pt", truncation=True, max_length=256, padding=True)
with torch.no_grad():
logits = model(**enc).logits
if n == 1:
vals = logits.reshape(-1).tolist()
else:
probs = torch.softmax(logits, dim=-1)
stufen = torch.arange(1, n + 1, dtype=probs.dtype)
vals = (probs * stufen).sum(-1).reshape(-1).tolist()
werte.extend(vals)
return werte
def bewerte_sections(md_by_num: dict[int, str]) -> dict[int, str]:
"""{num: section_md} → {num: Hinweis} nur für zu schwere Sections.
Leeres dict, wenn das Gate aus ist. Blockierend (CPU) — in to_thread aufrufen.
"""
if _modell() is None:
return {}
out: dict[int, str] = {}
for num, md in md_by_num.items():
saetze = _saetze(_prosa(md or ""))
if len(saetze) < 2: # fast nur Code / zu kurz → überspringen
continue
werte = _scores(saetze)
if not werte:
continue
schnitt = sum(werte) / len(werte)
hart = sum(1 for w in werte if w > LESBARKEIT_HART) / len(werte)
# Zu schwer = hoher Schnitt ODER zu viele harte Einzelsätze (Ausreißer-Nester).
if schnitt > LESBARKEIT_MAX or hart >= LESBARKEIT_HART_ANTEIL:
out[num] = (
f"Zu schwer lesbar (Ø {schnitt:.1f}/7, {hart * 100:.0f}% harte Sätze): "
"kürzere Sätze, einfachere Wörter, weniger Schachtelsätze, mehr Beispiele."
)
return out

View File

@@ -2,13 +2,14 @@ from pydantic import BaseModel, Field
from typing import Literal
FormatType = Literal[
"OnePager",
"MiniGuide",
"Guide",
"FullGuide",
"Rest",
]
ProviderType = Literal["claude", "minimax", "minimax-direkt", "lokal"]
ProviderType = Literal["claude", "minimax", "lokal"]
SourceType = Literal["thema", "projekt", "uni", "link"]
class GuideCreateRequest(BaseModel):
@@ -16,6 +17,7 @@ class GuideCreateRequest(BaseModel):
format: FormatType
instructions: str = Field(default="", max_length=2000)
provider: ProviderType = "claude"
ab_step: int | None = Field(default=None, ge=0, le=4) # Re-Run ab Guide-Schritt (0 Gliederung … 4 Lese-Prüfung); None = voll/Resume
class TopicCreateRequest(BaseModel):
@@ -26,6 +28,9 @@ class BausteineCreateRequest(BaseModel):
topic: str = Field(min_length=1, max_length=100)
instructions: str = Field(default="", max_length=2000)
provider: ProviderType = "claude"
source_type: SourceType = "thema"
source_ort: str = Field(default="", max_length=2000)
ab_phase: int | None = Field(default=None, ge=1, le=6) # Re-Run ab grober Phase (Position in _phasen(topic), 1-based); None = Resume/Fortsetzen ohne Löschen
class BausteineStep(BaseModel):
@@ -46,6 +51,37 @@ class ProjectResponse(BaseModel):
name: str
class FolderResponse(BaseModel):
name: str
ort: str # relativer Pfad ab Repo-Root (z.B. "projects/foo")
class BausteineQuelleUpdate(BaseModel):
topic: str = Field(min_length=1, max_length=100)
type: SourceType = "thema"
ort: str = Field(default="", max_length=2000)
spec: str = Field(default="", max_length=2000)
class BausteineQuelleResponse(BaseModel):
type: SourceType
ort: str
spec: str
class SubbausteinInfo(BaseModel):
titel: str
stufe: Literal["einfach", "mittel", "schwer"]
relevanz: Literal["relevant", "rand"] | None = None
class BausteinUebersicht(BaseModel):
num: int
titel: str
beschreibung: str = ""
subbausteine: list[SubbausteinInfo] = []
class ProviderInfo(BaseModel):
id: str
available: bool
@@ -156,3 +192,79 @@ class ProgressUpdate(BaseModel):
class ProgressResponse(BaseModel):
chapters: list[str]
# --- Baustein-Lernen ---
class BausteinChatRequest(BaseModel):
topic: str = Field(min_length=1, max_length=100)
baustein: str = Field(min_length=1, max_length=200)
section: str = Field(default="", max_length=20000) # ausführliche Fassung
section_kompakt: str = Field(default="", max_length=20000) # kompakte Fassung (Merksätze)
messages: list[ChatMessage] = Field(min_length=1)
provider: ProviderType = "claude"
class BausteinChatResponse(BaseModel):
reply: str
class BausteinPruefungRequest(BaseModel):
topic: str = Field(min_length=1, max_length=100)
baustein: str = Field(min_length=1, max_length=200)
section: str = Field(default="", max_length=20000) # ausführliche Fassung
section_kompakt: str = Field(default="", max_length=20000) # kompakte Fassung (Merksätze)
aktion: Literal[
"frage", "diskussion", "antwort", "antwort_pruefen",
"quiz_frage", "quiz_antwort", "lueck_frage", "lueck_antwort",
] = "frage"
frage: str = Field(default="", max_length=2000) # aktuell geprüfte Frage (für diskussion/antwort); Anker der Basis
auswahl: list[int] = [] # Quiz/Lückentext-Auswahl: vom Lerner gewählte Options-Indizes
korrekt: list[int] = [] # Quiz/Lückentext-Auswahl: korrekte Indizes (Client hält sie aus der Generierung)
loesung: str = Field(default="", max_length=500) # Lückentext frei: erwarteter Begriff
alternativen: list[str] = [] # Lückentext frei: akzeptierte Synonyme
eingabe: str = Field(default="", max_length=500) # Lückentext frei: getippter Begriff
schwer: bool = False # Variante: leicht (+1/1) vs schwer (+3/1)
letzte_bewertung: str = Field(default="", max_length=2000) # Feedback der letzten Bewertung (Kontext für diskussion)
vermeide: list[str] = [] # schon gestellte + vorgemerkte Fragen — sinngemäß nicht wiederholen
nachgefragt: bool = False # für diese Frage wurde nachgefragt → Gewinn auf +1 gedeckelt
begruendung: str = Field(default="", max_length=2000) # „Gründlich prüfen": warum mit der Bewertung unzufrieden
muster: str = Field(default="", max_length=2000) # gezogenes Frage-Muster (Saat); leer → Live-Generierung (Fallback)
# Basis wird serverseitig je Frage geführt (offene_frage-Anker) — nicht mehr vom Client.
cap: int = Field(default=6, ge=6, le=30) # Score-Deckel = Max des höchsten erstellten Formats (6/12/18/30)
messages: list[ChatMessage] = [] # Dialog bisher; leer = erste Frage
provider: ProviderType = "claude"
gruendlich: bool = False # „Gründlich prüfen": Bewertung mit starkem Modell (role guide)
class QuizOption(BaseModel):
text: str
korrekt: bool
class BausteinPruefungResponse(BaseModel):
frage: str | None = None
reply: str | None = None
feedback: str | None = None
punkte: int | None = None # Punkt-Delta dieser Antwort (2 … +3); schnell = voraussichtlich
bewertung: Literal["gut", "neutral", "schlecht"] | None = None # aus Vorzeichen, fürs Einfärben
optionen: list[QuizOption] | None = None # Quiz: 4 Optionen + Korrekt-Flags
satz: str | None = None # Lückentext: Satz mit Lücke (___)
loesung: str | None = None # Lückentext: erwarteter Begriff
alternativen: list[str] | None = None # Lückentext: akzeptierte Synonyme
gute_antworten: int
absolviert: bool
verstanden: bool = False
gemeistert: bool = False
class BausteinLernstand(BaseModel):
gute_antworten: int
streak: int = 0
absolviert: bool
verstanden: bool
gemeistert: bool
class BausteinLernstandResponse(BaseModel):
bausteine: dict[str, BausteinLernstand]

View File

@@ -1,157 +0,0 @@
"""OnePager-Pipeline: Recherche → Recherche-Prüfung → Bauen → Prüfung (7 Karten im 3×3-Raster)."""
from pathlib import Path
from fsutil import atomic_write_json
from jsonio import read_json_file as _json_datei
from pipeline import (
CANCELLED, FAILED, GenContext, _check_then_fix, _extra, _fail,
_prompt, _set_step, _timeout, is_guide_cancelled, run_single_slot,
)
async def _generate_onepager(
guide_id: str, topic: str, instructions: str, provider: str,
project: Path | None, content_path: Path,
) -> list[dict] | None:
ctx = GenContext(topic=topic, provider=provider, is_cancelled=lambda: is_guide_cancelled(guide_id), guide_id=guide_id)
# 3×3-Raster: 7 Karten mit festen Schlüsseln (Reihenfolge = Lesereihenfolge mobil)
KARTEN_KEYS = ("info", "eigenschaften", "beispiel", "zusammenhaenge", "voraussetzungen", "modern", "veraltet")
def karten_schema(data):
"""{"karten": {key: {titel, md}}} → Liste · sonst None."""
if not isinstance(data, dict):
return None
karten = data.get("karten")
if not isinstance(karten, dict):
return None
out = []
for key in KARTEN_KEYS:
k = karten.get(key)
if not isinstance(k, dict) or not isinstance(k.get("titel"), str) or not isinstance(k.get("md"), str):
return None
titel, md = k["titel"].strip(), k["md"].strip()
if not titel or len(md) < 5: # abgebrochene/leere Karten sind ungültig
return None
out.append({"key": key, "titel": titel, "md": md})
return out
d, stem = content_path.parent, content_path.stem
recherche_path = d / f"{stem}.recherche.md"
recherche_check_path = d / f"{stem}.recherche-check.json"
karten_path = d / f"{stem}.karten.json"
check_path = d / f"{stem}.onepager-check.json"
# Projekte bekommen eigene Recherche-Dimensionen — Produkt-Fragen
# (Version, Lizenz, Alternativen) laufen dort ins Leere.
if project:
source = _prompt("OnePager-Quelle-Projekt", project=project)
recherche_template = "OnePager-Recherche-Projekt"
recherche_check_template = "OnePager-Recherche-Check-Projekt"
else:
source = _prompt("OnePager-Quelle-Thema", topic=topic)
recherche_template = "OnePager-Recherche"
recherche_check_template = "OnePager-Recherche-Check"
def recherche_payload(result=None):
if not recherche_path.exists():
return None
text = recherche_path.read_text(encoding="utf-8").strip()
return text or None
# Schritt 1: Recherche — vorhandene Datei wird übernommen (Resume)
recherche = recherche_payload()
if recherche is None:
await _set_step(guide_id, 0, "Recherchiere…")
status, recherche = await run_single_slot(
ctx, "OnePager-Recherche",
key=f"{guide_id}-recherche",
prompt=_prompt(recherche_template, topic=topic, source=source, out_path=recherche_path, extra=_extra(instructions)),
role="quick", capabilities="files" if project else "full",
payload=recherche_payload, timeout=_timeout("onepager_recherche"),
)
if status == CANCELLED:
return None
if status == FAILED:
await _fail(guide_id, "OnePager-Recherche fehlgeschlagen")
return None
# Schritt 2: Recherche-Prüfung — notiert Probleme; Anpassung macht ein Recherche-Agent
status, fixed = await _check_then_fix(
ctx, name="Recherche", step=1,
check_key=f"{guide_id}-recherche-check",
check_prompt=_prompt(recherche_check_template, topic=topic, recherche=recherche, out_path=recherche_check_path),
check_path=recherche_check_path, check_timeout=_timeout("onepager_verify"),
fix_key=f"{guide_id}-recherche-fix",
build_fix_prompt=lambda probleme: _prompt(
"OnePager-Recherche-Fix",
topic=topic, source=source, recherche=recherche,
probleme="\n".join(f"- {p}" for p in probleme),
out_path=recherche_path, extra=_extra(instructions),
),
fix_payload=recherche_payload, fix_timeout=_timeout("onepager_recherche"),
fix_role="quick", fix_caps="files" if project else "full",
)
if status == CANCELLED:
return None
if status == FAILED:
await _fail(guide_id, "Recherche-Prüfung fehlgeschlagen")
return None
if fixed is not None:
recherche = fixed
# Schritt 3: Bauen — Karten nur aus der Faktenbasis (Resume: gültige Datei wird übernommen)
karten = karten_schema(_json_datei(karten_path))
if karten is None:
await _set_step(guide_id, 2, "Baue OnePager…")
karten_path.unlink(missing_ok=True)
status, karten = await run_single_slot(
ctx, "OnePager-Bauen",
key=f"{guide_id}-bauen",
prompt=_prompt("OnePager-Bauen", topic=topic, recherche=recherche, out_path=karten_path, extra=_extra(instructions)),
role="fast", capabilities="files",
payload=lambda result: karten_schema(_json_datei(karten_path)),
timeout=_timeout("onepager_bauen"),
)
if status == CANCELLED:
return None
if status == FAILED:
await _fail(guide_id, "OnePager-Bau fehlgeschlagen")
return None
def karten_block() -> str:
return "\n\n".join(f"### {k['titel']} [{k['key']}]\n{k['md']}" for k in karten)
# Schritt 4: Prüfung — notiert Probleme; Anpassung macht ein Bauen-Agent
status, fixed = await _check_then_fix(
ctx, name="OnePager", step=3,
check_key=f"{guide_id}-verify",
check_prompt=_prompt("OnePager-Verifikation", topic=topic, recherche=recherche, karten=karten_block(), out_path=check_path),
check_path=check_path, check_timeout=_timeout("onepager_verify"),
fix_key=f"{guide_id}-karten-fix",
build_fix_prompt=lambda probleme: _prompt(
"OnePager-Fix",
topic=topic, recherche=recherche, karten=karten_block(),
probleme="\n".join(f"- {p}" for p in probleme),
out_path=karten_path, extra=_extra(instructions),
),
fix_payload=lambda result: karten_schema(_json_datei(karten_path)),
fix_timeout=_timeout("onepager_bauen"),
on_fix_invalid=lambda: atomic_write_json(
karten_path, {"karten": {k["key"]: {"titel": k["titel"], "md": k["md"]} for k in karten}},
),
)
if status == CANCELLED:
return None
if status == FAILED:
await _fail(guide_id, "OnePager-Prüfung fehlgeschlagen")
return None
if fixed is not None:
karten = fixed
sections = [
{"num": i, "title": k["titel"], "md": k["md"], "key": k["key"]}
for i, k in enumerate(karten, 1)
]
return [{"title": topic, "sections": sections}]

View File

@@ -1,6 +1,6 @@
from pathlib import Path
from config import STORAGE_DIR, PROJECTS_DIR
from config import STORAGE_DIR, PROJECTS_DIR, PROJECT_ROOT
THEMEN_DIR = STORAGE_DIR / "themen"
@@ -21,6 +21,38 @@ def bausteine_path(topic: str) -> Path:
return topic_dir(topic) / "bausteine.md"
def subbausteine_path(topic: str) -> Path:
"""Sidecar: pro Baustein die Subbausteine mit Stufe (von allen Guides geteilt)."""
return topic_dir(topic) / "subbausteine.json"
def frage_muster_path(topic: str) -> Path:
"""Sidecar: pro Baustein vordefinierte Frage-Muster (Subbaustein × Typ → Beispielfrage)."""
return topic_dir(topic) / "frage_muster.json"
def quelle_path(topic: str) -> Path:
"""Persistierte Quellen-Wahl pro Thema: {type, ort, spec}."""
return topic_dir(topic) / "quelle.json"
def quelle_crawl_dir(topic: str) -> Path:
"""Zielordner für gecrawlte Link-Quellen (Seiten + PDF-.txt)."""
return topic_dir(topic) / "quelle"
def safe_ordner(ort: str) -> Path | None:
"""Ordnerpfad relativ zum Repo-Root, gesandboxt. None bei leer/Ausbruch (../, absolut außerhalb)."""
if not ort or not ort.strip():
return None
p = (PROJECT_ROOT / ort.strip()).resolve()
try:
p.relative_to(PROJECT_ROOT)
except ValueError:
return None
return p
def guide_content_path(topic: str, format_name: str) -> Path:
return topic_dir(topic) / "guides" / f"{format_name}.json"

View File

@@ -1,4 +1,4 @@
"""Pipeline-Grundbausteine: Agent-Races, Single-Slot, Check→Fix, Prompts, Guide-Status.
"""Pipeline-Grundbausteine: Agent-Races (mit Grace), Single-Slot, Schemata, Prompts, Guide-Status.
Hält den mutablen Pipeline-Zustand (Generierungs-Semaphore, Cancel-Set).
Zugriff auf das Cancel-Set NUR über die Funktionen hier — kopierte Referenzen
@@ -12,10 +12,11 @@ from datetime import datetime, timezone
from pathlib import Path
from typing import Callable
from agents import run_agent, kill_process
from agents import run_agent, kill_process, cancel_scope, clear_scope
from config import MAX_CONCURRENT_GENERATIONS, TEMPLATES_DIR, TIMEOUTS
from database import update_guide
from jsonio import read_json_file as _json_datei
from textkit import _STUFEN
log = logging.getLogger("creator.pipeline")
@@ -25,7 +26,8 @@ _cancelled: set[str] = set()
async def cancel_guide(guide_id: str) -> bool:
_cancelled.add(guide_id)
kill_process(guide_id)
cancel_scope(f"{guide_id}-") # wartende Agenten bailen vorm Spawn
kill_process(guide_id) # laufende Subprozesse killen
now = datetime.now(timezone.utc).isoformat()
await update_guide(guide_id, status="error", progress=None, error_msg="Abgebrochen — Fortschritt bleibt erhalten", updated_at=now)
return True
@@ -37,6 +39,7 @@ def is_guide_cancelled(guide_id: str) -> bool:
def clear_guide_cancelled(guide_id: str) -> None:
_cancelled.discard(guide_id)
clear_scope(f"{guide_id}-") # Scope leeren → Neustart blockiert nicht
async def _set_progress(guide_id: str, progress: str) -> None:
@@ -105,10 +108,88 @@ def _probleme_schema(data):
return out or None
def _str_liste(val) -> list[str] | None:
"""Liste nicht-leerer Strings → gestrippte Liste (leer erlaubt) · sonst None."""
if not isinstance(val, list) or not all(isinstance(x, str) for x in val):
return None
out = [x.strip() for x in val]
return None if any(not x for x in out) else out
def _rest_schema(data):
"""{"uebernehmen": [str]} → Liste (leer erlaubt) · sonst None."""
if not isinstance(data, dict):
return None
return _str_liste(data.get("uebernehmen"))
def _runde_schema(data, final: bool = False):
"""{"aufnehmen": [str], "rest": [str]} → (aufnehmen, rest) · sonst None.
final=True: letzte Klärungs-Runde — ein nicht-leerer Rest ist ungültig.
"""
if not isinstance(data, dict):
return None
aufnehmen = _str_liste(data.get("aufnehmen"))
rest = _str_liste(data.get("rest"))
if aufnehmen is None or rest is None or (final and rest):
return None
return aufnehmen, rest
def _stufen_schema(data, ids: set[int] | None = None):
"""{"stufen": {"1": "einfach", …}} → {id: stufe} · sonst None.
Stufe ∈ {einfach, mittel, schwer}. Sind `ids` gegeben, müssen mindestens
diese abgedeckt sein (Extras erlaubt); der Aufrufer filtert dann auf `ids`.
"""
if not isinstance(data, dict) or not isinstance(data.get("stufen"), dict) or not data["stufen"]:
return None
out: dict[int, str] = {}
for k, v in data["stufen"].items():
try:
num = int(k)
except (ValueError, TypeError):
return None
stufe = str(v).strip().casefold()
if stufe not in _STUFEN:
return None
out[num] = stufe
if ids is not None and not ids <= set(out):
return None
return out
_RELEVANZ = ("relevant", "rand")
def _relevanz_schema(data, ids: set[int] | None = None):
"""{"relevanz": {"1": "relevant", …}} → {id: relevanz} · sonst None.
Relevanz ∈ {relevant, rand} (binär). Wie `_stufen_schema`: sind `ids` gegeben,
müssen mindestens diese abgedeckt sein (Extras erlaubt).
"""
if not isinstance(data, dict) or not isinstance(data.get("relevanz"), dict) or not data["relevanz"]:
return None
out: dict[int, str] = {}
for k, v in data["relevanz"].items():
try:
num = int(k)
except (ValueError, TypeError):
return None
wert = str(v).strip().casefold()
if wert not in _RELEVANZ:
return None
out[num] = wert
if ids is not None and not ids <= set(out):
return None
return out
_MAX_RESTARTS = 2
async def _race(topic: str, label: str, slots: list[dict], quorum: int, timeout: int, provider: str, on_update=None, cancelled=None) -> list | None:
async def _race(topic: str, label: str, slots: list[dict], quorum: int, timeout: int, provider: str, on_update=None, cancelled=None, *, grace: int | None = None) -> list | None:
"""Startet alle Slots parallel und sammelt `quorum` gültige Ergebnisse.
Slot-Spec: {key, prompt, role, capabilities, payload}. `payload(result)`
@@ -116,9 +197,16 @@ async def _race(topic: str, label: str, slots: list[dict], quorum: int, timeout:
Fehler/Timeout/ungültig → Slot-Neustart (max. _MAX_RESTARTS). Sobald das
Quorum steht, werden die übrigen Agenten gekillt. None = Quorum verfehlt.
`cancelled()` → True bricht ab (keine Restarts, Rückgabe None).
Mit `grace` wird `quorum` zum Minimum: Das erste gültige Ergebnis startet
einen Timer von `grace` Sekunden. Nach dessen Ablauf werden laufende
Agenten nur gekillt, wenn das Minimum steht — sonst läuft das Race samt
Restarts weiter, bis es steht. Rückgabe: `quorum` bis `len(slots)` Ergebnisse.
"""
attempts = {i: 0 for i in range(len(slots))}
tasks: dict[asyncio.Task, int] = {}
loop = asyncio.get_running_loop()
deadline: float | None = None
def spawn(i: int) -> None:
slot = slots[i]
@@ -136,7 +224,15 @@ async def _race(topic: str, label: str, slots: list[dict], quorum: int, timeout:
while tasks:
if cancelled and cancelled():
return None
done, _ = await asyncio.wait(tasks.keys(), return_when=asyncio.FIRST_COMPLETED)
if deadline is not None and len(results) >= quorum and loop.time() >= deadline:
return results
# Grace gesetzt und Minimum erreicht → nur bis zum Deadline-Rest warten
wait_timeout = None
if deadline is not None and len(results) >= quorum:
wait_timeout = max(0.0, deadline - loop.time())
done, _ = await asyncio.wait(tasks.keys(), return_when=asyncio.FIRST_COMPLETED, timeout=wait_timeout)
if not done:
continue
for task in done:
i = tasks.pop(task)
payload, err = None, None
@@ -155,16 +251,24 @@ async def _race(topic: str, label: str, slots: list[dict], quorum: int, timeout:
if payload is not None:
results.append(payload)
if grace is not None and deadline is None:
deadline = loop.time() + grace
_log(topic, f"{label}: erstes Ergebnis — Grace {grace}s läuft")
if on_update:
on_update(len(results))
if len(results) >= quorum:
if len(results) >= quorum and (grace is None or loop.time() >= deadline):
return results
continue
_log(topic, f"{label} {i + 1} (Versuch {attempts[i] + 1}): {err}")
attempts[i] += 1
if attempts[i] <= _MAX_RESTARTS and not (cancelled and cancelled()):
# Steht das Minimum schon, sind Restarts sinnlos — der Neustart
# würde am Grace-Ende ohnehin gekillt.
satt = grace is not None and len(results) >= quorum
if attempts[i] <= _MAX_RESTARTS and not satt and not (cancelled and cancelled()):
spawn(i)
if len(results) >= quorum: # alle Slots durch, Minimum steht (nur mit grace erreichbar)
return results
_log(topic, f"{label}: Quorum {quorum} nicht erreicht ({len(results)} gültig)")
return None
finally:
@@ -184,7 +288,7 @@ class GenContext:
guide_id: str | None = None
# Ergebnis-Status von run_single_slot/_check_then_fix
# Ergebnis-Status von run_single_slot
OK, CANCELLED, FAILED = "ok", "cancelled", "failed"
@@ -205,47 +309,20 @@ async def run_single_slot(
return OK, res[0]
async def _check_then_fix(
ctx: GenContext, *, name: str, step: int,
check_key: str, check_prompt: str, check_path: Path, check_timeout: int,
fix_key: str, build_fix_prompt, fix_payload, fix_timeout: int,
fix_role: str = "fast", fix_caps: str = "files",
on_fix_invalid=None,
) -> tuple[str, object]:
"""Check→Fix-Muster: Prüf-Agent notiert Probleme (JSON), Fix-Agent behebt sie.
async def _gather_fortschritt(coros, total, melde, start=0):
"""Läuft `coros` nebenläufig und meldet Live-Fortschritt: `await melde(fertig, total)`
nach jedem Abschluss (und einmal initial). Ergebnisse in Reihenfolge, return_exceptions=True."""
done = start
async def wrap(c):
nonlocal done
try:
return await c
finally:
done += 1
await melde(done, total)
await melde(done, total)
return await asyncio.gather(*[wrap(c) for c in coros], return_exceptions=True)
Resume: existierende Check-Datei überspringt den ganzen Schritt.
Check ist fatal (FAILED), Fix nicht — Original bleibt; on_fix_invalid kann
das kanonische Original zurückschreiben, falls der Fix-Agent die
Artefakt-Datei zerschrieben hat.
Lese-Check (Multi-Slot, Section-genau) und Bausteine-Auswahl-Check
(Patch-Semantik) passen bewusst NICHT in dieses Muster.
→ (OK, neues_artefakt | None=unverändert) | (CANCELLED, None) | (FAILED, None)
"""
if check_path.exists():
return OK, None
await _set_step(ctx.guide_id, step, f"Prüfe {name}")
status, probleme = await run_single_slot(
ctx, f"{name}-Prüfung", key=check_key, prompt=check_prompt,
role="fast", capabilities="files",
payload=lambda result: _probleme_schema(_json_datei(check_path)),
timeout=check_timeout,
)
if status != OK:
return status, None
if not probleme:
return OK, None
_log(ctx.topic, f"{name}-Prüfung: {len(probleme)} Problem(e) notiert")
await _set_step(ctx.guide_id, step, f"Passe {name} an…")
status, fixed = await run_single_slot(
ctx, f"{name}-Fix", key=fix_key, prompt=build_fix_prompt(probleme),
role=fix_role, capabilities=fix_caps, payload=fix_payload, timeout=fix_timeout,
)
if status == CANCELLED:
return CANCELLED, None
if status == FAILED:
_log(ctx.topic, f"{name}-Fix ungültig — Original bleibt")
if on_fix_invalid:
on_fix_invalid()
return OK, None
return OK, fixed

View File

@@ -1,37 +1,76 @@
"""Lernschulden-Regeln: Progression und Deckel für offene Guides — die EINZIGE Quelle.
Regeln (nur Neu-Erstellungen; Themen, Bausteine, OnePager unbegrenzt):
- JE Format (MiniGuide/Guide/FullGuide) höchstens 3 erstellte, nicht absolvierte Guides
- Progression pro Thema: Guide erst nach absolviertem MiniGuide, FullGuide erst nach absolviertem Guide
Regeln (nur Neu-Erstellungen; Themen + Bausteine unbegrenzt):
- Format „Guide": höchstens 3 erstellte, nicht absolvierte Guides
- Keine Progression/Vorstufe mehr (nur ein Guide-Format).
- Absolviert: ALLE Bausteine (Section-Titel) des neuesten fertigen Guides haben
eine bestandene Prüfung. Rest ist read-only (kein Fortschritt, keine Prüfung).
Alle Funktionen arbeiten auf einmal geladenen Daten (lade_lernstand) — keine
Query-Schleifen mehr pro Guide.
"""
import json
from database import list_guides, list_progress_all
from database import list_baustein_levels_all, list_guides, list_progress_all
from guide import guide_slot_dateien
from paths import bausteine_path, guide_content_path
from textkit import _norm_titel
MAX_OFFENE_GUIDES = 3
VORSTUFE = {"Guide": "MiniGuide", "FullGuide": "Guide"}
FORMATE = ("MiniGuide", "Guide", "FullGuide")
# Nur noch EIN Format „Guide" (alle relevanten Bausteine, Prüfung 030). Keine Progression,
# keine Vorstufe → der Guide ist immer freischaltbar. „Rest"/FullGuide separat.
VORSTUFE: dict[str, str] = {}
FREISCHALT_LEVEL: dict[str, str] = {}
FORMATE = ("Guide",)
# Geforderter Score je Freischalt-Niveau — nur für die Sperr-Meldung.
_LEVEL_WORT = {
"absolviert": "absolvieren (alle Bausteine auf 6)",
"verstanden": "verstehen (alle Bausteine auf 12)",
"gemeistert": "meistern (alle Bausteine auf 18)",
}
async def lade_lernstand() -> tuple[list[dict], dict[str, set[str]]]:
"""Guides + kompletter Kapitel-Fortschritt in zwei Queries."""
return await list_guides(), await list_progress_all()
async def lade_lernstand() -> tuple[list[dict], dict[str, set[str]], dict[str, dict[str, set[str]]]]:
"""Guides + Kapitel-Fortschritt + Bausteine je Meilenstein.
levels: {"absolviert"/"verstanden"/"gemeistert": {topic → normalisierte Titel}}.
"""
roh = await list_baustein_levels_all()
levels = {
stufe: {topic: {_norm_titel(b) for b in titel} for topic, titel in pro_topic.items()}
for stufe, pro_topic in roh.items()
}
return await list_guides(), await list_progress_all(), levels
def _kapitel_titel(topic: str, fmt: str) -> set[str] | None:
def _content_json(topic: str, fmt: str) -> dict | None:
path = guide_content_path(topic, fmt)
if not path.exists():
return None
try:
chapters = json.loads(path.read_text(encoding="utf-8")).get("chapters", [])
return json.loads(path.read_text(encoding="utf-8"))
except ValueError:
return None
return {c.get("title") for c in chapters}
def _kapitel_titel(topic: str, fmt: str) -> set[str] | None:
content = _content_json(topic, fmt)
if content is None:
return None
return {c.get("title") for c in content.get("chapters", [])}
def _section_titel(topic: str, fmt: str) -> set[str] | None:
"""Normalisierte Baustein-Titel (Sections) aus dem Guide-Content."""
content = _content_json(topic, fmt)
if content is None:
return None
return {
_norm_titel(s.get("title", ""))
for ch in content.get("chapters", [])
for s in ch.get("sections", [])
}
def _neueste_done(guides: list[dict], fmt: str) -> dict[str, dict]:
@@ -44,44 +83,57 @@ def _neueste_done(guides: list[dict], fmt: str) -> dict[str, dict]:
return neueste
def _guide_absolviert(g: dict, progress: dict[str, set[str]]) -> bool:
titles = _kapitel_titel(g["topic"], g["format"])
return bool(titles) and titles <= progress.get(g["id"], set())
def _guide_alle(g: dict, progress: dict[str, set[str]], levelset: dict[str, set[str]]) -> bool:
"""Sind ALLE Bausteine des Guides auf dem geforderten Niveau?"""
sections = _section_titel(g["topic"], g["format"])
return bool(sections) and sections <= levelset.get(g["topic"], set())
def ist_absolviert(topic: str, fmt: str, guides: list[dict], progress: dict[str, set[str]]) -> bool:
"""Alle Kapitel des neuesten fertigen Guides (Thema+Format) abgehakt?"""
def ist_level(topic: str, fmt: str, guides: list[dict], progress: dict[str, set[str]], levelset: dict[str, set[str]]) -> bool:
"""Neuester fertiger Guide (Thema+Format): alle Bausteine auf dem Niveau von levelset?"""
g = _neueste_done(guides, fmt).get(topic)
return g is not None and _guide_absolviert(g, progress)
return g is not None and _guide_alle(g, progress, levelset)
def formate_stats(guides: list[dict], progress: dict[str, set[str]]) -> dict:
def ist_absolviert(topic: str, fmt: str, guides: list[dict], progress: dict[str, set[str]], levels: dict[str, dict[str, set[str]]]) -> bool:
"""Alle Bausteine des neuesten fertigen Guides absolviert (≥3)?"""
return ist_level(topic, fmt, guides, progress, levels["absolviert"])
def thema_abgeschlossen(topic: str, guides: list[dict], progress: dict[str, set[str]], levels: dict[str, dict[str, set[str]]]) -> bool:
"""Thema fertig: neuester fertiger Guide, alle Bausteine gemeistert (18)?"""
return ist_level(topic, "Guide", guides, progress, levels["gemeistert"])
def formate_stats(guides: list[dict], progress: dict[str, set[str]], levels: dict[str, dict[str, set[str]]]) -> dict:
"""Pro Format erstellt/absolviert — pro Thema zählt nur der neueste fertige Guide."""
formate = {}
for fmt in FORMATE:
neueste = _neueste_done(guides, fmt)
absolviert = sum(1 for g in neueste.values() if _guide_absolviert(g, progress))
absolviert = sum(1 for g in neueste.values() if _guide_alle(g, progress, levels["absolviert"]))
formate[fmt] = {"erstellt": len(neueste), "absolviert": absolviert}
return formate
def guide_lock(topic: str, fmt: str, guides: list[dict], progress: dict[str, set[str]]) -> str | None:
def guide_lock(topic: str, fmt: str, guides: list[dict], progress: dict[str, set[str]], levels: dict[str, dict[str, set[str]]]) -> str | None:
"""Grund, warum ein Neu-Start für Thema+Format gesperrt ist — None = erlaubt.
Exakt die Regeln aus POST /guides: Bausteine nötig, kein Duplikat-Start,
Lernschulden nur für echte Neu-Erstellungen (Resume/Regenerieren frei).
"""
if fmt != "OnePager" and not bausteine_path(topic).exists():
if not bausteine_path(topic).exists():
return "Erst Bausteine erstellen"
for g in guides:
if g["topic"] == topic and g["format"] == fmt and g["status"] in ("queued", "generating"):
return "Generierung läuft bereits"
content = guide_content_path(topic, fmt)
if fmt != "OnePager" and not content.exists() and not guide_slot_dateien(content):
if not content.exists() and not guide_slot_dateien(content):
vorstufe = VORSTUFE.get(fmt)
if vorstufe and not ist_absolviert(topic, vorstufe, guides, progress):
return f"Erst den {vorstufe} dieses Themas absolvieren"
stat = formate_stats(guides, progress).get(fmt, {"erstellt": 0, "absolviert": 0})
if vorstufe:
stufe = FREISCHALT_LEVEL[fmt] # absolviert=10 · verstanden=20 · gemeistert=30
if not ist_level(topic, vorstufe, guides, progress, levels[stufe]):
return f"Erst den {vorstufe} dieses Themas {_LEVEL_WORT[stufe]}"
stat = formate_stats(guides, progress, levels).get(fmt, {"erstellt": 0, "absolviert": 0})
offen = stat["erstellt"] - stat["absolviert"]
if offen >= MAX_OFFENE_GUIDES:
return f"Erst {fmt}s absolvieren — maximal {MAX_OFFENE_GUIDES} offene erlaubt ({offen} offen)"

View File

@@ -1,3 +1,7 @@
fastapi
uvicorn[standard]
aiosqlite
playwright
transformers
# torch NICHT hier listen — sonst zieht pip die CUDA-Variante (~2,5 GB).
# Es wird separat als CPU-Build installiert (Dockerfile + Makefile-Target `install`).

View File

@@ -4,21 +4,25 @@ import uuid
from datetime import datetime, timezone
from fastapi import APIRouter, HTTPException
from fastapi.responses import FileResponse
from fastapi.responses import FileResponse, Response
from agents import provider_available
from config import PROJECTS_DIR, PROVIDERS
from config import PROJECTS_DIR, UNI_DIR, PROVIDERS
from database import (
create_guide, delete_guide, get_guide, list_guides,
create_topic, list_topics as db_list_topics, delete_topic,
list_progress, set_progress, delete_progress,
create_element, list_elements, get_element, update_element, delete_element,
list_baustein_progress, get_baustein_progress, set_offene_frage,
set_baustein_score, set_baustein_absolviert, set_baustein_verstanden, set_baustein_gemeistert, delete_baustein_daten,
delete_topic_pipeline, delete_quelle, get_guide_content, delete_guide_content,
)
from bausteine import generate_bausteine, cancel_bausteine, bausteine_status, active_bausteine, reset_bausteine
from bausteine import generate_bausteine, cancel_bausteine, bausteine_status, active_bausteine, reset_bausteine, lade_quelle, lade_uebersicht, subbausteine_titel, lade_frage_muster
from elements import generate_element, chat_with_guide, chat_with_element, check_element, style_element, refine_suggestion
from guide import generate_guide, guide_slot_dateien
from lernen import NOETIG, MASTERY, MEISTERN, NIVEAUS, baustein_chat, baustein_diskussion, baustein_element_anlegen, pruefung_bewertung, pruefung_bewertung_schnell, pruefung_frage, pruefung_frage_variante, quiz_generieren, lueckwahl_generieren, lueckentext_generieren, lueckentext_pruefen, score_berechnen, floor_aus_meilensteinen, _zufall_malus
from guide import generate_guide, guide_slot_dateien, guide_fertig_step
from pipeline import cancel_guide
from regeln import FORMATE, formate_stats, guide_lock, ist_absolviert, lade_lernstand
from regeln import FORMATE, formate_stats, guide_lock, ist_absolviert, lade_lernstand, thema_abgeschlossen
from models import (
GuideCreateRequest, GuideResponse,
TopicCreateRequest,
@@ -28,8 +32,12 @@ from models import (
ElementUpdateRequest, ElementCheckRequest, ElementCheckResponse, ElementStyleResponse,
ElementRefineRequest, ElementRefineResponse,
ProgressUpdate, ProgressResponse, ProjectResponse, ProviderInfo,
FolderResponse, BausteineQuelleUpdate, BausteineQuelleResponse, BausteinUebersicht,
BausteinChatRequest, BausteinChatResponse,
BausteinPruefungRequest, BausteinPruefungResponse, BausteinLernstandResponse,
)
from paths import bausteine_topics, guide_content_path, project_dir, topic_dir
from paths import bausteine_topics, guide_content_path, project_dir, topic_dir, quelle_path, safe_ordner
from fsutil import atomic_write_json
router = APIRouter(prefix="/api")
@@ -53,18 +61,20 @@ async def get_topics():
@router.get("/stats")
async def get_stats():
"""Tracker: Themen-Anzahl + pro Format erstellt/absolviert."""
guides, progress = await lade_lernstand()
guides, progress, levels = await lade_lernstand()
themen = set(await db_list_topics()) | {g["topic"] for g in guides} | set(bausteine_topics())
if PROJECTS_DIR.is_dir():
themen |= {e.name for e in PROJECTS_DIR.iterdir() if e.is_dir()}
return {"themen": len(themen), "formate": formate_stats(guides, progress)}
return {"themen": len(themen), "formate": formate_stats(guides, progress, levels)}
@router.get("/topics/fortschritt")
async def topic_fortschritt(topic: str):
"""Absolviert-Status pro Format — fürs Freischalten der nächsten Ausbaustufe."""
guides, progress = await lade_lernstand()
return {fmt: ist_absolviert(topic, fmt, guides, progress) for fmt in FORMATE}
"""Absolviert-Status pro Format + Themen-Abschluss — fürs Freischalten der nächsten Ausbaustufe."""
guides, progress, levels = await lade_lernstand()
status = {fmt: ist_absolviert(topic, fmt, guides, progress, levels) for fmt in FORMATE}
status["abgeschlossen"] = thema_abgeschlossen(topic, guides, progress, levels)
return status
@router.post("/topics")
@@ -76,6 +86,10 @@ async def add_topic(req: TopicCreateRequest):
@router.delete("/topics")
async def remove_topic(topic: str):
await delete_topic(topic)
await delete_baustein_daten(topic)
await delete_topic_pipeline(topic)
await delete_quelle(topic) # Themen-Config (DB) — beim Thema-Löschen mit weg
await delete_guide_content(topic)
shutil.rmtree(topic_dir(topic), ignore_errors=True)
return {"ok": True}
@@ -103,6 +117,18 @@ async def remove_project(name: str):
return {"ok": True}
@router.get("/folders", response_model=list[FolderResponse])
async def list_folders(kind: str):
"""Ordner für die Quellen-Auswahl: kind=projekt → projects/, kind=uni → uni/."""
base = {"projekt": (PROJECTS_DIR, "projects"), "uni": (UNI_DIR, "uni")}.get(kind)
if base is None:
raise HTTPException(400, "kind muss 'projekt' oder 'uni' sein")
root, prefix = base
if not root.is_dir():
return []
return [{"name": e.name, "ort": f"{prefix}/{e.name}"} for e in sorted(root.iterdir()) if e.is_dir()]
# --- Bausteine ---
@router.get("/bausteine/status", response_model=BausteineStatusResponse)
@@ -121,7 +147,20 @@ async def create_bausteine(req: BausteineCreateRequest):
if bausteine_status(topic)["generating"]:
return {"ok": True, "status": "already_generating"}
await create_topic(topic)
asyncio.create_task(generate_bausteine(topic, req.instructions.strip(), req.provider))
qp = quelle_path(topic)
# Quelle nur beim ERSTEN Mal festschreiben; ▶/Resume erhält die bestehende Wahl.
if not qp.exists():
typ, ort = req.source_type, req.source_ort.strip()
if typ in ("projekt", "uni"):
ordner = safe_ordner(ort)
if ordner is None or not ordner.is_dir():
raise HTTPException(400, "Ordner ungültig oder nicht gefunden (Pfad relativ zum Projekt-Root, kein ../).")
elif typ == "link":
if not ort.lower().startswith(("http://", "https://")):
raise HTTPException(400, "Link muss mit http:// oder https:// beginnen.")
qp.parent.mkdir(parents=True, exist_ok=True)
atomic_write_json(qp, {"type": typ, "ort": ort, "spec": req.instructions.strip()})
asyncio.create_task(generate_bausteine(topic, req.instructions.strip(), req.provider, ab_phase=req.ab_phase))
return {"ok": True}
@@ -134,16 +173,281 @@ async def cancel_bausteine_route(topic: str):
@router.delete("/bausteine")
async def remove_bausteine(topic: str):
reset_bausteine(topic)
reset_bausteine(topic) # Dateien: Crawl + Sichtung + Inventar…Fragen weg; quelle.json bleibt
await delete_topic_pipeline(topic) # DB: Bausteine-Bereich weg; Themen-Config (quelle) bleibt
return {"ok": True}
def _validate_quelle(typ: str, ort: str) -> None:
"""Quellen-Eingabe prüfen (gleiche Regeln wie beim Erstellen)."""
if typ in ("projekt", "uni"):
ordner = safe_ordner(ort)
if ordner is None or not ordner.is_dir():
raise HTTPException(400, "Ordner ungültig oder nicht gefunden (Pfad relativ zum Projekt-Root, kein ../).")
elif typ == "link":
if not ort.lower().startswith(("http://", "https://")):
raise HTTPException(400, "Link muss mit http:// oder https:// beginnen.")
@router.get("/bausteine/quelle", response_model=BausteineQuelleResponse)
async def get_bausteine_quelle(topic: str):
return lade_quelle(topic)
@router.put("/bausteine/quelle", response_model=BausteineQuelleResponse)
async def update_bausteine_quelle(req: BausteineQuelleUpdate):
"""Nur speichern — KEINE Neugenerierung. Quellen-/Spec-Wahl überschreiben."""
topic, typ, ort = req.topic.strip(), req.type, req.ort.strip()
_validate_quelle(typ, ort)
qp = quelle_path(topic)
qp.parent.mkdir(parents=True, exist_ok=True)
daten = {"type": typ, "ort": ort, "spec": req.spec.strip()}
atomic_write_json(qp, daten)
return daten
@router.get("/bausteine/uebersicht", response_model=list[BausteinUebersicht])
async def get_bausteine_uebersicht(topic: str):
return await lade_uebersicht(topic)
@router.get("/bausteine/frage-muster")
async def get_frage_muster(topic: str, baustein: str):
"""Vordefinierte Frage-Muster eines Bausteins (leer = Fallback auf Live-Generierung)."""
return {"muster": await lade_frage_muster(topic, baustein)}
# --- Baustein-Lernen: Chat, Prüfung ---
@router.get("/bausteine/lernstand", response_model=BausteinLernstandResponse)
async def baustein_lernstand(topic: str):
"""Prüfungs-Stand pro Baustein (roher Titel als Key)."""
progress = await list_baustein_progress(topic)
bausteine = {
p["baustein"]: {
"gute_antworten": p["gute_antworten"],
"streak": p["streak"],
"absolviert": p["absolviert"] is not None,
"verstanden": p["verstanden"] is not None,
"gemeistert": p["gemeistert"] is not None,
}
for p in progress
}
return {"bausteine": bausteine}
@router.post("/bausteine/chat", response_model=BausteinChatResponse)
async def baustein_chat_route(req: BausteinChatRequest):
reply = await baustein_chat(
req.topic, req.baustein, req.section, req.section_kompakt,
[m.model_dump() for m in req.messages], provider=req.provider,
)
return {"reply": reply}
# Bewertungen je (topic, baustein) serialisieren — sonst überschreiben zwei
# gleichzeitige Bewertungen den absoluten Score mit veralteter Basis (Race).
_pruef_locks: dict[tuple[str, str], asyncio.Lock] = {}
def _pruef_lock(topic: str, baustein: str) -> asyncio.Lock:
key = (topic, baustein)
lock = _pruef_locks.get(key)
if lock is None:
lock = _pruef_locks[key] = asyncio.Lock()
return lock
def _basis(stand: dict, frage: str) -> tuple[int, bool]:
"""Score-Basis VOR der Frage. Gleiche offene Frage → Re-Bewertung auf derselben Basis
(idempotent); sonst neue Frage auf dem aktuellen Stand. → (basis, re_bewertung)."""
re_bewertung = stand["offene_frage"] == frage and stand["offene_basis"] is not None
return (stand["offene_basis"] if re_bewertung else stand["gute_antworten"]), re_bewertung
def _farbe(punkte: int) -> str:
"""Punkte-Delta → grobe Einfärbung der Bubble."""
return "gut" if punkte > 0 else ("neutral" if punkte == 0 else "schlecht")
def _erklaeren_delta(niveau: str, nachgefragt: bool) -> int:
"""Erklären-Form: Punkt-Delta aus dem Niveau (1…+3). Mit Nachfrage höchstens +1."""
delta = NIVEAUS.get(niveau, 0)
return min(delta, 1) if nachgefragt else delta
async def _punkte_buchen(req, frage: str, delta: int) -> dict:
"""Score driftfrei buchen (Lock + offene_frage-Anker + Meilensteine). Gemeinsam für
Quiz/Lückentext (deterministisch/schnell). → Response-Felder ohne feedback."""
async with _pruef_lock(req.topic, req.baustein):
stand = await get_baustein_progress(req.topic, req.baustein)
absolviert = stand["absolviert"] is not None
verstanden = stand["verstanden"] is not None
gemeistert = stand["gemeistert"] is not None
basis, re_bewertung = _basis(stand, frage)
if not re_bewertung:
await set_offene_frage(req.topic, req.baustein, frage, basis, 0)
floor = floor_aus_meilensteinen(absolviert, verstanden, gemeistert)
score = score_berechnen(basis, _zufall_malus(basis, delta), floor, req.cap)
punkte = score - basis
gute = await set_baustein_score(req.topic, req.baustein, score)
if score >= NOETIG and not absolviert:
absolviert = True
if await set_baustein_absolviert(req.topic, req.baustein):
asyncio.create_task(baustein_element_anlegen(req.topic, req.baustein, req.section, req.provider))
if score >= MASTERY and not verstanden:
await set_baustein_verstanden(req.topic, req.baustein)
verstanden = True
if score >= MEISTERN and not gemeistert:
await set_baustein_gemeistert(req.topic, req.baustein)
gemeistert = True
return {"punkte": punkte, "bewertung": _farbe(punkte), "gute_antworten": gute,
"absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert}
@router.post("/bausteine/pruefung", response_model=BausteinPruefungResponse)
async def baustein_pruefung_route(req: BausteinPruefungRequest):
stand = await get_baustein_progress(req.topic, req.baustein)
gute = stand["gute_antworten"]
absolviert = stand["absolviert"] is not None
verstanden = stand["verstanden"] is not None
gemeistert = stand["gemeistert"] is not None
kompakt = req.section_kompakt
msgs = [m.model_dump() for m in req.messages]
if req.aktion == "frage":
if req.muster.strip():
# Aus gezogenem Muster eine konkrete, leicht andere Frage formulieren (kein Dedup nötig).
frage = await pruefung_frage_variante(req.topic, req.baustein, req.section, kompakt, req.muster, provider=req.provider)
else:
# Fallback (kein Muster-Sidecar): Live-Generierung mit zufälligem Typ/Subbaustein.
subs = await subbausteine_titel(req.topic, req.baustein) # Fokus-Kandidaten (zufällig gewählt)
frage = await pruefung_frage(req.topic, req.baustein, req.section, kompakt, msgs, subbausteine=subs, vermeide=req.vermeide, provider=req.provider)
if frage is None:
raise HTTPException(502, "Frage fehlgeschlagen — bitte erneut versuchen")
return {"frage": frage, "gute_antworten": gute, "absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert}
if req.aktion == "diskussion":
if not req.frage.strip():
raise HTTPException(400, "Diskussion braucht eine laufende Frage")
reply = await baustein_diskussion(
req.topic, req.baustein, req.section, kompakt,
req.frage, req.letzte_bewertung or None, msgs, provider=req.provider,
)
if reply is None:
raise HTTPException(502, "Diskussion fehlgeschlagen — bitte erneut versuchen")
return {"reply": reply, "gute_antworten": gute, "absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert}
# --- Quiz: leicht (1 von 4) +1/1 · schwer (x von 4) +3/1 — deterministisch ---
if req.aktion == "quiz_frage":
if not req.muster.strip():
raise HTTPException(400, "Quiz braucht ein Muster")
quiz = await quiz_generieren(req.topic, req.baustein, req.section, kompakt, req.muster, schwer=req.schwer, provider=req.provider)
if quiz is None:
raise HTTPException(502, "Quiz-Frage fehlgeschlagen — bitte erneut versuchen")
return {"frage": quiz["frage"], "optionen": quiz["optionen"],
"gute_antworten": gute, "absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert}
if req.aktion == "quiz_antwort":
if not req.frage.strip():
raise HTTPException(400, "Quiz-Antwort braucht eine Frage")
getroffen = set(req.auswahl) == set(req.korrekt) # exakt die richtige Menge
delta = (3 if req.schwer else 1) if getroffen else -1
res = await _punkte_buchen(req, req.frage, delta)
res["feedback"] = "Richtig — alle korrekten getroffen." if getroffen else "Nicht ganz — die markierten waren richtig."
return res
# --- Lückentext: leicht (Begriff aus 4) +1/1 · schwer (frei tippen) +3/1 ---
if req.aktion == "lueck_frage":
if not req.muster.strip():
raise HTTPException(400, "Lückentext braucht ein Muster")
if req.schwer:
lt = await lueckentext_generieren(req.topic, req.baustein, req.section, kompakt, req.muster, provider=req.provider)
if lt is None:
raise HTTPException(502, "Lückentext fehlgeschlagen — bitte erneut versuchen")
return {"satz": lt["satz"], "loesung": lt["loesung"], "alternativen": lt["alternativen"],
"gute_antworten": gute, "absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert}
lw = await lueckwahl_generieren(req.topic, req.baustein, req.section, kompakt, req.muster, provider=req.provider)
if lw is None:
raise HTTPException(502, "Lückentext fehlgeschlagen — bitte erneut versuchen")
return {"satz": lw["satz"], "optionen": lw["optionen"],
"gute_antworten": gute, "absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert}
if req.aktion == "lueck_antwort":
if not req.frage.strip():
raise HTTPException(400, "Lückentext-Antwort braucht einen Satz")
if req.schwer: # frei getippt → Synonym-tolerante KI-Prüfung
ok = await lueckentext_pruefen(req.topic, req.baustein, req.frage, req.loesung, req.alternativen, req.eingabe, provider=req.provider)
feedback = "Richtig!" if ok else f"Nicht ganz — erwartet war „{req.loesung}“."
else: # Begriff aus 4 gewählt → deterministisch
ok = set(req.auswahl) == set(req.korrekt)
feedback = "Richtig!" if ok else "Nicht ganz — der markierte Begriff war richtig."
res = await _punkte_buchen(req, req.frage, (3 if req.schwer else 1) if ok else -1)
res["feedback"] = feedback
return res
# aktion "antwort" (Agent 1 schnell) / "antwort_pruefen" (Agent 2 genau).
if not any(m.get("role") == "user" for m in msgs):
raise HTTPException(400, "Antwort braucht eine Nutzer-Antwort")
if not req.frage.strip():
raise HTTPException(400, "Antwort braucht eine laufende Frage")
if req.aktion == "antwort":
# Agent 1: nur Vorschau — Niveau + voraussichtliche Punkte, NICHTS persistieren, kein Anker.
data = await pruefung_bewertung_schnell(
req.topic, req.baustein, req.section, kompakt, req.frage, msgs, provider=req.provider,
)
if data is None:
raise HTTPException(502, "Bewertung fehlgeschlagen — bitte erneut versuchen")
basis, _ = _basis(stand, req.frage)
floor = floor_aus_meilensteinen(absolviert, verstanden, gemeistert)
score = score_berechnen(basis, _zufall_malus(basis, _erklaeren_delta(data["niveau"], req.nachgefragt)), floor, req.cap)
punkte = score - basis
return {"feedback": data["feedback"], "punkte": punkte, "bewertung": _farbe(punkte),
"gute_antworten": gute, "absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert}
# aktion "antwort_pruefen" (Agent 2 genau): verbindlich, persistiert. Serialisiert je Baustein,
# Basis driftfrei über den offene_frage-Anker. NUR hier ändert sich der Score.
async with _pruef_lock(req.topic, req.baustein):
stand = await get_baustein_progress(req.topic, req.baustein)
absolviert = stand["absolviert"] is not None
verstanden = stand["verstanden"] is not None
gemeistert = stand["gemeistert"] is not None
basis, re_bewertung = _basis(stand, req.frage)
if not re_bewertung:
await set_offene_frage(req.topic, req.baustein, req.frage, basis, 0)
data = await pruefung_bewertung(
req.topic, req.baustein, req.section, kompakt, req.frage, msgs, provider=req.provider,
role="guide" if req.gruendlich else "judge", begruendung=req.begruendung,
)
if data is None:
raise HTTPException(502, "Bewertung fehlgeschlagen — bitte erneut versuchen")
floor = floor_aus_meilensteinen(absolviert, verstanden, gemeistert)
score = score_berechnen(basis, _zufall_malus(basis, _erklaeren_delta(data["niveau"], req.nachgefragt)), floor, req.cap)
punkte = score - basis
gute = await set_baustein_score(req.topic, req.baustein, score)
if score >= NOETIG and not absolviert:
absolviert = True
if await set_baustein_absolviert(req.topic, req.baustein):
asyncio.create_task(baustein_element_anlegen(req.topic, req.baustein, req.section, req.provider))
if score >= MASTERY and not verstanden:
await set_baustein_verstanden(req.topic, req.baustein)
verstanden = True
if score >= MEISTERN and not gemeistert:
await set_baustein_gemeistert(req.topic, req.baustein)
gemeistert = True
return {"feedback": data["feedback"], "punkte": punkte, "bewertung": _farbe(punkte),
"gute_antworten": gute, "absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert}
# --- Guides ---
@router.post("/guides", response_model=GuideResponse)
async def create(req: GuideCreateRequest):
guides, progress = await lade_lernstand()
grund = guide_lock(req.topic.strip(), req.format, guides, progress)
guides, progress, levels = await lade_lernstand()
grund = guide_lock(req.topic.strip(), req.format, guides, progress, levels)
if grund:
raise HTTPException(400 if grund == "Erst Bausteine erstellen" else 409, grund)
await create_topic(req.topic.strip())
@@ -159,7 +463,7 @@ async def create(req: GuideCreateRequest):
"updated_at": now,
}
await create_guide(guide)
asyncio.create_task(generate_guide(guide["id"], guide["topic"], guide["format"], guide["instructions"], req.provider))
asyncio.create_task(generate_guide(guide["id"], guide["topic"], guide["format"], guide["instructions"], req.provider, ab_step=req.ab_step))
return guide
@@ -171,8 +475,15 @@ async def list_all():
@router.get("/guides/locks")
async def guide_locks(topic: str):
"""Sperr-Gründe pro Format für den ▶-Button — None = erstellbar."""
guides, progress = await lade_lernstand()
return {fmt: guide_lock(topic, fmt, guides, progress) for fmt in ("OnePager", *FORMATE)}
guides, progress, levels = await lade_lernstand()
return {fmt: guide_lock(topic, fmt, guides, progress, levels) for fmt in ("FullGuide", "Rest", *FORMATE)}
@router.get("/guides/steps")
async def guide_steps(topic: str):
"""Höchster voll abgeschlossener Schritt-Index je Format (artefakt-basiert, -1 = keiner).
Treibt die klickbaren Schritt-Kugeln (wie die Bausteine-Phasen)."""
return {fmt: guide_fertig_step(guide_content_path(topic, fmt)) for fmt in ("Guide", "FullGuide", "Rest")}
@router.get("/guides/{guide_id}", response_model=GuideResponse)
@@ -190,7 +501,10 @@ async def guide_content(guide_id: str):
raise HTTPException(404, "Guide nicht gefunden")
if guide["status"] != "done":
raise HTTPException(404, "Inhalt nicht verfügbar")
path = guide_content_path(guide["topic"], guide["format"])
stored = await get_guide_content(guide["topic"], guide["format"]) # DB-first
if stored is not None:
return Response(content=stored, media_type="application/json")
path = guide_content_path(guide["topic"], guide["format"]) # Fallback: Datei (Alt-Themen)
if not path.exists():
raise HTTPException(404, "Datei nicht gefunden")
return FileResponse(path, media_type="application/json")
@@ -305,6 +619,7 @@ async def remove(guide_id: str, slots: bool = False):
# Content) bleibt fürs Resume erhalten, außer es wird explizit verlangt (slots=1).
rest = [g for g in await list_guides() if g["topic"] == guide["topic"] and g["format"] == guide["format"]]
if not rest:
await delete_guide_content(guide["topic"], guide["format"])
content = guide_content_path(guide["topic"], guide["format"])
if slots or content.exists():
for p in guide_slot_dateien(content):

View File

@@ -42,6 +42,30 @@ def _eindeutige_titel(entries: dict[int, str]) -> dict[int, str]:
return out
def _vormerge(listen: list[dict[int, str]]) -> list[tuple[str, int]]:
"""Vereinigt Baustein-Listen: exakte Titel-Dubletten (per _norm_titel) zusammenführen.
→ [("Titel — Beschreibung", nennungen)] in Erstnennungs-Reihenfolge.
nennungen = Anzahl der Listen, die den Titel nennen (Dublette innerhalb
einer Liste zählt nicht doppelt). Repräsentant ist die Erstnennung; ein
drittes ""-Segment (Quelle) wird verworfen.
"""
merged: dict[str, tuple[str, int]] = {}
for liste in listen:
gesehen: set[str] = set()
for text in liste.values():
key = _norm_titel(_titel(text))
if key in gesehen:
continue
gesehen.add(key)
if key in merged:
repr_text, n = merged[key]
merged[key] = (repr_text, n + 1)
else:
merged[key] = ("".join(text.split("")[:2]), 1)
return list(merged.values())
def _titel_index(entries: dict[int, str]) -> dict[str, int]:
return {_norm_titel(_titel(text)): num for num, text in entries.items()}
@@ -95,32 +119,96 @@ def _lade_bausteine(text: str) -> dict[int, str]:
_FRAGMENT_KAPITEL_RE = re.compile(r"<!--\s*kapitel\s*:\s*(.*?)\s*-->", re.IGNORECASE)
_FRAGMENT_SECTION_RE = re.compile(r"<!--\s*section\s*:\s*(.*?)\s*-->", re.IGNORECASE)
_FRAGMENT_SUB_RE = re.compile(r"<!--\s*sub\s*:\s*(.*?)\s*-->", re.IGNORECASE)
_FRAGMENT_BAUSTEIN_RE = re.compile(r"<!--\s*baustein\s*:\s*(.*?)\s*-->", re.IGNORECASE)
# Zwei Lese-Schichten je Section: kompakt (Merksätze) + ausführlich (Erklärung).
_FRAGMENT_KOMPAKT_RE = re.compile(r"<!--\s*kompakt\s*-->", re.IGNORECASE)
_FRAGMENT_AUSF_RE = re.compile(r"<!--\s*ausf(?:ü|ue)hrlich\s*-->", re.IGNORECASE)
_STUFEN = ("einfach", "mittel", "schwer")
def _parse_fragment(text: str) -> list[dict]:
"""Parst eine Writer-Datei → [{"kapitel", "titel", "md"}] in Datei-Reihenfolge."""
"""Parst eine Writer-Datei → [{"kapitel", "titel", "md", "kompakt", "subs"}] in Reihenfolge.
Zwei optionale Lese-Schichten je Section über `<!-- kompakt -->` / `<!-- ausführlich -->`:
Zeilen nach `kompakt` landen in `sec["kompakt"]`, sonst in `sec["md"]` (= ausführlich,
auch der Default ohne Marker → rückwärtskompatibel). Optionale `<!-- sub: -->`-Marker
sammeln sich weiter in `sec["subs"]`.
"""
sections: list[dict] = []
kapitel = None
current = None
cur_sub = None
cur_layer = "md" # Default: alles ohne Schicht-Marker ist die ausführliche Fassung
for line in text.splitlines():
s = line.strip()
m = _FRAGMENT_KAPITEL_RE.match(s)
if m:
kapitel = m.group(1)
current = None
cur_sub = None
continue
m = _FRAGMENT_SECTION_RE.match(s)
if m:
current = {"kapitel": kapitel, "titel": m.group(1), "md": []}
current = {"kapitel": kapitel, "titel": m.group(1), "md": [], "kompakt": [], "subs": []}
cur_sub = None
cur_layer = "md"
sections.append(current)
continue
if current is not None and _FRAGMENT_KOMPAKT_RE.match(s):
cur_layer = "kompakt"
cur_sub = None
continue
if current is not None and _FRAGMENT_AUSF_RE.match(s):
cur_layer = "md"
cur_sub = None
continue
m = _FRAGMENT_SUB_RE.match(s)
if m and current is not None:
teil = m.group(1).split("|", 1)
stufe = teil[0].strip().casefold()
cur_sub = {
"stufe": stufe if stufe in _STUFEN else "einfach",
"titel": teil[1].strip() if len(teil) == 2 else "",
"md": [],
}
current["subs"].append(cur_sub)
continue
if current is not None:
current["md"].append(line)
current[cur_layer].append(line)
if cur_sub is not None and cur_layer == "md":
cur_sub["md"].append(line)
for sec in sections:
sec["md"] = "\n".join(sec["md"]).strip()
sec["kompakt"] = "\n".join(sec["kompakt"]).strip()
for sub in sec["subs"]:
sub["md"] = "\n".join(sub["md"]).strip()
sec["subs"] = [sub for sub in sec["subs"] if sub["md"]]
return sections
def _parse_subbausteine(text: str) -> dict[str, list[str]]:
"""Parst eine Subbaustein-Datei → {Baustein-Titel: [Subbaustein, …]} in Reihenfolge.
Format: `<!-- baustein: Titel -->` gefolgt von Listenzeilen `- Subbaustein`.
"""
out: dict[str, list[str]] = {}
current = None
for line in text.splitlines():
s = line.strip()
m = _FRAGMENT_BAUSTEIN_RE.match(s)
if m:
current = m.group(1).strip()
out.setdefault(current, [])
continue
if current is None:
continue
m = re.match(r"[-*]\s+(.*\S)", s)
if m:
out[current].append(m.group(1).strip())
return {k: v for k, v in out.items() if v}
def _split_chunks(chapters: list[dict], n: int) -> list[list[dict]]:
"""Teilt Kapitel in bis zu n zusammenhängende Chunks, balanciert nach Section-Anzahl."""
n = max(1, min(n, len(chapters)))

View File

@@ -11,19 +11,26 @@
}
}
},
"minimax-direkt": {
"minimax-kalt": {
"npm": "@ai-sdk/anthropic",
"name": "MiniMax (ohne Thinking)",
"name": "MiniMax (kalt — niedrige Temperature, ohne Thinking)",
"options": {
"baseURL": "https://api.minimax.io/anthropic/v1",
"apiKey": "{env:MINIMAX_API_KEY}"
},
"models": {
"MiniMax-M3": {
"name": "MiniMax M3 (ohne Thinking)",
"name": "MiniMax M3 (kalt)",
"options": {
"temperature": 0.2,
"thinking": { "type": "disabled" }
}
},
"MiniMax-M2.7-highspeed": {
"name": "MiniMax M2.7 highspeed (kalt)",
"options": {
"temperature": 0.3
}
}
}
},

View File

@@ -12,6 +12,7 @@ services:
volumes:
- ./storage:/app/storage
- ./projects:/app/projects
- ./uni:/app/uni
- ./.claude-data:/home/app/.claude
labels:
- "traefik.enable=true"

View File

@@ -10,6 +10,7 @@
"dependencies": {
"dompurify": "^3.4.7",
"highlight.js": "^11.11.1",
"katex": "^0.17.0",
"marked": "^18.0.4",
"marked-highlight": "^2.2.4",
"vue": "^3.5.32"
@@ -1187,6 +1188,15 @@
],
"license": "CC-BY-4.0"
},
"node_modules/commander": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -1468,6 +1478,22 @@
"node": ">=6"
}
},
"node_modules/katex": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.17.0.tgz",
"integrity": "sha512-Vdw0ATsQ9V+LuegM/BTwQqV/6cTl5lbGcIrU+BCgLxyf6bo38ybOr372tuSIxir3CN720flu1meYR6XzNMwQnw==",
"funding": [
"https://opencollective.com/katex",
"https://github.com/sponsors/katex"
],
"license": "MIT",
"dependencies": {
"commander": "^8.3.0"
},
"bin": {
"katex": "cli.js"
}
},
"node_modules/kolorist": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz",

View File

@@ -11,6 +11,7 @@
"dependencies": {
"dompurify": "^3.4.7",
"highlight.js": "^11.11.1",
"katex": "^0.17.0",
"marked": "^18.0.4",
"marked-highlight": "^2.2.4",
"vue": "^3.5.32"

View File

@@ -1,14 +1,14 @@
<script setup>
import { ref, computed, watch, onMounted, nextTick } from 'vue'
import { fetchGuides, fetchTopics, createTopic as apiCreateTopic, deleteTopic as apiDeleteTopic, createGuide as apiCreate, deleteGuide, cancelGuide as apiCancel, fetchBausteineStatus, fetchActiveBausteine, createBausteine as apiCreateBausteine, cancelBausteine as apiCancelBausteine, deleteBausteine as apiDeleteBausteine, fetchProjects, deleteProject as apiDeleteProject, fetchProviders, fetchStats, fetchTopicFortschritt, fetchGuideLocks } from './api.js'
import { fetchGuides, fetchTopics, createTopic as apiCreateTopic, deleteTopic as apiDeleteTopic, createGuide as apiCreate, deleteGuide, cancelGuide as apiCancel, fetchBausteineStatus, fetchActiveBausteine, createBausteine as apiCreateBausteine, cancelBausteine as apiCancelBausteine, deleteBausteine as apiDeleteBausteine, fetchProviders, fetchStats, fetchTopicFortschritt, fetchGuideLocks, fetchGuideSteps, fetchFolders, updateQuelle as apiUpdateQuelle } from './api.js'
import { usePolling } from './composables/usePolling.js'
import TopicSidebar from './components/TopicSidebar.vue'
import TopicDetail from './components/TopicDetail.vue'
import BausteineUebersicht from './components/BausteineUebersicht.vue'
import ElementsSidebar from './components/elements/ElementsSidebar.vue'
import ElementsOverview from './components/ElementsOverview.vue'
const guides = ref([])
const projects = ref([])
const backendTopics = ref([])
const selectedTopic = ref(null)
const previewGuide = ref(null)
@@ -24,9 +24,13 @@ const bausteine = ref({ ...EMPTY_BAUSTEINE })
const activeBausteine = ref([])
const provider = ref(localStorage.getItem('provider') || 'claude')
const providers = ref([])
const folders = ref({ projekt: [], uni: [] }) // Ordner für die Quellen-Auswahl
const bausteineView = ref(false) // Bausteine-Übersicht im Hauptbereich
const ansichtModus = ref('kompakt') // kompakt | erklärend — je Thema, Default kompakt
const stats = ref(null)
const fortschritt = ref({})
const locks = ref({}) // Sperr-Gründe pro Format (Backend = einzige Regel-Quelle)
const guideStepsDone = ref({}) // höchster fertiger Schritt-Index je Format (artefakt-basiert)
const uiError = ref(null) // abgewiesene Aktionen (409/400) sichtbar machen
const elementsOpen = ref(false) // rechte Sidebar
const elementsView = ref(false) // Übersicht im Hauptbereich
@@ -47,6 +51,15 @@ function setProvider(id) {
localStorage.setItem('provider', id)
}
async function loadFolders() {
try {
const [projekt, uni] = await Promise.all([fetchFolders('projekt'), fetchFolders('uni')])
folders.value = { projekt, uni }
} catch (e) {
console.error('Fehler beim Laden der Ordner:', e)
}
}
async function loadProviders() {
try {
providers.value = await fetchProviders()
@@ -86,12 +99,7 @@ function onSidebarLeave() {
if (!sidebarPinned.value) sidebarSticky.value = false
}
const projectNames = computed(() => projects.value.map((p) => p.name))
const topics = computed(() => {
const isProject = new Set(projectNames.value)
return backendTopics.value.filter((t) => !isProject.has(t))
})
const topics = computed(() => backendTopics.value)
const doneByFormat = computed(() => {
const map = {}
@@ -163,10 +171,12 @@ async function loadBausteine() {
bausteine.value = await fetchBausteineStatus(selectedTopic.value)
fortschritt.value = await fetchTopicFortschritt(selectedTopic.value)
locks.value = await fetchGuideLocks(selectedTopic.value)
guideStepsDone.value = await fetchGuideSteps(selectedTopic.value)
} else {
bausteine.value = { ...EMPTY_BAUSTEINE }
fortschritt.value = {}
locks.value = {}
guideStepsDone.value = {}
}
if (activeBausteine.value.length && !polling.running()) startPolling()
} catch (e) {
@@ -174,15 +184,7 @@ async function loadBausteine() {
}
}
async function loadProjects() {
try {
projects.value = await fetchProjects()
} catch (e) {
console.error('Fehler beim Laden der Projekte:', e)
}
}
const FORMAT_ORDER = ['OnePager', 'MiniGuide', 'Guide', 'FullGuide']
const FORMAT_ORDER = ['Guide', 'FullGuide', 'Rest']
function autoPreview() {
const map = doneByFormat.value
@@ -201,7 +203,9 @@ function selectTopic(topic) {
sidebarSticky.value = false
elementsOpen.value = false
elementsView.value = false
bausteineView.value = false
elementOpenId.value = null
ansichtModus.value = localStorage.getItem('ansicht_' + topic) === 'erklärend' ? 'erklärend' : 'kompakt'
localStorage.setItem('lastTopic', topic)
loadBausteine()
nextTick(autoPreview)
@@ -232,11 +236,12 @@ async function handleResetBausteine() {
await loadBausteine()
}
async function handleBausteineClick({ instructions }) {
async function handleBausteineClick({ instructions, abPhase = null }) {
if (!selectedTopic.value) return
uiError.value = null
try {
await apiCreateBausteine(selectedTopic.value, instructions, provider.value)
// Quelle ist hier schon festgeschrieben; nur abPhase steuert den Re-Run-Umfang.
await apiCreateBausteine(selectedTopic.value, instructions, provider.value, undefined, undefined, abPhase)
} catch (e) {
uiError.value = e.message
return
@@ -245,7 +250,43 @@ async function handleBausteineClick({ instructions }) {
startPolling()
}
async function handleFormatClick({ format, instructions }) {
async function handleCreateThema({ topic, instructions, sourceType, sourceOrt }) {
uiError.value = null
try {
await apiCreateBausteine(topic, instructions, provider.value, sourceType, sourceOrt)
} catch (e) {
uiError.value = e.message
return
}
await loadTopics()
selectTopic(topic)
startPolling()
}
async function handleUpdateQuelle({ topic, type, ort, spec }) {
uiError.value = null
try {
await apiUpdateQuelle(topic, { type, ort, spec })
} catch (e) {
uiError.value = e.message
return
}
await loadBausteine() // Schritt-Anzeige folgt ggf. der neuen Quelle (z.B. Link → „Quelle laden")
}
function setAnsicht(modus) {
ansichtModus.value = modus
if (selectedTopic.value) localStorage.setItem('ansicht_' + selectedTopic.value, modus)
}
function handleOpenBausteineView() {
if (!selectedTopic.value) return
bausteineView.value = true
elementsView.value = false
previewGuide.value = null
}
async function handleFormatClick({ format, instructions, abStep = null }) {
if (!selectedTopic.value) return
// Kein Duplikat-Start: läuft für Thema+Format schon eine Generierung, ignorieren
const running = guides.value.some(
@@ -255,7 +296,7 @@ async function handleFormatClick({ format, instructions }) {
if (running) return
uiError.value = null
try {
await apiCreate(selectedTopic.value, format, instructions, provider.value)
await apiCreate(selectedTopic.value, format, instructions, provider.value, abStep)
} catch (e) {
uiError.value = e.message
return
@@ -264,23 +305,16 @@ async function handleFormatClick({ format, instructions }) {
startPolling()
}
async function handleDeleteProject(name) {
await apiDeleteProject(name)
if (selectedTopic.value === name) {
selectedTopic.value = null
previewGuide.value = null
}
await loadProjects()
}
function handlePreview(guide) {
previewGuide.value = guide
elementsView.value = false
bausteineView.value = false
}
function handleOpenElements() {
if (!selectedTopic.value) return
elementsView.value = true
bausteineView.value = false
// Rechte Sidebar bleibt zu — sie öffnet erst beim Klick auf ein Element.
}
@@ -325,10 +359,10 @@ async function handleDeleteTopic(topic) {
}
onMounted(async () => {
await Promise.all([loadGuides(), loadTopics(), loadProjects(), loadProviders()])
await Promise.all([loadGuides(), loadTopics(), loadProviders(), loadFolders()])
const savedTopic = localStorage.getItem('lastTopic')
const savedFormat = localStorage.getItem('lastFormat')
if (savedTopic && [...topics.value, ...projectNames.value].includes(savedTopic)) {
if (savedTopic && topics.value.includes(savedTopic)) {
selectTopic(savedTopic)
await nextTick()
const g = doneByFormat.value[savedFormat]
@@ -345,11 +379,11 @@ onMounted(async () => {
<div v-if="!sidebarPinned && sidebarSticky" class="sidebar-backdrop" @click="sidebarSticky = false"></div>
<TopicSidebar
:topics="topics"
:projects="projectNames"
:selectedTopic="selectedTopic"
:stats="stats"
:fortschritt="fortschritt"
:locks="locks"
:guideStepsDone="guideStepsDone"
:uiError="uiError"
:doneByFormat="doneByFormat"
:latestByFormat="latestByFormat"
@@ -361,16 +395,21 @@ onMounted(async () => {
:dark="darkMode"
:provider="provider"
:providers="providers"
:folders="folders"
:ansichtModus="ansichtModus"
@setProvider="setProvider"
@toggleDark="toggleDark"
@setAnsicht="setAnsicht"
@select="selectTopic"
@create="createTopic"
@createThema="handleCreateThema"
@updateQuelle="handleUpdateQuelle"
@openBausteineView="handleOpenBausteineView"
@formatClick="handleFormatClick"
@bausteineClick="handleBausteineClick"
@cancelBausteine="handleCancelBausteine"
@resetBausteine="handleResetBausteine"
@deleteTopic="handleDeleteTopic"
@deleteProject="handleDeleteProject"
@cancelGuide="handleCancel"
@deleteGuide="handleDeleteGuide"
@dismissError="handleDismissError"
@@ -380,8 +419,13 @@ onMounted(async () => {
@togglePin="toggleSidebarPin"
@sidebarLeave="onSidebarLeave"
/>
<BausteineUebersicht
v-if="selectedTopic && bausteineView"
:topic="selectedTopic"
@close="bausteineView = false"
/>
<ElementsOverview
v-if="selectedTopic && elementsView"
v-else-if="selectedTopic && elementsView"
:topic="selectedTopic"
:version="elementsVersion"
@open="handleOpenElementDetail"
@@ -392,8 +436,11 @@ onMounted(async () => {
:dark="darkMode"
:provider="provider"
:elementsOpen="elementsOpen"
:doneByFormat="doneByFormat"
:themaAbgeschlossen="!!fortschritt.abgeschlossen"
:ansichtModus="ansichtModus"
@progressChanged="loadStats(); loadBausteine()"
@openElements="elementsOpen = true"
@setAnsicht="setAnsicht"
/>
<div v-else class="empty-main">
<p>Thema in der Sidebar anlegen oder auswählen.</p>

View File

@@ -18,16 +18,21 @@ export async function fetchGuides() {
return res.json()
}
export async function fetchGuideSteps(topic) {
const res = await fetch(`${BASE}/guides/steps?topic=${encodeURIComponent(topic)}`)
return res.json()
}
export async function fetchGuideLocks(topic) {
const res = await fetch(`${BASE}/guides/locks?topic=${encodeURIComponent(topic)}`)
return res.json()
}
export async function createGuide(topic, format, instructions = '', provider = 'claude') {
export async function createGuide(topic, format, instructions = '', provider = 'claude', abStep = null) {
const res = await fetch(`${BASE}/guides`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ topic, format, instructions, provider }),
body: JSON.stringify({ topic, format, instructions, provider, ab_step: abStep }),
})
return jsonOrThrow(res)
}
@@ -42,11 +47,11 @@ export async function fetchBausteineStatus(topic) {
return res.json()
}
export async function createBausteine(topic, instructions = '', provider = 'claude') {
export async function createBausteine(topic, instructions = '', provider = 'claude', sourceType = 'thema', sourceOrt = '', abPhase = null) {
const res = await fetch(`${BASE}/bausteine`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ topic, instructions, provider }),
body: JSON.stringify({ topic, instructions, provider, source_type: sourceType, source_ort: sourceOrt, ab_phase: abPhase }),
})
return jsonOrThrow(res)
}
@@ -59,6 +64,41 @@ export async function deleteBausteine(topic) {
await fetch(`${BASE}/bausteine?topic=${encodeURIComponent(topic)}`, { method: 'DELETE' })
}
// --- Baustein-Lernen: Chat, Prüfung ---
export async function fetchBausteinLernstand(topic) {
const res = await fetch(`${BASE}/bausteine/lernstand?topic=${encodeURIComponent(topic)}`)
return jsonOrThrow(res)
}
export async function chatBaustein({ topic, baustein, section, section_kompakt = '', messages, provider }) {
const res = await fetch(`${BASE}/bausteine/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ topic, baustein, section, section_kompakt, messages, provider }),
})
return jsonOrThrow(res)
}
export async function pruefeBaustein({
topic, baustein, section, section_kompakt = '', provider,
aktion = 'frage', frage = '', letzte_bewertung = '', vermeide = [],
nachgefragt = false, begruendung = '', muster = '', cap = 6, messages = [], gruendlich = false,
auswahl = [], korrekt = [], loesung = '', alternativen = [], eingabe = '', schwer = false,
}) {
const res = await fetch(`${BASE}/bausteine/pruefung`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ topic, baustein, section, section_kompakt, aktion, frage, letzte_bewertung, vermeide, nachgefragt, begruendung, muster, cap, messages, provider, gruendlich, auswahl, korrekt, loesung, alternativen, eingabe, schwer }),
})
return jsonOrThrow(res)
}
export async function fetchFrageMuster(topic, baustein) {
const res = await fetch(`${BASE}/bausteine/frage-muster?topic=${encodeURIComponent(topic)}&baustein=${encodeURIComponent(baustein)}`)
return jsonOrThrow(res)
}
export async function fetchTopicFortschritt(topic) {
const res = await fetch(`${BASE}/topics/fortschritt?topic=${encodeURIComponent(topic)}`)
return res.json()
@@ -79,6 +119,30 @@ export async function fetchProjects() {
return res.json()
}
export async function fetchFolders(kind) {
const res = await fetch(`${BASE}/folders?kind=${encodeURIComponent(kind)}`)
return jsonOrThrow(res)
}
export async function fetchQuelle(topic) {
const res = await fetch(`${BASE}/bausteine/quelle?topic=${encodeURIComponent(topic)}`)
return jsonOrThrow(res)
}
export async function updateQuelle(topic, { type, ort = '', spec = '' }) {
const res = await fetch(`${BASE}/bausteine/quelle`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ topic, type, ort, spec }),
})
return jsonOrThrow(res)
}
export async function fetchBausteineUebersicht(topic) {
const res = await fetch(`${BASE}/bausteine/uebersicht?topic=${encodeURIComponent(topic)}`)
return jsonOrThrow(res)
}
export async function deleteProject(name) {
await fetch(`${BASE}/projects/${encodeURIComponent(name)}`, { method: 'DELETE' })
}
@@ -114,20 +178,6 @@ export async function deleteTopic(name) {
await fetch(`${BASE}/topics?topic=${encodeURIComponent(name)}`, { method: 'DELETE' })
}
export async function fetchProgress(id) {
const res = await fetch(`${BASE}/guides/${id}/progress`)
return res.json()
}
export async function setProgress(id, chapter, done) {
const res = await fetch(`${BASE}/guides/${id}/progress`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chapter, done }),
})
return res.json()
}
export async function chatGuide(id, { section, outline, messages, provider = 'claude' }) {
const res = await fetch(`${BASE}/guides/${id}/chat`, {
method: 'POST',

View File

@@ -7,6 +7,13 @@
margin: 0 0 0.5em;
}
/* KaTeX: lange Block-Formeln scrollen statt das Layout zu sprengen */
.markdown .katex-display {
overflow-x: auto;
overflow-y: hidden;
padding: 0.2em 0;
}
.markdown p:last-child {
margin-bottom: 0;
}

View File

@@ -0,0 +1,218 @@
<script setup>
import BausteinPanel from './BausteinPanel.vue'
import { renderMarkdown } from '../markdown.js'
const props = defineProps({
baustein: { type: Object, required: true }, // { title, md, num }
topic: { type: String, required: true },
provider: { type: String, default: 'claude' },
status: { type: Object, default: null },
cap: { type: Number, default: 6 },
tab: { type: String, default: 'pruefung' },
ansicht: { type: String, default: 'kompakt' }, // kompakt | erklärend — linke Guide-Spalte
hasPrev: { type: Boolean, default: false },
hasNext: { type: Boolean, default: false },
fortschritt: { type: Object, default: () => ({ total: 0, absolviert: 0, verstanden: 0, gemeistert: 0 }) },
})
import { computed, onMounted, onUnmounted, ref } from 'vue'
const emit = defineEmits(['close', 'prev', 'next', 'statusChanged', 'setAnsicht'])
const guideEl = ref(null) // linke Guide-Spalte (Scroll-Ziel für ALT+↑/↓)
const rightEl = ref(null) // rechte Spalte (Prüfungs-Panel mit Eingabefeld)
// ALT ist der Modifier der Fokus-Ansicht. Plain Pfeile bleiben Browser-Standard.
// ALT+←/→ blättert Baustein, ALT+↑/↓ scrollt den Guide. Ein reiner ALT-Tipp
// (drücken+loslassen ohne andere Taste) toggelt den Fokus aufs Eingabefeld.
let altAlone = false
function onKeyDown(e) {
if (e.key === 'Alt') { altAlone = true; e.preventDefault(); return } // unterdrückt Firefox-Menüleiste
if (e.ctrlKey || e.metaKey || !e.altKey) return // plain/andere → normal
altAlone = false // ALT+irgendwas = kein Lone-Tap
if (e.key === 'ArrowLeft') { e.preventDefault(); if (!e.repeat && props.hasPrev) emit('prev') }
else if (e.key === 'ArrowRight') { e.preventDefault(); if (!e.repeat && props.hasNext) emit('next') }
else if (e.key === 'ArrowUp') { e.preventDefault(); guideEl.value?.scrollBy(0, -120) }
else if (e.key === 'ArrowDown') { e.preventDefault(); guideEl.value?.scrollBy(0, 120) }
}
function onKeyUp(e) {
if (e.key === 'Alt') { e.preventDefault(); if (altAlone) { altAlone = false; toggleInput() } }
}
function resetAlt() { altAlone = false } // Fenster-Blur (ALT+Tab) → kein falscher Tipp
function toggleInput() {
// Antwortfeld der aktuellen Form: Erklären = textarea, Lückentext-frei = input.
const el = rightEl.value?.querySelector('textarea, input')
if (!el) return
document.activeElement === el ? el.blur() : el.focus()
}
onMounted(() => {
window.addEventListener('keydown', onKeyDown)
window.addEventListener('keyup', onKeyUp)
window.addEventListener('blur', resetAlt)
})
onUnmounted(() => {
window.removeEventListener('keydown', onKeyDown)
window.removeEventListener('keyup', onKeyUp)
window.removeEventListener('blur', resetAlt)
})
const pct = (n) => (100 * n / (props.fortschritt.total || 1)) + '%'
// Akzentfarbe nach Stand des aktuellen Bausteins: gemeistert=gold, verstanden=lila, absolviert=grün.
const standFarbe = computed(() => {
const s = props.status || {}
if (s.gemeistert) return '#d4af37'
if (s.verstanden) return '#8b5cf6'
if (s.absolviert) return 'var(--success-border)'
return 'var(--border)'
})
</script>
<template>
<div class="fokus-overlay" :style="{ '--stand': standFarbe }">
<div class="fokus-bar">
<div class="fokus-bar-inner">
<button class="fokus-btn" :disabled="!hasPrev" title="Voriger Baustein" @click="$emit('prev')"></button>
<button class="fokus-btn" :disabled="!hasNext" title="Nächster Baustein" @click="$emit('next')"></button>
<span class="fokus-titel">{{ baustein.title }}</span>
<span v-if="status?.gemeistert" class="stand-badge gold"> Gemeistert</span>
<span v-else-if="status?.verstanden" class="stand-badge lila"> Verstanden</span>
<span v-else-if="status?.absolviert" class="stand-badge gruen"> Absolviert</span>
<span class="fokus-spacer"></span>
<button class="fokus-btn" title="Vollansicht beenden" @click="$emit('close')"></button>
</div>
</div>
<div class="fokus-xp" :title="`${fortschritt.absolviert}/${fortschritt.total} absolviert · ${fortschritt.verstanden} verstanden · ${fortschritt.gemeistert} gemeistert`">
<div class="xp-seg gold" :style="{ width: pct(fortschritt.gemeistert) }"></div>
<div class="xp-seg lila" :style="{ width: pct(fortschritt.verstanden - fortschritt.gemeistert) }"></div>
<div class="xp-seg gruen" :style="{ width: pct(fortschritt.absolviert - fortschritt.verstanden) }"></div>
</div>
<div class="fokus-body">
<div ref="guideEl" class="fokus-col left">
<div class="markdown" v-html="renderMarkdown(ansicht === 'kompakt' ? (baustein.kompakt || baustein.md) : baustein.md)"></div>
</div>
<div ref="rightEl" class="fokus-col right">
<BausteinPanel
mode="full"
:key="baustein.title + '|' + tab"
:initial-tab="tab"
:topic="topic"
:baustein="baustein.title"
:section="baustein.md"
:section-kompakt="baustein.kompakt || ''"
:provider="provider"
:status="status"
:cap="cap"
:ansicht="ansicht"
@set-ansicht="$emit('setAnsicht', $event)"
@status-changed="$emit('statusChanged', $event)"
/>
</div>
</div>
</div>
</template>
<style scoped>
.fokus-overlay {
position: fixed;
inset: 0;
z-index: 40;
/* Halbtransparent + Blur: die Guide-Liste dahinter scheint verschwommen durch die grauen Flächen. */
background: color-mix(in srgb, var(--bg-preview) 60%, transparent);
backdrop-filter: blur(10px);
display: grid;
grid-template-rows: auto auto 1fr; /* Header / XP-Leiste / Body */
}
.fokus-bar {
padding: 0.5rem 0;
border-bottom: 1px solid var(--border);
background: var(--panel);
}
/* Header-Inhalt fluchtet mit dem Body (gleiche max-width + Innenabstand). */
.fokus-bar-inner {
display: flex;
align-items: center;
gap: 0.5rem;
max-width: 1600px;
width: 100%;
margin-inline: auto;
padding-inline: 1.25rem;
}
.fokus-spacer { flex: 1; }
.stand-badge {
margin-left: 0.5rem;
padding: 0.12rem 0.6rem;
font-size: 0.72rem; font-weight: 600;
border-radius: 999px; border: 1px solid; white-space: nowrap;
}
.stand-badge.gruen { background: var(--success-soft); border-color: var(--success-border); color: var(--success); }
.stand-badge.lila { background: color-mix(in srgb, #8b5cf6 16%, var(--panel)); border-color: #8b5cf6; color: #6d28d9; }
.stand-badge.gold { background: color-mix(in srgb, #d4af37 20%, var(--panel)); border-color: #d4af37; color: #8a6d12; }
/* Erfahrungsleiste oben: füllt sich von links — gold (gemeistert) → lila (verstanden) → grün (absolviert). */
.fokus-xp { display: flex; height: 6px; background: var(--panel-soft); }
.xp-seg { height: 100%; transition: width 0.3s ease; }
.xp-seg.gold { background: #d4af37; }
.xp-seg.lila { background: #8b5cf6; }
.xp-seg.gruen { background: var(--success-border); }
.fokus-titel { font-weight: 600; font-size: 0.95rem; margin-left: 0.5rem; }
.fokus-btn {
display: inline-flex; align-items: center; justify-content: center;
min-width: 2rem; height: 2rem; padding: 0 0.5rem;
font-size: 1rem;
border: 1px solid var(--border-strong);
border-radius: 6px;
background: var(--panel);
color: var(--text);
cursor: pointer;
}
.fokus-btn:hover { border-color: var(--accent); }
.fokus-btn:disabled { opacity: 0.4; cursor: default; }
/* Zwei abgehobene Karten auf grauem „Schreibtisch" (--bg-preview vom Overlay). */
.fokus-body {
min-height: 0;
display: flex;
gap: 1.25rem;
padding: 1.25rem;
max-width: 1600px;
width: 100%;
margin-inline: auto;
}
.fokus-col {
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
/* Karte kippt nach Baustein-Stand: farbiger Rahmen + getönter Hintergrund (grün/lila/gold). */
background: color-mix(in srgb, var(--stand) 7%, var(--panel));
border: 1px solid var(--stand);
border-top: 3px solid var(--stand);
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
}
/* Links: breite Lese-Karte, Text in Lesebreite zentriert. */
.fokus-col.left { flex: 1; padding: 2rem 2.5rem; }
.fokus-col.left > * { max-width: 74ch; margin-inline: auto; }
/* Rechts: Arbeits-Karte; Panel darin randlos (Karte ist der Rahmen). */
.fokus-col.right { flex: none; width: clamp(440px, 34%, 600px); padding: 1.5rem; }
.fokus-col.right :deep(.bp) { margin-top: 0; }
.fokus-col.right :deep(.bp-panel) { border: none; background: transparent; padding: 0; }
.fokus-h2 { font-size: 1.1rem; margin: 0 0 0.75rem; }
/* Desktop: die rechte Karte als Flex-Spalte — Prüfungs-/Chat-Verlauf füllt die
volle Höhe und scrollt intern, Eingabe + Buttons bleiben unten. */
@media (min-width: 901px) {
.fokus-col.right { display: flex; flex-direction: column; overflow: hidden; }
.fokus-col.right :deep(.bp) { flex: 1; display: flex; flex-direction: column; min-height: 0; }
.fokus-col.right :deep(.bp-panel) { flex: 1; display: flex; flex-direction: column; min-height: 0; overflow-y: auto; }
.fokus-col.right :deep(.bp-panel > div) { flex: 1; display: flex; flex-direction: column; min-height: 0; }
.fokus-col.right :deep(.bp-messages) { flex: 1; min-height: 0; max-height: none; }
}
@media (max-width: 900px) {
.fokus-body { flex-direction: column; overflow-y: auto; }
.fokus-col { overflow-y: visible; }
.fokus-col.right { width: auto; }
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,225 @@
<script setup>
import { ref, computed, watch } from 'vue'
import { fetchBausteineUebersicht } from '../api.js'
const props = defineProps({
topic: { type: String, required: true },
})
const emit = defineEmits(['close'])
const items = ref([])
const loading = ref(true)
const error = ref(null)
// Stufen-Reihenfolge + Anzeige-Label (Farbe via CSS-Klasse st-<key>)
const STUFEN = [
{ key: 'einfach', label: 'Einfach' },
{ key: 'mittel', label: 'Mittel' },
{ key: 'schwer', label: 'Schwer' },
]
watch(() => props.topic, load, { immediate: true })
async function load() {
loading.value = true
error.value = null
items.value = []
try {
items.value = await fetchBausteineUebersicht(props.topic)
} catch (e) {
error.value = 'Übersicht nicht verfügbar — bitte erst Bausteine erstellen.'
} finally {
loading.value = false
}
}
// Baustein relevant = hat ≥1 relevanten Subbaustein (gleiche Regel wie der Guide).
// Ohne Relevanz-Daten (Alt-Themen) nicht dimmen.
function relevant(b) {
const mitRelevanz = (b.subbausteine || []).filter((s) => s.relevanz)
return !mitRelevanz.length || mitRelevanz.some((s) => s.relevanz === 'relevant')
}
// Nur nicht-leere Stufen-Gruppen je Baustein (v-if + v-for nicht auf einem Element)
function gruppen(b) {
return STUFEN
.map((st) => ({ ...st, subs: (b.subbausteine || []).filter((s) => s.stufe === st.key) }))
.filter((g) => g.subs.length)
}
const subTotal = computed(() => items.value.reduce((n, b) => n + (b.subbausteine?.length || 0), 0))
</script>
<template>
<div class="bk-view">
<header class="bk-head">
<h1>{{ topic }}</h1>
<span class="bk-sub">Bausteine-Übersicht</span>
<span v-if="items.length" class="bk-count">{{ items.length }} Bausteine · {{ subTotal }} Subbausteine</span>
<span class="bk-spacer"></span>
<button class="bk-close" title="Schließen" @click="emit('close')"></button>
</header>
<div v-if="loading" class="bk-empty-state">Lade</div>
<div v-else-if="error" class="bk-empty-state">{{ error }}</div>
<div v-else-if="!items.length" class="bk-empty-state">Noch keine Bausteine.</div>
<div v-else class="bk-grid">
<article
v-for="b in items"
:key="b.num"
class="bk-card"
:class="{ 'bk-irrelevant': !relevant(b) }"
:title="relevant(b) ? null : 'Nicht relevant — kommt später in den „Rest“'"
>
<h3 class="bk-title"><span class="bk-num">{{ b.num }}</span>{{ b.titel }}</h3>
<p v-if="b.beschreibung" class="bk-desc">{{ b.beschreibung }}</p>
<div v-if="b.subbausteine && b.subbausteine.length" class="bk-stufen">
<div v-for="g in gruppen(b)" :key="g.key" class="bk-stufe" :class="'st-' + g.key">
<span class="bk-stufe-label">{{ g.label }}</span>
<ul>
<li v-for="s in g.subs" :key="s.titel" :class="{ rand: s.relevanz === 'rand' }">
{{ s.titel }}<span v-if="s.relevanz === 'rand'" class="rand-tag" title="Randthema — kommt später in den „Rest“">Rand</span>
</li>
</ul>
</div>
</div>
<p v-else class="bk-no-subs">Keine Subbausteine.</p>
</article>
</div>
</div>
</template>
<style scoped>
.bk-view {
flex: 1;
min-width: 0;
height: 100dvh;
display: flex;
flex-direction: column;
background: var(--bg-preview);
}
.bk-head {
display: flex;
align-items: baseline;
gap: 0.75rem;
padding: 1.25rem 2rem;
border-bottom: 1px solid var(--border);
background: var(--panel);
}
.bk-head h1 { font-size: 1.5rem; }
.bk-sub { color: var(--text-faint); font-size: 0.9rem; font-weight: 600; }
.bk-count { color: var(--text-muted); font-size: 0.82rem; }
.bk-spacer { flex: 1; }
.bk-close {
align-self: center;
border: 1px solid var(--border-strong);
border-radius: 6px;
background: var(--panel);
color: var(--text);
width: 2rem;
height: 2rem;
cursor: pointer;
}
.bk-close:hover { border-color: var(--accent); }
.bk-empty-state {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-muted);
}
.bk-grid {
flex: 1;
overflow-y: auto;
padding: 1.5rem 2rem 4rem;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 1rem;
align-content: start;
}
.bk-card {
background: var(--panel);
border: 1px solid var(--border);
border-top: 3px solid var(--accent-border);
border-radius: 10px;
padding: 1rem 1.1rem;
}
/* Nicht relevante Bausteine (kein relevanter Subbaustein) gedimmt */
.bk-card.bk-irrelevant { opacity: 0.5; }
.bk-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 1rem;
margin-bottom: 0.4rem;
}
.bk-num {
flex: 0 0 auto;
min-width: 26px;
height: 26px;
padding: 0 6px;
border-radius: 7px;
background: var(--accent);
color: var(--on-accent);
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 0.78rem;
font-weight: 700;
}
.bk-desc {
color: var(--text-muted);
font-size: 0.85rem;
line-height: 1.45;
margin-bottom: 0.7rem;
}
.bk-stufen { display: flex; flex-direction: column; gap: 0.55rem; }
.bk-stufe {
border-left: 3px solid var(--st);
padding-left: 0.6rem;
}
.bk-stufe.st-einfach { --st: var(--success-border); }
.bk-stufe.st-mittel { --st: var(--warning-border); }
.bk-stufe.st-schwer { --st: var(--danger); }
.bk-stufe-label {
font-size: 0.66rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--st);
}
.bk-stufe ul {
list-style: disc;
margin: 0.25rem 0 0;
padding-left: 1.15rem;
}
.bk-stufe li {
font-size: 0.85rem;
color: var(--text);
line-height: 1.35;
margin-bottom: 2px;
}
.bk-stufe li::marker { color: var(--text-faint); }
.bk-stufe li.rand { color: var(--text-faint); }
.rand-tag {
margin-left: 0.4rem;
font-size: 0.6rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.03em;
color: var(--text-faint);
border: 1px solid var(--border-strong);
border-radius: 4px;
padding: 0 4px;
}
.bk-no-subs { color: var(--text-faint); font-size: 0.8rem; font-style: italic; }
</style>

View File

@@ -1,35 +1,38 @@
<script setup>
import { computed, ref, watch, nextTick, onMounted, onUnmounted } from 'vue'
import { fetchGuideContent, chatGuide, fetchProgress, setProgress } from '../api.js'
import { fetchGuideContent, chatGuide, fetchBausteinLernstand } from '../api.js'
import { renderMarkdown } from '../markdown.js'
import { useChat } from '../composables/useChat.js'
import BausteinPanel from './BausteinPanel.vue'
import BausteinFokus from './BausteinFokus.vue'
const props = defineProps({
previewGuide: { type: Object, default: null },
dark: { type: Boolean, default: false },
provider: { type: String, default: 'claude' },
elementsOpen: { type: Boolean, default: false }, // Element-Sidebar offen → Chat nach links
doneByFormat: { type: Object, default: () => ({}) }, // Format → fertiger Guide (Themen-bezogen)
themaAbgeschlossen: { type: Boolean, default: false },
ansichtModus: { type: String, default: 'kompakt' }, // kompakt | erklärend
})
const emit = defineEmits(['progressChanged', 'openElements'])
const emit = defineEmits(['progressChanged', 'setAnsicht'])
const isOnePager = computed(() => props.previewGuide?.format === 'OnePager')
// Rotierende Kapitel-Akzentfarben (passend zum OnePager-Cheat-Sheet, ohne Rot)
// Rotierende Kapitel-Akzentfarben (ohne Rot)
const CH_COLORS = ['#3b82f6', '#8b5cf6', '#14b8a6', '#f59e0b', '#22c55e', '#6366f1']
// --- Inhalt laden ---
const content = ref(null)
const loadError = ref(null)
const doneChapters = ref(new Set())
const scrollEl = ref(null)
const lernstand = ref({}) // Prüfungs-Stand pro Baustein-Titel — VOR dem immediate-Watch (loadContent nutzt es)
watch(() => props.previewGuide?.id, loadContent, { immediate: true })
async function loadContent() {
content.value = null
loadError.value = null
doneChapters.value = new Set()
lernstand.value = {}
const g = props.previewGuide
if (!g || g.status !== 'done') return
try {
@@ -40,37 +43,59 @@ async function loadContent() {
return
}
try {
const res = await fetchProgress(g.id)
doneChapters.value = new Set(res.chapters || [])
lernstand.value = (await fetchBausteinLernstand(g.topic)).bausteine || {}
} catch { /* offline → leer */ }
nextTick(scrollToFirstOpen)
}
// Zum ersten noch offenen Kapitel springen — aber nur, wenn schon etwas erledigt ist.
function scrollToFirstOpen() {
if (!doneChapters.value.size || !content.value) return
const chapters = Array.from(scrollEl.value?.querySelectorAll('section.chapter') || [])
const firstOpen = chapters.find((el) => !el.classList.contains('ch-complete'))
if (firstOpen && firstOpen !== chapters[0]) firstOpen.scrollIntoView({ block: 'start' })
// --- Baustein-Lernen: Prüfungs-Stand pro Baustein-Titel (lernstand oben deklariert) ---
function onBausteinStatus(baustein, status) {
const warAbsolviert = lernstand.value[baustein]?.absolviert
lernstand.value = { ...lernstand.value, [baustein]: status }
if (status.absolviert && !warAbsolviert) emit('progressChanged') // Locks/Stats neu laden
}
// --- Kapitel-Fortschritt ---
async function toggleChapter(title) {
const newState = !doneChapters.value.has(title)
const optimistic = new Set(doneChapters.value)
if (newState) optimistic.add(title)
else optimistic.delete(title)
doneChapters.value = optimistic
try {
const res = await setProgress(props.previewGuide.id, title, newState)
doneChapters.value = new Set(res.chapters || [])
emit('progressChanged')
} catch {
const rollback = new Set(doneChapters.value)
if (newState) rollback.delete(title)
else rollback.add(title)
doneChapters.value = rollback
// --- Vollbild-Fokus: ein Baustein groß, Guide links + Prüfung rechts ---
const fokusIndex = ref(null) // Index in der flachen Baustein-Liste; null = zu
const fokusTab = ref('pruefung')
const bausteine = computed(() =>
!content.value ? [] : content.value.chapters.flatMap((ch) => ch.sections),
)
const fokusBaustein = computed(() => (fokusIndex.value === null ? null : bausteine.value[fokusIndex.value]))
function openFokus(baustein, tab) {
if (!istPruefbar(baustein)) return // reine Lese-Sections (Rest/Rand) öffnen keinen Prüfungs-Fokus
const i = bausteine.value.findIndex((s) => s.title === baustein.title)
if (i === -1) return
fokusIndex.value = i
fokusTab.value = tab || 'pruefung'
}
// Nur prüfbare Sections im Fokus anspringen (FullGuide hat dazwischen reine Lese-Sections).
function fokusPrev() {
for (let i = fokusIndex.value - 1; i >= 0; i--) if (istPruefbar(bausteine.value[i])) { fokusIndex.value = i; return }
}
function fokusNext() {
for (let i = fokusIndex.value + 1; i < bausteine.value.length; i++) if (istPruefbar(bausteine.value[i])) { fokusIndex.value = i; return }
}
// Erfahrungsleiste: Lernstand über alle Bausteine des Guides (sticky, absolviert ⊇ verstanden ⊇ gemeistert).
const fortschritt = computed(() => {
let a = 0, v = 0, g = 0
for (const s of bausteine.value) {
const l = lernstand.value[s.title] || {}
if (l.absolviert) a++
if (l.verstanden) v++
if (l.gemeistert) g++
}
return { total: bausteine.value.length, absolviert: a, verstanden: v, gemeistert: g }
})
// Prüfung läuft voll 030, sobald Guide ODER FullGuide fertig ist (gleicher Baustein-Score).
const cap = computed(() => ((props.doneByFormat?.Guide || props.doneByFormat?.FullGuide) ? 30 : 6))
// Section prüfbar? Rest nie. Sonst prüfbar, außer das Feld ist explizit false (FullGuide-Rand).
// Fehlt das Feld (alte Guides ohne `pruefbar`) → prüfbar, damit Bestands-Guides weiter funktionieren.
function istPruefbar(s) {
return props.previewGuide?.format !== 'Rest' && s.pruefbar !== false
}
// --- Chat (Mechanik in useChat; Kontext-Extraktion bleibt hier) ---
@@ -80,7 +105,7 @@ const chat = useChat((msgs) => {
section, outline, messages: msgs, provider: props.provider,
})
})
const { messages, input, loading, messagesEl, inputEl, send } = chat
const { messages, input, loading, messagesEl, inputEl, onScroll, send } = chat
const autoGrow = () => chat.autoGrow()
const chatOpen = ref(false)
const panelEl = ref(null)
@@ -155,16 +180,17 @@ function extractContext() {
<template>
<div class="detail">
<div v-if="previewGuide && content" ref="scrollEl" class="guide-scroll">
<div class="guide-content" :class="{ onepager: isOnePager }">
<div class="guide-content">
<header class="guide-head">
<h1>{{ previewGuide.topic }}</h1>
<span class="guide-format">{{ previewGuide.format }}</span>
<span v-if="themaAbgeschlossen" class="thema-done" title="Alle Bausteine gemeistert (18/18)"> Thema abgeschlossen</span>
</header>
<section
v-for="(ch, ci) in content.chapters"
:key="ch.title"
class="chapter"
:class="{ 'ch-complete': doneChapters.has(ch.title) }"
:style="{ '--ch-accent': CH_COLORS[ci % CH_COLORS.length] }"
>
<h2 class="chapter-title"><span class="ch-num">{{ ci + 1 }}</span>{{ ch.title }}</h2>
@@ -172,19 +198,28 @@ function extractContext() {
<article
v-for="s in ch.sections"
:key="s.num"
:class="['section-card', isOnePager && s.key ? 'op-card op-' + s.key : '']"
:style="isOnePager && s.key ? { gridArea: s.key } : null"
:class="['section-card', lernstand[s.title]?.gemeistert ? 'gemeistert' : (lernstand[s.title]?.verstanden ? 'verstanden' : (lernstand[s.title]?.absolviert ? 'absolviert' : ''))]"
>
<h3>{{ s.title }}</h3>
<div class="section-body markdown" v-html="renderMarkdown(s.md)"></div>
<h3 :class="{ 'baustein-klick': istPruefbar(s) }" @click="istPruefbar(s) && openFokus(s, 'pruefung')">
{{ s.title }}
<template v-if="istPruefbar(s)">
<span v-if="lernstand[s.title]?.gemeistert" class="baustein-done gemeistert" title="Gemeistert (25/25)"> Gemeistert</span>
<span v-else-if="lernstand[s.title]?.verstanden" class="baustein-done verstanden" title="Vollständig verstanden (10/10)"> Verstanden</span>
<span v-else-if="lernstand[s.title]?.absolviert" class="baustein-done" title="Prüfung bestanden"> Absolviert</span>
</template>
</h3>
<div class="section-body markdown" v-html="renderMarkdown(ansichtModus === 'kompakt' ? (s.kompakt || s.md) : s.md)"></div>
<BausteinPanel
v-if="istPruefbar(s)"
mode="trigger"
:baustein="s.title"
:status="lernstand[s.title]"
:cap="cap"
:topic="previewGuide.topic"
@open-fokus="(tab) => openFokus(s, tab)"
/>
</article>
</div>
<button
v-if="!isOnePager"
class="ch-toggle"
:class="{ 'is-done': doneChapters.has(ch.title) }"
@click="toggleChapter(ch.title)"
>{{ doneChapters.has(ch.title) ? '✓ Erledigt rückgängig' : 'Kapitel als erledigt markieren' }}</button>
</section>
</div>
</div>
@@ -197,15 +232,33 @@ function extractContext() {
<p>Guide-Format anklicken um zu generieren oder Vorschau zu öffnen.</p>
</div>
<button v-if="previewGuide && !chatOpen" class="chat-fab" :class="{ shifted: elementsOpen }" title="Fragen zum Guide" @click="openChat">💬</button>
<button v-if="previewGuide && !chatOpen && !elementsOpen" class="chat-fab elements-fab" title="Elemente öffnen" @click="emit('openElements')">🗂</button>
<BausteinFokus
v-if="fokusBaustein"
:baustein="fokusBaustein"
:topic="previewGuide.topic"
:provider="provider"
:fortschritt="fortschritt"
:status="lernstand[fokusBaustein.title]"
:cap="cap"
:tab="fokusTab"
:ansicht="ansichtModus"
:has-prev="fokusIndex > 0"
:has-next="fokusIndex < bausteine.length - 1"
@prev="fokusPrev"
@next="fokusNext"
@close="fokusIndex = null"
@set-ansicht="$emit('setAnsicht', $event)"
@status-changed="(st) => onBausteinStatus(st.baustein, st)"
/>
<button v-if="previewGuide && !chatOpen && fokusIndex === null" class="chat-fab" :class="{ shifted: elementsOpen }" title="Fragen zum Guide" @click="openChat">💬</button>
<div v-if="previewGuide && chatOpen" ref="panelEl" class="chat-panel" :class="{ shifted: elementsOpen }">
<header class="chat-header">
<span>Fragen zum Guide</span>
<button class="chat-close" title="Chat beenden" @click="closeChat">×</button>
</header>
<div ref="messagesEl" class="chat-messages">
<div ref="messagesEl" class="chat-messages" @scroll="onScroll">
<p v-if="!messages.length" class="chat-hint">Stell eine Frage zum aktuellen Abschnitt.</p>
<template v-for="(m, i) in messages" :key="i">
<div v-if="m.role === 'assistant'" class="chat-msg assistant markdown" v-html="renderMarkdown(m.content)"></div>
@@ -282,6 +335,16 @@ function extractContext() {
font-weight: 600;
}
.thema-done {
font-size: 0.8rem;
font-weight: 600;
padding: 0.15rem 0.6rem;
border-radius: 999px;
background: color-mix(in srgb, #d4af37 20%, var(--panel));
border: 1px solid #d4af37;
color: #8a6d12;
}
.chapter {
margin-bottom: 2.5rem;
}
@@ -310,10 +373,6 @@ function extractContext() {
font-weight: 700;
}
.chapter.ch-complete .sections {
opacity: 0.4;
}
.section-card {
background: var(--panel);
border: 1px solid var(--border);
@@ -322,8 +381,53 @@ function extractContext() {
margin-bottom: 0.75rem;
}
/* Guides: Karten tragen die Kapitel-Akzentfarbe (OnePager hat eigene op-card-Farben) */
.guide-content:not(.onepager) .section-card {
.baustein-done {
float: right;
margin-left: 0.5rem;
padding: 0.12rem 0.6rem;
font-size: 0.68em;
font-weight: 600;
line-height: 1.5;
border-radius: 999px;
background: var(--success-soft);
border: 1px solid var(--success-border);
color: var(--success);
white-space: nowrap;
}
/* Absolvierte Bausteine: Karte kippt sichtbar auf Grün */
.guide-content .section-card.absolviert {
border-color: var(--success-border);
border-top: 3px solid var(--success);
background: color-mix(in srgb, var(--success) 5%, var(--panel));
}
/* Verstandene Bausteine (10/10): Lila */
.baustein-done.verstanden {
background: color-mix(in srgb, #8b5cf6 16%, var(--panel));
border-color: #8b5cf6;
color: #6d28d9;
}
.guide-content .section-card.verstanden {
border-color: #8b5cf6;
border-top: 3px solid #8b5cf6;
background: color-mix(in srgb, #8b5cf6 7%, var(--panel));
}
/* Gemeisterte Bausteine (Meisterpfad 25/25): Gold */
.baustein-done.gemeistert {
background: color-mix(in srgb, #d4af37 20%, var(--panel));
border-color: #d4af37;
color: #8a6d12;
}
.guide-content .section-card.gemeistert {
border-color: #d4af37;
border-top: 3px solid #d4af37;
background: color-mix(in srgb, #d4af37 8%, var(--panel));
}
/* Guides: Karten tragen die Kapitel-Akzentfarbe */
.guide-content .section-card {
border-top: 3px solid color-mix(in srgb, var(--ch-accent, var(--accent)) 65%, transparent);
background: color-mix(in srgb, var(--ch-accent, var(--accent)) 3%, var(--panel));
}
@@ -336,136 +440,9 @@ function extractContext() {
}
}
/* OnePager: festes 3×3-Raster über volle Breite und Höhe.
Kein Lese-Zoom (bricht 100%-Höhen) — stattdessen sind die Schriften unten 1.2× skaliert. */
.guide-content.onepager {
max-width: none;
height: 100%;
zoom: 1;
padding: 0.9rem 1rem;
display: flex;
flex-direction: column;
}
.guide-content.onepager .guide-head,
.guide-content.onepager .chapter-title {
display: none; /* Thema steht in der Info-Karte — Platz fürs Raster */
}
.guide-content.onepager .chapter {
flex: 1;
min-height: 0;
margin-bottom: 0;
}
.guide-content.onepager .sections {
height: 100%;
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
grid-template-areas:
"info beispiel voraussetzungen"
"eigenschaften beispiel modern"
"eigenschaften zusammenhaenge veraltet";
gap: 0.6rem;
}
.guide-content.onepager .section-card {
margin-bottom: 0;
padding: 0.7rem 0.9rem;
min-height: 0;
overflow-y: auto;
h3 {
font-size: 1.06rem;
margin-bottom: 0.3rem;
}
.section-body {
font-size: 0.98rem;
}
}
/* Farbkodiertes Cheat-Sheet: feste Akzentfarbe + Icon pro Karte */
.op-info { --op-accent: #3b82f6; }
.op-eigenschaften { --op-accent: #8b5cf6; --op-icon: "☰"; }
.op-beispiel { --op-accent: #64748b; --op-icon: ""; }
.op-zusammenhaenge { --op-accent: #14b8a6; --op-icon: "⇄"; }
.op-voraussetzungen { --op-accent: #f59e0b; --op-icon: "✓"; }
.op-modern { --op-accent: #22c55e; --op-icon: "✦"; }
.op-veraltet { --op-accent: #ef4444; --op-icon: "⚠"; }
.guide-content.onepager .section-card.op-card {
border-top: 3px solid var(--op-accent);
background: color-mix(in srgb, var(--op-accent) 5%, var(--panel));
h3 {
color: var(--op-accent);
font-size: 0.95rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
}
h3::before {
content: var(--op-icon, "");
margin-right: 7px;
}
}
/* Info-Karte: Thema als große Headline statt Uppercase-Label */
.guide-content.onepager .op-card.op-info h3 {
font-size: 1.5rem;
text-transform: none;
letter-spacing: 0;
color: var(--text);
}
/* Mobil: eine Spalte in Quellreihenfolge (info → … → veraltet) */
@media (max-width: 900px) {
.guide-content.onepager {
height: auto;
}
.guide-content.onepager .sections {
height: auto;
display: flex;
flex-direction: column;
}
.guide-content.onepager .section-card {
overflow-y: visible;
}
}
.ch-toggle {
display: block;
width: 100%;
margin-top: 0.5rem;
padding: 0.8rem 1rem;
border: 1.5px dashed var(--border-strong);
border-radius: 10px;
background: var(--panel-soft);
color: var(--text-muted);
font: 600 0.9rem/1.2 inherit;
font-family: inherit;
text-align: center;
cursor: pointer;
transition: all 0.12s;
&:hover {
border-color: var(--accent);
color: var(--accent);
background: transparent;
}
&.is-done {
border-style: solid;
border-color: var(--success-border);
background: var(--success-soft);
color: var(--success);
}
}
/* Titel-Klick öffnet die Vollansicht. */
.section-card h3.baustein-klick { cursor: pointer; width: fit-content; }
.section-card h3.baustein-klick:hover { color: var(--accent); }
.empty-preview {
display: flex;
@@ -506,12 +483,6 @@ function extractContext() {
max-width: 70ch;
}
.onepager .section-card .markdown :deep(p),
.onepager .section-card .markdown :deep(ul),
.onepager .section-card .markdown :deep(ol) {
max-width: none; /* OnePager-Zellen sind selbst schmal genug */
}
/* --- Chat --- */
.chat-fab {
position: fixed;
@@ -533,10 +504,6 @@ function extractContext() {
background: var(--accent-hover);
}
.elements-fab {
right: 5.25rem;
}
/* Element-Sidebar (320px) offen → Chat links daneben anzeigen */
.chat-fab.shifted {
right: calc(1.5rem + 320px);

View File

@@ -1,14 +1,15 @@
<script setup>
import { ref, computed } from 'vue'
import { ref, reactive, computed } from 'vue'
import { useConfirm } from '../composables/useConfirm.js'
import { fetchQuelle } from '../api.js'
const props = defineProps({
topics: { type: Array, required: true },
projects: { type: Array, default: () => [] },
selectedTopic: { type: String, default: null },
stats: { type: Object, default: null },
fortschritt: { type: Object, default: () => ({}) },
locks: { type: Object, default: () => ({}) },
guideStepsDone: { type: Object, default: () => ({}) }, // höchster fertiger Schritt je Format (-1 = keiner)
uiError: { type: String, default: null },
doneByFormat: { type: Object, default: () => ({}) },
latestByFormat: { type: Object, default: () => ({}) },
@@ -20,16 +21,25 @@ const props = defineProps({
dark: { type: Boolean, default: false },
provider: { type: String, default: 'claude' },
providers: { type: Array, default: () => [] },
folders: { type: Object, default: () => ({ projekt: [], uni: [] }) },
ansichtModus: { type: String, default: 'kompakt' }, // kompakt | erklärend
})
const emit = defineEmits(['select', 'create', 'formatClick', 'bausteineClick', 'cancelBausteine', 'resetBausteine', 'deleteTopic', 'deleteProject', 'cancelGuide', 'deleteGuide', 'dismissError', 'dismissUiError', 'preview', 'openElements', 'togglePin', 'sidebarLeave', 'toggleDark', 'setProvider'])
const emit = defineEmits(['select', 'create', 'createThema', 'updateQuelle', 'formatClick', 'bausteineClick', 'cancelBausteine', 'resetBausteine', 'deleteTopic', 'cancelGuide', 'deleteGuide', 'dismissError', 'dismissUiError', 'preview', 'openBausteineView', 'openElements', 'togglePin', 'sidebarLeave', 'toggleDark', 'setAnsicht', 'setProvider'])
// Accordion: höchstens ein Panel offen. IDs: 'bausteine', 'fmt-<Format>', 'topic-<Name>'.
const openPanel = ref(null)
const isOpen = (id) => openPanel.value === id
function togglePanel(id) {
openPanel.value = openPanel.value === id ? null : id
}
function providerAvailable(id) {
const p = props.providers.find((x) => x.id === id)
return p ? p.available : true
}
const PROVIDER_LABELS = { claude: 'Claude', minimax: 'MiniMax', 'minimax-direkt': 'MiniMax direkt', lokal: 'Lokal' }
const PROVIDER_LABELS = { claude: 'Claude', minimax: 'MiniMax', lokal: 'Lokal' }
// Tracker oben in der Navigation: Themen gesamt, pro Format erstellt/absolviert
const trackerItems = computed(() => {
@@ -38,17 +48,14 @@ const trackerItems = computed(() => {
const fmt = (k) => `${f[k]?.absolviert ?? 0}/${f[k]?.erstellt ?? 0}`
return [
{ label: 'Themen', value: String(props.stats.themen ?? 0), title: 'Themen inkl. Projekte' },
{ label: 'MiniGuides', value: fmt('MiniGuide'), title: 'absolviert/erstellt' },
{ label: 'Guides', value: fmt('Guide'), title: 'absolviert/erstellt' },
{ label: 'FullGuides', value: fmt('FullGuide'), title: 'absolviert/erstellt' },
]
})
const formats = [
{ key: 'OnePager', label: 'OnePager' },
{ key: 'MiniGuide', label: 'MiniGuide' },
{ key: 'Guide', label: 'Guide' },
{ key: 'FullGuide', label: 'FullGuide' },
{ key: 'Rest', label: 'Rest' },
]
const bausteineState = computed(() => {
@@ -56,6 +63,17 @@ const bausteineState = computed(() => {
return props.bausteine.ready ? 'done' : 'none'
})
// Re-Run ab gewählter Phase (1-basiert). null = ab Anfang / Fortsetzen.
const gewaehltePhase = ref(null)
// Phasen anklickbar, sobald etwas (teil-)gebaut ist und gerade nicht generiert wird.
const phasenWaehlbar = computed(() => (props.bausteine.ready || props.bausteine.partial) && !props.bausteine.generating)
const gewaehltesLabel = computed(() => (props.bausteine.steps || [])[(gewaehltePhase.value || 0) - 1]?.label || '')
function phaseKlick(n) {
if (!phasenWaehlbar.value) return
gewaehltePhase.value = gewaehltePhase.value === n ? null : n
}
// Nur FREMDE Themen — das gewählte Thema zeigt seinen Fortschritt inline an der Zeile
const activeGenerations = computed(() => {
const bausteinLines = props.activeBausteine
@@ -79,7 +97,16 @@ function confirmResetBausteine() {
function handleBausteinePlay() {
if (bausteineState.value === 'generating') return
emit('bausteineClick', { instructions: '' })
// Nur „Neu generieren" (ready) nutzt eine Phase; Fortsetzen/Erstbau resumen ohne Löschen.
const abPhase = props.bausteine.ready ? (gewaehltePhase.value ?? 1) : null
emit('bausteineClick', { instructions: '', abPhase })
gewaehltePhase.value = null
}
// Name-Klick = Primäraktion: fertige Bausteine → Übersicht, sonst Panel auf/zu.
function onBausteineName() {
if (props.bausteine.ready && !props.bausteine.generating) emit('openBausteineView')
else togglePanel('bausteine')
}
function guideStatus(format) {
@@ -92,30 +119,36 @@ function guideStatus(format) {
return latest.status
}
// Schritt-Kugeln der Guide-Pipelines
const GUIDE_STEPS = ['Auswahl', 'Auswahl-Prüfung', 'Gliederung', 'Gliederungs-Prüfung', 'Schreiben', 'Lese-Prüfung']
const ONEPAGER_STEPS = ['Recherche', 'Recherche-Prüfung', 'Bauen', 'Prüfung']
// Schritt-Kugeln der Guide-Pipeline
const GUIDE_STEPS = ['Gliederung', 'Inhalte', 'Inhalts-Check', 'Schreiben', 'Lese-Prüfung']
// Kugeln werden wie bei den Bausteinen immer angezeigt:
// fertig = alle grün, laufend = live, abgebrochen = Teilfortschritt, sonst grau
// Kugeln aus dem artefakt-basierten „fertig"-Marker (wie Bausteine, nicht aus dem DB-Zähler):
// fertig = done. Läuft gerade → der nächste Schritt (fertig+1) ist aktiv.
function guideSteps(format) {
const labels = format === 'OnePager' ? ONEPAGER_STEPS : GUIDE_STEPS
const labels = GUIDE_STEPS
const fertig = props.guideStepsDone[format] ?? -1
const st = guideStatus(format)
if (st === 'generating' || st === 'queued') {
const step = props.latestByFormat[format]?.step ?? -1
return labels.map((label, i) => ({
label,
state: i < step ? 'done' : i === step ? 'active' : 'pending',
}))
}
if (props.doneByFormat[format]) {
return labels.map((label) => ({ label, state: 'done' }))
}
if (abgebrochen(format)) {
const step = props.latestByFormat[format]?.step ?? 0
return labels.map((label, i) => ({ label, state: i < step ? 'done' : 'pending' }))
}
return labels.map((label) => ({ label, state: 'pending' }))
const aktiv = st === 'generating' || st === 'queued' ? fertig + 1 : -1
return labels.map((label, i) => ({
label,
state: i <= fertig ? 'done' : i === aktiv ? 'active' : 'pending',
}))
}
// Re-Run ab Guide-Schritt (1-basierte Kugel je Format). null = voll/Resume.
const gewaehlterStep = reactive({})
// Kugeln klickbar, sobald Artefakte existieren (Marker ≥ 0 oder fertig) und nicht generiert wird.
function guideWaehlbar(format) {
const st = guideStatus(format)
if (st === 'generating' || st === 'queued') return false
return (props.guideStepsDone[format] ?? -1) >= 0 || st === 'done'
}
function guideStepKlick(format, n) {
if (!guideWaehlbar(format)) return
gewaehlterStep[format] = gewaehlterStep[format] === n ? null : n
}
function gewaehltesStepLabel(format) {
return GUIDE_STEPS[(gewaehlterStep[format] || 0) - 1] || ''
}
function errorMsg(format) {
@@ -131,11 +164,11 @@ function abgebrochen(format) {
return latest?.status === 'error' && (latest.error_msg || '').startsWith('Abgebrochen')
}
// Name-Klick: fertiger Guide → Vorschau, sonst Aktions-Panel auf/zu.
function handleFormatClick(format) {
const guide = props.doneByFormat[format]
if (guide) {
emit('preview', guide)
}
if (guide) emit('preview', guide)
else togglePanel('fmt-' + format)
}
// Sperr-Gründe kommen vom Backend (GET /guides/locks) — die Regeln existieren
@@ -147,7 +180,10 @@ function playLock(format) {
function handlePlay(format) {
if (playLock(format)) return
emit('formatClick', { format, instructions: '' })
// Gewählte Kugel (1-basiert) → ab_step (0-basiert). Nur bei (teil-)gebautem Guide.
const abStep = guideWaehlbar(format) && gewaehlterStep[format] ? gewaehlterStep[format] - 1 : null
emit('formatClick', { format, instructions: '', abStep })
gewaehlterStep[format] = null
}
// Flash-Message-Verhalten: × blendet nur aus, nichts wird gelöscht
@@ -175,21 +211,74 @@ function handleDelete(format) {
})
}
const newTopic = ref('')
// Erstellen-Bereich: inline aufklappbar (Name + weitere Infos + Quellen-Typ).
const dlg = ref(false)
const form = ref({ name: '', instructions: '', sourceType: 'thema', sourceOrt: '' })
const canCreate = computed(() => {
if (!form.value.name.trim()) return false
if (['link', 'projekt', 'uni'].includes(form.value.sourceType)) return !!form.value.sourceOrt.trim()
return true
})
function submit() {
const t = newTopic.value.trim()
if (!t) return
emit('create', t)
newTopic.value = ''
function toggleErstellen() {
if (!dlg.value) form.value = { name: '', instructions: '', sourceType: 'thema', sourceOrt: '' }
dlg.value = !dlg.value
}
function createThema() {
if (!canCreate.value) return
emit('createThema', {
topic: form.value.name.trim(),
instructions: form.value.instructions.trim(),
sourceType: form.value.sourceType,
sourceOrt: form.value.sourceOrt.trim(),
})
dlg.value = false
}
function confirmDeleteTopic(topic) {
armOrRun('topic-' + topic, () => emit('deleteTopic', topic))
}
function confirmDeleteProject(name) {
armOrRun('project-' + name, () => emit('deleteProject', name))
// --- Thema-Edit: Name-Klick wählt nur; das Chevron klappt die Quellen-Form auf/zu (Default zu) ---
const editTopic = ref(null)
const editForm = ref({ type: 'thema', ort: '', spec: '' })
const editLoading = ref(false)
const canSave = computed(() => {
if (['link', 'projekt', 'uni'].includes(editForm.value.type)) return !!editForm.value.ort.trim()
return true
})
async function toggleTopicPanel(t) {
emit('select', t)
if (openPanel.value === 'topic-' + t) { openPanel.value = null; return }
openPanel.value = 'topic-' + t
editTopic.value = t
editLoading.value = true
try {
const q = await fetchQuelle(t)
editForm.value = { type: q.type || 'thema', ort: q.ort || '', spec: q.spec || '' }
} catch {
editForm.value = { type: 'thema', ort: '', spec: '' }
} finally {
editLoading.value = false
}
}
function setEditType(t) {
editForm.value.type = t
editForm.value.ort = ''
}
function saveQuelle() {
if (!canSave.value || !editTopic.value) return
emit('updateQuelle', {
topic: editTopic.value,
type: editForm.value.type,
ort: editForm.value.ort.trim(),
spec: editForm.value.spec.trim(),
})
openPanel.value = null
}
</script>
@@ -212,12 +301,51 @@ function confirmDeleteProject(name) {
:title="dark ? 'Hellmodus' : 'Dunkelmodus'"
@click="emit('toggleDark')"
>{{ dark ? '☀' : '🌙' }}</button>
<div v-if="selectedTopic" class="ansicht-toggle">
<button :class="{ active: ansichtModus === 'kompakt' }" title="Kurzer Text" @click="emit('setAnsicht', 'kompakt')">
<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true">
<rect x="2" y="5" width="12" height="1.6" rx="0.8" />
<rect x="2" y="9" width="7" height="1.6" rx="0.8" />
</svg>
</button>
<button :class="{ active: ansichtModus === 'erklärend' }" title="Langer Text" @click="emit('setAnsicht', 'erklärend')">
<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true">
<rect x="2" y="3" width="12" height="1.4" rx="0.7" />
<rect x="2" y="6.3" width="12" height="1.4" rx="0.7" />
<rect x="2" y="9.6" width="12" height="1.4" rx="0.7" />
<rect x="2" y="12.9" width="8" height="1.4" rx="0.7" />
</svg>
</button>
</div>
<button class="new-topic-toggle" :class="{ active: dlg }" title="Thema erstellen" @click="toggleErstellen">+</button>
</div>
<!-- Erstellen: inline aufklappbar (kein Modal) -->
<div v-if="dlg" class="thema-panel">
<input class="dlg-input" v-model="form.name" placeholder="Thema-Name…" @keyup.enter="createThema" autofocus />
<textarea class="dlg-textarea" v-model="form.instructions" rows="2" placeholder="Weitere Infos (optional)…"></textarea>
<div class="dlg-sources">
<button :class="{ active: form.sourceType === 'thema' }" @click="form.sourceType = 'thema'; form.sourceOrt = ''">Thema</button>
<button :class="{ active: form.sourceType === 'link' }" @click="form.sourceType = 'link'; form.sourceOrt = ''">Link</button>
<button :class="{ active: form.sourceType === 'projekt' }" @click="form.sourceType = 'projekt'; form.sourceOrt = ''">Projekt</button>
<button :class="{ active: form.sourceType === 'uni' }" @click="form.sourceType = 'uni'; form.sourceOrt = ''">Uni</button>
</div>
<input
v-model="newTopic"
placeholder="Neues Thema…"
@keyup.enter="submit"
v-if="form.sourceType === 'link'"
class="dlg-input" v-model="form.sourceOrt"
placeholder="https://…" @keyup.enter="createThema"
/>
<button @click="submit" :disabled="!newTopic.trim()">+</button>
<select
v-else-if="form.sourceType === 'projekt' || form.sourceType === 'uni'"
class="dlg-input" v-model="form.sourceOrt"
>
<option value="" disabled>Ordner wählen</option>
<option v-for="fo in (folders[form.sourceType] || [])" :key="fo.ort" :value="fo.ort">{{ fo.name }}</option>
</select>
<div class="dlg-actions">
<button class="dlg-cancel" @click="dlg = false">Abbrechen</button>
<button class="dlg-create" :disabled="!canCreate" @click="createThema">Erstellen</button>
</div>
</div>
<div class="provider-toggle" v-if="providers.length">
<button
@@ -237,95 +365,93 @@ function confirmDeleteProject(name) {
<div class="progress-info" v-if="activeGenerations.length">
<div v-for="(line, i) in activeGenerations" :key="i">{{ line }}</div>
</div>
<div
class="format-row bausteine-row ord-bausteine"
:class="{ 'is-active': bausteineState === 'generating' || bausteine.partial }"
>
<div class="format-name bausteine-name">
<span class="format-label">Bausteine</span>
<span
v-if="bausteine.partial && bausteineState !== 'generating'"
class="resume-badge"
title="Abgebrochen — ▶ setzt fort"
>Pausiert</span>
<span class="step-dots">
<div class="ord-bausteine">
<div
class="format-row bausteine-row"
:class="{ 'is-active': bausteineState === 'generating' || bausteine.partial, 'row-open': isOpen('bausteine') }"
>
<button class="format-name bausteine-name" @click="onBausteineName">
<span class="format-label">Bausteine</span>
<span
v-for="s in (bausteine.steps || [])"
:key="s.label"
class="step-dot"
:class="s.state"
:title="s.state === 'active' ? (bausteine.progress || s.label) : s.label"
></span>
</span>
<span
v-if="bausteineState === 'generating'"
class="format-x"
:class="{ armed: pendingConfirm === 'bausteine' }"
title="Aktuellen Schritt abbrechen (Fortschritt bleibt)"
@click.stop="confirmCancelBausteine"
>{{ pendingConfirm === 'bausteine' ? 'Sicher?' : '×' }}</span>
<span
v-else-if="bausteine.partial"
class="format-x"
:class="{ armed: pendingConfirm === 'bausteine' }"
title="Fortschritt löschen"
@click.stop="confirmResetBausteine"
>{{ pendingConfirm === 'bausteine' ? 'Sicher?' : '×' }}</span>
v-if="bausteine.partial && bausteineState !== 'generating'"
class="resume-badge"
title="Abgebrochen — Fortsetzen möglich"
>Pausiert</span>
<span class="step-dots">
<span
v-for="(s, i) in (bausteine.steps || [])"
:key="s.label"
class="step-pill"
:class="[s.state, { sel: gewaehltePhase === i + 1, klickbar: phasenWaehlbar }]"
:title="(s.state === 'active' ? (bausteine.progress || s.label) : s.label) + (phasenWaehlbar ? ' — Klick: ab hier neu' : '')"
@click.stop="phaseKlick(i + 1)"
>{{ i + 1 }}</span>
</span>
</button>
<button class="panel-toggle" :class="{ open: isOpen('bausteine') }" title="Aktionen" @click.stop="togglePanel('bausteine')"></button>
</div>
<div class="format-actions">
<template v-if="bausteineState !== 'generating'">
<div v-if="isOpen('bausteine')" class="action-panel">
<template v-if="bausteineState === 'generating'">
<button class="panel-btn danger" :class="{ armed: pendingConfirm === 'bausteine' }" @click="confirmCancelBausteine">{{ pendingConfirm === 'bausteine' ? 'Sicher?' : 'Abbrechen' }}</button>
</template>
<template v-else>
<button class="panel-btn play" @click="handleBausteinePlay">{{ bausteine.partial ? 'Fortsetzen' : bausteine.ready ? (gewaehltePhase ? `Ab «${gewaehltesLabel}» neu` : 'Neu generieren') : 'Generieren' }}</button>
<button
class="action-btn play"
:title="bausteine.partial ? 'Fortsetzen' : bausteine.ready ? 'Bausteine neu erstellen' : 'Bausteine erstellen'"
@click="handleBausteinePlay"
></button>
v-if="bausteine.ready || bausteine.partial"
class="panel-btn danger"
:class="{ armed: pendingConfirm === 'bausteine' }"
@click="confirmResetBausteine"
>{{ pendingConfirm === 'bausteine' ? 'Sicher?' : 'Entfernen' }}</button>
</template>
</div>
<div v-if="bausteineState === 'generating'" class="format-progress">
{{ bausteine.progress || 'Wartend' }}
</div>
<div v-if="bausteine.error && !bausteine.error.startsWith('Abgebrochen')" class="format-error">
<span class="format-error-text">{{ bausteine.error }}</span>
</div>
</div>
<div v-if="bausteineState === 'generating'" class="format-progress ord-bausteine">
{{ bausteine.progress || 'Wartend' }}
</div>
<div v-if="bausteine.error && !bausteine.error.startsWith('Abgebrochen')" class="format-error ord-bausteine">
<span class="format-error-text">{{ bausteine.error }}</span>
</div>
<!-- OnePager (unabhängig von Bausteinen) steht per CSS-order vor der Bausteine-Zeile -->
<div v-for="f in formats" :key="f.key" :style="{ order: f.key === 'OnePager' ? 1 : 3 }">
<div :class="['format-row', 'fmt-' + guideStatus(f.key), { 'fmt-paused': abgebrochen(f.key) }]">
<!-- Formate stehen per CSS-order nach der Bausteine-Zeile (order 2) -->
<div v-for="f in formats" :key="f.key" :style="{ order: 3 }">
<div :class="['format-row', 'fmt-' + guideStatus(f.key), { 'fmt-paused': abgebrochen(f.key), 'row-open': isOpen('fmt-' + f.key) }]">
<button class="format-name" @click="handleFormatClick(f.key)">
<span class="format-label">{{ f.label }}</span>
<span
v-if="abgebrochen(f.key)"
class="resume-badge"
title="Abgebrochen — ▶ setzt fort"
title="Abgebrochen — Fortsetzen möglich"
>Pausiert</span>
<span class="step-dots" v-if="guideSteps(f.key).length">
<span
v-for="s in guideSteps(f.key)"
v-for="(s, i) in guideSteps(f.key)"
:key="s.label"
class="step-dot"
:class="s.state"
:title="s.state === 'active' ? (latestByFormat[f.key]?.progress || s.label) : s.label"
></span>
class="step-pill"
:class="[s.state, { sel: gewaehlterStep[f.key] === i + 1, klickbar: guideWaehlbar(f.key) }]"
:title="(s.state === 'active' ? (latestByFormat[f.key]?.progress || s.label) : s.label) + (guideWaehlbar(f.key) ? ' — Klick: ab hier neu' : '')"
@click.stop="guideStepKlick(f.key, i + 1)"
>{{ i + 1 }}</span>
</span>
<span
v-if="guideStatus(f.key) !== 'none' || abgebrochen(f.key)"
class="format-x"
:class="{ armed: pendingConfirm === 'fmt-' + f.key }"
@click.stop="handleDelete(f.key)"
:title="guideStatus(f.key) === 'generating' || guideStatus(f.key) === 'queued' ? 'Abbrechen'
: abgebrochen(f.key) ? 'Fortschritt löschen' : 'Löschen'"
>{{ pendingConfirm === 'fmt-' + f.key ? 'Sicher?' : '×' }}</span>
</button>
<div class="format-actions">
<template v-if="guideStatus(f.key) !== 'generating' && guideStatus(f.key) !== 'queued'">
<button
class="action-btn play"
:title="playLock(f.key) || (abgebrochen(f.key) ? 'Fortsetzen' : 'Generieren')"
:disabled="!!playLock(f.key)"
@click="handlePlay(f.key)"
></button>
</template>
</div>
<button class="panel-toggle" :class="{ open: isOpen('fmt-' + f.key) }" title="Aktionen" @click.stop="togglePanel('fmt-' + f.key)"></button>
</div>
<div v-if="isOpen('fmt-' + f.key)" class="action-panel">
<template v-if="guideStatus(f.key) === 'generating' || guideStatus(f.key) === 'queued'">
<button class="panel-btn danger" :class="{ armed: pendingConfirm === 'fmt-' + f.key }" @click="handleDelete(f.key)">{{ pendingConfirm === 'fmt-' + f.key ? 'Sicher?' : 'Abbrechen' }}</button>
</template>
<template v-else>
<button
class="panel-btn play"
:title="playLock(f.key) || (abgebrochen(f.key) ? 'Fortsetzen' : 'Generieren')"
:disabled="!!playLock(f.key)"
@click="handlePlay(f.key)"
>{{ gewaehlterStep[f.key] ? `Ab «${gewaehltesStepLabel(f.key)}» neu` : abgebrochen(f.key) ? 'Fortsetzen' : guideStatus(f.key) === 'done' ? 'Neu generieren' : 'Generieren' }}</button>
<button
v-if="guideStatus(f.key) !== 'none' || abgebrochen(f.key)"
class="panel-btn danger"
:class="{ armed: pendingConfirm === 'fmt-' + f.key }"
@click="handleDelete(f.key)"
>{{ pendingConfirm === 'fmt-' + f.key ? 'Sicher?' : abgebrochen(f.key) ? 'Fortschritt löschen' : 'Entfernen' }}</button>
</template>
</div>
<div
v-if="guideStatus(f.key) === 'generating' || guideStatus(f.key) === 'queued'"
@@ -347,34 +473,45 @@ function confirmDeleteProject(name) {
<li
v-for="t in topics"
:key="t"
:class="{ active: t === selectedTopic }"
@click="emit('select', t)"
:class="{ active: t === selectedTopic, 'li-open': isOpen('topic-' + t) }"
>
<span>{{ t }}</span>
<button
class="delete-topic"
:class="{ armed: pendingConfirm === 'topic-' + t }"
@click.stop="confirmDeleteTopic(t)"
title="Thema und alle Guides löschen"
>{{ pendingConfirm === 'topic-' + t ? 'Sicher?' : '×' }}</button>
<div class="topic-row">
<span class="topic-name" @click="emit('select', t)">{{ t }}</span>
<button class="panel-toggle" :class="{ open: isOpen('topic-' + t) }" title="Optionen" @click.stop="toggleTopicPanel(t)"></button>
</div>
<div v-if="isOpen('topic-' + t)" class="thema-panel edit-panel" @click.stop>
<p v-if="editLoading" class="dlg-hint">Lade</p>
<template v-else>
<textarea class="dlg-textarea" v-model="editForm.spec" rows="2" placeholder="Weitere Infos (optional)…"></textarea>
<div class="dlg-sources">
<button :class="{ active: editForm.type === 'thema' }" @click="setEditType('thema')">Thema</button>
<button :class="{ active: editForm.type === 'link' }" @click="setEditType('link')">Link</button>
<button :class="{ active: editForm.type === 'projekt' }" @click="setEditType('projekt')">Projekt</button>
<button :class="{ active: editForm.type === 'uni' }" @click="setEditType('uni')">Uni</button>
</div>
<input
v-if="editForm.type === 'link'"
class="dlg-input" v-model="editForm.ort"
placeholder="https://…"
/>
<select
v-else-if="editForm.type === 'projekt' || editForm.type === 'uni'"
class="dlg-input" v-model="editForm.ort"
>
<option value="" disabled>Ordner wählen</option>
<option v-for="fo in (folders[editForm.type] || [])" :key="fo.ort" :value="fo.ort">{{ fo.name }}</option>
</select>
<div class="dlg-actions">
<button
class="dlg-delete"
:class="{ armed: pendingConfirm === 'topic-' + t }"
@click="confirmDeleteTopic(t)"
>{{ pendingConfirm === 'topic-' + t ? 'Sicher?' : 'Löschen' }}</button>
<button class="dlg-create" :disabled="!canSave" @click="saveQuelle">Aktualisieren</button>
</div>
</template>
</div>
</li>
<template v-if="projects.length">
<li class="projects-divider">Projekte</li>
<li
v-for="p in projects"
:key="'project-' + p"
:class="{ active: p === selectedTopic, 'project-item': true }"
@click="emit('select', p)"
>
<span>{{ p }}</span>
<button
class="delete-topic"
:class="{ armed: pendingConfirm === 'project-' + p }"
@click.stop="confirmDeleteProject(p)"
title="Projekt entfernen (löscht ./projects-Ordner)"
>{{ pendingConfirm === 'project-' + p ? 'Sicher?' : '×' }}</button>
</li>
</template>
</ul>
</aside>
</template>
@@ -455,6 +592,22 @@ function confirmDeleteProject(name) {
border-color: var(--accent-border);
}
/* Ansicht-Umschalter: zwei Icon-Buttons (kurzer / langer Text) */
.ansicht-toggle { display: inline-flex; }
.new-topic .ansicht-toggle button {
padding: 4px 7px;
background: var(--bg);
color: var(--text-muted);
border: 1px solid var(--border-strong);
display: inline-flex;
align-items: center;
}
.new-topic .ansicht-toggle button:first-child { border-radius: 6px 0 0 6px; }
.new-topic .ansicht-toggle button:last-child { border-radius: 0 6px 6px 0; border-left: none; }
.new-topic .ansicht-toggle button:hover { color: var(--accent-hover); }
.new-topic .ansicht-toggle button.active { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.ansicht-toggle svg { fill: currentColor; display: block; }
.stats-bar {
display: flex;
padding: 0.5rem 0.75rem;
@@ -463,12 +616,12 @@ function confirmDeleteProject(name) {
}
.stat {
position: relative;
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 1px;
padding: 4px 2px;
justify-content: center;
padding: 7px 2px 4px;
background: var(--panel-soft);
border: 1px solid var(--border);
border-radius: 6px;
@@ -481,11 +634,18 @@ function confirmDeleteProject(name) {
color: var(--text);
}
/* Mini-Titel sitzt auf der oberen Kante des Badges (Legenden-Look) */
.stat-label {
font-size: 0.58rem;
position: absolute;
top: -0.42rem;
left: 50%;
transform: translateX(-50%);
padding: 0 3px;
font-size: 0.5rem;
text-transform: uppercase;
letter-spacing: 0.03em;
color: var(--text-faint);
background: var(--panel);
white-space: nowrap;
}
@@ -536,59 +696,35 @@ function confirmDeleteProject(name) {
}
.topic-list li {
padding: 0.6rem 1rem;
cursor: pointer;
font-size: 0.9rem;
color: var(--text);
transition: background 0.15s;
display: flex;
justify-content: space-between;
align-items: center;
}
.topic-list li:hover {
.topic-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
padding: 0.6rem 1rem;
cursor: pointer;
transition: background 0.15s;
}
.topic-row:hover {
background: var(--accent-soft);
}
.topic-list li.active {
.topic-list li.active .topic-row {
background: var(--accent-soft);
color: var(--accent-hover);
font-weight: 600;
}
.delete-topic {
display: none;
background: none;
border: none;
color: var(--danger);
font-size: 1.1rem;
cursor: pointer;
padding: 0 2px;
line-height: 1;
}
.topic-list li:hover .delete-topic {
display: block;
}
.projects-divider {
cursor: default;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-faint);
font-weight: 700;
padding: 0.6rem 1rem 0.3rem;
margin-top: 0.4rem;
border-top: 1px solid var(--border);
}
.projects-divider:hover {
background: none;
}
.topic-list li.project-item span::before {
content: '📁 ';
.topic-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Format section */
@@ -596,7 +732,7 @@ function confirmDeleteProject(name) {
display: flex;
align-items: center;
gap: 8px;
cursor: default;
cursor: pointer;
}
.step-dots {
@@ -605,23 +741,41 @@ function confirmDeleteProject(name) {
flex: 1;
}
.step-dot {
width: 10px;
height: 10px;
/* Grobe Phasen als nummerierte Pillen (15) — Anzeige + anklickbar für Re-Run ab hier. */
.step-pill {
display: inline-flex;
align-items: center;
justify-content: center;
width: 17px;
height: 17px;
border-radius: 50%;
background: var(--border-strong);
color: var(--bg);
font-size: 0.62rem;
font-weight: 700;
line-height: 1;
flex-shrink: 0;
border: 1.5px solid transparent;
}
.step-dot.done {
.step-pill.done {
background: var(--success-border);
}
.step-dot.active {
.step-pill.active {
background: var(--warning-border);
animation: dot-pulse 1.2s ease-in-out infinite;
}
.step-pill.klickbar {
cursor: pointer;
}
.step-pill.sel {
border-color: var(--text);
box-shadow: 0 0 0 1px var(--text);
}
@keyframes dot-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.35; }
@@ -637,7 +791,7 @@ function confirmDeleteProject(name) {
max-height: 60vh;
overflow-y: auto;
padding: 0.5rem 0;
/* flex + order: OnePager (order 1) vor Bausteine (order 2) vor den restlichen Formaten (order 3) */
/* flex + order: Bausteine (order 2) vor den Formaten (order 3) */
display: flex;
flex-direction: column;
}
@@ -710,13 +864,14 @@ function confirmDeleteProject(name) {
.format-name {
flex: 1;
min-width: 0;
background: none;
border: none;
text-align: left;
font-size: 0.85rem;
padding: 4px 8px;
border-radius: 4px;
cursor: default;
cursor: pointer;
color: var(--text-faint);
display: flex;
align-items: center;
@@ -724,6 +879,55 @@ function confirmDeleteProject(name) {
gap: 8px;
}
/* Accordion: Pfeil-Toggle + aufklappendes Aktions-Panel */
.panel-toggle {
flex: 0 0 auto;
background: none;
border: none;
cursor: pointer;
color: var(--text-faint);
font-size: 0.8rem;
line-height: 1;
padding: 4px 8px;
transition: transform 0.15s, color 0.15s;
}
.panel-toggle.open { transform: rotate(180deg); color: var(--text); }
.panel-toggle:hover { color: var(--accent); }
.format-row.row-open,
.topic-list li.li-open .topic-row { background: var(--panel-soft); }
.action-panel {
display: flex;
gap: 0.5rem;
padding: 0.15rem 0.75rem 0.55rem calc(0.75rem + 8px);
}
.panel-btn {
flex: 1;
padding: 0.45rem 0.6rem;
border: 1px solid var(--border-strong);
border-radius: 6px;
background: var(--panel);
color: var(--text);
font-size: 0.82rem;
font-weight: 600;
cursor: pointer;
transition: all 0.15s;
}
.panel-btn:hover { border-color: var(--accent); }
.panel-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.panel-btn:disabled:hover { border-color: var(--border-strong); }
.panel-btn.play {
color: var(--success);
background: var(--success-soft);
border-color: var(--success-border);
}
.panel-btn.play:hover { background: var(--success-soft-hover); }
.panel-btn.danger { color: var(--danger); }
.panel-btn.danger:hover { border-color: var(--danger); }
.panel-btn.armed { background: var(--danger); color: #fff; border-color: var(--danger); }
.format-x {
display: none;
color: var(--danger);
@@ -852,4 +1056,81 @@ function confirmDeleteProject(name) {
50% { opacity: 0.65; }
}
/* Erstellen — inline aufklappbar (kein Modal) */
.new-topic-toggle {
margin-left: auto; /* rechtsbündig statt volle Breite */
padding: 6px 12px;
border: 1px solid transparent;
border-radius: 6px;
background: var(--accent);
color: var(--on-accent);
cursor: pointer;
font: inherit;
font-size: 1.1rem;
line-height: 1;
text-align: center;
}
.new-topic-toggle:hover { background: var(--accent-hover); }
.new-topic-toggle.active { background: var(--accent-hover); }
.thema-panel {
margin: 0.4rem 0;
padding: 0.5rem;
background: var(--panel-soft);
border: 1px solid var(--border);
border-radius: 8px;
display: flex;
flex-direction: column;
gap: 0.35rem;
font-size: 0.85rem;
}
.dlg-input, .dlg-textarea {
width: 100%;
box-sizing: border-box;
padding: 0.4rem 0.5rem;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--panel);
color: var(--text);
font: inherit;
}
.dlg-input:focus, .dlg-textarea:focus { outline: none; border-color: var(--accent); }
.dlg-textarea { resize: vertical; min-height: 2rem; }
.dlg-sources { display: flex; gap: 0.3rem; }
.dlg-sources button {
flex: 1;
padding: 0.35rem 0.2rem;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--panel-soft);
color: var(--text);
cursor: pointer;
font-size: 0.8rem;
}
.dlg-sources button.active { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.dlg-hint { margin: 0; font-size: 0.75rem; color: var(--text-faint); }
.dlg-actions { display: flex; justify-content: flex-end; gap: 0.4rem; margin-top: 0.1rem; }
.dlg-actions button {
padding: 0.4rem 0.8rem;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--panel-soft);
color: var(--text);
cursor: pointer;
}
.dlg-create { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.dlg-create:disabled { opacity: 0.45; cursor: default; }
/* Thema-Edit-Panel: dezent in die Liste eingefügt — keine Box, nur ein linker Akzent */
.edit-panel {
margin: 0;
padding: 0.5rem 1rem 0.7rem 1.25rem;
background: var(--accent-soft);
border: none;
border-left: 3px solid var(--accent-border);
border-radius: 0;
}
.dlg-delete { margin-right: auto; color: var(--danger); }
.dlg-delete.armed { background: var(--danger); border-color: var(--danger); color: #fff; font-weight: 700; }
select.dlg-input { cursor: pointer; }
</style>

View File

@@ -11,7 +11,7 @@ const props = defineProps({
const emit = defineEmits(['changes'])
const chat = useChat((msgs) => chatElement(props.element.id, msgs, props.provider))
const { messages, input, loading, messagesEl, inputEl } = chat
const { messages, input, loading, messagesEl, inputEl, onScroll } = chat
// Anderes Element gewählt → Verlauf verwerfen
watch(() => props.element.id, () => chat.reset())
@@ -24,7 +24,7 @@ async function send() {
<template>
<div class="el-chat">
<div ref="messagesEl" class="chat-messages">
<div ref="messagesEl" class="chat-messages" @scroll="onScroll">
<p v-if="!messages.length" class="chat-hint">Schreib, was am Element geändert werden soll.</p>
<template v-for="(m, i) in messages" :key="i">
<div :class="['chat-msg', m.role]">{{ m.content }}</div>

View File

@@ -1,5 +1,10 @@
import { ref, nextTick } from 'vue'
// (Fast) am unteren Rand? Schwelle fängt Sub-Pixel und kleine Abstände ab.
export function istUnten(el, schwelle = 60) {
return el.scrollHeight - el.scrollTop - el.clientHeight < schwelle
}
// Gemeinsame Chat-Mechanik: senden, abbrechen (Run-Counter), scrollen, Fokus.
// performRequest(messages) → Promise<{ reply, … }>; send() gibt die Antwort
// zurück, damit der Aufrufer Extras (z. B. changes) auswerten kann.
@@ -9,11 +14,17 @@ export function useChat(performRequest) {
const loading = ref(false)
const messagesEl = ref(null) // Template-Ref: Nachrichten-Container
const inputEl = ref(null) // Template-Ref: Textarea
const stick = ref(true) // an den Boden „gepinnt" — nur dann auto-scrollen
let run = 0 // laufende Anfrage identifizieren; Abbruch ignoriert ihr Ergebnis
// @scroll-Handler: pinnt nur, wenn der Nutzer (fast) unten ist.
function onScroll() {
if (messagesEl.value) stick.value = istUnten(messagesEl.value)
}
async function scrollToBottom() {
await nextTick()
if (messagesEl.value) messagesEl.value.scrollTop = messagesEl.value.scrollHeight
if (messagesEl.value && stick.value) messagesEl.value.scrollTop = messagesEl.value.scrollHeight
}
function autoGrow(max = 140) {
@@ -43,6 +54,7 @@ export function useChat(performRequest) {
}
const text = input.value.trim()
if (!text) return null
stick.value = true // eigenes Senden = ans Ende; Hochscrollen während des Wartens setzt es wieder false
const current = ++run
messages.value.push({ role: 'user', content: text })
input.value = ''
@@ -52,7 +64,13 @@ export function useChat(performRequest) {
try {
const res = await performRequest(messages.value)
if (current !== run) return null
messages.value.push({ role: 'assistant', content: res.reply || '…' })
// Prüfung liefert `frage` (+ getrenntes `feedback`); andere Chats `reply`.
messages.value.push({
role: 'assistant',
content: res.frage ?? res.reply ?? '…',
feedback: res.feedback ?? null,
bewertung: res.bewertung ?? null,
})
return res
} catch {
if (current !== run) return null
@@ -67,5 +85,5 @@ export function useChat(performRequest) {
}
}
return { messages, input, loading, messagesEl, inputEl, send, cancel, reset, scrollToBottom, autoGrow }
return { messages, input, loading, messagesEl, inputEl, stick, onScroll, send, cancel, reset, scrollToBottom, autoGrow }
}

View File

@@ -2,6 +2,8 @@ import { marked } from 'marked'
import { markedHighlight } from 'marked-highlight'
import hljs from 'highlight.js'
import 'highlight.js/styles/github-dark.css'
import katex from 'katex'
import 'katex/dist/katex.min.css'
import DOMPurify from 'dompurify'
marked.use(markedHighlight({
@@ -15,6 +17,40 @@ marked.use(markedHighlight({
}))
marked.setOptions({ breaks: true, gfm: true })
// LaTeX-Mathe via KaTeX. Eigene marked-Extensions (statt marked-katex-extension,
// die marked v18 hinterherhinkt). marked tokenisiert Code zuerst → $…$ in Code-
// Blöcken wird NICHT als Mathe erkannt. throwOnError:false zeigt defektes TeX rot.
function renderTex(tex, displayMode) {
return katex.renderToString(tex, { displayMode, throwOnError: false, output: 'html' })
}
const blockMath = {
name: 'blockMath',
level: 'block',
start(src) { const i = src.indexOf('$$'); return i < 0 ? undefined : i },
tokenizer(src) {
const m = /^\$\$([\s\S]+?)\$\$/.exec(src)
if (m) return { type: 'blockMath', raw: m[0], text: m[1].trim() }
},
renderer(token) { return renderTex(token.text, true) },
}
const inlineMath = {
name: 'inlineMath',
level: 'inline',
start(src) { const i = src.indexOf('$'); return i < 0 ? undefined : i },
tokenizer(src) {
// $…$: kein $$, kein Leerzeichen direkt hinter dem öffnenden $ und vor dem
// schließenden $ (pandoc-Stil) → mindert Kollisionen mit Fließtext-Dollarzeichen.
const m = /^\$(?![\s$])((?:\\\$|[^$])+?)\$/.exec(src)
if (!m || /\s$/.test(m[1])) return
return { type: 'inlineMath', raw: m[0], text: m[1].trim() }
},
renderer(token) { return renderTex(token.text, false) },
}
marked.use({ extensions: [blockMath, inlineMath] })
// Rohes HTML im Markdown (z. B. <p>, <img> ohne Backticks aus Agenten-Output)
// als Text anzeigen statt rendern — sonst verschluckt der Browser den Inhalt.
marked.use({
@@ -29,3 +65,9 @@ marked.use({
export function renderMarkdown(text) {
return DOMPurify.sanitize(marked.parse(text || ''))
}
// Inline-Variante (kein <p>-Wrapping) für kurze Texte wie Quiz-Optionen oder
// Lückentext-Satzteile — rendert $…$-Mathe und Markdown ohne Block-Umbruch.
export function renderMarkdownInline(text) {
return DOMPurify.sanitize(marked.parseInline(text || ''))
}

View File

@@ -0,0 +1,31 @@
// Geteilter Prüfungs-State pro Baustein.
// Die durablen Refs leben hier als Modul-Map: alle BausteinPanel-Instanzen desselben
// Bausteins nutzen DIESELBEN Refs. So überlebt der Verlauf das Remounten (Baustein-
// Wechsel, Fokus zu/auf) UND eine spät eintreffende Frage ist sofort reaktiv sichtbar.
// Geht bei Page-Reload verloren (Modul re-init) — bewusst kein DB-Aufwand.
import { ref } from 'vue'
const store = new Map() // key "topic::baustein" -> Refs (Verlauf + Fragen-Pool)
export function usePruefSlot(key) {
if (!store.has(key)) {
store.set(key, {
messages: ref([]),
phase: ref('idle'),
aktuelleFrage: ref(''),
letztesFeedback: ref(''),
pool: ref([]), // vorformulierte Frage-Objekte der AKTUELLEN Form ({form, ...})
inflight: ref(0), // laufende Pool-Generierungen (instanzübergreifend koordiniert)
poolForm: ref(''), // Form, für die der Pool gefüllt ist — bei Wechsel leeren
musterQuelle: ref([]), // unveränderliche Voll-Liste der Frage-Muster (leer = Fallback)
musterPool: ref([]), // Arbeitskopie: gezogene Saat ohne Zurücklegen; leer → Reset
musterGeladen: ref(false), // Muster-Sidecar schon abgerufen?
quizAktuell: ref(null), // laufende Quiz-Frage {frage, optionen, gewaehlt, fertig, ...}
lueckAktuell: ref(null), // laufende Lückentext-Aufgabe {satz, loesung, ..., fertig}
})
}
return store.get(key)
}
export const clearPruef = (key) => store.delete(key)

View File

@@ -10,6 +10,12 @@ export default defineConfig({
vue(),
vueDevTools(),
],
// Default-cssTarget enthält alte Browser (firefox78) — esbuild optimiert dann die
// unprefixed backdrop-filter weg und lässt nur -webkit (in Firefox unsichtbar).
// Moderne Ziele + Safari erzwingen, dass BEIDE Varianten erhalten bleiben.
build: {
cssTarget: ['chrome90', 'firefox103', 'safari15', 'edge90'],
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))

167
recherche.txt Normal file
View File

@@ -0,0 +1,167 @@
================================================================================
RECHERCHE: Guide-Generierungskette erweitern
Ziel: Lernen ohne Angst vor (a) Falschem, (b) fehlenden Infos,
(c) nervigem Aufbau, (d) Überforderung durch Struktur/Auswahl/Lesbarkeit.
Bestehende Kette: Auswahl → Gliederung → Inhalte → Inhalts-Check → Writer → Lese-Check.
================================================================================
## META-ERKENNTNIS (gilt übergreifend)
- "Mehr gleiche Agenten" sättigt früh und erzeugt Echo-Kammer: identische Modelle
teilen dieselben systematischen Fehler. Voting/Debatte schlagen Single-Agent nur
moderat und nicht zuverlässig besser als Self-Consistency.
- Echte Hebel: HETEROGENITÄT (andere Modelle), EXTERNE ERDUNG (Quellen/Tools/Websuche),
ROLLEN-TRENNUNG, GATES zwischen Stufen.
- Selbst-Korrektur OHNE externes Feedback verbessert Faktualität kaum, kann verschlechtern
(CommonSenseQA 75,8 → 38,1 nach 1 Runde). Nur MIT Quelle/Tool/Oracle einsetzen.
- ~41,8 % der Multi-Agent-Fehler sind System-/Spezifikations-Design, nicht Modell-Fähigkeit
(MAST). → Architektur (Rollen, Gates, Stopp-Logik) ist der größte Hebel.
================================================================================
A) GEGEN FALSCHES (faktische Korrektheit)
================================================================================
1. RAG / Grounding gegen verlässliche Quellen — stärkster Hebel gegen erfundene Fakten.
Generierung auf abgerufene Quell-Passagen beschränken. (Inhalte/Prüfen)
2. Zitat-Pflicht + Attribution: jeder Fakt braucht Inline-Quelle; gilt nur als belegt,
wenn aus dem Zitat ableitbar. Macht Fehler sichtbar/prüfbar. (Schreiben/Prüfen)
3. Claim-Extraktion + Verifikation (FActScore-Stil): Text in atomare Fakten zerlegen,
jeden einzeln gegen Quelle prüfen. Fängt einzelne falsche Sätze. (Prüfen)
4. SAFE (Search-Augmented Factuality Evaluator): Claims zerlegen → pro Claim Websuche →
per Reasoning prüfen. Übertrifft menschliche Annotatoren, ~20x billiger.
Ideal, da Websuche schon vorhanden. (Prüfen)
5. Abstention bei Unsicherheit: unbelegte/inkonsistente Fakten weglassen oder markieren
statt raten. Tauscht Coverage gegen Korrektheit — für Lerninhalte oft richtig.
6. Chain-of-Verification (CoVe): Entwurf → Verifikationsfragen → einzeln beantworten →
revidieren. Günstig (reines Prompting). (Prüfen, pro Baustein)
7. Self-Consistency: mehrere Pfade samplen, Mehrheit wählen. Nur für Varianz-Reduktion,
NICHT für Wahrheit (systematische Fehler bleiben). Sweet Spot 1020 Samples.
8. Post-hoc Revision (RARR): fertigen Text nehmen, pro Claim Evidenz suchen, widersprechende
Claims editieren, Originaltext maximal erhalten. (zwischen Schreiben und Lesbarkeit)
9. Stärkerer Verifier statt mehr gleicher Agenten: generieren günstig, verifizieren mit
stärkerem/anderem Modell. Lohnt bei schwierigen/seltenen Fakten. (Prüfen)
10. Self-Refine/Reflexion NUR mit externem Feedback (Tool/Quelle). Intrinsisch verschlechtert.
Quellen A:
- SAFE / Long-form Factuality: https://openreview.net/pdf?id=4M9f8VMt2C
- FActScore (Primer): https://aman.ai/primers/ai/factuality-in-LLMs/
- Chain-of-Verification: https://arxiv.org/pdf/2309.11495
- Grounded Attribution + Refuse: https://arxiv.org/pdf/2409.11242
- Self-Correction braucht externes Feedback: https://arxiv.org/abs/2310.01798
- When Can LLMs Self-Correct (TACL): https://direct.mit.edu/tacl/article/doi/10.1162/tacl_a_00713/125177/
- RARR: https://aclanthology.org/2023.acl-long.910/
- Weaver / Generation-Verification-Gap: https://arxiv.org/pdf/2506.18203
- Abstention-Survey: https://direct.mit.edu/tacl/article/doi/10.1162/tacl_a_00754/131566/
================================================================================
B) GEGEN LÜCKEN (Vollständigkeit)
================================================================================
1. Backward Design (Stufe 0): zuerst Lernziele definieren, DANN Inhalt generieren.
3 Schichten: "familiar with" / "important to know" / "enduring understanding".
2. Lernziele explizit als Anker-Liste (Bloom-Verben). Jeder Abschnitt wird genau einem
Lernziel zugeordnet. Ziel ohne Inhalt = Lücke; Inhalt ohne Ziel = Ballast.
3. Curriculum-Mapping / Alignment-Matrix (Lernziel × Abschnitt × Assessment) als eigene
Stufe: prüft Abdeckung, Doppelungen, hängende Ziele gleichzeitig.
4. "What's missing"-Critic-Agent: bekommt Thema + Guide + Referenzfakten, sucht NUR
fehlende korrekte Punkte, bessert minimal nach. Discrimination von Revision trennen.
5. "Das Wichtige" verlässlich via Mehrquellen-Konsens (Triangulation): Standard-Curricula
+ mehrere unabhängige Quellen (Schnittmenge = Kern) + LLM-Delphi (Konsens-Topics).
Quellen B:
- Backward Design: https://fctl.ucf.edu/teaching-resources/course-design/backward-design/
- Understanding by Design: https://en.wikipedia.org/wiki/Understanding_by_Design
- GPT-4 Learning Objectives: https://arxiv.org/pdf/2306.17459
- Curriculum Mapping: https://pce.sandiego.edu/curriculum-mapping/
- Critique-Guided Improvement: https://arxiv.org/pdf/2503.16024
- Delphi-Konsens (PMC): https://pmc.ncbi.nlm.nih.gov/articles/PMC8178515/
================================================================================
C) GEGEN ÜBERFORDERUNG (Struktur, Reihenfolge, Lesbarkeit)
================================================================================
1. Prerequisite-/Concept-Dependency-Graph: Konzepte = Knoten, "X braucht Y" = Kante.
Topologische Sortierung → gültige Reihenfolge (Basics zuerst). LLM extrahiert Kanten,
Zyklus-Check. Knoten mit vielen ausgehenden Kanten = Kernkonzepte, früh.
2. Knowledge-Space-Theorie (Doignon/Falmagne, ALEKS): erlaubte Wissenszustände statt fixer
Linie. Validiert Reihenfolge, schließt unmögliche Pfade aus.
3. Bloom-Progression: Remember → Understand → Apply → Analyze → Evaluate → Create.
Jeden Abschnitt taggen, monotone Progression prüfen.
4. Cognitive Load GLOBAL budgetieren (nicht nur pro Abschnitt): Working Memory ~47
Einheiten. Neue Konzepte/Abschnitt begrenzen, Spitzen glätten, schwer/leicht abwechseln.
5. Chunking + einfach→komplex: Teilfertigkeiten zuerst, dann zusammensetzen. Lange
Abschnitte automatisch splitten (Max-Chunk-Regel).
6. Scaffolding mit Fading: anfangs viele Worked Examples, Stütze schrittweise zurücknehmen.
Achtung Expertise-Reversal: was Novizen hilft, bremst Fortgeschrittene.
7. Diátaxis-Struktur: Tutorial / How-to / Reference / Explanation NICHT mischen
(häufigste Ursache verwirrender Doku). Für Lern-Guide: Tutorial + Explanation trennen.
8. Spiral-Curriculum + Spaced Retrieval: Kernideen wiederkehren lassen, aktives Abrufen.
+ Lesbarkeit (frühere Recherche): Ø-Satz ≤ 20 W, keiner > 40; ≤ ~4 neue Begriffe/Abschnitt;
eine Idee pro Absatz; Listen statt Aufzählungssätze.
Quellen C:
- Concept Graph (CMU): https://www.cs.cmu.edu/~hanxiaol/publications/yang-wsdm15.pdf
- Prerequisite Relations (AAAI): https://ojs.aaai.org/index.php/AAAI/article/view/10550
- Knowledge Spaces / ALEKS: https://www.aleks.com/about_aleks/Science_Behind_ALEKS.pdf
- Cognitive Load Strategien: https://www.structural-learning.com/post/cognitive-load-theory-a-teachers-guide
- Expertise Reversal: https://en.wikipedia.org/wiki/Expertise_reversal_effect
- Diátaxis: https://diataxis.fr/start-here/
- Spiral Curriculum: https://www.structural-learning.com/post/the-spiral-curriculum-a-teachers-guide
================================================================================
D) ARCHITEKTUR-METHODEN (Pipeline-Qualität)
================================================================================
1. GeneratorCritic-Loop NUR mit externer Erdung (Self-Refine ~+20 % bei offenen Aufgaben;
intrinsisch ohne Signal verschlechtert). Für Faktenprüfung Tool-Erdung (CRITIC).
2. Stopp-Kriterien: fast alle Gewinne in Runde 12. Stoppen bei Stabilität/Score-Delta<1/
Draft-Ähnlichkeit>0,90. Bestes Draft per Validierung wählen, nicht das letzte.
3. Rollen-Spezialisierung statt Klone: Faktenprüfer ≠ Didaktiker ≠ Lektor ≠ Zielgruppen-
Anwalt. Erlaubt kleinere/billigere Modelle pro Teilschritt.
4. Heterogenität ist der Wirkstoff von Debatte: Gewinn kommt aus VERSCHIEDENEN Modellen,
nicht aus dem Mechanismus. Identische Modelle ≈ Self-Consistency, nur teurer.
5. LLM-as-Judge mit Rubrik, BINÄR (pass/fail) statt 15; Multi-Kriterien getrennt; vage
Begriffe definieren; niedrige Temperatur; gegen 1 Experten kalibrieren.
6. Eval-Harness + Regression-Evals (CI-Gate): Golden-Set (2550 Fälle reichen anfangs),
wächst mit jedem Produktionsfehler. PR/Release blockt unter Schwelle.
7. Diminishing Returns: Voting Sweet Spot 1020 Samples; Debatte-Plateau ~3 Runden;
bei sequentiellen Aufgaben degradiert jede Multi-Agent-Variante um 3970 %.
8. Fehler-Akkumulation: 95 %/Schritt × 10 = 59 %; 90 % → 35 %. Verifier-Gate ZWISCHEN
die Stufen; zentrale Koordination begrenzt Verstärkung (4,4x statt 17,2x).
9. Pitfall Sycophancy/Konsens-Bias: Modelle stimmen zu (~4258 %); Peer-Druck-Flip ~48 %.
Fix STRUKTURELL: Mehrheit klein halten (6→3 Agenten senkt Konformität 70 %→33 %),
isolierte Selbstkorrektur vor Konsens. Prompt-Nudges ("sei standhaft") wirken NICHT.
10. Pitfall Self-Preference: Judge bevorzugt eigene Outputs (GPT-4 ~+10 %, Claude ~+25 %).
Fix: Judge ≠ Generator-Modell (Cross-Model-Judging), Positionen tauschen+mitteln.
11. Human-in-the-Loop kalibrierend, NICHT als blindes Reward (OpenAI GPT-4o-Sycophancy-
Vorfall April 2025 durch Thumbs-up/down). Experten-Review-Gate behalten.
12. MAST-Failure-Taxonomie als Architektur-Checkliste (System-Design / Inter-Agent /
Verification-Termination).
Quellen D:
- Self-Refine: https://arxiv.org/pdf/2303.17651
- CRITIC: https://arxiv.org/abs/2305.11738
- Multiagent Debate: https://arxiv.org/abs/2305.14325 ; Stop Overvaluing: https://arxiv.org/pdf/2502.08788
- LLM-as-Judge (MT-Bench): https://arxiv.org/abs/2306.05685 ; G-Eval: https://aclanthology.org/2023.emnlp-main.153/
- Eval-Prozess (Hamel): https://hamel.dev/blog/posts/llm-judge/
- Google Scaling Agent Systems: https://research.google/blog/towards-a-science-of-scaling-agent-systems-when-and-why-agent-systems-work/
- Sycophancy: https://arxiv.org/abs/2310.13548 ; Conformity: https://arxiv.org/abs/2505.21588
- MAST: https://arxiv.org/abs/2503.13657
- OpenAI Sycophancy-Postmortem: https://venturebeat.com/ai/openai-rolls-back-chatgpts-sycophancy-and-explains-what-went-wrong
================================================================================
EMPFOHLENE NÄCHSTE STUFEN FÜR UNSERE KETTE (priorisiert, größter Nutzen/Aufwand)
================================================================================
0. (Stufe ganz vorn) Lernziele definieren (Backward Design) + "das Wichtige" via
Mehrquellen-Konsens → ankert Auswahl UND Vollständigkeit.
1. Reihenfolge: Prerequisite-Graph + topologische Sortierung in/nach der Gliederung
(Basics zuerst, keine Vorgriffe).
2. Faktencheck-Gate (SAFE/CoVe) in der Inhalts-Prüfung — nutzt vorhandene Websuche;
Verifier = anderes/stärkeres Modell, BINÄRE Urteile.
3. Abstention: unbelegte Fakten raus/markieren.
4. Coverage-/"Was-fehlt"-Critic + Lernziel↔Inhalt-Mapping als Schluss-Gate (zurück bei Lücken).
5. Cognitive-Load-Pass über den ganzen Guide (neue Begriffe/Abschnitt begrenzen, schwer/leicht mischen).
6. Architektur-Hygiene: Gate zwischen jeder Stufe (Judge ≠ Generator), Loops nach 12 Runden
stoppen, Voting 1020 Samples, kleine Mehrheiten (Konsens-Bias), Golden-Set-Regression-Evals.
WARNUNG: NICHT auf "mehr gleiche Agenten" setzen. Heterogenität + externe Erdung + Gates.

View File

@@ -1,44 +1,74 @@
SECTION-AUFBAU
Jeder Baustein wird GENAU eine Section mit:
1. Titel — der Baustein-Titel (kommt aus dem Marker, nicht in den Body schreiben)
2. Beschreibung — was es ist und wozu: MAXIMAL 12 Sätze
3. Beispiele — KURZ und SIMPEL: das Minimalbeispiel im themengerechten Format (siehe BEISPIELFORMAT), keine Realwelt-Komplexität. Höchstens 1 knapper Satz Einordnung dazu. Ein Beispiel pro relevanter Variante: simple Bausteine eines, variantenreiche mehrere. Geordnet vom Üblichen zum Speziellen. Weglassen, wenn ohne Mehrwert.
Jeder Baustein ist ein kleiner, eigenständiger Lern-Guide: er stellt EIN Konzept vor, erklärt es von Grund auf und macht es nutzbar. Zielgruppe: ein Junior-Entwickler, der das Thema NEU lernt und KEIN Vorwissen mitbringt. Du holst ihn ab und bringst ihm die Sache bei. Eine Section ist kein Stichwort-Zettel zum Nachschlagen.
Aufbau je Baustein — drei Beats, fließend ineinander, OHNE Zwischenüberschriften:
1. Einordnung (PFLICHT, der Ankerpunkt) — welches Problem löst der Baustein, wozu braucht man ihn? Knüpfe an etwas Bekanntes/Alltägliches an, bevor das Neue kommt. Ohne diesen Anker steht ein Neuling im Leeren. Nie weglassen.
2. Erklärung — was es ist UND wie/warum es funktioniert. Alltagssprache, von der Intuition zum Detail. JEDEN Fachbegriff beim ersten Auftreten in einem Halbsatz auflösen — auch Begriffe aus dem Section-Titel oder anderen Bausteinen NIE als bekannt voraussetzen. Eine Analogie oder ein Bild ist erlaubt und oft besser als eine Definition. „Wie"-Abläufe Schritt für Schritt zeigen (nicht nur das Ergebnis nennen). Kleine Beispiele/Mini-Snippets dürfen schon hier mitten im Text stehen, wo sie einen Punkt sofort greifbar machen.
3. Beispiel(e) — das Konzept konkret gemacht (siehe BEISPIELFORMAT).
LESBARKEIT — wichtiger als Kürze:
- Kurze Sätze: eine Aussage pro Satz. Richtwert höchstens ~20 Wörter, nie über 25. Keine Schachtelsätze mit mehreren Einschüben (Gedankenstrich-Einschübe vermeiden).
- Kurze Absätze: eine Idee pro Absatz. Lieber zwei kurze Absätze als ein dichter Block. Keine Textwand.
- Aufzählungen (Schritte, Optionen, Anforderungen, mehrere gleichrangige Punkte) als Markdown-Liste mit `-`, NIE in einen langen Aufzählungssatz pressen.
- Wenige neue Fachbegriffe pro Section. Jeden beim ersten Auftreten in Alltagssprache auflösen. Lieber eine Stufe einfacher erklären als mehr Fakten stapeln.
- Im Zweifel ein Satz mehr und klar — statt verdichtet. Verständlichkeit schlägt Knappheit.
LÄNGE — so lang wie nötig:
- KEIN festes Wortlimit. Die Länge richtet sich nach der Schwierigkeit des Konzepts.
- Verständnis-Test: Versteht ein NEULING das Konzept allein aus dieser Section, ohne anderswo nachzulesen? Wenn nein → einen Schritt mehr erklären (Warum + Wie), NICHT verdichten. Diese Section trägt die volle Tiefe selbst — es gibt keine zweite Ausbaustufe mehr, die nachliefert.
- Weglassen: Füllsätze, Einleitungsfloskeln („In diesem Abschnitt…"), Wiederholungen, Fazit. Nicht jeden Randfall nennen — das Übliche erklären, seltene Varianten in die Beispiele.
BEISPIELFORMAT — am Thema ausrichten, nicht pauschal an Code:
- Code-/Tool-Thema (Sprache, Framework, CLI, Konfiguration): Codeblock mit Sprachangabe, wenige Zeilen, Minimalbeispiel.
- Sprach-Thema (Vokabeln, Grammatik, Formulierungen): 13 Beispielsätze oder ein Mini-Dialog, fremdsprachiger Teil *kursiv*, deutsche Übersetzung in Klammern wo nötig.
- Konzept-Thema (Psychologie, Kommunikation, Methoden, Theorie): ein Mini-Szenario in 24 Sätzen (Situation → Anwendung → Wirkung), ein Schema oder eine Formel.
- Konzept-Thema (Psychologie, Kommunikation, Methoden, Theorie, Mathe): ein Mini-Szenario in 24 Sätzen (Situation → Anwendung → Wirkung), ein Schema oder eine durchgerechnete Formel mit kleinen Zahlen.
Mischthemen: pro Beispiel das Format wählen, das den Punkt am direktesten zeigt.
Ein Beispiel ist immer KONKRET (echter Code, echte Sätze, echte Situation) — nie die Beschreibung, was ein Beispiel zeigen würde.
Jedes Beispiel benennt seine Variante: in Code als Kommentar in der Code-Syntax (z. B. `<!-- Einzelner Absatz -->`, `// Mit Default-Wert`), in Prosa als vorangestelltes fettes Label (z. B. **Höfliche Bitte:**).
Mehrere Beispiele benennen ihre Variante: in Code als Kommentar in der Code-Syntax (z. B. `<!-- Einzelner Absatz -->`, `// Mit Default-Wert`), in Prosa als vorangestelltes fettes Label (z. B. **Höfliche Bitte:**). Bei nur einem Beispiel ist kein Label nötig.
Jede Section ist ATOMAR: allein verständlich, ohne dass der Leser eine andere Section gelesen hat. Test: Ergibt der Text Sinn, wenn man NUR diese Section liest? Verweise auf andere Bausteine sind erlaubt, ihr Inhalt darf aber nie vorausgesetzt werden — benutzte Begriffe in einem Halbsatz auflösen.
Umfang: kurz. Die Länge einer Section kommt aus der ZAHL der Beispiele (Varianten), nie aus langen Texten.
Tonalität: klares, direktes Deutsch. Du erklärst, du referierst nicht. Praxisorientiert, ohne Füllsätze.
Tonalität: klares Deutsch, direkt, praxisorientiert. Fachbegriffe beim ersten Auftreten kurz erklären. Keine Füllsätze, keine Einleitungsfloskeln.
Markdown im Section-Body: erklärende Absätze in normalem Text, Aufzählungen als Markdown-Liste (`-`), `inline-code` für Bezeichner, Codeblöcke mit Sprachangabe NUR für Code-Beispiele — Beispielsätze, Dialoge und Szenarien als normaler Text, NIE in einen Codeblock zwingen. **fett** sparsam für Kernaussagen und Beispiel-Labels. Keine eigenen Überschriften außer `### Beispiel` bzw. `### Beispiele` vor den Beispielen.
Markdown im Section-Body: normale Absätze, `inline-code` für Bezeichner, Codeblöcke mit Sprachangabe NUR für Code-Beispiele — Beispielsätze, Dialoge und Szenarien als normaler Text, NIE in einen Codeblock zwingen. **fett** sparsam für Kernaussagen. Keine eigenen Überschriften außer `### Beispiel` bzw. `### Beispiele` vor den Beispielen.
Mathematik IMMER als LaTeX schreiben: inline zwischen `$…$` (z. B. `$\Sigma^*$`, `$L \subseteq U$`, `$k = 3$`), abgesetzte Formeln zwischen `$$…$$`. KEINE Unicode-Sonderzeichen als Mathe-Ersatz (nicht `x₁`, `¬`, ``, `≤` — stattdessen `$x_1$`, `$\neg$`, `$\lor$`, `$\le$`) und keine nackten Formeln ohne `$`. Außerhalb von Mathe normaler Text.
Beispiel einer fertigen Section (Code-Thema, nur der Body):
Arrays speichern mehrere Werte unter einem Namen. PHP unterscheidet indizierte Arrays (`[0 => 'a']`) und assoziative Arrays (`['key' => 'wert']`) — intern sind beide geordnete Hashmaps.
Arrays lösen ein simples Problem: Du willst viele Werte unter einem Namen halten, statt für jeden eine eigene Variable. In PHP gibt es zwei Sorten. Indizierte Arrays nummerieren die Werte durch (`[0 => 'a']`). Assoziative Arrays geben jedem Wert einen eigenen Schlüssel (`['key' => 'wert']`) — praktisch, wenn die Position egal ist, der Name aber zählt. Intern sind beide dasselbe: geordnete Hashmaps.
### Beispiel
```php
$preise = ['apfel' => 1.20, 'birne' => 1.50];
$preise['kirsche'] = 3.90; // ergänzen
echo $preise['apfel']; // 1.2
$preise['kirsche'] = 3.90; // neuen Schlüssel ergänzen
echo $preise['apfel']; // 1.2 — Zugriff über den Namen
```
Assoziative Arrays sind der Arbeitsalltag: Datenbankzeilen, Konfiguration, JSON.
So sieht der Alltag aus: Datenbankzeilen, Konfiguration, JSON landen fast immer in assoziativen Arrays.
Beispiel einer fertigen Section (Konzept-Thema, nur der Body):
Paraphrasieren wiederholt die Aussage des Gegenübers in eigenen Worten, um Verständnis zu prüfen und Eskalation zu bremsen.
Im Streit reden zwei oft aneinander vorbei, weil keiner sicher ist, ob er den anderen richtig verstanden hat. Paraphrasieren setzt genau hier an: Du wiederholst die Aussage des Gegenübers in eigenen Worten und fragst nach, ob das so stimmt. Das prüft dein Verständnis und nimmt Tempo aus dem Konflikt — der andere fühlt sich gehört, statt sich verteidigen zu müssen. Wichtig: Du bestätigst nicht den Vorwurf, du spiegelst nur die Botschaft dahinter.
### Beispiel
**Vorwurf abfedern:**
A: „Nie hältst du dich an Absprachen!"
B: „Du bist sauer, weil ich den Termin gestern verschoben habe — richtig?"
Die Paraphrase bestätigt nicht den Vorwurf, sondern prüft die Botschaft dahinter.
B übernimmt nicht das Wort „nie", sondern benennt das konkrete Anliegen. Das öffnet das Gespräch, statt es zu eskalieren.
Beispiel einer fertigen Section mit Aufzählung (Liste statt Aufzählungssatz):
Bevor du Shopware installierst, muss dein Server die Software tragen können. Sonst bricht die Installation ab. Shopware 6 braucht ein paar feste Bausteine:
- **PHP 8.2, 8.3 oder 8.4** — die Sprache, in der Shopware läuft.
- **MySQL ab 8.0.17** oder **MariaDB ab 10.11** — die Datenbank für deine Artikel und Bestellungen.
- **Composer ab 2.2** — lädt die PHP-Bibliotheken, die Shopware mitbringt.
- **Node.js 20+** — baut die JavaScript- und CSS-Dateien zusammen.
### Beispiel
```bash
php -v # PHP-Version prüfen
composer -V # Composer-Version
node -v # Node-Version
```
Stimmt eine Version nicht, aktualisierst du sie zuerst. Eine zu alte Version ist die häufigste Ursache für eine fehlgeschlagene Installation.

View File

@@ -0,0 +1,34 @@
Du bist Qualitäts-Prüfer für Bewertungen in einer Prüfung zum Baustein "{baustein}" aus dem Lern-Guide zum Thema "{topic}". Ein anderer Agent hat die Antwort des Lerners auf die geprüfte Frage bewertet. Prüfe, ob die Bewertung fair und korrekt ist.
GEPRÜFTE FRAGE:
{frage}
BAUSTEIN AUS DEM GUIDE:
{section_block}
KOMPAKTE FASSUNG (Merksätze, falls vorhanden):
{kompakt_block}
PRÜFUNGS-VERLAUF (Antwort des Lerners und etwaige Diskussion):
{transcript}
ZU PRÜFENDE BEWERTUNG (enthält das vergebene Niveau):
{bewertung_block}
NIVEAU-SKALA (Anteil des getroffenen Kerns):
- kaum < 25 % · teilweise 2549 % · solide 5074 % · stark 7599 % · komplett 100 %.
PRÜFE GEGEN DIESE KRITERIEN:
- Passt das Niveau zum tatsächlichen Anteil des Kerns? Zu STRENG (richtige/vollständige Antwort zu niedrig) ODER zu MILDE (falsche/dünne Antwort zu hoch) → Fehlurteil. Beide Richtungen prüfen.
- Kern sachlich FALSCH (Gegenteil) → muss "kaum" sein, egal wie selbstsicher.
- Kein Fordern über das Material hinaus: „Nicht im Material" darf nie zu Lasten gehen.
- Kein Ablese-Test: sachlich RICHTIG mit anderen Worten/Synonymen ist voll zu werten, nicht abzuwerten.
- Asymmetrie: Weltwissen nur zum ANERKENNEN richtiger Antworten, nie zum strengeren Fordern.
- Widerspruchs-Check: Feedback passt zum Niveau und widerspricht sich nicht.
- Prüfe die Antwort SELBST auf Korrektheit (Material UND Logik), nicht nur die Fairness.
Beanstande NUR echte Fehlurteile. Ist das Niveau fair, korrekt und materialtreu, ist es in Ordnung.
Gib NUR JSON aus (kein weiterer Text):
- Bewertung in Ordnung: {{"ok": true}}
- Sonst: {{"probleme": ["was an der Bewertung falsch ist"]}}

View File

@@ -0,0 +1,49 @@
Du bewertest die Antwort eines Lerners auf die geprüfte Frage — Baustein "{baustein}" aus dem Lern-Guide zum Thema "{topic}".
GEPRÜFTE FRAGE:
{frage}
BAUSTEIN AUS DEM GUIDE:
{section_block}
KOMPAKTE FASSUNG (Merksätze, falls vorhanden):
{kompakt_block}
PRÜFUNGS-VERLAUF (Antwort des Lerners und etwaige Diskussion):
{transcript}
UNZUFRIEDENHEIT DES LERNERS MIT EINER FRÜHEREN BEWERTUNG (falls vorhanden — ernst nehmen, aber nur nachgeben, wenn er sachlich recht hat):
{begruendung_block}
Bewerte die Antwort auf die GEPRÜFTE FRAGE — auf Basis der Antwort UND der Diskussion im Verlauf.
FINALER STAND ZÄHLT — nicht die erste Aussage:
- Bewerte den Erkenntnisstand, den der Lerner am ENDE des Verlaufs SELBST erreicht hat.
- Zuerst falsch, dann im Dialog selbst korrigiert = zählt (erlaubter Lernweg).
- ABER: Hat der Tutor die Lösung vorgesagt und der Lerner sie nur nachgesprochen ("ja", "genau", bloße Wiederholung), zählt sie NICHT. Der tragende Schluss muss vom Lerner kommen.
NIVEAU — wie viel vom KERN der Frage ist richtig getroffen? Wähle GENAU EINS:
- "kaum": unter 25 % — fast nichts richtig, oder klar falsch / das Gegenteil.
- "teilweise": 2549 % — ein Bruchstück stimmt, der Kern fehlt.
- "solide": 5074 % — der Kern ist richtig getroffen, Details fehlen.
- "stark": 7599 % — größtenteils vollständig und richtig, nur eine kleine Lücke.
- "komplett": 100 % — der Kern ist vollständig und korrekt beantwortet.
MASSSTAB:
- Gemessen an dem, was die FRAGE verlangt — in JEDER korrekten Formulierung, nicht am Wortlaut des Guides. Für "komplett" muss die Frage erfüllt sein, mehr nicht — verlange keine ideale Vollantwort.
- Ist der Kern sachlich FALSCH (Gegenteil), ist es "kaum" — egal wie selbstsicher formuliert.
- Maßstab ist die ursprünglich GEPRÜFTE FRAGE, nicht tiefere Folge-Fragen. Tieferes Bohren erhöht die Messlatte NICHT.
DU PRÜFST VERSTÄNDNIS, NICHT ABLESEN — asymmetrische Material-Grenze:
- FORDERN: Verlange NIE mehr, als Frage und Material hergeben. „Nicht im Material" geht nie zu Lasten des Lerners.
- AKZEPTIEREN: Sachlich RICHTIG zählt hoch — auch mit anderen Worten oder korrektem Wissen über den Guide hinaus. Synonyme zählen voll.
- WELTWISSEN: nur zum ANERKENNEN richtiger Antworten, nie zum strengeren Fordern.
- Behaupte nichts Erfundenes. Gib nach, wenn der Lerner sachlich recht hat — nicht aus Höflichkeit oder auf bloßes Beharren.
FELD `feedback`: max. 1 Satz, sprich den Lerner direkt an. Begründe knapp das Niveau. KEINE neue Frage. Kein Widerspruch — bejahe nicht die Antwort und nenne zugleich die Gegen-Lösung.
HINWEISE DES PRÜFERS ZUR LETZTEN FASSUNG:
{kritik_block}
Gib NUR dieses JSON aus (kein weiterer Text):
{{"feedback": "ein Satz", "niveau": "kaum" | "teilweise" | "solide" | "stark" | "komplett"}}

View File

@@ -0,0 +1,19 @@
Du bist ein hilfreicher Tutor für den Baustein "{baustein}" aus dem Lern-Guide zum Thema "{topic}". Ein Leser stellt dir Fragen zu genau diesem Baustein.
BAUSTEIN AUS DEM GUIDE:
{section_block}
KOMPAKTE FASSUNG (Merksätze, falls vorhanden):
{kompakt_block}
BISHERIGER CHAT-VERLAUF:
{transcript}
Antworte als Assistent auf die letzte Nutzer-Nachricht.
WICHTIG Antwortstil:
- KURZ und EINFACH: 13 Sätze, klare Sprache.
- Keine Einleitung, keine Wiederholung der Frage, kein Markdown-Drumherum.
- Bleib beim Baustein; nutze Guide-Fassung und Vertiefung als Kontext.
Gib NUR die Antwort aus, kein Präfix wie "Assistent:".

View File

@@ -0,0 +1,32 @@
Du bist Qualitäts-Prüfer für Prüfungsfragen in einem Lern-Guide zum Thema "{topic}", Baustein "{baustein}". Ein anderer Agent hat eine Frage formuliert. Prüfe sie streng.
BAUSTEIN AUS DEM GUIDE:
{section_block}
KOMPAKTE FASSUNG (Merksätze, falls vorhanden):
{kompakt_block}
BISHERIGER PRÜFUNGS-VERLAUF (nur frühere Fragen und Antworten):
{transcript}
BEREITS GESTELLT / SCHON VORGEMERKT (Frage darf keiner davon gleichen):
{vermeide_block}
GEWÜNSCHTER FRAGETYP / FOKUS (zur Orientierung):
{typ_block}
{fokus_block}
ZU PRÜFENDE FRAGE:
{frage}
PRÜFE GEGEN DIESE KRITERIEN:
- Stil: GENAU EINE Frage, ein einziges Fragezeichen, eine einzige Sache. Kein Mehrteiler ("und"/"sowie", "sowohl … als auch …", "nenne drei …").
- Kürze: maximal 12 Sätze, kein Szenario-Aufbau über mehrere Sätze, keine lange Vorrede.
- KEINE Wiederholung — beanstande, wenn die Frage einer aus Verlauf ODER Vermeide-Liste SINNGEMÄSS zu ähnlich ist (gleiche Kernsache, nur umformuliert).
- Fachlich korrekt: Die Frage muss aus dem Material oben beantwortbar sein und darf der Referenz NICHT widersprechen. Keine erfundenen Zusatzannahmen.
Beanstande NUR echte Verstöße. Ist die Frage knapp, einzeln und korrekt, ist sie in Ordnung — verlange nichts darüber hinaus.
Gib NUR JSON aus (kein weiterer Text):
- Alles in Ordnung: {{"ok": true}}
- Sonst: {{"probleme": ["kurzer Mangel 1", "kurzer Mangel 2"]}}

View File

@@ -0,0 +1,21 @@
Du bist Prüfer in einem Lern-Guide zum Thema "{topic}", Baustein "{baustein}". Formuliere dem Lerner EINE Verständnisfrage. Du bekommst dafür ein **Muster** — eine Beispielfrage. Stelle dieselbe Sache, aber im Wortlaut **leicht anders** (andere Formulierung, gleicher Kern und gleiche Schwierigkeit).
MUSTER (Vorlage — gleiche Sache, neuer Wortlaut):
{muster}
BAUSTEIN AUS DEM GUIDE (fachliche Referenz — die Frage darf ihr nie widersprechen):
{section_block}
KOMPAKTE FASSUNG (Merksätze, falls vorhanden):
{kompakt_block}
HARTE REGELN:
- GENAU EINE Frage. Ein einziges Fragezeichen. Eine einzige Sache.
- Maximal 12 Sätze, keine Vorrede, kein Szenario-Aufbau.
- Verboten: zwei Fragen mit "und"/"sowie" verketten, "nenne drei …", Aufzähl-Forderungen.
- Gleiche Kernfrage wie das Muster, nur anders formuliert — KEINE neue Sache, KEINE wörtliche Kopie.
- Frag nur, was aus dem Material beantwortbar ist. Keine erfundenen Zusatzannahmen.
- Sprich den Lerner direkt an, klares Deutsch, keine Floskeln.
Gib NUR dieses JSON aus (kein weiterer Text):
{{"frage": "genau eine kurze Frage"}}

View File

@@ -0,0 +1,41 @@
Du bist Prüfer in einem Lern-Guide zum Thema "{topic}". Stelle dem Lerner EINE Verständnisfrage zum Baustein "{baustein}". Der Lerner sieht das Material — frage nach Verständnis und Transfer, nicht nach Abgelesenem.
BAUSTEIN AUS DEM GUIDE:
{section_block}
KOMPAKTE FASSUNG (Merksätze, falls vorhanden):
{kompakt_block}
BISHERIGER PRÜFUNGS-VERLAUF (nur frühere Fragen und Antworten):
{transcript}
BEREITS GESTELLT / SCHON VORGEMERKT — auch SINNGEMÄSS NICHT wiederholen:
{vermeide_block}
GEWÜNSCHTER FRAGETYP:
{typ_block}
FOKUS:
{fokus_block}
HARTE REGELN FÜR DIE FRAGE — wichtiger als alles andere:
- GENAU EINE Frage. Ein einziges Fragezeichen. Eine einzige Sache.
- Maximal 12 Sätze. Kein Szenario-Aufbau, keine Vorrede, kein "Angenommen … und außerdem …".
- Verboten: zwei Fragen mit "und"/"sowie" verketten, "nenne drei …", "sowohl … als auch …", Aufzähl-Forderungen.
- Frag nach EINEM Gedanken: ein Warum, eine Konsequenz, eine Abgrenzung, die Anwendung auf EIN kurzes Beispiel, einen Fehler finden.
- KEIN Faktenabruf ("welche Daten…", "wie viele…", "was enthält…"): das prüft Auswendiglernen, nicht Verständnis.
- Frag nur zu dem, was der Abschnitt wirklich erklärt — nicht zu am Rand Erwähntem (ein Stichwort), das er nicht ausführt.
- Passt eine Transferfrage nicht in einen Satz, wähle eine einfachere Frage.
- Wiederhole KEINE Frage aus Verlauf oder Vermeide-Liste — auch nicht sinngemäß/umformuliert. Frag eine ANDERE Sache (anderer Aspekt, anderer Subbaustein).
FACHLICHE REFERENZ — WICHTIG:
- Die Guide-Fassung und die Vertiefung oben sind die Referenz. Deine Frage darf ihr NIE widersprechen.
- Erfinde keine Zusatzannahmen (z. B. fehlende Eingaben, geänderte Definitionen). Frag nur, was aus dem Material folgt.
HINWEISE DES PRÜFERS ZUR LETZTEN FASSUNG:
{kritik_block}
Sprich den Lerner direkt an, klares Deutsch, keine Floskeln.
Gib NUR dieses JSON aus (kein weiterer Text):
{{"frage": "genau eine kurze Frage"}}

View File

@@ -0,0 +1,18 @@
Du prüfst eine Lückentext-Antwort in einer Lern-Prüfung zum Thema "{topic}", Baustein "{baustein}". Der Lerner hat einen Begriff in die Lücke getippt. Entscheide, ob er fachlich passt.
SATZ MIT LÜCKE:
{satz}
ERWARTETE LÖSUNG: {loesung}
AKZEPTIERTE ALTERNATIVEN: {alternativen}
ANTWORT DES LERNERS: {eingabe}
PRÜFE:
- Trifft die Antwort denselben Begriff wie die Lösung? Synonyme, andere Schreibweise,
Tippfehler, Singular/Plural, mit/ohne Artikel → gilt als RICHTIG.
- Ein fachlich anderer Begriff oder das Gegenteil → FALSCH.
- Sei großzügig bei der Form, streng bei der Sache.
Gib NUR dieses JSON aus (kein weiterer Text):
{{"richtig": true}}

View File

@@ -0,0 +1,26 @@
Du baust eine Lückentext-Aufgabe für eine Lern-Prüfung zum Thema "{topic}", Baustein "{baustein}". Du bekommst ein Muster als Vorlage — verwandle die Kernidee in einen Satz mit genau EINER Lücke.
MUSTER (Vorlage — gleiche Kernsache, als Lückensatz):
{muster}
BAUSTEIN AUS DEM GUIDE (fachliche Referenz — der Satz darf ihr nie widersprechen):
{section_block}
KOMPAKTE FASSUNG (Merksätze, falls vorhanden):
{kompakt_block}
AUFGABE:
- Schreibe EINEN kurzen Aussagesatz (höchstens ~15 Wörter, keine Nebensatz-Kaskade).
- Ersetze den **Schlüsselbegriff** durch `___` (genau eine Lücke).
- Die Lücke ist ein **Fachbegriff als Wort** (z. B. „Alphabet", „polynomiell") — KEIN
mathematisches Symbol oder Formelteil (nicht `L_2`, nicht `f(w)`). Sie steht **außerhalb**
von `$…$` (nie innerhalb einer Formel).
- Die Lücke prüft aktives Abrufen — sie steht für einen zentralen Fachbegriff, nicht für ein Füllwort.
- Der Satz gibt genug Kontext, dass die Lösung eindeutig ist (keine Raterei).
- Fachbegriffe/Formeln dürfen als $…$ gesetzt werden (z. B. $V(w,c)$).
- `loesung` = der erwartete Begriff (kurz, wie er in die Lücke gehört).
- `alternativen` = akzeptierte Synonyme/Schreibweisen (kann leer sein), KEINE falschen.
- Klares Deutsch, ein Satz, genau ein `___`.
Gib NUR dieses JSON aus (kein weiterer Text):
{{"satz": "Ein ___ ist eine endliche Menge von Symbolen.", "loesung": "Alphabet", "alternativen": ["Zeichenvorrat"]}}

View File

@@ -0,0 +1,27 @@
Du baust eine Lückentext-Aufgabe MIT AUSWAHL für eine Lern-Prüfung zum Thema "{topic}", Baustein "{baustein}". Du bekommst ein Muster als Vorlage — verwandle die Kernidee in einen Satz mit genau EINER Lücke und biete vier Begriffe zur Auswahl.
MUSTER (Vorlage — gleiche Kernsache):
{muster}
BAUSTEIN AUS DEM GUIDE (fachliche Referenz — der Satz darf ihr nie widersprechen):
{section_block}
KOMPAKTE FASSUNG (Merksätze, falls vorhanden):
{kompakt_block}
AUFGABE:
- Schreibe EINEN kurzen Aussagesatz (höchstens ~15 Wörter). Ersetze den **Schlüsselbegriff** durch `___` (genau eine Lücke).
- Die Lücke ist ein **Fachbegriff als Wort** — KEIN mathematisches Symbol/Formelteil
(nicht `L_2`, nicht `f(w)`). Sie steht **außerhalb** von `$…$` (nie innerhalb einer Formel).
- Biete GENAU VIER Begriffe als Optionen — **genau einer** passt in die Lücke.
- Die drei falschen sind plausibel, aber klar falsch (Verwechslung, verwandter Begriff) — keine Albernheiten.
- Optionen kurz (Begriff oder kurzer Halbsatz), ähnlich lang.
- Fachbegriffe/Formeln dürfen als $…$ gesetzt werden.
Gib NUR dieses JSON aus (kein weiterer Text):
{{"satz": "Ein ___ ist eine endliche Menge von Symbolen.", "optionen": [
{{"text": "Alphabet", "korrekt": true}},
{{"text": "Wort", "korrekt": false}},
{{"text": "Sprache", "korrekt": false}},
{{"text": "Zustand", "korrekt": false}}
]}}

View File

@@ -0,0 +1,45 @@
Du bist Tutor in einer Prüfung zum Baustein "{baustein}" (Thema "{topic}"). Du sprichst DIREKT mit dem Lerner ("du") und führst ihn durch die Frage. Du diskutierst, du bewertest NICHT.
GEPRÜFTE FRAGE:
{frage}
DEINE LETZTE BEWERTUNG (falls vorhanden):
{letzte_bewertung_block}
BAUSTEIN AUS DEM GUIDE:
{section_block}
KOMPAKTE FASSUNG (Merksätze, falls vorhanden):
{kompakt_block}
BISHERIGER VERLAUF:
{transcript}
Antworte als Tutor auf die letzte Nutzer-Nachricht.
TON & FORM:
- Sprich den Lerner mit "du" an. Rede MIT ihm, nicht über ihn.
- Gib nur deine Antwort aus: keine Vorrede, kein Meta-Kommentar, keine Beschreibung des Gesprächs oder deiner Rolle.
- Höchstens 3 Sätze. Prüfe das, bevor du sendest — kürze sonst.
SO FÜHRST DU:
- FOKUS: Maßstab ist die GEPRÜFTE FRAGE oben — NICHT das tiefstmögliche Detail. Ist diese eine Frage beantwortet, ist sie erledigt. Verschiebe das Ziel NIE auf tiefere Folge-Mechanismen ("aber welcher genaue Mechanismus dahinter…"), die über die gestellte Frage hinausgehen.
- Verrate die Lösung NIE — auch nicht bestätigend ("Genau, es ist X"). Höchstens ein kleiner Anstoß.
- Höchstens EINE Gegenfrage: ein Fragezeichen, eine Sache. Kein Mehrteiler.
- Frag nach dem Warum/Wozu/der Konsequenz — nicht nach Fakten oder Listen ("welche Daten…", "nenne die…").
- Nachfragen NUR, solange die gestellte Frage noch NICHT beantwortet ist. Nicht, um eine korrekte Antwort weiter zu vertiefen.
- Ist die Frage unklar: erkläre die FRAGE, nicht die Antwort.
MATERIAL-GRENZE:
- Frag nur, was der Abschnitt oben EXPLIZIT erklärt. Spiegle die Tiefe der Quelle: nur kurz Erwähntes (ein Stichwort) bleibt kurz — bau daraus keine Detailfrage.
- Was nicht im Material steht, sagst du offen ("das steht nicht im Guide"). Verlange nie mehr, als der Text hergibt.
ABSCHLUSS & FAIRNESS:
- Hat der Lerner die GESTELLTE FRAGE beantwortet (Kern in eigenen Worten oder angewandt) — auch über mehrere Schritte: erkenne es kurz an und schlage vor, den Verlauf bewerten zu lassen. Bohre NICHT tiefer, auch wenn es ein noch genaueres Detail gäbe.
- Zeigt der Lerner sachlich, dass deine Frage falsch, unklar oder material-fremd war: gib ihm recht und passe an. Gib aber NICHT aus Höflichkeit oder auf bloßes Beharren nach — nur ein echtes Sach-Argument zählt.
- Du vergibst KEINE Bewertung und stellst KEINE neue Prüfungsfrage.
So NICHT: "Der Lerner fragt, was nicht behandelt wird — schauen wir uns an, was der Guide sagt…"
So JA: "Stimmt — das steht so nicht im Guide. Was sagt der Abschnitt denn konkret über …?"
Antworte direkt, in höchstens 3 Sätzen, und sprich den Lerner mit "du" an. Kein "Assistent:"-Präfix, kein Markdown-Drumherum.

View File

@@ -0,0 +1,29 @@
Du baust eine Multiple-Choice-Frage für eine Lern-Prüfung zum Thema "{topic}", Baustein "{baustein}". Du bekommst ein Muster als Vorlage — stelle dieselbe Sache als Auswahlfrage.
MUSTER (Vorlage — gleiche Kernfrage, als Multiple-Choice):
{muster}
BAUSTEIN AUS DEM GUIDE (fachliche Referenz — Optionen dürfen ihr nie widersprechen):
{section_block}
KOMPAKTE FASSUNG (Merksätze, falls vorhanden):
{kompakt_block}
AUFGABE:
- Formuliere EINE klare Frage und GENAU VIER Antwortoptionen.
- {anzahl_block}
- Jede richtige Option ist sachlich aus dem Material belegbar. Jede falsche ist plausibel,
aber klar falsch (typischer Irrtum, Verwechslung, Halbwahrheit) — keine Albernheiten.
- **Optionen SEHR KURZ:** ein Stichwort, Begriff oder Halbsatz, höchstens ~8 Wörter.
KEINE ganzen Sätze, KEIN „Weil …"-Vorspann, keine Begründungs-Prosa. Nur der Kern.
- Alle vier Optionen ähnlich lang und im gleichen Stil. Keine verräterischen Hinweise.
- Die Frage selbst knapp (1 Satz). Klares Deutsch.
- Fachbegriffe/Formeln dürfen als $…$ gesetzt werden (z. B. $V(w,c)=A(w)$).
Gib NUR dieses JSON aus (kein weiterer Text):
{{"frage": "die Frage", "optionen": [
{{"text": "Option A", "korrekt": true}},
{{"text": "Option B", "korrekt": false}},
{{"text": "Option C", "korrekt": false}},
{{"text": "Option D", "korrekt": true}}
]}}

View File

@@ -1,16 +0,0 @@
Eine konsolidierte Baustein-Liste zum Thema "{topic}" wurde aus drei Recherchen erstellt. Prüfe sie auf Verluste und Duplikate.
TITEL DER RECHERCHEN:
{results}
KONSOLIDIERTE LISTE:
{auswahl}
Prüfe genau zwei Dinge:
1. FEHLT ein Konzept, das in den Recherchen vorkommt, aber in der konsolidierten Liste nicht enthalten ist — auch nicht unter anderem Titel oder in einem Sammeleintrag? Die Zusammenfassung mehrerer Mikro-Einträge zu einer Lerneinheit ist KEIN Verlust — fehlend ist ein Konzept nur, wenn es nirgends, auch nicht innerhalb eines zusammengefassten Bausteins, enthalten ist.
2. Beschreiben mehrere Einträge der Liste DASSELBE Konzept? Der beste bleibt, die übrigen werden gestrichen.
Schreibe NUR die JSON-Datei nach: {out_path}
Format (Titel EXAKT wie in der konsolidierten Liste; nichts zu tun = leere Listen):
{{"nachtraege": ["Titel — Kurzbeschreibung"], "streichen": ["Exakter Titel aus der Liste"]}}

View File

@@ -0,0 +1,19 @@
Zum Thema "{topic}" haben {n} Agenten unabhängig über strittige Baustein-Einträge entschieden. Die Zahl in Klammern sagt, wie viele Agenten den Eintrag übernehmen wollen. Sortiere die Einträge.
BESCHLOSSENER KONSENS (nur Kontext — nicht ändern):
{konsens}
STRITTIGE EINTRÄGE MIT VOTUM:
{rest}
Regeln:
- Von der MEHRHEIT der {n} Agenten übernommen und keine Dublette zum Konsens → `aufnehmen`.
- Von NIEMANDEM übernommen oder Dublette zum Konsens → verwerfen (in keine Liste).
- Uneindeutig (Minderheits-Votum, fachlich unklar) → `rest`. Diese Einträge gehen in eine weitere Runde.{final}
- Übernimm Einträge wörtlich ("Titel — Kurzbeschreibung"), nicht umformulieren.
- Sind alle Einträge entschieden, ist `rest` LEER — das ist ein gültiges Ergebnis.
Schreibe NUR die JSON-Datei nach: {out_path}
Format (kein weiterer Text in der Datei):
{{"aufnehmen": ["Titel — Kurzbeschreibung"], "rest": ["Titel — Kurzbeschreibung"]}}

View File

@@ -1,16 +1,19 @@
Drei Recherche-Agenten haben unabhängig voneinander die Bausteine des Themas "{topic}" ermittelt. Konsolidiere ihre Ergebnisse zur endgültigen Baustein-Liste.
Bei der Auswahl der Bausteine zum Thema "{topic}" sind einige Einträge strittig. Entscheide für jeden strittigen Eintrag: aufnehmen oder verwerfen.
{results}
BESCHLOSSENER KONSENS (nur Kontext — nicht ändern):
{konsens}
STRITTIGE EINTRÄGE:
{rest}
Regeln:
- Vereinige die Listen: erkenne gleiche Konzepte unter verschiedenen Titeln und führe sie zu einem Baustein zusammen.
- Ein Baustein löst GENAU EIN PROBLEM. Einträge, die Varianten derselben Lösung sind, werden zu EINEM Baustein zusammengefasst (richtig: ein Baustein `<input>` für alle Typen, ein Baustein "Modalverben" für alle Modalverben; falsch: je ein Eintrag pro input-Typ oder pro Verb, aber auch Sammeleinträge, die mehrere Probleme mischen).
- Ein Baustein ist ATOMAR: genau eine Idee, vollständig in sich. Test: Man kann nichts entfernen, ohne ihn unvollständig zu machen — und es fehlt nichts, um ihn zu verstehen.
- KONSOLIDIERE die Granularität: ein Baustein ist eine LERNEINHEIT, kein Lexikon-Eintrag. Liefern die Recherchen dutzende Mikro-Einträge derselben Sorte (eine CSS-Eigenschaft, ein Verb, eine Geste pro Eintrag), fasse sie nach Problem zusammen (richtig: "Flexbox-Ausrichtung" statt sechs Einträge für justify-content, align-items, …). Mehr als ~150 Bausteine sind fast immer ein Granularitäts-Problem — prüfe dann gezielt auf solche Serien.
- Verwirf Bausteine, die erfunden wirken. Eine fehlende Quelle allein ist kein Streichgrund, wenn mindestens zwei Recherchen den Baustein unabhängig nennen. Behalte im Zweifel, was mindestens eine Recherche belegt.
- KEINE Kategorien, KEINE Bewertung — eine flache, durchnummerierte Liste.
- Lass die Quellen weg. Titel und Kurzbeschreibung (max. ~12 Wörter) auf DEUTSCH (Code-Bezeichner bleiben original). Jeder Titel muss EINDEUTIG sein.
- AUFNEHMEN, wenn der Eintrag eine eigenständige Lerneinheit des Themas ist, die der Konsens nicht abdeckt — auch nicht unter anderem Titel oder innerhalb eines Sammeleintrags.
- VERWERFEN, wenn er eine Dublette zum Konsens ist, nur eine Variante/Vertiefung eines vorhandenen Bausteins, Mikro-Granularität oder fachlich zweifelhaft.
- Übernimm aufgenommene Einträge wörtlich ("Titel — Kurzbeschreibung"), nicht umformulieren.
- Nichts Aufnehmenswertes dabei → leere Liste. Das ist ein gültiges Ergebnis.
Schreibe NUR die Markdown-Datei nach: {out_path}
Schreibe NUR die JSON-Datei nach: {out_path}
Format: GENAU eine Zeile pro Baustein: `N. Titel — Kurzbeschreibung`. Kein weiterer Text in der Datei.
Format (kein weiterer Text in der Datei):
{{"uebernehmen": ["Titel — Kurzbeschreibung"]}}
Nichts aufnehmen: {{"uebernehmen": []}}

View File

@@ -0,0 +1,17 @@
Zum Thema "{topic}" sind unten Bausteine, die in der Recherche nur EINMAL genannt wurden. Die Quelle wurde vollständig durchsucht. Entscheide jeden Eintrag einzeln.
BEREITS BESTÄTIGTES INVENTAR (nur Kontext — nicht ändern, nicht duplizieren):
{konsens}
EINZELN GENANNTE EINTRÄGE (jeweils entscheiden):
{rest}
Regeln:
- Fachlich valider, eigenständiger Baustein und KEINE Dublette/Variante des Inventars → `aufnehmen`.
- Erfunden, zu vage, oder Dublette/Variante eines Inventar-Bausteins → verwerfen (in keine Liste).
- Übernimm aufgenommene Einträge WÖRTLICH ("Titel — Kurzbeschreibung"), nicht umformulieren.{final}
Schreibe NUR die JSON-Datei nach: {out_path}
Format (kein weiterer Text in der Datei):
{{"aufnehmen": ["Titel — Kurzbeschreibung"], "rest": []}}

View File

@@ -0,0 +1 @@
Die Quelle wurde von einer Website gecrawlt und liegt als Text-Dateien im Ordner {project} (Seiten als .txt, PDFs als gleichnamige .txt — lies IMMER die .txt). Jede Seiten-Datei beginnt mit einer `QUELLE:`-Zeile (Ursprungs-URL). Verschaffe dir mit Bash (ls/find) einen Überblick und lies die Dateien mit dem Read-Tool. ZIEL: Klausur-Vorbereitung. Erfasse die PRÜFUNGSRELEVANTEN Bausteine: Definitionen, Kernkonzepte, Verfahren/Algorithmen, Formeln, typische Aufgaben. Nur was in den Dateien steht — nichts Erfundenes, kein externes Wissen dazu.

View File

@@ -1 +1 @@
Das Thema ist das Projekt unter {project}. Verschaffe dir mit Bash (ls/find) einen Überblick und lies README, Doku-Ordner und den relevanten Quellcode mit dem Read-Tool. PDFs liegen als gleichnamige .txt-Dateien vor — lies IMMER die .txt, nie das PDF. Die Bausteine müssen das echte Projekt widerspiegeln, nichts Erfundenes.
Das Thema ist das Projekt unter {project}. Verschaffe dir mit Bash (ls/find) einen Überblick und lies README, Doku-Ordner und den relevanten Quellcode mit dem Read-Tool. PDFs liegen als gleichnamige .txt-Dateien vor — lies IMMER die .txt, nie das PDF. ZIEL: das Projekt VERSTEHEN, nicht nachprogrammieren. Erfasse die Bausteine, die erklären, wie es funktioniert: die Features/Funktionen, die Architektur und Komponenten, die wichtigen Abläufe/Flows (z.B. Request→Response, Datenfluss), die zentralen Konzepte und Entscheidungen. Nicht Zeile-für-Zeile-Code, sondern das Verständnis-Gerüst. Die Bausteine müssen das echte Projekt widerspiegeln, nichts Erfundenes.

View File

@@ -0,0 +1 @@
Das Lernmaterial liegt im Ordner {project} (z.B. Vorlesungsfolien, Skripte, Übungsblätter; PDFs liegen als gleichnamige .txt-Dateien vor — lies IMMER die .txt, nie das PDF). Verschaffe dir mit Bash (ls/find) einen Überblick und lies die Dateien mit dem Read-Tool. ZIEL: Klausur-Vorbereitung. Erfasse die PRÜFUNGSRELEVANTEN Bausteine: Definitionen, Kernkonzepte, Verfahren/Algorithmen, Formeln und Sätze, typische Aufgaben- und Fragetypen. Die Bausteine müssen das echte Material widerspiegeln — nur was darin vorkommt, nichts Erfundenes, kein externes Lehrbuchwissen dazuerfinden.

View File

@@ -0,0 +1,16 @@
{n} Recherche-Agenten haben unabhängig voneinander die Bausteine des Themas "{topic}" ermittelt. Exakt gleiche Titel wurden bereits zusammengeführt; die Zahl in Klammern sagt, wie viele Recherchen den Baustein nennen. Konsolidiere die Liste.
{eintraege}
Regeln:
- Erkenne GLEICHE Konzepte unter verschiedenen Titeln und führe sie zu einem Baustein zusammen. Die Nennungszahlen der zusammengeführten Einträge addieren sich dabei (pro Recherche zählt ein Konzept nur einmal).
- Ein Baustein löst GENAU EIN PROBLEM. Einträge, die Varianten derselben Lösung sind, werden zu EINEM Baustein zusammengefasst (richtig: ein Baustein `<input>` für alle Typen, ein Baustein "Modalverben" für alle Modalverben; falsch: je ein Eintrag pro input-Typ oder pro Verb, aber auch Sammeleinträge, die mehrere Probleme mischen).
- Ein Baustein ist ATOMAR: genau eine Idee, vollständig in sich. Test: Man kann nichts entfernen, ohne ihn unvollständig zu machen — und es fehlt nichts, um ihn zu verstehen.
- KONSOLIDIERE die Granularität: ein Baustein ist eine LERNEINHEIT, kein Lexikon-Eintrag. Liefern die Recherchen dutzende Mikro-Einträge derselben Sorte (eine CSS-Eigenschaft, ein Verb, eine Geste pro Eintrag), fasse sie nach Problem zusammen (richtig: "Flexbox-Ausrichtung" statt sechs Einträge für justify-content, align-items, …). Mehr als ~150 Bausteine sind fast immer ein Granularitäts-Problem — prüfe dann gezielt auf solche Serien.
- Teile danach in zwei Listen: Bausteine, die (nach dem Zusammenführen) von MINDESTENS ZWEI Recherchen genannt werden → `bausteine`. Nur einmal Genanntes oder fachlich Zweifelhaftes → `rest`. Verwirf nur, was offensichtlich erfunden ist.
- Lass die Quellen weg. Titel und Kurzbeschreibung (max. ~12 Wörter) auf DEUTSCH (Code-Bezeichner bleiben original). Jeder Titel muss EINDEUTIG sein.
Schreibe NUR die JSON-Datei nach: {out_path}
Format (jeder Eintrag ein String "Titel — Kurzbeschreibung"; kein weiterer Text in der Datei):
{{"bausteine": ["Titel — Kurzbeschreibung"], "rest": ["Titel — Kurzbeschreibung"]}}

View File

@@ -15,4 +15,6 @@ Regeln:
Schreibe NUR die Markdown-Datei nach: {bausteine_path}
Format: GENAU eine Zeile pro Baustein: `N. Titel — Kurzbeschreibung — Quelle`
Die Quelle (3. Segment) MUSS der exakte Dateiname bzw. die URL der Crawl-Seite sein, aus der der Baustein stammt — sie steuert die Abdeckungs-Prüfung.
{fokus}
{extra}

View File

@@ -0,0 +1,21 @@
Du bist Qualitäts-Prüfer für den Fragen-Vorrat einer Lern-Prüfung zum Thema "{topic}". Ein anderer Agent hat Frage-Muster für **mehrere Bausteine** erstellt. Prüfe sie und gib eine **bereinigte** Endfassung zurück.
ZU PRÜFENDE MUSTER (gruppiert je Baustein; Zeilen: [typ] (subbaustein) Frage):
{tabelle}
PRÜFE UND BEREINIGE GEGEN DIESE KRITERIEN (je Baustein getrennt):
- **Stil:** jede Frage GENAU EINE Frage, ein Fragezeichen, eine Sache. Kein "und"/"sowie", keine Aufzählung. Maximal 12 Sätze. Verstöße → umformulieren.
- **Eindeutig beantwortbar:** aus dem Baustein-Wissen klar beantwortbar, keine erfundenen Annahmen. Unbeantwortbares → korrigieren oder entfernen.
- **Distinkt:** keine zwei Muster desselben Bausteins, die sinngemäß dieselbe Frage stellen (auch nicht über Typen hinweg). Dubletten → eines behalten, das andere entfernen.
- **Typ-Treue:** die Frage passt zum angegebenen Typ; sonst Typ anpassen oder Muster entfernen.
- `baustein`, `subbaustein` und `typ` der Einträge unverändert übernehmen (nur `frage` darf sich ändern); nur entfernen, nicht neu erfinden.
Behalte gute Muster unverändert. Ändere nur, was wirklich gegen die Kriterien verstößt.
Schreibe die bereinigte Endfassung ALLER Bausteine als EIN JSON in die Datei {out_path} (nutze dein Schreib-Werkzeug), GENAU in diesem Format:
{{"muster": [
{{"baustein": "<exakter Baustein-Titel>", "subbaustein": "<exakter Titel>", "typ": "<Schlüssel>", "frage": "<eine konkrete Frage>"}}
]}}
Gib sonst keinen Text aus.
{extra}

View File

@@ -0,0 +1,28 @@
Du baust den Fragen-Vorrat für eine Lern-Prüfung zum Thema "{topic}". Erzeuge **Frage-Muster** — je eine konkrete Beispielfrage pro Subbaustein und Fragetyp. Aus diesen Mustern werden später bei der Prüfung leicht abgewandelte Fragen formuliert.
Du bearbeitest **mehrere Bausteine**. Jeder Baustein hat seine eigenen relevanten Subbausteine:
{bausteine}
FRAGETYPEN (Schlüssel : Bedeutung):
{typen}
AUFGABE (für **jeden** der Bausteine oben):
- Erzeuge für **jeden** seiner Subbausteine zu **jedem** passenden Fragetyp **ein** Muster (volles Kreuzprodukt).
- Lasse eine Kombination **weg**, wenn der Typ klar nicht passt (z.B. Code-Vorhersage bei reiner Theorie ohne Code). Nichts erzwingen.
- Jedes Muster ist EINE konkrete, knappe Frage (12 Sätze, genau ein Fragezeichen, eine Sache) — kein Mehrteiler, keine Aufzählung.
- Die Frage muss aus dem Baustein-Wissen beantwortbar sein. Erfinde keine Zusatzannahmen.
- Innerhalb eines Subbausteins sollen die Muster verschiedener Typen **wirklich verschiedene** Aspekte treffen — keine Umformulierungen derselben Frage.
- Der `typ` ist exakt einer der Schlüssel oben. `baustein` ist exakt einer der Baustein-Titel oben. `subbaustein` ist exakt einer der Subbaustein-Titel des jeweiligen Bausteins.
HARTE STIL-REGELN JE FRAGE:
- Genau EINE Frage, ein Fragezeichen, eine Sache. Kein "und"/"sowie", kein "nenne drei …".
- Maximal 12 Sätze, keine Vorrede, kein Szenario-Aufbau.
- Direkte Ansprache, klares Deutsch.
{extra}
Schreibe ALLE Muster aller Bausteine als EIN JSON in die Datei {out_path} (nutze dein Schreib-Werkzeug), GENAU in diesem Format:
{{"muster": [
{{"baustein": "<exakter Baustein-Titel>", "subbaustein": "<exakter Subbaustein-Titel>", "typ": "<Schlüssel>", "frage": "<eine konkrete Frage>"}}
]}}
Gib sonst keinen Text aus.

View File

@@ -1,24 +0,0 @@
Prüfe die Baustein-Auswahl für einen Lern-Guide zum Thema "{topic}" (Format: {format_name}).
Der Auftrag an die Auswahl war: {auswahl_auftrag}
INVENTAR (alle verfügbaren Bausteine):
{bausteine}
GETROFFENE AUSWAHL:
{auswahl}
Prüfe:
1. Fehlt etwas, das der Leser für diesen Zweck zwingend braucht?
2. Ist etwas drin, das dem Zweck nicht dient — Interna, Nischenfälle, Doppelungen (mehrere Lösungen fürs selbe Problem)?
3. Passt der Umfang zum Auftrag?
Du PRÜFST nur und notierst Probleme — du änderst die Auswahl nicht.
Schreibe NUR die JSON-Datei nach: {out_path}
Format — Auswahl in Ordnung:
{{"ok": true}}
Sonst (kurz und konkret, maximal 10 Punkte, Baustein-Titel exakt nennen):
{{"probleme": ["…", "…"]}}
{extra}

View File

@@ -1,21 +0,0 @@
Korrigiere die Baustein-Auswahl für einen Lern-Guide zum Thema "{topic}" (Format: {format_name}).
Der Auftrag an die Auswahl war: {auswahl_auftrag}
INVENTAR (alle verfügbaren Bausteine):
{bausteine}
BISHERIGE AUSWAHL:
{auswahl}
NOTIERTE PROBLEME (von der Prüfung):
{probleme}
Behebe NUR die notierten Probleme — alles andere bleibt unverändert.
Verwende die Titel EXAKT so, wie sie im Inventar stehen. Keine neuen erfinden.
Schreibe NUR die vollständige, korrigierte JSON-Datei nach: {out_path}
Format:
{{"bausteine": ["Exakter Titel", "Exakter Titel"]}}
{extra}

View File

@@ -1,18 +0,0 @@
Wähle die Bausteine für einen Lern-Guide zum Thema "{topic}" (Format: {format_name}).
BAUSTEINE (unsortiertes Inventar):
{bausteine}
{auswahl_auftrag}
Denke vom Ziel her: Was soll der Leser am Ende KÖNNEN?
- Wähle, was der Leser dafür praktisch braucht und wirklich benutzt.
- Lass weg: Interna (was das Werkzeug oder die Theorie intern tut, ohne dass man es selbst anfasst), Spezialfälle und Alternativen zum selben Problem — ein Weg reicht.
- "Klingt fundamental" ist kein Kriterium. Frage stattdessen: Fasst der Leser das selbst an?
- Verwende die Titel EXAKT so, wie sie in der Liste stehen. Keine neuen erfinden.
Schreibe NUR die JSON-Datei nach: {out_path}
Format:
{{"bausteine": ["Exakter Titel", "Exakter Titel"]}}
{extra}

View File

@@ -1,24 +0,0 @@
Prüfe die Gliederung eines Lern-Guides zum Thema "{topic}" (Format: {format_name}).
Zielgruppe: Anfänger. Zweck: {zweck}.
GEWÄHLTE BAUSTEINE (müssen alle vorkommen):
{auswahl}
GLIEDERUNG:
{gliederung}
Prüfe:
1. Kommt jeder gewählte Baustein in GENAU einem Kapitel vor (nichts fehlt, nichts doppelt, nichts erfunden)?
2. Führt Kapitel 1 zum schnellsten sichtbaren Ergebnis — oder beginnt es mit Theorie/Interna?
3. Stehen Voraussetzungen vor dem, was auf ihnen aufbaut? Konkretes vor Abstraktem?
4. Kapitelgrößen 37, Kapiteltitel kurz und konkret?
Du PRÜFST nur und notierst Probleme — du änderst die Gliederung nicht.
Schreibe NUR die JSON-Datei nach: {out_path}
Format — Gliederung in Ordnung:
{{"ok": true}}
Sonst (kurz und konkret, maximal 10 Punkte):
{{"probleme": ["…", "…"]}}
{extra}

View File

@@ -1,20 +0,0 @@
Korrigiere die Gliederung eines Lern-Guides zum Thema "{topic}" (Format: {format_name}).
GEWÄHLTE BAUSTEINE (müssen alle vorkommen):
{auswahl}
BISHERIGE GLIEDERUNG:
{gliederung}
NOTIERTE PROBLEME (von der Prüfung):
{probleme}
Behebe NUR die notierten Probleme — alles andere bleibt unverändert.
- JEDER gewählte Baustein landet in GENAU einem Kapitel.
- Verwende die Titel EXAKT so, wie sie in der Liste stehen.
Schreibe NUR die vollständige, korrigierte JSON-Datei nach: {out_path}
Format:
{{"kapitel": [{{"titel": "Grundlagen", "bausteine": ["Exakter Titel", "Exakter Titel"]}}]}}
{extra}

View File

@@ -0,0 +1,24 @@
{n} Agenten haben die feste Baustein-Auswahl für {format_name} zum Thema "{topic}" unabhängig gegliedert ({zweck}). Führe die Vorschläge zu EINER kohärenten Gliederung zusammen.
GEWÄHLTE BAUSTEINE (jeder mit seiner NUMMER — die Auswahl steht fest):
{bausteine}
DIE VORSCHLÄGE (je Kapitel die Baustein-Nummern):
{gliederungen}
Vorgehen — MERGEN, nicht nur einen wählen:
- Wo die Vorschläge übereinstimmen (Bausteine im selben Kapitel, ähnliche Reihenfolge) → übernehmen.
- Wo sie sich unterscheiden → selbst entscheiden: bester roter Faden, vom Bekannten zum Unbekannten, erster Erfolg vor Theorie.
- Nimm den besten Kapitelschnitt und die klarsten Kapiteltitel aus allen Vorschlägen.
- Ergebnis ist EINE Gliederung, die besser ist als jeder einzelne Vorschlag.
Regeln:
- JEDER Baustein in GENAU einem Kapitel. Keinen weglassen, keinen doppelt.
- Ordne per NUMMER. Verwende nur die Nummern aus der Liste oben, jede genau einmal.
- 37 Bausteine pro Kapitel; Kapiteltitel kurz und konkret.
Schreibe NUR die JSON-Datei nach: {out_path}
Format (nummern = die Baustein-Nummern in der gewünschten Reihenfolge):
{{"kapitel": [{{"titel": "Grundlagen", "nummern": [3, 1, 7]}}]}}
{extra}

View File

@@ -1,6 +1,6 @@
Plane die Gliederung eines Lern-Guides zum Thema "{topic}" (Format: {format_name}).
GEWÄHLTE BAUSTEINE (unsortiert — die Auswahl steht fest, du ordnest nur):
GEWÄHLTE BAUSTEINE (jeder mit seiner NUMMER — die Auswahl steht fest, du ordnest nur):
{bausteine}
REIHENFOLGE — vom Bekannten zum Unbekannten:
@@ -10,12 +10,12 @@ REIHENFOLGE — vom Bekannten zum Unbekannten:
Regeln:
- JEDER gewählte Baustein landet in GENAU einem Kapitel. Keinen weglassen, keine neuen erfinden.
- Verwende die Titel EXAKT so, wie sie in der Liste stehen.
- Ordne per NUMMER. Verwende nur die Nummern aus der Liste oben, jede genau einmal.
- 37 Bausteine pro Kapitel; die Kapitelzahl folgt aus dem Thema.
- Kapiteltitel kurz und konkret.
Schreibe NUR die JSON-Datei nach: {out_path}
Format:
{{"kapitel": [{{"titel": "Grundlagen", "bausteine": ["Exakter Titel", "Exakter Titel"]}}]}}
Format (nummern = die Baustein-Nummern in der gewünschten Reihenfolge):
{{"kapitel": [{{"titel": "Grundlagen", "nummern": [3, 1, 7]}}]}}
{extra}

View File

@@ -0,0 +1,19 @@
Prüfe die gesammelten Inhalte für Bausteine eines Lern-Guides zum Thema "{topic}" (Format: {format_name}). Zielgruppe: Anfänger. Es ist noch kein Guide-Text — nur die Inhalts-Stichpunkte, die später gelehrt werden.
INHALTE:
{sections}
Prüfe jeden Baustein:
1. Korrektheit: Sind die Punkte und Fakten sachlich richtig und belegbar? Nichts Halluziniertes, keine erfundenen Werte/Versionen.
2. Vollständigkeit: Fehlt etwas Wesentliches, das ein Anfänger zum Verständnis des Bausteins braucht?
3. Scope: Nicht mehr, als der Baustein hergibt — nichts am Rand Erwähntes aufgeblasen; aber auch keine zentrale Lücke.
Du PRÜFST nur und notierst Probleme — du änderst nichts. Nur echte Mängel notieren, keine Geschmacksfragen.
Schreibe NUR die JSON-Datei nach: {out_path}
Format — alles in Ordnung:
{{"ok": true}}
Sonst (Baustein-Titel EXAKT wie oben):
{{"probleme": [{{"section": "Exakter Baustein-Titel", "problem": "…"}}]}}
{extra}

View File

@@ -0,0 +1,16 @@
Überarbeite einzelne Baustein-Inhalte eines Lern-Guides zum Thema "{topic}". Pro Baustein ist ein PROBLEM notiert (Korrektheit, Vollständigkeit oder Scope). Behebe NUR das notierte Problem; was in Ordnung ist, bleibt erhalten.
{facts}
AUFTRÄGE — je Baustein das Problem und der aktuelle Inhalt:
{auftraege}
Schreibe NUR die Datei {out_path} — pro überarbeitetem Baustein ein section-Marker (Titel EXAKT wie im Auftrag), darunter die korrigierten Punkte:
<!-- section: Exakter Baustein-Titel -->
- Kernpunkt …
- Kernpunkt …
Beispiel: kurze Idee, was das Beispiel zeigt
Die Marker-Zeile exakt so schreiben. Kein Text außerhalb der Sections.
{extra}

View File

@@ -0,0 +1,26 @@
Identifiziere für jeden zugeteilten Baustein eines Lern-Guides zum Thema "{topic}", WAS ein Anfänger ohne Vorwissen verstehen muss. Du schreibst noch keinen Guide-Text — du sammelst nur die Inhalte, die später gelehrt werden.
Dir zugeteilt sind folgende Kapitel und Bausteine — verbindlich: jeder zugeteilte Baustein muss vorkommen, keine zusätzlichen erfinden. Unter jedem Baustein stehen seine SUBBAUSTEINE mit Stufe (`[einfach]`/`[mittel]`/`[schwer]`) — die Teilpunkte, die gelehrt werden:
{zuteilung}
{facts}
Sammle pro Baustein:
- Kernpunkte / Lernziele: was muss der Leser begreifen? Decke JEDEN gelisteten Subbaustein mit mindestens einem Kernpunkt ab. Hat ein Baustein keine Subbausteine, sammle 37 knappe Punkte.
- Voraussetzungen: welche Begriffe/Ideen muss man vorher kennen, um das hier zu verstehen? Jede in einem Halbsatz erklärbar — das sind die Ankerpunkte für Neulinge.
- Typische Hürden: wo verstehen Anfänger es erfahrungsgemäß falsch oder stolpern?
- Belegte Fakten (Versionen, Namen, Werte) — nichts Unbelegtes; Unsicheres per Websuche prüfen.
- Eine konkrete Beispiel-Idee: was soll das Beispiel zeigen?
- Scope: nur was DIESER Baustein hergibt. Nicht mehr, nicht weniger. Am Rand Erwähntes nicht zum Thema aufblasen.
Schreibe NUR die Datei {out_path} — pro Baustein ein section-Marker (Titel EXAKT aus der Zuteilung), darunter die Punkte:
<!-- section: Exakter Baustein-Titel -->
- Kernpunkt …
- Kernpunkt …
Voraussetzung: was vorher kurz erklärt werden muss (Anker)
Hürde: typischer Anfänger-Irrtum
Beispiel: kurze Idee, was das Beispiel zeigt
Die Marker-Zeile exakt so schreiben. Kein Text außerhalb der Sections, kein Fließtext-Guide.
{extra}

View File

@@ -8,9 +8,14 @@ SECTIONS:
{sections}
Prüfe jede Section:
1. Ist die Beschreibung für Anfänger verständlich und maximal 12 Sätze?
2. Sind die Beispiele kurz, simpel, plausibel korrekt — und im themengerechten Format laut Spezifikation (kein Codeblock um Prosa-Beispiele, kein Prosa-Pseudo-Beispiel, wo Code gefragt ist)?
3. Ist das Markdown sauber (keine abgebrochenen Code-Blöcke, keine Platzhalter, kein Fremdtext)?
1. Lehrt die Section das Konzept für einen Anfänger ohne Vorwissen verständlich — ordnet sie es ein, erklärt sie das Wie/Warum, macht ein Beispiel es konkret? Nicht so verdichtet, dass nur jemand sie versteht, der das Thema schon kennt.
2. Lesbarkeit (echte Mängel notieren):
- Sätze über ~25 Wörter oder Schachtelsätze mit mehreren Einschüben.
- Eine Aufzählung (Schritte/Optionen/Anforderungen) als langer Fließtext-Satz, die eine Markdown-Liste sein sollte.
- Textwand: ein dichter Block ohne Absätze, der sich in mehrere teilen ließe.
- Mehr als ~4 neue Fachbegriffe ohne Erklärung beim ersten Auftreten.
3. Sind die Beispiele kurz, simpel, plausibel korrekt — und im themengerechten Format laut Spezifikation (kein Codeblock um Prosa-Beispiele, kein Prosa-Pseudo-Beispiel, wo Code gefragt ist)?
4. Ist das Markdown sauber (keine abgebrochenen Code-Blöcke, keine Platzhalter, kein Fremdtext)?
Du PRÜFST nur und notierst Probleme — du änderst nichts. Nur echte Mängel notieren, keine Geschmacksfragen.

View File

@@ -10,10 +10,15 @@ ZU ÜBERARBEITEN — je Section der aktuelle Inhalt und das notierte Problem:
Behebe pro Section NUR das notierte Problem; was in Ordnung ist, bleibt inhaltlich erhalten.
Schreibe NUR die Datei {out_path} in GENAU diesem Format — für JEDE beanstandete Section ein section-Marker (Titel EXAKT wie oben), darunter der vollständige neue Markdown-Body:
Jede Section hat ZWEI Fassungen mit denselben Inhalten: **kompakt** (je Subbaustein ein Merksatz als Stichpunkt) und **ausführlich** (zusammenhängender Anfänger-Text). Gib beide neu aus und halte sie deckungsgleich.
Schreibe NUR die Datei {out_path} in GENAU diesem Format — für JEDE beanstandete Section ein section-Marker (Titel EXAKT wie oben) mit `kompakt`- und `ausführlich`-Block:
<!-- section: Exakter Section-Titel -->
Beschreibung…
<!-- kompakt -->
- Merksatz je Subbaustein (knapp, ohne Erklärung)
<!-- ausführlich -->
Erklärung (Einordnung → Wie/Warum) laut SECTION-SPEZIFIKATION…
### Beispiel
(Beispiel im themengerechten Format laut SECTION-SPEZIFIKATION: Codeblock NUR bei Code-Themen, sonst Beispielsätze oder Mini-Szenario)

View File

@@ -1,23 +1,43 @@
Schreibe Sections für einen Lern-Guide zum Thema "{topic}" (Format: {format_name}).
Dir zugeteilt sind folgende Kapitel und Bausteine — verbindlich: jede zugeteilte Section muss vorkommen, keine zusätzlichen erfinden:
Dir zugeteilt sind folgende Kapitel und Bausteine — verbindlich: jede zugeteilte Section muss vorkommen, keine zusätzlichen erfinden. Unter jedem Baustein stehen seine SUBBAUSTEINE mit Stufe (`[einfach]`/`[mittel]`/`[schwer]`):
{zuteilung}
{facts}
Das Format ({format_name}) beeinflusst NICHT, wie ein Baustein geschrieben wird — nur, wie viele Bausteine das Thema abdecken. Derselbe Baustein bekommt in MiniGuide, Guide und FullGuide denselben Text.
Beispiel-Tiefe für dieses Format ({format_name}): MiniGuide = nur die üblichen Varianten eines Bausteins, Guide = die gängigen Varianten, FullGuide = alle relevanten Varianten inkl. Nischenfällen. Eine Variante ist eine eigenständige Verwendungsform des Bausteins — Code-Variante, Satzmuster oder Anwendungsfall.
Jeder Baustein bekommt ZWEI Fassungen mit DENSELBEN Inhalten (dieselben Subbausteine):
- **kompakt**: je Subbaustein EIN Merksatz als Stichpunkt (`- …`). Zum Erinnern. Nur benennen, nicht erklären. Gleiche Reihenfolge wie ausführlich.
- **ausführlich**: der zusammenhängende Anfänger-Lerntext (siehe unten).
Beide MÜSSEN dieselben Subbausteine abdecken. Nichts, das nur in einer Fassung steht.
SECTION-SPEZIFIKATION:
SO SCHREIBST DU die ausführliche Fassung — EIN zusammenhängender Text für einen Junior, der das Thema NEU lernt:
- Beginne mit dem Anker (Einordnung): welches Problem/wozu, angeknüpft an etwas Bekanntes — erst dann das Neue. Pflicht, nie weglassen.
- Löse JEDEN Fachbegriff bei Erstnennung in einem Halbsatz auf. Setze nichts voraus — auch keine Begriffe aus dem Titel oder anderen Bausteinen. Nutze die „Voraussetzung"-Hinweise aus den Inhalten als Anker, die „Hürde"-Hinweise, um Missverständnisse vorweg auszuräumen.
- Die Subbausteine sind deine INHALTS-CHECKLISTE, keine Überschriften: webe sie in den Fließtext ein. Behandle jeden, aber als Teil eines Ganzen — nicht als isolierten Mini-Absatz.
- Reihenfolge: erst die `[einfach]`-Punkte (das Fundament), dann `[mittel]` (Details/Varianten), zuletzt `[schwer]` (Sonderfälle/Tücken — knapp, als fortgeschrittener Hinweis).
- Diese Fassung trägt die volle Tiefe selbst — es gibt keine zweite Ausbaustufe. „Wie"-Abläufe Schritt für Schritt zeigen, nicht nur das Ergebnis.
- KEIN festes Längen-Limit. So lang wie nötig, damit ein Neuling es OHNE Vorwissen versteht. Im Zweifel ein Satz mehr und klar.
- Beispiele großzügig — auch kleine Mini-Beispiele mitten im Text, wo sie einen Punkt sofort greifbar machen, zusätzlich zum `### Beispiel` am Ende (Format laut Spezifikation).
GEPRÜFTE INHALTE je Baustein — das ist verbindlich, was gelehrt werden muss:
{inhalte}
Recherchiere NICHT und suche NICHT im Web. Alle nötigen Fakten stehen in diesen geprüften Inhalten — nutze ausschließlich sie. Erfinde nichts dazu, lass nichts Wesentliches weg. Lehre simpel: kurze Sätze, Listen für Aufzählungen, ein Anfänger versteht es sofort.
SECTION-SPEZIFIKATION (gilt je Baustein):
{spec}
Schreibe NUR die Datei {out_path} in GENAU diesem Format — pro Kapitel ein kapitel-Marker, pro Baustein ein section-Marker (Titel EXAKT aus der Zuteilung), darunter der Markdown-Body:
Schreibe NUR die Datei {out_path} in GENAU diesem Format — pro Kapitel ein kapitel-Marker, pro Baustein ein section-Marker (Titel EXAKT aus der Zuteilung), darin ein `kompakt`- und ein `ausführlich`-Block:
<!-- kapitel: Kapiteltitel -->
<!-- section: Exakter Baustein-Titel -->
Beschreibung…
<!-- kompakt -->
- Merksatz je Subbaustein (knapp, ohne Erklärung)
<!-- ausführlich -->
Erklärung (Einordnung → Wie/Warum) laut SECTION-SPEZIFIKATION…
### Beispiel
(Beispiel im themengerechten Format laut SECTION-SPEZIFIKATION: Codeblock NUR bei Code-Themen, sonst Beispielsätze oder Mini-Szenario)
Die Marker-Zeilen exakt so schreiben. Kein Text außerhalb der Sections, kein Dokument-Titel, kein Inhaltsverzeichnis.
{extra}
Die Marker-Zeilen exakt so schreiben (jede Section hat genau einen `<!-- kompakt -->`- und einen `<!-- ausführlich -->`-Block). Kein Text außerhalb der Sections, kein Dokument-Titel, kein Inhaltsverzeichnis.
{extra}

View File

@@ -1,29 +0,0 @@
Baue aus der Faktenbasis einen OnePager zum Thema "{topic}" — ein Übersichtsblatt im 3×3-Raster auf einer Seite.
FAKTENBASIS (alleinige Quelle, nichts hinzuerfinden):
{recherche}
Erstelle GENAU diese 7 Karten (JSON-Schlüssel exakt so):
- "info" — Titel: "{topic}". Kurzbeschreibung in 12 Sätzen, darunter Eckdaten als Stichpunkte (je nach Thema: Art/Typ, Version/Lizenz/Verbreitung ODER Ursprung/Stand/Anwendungsfelder).
- "eigenschaften" — Titel: "Kerneigenschaften". Was einen IM Thema erwartet: kleine Übersicht der Inhalte/Teilgebiete.
- "beispiel" — Titel: "Beispiel". EIN anschauliches, typisches Beispiel mit einem Satz Erklärung — Markdown-Codeblock bei Code-Themen, sonst Beispielsätze oder Mini-Szenario als normaler Text.
- "zusammenhaenge" — Titel: "Zusammenhänge". Mit welchen ANDEREN Themen es zusammenhängt — Nachbarthemen außerhalb dieses Themas, keine Inhalte des Themas selbst.
- "voraussetzungen" — Titel: "Voraussetzungen". Welche Themen man vorher bearbeitet haben sollte, um hier klarzukommen.
- "modern" — Titel: "Neu & aktuell". NUR was in den letzten Jahren neu dazugekommen ist (Features, Erkenntnisse, Empfehlungen). Gibt es nichts Neues: ehrlich "Keine." mit einem Satz Begründung.
- "veraltet" — Titel: "Veraltet & überholt". Was nicht mehr verwendet wird bzw. als überholt gilt. Gibt es nichts Veraltetes: ehrlich "Keine." mit einem Satz Begründung — nichts erfinden.
KOMPAKTHEIT — der OnePager muss OHNE Scrollen auf eine Bildschirmseite passen:
- Maximal 5 Stichpunkte pro Karte, je maximal ~8 Wörter (Schlagwort + halber Satz).
- Nur das Wichtigste — nicht alle Varianten aufzählen. Weglassen schlägt Vollständigkeit.
- Keine Tabellen, keine verschachtelten Listen, keine Einleitungssätze.
- Beispiel maximal ~12 kurze Zeilen (Code) bzw. ~5 Zeilen (Prosa).
Inhalt auf DEUTSCH, alles aus der Faktenbasis belegbar.
Stichpunkte als Markdown-Liste mit fettem Schlagwort: `- **Schlagwort**: Rest` (kein rohes •).
Code-Bezeichner und HTML-Tags im Text IMMER in Backticks (`<p>`, `src`) — nie roh.
Schreibe NUR die JSON-Datei nach: {out_path}
Format:
{{"karten": {{"info": {{"titel": "{topic}", "md": "…"}}, "eigenschaften": {{"titel": "Kerneigenschaften", "md": "…"}}, "beispiel": {{"titel": "Beispiel", "md": "…"}}, "zusammenhaenge": {{"titel": "Zusammenhänge", "md": "…"}}, "voraussetzungen": {{"titel": "Voraussetzungen", "md": "…"}}, "modern": {{"titel": "Neu & aktuell", "md": "…"}}, "veraltet": {{"titel": "Veraltet & überholt", "md": "…"}}}}}}
{extra}

View File

@@ -1,18 +0,0 @@
Korrigiere die Karten eines OnePagers zum Thema "{topic}".
FAKTENBASIS (alleinige Quelle, nichts hinzuerfinden):
{recherche}
BISHERIGE KARTEN:
{karten}
NOTIERTE PROBLEME (von der Prüfung):
{probleme}
Behebe NUR die notierten Probleme — alle anderen Karten bleiben unverändert.
Schreibe NUR die vollständige, korrigierte JSON-Datei (alle 7 Karten) nach: {out_path}
Format:
{{"karten": {{"info": {{"titel": "…", "md": "…"}}, "eigenschaften": {{"titel": "…", "md": "…"}}, "beispiel": {{"titel": "…", "md": "…"}}, "zusammenhaenge": {{"titel": "…", "md": "…"}}, "voraussetzungen": {{"titel": "…", "md": "…"}}, "modern": {{"titel": "…", "md": "…"}}, "veraltet": {{"titel": "…", "md": "…"}}}}}}
{extra}

View File

@@ -1 +0,0 @@
Das Thema ist das Projekt unter {project}. Verschaffe dir mit Bash (ls/find) einen Überblick und lies README, Doku und den relevanten Quellcode mit dem Read-Tool. PDFs liegen als gleichnamige .txt-Dateien vor — lies IMMER die .txt, nie das PDF. Erfasse Zweck, Architektur und die wichtigsten Konzepte — nichts Erfundenes.

View File

@@ -1 +0,0 @@
Recherchiere per Websuche: aktuellen Stand (Version bzw. Forschungs-/Praxisstand), die Kernkonzepte und die wichtigsten Fakten zu "{topic}". Nimm nur auf, was du in der Recherche belegt hast.

View File

@@ -1,27 +0,0 @@
Prüfe die Faktenbasis für einen OnePager zum Projekt "{topic}".
FAKTENBASIS:
{recherche}
Sie muss diese Dimensionen abdecken:
1. Kurzbeschreibung (Art des Projekts, Gegenstand)
2. Technische Daten (Technologie/Format, Umfang, Stand/Aktualität)
3. Inhaltsübersicht (was einen im Projekt erwartet)
4. Ein typisches Beispiel aus dem Projekt
5. Zusammenhänge mit ANDEREN Themen (Nachbarthemen außerhalb des Projektinhalts)
6. Voraussetzungen (vorher zu bearbeitende Themen)
7. Neuerungen der letzten Jahre vs. nicht mehr Verwendetes (oder die ausdrückliche Feststellung, dass es jeweils nichts gibt)
Prüfe:
1. Ist jede Dimension mit konkreten Fakten aus den Projektdateien belegt (Namen, Zahlen — nicht vage)?
2. Hat jeder Punkt einen Dateipfad als Quelle?
3. Wirkt etwas erfunden — also nicht aus dem Projekt belegbar?
Du PRÜFST nur und notierst Probleme — du änderst nichts.
Schreibe NUR die JSON-Datei nach: {out_path}
Format — alles in Ordnung:
{{"ok": true}}
Sonst (kurz und konkret, maximal 10 Punkte):
{{"probleme": ["…", "…"]}}

View File

@@ -1,27 +0,0 @@
Prüfe die Faktenbasis für einen OnePager zum Thema "{topic}".
FAKTENBASIS:
{recherche}
Sie muss diese Dimensionen abdecken:
1. Kurzbeschreibung (12 Sätze)
2. Eckdaten (Art/Typ; bei Software: Version, Lizenz/Kosten; bei Sprachen, Methoden, Theorien: Ursprung/Urheber, heutiger Stand, Anwendungsfelder)
3. Inhaltsübersicht (was einen im Thema erwartet)
4. Ein typisches Beispiel im themengerechten Format (Code, Beispielsätze oder Mini-Szenario)
5. Zusammenhänge mit ANDEREN Themen (Nachbarthemen, nicht Inhalte des Themas selbst)
6. Voraussetzungen (vorher zu bearbeitende Themen)
7. Neuerungen der letzten Jahre vs. nicht mehr Verwendetes (oder die ausdrückliche Feststellung, dass es jeweils nichts gibt)
Prüfe:
1. Ist jede Dimension mit konkreten Fakten belegt (Namen, Zahlen, Versionen bzw. Urheber/Jahreszahlen — nicht vage)?
2. Hat jeder Punkt eine Quelle?
3. Wirkt etwas erfunden oder widersprüchlich?
Du PRÜFST nur und notierst Probleme — du änderst nichts.
Schreibe NUR die JSON-Datei nach: {out_path}
Format — alles in Ordnung:
{{"ok": true}}
Sonst (kurz und konkret, maximal 10 Punkte):
{{"probleme": ["…", "…"]}}

View File

@@ -1,16 +0,0 @@
Überarbeite die Faktenbasis für einen OnePager zum Thema "{topic}".
{source}
BISHERIGE FAKTENBASIS:
{recherche}
NOTIERTE PROBLEME (von der Prüfung):
{probleme}
Behebe NUR die notierten Probleme — Fehlendes anhand der oben genannten Quelle ergänzen, Vages konkretisieren, Unbelegtes belegen oder streichen. Alles andere bleibt erhalten.
Schreibe die VOLLSTÄNDIGE, überarbeitete Markdown-Datei nach: {out_path}
Kompakt, faktenorientiert, mit Quelle pro Punkt.
{extra}

View File

@@ -1,17 +0,0 @@
Sammle die Faktenbasis für einen OnePager — ein Übersichtsblatt auf einer Seite — zum Projekt "{topic}".
{source}
Erfasse gezielt diese Dimensionen:
1. Kurzbeschreibung: Was ist "{topic}" in 12 Sätzen (Art des Projekts, Gegenstand)?
2. Technische Daten: Technologie/Format, Umfang (Dateien/Seiten/Module), Stand/Aktualität.
3. Inhaltsübersicht: Was erwartet einen — die wichtigsten Inhalte/Teilgebiete des Projekts.
4. Beispiel: ein typisches, konkretes Beispiel aus dem Projekt (zentraler Code-Flow bzw. Kerninhalt).
5. Zusammenhänge: mit welchen ANDEREN Themen es zusammenhängt (Nachbarthemen außerhalb des Projektinhalts).
6. Voraussetzungen: welche Themen man vorher bearbeitet haben sollte, um das Projekt zu verstehen.
7. Neuerungen vs. Veraltetes: was im Themenfeld in den letzten Jahren neu dazugekommen ist — und was nicht mehr verwendet wird (falls es nichts gibt, jeweils ausdrücklich notieren).
Schreibe NUR die Markdown-Datei nach: {out_path}
Kompakt, faktenorientiert, mit Quelle (Dateipfad) pro Punkt. Nichts Erfundenes — nur was in den Projektdateien belegt ist. Die Datei ist die alleinige Faktenbasis für den OnePager.
{extra}

View File

@@ -1,17 +0,0 @@
Sammle die Faktenbasis für einen OnePager — ein Übersichtsblatt auf einer Seite — zum Thema "{topic}".
{source}
Recherchiere gezielt diese Dimensionen:
1. Kurzbeschreibung: Was ist "{topic}" in 12 Sätzen?
2. Eckdaten: Art/Typ des Themas; bei Software: aktuelle Version, Lizenz/Kosten, Verbreitung; bei Sprachen, Methoden, Theorien: Ursprung/Urheber, heutiger Stand, typische Anwendungsfelder.
3. Inhaltsübersicht: Was erwartet einen im Thema — die wichtigsten Inhalte/Teilgebiete.
4. Beispiel: ein minimales, typisches Beispiel im themengerechten Format (Code-Beispiel, Beispielsätze/Mini-Dialog oder Mini-Szenario).
5. Zusammenhänge: mit welchen ANDEREN Themen es zusammenhängt (Nachbarthemen außerhalb von "{topic}").
6. Voraussetzungen: welche Themen man vorher bearbeitet haben sollte.
7. Neuerungen vs. Veraltetes: was in den letzten Jahren neu dazugekommen ist — und was nicht mehr verwendet wird (falls es nichts gibt, jeweils ausdrücklich notieren).
Schreibe NUR die Markdown-Datei nach: {out_path}
Kompakt, faktenorientiert, mit Quelle (URL bzw. Dateipfad) pro Punkt. Die Datei ist die alleinige Faktenbasis für den OnePager.
{extra}

View File

@@ -1,22 +0,0 @@
Prüfe einen OnePager zum Thema "{topic}" gegen seine Faktenbasis.
FAKTENBASIS:
{recherche}
ONEPAGER-KARTEN:
{karten}
Prüfe:
1. Sind alle 7 Karten vollständig ausgefüllt (keine abgebrochenen oder leeren Inhalte, keine Platzhalter)?
2. Stimmen alle Aussagen mit der Faktenbasis überein? Nichts Erfundenes?
3. Ist jede Karte KOMPAKT — maximal 5 kurze Stichpunkte (je ~8 Wörter), keine Tabellen, Beispiel maximal ~12 Zeilen? Zu lange Karten sind ein Problem.
4. Ist jede Karte für sich verständlich? Ist das Beispiel konkret und plausibel — lauffähig wirkender Code bzw. realistische Sätze/realistisches Szenario?
Du PRÜFST nur und notierst Probleme — du änderst nichts. Nenne die betroffene Karte über ihren Schlüssel (info, eigenschaften, beispiel, zusammenhaenge, voraussetzungen, modern, veraltet).
Schreibe NUR die JSON-Datei nach: {out_path}
Format — alles in Ordnung:
{{"ok": true}}
Sonst (kurz und konkret, maximal 10 Punkte):
{{"probleme": ["beispiel: …", "fakten in info veraltet: …"]}}

View File

@@ -0,0 +1,18 @@
Mehrere Agenten haben die Subbausteine des Themas "{topic}" nach Relevanz bewertet (relevant/rand). Bei einigen sind sie sich uneinig. Entscheide für jeden strittigen Subbaustein die finale Relevanz.
STRITTIGE SUBBAUSTEINE (mit den abgegebenen Stimmen):
{strittig}
Relevanz:
- **relevant**: Kern-/Standardwissen, ein ernsthafter Lerner braucht es.
- **rand**: Randnotiz, Spezial-/Nischenfall, „nice to know" — nur für den Komplett-Guide.
Regeln:
- Wäge die Stimmen ab, entscheide fachlich. Im Zweifel **relevant**.
- Für JEDE strittige Nummer genau einen Wert.
Schreibe NUR die JSON-Datei nach: {out_path}
Format (kein weiterer Text):
{{"relevanz": {{"1": "relevant", "4": "rand"}}}}
{extra}

View File

@@ -0,0 +1,19 @@
Bewerte jeden Subbaustein des Themas "{topic}" nach Relevanz: relevant oder rand. Die Relevanz steuert, ob ein Punkt in die fokussierten Guides kommt oder nur in den Komplett-Guide.
SUBBAUSTEINE (Nummer in Klammern der zugehörige Baustein):
{subbausteine}
Relevanz:
- **relevant**: Kern- oder Standardwissen des Themas. Ein ernsthafter Lerner braucht es im normalen Gebrauch. Was man wirklich kennen sollte.
- **rand**: Randnotiz, Spezial-/Nischenfall, „nice to know", Deprecated/Exotik. Für die meisten Lerner verzichtbar — gehört nur in einen Komplett-Guide.
Regeln:
- Bewerte jeden Punkt einzeln, gemessen an SEINEM Baustein und am Thema.
- Im Zweifel **relevant** — lieber zu viel als versehentlich Wichtiges weglassen.
- Urteile nur — erfinde nichts dazu, ändere keine Subbausteine.
Schreibe NUR die JSON-Datei nach: {out_path}
Format (für JEDE Nummer genau ein Wert; kein weiterer Text):
{{"relevanz": {{"1": "relevant", "2": "rand", "3": "relevant"}}}}
{extra}

View File

@@ -0,0 +1,19 @@
Mehrere Agenten haben die Subbausteine des Themas "{topic}" nach Schwierigkeit eingestuft. Bei einigen sind sie sich uneinig. Entscheide für jeden strittigen Subbaustein die finale Stufe.
STRITTIGE SUBBAUSTEINE (mit den abgegebenen Stimmen):
{strittig}
Stufe = Lern-/Verständnisaufwand (nicht Seltenheit/Obskurität):
- **einfach**: Basis-Fakt/Definition, sofort verständlich. Auch Versions-Details, Pfade, Konstanten, einzelne Fakten.
- **mittel**: braucht Erklärung — mehrere Teile, Zusammenhang, typische Anwendung.
- **schwer**: komplexes Zusammenspiel mehrerer Konzepte, tieferes Verständnis nötig. NICHT „schwer" nur weil selten/neu/Edge-Case.
Regeln:
- Wäge die Stimmen ab, entscheide fachlich nach Lernaufwand. Im Zweifel die niedrigere Stufe.
- Für JEDE strittige Nummer genau eine Stufe.
Schreibe NUR die JSON-Datei nach: {out_path}
Format (kein weiterer Text):
{{"stufen": {{"1": "einfach", "4": "schwer"}}}}
{extra}

View File

@@ -0,0 +1,27 @@
Stufe jeden Subbaustein des Themas "{topic}" nach **Lern-/Verständnisaufwand** ein: einfach, mittel oder schwer. Die Stufe steuert, wann ein Lerner den Punkt sieht (Anfänger sehen nur „einfach", Fortgeschrittene alles).
SUBBAUSTEINE (nach Baustein gruppiert):
{subbausteine}
Was die Stufe misst — wie viel Aufwand, den Punkt zu VERSTEHEN und ANZUWENDEN:
- **einfach**: Basis-Fakt oder Definition. Sofort verständlich. Der Einstieg in den Baustein.
- **mittel**: braucht etwas Erklärung. Mehrere Teile, ein Zusammenhang, eine typische Anwendung.
- **schwer**: komplexes Zusammenspiel mehrerer Konzepte. Setzt tieferes Verständnis voraus, ist gedanklich anspruchsvoll.
WICHTIG — was NICHT die Stufe bestimmt:
- Seltenheit ist KEINE Schwierigkeit. Ein Nischen-Feature kann trotzdem einfach sein.
- Versions-Details (z.B. „seit 6.4.12.0"), Konstanten, Pfade, einzelne Fakten → meist **einfach**, nicht schwer.
- Edge-Cases/Gotchas, die nur eine simple Regel sind (z.B. „Name muss dem Ordner entsprechen") → **einfach/mittel**, nicht schwer.
- „schwer" NUR, wenn der Punkt selbst gedanklich schwer ist — nicht weil er obskur oder neu ist.
Regeln:
- Stufe RELATIV im jeweiligen Baustein: vergleiche die Punkte eines Bausteins miteinander.
- Die meisten Punkte sind einfach/mittel. „schwer" nur für die echten Brocken.
- Jeder Baustein hat mindestens einen „einfach"-Punkt (den Einstieg). Im Zweifel die niedrigere Stufe.
- Urteile nur — erfinde nichts dazu, ändere keine Subbausteine.
Schreibe NUR die JSON-Datei nach: {out_path}
Format (für JEDE Nummer genau eine Stufe; kein weiterer Text):
{{"stufen": {{"1": "einfach", "2": "mittel", "3": "schwer"}}}}
{extra}

View File

@@ -0,0 +1,19 @@
Unten je Baustein des Themas "{topic}" die gefundenen Subbausteine. Gib je Baustein die **bereinigte** finale Liste zurück.
{bausteine}
Regeln:
- **Dubletten zusammenführen:** Subbausteine, die denselben Punkt mit anderen Worten sagen, sind EINER. Behalte den klarsten, streiche die Umformulierungen. (z.B. „Broadcast Mode verfügbar" und „Broadcast-Modus aktivieren" → einer.)
- Behalte alle fachlich **DISTINKTEN** Teilpunkte vollständig — nichts Wesentliches weglassen.
- Verwirf, was zu fein granular, am Rand des Themas oder fachlich zweifelhaft ist.
- Jeder Punkt atomar (eine Aussage). Behaltene Punkte WÖRTLICH übernehmen, nicht umformulieren, nichts erfinden.
- Die Anzahl folgt der Schwierigkeit: lieber wenige distinkte als viele redundante Punkte.
Schreibe NUR die Datei {out_path} — pro Baustein ein baustein-Marker (Titel EXAKT wie oben), darunter die finale Subbaustein-Liste:
<!-- baustein: Exakter Baustein-Titel -->
- Subbaustein
- Subbaustein
Die Marker-Zeile exakt so schreiben. Jeder Baustein muss vorkommen. Kein Text außerhalb der Bausteine.
{extra}

View File

@@ -0,0 +1,29 @@
Zerlege jeden zugeteilten Baustein des Themas "{topic}" in seine SUBBAUSTEINE — die einzelnen lernbaren Teilpunkte, aus denen das Konzept besteht. Ein späterer Guide wird PRO Subbaustein einen kurzen Text schreiben.
Dir zugeteilt — verbindlich: jeder Baustein muss vorkommen, keine zusätzlichen erfinden:
{zuteilung}
Was ein Subbaustein ist:
- Ein einzelner Teilpunkt, den man am Baustein lernen muss. Eine Aussage, ein Aspekt, eine Tücke.
- Beispiele: Baustein `<img>``src` (Bildquelle), `alt` (Alternativtext), leeres `alt` für Deko-Bilder, `width`/`height` gegen Layout-Sprünge, Void-Element ohne Endtag. Baustein `<p>` → Textabsatz als Block, erlaubte Inline-Kinder, keine Verschachtelung.
ENTSCHEIDEND — die Anzahl folgt der Schwierigkeit:
- Ein einfacher, trivialer Baustein hat WENIGE Subbausteine (12). Blähe ihn NICHT auf.
- Ein komplexer, reichhaltiger Baustein hat VIELE Subbausteine. Lass nichts Wesentliches weg.
- Es gibt KEINE Zielzahl. `<img>` muss deutlich mehr Subbausteine haben als `<p>`.
Regeln:
- Jeder Subbaustein ist atomar: genau ein Teilpunkt. Keine zwei Aspekte in einen Punkt.
- Nur was DIESER Baustein hergibt (Scope). Nichts am Rand Erwähntes aufblasen.
- Belegt, nicht erfunden. Unsicheres per Websuche prüfen.
- Subbaustein-Titel auf DEUTSCH (Code-Bezeichner bleiben original), max. ~10 Wörter, eine Aussage.
Schreibe NUR die Datei {out_path} — pro Baustein ein baustein-Marker (Titel EXAKT aus der Zuteilung), darunter die Subbausteine als Liste:
<!-- baustein: Exakter Baustein-Titel -->
- Erster Subbaustein
- Zweiter Subbaustein
Die Marker-Zeile exakt so schreiben. Kein Text außerhalb der Bausteine.
{bekannt}
{extra}

4456
uni/aak/Skript.pdf Normal file

File diff suppressed because it is too large Load Diff

2562
uni/aak/Skript.txt Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,817 @@
CHRISTIAN-ALBRECHTS-UNIVERSITÄT ZU KIEL
Institut für Informatik, Arbeitsgruppe Algorithmen und Komplexität
Prof. Dr. K. Jansen, B. Schumacher, C. Wambsganz
09.06.2025
Hausaufgaben zur Vorlesung »Analyse von Algorithmen und
Komplexität«
Blatt 9
Definition 1 (Vertex Cover (VC)). Für einen (ungerichteten) Graphen G = (V, E) ist ein Vertex
Cover eine Menge von Knoten C ⊆ V , sodass u ∈ C oder v ∈ C für alle { u, v } ∈ E gilt, also ist
für alle Kanten mindestens ein Endpunkt in C.
Für das V ERTEX C OVER Problem ist neben einem Graphen G eine Zahl k ∈ N≥0 gegeben und
es wird gefragt, ob ein Vertex Cover mit Größe (Anzahl der Knoten) höchstens k in G existiert.
Definition 2 (Feedback Vertex Set (FVS)). Für einen gerichteten Graphen G = (V, E) ist ein
Feedback Vertex Set eine Menge von Knoten X ⊆ V , sodass G \ X kreisfrei ist.
Für das F EEDBACK V ERTEX S ET Problem ist neben einem Graphen G eine Zahl k ∈ N≥0 gege-
ben und es wird gefragt, ob ein Feedback Vertex Set mit Größe (Anzahl der Knoten) höchstens
k in G existiert.
1 6 2
5
8 7
3
4
Abbildung 1: Beispiel Graph.
Hausaufgabe 9.1 (Nichtdeterministischer Algorithmus für VC) (5 Punkte)
1. Finden Sie ein minimales Vertex Cover in Abbildung 1. Für ein Vertex Cover ist die
Orientierung der Kanten nicht relevant.
2. Zeigen Sie V ERTEX C OVER ∈ NP, indem Sie einen nichtdeterministischen Algorithmus
angeben, der V ERTEX C OVER in Polynomialzeit löst. Begründen Sie die Korrektheit und
die Laufzeit von Ihrem Algorithmus.
Lösung. 1. Ein minimales Vertex Cover ist zum Beispiel { 3, 5, 6, 7, 8 }.
2. Unser nichtdeterministischer Algorithmus geht wie folgt vor: Für jeden Knoten wird
nichtdeterministisch entschieden, ob dieser im Vertex Cover sein soll. Nachdem dies für
alle Knoten entschieden wurde, wird überprüft, ob nicht mehr als k Knoten gewählt wur-
den und ob für jede Kante mindestens ein Endpunkt im Vertex Cover liegt.
Existiert ein Vertex Cover im Graphen, existiert auch eine Reihe von nichtdeterministi-
schen Entscheidungen, die diese Teilmenge von Knoten auswählt. Diese Teilmenge wird
dann auch von dem Algorithmus akzeptiert. Damit ist der Algorithmus korrekt.
Beide diese Schritte können naiv in Polynomialzeit erledigt werden, also löst dieser Al-
gorithmus das Vertex Cover Problem in Polynomialzeit.
Bewertung 1 Punkt für das Beispiel und 4 Punkte für den Algorithmus, dabei 2 Punkte für
die Beschreibung und jeweils 1 Punkt für Korrektheit und Laufzeit.
Hausaufgabe 9.2 (Polynomieller Verifizierer für FVS) (5 Punkte)
1. Finden Sie ein minimales Feedback Vertex Set in Abbildung 1.
2. Zeigen Sie F EEDBACK V ERTEX S ET ∈ NP, indem Sie einen polynomiellen Verifizierer
für F EEDBACK V ERTEX S ET angeben. Begründen Sie die Korrektheit und die Laufzeit
von Ihrem Verifizierer.
Lösung. 1. Ein minimales FVS ist zum Beispiel { 5, 6 }.
2. Das Zertifikat hat die Form einer Teilmenge X ⊆ V , die beschreibt, welche Knoten das
Feedback Vertex Set bilden sollen. Zum Beispiel über ein Bit pro Knoten. Nun wird
überprüft, ob |X| ≤ k gilt und ob G \ X azyklisch ist.
Hat der Graph ein Feedback Vertex Set von Größe maximal k, gibt es ein Zertifikat, wel-
ches dieses beschreibt. Dieses Zertifikat wird dann von unserem Verifizierer akzeptiert,
damit ist der Verifizierer korrekt.
Die Größe von X zu überprüfen ist naiv in Polynomialzeit möglich. Für den zweiten Test
bietet sich der Algorithmus zur topologischen Sortierung aus der Vorlesung an, welcher in
Polynomialzeit eine topologische Sortierung findet, wenn der gegebene Graph azyklisch
ist. Insgesamt ist dieser Verifizierer korrekt und auch ein polynomieller Verifizierer.
Bewertung 1 Punkt für das Beispiel und 4 Punkte für den Verifizierer, dabei 2 Punkte für die
Beschreibung und jeweils 1 Punkt für Korrektheit und Laufzeit.
Abgabe: 16.06.2025, 10:00 in Moodle bitte beachten Sie die Informationen zum Abgabefor-
mat im Moodlekurs.
Hausaufgabe 9
Marek Lenczewski
Matrikelnummer: 1025252
22. Juni 2025
Hausaufgabe 9.1
Teil 1
Ein minimales Vertex Cover ist C = {2, 7, 5, 4, 1}.
Die Minimale Knotenanzahl ist 5.
Teil 2
Der Algorithmus arbeitet wie folgt:
• Zuerst wird für jeden Knoten v ∈ V nichtdeterministisch entschieden, ob dieser in das
Vertex Cover aufgenommen wird. Dabei wird drauf geachtet, dass die Anzahl k nicht
übersteigt.
• Danach wird geprüft, ob für jede Kante {u, v} ∈ E entweder v ∈ C oder u ∈ C oder beides
zutrifft.
• Falls dies für alle Kanten gilt, akzeptieren wir. Andernfalls lehnen wir ab.
Existiert ein Vertex Cover der Größe höchstens k, dann gibt es auch eine, nichtdeterminis-
tisch ausgewählte, Knotenmenge, die dazu passt. Von dieser werden alle Kanten abgedeckt und
der Algorithmus akzeptiert. Falls es kein Vertex Cover gibt, dann lehnt der Algorithmus ab.
Die Auswahl der Knoten und die Prüfung der Kanten läuft in polynomieller Zeit, also ist dies
ein polynomieller nichtdeterministischer Algorithmus, der VERTEX COVER löst und damit ist
V ERT EXCOV ER ∈ N P .
Hausaufgabe 9.2
Teil 1
Ein minimales Feedback Vertex Set ist X = {1, 5}
Teil 2
Der Verifizierer arbeitet wie folgt:
• Es wird eine Teilmenge X ⊆ V übergeben, die das Feedback Vertex Set darstellen soll.
• Dann wird geprüft, ob |X| ≤ k gilt und es wird abgelehnt, falls es nicht gilt.
• Danach wird ein Graph G = G \ X konstruiert, in dem alle Knoten aus X und alle an X
angrenzenden Kanten entfernt wurden.
1
• Auf dem Graphen G wird eine Tiefensuche durchgeführt, dabei werden Knoten während
der Bearbeitung grau markiert und beim Backtracking schwarz.
• Wird eine Kante zu einem grauen Knoten entdeckt, dann wird abgelehnt, da dies einen
Kreis anzeigt.
• Falls die Tiefensuche den gesamten Graphen durchläuft ohne abzulehnen, dann wird ak-
zeptiert.
Existiert ein Feedback Vertex Set der Größe höchstens k, dann gibt es auch ein Zertifikat, das
genau diese Knotenmenge beschreibt. Alle Teilmengen, die die Tiefensuche überstehen, werden
von dem Verifizierer akzeptiert und sonst abgelehnt.
Die Prüfung der übergebenen Teilmenge und die Konstruktion des Graphen laufen in polyno-
mieller Zeit. Ebenfalls die Tiefensuche läuft in polynomieller Zeit, somit ist auch der Verifizierer
polynomiell und verifiziert die Lösung für das FEEDBACK VERTEX SET in polynomieller
Zeit. Also gilt F EEDBACKV ERT EXSET ∈ N P .
2
CHRISTIAN-ALBRECHTS-UNIVERSITÄT ZU KIEL
Institut für Informatik, Arbeitsgruppe Algorithmen und Komplexität
Prof. Dr. K. Jansen, B. Schumacher, C. Wambsganz
16.06.2025
Hausaufgaben zur Vorlesung »Analyse von Algorithmen und
Komplexität«
Blatt 10
Definition 1 (Feedback Vertex Set (FVS)). Für einen gerichteten Graphen G = (V, E) ist ein
Feedback Vertex Set eine Menge von Knoten X ⊆ V , sodass G \ X kreisfrei ist.
Für das F EEDBACK V ERTEX S ET Problem ist neben einem Graphen G eine Zahl k ∈ N≥0 gege-
ben und es wird gefragt, ob ein Feedback Vertex Set mit Größe (Anzahl der Knoten) höchstens
k in G existiert.
Hausaufgabe 10.1 (F EEDBACK V ERTEX S ET ist NP-vollständig) (5 Punkte)
Zeigen Sie, dass F EEDBACK V ERTEX S ET NP-vollständig ist.
Hinweis: Reduzieren Sie V ERTEX C OVER auf F EEDBACK V ERTEX S ET. Wenn ein Vertex Cover
aus einem Graphen entfernt wird, hat dieser keine Kanten mehr.
Lösung. F EEDBACK V ERTEX S ET ∈ NP ist bekannt aus der letzten Hausaufgabe. Nun zeigen
wir eine Reduktion von V ERTEX C OVER auf F EEDBACK V ERTEX S ET. Betrachte die folgende
Reduktion:
Für eine gegebene V ERTEX C OVER Instanz (G = (V, E), k) erzeuge den Graphen G = (V, E )
mit E := { (u, v), (v, u) | {u, v} ∈ E }. Gebe (G , k) als F EEDBACK V ERTEX S ET Instanz aus.
Korrektheit:
⇒ (Falls (G, k) eine Ja-Instanz von V ERTEX C OVER ist, dann ist (G , k) eine Ja-Instanz von
F EEDBACK V ERTEX S ET.):
Sei C ⊆ V ein Vertex Cover mit Größe kleiner oder gleich k. Dann enthält G \C keine Kanten.
Damit hat G \C ebenfalls keine Kanten und somit keine Kreise.
⇐ (Falls (G , k) eine Ja-Instanz von F EEDBACK V ERTEX S ET ist, dann ist (G, k) eine Ja-Instanz
von V ERTEX C OVER.):
Sei X ⊆ V ein Feedback Vertex Set mit Größe kleiner oder gleich k. Dann ist X ein Vertex
Cover in G, da ansonsten eine Kante {u, v} ∈ E existiert, für die u ∈
/ X ∧v ∈
/ X gilt. Damit wäre
aber (u, v, u) ein Kreis in G und X somit kein Feedback Vertex Set.
Laufzeit: Das Erzeugen der Menge E geht in Zeit O(|E|). Insgesamt ist die Reduktion dem-
nach in Laufzeit O(|E|), also in polynomieller Laufzeit, durchführbar.
Also existiert eine polynomielle Reduktion von V ERTEX C OVER auf F EEDBACK V ERTEX S ET.
Da V ERTEX C OVER nach den aktuellen Präsenzaufgaben NP-vollständig ist und
F EEDBACK V ERTEX S ET ∈ NP gilt, ist somit F EEDBACK V ERTEX S ET NP-vollständig.
Bewertung 2 Punkte für die Konstruktion, 1 Punkt für den 1. Fall, 1 Punkt für den 2. Fall,
0.5 Punkte für die Laufzeit, 0.5 Punkte für die Beweisführung im Ganzen
Hausaufgabe 10.2 (Turingmaschinen) (5 Punkte)
Entwerfen Sie eine Turingmaschine für die Sprache L = { w ∈ Σ∗ | w ist ein Palindrom } über
einem gegebenen Alphabet Σ und geben Sie die Laufzeit Ihrer Turingmaschine an. Begründen
Sie dabei, warum Ihre Turingmaschine korrekt ist und die angegebene Laufzeit hat.
Lösung. Die Turingmaschine sieht folgendermaßen aus:
(1) Falls kein Wort auf dem Band steht, akzeptiere.
(2) Falls nur ein Buchstabe auf dem Band steht, akzeptiere.
(3) Lese den ersten Buchstaben und bewege danach den Kopf zum letzten Buchstaben. Falls
die beiden Buchstaben verschieden sind, verwerfe. Ansonsten ersetze beide Buchstaben
durch ein neues Symbol x. Ersetze dafür zuerst den letzten Buchstaben und bewege dann
den Kopf zum ersten Buchstaben.
(4) Gehe zu Schritt (1).
Korrektheit: Die Turingmaschine vergleicht immer den ersten und den letzten Buchstaben.
Wenn die Buchstaben nicht gleich sind, ist das Wort kein Palindrom und es wird verworfen.
Ansonsten werden die Buchstaben rausgestrichen (siehe Schritt (3)). Diese Prozedur wird so-
lange wiederholt, bis man in der Mitte des Wortes angekommen ist und entweder nur noch ein
Buchstabe oder kein Buchstabe vorhanden ist. In beiden Fällen ist das Wort ein Palindrom und
es wird akzeptiert (siehe Schritte (1) und (2)).
Laufzeit: Die Schritte (1) bis (3) benötigen jeweils eine Laufzeit von O(n), wobei n hier die
Länge der Eingabe bezeichnet. Da in jedem Durchlauf der Schritte (1) bis (3) zwei Buchstaben
raus gestrichen werden oder das Wort akzeptiert oder verworfen wird, gibt es maximal ⌊n/2⌋
Durchläufe, bis nur noch ein oder kein Buchstabe auf dem Band stehen und somit in den Schrit-
ten (1) oder (2) akzeptiert wird. Damit ergibt sich insgesamt eine Laufzeit von O(n2 ).
Bewertung 2 Punkte für die Turingmaschine, 1 Punkt für die Korrektheit und 2 Punkte für
die Laufzeit: 1 Punkt Angabe, 1 Punkt Begründung
Abgabe: 23.06.2025, 10:00 in Moodle bitte beachten Sie die Informationen zum Abgabefor-
mat im Moodlekurs.
Hausaufgabe 10
Marek Lenczewski
Matrikelnummer: 1025252
22. Juni 2025
Hausaufgabe 10.1
Teil 1: FeedbackVertexSet ∈ NP
Zuerst brauchen wir einen polynomiellen Verifizierer, um zu zeigen dass FeedbackVertexSet
∈ NP.
Der Verifizierer bekommt einen Graphen G und eine Teilmenge X, dieser arbeitet wie folgt:
• Prüfe, ob |X| ≤ k, sonst ablehnen
• Den Graphen G = G \ X konstruieren
• Tiefensuche auf G durchführen, dabei werden Knoten vom aktuellen Pfad als grau mar-
kiert und nach vollständiger Bearbeitung als schwarz. Wird eine Kante zu einem grauen
Knoten gefunden, zeigt dies einen Kreis an und wir lehnen ab
• Akzeptieren, wenn die Tiefensuche nichts findet
Die Schritte 1-4 laufen in polynomieller Zeit, somit läuft der Verifizierer in polynomieller
Zeit. Damit ist F eedbackV ertexSet ∈ N P .
Teil 2: FeedbackVertexSet ist NP-schwer
Jetzt brauchen wir eine Reduktion von VertexCover auf FeedbackVertexSet, um zu zeigen,
dass F eedbackV ertexSet N P Schwer ist.
Konstruktion: Sei dafür ein VertexCover gegeben mit einem Graphen G und einer Zahl k.
Aus G wird ein G , wobei jede ungerichtete Kante {u, v} ∈ E durch zwei gerichteten Kanten
(u, v) und (v, u) zu E ersetzt wird. Das k wird einfach übernommen k = k .
Beweis VertexCover nach FeedbackVertexSet:
• Sei C ein VertexCover von G mit |C| ≤ k
• Da C ein VertexCover ist, wird jede Kante mit C verbunden
• Wenn C entfernt wird, dann hat G \ C keine Kanten mehr
• Dann hat auch G \ C keine Kanten und ist somit Kreisfrei
• Damit ist C ein FeedbackVertexSet für G mit |C| ≤ k
Beweis FeedbackVertexSet nach VertexCover:
1
• Sei X ein FeedbackVertexSet von G mit |X| ≤ k
• Dann ist G \ X kreisfrei
• Angenommen X wäre kein VertexCover von G, dann gäbe es eine Kante {u, v} ∈ E mit
u, v ∈
/X
• Somit wären u, v noch in G \ X vorhanden
• Die Kanten (u, v) und (v, u) würden, durch die Konstruktion, einen Kreis erzeugen
• Das ist ein Widerspruch, somit ist G \ X kreisfrei und X ist ein VertexCover
Da F eedbackV ertexSet ∈ N P und N P schwer ist, folgt dass F eedbackV ertexSet NP-
Vollständig ist. □
Hausaufgabe 10.2
Lösung. Die Turingmaschine arbeitet wie folgt:
1. Falls das Band leer ist oder nur aus Markierungen besteht, akzeptiere
2. Merke das erste unmarkierte Zeichen und ersetze es durch X
3. Laufe zum letzten unmarkierten Zeichen oder verwefe
4. Falls dieses Zeichen nicht mit dem gemerkten übereinstimmt, verwerfe
5. Ersetze es durch X und gehe zurück zum Anfang
6. Gehe zu 1
Korrektheit: Ein Wort ist genau dann ein Palindrom, wenn es sich von vorne und hin-
ten gleich liest. Dies prüft die Turingmaschine, indem sie Schrittweise die äußeren Buchstarben
verifiziert, bis nur noch einer oder keiner Vorhanden ist, dann akzeptiert ist und wenn eine Un-
stimmigkeit auftritt, dann lehnt sie ab.
Laufzeit: Die Turingmaschine hat eine Laufzeit von O(n2 ). In jedem Durchlauf macht sie
O(n) Schritte und das mit O(n) Durchläufen. □
2
CHRISTIAN-ALBRECHTS-UNIVERSITÄT ZU KIEL
Institut für Informatik, Arbeitsgruppe Algorithmen und Komplexität
Prof. Dr. K. Jansen, B. Schumacher, C. Wambsganz
23.06.2025
Hausaufgaben zur Vorlesung »Analyse von Algorithmen und
Komplexität«
Blatt 11
Definition 1 (Dreiecksüberdeckung eines Graphen (∆ C OVER)). Gegeben ist ein endlicher un-
gerichteter Graph G = (V, E) und eine Zahl k ∈ N. Dieser Graph hat eine Dreiecksüberdeckung,
wenn eine Menge von Knoten C∆ ⊆ V mit |C∆ | ≤ k existiert, so dass zu jedem Teilgraphen (Drei-
eck) D = (VD , ED ) ⊆ (V, E) mit |VD | = 3 und ED = { {v1 , v2 } ∈ VD ×VD | v1 ̸= v2 } von G gilt,
v ∈ C∆ für mindestens ein v ∈ VD .
Hausaufgabe 11.1 (10 Punkte)
(a) (1 Punkt) Markieren Sie im folgenden Graphen (Abbildung 1 (a)) ein V ERTEX C OVER
der Größe 5.
(b) (1 Punkt) Markieren Sie im folgenden Graphen (Abbildung 1 (b)) eine Dreiecksüberde-
ckung der Größe 5.
(c) (8 Punkte) Zeigen Sie, dass ∆ C OVER NP-vollständig ist.
(a) (b)
Abbildung 1: Beispielgraph
Lösung.
(a) (b)
Abbildung 2: Beispielgraph Lösung
(c) Wir zeigen zunächst, ∆ C OVER ∈ NP mittels eines polynomiellen Verifizierers.
Der polynomielle Verifizierer erhält als Zertifikat eine Menge von Knoten C∆ ⊆ V . Zunächst
überprüft er, ob |C∆ | ≤ k. Falls dies nicht der Fall ist, verwerfe die Instanz. Ansonsten überprüfe
für alle Teilgraphen D = (VD , ED ) ⊆ (V, E) mit |VD | = 3 und ED = { {v1 , v2 } ∈ VD ×VD | v1 ̸= v2 }
von G, ob für mindestens ein v ∈ VD auch v ∈ C∆ gilt. Falls dies gilt, akzeptiere. Ansonsten ver-
werfe.
Korrektheit: Der Verifizierer überprüft die geforderten Eigenschaften. Falls ein Graph eine Drei-
ecksüberdeckung besitzt, so existiert ein entsprechendes Zertifikat C∆ und der Verifizierer ak-
zeptiert die Instanz. Falls ein Graph keine Dreiecksüberdeckung besitzt, exisitert kein valides
entsprechendes Zertifikat C∆ und der Verifizierer verwirft die Instanz.
Laufzeit: Es gibt maximal |V |3 verschiedene Teilmengen, die ein Dreieck bilden könnten und
daher überprüft werden müssen. Die Überprüfung geht in O(|V |) Zeit. Da alle anderen Schritte
des Verifizierers in O(1) Zeit erledigt werden können, ergibt sich somit insgesamt eine Laufzeit
von O(|V |4 ), was einer polynomiellen Laufzeit entspricht.
Also gilt ∆ C OVER ∈ NP.
Nun reduzieren wir V ERTEX C OVER auf ∆ C OVER. Betrachte folgende Reduktion:
Sei I = (G = (V, E), k) eine Instanz von V ERTEX C OVER. Bilde den Graphen G = (V , E ) mit
V = V { ve | e ∈ E } und E = E { {ve , e1 }, {ve , e2 } | e = {e1 , e2 } ∈ E }. Gebe I = (G , k)
als ∆ C OVER Instanz aus.
Korrektheit:
⇒ (Falls (G, k) eine Ja-Instanz von V ERTEX C OVER ist, dann ist (G , k) eine Ja-Instanz von ∆
C OVER.):
Sei C ein Vertex Cover mit Größe kleiner oder gleich k von G. Dann ist C ebenfalls eine Drei-
ecksüberdeckung mit Größe kleiner oder gleich k. Offensichtlich gilt |C| ≤ k. Da C ein Vertex
Cover von G ist, gilt für jede Kante {u, v} ∈ E, dass u ∈ C oder v ∈ C. Für alle Teilgraphen
D = (VD , ED ) ⊆ (V, E) mit |VD | = 3 und ED = { {v1 , v2 } ∈ VD ×VD | v1 ̸= v2 } von G gilt damit
direkt, dass mindestens ein v ∈ VD auch in C enthalten ist. Der Graph G besitzt neben allen
solchen Teilgraphen in G zusätzlich solche Teilgraphen (Dreiecke), die durch die Knotenmenge
{ ve | e ∈ E } und die entsprechenden Kanten { e, {ve , e1 }, {ve , e2 } | e = {e1 , e2 } ∈ E } entste-
hen. Für diese Dreiecke gilt aber auch, dass mindestens ein Knoten, nämlich e1 oder e2 , in C
enthalten sein muss, da C ein Vertex Cover ist.
⇐ (Falls (G , k) eine Ja-Instanz von ∆ C OVER ist, dann ist (G, k) eine Ja-Instanz von V ERTEX -
C OVER.):
Sei C∆ eine Dreiecksüberdeckung von G der Größe |C∆ | ≤ k. Ohne Beschränkung der All-
gemeinheit können wir davon ausgehen, dass C∆ ⊆ V gilt: Falls für einen Knoten ve ∈ C∆ gilt,
können wir e1 zu C∆ hinzufügen und ve aus C∆ löschen. Dadurch bleibt die Überdeckungseigen-
schaft erhalten und die Menge wird höchstens kleiner. Nun gilt also C∆ ⊆ V . Die Menge C∆ ist
auch ein Vertex Cover mit Größe kleiner oder gleich k von G. Offensichtlich gilt |C∆ | ≤ k. Da für
alle Teilgraphen D = (VD , ED ) ⊆ (V , E ) mit |VD | = 3 und ED = { {v1 , v2 } ∈ VD ×VD | v1 ̸= v2 }
von G gilt, dass mindestens ein Knoten v ∈ VD ebenfalls in C∆ enthalten sein muss, gilt dies
auch für die Dreiecke {ve , e1 , e2 }. Da außerdem C∆ ⊆ V gilt, gilt e1 ∈ C∆ oder e2 ∈ C∆ für alle
Kanten e ∈ E.
Laufzeit: Das Hinzufügen von einem neuen Knoten und zwei neuen Kanten pro Kante geht in
O(|E|). Damit geht die gesamte Reduktion in O(|E|), also in polynomieller Zeit.
Also existiert eine polynomielle Reduktion von V ERTEX C OVER auf ∆ C OVER. Da V ERTEX -
C OVER NP-vollständig aus den Präsenzaufgaben bekannt ist und ∆ C OVER ∈ NP gilt, ist somit
∆ C OVER NP-vollständig.
Bewertung (a) und (b) jeweils 1 Punkt, (c) gibt 8 Punkte: 1 Punkt Angabe Verifizierer/NDTM,
0,5 Punkte Korrektheit Verifizierer/NDTM, 0,5 Punkte Laufzeit Verifizierer/NDTM, 1 Punkt
Angabe Reduktion, 3 Punkte Korrektheit (je Richtung 1,5 Punkte), 1 Punkt Laufzeit Reduktion,
1 Punkt Beweisführung
Abgabe: 30.06.2025, 10:00 in Moodle bitte beachten Sie die Informationen zum Abgabefor-
mat im Moodlekurs.
Hausaufgabe 11
Marek Lenczewski
Matrikelnummer: 1025252
28. Juni 2025
Hausaufgabe 11.1
a
1
2 3
4 5
6 10
7 9
8
b
a b c
d e
f g
h i j
k l m n
o p q
r s
t u v
c
Teil 1: ∆-Cover ∈ NP
1
Zuerst brauchen wir einen polynomiellen Verifizierer, um zu zeigen, dass ∆-Cover ∈ NP.
Der Verifizierer bekommt einen Graphen G = (V, E), Zahl k und ein Zertifikat C∆ ⊆ V und
arbeitet wie folgt:
1. Prüfe, ob |C∆ | ≤ k
2. Prüfe alle Knotentripel {a, b, c} ⊆ V , ob sie ein Dreieck bilden (also ob {a, b}, {b, c} und
{a, c} ∈ E zutrifft) und ob a, b oder c ∈ C∆
3. Ablehnen, falls ein Dreieck den test nicht besteht, sonst akzeptieren
Die Schritte 1 und 3 laufen in O(1) und Schritt zwei läuft in O(|V |3 ), das ergibt eine poly-
nomielle Laufzeit. Damit ist ∆-Cover ∈ NP.
Teil 2: ∆-Cover ist NP-schwer
Jetzt brauchen wir eine Reduktion von VertexCover auf ∆-Cover, um zu zeigen, dass
∆-Cover NP-schwer ist.
Konstruktion: Sei dafür ein VertexCover gegeben mit einem Graphen G = (V, E) und
einer Zahl k. Aus G wird G = (V , E ), wobei für jede Kante e = {u, v} ∈ E ein neuer Kno-
ten we ∈ V hinzugefügt wird und mit beiden Knoten u, v verbunden wird, sodass die Kanten
{v, we } und {u, we } zu E dazu kommen. Das k = k wird übernommen. So werden aus allen
Kanten Dreiecke.
Beweis VertexCover nach ∆-Cover
• Sei C ein VertexCover für G mit |C| ≤ k
• Zu zeigen sei, dass C auch ein ∆-Cover für G mit |C| ≤ k ist
• Da C ein VertexCover ist, gilt u ∈ C oder v ∈ C
• Jedes Dreieck in G hat die Form {u, v, we } für eine Kante e = {u, v} ∈ E
• Somit hat jedes Dreieck mindestens einen Knoten in C
• Also ist C ein ∆-Cover für G
Beweis ∆-Cover nach VertexCover
• Sei C∆ ein ∆-Cover für G mit |C∆ | ≤ k
• Jede Kante e = {u, v} ∈ E bildet durch {u, v, we } ein Dreieck in G
• Also gilt u, v oder we ∈ C∆ für alle Kanten in G
• Falls we ∈ C∆ , dann kann man we durch u oder v tauschen ohne die Eigenschaft von
∆-Cover zu verletzen, sodass u oder v ∈ C∆ für alle Kanten in G gilt
• Da sich die Knoten aus C∆ nur vertauschen gilt weiter |C| = |C∆ | ≤ k
• Weiter sind in C∆ nur noch die Knoten aus V , somit gilt u ∈ C oder v ∈ C für alle Kanten
E
• Also ist C ein VertexCover für G
Da ∆-Cover ∈ NP und NP-schwer ist, folgt dass ∆-Cover NP-vollständig ist. □
2
CHRISTIAN-ALBRECHTS-UNIVERSITÄT ZU KIEL
Institut für Informatik, Arbeitsgruppe Algorithmen und Komplexität
Prof. Dr. K. Jansen, B. Schumacher, C. Wambsganz
30.06.2025
Hausaufgaben zur Vorlesung »Analyse von Algorithmen und
Komplexität«
Blatt 12
Hausaufgabe 12.1 (Lower Bound: V ERTEX C OVER) (5 Punkte)
Betrachten Sie die Reduktion aus der Musterlösung zu Präsenzserie 10 von C LIQUE auf V ER -
TEX C OVER .
1. Geben Sie die Anzahl der Knoten, die Anzahl der Kanten und den Wert k für die re-
sultierende V ERTEX C OVER Instanz in Abhängigkeit von der originalen C LIQUE Instanz
an.
2. Zeigen Sie Lower Bounds für V ERTEX C OVER bezüglich Anzahl der Knoten, Anzahl
der Kanten und k basierend auf der ETH und der Reduktion aus der Musterlösung zu
Präsenzserie 10.
Lösung. 1. Sei I = (G = (V, E), k) eine Instanz von C LIQUE und I = (G = (V , E ), k ) die
aus der Reduktion resultierende Instanz von V ERTEX C OVER. Es gilt |V | = |V |, |E | =
1 2
2 |V |(|V | 1) |E| ≤ |V | und k = |V | k ≤ |V |.
2. • Angenommen es gäbe einen Algorithmus, der V ERTEX C OVER in 2o(|V |) · |I|O(1)
löst. Durch die Kombination der Reduktion von C LIQUE auf V ERTEX C OVER und
diesem Algorithmus, existiert dann auch ein 2o(|V |) ·|I|O(1) Algorithmus für C LIQUE,
da |V | = |V | nach Aufgabenteil 1. Aus der Musterlösung der Präsenzserie 12 folgt
somit, dass die ETH falsch ist.
Damit kann es so einen Algorithmus für V ERTEX C OVER nur geben, wenn die ETH
falsch ist.
• Angenommen es gäbe einen Algorithmus, der V ERTEX C OVER in 2o( |E |) · |I|O(1)
löst. Durch die Kombination der Reduktion von C LIQUE auf V ERTEX C OVER und
diesem Algorithmus, existiert dann auch ein 2o(|V |) ·|I|O(1) Algorithmus für C LIQUE,
da |E | ≤ |V |2 nach Aufgabenteil 1. Aus der Musterlösung der Präsenzserie 12 folgt
somit, dass die ETH falsch ist.
Damit kann es so einen Algorithmus für V ERTEX C OVER nur geben, wenn die ETH
falsch ist.
• Angenommen es gäbe einen Algorithmus, der V ERTEX C OVER in 2o(k ) · |I|O(1) löst.
Durch die Kombination der Reduktion von C LIQUE auf V ERTEX C OVER und die-
sem Algorithmus, existiert dann auch ein 2o(|V |) · |I|O(1) Algorithmus für C LIQUE,
da k ≤ |V | nach Aufgabenteil 1. Aus der Musterlösung der Präsenzserie 12 folgt
somit, dass die ETH falsch ist.
Damit kann es so einen Algorithmus für V ERTEX C OVER nur geben, wenn die ETH
falsch ist.
Bewertung 1 Punkt für den ersten Teil und 1.5 + 1.5 + 1 für den zweiten.
Definition 1 (H ITTING S ET). Bei diesem Problem ist ein Universum U, Teilmengen des Uni-
versums F1 , . . . , Fr ⊆ U und eine Zahl k ∈ N≥0 gegeben. Gibt es eine Menge S ⊆ U mit |U| ≤ k
und S ∩ Fi ̸= 0/ für alle i ∈ [r]?
Anschaulicher wird bei diesem Problem nach maximal k Elementen aus dem Universum ge-
fragt, sodass diese Elemente alle gegebenen Mengen Fi „treffen“.
Hausaufgabe 12.2 (Lower Bound: H ITTING S ET) (5 Punkte)
Betrachten Sie folgende Reduktion von 3-SAT auf H ITTING S ET.
Für eine gegebene 3-SAT Instanz I mit n Variablen x1 , . . . , xn und m Klauseln C1 , . . . ,Cm : Als
Universum wählen wir U = { x1 , x̄1 , . . . , xn , x̄n }. Für jede Variable xi fügen wir die Menge Fi :=
{ xi , x̄i } hinzu. Für jede Klausel C j fügen wir die Menge Fn+ j := C j hinzu. Wir setzen k := n
und erhalten damit eine H ITTING S ET Instanz I.
1. Geben Sie die Größe des Universums, die Anzahl der Mengen und die Zahl k aus der
resultierenden H ITTING S ET Instanz in Abhängigkeit von der Anzahl der Variablen n und
der Anzahl der Klauseln m aus der originalen 3-SAT Instanz an.
2. Beweisen Sie Lower Bounds für H ITTING S ET bezüglich Universumsgröße, Anzahl der
Mengen und der Zahl k basierend auf der obigen Reduktion und der ETH.
Lösung. 1. Für die Universumsgröße gilt |U| = 2n, für die Anzahl der Mengen gilt r =
n + m und es gilt k = n.
2. • Angenommen es gäbe einen Algorithmus, der H ITTING S ET in 2o(|U|) · |I|O(1) löst.
Durch die Kombination der Reduktion von 3-SAT auf H ITTING S ET und diesem
Algorithmus, existiert dann auch ein 2o(n) · |I|O(1) Algorithmus für 3-SAT, da |U| =
2n nach Aufgabenteil 1. Damit ist die ETH falsch.
Damit kann es so einen Algorithmus für H ITTING S ET nur geben, wenn die ETH
falsch ist.
• Angenommen es gäbe einen Algorithmus, der H ITTING S ET in 2o(r) · |I|O(1) löst.
Durch die Kombination der Reduktion von 3-SAT auf H ITTING S ET und diesem
Algorithmus, existiert dann auch ein 2o(m) · |I|O(1) Algorithmus für 3-SAT, da r =
n + m nach Aufgabenteil 1 und n ≤ 3m für 3-SAT gilt. Aus dem Sparsification
Lemma folgt damit, dass die ETH falsch ist.
Damit kann es so einen Algorithmus für H ITTING S ET nur geben, wenn die ETH
falsch ist.
• Angenommen es gäbe einen Algorithmus, der H ITTING S ET in 2o(k) · |I|O(1) löst.
Durch die Kombination der Reduktion von 3-SAT auf H ITTING S ET und diesem
Algorithmus, existiert dann auch ein 2o(n) · |I|O(1) Algorithmus für 3-SAT, da k = n
nach Aufgabenteil 1. Damit ist die ETH falsch.
Damit kann es so einen Algorithmus für H ITTING S ET nur geben, wenn die ETH
falsch ist.
Bewertung 1 Punkt für den ersten Teil und 1.5 + 1.5 + 1 für den zweiten.
Abgabe: 07.07.2025, 10:00 in Moodle bitte beachten Sie die Informationen zum Abgabefor-
mat im Moodlekurs.
Hausaufgabe 12
Marek Lenczewski
Matrikelnummer: 1025252
6. Juli 2025
Hausaufgabe 12.1.1
Die Knotenzahl bleibt gleich: |V | = |V | = n
Die Kantenzahl entspricht den nicht vorhandenen Kanten: |E | = |E|
k entspricht allen Knoten, die vorher nicht in der Clique waren: k = n k
Hausaufgabe 12.1.2
Wir wissen, dass 3-SAT laut ETH mindestens 2Ω(n) braucht, wobei n für die Anzahl der Varia-
blen steht.
In der Vorlesung wurde eine Reduktion von 3-SAT auf Clique gezeigt. In der Präsenzaufgabe
10.2 wurde eine Reduktion von Clique auf VertexCover gezeigt. Also gibt es eine Reduktion von
3-SAT zu VertexCover. Somit kann VertexCover nicht schneller lösbar sein als 3-SAT, weil sonst
3-SAT schneller werden würde, was nach ETH nicht geht.
So ergeben sich die folgenden Lower Bounds:
• Bezüglich Anzahl der Knoten: 2Ω(n) , wobei n = |V |. Die Knotenzahl verändert sich
nicht bei den Reduktionen.
• Bezüglich Anzahl der Kanten: 2Ω( m) , wobei m = |E |. Die Kanten können bei den
Reduktionen zu einem quadratischen Wachstum der Kanten führen.
• Bezüglich k : 2Ω(k ) . Bei der Reduktion gilt k = n k, wobei k linear von n abhängt.
1
Hausaufgabe 12.2.1
• Größe des Universums: |U | = 2n. Für jede der n Variablen gibt es zwei Elemente (xi
und xi )
• Anzahl der Mengen: n + m. n sind die Variablenmengen und m die Klauselmengen. (Fi
und Fn+j )
• Parameter: k = n. Wird in der Reduktion so übernommen.
Hausaufgabe 12.2.2
Wir wissen, dass 3-SAT laut ETH mindestens 2Ω(n) braucht, wobei n für die Anzahl der Varia-
blen steht. Somit kann HittingSet nicht schneller sein.
Es ergeben sich die folgenden Lower Bounds für HittingSet:
• Bezüglich Größe des Universums: 2Ω(|U |/2) , da für jede Variable zwei Elemente ins
Universum aufgenommen werden
• Bezüglich Anzahl der Mengen: 2Ω(n) , wobei die Anzahl der Variablen- und Klausel-
mengen (m + n) mindestens n ist
• Bezüglich Zahl k: 2Ω(k) , da in der Reduktion k = n gilt.
2
CHRISTIAN-ALBRECHTS-UNIVERSITÄT ZU KIEL
Institut für Informatik, Arbeitsgruppe Algorithmen und Komplexität
Prof. Dr. K. Jansen, B. Schumacher, C. Wambsganz
07.07.2025
Hausaufgaben zur Vorlesung »Analyse von Algorithmen und
Komplexität«
Blatt 13
Definition 1 (M AX -3-S AT). Bei dem Problem M AX -3-S AT ist eine Formel φ in konjunktiver
Normalform gegeben, wobei jede Klausel 3 Literale enthält. Gesucht ist eine Belegung β der
Variablen, die die Anzahl v(β ) der erfüllten Klauseln maximiert.
Hausaufgabe 13.1 (M AX -3-S AT) (10 Punkte)
Betrachten Sie folgenden Algorithmus A:
• Sei β0 die Belegung, die alle Variablen auf false setzt.
• Sei β1 die Belegung, die alle Variablen auf true setzt.
• Falls v(β0 ) ≥ v(β1 ), gib β0 zurück.
• Sonst gib β1 zurück.
(a) (6 Punkte) Zeigen Sie, dass obiger Algorithmus Güte 2 hat, d.h. v(A(φ )) ≥ 12 v(OPT (φ ))
gilt für alle Eingaben φ .
(b) (4 Punkte) Geben Sie eine Formel an, bei der der Algorithmus eine Belegung ausgibt, die
genau die Hälfte der maximal erfüllbaren Klauseln erfüllt. Begründen Sie kurz, warum
Ihre Formel geeignet ist.
Hinweis: Sie brauchen nur höchstens zwei Klauseln und vier Variablen.
Lösung. (a) Für jede Klausel C gilt, dass sie von mindestens einer der Belegungen β0 und
β1 erfüllt wird. Wir zeigen nun per Widerspruch, dass der obige Algorithmus eine Güte
von 2 hat:
Angenommen, es gilt v(A(φ )) < 12 v(OPT (φ )) für eine Formel φ . Sei m die Anzahl an
Klauseln in φ . Dann gilt v(OPT (φ )) ≤ m. Da v(A(φ )) < 12 v(OPT (φ )) ≤ 12 m, gilt v(β0 ) <
1 1
2 m und v(β1 ) < 2 m. Da jede Klausel C allerdings von mindestens einer der Belegungen
β0 und β1 erfüllt wird, gilt v(β0 ) ≥ 12 m oder v(β1 ) ≥ 12 m. Dies ist ein Widerspruch.
Also gilt v(A(φ )) ≥ 21 v(OPT (φ )) für alle Eingaben φ und obiger Algorithmus hat Güte
2.
(b) Betrachte folgende Formel: φ = (x1 x2 x3 )∧(¬x0 ¬x2 ¬x3 ) Mit β = {x0 → f alse, x1 →
true, x2 → true, x3 → true} gilt v(β ) = 2. Da φ nur zwei Klauseln hat, gilt somit v(OPT (φ )) =
2. Es gilt allerdings v(β0 ) = 1, da hier nur die zweite Klausel erfüllt ist, und v(β1 ) = 1,
da hier nur die erste Klausel erfüllt ist. Somit gilt v(A(φ )) = 1 = 12 · 2 = 21 v(OPT (φ )).
Bewertung (a) 2 Punkte korrekte Beweisführung im Allgemeinen, 2 Punkte Abschätzung
Algorithmus, 2 Punkte Abschätzung Optimum; (b) 2 Punkte für Angabe einer korrekten Formel,
1 Punkt Ausgabe Algorithmus, 1 Punkt Optimum
Abgabe: 14.07.2025, 10:00 in Moodle bitte beachten Sie die Informationen zum Abgabefor-
mat im Moodlekurs.
Hausaufgabe 13
Marek Lenczewski
Matrikelnummer: 1025252
13. Juli 2025
Hausaufgabe 13.1.a
Zu zeigen ist, dass der Algorithmus die Güte 2 hat, also die Lösung vom Algorithmus mindestens
halb so gut ist wie die optimale Lösung. Also ν(A(ϕ)) ≥ 21 ν(OP T (ϕ)) für alle Eingaben ϕ.
Sei ϕ eine beliebige 3-SAT-Formel mit m Klauseln C1 , C2 , . . . , Cm , wobei jede Klausel genau
3 Literale enthält.
Jede Klausel Ci ist wahr, wenn β0 wahr ist oder wenn β1 wahr ist oder beides. Da die Varia-
blen in Ci nur positiv oder negativ sein können, muss dabei mindestens β0 oder β1 wahr werden.
Der Algorithmus wählt die Belegung β0 oder β1 aus, die die meisten Klauseln Ci erfüllt.
Somit ist die Anzahl der ausgewählten Klauseln Ci mindestens m2 , da eine bei jeder Klausel
mindestens eine Belegung wahr werden muss und somit hat mindestens eine Belegung mindes-
tens m
2 wahre Klauseln.
Sei ν0 = Anzahl der von β0 erfüllten Klauseln und ν1 = Anzahl der von β1 erfüllten Klauseln.
So ergibt sich ν(A(ϕ)) = max{ν0 , ν1 } ≥ ν0 +ν
2
1
≥m
2.
Da die optimale Lösung höchstens alle m Klauseln erfüllen kann, gilt ν(OP T (ϕ)) ≤ m.
ν(OP T (ϕ))
Daraus folgt: ν(A(ϕ)) ≥ m
2 ≥ 2 .
Somit hat der Algorithmus eine Güte von 2.
1
Hausaufgabe 13.1.b
Der Algorithmus erfüllt genau die Hälfte der maximal erfüllbaren Klauseln, wenn jede Klausel
nur aus positiven oder negativen Variablen besteht und sich diese genau gleich aufteilen.
Formel: ϕ = (x1 x2 x3 ) ∧ (¬x1 ¬x2 ¬x3 )
Unter β0 (alle false):
• C1 = (x1 x2 x3 ) = (false false false) = false
• C2 = (¬x1 ¬x2 ¬x3 ) = (true true true) = true
ν(β0 ) = 1
Unter β1 (alle true):
• C1 = (x1 x2 x3 ) = (true true true) = true
• C2 = (¬x1 ¬x2 ¬x3 ) = (false false false) = false
ν(β1 ) = 1
Der Algorithmus wählt ν(A(ϕ)) = max{ν(β0 ), ν(β1 )} = max{1, 1} = 1.
Eine optimale Lösung wäre zum Beispiel x1 = true, x2 = false, x3 = false.
• C1 = (true false false) = true
• C2 = (false true true) = true
Daraus ergibt sich ν(OP T (ϕ)) = 2, also ν(A(ϕ)) = ν(OP2T (ϕ)) . Somit hat es die Güte 2.
2

5320
uni/aak/alle_klausuren.pdf Normal file

File diff suppressed because it is too large Load Diff

863
uni/aak/alle_klausuren.txt Normal file
View File

@@ -0,0 +1,863 @@
CHRISTIAN-ALBRECHTS-UNIVERSITÄT ZU KIEL
Institut für Informatik, Arbeitsgruppe Algorithmen und Komplexität
Prof. Dr. K. Jansen
19. Juli 2024
Modulprüfung zur Vorlesung
»Analyse von Algorithmen und Komplexität (Komplexitätsteil)«
SS 2024
Name: Matrikel-Nr.:
Hinweise:
• Bearbeiten Sie alle Aufgaben.
• Sie haben 10 Minuten Einlesezeit. Danach haben Sie 120 Minuten Zeit für die Klausurbearbeitung.
• Erlaubte Hilfsmittel: Ein doppelseitig handschriftlich beschriebenes Blatt Papier, (bunte) Stifte (kein
Rot, kein Grün, kein Bleistift).
• Einsichtnahme: 18.08.2024.
Note 4,0 3,7 3,3 3,0 2,7 2,3 2,0 1,7 1,3 1,0
Mindestpunkzahl 17,5 19,5 21,5 23,5 25 27 29 31 33 35
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 1 von 12
Aufgabe 1 KOMPLEXITÄT: A NWENDUNG LPT (4+2+2+2 Punkte)
Gegeben ist folgende Scheduling Instanz:
Jj J1 J2 J3 J4 J5 J6 J7
m=3
pj 5 3 7 8 1 4 2
a) Wenden Sie den LPT-Scheduling Algorithmus auf die obige Instanz an. Geben Sie dafür alle
relevanten Zwischenschritte an, sowie die Reihenfolge in der die Jobs auf Maschinen platziert
werden. Stellen Sie den resultierende Schedule graphisch dar. Eine beispielhafte Darstellung
ist:
J3
J6
J2
J5 J7
J1
J4
m1 m2 m3
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 2 von 12
b) Geben Sie die approximative Güte des Algorithmus LPT an.
c) Geben Sie eine Instanz für m = 2 Maschinen an, bei denen der Algorithmus ListScheduling
eine Güte von 2 1/m erzielt. Geben Sie zusätzlich an, welche Werte die optimale Lösung
OPT und die Lösung von LPT für diese Instanz erzielen.
d) Geben Sie eine Instanz für m = 3 Maschinen an, bei denen der Algorithmus ListScheduling
eine Güte von 2 1/m erzielt. Geben Sie zusätzlich an, welche Werte die optimale Lösung
OPT und die Lösung von LPT für diese Instanz erzielen.
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 3 von 12
Aufgabe 2 KOMPLEXITÄT: B EWEIS : K -C LIQUE (10 Punkte)
Beweisen Sie folgende Aussage aus der Vorlesung:
SAT ≤ k-Clique.
Hinweis: NICHT 3-SAT, sondern allgemeines SAT!
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 4 von 12
Aufgabe 3 KOMPLEXITÄT: NP-VOLLSTÄNDIGKEIT (5+5 Punkte) a)
Wir betrachten folgendes Problem:
Problem: CliqueAndIndependetSet
Eingabe: Ein ungerichteter Graph G = (V, E) mit V = 1, . . . , |V | und eine ganze Zahl k ≥ 1.
Entscheide: Gibt es in G sowohl ein Independent Set (Menge von Knoten, die paarweise
nicht adjazent sind, d.h. zwischen denen es keine Kanten gibt) der Größe k und als auch eine
Clique der Größe k?
Zeigen Sie die NP-Schwere von CliqueAndIndependetSet durch Angabe einer Reduktion
eines nach Vorlesung/Übung bekanntermaßen NP-schweren Problems.
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 5 von 12
b) Wir betrachten folgendes Problem:
Problem: (a1 = 1)-SubsetSum
Eingabe: Eine Menge von n Items, jedes Item i ∈ [n] hat eine Größe ai ∈ N>0 , wobei a1 = 1,
und ein Zielwert T .
Entscheide: Gibt es I ⊆ [n] sodass ∑i∈I ai = T ?
Zeigen Sie die NP-Schwere von (a1 = 1)-SubsetSum durch Angabe einer Reduktion eines
nach Vorlesung/Übung bekanntermaßen NP-schweren Problems.
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 6 von 12
Aufgabe 4 KOMPLEXITÄT: A PPROXIMATIVE A LGORITHMEN (3+7 Punkte)
Wir betrachten folgendes Problem:
Problem: A PPROXIMATE S UBSET S UM
Eingabe: n ganze Zahlen a1 , . . . an und ein Zielwert T . Es gilt ai ≤ T ∀i ∈ [n] sowie ∑i∈[n] ai >
T.
Ziel: Wähle Zahlenmenge S ⊆ {1, . . . n} mit ∑ j∈S a j ≤ T und maximiere ∑ j∈S a j .
Betrachten Sie zu diesem Problem den folgenden Algorithmus GA.
Algorithmus GA((A,T))
1 sortiere Zahlen absteigend, sodass a1 ≥ a2 ≥ · · · ≥ an gilt;
2 integer i = 1;
3 integer sum = 0;
4 boolean tooLarge = false;
5 while i ≤ n and tooLarge = false do
6 if sum+ai ≤ T then
7 sum=sum+ai ;
8 i = i + 1;
9 else
10 tooLarge = true;
11 fi
12 od
13 return sum;
a) Geben Sie eine Konstruktionsvorschrift für eine Instanz I an, mit der der Algorithmus GA
beliebig nah an eine Güte von 2 kommt, also A(I) ≈ OPT2 (I) .
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 7 von 12
b) Zeigen Sie, dass der Algorithmus GA eine approximative Güte von 2 hat, also A(I) ≥ OPT2 (I)
gilt.
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 8 von 12
Aufgabe 5 KOMPLEXITÄT: ETH (5+5 Punkte)
a) Betrachten Sie nachfolgende Reduktion von 3-SAT auf k-Color:
1. Eliminiere doppelte Klauseln und Variablen, die in keiner Klausel auftauchen
2. Elimiere Klauseln die eine Variable und ihre Negation enthalten (diese sind immer erfüllt)
3. Seien C1 , . . . ,Cm die Klauseln in der SAT Formel
4. Definiere 
V := { xi , x̄i , vi | i ∈ [n] } C j j ∈ [m] {z} ,
sowie

E := {vi , v j }, {vi , x j }, {vi , x̄ j } i ∈ [n], j ∈ [n] \ {i}
{ {xi , x̄i } | i ∈ [n] }

{xi ,C j } i ∈ [n], j ∈ [m], xi ∈ / Cj

{x̄i ,C j } i ∈ [n], j ∈ [m], x̄i ∈ / Cj
{ {vi , z} | i ∈ [n] }

{C j , z} j ∈ [m]
Welche Lower Bounds ergeben sich unter Annahme der ETH for k-Colour aus dieser Re-
duktion in Hinblick auf
(i) die Anzahl der Knoten |V |; sowie
(ii) die Anzahl der Kanten |E|?
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 9 von 12
b) Wir betrachten folgendes Scheduling Problem:
Problem: 2|prec, pi ∈ {1, 2}|Cmax
Eingabe: Eine Menge J von n Jobs, jeder Job J ∈ J hat eine Ausführungszeit pJ ∈ {1, 2}
sowie Präzedenzconstraints in Form eines gerichteten, azyklischem Graphen G = (J , A),
sowie ein ganze Zahl T ≥ 0.
Entscheide: Existiert ein Schedule, d.h. eine Abbildung, die jedem Job eine Startzeit und
eine Maschine zuweist, sodass (a) sich Jobs auf einer Maschine nicht überlappen (b) für alle
Jobs j, k ∈ J mit ( j, k) ∈ A, dass k erst startet nachdem j vollständig abgearbeitet ist und
(c) der Makespan höchstens T ist?
Wir betrachten die folgende Reduktion von k-Clique auf 2|prec, pi ∈ {1, 2}|Cmax .
Seien G = (V, E) und k gegeben. Wir setzen n = 4|V | + 3|E| und erstellen einen Knoten-Job Ji
für alle i ∈ V mit pi = 1. Des Weiteren erstellen wir Kanten-Jobs J{i, j} für jede Kante {i, j} ∈ E
mit p{i, j} = 2. Abschließend erstellen wir noch 3|V | + 2|E| Dummy Jobs. Diese Jobs haben
Ausführungszeit pdummy = 1.
Wir setzen unsere Präzedenzen nach dem folgenden Schema:
1) (Ji , J{i, j} ) für alle i ∈ V, {i, j} ∈ E
2) Präzedenzen innerhalb der Dummy Jobs.
Zuletzt setzen wir den Makespan T = 2|V | + 2|E|.
(i) Geben Sie die Lower Bounds für 2|prec, pi ∈ {1, 2}|Cmax in Hinblick auf n an, die sich
unter der ETH aus dieser Reduktion, basierend auf den Lower Bounds für k-Clique,
ergeben.
(ii) Geben Sie die Lower Bounds für 2|prec, pi ∈ {1, 2}|Cmax in Hinblick auf T an, die sich
unter der ETH aus dieser Reduktion, basierend auf den Lower Bounds für k-Clique,
ergeben.
Hinweis: Ein beispielhafter Schedule ist unten dargestellt. Die Präzedenzen sind im Graphen
durch gerichtete Kanten abgebildet, daher startet J4 erst nach dem Ende von J1 . Nutzen Sie
für diese Aufgabe die bekannten Lower Bounds aus der Präsenzübung. Diese wurden aus der
Reduktion von 3-SAT auf k-Clique hergeleitet. Hierbei ist der Clique Graph zusammenhän-
gend!
J3
J1
J3 J4
J4
J1 J2
J2
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 10 von 12
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 11 von 12
Aufgabe 6 B ONUSAUFGABE (10 Punkte)
Beweisen oder widerlegen Sie folgende Aussage:
Wenn die ETH fehlschlägt gilt P = NP.
Viel Erfolg!
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 12 von 12
CHRISTIAN-ALBRECHTS-UNIVERSITÄT ZU KIEL
Institut für Informatik, Arbeitsgruppe Algorithmen und Komplexität
Prof. Dr. K. Jansen
11. Oktober 2024
Modulprüfung zur Vorlesung
»Analyse von Algorithmen und Komplexität (Komplexitätsteil)«
SS 2024
Name: Matrikel-Nr.:
Hinweise:
• Bearbeiten Sie alle Aufgaben.
• Sie haben 10 Minuten Einlesezeit. Danach haben Sie 120 Minuten Zeit für die Klausurbearbeitung.
• Erlaubte Hilfsmittel: Ein doppelseitig handschriftlich beschriebenes Blatt Papier, (bunte) Stifte (kein
Rot, kein Grün, kein Bleistift).
• Einsichtnahme: 30.10.2024
Note 4,0 3,7 3,3 3,0 2,7 2,3 2,0 1,7 1,3 1,0
Mindestpunktzahl 17,5 19,5 21,5 23,5 25 27 29 31 33 35
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 1 von 16
Aufgabe 1 ∆T SP1 (6+2+2 Punkte)
Gegeben sei folgender Graph G = (V, E), bei dem die Kantengewichte jeweils an der nach außen
gerichteten Seite der Kante stehen:
a
4 2
3
e b
4 2
4 3
6 5
d c
2
a) Wenden Sie den Algorithmus ∆T SP1 auf den Graphen G beginnend bei a an. Geben Sie dabei
die Ergebnisse jedes Zwischenschrittes des Algorithmus an.
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 2 von 16
b) Beschreiben Sie, was der Algorithmus ∆T SP1 berechnet und geben Sie die Bedingungen an,
die an den Graphen gestellt sind, damit der Algorithmus eine korrekte Lösung berechnet.
c) Begründen Sie, warum der Algorithmus ∆T SP1 eine approximative Güte von 2 besitzt.
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 3 von 16
Aufgabe 2 KOMPLEXITÄT: VORLESUNGSBEWEIS SAT (10 Punkte)
Beweisen Sie folgende Aussage:
3-SAT ist NP-vollständig. Zeigen Sie dafür SAT ≤ 3-SAT, nehmen Sie also 3-SAT ∈ NP als
gegeben an.
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 4 von 16
Aufgabe 3 KOMPLEXITÄT: NP-VOLLSTÄNDIGKEIT (5+5 Punkte)
a) Wir betrachten folgendes Problem:
Problem: Hitchhikers-HamiltonianCycle
Eingabe: Ein ungerichteter Graph G = (V, E) mit V = {1, . . . , |V |} und für alle v ∈ V gilt
deg(v) ≥ 42.
Entscheide: Gibt es einen Hamiltonkreis in G?
Beweisen Sie die NP-Schwere von Hitchhikers-HamiltonianCycle durch Angabe einer
Reduktion eines nach Vorlesung/Übung bekanntermaßen NP-schweren Problems.
Hinweis: Als Hilfe, geben Sie graphisch die Reduktion für Knotengrad ≥ 10 an.
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 5 von 16
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 6 von 16
b) Wir betrachten folgendes Problem:
Problem: AtMostTwoPerSize-SubsetSum
Eingabe: Ein ganzzahliger Zielwert T > 0, eine Menge von n Items, jedes Item i ∈ [n]
hat eine Größe ai ∈ N>0 , jede Größe tritt höchstens zwei Mal auf, d.h. für alle i ∈ [n] gilt
| { i ∈ [n] | ai = ai } | ≤ 2.
Entscheide: Gibt es S ⊆ [n] sodass ∑i∈S ai = T ?
Beweisen Sie die NP-Schwere von AtMostTwoPerSize-SubsetSum durch Angabe einer Re-
duktion eines nach Vorlesung/Übung bekanntermaßen NP-schweren Problems.
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 7 von 16
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 8 von 16
Aufgabe 4 KOMPLEXITÄT: A PPROXIMATIVE A LGORITHMEN (10+(10) Punkte)
Wir betrachten das aus der Vorlesung bekannte Makespan Scheduling Problem P||Cmax . Dazu ist
folgender Algorithmus gegeben:
Algorithmus ROUND ROBIN S CHEDULING(I=(J,m))
1 sortiere die Jobs in J so, dass p1 ≥ p2 ≥ · · · ≥ pn gilt;
2 setze B1 = · · · = Bm = 0;
/
3 integer j = 1;
4 integer i = 1;
5 while j <= n do
6 Platziere Job J j auf Maschine Mi ;
7 Bi = Bi {J j };
8 j = j + 1;
9 if i < m then
10 i=i+1
11 else
12 i=1
13 fi
14 od
15 return B1 , . . . , Bm
Betrachten Sie folgendes Beispiel zu der Funktionsweise des Round Robin Scheduling Algorith-
mus. Gegeben sind 3 Maschinen und 6 Jobs J1 , . . . J6 , die bereits richtig sortiert sind. Es gilt
p1 = 7, p2 = 5, p3 = 5, p4 = 4, p5 = 3, p6 = 1. Daraus ergibt sich folgender Schedule:
J4
J5
J6
J1
J2 J3
m1 m2 m3
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 9 von 16
a) Zeigen Sie per Widerspruch, dass der Algorithmus Round Robin Scheduling eine approxima-
tive Güte von 2 hat.
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 10 von 16
b) BONUS: Geben Sie die Konstruktionsvorschrift für eine Instanz an, mit der der Algorithmus
Round Robin Scheduling beliebig nah an eine Güte von 2 kommt, also A(I) ≈ 2 · OPT (I).
Nutzen Sie dafür mindestens 3 Maschinen und 7 Jobs, womit eine Rate von A(I) = 53 OPT
zu erreichen ist. Geben sie die optimale Makespan an und die, die der Algorithmus bei Ihrer
Instanz erreicht. Für eine Güte von 53 können bis zu 3 Punkten erreicht werden, bei einer
Güte von 47 sind bis zu 5 Punkte zu erreichen. Geben Sie eine Konstruktionsvorschrift an,
bei denen bei einer beliebigen, festen Anzahl von Maschinen m eine Güte von 2 m1 erreicht
wird, können Sie die vollen 10 Punkte erreichen.
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 11 von 16
Aufgabe 5 KOMPLEXITÄT: ETH (5+5 Punkte)
a) Wir betrachten folgendes Problem:
Problem: DominatingSet
Eingabe: Ein ungerichteter Graph G = (V, E) mit V = 1, . . . , |V | und eine Ganzzahl k ≥ 1.
Entscheide: Gibt es eine Menge M ⊆ V der Kardinalität |M| = k derart, dass jeder Knoten
v ∈ V entweder in M ist oder zu einem Knoten in M benachbart ist.
Wir untersuchen nun folgende Reduktion von 3-SAT auf DominatingSet:
1. Seien C1 , . . . ,Cm die Klauseln der 3-SAT-Formel
2. Seien x1 , . . . , xn die Variablen der 3-SAT-Formel
3. Erzeuge die Knoten xi , xi und di für jede Variable xi
4. Erzeuge die Kanten {xi , xi }, {xi , di } und {di xi } für jede Variable xi
5. Für jede Klausel C j erzeuge einen Knoten C j
6. Für jedes Literal einer Klausel C j erzeuge die Kanten {C j , }
Welche unteren Schranken für die Laufzeit folgen damit für DominatingSet unter der ETH
in Bezug auf
(i) die Anzahl der Knoten |V |?
(ii) die Anzahl der Kanten |E|?
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 12 von 16
b) Wir betrachten folgendes Problem:
Problem: GridTiling
Eingabe: Zwei ganze Zahlen k und j und für jedes i ∈ [k] und j ∈ [k] eine Menge Si, j ⊆ [n]2 .
Entscheide: Gibt es (ei, j )(i, j)∈[k]2 derart, dass
1. für alle (i, j) ∈ [k]2 gilt ei, j ∈ Si, j ;
2. für alle i ∈ [k] und j ∈ [k 1] gilt für ei, j = (a, b) und ei, j+1 = (a , b ), dass a = a (dh. die
ausgewählten Tupel müssen in der ersten Komponente übereinstimmen, wenn der erste
Index derselbe ist); und
3. für alle i ∈ [k 1] und j ∈ [k] gilt für ei, j = (a, b) und ei+1, j = (a , b ), dass b = b (dh.
die ausgewählten Tupel müssen in der zweiten Komponente übereinstimmen, wenn der
zweite Index derselbe ist)
Betrachten Sie folgende Beispielinstanz für k = 2, die Elemente der Lösung sind unterstrichen:
S1,1 = {(1, 1), (2, 2)} S1,2 = {(1, 2), (2, 1), (2, 2)}
S2,1 = {(1, 2), (2, 1)} S2,2 = {(1, 1), (2, 2)}
Wir untersuchen folgende Reduktion von Clique:
1. Sei G = (V, E) und eine Zahl k als zu untersuchende Clique Instanz gegeben
2. Wir nehmen ohne Beschränkung der Allgemeinheit an, dass V = {1, . . . , |V |} sowie k ≥ 2
gilt und dass keine isolierten Knoten existieren.
3. Wir übernehmen den Wert von k
4. Für jedes Paar (i, j) ∈ [k]2 definieren wir
(
{ (a, a) | a ∈ V } if i = j
Si, j :=
{ (a, b) | a ̸= b, {a, b} ∈ E } if i ̸= j
Welche unteren Schranken für die Laufzeit folgen damit für Grid Tiling (mit der aus der
Vorlesung bekannten Reduktion von Clique auf 3-SAT) in Hinblick auf
(i) der Gesamtanzahl X := ∑(i, j)∈[k]2 |Si, j | der Tupel in den Mengen; sowie
(ii) der maximalen Anzahl Y := max(i, j)∈[k]2 |Si, j | an Tupeln in einer der Mengen?
Hinweis: Aus der Reduktion in der Vorlesung wissen wir, dass Clique selbst dann noch NP-
schwer ist, wenn k ∈ Θ(|V |) gilt. Sie dürfen also annehmen, dass es eine (globale) Konstante
c > 0 gibt, sodass c · n ≤ k ≤ n.
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 13 von 16
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 14 von 16
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 15 von 16
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 16 von 16
CHRISTIAN-ALBRECHTS-UNIVERSITÄT ZU KIEL
Institut für Informatik, Arbeitsgruppe Algorithmen und Komplexität
Prof. Dr. K. Jansen
14. Juli 2023
Modulprüfung zur Vorlesung
»Analyse von Algorithmen und Komplexität (Komplexitätsteil)«
SS 2023
Name: Matrikel-Nr.:
Hinweise:
• Bearbeiten Sie alle Aufgaben.
• Sie haben 10 Minuten Einlesezeit. Danach haben Sie 120 Minuten Zeit für die Klausurbearbeitung.
• Erlaubte Hilfsmittel: Ein doppelseitig handschriftlich beschriebenes Blatt Papier, (bunte) Stifte (kein
Rot, kein Grün, kein Bleistift).
• Einsichtnahme: Nach Absprache.
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 1 von 11
Aufgabe 1 RUCKSACKPROBLEM (3+2+5 Punkte)
Wir betrachten die Anwendung der Approximationsheuristiken für das Rucksackproblem aus der
Vorlesung.
Gegeben sei dafür folgende Instanz des Rucksackproblems mit Kapazität B = 16. Items sind in
der Form (pi , wi ) angegeben.
I = [(1, 4), (1, 1), (1, 3), (11, 13), (3, 3), (5, 6), (1, 5), 16]
a) Wenden Sie den normalen Greedy-Algorithmus auf obige Instanz an. Begründen Sie kurz,
warum die gewählten Items ausgewählt werden. Was ist der Lösungswert?
b) Wenden Sie nun den ModifiedGreedy-Algorithmus an. Wie ändert sich die Lösung?
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 2 von 11
c) Was ist die Güte des ModifiedGreedy-Algorithmus? Geben Sie eine kurze Idee an, wie man
diesen Ansatz modifizieren kann, um die Güte auf 32 oder 43 zu verbessern.
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 3 von 11
Aufgabe 2 L ONGEST PATH (3+3+4 Punkte)
Betrachten Sie das folgende Problem:
Problem: L ONGEST PATH
Eingabe: Eine ungerichteter Graph G = (V, E) und eine Zahl k.
Entscheide: Gibt es einen Pfad in G, der mindestens Länge k hat?
Zeigen Sie auf verschiedene Weisen, dass das Problem in NP liegt:
a) Geben Sie ein Zertifikat für L ONGEST PATH an.
b) Beschreiben Sie einen Verifizierer für L ONGEST PATH und geben Sie dessen Laufzeit konkret
an.
c) Beschreiben Sie eine nicht-deterministische Turing-Maschine (in Worten), die L ONGEST PATH
löst. Geben Sie auch hier die Laufzeit konkret an.
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 4 von 11
Aufgabe 3 A PPROXIMATIVE A LGORITHMEN (6+4 Punkte)
Das Problem M AX -3-S AT sei folgendermaßen definiert:
Problem: M AX -3-S AT
Eingabe: Eine Formel φ in konjunktiver Normalform, wobei jede Klausel drei Literale enthält.
Ausgabe: Eine Belegung β der Variablen, die die Anzahl v(β ) der erfüllten Klauseln maximiert.
Betrachten Sie folgenden Algorithmus A:
• Sei β0 die Belegung, die alle Variablen auf false setzt.
• Sei β1 die Belegung, die alle Variablen auf true setzt.
• Falls v(β0 ) ≥ v(β1 ), gib β0 zurück.
• Sonst gib β1 zurück.
a) Zeigen Sie, dass obiger Algorithmus Güte 2 hat, d.h. A(φ ) ≥ 12 OPT (φ ) gilt für alle Eingaben
φ.
b) Geben Sie eine Formel an, bei der der Algorithmus eine Belegung ausgibt, die genau die Hälfte
der maximal erfüllbaren Klauseln erfüllt. Begründen Sie kurz, warum Ihre Formel geeignet ist.
Hinweis: Sie brauchen nur höchstens zwei Klauseln und sechs Variablen.
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 5 von 11
Aufgabe 4 B EWEIS ZUR VORLESUNG (10 Punkte)
Betrachten Sie folgendes Problem aus der Vorlesung:
Problem: k-C LIQUE
Eingabe: Ein ungerichteter Graph G = (V, E) und eine Zahl k ≥ 1. Eine Clique ist eine Teil-
menge C ⊆ V mit {u, v} ∈ E für alle u, v ∈ C mit u ̸= v.
Entscheide: Hat der gegebene Graph G eine Clique C ⊆ V mit mindestens k Knoten (d.h. mit
|C| ≥ k)?
Beweisen Sie: Das Problem k-C LIQUE ist NP-vollständig.
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 6 von 11
Aufgabe 5 NP UND NP-VOLLSTÄNDIGKEIT (4+4+7 Punkte)
a) Beweisen Sie die NP-Vollständigkeit von S UBSET S UM, bei der jede Itemgröße jeweils durch
3 oder durch 7 teilbar ist.
b) Beweisen Sie die NP-Vollständigkeit von 3-C OLOR, wobei jeder Knoten im Graph mindes-
tens Grad 3 hat.
c) Zeigen Sie: Für kein α > 1 gibt es einen approximativen Algorithmus mit Güte α (d.h. mit
Zielfunktionswert ≤ α · OPT) für TSP, außer P = NP (Denken Sie an Wiliam Rowan Hamil-
ton).
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 7 von 11
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 8 von 11
Aufgabe 6 U NTERE S CHRANKEN UND ETH (7+8 Punkte)
Betrachten Sie folgende Reduktionen:
Problem: H ITTING S ET
Eingabe: Eine Menge U sowie r Teilmengen F1 , . . . , Fr ⊆ U und eine Zahl k.
Entscheide: Gibt es eine Menge S ⊆ U mit |S| ≤ k und S ∩ Fi ̸= 0/ für alle i ∈ [r]?
a) 3-SAT ⪯ H ITTING S ET: Seien v1 , . . . , vn die Variablen der Formel und C1 , . . . ,Cm die Klau-
seln. Setze U = { v1 , v1 , . . . , vn , vn } und k = n. Für jede Variable vi definiere eine Menge
Fi = {vi , vi } und für jede Klausel C j definiere eine Menge Fn+ j = C j
Welche unteren Schranken ergeben sich durch obige Reduktion für die Laufzeit von Algorith-
men für H ITTING S ET unter Annahme der ETH in Abhängigkeit von
(i) r?
(ii) |U|?
Beweisen Sie die entsprechenden unteren Schranken!
Hinweis: Sie dürfen wie in den Hausaufgaben zur ETH Faktoren, die polynomiell in der
Eingabekodierung sind, vernachlässigen.
b) 3-S AT ⪯ S UBSET S UM: Seien v1 , . . . , vn die Variablen der Formel und C1 , . . . ,Cm die Klauseln.
Für jede Variable vi erzeuge zwei Items ai und bi , mit den Größen
s(ai ) = 10i1 + ∑ 10n+ j1 und s(bi ) = 10i1 + ∑ 10n+ j1 .
j∈[m] j∈[m]
xi ∈C j xi ∈C j
Zusätzlich erzeuge zwei Items c j und d j für jede Klausel C j mit Größen
s(c j ) = s(d j ) = 10n+ j1 .
Der Zielwert sei
B = ∑ 3 · 10n+ j1 + ∑ 10i1 .

j∈[m] i∈[n]
Welche unteren Schranken ergeben sich durch obige Reduktion für die Laufzeit von Algorith-
men für S UBSET S UM unter Annahme der ETH in Abhängigkeit von
(i) der Anzahl der unterschiedlichen Itemgrößen d?
(ii) der binären Kodierungslänge L der größten Itemgröße ∆ (d.h. L = log(∆))?
Beweisen Sie die entsprechenden unteren Schranken!
Hinweis: Sie dürfen auch hier wie in den Hausaufgaben zur ETH Faktoren, die polynomiell
in der Eingabekodierung sind, vernachlässigen.
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 9 von 11
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 10 von 11
Viel Erfolg!
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 11 von 11
CHRISTIAN-ALBRECHTS-UNIVERSITÄT ZU KIEL
Institut für Informatik, Arbeitsgruppe Algorithmen und Komplexität
Prof. Dr. K. Jansen
19. Oktober 2023
Modulprüfung zur Vorlesung
»Analyse von Algorithmen und Komplexität (Komplexitätsteil)«
SS 2023
Name: Matrikel-Nr.:
Hinweise:
• Bearbeiten Sie alle Aufgaben.
• Sie haben 120 Minuten Zeit für die Klausurbearbeitung.
• Erlaubte Hilfsmittel: Ein einseitig handschriftlich beschriebenes Blatt Papier, (bunte) Stifte (kein Rot,
kein Grün, kein Bleistift, kein Tintenkiller, kein Tipp-Ex).
• Schreiben Sie auf jedes Blatt ihren Namen.
• Wenn Sie Schmierzettel verwenden um eine Aufgabe zu bearbeiten, verweisen Sie in der Aufgabe auf
diese.
• Einsichtnahme: 2.11.23, 13:30-14:30 Uhr.
• Die Klausur hat 6 Aufgaben auf 13 Seiten überprüfen Sie bitte vor der Bearbeitung, dass Ihre Klausur
vollständig ist.
Aufgabe 1 C HRISTOFIDES A LGORITHMUS (3+7 Punkte)
Gegeben sei folgender Graph G = (V, E) :
d
2
3
3
a c
1
2
b
a) Geben Sie an, was Christofides Algorithmus berechnet, und welche approximative Güte er
hat. Welche Eigenschaft müssen die Distanzen neben der Symmetrie noch erfüllen, damit
Christofides Algorithmus korrekt arbeitet.
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 1 von 19
b) Wenden Sie den Algorithmus von Christofides auf den Graphen G mit Startknoten a an. Ge-
ben Sie dabei alle Graphen an, die in Zwischenschritten entstehen. Erwähnen Sie ebenfalls
Schritte, die bei dieser Anwendung zu keiner Änderung führen, die der Algorithmus aber
überprüfen muss.
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 2 von 19
Aufgabe 2 D OMINATING S ET (3+3+4 Punkte)
Betrachten Sie das folgende Problem:
Problem: D OMINATING S ET
Eingabe: Ein ungerichteter Graph G = (V, E) (wobei V = {1, . . . , n} für eine Zahl n ∈ N≥1 )
und eine Zahl k ∈ N0 .
Entscheide: Gibt es eine Teilmenge D ⊆ V der Knoten mit |D| ≤ k, sodass jeder Knoten v ∈ V
in D enthalten ist oder benachbart zu mindestens einem der Knoten in D ist?
Zeigen Sie auf verschiedene Weisen, dass das Problem in NP liegt:
a) Geben Sie ein Zertifikat für D OMINATING S ET an.
b) Beschreiben Sie einen polynomiellen Verifizierer für D OMINATING S ET und schätzen Sie die
Laufzeit in O-Notation konkret ab.
c) Beschreiben Sie (in Worten) eine polynomielle, nicht-deterministische Turing-Maschine, die
D OMINATING S ET löst. Schätzen Sie auch hier die Laufzeit in O-Notation konkret ab.
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 3 von 19
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 4 von 19
Aufgabe 3 A PPROXIMATIVE A LGORITHMEN (6+4 Punkte)
Das Problem M IN -E DGE -C OVER sei folgendermaßen definiert:
Problem: M IN -E DGE -C OVER
Eingabe: Ein zusammenhängender, ungerichteter Graph G = (V, E).
Ausgabe: Eine Teilmenge der Kanten C ⊆ E mit minimaler Kardinalität, sodass jeder Knoten
v ∈ V zu mindestens einer Kante aus C inzident ist.
Betrachten Sie folgenden Algorithmus A:
• Gehe (in beliebiger Reihenfolge) alle Knoten durch:
• Falls der aktuelle Knoten v noch nicht abgedeckt ist, wähle zufällig eine der zu v inzidenten
Kanten und füge sie zu C hinzu.
• Gib anschließend die entstehende Menge C zurück.
a) Zeigen Sie, dass obiger Algorithmus Güte 2 hat, d.h. A(G) ≤ 2 · OPT(G) gilt für alle Eingaben
G.
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 5 von 19
b) Sei n ∈ N≥4 gerade. Betrachten Sie den Graphen Gn , der genau n Knoten und n Kanten hat und
einen Kreis beschreibt. D.h. Gn = ({1, . . . , n}, {{i, i+1} | i ∈ {1, . . . , n1}}{{n, 1}}). Wie viele
Kanten hat ein M IN -E DGE -C OVER des Graphen Gn ? Wie schlecht ist die Approximationsrate
des Algorithmus A für Gn im schlimmsten Fall? Geben Sie eine Reihenfolge der Knoten (für
Schritt 1) und eine Auswahl der Kanten (für Schritt 2) an, die zu einer möglichst schlechten
Approximationsrate führen.
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 6 von 19
Aufgabe 4 B EWEIS S CHEDULING AUF IDENTISCHEN M ASCHINEN (10 Punkte)
Betrachten Sie das Scheduling-Problem P||Cmax :
Problem: P||Cmax
Eingabe: Eine Liste L = (J1 , . . . , Jn ) von n Jobs mit Ausführungszeiten p1 , . . . , pn ∈ N, und m
identische Maschinen.
Ausgabe: Ein Schedule (Partition von J in m Teilmengen B1 , . . . , Bm ) mit minimaler maximaler
Last Cmax := max ∑ p j .
1≤i≤m J ∈B
j i
Betrachten Sie den folgenden Algorithmus ListScheduling, der einen Schedule berechnet. Sei
LS(I) die Last Cmax eines Schedules des Algorithmus, und OPT(I) die Last Cmax eines optimalen
Schedules zur Eingabe I. Zeigen Sie: Für alle Eingaben I = (L, m) gilt LS(I)/OPT(I) ≤ 2 1/m.
Algorithmus L IST S CHEDULING(L = (J1 , . . . , Jn ), m)
1 for i = 1 to m do
2 Ei = 0; Bi = 0/ ;
3 od
4 for j = 1 to n do
5 wähle Job J j aus Liste L;
6 wähle Maschine Mi mit minimaler Last Ei ;
7 Bi = Bi {J j };
8 Ei = Ei + p j ;
9 od
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 7 von 19
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 8 von 19
Aufgabe 5 NP UND NP-VOLLSTÄNDIGKEIT (4+5+6 Punkte)
Problem: H AMILTONIAN PATH
Eingabe: Ein ungerichteter Graph G = (V, E)
Entscheide: Existiert ein Pfad in G, der jeden Knoten genau einmal besucht?
Problem: H AMILTONIAN C YCLE
Eingabe: Ein ungerichteter Graph G = (V, E)
Entscheide: Existiert ein Kreis in G, der jeden Knoten genau einmal besucht?
(a) Sei L ⊆ S UBSET S UM die Menge der (positiven) SubsetSum-Instanzen, bei denen keine der
Itemgrößen eine Zweierpotenz ist. Zeigen Sie, dass L NP-vollständig ist.
(b) Geben Sie eine polynomielle Reduktion von H AMILTONIAN PATH auf H AMILTONIAN C Y-
CLE an.
(c) Geben Sie eine polynomielle Reduktion von H AMILTONIAN C YCLE auf H AMILTONIAN -
PATH an.
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 9 von 19
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 10 von 19
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 11 von 19
Aufgabe 6 U NTERE S CHRANKEN UND ETH (5+5 Punkte)
Im Folgenden bezeichne ⟨I⟩ die Kodierungslänge der Instanz.
(a) Wir betrachten folgendes Problem:
Problem: H ITTING S ET
Eingabe: Eine Menge U sowie r Teilmengen F1 , . . . , Fr ⊆ U und eine Zahl k .
Entscheide: Gibt es eine Menge S ⊆ U mit |S | ≤ k und S ∩ Fi ̸= 0/ für alle i ∈ {1, . . . , r }?
Sie dürfen nutzen, dass sich H ITTING S ET unter Annahme der ETH nicht in Zeit 2o(r ) ⟨I⟩O(1)
und nicht in Zeit 2o(|U |) ⟨I⟩O(1) lösen lässt.
Problem: S ET C OVER
Eingabe: Eine Menge U sowie r Teilmengen F1 , . . . , Fr ⊆ U und eine Zahl k.
Entscheide: Gibt es eine Menge S ⊆ {1, . . . , r} mit |S| ≤ k und i∈S Fi = U?
S
Welche unteren Schranken ergeben sich
(i) in Hinblick auf |U| sowie
(ii) in Hinblick auf r
für S ET C OVER unter Annahme der ETH aus den unteren Schranken für H ITTING S ET, wenn
man folgende Reduktion von SetCover auf HittingSet verwendet?
Sei (F ,U, k) eine S ET C OVER-Instanz. Setze V1 := U und V2 := {1, . . . , r}. Sei V := V1 V˙ 2.
Weiter sei E = { {v, w} | v ∈ V1 , w ∈ V2 , v ∈ Fw }. Damit ist G = (V, E) ein bipartiter Graph
mit Partitionen V1 ,V2 . Setze U := {1, . . . , r}. Erzeuge nun eine Menge Fw für jeden Knoten
w ∈ V1 , indem Fw := { v ∈ V2 | {v, w} ∈ E } gesetzt wird. Sei F := { Fw | w ∈ V1 }. Mit k = k
iie berechnete Instanz ist nun durch (F ,U , k ) gegeben.
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 12 von 19
(b) Wir wollen uns mit einem Problem beschäftigen, das ein mächtiges Werkzeug im Bereich
der Optimierung ist: Integrale Lineare Programme (ILPs).
Problem: ILP-F EASIBILITY
Eingabe: Eine Matrix A ∈ ZM×N , eine rechte Seite b ∈ ZM .
Entscheide: Gibt es einen Vektor x ∈ ZN≥0 mit Ax ≤ b, d.h. Ai xi ≤ bi für alle i ∈ {1, . . . , M}?
Welche unteren Schranken ergeben sich
(i) in Hinblick auf M sowie
(ii) im Hinblick auf N
für ILP-F EASIBILITY unter Annahme der ETH, wenn man folgende Reduktion von 3-SAT
auf ILP-F EASIBILITY verwendet?
Sei φ eine 3-SAT Instanz mit m Klauseln (Ci )i∈m und n Variablen. Analog zu linearen Glei-
chungssystemen (Ax = b), die aus M Gleichungen bestehen, haben wir bei Ax ≤ b einen
Satz von M linearen Ungleichungen. Wir konstruieren diese wie folgt: Wir nutzen N := 2n
Variablen in unserem ILP, eine für jedes (mögliche) Literal in der SAT-Formel. Für je-
de SAT-Variable v sei xv die ILP-Variable, die zum SAT-Literal v korrespondiert, und xv̄
die, die zum SAT-Literal v̄ korrespondiert. Für jede Klausel Ci erzeugen wir eine Unglei-
chung ∈Ci x1. Weiter erzeugen wir für jede SAT-Variable v zwei Ungleichungen:
xv +xv̄ ≤ 1 und xv xv̄ ≤ 1. Durch das Eintragen der entsprechenden Koeffizienten in eine
erweiterte Koeffizientenmatrix (bestehend aus Matrix A und rechter Seite b) erhalten wir die
ILP-F EASIBILITY Instanz.
Viel Erfolg!
Klausur »Analyse von Algorithmen und Komplexität (Komplexitätsteil)« Seite 13 von 19

Binary file not shown.

View File

@@ -0,0 +1,480 @@
CHRISTIAN-ALBRECHTS-UNIVERSITÄT ZU KIEL
Institut für Informatik, Arbeitsgruppe Algorithmen und Komplexität
Prof. Dr. K. Jansen, B. Schumacher, C. Wambsganz
09.06.2025
Präsenzaufgaben zur Vorlesung »Analyse von Algorithmen und
Komplexität«
Blatt 9
Definition 1 (Knapsack (Entscheidungsvariante)). Gegeben ist eine Menge mit n Gegenständen
mit Gewichten w1 , . . . , wn ∈ N≥0 und Profiten p1 , . . . , pn ∈ N≥0 . Weiter sind eine Kapazität
K ∈ N≥0 und ein Zielprofit P ∈ N≥0 gegeben. Bei dem K NAPSACK Problem muss entschieden
werden, ob eine Teilmenge S ⊆ [n] existiert, sodass
∑ wi ≤ K und ∑ pi ≥ P
i∈S i∈S
gilt.
Präsenzaufgabe 9.1 (K NAPSACK in NP)
Zeigen Sie K NAPSACK ∈ NP auf zwei verschiedene Wege.
1. Geben Sie einen nicht deterministischen Algorithmus an, der das Problem in polynomi-
eller Zeit löst.
2. Geben Sie einen polynomiellen Verifizierer für das Problem an.
Wie hängen diese beiden Methoden zusammen?
Lösung. 1. Der Algorithmus arbeitet wie folgt: Für jeden Gegenstand entscheiden wir nicht-
deterministisch, ob er in den Rucksack soll. Anschließend akzeptieren wir, falls die Kapazitäts-
und Profitbedingung erfüllt sind.
Existiert eine valide Rucksackfüllung mit genügend Profit, dann gibt es auch eine Reihe
von nichtdeterministischen Entscheidungen, die diese Füllung findet. Diese wird dann
auch vom Algorithmus akzeptiert.
Der Algorithmus macht polynomiell viele nichtdeterministische Entscheidungen und die
Überprüfung der Eigenschaften ist ebenfalls in Polynomialzeit möglich, also ist dies ein
polynomieller, nichtdeterministischer Algorithmus, der K NAPSACK löst.
2. Unser Zertifikat ist eine Teilmenge S ⊆ [n], die die Auswahl für den Rucksack darstellen
soll. Dies kann zum Beispiel über ein Bit pro Gegenstand realisiert werden. Um dieses
Zertifikat zu verifizieren, berechnen wir ∑i∈S wi und ∑i∈S pi und vergleichen die Werte
mit K und P und akzeptieren entsprechend.
Existiert eine valide Rucksackfüllung mit genügend Profit, dann gibt es auch ein Zertifi-
kat, das diese Füllung beschreibt. Dieses wird dann auch vom Verifizierer akzeptiert.
Das Ausrechnen der Summen und das Überprüfen ist in polynomieller Zeit möglich, also
ist dies ein polynomieller Verifizierer.
In beiden Wegen wird die extra Ressource, die wir zur Verfügung haben, genutzt, um eine
Teilmenge (einen möglichen Rucksack) zu bestimmen und zu überprüfen. In der Vorlesung
wird noch genau gezeigt, dass diese beiden Wege tatsächlich äquivalent sind.
Präsenzaufgabe 9.2 (Transitivität von Polynomialzeitreduktionen)
Zeigen Sie, dass Polynomialzeitreduktionen transitiv sind, d.h., dass für alle Entscheidungspro-
bleme L1 , L2 , L3 ⊆ Σ∗ gilt, dass
L1 ⪯ L2 ∧ L2 ⪯ L3 =⇒ L1 ⪯ L3 .
Lösung. Seien L1 , L2 , L3 Sprachen. Zudem sei R1 ein Algorithmus, der L1 nach L2 in O(na )
reduziert, und R2 ein Algorithmus, der L2 nach L3 in O(nb ) reduziert.
Sei x eine Instanz von L1 der Größe n. Dann benötigt der Aufruf von R1 auf x höchstens O(na )
Zeit.
Sei x die Ausgabe von R1 . Dann hat x eine Größe von höchstens O(na ). Diese Schranke für
die Ausgabe ergibt sich aus der Laufzeit der Reduktion. Der Aufruf von R2 auf x benötigt nun
höchstens O((na )b ) = O(nab ) Zeit. Damit ist die Reduktion von L1 auf L3 in polynomieller Zeit
möglich.
Die Korrektheit ergibt sich aus der Transitivität der Äquivalenz: Sei x die Ausgabe von R2 auf
x , dann gilt
x ∈ L1 ⇐⇒ x ∈ L2 und x ∈ L2 ⇐⇒ x ∈ L3 ,
also auch x ∈ L1 ⇐⇒ x ∈ L3 .
CHRISTIAN-ALBRECHTS-UNIVERSITÄT ZU KIEL
Institut für Informatik, Arbeitsgruppe Algorithmen und Komplexität
Prof. Dr. K. Jansen, B. Schumacher, C. Wambsganz
16.06.2025
Präsenzaufgaben zur Vorlesung »Analyse von Algorithmen und
Komplexität«
Blatt 10
Präsenzaufgabe 10.1 (Fragen über Fragen)
Diskutieren Sie die folgenden Aussagen (Gelten sie allgemein? Gelten sie nicht? Gelten sie
unter bestimmten Bedingungen? Welche?):
(i) Für jede Sprache, die von einer NDTM in polynomieller Zeit akzeptiert werden kann,
existiert eine DTM, die ebenfalls die Sprache in polynomieller Zeit akzeptiert.
(ii) A ist NP-schwer =⇒ A ∈ P
Lösung. (i) Die Aussage gilt nur, wenn P = NP. Dann liegen alle Probleme in NP (von
NDTM in polynomieller Zeit lösbar) auch in P (von DTM in polynomieller Zeit lösbar).
Falls P ̸= NP, dann nein, weil sonst folgender Widerspruch auftritt: ein Problem in NP \ P
ist von einer NDTM, nicht aber von einer DTM in polynomieller Zeit lösbar. Könnte eine
DTM die NDTM in polynomieller Zeit simulieren, wäre das Problem auch in P.
(ii) Falsch. Es gibt NP-schwere Probleme, die nicht in NP liegen, z.B. das H ALTE-Problem.
Diese liegen folglich auch definitiv nicht in P. NP-schwer ist eine untere Schranke, ∈ NP
eine obere, und wenn beide erfüllt sind, ist ein Problem NP-vollständig.
Definition 1 (Vertex Cover (VC)). Für einen (ungerichteten) Graphen G = (V, E) ist ein Vertex
Cover eine Menge von Knoten C ⊆ V , sodass u ∈ C oder v ∈ C für alle { u, v } ∈ E gilt, also ist
für alle Kanten mindestens ein Endpunkt in C.
Für das V ERTEX C OVER Problem ist neben einem Graphen G eine Zahl k ∈ N≥0 gegeben und
es wird gefragt, ob ein Vertex Cover mit Größe (Anzahl der Knoten) höchstens k in G existiert.
Präsenzaufgabe 10.2 (V ERTEX C OVER ist NP-vollständig)
Zeigen Sie, dass V ERTEX C OVER NP-vollständig ist.
Hinweis: Reduzieren Sie C LIQUE auf V ERTEX C OVER.
Lösung. V ERTEX C OVER ∈ NP ist bekannt aus der letzten Hausaufgabe.
Vorüberlegung zur Reduktion von C LIQUE auf V ERTEX C OVER: Sei eine Instanz mit G =
(V, E) und k ∈ N≥0 von C LIQUE gegeben. Wir invertieren zunächst den Graphen G und erhal-
ten damit G . Nun enthält G genau dann eine unabhängige Menge der Größe , wenn G eine
Clique der Größe enthält. Wenn ein Graph mit n Knoten eine unabhängige Menge mit min-
destens k Knoten hat, dann hat der Graph auch ein Vertex Cover mit maximal n k Knoten
(das Komplement zur unabhängigen Menge). Unsere Instanz für V ERTEX C OVER ist also G
und n k. (k ≤ n können wir annehmen)
Betrachte nun folgende Reduktion:
Sei eine Instanz mit G = (V, E) und k ∈ N≥0 von C LIQUE gegeben. Invertiere den Graphen G
und erhalte damit G . Gebe nun G und n k als Instanz von V ERTEX C OVER zurück.
Korrektheit:
⇒ (Falls (G, k) eine Ja-Instanz von C LIQUE ist, dann ist (G , n k) eine Ja-Instanz von V ER -
TEX C OVER .):
G enthalte eine Clique C der Größe ≥ k. Dann ist V := V \C ein Vertex Cover von G der Größe
≤ n k. Da |C| ≥ k, gilt |V | ≤ n k. Da C eine Clique ist, also alle Knoten aus C mit allen
anderen Knoten aus C verbunden sind, gibt es im invertierten Graphen G keine Kante zwischen
Knoten aus C. Also gilt für jede Kante {u, v} ∈ E , dass u ∈ / C und somit u ∈ V oder
/ C oder v ∈
v ∈ V .
⇐ (Falls (G , n k) eine Ja-Instanz von V ERTEX C OVER ist, dann ist (G, k) eine Ja-Instanz von
C LIQUE.):
G enthalte ein Vertex Cover V der Größe ≤ nk. Dann ist C := V \V eine Clique der Größe ≥
k in G. Da |V | ≤ n k, gilt |C| ≥ k. Für jede Menge {u, v} ⊆ C gilt, dass {u, v} ∈
/ E . Ansonsten
wäre V kein korrektes Vertex Cover. Da G der invertierte Graph von G ist, gilt somit {u, v} ∈ E
für jede Menge {u, v} ⊆ C.
Laufzeit: Das Invertieren des Graphen kann naiv in O(|V |2 · |E|) Zeit, also in polynomieller
Zeit, erledigt werden.
Also existiert eine polynomielle Reduktion von C LIQUE auf V ERTEX C OVER. Da C LIQUE
NP-vollständig ist und V ERTEX C OVER ∈ NP gilt, ist somit V ERTEX C OVER NP-vollständig.
Präsenzaufgabe 10.3 (Turingmaschinen)
n
Entwerfen Sie eine Turingmaschine für die Sprache L = { 02 | n ∈ N } über dem Alphabet
Σ = {0} und geben Sie die Laufzeit Ihrer Turingmaschine an. Begründen Sie die Korrektheit
und die Laufzeit Ihrer Turingmaschine. Diskutieren Sie die Laufzeit Ihrer Turingmaschine im
Vergleich zu einem Algorithmus (mit RAM) für diese Sprache.
Lösung. Die Turingmaschine sieht folgendermaßen aus:
(1) Falls genau eine 0 auf dem Band steht, akzeptiere.
(2) Bewege den Kopf von links nach rechts über die Eingabe und ersetze jede zweite 0 durch
ein neues Symbol x.
(3) Falls das letzte Symbol der Eingabe nun eine 0 ist, verwerfe.
(4) Gehe zu Schritt (1).
Korrektheit: Die Sprache L besteht aus allen Wörtern, die nur Nullen enthalten und deren Länge
eine Zweierpotenz ist. Eine Zahl ist genau dann eine Zweierpotenz, wenn sie nach wiederholtem
Teilen durch 2 irgendwann eine 1 ergibt. Genau das überprüft die Turingmaschine. Wenn bei
diesem wiederholten Teilen eine ungerade Zahl auftritt, ist die Zahl keine Zweierpotenz. In
diesem Fall verwirft die Turingmaschine die Eingabe (siehe Schritt (3)).
Laufzeit: Die angegebene Turingmaschine hat eine Laufzeit von O(n log(n)), wobei n hier die
Länge der Eingabe bezeichnet. Schritt (1) und Schritt (2) benötigen jeweils eine Laufzeit von
O(n). Schritt (3) geht in konstanter Laufzeit O(1). Schritt (4) wiederholt die Schritte (1) bis (3)
bis nur noch eine 0 auf dem Band steht oder vorher abgebrochen wird. Da in jedem Durchlauf
der Schritte (1) bis (3) die Hälfte der Nullen (abgerundet) durch x Symbole ersetzt wird, steht
nach log n Durchläufen nur noch eine Null auf dem Band und es wird nach Schritt (1) akzeptiert
oder es steht nach ⌊log n⌋ 1 Durchläufen eine ungerade Anzahl von Nullen auf dem Band und
es wird nach Schritt (3) verworfen. Also gibt es maximal log n + 1 Durchläufe.
Diskussion: Ein Algorithmus mit RAM könnte folgendermaßen aussehen:
1. Zähle die Buchstaben und erhalte Anzahl m.
2. Falls log m ganzzahlig ist, akzeptiere. Sonst verwerfe.
Dieser Algorithmus hat eine Laufzeit von O(n), wobei n hier die Länge der Eingabe bezeichnet.
Ein Algorithmus mit RAM kann also den zusätzlichen Speicher nutzen, um wesentlich schneller
eine Aufgabe zu erledigen, im Vergleich zu einem Algorithmus, der auf einer Turingmaschine
läuft.
CHRISTIAN-ALBRECHTS-UNIVERSITÄT ZU KIEL
Institut für Informatik, Arbeitsgruppe Algorithmen und Komplexität
Prof. Dr. K. Jansen, B. Schumacher, C. Wambsganz
23.06.2025
Präsenzaufgaben zur Vorlesung »Analyse von Algorithmen und
Komplexität«
Blatt 11
Präsenzaufgabe 11.1 (Fragen über Fragen)
Diskutieren Sie nachfolgende Aussagen. Sind diese wahr oder falsch?
(a) Für L ∈ NP gilt, aus L ⪯ 3-SAT folgt, dass L NP-vollständig ist.
(b) Gilt für eine Sprache L, dass sowohl L ⪯ 3-SAT als auch 3-SAT ⪯ L, dann ist L NP-vollständig.
(c) Sei L NP-vollständig. Dann gilt L ∈ P genau dann, wenn P = NP.
(d) Es ist möglich, dass 3-SAT ∈ P und C LIQUE ∈
/ P.
Lösung.
/ Offensichtlich gilt L ∈ NP. Aus L ⪯ 3-SAT folgt nur,
(a) Nein, ein Gegenbeispiel ist L = 0.
dass sich L auf 3-SAT reduzieren lässt, nicht anders herum.
(b) Das ist die Definition von NP-vollständig.
(c) Korrekt, da sich jedes Problem auf L reduzieren lässt nach Def. NP-vollständig (und offen-
sichtlich P ⊆ NP).
(d) Nein, weil 3-SAT NP-vollständig ist, lässt sich C LIQUE (liegt bekannterweise in NP, ist
sogar NP-vollständig) auf 3-SAT reduzieren.
Präsenzaufgabe 11.2 (NP-Vollständigkeit)
In dem Problem C LIQUE -N OMEMBER ist ein Graph G = (V, E), ein Knoten v ∈ V sowie eine
Zahl k ∈ N gegeben und es soll entschieden werden, ob es eine Clique mit k Knoten gibt, die
den Knoten v nicht enthält.
Zeigen Sie, dass C LIQUE -N OMEMBER NP-vollständig ist, indem Sie eine Reduktion von C LI -
QUE auf C LIQUE -N OMEMBER angeben. Sie können davon ausgehen, dass C LIQUE -N OMEMBER ∈
NP ist.
Lösung. Clique-Nomember ist NP-schwer:
Sei (G = (V, E), k) eine Eingabe für das Clique Problem.
Idee: Füge einen neuen isolierten Knoten v zu G hinzu.
Wir geben zuerst die Abbildung an: Sei v ̸∈ V . Wir definieren die Eingabe für das Problem
C LIQUE -N OMEMBER als den Graphen G := (V {v}, E), den Knoten v als Knoten, der nicht
benutzt werden darf, und k als gesuchte Cliquengröße. (G , v, k) ist eine gültige Eingabe für
C LIQUE -N OMEMBER und in polynomieller Zeit berechenbar.
Wir zeigen nun: G hat eine k-Clique genau dann, wenn G eine k-Clique ohne v beinhaltet.
⇒:
Sei (G, k) eine Ja-Instanz des C LIQUE Problems. Dann enthält G eine k-Clique C. Dann ist C
auch eine k-Clique in G , da bei der Abbildung keine Kanten entfernt wurden. Da v ̸∈ V , enthält
C den Knoten v nicht. Damit ist C eine k-Clique in G ohne den Knoten v. (G , v, k) ist also eine
Ja-Instanz des C LIQUE -N OMEMBER Problems.
⇐:
Sei (G , v, k) eine Ja-Instanz des C LIQUE -N OMEMBER Problems. Dann enthält G eine k-Clique
C mit v ̸∈ C. Da bei der Abbildung an dem Graphen nichts geändert wurde, außer der Knoten v
hinzugefügt, muss C auch eine k-Clique in G sein. Damit ist (G, k) eine Ja-Instanz des C LIQUE
Problems.
Also existiert eine polynomielle Reduktion von C LIQUE auf C LIQUE -N OMEMBER. Da C LI -
QUE NP-vollständig ist und C LIQUE -N OMEMBER ∈ NP nach Aufgabenstellung gilt, ist somit
C LIQUE -N OMEMBER NP-vollständig.
Präsenzaufgabe 11.3 (NP-Vollständigkeit)
Für das Problem S UBSET S UM C ARDINALITY seien n ganze Zahlen c1 , . . . , cn ∈ N>0 (wobei n
gerade sei) sowie eine Zahl K ∈ N gegeben. Es soll entschieden werden, ob es eine Teilmenge
S ⊂ {1, . . . , n} gibt mit |S| = n/2 und ∑i∈S ci = K.
Zeigen Sie, dass das Problem S UBSET S UM C ARDINALITY NP-vollständig ist. Sie können
davon ausgehen, dass S UBSET S UM C ARDINALITY ∈ NP ist.
Lösung. Subset Sum Cardinality ist NP-schwer:
S UBSET S UM Problem: Gegeben: n ganze Zahlen c1 , . . . , cn ∈ N>0 und Zahl K ∈ N. Frage: Gibt
es eine Teilmenge S ⊆ {0, . . . , n} mit ∑i∈S ci = K?
Reduktion: Sei I = (c1 , . . . , cn , K) eine Eingabe für das S UBSET S UM Problem. Definiere ci :=
ci + 1 für i ≤ n und ci := 1 für i ∈ {n + 1, . . . , 2n}. Wir definieren die Eingabe für das Problem
S UBSET S UM C ARDINALITY als I = (c1 , . . . , c2n , K + n).
Wir zeigen nun: Es gibt eine Lösung für I genau dann, wenn es eine Lösung für I gibt.
⇒:
Sei I eine Ja-Instanz des S UBSET S UM Problems. Dann gibt es eine Menge S ⊆ {1, . . . , n} mit
∑i∈S ci = K. Es gilt also ∑i∈S ci = ∑i∈S (ci + 1) = K + |S|. Sei nun S = {n + 1, . . . , 2n |S|}. Es
gilt |S S | = n und ∑i∈SS ci = ∑i∈S ci + ∑i∈S ci = ∑i∈S (ci + 1) + ∑i∈S 1 = K + |S| + (n |S|) =
K + n. S S ist also eine Lösung für I und I damit eine Ja-Instanz.
⇐:
Es sei I eine Ja-Instanz. Dann gibt es also eine Menge S ⊆ {1, . . . , 2n} mit ∑i∈S ci = K + n und
|S | = n. Sei S := S ∩{1, . . . , n}. Es gilt ∑i∈S ci ∑i∈S ci = n|S|, da die Items in {n+1, . . . , 2n}
das Gewicht 1 haben, und genau n |S| Items übrig sind. Damit gilt ∑i∈S (ci + 1) = ∑i∈S ci =
K + n (n |S|) = K + |S|, woraus folgt, dass ∑i∈S ci = K. Damit ist S eine Lösung für I und
somit I eine Ja-Instanz.
Laufzeit: Die Reduktion kann naiv in einer Zeit von O(n) implementiert werden. Das ist eine
polynomielle Laufzeit.
Also existiert eine polynomielle Reduktion von S UBSET S UM auf S UBSET S UM C ARDINALI -
TY . Da nach Skript S UBSET S UM NP-vollständig ist und S UBSET S UM C ARDINALITY ∈ NP
nach Aufgabenstellung gilt, ist somit S UBSET S UM C ARDINALITY NP-vollständig.
CHRISTIAN-ALBRECHTS-UNIVERSITÄT ZU KIEL
Institut für Informatik, Arbeitsgruppe Algorithmen und Komplexität
Prof. Dr. K. Jansen, B. Schumacher, C. Wambsganz
30.06.2025
Präsenzaufgaben zur Vorlesung »Analyse von Algorithmen und
Komplexität«
Blatt 12
Präsenzaufgabe 12.1
Im Folgenden sollen Sie herausfinden (und begründen), welche Lower Bounds sich über ETH
und die Reduktionen aus der Vorlesung ergeben. Nutzen Sie hierfür auch das Sparsification
Lemma.
Welche Schranken für die Laufzeit in Abhängigkeit von den natürlichen Parametern (Anzahl
Knoten, Anzahl Kanten, Anzahl Items, Anzahl unterschiedlicher Items, Anzahl Tripel, Kardi-
nalität der Grundmenge, Anzahl unterschiedlicher Tripel, Anzahl unterschiedlicher auftretender
Zahlen, größte auftretende Zahl, . . . ) ergeben sich jeweils aus folgenden (zum Großteil aus der
Vorlesung bekannten) Reduktionen (Laufzeitfaktoren, die polynomiell in der Eingabekodierung
sind, dürfen vernachlässigt werden)?
Schätzen Sie dazu zunächst die verschiedenen Parameter der resultierenden Instanz mithilfe der
Parameter der eingegebenen Instanz geeignet ab.
1. 3-SAT ⪯ k-C LIQUE
2. k-C LIQUE ⪯ k-I NDEPENDENT S ET (Problemdefinition für die Antwort nicht relevant; in
der Reduktion wird der Graph G = (V, E) auf den Komplementgraph (V, E) abgebildet)
3. SAT ⪯ 3-D IMENSIONAL M ATCHING (Hinweis: 3-SAT ⊆ SAT)
4. 3-D IMENSIONAL M ATCHING ⪯ 3-E XACT C OVER
5. 3-E XACT C OVER ⪯ S UBSET S UM
Lösung. Angenommen ETH gilt. Das Sparsification Lemma impliziert, dass man 3-SAT nicht
in 2o(m) · |I|O(1) lösen kann.
1. In der Reduktion werden |V | = O(m) Knoten und |E| = O(m2 ) Kanten erstellt und k = m.
Außerdem gilt n ≤ 3m ∈ O(m).
• Lower bound bzgl. der Anzahl an Knoten: 2o(|V |) · |I|O(1)
Angenommen, es existiert ein Algorithmus, der k-Clique in 2o(|V |) ·|I|O(1) löst. Dann
gibt es auch einen Algorithmus, der 3-SAT in 2o(m) · |I|O(1) löst (Reduktion auf k-
Clique + k-Cliquen Algorithmus). Nach dem Sparsification Lemma gibt es dies nur,
wenn die ETH nicht gilt.
• Lower bound bzgl. der Anzahl an Kanten: 2o( |E|) · |I|O(1) √
Angenommen, es existiert ein Algorithmus, der k-Clique in 2o( |E|) · |I|O(1) löst.
Da |E| ∈ O(m2 ), gibt es dann auch einen Algorithmus, der 3-SAT in 2o(m) · |I|O(1)
löst (Reduktion auf k-Clique + k-Cliquen Algorithmus). Nach dem Sparsification
Lemma gibt es dies nur, wenn die ETH nicht gilt.
• Lower bound bzgl. k: 2o(k) · |I|O(1)
Angenommen, es existiert ein Algorithmus, der k-Clique in 2o(k) · |I|O(1) löst. Dann
gibt es auch einen Algorithmus, der 3-SAT in 2o(m) · |I|O(1) löst (Reduktion auf k-
Clique + k-Cliquen Algorithmus). Nach dem Sparsification Lemma gibt es dies nur,
wenn die ETH nicht gilt.
2. In der Reduktion werden genau |V | Knoten und maximal |E| = O(|V |2 ) Kanten erstellt.
• Lower bound bzgl. der Anzahl an Knoten: 2o(|V |) · |I|O(1)
Angenommen, es existiert ein Algorithmus, der k-Independent-Set in 2o(|V |) · |I|O(1)
löst. Dann gibt es auch einen Algorithmus, der k-Clique in 2o(|V |) · |I|O(1) löst (Re-
duktion auf k-Independent-Set + k-Independent-Set Algorithmus). Nach Aufgaben-
teil 1, gibt es diesen nur, wenn die ETH nicht gilt.
o( |E|)
• Lower bound bzgl. der Anzahl an Kanten: 2 · |I|O(1) √
Angenommen, es existiert ein Algorithmus, der k-Independen-Set in 2o( |E|) ·|I|O(1)
löst. Da |E| ≤ |V |2 , gibt es dann auch einen Algorithmus, der k-Clique in 2o(|V |) ·
|I|O(1) löst (Reduktion auf k-Independent-Set + k-Independent-Set Algorithmus).
Nach Aufgabenteil 1, gibt es diesen nur, wenn die ETH nicht gilt.
3. In der Reduktion werden Mengen mit |U| = |V | = |W | = O(mn) Elementen erstellt und
|T | = O(m2 n2 ) Tupel. Es gilt n ≤ 3m ∈ O(m) für 3-SAT.
• Lower bound bzgl. der Anzahl an Elementen: 2o( |V |) · |I|O(1) √
Angenommen, es existiert ein Algorithmus, der 3-Dimensional-Matching in 2o( |V |) ·
|I|O(1) löst. Da |V | ∈ O(mn) ≤ O(m2 ), gibt es dann auch einen Algorithmus, der
3-SAT in 2o(m) · |I|O(1) löst (Reduktion von SAT auf 3-Dimensional-Matching an-
gewandt auf 3-SAT Instanzen + 3-Dimensional-Matching Algorithmus). Nach dem
Sparsification Lemma geht dies nur, wenn die ETH nicht gilt.
4
• Lower bound bzgl. der Kardinalität von T : 2o( |T |) · |I|O(1) √
4
Angenommen, es existiert ein Algorithmus, der 3-Dimensional-Matching in 2o( |T |) ·
|I|O(1) löst. Da |T | ∈ O(m2 n2 ) ≤ O(m4 ), gibt es dann auch einen Algorithmus, der
3-SAT in 2o(m) · |I|O(1) löst (Reduktion von SAT auf 3-Dimensional-Matching an-
gewandt auf 3-SAT Instanzen + 3-Dimensional-Matching Algorithmus). Nach dem
Sparsification Lemma geht dies nur, wenn die ETH nicht gilt.
4. Das 3-dimensionale Matching ist ein Spezialfall vom 3-Exact Cover. Daher gelten die
Lower Bounds von 3-dimensionalem Matching auch für 3-Exact Cover. Da |U| = 3m ∈
O(|V |), ergibt sich, dass es für 3-Exact Cover unter der ETH keinen Algorithmus √ mit
o( m) O(1) o( |V |)
Laufzeit 2 · |I| geben kann, da sonst 3-dimensionales Matching in 2 ·
|I|O(1) lösbar wäre. Und da |F| = |T |, √ ergibt sich, dass es für 3-Exact Cover unter der ETH
o( 4 |F|)
keinen Algorithmus
√ mit Laufzeit 2 · |I|O(1) geben kann, da sonst 3-dimensionales
4
Matching in 2o( |T |) · |I|O(1) lösbar wäre.
4
5. In der Reduktion wird n = |F| gesetzt. Lower bound bzgl. n: 2o( n)√· |I|O(1)
4
Angenommen, es existiert ein Algorithmus, der SubsetSum in 2o( n) · |I|O(1) löst. Dann
4
|F|)
gibt es auch einen Algorithmus, der 3-Exact Cover in 2o( · |I|O(1) löst. Nach Aufga-
benteil 4, gibt es diesen nur, wenn die ETH nicht gilt.
CHRISTIAN-ALBRECHTS-UNIVERSITÄT ZU KIEL
Institut für Informatik, Arbeitsgruppe Algorithmen und Komplexität
Prof. Dr. K. Jansen, B. Schumacher, C. Wambsganz
07.07.2025
Präsenzaufgaben zur Vorlesung »Analyse von Algorithmen und
Komplexität«
Blatt 13
Präsenzaufgabe 13.1 (Vertex Cover)
Ein Vertex Cover in einem Graphen G = (V, E) ist eine Menge von Knoten C ⊆ V , so dass für
jede Kante {v, w} ∈ E gilt, dass v ∈ C oder w ∈ C. Beim Optimierungsproblem Minimum Vertex
Cover soll die kardinalitätskleinste Menge C ⊆ V gefunden werden, die ein Vertex Cover von
G ist.
Gegeben ist der folgende approximative Algorithmus mit Güte 2 für VertexCover:
Idee: Wähle eine beliebige Kante. Wenn einer der beiden Endknoten bereits in C ist, verwirf
diese Kante. Sonst, füge beide Endknoten zur Menge C hinzu. Mache dies für jede Kante.
Algorithmus 2A PPROX CV(G = (V, E))
1 C = {}
2 foreach {u, v} ∈ E do
3 if u ̸∈ C and v ̸∈ C then
4 C = C {u, v}
5 fi
6 od
7 return C
Beweisen Sie die Güte.
Lösung. Beweis: Sei C ein minimales Vertex Cover. Wir zeigen |C| ≤ 2|C |.
Sei A die Menge der Kanten, für die beide Endknoten zu C hinzugefügt wurden. Es gilt |C| =
2|A|, da beide Knoten jeder ausgewählten Kante zu C hinzugefügt wurden und alle anderen
Kanten nicht weiter betrachtet werden. Damit folgt auch, dass keine zwei Kanten aus A einen
Knoten gemeinsam haben können. Sei nun C ein minimales Vertex Cover. C enthält nach
Definition mindestens einen Knoten jeder Kante, also insbesondere einen Knoten jeder Kante
aus A. Da keine zwei Kanten aus A vom gleichen Knoten aus C überdeckt werden können, gilt
|C | ≥ |A|. Es folgt |C| = 2|A| ≤ 2|C |. Die Lösung C von 2ApproxCV ist also maximal doppelt
so groß wie die optimale Lösung C . Damit berechnet der Algorithmus eine 2-Approximation.
Präsenzaufgabe 13.2 (TSP)
In der Vorlesung haben Sie zwei approximative Algorithmen für das TSP-Problem kennen ge-
lernt. Der erste Algorithmus ∆T SP1 hat eine Approximationsrate von 2, während der zweite
Algorithmus ∆T SP2 von Christofides eine Approximationsrate von 3/2 besitzt. In dieser Übung
soll gezeigt werden, dass die zweite Rate scharf ist. D.h., dass es eine Folge von Instanzen gibt,
auf denen der Algorithmus immer näher an diese Rate heran kommt.
Wir betrachten den vollständigen Graphen Kn = (V, E) für ein n ∈ N≥6 mit n mod 4 = 2 und
die nachfolgende Gewichtsfunktion d2 : E → R+ . Zeigen Sie, dass
(a) OPT((Kn , d2 )) = n gilt und
(b) eine mögliche Ausführung von ∆TSP2 existiert, die auf Eingabe (Kn , d2 ) eine Tour der
Länge (n 1) + n/2 berechnet.
1 1 1 1
1 3 5 7 9 n1
1 1 1 1 1 1
1 1 1 1
2 4 6 8 10 n
Wir definieren E2 := {{i, i + 2}|i ∈ {1, . . . , n 2}} {{2i + 1, 2i + 2}|i ∈ {0, . . . , n/2 1}} und
d2 , so dass alle Kanten aus E2 auf 1 abgebildet werden und alle Kanten aus E \ E2 auf den Wert
des kürzesten Wegs zwischen diesen Knoten in dem oben abgebildeten Graphen abgebildet
werden.
Lösung. Wichtig bei dieser Aufgabe ist der folgende Fakt: Bei beiden Algorithmen wird ir-
gendein MST genommen und dann irgendeine Reihenfolge der Knoten. Typischerweise wird
diese Reihenfolge durch bestimmte Knoten- oder Kanten-Labels festgelegt (wird zum Beispiel
der Kruskal-Algorithmus zur Konstruktion des MST genutzt, so werden dort die Kanten nach
Gewicht sortiert. Kanten gleichen Gewichts müssen also auch noch durch irgendeine Reihen-
folge festgelegt werden.) Wir gehen also davon aus, dass wir diese Reihenfolge auch wählen
können. Sollte der Algorithmus also einen “besseren” MST oder eine “bessere” Knotenreihen-
folge nutzen, passen wir die Labels dementsprechend an.
Ein möglicher MST ist durch die unten angegebene Zick-Zack-Tour gegeben oder formal durch
h [ i
ET = {{ j, j + 1}, { j + 1, j + 3}, { j + 3, j + 2}, { j + 2, j + 4}}} {{n 1, n}}.
1≤ j≤n5
j mod 4=1
1 1 1 1
1 3 5 7 n3 n1
1 1 1 1 1 1
1 1 1 1
2 4 6 8 n2 n
Es gilt d(T ) = n 1 für die Kosten d(T ) des MST T , da nur Kanten mit Kosten 1 gewählt
werden und n 1 Kanten gewählt werden müssen, um den Zusammenhang sicher zu stellen.
Da jeder Knoten im MST außer 1 und n geraden Grad hat, ist das Matching K sehr simpel,
nämlich K = {{1, n}}. Die Kosten der Kante {1, n} sind n/2 (der kürzeste Pfad von 1 zu n)
und somit gilt d(K) = n/2 für die Kosten von K. Die Rundreise R entspricht einfach dem MST
vereinigt mit der Matchingkante und hat somit Kosten: d(R) = n 1 + n/2.
Die optimale Tour würde erst “oben” und dann “unten” entlang laufen, also von Knoten 1 zu
Knoten 3 gehen, dann zu Knoten 5 und so weiter, bis Knoten n 1 erreicht wurde. Danach
folgen Knoten n, n 2, n 4 und so weiter. Diese Tour hat offensichtlich Kosten n, da wir n
Kanten mit Gewicht 1 nutzen. Somit gilt OPT((Kn , d2 )) = n und wir haben eine Rate von
∆T SP2 ((Kn , d2 )) n 1 + n/2 1
= = 3/2 ≤ 3/2.
OPT((Kn , d2 )) n n
Da limn→∞ 3/2 n1 = 3/2, kommen wir der Rate von 3/2 also beliebig nahe.