feat(ops): /operations — מוני-תור אחידים, "מה רץ עכשיו", וניהול-תהליכים
הדף הציג את התורים באופן לא-אחיד (by_status גולמי), בלי הבחנה בין "ממתין"
(בקלוג: status=pending) ל"בתור" (התור הפעיל: requested_at IS NOT NULL), בלי
הצגת הפריט שרץ כרגע, ובלי שום שליטה בתהליכים.
מה נוסף:
1. כרטיסי-תור אחידים — בתור / ממתין(בקלוג) / בעיבוד / הושלם / נכשל + "רץ עכשיו"
(citation/case_number של הפריט בעיבוד) לכל drain (אחזור-פסיקה, מטא-דאטה,
הלכות, יומונים). שערי-אנוש (אישור-הלכות, פסיקה-חסרה) נשארים מוני-סטטוס.
2. פאנל ניהול-תהליכים בסגנון "שירותי Windows":
- דמון (court-fetch-service/xvfb/chat/reaper): הפעל-מחדש / עצור / הפעל.
- cron drain: "הרץ עכשיו" (pm2 restart) + מתג הפעל/כבה תזמון.
3. כל תגי-הסטטוס מתורגמים לעברית.
מנגנון:
- הפעל/כבה תזמון = דגל ב-DB (טבלה drain_controls). pm2 cron_restart מחיה תהליך
שעוצר ב-stop, לכן ה"כיבוי" האמין הוא דגל שכל drain בודק ב-startup (no-op מיידי
כשכבוי). הקונטיינר כותב/קורא ישירות מ-DB.
- הרץ-עכשיו + restart/stop/start = proxy ל-pm2 דרך endpoint חדש בגשר-המארח
(court_fetch_service /pm2/control), מאובטח Bearer + whitelist ל-legal-* בלבד.
- יומונים: drain_digests הועבר מ-crontab ל-pm2 (legal-digest-drain.config.cjs)
כדי שיופיע ויהיה שליט כמו כל drain. drain_halacha_queue.py הובא לבקרת-גרסאות.
Invariants: מקיים G2 (הרחבת /operations + הגשר הקיים, לא מסלול מקביל) ו-G1
(drain_controls = מקור-אמת יחיד לכיבוי, נורמליזציה במקור ולא תיקון-בקריאה).
אין בליעת שגיאות שקטה (הגשר מחזיר {ok,error}; המוטציות מציגות toast).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -98,7 +98,8 @@
|
||||
| `drain_halacha_queue.py` | python | ריקון תור חילוץ ההלכות (`process_pending_extractions kind='halacha'`) ב-batches של 4 עד שהתור ריק (2 סבבים ריקים). חילוץ-הלכות נשאר על claude_session (לא Gemini). self-heal ל-orphaned `processing`. ההלכות נוחתות `pending_review` (שער-יו"ר). | דרך `legal-halacha-drain.config.cjs` (pm2 cron) / ידני |
|
||||
| `legal-halacha-drain.config.cjs` | pm2/js | **תזמון כל שעתיים של `drain_halacha_queue.py`** (cron `47 */2 * * *`, `HALACHA_DRAIN_CRON` לעקיפה) — מונע סתימה של תור חילוץ-ההלכות. קצב שמרני (Claude איטי + כל ריצה מוסיפה לתור-אישור-היו"ר). דורש claude CLI. התקנה: `pm2 start scripts/legal-halacha-drain.config.cjs && pm2 save`. | pm2 cron (host-side) |
|
||||
| `ingest_digests_batch.py` | python | קליטת batch של יומוני "כל יום" מ-`data/digests/incoming/` דרך המסלול העצמאי של קורפוס-הגילוי (`digest_library.ingest_digest`) — חילוץ-LLM (תג-מושג, כותרת-הלכה, מראה-מקום, שני-תאריכים), embedding יחיד, ו-autolink לפסק המקורי (X12/INV-DIG3). רצף (לא מקבילי). מזהה-יומון+תאריך נגזרים משם-הקובץ; העלון החודשי מדולג. **לא מעביר קבצים** — ה-DB (content_hash) הוא מקור-האמת היחיד; הרצה חוזרת מדלגת על קיימים (`exists`). config מ-`~/.env`. | ידני, per-batch (חלופה ל-MCP `digest_upload`) |
|
||||
| `drain_digests.py` | python | ריקון תור ההעשרה של יומונים (X12): מעבד כל digest בסטטוס `pending` דרך `digest_library.enrich_digest` (חילוץ-LLM Sonnet + embedding + autolink). מקבילי (CONCURRENCY=3, env-tunable), idempotent. מוסיף `~/.local/bin` ל-PATH כדי שה-claude CLI יימצא תחת cron. | **cron יומי** (10:00, אחרי ה-poll של n8n; flock למניעת חפיפה → `data/digests/drain.log`) + ידני אחרי backfill. חלופת-MCP: `digest_process_pending` |
|
||||
| `drain_digests.py` | python | ריקון תור ההעשרה של יומונים (X12): מעבד כל digest בסטטוס `pending` דרך `digest_library.enrich_digest` (חילוץ-LLM Sonnet + embedding + autolink). מקבילי (CONCURRENCY=3, env-tunable), idempotent. מוסיף `~/.local/bin` ל-PATH כדי שה-claude CLI יימצא תחת cron. בודק דגל `drain_controls('legal-digest-drain')` ב-startup → no-op כשכבוי מ-/operations. | דרך `legal-digest-drain.config.cjs` (pm2 cron) + ידני אחרי backfill. חלופת-MCP: `digest_process_pending` |
|
||||
| `legal-digest-drain.config.cjs` | pm2/js | **תזמון כל שעתיים של `drain_digests.py`** (cron `0 */2 * * *`, `DIGEST_DRAIN_CRON` לעקיפה) — הועבר מ-crontab של המערכת ל-pm2 כדי שיופיע ויהיה שליט בדף `/operations` (הרץ-עכשיו/הפעל/כבה). `autorestart:false` (one-shot per tick). דורש claude CLI + `VOYAGE_API_KEY`. התקנה: `pm2 start scripts/legal-digest-drain.config.cjs && pm2 save`. | pm2 cron (host-side) |
|
||||
|
||||
## סקריפטים שנמחקו (git history בלבד)
|
||||
|
||||
|
||||
@@ -22,9 +22,15 @@ import sys
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "mcp-server", "src"))
|
||||
|
||||
from legal_mcp.services import court_fetch_orchestrator as orch
|
||||
from legal_mcp.services import db
|
||||
|
||||
|
||||
async def main() -> int:
|
||||
# /operations "disable" switch — no-op immediately if turned off (pm2
|
||||
# cron_restart can still fire a stopped job, so the gate lives in the DB).
|
||||
if await db.is_drain_disabled("legal-court-fetch-drain"):
|
||||
print("===SKIP=== legal-court-fetch-drain disabled via /operations", flush=True)
|
||||
return 0
|
||||
limit = int(sys.argv[1]) if len(sys.argv) > 1 else 5
|
||||
res = await orch.drain_pending(limit=limit)
|
||||
print(f"===court-fetch drain=== processed={res.get('processed', 0)} "
|
||||
|
||||
@@ -12,8 +12,9 @@ rows still 'pending'; safe to re-run. The DB is the single source of truth.
|
||||
|
||||
Used two ways:
|
||||
1. Manually after a backfill: mcp-server/.venv/bin/python scripts/drain_digests.py
|
||||
2. Daily cron (after the n8n 09:30 Gmail poll) — see crontab; runs under flock
|
||||
so a slow run never overlaps the next. Logs to data/digests/drain.log.
|
||||
2. pm2 cron ``legal-digest-drain`` (scripts/legal-digest-drain.config.cjs) —
|
||||
one-shot per tick. Controllable from the /operations dashboard (run-now /
|
||||
enable / disable). Logs to data/digests/drain.log.
|
||||
|
||||
claude CLI must be on PATH (the cron line prepends ~/.local/bin). Config
|
||||
(POSTGRES_URL, VOYAGE_API_KEY) auto-loads from ~/.env via legal_mcp.config.
|
||||
@@ -36,6 +37,12 @@ CONCURRENCY = int(os.environ.get("DIGEST_DRAIN_CONCURRENCY", "3"))
|
||||
|
||||
async def main() -> int:
|
||||
pool = await db.get_pool()
|
||||
# /operations "disable" switch — no-op immediately if turned off (pm2
|
||||
# cron_restart can still fire a stopped job, so the gate lives in the DB).
|
||||
if await db.is_drain_disabled("legal-digest-drain"):
|
||||
print("===SKIP=== legal-digest-drain disabled via /operations", flush=True)
|
||||
await db.close_pool()
|
||||
return 0
|
||||
# get_pool() runs schema migrations first — incl. the V32 digest_kind backfill
|
||||
# that classifies legacy rows — so the failure check below is safe from the
|
||||
# very first run (no legacy row has digest_kind='').
|
||||
|
||||
47
scripts/drain_halacha_queue.py
Normal file
47
scripts/drain_halacha_queue.py
Normal file
@@ -0,0 +1,47 @@
|
||||
"""Drain the halacha extraction queue for the incoming batch.
|
||||
|
||||
Calls the canonical process_pending_extractions(kind='halacha') in small batches
|
||||
until the queue is empty (two consecutive zero-progress rounds). Serial + global
|
||||
advisory-lock coordinated inside the service — avoids concurrent Claude load spikes.
|
||||
|
||||
Run: mcp-server/.venv/bin/python scripts/drain_halacha_queue.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "mcp-server", "src"))
|
||||
|
||||
from legal_mcp.services import db
|
||||
from legal_mcp.services import precedent_library as pl
|
||||
|
||||
|
||||
async def main():
|
||||
# /operations "disable" switch — no-op immediately if turned off (pm2
|
||||
# cron_restart can still fire a stopped job, so the gate lives in the DB).
|
||||
if await db.is_drain_disabled("legal-halacha-drain"):
|
||||
print("===SKIP=== legal-halacha-drain disabled via /operations", flush=True)
|
||||
return
|
||||
total = 0
|
||||
empty_rounds = 0
|
||||
rnd = 0
|
||||
while empty_rounds < 2:
|
||||
rnd += 1
|
||||
out = await pl.process_pending_extractions(kind="halacha", limit=4)
|
||||
processed = out.get("processed", 0)
|
||||
total_pending = out.get("total_pending", 0)
|
||||
total += processed
|
||||
print(f"[round {rnd}] processed={processed} total_pending={total_pending} status={out.get('status')}", flush=True)
|
||||
for r in out.get("results", []):
|
||||
print(f" {r.get('case_number')}: {r.get('status')} stored={r.get('stored')} retry={r.get('retry_attempts')}", flush=True)
|
||||
if processed == 0:
|
||||
empty_rounds += 1
|
||||
await asyncio.sleep(5)
|
||||
else:
|
||||
empty_rounds = 0
|
||||
print(f"\n===DONE=== total halachot rounds processed; cases handled cumulatively={total}", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -18,10 +18,16 @@ import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "mcp-server", "src"))
|
||||
|
||||
from legal_mcp.services import db
|
||||
from legal_mcp.services import precedent_library as pl
|
||||
|
||||
|
||||
async def main() -> int:
|
||||
# /operations "disable" switch — no-op immediately if turned off (pm2
|
||||
# cron_restart can still fire a stopped job, so the gate lives in the DB).
|
||||
if await db.is_drain_disabled("legal-metadata-drain"):
|
||||
print("===SKIP=== legal-metadata-drain disabled via /operations", flush=True)
|
||||
return 0
|
||||
batch = int(sys.argv[1]) if len(sys.argv) > 1 else 10
|
||||
total = 0
|
||||
empty_rounds = 0
|
||||
|
||||
37
scripts/legal-digest-drain.config.cjs
Normal file
37
scripts/legal-digest-drain.config.cjs
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* pm2 ecosystem entry for legal-digest-drain — scheduled (every 2 h) drain of
|
||||
* the digest-enrichment queue (X12: "כל יום" yomonim → Sonnet enrichment +
|
||||
* embedding + autolink). Migrated from a bare system crontab line to pm2 so it
|
||||
* appears in — and is controllable from — the /operations dashboard (run-now /
|
||||
* enable / disable) like every other drain.
|
||||
*
|
||||
* Pattern: cron_restart fires the script on schedule; autorestart:false → runs
|
||||
* once and exits (pm2 shows "stopped" between ticks — expected). The script
|
||||
* already serialises itself (it self-heals stale 'processing' rows), so no flock
|
||||
* is needed under pm2's one-shot model.
|
||||
*
|
||||
* Requires (host ~/.env via legal_mcp.config): POSTGRES_URL, VOYAGE_API_KEY, and
|
||||
* the local `claude` CLI on PATH (the script prepends ~/.local/bin).
|
||||
*
|
||||
* Install (once):
|
||||
* pm2 start /home/chaim/legal-ai/scripts/legal-digest-drain.config.cjs
|
||||
* pm2 save
|
||||
* Run now (manual): mcp-server/.venv/bin/python scripts/drain_digests.py
|
||||
* Schedule override: DIGEST_DRAIN_CRON (default every 2 h at :00).
|
||||
*/
|
||||
const cron = process.env.DIGEST_DRAIN_CRON || "0 */2 * * *";
|
||||
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: "legal-digest-drain",
|
||||
cwd: "/home/chaim/legal-ai",
|
||||
script: "/home/chaim/legal-ai/mcp-server/.venv/bin/python",
|
||||
args: "scripts/drain_digests.py",
|
||||
env: { HOME: "/home/chaim", PYTHONUNBUFFERED: "1" },
|
||||
autorestart: false, // one-shot per cron tick
|
||||
cron_restart: cron,
|
||||
max_memory_restart: "800M",
|
||||
},
|
||||
],
|
||||
};
|
||||
Reference in New Issue
Block a user