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 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]:
|
||||||
|
|||||||
@@ -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
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 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
|
||||||
|
|||||||
Reference in New Issue
Block a user