From 32f6fab16b02c637e6f3959e24278931ed422486 Mon Sep 17 00:00:00 2001 From: team3 Date: Fri, 12 Jun 2026 07:52:33 +0200 Subject: [PATCH] Backend: Python-Logging statt print, Diagnose in allen Fallbacks Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/agents.py | 21 +++++++++++++++++++-- backend/generator.py | 17 +++++++++++++++-- backend/logsetup.py | 11 +++++++++++ backend/main.py | 4 ++++ 4 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 backend/logsetup.py diff --git a/backend/agents.py b/backend/agents.py index d81ff7c..70ea28b 100644 --- a/backend/agents.py +++ b/backend/agents.py @@ -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]: diff --git a/backend/generator.py b/backend/generator.py index cd0a06d..8892964 100644 --- a/backend/generator.py +++ b/backend/generator.py @@ -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 diff --git a/backend/logsetup.py b/backend/logsetup.py new file mode 100644 index 0000000..3288303 --- /dev/null +++ b/backend/logsetup.py @@ -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", + ) diff --git a/backend/main.py b/backend/main.py index bd63594..cae9709 100644 --- a/backend/main.py +++ b/backend/main.py @@ -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