feat(hybrid-gateway): integrate muse-cli with Cloudflare netns isolation, symmetric sidechat routing, and 646-pip sync unblock
This commit is contained in:
Executable
+129
@@ -0,0 +1,129 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user