Ops hardening: lead re-import, SMS health-watch, ambient office murmur
- scripts/reimport_leads.py drains appointment_requests.jsonl into Odoo (fallback leads were captured but invisible to staff forever); server.py now warns at startup when leads are pending. - scripts/healthwatch.sh (cron, every minute): after 3 consecutive /health failures sends a Twilio SMS to ALERT_SMS_TO (30-min cooldown) + a recovery SMS. systemd handles restarts; this makes a human aware when that isn't enough. Tested end-to-end (DOWN + RECOVERED SMS delivered). - Ambient office murmur (assets/office_ambience.wav, generated by scripts/make_ambience.py from low-passed unintelligible Kokoro babble + room tone) mixed continuously into outbound audio via pipecat SoundfileMixer, so callers hear a live office instead of dead silence while the agent thinks. AMBIENT_VOLUME/AMBIENT_ENABLED to tune/disable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
96
scripts/reimport_leads.py
Executable file
96
scripts/reimport_leads.py
Executable file
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Re-import fallback leads (appointment_requests.jsonl) into Odoo.
|
||||
|
||||
When Odoo is unreachable during a call, the lead is saved to the JSONL fallback so it's
|
||||
never lost — but nothing pushed it into Odoo afterwards, so staff would never see it.
|
||||
This script drains the file: each record is written to Odoo; successes are removed,
|
||||
failures are kept for the next run. server.py also warns at startup when the file has
|
||||
pending leads.
|
||||
|
||||
Usage (loads ../.env itself):
|
||||
python scripts/reimport_leads.py # import pending leads
|
||||
python scripts/reimport_leads.py --dry-run # show what would be imported
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
PROJ = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(PROJ))
|
||||
|
||||
|
||||
def load_env(path):
|
||||
"""Minimal .env loader (KEY=VALUE lines; doesn't override existing env)."""
|
||||
if not path.exists():
|
||||
return
|
||||
for line in path.read_text().splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
k, v = line.split("=", 1)
|
||||
v = v.split(" #")[0].strip().strip("'\"")
|
||||
os.environ.setdefault(k.strip(), v)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--dry-run", action="store_true")
|
||||
args = ap.parse_args()
|
||||
|
||||
load_env(PROJ / ".env")
|
||||
jsonl = Path(os.environ.get("REQUESTS_LOG", PROJ / "appointment_requests.jsonl"))
|
||||
if not jsonl.exists() or jsonl.stat().st_size == 0:
|
||||
print("No pending fallback leads — nothing to do.")
|
||||
return 0
|
||||
|
||||
from odoo_client import create_appointment_request # after env is loaded
|
||||
|
||||
kept, imported = [], 0
|
||||
lines = [ln for ln in jsonl.read_text().splitlines() if ln.strip()]
|
||||
print(f"{len(lines)} pending lead(s) in {jsonl}")
|
||||
for ln in lines:
|
||||
try:
|
||||
rec = json.loads(ln)
|
||||
except json.JSONDecodeError:
|
||||
print(f" SKIP (unparseable, kept): {ln[:80]}")
|
||||
kept.append(ln)
|
||||
continue
|
||||
kind = rec.get("kind", "appointment")
|
||||
label = f"{kind}: {rec.get('patient_name')} / {rec.get('reason')} ({rec.get('ts')})"
|
||||
if args.dry_run:
|
||||
print(f" would import {label}")
|
||||
kept.append(ln)
|
||||
continue
|
||||
try:
|
||||
if kind == "callback":
|
||||
reason = rec.get("reason") or "callback request"
|
||||
else:
|
||||
reason = f"[{rec.get('location') or 'location TBD'}] {rec.get('reason') or ''}".strip()
|
||||
model, rec_id = create_appointment_request(
|
||||
patient_name=rec.get("patient_name"),
|
||||
callback_number=rec.get("callback_number"),
|
||||
reason=f"{reason} (re-imported from fallback)",
|
||||
preferred_time=rec.get("preferred_time"),
|
||||
insurance=rec.get("insurance"),
|
||||
call_sid=rec.get("call_sid"),
|
||||
kind=kind,
|
||||
)
|
||||
imported += 1
|
||||
print(f" OK -> Odoo {model} id={rec_id}: {label}")
|
||||
except Exception as e:
|
||||
print(f" FAILED (kept for next run): {label} — {e}")
|
||||
kept.append(ln)
|
||||
|
||||
if not args.dry_run:
|
||||
if kept:
|
||||
jsonl.write_text("\n".join(kept) + "\n")
|
||||
else:
|
||||
jsonl.unlink()
|
||||
print(f"Done: {imported} imported, {len(kept)} kept.")
|
||||
return 0 if not kept or args.dry_run else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user