Files
avc-phone-ai/scripts/reimport_leads.py
tocmo0nlord 1e7b7844a4 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>
2026-07-04 16:35:34 +00:00

97 lines
3.4 KiB
Python
Executable File

#!/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())