feat(ralph): rate limit budget tracking + whatsapp text-keywords
Task #1 — Rate limit budget tracking MVP: - tools/ralph_usage.py: pure functions (extract_usage_entry, parse_usage_jsonl, aggregate_by_day/_project, filter_by_days, summarize) + CLI append/summarize subcommands. Atomic write via temp+rename. - tools/ralph/ralph.sh: după fiecare claude -p, append usage entry derivat din JSON envelope la <project>/scripts/ralph/usage.jsonl. Best-effort, niciodată blochează rularea (|| true). - dashboard/handlers/ralph.py: GET /api/ralph/usage[?days=N] aggregează cross- project și returnează {today_cost, today_runs, by_project, by_day, ...}. Task #2 — WhatsApp text-keyword commands: - src/router.py: helper _translate_whatsapp_text mapează "aprob"/"stop <slug>"/ "stare [<slug>]" → /a, /k, /l. Apelat DOAR pe adapter whatsapp în _try_ralph_dispatch (Discord/TG nu sunt afectate). NU acoperim propose intentionat — descrierea liberă e prea fragilă pentru parsing text-only. Tests: 49 noi (test_ralph_usage 28 + test_whatsapp_keywords 21) + 4 noi în test_dashboard_ralph_endpoint pentru /api/ralph/usage. Toate trec; regression suite (test_router, test_router_planning, test_dashboard_ralph_endpoint, test_whatsapp) — 90/90 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -241,8 +241,51 @@ def _maybe_whatsapp_redirect(text: str, adapter_name: str | None) -> str:
|
||||
return text
|
||||
|
||||
|
||||
def _translate_whatsapp_text(text: str) -> str | None:
|
||||
"""Translate WhatsApp text-keyword commands to slash equivalents.
|
||||
|
||||
Acoperă **doar** keyword-urile robuste (single-token + opțional slug):
|
||||
- `aprob` → `/a` (listează pending)
|
||||
- `aprob <slug>` → `/a <slug>` (aprobă proiect)
|
||||
- `stop <slug>` → `/k <slug>` (oprește Ralph)
|
||||
- `stare` → `/l` (status global)
|
||||
- `stare <slug>` → `/l <slug>` (status filtrat)
|
||||
|
||||
NU acoperă `propose` — descrierea liberă e prea fragilă pentru parsing
|
||||
text-only (utilizatorii ar trimite descrieri multi-line care s-ar
|
||||
interpreta greșit). Pentru propose, redirecționăm spre Discord/Telegram.
|
||||
|
||||
Returnează slash command translatat sau None dacă text-ul nu match.
|
||||
Case-insensitive pe keyword (slug-ul rămâne ca în input).
|
||||
|
||||
Apelat DOAR pe adapter `whatsapp` în router (nu vrem ca un user pe
|
||||
Discord să zică „stop" și să se întâmple ceva).
|
||||
"""
|
||||
if not text or not text.strip():
|
||||
return None
|
||||
|
||||
parts = text.strip().split(None, 1)
|
||||
keyword = parts[0].lower()
|
||||
rest = parts[1].strip() if len(parts) > 1 else ""
|
||||
|
||||
if keyword == "aprob":
|
||||
return f"/a {rest}".rstrip()
|
||||
if keyword == "stop" and rest:
|
||||
# `stop` fără slug ar putea fi colocvial („stop, am uitat ceva") — nu translatăm.
|
||||
return f"/k {rest}"
|
||||
if keyword == "stare":
|
||||
return f"/l {rest}".rstrip()
|
||||
return None
|
||||
|
||||
|
||||
def _try_ralph_dispatch(text: str, adapter_name: str | None = None) -> str | None:
|
||||
"""Parse and dispatch Ralph commands. Returns response string or None if no match."""
|
||||
# WhatsApp keyword preprocessing — doar pe whatsapp, înainte de dispatch.
|
||||
if adapter_name == "whatsapp":
|
||||
translated = _translate_whatsapp_text(text)
|
||||
if translated is not None:
|
||||
text = translated
|
||||
|
||||
low = text.lower()
|
||||
first = low.split(None, 1)[0] if low else ""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user