deploy DOM search v6 update
This commit is contained in:
+40
-20
@@ -1322,8 +1322,13 @@ def scheduler_loop() -> None:
|
||||
time.sleep(30)
|
||||
|
||||
|
||||
def ai_generate_message(prospect: dict[str, Any], offer: str) -> dict[str, Any]:
|
||||
"""Generate an evidence-grounded, human-editable outreach draft."""
|
||||
def ai_generate_message(prospect: dict[str, Any], offer: str, tone: str = "warm", goal: str = "conversation") -> dict[str, Any]:
|
||||
"""Generate an evidence-grounded, human-editable outreach draft.
|
||||
|
||||
``tone`` and ``goal`` are deliberately small, validated controls rather
|
||||
than an unrestricted prompt field. This keeps drafts consistent while
|
||||
still letting the owner choose how direct the first contact should feel.
|
||||
"""
|
||||
runtime = ai_runtime_settings()
|
||||
ai_base_url = runtime["base_url"]
|
||||
ai_api_key = runtime["api_key"]
|
||||
@@ -1341,6 +1346,16 @@ def ai_generate_message(prospect: dict[str, Any], offer: str) -> dict[str, Any]:
|
||||
evidence_ids.append("public_email_listing")
|
||||
if prospect.get("phone"):
|
||||
evidence_ids.append("public_phone_listing")
|
||||
tone_options = {"warm": "warm and approachable", "direct": "direct and concise", "consultative": "consultative and helpful"}
|
||||
goal_options = {"conversation": "start a low-pressure conversation", "review": "offer a short website/hosting review", "quote": "invite them to request pricing or a quote"}
|
||||
tone_key = str(tone or "warm").strip().lower()
|
||||
goal_key = str(goal or "conversation").strip().lower()
|
||||
if tone_key not in tone_options:
|
||||
tone_key = "warm"
|
||||
if goal_key not in goal_options:
|
||||
goal_key = "conversation"
|
||||
tone_instruction = tone_options[tone_key]
|
||||
goal_instruction = goal_options[goal_key]
|
||||
evidence = {
|
||||
"evidence_ids": evidence_ids,
|
||||
"business_name": prospect.get("name"),
|
||||
@@ -1354,30 +1369,35 @@ def ai_generate_message(prospect: dict[str, Any], offer: str) -> dict[str, Any]:
|
||||
"url": prospect.get("website"), "status": prospect.get("website_status"),
|
||||
"http_status": prospect.get("website_http_status"), "title": prospect.get("website_title"),
|
||||
"technology": prospect.get("website_technology"), "https": bool(prospect.get("website_https")),
|
||||
"latency_ms": prospect.get("website_latency_ms"),
|
||||
"latency_ms": prospect.get("website_latency_ms"), "error": prospect.get("website_error"),
|
||||
},
|
||||
"public_emails": prospect.get("emails"),
|
||||
"opportunity_score": prospect.get("opportunity_score"),
|
||||
"score_reasons": _json_list(prospect.get("score_reasons")),
|
||||
}
|
||||
system = (
|
||||
"You are a senior B2B copywriter for a South African web-hosting and website-design business. "
|
||||
"Create a short, respectful, genuinely personalised first-contact email from the supplied evidence. "
|
||||
"Use only stored facts and the user's offer; never invent a person's name, company activity, price, "
|
||||
"service, problem, result, testimonial, urgency, or link. Do not mention scraping, scoring, hidden "
|
||||
"analysis, or imply certainty where the evidence is uncertain. Do not insult the prospect or call a "
|
||||
"site bad; use neutral wording such as 'may be having trouble loading' when appropriate. "
|
||||
"If there is no website, do not say you reviewed their website. If there is no mockup URL in the offer, "
|
||||
"do not invent one. Keep the email around 90-150 words, with a natural greeting using the business name, "
|
||||
"one specific evidence-based observation, the exact offer/instructions supplied by the user, one simple "
|
||||
"call to action, and a polite opt-out sentence. Avoid spammy hype, excessive emojis, all-caps, and fake "
|
||||
"personal familiarity. Return ONLY strict JSON with exactly these keys: subject, message, rationale, "
|
||||
"recommended_offer, unknowns, evidence_used, follow_up. subject is 5-10 natural words. message is plain "
|
||||
"text. rationale is concise. recommended_offer is a short summary of the user's offer. unknowns is an "
|
||||
"array of facts that should be confirmed. evidence_used is an array containing only the supplied evidence "
|
||||
"IDs. follow_up is a short human review note, not part of the email."
|
||||
"You are an expert B2B copywriter for a small South African web-hosting and website-design business. "
|
||||
"Write one genuinely human first-contact email, not a mass-marketing template. The desired voice is "
|
||||
f"{tone_instruction}; the goal is to {goal_instruction}. "
|
||||
"Use only the stored evidence and the user's offer. Never invent a person's name, company activity, "
|
||||
"price, service, problem, result, testimonial, urgency, authority, or link. Do not mention scraping, "
|
||||
"scoring, hidden analysis, AI, or this instruction. Do not say you reviewed a website when no website "
|
||||
"was found. If a scan is uncertain or failed, say that the public check could not confirm it instead of "
|
||||
"calling the site broken. Do not insult the prospect or use alarmist language. "
|
||||
"Start with a concrete observation tied to one supplied fact; avoid empty openings such as 'I hope you "
|
||||
"are well' or 'I came across your business'. Use South African English and the business name exactly as "
|
||||
"provided. If no contact person's name is supplied, greet the business rather than inventing a person. "
|
||||
"Keep the body plain text and about 90-140 words: greeting, one specific observation, a natural bridge "
|
||||
"to the exact offer, one low-pressure call to action, and a brief respectful opt-out sentence. Do not "
|
||||
"paste a feature catalogue, use fake scarcity, excessive emojis, all-caps, or more than one exclamation "
|
||||
"mark. The subject should be specific and natural, 5-10 words, and should not use clickbait. "
|
||||
"Return ONLY strict JSON with exactly these keys: subject, message, rationale, recommended_offer, "
|
||||
"unknowns, evidence_used, follow_up. subject and message are plain text. rationale is concise. "
|
||||
"recommended_offer is a short summary of the user's offer. unknowns is an array of facts to confirm. "
|
||||
"evidence_used is an array containing only supplied evidence IDs. follow_up is a short human review note, "
|
||||
"not part of the email."
|
||||
)
|
||||
prompt = json.dumps({"evidence": evidence, "user_offer_or_instruction": offer[:2000]}, ensure_ascii=False)
|
||||
prompt = json.dumps({"evidence": evidence, "user_offer_or_instruction": offer[:2000], "tone": tone_key, "goal": goal_key}, ensure_ascii=False)
|
||||
headers = {"Authorization": "Bearer " + ai_api_key} if ai_api_key else {}
|
||||
request_payload: dict[str, Any] = {
|
||||
"model": ai_model,
|
||||
@@ -1880,7 +1900,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
if not row: return self.send_json(404, {"error":"not_found"})
|
||||
payload = self.read_json(); offer = str(payload.get("offer") or "").strip()
|
||||
if not offer: return self.send_json(400, {"error":"offer_required"})
|
||||
draft = ai_generate_message(dict(row), offer)
|
||||
draft = ai_generate_message(dict(row), offer, tone=str(payload.get("tone") or "warm"), goal=str(payload.get("goal") or "conversation"))
|
||||
stamp = now(); conn.execute("UPDATE prospects SET ai_subject=?,ai_message=?,ai_offer=?,ai_reasoning=?,ai_unknowns=?,ai_evidence_ids=?,ai_follow_up=?,ai_checked_at=? WHERE id=?", (draft["subject"],draft["message"],offer,draft["rationale"],json.dumps(draft.get("unknowns") or [], ensure_ascii=False),json.dumps(draft.get("evidence_used") or [], ensure_ascii=False),draft.get("follow_up") or "",stamp,int(message_match.group(1)))); conn.commit()
|
||||
return self.send_json(200, {**draft,"ai_checked_at":stamp,"prospect_id":int(message_match.group(1))})
|
||||
except RuntimeError as exc: return self.send_json(503, {"error":str(exc)})
|
||||
|
||||
Reference in New Issue
Block a user