from __future__ import annotations import hashlib import json import os from typing import Any from urllib import error, request from services.constants import RISK_LEVELS DNA_ALPHABET = set("ACGTN") PROTEIN_ALPHABET = set("ABCDEFGHIKLMNPQRSTVWXYZ*") CATEGORY_BANK = { "DNA": { "SAFE": ( "Routine metabolic gene signature", "Common structural cassette", "Low-concern regulatory context", ), "REVIEW": ( "Ambiguous host-interaction signal", "Regulatory activity worth analyst review", "Unresolved functional control pattern", ), "HIGH": ( "Elevated host-interaction signature", "Escalation-priority functional signal", "High-concern regulation-linked pattern", ), }, "PROTEIN": { "SAFE": ( "Routine enzyme-like profile", "Low-concern scaffold signature", "Common cellular maintenance pattern", ), "REVIEW": ( "Ambiguous membrane-associated profile", "Unresolved signaling-like pattern", "Review-level interaction motif cluster", ), "HIGH": ( "Elevated interaction-associated profile", "Escalation-priority effector-like pattern", "High-concern modulation signature", ), }, } def _clamp(value: float, lower: float = 0.0, upper: float = 1.0) -> float: return max(lower, min(upper, value)) def _normalize_sequence(sequence: str) -> str: return "".join(sequence.split()).upper() def _hash_unit(*parts: str) -> float: digest = hashlib.sha256("|".join(parts).encode("utf-8")).hexdigest() return int(digest[:8], 16) / 0xFFFFFFFF def _longest_run(sequence: str) -> int: if not sequence: return 0 longest = 1 current = 1 for index in range(1, len(sequence)): if sequence[index] == sequence[index - 1]: current += 1 longest = max(longest, current) else: current = 1 return longest def _error_response(model_name: str, error_text: str, data_source: str = "unknown") -> dict[str, Any]: return { "ok": False, "hazard_score": None, "risk_level": None, "confidence": None, "category": None, "explanation": None, "baseline_result": None, "model_name": model_name, "error": error_text, "data_source": data_source, } def _validate_sequence(sequence: str, seq_type: str) -> tuple[str | None, str | None]: normalized = _normalize_sequence(sequence) if not normalized: return None, "empty_sequence_input" if seq_type not in {"DNA", "PROTEIN"}: return None, "unsupported_sequence_type" if len(normalized) > 50_000: return None, ( "sequence_too_long: Sequences over 50 kb exceed this prototype's scope. " "Please split into smaller chunks and screen each separately." ) allowed = DNA_ALPHABET if seq_type == "DNA" else PROTEIN_ALPHABET invalid = sorted({character for character in normalized if character not in allowed}) if invalid: return None, f"invalid_{seq_type.lower()}_characters:{''.join(invalid[:8])}" return normalized, None def _risk_level_from_score(score: float) -> str: if score >= 0.72: return "HIGH" if score >= 0.42: return "REVIEW" return "SAFE" def _pick_category(seq_type: str, risk_level: str, sequence: str) -> str: bank = CATEGORY_BANK[seq_type][risk_level] index = int(_hash_unit(seq_type, risk_level, sequence) * len(bank)) % len(bank) return bank[index] def _baseline_result(risk_level: str) -> str: if risk_level == "SAFE": return "Heuristic baseline: routine profile, no alert." if risk_level == "REVIEW": return "Heuristic baseline: partial signal overlap, manual review recommended." return "Heuristic baseline: inconclusive; function-aware adapter retained a high-priority flag." def _offline_explanation(seq_type: str, risk_level: str) -> str: display_type = "DNA" if seq_type == "DNA" else "protein" if risk_level == "SAFE": return f"BioLens heuristic {display_type} screening found a low-concern functional profile with no elevated review signals." if risk_level == "REVIEW": return f"BioLens heuristic {display_type} screening found ambiguous functional cues — manual analyst review recommended." return f"BioLens heuristic {display_type} screening found multiple elevated functional cues — this case should be escalated." def _screen_offline(sequence: str, seq_type: str, model_name: str, data_source: str = "biolens-offline") -> dict[str, Any]: length = len(sequence) hash_factor = _hash_unit(sequence, seq_type) if seq_type == "DNA": gc_fraction = (sequence.count("G") + sequence.count("C")) / length unknown_fraction = sequence.count("N") / length motif_hits = sum(sequence.count(motif) for motif in ("ATG", "TATA", "CGCG", "GGG")) repeat_factor = min(max(_longest_run(sequence) - 3, 0) * 0.018, 0.18) score = _clamp( 0.12 + min(length / 1800, 0.22) + abs(gc_fraction - 0.5) * 0.30 + min(motif_hits * 0.028, 0.18) + repeat_factor + min(unknown_fraction * 0.45, 0.12) + hash_factor * 0.15 ) confidence = _clamp(0.58 + min(length / 900, 0.20) + (1 - abs(gc_fraction - 0.5)) * 0.15 + hash_factor * 0.07) pathogenicity = _clamp(0.2 + abs(gc_fraction - 0.5) * 0.4 + min(motif_hits * 0.05, 0.3)) evasion_potential = _clamp(0.1 + repeat_factor * 2 + hash_factor * 0.2) synthesis_feasibility = _clamp(0.9 - min(length / 5000, 0.8)) environmental_resilience = _clamp(0.3 + gc_fraction * 0.4) host_range = _clamp(0.4 + hash_factor * 0.3) attr_positions = [i for i, c in enumerate(sequence) if c in "GC" and i % 7 == 0] attr_scores = [round(_clamp(0.4 + hash_factor * 0.6), 3) for _ in attr_positions] regions = [{"start": 0, "end": min(30, length), "label": "GC-rich motif region", "score": round(pathogenicity, 3)}] else: hydrophobic_fraction = sum(character in "AVILMFWYC" for character in sequence) / length charged_fraction = sum(character in "KRDEH" for character in sequence) / length motif_hits = sum(sequence.count(motif) for motif in ("KK", "RR", "KR", "GP", "GG")) low_complexity = 1 - (len(set(sequence)) / max(1, min(20, length))) repeat_factor = min(max(_longest_run(sequence) - 2, 0) * 0.022, 0.20) score = _clamp( 0.14 + min(length / 1200, 0.20) + abs(hydrophobic_fraction - 0.34) * 0.38 + abs(charged_fraction - 0.22) * 0.24 + min(motif_hits * 0.024, 0.16) + min(low_complexity * 0.22, 0.18) + repeat_factor + hash_factor * 0.15 ) confidence = _clamp( 0.56 + min(length / 700, 0.20) + (1 - abs(charged_fraction - 0.22)) * 0.12 + hash_factor * 0.08 ) pathogenicity = _clamp(0.3 + abs(charged_fraction - 0.2) * 0.5 + min(motif_hits * 0.05, 0.3)) evasion_potential = _clamp(0.2 + low_complexity * 0.4 + hash_factor * 0.2) synthesis_feasibility = _clamp(0.8 - min(length / 2000, 0.7)) environmental_resilience = _clamp(0.2 + hydrophobic_fraction * 0.5) host_range = _clamp(0.5 + hash_factor * 0.2) attr_positions = [i for i, c in enumerate(sequence) if c in "KRDEH" and i % 3 == 0] attr_scores = [round(_clamp(0.5 + hash_factor * 0.5), 3) for _ in attr_positions] regions = [{"start": 0, "end": min(20, length), "label": "Charged cluster", "score": round(pathogenicity, 3)}] risk_level = _risk_level_from_score(score) category = _pick_category(seq_type, risk_level, sequence) return { "ok": True, "hazard_score": round(score, 3), "risk_level": risk_level, "confidence": round(confidence, 3), "category": category, "explanation": _offline_explanation(seq_type, risk_level), "baseline_result": _baseline_result(risk_level), "model_name": model_name, "error": None, "data_source": "biolens-offline", "threat_breakdown": { "pathogenicity": round(pathogenicity, 3), "evasion_potential": round(evasion_potential, 3), "synthesis_feasibility": round(synthesis_feasibility, 3), "environmental_resilience": round(environmental_resilience, 3), "host_range": round(host_range, 3), }, "attribution_data": { "positions": attr_positions, "scores": attr_scores, "regions": regions, } } def _coerce_integrated_response(payload: dict[str, Any], fallback_model_name: str) -> dict[str, Any]: try: if not payload.get("ok"): return _error_response( str(payload.get("model_name") or fallback_model_name), str(payload.get("error") or "integration_failure"), data_source="synthguard-api", ) risk_level = str(payload.get("risk_level")) if risk_level not in RISK_LEVELS: raise ValueError("invalid_risk_level") hazard_score = _clamp(float(payload.get("hazard_score"))) confidence = _clamp(float(payload.get("confidence"))) return { "ok": True, "hazard_score": round(hazard_score, 3), "risk_level": risk_level, "confidence": round(confidence, 3), "category": str(payload.get("category") or "Unspecified"), "explanation": str(payload.get("explanation") or "No explanation provided."), "baseline_result": str(payload.get("baseline_result")) if payload.get("baseline_result") else None, "model_name": str(payload.get("model_name") or fallback_model_name), "error": None, "data_source": "synthguard-api", "threat_breakdown": payload.get("threat_breakdown"), "attribution_data": payload.get("attribution_data"), } except (TypeError, ValueError) as exc: return _error_response(fallback_model_name, f"invalid_integrated_response:{exc}", data_source="synthguard-api") _DEFAULT_ENDPOINT = "https://seyomi-synthguard-api.hf.space/biolens/screen" def get_base_url() -> str: """Derive the base API URL from the configured endpoint.""" endpoint = os.getenv("SYNTHSCREEN_ENDPOINT", _DEFAULT_ENDPOINT) for suffix in ("/biolens/screen", "/screen", "/protein/screen"): if endpoint.endswith(suffix): return endpoint[: -len(suffix)] return endpoint.rstrip("/") def get_api_health() -> dict[str, Any]: """GET /health""" url = f"{get_base_url()}/health" try: req = request.Request(url, method="GET") with request.urlopen(req, timeout=5) as resp: return {"ok": resp.status == 200, "status": resp.status} except Exception as exc: return {"ok": False, "error": str(exc)} def screen_dna_direct(sequence: str) -> dict[str, Any]: """POST /screen""" url = f"{get_base_url()}/screen" payload = json.dumps({"sequence": sequence}).encode("utf-8") req = request.Request(url, data=payload, headers={"Content-Type": "application/json"}, method="POST") try: with request.urlopen(req, timeout=30) as resp: return json.loads(resp.read().decode("utf-8")) except Exception as exc: return {"ok": False, "error": str(exc)} def screen_protein_direct(sequence: str) -> dict[str, Any]: """POST /protein/screen""" url = f"{get_base_url()}/protein/screen" payload = json.dumps({"sequence": sequence}).encode("utf-8") req = request.Request(url, data=payload, headers={"Content-Type": "application/json"}, method="POST") try: with request.urlopen(req, timeout=30) as resp: return json.loads(resp.read().decode("utf-8")) except Exception as exc: return {"ok": False, "error": str(exc)} def screen_batch(sequences: list[str]) -> dict[str, Any]: """POST /screen/batch""" url = f"{get_base_url()}/screen/batch" payload = json.dumps({"sequences": sequences}).encode("utf-8") req = request.Request(url, data=payload, headers={"Content-Type": "application/json"}, method="POST") try: with request.urlopen(req, timeout=60) as resp: return json.loads(resp.read().decode("utf-8")) except Exception as exc: return {"ok": False, "error": str(exc)} def submit_split_order(sequences: list[str]) -> dict[str, Any]: """POST /split/submit""" url = f"{get_base_url()}/split/submit" payload = json.dumps({"sequences": sequences}).encode("utf-8") req = request.Request(url, data=payload, headers={"Content-Type": "application/json"}, method="POST") try: with request.urlopen(req, timeout=60) as resp: return json.loads(resp.read().decode("utf-8")) except Exception as exc: return {"ok": False, "error": str(exc)} def get_model_info() -> dict[str, Any]: """GET /model/info""" url = f"{get_base_url()}/model/info" try: req = request.Request(url, method="GET") with request.urlopen(req, timeout=10) as resp: return json.loads(resp.read().decode("utf-8")) except Exception as exc: return {"ok": False, "error": str(exc)} def _screen_online(sequence: str, seq_type: str, model_name: str) -> dict[str, Any]: """Call the SynthGuard /biolens/screen endpoint for DNA or PROTEIN sequences.""" endpoint = f"{get_base_url()}/biolens/screen" payload = json.dumps({"sequence": sequence, "seq_type": seq_type}).encode("utf-8") timeout_seconds = float(os.getenv("SYNTHSCREEN_TIMEOUT_SECONDS", "15")) request_object = request.Request( endpoint, data=payload, headers={"Content-Type": "application/json"}, method="POST", ) try: with request.urlopen(request_object, timeout=timeout_seconds) as response: body = response.read().decode("utf-8") return _coerce_integrated_response(json.loads(body), model_name) except error.HTTPError as exc: return _error_response(model_name, f"api_http_error:{exc.code}", data_source="synthguard-api") except error.URLError as exc: if isinstance(exc.reason, TimeoutError) or "timed out" in str(exc.reason).lower(): return _error_response( model_name, "api_timeout:The SynthGuard API is waking up or overloaded. Switch to Offline mode to use the local heuristic engine.", data_source="synthguard-api", ) return _error_response(model_name, f"api_connection_error:{exc.reason}", data_source="synthguard-api") except (TimeoutError, ValueError, json.JSONDecodeError) as exc: return _error_response(model_name, f"api_parse_error:{exc}", data_source="synthguard-api") def screen_sequence(sequence: str, seq_type: str) -> dict[str, Any]: """ Screen a DNA or protein sequence through the BioLens adapter contract. Modes: offline — BioLens built-in heuristic engine, no internet required. online — Live SynthGuard API (/biolens/screen) for both DNA and PROTEIN. """ mode = os.getenv("BIOLENS_MODE", "offline").strip().lower() or "offline" # Accept legacy 'integrated'/'demo' values gracefully. if mode in {"integrated", "demo"}: mode = "online" if mode == "integrated" else "offline" model_name = f"biolens-{mode}-adapter" normalized, validation_error = _validate_sequence(sequence, seq_type) if validation_error: return _error_response(model_name, validation_error, data_source="biolens-validation") short_seq = len(normalized) < 20 if mode == "offline": result = _screen_offline(normalized, seq_type, model_name) elif mode == "online": # SynthGuard /biolens/screen handles both DNA and PROTEIN natively. result = _screen_online(normalized, seq_type, model_name) else: return _error_response(model_name, f"unsupported_runtime_mode:{mode}", data_source="biolens-config") if short_seq and result.get("ok"): result["explanation"] = ( "Short sequences carry higher false-positive uncertainty. " + (result.get("explanation") or "") ) return result