""" 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, )