alphafold-adaptive-api / adaptive.py
joduor's picture
Deploy AlphaFold Adaptive API with morphic simulations and ESM-2
17b2088 verified
Raw
History Blame Contribute Delete
6.27 kB
"""
Adaptive context engine.
Tracks user session history, infers expertise level, and generates
context-aware system prompt extensions for the custom GPT.
"""
from __future__ import annotations
import time
from collections import defaultdict
from dataclasses import dataclass, field
from typing import Any
# ── In-memory session store (per HF Space instance) ───────────────────────────
@dataclass
class Session:
session_id: str
created_at: float = field(default_factory=time.time)
proteins_visited: list[str] = field(default_factory=list)
question_complexity: list[int] = field(default_factory=list)
domains_of_interest: list[str] = field(default_factory=list)
interaction_count: int = 0
expertise_score: float = 0.5 # 0=novice, 1=expert
preferred_depth: str = "balanced" # brief | balanced | deep
_sessions: dict[str, Session] = {}
def get_session(session_id: str) -> Session:
if session_id not in _sessions:
_sessions[session_id] = Session(session_id=session_id)
return _sessions[session_id]
# ── Expertise inference ────────────────────────────────────────────────────────
EXPERT_TERMS = {
"plddt", "alphafold", "uniprot", "pfam", "ptm", "idr", "cryo-em",
"x-ray crystallography", "ramachandran", "secondary structure",
"disordered region", "binding affinity", "allosteric", "catalytic triad",
"homology", "ortholog", "paralog", "domain", "residue", "motif",
"b-factor", "resolution", "rmsd", "torsion angle",
}
NOVICE_TERMS = {
"what is", "explain", "how does", "simple", "basic", "beginner",
"tell me about", "meaning of", "what does",
}
def _infer_complexity(query: str) -> int:
"""Return 0 (novice) to 2 (expert) based on query vocabulary."""
q = query.lower()
expert_hits = sum(1 for t in EXPERT_TERMS if t in q)
novice_hits = sum(1 for t in NOVICE_TERMS if t in q)
if expert_hits >= 2:
return 2
if expert_hits == 1:
return 1
if novice_hits > 0:
return 0
return 1
# ── Adaptive system prompt builder ────────────────────────────────────────────
DEPTH_INSTRUCTIONS = {
"brief": (
"Keep responses concise. Use plain language. "
"Avoid jargon. Focus on practical takeaways."
),
"balanced": (
"Balance scientific accuracy with clarity. "
"Define technical terms on first use. "
"Provide one concrete example per concept."
),
"deep": (
"Assume expert-level structural biology knowledge. "
"Include mechanistic detail, cite pLDDT thresholds, "
"reference domain databases (Pfam, InterPro), and discuss "
"functional implications at the residue level."
),
}
def build_adaptive_prompt(session: Session, current_query: str = "") -> str:
"""Return a context block that the GPT prepends to its system prompt."""
if current_query:
c = _infer_complexity(current_query)
session.question_complexity.append(c)
session.interaction_count += 1
# Exponential moving average for expertise score
alpha = 0.3
session.expertise_score = (
alpha * (c / 2.0) + (1 - alpha) * session.expertise_score
)
# Decide depth
if session.expertise_score > 0.65:
session.preferred_depth = "deep"
elif session.expertise_score < 0.35:
session.preferred_depth = "brief"
else:
session.preferred_depth = "balanced"
depth_instr = DEPTH_INSTRUCTIONS[session.preferred_depth]
# Build context block
history_line = ""
if session.proteins_visited:
recent = session.proteins_visited[-5:]
history_line = f"Proteins discussed this session: {', '.join(recent)}."
domain_line = ""
if session.domains_of_interest:
domains = list(set(session.domains_of_interest[-6:]))
domain_line = f"User's areas of interest: {', '.join(domains)}."
prompt = f"""[ADAPTIVE CONTEXT β€” session {session.session_id[:8]}]
Detected expertise level: {session.preferred_depth.upper()} ({session.expertise_score:.2f}/1.0).
Instruction: {depth_instr}
{history_line}
{domain_line}
Interaction count this session: {session.interaction_count}.
Tailor every response to the above profile without explicitly mentioning it."""
return prompt.strip()
def update_session_protein(session: Session, uniprot_id: str, domains: list[str]) -> None:
if uniprot_id not in session.proteins_visited:
session.proteins_visited.append(uniprot_id)
session.domains_of_interest.extend(domains)
# ── Adaptive follow-up question generator ────────────────────────────────────
_FOLLOWUPS: dict[str, list[str]] = {
"novice": [
"What does this protein actually do in the human body?",
"Is this protein associated with any disease?",
"What happens if this protein is mutated?",
"Can you explain what the confidence colours mean?",
],
"balanced": [
"Which residues form the active site or binding interface?",
"Are there known pathogenic mutations in this protein?",
"What structural domains does this protein contain?",
"How does pLDDT score vary across regions β€” and what does that imply?",
],
"expert": [
"What is the allosteric communication network within this structure?",
"Which disordered regions are functionally important?",
"How does the predicted structure compare to experimental PDB entries?",
"What post-translational modifications are predicted at high-pLDDT residues?",
],
}
def generate_followups(session: Session, n: int = 3) -> list[str]:
level = session.preferred_depth
key = {"brief": "novice", "balanced": "balanced", "deep": "expert"}[level]
pool = _FOLLOWUPS[key]
visited = len(session.proteins_visited)
start = (visited * 2) % len(pool)
return [pool[(start + i) % len(pool)] for i in range(n)]