#!/usr/bin/env python3 """Chat rate metric: messages per minute for main chat and each side chat. Polls muse-chat-api.py for message counts, calculates delta vs previous poll, logs rates to a time-series file. Usage: chat-rate.py --account --cdp-port [--interval 60] """ import json, subprocess, sys, time, os, argparse from datetime import datetime, timezone API = os.path.expanduser("~/Projects/NetVM/bin/muse-chat-api.py") STATE_FILE = os.path.expanduser("~/Projects/NetVM/logs/chat-rate-state.json") LOG_FILE = os.path.expanduser("~/Projects/NetVM/logs/chat-rate.log") def run_api(account, *args): """Run muse-chat-api.py and return stdout.""" cmd = ["python3", API, "--account", account] + list(args) # Note: cdp-port is baked into the account config, not passed here result = subprocess.run(cmd, capture_output=True, text=True, timeout=60) return result.stdout def count_messages(text): """Count messages in API output. Messages are separated by '---'.""" # The API outputs messages separated by ---\n parts = [p.strip() for p in text.split("---") if p.strip()] # Filter out non-message lines (headers, etc.) # Messages typically have substantial content return len([p for p in parts if len(p) > 10]) def get_side_chats(account): """List side chat names.""" out = run_api(account, "sidechat", "list") # Parse: names and timestamps separated by blank lines # Format: "Side chats\n\n\n\n\n\n\n\n..." lines = [l.strip() for l in out.split("\n") if l.strip()] chats = [] # Skip header "Side chats", then pair up (name, timestamp) lines = [l for l in lines if l != "Side chats"] # Lines alternate: name, timestamp, name, timestamp... for i in range(0, len(lines), 2): if i < len(lines): name = lines[i] # Verify next is a timestamp (ends with m/h/d) if i + 1 < len(lines) and lines[i+1][-1] in "mhd": chats.append(name) return chats def get_chat_count(account, chat_name=None): """Get message count for main or a side chat.""" if chat_name: # Switch to side chat, get messages, switch back run_api(account, "sidechat", "use", chat_name) out = run_api(account, "messages") run_api(account, "sidechat", "main") # switch back else: out = run_api(account, "messages") return count_messages(out) def main(): p = argparse.ArgumentParser() p.add_argument("--account", required=True) p.add_argument("--interval", type=int, default=60, help="poll interval seconds") p.add_argument("--once", action="store_true", help="single poll, no loop") args = p.parse_args() # Load previous state prev = {} if os.path.exists(STATE_FILE): with open(STATE_FILE) as f: prev = json.load(f) def poll(): now = datetime.now(timezone.utc).isoformat() counts = {} # Main chat try: counts["main"] = get_chat_count(args.account) except Exception as e: print(f"main: error {e}", file=sys.stderr) # Side chats try: sc_list = get_side_chats(args.account) print(f"DEBUG: found {len(sc_list)} side chats", file=sys.stderr) for sc in sc_list: try: counts[f"side:{sc}"] = get_chat_count(args.account, sc) except Exception as e: print(f"side:{sc}: error {e}", file=sys.stderr) except Exception as e: print(f"sidechat list: error {e}", file=sys.stderr) # Calculate rates results = [] for chat, count in counts.items(): rate = 0.0 if chat in prev: prev_count, prev_time = prev[chat] dt = (datetime.fromisoformat(now) - datetime.fromisoformat(prev_time)).total_seconds() / 60.0 if dt > 0: rate = (count - prev_count) / dt results.append((now, chat, count, round(rate, 2))) prev[chat] = (count, now) # Log os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True) with open(LOG_FILE, "a") as f: for ts, chat, count, rate in results: f.write(f"{ts} {chat} count={count} rate={rate}/min\n") # Save state with open(STATE_FILE, "w") as f: json.dump(prev, f) # Print for ts, chat, count, rate in results: print(f"{chat}: {count} msgs, {rate}/min") if args.once: poll() else: while True: poll() time.sleep(args.interval) if __name__ == "__main__": main()