This commit is contained in:
team3
2026-06-07 16:34:17 +02:00
parent ab8c577899
commit 58fd209174
8 changed files with 437 additions and 189 deletions

View File

@@ -1500,48 +1500,69 @@ async def check_element(element: dict, provider: str = DEFAULT_PROVIDER) -> list
return None
async def _element_rewrite(template: str, element: dict, provider: str, **extra) -> tuple[str, dict | None]:
"""Gemeinsames Muster: Element + Template → (Antwort, neue Felder|None)."""
def _element_json(element: dict) -> str:
return json.dumps(
{k: element[k] for k in ("title", "description", "examples", "hints")},
ensure_ascii=False, indent=1,
)
def _validate_change(c, element: dict) -> dict | None:
"""Validiert einen Änderungs-Vorschlag aus KI-Output gegen das Element."""
if not isinstance(c, dict):
return None
text = str(c.get("text", "")).strip()
action = c.get("action")
target = c.get("target")
index = c.get("index")
content = str(c.get("content", "")).strip()
if not text or action not in ("entfernen", "anpassen", "hinzufuegen"):
return None
if target not in ("title", "description", "examples", "hints"):
return None
if action in ("anpassen", "hinzufuegen") and not content:
return None
if action == "entfernen" 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"):
if not isinstance(index, int) or not (0 <= index < len(element[target])):
return None
else:
index = None
if target == "examples" and action in ("anpassen", "hinzufuegen"):
content = _fence(content)
return {"text": text, "action": action, "target": target, "index": index, "content": content}
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."
try:
element_json = json.dumps(
{k: element[k] for k in ("title", "description", "examples", "hints")},
ensure_ascii=False, indent=1,
transcript = "\n".join(
f"{'Nutzer' if m.get('role') == 'user' else 'Assistent'}: {m.get('content', '')}"
for m in messages
)
prompt = _prompt(template, topic=element["topic"], element_json=element_json, **extra)
prompt = _prompt("Element-Chat", topic=element["topic"], element_json=_element_json(element), transcript=transcript)
returncode, stdout, _ = await run_agent(
f"element-{template.lower()}-" + str(uuid.uuid4()), prompt, 240,
provider=provider, role="fast", capabilities="none",
"element-chat-" + str(uuid.uuid4()), prompt, 240, provider=provider, role="fast", capabilities="none"
)
if returncode != 0:
return fehler, None
return fehler, []
data = _parse_json_text(stdout)
if not isinstance(data, dict):
return fehler, None
fields = _element_fields(data.get("element"))
reply = str(data.get("reply", "")).strip() or ("Erledigt." if fields else fehler)
return reply, fields
return fehler, []
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)
return reply, changes
except Exception:
return fehler, None
async def chat_with_element(element: dict, messages: list[dict], provider: str = DEFAULT_PROVIDER) -> tuple[str, dict | None]:
"""Passt ein Element per Chat an. Gibt (Antwort, neue Felder|None) zurück."""
transcript = "\n".join(
f"{'Nutzer' if m.get('role') == 'user' else 'Assistent'}: {m.get('content', '')}"
for m in messages
)
return await _element_rewrite("Element-Chat", element, provider, transcript=transcript)
return fehler, []
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."""
try:
element_json = json.dumps(
{k: element[k] for k in ("title", "description", "examples", "hints")},
ensure_ascii=False, indent=1,
)
prompt = _prompt("Element-Stil", topic=element["topic"], element_json=element_json)
prompt = _prompt("Element-Stil", 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"
)
@@ -1550,32 +1571,28 @@ async def style_element(element: dict, provider: str = DEFAULT_PROVIDER) -> list
data = _parse_json_text(stdout)
if not isinstance(data, dict):
return None
changes = []
for c in data.get("changes", []):
if not isinstance(c, dict):
continue
text = str(c.get("text", "")).strip()
action = c.get("action")
target = c.get("target")
index = c.get("index")
content = str(c.get("content", "")).strip()
if not text or action not in ("entfernen", "anpassen", "hinzufuegen"):
continue
if target not in ("title", "description", "examples", "hints"):
continue
if action in ("anpassen", "hinzufuegen") and not content:
continue
if action == "entfernen" and target not in ("examples", "hints"):
continue
# Index nur für anpassen/entfernen in Listen-Feldern; muss existieren
if target in ("examples", "hints") and action in ("anpassen", "entfernen"):
if not isinstance(index, int) or not (0 <= index < len(element[target])):
continue
else:
index = None
if target == "examples" and action in ("anpassen", "hinzufuegen"):
content = _fence(content)
changes.append({"text": text, "action": action, "target": target, "index": index, "content": content})
return changes
return [v for c in data.get("changes", []) if (v := _validate_change(c, element))]
except Exception:
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."""
try:
prompt = _prompt(
"Element-Refine",
topic=element["topic"], element_json=_element_json(element),
suggestion_json=json.dumps(suggestion, ensure_ascii=False, indent=1),
instruction=instruction,
)
returncode, stdout, _ = await run_agent(
"element-refine-" + str(uuid.uuid4()), prompt, 240, provider=provider, role="fast", capabilities="none"
)
if returncode != 0:
return None
data = _parse_json_text(stdout)
if not isinstance(data, dict):
return None
return _validate_change(data.get("change"), element)
except Exception:
return None

