from __future__ import annotations from core.events.behavior import parse_payload from core.events.manticore import ( get_behavioral_events_for_range, get_behavioral_latest_states, ) from core.events.shadow import ( get_shadow_behavioral_events_for_range, get_shadow_behavioral_latest_states, ) from core.models import Person, User def _behavioral_state_from_kind(kind: str) -> tuple[str, float]: normalized = str(kind or "").strip().lower() if normalized == "presence_unavailable": return ("unavailable", 0.95) if normalized == "composing_abandoned": return ("fading", 0.8) if normalized in { "presence_available", "message_read", "message_delivered", "composing_started", "composing_stopped", "message_sent", }: return ("available", 0.75) return ("unknown", 0.5) def spans_for_range( *, user: User, person: Person, start_ts: int, end_ts: int, service: str = "", limit: int = 200, ): service_filter = str(service or "").strip().lower() try: rows = get_behavioral_events_for_range( user_id=int(user.id), person_id=str(person.id), start_ts=int(start_ts), end_ts=int(end_ts), transport=service_filter, ) except Exception: rows = [] if not rows: rows = get_shadow_behavioral_events_for_range( user=user, person_id=str(person.id), start_ts=int(start_ts), end_ts=int(end_ts), transport=service_filter, ) spans = [] current = None for row in list(rows or []): transport = str(row.get("transport") or "").strip().lower() if service_filter and transport != service_filter: continue ts = int(row.get("ts") or 0) state, confidence = _behavioral_state_from_kind(str(row.get("kind") or "")) if current is None or str(current.get("state")) != state: if current is not None: spans.append(current) current = { "id": 0, "service": transport or service_filter, "state": state, "start_ts": ts, "end_ts": ts, "confidence_start": float(confidence), "confidence_end": float(confidence), "payload": { "source": "manticore_behavioral", "kind": str(row.get("kind") or "").strip().lower(), "raw_payload": parse_payload(row.get("payload")), }, } continue current["end_ts"] = max(int(current.get("end_ts") or 0), ts) current["confidence_end"] = float(confidence) payload = dict(current.get("payload") or {}) payload["kind"] = str(row.get("kind") or "").strip().lower() current["payload"] = payload if current is not None: spans.append(current) return list(reversed(spans[: max(1, min(int(limit or 200), 500))])) def latest_state_for_people(*, user: User, person_ids: list, service: str = "") -> dict: out = {} if not person_ids: return out service_filter = str(service or "").strip().lower() try: rows = get_behavioral_latest_states( user_id=int(user.id), person_ids=[str(value) for value in person_ids], transport=service_filter, ) except Exception: rows = [] if not rows: rows = get_shadow_behavioral_latest_states( user=user, person_ids=[str(value) for value in person_ids], transport=service_filter, ) seen = set() for row in list(rows or []): person_key = str(row.get("person_id") or "").strip() transport = str(row.get("transport") or "").strip().lower() if service_filter and transport != service_filter: continue if not person_key or person_key in seen: continue seen.add(person_key) state, confidence = _behavioral_state_from_kind(str(row.get("kind") or "")) out[person_key] = { "state": state, "confidence": float(confidence), "service": transport or service_filter, "ts": int(row.get("ts") or 0), "source_kind": f"behavioral:{str(row.get('kind') or '').strip().lower()}", } return out