Merge branch 'ralph/instrumentation' — rate limit budget + WhatsApp keywords
Rate limit budget tracking (TODO P2): - tools/ralph_usage.py — pure functions extract/parse/aggregate; CLI subcomenzi append/summarize. Atomic write JSONL. - tools/ralph/ralph.sh: după fiecare claude -p, append usage entry la workspace/<slug>/scripts/ralph/usage.jsonl (best-effort) - dashboard/handlers/ralph.py: GET /api/ralph/usage[?days=N] cross-project aggregation cu today_cost, today_runs, by_project, by_day WhatsApp text-keyword commands (TODO P3): - src/router.py: helper _translate_whatsapp_text — `aprob <slug>` → `/a <slug>`, `stop <slug>` → `/k <slug>`, `stare`/`stare <slug>` → `/l`/`/l <slug>`. Aplicat DOAR pe adapter whatsapp în _try_ralph_dispatch (Discord/TG nu sunt afectate). Propose intentionally NOT covered (descrierea fragilă). Tests: 53 noi (28 ralph_usage + 21 whatsapp_keywords + 4 dashboard endpoint extend) + 0 regressions pe modulele atinse.
This commit is contained in:
@@ -163,6 +163,8 @@ class TaskBoardHandler(
|
||||
self.handle_eco_doctor()
|
||||
elif self.path == '/api/ralph/status' or self.path.startswith('/api/ralph/status?'):
|
||||
self.handle_ralph_status()
|
||||
elif self.path == '/api/ralph/usage' or self.path.startswith('/api/ralph/usage?'):
|
||||
self.handle_ralph_usage()
|
||||
elif self.path.startswith('/api/ralph/'):
|
||||
# /api/ralph/<slug>/log or /api/ralph/<slug>/prd
|
||||
parts = self.path.split('?', 1)[0].split('/')
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"""Ralph live dashboard endpoints (W3).
|
||||
"""Ralph live dashboard endpoints (W3 + instrumentation).
|
||||
|
||||
Endpoints:
|
||||
GET /api/ralph/status — toate proiectele Ralph (cards data)
|
||||
GET /api/ralph/<slug>/log — tail progress.txt (default 100 lines)
|
||||
GET /api/ralph/<slug>/prd — full prd.json content
|
||||
GET /api/ralph/usage[?days=N] — rate limit budget summary (cross-project)
|
||||
POST /api/ralph/<slug>/stop — SIGTERM la Ralph PID
|
||||
|
||||
Polling: 5s din ralph.html (suficient pentru iter 8-15min Ralph).
|
||||
@@ -14,18 +15,30 @@ Citește status din `~/workspace/<slug>/scripts/ralph/`:
|
||||
- progress.txt → log human-readable
|
||||
- logs/iteration-*.log → mtime ultimului iter
|
||||
- .ralph.pid → PID activ (verificat cu os.kill 0)
|
||||
- usage.jsonl → token/cost log per iter (instrumentation MVP)
|
||||
|
||||
Reuse path constants din `dashboard/constants.py` (WORKSPACE_DIR).
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from urllib.parse import unquote
|
||||
|
||||
import constants
|
||||
|
||||
# Best-effort import of pure functions for /api/ralph/usage (instrumentation MVP).
|
||||
# Helper lives at <repo>/tools/ralph_usage.py — sibling of `dashboard/`.
|
||||
_TOOLS_DIR = Path(__file__).resolve().parents[2] / "tools"
|
||||
if str(_TOOLS_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(_TOOLS_DIR))
|
||||
try:
|
||||
import ralph_usage # type: ignore
|
||||
except ImportError: # pragma: no cover — diagnostic only
|
||||
ralph_usage = None # type: ignore
|
||||
|
||||
|
||||
# Path Ralph per proiect (mereu în scripts/ralph/)
|
||||
def _ralph_dir(project_dir: Path) -> Path:
|
||||
@@ -259,6 +272,58 @@ class RalphHandlers:
|
||||
except Exception as exc:
|
||||
self.send_json({"error": str(exc)}, 500)
|
||||
|
||||
# ── /api/ralph/usage (GET) ─────────────────────────────────
|
||||
def handle_ralph_usage(self):
|
||||
"""Returnează rate limit budget summary cross-project.
|
||||
|
||||
Citește toate `~/workspace/<slug>/scripts/ralph/usage.jsonl`, le concatenează,
|
||||
rulează `ralph_usage.summarize` cu `?days=N` (default 7).
|
||||
|
||||
Răspuns:
|
||||
{
|
||||
"today": "YYYY-MM-DD",
|
||||
"today_cost": float,
|
||||
"today_runs": int,
|
||||
"window_days": N,
|
||||
"window_cost": float,
|
||||
"window_runs": int,
|
||||
"by_project": {...},
|
||||
"by_day": {...},
|
||||
"total_cost": float,
|
||||
"total_runs": int
|
||||
}
|
||||
"""
|
||||
try:
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
qs = parse_qs(urlparse(self.path).query)
|
||||
try:
|
||||
days = int(qs.get("days", ["7"])[0])
|
||||
if days <= 0:
|
||||
days = 7
|
||||
if days > 365:
|
||||
days = 365
|
||||
except ValueError:
|
||||
days = 7
|
||||
|
||||
if ralph_usage is None:
|
||||
self.send_json({"error": "ralph_usage helper unavailable"}, 500)
|
||||
return
|
||||
|
||||
entries: list[dict] = []
|
||||
if constants.WORKSPACE_DIR.exists():
|
||||
for entry in sorted(constants.WORKSPACE_DIR.iterdir()):
|
||||
if not entry.is_dir() or entry.name.startswith("."):
|
||||
continue
|
||||
usage_path = _ralph_dir(entry) / "usage.jsonl"
|
||||
if usage_path.exists():
|
||||
entries.extend(ralph_usage.parse_usage_jsonl(usage_path))
|
||||
|
||||
summary = ralph_usage.summarize(entries, days=days)
|
||||
summary["fetchedAt"] = datetime.now().isoformat()
|
||||
self.send_json(summary)
|
||||
except Exception as exc:
|
||||
self.send_json({"error": str(exc)}, 500)
|
||||
|
||||
# ── /api/ralph/<slug>/stop (POST) ──────────────────────────
|
||||
def handle_ralph_stop(self, slug: str):
|
||||
"""Trimite SIGTERM la Ralph PID. Verifică că PID-ul e în WORKSPACE_DIR."""
|
||||
|
||||
Reference in New Issue
Block a user