Backend: Python-Logging statt print, Diagnose in allen Fallbacks

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
team3
2026-06-12 07:52:33 +02:00
parent d97ec48bf1
commit 32f6fab16b
4 changed files with 49 additions and 4 deletions

View File

@@ -5,15 +5,19 @@ jeweilige Provider fehl — der andere läuft unverändert weiter.
"""
import asyncio
import logging
import os
import re
import shutil
import tempfile
import time
import urllib.request
from pathlib import Path
from config import PROVIDERS, DEFAULT_PROVIDER
log = logging.getLogger("creator.agents")
_active_processes: dict[str, asyncio.subprocess.Process] = {}
# Capability → Claude --allowedTools
@@ -54,7 +58,11 @@ def provider_available(provider: str) -> bool:
def kill_process(agent_key_prefix: str) -> None:
"""Killt alle aktiven Prozesse, deren Key mit dem Prefix beginnt (deckt -plan/-w1… ab)."""
for key, process in list(_active_processes.items()):
if key.startswith(agent_key_prefix) and process.returncode is None:
if process.returncode is not None: # tote Einträge beim Iterieren aufräumen
_active_processes.pop(key, None)
continue
if key.startswith(agent_key_prefix):
log.debug("kill agent %s", key)
process.kill()
@@ -76,6 +84,7 @@ async def run_agent(
async def _communicate(agent_key: str, cmd: list[str], stdin_data: bytes | None, timeout: int) -> tuple[int, str, str]:
start = time.monotonic()
process = await asyncio.create_subprocess_exec(
*cmd,
stdin=asyncio.subprocess.PIPE if stdin_data is not None else asyncio.subprocess.DEVNULL,
@@ -95,10 +104,18 @@ async def _communicate(agent_key: str, cmd: list[str], stdin_data: bytes | None,
await asyncio.wait_for(process.wait(), timeout=5)
except asyncio.TimeoutError:
pass
log.info("agent %s: Timeout nach %ds", agent_key, timeout)
raise
log.info(
"agent %s: exit %s nach %.1fs (%d Bytes stdout)",
agent_key, process.returncode, time.monotonic() - start, len(stdout),
)
return process.returncode, stdout.decode("utf-8", errors="replace"), stderr.decode("utf-8", errors="replace")
finally:
_active_processes.pop(agent_key, None)
# Pop nur bei Identität: ein Slot-Restart unter demselben Key darf den
# NEUEN Prozess nicht aus dem Tracking werfen.
if _active_processes.get(agent_key) is process:
del _active_processes[agent_key]
async def _run_claude_cli(agent_key: str, prompt: str, timeout: int, role: str, capabilities: str) -> tuple[int, str, str]:

View File

@@ -1,5 +1,6 @@
import asyncio
import json
import logging
import math
import shutil
import subprocess
@@ -45,8 +46,11 @@ def _extra(instructions: str) -> str:
return f"\n\nZUSÄTZLICHE ANWEISUNGEN VOM NUTZER:\n{instructions}\n" if instructions else ""
log = logging.getLogger("creator.generator")
def _log(topic: str, msg: str) -> None:
print(f"[generator] {topic}: {msg}", flush=True)
log.info("[%s] %s", topic, msg)
def _claude_error(label: str, returncode: int, stdout: str, stderr: str) -> str:
@@ -112,7 +116,8 @@ def _json_datei(path: Path):
text = path.read_text(encoding="utf-8").strip()
text = re.sub(r"^```(?:json)?\s*|\s*```$", "", text)
return json.loads(text)
except Exception:
except Exception as e:
log.debug("JSON-Datei ungültig: %s (%s)", path, e)
return None
@@ -580,6 +585,7 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
encoding="utf-8",
)
except Exception as e:
log.exception("[%s] Bausteine-Generierung fehlgeschlagen", topic)
_bausteine_errors[topic] = str(e)[:2000]
finally:
# Kein Datei-Cleanup: Zwischendateien bleiben für Resume bzw. Nachvollziehbarkeit.
@@ -1317,6 +1323,7 @@ async def generate_guide(guide_id: str, topic: str, format_name: str, instructio
except FileNotFoundError:
await _fail(guide_id, "Bausteine fehlen")
except Exception as e:
log.exception("[%s] Guide-Generierung fehlgeschlagen (%s)", topic, guide_id)
await _fail(guide_id, str(e)[:2000])
finally:
_cancelled.discard(guide_id)
@@ -1349,6 +1356,7 @@ async def chat_with_guide(topic: str, format_name: str, section: str, outline: s
reply = stdout.strip()
return reply or "Entschuldigung, ich habe keine Antwort erhalten."
except Exception:
log.warning("[%s] Guide-Chat fehlgeschlagen", topic, exc_info=True)
return "Entschuldigung, das hat nicht geklappt. Bitte versuche es erneut."
@@ -1432,6 +1440,7 @@ 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)
return fallback
@@ -1488,6 +1497,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)
return None
@@ -1545,6 +1555,7 @@ async def chat_with_element(element: dict, messages: list[dict], provider: str =
reply = str(data.get("reply", "")).strip() or ("Vorschläge erstellt." if changes else fehler)
return reply, changes
except Exception:
log.warning("[%s] Element-Chat fehlgeschlagen", element.get("topic", "?"), exc_info=True)
return fehler, []
@@ -1562,6 +1573,7 @@ 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)
return None
@@ -1584,4 +1596,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)
return None

11
backend/logsetup.py Normal file
View File

@@ -0,0 +1,11 @@
"""Zentrales Logging-Setup — einmal in main.py aufrufen, bevor die App entsteht."""
import logging
import os
def setup_logging() -> None:
logging.basicConfig(
level=os.environ.get("LOG_LEVEL", "INFO").upper(),
format="%(asctime)s %(levelname)s %(name)s %(message)s",
)

View File

@@ -3,6 +3,10 @@ from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from logsetup import setup_logging
setup_logging()
from config import FRONTEND_DIST, STORAGE_DIR
from database import init_db, close_db
from routes import router