This commit is contained in:
team3
2026-06-30 00:14:18 +02:00
parent 3e3559aa8f
commit c794fcaccf
152 changed files with 9485 additions and 9583 deletions

View File

@@ -1,4 +1,4 @@
"""Elemente (persönliche Zusammenfassung) und Tutor-Chat zum Guide."""
"""Elements (personal summary) and tutor chat for the guide."""
import json
import logging
@@ -6,25 +6,25 @@ import uuid
from agents import run_agent
from config import DEFAULT_PROVIDER
from jsonio import parse_json_text as _parse_json_text, read_json_file as _json_datei
from paths import bausteine_path, guide_content_path
from jsonio import parse_json_text as _parse_json_text, read_json_file as _read_json_file
from paths import blocks_path, guide_content_path
from pipeline import _prompt
log = logging.getLogger("creator.elements")
# --- Tutor-Chat ---
# --- Tutor chat ---
def _build_guide_chat_prompt(topic: str, format_name: str, section: str, outline: str, messages: list[dict]) -> str:
transcript = "\n".join(
f"{'Nutzer' if m.get('role') == 'user' else 'Assistent'}: {m.get('content', '')}"
f"{'User' if m.get('role') == 'user' else 'Assistant'}: {m.get('content', '')}"
for m in messages
)
return _prompt(
"Chat",
topic=topic, format_name=format_name,
outline_block=outline.strip() or "(keine)",
section_block=section.strip() or "(kein Abschnitt erkannt)",
outline_block=outline.strip() or "(none)",
section_block=section.strip() or "(no section detected)",
transcript=transcript,
)
@@ -36,62 +36,62 @@ async def chat_with_guide(topic: str, format_name: str, section: str, outline: s
"chat-" + str(uuid.uuid4()), prompt, 240, provider=provider, role="fast", capabilities="none", lane="interactive"
)
if returncode != 0:
return "Entschuldigung, das hat nicht geklappt. Bitte versuche es erneut."
return "Sorry, that didn't work. Please try again."
reply = stdout.strip()
return reply or "Entschuldigung, ich habe keine Antwort erhalten."
return reply or "Sorry, I didn't get a response."
except Exception:
log.warning("[%s] Guide-Chat fehlgeschlagen", topic, exc_info=True)
return "Entschuldigung, das hat nicht geklappt. Bitte versuche es erneut."
log.warning("[%s] Guide chat failed", topic, exc_info=True)
return "Sorry, that didn't work. Please try again."
# --- Elemente ---
# --- Elements ---
def _element_fields(data: dict) -> dict | None:
"""Validiert KI-Element-JSON und normalisiert auf die DB-Felder."""
"""Validate AI element JSON and normalize it onto the DB fields."""
if not isinstance(data, dict):
return None
title = str(data.get("title", "")).strip()
if not title:
return None
listen = {}
lists = {}
for key in ("examples", "hints"):
raw = data.get(key, [])
listen[key] = [str(e).strip() for e in raw if str(e).strip()] if isinstance(raw, list) else []
lists[key] = [str(e).strip() for e in raw if str(e).strip()] if isinstance(raw, list) else []
return {
"title": title[:200],
"description": str(data.get("description", "")).strip(),
"examples": listen["examples"],
"hints": listen["hints"],
"examples": lists["examples"],
"hints": lists["hints"],
}
def _topic_context(topic: str, limit: int = 12000) -> str:
"""Bausteine + Guide-Inhalte des Themas als Kontext-Text (gekürzt)."""
"""Blocks + guide content of the topic as context text (truncated)."""
parts: list[str] = []
bp = bausteine_path(topic)
bp = blocks_path(topic)
if bp.exists():
parts.append(bp.read_text(encoding="utf-8"))
for fmt in ("Guide", "FullGuide"): # bester verfügbarer Prosa-Guide als Chat-Kontext
content = _json_datei(guide_content_path(topic, fmt))
for fmt in ("Guide", "FullGuide"): # best available prose guide as chat context
content = _read_json_file(guide_content_path(topic, fmt))
if content:
for ch in content.get("chapters", []):
for sec in ch.get("sections", []):
parts.append(sec if isinstance(sec, str) else json.dumps(sec, ensure_ascii=False))
break # bester verfügbarer Guide reicht
break # the best available guide is enough
text = "\n\n".join(parts).strip()
return text[:limit] if text else "(kein Material vorhanden)"
return text[:limit] if text else "(no material available)"
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": []}
"""Create element fields via AI. Fallback: only the title from the keyword."""
fallback = {"title": hint.strip() or "New 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)",
topic=topic, hint=hint.strip() or "(none — pick a core concept yourself)",
context=context,
)
returncode, stdout, _ = await run_agent(
@@ -101,12 +101,12 @@ async def generate_element(topic: str, hint: str, provider: str = DEFAULT_PROVID
return fallback
return _element_fields(_parse_json_text(stdout)) or fallback
except Exception:
log.warning("[%s] Element-Erstellung fehlgeschlagen", topic, exc_info=True)
log.warning("[%s] Element creation failed", topic, exc_info=True)
return fallback
def _parse_suggestions(stdout: str) -> list[dict] | None:
"""Validiert Vorschlags-JSON aus KI-Output. None bei ungültigem JSON."""
"""Validate suggestion JSON from AI output. None on invalid JSON."""
data = _parse_json_text(stdout)
if not isinstance(data, dict):
return None
@@ -123,7 +123,7 @@ def _parse_suggestions(stdout: str) -> list[dict] | None:
async def check_element(element: dict, provider: str = DEFAULT_PROVIDER) -> list[dict] | None:
"""Zweischrittige Prüfung auf fehlende Infos: RechercheVerifizieren. None bei Fehler."""
"""Two-step check for missing info: research → verify. None on error."""
try:
element_json = json.dumps(
{k: element[k] for k in ("title", "description", "examples", "hints")},
@@ -131,7 +131,7 @@ async def check_element(element: dict, provider: str = DEFAULT_PROVIDER) -> list
)
context = _topic_context(element["topic"])
# Schritt 1: Recherchebreit Kandidaten sammeln
# Step 1: research — collect candidates broadly
prompt = _prompt("Element-Check", topic=element["topic"], element_json=element_json, context=context)
returncode, stdout, _ = await run_agent(
"element-check-" + str(uuid.uuid4()), prompt, 240, provider=provider, role="fast", capabilities="none", lane="interactive"
@@ -144,7 +144,7 @@ async def check_element(element: dict, provider: str = DEFAULT_PROVIDER) -> list
if not candidates:
return []
# Schritt 2: Verifizieren — nur Wichtiges, nicht Redundantes durchlassen
# Step 2: verify — only let important, non-redundant items through
prompt = _prompt(
"Element-Verify",
topic=element["topic"], element_json=element_json,
@@ -158,7 +158,7 @@ async def check_element(element: dict, provider: str = DEFAULT_PROVIDER) -> list
return None
return _parse_suggestions(stdout)
except Exception:
log.warning("[%s] Element-Prüfung fehlgeschlagen", element.get("topic", "?"), exc_info=True)
log.warning("[%s] Element check failed", element.get("topic", "?"), exc_info=True)
return None
@@ -170,7 +170,7 @@ def _element_json(element: dict) -> str:
def _validate_change(c, element: dict) -> dict | None:
"""Validiert einen Änderungs-Vorschlag aus KI-Output gegen das Element."""
"""Validate a change suggestion from AI output against the element."""
if not isinstance(c, dict):
return None
text = str(c.get("text", "")).strip()
@@ -178,16 +178,16 @@ def _validate_change(c, element: dict) -> dict | None:
target = c.get("target")
index = c.get("index")
content = str(c.get("content", "")).strip()
if not text or action not in ("entfernen", "anpassen", "hinzufuegen"):
if not text or action not in ("remove", "adjust", "add"):
return None
if target not in ("title", "description", "examples", "hints"):
return None
if action in ("anpassen", "hinzufuegen") and not content:
if action in ("adjust", "add") and not content:
return None
if action == "entfernen" and target not in ("examples", "hints"):
if action == "remove" and target not in ("examples", "hints"):
return None
# Index nur für anpassen/entfernen in Listen-Feldern; muss existieren
if target in ("examples", "hints") and action in ("anpassen", "entfernen"):
# Index only for adjust/remove on list fields; must exist
if target in ("examples", "hints") and action in ("adjust", "remove"):
if not isinstance(index, int) or not (0 <= index < len(element[target])):
return None
else:
@@ -196,11 +196,11 @@ def _validate_change(c, element: dict) -> dict | None:
async def chat_with_element(element: dict, messages: list[dict], provider: str = DEFAULT_PROVIDER) -> tuple[str, list[dict]]:
"""Chat zum Element. Gibt (Antwort, Änderungs-Vorschläge) zurück — ändert nichts direkt."""
fehler = "Entschuldigung, das hat nicht geklappt. Bitte versuche es erneut."
"""Chat about the element. Returns (reply, change suggestions) — changes nothing directly."""
error = "Sorry, that didn't work. Please try again."
try:
transcript = "\n".join(
f"{'Nutzer' if m.get('role') == 'user' else 'Assistent'}: {m.get('content', '')}"
f"{'User' if m.get('role') == 'user' else 'Assistant'}: {m.get('content', '')}"
for m in messages
)
prompt = _prompt("Element-Chat", topic=element["topic"], element_json=_element_json(element), transcript=transcript)
@@ -208,22 +208,22 @@ async def chat_with_element(element: dict, messages: list[dict], provider: str =
"element-chat-" + str(uuid.uuid4()), prompt, 240, provider=provider, role="fast", capabilities="none", lane="interactive"
)
if returncode != 0:
return fehler, []
return error, []
data = _parse_json_text(stdout)
if not isinstance(data, dict):
return fehler, []
return error, []
changes = [v for c in data.get("changes", []) if (v := _validate_change(c, element))]
reply = str(data.get("reply", "")).strip() or ("Vorschläge erstellt." if changes else fehler)
reply = str(data.get("reply", "")).strip() or ("Suggestions created." if changes else error)
return reply, changes
except Exception:
log.warning("[%s] Element-Chat fehlgeschlagen", element.get("topic", "?"), exc_info=True)
return fehler, []
log.warning("[%s] Element chat failed", element.get("topic", "?"), exc_info=True)
return error, []
async def style_element(element: dict, provider: str = DEFAULT_PROVIDER) -> list[dict] | None:
"""Prüft ein Element auf die Stil-Regeln und schlägt Änderungen vor. None bei Fehler."""
"""Check an element against the style rules and suggest changes. None on error."""
try:
prompt = _prompt("Element-Stil", topic=element["topic"], element_json=_element_json(element))
prompt = _prompt("Element-Style", topic=element["topic"], element_json=_element_json(element))
returncode, stdout, _ = await run_agent(
"element-stil-" + str(uuid.uuid4()), prompt, 240, provider=provider, role="fast", capabilities="none", lane="interactive"
)
@@ -234,12 +234,12 @@ async def style_element(element: dict, provider: str = DEFAULT_PROVIDER) -> list
return None
return [v for c in data.get("changes", []) if (v := _validate_change(c, element))]
except Exception:
log.warning("[%s] Stil-Prüfung fehlgeschlagen", element.get("topic", "?"), exc_info=True)
log.warning("[%s] Style check failed", element.get("topic", "?"), exc_info=True)
return None
async def refine_suggestion(element: dict, suggestion: dict, instruction: str, provider: str = DEFAULT_PROVIDER) -> dict | None:
"""Überarbeitet einen einzelnen Vorschlag nach Nutzer-Anweisung. None bei Fehler."""
"""Revise a single suggestion per user instruction. None on error."""
try:
prompt = _prompt(
"Element-Refine",
@@ -257,5 +257,5 @@ async def refine_suggestion(element: dict, suggestion: dict, instruction: str, p
return None
return _validate_change(data.get("change"), element)
except Exception:
log.warning("[%s] Vorschlags-Überarbeitung fehlgeschlagen", element.get("topic", "?"), exc_info=True)
log.warning("[%s] Suggestion revision failed", element.get("topic", "?"), exc_info=True)
return None