update
This commit is contained in:
@@ -21,7 +21,8 @@ from agents import kill_process
|
||||
from config import KONSENS_GRACE, KONSENS_MAX_RUNDEN, DEFAULT_PROVIDER
|
||||
from fsutil import atomic_write_text, atomic_write_json
|
||||
from jsonio import read_json_file as _json_datei
|
||||
from paths import arbeit_dir, bausteine_path, project_dir, subbausteine_path
|
||||
from paths import arbeit_dir, bausteine_path, project_dir, subbausteine_path, quelle_path, quelle_crawl_dir, safe_ordner
|
||||
from crawl import crawl
|
||||
from pipeline import (
|
||||
CANCELLED, FAILED, GenContext, _extra, _log, _prompt, _race, _rest_schema,
|
||||
_runde_schema, _semaphore, _str_liste, _stufen_schema, _timeout, run_single_slot,
|
||||
@@ -52,20 +53,45 @@ BAUSTEINE_STEPS = (
|
||||
)
|
||||
|
||||
|
||||
def lade_quelle(topic: str) -> dict:
|
||||
"""Persistierte Quellen-Wahl lesen. Fallback (Alt-Themen ohne quelle.json):
|
||||
existiert projects/<topic> → projekt, sonst thema."""
|
||||
q = _json_datei(quelle_path(topic))
|
||||
if isinstance(q, dict) and q.get("type") in ("thema", "projekt", "uni", "link"):
|
||||
return q
|
||||
if project_dir(topic).is_dir():
|
||||
return {"type": "projekt", "ort": f"projects/{topic}", "spec": ""}
|
||||
return {"type": "thema", "ort": "", "spec": ""}
|
||||
|
||||
|
||||
def quelle_ordner(topic: str) -> Path | None:
|
||||
"""Ordner-Quelle (projekt/uni → Pfad, link → Crawl-Ordner) — sonst None (thema)."""
|
||||
q = lade_quelle(topic)
|
||||
if q["type"] == "link":
|
||||
return quelle_crawl_dir(topic)
|
||||
if q["type"] in ("projekt", "uni"):
|
||||
return safe_ordner(q.get("ort", ""))
|
||||
return None
|
||||
|
||||
|
||||
def _crawl_fertig(topic: str) -> bool:
|
||||
return (quelle_crawl_dir(topic) / ".done").exists() # Marker erst bei sauberem Abschluss
|
||||
|
||||
|
||||
def _bausteine_steps(topic: str) -> tuple:
|
||||
"""Projekte haben einen zusätzlichen Schritt (Ergänzung), nach der Klärung eingefügt.
|
||||
"""Schritte je Quelle: link bekommt vorne „Quelle laden", projekt zusätzlich „Ergänzung".
|
||||
|
||||
Subbausteine + Stufen sind je drei Phasen (Finden, Wählen, Klären). Pro Phase
|
||||
laufen alle Pakete parallel; der Schritt bleibt, bis das letzte Paket fertig ist.
|
||||
"""
|
||||
q = lade_quelle(topic)
|
||||
base = ("Recherche", "Konsolidierung", "Klärung")
|
||||
rest = (
|
||||
"Subbausteine finden", "Subbausteine wählen", "Subbausteine klären",
|
||||
"Stufen finden", "Stufen wählen", "Stufen klären",
|
||||
)
|
||||
if project_dir(topic).is_dir():
|
||||
return base + ("Ergänzung",) + rest
|
||||
return base + rest
|
||||
mitte = base + (("Ergänzung",) if q["type"] == "projekt" else ()) + rest
|
||||
return (("Quelle laden",) if q["type"] == "link" else ()) + mitte
|
||||
|
||||
|
||||
def _step_idx(topic: str, name: str) -> int:
|
||||
@@ -111,18 +137,21 @@ def cancel_bausteine(topic: str) -> bool:
|
||||
def _resume_step(topic: str) -> int:
|
||||
"""Erster noch offener Schritt anhand der persistierten Zwischendateien."""
|
||||
files = _bausteine_files(topic)
|
||||
q = lade_quelle(topic)
|
||||
if q["type"] == "link" and not _crawl_fertig(topic):
|
||||
return _step_idx(topic, "Quelle laden")
|
||||
if sum(p.exists() for p in files["recherche"]) < 3:
|
||||
return 0
|
||||
return _step_idx(topic, "Recherche")
|
||||
if not files["recherche_mapping"].exists():
|
||||
return 1
|
||||
return _step_idx(topic, "Konsolidierung")
|
||||
mapping = _mapping_schema(_json_datei(files["recherche_mapping"]))
|
||||
geklaert = mapping is not None and (
|
||||
not mapping[1] # kein strittiger Rest
|
||||
or any((r := _runde_schema(_json_datei(p))) is not None and not r[1] for p in files["mapping"].values())
|
||||
)
|
||||
if not geklaert:
|
||||
return 2
|
||||
if project_dir(topic).is_dir() and not files["ergaenzung"].exists():
|
||||
return _step_idx(topic, "Klärung")
|
||||
if q["type"] == "projekt" and not files["ergaenzung"].exists():
|
||||
return _step_idx(topic, "Ergänzung")
|
||||
if _sidecar_schema(_json_datei(files["sidecar"])) is not None:
|
||||
return len(_bausteine_steps(topic))
|
||||
@@ -166,6 +195,8 @@ def reset_bausteine(topic: str) -> None:
|
||||
files = _bausteine_files(topic)
|
||||
files["final"].unlink(missing_ok=True)
|
||||
files["sidecar"].unlink(missing_ok=True) # liegt im Themen-Root, nicht in arbeit/
|
||||
quelle_path(topic).unlink(missing_ok=True)
|
||||
shutil.rmtree(quelle_crawl_dir(topic), ignore_errors=True) # gecrawlte Link-Quelle
|
||||
shutil.rmtree(files["arbeit"], ignore_errors=True)
|
||||
_bausteine_errors.pop(topic, None)
|
||||
|
||||
@@ -209,9 +240,12 @@ def _pdfs_konvertieren(project: Path) -> None:
|
||||
raise RuntimeError(f"PDF-Konvertierung fehlgeschlagen ({pdf.name}): {e}") from e
|
||||
|
||||
|
||||
def _build_recherche_prompt(topic: str, out_path: Path, instructions: str = "", project: Path | None = None) -> str:
|
||||
if project:
|
||||
source = _prompt("Bausteine-Quelle-Projekt", project=project)
|
||||
_QUELLE_TEMPLATE = {"projekt": "Bausteine-Quelle-Projekt", "uni": "Bausteine-Quelle-Uni", "link": "Bausteine-Quelle-Link"}
|
||||
|
||||
|
||||
def _build_recherche_prompt(topic: str, out_path: Path, instructions: str, typ: str, ordner: Path | None) -> str:
|
||||
if typ in _QUELLE_TEMPLATE:
|
||||
source = _prompt(_QUELLE_TEMPLATE[typ], project=ordner)
|
||||
else:
|
||||
source = _prompt("Bausteine-Quelle-Thema", topic=topic)
|
||||
return _prompt(
|
||||
@@ -337,7 +371,7 @@ async def _subbausteine_block(ctx: GenContext, set_p, files: dict, entries: dict
|
||||
topic, provider, is_cancelled = ctx.topic, ctx.provider, ctx.is_cancelled
|
||||
arbeit = files["arbeit"]
|
||||
idx = _titel_index(entries)
|
||||
caps = "files" if project_dir(topic).is_dir() else "full"
|
||||
caps = "files" if quelle_ordner(topic) else "full"
|
||||
nums = list(entries)
|
||||
chunks = _chunk_nums(nums, _n_chunks(len(nums)))
|
||||
n = len(chunks)
|
||||
@@ -548,7 +582,9 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
|
||||
|
||||
files = _bausteine_files(topic)
|
||||
final_path = files["final"]
|
||||
project = project_dir(topic) if project_dir(topic).is_dir() else None
|
||||
q = lade_quelle(topic)
|
||||
ordner = quelle_ordner(topic) # projekt/uni/link → Ordner, thema → None
|
||||
instructions = q.get("spec") or instructions # persistierte Spezifikation bevorzugen (auch bei Resume)
|
||||
|
||||
def set_p(msg: str, step: int | None = None) -> None:
|
||||
_bausteine_progress[topic] = msg
|
||||
@@ -566,8 +602,18 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
|
||||
try:
|
||||
async with _semaphore:
|
||||
files["arbeit"].mkdir(parents=True, exist_ok=True)
|
||||
if project:
|
||||
await asyncio.to_thread(_pdfs_konvertieren, project)
|
||||
# Link-Quelle: erst crawlen (gleiche Domain, begrenzt) → wird zur Ordner-Quelle.
|
||||
if q["type"] == "link" and not _crawl_fertig(topic):
|
||||
set_p("Quelle laden (Crawl)…", step=_step_idx(topic, "Quelle laden"))
|
||||
n = await asyncio.to_thread(crawl, q["ort"], ordner, cancelled=is_cancelled)
|
||||
if is_cancelled():
|
||||
abgebrochen()
|
||||
return
|
||||
if not n:
|
||||
_bausteine_errors[topic] = "Crawl ergab keine Inhalte — Link/Domain prüfen"
|
||||
return
|
||||
if ordner:
|
||||
await asyncio.to_thread(_pdfs_konvertieren, ordner)
|
||||
# „Neu erstellen": NUR wenn wirklich alles fertig ist (bausteine.md UND
|
||||
# Sidecar) → kompletter Frischstart. Liegt bausteine.md ohne Sidecar vor,
|
||||
# ist das ein Teil-Stand (Block B/C offen) → Resume, nicht wischen.
|
||||
@@ -587,13 +633,13 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
|
||||
else:
|
||||
offen.append((i, path))
|
||||
vorhanden = len(recherchen)
|
||||
set_p(f"Recherche läuft ({vorhanden} gültig, min. 3)…", step=0)
|
||||
set_p(f"Recherche läuft ({vorhanden} gültig, min. 3)…", step=_step_idx(topic, "Recherche"))
|
||||
if vorhanden < 3:
|
||||
caps = "files" if project else "full"
|
||||
caps = "files" if ordner else "full"
|
||||
slots = [
|
||||
{
|
||||
"key": f"bausteine-{topic}-recherche-{i}",
|
||||
"prompt": _build_recherche_prompt(topic, path, instructions, project),
|
||||
"prompt": _build_recherche_prompt(topic, path, instructions, q["type"], ordner),
|
||||
"role": "quick", "capabilities": caps,
|
||||
"payload": (lambda result, p=path: _file_payload(p)),
|
||||
}
|
||||
@@ -616,7 +662,7 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
|
||||
# für semantische Dubletten und Konsens/Rest-Teilung (fatal)
|
||||
mapping = _mapping_schema(_json_datei(files["recherche_mapping"]))
|
||||
if mapping is None:
|
||||
set_p("Konsolidiere Recherche…", step=1)
|
||||
set_p("Konsolidiere Recherche…", step=_step_idx(topic, "Konsolidierung"))
|
||||
files["recherche_mapping"].unlink(missing_ok=True)
|
||||
gemergt = _vormerge([_parse_auswahl(t) for t in recherchen])
|
||||
eintraege = "\n".join(f"{i}. {text} ({n}× genannt)" for i, (text, n) in enumerate(gemergt, 1))
|
||||
@@ -648,7 +694,7 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
|
||||
while rest and runde < KONSENS_MAX_RUNDEN:
|
||||
runde += 1
|
||||
final_runde = runde == KONSENS_MAX_RUNDEN
|
||||
set_p(f"Klärung läuft (Runde {runde}/{KONSENS_MAX_RUNDEN})…", step=2)
|
||||
set_p(f"Klärung läuft (Runde {runde}/{KONSENS_MAX_RUNDEN})…", step=_step_idx(topic, "Klärung"))
|
||||
mapping_path = files["mapping"][runde]
|
||||
|
||||
# Resume: fertiges Runden-Mapping wird direkt übernommen
|
||||
@@ -732,8 +778,8 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
|
||||
|
||||
# Nur Projekte: Themenfeld-Ergänzung — Skript/Projekt ist ein Ausschnitt,
|
||||
# ein Web-Agent ergänzt kanonisch fehlende Bausteine, markiert mit [Ergänzung].
|
||||
if project:
|
||||
set_p("Ergänze Themenfeld…", step=3)
|
||||
if q["type"] == "projekt":
|
||||
set_p("Ergänze Themenfeld…", step=_step_idx(topic, "Ergänzung"))
|
||||
erg_path = files["ergaenzung"]
|
||||
ergaenzungen = _ergaenzung_schema(_json_datei(erg_path))
|
||||
if ergaenzungen is None:
|
||||
|
||||
Reference in New Issue
Block a user