"""Centralised logging for MealGraph. Agents and tools used to ``print`` directly to stdout. That worked in a notebook but coupled the agentic system to the I/O layer. This module provides a single ``get_logger`` entrypoint so: * user-mode emoji status lines flow through ``logger.info`` (visible by default), * debug-mode raw LLM dumps flow through ``logger.debug`` (hidden unless ``settings.debug_mode`` is True), * later phases can attach extra handlers (SSE event stream for the API, JSON file handler for trace persistence, etc.) without touching agent code. Idempotent: calling :func:`configure_logging` more than once is a no-op unless ``force=True``. """ from __future__ import annotations import logging import sys from config import get_settings _BASE = "mealgraph" _CONFIGURED = False def configure_logging(*, force: bool = False) -> None: """Wire up the ``mealgraph`` logger tree. Reads ``settings.debug_mode`` to choose between INFO (user mode) and DEBUG. Safe to call from library code; only the first call attaches a handler. Reconfigures ``sys.stdout`` to UTF-8 when possible (Windows defaults to cp1252 which chokes on the emoji in agent status messages). Falls back to a 'replace' error handler so a stray glyph never crashes a log call. """ global _CONFIGURED if _CONFIGURED and not force: return settings = get_settings() level = logging.DEBUG if settings.debug_mode else logging.INFO # Best-effort: re-encode stdout to UTF-8 so the emoji status lines render. reconf = getattr(sys.stdout, "reconfigure", None) if callable(reconf): try: reconf(encoding="utf-8", errors="replace") except Exception: # pragma: no cover - depends on stream type pass handler = logging.StreamHandler(sys.stdout) handler.setLevel(level) handler.setFormatter(logging.Formatter("%(message)s")) root = logging.getLogger(_BASE) root.handlers = [handler] root.setLevel(level) root.propagate = False _CONFIGURED = True def get_logger(name: str) -> logging.Logger: """Return a sub-logger under the ``mealgraph`` namespace. Conventional names: ``agents.coach``, ``agents.medical``, ``tools.computation``, ``utils.api_pool``. """ if not _CONFIGURED: configure_logging() return logging.getLogger(f"{_BASE}.{name}") def refresh_level() -> None: """Re-read ``settings.debug_mode`` and adjust handler levels in place. Call this after toggling debug mode at runtime. """ settings = get_settings() level = logging.DEBUG if settings.debug_mode else logging.INFO root = logging.getLogger(_BASE) root.setLevel(level) for handler in root.handlers: handler.setLevel(level) __all__ = ["configure_logging", "get_logger", "refresh_level"]