Spaces:
Sleeping
Sleeping
| """ | |
| utils/logger.py | |
| --------------- | |
| Centralised logging utility for the application. | |
| Usage: | |
| from utils.logger import get_logger, get_session_logger, new_session_id | |
| from utils.logger import log_chat, log_unknown, log_email, log_message, push_logs | |
| log = get_logger(__name__) | |
| log.info("something happened") | |
| # Inside chat() — tags every line with [session_id[:8]] for multi-user correlation | |
| session_log = get_session_logger(log, session_id) | |
| session_log.info("User query: %s", message) | |
| # → 2026-03-20 01:40:09.412 INFO agents.profile.profile [e051f394] User query: ... | |
| log_chat("user question", "agent answer") | |
| log_unknown("unanswered question") | |
| log_email("user@example.com") | |
| log_message("some debug message") | |
| push_logs() # upload CSV to HuggingFace | |
| """ | |
| import csv | |
| import contextvars | |
| import logging | |
| import os | |
| import uuid | |
| from datetime import datetime | |
| from pathlib import Path | |
| from typing import MutableMapping, Any | |
| from dotenv import load_dotenv | |
| try: | |
| from huggingface_hub import HfApi | |
| except ImportError: | |
| HfApi = None | |
| load_dotenv(override=True) | |
| # --------------------------------------------------------------------------- | |
| # Constants — read from .env with sensible defaults | |
| # --------------------------------------------------------------------------- | |
| LOG_FILE = os.getenv("LOG_FILE", "logs/chat_logs.csv") | |
| LOG_LEVEL = os.getenv("LOG_LEVEL", logging.INFO) | |
| HF_DATASET = os.getenv("HF_LOG_DATASET", "") | |
| SESSION_ID = "system" | |
| CSV_HEADERS = ["timestamp", "session_id", "event_type", "question", "answer", "email", "message"] | |
| # context var for current session id (Gradio session_hash) | |
| current_session_id = contextvars.ContextVar("current_session_id", default=SESSION_ID) | |
| # --------------------------------------------------------------------------- | |
| # Python logger setup | |
| # --------------------------------------------------------------------------- | |
| class _SessionIdFilter(logging.Filter): | |
| """Attach session_id from contextvar to each log record.""" | |
| def filter(self, record: logging.LogRecord) -> bool: | |
| record.session_id = get_current_session_id() | |
| return True | |
| def get_logger(name: str) -> logging.Logger: | |
| """ | |
| Returns a module-level logger with consistent formatting. | |
| Format: 2026-03-20 01:40:09.412 INFO [e051f394] utils.rag:_build_context:123 Message here | |
| ^--- date/time + ms ---^ ^session^ ^level^ ^module:function:line^ | |
| Milliseconds are added via %(msecs)03d combined with the datefmt. | |
| LOG_LEVEL is read from .env (default INFO). | |
| Call once at the top of each file: | |
| logger = get_logger(__name__) | |
| """ | |
| logger = logging.getLogger(name) | |
| if not logger.handlers: | |
| handler = logging.StreamHandler() | |
| handler.addFilter(_SessionIdFilter()) | |
| handler.setFormatter(logging.Formatter( | |
| "%(asctime)s.%(msecs)03d %(levelname)-8s [%(session_id)s] %(name)s:%(funcName)s:%(lineno)d %(message)s", | |
| datefmt="%Y-%m-%d %H:%M:%S", | |
| )) | |
| logger.addHandler(handler) | |
| logger.setLevel(LOG_LEVEL) | |
| return logger | |
| _log = get_logger(__name__) | |
| # --------------------------------------------------------------------------- | |
| # Session-scoped logger adapter | |
| # --------------------------------------------------------------------------- | |
| class _SessionAdapter(logging.LoggerAdapter): | |
| """ | |
| Wraps a logger and prepends [session_id] to every message so you can | |
| correlate all log lines for one user when multiple visitors are active. | |
| With two concurrent users the log will look like: | |
| 01:40:09.412 INFO profile [e051f39412345678] User query: how to contact archana | |
| 01:40:09.615 INFO profile [a3b7c29187654321] User query: what are her skills | |
| 01:40:10.001 INFO profile [e051f39412345678] RAG → 1 chunk(s) | topics: ['contact'] | |
| 01:40:10.204 INFO profile [a3b7c29187654321] RAG → 2 chunk(s) | topics: ['skills'] | |
| grep "[e051f39412345678]" to follow a single user's entire session. | |
| """ | |
| def process(self, msg: str, kwargs: MutableMapping[str, Any]): | |
| sid = self.extra.get("session_id", "--------") | |
| return f"[{sid}] {msg}", kwargs | |
| def set_current_session_id(session_id: str) -> None: | |
| """Set session id for current context (used by log_event, wrappers, LLM calls).""" | |
| current_session_id.set(session_id) | |
| def get_current_session_id() -> str: | |
| """Get effective session id for current context. | |
| Returns: | |
| - real Gradio session_hash if set | |
| - "system" if not set (for non-user actions / startup) | |
| """ | |
| return current_session_id.get() or "system" | |
| def get_session_logger(base_logger: logging.Logger, session_id: str) -> _SessionAdapter: | |
| """ | |
| Returns a LoggerAdapter that tags every line with [session_id]. | |
| Call once per chat() invocation and use the returned logger for all | |
| turn-scoped log lines: | |
| sid = request.session_hash or new_session_id() | |
| slog = get_session_logger(logger, sid) | |
| slog.info("User query: %s", message) | |
| """ | |
| set_current_session_id(session_id) | |
| return _SessionAdapter(base_logger, {"session_id": session_id}) | |
| def new_session_id() -> str: | |
| """Generate a fresh UUID — fallback when Gradio session hash is unavailable.""" | |
| return str(uuid.uuid4()) | |
| # --------------------------------------------------------------------------- | |
| # CSV chat log | |
| # --------------------------------------------------------------------------- | |
| def _ensure_log_file() -> None: | |
| """Create the CSV log file with headers if it doesn't exist.""" | |
| path = Path(LOG_FILE) | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| if not path.exists(): | |
| with open(path, "w", newline="", encoding="utf-8") as f: | |
| csv.writer(f).writerow(CSV_HEADERS) | |
| def log_event( | |
| event_type: str, | |
| question: str = "", | |
| answer: str = "", | |
| email: str = "", | |
| message: str = "", | |
| session_id: str = "", | |
| ) -> None: | |
| """Write a single event row to the CSV log. | |
| Args: | |
| event_type: Type of event (chat, unknown_question, email_capture, debug) | |
| question: User question | |
| answer: Assistant answer | |
| email: User email if captured | |
| message: Debug message | |
| session_id: User's session ID from Gradio (request.session_hash). | |
| Fallback to global SESSION_ID if not provided. | |
| """ | |
| try: | |
| _ensure_log_file() | |
| uid = session_id or get_current_session_id() | |
| if not uid: | |
| uid = "system" | |
| row = [ | |
| datetime.utcnow().isoformat(), | |
| uid, | |
| event_type, | |
| question, | |
| answer, | |
| email, | |
| message, | |
| ] | |
| with open(LOG_FILE, "a", newline="", encoding="utf-8") as f: | |
| csv.writer(f).writerow(row) | |
| _log.info("Event [%s]: %s", uid, event_type) | |
| except Exception as e: | |
| _log.error("Failed to write log event: %s", e, exc_info=True) | |
| # --------------------------------------------------------------------------- | |
| # Convenience wrappers — now accept session_id for per-user tracking | |
| # --------------------------------------------------------------------------- | |
| def log_chat(question: str, answer: str, session_id: str = "") -> None: | |
| """Log a chat exchange to CSV. | |
| Args: | |
| question: User question | |
| answer: Assistant answer | |
| session_id: User's Gradio session hash (from request.session_hash) | |
| """ | |
| log_event("chat", question=question, answer=answer, session_id=session_id) | |
| def log_unknown(question: str, session_id: str = "") -> None: | |
| """Log an unanswered question to CSV. | |
| Args: | |
| question: User question that couldn't be answered | |
| session_id: User's Gradio session hash | |
| """ | |
| log_event("unknown_question", question=question, session_id=session_id) | |
| def log_email(email: str, session_id: str = "") -> None: | |
| """Log a captured email to CSV. | |
| Args: | |
| email: User's email address | |
| session_id: User's Gradio session hash | |
| """ | |
| log_event("email_capture", email=email, session_id=session_id) | |
| def log_message(message: str, session_id: str = "") -> None: | |
| """Log a debug message to CSV. | |
| Args: | |
| message: Debug message | |
| session_id: User's Gradio session hash | |
| """ | |
| log_event("debug", message=message, session_id=session_id) | |
| # --------------------------------------------------------------------------- | |
| # HuggingFace upload | |
| # --------------------------------------------------------------------------- | |
| def push_logs() -> bool: | |
| """ | |
| Upload the CSV log to a HuggingFace dataset repo. | |
| Set HF_LOG_DATASET in .env to enable. | |
| Returns True on success, False on failure. | |
| """ | |
| if not HF_DATASET: | |
| _log.warning("HF_LOG_DATASET not set — skipping push.") | |
| return False | |
| if not os.path.exists(LOG_FILE): | |
| _log.warning("Log file '%s' not found — skipping push.", LOG_FILE) | |
| return False | |
| if HfApi is None: | |
| _log.error("HfApi is not available; install huggingface_hub to push logs") | |
| return False | |
| try: | |
| HfApi().upload_file( | |
| path_or_fileobj=LOG_FILE, | |
| path_in_repo="chat_logs.csv", | |
| repo_id=HF_DATASET, | |
| repo_type="dataset", | |
| ) | |
| _log.info("Logs pushed to HuggingFace dataset: %s", HF_DATASET) | |
| return True | |
| except Exception as e: | |
| _log.error("Failed to push logs to HuggingFace: %s", e) | |
| return False |