Capture phone (office) + mobile (cell) on card intake; ship v0.1.0:98
Completes business-card contact capture. The transcription prompt now labels Phone/Mobile/Fax on separate lines, and the extractor maps an office/main number -> phone and a cell -> mobile, never a fax. Both carry the same digit-in-source integrity rule as email/LinkedIn: a number is kept only if its digits literally appear in the source (or, on revise, the instruction) -- never minted. The proposal card shows Phone + Mobile and they're editable (aliases phone/tel/office, mobile/cell). Server: _upsert_contact_from_fundraising now accepts contact.phone + contact.mobile and writes them to the canonical contact record (contact-level, not grid pills), shipped in s9pk v0.1.0:98. No schema change -- the contacts columns already exist. 41/41 backend suite green + the matrix_intake units; card flow end-to-end is live-smoke.
This commit is contained in:
@@ -26,6 +26,9 @@ SYSTEM = (
|
||||
' "contact_title": the person\'s role/title if stated, else null.\n'
|
||||
' "city": the person\'s city or location if stated (e.g. "New York"), else null.\n'
|
||||
' "linkedin_url": the person\'s LinkedIn URL if explicitly present, else null. Never invent one.\n'
|
||||
' "phone": the office/main/direct phone number if present (a line labeled Phone/Tel/Office/'
|
||||
'Direct, or a single unlabeled number); never a fax or a cell. Else null.\n'
|
||||
' "mobile": the cell/mobile number if present (a line labeled Cell/Mobile); never a fax. Else null.\n'
|
||||
' "note": any meeting note, context, or next step, else null.\n'
|
||||
"Use null (not empty string) for anything not present."
|
||||
)
|
||||
@@ -56,7 +59,12 @@ _EMAIL_RE = re.compile(r"[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}")
|
||||
_LINKEDIN_RE = re.compile(r"(?:https?://)?(?:[a-z]{2,3}\.)?linkedin\.com/[A-Za-z0-9_%/\-.]+", re.I)
|
||||
_VALID_INTENTS = {"new_investor", "meeting_note", "unclear"}
|
||||
_FIELDS = ("intent", "investor_name", "contact_name", "contact_email", "contact_title",
|
||||
"city", "linkedin_url", "note")
|
||||
"city", "linkedin_url", "phone", "mobile", "note")
|
||||
|
||||
|
||||
def _digits(s):
|
||||
"""Bare digit run of a string (drops spaces/dashes/parens/dots), for phone-integrity checks."""
|
||||
return re.sub(r"\D", "", str(s or ""))
|
||||
|
||||
|
||||
def _clean(v):
|
||||
@@ -89,6 +97,15 @@ def normalize(raw, source_text=""):
|
||||
lm = _LINKEDIN_RE.search(source_text or "")
|
||||
out["linkedin_url"] = lm.group(0).rstrip(".,;:!?)]}>\"'") if lm else None
|
||||
|
||||
# Phone integrity: keep a number (in its printed formatting) only if its digit run actually
|
||||
# appears in the source — the model must never mint or "complete" a number. phone = the
|
||||
# office/main line, mobile = the cell; which is which is the model's call (prompted), this
|
||||
# only validates that the number is real. (≥7 digits avoids matching a stray short run.)
|
||||
src_digits = _digits(source_text)
|
||||
for f in ("phone", "mobile"):
|
||||
d = _digits(out.get(f))
|
||||
out[f] = out.get(f) if (len(d) >= 7 and d in src_digits) else None
|
||||
|
||||
# City is left as a plain extracted field (no source gate): a wrong city is low-harm and the
|
||||
# human sees it on the card before approving, unlike a wrong email/LinkedIn.
|
||||
|
||||
@@ -114,10 +131,10 @@ REVISE_SYSTEM = (
|
||||
"team member typed. You are given the CURRENT proposal as JSON and an INSTRUCTION. Apply "
|
||||
"the instruction and reply with ONLY the full revised JSON object, these keys:\n"
|
||||
' "investor_name", "contact_name", "contact_email", "contact_title", "city", '
|
||||
'"linkedin_url", "note".\n'
|
||||
'"linkedin_url", "phone", "mobile", "note".\n'
|
||||
"Change ONLY what the instruction asks; copy every other field through unchanged. Use null "
|
||||
"for a field the instruction clears or that is genuinely absent. Never invent an email "
|
||||
"address or a LinkedIn URL."
|
||||
"address, a LinkedIn URL, or a phone number."
|
||||
)
|
||||
|
||||
_REVISABLE = ("investor_name", "contact_name", "contact_title", "city", "note")
|
||||
@@ -143,6 +160,14 @@ def _apply_revision(proposal, model_out, instruction):
|
||||
lm = _LINKEDIN_RE.search(instruction or "")
|
||||
if lm:
|
||||
out["linkedin_url"] = lm.group(0).rstrip(".,;:!?)]}>\"'")
|
||||
# Phone/mobile too: a revised number is accepted only if its digits appear in the instruction
|
||||
# (never let the model mint one); otherwise the existing value is kept.
|
||||
instr_digits = _digits(instruction)
|
||||
for f in ("phone", "mobile"):
|
||||
if f in model_out:
|
||||
cand = _clean(model_out.get(f))
|
||||
d = _digits(cand)
|
||||
out[f] = cand if (cand and len(d) >= 7 and d in instr_digits) else out.get(f)
|
||||
# Don't let a revision strip the proposal down to nothing actionable.
|
||||
if not out.get("investor_name") and not out.get("contact_name"):
|
||||
out["investor_name"] = proposal.get("investor_name")
|
||||
|
||||
Reference in New Issue
Block a user