Download customer_handler.py from Logan795/Openclaw: direct link, hf CLI and curl.
- Browser
- Download file 17.4 kB
-
https://huggingface.co/spaces/Logan795/Openclaw/resolve/main/customer_handler.py
- Command line
-
hf download hf://spaces/Logan795/Openclaw/customer_handler.py
-
curl -L -o customer_handler.py https://huggingface.co/spaces/Logan795/Openclaw/resolve/main/customer_handler.py
17.4 kB
| """customer_handler.py β Full state machine for every lead. | |
| States: | |
| FOUND β OUTREACH_SENT β DETAILS_REQUESTED β DEMO_BUILDING | |
| β DEMO_SENT β NEGOTIATING β PAYMENT_PENDING | |
| β PAYMENT_VERIFIED β DELIVERED / REJECTED | |
| """ | |
| import asyncio | |
| import logging | |
| from typing import Optional | |
| import ai_agent | |
| import database as db | |
| import email_manager | |
| import website_builder | |
| import netlify_deployer | |
| import scraper as scraper_mod | |
| from config import PAYPAL_EMAIL, BANK_INFO, SENDER_EMAILS | |
| log = logging.getLogger(__name__) | |
| _admin_notify_fn = None | |
| def set_admin_notifier(fn): | |
| global _admin_notify_fn | |
| _admin_notify_fn = fn | |
| async def _notify(text: str): | |
| if _admin_notify_fn: | |
| try: | |
| await _admin_notify_fn(text) | |
| except Exception as e: | |
| log.error("Admin notify error: %s", e) | |
| def _sender(lead) -> Optional[dict]: | |
| sid = lead["sender_email_id"] | |
| for a in SENDER_EMAILS: | |
| if a["id"] == sid: | |
| return a | |
| return None | |
| def _history(lead_id: int) -> str: | |
| logs = db.get_email_history(lead_id) | |
| if not logs: | |
| return "No prior conversation." | |
| parts = [] | |
| for e in logs[-6:]: | |
| who = "Us" if e["direction"] == "outbound" else "Customer" | |
| parts.append(f"[{who}]: {e['body'][:100]}...") | |
| return "\n".join(parts) | |
| async def _reply_to(lead, reply: dict, body: str, subject_prefix: str = "Re: "): | |
| subj = subject_prefix + reply["subject"] | |
| sender = _sender(lead) | |
| used = await email_manager.send_email(reply["from_email"], subj, body, sender=sender) | |
| if used: | |
| db.log_email(lead["id"], used, "outbound", subj, body) | |
| # ββ Build and deploy demo site ββββββββββββββββββββββββββββββββββββββββββββ | |
| async def _build_and_deploy(lead_id: int, extra_details: dict = None) -> tuple: | |
| """Returns (site_id, live_url, zip_path).""" | |
| lead = db.get_lead(lead_id) | |
| extra = extra_details or {} | |
| db.update_lead(lead_id, state="DEMO_BUILDING") | |
| # Scrape extra business details from Google Maps if not provided | |
| if not extra: | |
| log.info("π Fetching business details for '%s'...", lead["business_name"]) | |
| try: | |
| extra = await scraper_mod.fetch_business_details_for_lead(lead_id) | |
| except Exception as e: | |
| log.warning("Could not fetch extra details: %s", e) | |
| # DeepSeek R1 generates content | |
| content = ai_agent.generate_site_content( | |
| business_name = lead["business_name"], | |
| category = lead["category"] or "business", | |
| address = lead["address"] or "", | |
| phone = lead["phone"] or "", | |
| email = lead["email"], | |
| business_hours = extra.get("hours",""), | |
| description = extra.get("description",""), | |
| social_links = [], | |
| ) | |
| # Build HTML | |
| site_dir = website_builder.build_site_from_content(lead_id, content) | |
| zip_path = website_builder.get_site_zip_path(site_dir) | |
| # Deploy to Netlify | |
| site_id, live_url = netlify_deployer.deploy_for_lead( | |
| lead_id, lead["business_name"], zip_path | |
| ) | |
| # Store zip path for possible re-deploy after payment | |
| db.update_lead(lead_id, | |
| state="DEMO_SENT", | |
| netlify_site_id=site_id, | |
| netlify_url=live_url, | |
| notes=zip_path) # reuse notes field for zip path | |
| return site_id, live_url, zip_path | |
| # ββ Main reply handler ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def handle_reply(reply: dict): | |
| lead_id = reply["lead_id"] | |
| body = reply["body"] | |
| from_email = reply["from_email"] | |
| lead = db.get_lead(lead_id) | |
| if not lead: | |
| return | |
| # Log inbound | |
| db.log_email(lead_id, from_email, "inbound", | |
| reply["subject"], body, reply.get("message_id","")) | |
| intent = ai_agent.classify_reply(body) | |
| log.info("π§ Lead #%d '%s' β %s", lead_id, lead["business_name"], intent) | |
| history = _history(lead_id) | |
| # ββ NOT INTERESTED ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if intent == "NEGATIVE": | |
| resp = ai_agent.compose_reply("NEGATIVE", lead["business_name"], history_summary=history) | |
| await _reply_to(lead, reply, resp) | |
| db.update_lead(lead_id, state="REJECTED") | |
| return | |
| # ββ POSITIVE β ask for business details before building βββββββββββββββ | |
| if intent == "POSITIVE": | |
| ask_body = ai_agent.generate_details_request_email(lead["business_name"]) | |
| await _reply_to(lead, reply, ask_body) | |
| db.update_lead(lead_id, state="DETAILS_REQUESTED") | |
| await _notify( | |
| f"β¨ *Positive Reply!*\n" | |
| f"Business: *{lead['business_name']}*\n" | |
| f"Email: {from_email}\n" | |
| "Asked them for business details (photos, hours, description)." | |
| ) | |
| return | |
| # ββ CUSTOMER SENDS DETAILS β build + deploy demo ββββββββββββββββββββββ | |
| if intent == "DETAILS" or ( | |
| lead["state"] == "DETAILS_REQUESTED" and intent == "NEUTRAL" | |
| ): | |
| await _notify( | |
| f"π *Details received from {lead['business_name']}*\n" | |
| "Building their demo website now..." | |
| ) | |
| try: | |
| _, live_url, _ = await _build_and_deploy(lead_id) | |
| except Exception as e: | |
| log.error("Build error for lead #%d: %s", lead_id, e) | |
| await _notify(f"β οΈ Build failed for lead #{lead_id}: {e}") | |
| return | |
| resp = ( | |
| f"Hi {lead['business_name']} team! π\n\n" | |
| f"Your demo website is ready! Check it out here:\n\n" | |
| f"π {live_url}\n\n" | |
| "Let me know what you think! If you'd like any changes to colours, " | |
| "text, layout β just reply and I'll update it right away.\n\n" | |
| "If you're happy with it, I'll give you the final price to make it permanently yours!\n\n" | |
| "Best,\nWebReach Team" | |
| ) | |
| await _reply_to(lead, reply, resp) | |
| await _notify( | |
| f"π *Demo Deployed!*\n" | |
| f"Business: *{lead['business_name']}*\n" | |
| f"URL: {live_url}" | |
| ) | |
| return | |
| # ββ EDIT REQUEST ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if intent == "EDIT_REQUEST": | |
| edits = ai_agent.extract_edit_instructions(body) | |
| db.update_lead(lead_id, state="DEMO_SENT", | |
| notes=(db.get_lead(lead_id)["notes"] or "") + f" | EDITS: {edits}") | |
| # Rebuild with same details but note the edits | |
| try: | |
| _, live_url, _ = await _build_and_deploy(lead_id) | |
| except Exception as e: | |
| log.error("Rebuild error: %s", e) | |
| return | |
| resp = ai_agent.compose_reply( | |
| "EDIT_REQUEST", lead["business_name"], | |
| netlify_url=live_url, extra=edits, history_summary=history | |
| ) | |
| await _reply_to(lead, reply, resp) | |
| return | |
| # ββ SATISFIED β quote price βββββββββββββββββββββββββββββββββββββββββββ | |
| if intent == "SATISFIED": | |
| price = ai_agent.calculate_price( | |
| lead["business_name"], lead["category"] or "", history | |
| ) | |
| db.update_lead(lead_id, state="NEGOTIATING", price_quoted=price) | |
| resp = ai_agent.compose_reply( | |
| "SATISFIED", lead["business_name"], | |
| netlify_url=lead["netlify_url"] or "", | |
| price=price, history_summary=history | |
| ) | |
| await _reply_to(lead, reply, resp) | |
| return | |
| # ββ PRICING QUESTION ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if intent == "PRICING": | |
| price = ai_agent.calculate_price( | |
| lead["business_name"], lead["category"] or "", history | |
| ) | |
| db.update_lead(lead_id, state="NEGOTIATING", price_quoted=price) | |
| resp = ai_agent.compose_reply( | |
| "PRICING", lead["business_name"], price=price, history_summary=history | |
| ) | |
| await _reply_to(lead, reply, resp) | |
| return | |
| # ββ CUSTOMER CLAIMS PAID ββββββββββββββββββββββββββββββββββββββββββββββ | |
| if intent == "PAID": | |
| db.update_lead(lead_id, state="PAYMENT_PENDING") | |
| db.add_payment_request(lead_id, lead["price_quoted"] or 0) | |
| # Deploy the real site immediately (but it's not "approved" yet) | |
| live_url = lead["netlify_url"] or "" | |
| if not live_url: | |
| try: | |
| _, live_url, _ = await _build_and_deploy(lead_id) | |
| except Exception as e: | |
| log.error("Deploy error: %s", e) | |
| # Ask about domain (explain what it is simply) | |
| domain_ask = ( | |
| f"Hi {lead['business_name']} team!\n\n" | |
| "Thank you β we're verifying your payment now and will confirm shortly. β \n\n" | |
| "Quick question: do you have a **domain name** for your website?\n\n" | |
| "A domain is your website address β like *mybusiness.com* β " | |
| "so people can find you easily instead of a long link.\n\n" | |
| "If you have one, just reply with it and we'll connect it for you.\n" | |
| "If you don't have one yet, no worries β your site will work on our link for now, " | |
| "and we can help you get a domain later!\n\n" | |
| "Best,\nWebReach Team" | |
| ) | |
| await _reply_to(lead, reply, domain_ask) | |
| # Alert admin on Telegram | |
| await _notify( | |
| f"π° *PAYMENT CLAIM*\n" | |
| f"Lead #: `{lead_id}`\n" | |
| f"Business: *{lead['business_name']}*\n" | |
| f"Customer: {from_email}\n" | |
| f"Amount: ${lead['price_quoted'] or '?':.0f}\n" | |
| f"Site: {live_url}\n\n" | |
| f"β /verify {lead_id} β payment confirmed\n" | |
| f"β /reject {lead_id} β not paid, block site" | |
| ) | |
| return | |
| # ββ DOMAIN PROVIDED βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if intent == "OWN_DOMAIN_PROVIDED": | |
| domain = ai_agent.extract_domain_from_reply(body) | |
| if domain: | |
| db.update_lead(lead_id, custom_domain=domain) | |
| resp = ai_agent.compose_reply( | |
| "OWN_DOMAIN_PROVIDED", lead["business_name"], | |
| extra=domain or "domain not clear", history_summary=history | |
| ) | |
| await _reply_to(lead, reply, resp) | |
| return | |
| # ββ ASKING ABOUT DOMAIN βββββββββββββββββββββββββββββββββββββββββββββββ | |
| if intent == "DOMAIN": | |
| resp = ai_agent.compose_reply("DOMAIN", lead["business_name"], history_summary=history) | |
| await _reply_to(lead, reply, resp) | |
| return | |
| # ββ ASKING FOR SOURCE CODES βββββββββββββββββββββββββββββββββββββββββββ | |
| if intent == "CODES": | |
| if lead["payment_confirmed"]: | |
| # Payment already verified β send codes notice | |
| resp = ( | |
| f"Hi {lead['business_name']} team,\n\n" | |
| "Great news β since your payment is verified, we'll send over your website files shortly!\n\n" | |
| "We'll email them to you as a ZIP file within 24 hours.\n\n" | |
| "Best,\nWebReach Team" | |
| ) | |
| await _notify( | |
| f"π¦ *Code Request β APPROVED*\n" | |
| f"Lead #{lead_id}: *{lead['business_name']}*\n" | |
| "Payment already verified. Please send them the ZIP." | |
| ) | |
| else: | |
| # Not yet verified β hold | |
| resp = ai_agent.compose_reply("CODES", lead["business_name"], history_summary=history) | |
| await _notify( | |
| f"β οΈ *Code Request β NOT VERIFIED*\n" | |
| f"Lead #{lead_id}: *{lead['business_name']}*\n" | |
| "Payment NOT confirmed. Holding codes." | |
| ) | |
| await _reply_to(lead, reply, resp) | |
| return | |
| # ββ OTHER HOST ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if intent == "OTHER_HOST": | |
| resp = ai_agent.compose_reply("OTHER_HOST", lead["business_name"], history_summary=history) | |
| await _reply_to(lead, reply, resp) | |
| db.update_lead(lead_id, notes="Wants own hosting") | |
| return | |
| # ββ NEUTRAL / FALLBACK ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| resp = ai_agent.compose_reply("NEUTRAL", lead["business_name"], history_summary=history) | |
| await _reply_to(lead, reply, resp) | |
| # ββ Admin: verify payment βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def verify_payment(lead_id: int): | |
| """Admin confirmed payment β unblock/redeploy and deliver.""" | |
| lead = db.get_lead(lead_id) | |
| if not lead: | |
| return | |
| db.confirm_payment(lead_id) | |
| live_url = lead["netlify_url"] or "" | |
| # Attach custom domain if provided | |
| if lead["custom_domain"] and lead["netlify_site_id"]: | |
| try: | |
| live_url = netlify_deployer.set_custom_domain( | |
| lead["netlify_site_id"], lead["custom_domain"] | |
| ) | |
| db.update_lead(lead_id, netlify_url=live_url) | |
| except Exception as e: | |
| log.warning("Domain attach error: %s", e) | |
| # Email customer with live URL | |
| sender = _sender(lead) | |
| delivery_body = ( | |
| f"Hi {lead['business_name']} team! π\n\n" | |
| "Your payment has been confirmed β your website is now fully LIVE!\n\n" | |
| f"π {live_url}\n\n" | |
| + (f"Your custom domain *{lead['custom_domain']}* has been connected. " | |
| "DNS changes can take up to 24 hours to fully propagate worldwide.\n\n" | |
| if lead["custom_domain"] else "") | |
| + "Your website will stay live. If you'd like any future changes, just reach out!\n\n" | |
| "Thank you for choosing us β we'd love a review when you get a chance! π\n\n" | |
| "Best,\nWebReach Team" | |
| ) | |
| used = await email_manager.send_email(lead["email"], "π Your website is LIVE!", delivery_body, sender=sender) | |
| if used: | |
| db.log_email(lead_id, used, "outbound", "π Your website is LIVE!", delivery_body) | |
| db.update_lead(lead_id, state="DELIVERED") | |
| log.info("π Delivered to lead #%d", lead_id) | |
| await _notify( | |
| f"β *DELIVERED!*\n" | |
| f"Business: *{lead['business_name']}*\n" | |
| f"URL: {live_url}" | |
| ) | |
| # ββ Admin: reject payment β block site βββββββββββββββββββββββββββββββββββ | |
| async def reject_payment(lead_id: int): | |
| """Admin said not paid β block the Netlify site.""" | |
| lead = db.get_lead(lead_id) | |
| if not lead: | |
| return | |
| if lead["netlify_site_id"]: | |
| try: | |
| netlify_deployer.block_site(lead["netlify_site_id"]) | |
| log.info("π Site blocked for lead #%d", lead_id) | |
| except Exception as e: | |
| log.error("Block site error: %s", e) | |
| db.update_lead(lead_id, state="NEGOTIATING", notes="Payment rejected β site blocked") | |
| # Tell customer politely | |
| sender = _sender(lead) | |
| msg = ( | |
| f"Hi {lead['business_name']} team,\n\n" | |
| "We weren't able to verify your payment yet. " | |
| "The demo has been temporarily paused.\n\n" | |
| "Once payment is confirmed, we'll restore it immediately. " | |
| f"Please send payment to:\n" | |
| f"π³ PayPal: {PAYPAL_EMAIL}\n" | |
| f"π¦ {BANK_INFO}\n\n" | |
| "Reply once you've sent it and we'll reactivate your site right away!\n\n" | |
| "Best,\nWebReach Team" | |
| ) | |
| used = await email_manager.send_email(lead["email"], "Payment Verification Needed", msg, sender=sender) | |
| if used: | |
| db.log_email(lead_id, used, "outbound", "Payment Verification Needed", msg) | |
| await _notify(f"π Site blocked for lead #{lead_id}: *{lead['business_name']}*") | |
| # ββ Poll all replies ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def process_all_replies(): | |
| replies = await email_manager.check_all_replies() | |
| if replies: | |
| log.info("π¬ Processing %d replies", len(replies)) | |
| for r in replies: | |
| await handle_reply(r) | |
| else: | |
| log.debug("π No new replies") | |