@@ -22,14 +22,14 @@ from pathlib import Path
import database as db
from agents import kill_process , cancel_scope , clear_scope , run_agent
from config import KONSENS_GRACE , RECHERCHE_GRACE , KONSENS_MAX_RUNDEN , DEFAULT_PROVIDER , CRAWL_KEEP_PATTERNS , CRAWL_NOISE_PATTERNS , CRAWL_MIN_CHARS
from config import KONSENS_GRACE , RECHERCHE_GRACE , KONSENS_MAX_RUNDEN , DEFAULT_PROVIDER , CRAWL_KEEP_PATTERNS , CRAWL_NOISE_PATTERNS , CRAWL_MIN_CHARS , QUELLE_RELEVANZ_CHUNK , QUELLE_RELEVANZ_SNIPPET
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 , frage_muster_path , project_dir , subbausteine_path , quelle_path , quelle_crawl_dir , safe_ordner
from crawl import crawl
from pipeline import (
CANCELLED , FAILED , GenContext , _extra , _gather_fortschritt , _log , _prompt , _race , _relevanz_schema ,
_runde_schema , _semaphore , _str_liste , _stufen_schema , _timeout , run_single_slot ,
CANCELLED , FAILED , GenContext , _extra , _gather_fortschritt , _janein_schema , _log , _prompt , _race ,
_relevanz_schema , _ runde_schema , _semaphore , _str_liste , _stufen_schema , _timeout , run_single_slot ,
)
from textkit import (
_eindeutige_titel , _lade_bausteine , _norm_titel , _parse_auswahl , _parse_subbausteine , _titel ,
@@ -57,6 +57,8 @@ KONSOLIDIERUNG_CHUNK = 600 # bis hierher EIN globaler Judge (dedupt alles); dar
FRAGE_CHUNK_SUBS = 50 # Ziel-Summe relevanter Subs je Chunk
FAKTEN_CHUNK_SUBS = 25 # Fakten-Extraktion: kleinere Chunks (Fakten sind umfangreicher als Muster)
FAKTEN_CHECK_PANEL = 3 # Judges je Chunk im Fakten-Check (Mehrheit beanstandet)
KONSOLIDIERUNG_PANEL = 3 # Mapping-Judges je Chunk (Panel → Reconcile statt Einzel-Judge)
SUBBAUSTEIN_PANEL = 3 # Source-Judges in der Subbaustein-Klärung (Mehrheit statt Einzel-Judge)
log = logging . getLogger ( " creator.bausteine " )
@@ -678,7 +680,9 @@ async def _subbausteine_block(ctx: GenContext, set_p, files: dict, entries: dict
bl . append ( f " <!-- baustein: { titel_by_num [ num ] } --> \n " + " \n " . join ( f " - { s } " for s in subs ) )
if not bl :
return " "
return " \n \n BEREITS GEFUNDEN — bestätige diese Subbausteine erneut UND ergänze fehlende: \n " + " \n " . join ( bl )
# Bekanntes NICHT erneut auflisten lassen (sonst bläht Re-Bestätigung den Mention-Count auf,
# Self-Bias/Echo) — nur Fehlendes ergänzen. Der Zähler bleibt so ein ehrliches Konsens-Signal.
return ( " \n \n BEREITS ERFASST — liste diese NICHT erneut. Finde nur, was FEHLT: \n " + " \n " . join ( bl ) )
# Phase „Subbausteine finden": je Paket Loop bis 0 neue Subs / Zeit-Kappe.
async def _finde ( c , chunk ) :
@@ -741,30 +745,74 @@ async def _subbausteine_block(ctx: GenContext, set_p, files: dict, entries: dict
await db . set_subbaustein_felder ( topic , norm_by_num [ num ] , s [ " sub_norm " ] ,
status = ( " konsens " if s [ " nennungen " ] > = 2 else " verworfen " ) )
# Phase „Subbausteine klären": Judge je Paket bereinigt die Konsens-Liste.
# Phase „Subbausteine klären": Source-Panel (SUBBAUSTEIN_PANEL Judges) prüft Konsens + Unsicher (1× )
# gegen die Quelle; Code-Mehrheit je Sub. Externes, mehrstimmiges Gate gegen Einzel-Judge-Bias + Echo.
async def _klaere ( c , chunk ) :
fp = arbeit / f " subbaustein-final-c { c } .md "
if _parse_subbausteine ( _read ( fp ) ) :
return
bloecke , hat = [ ] , False
konsens_by_num : dict [ int , list [ str ] ] = { }
for num in chunk :
sub s = [ s [ " sub_titel " ] for s in await db . list_subbausteine ( topic , norm_by_num [ num ] ) if s [ " status " ] == " konsens " ]
zeile n = " \n " . join ( f " - { s } " for s in subs ) if subs else " - (keiner) "
bloecke . append ( f " BAUSTEIN: { titel_by_num [ num ] } \n K onsens: \n { zeilen } " )
if subs :
hat = True
row s = await db . list_subbausteine ( topic , norm_by_num [ num ] )
ko n = [ s [ " sub_titel " ] for s in rows if s[ " status " ] == " konsens " ]
uns = [ s [ " sub_titel " ] for s in rows if s [ " status " ] != " k onsens" and s [ " nennungen " ] == 1 ]
konsens_by_num [ num ] = kon
if not kon and not uns :
continue
hat = True
k_zeilen = " \n " . join ( f " - { s } " for s in kon ) if kon else " - (keiner) "
u_zeilen = " \n " . join ( f " - { s } " for s in uns ) if uns else " - (keiner) "
bloecke . append ( f " BAUSTEIN: { titel_by_num [ num ] } \n Konsens (≥2 Finder): \n { k_zeilen } \n Unsicher (1× — streng gegen Quelle prüfen): \n { u_zeilen } " )
if not hat :
return
status , _ = await run_single_slot (
ctx , f " Subbaustein-Klärung { c } " ,
key = f " bausteine - { topi c} -subbaustein-final-c { c } " ,
prompt = _prompt ( " Subbaustein-Mapping " , topic = topic , source = source , bausteine = " \n \n " . join ( bloecke ) , out_path = fp , extra = _extra ( instructions ) ) ,
role = " judge " , capabilities = caps ,
payload = lambda result , p = fp : _parse_subbausteine ( _read ( p ) ) or None ,
timeout = _timeout ( " subbaustein_check " , len ( chunk ) ) ,
)
if status == FAILED :
chunk_idx = _titel_index ( { num : titel_by_num [ num ] for num in chunk } )
paths = [ arbeit / f " sub baustein-final-c { c } -j { j } .md " for j in range ( 1 , SUBBAUSTEIN_PANEL + 1 ) ]
offen = [ ( j , p ) for j , p in enumerate ( paths , 1 ) if _parse_subbausteine ( _read ( p ) ) is None ]
for _ , p in offen :
p. unlink ( missing_ok = True )
if offen :
slots = [ {
" key " : f " bausteine- { topic } -subbaustein-final-c { c } -j { j } " ,
" prompt " : _prompt ( " Subbaustein-Mapping " , topic = topic , source = source , bausteine = " \n \n " . join ( bloecke ) , out_path = p , extra = _extra ( instructions ) ) ,
" role " : " judge " , " capabilities " : caps ,
" payload " : ( lambda result , p = p : _parse_subbausteine ( _read ( p ) ) or None ) ,
} for j , p in offen ]
vorhanden = SUBBAUSTEIN_PANEL - len ( offen )
await _race ( topic , f " Subbaustein-Klärung { c } " , slots , max ( 1 , 2 - vorhanden ) ,
_timeout ( " subbaustein_check " , len ( chunk ) ) , provider , cancelled = is_cancelled , grace = KONSENS_GRACE )
if is_cancelled ( ) :
return
outs = [ d for p in paths if ( d := _parse_subbausteine ( _read ( p ) ) ) ]
if not outs : # Panel komplett gescheitert → Konsens übernehmen (wie bisher der Fallback)
_log ( topic , f " Subbaustein-Klärung Paket { c } fehlgeschlagen — Konsens übernommen " )
text = " \n \n " . join ( f " <!-- baustein: { titel_by_num [ num ] } --> \n " + " \n " . join ( f " - { s } " for s in konsens_by_num [ num ] )
for num in chunk if konsens_by_num [ num ] )
atomic_write_text ( fp , text )
return
# Code-Mehrheit je Baustein/Sub-Norm: behalten wenn Mehrheit der Judges ihn führt (Tie → behalten).
bloecke_out = [ ]
for num in chunk :
votes : dict [ str , int ] = { }
form : dict [ str , str ] = { }
for d in outs :
seen = set ( )
for marker , subs in d . items ( ) :
if _titel_aufloesen ( chunk_idx , marker ) != num :
continue
for sub in subs :
sn = _norm_titel ( sub )
if not sn or sn in seen :
continue
seen . add ( sn )
form . setdefault ( sn , sub )
votes [ sn ] = votes . get ( sn , 0 ) + 1
kept = [ form [ sn ] for sn in form if votes [ sn ] * 2 > = len ( outs ) ]
if kept :
bloecke_out . append ( f " <!-- baustein: { titel_by_num [ num ] } --> \n " + " \n " . join ( f " - { s } " for s in kept ) )
atomic_write_text ( fp , " \n \n " . join ( bloecke_out ) )
await _gather_fortschritt ( [ _klaere ( c , chunk ) for c , chunk in enumerate ( chunks , 1 ) ] , n , _melde_p ( set_p , topic , " Subbausteine klären " ) )
if is_cancelled ( ) :
@@ -806,6 +854,9 @@ async def _stufen_block(ctx: GenContext, set_p, files: dict, roh: dict, instruct
→ { Baustein-Titel: [ { titel, stufe}, …]} oder None. """
topic , provider , is_cancelled = ctx . topic , ctx . provider , ctx . is_cancelled
arbeit = files [ " arbeit " ]
# Kernpunkte je Sub als knapper Kontext (fundiertere Einstufung; Klassifikation braucht wenig).
fakten_map = _json_datei ( files [ " fakten " ] )
fakten_map = fakten_map if isinstance ( fakten_map , dict ) else { }
items = [ ( titel , sub ) for titel , subs in roh . items ( ) for sub in subs ] # globale id = index+1
if not items :
return { titel : [ ] for titel in roh }
@@ -843,6 +894,8 @@ async def _stufen_block(ctx: GenContext, set_p, files: dict, roh: dict, instruct
enum_zeilen . append ( f " \n BAUSTEIN: { b } " )
cur_b = b
enum_zeilen . append ( f " { k } . { sub } " )
if ( kz := _kern_zeile ( fakten_map . get ( b , { } ) . get ( _norm_titel ( sub ) ) ) ) :
enum_zeilen . append ( f " { kz } " )
enum = " \n " . join ( enum_zeilen ) . strip ( )
offen = [ ( i , p ) for i , p in enumerate ( paths , 1 ) if not _stufen_schema ( _json_datei ( p ) , local_set ) ]
slots = [ {
@@ -978,10 +1031,18 @@ def _fakten_check_schema(data) -> list[tuple[str, bool]] | None:
for p in pr if isinstance ( p , dict ) and ( sn := _norm_titel ( str ( p . get ( " subbaustein " , " " ) ) ) ) ]
def _kern_zeile ( fk ) - > str :
""" Knappe Kernpunkt-Zeile für Klassifikation (Stufe/Relevanz) — weniger Kontext genügt dort.
Leer, wenn keine Fakten/Kernpunkte (Altbestand). """
if not isinstance ( fk , dict ) or not fk . get ( " kernpunkte " ) :
return " "
return " Kern: " + " · " . join ( str ( k ) for k in fk [ " kernpunkte " ] )
def _fakten_zeilen ( fk : dict ) - > str :
z = [ ]
if fk . get ( " kernpunkte " ) :
z . append ( " Kernpunkte: " + " · " . join ( fk [ " kernpunkte " ] ) )
z . append ( " Kernpunkte: " + " · " . join ( str ( k ) for k in fk [ " kernpunkte " ] ) )
if fk . get ( " voraussetzungen " ) :
z . append ( " Voraussetzung: " + fk [ " voraussetzungen " ] )
if fk . get ( " huerden " ) :
@@ -1015,6 +1076,7 @@ async def _fakten_block(ctx, set_p, files: dict, roh: dict, q: dict, ordner, ins
chunks = _lpt_chunks ( [ len ( subs ) for _ , subs in bausteine ] , FAKTEN_CHUNK_SUBS )
def roh_path ( ci ) : return arbeit / f " fakten-c { ci } .json "
def erg_path ( ci ) : return arbeit / f " fakten-erg-c { ci } .json "
def chk_path ( ci , j ) : return arbeit / f " fakten-check-c { ci } -j { j } .json "
def fix_path ( ci ) : return arbeit / f " fakten-fix-c { ci } .json "
def ctitel ( idxs ) : return [ bausteine [ i ] [ 0 ] for i in idxs ]
@@ -1037,6 +1099,33 @@ async def _fakten_block(ctx, set_p, files: dict, roh: dict, q: dict, ordner, ins
out . setdefault ( bt , { } ) [ _norm_titel ( sub ) ] = { " sub " : sub , * * { k : e [ k ] for k in _FAKTEN_FELDER } }
return out
# Roh-Fakten + Completeness-Ergänzungen vereinigen (Recall): nur Subs, die in roh existieren.
def _chunk_fakten ( ci ) :
roh = roh_map ( ci , roh_path ( ci ) )
erg = roh_map ( ci , erg_path ( ci ) ) if erg_path ( ci ) . exists ( ) else { }
if not erg :
return roh
for bt , fm in roh . items ( ) :
ebt = erg . get ( bt , { } )
for sn , fk in fm . items ( ) :
ek = ebt . get ( sn )
if not ek :
continue
seen = { str ( k ) . strip ( ) . casefold ( ) for k in fk . get ( " kernpunkte " , [ ] ) }
for k in ek . get ( " kernpunkte " , [ ] ) :
if str ( k ) . strip ( ) . casefold ( ) not in seen :
seen . add ( str ( k ) . strip ( ) . casefold ( ) )
fk [ " kernpunkte " ] . append ( k )
seent = { bf [ " text " ] . strip ( ) . casefold ( ) for bf in fk . get ( " belegte_fakten " , [ ] ) }
for bf in ek . get ( " belegte_fakten " , [ ] ) :
if bf [ " text " ] . strip ( ) . casefold ( ) not in seent :
seent . add ( bf [ " text " ] . strip ( ) . casefold ( ) )
fk [ " belegte_fakten " ] . append ( bf )
for f in ( " voraussetzungen " , " huerden " , " beispiel_idee " ) :
if not fk . get ( f ) and ek . get ( f ) :
fk [ f ] = ek [ f ]
return roh
# Phase „Fakten finden": 1 Generator je Chunk.
async def _finde ( ci , idxs ) :
fp = roh_path ( ci )
@@ -1058,10 +1147,38 @@ async def _fakten_block(ctx, set_p, files: dict, roh: dict, q: dict, ordner, ins
_bausteine_errors [ topic ] = " Fakten-Extraktion fehlgeschlagen "
return None
# Phase „Fakten ergänzen" (Recall): ein gezielter Gap-Hunt je Chunk sucht source-belegte Fakten, die
# der Single-Pass übersah. Best-effort — schlägt nie fehl (keine erg-Datei → Merge nutzt nur roh).
async def _ergaenze ( ci , idxs ) :
ep = erg_path ( ci )
if _fakten_schema ( _json_datei ( ep ) ) :
return
per = roh_map ( ci , roh_path ( ci ) )
if not per :
return
block = " \n \n " . join (
f " BAUSTEIN: { bt } \n SUBBAUSTEINE (mit bereits erfassten Fakten): \n " + " \n " . join (
f " - { fk [ ' sub ' ] } \n Erfasst: " + ( " ; " . join (
list ( fk . get ( " kernpunkte " , [ ] ) ) + [ bf [ " text " ] for bf in fk . get ( " belegte_fakten " , [ ] ) ] ) or " (nichts) " )
for fk in fm . values ( ) )
for bt , fm in per . items ( ) )
subs_total = sum ( len ( bausteine [ i ] [ 1 ] ) for i in idxs )
await run_single_slot (
ctx , f " Fakten ergänzen { ci } " , key = f " bausteine- { topic } -fakten-erg-c { ci } " ,
prompt = _prompt ( " Fakten-Ergaenzung " , topic = topic , source = source , bausteine = block , out_path = ep , extra = _extra ( instructions ) ) ,
role = " guide " , capabilities = caps ,
payload = lambda result , p = ep : _fakten_schema ( _json_datei ( p ) ) ,
timeout = _timeout ( " inhalt " , subs_total ) )
set_p ( " Fakten ergänzen… " , step = _step_idx ( topic , " Fakten finden " ) )
await _gather_fortschritt ( [ _ergaenze ( ci , idxs ) for ci , idxs in enumerate ( chunks ) ] , len ( chunks ) , _melde_p ( set_p , topic , " Fakten finden " ) )
if is_cancelled ( ) :
return None
# Phase „Fakten prüfen": FAKTEN_CHECK_PANEL Judges je Chunk. Zwei Mehrheits-Mengen:
# beanstandet (Fakt ungenau → korrigieren) und verwerfen (Sub nicht belegbar → entfernen).
async def _pruefe ( ci , idxs ) :
per = roh_map ( ci , roh_path ( ci ) )
per = _chunk_fakten ( ci ) # roh + Ergänzungen → Panel verifiziert die Vereinigung
if not per :
return ci , set ( ) , set ( )
fakten_text = " \n \n " . join ( f " SUBBAUSTEIN: { fk [ ' sub ' ] } \n { _fakten_zeilen ( fk ) } " for fm in per . values ( ) for fk in fm . values ( ) )
@@ -1132,7 +1249,7 @@ async def _fakten_block(ctx, set_p, files: dict, roh: dict, q: dict, ordner, ins
ergebnis : dict [ str , dict ] = { }
verworfen_map : dict [ str , set ] = { }
for ci in range ( len ( chunks ) ) :
per = roh_map ( ci , roh_path ( ci ) )
per = _chunk_fakten ( ci ) # roh + Ergänzungen (Recall); Fix überschreibt nur Korrigierte
fix = roh_map ( ci , fix_path ( ci ) ) if fix_path ( ci ) . exists ( ) else { }
verw = verwerfen . get ( ci , set ( ) )
for bt , fm in per . items ( ) :
@@ -1153,7 +1270,7 @@ async def _relevanz_block(ctx: GenContext, set_p, files: dict, sidecar: dict, in
→ { gid: relevanz} oder None bei Abbruch/Recherche-Fehler. Default bei Lücke/Streit: ' relevant ' . """
topic , provider , is_cancelled = ctx . topic , ctx . provider , ctx . is_cancelled
arbeit = files [ " arbeit " ]
items = [ ( titel , sub [ " titel " ] ) for titel , subs in sidecar . items ( ) for sub in subs ] # globale id = index+1
items = [ ( titel , sub [ " titel " ] , sub . get ( " fakten " ) ) for titel , subs in sidecar . items ( ) for sub in subs ] # globale id = index+1
if not items :
return { }
chunks = _chunk_nums ( list ( range ( len ( items ) ) ) , _n_chunks ( len ( items ) , STUFE_CHUNK ) )
@@ -1172,7 +1289,12 @@ async def _relevanz_block(ctx: GenContext, set_p, files: dict, sidecar: dict, in
vorhanden = sum ( 1 for p in paths if _relevanz_schema ( _json_datei ( p ) , local_set ) )
if vorhanden > = 2 :
return True
enum = " \n " . join ( f " { k } . [ { items [ j ] [ 0 ] } ] { items [ j ] [ 1 ] } " for k , j in enumerate ( item_idxs , 1 ) )
enum_zeilen = [ ]
for k , j in enumerate ( item_idxs , 1 ) :
enum_zeilen . append ( f " { k } . [ { items [ j ] [ 0 ] } ] { items [ j ] [ 1 ] } " )
if ( kz := _kern_zeile ( items [ j ] [ 2 ] ) ) :
enum_zeilen . append ( f " { kz } " )
enum = " \n " . join ( enum_zeilen )
offen = [ ( i , p ) for i , p in enumerate ( paths , 1 ) if not _relevanz_schema ( _json_datei ( p ) , local_set ) ]
slots = [ {
" key " : f " bausteine- { topic } -relevanz-c { c } - { i } " ,
@@ -1280,9 +1402,16 @@ async def _frage_muster_block(ctx: GenContext, set_p, files: dict, sidecar: dict
topic , is_cancelled = ctx . topic , ctx . is_cancelled
arbeit = files [ " arbeit " ]
# ALLE Subbausteine (auch rand) bekommen ein Muster — Rand ist in der FGuide-Ebene prüfbar.
# fakten_by: voller Fakten-Kontext je Sub (Generierung profitiert davon — bessere Fragen).
bausteine = [ ]
fakten_by : dict [ tuple , dict ] = { }
for titel , subs in sidecar . items ( ) :
alle = [ s [ " titel " ] for s in subs if isinstance ( s , dict ) and str ( s . get ( " titel " , " " ) ) . strip ( ) ]
alle = [ ]
for s in subs :
if isinstance ( s , dict ) and ( st := str ( s . get ( " titel " , " " ) ) . strip ( ) ) :
alle . append ( st )
if isinstance ( s . get ( " fakten " ) , dict ) :
fakten_by [ ( titel , _norm_titel ( st ) ) ] = s [ " fakten " ]
if alle :
bausteine . append ( ( titel , alle ) )
if not bausteine :
@@ -1303,8 +1432,14 @@ async def _frage_muster_block(ctx: GenContext, set_p, files: dict, sidecar: dict
fp = roh_path ( ci )
if _frage_muster_chunk_schema ( _json_datei ( fp ) ) :
return # Resume
def _sub_zeile ( bi , s ) :
zeile = f " - { s } "
fk = fakten_by . get ( ( bausteine [ bi ] [ 0 ] , _norm_titel ( s ) ) )
if fk and ( ft := _fakten_zeilen ( fk ) ) :
zeile + = " \n " + " \n " . join ( " " + l for l in ft . split ( " \n " ) )
return zeile
block = " \n \n " . join (
f " BAUSTEIN: { bausteine [ i ] [ 0 ] } \n SUBBAUSTEINE: \n " + " \n " . join ( f " - { s } " for s in bausteine [ i ] [ 1 ] )
f " BAUSTEIN: { bausteine [ i ] [ 0 ] } \n SUBBAUSTEINE: \n " + " \n " . join ( _sub_zeile ( i , s ) for s in bausteine [ i ] [ 1 ] )
for i in idxs
)
subs_total = sum ( len ( bausteine [ i ] [ 1 ] ) for i in idxs )
@@ -1499,7 +1634,89 @@ def _sichte_regeln(ordner, pages: list[str]) -> tuple[list[str], list[str]]:
return content , noise
async def _quelle_aufbereiten ( ctx : GenContext , set_p , files : dict , q : dict , ordne r) - > bool :
def _seite_snippet ( ordner , fn : str ) - > tuple [ st r, str ] :
""" (url, snippet) einer Crawl-Seite für das Relevanz-Gate. url aus der QUELLE:-Zeile;
snippet = Body-Auszug (Navigations-Boilerplate steht vorn — der Prompt ignoriert es).
URL ist das Primärsignal (sprechender Slug), der Snippet stützt nur. """
zeilen = _read ( Path ( ordner ) / fn ) . splitlines ( )
url = zeilen [ 0 ] [ len ( " QUELLE: " ) : ] . strip ( ) if zeilen and zeilen [ 0 ] . startswith ( " QUELLE: " ) else " "
body = " \n " . join ( zeilen [ 1 : ] ) . strip ( )
snippet = " " . join ( body . split ( ) ) [ : QUELLE_RELEVANZ_SNIPPET ]
return ( url or fn ) , snippet
async def _relevanz_sichtung ( ctx : GenContext , set_p , files : dict , ordner , content : list [ str ] , spec : str , instructions : str ) - > tuple [ list [ str ] , list [ str ] ] :
""" LLM-Themen-Gate nach dem Regel-Filter: jede Content-Seite ja/nein gegen die Spec.
Off-topic (anderes Fachgebiet) → raus. Muster wie `_relevanz_block`: kleine Pakete, 3 Rater
(`fast`), 2-von-3-Konsens. KONSERVATIV: nur bei klarer „nein " -Mehrheit droppen; Streit/Lücke/
Race-Fehler → behalten. SAFETY: würde das Gate ≥80 % (oder alles) droppen, bleibt alles
(Spec-Mismatch/Bug soll die Quelle nicht leeren). → (behalten, raus) als Dateinamen. """
topic , provider , is_cancelled = ctx . topic , ctx . provider , ctx . is_cancelled
arbeit = files [ " arbeit " ]
pages = sorted ( content )
if not pages :
return content , [ ]
items = [ _seite_snippet ( ordner , fn ) for fn in pages ] # Index deckt sich mit `pages`
chunks = _chunk_nums ( list ( range ( len ( pages ) ) ) , _n_chunks ( len ( pages ) , QUELLE_RELEVANZ_CHUNK ) )
n = len ( chunks )
def rater_paths ( c ) :
return [ arbeit / f " quelle-relevanz-c { c } - { i } .json " for i in ( 1 , 2 , 3 ) ]
def lset ( idxs ) :
return set ( range ( 1 , len ( idxs ) + 1 ) )
async def _rate ( c , idxs ) :
local_set = lset ( idxs )
paths = rater_paths ( c )
vorhanden = sum ( 1 for p in paths if _janein_schema ( _json_datei ( p ) , local_set ) )
if vorhanden > = 2 :
return True
enum_zeilen = [ ]
for k , j in enumerate ( idxs , 1 ) :
url , snip = items [ j ]
enum_zeilen . append ( f " { k } . { url } " )
if snip :
enum_zeilen . append ( f " { snip } " )
enum = " \n " . join ( enum_zeilen )
offen = [ ( i , p ) for i , p in enumerate ( paths , 1 ) if not _janein_schema ( _json_datei ( p ) , local_set ) ]
slots = [ {
" key " : f " bausteine- { topic } -quelle-relevanz-c { c } - { i } " ,
" prompt " : _prompt ( " Quelle-Relevanz " , topic = topic , spec = spec , seiten = enum , out_path = p , extra = _extra ( instructions ) ) ,
" role " : " fast " , " capabilities " : " files " ,
" payload " : ( lambda result , p = p , ids = local_set : _janein_schema ( _json_datei ( p ) , ids ) ) ,
} for i , p in offen ]
neu = await _race ( topic , f " Relevanz-Sichtung Paket { c } " , slots , 2 - vorhanden , _timeout ( " relevanz " , len ( idxs ) ) , provider , cancelled = is_cancelled , grace = KONSENS_GRACE )
return not is_cancelled ( ) and neu is not None
_qidx = _step_idx ( topic , " Quelle aufbereiten " ) # Gate läuft im Quelle-Schritt (kein eigener Step)
set_p ( f " Prüfe Relevanz zur Spec ( { n } Pakete)… " , step = _qidx )
async def _melde_sichtung ( d , t ) :
set_p ( f " Prüfe Relevanz zur Spec { d } / { t } … " , step = _qidx )
await _gather_fortschritt ( [ _rate ( c , idxs ) for c , idxs in enumerate ( chunks , 1 ) ] , n , _melde_sichtung )
if is_cancelled ( ) :
return content , [ ] # Abbruch → nichts droppen (Caller bricht ab)
# Vote je Seite: nur eine klare „nein"-Mehrheit (≥2 und mehr als „ja") wirft raus.
raus : list [ str ] = [ ]
for c , idxs in enumerate ( chunks , 1 ) :
local_set = lset ( idxs )
rater = [ d for p in rater_paths ( c ) if ( d := _janein_schema ( _json_datei ( p ) , local_set ) ) ]
for k in range ( 1 , len ( idxs ) + 1 ) :
stimmen = [ d [ k ] for d in rater if k in d ]
nein , ja = stimmen . count ( " nein " ) , stimmen . count ( " ja " )
if nein > = 2 and nein > ja :
raus . append ( pages [ idxs [ k - 1 ] ] )
if raus and len ( raus ) > = max ( 1 , int ( len ( pages ) * 0.8 ) ) :
_log ( topic , f " Relevanz-Sichtung: würde { len ( raus ) } / { len ( pages ) } droppen — verworfen (Spec-Mismatch?), alles behalten " )
return content , [ ]
raus_set = set ( raus )
behalten = [ fn for fn in pages if fn not in raus_set ]
return behalten , raus
async def _quelle_aufbereiten ( ctx : GenContext , set_p , files : dict , q : dict , ordner , instructions : str ) - > bool :
""" Schritt „Quelle aufbereiten " : Crawl (link) + PDF-Konvert + Content/Noise-Sichtung.
Persistiert die Sichtung in der Coverage-Tabelle (inhalt). → True (ok) / False (Abbruch/Fehler).
thema: nichts. projekt/uni: nur PDFs (kuratierter Ordner, keine Sichtung). """
@@ -1525,8 +1742,15 @@ async def _quelle_aufbereiten(ctx: GenContext, set_p, files: dict, q: dict, ordn
set_p ( " Sichte Seiten… " , step = _step_idx ( topic , " Quelle aufbereiten " ) )
await db . delete_coverage ( topic )
content , noise = _sichte_regeln ( ordner , pages ) # deterministischer Regel-Filter
if q . get ( " spec " ) and content : # Themen-Gate: trennt das Fachgebiet (Regeln können das nicht)
content , raus = await _relevanz_sichtung ( ctx , set_p , files , ordner , content , q [ " spec " ] , instructions )
if is_cancelled ( ) :
return False
if raus :
noise = sorted ( set ( noise ) | set ( raus ) )
_log ( topic , f " LLM-Relevanz: { len ( raus ) } Seiten off-topic → Noise " )
await db . mark_inhalt ( topic , sorted ( content ) , sorted ( noise ) )
_log ( topic , f " Sichtung: { len ( content ) } Content / { len ( noise ) } Noise von { len ( pages ) } (Regeln) " )
_log ( topic , f " Sichtung: { len ( content ) } Content / { len ( noise ) } Noise von { len ( pages ) } (Regeln + LLM-Gate ) " )
await db . set_step_status ( topic , " Quelle aufbereiten " , " fertig " )
return True
@@ -1673,8 +1897,10 @@ async def _recherche_batch(ctx: GenContext, set_p, files: dict, q: dict, ordner,
async def _konsolidiere ( ctx : GenContext , set_p , files : dict ) - > bool :
""" Judge mergt Kandidaten semantisch + teilt in Konsens (≥2)/Rest (1× ); Status in DB. """
topic , is_c anc elled = ctx . topic , ctx . is_cancelled
""" Panel (KONSOLIDIERUNG_PANEL Judges) mergt Kandidaten semantisch; ein Reconcile-Judge führt die
P anel-Ausgaben zur finalen Konsens (≥2)/Rest (1× )-Liste zusammen. Status in DB.
Panel statt Einzel-Judge: ein einzelner Judge ist bias-anfällig (Position/Verbosity) und instabil. """
topic , provider , is_cancelled = ctx . topic , ctx . provider , ctx . is_cancelled
if await db . get_step_status ( topic , " Konsolidierung " ) == " fertig " :
return True
set_p ( " Konsolidiere Recherche… " , step = _step_idx ( topic , " Konsolidierung " ) )
@@ -1684,27 +1910,81 @@ async def _konsolidiere(ctx: GenContext, set_p, files: dict) -> bool:
return False
arbeit = files [ " arbeit " ]
chunks = _chunk_nums ( kandidaten , max ( 1 , math . ceil ( len ( kandidaten ) / KONSOLIDIERUNG_CHUNK ) ) )
async def _map_panel ( c : int , eintraege : str , anzahl : int ) :
""" 3 Mapping-Judges über `eintraege` → Reconcile-Judge → (konsens, rest). None bei Abbruch/Fehler. """
paths = [ arbeit / f " konsolidierung-c { c } -j { j } .json " for j in range ( 1 , KONSOLIDIERUNG_PANEL + 1 ) ]
offen = [ ( j , p ) for j , p in enumerate ( paths , 1 ) if _mapping_schema ( _json_datei ( p ) ) is None ]
for _ , p in offen :
p . unlink ( missing_ok = True )
if offen :
slots = [ {
" key " : f " bausteine- { topic } -konsolidierung-c { c } -j { j } " ,
" prompt " : _prompt ( " Bausteine-Recherche-Mapping " , topic = topic , n = RECHERCHE_READERS , eintraege = eintraege , out_path = p ) ,
" role " : " judge " , " capabilities " : " files " ,
" payload " : ( lambda result , p = p : _mapping_schema ( _json_datei ( p ) ) ) ,
} for j , p in offen ]
vorhanden = KONSOLIDIERUNG_PANEL - len ( offen )
await _race ( topic , f " Konsolidierung { c } " , slots , max ( 1 , 2 - vorhanden ) ,
_timeout ( " recherche_mapping " , anzahl ) , provider , cancelled = is_cancelled , grace = KONSENS_GRACE )
if is_cancelled ( ) :
return None
outs = [ m for p in paths if ( m := _mapping_schema ( _json_datei ( p ) ) ) ]
if not outs :
return None
# Union der Panel-Titel; je Titel zählen, wie viele Judges ihn als Konsens führen.
kvotes : dict [ str , int ] = { }
form : dict [ str , str ] = { } # norm → Anzeigetitel (erstes Vorkommen)
order : list [ str ] = [ ]
for kk , rr in outs :
for t in kk + rr :
nt = _norm_titel ( _titel ( t ) )
if not nt :
continue
if nt not in form :
form [ nt ] = t
order . append ( nt )
kvotes . setdefault ( nt , 0 )
for t in kk :
nt = _norm_titel ( _titel ( t ) )
if nt :
kvotes [ nt ] = kvotes . get ( nt , 0 ) + 1
# Reconcile: ein Merge-Judge über die Union, annotiert mit Judge-Stimmen ("k× genannt").
rp = arbeit / f " konsolidierung-c { c } -reconcile.json "
recon = _mapping_schema ( _json_datei ( rp ) )
if recon is None :
rp . unlink ( missing_ok = True )
eintraege_r = " \n " . join ( f " { i } . { form [ nt ] } ( { max ( 1 , kvotes [ nt ] ) } × genannt)" for i , nt in enumerate ( order , 1 ) )
status , recon = await run_single_slot (
ctx , f " Konsolidierung Reconcile { c } " ,
key = f " bausteine- { topic } -konsolidierung-c { c } -reconcile " ,
prompt = _prompt ( " Bausteine-Recherche-Mapping " , topic = topic , n = KONSOLIDIERUNG_PANEL , eintraege = eintraege_r , out_path = rp ) ,
role = " judge " , capabilities = " files " ,
payload = lambda result , p = rp : _mapping_schema ( _json_datei ( p ) ) ,
timeout = _timeout ( " recherche_mapping " , len ( order ) ) ,
)
if status == CANCELLED :
return None
recon = recon if status != FAILED else None
if recon :
return recon
# Fallback (Reconcile gescheitert): Code-Mehrheit — Konsens, wenn Mehrheit der Judges Konsens sagt.
konsens = [ form [ nt ] for nt in order if kvotes [ nt ] * 2 > = len ( outs ) and kvotes [ nt ] > 0 ]
kset = { _norm_titel ( _titel ( t ) ) for t in konsens }
return konsens , [ form [ nt ] for nt in order if nt not in kset ]
konsens , rest = [ ] , [ ]
for c , chunk in enumerate ( chunks , 1 ) :
fp = arbeit / f " konsolidierung-c { c } .json "
fp . unlink ( missing_ok = True )
eintraege = " \n " . join (
f " { i } . { b [ ' titel ' ] } — { b [ ' beschreibung ' ] } ( { b [ ' nennungen ' ] } × genannt)" for i , b in enumerate ( chunk , 1 )
)
status , mapping = await run_single_slot (
ctx , f " Konsolidierung { c } " ,
key = f " bausteine- { topic } -konsolidierung-c { c } " ,
prompt = _prompt ( " Bausteine-Recherche-Mapping " , topic = topic , n = RECHERCHE_READERS , eintraege = eintraege , out_path = fp ) ,
role = " judge " , capabilities = " files " ,
payload = lambda result , p = fp : _mapping_schema ( _json_datei ( p ) ) ,
timeout = _timeout ( " recherche_mapping " , len ( chunk ) ) ,
)
if status == CANCELLED :
return False
if status == FAILED :
res = await _map_panel ( c , eintraege , len ( chunk ) )
if res is None :
if is_cancelled ( ) :
return False
_bausteine_errors [ topic ] = " Recherche-Mapping fehlgeschlagen "
return False
k , r = mapping
k , r = res
konsens + = k
rest + = r
# Bei mehreren Chunks: ein globaler Merge-Pass über die vereinten Konsens-Einträge,
@@ -1737,39 +2017,51 @@ async def _konsolidiere(ctx: GenContext, set_p, files: dict) -> bool:
async def _klaere_inventar ( ctx : GenContext , set_p , files : dict ) - > bool :
""" 1 Judge entscheidet über den Rest (1× -Genannte): aufnehmen → Konsens, sonst verworfen. """
topic , is_c anc elled = ctx . topic , ctx . is_cancelled
""" Panel (KONSOLIDIERUNG_PANEL Judges) entscheidet über den Rest (1× -Genannte): Mehrheit `aufnehmen`
→ Konsens, sonst verworfen. P anel statt Einzel-Judge — der Rest-Schnitt ist der schärfste Eingriff;
ein einzelner Judge ist hier zu instabil. Konservativer Tie → behalten (nie ein Konzept verlieren). """
topic , provider , is_cancelled = ctx . topic , ctx . provider , ctx . is_cancelled
if await db . get_step_status ( topic , " Klärung " ) == " fertig " :
return True
set_p ( " Klärung läuft… " , step = _step_idx ( topic , " Klärung " ) )
rest_rows = await db . list_bausteine ( topic , status = " rest " )
if rest_rows :
konsens = [ b [ " titel " ] for b in await db . list_bausteine ( topic , status = " konsens " ) ]
fp = files [ " arbeit " ] / " klaerung.json "
fp . unlink ( missing_ok = True )
status , ergebnis = await run_single_slot (
ctx , " Klärung " ,
key = f " bausteine- { topic } -klaerung " ,
prompt = _prompt (
" Bausteine-Klaerung " , topic = topic ,
konsens = " \n " . join ( f " - { t } " for t in konsens ) or " (noch leer) " ,
rest = " \n " . join ( f " - { b [ ' titel ' ] } " for b in rest_rows ) ,
final = " \n - Entscheide JEDEN Eintrag. `rest` MUSS leer sein. " ,
out_path = fp ,
) ,
role = " judge " , capabilities = " files " ,
payload = lambda result , p = fp : _runde_schema ( _json_datei ( p ) , final = True ) ,
timeout = _timeout ( " auswahl_mapping " , len ( rest_rows ) ) ,
)
if status == CANCELLED :
arbeit = files [ " arbeit " ]
paths = [ arbeit / f " klaerung-j { j } .json " for j in range ( 1 , KONSOLIDIERUNG_PANEL + 1 ) ]
offen = [ ( j , p ) for j , p in enumerate ( paths , 1 ) if _ runde_schema ( _json_datei ( p ) , final = True ) is None ]
for _ , p in offen :
p . unlink ( missing_ok = True )
if offen :
slots = [ {
" key " : f " bausteine- { topic } -klaerung-j { j } " ,
" prompt " : _prompt (
" Bausteine-Klaerung " , topic = topic ,
konsens = " \n " . join ( f " - { t } " for t in konsens ) or " (noch leer) " ,
rest = " \n " . join ( f " - { b [ ' titel ' ] } " for b in rest_rows ) ,
final = " \n - Entscheide JEDEN Eintrag. `rest` MUSS leer sein. " ,
out_path = p ,
) ,
" role " : " judge " , " capabilities " : " files " ,
" payload " : ( lambda result , p = p : _runde_schema ( _json_datei ( p ) , final = True ) ) ,
} for j , p in offen ]
vorhanden = KONSOLIDIERUNG_PANEL - len ( offen )
await _race ( topic , " Klärung " , slots , max ( 1 , 2 - vorhanden ) ,
_timeout ( " auswahl_mapping " , len ( rest_rows ) ) , provider , cancelled = is_cancelled , grace = KONSENS_GRACE )
if is_cancelled ( ) :
return False
if st atu s == FAILED :
outs = [ r for p in p ath s if ( r := _runde_schema ( _json_datei ( p ) , final = True ) ) ]
if not outs :
_bausteine_errors [ topic ] = " Klärung fehlgeschlagen "
return False
aufnehmen , _ = ergebnis
auf_norm = { _norm_ti tel ( _titel ( t ) ) for t in aufnehmen }
# Mehrheit je Rest-Eintrag (per Norm-Titel). Tie → behalten (votes*2 >= n).
vo tes : dict [ str , int ] = { }
for aufnehmen , _ in outs :
for nt in { _norm_titel ( _titel ( t ) ) for t in aufnehmen } :
votes [ nt ] = votes . get ( nt , 0 ) + 1
for b in rest_rows :
await db . set_baustein_status ( topic , b [ " titel_norm " ] , " konsens " if b [ " titel_norm " ] in auf_norm else " verworfen " )
auf = votes . get ( b [ " titel_norm " ] , 0 ) * 2 > = len ( outs )
await db . set_baustein_status ( topic , b [ " titel_norm " ] , " konsens " if auf else " verworfen " )
await db . set_step_status ( topic , " Klärung " , " fertig " )
return True
@@ -1808,14 +2100,99 @@ def _gliederung_schema(data, valid: set[int]):
return { " kapitel " : out }
def _prereq_schema ( data , valid : set [ int ] ) - > dict [ int , list [ int ] ] :
""" { " prereqs " : { " 3 " : [1, 7]}} → { num: [prereq-nums]} · nur Nummern aus `valid`, ohne Selbstkante.
Ungültig/leer → {} (Best-effort: dann Original-Reihenfolge). """
if not isinstance ( data , dict ) or not isinstance ( data . get ( " prereqs " ) , dict ) :
return { }
out : dict [ int , list [ int ] ] = { }
for k , v in data [ " prereqs " ] . items ( ) :
try :
num = int ( k )
except ( ValueError , TypeError ) :
continue
if num not in valid or not isinstance ( v , list ) :
continue
pres = [ ]
for p in v :
try :
p = int ( p )
except ( ValueError , TypeError ) :
continue
if p in valid and p != num and p not in pres :
pres . append ( p )
if pres :
out [ num ] = pres
return out
def _topo_order ( nums : list [ int ] , edges : dict [ int , list [ int ] ] ) - > list [ int ] :
""" Kahn-Topo-Sort: Voraussetzungen zuerst. `edges[num]` = Nummern, die VOR num kommen müssen.
Stabiler Tie-Break (Original-Reihenfolge von `nums`); Zyklen werden gebrochen (nie Deadlock). """
pos = { n : i for i , n in enumerate ( nums ) }
# Resteingangsgrad nur über gültige Knoten; Selbst-/Fremdkanten ignoriert.
pre = { n : [ p for p in edges . get ( n , [ ] ) if p in pos and p != n ] for n in nums }
fertig : list [ int ] = [ ]
erledigt : set [ int ] = set ( )
rest = list ( nums )
while rest :
bereit = [ n for n in rest if all ( p in erledigt for p in pre [ n ] ) ]
if not bereit : # Zyklus → den in Original-Reihenfolge frühesten Rest-Knoten erzwingen
bereit = [ min ( rest , key = lambda n : pos [ n ] ) ]
nxt = min ( bereit , key = lambda n : pos [ n ] ) # stabil: kleinste Original-Position zuerst
fertig . append ( nxt )
erledigt . add ( nxt )
rest . remove ( nxt )
return fertig
async def _lernreihenfolge ( ctx : GenContext , set_p , files : dict , entries : dict , valid : set [ int ] , instructions : str ) - > dict :
""" entries (num→titel) in Lernreihenfolge bringen: LLM extrahiert Prereq-Kanten aus den
extrahierten `voraussetzungen`, Code löst per Topo-Sort. Best-effort → sonst entries unverändert. """
if len ( entries ) < 3 :
return entries
topic = ctx . topic
fakten_map = _json_datei ( files [ " fakten " ] )
fakten_map = fakten_map if isinstance ( fakten_map , dict ) else { }
def _hint ( titel ) :
fm = fakten_map . get ( titel ) or { }
vs = [ v for fk in fm . values ( ) if isinstance ( fk , dict ) and ( v := str ( fk . get ( " voraussetzungen " , " " ) ) . strip ( ) ) ]
return " · " . join ( dict . fromkeys ( vs ) )
pp = files [ " arbeit " ] / " gliederung-prereqs.json "
def _payload ( result , p = pp ) :
d = _json_datei ( p )
return d if isinstance ( d , dict ) and " prereqs " in d else None
vorhanden = _json_datei ( pp )
if not ( isinstance ( vorhanden , dict ) and " prereqs " in vorhanden ) :
zeilen = [ f " { n } . { t } " + ( f " \n braucht vorher: { h } " if ( h := _hint ( t ) ) else " " ) for n , t in entries . items ( ) ]
set_p ( " Gliederung — Lernreihenfolge… " , step = _step_idx ( topic , " Gliederung " ) )
await run_single_slot (
ctx , " Gliederung-Voraussetzungen " , key = f " bausteine- { topic } -gliederung-prereqs " ,
prompt = _prompt ( " Gliederung-Voraussetzungen " , topic = topic , bausteine = " \n " . join ( zeilen ) , out_path = pp , extra = _extra ( instructions ) ) ,
role = " guide " , capabilities = " files " , payload = _payload , timeout = _timeout ( " plan " , len ( entries ) ) )
edges = _prereq_schema ( _json_datei ( pp ) , valid )
if not edges :
return entries # keine/ungültige Kanten → Original-Reihenfolge (kein Regress)
ordered = _topo_order ( list ( entries ) , edges )
return { n : entries [ n ] for n in ordered }
async def _gliederung_block ( ctx : GenContext , set_p , files : dict , entries : dict , instructions : str ) - > dict :
""" Format-agnostische Gliederung über ALLE Bausteine — 3 Vorschläge → Judge merged.
Bricht nie ab: 0 gültige → ein Kapitel mit allem; fehlende Bausteine landen in „Weitere " .
→ { " kapitel " :[ { titel,nummern}]} (auch in files[ " gliederung " ]). """
topic , is_cancelled = ctx . topic , ctx . is_cancelled
valid = set ( entries )
liste = " \n " . join ( f " { n } . { t } " for n , t in entries . items ( ) )
step = _step_idx ( topic , " Gliederung " )
# Lernreihenfolge gründen (LLM-Modulo): LLM extrahiert Prereq-Kanten aus den extrahierten
# `voraussetzungen`, Code löst per Topo-Sort. Best-effort → sonst Original-Reihenfolge.
entries = await _lernreihenfolge ( ctx , set_p , files , entries , valid , instructions )
liste = " \n " . join ( f " { n } . { t } " for n , t in entries . items ( ) )
set_p ( " Gliederung — Vorschläge… " , step = step )
async def _vorschlag ( i , path ) :
@@ -1894,6 +2271,26 @@ def _beispiel_schema(data):
return out
def _beispiel_check_schema ( data ) :
""" Worked-Example-Check → { " ok " : true} → set() (alles korrekt); { " probleme " :[ { " index " :N}]} →
{ N, …} (1-basierte beanstandete Indizes); None bei kaputt. """
if not isinstance ( data , dict ) :
return None
if data . get ( " ok " ) is True :
return set ( )
pr = data . get ( " probleme " )
if not isinstance ( pr , list ) :
return None
out : set [ int ] = set ( )
for p in pr :
if isinstance ( p , dict ) :
try :
out . add ( int ( p . get ( " index " ) ) )
except ( ValueError , TypeError ) :
continue
return out
_ARTEFAKT_SCHEMA = { " karteikarte " : _karten_schema , " beispiel " : _beispiel_schema }
_ARTEFAKT_PROMPT = { " karteikarte " : " Artefakt-Karteikarte " , " beispiel " : " Artefakt-Beispiel " }
_ARTEFAKT_SCHRITT = { " karteikarte " : " Karteikarten " , " beispiel " : " Beispiele " }
@@ -1907,9 +2304,11 @@ def _artefakte_komplett(files: dict) -> bool:
async def _artefakte_block ( ctx : GenContext , set_p , files : dict , sidecar : dict , instructions : str ) - > dict | None :
""" Lern-Artefakte je Typ aus den gespeicherten Fakten erzeugen — ein Generierungs-Durchlauf
je Typ über Chunks, keine Verifikation (Bonus-Material). → { typ: [eintraege]} (auch in files). """
topic , is_cancelled = ctx . topic , ctx . is_cancelled
je Typ über Chunks. Worked Examples werden gegen die Fakten verifiziert (falsche verworfen);
Karteikarten sind risikoarm und bleiben ungeprüft. → { typ: [eintraege]} (auch in files). """
topic , provider , is_cancelled = ctx . topic , ctx . provider , ctx . is_cancelled
arbeit = files [ " arbeit " ]
caps = " files "
# Bausteine mit Subs + Fakten-Zeilen als Input-Block (extract-once aus den Fakten).
bausteine = [ ]
for btitel , subs in sidecar . items ( ) :
@@ -1935,6 +2334,36 @@ async def _artefakte_block(ctx: GenContext, set_p, files: dict, sidecar: dict, i
def block_text ( idxs ) :
return " \n \n " . join ( f " BAUSTEIN: { bausteine [ i ] [ 0 ] } \n SUBBAUSTEINE: \n " + " \n " . join ( bausteine [ i ] [ 1 ] ) for i in idxs )
# Worked Examples gegen die Fakten prüfen (Panel-Mehrheit) — falsche verwerfen. CoT-Schritte sind
# fehleranfällig; ein falsches Beispiel prägt ein fehlerhaftes Schema ein → kein Beispiel > falsches.
async def _pruefe_beispiele ( ci , idxs , items ) :
if is_cancelled ( ) or not items :
return items
def cpath ( j ) : return arbeit / f " artefakt-beispiel-check-c { ci } -j { j } .json "
beispiele_txt = " \n \n " . join (
f " { k } . PROBLEM: { e [ ' problem ' ] } \n SCHRITTE: " + " | " . join ( e . get ( " schritte " , [ ] ) )
+ ( f " \n ERGEBNIS: { e [ ' ergebnis ' ] } " if e . get ( " ergebnis " ) else " " )
for k , e in enumerate ( items , 1 ) )
offen = [ j for j in ( 1 , 2 , 3 ) [ : FAKTEN_CHECK_PANEL ] if _beispiel_check_schema ( _json_datei ( cpath ( j ) ) ) is None ]
if offen :
await asyncio . gather ( * [
run_agent ( f " bausteine- { topic } -artefakt-beispiel-check-c { ci } -j { j } " ,
_prompt ( " Artefakt-Beispiel-Check " , topic = topic , fakten = block_text ( idxs ) , beispiele = beispiele_txt , out_path = cpath ( j ) , extra = _extra ( instructions ) ) ,
_timeout ( " inhalt_check " , len ( items ) ) , provider = provider , role = " judge " , capabilities = caps )
for j in offen ] , return_exceptions = True )
outs = [ s for j in ( 1 , 2 , 3 ) [ : FAKTEN_CHECK_PANEL ] if ( s := _beispiel_check_schema ( _json_datei ( cpath ( j ) ) ) ) is not None ]
if not outs :
return items # keine Prüfung möglich → behalten (Best-effort)
votes : dict [ int , int ] = { }
for s in outs :
for idx in s :
votes [ idx ] = votes . get ( idx , 0 ) + 1
schwelle = len ( outs ) / 2
raus = { idx for idx , v in votes . items ( ) if v > schwelle } # Mehrheit (≥2 von 3) beanstandet → raus
if raus :
_log ( topic , f " Worked-Example-Check Chunk { ci } : { len ( raus ) } / { len ( items ) } verworfen " )
return [ e for k , e in enumerate ( items , 1 ) if k not in raus ]
ergebnis : dict [ str , list ] = { }
for typ in ARTEFAKT_TYPEN :
schema = _ARTEFAKT_SCHEMA [ typ ]
@@ -1957,7 +2386,10 @@ async def _artefakte_block(ctx: GenContext, set_p, files: dict, sidecar: dict, i
return None
eintraege : list = [ ]
for ci in range ( len ( chunks ) ) :
eintraege + = schema ( _json_datei ( apath ( ci ) ) ) or [ ]
chunk_items = schema ( _json_datei ( apath ( ci ) ) ) or [ ]
if typ == " beispiel " and chunk_items :
chunk_items = await _pruefe_beispiele ( ci , chunks [ ci ] , chunk_items )
eintraege + = chunk_items
ergebnis [ typ ] = eintraege
atomic_write_json ( files [ " artefakte " ] , ergebnis , indent = 1 )
return ergebnis
@@ -2069,7 +2501,7 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
_reset_ab_phase ( topic , label )
await _reset_db_ab_phase ( topic , label )
# Schritt „Quelle aufbereiten": Crawl (link) + PDFs + Content/Noise-Sichtung.
if not await _quelle_aufbereiten ( ctx , set_p , files , q , ordner ) :
if not await _quelle_aufbereiten ( ctx , set_p , files , q , ordner , instructions ):
if is_cancelled ( ) :
abgebrochen ( )
return