View File

@@ -136,7 +136,17 @@ class ElementChatRequest(BaseModel):
class ElementChatResponse(BaseModel):
reply: str
element: ElementResponse
changes: list[ElementStyleChange] = []
class ElementRefineRequest(BaseModel):
suggestion: ElementStyleChange
instruction: str = Field(min_length=1, max_length=2000)
provider: ProviderType = "claude"
class ElementRefineResponse(BaseModel):
change: ElementStyleChange
class ProgressUpdate(BaseModel):

View File

@@ -18,7 +18,7 @@ from database import (
from generator import (
generate_guide, cancel_guide, chat_with_guide, guide_slot_dateien,
generate_bausteine, cancel_bausteine, bausteine_status, active_bausteine, reset_bausteine,
generate_element, chat_with_element, check_element, style_element,
generate_element, chat_with_element, check_element, style_element, refine_suggestion,
)
from models import (
GuideCreateRequest, GuideResponse,
@@ -27,6 +27,7 @@ from models import (
GuideChatRequest, GuideChatResponse,
ElementCreateRequest, ElementChatRequest, ElementChatResponse, ElementResponse,
ElementUpdateRequest, ElementCheckRequest, ElementCheckResponse, ElementStyleResponse,
ElementRefineRequest, ElementRefineResponse,
ProgressUpdate, ProgressResponse, ProjectResponse, ProviderInfo,
)
from paths import bausteine_path, bausteine_topics, guide_content_path, project_dir, topic_dir
@@ -288,12 +289,19 @@ async def element_chat(element_id: str, req: ElementChatRequest):
element = await get_element(element_id)
if element is None:
raise HTTPException(404, "Element nicht gefunden")
reply, fields = await chat_with_element(element, [m.model_dump() for m in req.messages], provider=req.provider)
if fields:
now = datetime.now(timezone.utc).isoformat()
await update_element(element_id, **fields, updated_at=now)
element = await get_element(element_id)
return {"reply": reply, "element": element}
reply, changes = await chat_with_element(element, [m.model_dump() for m in req.messages], provider=req.provider)
return {"reply": reply, "changes": changes}
@router.post("/elements/{element_id}/refine", response_model=ElementRefineResponse)
async def element_refine(element_id: str, req: ElementRefineRequest):
element = await get_element(element_id)
if element is None:
raise HTTPException(404, "Element nicht gefunden")
change = await refine_suggestion(element, req.suggestion.model_dump(), req.instruction, provider=req.provider)
if change is None:
raise HTTPException(502, "Überarbeitung fehlgeschlagen — bitte erneut versuchen")
return {"change": change}
@router.put("/elements/{element_id}", response_model=ElementResponse)