137 lines
4.4 KiB
Python
137 lines
4.4 KiB
Python
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
|