forked from sagnik/Velocity-OS
Initial commit: Velocity-OS migration
This commit is contained in:
217
core/services/services/auto_mode_matcher.py
Normal file
217
core/services/services/auto_mode_matcher.py
Normal file
@@ -0,0 +1,217 @@
|
||||
"""
|
||||
backend/services/auto_mode_matcher.py - Post-session lead matching for Sentinel auto mode.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
import asyncpg
|
||||
|
||||
|
||||
@dataclass
|
||||
class AutoModeMatchResult:
|
||||
action: str
|
||||
lead_id: str
|
||||
confidence: float
|
||||
rationale: str
|
||||
tags_applied: list[str]
|
||||
|
||||
|
||||
def _normalise_plate(plate: str | None) -> str | None:
|
||||
if not plate:
|
||||
return None
|
||||
cleaned = "".join(ch for ch in plate.upper() if ch.isalnum())
|
||||
return cleaned or None
|
||||
|
||||
|
||||
async def _find_match_by_plate(
|
||||
conn: asyncpg.Connection,
|
||||
normalized_plate: str,
|
||||
) -> tuple[str, float, str] | None:
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
SELECT linked_lead_id::text AS lead_id
|
||||
FROM cctv_events
|
||||
WHERE regexp_replace(COALESCE(license_plate, ''), '[^A-Za-z0-9]', '', 'g') = $1
|
||||
AND linked_lead_id IS NOT NULL
|
||||
ORDER BY captured_at DESC
|
||||
LIMIT 1
|
||||
""",
|
||||
normalized_plate,
|
||||
)
|
||||
if row:
|
||||
return row["lead_id"], 0.96, "matched_existing_plate"
|
||||
return None
|
||||
|
||||
|
||||
async def _find_match_by_tags(
|
||||
conn: asyncpg.Connection,
|
||||
tags: list[str],
|
||||
) -> tuple[str, float, str] | None:
|
||||
if not tags:
|
||||
return None
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
SELECT id::text AS lead_id,
|
||||
COALESCE(cardinality(tags & $1::text[]), 0) AS overlap,
|
||||
last_active
|
||||
FROM leads_intelligence
|
||||
WHERE tags && $1::text[]
|
||||
AND status IN ('engaged', 'qualified', 'hot')
|
||||
ORDER BY overlap DESC, last_active DESC
|
||||
LIMIT 1
|
||||
""",
|
||||
tags,
|
||||
)
|
||||
if row and row["overlap"] > 0:
|
||||
confidence = min(0.65 + (0.1 * int(row["overlap"])), 0.85)
|
||||
return row["lead_id"], confidence, "matched_tag_overlap"
|
||||
return None
|
||||
|
||||
|
||||
async def _create_auto_lead(
|
||||
conn: asyncpg.Connection,
|
||||
*,
|
||||
wealth_indicator: str,
|
||||
tags: list[str],
|
||||
session_id: str,
|
||||
) -> str:
|
||||
name = f"Auto Visitor {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M')}"
|
||||
qualification = "whale" if wealth_indicator == "HNI" else "potential"
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
INSERT INTO leads_intelligence
|
||||
(name, source, status, qualification, quantum_dynamics_score, tags, last_message)
|
||||
VALUES
|
||||
($1, 'walkin', 'new', $2::qualification_enum, 50, $3::text[], $4)
|
||||
RETURNING id::text
|
||||
""",
|
||||
name,
|
||||
qualification,
|
||||
tags,
|
||||
f"Auto-created from Sentinel auto mode session {session_id}",
|
||||
)
|
||||
return row["id"]
|
||||
|
||||
|
||||
async def auto_mode_match_session(
|
||||
conn: asyncpg.Connection,
|
||||
*,
|
||||
session_id: str,
|
||||
) -> AutoModeMatchResult:
|
||||
session = await conn.fetchrow(
|
||||
"""
|
||||
SELECT id::text, lead_id::text, session_mode, auto_mode_evidence, final_qd_score
|
||||
FROM perception_sessions
|
||||
WHERE id = $1::uuid
|
||||
""",
|
||||
session_id,
|
||||
)
|
||||
if not session:
|
||||
raise ValueError(f"Session {session_id} not found.")
|
||||
if session["session_mode"] != "auto":
|
||||
raise ValueError("auto_mode_match_session can only be used for auto sessions.")
|
||||
if session["lead_id"]:
|
||||
return AutoModeMatchResult(
|
||||
action="already_linked",
|
||||
lead_id=session["lead_id"],
|
||||
confidence=1.0,
|
||||
rationale="session_already_has_lead",
|
||||
tags_applied=[],
|
||||
)
|
||||
|
||||
evidence: dict[str, Any] = dict(session["auto_mode_evidence"] or {})
|
||||
normalized_plate = _normalise_plate(evidence.get("license_plate"))
|
||||
inferred_tags = list(dict.fromkeys((evidence.get("tags") or []) + (evidence.get("nemoclaw_tags") or [])))
|
||||
wealth_indicator = str(evidence.get("wealth_indicator") or "unknown")
|
||||
|
||||
match: tuple[str, float, str] | None = None
|
||||
if normalized_plate:
|
||||
match = await _find_match_by_plate(conn, normalized_plate)
|
||||
if not match:
|
||||
match = await _find_match_by_tags(conn, inferred_tags)
|
||||
|
||||
action = "linked_existing" if match else "created_new"
|
||||
if match:
|
||||
lead_id, confidence, rationale = match
|
||||
else:
|
||||
lead_id = await _create_auto_lead(
|
||||
conn,
|
||||
wealth_indicator=wealth_indicator,
|
||||
tags=inferred_tags,
|
||||
session_id=session_id,
|
||||
)
|
||||
confidence = 0.55
|
||||
rationale = "created_new_from_auto_mode"
|
||||
|
||||
await conn.execute(
|
||||
"""
|
||||
UPDATE perception_sessions
|
||||
SET lead_id = $1::uuid,
|
||||
auto_mode_matched_at = NOW(),
|
||||
auto_mode_evidence = auto_mode_evidence || $2::jsonb
|
||||
WHERE id = $3::uuid
|
||||
""",
|
||||
lead_id,
|
||||
{
|
||||
"match_action": action,
|
||||
"match_confidence": confidence,
|
||||
"match_rationale": rationale,
|
||||
},
|
||||
session_id,
|
||||
)
|
||||
|
||||
await conn.execute(
|
||||
"""
|
||||
UPDATE cctv_events
|
||||
SET linked_lead_id = $1::uuid
|
||||
WHERE linked_session_id = $2::uuid
|
||||
AND linked_lead_id IS NULL
|
||||
""",
|
||||
lead_id,
|
||||
session_id,
|
||||
)
|
||||
|
||||
if inferred_tags:
|
||||
await conn.execute(
|
||||
"""
|
||||
UPDATE leads_intelligence
|
||||
SET tags = ARRAY(
|
||||
SELECT DISTINCT unnest(COALESCE(tags, ARRAY[]::text[]) || $1::text[])
|
||||
),
|
||||
quantum_dynamics_score = COALESCE($2, quantum_dynamics_score),
|
||||
updated_at = NOW()
|
||||
WHERE id = $3::uuid
|
||||
""",
|
||||
inferred_tags,
|
||||
session["final_qd_score"],
|
||||
lead_id,
|
||||
)
|
||||
|
||||
await conn.execute(
|
||||
"""
|
||||
INSERT INTO omnichannel_logs (event_type, lead_id, payload)
|
||||
VALUES ('LEAD_TAGGED', $1::uuid, $2::jsonb)
|
||||
""",
|
||||
lead_id,
|
||||
{
|
||||
"source": "auto_mode_matcher",
|
||||
"session_id": session_id,
|
||||
"tags_added": inferred_tags,
|
||||
"match_action": action,
|
||||
"match_confidence": confidence,
|
||||
"match_rationale": rationale,
|
||||
},
|
||||
)
|
||||
|
||||
return AutoModeMatchResult(
|
||||
action=action,
|
||||
lead_id=lead_id,
|
||||
confidence=confidence,
|
||||
rationale=rationale,
|
||||
tags_applied=inferred_tags,
|
||||
)
|
||||
Reference in New Issue
Block a user