system-prompts-and-models-o.../salesflow-saas/backend/app/services/intelligence_plane_control.py
Sami Assiri d8bb836614 feat(dealix): ship revenue discovery launch hardening
Add revenue discovery APIs/services, launch verification gates, CI quality checks, and frontend E2E/docs updates to prepare the branch for production go-live.

Made-with: Cursor
2026-04-15 17:51:23 +03:00

142 lines
5.0 KiB
Python

"""Lightweight cache, rate limits, and audit hooks for intelligence endpoints (no Celery required for MVP)."""
from __future__ import annotations
import json
import logging
import os
import threading
import time
from collections import defaultdict
from typing import Any
logger = logging.getLogger("dealix.intelligence_plane")
_LOCK = threading.Lock()
# client_key -> list of unix timestamps (sliding window)
_RATE_BUCKETS: dict[str, list[float]] = defaultdict(list)
# cache key -> (expires_at, payload)
_CACHE: dict[str, tuple[float, Any]] = {}
DEFAULT_WINDOW_SEC = 3600
DEFAULT_MAX_PER_WINDOW = 60
CACHE_TTL_SEC = 120
def _now() -> float:
return time.time()
def _client_key(forwarded: str | None, fallback: str) -> str:
if forwarded:
return forwarded.split(",")[0].strip() or fallback
return fallback
def check_rate_limit(
*,
client_ip: str,
x_forwarded_for: str | None,
tenant_id: str | None = None,
max_per_window: int | None = None,
window_sec: int | None = None,
) -> tuple[bool, str]:
"""
Returns (allowed, reason). Uses tenant_id when provided, else client IP.
"""
max_n = max_per_window or int(os.getenv("DEALIX_INTEL_RATE_LIMIT", str(DEFAULT_MAX_PER_WINDOW)))
win = float(window_sec or os.getenv("DEALIX_INTEL_RATE_WINDOW_SEC", str(DEFAULT_WINDOW_SEC)))
key = f"t:{tenant_id}" if tenant_id else f"ip:{_client_key(x_forwarded_for, client_ip)}"
t = _now()
with _LOCK:
bucket = _RATE_BUCKETS[key]
bucket[:] = [x for x in bucket if t - x < win]
if len(bucket) >= max_n:
return False, f"rate_limited:{key}"
bucket.append(t)
return True, "ok"
def cache_get(key: str) -> Any | None:
with _LOCK:
row = _CACHE.get(key)
if not row:
return None
exp, payload = row
if exp < _now():
del _CACHE[key]
return None
return payload
def cache_set(key: str, payload: Any, ttl_sec: int | None = None) -> None:
ttl = float(ttl_sec or os.getenv("DEALIX_INTEL_CACHE_TTL_SEC", str(CACHE_TTL_SEC)))
with _LOCK:
_CACHE[key] = (_now() + ttl, payload)
def audit_ai_decision(
*,
operation: str,
tenant_id: str | None,
model_id: str | None,
user_id: str | None = None,
extra: dict[str, Any] | None = None,
) -> None:
"""Structured log line for later SIEM / golden-set review (no PII in values)."""
payload = {
"op": operation,
"tenant_id": str(tenant_id) if tenant_id else None,
"user_id": str(user_id) if user_id else None,
"model_id": model_id,
**(extra or {}),
}
logger.info("ai_audit %s", json.dumps(payload, ensure_ascii=False))
def deep_enrich_enabled_for_tenant(tenant_id: str | None) -> bool:
"""Optional heavy enrichment; default off unless DEALIX_DEEP_ENRICH_DEFAULT or allow-list."""
if os.getenv("DEALIX_DEEP_ENRICH_DEFAULT", "").lower() in ("1", "true", "yes"):
return True
raw = os.getenv("DEALIX_DEEP_ENRICH_TENANTS", "")
if not tenant_id or not raw.strip():
return False
allow = {x.strip() for x in raw.split(",") if x.strip()}
return str(tenant_id) in allow
def intelligence_feature_snapshot(*, tenant_id: str | None) -> dict[str, Any]:
"""Effective flags for workspace / ops (no secrets). Prefer JWT tenant over spoofed headers."""
tavily = bool((os.getenv("TAVILY_API_KEY") or "").strip())
allow_search_env = os.getenv("DEALIX_ALLOW_LICENSED_SEARCH", "true").lower() not in ("0", "false", "no")
deep_default = os.getenv("DEALIX_DEEP_ENRICH_DEFAULT", "").lower() in ("1", "true", "yes")
deep_tenants = os.getenv("DEALIX_DEEP_ENRICH_TENANTS", "").strip()
deep_list = {x.strip() for x in deep_tenants.split(",") if x.strip()}
deep_for_tenant = deep_default or (bool(tenant_id) and str(tenant_id) in deep_list)
tavily_allow = tenant_may_use_licensed_search(tenant_id)
return {
"licensed_web_search_configured": tavily,
"licensed_web_search_allowed": tavily and allow_search_env and tavily_allow,
"deep_enrichment_enabled": deep_for_tenant,
"intel_rate_limit_per_window": int(os.getenv("DEALIX_INTEL_RATE_LIMIT", str(DEFAULT_MAX_PER_WINDOW))),
"intel_cache_ttl_sec": int(float(os.getenv("DEALIX_INTEL_CACHE_TTL_SEC", str(CACHE_TTL_SEC)))),
"enrich_idempotent_daily": os.getenv("DEALIX_ENRICH_IDEMPOTENT_DAILY", "true").lower()
not in ("0", "false", "no"),
"async_enrich_jobs_enabled": os.getenv("DEALIX_ASYNC_ENRICH_JOBS", "true").lower()
not in ("0", "false", "no"),
}
def tenant_may_use_licensed_search(tenant_id: str | None) -> bool:
"""
Optional allow-list for Tavily-class search. Empty env → any tenant (or anonymous) may use search if keys exist.
"""
raw = os.getenv("DEALIX_TAVILY_TENANT_ALLOWLIST", "").strip()
if not raw:
return True
if not tenant_id:
return True
allow = {x.strip() for x in raw.split(",") if x.strip()}
return str(tenant_id) in allow