This commit is contained in:
Team3
2026-05-27 01:00:33 +02:00
parent ad2f3e4786
commit 351f330db0
10 changed files with 1184 additions and 13 deletions

View File

@@ -1,6 +1,9 @@
import asyncio
import json
import re
import subprocess
import tempfile
import uuid
from datetime import datetime, timezone
from pathlib import Path
@@ -13,7 +16,14 @@ from config import (
REVIEW_TIMEOUTS,
STORAGE_DIR,
)
from database import update_guide
from database import (
update_guide,
create_baustein,
create_suggestions,
delete_pending_suggestions,
list_bausteine,
update_baustein,
)
_semaphore = asyncio.Semaphore(MAX_CONCURRENT_GENERATIONS)
_active_processes: dict[str, asyncio.subprocess.Process] = {}
@@ -35,12 +45,13 @@ async def _set_progress(guide_id: str, progress: str) -> None:
await update_guide(guide_id, progress=progress, updated_at=now)
async def _run_claude(guide_id: str, prompt: str, timeout: int) -> tuple[int, str, str]:
async def _run_claude(guide_id: str, prompt: str, timeout: int, tools: str | None = "Write,Bash,Read,WebSearch,WebFetch") -> tuple[int, str, str]:
cmd = [CLAUDE_CLI, "-p"]
if tools:
cmd += ["--allowedTools", tools]
cmd += ["--dangerously-skip-permissions"]
process = await asyncio.create_subprocess_exec(
CLAUDE_CLI,
"-p",
"--allowedTools", "Write,Bash,Read,WebSearch,WebFetch",
"--dangerously-skip-permissions",
*cmd,
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
@@ -397,3 +408,136 @@ async def rework_guide(guide_id: str, topic: str, format_name: str, instructions
async def _fail(guide_id: str, msg: str) -> None:
now = datetime.now(timezone.utc).isoformat()
await update_guide(guide_id, status="error", progress=None, error_msg=msg, updated_at=now)
# --- Bausteine ---
_suggestions_generating: set[str] = set()
def is_suggestions_generating(topic: str) -> bool:
return topic in _suggestions_generating
def _parse_json(text: str):
text = text.strip()
text = re.sub(r"^```(?:json)?\s*", "", text)
text = re.sub(r"\s*```$", "", text)
return json.loads(text)
def _build_suggestions_prompt(topic: str, html_paths: list[Path], existing_titles: list[str]) -> str:
spec = (TEMPLATES_DIR / "Format" / "Baustein.md").read_text(encoding="utf-8")
reference = (TEMPLATES_DIR / "Referenz" / "Baustein.md").read_text(encoding="utf-8")
existing_list = "\n".join(f"- {t}" for t in existing_titles) if existing_titles else "(keine)"
if html_paths:
read_instructions = "\n".join(f"- Lies: {p}" for p in html_paths)
guides_section = f"""SCHRITT 1 — Guides lesen:
{read_instructions}
"""
else:
guides_section = ""
return f"""Schlage fundamentale Bausteine (Kernkonzepte) zum Thema "{topic}" vor.
{guides_section}Bereits vorhandene Bausteine (NICHT erneut vorschlagen):
{existing_list}
FORMAT-SPEZIFIKATION:
{spec}
REFERENZ-BEISPIEL:
{reference}
Schlage bis zu 20 Bausteine vor. Antworte AUSSCHLIESSLICH mit einem JSON-Array. Jedes Element hat:
- "title"
- "description"
- "purpose"
- "examples": Array mit 4 Objekten {{"label": "...", "code": "..."}}
Orientiere dich an der Spezifikation und Referenz. NUR das JSON-Array, kein weiterer Text.
"""
def _build_baustein_detail_prompt(topic: str, title: str) -> str:
spec = (TEMPLATES_DIR / "Format" / "Baustein.md").read_text(encoding="utf-8")
reference = (TEMPLATES_DIR / "Referenz" / "Baustein.md").read_text(encoding="utf-8")
return f"""Generiere Details für den Baustein "{title}" im Kontext des Themas "{topic}".
FORMAT-SPEZIFIKATION:
{spec}
REFERENZ-BEISPIEL:
{reference}
Antworte AUSSCHLIESSLICH mit einem JSON-Objekt mit den Feldern "description", "purpose", "examples".
"examples" ist ein Array mit 4 Objekten {{"label": "...", "code": "..."}}.
Orientiere dich an der Spezifikation und Referenz. Kein weiterer Text, nur das JSON.
"""
async def generate_suggestions(topic: str, html_paths: list[Path]) -> None:
_suggestions_generating.add(topic)
try:
existing = await list_bausteine(topic)
existing_titles = [b["title"] for b in existing]
await delete_pending_suggestions(topic)
prompt = _build_suggestions_prompt(topic, html_paths, existing_titles)
tools = "Read" if html_paths else None
returncode, stdout, stderr = await _run_claude("suggestions-" + topic, prompt, 180, tools=tools)
if returncode != 0:
return
items = _parse_json(stdout)
if not isinstance(items, list):
return
now = datetime.now(timezone.utc).isoformat()
suggestions = []
for item in items[:20]:
suggestions.append({
"id": str(uuid.uuid4()),
"topic": topic,
"title": item.get("title", ""),
"description": item.get("description", ""),
"purpose": item.get("purpose", ""),
"example": json.dumps(item.get("examples", []), ensure_ascii=False),
"status": "pending",
"created_at": now,
})
if suggestions:
await create_suggestions(suggestions)
except Exception:
pass
finally:
_suggestions_generating.discard(topic)
async def generate_baustein_detail(baustein_id: str, topic: str, title: str) -> None:
try:
prompt = _build_baustein_detail_prompt(topic, title)
returncode, stdout, stderr = await _run_claude("baustein-" + baustein_id, prompt, 60, tools=None)
if returncode != 0:
return
data = _parse_json(stdout)
if not isinstance(data, dict):
return
now = datetime.now(timezone.utc).isoformat()
await update_baustein(
baustein_id,
description=data.get("description", ""),
purpose=data.get("purpose", ""),
example=json.dumps(data.get("examples", []), ensure_ascii=False),
updated_at=now,
)
except Exception:
pass