Files
box/bin/chat-rate.py

130 lines
4.7 KiB
Python
Executable File

#!/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 <name> --cdp-port <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<name>\n\n<timestamp>\n\n<name>\n\n<timestamp>..."
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()