update
This commit is contained in:
@@ -24,7 +24,7 @@ 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).
|
# Deckel für gleichzeitige CLI-Agenten-Prozesse (über alle Generierungen hinweg).
|
||||||
# Eigene Spur für interaktive Aufrufe (Chat, Elemente), damit sie nicht hinter
|
# Eigene Spur für interaktive Aufrufe (Chat, Elemente), damit sie nicht hinter
|
||||||
# laufenden Writern in der Warteschlange hängen.
|
# laufenden Writern in der Warteschlange hängen.
|
||||||
MAX_CONCURRENT_AGENTS = 20
|
MAX_CONCURRENT_AGENTS = 30
|
||||||
MAX_CONCURRENT_INTERACTIVE = 4
|
MAX_CONCURRENT_INTERACTIVE = 4
|
||||||
|
|
||||||
# Grace-Fenster der Konsens-Races (Bausteine, Guide, OnePager): Nach dem ersten
|
# Grace-Fenster der Konsens-Races (Bausteine, Guide, OnePager): Nach dem ersten
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
Auswahl: 5 Agenten (min. 3, Grace) → Code-Voting (Mehrheit = Konsens) →
|
Auswahl: 5 Agenten (min. 3, Grace) → Code-Voting (Mehrheit = Konsens) →
|
||||||
Mapping-Agent sortiert Strittiges → Klärungs-Loop (max. KONSENS_MAX_RUNDEN).
|
Mapping-Agent sortiert Strittiges → Klärungs-Loop (max. KONSENS_MAX_RUNDEN).
|
||||||
Gliederung: 5 Vorschläge (min. 3, Grace) → ein Judge wählt und kombiniert.
|
Gliederung: 3 Vorschläge (Grace) → ein Judge wählt und kombiniert.
|
||||||
Schreiben: Writer pro Chunk. Lese-Prüfung: Check→Fix-Loop (max. Runden-Cap),
|
Schreiben: Writer je Baustein. Lese-Prüfung: Check→Fix (eine Runde),
|
||||||
Folgerunden prüfen nur ersetzte Sections; danach bleiben Beanstandungen stehen.
|
Folgerunden prüfen nur ersetzte Sections; danach bleiben Beanstandungen stehen.
|
||||||
Schritt-Dateien bleiben liegen → Abbruch erhält Fortschritt, ▶ setzt am offenen Schritt fort.
|
Schritt-Dateien bleiben liegen → Abbruch erhält Fortschritt, ▶ setzt am offenen Schritt fort.
|
||||||
"""
|
"""
|
||||||
@@ -46,6 +46,10 @@ GUIDE_STEPS = ("Auswahl", "Gliederung", "Inhalte", "Inhalts-Check", "Schreiben",
|
|||||||
WRITER_SECTIONS = 30
|
WRITER_SECTIONS = 30
|
||||||
WRITER_MAX = 20
|
WRITER_MAX = 20
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
|
||||||
def _load_subbausteine(topic: str) -> dict[str, list[dict]]:
|
def _load_subbausteine(topic: str) -> dict[str, list[dict]]:
|
||||||
"""Sidecar laden: {Baustein-Titel: [{titel, stufe}, …]}. Fehlt sie → {} (Fallback)."""
|
"""Sidecar laden: {Baustein-Titel: [{titel, stufe}, …]}. Fehlt sie → {} (Fallback)."""
|
||||||
@@ -85,7 +89,7 @@ def _guide_files(content_path: Path) -> dict:
|
|||||||
for n in runden
|
for n in runden
|
||||||
},
|
},
|
||||||
"auswahl_mapping": {n: d / f"{stem}.auswahl-mapping-r{n}.json" for n in runden},
|
"auswahl_mapping": {n: d / f"{stem}.auswahl-mapping-r{n}.json" for n in runden},
|
||||||
"gliederung_slots": [d / f"{stem}.gliederung-{i}.json" for i in (1, 2, 3, 4, 5)],
|
"gliederung_slots": [d / f"{stem}.gliederung-{i}.json" for i in (1, 2, 3)],
|
||||||
"gliederung": d / f"{stem}.gliederung.json", # Judge-Ausgabe
|
"gliederung": d / f"{stem}.gliederung.json", # Judge-Ausgabe
|
||||||
# chunk-/lese-check-/fix-Dateien sind dynamisch:
|
# chunk-/lese-check-/fix-Dateien sind dynamisch:
|
||||||
# {stem}.chunk-i.md, {stem}.lese-check-r{n}-{i}.json, {stem}.fix-r{n}-{i}.md
|
# {stem}.chunk-i.md, {stem}.lese-check-r{n}-{i}.json, {stem}.fix-r{n}-{i}.md
|
||||||
@@ -97,6 +101,28 @@ 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]
|
return [p for p in content_path.parent.glob(f"{content_path.stem}.*") if p != content_path]
|
||||||
|
|
||||||
|
|
||||||
|
# Slot-Datei-Globs je Schritt (Index = GUIDE_STEPS). Stem-verankert, kollisionsfrei.
|
||||||
|
_STEP_GLOBS = (
|
||||||
|
("auswahl-*", "auswahl-mapping-*"), # 0 Auswahl (deterministisch → meist leer)
|
||||||
|
("gliederung*",), # 1 Gliederung
|
||||||
|
("inhalt-chunk-*",), # 2 Inhalte
|
||||||
|
("inhalt-check-*", "inhalt-fix-*"), # 3 Inhalts-Check
|
||||||
|
("chunk-*",), # 4 Schreiben
|
||||||
|
("lese-check-*", "fix-r*"), # 5 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)
|
||||||
|
|
||||||
|
|
||||||
def _resolve_auswahl(data, entries: dict[int, str], k_min: int, k_max: int) -> list[int] | None:
|
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."""
|
"""{"bausteine": [Titel]} → Nummern; None bei Schema-Verstoß/Drift/falschem Umfang."""
|
||||||
if not isinstance(data, dict) or not isinstance(data.get("bausteine"), list):
|
if not isinstance(data, dict) or not isinstance(data.get("bausteine"), list):
|
||||||
@@ -455,12 +481,12 @@ async def _generate_sections(
|
|||||||
soll = len(sel_entries)
|
soll = len(sel_entries)
|
||||||
sel_liste = "\n".join(f"- {t}" for t in sel_entries.values())
|
sel_liste = "\n".join(f"- {t}" for t in sel_entries.values())
|
||||||
|
|
||||||
# Schritt 1: Gliederung — bis zu 5 Vorschläge (Grace), ein Judge wählt. Bricht NIE ab:
|
# Schritt 1: Gliederung — bis zu 3 Vorschläge (Grace), ein Judge wählt. Bricht NIE ab:
|
||||||
# 0 gültige → Code-Fallback, 1 → direkt, ≥2 → Judge (mit Vorschlag als Rückfall).
|
# 0 gültige → Code-Fallback, 1 → direkt, ≥2 → Judge (mit Vorschlag als Rückfall).
|
||||||
# Gültiges gliederung.json (auch aus Altläufen) überspringt den Schritt.
|
# Gültiges gliederung.json (auch aus Altläufen) überspringt den Schritt.
|
||||||
plan = _resolve_gliederung(_json_datei(files["gliederung"]), sel_entries, soll, soll)
|
plan = _resolve_gliederung(_json_datei(files["gliederung"]), sel_entries, soll, soll)
|
||||||
if plan is None:
|
if plan is None:
|
||||||
await _set_step(guide_id, 1, "Gliederungs-Vorschläge (5 Agenten)…")
|
await _set_step(guide_id, 1, "Gliederungs-Vorschläge (3 Agenten)…")
|
||||||
files["gliederung"].unlink(missing_ok=True)
|
files["gliederung"].unlink(missing_ok=True)
|
||||||
vorschlaege: list[list[dict]] = []
|
vorschlaege: list[list[dict]] = []
|
||||||
offen = []
|
offen = []
|
||||||
@@ -706,11 +732,11 @@ async def _generate_sections(
|
|||||||
)
|
)
|
||||||
|
|
||||||
scope = chunk_nums
|
scope = chunk_nums
|
||||||
for runde in range(1, KONSENS_MAX_RUNDEN + 1):
|
for runde in range(1, LESE_RUNDEN + 1):
|
||||||
check_paths = [content_path.parent / f"{content_path.stem}.lese-check-r{runde}-{i}.json" for i in range(1, writer_count + 1)]
|
check_paths = [content_path.parent / f"{content_path.stem}.lese-check-r{runde}-{i}.json" for i in range(1, writer_count + 1)]
|
||||||
offen_checks = [i for i, p in enumerate(check_paths) if scope[i] and _lese_probleme_schema(_json_datei(p)) is None]
|
offen_checks = [i for i, p in enumerate(check_paths) if scope[i] and _lese_probleme_schema(_json_datei(p)) is None]
|
||||||
if offen_checks:
|
if offen_checks:
|
||||||
await _set_step(guide_id, 5, f"Prüfe Lesbarkeit (Runde {runde}/{KONSENS_MAX_RUNDEN})…")
|
await _set_step(guide_id, 5, "Prüfe Lesbarkeit…")
|
||||||
slots = [{
|
slots = [{
|
||||||
"key": f"{guide_id}-lese-check-r{runde}-{i + 1}",
|
"key": f"{guide_id}-lese-check-r{runde}-{i + 1}",
|
||||||
"prompt": _prompt(
|
"prompt": _prompt(
|
||||||
@@ -787,8 +813,8 @@ async def _generate_sections(
|
|||||||
_log(topic, f"Lese-Prüfung Runde {runde}: {len(ersetzt)} Section(s) überarbeitet")
|
_log(topic, f"Lese-Prüfung Runde {runde}: {len(ersetzt)} Section(s) überarbeitet")
|
||||||
if not ersetzt:
|
if not ersetzt:
|
||||||
break
|
break
|
||||||
if runde == KONSENS_MAX_RUNDEN:
|
if runde == LESE_RUNDEN:
|
||||||
_log(topic, f"Lese-Prüfung: Cap erreicht — letzte Überarbeitung bleibt ungeprüft")
|
_log(topic, f"Lese-Prüfung: 1 Runde — Überarbeitung bleibt ungeprüft")
|
||||||
break
|
break
|
||||||
scope = [[num for num in nums if num in ersetzt] for nums in chunk_nums]
|
scope = [[num for num in nums if num in ersetzt] for nums in chunk_nums]
|
||||||
|
|
||||||
@@ -835,7 +861,7 @@ async def reconcile_guides() -> None:
|
|||||||
await update_guide(g["id"], status="error", error_msg="Inhalt fehlt — neu generieren", updated_at=now)
|
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:
|
async with _semaphore:
|
||||||
now = datetime.now(timezone.utc).isoformat()
|
now = datetime.now(timezone.utc).isoformat()
|
||||||
await update_guide(guide_id, status="generating", progress="Starte…", updated_at=now)
|
await update_guide(guide_id, status="generating", progress="Starte…", updated_at=now)
|
||||||
@@ -851,9 +877,12 @@ async def generate_guide(guide_id: str, topic: str, format_name: str, instructio
|
|||||||
if project:
|
if project:
|
||||||
await asyncio.to_thread(_pdfs_konvertieren, 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.
|
# 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):
|
for p_alt in guide_slot_dateien(content_path):
|
||||||
p_alt.unlink(missing_ok=True)
|
p_alt.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class GuideCreateRequest(BaseModel):
|
|||||||
format: FormatType
|
format: FormatType
|
||||||
instructions: str = Field(default="", max_length=2000)
|
instructions: str = Field(default="", max_length=2000)
|
||||||
provider: ProviderType = "claude"
|
provider: ProviderType = "claude"
|
||||||
|
ab_step: int | None = Field(default=None, ge=0, le=5) # Re-Run ab Guide-Schritt (0 Auswahl … 5 Lese-Prüfung); None = voll/Resume
|
||||||
|
|
||||||
|
|
||||||
class TopicCreateRequest(BaseModel):
|
class TopicCreateRequest(BaseModel):
|
||||||
|
|||||||
@@ -458,7 +458,7 @@ async def create(req: GuideCreateRequest):
|
|||||||
"updated_at": now,
|
"updated_at": now,
|
||||||
}
|
}
|
||||||
await create_guide(guide)
|
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
|
return guide
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -283,7 +283,7 @@ function handleOpenBausteineView() {
|
|||||||
previewGuide.value = null
|
previewGuide.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleFormatClick({ format, instructions }) {
|
async function handleFormatClick({ format, instructions, abStep = null }) {
|
||||||
if (!selectedTopic.value) return
|
if (!selectedTopic.value) return
|
||||||
// Kein Duplikat-Start: läuft für Thema+Format schon eine Generierung, ignorieren
|
// Kein Duplikat-Start: läuft für Thema+Format schon eine Generierung, ignorieren
|
||||||
const running = guides.value.some(
|
const running = guides.value.some(
|
||||||
@@ -293,7 +293,7 @@ async function handleFormatClick({ format, instructions }) {
|
|||||||
if (running) return
|
if (running) return
|
||||||
uiError.value = null
|
uiError.value = null
|
||||||
try {
|
try {
|
||||||
await apiCreate(selectedTopic.value, format, instructions, provider.value)
|
await apiCreate(selectedTopic.value, format, instructions, provider.value, abStep)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
uiError.value = e.message
|
uiError.value = e.message
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -23,11 +23,11 @@ export async function fetchGuideLocks(topic) {
|
|||||||
return res.json()
|
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`, {
|
const res = await fetch(`${BASE}/guides`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
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)
|
return jsonOrThrow(res)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue'
|
import { ref, reactive, computed } from 'vue'
|
||||||
import { useConfirm } from '../composables/useConfirm.js'
|
import { useConfirm } from '../composables/useConfirm.js'
|
||||||
import { fetchQuelle } from '../api.js'
|
import { fetchQuelle } from '../api.js'
|
||||||
|
|
||||||
@@ -144,6 +144,21 @@ function guideSteps(format) {
|
|||||||
return labels.map((label) => ({ label, state: 'pending' }))
|
return labels.map((label) => ({ label, state: 'pending' }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-Run ab Guide-Schritt (1-basierte Kugel je Format). null = voll/Resume.
|
||||||
|
const gewaehlterStep = reactive({})
|
||||||
|
// Kugeln klickbar, sobald der Guide (teil-)gebaut ist und gerade nicht generiert wird.
|
||||||
|
function guideWaehlbar(format) {
|
||||||
|
const st = guideStatus(format)
|
||||||
|
return (st === 'done' || abgebrochen(format)) && st !== 'generating' && st !== 'queued'
|
||||||
|
}
|
||||||
|
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) {
|
function errorMsg(format) {
|
||||||
const latest = props.latestByFormat[format]
|
const latest = props.latestByFormat[format]
|
||||||
if (latest?.status !== 'error' || props.dismissedErrors.has(latest.id)) return ''
|
if (latest?.status !== 'error' || props.dismissedErrors.has(latest.id)) return ''
|
||||||
@@ -173,7 +188,10 @@ function playLock(format) {
|
|||||||
|
|
||||||
function handlePlay(format) {
|
function handlePlay(format) {
|
||||||
if (playLock(format)) return
|
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
|
// Flash-Message-Verhalten: × blendet nur aus, nichts wird gelöscht
|
||||||
@@ -413,12 +431,13 @@ function saveQuelle() {
|
|||||||
>Pausiert</span>
|
>Pausiert</span>
|
||||||
<span class="step-dots" v-if="guideSteps(f.key).length">
|
<span class="step-dots" v-if="guideSteps(f.key).length">
|
||||||
<span
|
<span
|
||||||
v-for="s in guideSteps(f.key)"
|
v-for="(s, i) in guideSteps(f.key)"
|
||||||
:key="s.label"
|
:key="s.label"
|
||||||
class="step-dot"
|
class="step-pill"
|
||||||
:class="s.state"
|
: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"
|
:title="(s.state === 'active' ? (latestByFormat[f.key]?.progress || s.label) : s.label) + (guideWaehlbar(f.key) ? ' — Klick: ab hier neu' : '')"
|
||||||
></span>
|
@click.stop="guideStepKlick(f.key, i + 1)"
|
||||||
|
>{{ i + 1 }}</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="panel-toggle" :class="{ open: isOpen('fmt-' + f.key) }" title="Aktionen" @click.stop="togglePanel('fmt-' + f.key)">▾</button>
|
<button class="panel-toggle" :class="{ open: isOpen('fmt-' + f.key) }" title="Aktionen" @click.stop="togglePanel('fmt-' + f.key)">▾</button>
|
||||||
@@ -433,7 +452,7 @@ function saveQuelle() {
|
|||||||
:title="playLock(f.key) || (abgebrochen(f.key) ? 'Fortsetzen' : 'Generieren')"
|
:title="playLock(f.key) || (abgebrochen(f.key) ? 'Fortsetzen' : 'Generieren')"
|
||||||
:disabled="!!playLock(f.key)"
|
:disabled="!!playLock(f.key)"
|
||||||
@click="handlePlay(f.key)"
|
@click="handlePlay(f.key)"
|
||||||
>{{ abgebrochen(f.key) ? 'Fortsetzen' : guideStatus(f.key) === 'done' ? 'Neu generieren' : 'Generieren' }}</button>
|
>{{ gewaehlterStep[f.key] ? `Ab «${gewaehltesStepLabel(f.key)}» neu` : abgebrochen(f.key) ? 'Fortsetzen' : guideStatus(f.key) === 'done' ? 'Neu generieren' : 'Generieren' }}</button>
|
||||||
<button
|
<button
|
||||||
v-if="guideStatus(f.key) !== 'none' || abgebrochen(f.key)"
|
v-if="guideStatus(f.key) !== 'none' || abgebrochen(f.key)"
|
||||||
class="panel-btn danger"
|
class="panel-btn danger"
|
||||||
|
|||||||
Reference in New Issue
Block a user