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 asyncio
import logging
import os import os
import re import re
import shutil import shutil
import tempfile import tempfile
import time
import urllib.request import urllib.request
from pathlib import Path from pathlib import Path
from config import PROVIDERS, DEFAULT_PROVIDER from config import PROVIDERS, DEFAULT_PROVIDER
log = logging.getLogger("creator.agents")
_active_processes: dict[str, asyncio.subprocess.Process] = {} _active_processes: dict[str, asyncio.subprocess.Process] = {}
# Capability → Claude --allowedTools # Capability → Claude --allowedTools
@@ -54,7 +58,11 @@ def provider_available(provider: str) -> bool:
def kill_process(agent_key_prefix: str) -> None: def kill_process(agent_key_prefix: str) -> None:
"""Killt alle aktiven Prozesse, deren Key mit dem Prefix beginnt (deckt -plan/-w1… ab).""" """Killt alle aktiven Prozesse, deren Key mit dem Prefix beginnt (deckt -plan/-w1… ab)."""
for key, process in list(_active_processes.items()): 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() 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]: 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( process = await asyncio.create_subprocess_exec(
*cmd, *cmd,
stdin=asyncio.subprocess.PIPE if stdin_data is not None else asyncio.subprocess.DEVNULL, 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) await asyncio.wait_for(process.wait(), timeout=5)
except asyncio.TimeoutError: except asyncio.TimeoutError:
pass pass
log.info("agent %s: Timeout nach %ds", agent_key, timeout)
raise 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") return process.returncode, stdout.decode("utf-8", errors="replace"), stderr.decode("utf-8", errors="replace")
finally: 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]: 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 asyncio
import json import json
import logging
import math import math
import shutil import shutil
import subprocess 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 "" 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: 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: 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 = path.read_text(encoding="utf-8").strip()
text = re.sub(r"^```(?:json)?\s*|\s*```$", "", text) text = re.sub(r"^```(?:json)?\s*|\s*```$", "", text)
return json.loads(text) return json.loads(text)
except Exception: except Exception as e:
log.debug("JSON-Datei ungültig: %s (%s)", path, e)
return None return None
@@ -580,6 +585,7 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
encoding="utf-8", encoding="utf-8",
) )
except Exception as e: except Exception as e:
log.exception("[%s] Bausteine-Generierung fehlgeschlagen", topic)
_bausteine_errors[topic] = str(e)[:2000] _bausteine_errors[topic] = str(e)[:2000]
finally: finally:
# Kein Datei-Cleanup: Zwischendateien bleiben für Resume bzw. Nachvollziehbarkeit. # 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: except FileNotFoundError:
await _fail(guide_id, "Bausteine fehlen") await _fail(guide_id, "Bausteine fehlen")
except Exception as e: except Exception as e:
log.exception("[%s] Guide-Generierung fehlgeschlagen (%s)", topic, guide_id)
await _fail(guide_id, str(e)[:2000]) await _fail(guide_id, str(e)[:2000])
finally: finally:
_cancelled.discard(guide_id) _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() reply = stdout.strip()
return reply or "Entschuldigung, ich habe keine Antwort erhalten." return reply or "Entschuldigung, ich habe keine Antwort erhalten."
except Exception: except Exception:
log.warning("[%s] Guide-Chat fehlgeschlagen", topic, exc_info=True)
return "Entschuldigung, das hat nicht geklappt. Bitte versuche es erneut." 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 fallback
return _element_fields(_parse_json_text(stdout)) or fallback return _element_fields(_parse_json_text(stdout)) or fallback
except Exception: except Exception:
log.warning("[%s] Element-Erstellung fehlgeschlagen", topic, exc_info=True)
return fallback return fallback
@@ -1488,6 +1497,7 @@ async def check_element(element: dict, provider: str = DEFAULT_PROVIDER) -> list
return None return None
return _parse_suggestions(stdout) return _parse_suggestions(stdout)
except Exception: except Exception:
log.warning("[%s] Element-Prüfung fehlgeschlagen", element.get("topic", "?"), exc_info=True)
return None 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) reply = str(data.get("reply", "")).strip() or ("Vorschläge erstellt." if changes else fehler)
return reply, changes return reply, changes
except Exception: except Exception:
log.warning("[%s] Element-Chat fehlgeschlagen", element.get("topic", "?"), exc_info=True)
return fehler, [] return fehler, []
@@ -1562,6 +1573,7 @@ async def style_element(element: dict, provider: str = DEFAULT_PROVIDER) -> list
return None return None
return [v for c in data.get("changes", []) if (v := _validate_change(c, element))] return [v for c in data.get("changes", []) if (v := _validate_change(c, element))]
except Exception: except Exception:
log.warning("[%s] Stil-Prüfung fehlgeschlagen", element.get("topic", "?"), exc_info=True)
return None return None
@@ -1584,4 +1596,5 @@ async def refine_suggestion(element: dict, suggestion: dict, instruction: str, p
return None return None
return _validate_change(data.get("change"), element) return _validate_change(data.get("change"), element)
except Exception: except Exception:
log.warning("[%s] Vorschlags-Überarbeitung fehlgeschlagen", element.get("topic", "?"), exc_info=True)
return None 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 import FastAPI
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from logsetup import setup_logging
setup_logging()
from config import FRONTEND_DIST, STORAGE_DIR from config import FRONTEND_DIST, STORAGE_DIR
from database import init_db, close_db from database import init_db, close_db
from routes import router from routes import router