Spaces:
Running
Running
Download brain/services/proactive.py from chintakapp/prova-core: direct link, hf CLI and curl.
- Browser
- Download file 6.29 kB
-
https://huggingface.co/spaces/chintakapp/prova-core/resolve/main/brain/services/proactive.py
- Command line
-
hf download hf://spaces/chintakapp/prova-core/brain/services/proactive.py
-
curl -L -o proactive.py https://huggingface.co/spaces/chintakapp/prova-core/resolve/main/brain/services/proactive.py
6.29 kB
| """Deciding whether Prova messages first. Port of ``work/ProactiveWorker.kt``; blueprint 10.3. | |
| The decision is the interesting part, not the sending. A companion that pings at 3 am, or three | |
| times mid-conversation, or keeps talking into a silence it never got an answer to, stops feeling | |
| like a person very quickly. Every gate below exists to prevent one of those. | |
| Each run returns exactly one ``Decision`` naming the gate that stopped it and the numbers the | |
| decision was made on β Chintak's lesson: check-ins were arriving at 5 am and nothing said why. | |
| Gates, in order (Chintak's actual order, which the blueprint's list compresses): | |
| 1. ``notifications_off`` β proactive is switched off for every mode | |
| 2. ``quiet_hours`` β weekday 00:00β07:00, weekend 01:00β07:00 by default | |
| 3. ``daily_cap`` β 3 a day by default, 1β5 | |
| 4. ``no_history`` β never open with "you've been quiet" to someone who never spoke | |
| 5. ``too_recent`` β silence shorter than the bar. The bar is 45 min inside a routine | |
| window (lunch, leaving the office), 60 min at the weekend, and the | |
| configured gap (5 h) otherwise | |
| 6. ``unanswered_backoff`` β the newest message is a check-in he never answered | |
| 7. ``failure_backoff`` β a generation failed less than 6 h ago | |
| """ | |
| import logging | |
| from dataclasses import dataclass | |
| from datetime import datetime, timedelta | |
| from typing import Any | |
| from ..persona.routine import WeeklyRoutine, active_window, clock, is_weekend, minute_of_day | |
| from ..persona.types import Mode | |
| log = logging.getLogger(__name__) | |
| DEFAULTS = { | |
| "gfEnabled": True, | |
| "normalEnabled": False, | |
| "minGapMin": 5 * 60, | |
| "maxPerDay": 3, | |
| "quietStart": 0, | |
| "quietEnd": 7 * 60, | |
| "weekendQuietStart": 60, | |
| "weekendQuietEnd": 7 * 60, | |
| } | |
| WEEKEND_MIN_GAP_MINUTES = 60 | |
| ROUTINE_MIN_SILENCE_MINUTES = 45 | |
| BACKOFF = timedelta(hours=6) | |
| MIN_GAP_FLOOR_MINUTES = 5 | |
| MAX_PER_DAY_RANGE = (1, 5) | |
| class Decision: | |
| send: bool | |
| gate: str | |
| detail: str | |
| mode: Mode = Mode.GF | |
| #: "2026-09-19:lunch" when a routine window opened the gate; recorded only on success. | |
| stamp: str | None = None | |
| def proactive_settings(settings: dict[str, Any]) -> dict[str, Any]: | |
| merged = {**DEFAULTS, **(settings.get("proactive") or {})} | |
| low, high = MAX_PER_DAY_RANGE | |
| merged["maxPerDay"] = max(low, min(high, int(merged["maxPerDay"]))) | |
| merged["minGapMin"] = max(MIN_GAP_FLOOR_MINUTES, int(merged["minGapMin"])) | |
| return merged | |
| def in_quiet_hours(minute: int, start: int, end: int) -> bool: | |
| """Handles windows that wrap midnight (23:00β07:00) as well as ones that do not.""" | |
| if start == end: | |
| return False | |
| if start < end: | |
| return start <= minute < end | |
| return minute >= start or minute < end | |
| def silence_minutes(last_at: datetime, now: datetime) -> int: | |
| return max(0, int((now - last_at).total_seconds() // 60)) | |
| async def decide( | |
| store: Any, user_id: str, settings: dict[str, Any], now: datetime, *, test_mode: bool = False | |
| ) -> Decision: | |
| """Every gate, in order. ``test_mode`` is PROACTIVE_TEST_MODE: it ignores them all.""" | |
| config = proactive_settings(settings) | |
| mode = Mode.GF if config["gfEnabled"] else Mode.NORMAL | |
| if test_mode: | |
| return Decision(True, "test_mode", "PROACTIVE_TEST_MODE ignores every gate", mode) | |
| if not config["gfEnabled"] and not config["normalEnabled"]: | |
| return Decision(False, "notifications_off", "proactive is off in both modes", mode) | |
| weekend = is_weekend(now) | |
| minute = minute_of_day(now) | |
| start = config["weekendQuietStart"] if weekend else config["quietStart"] | |
| end = config["weekendQuietEnd"] if weekend else config["quietEnd"] | |
| if in_quiet_hours(minute, start, end): | |
| label = "weekend" if weekend else "weekday" | |
| return Decision( | |
| False, | |
| "quiet_hours", | |
| f"{clock(minute)} is inside the {label} window {clock(start)}-{clock(end)}", | |
| mode, | |
| ) | |
| midnight = now.replace(hour=0, minute=0, second=0, microsecond=0) | |
| sent_today = await store.proactive_count(user_id, midnight) | |
| if sent_today >= config["maxPerDay"]: | |
| return Decision( | |
| False, "daily_cap", f"{sent_today} of {config['maxPerDay']} already sent today", mode | |
| ) | |
| last = await store.last_message(user_id) | |
| if last is None: | |
| return Decision(False, "no_history", "no conversation to continue", mode) | |
| last_at = last["created_at"] | |
| if last_at.tzinfo is None: | |
| last_at = last_at.replace(tzinfo=now.tzinfo) | |
| silent = silence_minutes(last_at, now) | |
| routine = WeeklyRoutine.from_settings((settings.get("routine") or {}).get("week")) | |
| off_today = settings.get("offTodayDate") == now.date().isoformat() | |
| window = active_window(routine, now, off_today) | |
| stamp = f"{now.date().isoformat()}:{window.id}" if window else None | |
| if stamp and await store.has_proactive_stamp(user_id, stamp): | |
| window, stamp = None, None | |
| if window is not None: | |
| required, reason = ROUTINE_MIN_SILENCE_MINUTES, f"routine window '{window.id}'" | |
| elif weekend: | |
| required, reason = WEEKEND_MIN_GAP_MINUTES, "weekend gap" | |
| else: | |
| required, reason = config["minGapMin"], "configured weekday gap" | |
| if silent < required: | |
| return Decision( | |
| False, "too_recent", f"silent for {silent} min, {reason} needs {required} min", mode | |
| ) | |
| # A check-in he never answered: do not stack another on top of it. | |
| if last["role"] == "assistant" and last.get("is_proactive"): | |
| if now - last_at < BACKOFF: | |
| return Decision( | |
| False, | |
| "unanswered_backoff", | |
| f"last check-in {silent} min ago is still unanswered", | |
| mode, | |
| ) | |
| failed_at = await store.last_proactive_failure(user_id) | |
| if failed_at is not None: | |
| if failed_at.tzinfo is None: | |
| failed_at = failed_at.replace(tzinfo=now.tzinfo) | |
| if now - failed_at < BACKOFF: | |
| return Decision(False, "failure_backoff", "a check-in failed less than 6 h ago", mode) | |
| return Decision(True, "sent", f"{reason}, silent for {silent} min", mode, stamp) | |