PRODUCTAGENTharrisonhjohnson/productagent · /harnesses/todo/bot/behavior_state.py
000%

behavior_state.py

view on github ↗106 lines · python
"""
Mac-native behavioral state inference for NAVI.

Reads ambient macOS signals to predict one of 5 behavioral states,
used by the scheduler to decide whether to send a message now or defer it.
"""
import subprocess
import re
import logging
from datetime import datetime
from typing import Optional

logger = logging.getLogger(__name__)

# Import known SSIDs from config — leave None to skip SSID-based detection
try:
    import config as _config
    HOME_SSID = getattr(_config, 'BEHAVIOR_HOME_SSID', None)
    OFFICE_SSID = getattr(_config, 'BEHAVIOR_OFFICE_SSID', None)
except ImportError:
    HOME_SSID = None
    OFFICE_SSID = None


def _run(cmd: list, timeout: int = 3) -> str:
    try:
        r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
        return r.stdout
    except Exception:
        return ""


def get_wifi_ssid() -> Optional[str]:
    out = _run(["networksetup", "-getairportnetwork", "en0"])
    m = re.search(r"Current Wi-Fi Network: (.+)", out)
    return m.group(1).strip() if m else None


def get_battery() -> dict:
    out = _run(["pmset", "-g", "batt"])
    pct_m = re.search(r"(\d+)%", out)
    return {
        "percent": int(pct_m.group(1)) if pct_m else 50,
        "charging": "AC Power" in out or "charging" in out.lower(),
    }


def get_screen_idle_seconds() -> int:
    """Returns seconds since last user input, or -1 on failure."""
    out = _run(["ioreg", "-c", "IOHIDSystem"])
    m = re.search(r'"HIDIdleTime"\s*=\s*(\d+)', out)
    if m:
        return int(m.group(1)) // 1_000_000_000
    return -1


def get_state() -> str:
    """
    Infer behavioral state from macOS ambient signals.
    Returns one of: HEADS_DOWN, ON_THE_MOVE, OFF_THE_CLOCK, AWAY, AVAILABLE
    """
    now = datetime.now()
    hour = now.hour
    is_weekend = now.weekday() >= 5

    ssid = get_wifi_ssid()
    battery = get_battery()
    idle_s = get_screen_idle_seconds()

    at_home = bool(HOME_SSID and ssid == HOME_SSID)
    at_office = bool(OFFICE_SSID and ssid == OFFICE_SSID)
    ssids_configured = bool(HOME_SSID or OFFICE_SSID)
    at_known = (at_home or at_office) if ssids_configured else True

    is_sleep_hours = hour < 7 or hour >= 23
    is_work_hours = not is_weekend and 8 <= hour < 19
    is_evening = 19 <= hour < 23
    screen_idle = idle_s > 300 if idle_s >= 0 else False       # 5 min
    screen_deep_idle = idle_s > 1800 if idle_s >= 0 else False  # 30 min

    if is_sleep_hours or screen_deep_idle:
        return "AWAY"

    if not at_known:
        return "ON_THE_MOVE"

    if is_evening and (at_home or not ssids_configured):
        return "OFF_THE_CLOCK"

    if is_work_hours and at_known and not screen_idle and battery["charging"]:
        return "HEADS_DOWN"

    if is_work_hours and at_known:
        return "AVAILABLE"

    return "AVAILABLE"


def is_good_time_to_interrupt() -> bool:
    """True when it's appropriate to fire a scheduled push message."""
    try:
        state = get_state()
        return state in ("AVAILABLE", "HEADS_DOWN")
    except Exception as e:
        logger.warning(f"behavior_state check failed, defaulting to True: {e}")
        return True
flow-designglobal
SKILL.md74 lines
SKILL.md162 lines
pm-strategistglobal
pm-strategist.md51 lines
todosystem
bot31 items
agent_executor.py344 lines
behavior_state.py106 lines
BOT.md175 lines
config.py76 lines
git_sync.py87 lines
hyrule_server.py284 lines
manage_bot.sh47 lines
mcp_server.py324 lines
mobile_mode.py229 lines
navi_tab.py95 lines
prework_engine.py426 lines
run_bot.sh18 lines
run_hyrule.sh38 lines
scheduler.py958 lines
session.py264 lines
telegram_bot.py3328 lines
todo_manager.py1130 lines
ventures_signal.py314 lines
voice_handler.py203 lines
VOICE_SETUP.md112 lines
warroom_manager.py1004 lines
README.md44 lines
night-orderssystem
README.md84 lines
loops-spec.md212 lines
settings.json116 lines
README.md59 lines