130 lines
4.7 KiB
Python
Executable File
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()
|