3 browser-native chat surfaces (atlas-chat / atlas-hud / atlas-command, no build step) + 11 Python automation scripts (cluster, tray, finance, reminders, autonomy loops) + 2 patterns documented (cluster-then-polish, anti-impulse R1-R10) MIT licensed, anonymized from production stack.
149 lines
4.7 KiB
Python
149 lines
4.7 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
atlas_lead_enrich.py — Per-prospect WhatsApp message generator.
|
|
|
|
Use:
|
|
atlas_lead_enrich.py "Lunchroom Carrousel" "Oude Markt" --sector lunchroom --ig "ze posten dagmenu's"
|
|
|
|
Generates a personalized WhatsApp message using cluster_then_polish pattern.
|
|
|
|
Output: text printed + saved to ~/.config/atlas/drafts/lead_<slug>.md
|
|
"""
|
|
import sys
|
|
import re
|
|
import argparse
|
|
import io
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
|
|
if sys.stdout.encoding != 'utf-8':
|
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
|
|
|
sys.path.insert(0, r"C:\Users\Gebruiker\.config\atlas")
|
|
|
|
try:
|
|
from cluster_then_polish import draft, save_draft
|
|
except ImportError:
|
|
print("Need cluster_then_polish.py — see ~/.config/atlas/")
|
|
sys.exit(1)
|
|
|
|
SECTOR_TEMPLATES = {
|
|
"lunchroom": {
|
|
"package": "Werkplek STARTER €799",
|
|
"pain": "bonnetjes tellen 4-5u/wk",
|
|
"ref": "Couscousbar",
|
|
},
|
|
"koffiebar": {
|
|
"package": "Werkplek STARTER €799",
|
|
"pain": "voorraad bijhouden + dagomzet tellen",
|
|
"ref": "Coffee Almahaba",
|
|
},
|
|
"friteshouse": {
|
|
"package": "Werkplek PRO €1.499",
|
|
"pain": "afhalen-rush volgorde + tap-pay",
|
|
"ref": "Dushi Sazon",
|
|
},
|
|
"cafetaria": {
|
|
"package": "Werkplek PRO €1.499",
|
|
"pain": "voorraad-melding + dag-rapportage",
|
|
"ref": "Cafetaria Online",
|
|
},
|
|
"kapper": {
|
|
"package": "Werkplek STARTER €799",
|
|
"pain": "gemiste afspraken want naast knippen ook bellen/typen",
|
|
"ref": "Charif's Barbershop",
|
|
},
|
|
"barber": {
|
|
"package": "Werkplek STARTER €799",
|
|
"pain": "afsprakenbeheer + kassa zonder onderbreken",
|
|
"ref": "Charif's Barbershop",
|
|
},
|
|
"horeca": {
|
|
"package": "Werkplek KASSA €2.499",
|
|
"pain": "POS + voorraad + rapportage in één",
|
|
"ref": "Bisogno",
|
|
},
|
|
"retail": {
|
|
"package": "Werkplek PRO €1.499",
|
|
"pain": "voorraad + kassa + dag-overzicht",
|
|
"ref": "Le Miris",
|
|
},
|
|
}
|
|
|
|
|
|
def slugify(name):
|
|
s = re.sub(r"[^a-z0-9]+", "-", name.lower())
|
|
return s.strip("-")[:40]
|
|
|
|
|
|
def build_message(name, location, sector, ig_hint, voornaam_hint=""):
|
|
"""Build personalized WhatsApp via cluster + polish."""
|
|
t = SECTOR_TEMPLATES.get(sector.lower(), SECTOR_TEMPLATES["horeca"])
|
|
voornaam = voornaam_hint or "{voornaam}"
|
|
|
|
prompt = f"""Schrijf één persoonlijk cold-WhatsApp bericht voor een Tilburgse zaak. NL, casual maar professional.
|
|
|
|
ZAAK: {name}
|
|
LOCATIE: {location}
|
|
SECTOR: {sector}
|
|
IG/CONTEXT HINT: {ig_hint}
|
|
EIGENAAR VOORNAAM: {voornaam}
|
|
|
|
AANBOD: {t['package']}
|
|
PIJNPUNT: {t['pain']}
|
|
REFERENTIE: {t['ref']}
|
|
|
|
REGELS:
|
|
- Open met "Hoi {voornaam}," + 1 zin die TONE-PERSOONLIJK aansluit op IG-context hint
|
|
- Korte pijn-kwestie noemen (max 1 zin)
|
|
- Aanbod met prijs upfront
|
|
- Zachte CTA: 15-20 min koffie of bezoek, geen verkoop-druk
|
|
- Max 80 woorden totaal
|
|
- Eindigt met één concrete vraag
|
|
|
|
Output ALLEEN het bericht, geen toelichting."""
|
|
|
|
# task=general → qwen2.5:7b on atlas-01 (always-on, NL-decent)
|
|
return draft(prompt, task="general", node_pref="atlas-01",
|
|
max_tokens=400, temperature=0.6, purpose="lead_outreach")
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("name", help="Zaak naam")
|
|
ap.add_argument("location", help="Adres / wijk")
|
|
ap.add_argument("--sector", required=True, choices=list(SECTOR_TEMPLATES.keys()))
|
|
ap.add_argument("--ig", default="", help="IG/context detail om aan te haken")
|
|
ap.add_argument("--voornaam", default="", help="Eigenaar voornaam")
|
|
ap.add_argument("--out", default=None, help="Save path (auto if omitted)")
|
|
args = ap.parse_args()
|
|
|
|
print(f"Generating message for: {args.name} ({args.sector}) at {args.location}")
|
|
print(f"Hook: {args.ig or '(none)'}")
|
|
print()
|
|
|
|
result = build_message(args.name, args.location, args.sector, args.ig, args.voornaam)
|
|
if not result.get("ok"):
|
|
print("ERR:", result.get("err"))
|
|
sys.exit(1)
|
|
|
|
print("=" * 60)
|
|
print(result["text"])
|
|
print("=" * 60)
|
|
print(f"\n[{result['node']}/{result['model']} {result['elapsed_s']}s]")
|
|
print(f"polish_status: {result['polish_status']}")
|
|
print(f"HINT: {result['hint']}")
|
|
print()
|
|
|
|
out_path = args.out or (
|
|
Path.home() / ".config" / "atlas" / "drafts" /
|
|
f"lead_{slugify(args.name)}_{datetime.now().strftime('%Y%m%d_%H%M')}.md"
|
|
)
|
|
saved = save_draft(result, out_path)
|
|
print(f"\nSaved → {saved}")
|
|
print("\nReview, edit if needed, copy to WhatsApp, send.")
|
|
print("After sending: mark_polished(draft_id) to track conversion.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|