Ctrl K
ComunidadGuías…

scripts/_session_dedup.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""
XKB Session Deduplication
=========================
Tracks which (source_file, section) pairs have already been surfaced
in the current session, so the same content is not pushed repeatedly.

Session = a temp JSON file at /tmp/xkb-session-shown.json.
TTL = 4 hours. After that, the session resets automatically.

Usage:
    from _session_dedup import filter_new, mark_shown, clear_session
"""
from __future__ import annotations

import json
import os
from datetime import datetime, timezone, timedelta
from pathlib import Path

SESSION_FILE = Path(os.getenv("XKB_SESSION_FILE", "/tmp/xkb-session-shown.json"))
SESSION_TTL_HOURS = int(os.getenv("XKB_SESSION_TTL_HOURS", "4"))


def _now() -> datetime:
    return datetime.now(timezone.utc)


def _load() -> dict:
    """Load session state. Returns empty session if missing or expired."""
    if not SESSION_FILE.exists():
        return {"started_at": _now().isoformat(), "shown": []}
    try:
        data = json.loads(SESSION_FILE.read_text(encoding="utf-8"))
        started = datetime.fromisoformat(data["started_at"])
        if _now() - started > timedelta(hours=SESSION_TTL_HOURS):
            return {"started_at": _now().isoformat(), "shown": []}
        return data
    except Exception:
        return {"started_at": _now().isoformat(), "shown": []}


def _save(data: dict) -> None:
    try:
        SESSION_FILE.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
    except Exception:
        pass  # session dedup never breaks the main flow


def _key(source_file: str, section: str) -> str:
    return f"{source_file.split('/')[-1]}::{section}"


def filter_new(results: list) -> tuple[list, list]:
    """
    Split results into (new_results, already_seen_results).
    Works with any objects that have .source_file and .section attributes,
    or dicts with 'source_file' and 'section' keys.
    """
    data = _load()
    shown_keys = {entry["key"] for entry in data.get("shown", [])}

    new_results = []
    already_seen = []
    for r in results:
        if isinstance(r, dict):
            sf = r.get("source_file", "")
            sec = r.get("section", "")
        else:
            sf = getattr(r, "source_file", "")
            sec = getattr(r, "section", "")
        k = _key(sf, sec)
        if k in shown_keys:
            already_seen.append(r)
        else:
            new_results.append(r)

    return new_results, already_seen


def mark_shown(results: list) -> None:
    """Record results as shown in the current session."""
    if not results:
        return
    data = _load()
    shown = data.get("shown", [])
    existing_keys = {entry["key"] for entry in shown}
    ts = _now().isoformat()

    for r in results:
        if isinstance(r, dict):
            sf = r.get("source_file", "")
            sec = r.get("section", "")
        else:
            sf = getattr(r, "source_file", "")
            sec = getattr(r, "section", "")
        k = _key(sf, sec)
        if k not in existing_keys:
            shown.append({"key": k, "ts": ts})
            existing_keys.add(k)

    data["shown"] = shown
    _save(data)


def clear_session() -> None:
    """Force reset the session (e.g. when user starts a new conversation)."""
    _save({"started_at": _now().isoformat(), "shown": []})


def session_stats() -> dict:
    """Return current session info for debugging."""
    data = _load()
    started = datetime.fromisoformat(data["started_at"])
    age_minutes = int((_now() - started).total_seconds() / 60)
    return {
        "started_at": data["started_at"],
        "age_minutes": age_minutes,
        "shown_count": len(data.get("shown", [])),
        "ttl_hours": SESSION_TTL_HOURS,
    }