deploy ProspectOS workspace v8 final
This commit is contained in:
+96
-27
@@ -2008,6 +2008,7 @@ def ai_generate_message(prospect: dict[str, Any], offer: str, tone: str = "warm"
|
||||
profile = company_profile_settings()
|
||||
profile_for_prompt = {
|
||||
"name": profile.get("name"),
|
||||
"sender_name": os.environ.get("OWNER_NAME", "").strip()[:160],
|
||||
"website_url": profile.get("website_url"),
|
||||
"description": profile.get("description"),
|
||||
"services": profile.get("services"),
|
||||
@@ -2020,6 +2021,7 @@ def ai_generate_message(prospect: dict[str, Any], offer: str, tone: str = "warm"
|
||||
evidence = {
|
||||
"evidence_ids": evidence_ids,
|
||||
"business_name": prospect.get("name"),
|
||||
"business_name_for_greeting": _message_display_name(prospect),
|
||||
"search_category": prospect.get("search_category") or prospect.get("category"),
|
||||
"address_or_location": prospect.get("address"),
|
||||
"listing_description": prospect.get("description"),
|
||||
@@ -2059,21 +2061,29 @@ def ai_generate_message(prospect: dict[str, Any], offer: str, tone: str = "warm"
|
||||
"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. "
|
||||
"Do not mention Google, Maps, directories, scraping, search results, or how the prospect was found; use the "
|
||||
"supplied facts naturally without narrating the research process. Start with a concrete observation tied to one supplied fact and choose the best client-specific angle from "
|
||||
"client_specific_strategy; do not force the same missing-website angle on every prospect. Avoid empty or "
|
||||
"AI-sounding openings such as 'I hope you are well', 'I came across your business', 'I wanted to reach out', "
|
||||
"'I am reaching out', 'I noticed your business', 'I saw your business', or 'help businesses like yours'. 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 usually 70-130 words. Use one or two natural paragraphs, one specific "
|
||||
"observation, a clear bridge to the exact offer, and one low-pressure call to action. Add a brief respectful "
|
||||
"Do not mention Google, Maps, directories, scraping, search results, or how the prospect was found as a platform; use the "
|
||||
"supplied facts naturally without narrating the research process. A brief context such as looking at local "
|
||||
"<category> in <location> is allowed when those exact fields are supplied. Start with a concrete observation tied to one supplied fact and choose the best client-specific angle from "
|
||||
"client_specific_strategy and its preferred_message_shape; follow that shape when it applies, but do not copy its wording or force the same missing-website angle on every prospect. Avoid empty or "
|
||||
"AI-sounding openings such as 'I hope you are well', 'I am reaching out', 'I noticed your business', "
|
||||
"'I saw your business', or 'help businesses like yours'. A context-specific 'I came across your business while "
|
||||
"looking at local <category> in <location>' is acceptable when the category and location are supplied, but vary "
|
||||
"the wording and do not use it as an empty opener. 'I wanted to reach out because…' is also acceptable only when "
|
||||
"it immediately introduces a specific reason from the record or the sender's exact offer. Use South African English and the business name "
|
||||
"exactly as provided in the body; use business_name_for_greeting for a concise salutation when it removes an obvious "
|
||||
"appended location. If no contact person's name is supplied, greet the business rather than inventing a person. "
|
||||
"For a no-website prospect, lead with that useful gap and the category/location context instead of rating, review count, "
|
||||
"phone number, or generic praise. Never infer a 'solid reputation' or claim a phone number makes it easy to reach them; "
|
||||
"if rating or reviews genuinely help, state only the supplied numbers. Keep the body plain text and usually 120-180 words. "
|
||||
"Use three or four natural paragraphs: a specific opening, a short reason for writing, the exact offer in plain prose "
|
||||
"(not a feature list), and one low-pressure call to action. Add a brief respectful "
|
||||
"opt-out sentence when it fits naturally. 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. "
|
||||
"Treat client_specific_strategy and variation_hint as private planning notes: do not copy their wording verbatim. "
|
||||
"Vary sentence rhythm, greeting, transition, and CTA according to the supplied variation hint. Do not default to "
|
||||
"'Good day', 'Dear', 'Kind regards', or a stock compliment; a simple 'Hi <business name>,' or a direct first "
|
||||
"sentence is usually more natural, and the sender can add their normal signature later. Before returning, "
|
||||
"'Good day' or 'Dear' by default; a simple 'Hi <business name>,' or a direct first sentence is usually more natural. "
|
||||
"Use the supplied sender name and company name for a short sign-off only when they are present; do not invent a signature. Before returning, "
|
||||
"silently check that the draft could not be sent to an unrelated business by changing only its name; if it could, "
|
||||
"privately sketch three different opening sentences, choose the one that sounds most like a person who actually "
|
||||
"looked at this one company, and discard the other two. Then check that the message would still be believable if "
|
||||
@@ -2084,7 +2094,8 @@ def ai_generate_message(prospect: dict[str, Any], offer: str, tone: str = "warm"
|
||||
"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. If a selected promotion is supplied, mention it naturally only when it is active and its exact terms are supplied; never invent an expiry, discount, price, or eligibility rule. "
|
||||
"If a mockup_preview URL is supplied, you may mention it once as an optional concept preview. Use the exact URL, "
|
||||
"If a mockup_preview URL is supplied, you may mention it once as an optional concept preview. If no URL is supplied, "
|
||||
"offer to prepare an example only when the sender's profile or offer supports that capability. Use the exact URL, "
|
||||
"call it a concept/preview rather than a live or finished site, and never imply that the prospect approved it."
|
||||
)
|
||||
prompt = json.dumps({"evidence": evidence, "user_offer_or_instruction": offer[:2000], "tone": tone_key, "goal": goal_key}, ensure_ascii=False)
|
||||
@@ -2110,7 +2121,7 @@ def ai_generate_message(prospect: dict[str, Any], offer: str, tone: str = "warm"
|
||||
return []
|
||||
return [str(item).strip()[:size] for item in value if str(item).strip()][:limit]
|
||||
used = [item for item in text_list(parsed.get("evidence_used"), limit=10, size=80) if item in evidence_ids]
|
||||
message_text = _clean_generated_message(parsed["message"], prospect.get("name"))
|
||||
message_text = _clean_generated_message(parsed["message"], _message_display_name(prospect))
|
||||
mockup_url = str(mockup.get("share_url") or "").strip() if mockup else ""
|
||||
# The link is an explicit part of the requested workflow. If the model
|
||||
# omits it despite receiving the evidence, append a clear editable line.
|
||||
@@ -2130,6 +2141,45 @@ def ai_generate_message(prospect: dict[str, Any], offer: str, tone: str = "warm"
|
||||
}
|
||||
|
||||
|
||||
def _message_display_name(prospect: dict[str, Any]) -> str:
|
||||
"""Return a concise greeting name without an obvious location suffix.
|
||||
|
||||
Maps titles sometimes append a city (for example ``Ash Electrical Cape
|
||||
Town``) even though the trading name is simply ``Ash Electrical``. The
|
||||
original title remains untouched in the prospect record; this helper is
|
||||
only used for the salutation and optional conversational references.
|
||||
"""
|
||||
raw = re.sub(r"\s+", " ", str(prospect.get("name") or "").strip()).strip(" ,-|–—")
|
||||
if not raw:
|
||||
return "Business"
|
||||
location = str(prospect.get("address") or "").strip()
|
||||
suffixes = ["South Africa"]
|
||||
for part in re.split(r"[,|/·]+", location):
|
||||
part = re.sub(r"\s+", " ", part).strip(" ,-|–—")
|
||||
if len(part) >= 3:
|
||||
suffixes.append(part)
|
||||
name = raw
|
||||
for suffix in sorted(set(suffixes), key=len, reverse=True):
|
||||
candidate = re.sub(r"(?:\s*[-–—,:|]\s*|\s+)" + re.escape(suffix) + r"$", "", name, flags=re.I).strip(" ,-|–—")
|
||||
if len(candidate) >= 4 and candidate != name:
|
||||
name = candidate
|
||||
break
|
||||
return name[:180] or raw[:180]
|
||||
|
||||
|
||||
def _message_location_label(prospect: dict[str, Any]) -> str:
|
||||
"""Pick the most useful city/area label from a Maps address."""
|
||||
raw = re.sub(r"\s+", " ", str(prospect.get("address") or "").strip())
|
||||
if not raw:
|
||||
return ""
|
||||
parts = [re.sub(r"\s+", " ", part).strip(" ,-|–—") for part in re.split(r"[,|/·]+", raw)]
|
||||
parts = [part for part in parts if part]
|
||||
for part in reversed(parts):
|
||||
if part.casefold() not in {"south africa", "za", "sa"}:
|
||||
return part[:180]
|
||||
return parts[-1][:180] if parts else ""
|
||||
|
||||
|
||||
def _message_strategy_context(prospect: dict[str, Any], mockup: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
"""Describe the strongest client-specific angle for a draft.
|
||||
|
||||
@@ -2138,6 +2188,7 @@ def _message_strategy_context(prospect: dict[str, Any], mockup: dict[str, Any] |
|
||||
while keeping each suggested angle traceable to a stored field.
|
||||
"""
|
||||
name = str(prospect.get("name") or "").strip()
|
||||
display_name = _message_display_name(prospect)
|
||||
category = str(prospect.get("search_category") or prospect.get("category") or "").strip()
|
||||
website = str(prospect.get("website") or "").strip()
|
||||
status = str(prospect.get("website_status") or "unknown").strip().lower()
|
||||
@@ -2150,7 +2201,8 @@ def _message_strategy_context(prospect: dict[str, Any], mockup: dict[str, Any] |
|
||||
signals.append("the listed location: " + str(prospect.get("address"))[:240])
|
||||
if prospect.get("description") and str(prospect.get("description")).strip().lower() not in {category.lower(), "unknown"}:
|
||||
signals.append("the public listing description: " + str(prospect.get("description"))[:240])
|
||||
if not website or status in {"no_detected_website", "missing"}:
|
||||
no_website = not website or status in {"no_detected_website", "missing"}
|
||||
if no_website:
|
||||
primary = [
|
||||
"No dedicated website is linked publicly for this business; frame that as an optional visibility opportunity, without implying the business is failing.",
|
||||
"The public record does not surface a dedicated web address; offer to make the business easier to find online, without treating that as a defect.",
|
||||
@@ -2208,16 +2260,28 @@ def _message_strategy_context(prospect: dict[str, Any], mockup: dict[str, Any] |
|
||||
]
|
||||
variant_index = variant_index % len(variant_options)
|
||||
variant = variant_options[variant_index]
|
||||
location_label = _message_location_label(prospect)
|
||||
if no_website:
|
||||
if category and location_label:
|
||||
shape = f"Preferred flow: greet {display_name}; say you came across the business while looking at local {category} in {location_label}; mention that a dedicated website was not apparent; give a short reason for writing; present the exact offer in natural prose; offer to prepare a quick example if the sender profile supports website design; sign off briefly and add a respectful opt-out."
|
||||
else:
|
||||
shape = "Preferred flow: greet the business; mention the missing dedicated website in a careful, non-judgemental way; give a short reason for writing; present the exact offer in natural prose; offer a quick example if supported; sign off briefly and add a respectful opt-out."
|
||||
elif status in {"broken", "unreachable", "error", "timeout", "slow", "http_only", "redirect"}:
|
||||
shape = "Preferred flow: lead with the measured website-check signal, explain the reason for writing in one sentence, present only the exact relevant offer, then use a low-pressure CTA and brief sign-off."
|
||||
else:
|
||||
shape = "Preferred flow: acknowledge the existing website without inventing a flaw, use one supplied category or location detail, present a genuinely optional review or improvement, and close without pressure."
|
||||
return {
|
||||
"business_name_for_greeting": display_name,
|
||||
"primary_signal": primary,
|
||||
"available_personalisation": signals[:8],
|
||||
"variation_hint": variant,
|
||||
"greeting_hint": greeting_options[variant_index],
|
||||
"closing_hint": closing_options[variant_index],
|
||||
"preferred_message_shape": shape,
|
||||
"specificity_rule": "Use one or two supplied facts naturally; if the record is sparse, be honest and keep the message short rather than filling the gap with generic praise.",
|
||||
"banned_generic_phrases": [
|
||||
"I hope you are well", "I came across your business", "I wanted to reach out",
|
||||
"I am reaching out", "I noticed your business", "I saw your business", "help businesses like yours", "elevate your digital presence",
|
||||
"I hope you are well", "I am reaching out", "I noticed your business", "I saw your business",
|
||||
"help businesses like yours", "elevate your digital presence",
|
||||
],
|
||||
}
|
||||
|
||||
@@ -2256,8 +2320,6 @@ def _clean_generated_message(value: Any, business_name: str = "") -> str:
|
||||
# the email.
|
||||
leading_patterns = (
|
||||
r"^\s*I\s+hope\s+(?:you(?:'re|\s+are)\s+(?:doing\s+)?well|this\s+email\s+finds\s+you\s+well)\s*[.!?,:-]*\s*",
|
||||
r"^\s*I\s+wanted\s+to\s+reach\s+out(?:\s+to\s+you)?(?:\s+(?:because|as|since))?\s*[,.:;-]*\s*",
|
||||
r"^\s*I\s+came\s+across\s+your\s+business(?:\s+and)?\s*[,.:;-]*\s*",
|
||||
r"^\s*I\s+noticed\s+your\s+business\s*[,.:;-]*\s*",
|
||||
r"^\s*I\s+saw\s+that\s*[,.:;-]*\s*",
|
||||
)
|
||||
@@ -2291,6 +2353,7 @@ def _message_prompt_context(prospect: dict[str, Any], promotion: dict[str, Any]
|
||||
profile = company_profile_settings()
|
||||
profile_for_prompt = {
|
||||
"name": profile.get("name"),
|
||||
"sender_name": os.environ.get("OWNER_NAME", "").strip()[:160],
|
||||
"website_url": profile.get("website_url"),
|
||||
"description": profile.get("description"),
|
||||
"services": profile.get("services"),
|
||||
@@ -2303,6 +2366,7 @@ def _message_prompt_context(prospect: dict[str, Any], promotion: dict[str, Any]
|
||||
evidence = {
|
||||
"evidence_ids": evidence_ids,
|
||||
"business_name": prospect.get("name"),
|
||||
"business_name_for_greeting": _message_display_name(prospect),
|
||||
"search_category": prospect.get("search_category") or prospect.get("category"),
|
||||
"address_or_location": prospect.get("address"),
|
||||
"listing_description": prospect.get("description"),
|
||||
@@ -2386,19 +2450,24 @@ def ai_revise_message(prospect: dict[str, Any], current_subject: str, current_me
|
||||
"testimonial, urgency, authority, eligibility rule, or link. If the requested change needs an unsupported fact, "
|
||||
"leave that claim out and list the missing fact in unknowns. Do not mention scraping, scoring, hidden analysis, "
|
||||
"AI, or this instruction. Do not mention Google, Maps, directories, scraping, search results, or how the prospect "
|
||||
"was found. Do not say you reviewed a website when none was found; if a scan is uncertain or "
|
||||
"was found as a platform. A brief context such as looking at local <category> in <location> is allowed when "
|
||||
"those exact fields are supplied; a specific 'I came across your business while looking at…' opening is acceptable "
|
||||
"when it is followed by a real observation, but it must not become an empty repeated template. Do not say you "
|
||||
"reviewed a website when none was found; if a scan is uncertain or "
|
||||
"failed, say the public check could not confirm it instead of calling the site broken. Do not insult the prospect. "
|
||||
"Use the client_specific_strategy to choose one relevant angle, but treat it as private planning notes and do not "
|
||||
"copy its wording verbatim. Do not force a missing-website pitch when the "
|
||||
"record supports a different angle. Keep South African English and the business name exactly as supplied. Keep "
|
||||
"the body plain text and usually 70-130 words. Use one or two natural paragraphs, one evidence-based "
|
||||
"observation, a clear bridge to the exact offer, and one low-pressure call to action. Add a brief respectful "
|
||||
"Use the client_specific_strategy and preferred_message_shape to choose one relevant angle, but treat them as private planning notes and do not copy their wording verbatim. Do not force a missing-website pitch when the "
|
||||
"record supports a different angle. For a no-website prospect, lead with that gap and the category/location context "
|
||||
"instead of rating, review count, phone number, or generic praise; never infer a solid reputation or claim a phone "
|
||||
"number makes it easy to reach them. Keep South African English and the business name exactly as supplied in the "
|
||||
"body; use business_name_for_greeting for a concise salutation when it removes an obvious appended location. Keep "
|
||||
"the body plain text and usually 120-180 words. Use three or four natural paragraphs: a specific opening, a short "
|
||||
"reason for writing, the exact offer in plain prose, and one low-pressure call to action. Add a brief respectful "
|
||||
"opt-out sentence when it fits naturally. Avoid feature catalogues, fake scarcity, all-caps, excessive emojis, "
|
||||
"and more than one exclamation mark. Keep the subject specific and natural (5-10 words). Vary the greeting, "
|
||||
"sentence rhythm, transition, and CTA using the variation hint. Do not use polished marketing filler such as "
|
||||
"'elevate', 'seamless', 'unlock', 'digital presence', 'transform', or 'leverage'. Do not default to 'Good day', "
|
||||
"'Dear', or 'Kind regards'; a brief 'Hi <business name>,' or a direct opening is more natural, and the sender "
|
||||
"can add their signature later. Preserve any exact "
|
||||
"'elevate', 'seamless', 'unlock', 'digital presence', 'transform', or 'leverage'. Do not default to 'Good day' "
|
||||
"or 'Dear'; a brief 'Hi <business name>,' or a direct opening is more natural. Use the supplied sender name and "
|
||||
"company name for a short sign-off only when present; do not invent a signature. Preserve any exact "
|
||||
"mockup preview URL supplied in the evidence; call it a concept/preview, never a live or finished site, and "
|
||||
"never imply approval. Before returning, silently compare the revised opening with three alternatives and keep the "
|
||||
"one that is most specific to this record and sounds natural when read aloud. Return ONLY strict JSON with exactly these keys: subject, message, rationale, "
|
||||
@@ -2434,7 +2503,7 @@ def ai_revise_message(prospect: dict[str, Any], current_subject: str, current_me
|
||||
return []
|
||||
return [str(item).strip()[:size] for item in value if str(item).strip()][:limit]
|
||||
used = [item for item in text_list(parsed.get("evidence_used"), limit=10, size=80) if item in evidence_ids]
|
||||
message_text = _clean_generated_message(parsed["message"], prospect.get("name"))
|
||||
message_text = _clean_generated_message(parsed["message"], _message_display_name(prospect))
|
||||
mockup_url = str(mockup.get("share_url") or "").strip() if mockup else ""
|
||||
if mockup_url and mockup_url not in message_text:
|
||||
message_text = (message_text.rstrip() + "\n\nI also prepared a concept preview for you: " + mockup_url).strip()[:8_000]
|
||||
|
||||
Reference in New Issue
Block a user