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:
@@ -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]:
|
||||
|
||||
@@ -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
11
backend/logsetup.py
Normal 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",
|
||||
)
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user