update
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user