This commit is contained in:
Team3
2026-07-04 21:27:47 +02:00
parent c05421a8c1
commit 2d9ca00b47
3 changed files with 71 additions and 9 deletions

View File

@@ -656,7 +656,18 @@ def _finder_material(folder, sources: list[str] | None, queries: list[str]) -> t
return "", "Backed, not invented. Verify uncertain points via web search.", ("files" if folder else "full") return "", "Backed, not invented. Verify uncertain points via web search.", ("files" if folder else "full")
def _build_research_prompt(topic: str, out_path: Path, instructions: str, type: str, folder: Path | None, fokus: str = "", section: str = "") -> str: def material_folder(topic: str) -> Path | None:
"""Korpus fürs Inline-Material: echte Quelle (uni/projekt/link) oder bei thema die
Fundstellen der Research-Reader (arbeit/material/*.txt). Nur für Evidence-Packs —
Konsens-Gate und QA messen unverändert gegen die konfigurierte Quelle (Messinvarianz)."""
f = source_folder(topic)
if f is not None:
return f
d = arbeit_dir(topic) / "material"
return d if d.is_dir() and any(d.glob("*.txt")) else None
def _build_research_prompt(topic: str, out_path: Path, instructions: str, type: str, folder: Path | None, fokus: str = "", section: str = "", material_path: Path | None = None) -> str:
if section: if section:
# Section mode (uni/projekt): text directly in the prompt → small context, no file reading. # Section mode (uni/projekt): text directly in the prompt → small context, no file reading.
source = section source = section
@@ -664,6 +675,13 @@ def _build_research_prompt(topic: str, out_path: Path, instructions: str, type:
source = _prompt(_SOURCE_TEMPLATE[type], project=folder) source = _prompt(_SOURCE_TEMPLATE[type], project=folder)
else: else:
source = _prompt("Blocks-Source-Thema", topic=topic) source = _prompt("Blocks-Source-Thema", topic=topic)
if material_path is not None:
# thema: Fundstellen sichern — sie werden das Inline-Material der Folgeschritte
# (Finder/Facts liefen sonst mit eigener Websuche → Reasoning-Schleifen, Retries)
source += ("\n\nALSO write the file " + str(material_path) + " as you research: for every "
"source you use, append one line with the URL followed by the relevant excerpt "
"(plain text). Later steps back all facts ONLY against this material — "
"an excerpt you skip here cannot back anything later.")
return _prompt( return _prompt(
"Blocks-Research", "Blocks-Research",
topic=topic, source=source, blocks_path=out_path, focus=fokus, extra=_extra(instructions), topic=topic, source=source, blocks_path=out_path, focus=fokus, extra=_extra(instructions),
@@ -790,6 +808,7 @@ async def _subblocks_block(ctx: GenContext, set_p, files: dict, entries: dict, i
topic, provider, is_cancelled = ctx.topic, ctx.provider, ctx.is_cancelled topic, provider, is_cancelled = ctx.topic, ctx.provider, ctx.is_cancelled
work_dir = files["arbeit"] work_dir = files["arbeit"]
folder = source_folder(topic) folder = source_folder(topic)
mat = material_folder(topic) # thema: Research-Fundstellen als Inline-Korpus
caps = "files" if folder else "full" caps = "files" if folder else "full"
# Source for the evidence exam in the clarify step (discards invented/unsupportable subs). # Source for the evidence exam in the clarify step (discards invented/unsupportable subs).
_type = load_source(topic).get("type", "thema") _type = load_source(topic).get("type", "thema")
@@ -896,7 +915,7 @@ async def _subblocks_block(ctx: GenContext, set_p, files: dict, entries: dict, i
# Material once per package (excerpts are round-invariant; one query per block # Material once per package (excerpts are round-invariant; one query per block
# keeps the coverage guarantee of _evidence_pack). # keeps the coverage guarantee of _evidence_pack).
material, backing, round_caps = await asyncio.to_thread( material, backing, round_caps = await asyncio.to_thread(
_finder_material, folder, sources, [str(entries[num]) for num in chunk]) _finder_material, mat, sources, [str(entries[num]) for num in chunk])
start = time.monotonic() start = time.monotonic()
round_n = 0 round_n = 0
while not is_cancelled(): while not is_cancelled():
@@ -1031,9 +1050,9 @@ async def _subblocks_block(ctx: GenContext, set_p, files: dict, entries: dict, i
if pending: if pending:
# Inline evidence: corpus excerpts in the prompt (no self-research); the judge # Inline evidence: corpus excerpts in the prompt (no self-research); the judge
# answers as TEXT, the engine persists the j-file (resume + majority unchanged). # answers as TEXT, the engine persists the j-file (resume + majority unchanged).
ev = _evidence_pack(folder, sources, ev = _evidence_pack(mat, sources,
[title_by_num[num] for num in chunk] [title_by_num[num] for num in chunk]
+ [s for num in chunk for s in shown_by_num.get(num, [])]) if folder else "" + [s for num in chunk for s in shown_by_num.get(num, [])]) if mat else ""
j_source = _prompt("Blocks-Source-Inline", excerpts=ev) if ev else source j_source = _prompt("Blocks-Source-Inline", excerpts=ev) if ev else source
def _sink(result, p): def _sink(result, p):
@@ -1147,7 +1166,7 @@ async def _subblocks_block(ctx: GenContext, set_p, files: dict, entries: dict, i
"Subbausteine. Suche gezielt nach WEITEREN belegbaren Kernaspekten, die " "Subbausteine. Suche gezielt nach WEITEREN belegbaren Kernaspekten, die "
"oben fehlen. Nimm NUR auf, was die Quellen wirklich hergeben — nicht aufblähen.") "oben fehlen. Nimm NUR auf, was die Quellen wirklich hergeben — nicht aufblähen.")
material, backing, round_caps = await asyncio.to_thread( material, backing, round_caps = await asyncio.to_thread(
_finder_material, folder, sources, [str(entries[num]) for num in lacking]) _finder_material, mat, sources, [str(entries[num]) for num in lacking])
paths = [work_dir / f"subblock-x{k}-c{c}-{i}.md" for i in (1, 2, 3)] paths = [work_dir / f"subblock-x{k}-c{c}-{i}.md" for i in (1, 2, 3)]
keys = [f"blocks-{topic}-{ns}subblock-x{k}-c{c}-{i}" for i in (1, 2, 3)] keys = [f"blocks-{topic}-{ns}subblock-x{k}-c{c}-{i}" for i in (1, 2, 3)]
new = await _one_round(f"{lbl}Subblocks catch-up {c} X{k}", lacking, assignment, paths, keys, known, focus, material, backing, round_caps) new = await _one_round(f"{lbl}Subblocks catch-up {c} X{k}", lacking, assignment, paths, keys, known, focus, material, backing, round_caps)
@@ -1494,7 +1513,7 @@ async def _luecken_runde(ctx: GenContext, files: dict, title: str, luecken: list
known = ("\n\nBEREITS ERFASST — liste diese NICHT erneut:\n" known = ("\n\nBEREITS ERFASST — liste diese NICHT erneut:\n"
+ "\n".join(f"- {s}" for s in have)) if have else "" + "\n".join(f"- {s}" for s in have)) if have else ""
material, backing, caps = await asyncio.to_thread( material, backing, caps = await asyncio.to_thread(
_finder_material, folder, sources, [title] + list(luecken)) _finder_material, material_folder(topic), sources, [title] + list(luecken))
paths = [work_dir / f"luecken-{ns}r1-{i}.md" for i in (1, 2, 3)] paths = [work_dir / f"luecken-{ns}r1-{i}.md" for i in (1, 2, 3)]
slots = [{ slots = [{
"key": f"blocks-{topic}-{ns}luecken-r1-{i}", "key": f"blocks-{topic}-{ns}luecken-r1-{i}",
@@ -1901,7 +1920,8 @@ async def _facts_block(ctx, set_p, files: dict, raw: dict, q: dict, folder, inst
Zeichen) und endeten mit leerem Turn — Retry-Wellen à 6090 s seriell pro Block. Zeichen) und endeten mit leerem Turn — Retry-Wellen à 6090 s seriell pro Block.
No-Tool-Calls mit Inline-Material hatten 0 solcher Fälle (Muster: Facts-Check). No-Tool-Calls mit Inline-Material hatten 0 solcher Fälle (Muster: Facts-Check).
Fail-open: ohne Treffer bleibt die alte Selbst-Recherche.""" Fail-open: ohne Treffer bleibt die alte Selbst-Recherche."""
ev = _evidence_pack(folder, sources, queries) if folder else "" mat = material_folder(topic)
ev = _evidence_pack(mat, sources, queries) if mat else ""
if ev: if ev:
return _prompt("Blocks-Source-Inline", excerpts=ev), "none" return _prompt("Blocks-Source-Inline", excerpts=ev), "none"
return source, caps return source, caps
@@ -1974,7 +1994,8 @@ async def _facts_block(ctx, set_p, files: dict, raw: dict, q: dict, folder, inst
cites = [bf.get("source", "") for fm in per.values() for fk in fm.values() cites = [bf.get("source", "") for fm in per.values() for fk in fm.values()
for bf in fk.get("cited_facts", [])] for bf in fk.get("cited_facts", [])]
fallback = [blocks[i][0] for i in idxs] + [fk["sub"] for fm in per.values() for fk in fm.values()] fallback = [blocks[i][0] for i in idxs] + [fk["sub"] for fm in per.values() for fk in fm.values()]
ev = _cited_evidence(folder, sources, cites, fallback) if folder else "" mat = material_folder(topic)
ev = _cited_evidence(mat, sources, cites, fallback) if mat else ""
c_source = _prompt("Blocks-Source-Inline", excerpts=ev) if ev else source c_source = _prompt("Blocks-Source-Inline", excerpts=ev) if ev else source
pending = [j for j in panel if _facts_check_schema(_json_file(chk_path(ci, j))) is None] pending = [j for j in panel if _facts_check_schema(_json_file(chk_path(ci, j))) is None]
tmap = {asyncio.create_task( tmap = {asyncio.create_task(

View File

@@ -209,6 +209,10 @@ async def _research_once(ctx: GenContext, flow: Flow, q: dict, folder, instructi
work_dir = flow.work_dir work_dir = flow.work_dir
caps = "files" if folder else "full" caps = "files" if folder else "full"
p = work_dir / f"research-{tag}.md" p = work_dir / f"research-{tag}.md"
mp = None
if folder is None: # thema: Fundstellen als Material für die Inline-Folgeschritte sichern
(work_dir / "material").mkdir(parents=True, exist_ok=True)
mp = work_dir / "material" / f"research-{tag}.txt"
stop = asyncio.Event() stop = asyncio.Event()
buf: list[str] = [] # assistant text streamed live from the JSON events buf: list[str] = [] # assistant text streamed live from the JSON events
@@ -237,7 +241,7 @@ async def _research_once(ctx: GenContext, flow: Flow, q: dict, folder, instructi
await run_single_slot( await run_single_slot(
ctx, f"Research {tag}", key=f"blocks-{ctx.topic}-research-{tag}", ctx, f"Research {tag}", key=f"blocks-{ctx.topic}-research-{tag}",
prompt=_build_research_prompt(ctx.topic, p, instructions, q["type"], folder, prompt=_build_research_prompt(ctx.topic, p, instructions, q["type"], folder,
fokus=fokus, section=section), fokus=fokus, section=section, material_path=mp),
role="quick", capabilities=caps, role="quick", capabilities=caps,
payload=(lambda result, p=p: _file_payload(p)), payload=(lambda result, p=p: _file_payload(p)),
timeout=RESEARCH_RUNTIME, on_line=_on_line, timeout=RESEARCH_RUNTIME, on_line=_on_line,

View File

@@ -372,6 +372,41 @@ async def test_clarify_inline_evidence_no_tools(sub_env, monkeypatch, tmp_path):
assert list(tmp_path.glob("subblock-final-*-j*.md")) # Engine persistiert die Judge-Antwort assert list(tmp_path.glob("subblock-final-*-j*.md")) # Engine persistiert die Judge-Antwort
async def test_thema_nutzt_research_material_inline(sub_env, monkeypatch, tmp_path):
"""thema mit Research-Fundstellen (arbeit/material/*.txt): Finder bekommt sie inline
und läuft ohne Tools — vorher eigene Websuche pro Call (Reasoning-Schleifen, Retries)."""
db, ctx, files = sub_env
monkeypatch.setattr(blx, "source_folder", lambda t: None)
md = tmp_path / "arbeit" / "material"
md.mkdir(parents=True)
(md / "research-1.txt").write_text(
"https://example.org/alpha\nAlpha Grundlagen: der Kernbegriff, gut belegt.\n",
encoding="utf-8")
monkeypatch.setattr(blx, "arbeit_dir", lambda t: tmp_path / "arbeit")
fake, prompts = _mk_race({1: ["Alpha Grundlagen"], 2: ["Alpha Grundlagen"]})
monkeypatch.setattr(blx, "_race", fake)
raw = await blx._subblocks_block(ctx, lambda *a, **k: None, files, {1: "Alpha — Grundkonzept"},
"", wipe=False, ns="x-")
assert raw == {"Alpha": ["Alpha Grundlagen"]}
finders = [s for s in fake.slots_seen if "-r1-" in s["key"]]
assert finders and all(s["capabilities"] == "none" for s in finders)
assert "── research-1.txt" in finders[0]["prompt"] # Fundstellen inline
def test_material_folder_fallbacks(monkeypatch, tmp_path):
"""Echte Quelle gewinnt; sonst arbeit/material mit Inhalt; sonst None."""
monkeypatch.setattr(blx, "source_folder", lambda t: tmp_path / "quelle")
assert blx.material_folder("t") == tmp_path / "quelle"
monkeypatch.setattr(blx, "source_folder", lambda t: None)
monkeypatch.setattr(blx, "arbeit_dir", lambda t: tmp_path / "arbeit")
assert blx.material_folder("t") is None # kein Material-Ordner
md = tmp_path / "arbeit" / "material"
md.mkdir(parents=True)
assert blx.material_folder("t") is None # leer zählt nicht
(md / "research-1.txt").write_text("x", encoding="utf-8")
assert blx.material_folder("t") == md
async def test_panel_2of3_kehrt_bei_einigkeit_zurueck(tmp_path): async def test_panel_2of3_kehrt_bei_einigkeit_zurueck(tmp_path):
"""Zwei übereinstimmende Verdicts → Rückkehr ohne den langsamen Dritten; sein """Zwei übereinstimmende Verdicts → Rückkehr ohne den langsamen Dritten; sein
Ergebnis wird detached nachpersistiert (Resume).""" Ergebnis wird detached nachpersistiert (Resume)."""
@@ -418,6 +453,7 @@ async def test_facts_check_inline_cited_evidence(sub_env, monkeypatch, tmp_path)
die Check-Datei schreibt die Engine aus der Text-Antwort.""" die Check-Datei schreibt die Engine aus der Text-Antwort."""
db, ctx, files = sub_env db, ctx, files = sub_env
d = _corpus(tmp_path) d = _corpus(tmp_path)
monkeypatch.setattr(blx, "source_folder", lambda t: d)
facts = {"facts": [{"block": "Alpha", "subblock": "Sub Eins", "key_points": ["k"], facts = {"facts": [{"block": "Alpha", "subblock": "Sub Eins", "key_points": ["k"],
"prerequisites": "", "hurdles": "", "prerequisites": "", "hurdles": "",
"cited_facts": [{"text": "T", "source": "Skript.txt, Z.2"}], "cited_facts": [{"text": "T", "source": "Skript.txt, Z.2"}],
@@ -455,6 +491,7 @@ async def test_facts_find_inline_evidence_no_tools(sub_env, monkeypatch, tmp_pat
verloren sich in Reasoning-Schleifen und endeten mit leerem Turn (Retry-Wellen).""" verloren sich in Reasoning-Schleifen und endeten mit leerem Turn (Retry-Wellen)."""
db, ctx, files = sub_env db, ctx, files = sub_env
d = _corpus(tmp_path) d = _corpus(tmp_path)
monkeypatch.setattr(blx, "source_folder", lambda t: d)
seen = [] seen = []
facts = {"facts": [{"block": "Alpha", "subblock": "Sub Eins", "key_points": ["k"], facts = {"facts": [{"block": "Alpha", "subblock": "Sub Eins", "key_points": ["k"],
"prerequisites": "", "hurdles": "", "prerequisites": "", "hurdles": "",