Matrix intake: fuzzy investor matching + conversational in-thread edits (v0.1.0:86)
Close the two locked post-deploy enhancements for the Matrix intake bot.
Fuzzy matching (server-side, ships in the s9pk): new find_intake_candidates in
server.py returns ranked deterministic near-matches (difflib name similarity +
token-set Jaccard, legal-suffix-aware, + email Levenshtein <= 2); GET
/api/intake/match now returns {match, candidates}. The bot surfaces a numbered
shortlist so a near-duplicate (Charlie/Charles, Acme Capital vs Acme Capital LLC,
a one-char email typo) is confirmed by a human instead of silently creating a
second investor. Exact match still auto-attaches; fuzzy candidates are never
auto-attached. The optional LLM-judge re-rank is deferred.
Conversational edits (bot-side, ships on the Spark): any in-thread reply that
isn't yes/no/edit field=value is treated as a natural-language revision and
re-run through local Qwen (parse.revise). Email integrity is preserved -- a
changed address must literally appear in the instruction; the model's email
field is structurally unreachable. No-op revisions re-prompt.
Docs/current-state brought current; 27/27 backend tests green.
This commit is contained in:
@@ -58,6 +58,61 @@ def test_subject_blank_when_note_present_else_provenance_label():
|
||||
assert no_note["subject"] == "Intake (Matrix)"
|
||||
|
||||
|
||||
def _with_stub_authed(reply, capture=None):
|
||||
"""Swap crm_client._authed for a canned (status, data); return a restorer."""
|
||||
orig = crm_client._authed
|
||||
|
||||
def fake(method, path, body=None):
|
||||
if capture is not None:
|
||||
capture["path"] = path
|
||||
return reply
|
||||
|
||||
crm_client._authed = fake
|
||||
return orig
|
||||
|
||||
|
||||
def test_match_parses_exact_match():
|
||||
cap = {}
|
||||
orig = _with_stub_authed((200, {"data": {
|
||||
"match": {"id": "rowAcme", "investor_name": "Acme Capital", "matched_on": "name"},
|
||||
"candidates": [],
|
||||
}}), cap)
|
||||
try:
|
||||
res = crm_client.match({"investor_name": "Acme Capital", "contact_email": ""})
|
||||
finally:
|
||||
crm_client._authed = orig
|
||||
assert res["match"] == {"id": "rowAcme", "name": "Acme Capital"}
|
||||
assert res["candidates"] == []
|
||||
assert "q=Acme" in cap["path"] # the query was forwarded
|
||||
|
||||
|
||||
def test_match_returns_ranked_candidates_when_no_exact():
|
||||
orig = _with_stub_authed((200, {"data": {"match": None, "candidates": [
|
||||
{"id": "rowCharlie", "investor_name": "Charlie Brown", "score": 0.92, "matched_on": "name"},
|
||||
{"id": "rowBeta", "investor_name": "Beta Capital LLC", "score": 0.86, "matched_on": "name"},
|
||||
]}}))
|
||||
try:
|
||||
res = crm_client.match({"investor_name": "Charles Brown"})
|
||||
finally:
|
||||
crm_client._authed = orig
|
||||
assert res["match"] is None
|
||||
assert [c["id"] for c in res["candidates"]] == ["rowCharlie", "rowBeta"]
|
||||
assert res["candidates"][0]["name"] == "Charlie Brown"
|
||||
assert res["candidates"][0]["matched_on"] == "name"
|
||||
|
||||
|
||||
def test_match_no_query_skips_network():
|
||||
def boom(*a, **k):
|
||||
raise AssertionError("should not hit the network when there's nothing to match on")
|
||||
orig = crm_client._authed
|
||||
crm_client._authed = boom
|
||||
try:
|
||||
res = crm_client.match({"investor_name": None, "contact_name": None, "contact_email": None})
|
||||
finally:
|
||||
crm_client._authed = orig
|
||||
assert res == {"match": None, "candidates": []}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
fns = [v for k, v in sorted(globals().items()) if k.startswith("test_") and callable(v)]
|
||||
for fn in fns:
|
||||
|
||||
Reference in New Issue
Block a user