Spaces:
Running
Running
github-actions[bot] commited on
Commit Β·
0269d22
1
Parent(s): 8aa2c1c
Deploy 9eecba1
Browse filesGive Evo 2 a place in the product, not just on a GPU
Source: https://github.com/WINTER4000/turingDNA/commit/9eecba109b5215f1a319c81fee2d9132d7f46140
- dee/server.py +110 -0
- dee/static/app.css +123 -0
- dee/static/app.js +383 -2
- dee/static/index.html +167 -3
- tests/test_dna_endpoints.py +176 -0
dee/server.py
CHANGED
|
@@ -819,6 +819,12 @@ _RL_RULES = [
|
|
| 819 |
("/api/primers/multiplex", (30, 60)),
|
| 820 |
("/api/primers/analyze", (40, 60)),
|
| 821 |
("/api/crispr", (40, 60)),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 822 |
("/api/plasmid", (60, 60)),
|
| 823 |
("/api/ping", (60, 60)), # dwell heartbeat β its own
|
| 824 |
# bucket so ~2/min never eats
|
|
@@ -864,6 +870,8 @@ _EVENT_KINDS = {
|
|
| 864 |
"/api/primers/save": "primer_save",
|
| 865 |
"/api/align": "sequence_align",
|
| 866 |
"/api/de/round2": "de_round2",
|
|
|
|
|
|
|
| 867 |
}
|
| 868 |
# Sort longest-prefix-first so the most specific rule matches.
|
| 869 |
_RL_RULES.sort(key=lambda r: len(r[0]), reverse=True)
|
|
@@ -3300,6 +3308,108 @@ def create_app() -> Flask:
|
|
| 3300 |
resp.headers["Cache-Control"] = "no-store, max-age=0"
|
| 3301 |
return resp
|
| 3302 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3303 |
@app.get("/api/benchmarks")
|
| 3304 |
def benchmarks() -> Response:
|
| 3305 |
"""The receipts β how well the engine's zero-shot ranking predicts
|
|
|
|
| 819 |
("/api/primers/multiplex", (30, 60)),
|
| 820 |
("/api/primers/analyze", (40, 60)),
|
| 821 |
("/api/crispr", (40, 60)),
|
| 822 |
+
# Evo 2 (7B) on a rented GPU β the most expensive call in the product by a
|
| 823 |
+
# wide margin, and unlike ESM-2's Achilles tier there is no free local path
|
| 824 |
+
# to fall back to. Tighter than every other tool bucket on purpose: this
|
| 825 |
+
# limit is about the GPU bill, not about protecting a worker thread.
|
| 826 |
+
("/api/dna/generate", (6, 60)), # autoregressive β priciest
|
| 827 |
+
("/api/dna", (12, 60)),
|
| 828 |
("/api/plasmid", (60, 60)),
|
| 829 |
("/api/ping", (60, 60)), # dwell heartbeat β its own
|
| 830 |
# bucket so ~2/min never eats
|
|
|
|
| 870 |
"/api/primers/save": "primer_save",
|
| 871 |
"/api/align": "sequence_align",
|
| 872 |
"/api/de/round2": "de_round2",
|
| 873 |
+
"/api/dna/score": "dna_score",
|
| 874 |
+
"/api/dna/generate": "dna_generate",
|
| 875 |
}
|
| 876 |
# Sort longest-prefix-first so the most specific rule matches.
|
| 877 |
_RL_RULES.sort(key=lambda r: len(r[0]), reverse=True)
|
|
|
|
| 3308 |
resp.headers["Cache-Control"] = "no-store, max-age=0"
|
| 3309 |
return resp
|
| 3310 |
|
| 3311 |
+
# ββ DNA-level scoring / generation (Evo 2) βββββββββββββββββββββββββββ
|
| 3312 |
+
# Until now the ONLY route to Evo 2 was the agent deciding to call
|
| 3313 |
+
# score_or_generate_dna mid-conversation. The engine computed dna_models
|
| 3314 |
+
# for /api/models and no frontend read it, so a capability that runs on a
|
| 3315 |
+
# 7B genome model was, in practice, unreachable by clicking. These are the
|
| 3316 |
+
# two endpoints the DNA view calls.
|
| 3317 |
+
#
|
| 3318 |
+
# Both are deliberately thin: dee/core/dna_scoring.py already owns tier
|
| 3319 |
+
# rules, bounds and the "not configured is not the same as broken"
|
| 3320 |
+
# distinction, and duplicating any of that here would give the product two
|
| 3321 |
+
# answers to the same question.
|
| 3322 |
+
|
| 3323 |
+
def _dna_signin_gate(where: str):
|
| 3324 |
+
"""Same contract as CRISPR/primers: no anonymous use. Distinct from
|
| 3325 |
+
the trial timer β the 403 carries kind=signin_required so auth.js
|
| 3326 |
+
routes to /signin rather than showing the trial modal."""
|
| 3327 |
+
auth = _auth.get_auth()
|
| 3328 |
+
if auth.anonymous:
|
| 3329 |
+
return jsonify({
|
| 3330 |
+
"error": ("DNA scoring requires a free account. Sign in or "
|
| 3331 |
+
"create one to keep going."),
|
| 3332 |
+
"kind": "signin_required",
|
| 3333 |
+
"signup_url": f"https://turingdna.com/signin/?from={where}",
|
| 3334 |
+
}), 403
|
| 3335 |
+
return None
|
| 3336 |
+
|
| 3337 |
+
@app.post("/api/dna/score")
|
| 3338 |
+
def dna_score() -> Response:
|
| 3339 |
+
gate = _dna_signin_gate("dna")
|
| 3340 |
+
if gate is not None:
|
| 3341 |
+
return gate
|
| 3342 |
+
|
| 3343 |
+
body = request.get_json(force=True, silent=True) or {}
|
| 3344 |
+
reference = "".join(str(body.get("reference") or "").split()).upper()
|
| 3345 |
+
raw_variants = body.get("variants") or []
|
| 3346 |
+
tier = str(body.get("tier") or "achilles").strip().lower()
|
| 3347 |
+
|
| 3348 |
+
if not reference:
|
| 3349 |
+
return jsonify({"error": "missing 'reference'"}), 400
|
| 3350 |
+
if not isinstance(raw_variants, list) or not raw_variants:
|
| 3351 |
+
return jsonify({"error": "missing 'variants'"}), 400
|
| 3352 |
+
variants = [str(v).strip().upper() for v in raw_variants if str(v).strip()]
|
| 3353 |
+
if not variants:
|
| 3354 |
+
return jsonify({"error": "missing 'variants'"}), 400
|
| 3355 |
+
|
| 3356 |
+
try:
|
| 3357 |
+
result = _dna_scoring.score_dna_variants(reference, variants, tier=tier)
|
| 3358 |
+
except _dna_scoring.DnaModelUnavailable as exc:
|
| 3359 |
+
# Customer-safe by construction β dna_scoring keeps env-var names
|
| 3360 |
+
# and repo paths in the log, not in this message.
|
| 3361 |
+
return jsonify({"error": str(exc), "kind": "dna_unavailable"}), 503
|
| 3362 |
+
except ValueError as exc:
|
| 3363 |
+
return jsonify({"error": str(exc)}), 400
|
| 3364 |
+
except Exception: # noqa: BLE001
|
| 3365 |
+
app.logger.exception("dna_score failed")
|
| 3366 |
+
return jsonify({
|
| 3367 |
+
"error": ("The DNA model didn't return a result for that "
|
| 3368 |
+
"request. Nothing was scored β try again, and if it "
|
| 3369 |
+
"keeps failing the backend is having a moment."),
|
| 3370 |
+
"kind": "dna_error",
|
| 3371 |
+
}), 502
|
| 3372 |
+
return jsonify(result)
|
| 3373 |
+
|
| 3374 |
+
@app.post("/api/dna/generate")
|
| 3375 |
+
def dna_generate() -> Response:
|
| 3376 |
+
gate = _dna_signin_gate("dna")
|
| 3377 |
+
if gate is not None:
|
| 3378 |
+
return gate
|
| 3379 |
+
|
| 3380 |
+
body = request.get_json(force=True, silent=True) or {}
|
| 3381 |
+
prompt = "".join(str(body.get("prompt") or "").split()).upper()
|
| 3382 |
+
tier = str(body.get("tier") or "prometheus").strip().lower()
|
| 3383 |
+
if not prompt:
|
| 3384 |
+
return jsonify({"error": "missing 'prompt'"}), 400
|
| 3385 |
+
|
| 3386 |
+
try:
|
| 3387 |
+
n_tokens = int(body.get("n_tokens") or 200)
|
| 3388 |
+
temperature = float(body.get("temperature") or 1.0)
|
| 3389 |
+
top_k = int(body.get("top_k") or 4)
|
| 3390 |
+
except (TypeError, ValueError):
|
| 3391 |
+
return jsonify({"error": "n_tokens/temperature/top_k must be numbers"}), 400
|
| 3392 |
+
|
| 3393 |
+
try:
|
| 3394 |
+
result = _dna_scoring.generate_dna_sequence(
|
| 3395 |
+
prompt, n_tokens=n_tokens, tier=tier,
|
| 3396 |
+
temperature=temperature, top_k=top_k)
|
| 3397 |
+
except _dna_scoring.DnaModelUnavailable as exc:
|
| 3398 |
+
# Also the path for "generation is Prometheus-only" β enforced in
|
| 3399 |
+
# dna_scoring AND again in modal/evo2_scoring, so a caller that
|
| 3400 |
+
# skips this route still gets refused.
|
| 3401 |
+
return jsonify({"error": str(exc), "kind": "dna_unavailable"}), 503
|
| 3402 |
+
except ValueError as exc:
|
| 3403 |
+
return jsonify({"error": str(exc)}), 400
|
| 3404 |
+
except Exception: # noqa: BLE001
|
| 3405 |
+
app.logger.exception("dna_generate failed")
|
| 3406 |
+
return jsonify({
|
| 3407 |
+
"error": ("The DNA model didn't return a sequence for that "
|
| 3408 |
+
"prompt. Nothing was generated β try again."),
|
| 3409 |
+
"kind": "dna_error",
|
| 3410 |
+
}), 502
|
| 3411 |
+
return jsonify(result)
|
| 3412 |
+
|
| 3413 |
@app.get("/api/benchmarks")
|
| 3414 |
def benchmarks() -> Response:
|
| 3415 |
"""The receipts β how well the engine's zero-shot ranking predicts
|
dee/static/app.css
CHANGED
|
@@ -9691,3 +9691,126 @@ body.de-agent-run .dna-edit-actions { display: none; }
|
|
| 9691 |
font-size: 10px; line-height: 1.5; color: var(--ink-faint);
|
| 9692 |
border-top: 1px solid var(--line); padding-top: 7px;
|
| 9693 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9691 |
font-size: 10px; line-height: 1.5; color: var(--ink-faint);
|
| 9692 |
border-top: 1px solid var(--line); padding-top: 7px;
|
| 9693 |
}
|
| 9694 |
+
|
| 9695 |
+
/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 9696 |
+
DNA Design (Evo 2) β mirrors the primer/CRISPR card idiom deliberately,
|
| 9697 |
+
so the newest tool doesn't announce itself as a bolt-on.
|
| 9698 |
+
Tokens: --bg-card / --bg-raised / --line-strong / --ink-*. There is no
|
| 9699 |
+
--surface or --surface-raised in this stylesheet; using them renders
|
| 9700 |
+
invisible components and no test catches it.
|
| 9701 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 9702 |
+
|
| 9703 |
+
.dna-mode-row { display: flex; gap: 6px; margin-bottom: 16px; }
|
| 9704 |
+
.dna-mode {
|
| 9705 |
+
padding: 7px 14px; border-radius: var(--r-2);
|
| 9706 |
+
border: 1px solid var(--line-strong); background: var(--gray-0);
|
| 9707 |
+
color: var(--ink-soft); font-size: 12.5px; cursor: pointer;
|
| 9708 |
+
font-family: inherit;
|
| 9709 |
+
}
|
| 9710 |
+
.dna-mode:hover { color: var(--ink); border-color: var(--line-bold); }
|
| 9711 |
+
.dna-mode.is-active {
|
| 9712 |
+
background: var(--ink-strong); color: var(--on-ink);
|
| 9713 |
+
border-color: var(--ink-strong);
|
| 9714 |
+
}
|
| 9715 |
+
|
| 9716 |
+
.dna-textarea {
|
| 9717 |
+
width: 100%; box-sizing: border-box; resize: vertical;
|
| 9718 |
+
font-family: var(--font-mono); font-size: 13px; line-height: 1.5;
|
| 9719 |
+
padding: 12px 14px; border: 1px solid var(--line-strong); border-radius: var(--r-3);
|
| 9720 |
+
background: var(--gray-0); color: var(--ink);
|
| 9721 |
+
}
|
| 9722 |
+
.dna-textarea:focus { outline: none; border-color: var(--brand); }
|
| 9723 |
+
|
| 9724 |
+
.dna-meta { margin: 6px 0 14px; font-size: 11.5px; color: var(--ink-faint); }
|
| 9725 |
+
.dna-meta.is-warn { color: var(--warning); }
|
| 9726 |
+
|
| 9727 |
+
.dna-select {
|
| 9728 |
+
width: 100%; box-sizing: border-box; padding: 9px 12px;
|
| 9729 |
+
border: 1px solid var(--line-strong); border-radius: var(--r-2);
|
| 9730 |
+
background: var(--gray-0); color: var(--ink); font-size: 13px;
|
| 9731 |
+
font-family: inherit;
|
| 9732 |
+
}
|
| 9733 |
+
.dna-select:focus { outline: none; border-color: var(--brand); }
|
| 9734 |
+
/* A tier that exists but isn't reachable stays visible and disabled β that
|
| 9735 |
+
is a more useful statement than quietly dropping the row. */
|
| 9736 |
+
.dna-select option:disabled { color: var(--ink-disabled); }
|
| 9737 |
+
|
| 9738 |
+
.dna-gen-knobs { display: flex; gap: 14px; flex-wrap: wrap; margin-bottom: 14px; }
|
| 9739 |
+
.dna-knob { display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--ink-soft); }
|
| 9740 |
+
.dna-knob input {
|
| 9741 |
+
width: 110px; padding: 7px 10px; font-family: var(--font-mono); font-size: 13px;
|
| 9742 |
+
border: 1px solid var(--line-strong); border-radius: var(--r-2);
|
| 9743 |
+
background: var(--gray-0); color: var(--ink);
|
| 9744 |
+
}
|
| 9745 |
+
.dna-knob input:focus { outline: none; border-color: var(--brand); }
|
| 9746 |
+
|
| 9747 |
+
.dna-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 14px; }
|
| 9748 |
+
|
| 9749 |
+
/* The wait matters: a cold 7B container is ~80s and a silent spinner over
|
| 9750 |
+
that long is indistinguishable from a hang. */
|
| 9751 |
+
.dna-wait {
|
| 9752 |
+
margin-top: 12px; font-size: 12.5px; color: var(--ink-soft);
|
| 9753 |
+
background: var(--gray-1); border-left: 3px solid var(--line-bold);
|
| 9754 |
+
padding: 9px 12px; border-radius: var(--r-2);
|
| 9755 |
+
}
|
| 9756 |
+
|
| 9757 |
+
.dna-results-head {
|
| 9758 |
+
display: flex; align-items: baseline; justify-content: space-between;
|
| 9759 |
+
gap: 12px; flex-wrap: wrap; margin-bottom: 10px;
|
| 9760 |
+
}
|
| 9761 |
+
.dna-results-title { font-size: 14px; color: var(--ink-strong); font-weight: 600; }
|
| 9762 |
+
.dna-results-meta { font-size: 11.5px; color: var(--ink-faint); font-family: var(--font-mono); }
|
| 9763 |
+
|
| 9764 |
+
.dna-table { width: 100%; border-collapse: collapse; font-size: 13px; margin-top: 8px; }
|
| 9765 |
+
.dna-table th {
|
| 9766 |
+
text-align: left; font-size: 11px; text-transform: uppercase;
|
| 9767 |
+
letter-spacing: .06em; color: var(--ink-faint); font-weight: 600;
|
| 9768 |
+
padding: 6px 10px 6px 0; border-bottom: 1px solid var(--line);
|
| 9769 |
+
}
|
| 9770 |
+
.dna-table td { padding: 8px 10px 8px 0; border-bottom: 1px solid var(--line); vertical-align: middle; }
|
| 9771 |
+
.dna-table td:last-child, .dna-table th:last-child { width: 42%; padding-right: 0; }
|
| 9772 |
+
|
| 9773 |
+
.dna-delta { white-space: nowrap; }
|
| 9774 |
+
.dna-delta--neg { color: var(--danger); }
|
| 9775 |
+
.dna-delta--pos { color: var(--success); }
|
| 9776 |
+
|
| 9777 |
+
.dna-bar { display: block; height: 8px; border-radius: 999px; min-width: 2px; }
|
| 9778 |
+
.dna-bar--neg { background: color-mix(in srgb, var(--danger) 55%, transparent); }
|
| 9779 |
+
.dna-bar--pos { background: color-mix(in srgb, var(--success) 55%, transparent); }
|
| 9780 |
+
|
| 9781 |
+
.dna-scale-note { margin-top: 10px; font-size: 11.5px; color: var(--ink-faint); line-height: 1.5; }
|
| 9782 |
+
|
| 9783 |
+
/* Refused variants are an input finding, never folded into "no result". */
|
| 9784 |
+
.dna-skipped {
|
| 9785 |
+
margin-top: 16px; padding: 12px 14px; border-radius: var(--r-2);
|
| 9786 |
+
background: color-mix(in srgb, var(--warning) 8%, transparent);
|
| 9787 |
+
border-left: 3px solid var(--warning);
|
| 9788 |
+
}
|
| 9789 |
+
.dna-skipped-hd { margin: 0 0 6px; font-size: 12px; font-weight: 600; color: var(--warning); }
|
| 9790 |
+
.dna-skipped ul { margin: 0; padding-left: 18px; font-size: 12.5px; color: var(--ink-soft); }
|
| 9791 |
+
.dna-skipped li { margin: 3px 0; }
|
| 9792 |
+
.dna-skipped-why { margin: 8px 0 0; font-size: 11.5px; color: var(--ink-faint); line-height: 1.5; }
|
| 9793 |
+
|
| 9794 |
+
.dna-generated {
|
| 9795 |
+
margin: 8px 0 0; padding: 12px 14px; border-radius: var(--r-2);
|
| 9796 |
+
background: var(--gray-1); border: 1px solid var(--line);
|
| 9797 |
+
font-size: 12.5px; line-height: 1.6; color: var(--ink);
|
| 9798 |
+
white-space: pre-wrap; word-break: break-all; overflow-x: auto;
|
| 9799 |
+
}
|
| 9800 |
+
.dna-empty { font-size: 13px; color: var(--ink-faint); margin: 8px 0 0; }
|
| 9801 |
+
|
| 9802 |
+
/* Mobile: the standing rule is 44px tap targets and nothing under 12px. */
|
| 9803 |
+
@media (max-width: 720px) {
|
| 9804 |
+
.dna-mode { padding: 11px 16px; min-height: 44px; font-size: 13px; }
|
| 9805 |
+
.dna-actions { flex-direction: column-reverse; }
|
| 9806 |
+
.dna-actions button { width: 100%; min-height: 44px; }
|
| 9807 |
+
.dna-knob input { width: 100%; min-height: 44px; }
|
| 9808 |
+
.dna-gen-knobs { flex-direction: column; gap: 10px; }
|
| 9809 |
+
.dna-table { font-size: 12px; }
|
| 9810 |
+
.dna-table td:last-child, .dna-table th:last-child { width: 30%; }
|
| 9811 |
+
/* The standing rule is nothing under 12px on a phone. Shared components
|
| 9812 |
+
(<code>, <summary>) sit at 11β11.5px across every existing view and
|
| 9813 |
+
are left alone here β but everything this tool owns meets it. */
|
| 9814 |
+
.dna-meta, .dna-results-meta, .dna-scale-note,
|
| 9815 |
+
.dna-skipped-why, .dna-table th { font-size: 12px; }
|
| 9816 |
+
}
|
dee/static/app.js
CHANGED
|
@@ -390,7 +390,9 @@ renderGutter();
|
|
| 390 |
// existed): 'turing' leads the list and is the no-hash fallback. The other
|
| 391 |
// four tools stay one click away for hands-on work β Turing just isn't a
|
| 392 |
// nav-rail peer anymore, it's what a session starts with.
|
| 393 |
-
|
|
|
|
|
|
|
| 394 |
|
| 395 |
// ββ UI mode flag (Mission-Control-+-Bench re-architecture, 2026-07-13) ββ
|
| 396 |
// 'bench' is now the default UI; ?ui=classic remains as a rollback escape
|
|
@@ -10311,6 +10313,7 @@ function runOracle(opts){
|
|
| 10311 |
// The tools, for the βK command palette (verbs on the current work).
|
| 10312 |
const TOOLS = [
|
| 10313 |
{ label: 'Evolve a sequence', hint: 'Directed evolution', route: 'design' },
|
|
|
|
| 10314 |
{ label: 'Build a plasmid', hint: 'Map & annotate a construct', route: 'plasmid' },
|
| 10315 |
{ label: 'Design CRISPR guides', hint: 'Guides + specificity', route: 'crispr' },
|
| 10316 |
{ label: 'Check primers', hint: 'Tm, dimers, specificity', route: 'primers' },
|
|
@@ -10348,6 +10351,10 @@ function runOracle(opts){
|
|
| 10348 |
{ route: 'plasmid', label: 'Build a plasmid', kw: ['plasmid','vector','clone','clonin','annotate','assembl','gibson','golden','backbone','insert','map '] },
|
| 10349 |
{ route: 'crispr', label: 'Design CRISPR guides', kw: ['crispr','guide','grna','sgrna','knockout','knock out','cas9','base edit','base-edit','excis','disrupt'] },
|
| 10350 |
{ route: 'primers', label: 'Check primers', kw: ['primer',' pcr','anneal','dimer','amplif','oligo','melting'] },
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10351 |
];
|
| 10352 |
function matchIntent(q) {
|
| 10353 |
const s = ' ' + String(q || '').toLowerCase() + ' ';
|
|
@@ -10944,7 +10951,11 @@ function runOracle(opts){
|
|
| 10944 |
// keep working. Only ever active in the opt-in bench UI.
|
| 10945 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 10946 |
(function () {
|
| 10947 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10948 |
let current = null; // { name, sub, phaseIdx, route }
|
| 10949 |
|
| 10950 |
function el(id) { return document.getElementById(id); }
|
|
@@ -11894,3 +11905,373 @@ if (document.readyState === 'loading') {
|
|
| 11894 |
} else {
|
| 11895 |
initModelPicker();
|
| 11896 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 390 |
// existed): 'turing' leads the list and is the no-hash fallback. The other
|
| 391 |
// four tools stay one click away for hands-on work β Turing just isn't a
|
| 392 |
// nav-rail peer anymore, it's what a session starts with.
|
| 393 |
+
// 'dna' (Evo 2) sits next to 'design': DE designs at the protein level, DNA
|
| 394 |
+
// Design at the nucleotide level. Same phase of the loop, different molecule.
|
| 395 |
+
const ROUTES = ['mission', 'turing', 'structure', 'plasmid', 'design', 'dna', 'crispr', 'primers', 'docs'];
|
| 396 |
|
| 397 |
// ββ UI mode flag (Mission-Control-+-Bench re-architecture, 2026-07-13) ββ
|
| 398 |
// 'bench' is now the default UI; ?ui=classic remains as a rollback escape
|
|
|
|
| 10313 |
// The tools, for the βK command palette (verbs on the current work).
|
| 10314 |
const TOOLS = [
|
| 10315 |
{ label: 'Evolve a sequence', hint: 'Directed evolution', route: 'design' },
|
| 10316 |
+
{ label: 'Score a DNA change', hint: 'Promoters, splice sites, UTRs', route: 'dna' },
|
| 10317 |
{ label: 'Build a plasmid', hint: 'Map & annotate a construct', route: 'plasmid' },
|
| 10318 |
{ label: 'Design CRISPR guides', hint: 'Guides + specificity', route: 'crispr' },
|
| 10319 |
{ label: 'Check primers', hint: 'Tm, dimers, specificity', route: 'primers' },
|
|
|
|
| 10351 |
{ route: 'plasmid', label: 'Build a plasmid', kw: ['plasmid','vector','clone','clonin','annotate','assembl','gibson','golden','backbone','insert','map '] },
|
| 10352 |
{ route: 'crispr', label: 'Design CRISPR guides', kw: ['crispr','guide','grna','sgrna','knockout','knock out','cas9','base edit','base-edit','excis','disrupt'] },
|
| 10353 |
{ route: 'primers', label: 'Check primers', kw: ['primer',' pcr','anneal','dimer','amplif','oligo','melting'] },
|
| 10354 |
+
// DNA-level, so the vocabulary is regulatory/non-coding rather than
|
| 10355 |
+
// protein. Deliberately LAST: 'design'/'crispr' above own the terms a
|
| 10356 |
+
// protein or editing question would use, and matching is first-hit.
|
| 10357 |
+
{ route: 'dna', label: 'Score a DNA change', kw: ['promoter','enhancer','splice','utr',' rbs','ribosome binding','non-coding','noncoding','regulatory','intron','terminator','operator','evo 2','evo2','nucleotide'] },
|
| 10358 |
];
|
| 10359 |
function matchIntent(q) {
|
| 10360 |
const s = ' ' + String(q || '').toLowerCase() + ' ';
|
|
|
|
| 10951 |
// keep working. Only ever active in the opt-in bench UI.
|
| 10952 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 10953 |
(function () {
|
| 10954 |
+
// 'dna' sits next to 'design' for the same reason it does in the nav:
|
| 10955 |
+
// both are the Design phase, one at the protein level and one at the
|
| 10956 |
+
// nucleotide level. Bench is the DEFAULT UI β a route missing from this
|
| 10957 |
+
// list has a nav entry that goes nowhere.
|
| 10958 |
+
const TAB_ROUTES = ['structure', 'design', 'dna', 'plasmid', 'crispr', 'primers'];
|
| 10959 |
let current = null; // { name, sub, phaseIdx, route }
|
| 10960 |
|
| 10961 |
function el(id) { return document.getElementById(id); }
|
|
|
|
| 11905 |
} else {
|
| 11906 |
initModelPicker();
|
| 11907 |
}
|
| 11908 |
+
|
| 11909 |
+
|
| 11910 |
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 11911 |
+
// DNA Design β Evo 2 (7B) scoring and generation
|
| 11912 |
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 11913 |
+
// The DNA-level counterpart to Directed Evolution. Before this view, the only
|
| 11914 |
+
// way to reach Evo 2 was for Turing to decide to call score_or_generate_dna
|
| 11915 |
+
// mid-conversation: /api/models computed `dna_models` and NOTHING in the
|
| 11916 |
+
// frontend read it, so the one capability that understands non-coding and
|
| 11917 |
+
// regulatory sequence was unreachable by clicking.
|
| 11918 |
+
//
|
| 11919 |
+
// Two things this file is careful about, both learned the hard way elsewhere
|
| 11920 |
+
// in this codebase:
|
| 11921 |
+
//
|
| 11922 |
+
// Β· Availability is what the SERVER says, per tier. A tier whose backend
|
| 11923 |
+
// isn't reachable is disabled with its reason shown, never offered and
|
| 11924 |
+
// then failed. (/api/models' `available` is a live probe now β see
|
| 11925 |
+
// dee/core/modal_client.reachable.)
|
| 11926 |
+
// Β· A cold 7B container takes ~80s (warm ~23s, both measured). A spinner
|
| 11927 |
+
// with no elapsed time over that long is indistinguishable from a hang,
|
| 11928 |
+
// which is the exact complaint the Interaction Radar's elapsed-time work
|
| 11929 |
+
// already answered once.
|
| 11930 |
+
// βββββββββββββββββββββββββββββββββββββββοΏ½οΏ½βββββββββββββββββββββββββββββββ
|
| 11931 |
+
(function () {
|
| 11932 |
+
const $ = (id) => document.getElementById(id);
|
| 11933 |
+
const esc = (s) => (typeof escapeHtml === 'function' ? escapeHtml(String(s)) : String(s));
|
| 11934 |
+
|
| 11935 |
+
// The E. coli lac promoter / operator region β the same fragment used to
|
| 11936 |
+
// verify the backend end to end, with substitutions whose WT bases are
|
| 11937 |
+
// real. An example that gets refused would teach the wrong lesson.
|
| 11938 |
+
const EXAMPLE_REF =
|
| 11939 |
+
'TTTACACTTTATGCTTCCGGCTCGTATGTTGTGTGGAATTGTGAGCGGATAACAATTTCA' +
|
| 11940 |
+
'CACAGGAAACAGCTATGACCATGATTACGGATTCACTGGCCGTCGTTTTACAA';
|
| 11941 |
+
const EXAMPLE_VARIANTS = 'T10G\nG20C\nT30A\nG45C';
|
| 11942 |
+
|
| 11943 |
+
let mode = 'score';
|
| 11944 |
+
let tiers = [];
|
| 11945 |
+
let busy = false;
|
| 11946 |
+
let timerId = null;
|
| 11947 |
+
|
| 11948 |
+
function view() { return document.querySelector('[data-view="dna"]'); }
|
| 11949 |
+
|
| 11950 |
+
// ββ tier picker, driven by what the server says is reachable βββββββββ
|
| 11951 |
+
async function loadTiers() {
|
| 11952 |
+
const sel = $('dnaTier');
|
| 11953 |
+
if (!sel) return;
|
| 11954 |
+
try {
|
| 11955 |
+
const res = await fetch('/api/models');
|
| 11956 |
+
const data = await res.json();
|
| 11957 |
+
tiers = (data && data.dna_models) || [];
|
| 11958 |
+
} catch (_) {
|
| 11959 |
+
tiers = [];
|
| 11960 |
+
}
|
| 11961 |
+
if (!tiers.length) {
|
| 11962 |
+
sel.innerHTML = '<option value="">DNA scoring isn\'t available here</option>';
|
| 11963 |
+
sel.disabled = true;
|
| 11964 |
+
setTierNote('');
|
| 11965 |
+
return;
|
| 11966 |
+
}
|
| 11967 |
+
sel.disabled = false;
|
| 11968 |
+
sel.innerHTML = tiers.map((t) => {
|
| 11969 |
+
// Disabled rather than hidden: "this tier exists but isn't running
|
| 11970 |
+
// right now" is a different, more useful statement than silence.
|
| 11971 |
+
const off = t.available ? '' : ' disabled';
|
| 11972 |
+
const suffix = t.available ? '' : ' β unavailable';
|
| 11973 |
+
return `<option value="${esc(t.id)}"${off}>${esc(t.label)}${suffix}</option>`;
|
| 11974 |
+
}).join('');
|
| 11975 |
+
const firstUp = tiers.find((t) => t.available);
|
| 11976 |
+
if (firstUp) sel.value = firstUp.id;
|
| 11977 |
+
syncTier();
|
| 11978 |
+
}
|
| 11979 |
+
|
| 11980 |
+
function currentTier() {
|
| 11981 |
+
const sel = $('dnaTier');
|
| 11982 |
+
return tiers.find((t) => t.id === (sel && sel.value)) || null;
|
| 11983 |
+
}
|
| 11984 |
+
|
| 11985 |
+
function setTierNote(text) {
|
| 11986 |
+
const el = $('dnaTierNote');
|
| 11987 |
+
if (el) el.textContent = text || '';
|
| 11988 |
+
}
|
| 11989 |
+
|
| 11990 |
+
function syncTier() {
|
| 11991 |
+
const t = currentTier();
|
| 11992 |
+
const runBtn = $('dnaRun');
|
| 11993 |
+
if (!t) {
|
| 11994 |
+
setTierNote('');
|
| 11995 |
+
if (runBtn) runBtn.disabled = true;
|
| 11996 |
+
return;
|
| 11997 |
+
}
|
| 11998 |
+
let note = t.note || '';
|
| 11999 |
+
if (mode === 'generate' && !t.generation) {
|
| 12000 |
+
// Say it here rather than letting the server 503 after a click.
|
| 12001 |
+
note += ' Generation is Prometheus-only β the checkpoint is the '
|
| 12002 |
+
+ 'same on both tiers, generation access is the difference.';
|
| 12003 |
+
}
|
| 12004 |
+
if (t.max_reference_nt) {
|
| 12005 |
+
note += ` Up to ${Number(t.max_reference_nt).toLocaleString()} nt per call.`;
|
| 12006 |
+
}
|
| 12007 |
+
setTierNote(note.trim());
|
| 12008 |
+
if (runBtn) {
|
| 12009 |
+
runBtn.disabled = busy || !t.available
|
| 12010 |
+
|| (mode === 'generate' && !t.generation);
|
| 12011 |
+
}
|
| 12012 |
+
}
|
| 12013 |
+
|
| 12014 |
+
// ββ mode βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 12015 |
+
function setMode(next) {
|
| 12016 |
+
mode = next;
|
| 12017 |
+
const scoreBtn = $('dnaModeScore');
|
| 12018 |
+
const genBtn = $('dnaModeGenerate');
|
| 12019 |
+
if (scoreBtn) {
|
| 12020 |
+
scoreBtn.classList.toggle('is-active', mode === 'score');
|
| 12021 |
+
scoreBtn.setAttribute('aria-selected', String(mode === 'score'));
|
| 12022 |
+
}
|
| 12023 |
+
if (genBtn) {
|
| 12024 |
+
genBtn.classList.toggle('is-active', mode === 'generate');
|
| 12025 |
+
genBtn.setAttribute('aria-selected', String(mode === 'generate'));
|
| 12026 |
+
}
|
| 12027 |
+
const sp = $('dnaScorePane');
|
| 12028 |
+
const gp = $('dnaGeneratePane');
|
| 12029 |
+
if (sp) sp.hidden = mode !== 'score';
|
| 12030 |
+
if (gp) gp.hidden = mode !== 'generate';
|
| 12031 |
+
const run = $('dnaRun');
|
| 12032 |
+
if (run) run.textContent = mode === 'score' ? 'Score variants' : 'Generate sequence';
|
| 12033 |
+
// Generation needs Prometheus; jump there if the current pick can't.
|
| 12034 |
+
if (mode === 'generate') {
|
| 12035 |
+
const t = currentTier();
|
| 12036 |
+
if (t && !t.generation) {
|
| 12037 |
+
const gen = tiers.find((x) => x.generation && x.available);
|
| 12038 |
+
if (gen) $('dnaTier').value = gen.id;
|
| 12039 |
+
}
|
| 12040 |
+
}
|
| 12041 |
+
hideResults();
|
| 12042 |
+
syncTier();
|
| 12043 |
+
}
|
| 12044 |
+
|
| 12045 |
+
// ββ input helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 12046 |
+
function cleanSeq(raw) {
|
| 12047 |
+
// Tolerate FASTA headers and whitespace β people paste what they have.
|
| 12048 |
+
return String(raw || '')
|
| 12049 |
+
.split('\n').filter((l) => !l.trim().startsWith('>')).join('')
|
| 12050 |
+
.replace(/\s+/g, '').toUpperCase();
|
| 12051 |
+
}
|
| 12052 |
+
|
| 12053 |
+
function refMeta() {
|
| 12054 |
+
const el = $('dnaRefMeta');
|
| 12055 |
+
if (!el) return;
|
| 12056 |
+
const seq = cleanSeq($('dnaReference') && $('dnaReference').value);
|
| 12057 |
+
if (!seq) { el.hidden = true; return; }
|
| 12058 |
+
const bad = seq.replace(/[ACGT]/g, '');
|
| 12059 |
+
const gc = seq ? Math.round(100 * (seq.match(/[GC]/g) || []).length / seq.length) : 0;
|
| 12060 |
+
el.hidden = false;
|
| 12061 |
+
el.textContent = bad
|
| 12062 |
+
? `${seq.length.toLocaleString()} nt Β· ${gc}% GC Β· ${new Set(bad).size} non-ACGT character(s) β those will be rejected`
|
| 12063 |
+
: `${seq.length.toLocaleString()} nt Β· ${gc}% GC`;
|
| 12064 |
+
el.classList.toggle('is-warn', Boolean(bad));
|
| 12065 |
+
}
|
| 12066 |
+
|
| 12067 |
+
function showError(msg) {
|
| 12068 |
+
const el = $('dnaError');
|
| 12069 |
+
if (!el) return;
|
| 12070 |
+
el.hidden = false;
|
| 12071 |
+
el.textContent = msg;
|
| 12072 |
+
}
|
| 12073 |
+
function clearError() {
|
| 12074 |
+
const el = $('dnaError');
|
| 12075 |
+
if (el) { el.hidden = true; el.textContent = ''; }
|
| 12076 |
+
}
|
| 12077 |
+
function hideResults() {
|
| 12078 |
+
const c = $('dnaResultsCard');
|
| 12079 |
+
if (c) c.hidden = true;
|
| 12080 |
+
}
|
| 12081 |
+
|
| 12082 |
+
// ββ the wait βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 12083 |
+
function startWait() {
|
| 12084 |
+
const el = $('dnaWait');
|
| 12085 |
+
if (!el) return;
|
| 12086 |
+
const t0 = Date.now();
|
| 12087 |
+
el.hidden = false;
|
| 12088 |
+
const tick = () => {
|
| 12089 |
+
const s = Math.round((Date.now() - t0) / 1000);
|
| 12090 |
+
// Honest, and specific about WHY it might be slow. "Loadingβ¦" for
|
| 12091 |
+
// 80 seconds reads as broken; naming the cold start does not.
|
| 12092 |
+
el.textContent = s < 25
|
| 12093 |
+
? `Scoring on the GPU β ${s}s`
|
| 12094 |
+
: `Still going β ${s}s. A cold container loads a 7B checkpoint `
|
| 12095 |
+
+ `before its first pass; that's usually under 90s.`;
|
| 12096 |
+
};
|
| 12097 |
+
tick();
|
| 12098 |
+
timerId = setInterval(tick, 1000);
|
| 12099 |
+
}
|
| 12100 |
+
function stopWait() {
|
| 12101 |
+
if (timerId) { clearInterval(timerId); timerId = null; }
|
| 12102 |
+
const el = $('dnaWait');
|
| 12103 |
+
if (el) { el.hidden = true; el.textContent = ''; }
|
| 12104 |
+
}
|
| 12105 |
+
|
| 12106 |
+
// ββ results ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 12107 |
+
function renderScores(data) {
|
| 12108 |
+
const card = $('dnaResultsCard');
|
| 12109 |
+
const body = $('dnaResultsBody');
|
| 12110 |
+
const meta = $('dnaResultsMeta');
|
| 12111 |
+
if (!card || !body) return;
|
| 12112 |
+
|
| 12113 |
+
const scores = (data.scores || []).slice().sort(
|
| 12114 |
+
(a, b) => (a.delta_ll || 0) - (b.delta_ll || 0));
|
| 12115 |
+
const skipped = data.skipped || [];
|
| 12116 |
+
|
| 12117 |
+
const bits = [];
|
| 12118 |
+
if (typeof data.reference_ll === 'number') {
|
| 12119 |
+
bits.push(`reference log-likelihood ${data.reference_ll.toFixed(4)}`);
|
| 12120 |
+
}
|
| 12121 |
+
if (data.checkpoint) bits.push(esc(data.checkpoint));
|
| 12122 |
+
if (typeof data.elapsed_s === 'number') bits.push(`${data.elapsed_s}s`);
|
| 12123 |
+
if (meta) meta.textContent = bits.join(' Β· ');
|
| 12124 |
+
|
| 12125 |
+
let html = '';
|
| 12126 |
+
if (scores.length) {
|
| 12127 |
+
// Scale bars against the largest |delta| in THIS result set, and
|
| 12128 |
+
// say so β a bar with an unstated scale invites reading it as an
|
| 12129 |
+
// absolute effect size.
|
| 12130 |
+
const max = Math.max.apply(null, scores.map((s) => Math.abs(s.delta_ll || 0))) || 1;
|
| 12131 |
+
html += '<table class="dna-table"><thead><tr>'
|
| 12132 |
+
+ '<th>Variant</th><th>Δ log-likelihood</th><th>Effect</th>'
|
| 12133 |
+
+ '</tr></thead><tbody>';
|
| 12134 |
+
scores.forEach((s) => {
|
| 12135 |
+
const d = Number(s.delta_ll || 0);
|
| 12136 |
+
const pct = Math.round(100 * Math.abs(d) / max);
|
| 12137 |
+
const dir = d < 0 ? 'neg' : 'pos';
|
| 12138 |
+
html += `<tr><td class="mono">${esc(s.label)}</td>`
|
| 12139 |
+
+ `<td class="mono dna-delta dna-delta--${dir}">${d.toFixed(4)}</td>`
|
| 12140 |
+
+ `<td><span class="dna-bar dna-bar--${dir}" style="width:${pct}%"></span></td></tr>`;
|
| 12141 |
+
});
|
| 12142 |
+
html += '</tbody></table>';
|
| 12143 |
+
html += '<p class="dna-scale-note">Bars are relative to the largest '
|
| 12144 |
+
+ 'effect in this set, not an absolute scale. Negative = the '
|
| 12145 |
+
+ 'model finds the change less likely than what\'s there now.</p>';
|
| 12146 |
+
}
|
| 12147 |
+
|
| 12148 |
+
if (skipped.length) {
|
| 12149 |
+
// Never fold these into "no result". A refused variant is a
|
| 12150 |
+
// statement about the INPUT, and hiding it would let a typo look
|
| 12151 |
+
// like a shorter answer.
|
| 12152 |
+
html += '<div class="dna-skipped"><p class="dna-skipped-hd">'
|
| 12153 |
+
+ `Not scored (${skipped.length})</p><ul>`;
|
| 12154 |
+
skipped.forEach((s) => {
|
| 12155 |
+
const label = esc(s.label || s.variant || s);
|
| 12156 |
+
const why = s.why || s.reason || 'the reference base at that position does not match';
|
| 12157 |
+
html += `<li><span class="mono">${label}</span> β ${esc(why)}</li>`;
|
| 12158 |
+
});
|
| 12159 |
+
html += '</ul><p class="dna-skipped-why">These were refused rather than '
|
| 12160 |
+
+ 'scored: positions are 1-based against the reference above, and '
|
| 12161 |
+
+ 'a mismatched WT base usually means an off-by-one or the wrong '
|
| 12162 |
+
+ 'strand.</p></div>';
|
| 12163 |
+
}
|
| 12164 |
+
|
| 12165 |
+
if (!scores.length && !skipped.length) {
|
| 12166 |
+
html = '<p class="dna-empty">The model returned no scores for that request.</p>';
|
| 12167 |
+
}
|
| 12168 |
+
|
| 12169 |
+
body.innerHTML = html;
|
| 12170 |
+
card.hidden = false;
|
| 12171 |
+
}
|
| 12172 |
+
|
| 12173 |
+
function renderGenerated(data) {
|
| 12174 |
+
const card = $('dnaResultsCard');
|
| 12175 |
+
const body = $('dnaResultsBody');
|
| 12176 |
+
const meta = $('dnaResultsMeta');
|
| 12177 |
+
if (!card || !body) return;
|
| 12178 |
+
const seq = String(data.sequence || data.generated || '');
|
| 12179 |
+
const bits = [];
|
| 12180 |
+
if (seq) bits.push(`${seq.length.toLocaleString()} nt generated`);
|
| 12181 |
+
if (data.checkpoint) bits.push(esc(data.checkpoint));
|
| 12182 |
+
if (typeof data.elapsed_s === 'number') bits.push(`${data.elapsed_s}s`);
|
| 12183 |
+
if (meta) meta.textContent = bits.join(' Β· ');
|
| 12184 |
+
body.innerHTML = seq
|
| 12185 |
+
? `<pre class="dna-generated mono">${esc(seq.replace(/(.{60})/g, '$1\n'))}</pre>`
|
| 12186 |
+
+ '<p class="dna-scale-note">Sampled sequence β plausible to the model, '
|
| 12187 |
+
+ 'not validated. Nothing here has been checked for function, '
|
| 12188 |
+
+ 'synthesisability, or anything else.</p>'
|
| 12189 |
+
: '<p class="dna-empty">The model returned no sequence.</p>';
|
| 12190 |
+
card.hidden = false;
|
| 12191 |
+
}
|
| 12192 |
+
|
| 12193 |
+
// ββ run ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 12194 |
+
async function run() {
|
| 12195 |
+
if (busy) return;
|
| 12196 |
+
clearError();
|
| 12197 |
+
hideResults();
|
| 12198 |
+
|
| 12199 |
+
const tier = ($('dnaTier') && $('dnaTier').value) || '';
|
| 12200 |
+
if (!tier) { showError('No DNA model tier is available on this deployment.'); return; }
|
| 12201 |
+
|
| 12202 |
+
let url, payload;
|
| 12203 |
+
if (mode === 'score') {
|
| 12204 |
+
const reference = cleanSeq($('dnaReference') && $('dnaReference').value);
|
| 12205 |
+
const variants = String(($('dnaVariants') && $('dnaVariants').value) || '')
|
| 12206 |
+
.split(/[\n,;]+/).map((v) => v.trim().toUpperCase()).filter(Boolean);
|
| 12207 |
+
if (!reference) { showError('Paste a reference sequence first.'); return; }
|
| 12208 |
+
if (!variants.length) { showError('Add at least one variant, like T10G.'); return; }
|
| 12209 |
+
url = '/api/dna/score';
|
| 12210 |
+
payload = { reference, variants, tier };
|
| 12211 |
+
} else {
|
| 12212 |
+
const prompt = cleanSeq($('dnaPrompt') && $('dnaPrompt').value);
|
| 12213 |
+
if (!prompt) { showError('Paste a starting sequence first.'); return; }
|
| 12214 |
+
url = '/api/dna/generate';
|
| 12215 |
+
payload = {
|
| 12216 |
+
prompt, tier,
|
| 12217 |
+
n_tokens: Number(($('dnaNTokens') || {}).value || 200),
|
| 12218 |
+
temperature: Number(($('dnaTemperature') || {}).value || 1.0),
|
| 12219 |
+
top_k: Number(($('dnaTopK') || {}).value || 4),
|
| 12220 |
+
};
|
| 12221 |
+
}
|
| 12222 |
+
|
| 12223 |
+
const btn = $('dnaRun');
|
| 12224 |
+
busy = true;
|
| 12225 |
+
if (btn) btn.disabled = true;
|
| 12226 |
+
startWait();
|
| 12227 |
+
try {
|
| 12228 |
+
const res = await fetch(url, {
|
| 12229 |
+
method: 'POST',
|
| 12230 |
+
headers: { 'Content-Type': 'application/json' },
|
| 12231 |
+
body: JSON.stringify(payload),
|
| 12232 |
+
});
|
| 12233 |
+
const data = await res.json();
|
| 12234 |
+
if (!res.ok) {
|
| 12235 |
+
if (data && data.kind === 'signin_required') {
|
| 12236 |
+
window.dispatchEvent(new Event('td:signin-required'));
|
| 12237 |
+
return; // the modal IS the message
|
| 12238 |
+
}
|
| 12239 |
+
throw new Error(data.error || 'The DNA model call failed.');
|
| 12240 |
+
}
|
| 12241 |
+
if (mode === 'score') renderScores(data);
|
| 12242 |
+
else renderGenerated(data);
|
| 12243 |
+
} catch (err) {
|
| 12244 |
+
showError((err && err.message) || String(err));
|
| 12245 |
+
} finally {
|
| 12246 |
+
busy = false;
|
| 12247 |
+
stopWait();
|
| 12248 |
+
syncTier();
|
| 12249 |
+
}
|
| 12250 |
+
}
|
| 12251 |
+
|
| 12252 |
+
// ββ wire βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 12253 |
+
function init() {
|
| 12254 |
+
if (!view()) return;
|
| 12255 |
+
const on = (id, ev, fn) => { const el = $(id); if (el) el.addEventListener(ev, fn); };
|
| 12256 |
+
on('dnaModeScore', 'click', () => setMode('score'));
|
| 12257 |
+
on('dnaModeGenerate', 'click', () => setMode('generate'));
|
| 12258 |
+
on('dnaTier', 'change', syncTier);
|
| 12259 |
+
on('dnaRun', 'click', run);
|
| 12260 |
+
on('dnaReference', 'input', refMeta);
|
| 12261 |
+
on('dnaExample', 'click', () => {
|
| 12262 |
+
setMode('score');
|
| 12263 |
+
const r = $('dnaReference'), v = $('dnaVariants');
|
| 12264 |
+
if (r) r.value = EXAMPLE_REF;
|
| 12265 |
+
if (v) v.value = EXAMPLE_VARIANTS;
|
| 12266 |
+
refMeta();
|
| 12267 |
+
clearError();
|
| 12268 |
+
});
|
| 12269 |
+
loadTiers();
|
| 12270 |
+
}
|
| 12271 |
+
|
| 12272 |
+
if (document.readyState === 'loading') {
|
| 12273 |
+
document.addEventListener('DOMContentLoaded', init);
|
| 12274 |
+
} else {
|
| 12275 |
+
init();
|
| 12276 |
+
}
|
| 12277 |
+
}());
|
dee/static/index.html
CHANGED
|
@@ -112,7 +112,7 @@
|
|
| 112 |
<!-- ?v= query bumps invalidate browser + iframe asset caches when app.css /
|
| 113 |
app.js change. Bump these numbers whenever you ship a frontend update β
|
| 114 |
without them, users keep getting the stale file for up to a week. -->
|
| 115 |
-
<link rel="stylesheet" href="/static/app.css?v=
|
| 116 |
<!-- The work catalog + the draggable rail. Kept out of app.css so two new
|
| 117 |
self-contained surfaces stay reviewable; every colour is an app.css
|
| 118 |
token, so both themes work with nothing added. -->
|
|
@@ -250,10 +250,11 @@
|
|
| 250 |
</a>
|
| 251 |
|
| 252 |
<!--
|
| 253 |
-
THE LOOP β the
|
| 254 |
scattered tab-list. A hairline spine (.nav-loop::before) ties
|
| 255 |
them together; each name carries its loop phase on the right.
|
| 256 |
Β· Directed Evolution β ESM-2 zero-shot variant libraries (Design)
|
|
|
|
| 257 |
Β· Plasmid Editor β map / annotate / clone a construct (Build)
|
| 258 |
Β· CRISPR β knockout & base-edit guide design (Edit)
|
| 259 |
Β· Primer Analysis β score & rank candidate primers (Verify)
|
|
@@ -273,6 +274,26 @@
|
|
| 273 |
<span class="nav-step-nm">Directed Evolution</span>
|
| 274 |
<span class="nav-step-ph">Design</span>
|
| 275 |
</a>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 276 |
<a class="nav-item nav-step" href="#plasmid" data-analytics="nav-plasmid" title="Plasmid Editor">
|
| 277 |
<span class="nav-icon" aria-hidden="true">
|
| 278 |
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
|
@@ -507,6 +528,7 @@
|
|
| 507 |
<nav class="bench-tabs" id="benchTabs" aria-label="Construct artifacts">
|
| 508 |
<button class="bench-tab" data-route="structure" data-analytics="bench-tab-structure" type="button">Structure</button>
|
| 509 |
<button class="bench-tab" data-route="design" data-analytics="bench-tab-design" type="button">Library</button>
|
|
|
|
| 510 |
<button class="bench-tab" data-route="plasmid" data-analytics="bench-tab-plasmid" type="button">Map</button>
|
| 511 |
<button class="bench-tab" data-route="crispr" data-analytics="bench-tab-crispr" type="button">Guides</button>
|
| 512 |
<button class="bench-tab" data-route="primers" data-analytics="bench-tab-primers" type="button">Primers</button>
|
|
@@ -1104,6 +1126,119 @@
|
|
| 1104 |
by the per-user /dashboard/ page on the landing site
|
| 1105 |
(task #98). See sidebar comment above for context. -->
|
| 1106 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1107 |
<!-- βββββββββββββββββββββββββββββ CRISPR view (Cas9 knockout) βββ
|
| 1108 |
Paste a gene β ranked SpCas9 sgRNAs with on-target
|
| 1109 |
scoring. Sign-in gated server-side; anonymous users
|
|
@@ -2128,6 +2263,35 @@
|
|
| 2128 |
</ul>
|
| 2129 |
</section>
|
| 2130 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2131 |
<section>
|
| 2132 |
<h3>Directed Evolution</h3>
|
| 2133 |
<p>Design a smart mutation library for an existing protein using ESM-2 zero-shot scoring. Provide a wild-type sequence (or send a CDS from the Plasmid Editor); receive a library of multi-mutant variants ranked by predicted evolutionary fitness, codon-optimized for your host, ready to order.</p>
|
|
@@ -2561,7 +2725,7 @@
|
|
| 2561 |
<!-- Cloning reference data must load before app.js so the Designer
|
| 2562 |
can read VECTORS / ENZYMES / CLONING_METHODS / TAGS / LINKERS. -->
|
| 2563 |
<script src="/static/cloning_db.js?v=20260530-ui-polish" defer></script>
|
| 2564 |
-
<script src="/static/app.js?v=
|
| 2565 |
<!-- The decision trace, BEFORE cockpit.js: applyEvent calls TDTrace.push
|
| 2566 |
on the very first event, and both are `defer`, so document order is
|
| 2567 |
load order. Loading it after would drop the opening events of a
|
|
|
|
| 112 |
<!-- ?v= query bumps invalidate browser + iframe asset caches when app.css /
|
| 113 |
app.js change. Bump these numbers whenever you ship a frontend update β
|
| 114 |
without them, users keep getting the stale file for up to a week. -->
|
| 115 |
+
<link rel="stylesheet" href="/static/app.css?v=20260812-dna" />
|
| 116 |
<!-- The work catalog + the draggable rail. Kept out of app.css so two new
|
| 117 |
self-contained surfaces stay reviewable; every colour is an app.css
|
| 118 |
token, so both themes work with nothing added. -->
|
|
|
|
| 250 |
</a>
|
| 251 |
|
| 252 |
<!--
|
| 253 |
+
THE LOOP β the tools as one ordered workflow, not a
|
| 254 |
scattered tab-list. A hairline spine (.nav-loop::before) ties
|
| 255 |
them together; each name carries its loop phase on the right.
|
| 256 |
Β· Directed Evolution β ESM-2 zero-shot variant libraries (Design)
|
| 257 |
+
Β· DNA Design β Evo 2 zero-shot nucleotide scoring (Design)
|
| 258 |
Β· Plasmid Editor β map / annotate / clone a construct (Build)
|
| 259 |
Β· CRISPR β knockout & base-edit guide design (Edit)
|
| 260 |
Β· Primer Analysis β score & rank candidate primers (Verify)
|
|
|
|
| 274 |
<span class="nav-step-nm">Directed Evolution</span>
|
| 275 |
<span class="nav-step-ph">Design</span>
|
| 276 |
</a>
|
| 277 |
+
<!--
|
| 278 |
+
DNA Design sits BESIDE Directed Evolution, not as a
|
| 279 |
+
fifth phase: DE designs at the PROTEIN level (ESM-2),
|
| 280 |
+
this designs at the DNA level (Evo 2). Both are the
|
| 281 |
+
loop's "Design" step, which is why they share a phase
|
| 282 |
+
label β inventing a fifth phase here would break the
|
| 283 |
+
Design/Build/Edit/Verify story the landing page and the
|
| 284 |
+
Start Here router are both built on.
|
| 285 |
+
-->
|
| 286 |
+
<a class="nav-item nav-step" href="#dna" data-analytics="nav-dna" title="DNA Design">
|
| 287 |
+
<span class="nav-icon" aria-hidden="true">
|
| 288 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
| 289 |
+
<path d="M5 3c0 5 14 6 14 9s-14 4-14 9"/>
|
| 290 |
+
<path d="M19 3c0 5-14 6-14 9s14 4 14 9"/>
|
| 291 |
+
<path d="M8 6h8M7 9.6h10M7 14.4h10M8 18h8"/>
|
| 292 |
+
</svg>
|
| 293 |
+
</span>
|
| 294 |
+
<span class="nav-step-nm">DNA Design</span>
|
| 295 |
+
<span class="nav-step-ph">Design</span>
|
| 296 |
+
</a>
|
| 297 |
<a class="nav-item nav-step" href="#plasmid" data-analytics="nav-plasmid" title="Plasmid Editor">
|
| 298 |
<span class="nav-icon" aria-hidden="true">
|
| 299 |
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
|
|
|
| 528 |
<nav class="bench-tabs" id="benchTabs" aria-label="Construct artifacts">
|
| 529 |
<button class="bench-tab" data-route="structure" data-analytics="bench-tab-structure" type="button">Structure</button>
|
| 530 |
<button class="bench-tab" data-route="design" data-analytics="bench-tab-design" type="button">Library</button>
|
| 531 |
+
<button class="bench-tab" data-route="dna" data-analytics="bench-tab-dna" type="button">DNA</button>
|
| 532 |
<button class="bench-tab" data-route="plasmid" data-analytics="bench-tab-plasmid" type="button">Map</button>
|
| 533 |
<button class="bench-tab" data-route="crispr" data-analytics="bench-tab-crispr" type="button">Guides</button>
|
| 534 |
<button class="bench-tab" data-route="primers" data-analytics="bench-tab-primers" type="button">Primers</button>
|
|
|
|
| 1126 |
by the per-user /dashboard/ page on the landing site
|
| 1127 |
(task #98). See sidebar comment above for context. -->
|
| 1128 |
|
| 1129 |
+
<!-- βββββββββββββββββββββββββββββ DNA Design (Evo 2) ββββββββββββ
|
| 1130 |
+
The DNA-level counterpart to Directed Evolution. DE scores
|
| 1131 |
+
amino-acid substitutions with ESM-2; this scores NUCLEOTIDE
|
| 1132 |
+
substitutions with Evo 2 (7B), which is the only thing in
|
| 1133 |
+
this product that understands non-coding and regulatory
|
| 1134 |
+
sequence at all β promoters.py is a curated lookup table and
|
| 1135 |
+
exon.py is coordinate arithmetic, neither is a model.
|
| 1136 |
+
|
| 1137 |
+
Every call here is a metered GPU call: unlike protein's
|
| 1138 |
+
Achilles tier (35M on the Space's own CPU) there is no free
|
| 1139 |
+
local path for a 7B model. Hence the account gate, the
|
| 1140 |
+
tighter rate-limit bucket, and the honest wait copy β a cold
|
| 1141 |
+
container is ~80s, warm ~23s, and a dead spinner over that
|
| 1142 |
+
long reads as a hang.
|
| 1143 |
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ -->
|
| 1144 |
+
<section class="view view--dna" data-view="dna" hidden>
|
| 1145 |
+
<header class="view-head">
|
| 1146 |
+
<h1 class="view-title"><em>DNA Design</em></h1>
|
| 1147 |
+
<p class="view-sub">Score changes to DNA itself β promoters, splice sites, UTRs,
|
| 1148 |
+
any non-coding stretch β against a genome foundation model. Directed Evolution
|
| 1149 |
+
asks whether a protein change is tolerated; this asks whether a
|
| 1150 |
+
<em>sequence</em> change is, in the context it actually sits in.</p>
|
| 1151 |
+
</header>
|
| 1152 |
+
|
| 1153 |
+
<section class="card dna-input-card">
|
| 1154 |
+
<div class="dna-mode-row" role="tablist" aria-label="What to do">
|
| 1155 |
+
<button class="dna-mode is-active" type="button" id="dnaModeScore"
|
| 1156 |
+
role="tab" aria-selected="true">Score variants</button>
|
| 1157 |
+
<button class="dna-mode" type="button" id="dnaModeGenerate"
|
| 1158 |
+
role="tab" aria-selected="false">Generate sequence</button>
|
| 1159 |
+
</div>
|
| 1160 |
+
|
| 1161 |
+
<!-- ββ Score ββ -->
|
| 1162 |
+
<div id="dnaScorePane">
|
| 1163 |
+
<label class="field-label" for="dnaReference">Reference sequence</label>
|
| 1164 |
+
<p class="field-hint">The stretch of DNA your change sits in β a promoter, a
|
| 1165 |
+
splice junction, a UTR. Context is the point: the same substitution scores
|
| 1166 |
+
differently depending on what surrounds it, which is exactly what a lookup
|
| 1167 |
+
table cannot tell you.</p>
|
| 1168 |
+
<textarea id="dnaReference" class="dna-textarea mono" rows="5" spellcheck="false"
|
| 1169 |
+
placeholder="Paste DNA (A/C/G/T)β¦"></textarea>
|
| 1170 |
+
<p class="dna-meta" id="dnaRefMeta" hidden></p>
|
| 1171 |
+
|
| 1172 |
+
<label class="field-label" for="dnaVariants">Variants to score</label>
|
| 1173 |
+
<p class="field-hint">One per line, as <code><WT base><position><new base></code>
|
| 1174 |
+
β e.g. <code>T10G</code>. Positions are 1-based against the reference above.
|
| 1175 |
+
A variant whose WT base doesn't match the reference is <strong>refused, not
|
| 1176 |
+
scored</strong>, and listed separately so a typo can't quietly shrink your table.</p>
|
| 1177 |
+
<textarea id="dnaVariants" class="dna-textarea mono" rows="4" spellcheck="false"
|
| 1178 |
+
placeholder="T10G G20C T30A"></textarea>
|
| 1179 |
+
</div>
|
| 1180 |
+
|
| 1181 |
+
<!-- ββ Generate ββ -->
|
| 1182 |
+
<div id="dnaGeneratePane" hidden>
|
| 1183 |
+
<label class="field-label" for="dnaPrompt">Starting sequence</label>
|
| 1184 |
+
<p class="field-hint">The model continues from what you paste. Prometheus tier only β
|
| 1185 |
+
generation is the one real capability difference between the DNA tiers
|
| 1186 |
+
(the checkpoint is identical).</p>
|
| 1187 |
+
<textarea id="dnaPrompt" class="dna-textarea mono" rows="5" spellcheck="false"
|
| 1188 |
+
placeholder="Paste the DNA to continue from (A/C/G/T)β¦"></textarea>
|
| 1189 |
+
<div class="dna-gen-knobs">
|
| 1190 |
+
<label class="dna-knob">Bases to generate
|
| 1191 |
+
<input type="number" id="dnaNTokens" value="200" min="1" max="2000" />
|
| 1192 |
+
</label>
|
| 1193 |
+
<label class="dna-knob">Temperature
|
| 1194 |
+
<input type="number" id="dnaTemperature" value="1.0" min="0.1" max="2" step="0.1" />
|
| 1195 |
+
<span class="field-hint">Lower = more conservative.</span>
|
| 1196 |
+
</label>
|
| 1197 |
+
<label class="dna-knob">Top-k
|
| 1198 |
+
<input type="number" id="dnaTopK" value="4" min="1" max="4" />
|
| 1199 |
+
<span class="field-hint">There are only 4 bases.</span>
|
| 1200 |
+
</label>
|
| 1201 |
+
</div>
|
| 1202 |
+
</div>
|
| 1203 |
+
|
| 1204 |
+
<label class="field-label" for="dnaTier">Model tier</label>
|
| 1205 |
+
<select id="dnaTier" class="dna-select"></select>
|
| 1206 |
+
<p class="field-hint" id="dnaTierNote"></p>
|
| 1207 |
+
|
| 1208 |
+
<div class="dna-actions">
|
| 1209 |
+
<button class="ghost" type="button" id="dnaExample"
|
| 1210 |
+
title="Loads the E. coli lac promoter / operator region and three real substitutions">Try an example</button>
|
| 1211 |
+
<button class="primary primary-lg" type="button" id="dnaRun">Score variants</button>
|
| 1212 |
+
</div>
|
| 1213 |
+
<p class="dna-wait" id="dnaWait" hidden></p>
|
| 1214 |
+
<div class="error-banner" id="dnaError" hidden></div>
|
| 1215 |
+
</section>
|
| 1216 |
+
|
| 1217 |
+
<section class="card dna-results-card" id="dnaResultsCard" hidden>
|
| 1218 |
+
<div class="dna-results-head">
|
| 1219 |
+
<span class="dna-results-title">Predicted effect</span>
|
| 1220 |
+
<span class="dna-results-meta" id="dnaResultsMeta"></span>
|
| 1221 |
+
</div>
|
| 1222 |
+
<details class="how-to-read">
|
| 1223 |
+
<summary>How to read this</summary>
|
| 1224 |
+
<div class="how-to-read-body">
|
| 1225 |
+
<p><strong>Δ log-likelihood</strong> — how much more, or less, likely the
|
| 1226 |
+
model finds your sequence after the change. <strong>Negative</strong> means the
|
| 1227 |
+
model is more surprised by the variant than by the reference: the change breaks a
|
| 1228 |
+
pattern the model learned from real genomes. <strong>Near zero</strong> means
|
| 1229 |
+
it's unremarkable. <strong>Positive</strong> means the variant looks
|
| 1230 |
+
<em>more</em> typical than what's there now.</p>
|
| 1231 |
+
<p>This is <strong>zero-shot</strong> β no training on your system, no measured data.
|
| 1232 |
+
It ranks candidates; it does not predict expression level, and it is not a
|
| 1233 |
+
substitute for a reporter assay. Treat it as a prior for what to test first.</p>
|
| 1234 |
+
<p><strong>Reference log-likelihood</strong> is the model's score for your unchanged
|
| 1235 |
+
sequence, shown so the deltas have something to sit against.</p>
|
| 1236 |
+
</div>
|
| 1237 |
+
</details>
|
| 1238 |
+
<div id="dnaResultsBody"></div>
|
| 1239 |
+
</section>
|
| 1240 |
+
</section>
|
| 1241 |
+
|
| 1242 |
<!-- βββββββββββββββββββββββββββββ CRISPR view (Cas9 knockout) βββ
|
| 1243 |
Paste a gene β ranked SpCas9 sgRNAs with on-target
|
| 1244 |
scoring. Sign-in gated server-side; anonymous users
|
|
|
|
| 2263 |
</ul>
|
| 2264 |
</section>
|
| 2265 |
|
| 2266 |
+
<section>
|
| 2267 |
+
<h3>DNA Design</h3>
|
| 2268 |
+
<p>The DNA-level counterpart to Directed Evolution. Everything else here works on
|
| 2269 |
+
protein (ESM-2) or on deterministic sequence bookkeeping; this is the only tool
|
| 2270 |
+
that reads <em>nucleotides</em> with a learned model β <strong>Evo 2</strong>
|
| 2271 |
+
(Arc Institute, 7B, Apache 2.0), trained autoregressively on genomes.</p>
|
| 2272 |
+
<ul class="docs-list">
|
| 2273 |
+
<li><strong>Scoring.</strong> Ξ log-likelihood for each point substitution against
|
| 2274 |
+
a reference you supply: how much more, or less, likely the model finds the
|
| 2275 |
+
sequence after your change. Negative = the change breaks a pattern the model
|
| 2276 |
+
learned from real genomes.</li>
|
| 2277 |
+
<li><strong>Context is the point.</strong> The same substitution scores differently
|
| 2278 |
+
depending on what surrounds it. That is what a curated parts table cannot do,
|
| 2279 |
+
and it is why this works on non-coding sequence β promoters, splice sites,
|
| 2280 |
+
UTRs, terminators β that the rest of the toolkit only annotates.</li>
|
| 2281 |
+
<li><strong>Generation.</strong> Continues a sequence you paste. Prometheus tier
|
| 2282 |
+
only; the checkpoint is identical on both tiers, generation access is the
|
| 2283 |
+
difference.</li>
|
| 2284 |
+
<li><strong>Refusals are shown.</strong> A variant whose wild-type base disagrees
|
| 2285 |
+
with your reference is listed as not scored, with the reason β never folded
|
| 2286 |
+
into a quietly shorter table.</li>
|
| 2287 |
+
</ul>
|
| 2288 |
+
<p class="docs-callout">Zero-shot: no training on your system and no measured data. It
|
| 2289 |
+
ranks candidates and gives you a prior for what to test first β it does
|
| 2290 |
+
<strong>not</strong> predict expression level and does not replace a reporter
|
| 2291 |
+
assay. Every call runs on a rented GPU; unlike protein's Achilles tier there is no
|
| 2292 |
+
free local path for a 7B model, so DNA scoring needs an account.</p>
|
| 2293 |
+
</section>
|
| 2294 |
+
|
| 2295 |
<section>
|
| 2296 |
<h3>Directed Evolution</h3>
|
| 2297 |
<p>Design a smart mutation library for an existing protein using ESM-2 zero-shot scoring. Provide a wild-type sequence (or send a CDS from the Plasmid Editor); receive a library of multi-mutant variants ranked by predicted evolutionary fitness, codon-optimized for your host, ready to order.</p>
|
|
|
|
| 2725 |
<!-- Cloning reference data must load before app.js so the Designer
|
| 2726 |
can read VECTORS / ENZYMES / CLONING_METHODS / TAGS / LINKERS. -->
|
| 2727 |
<script src="/static/cloning_db.js?v=20260530-ui-polish" defer></script>
|
| 2728 |
+
<script src="/static/app.js?v=20260812-dna" defer></script>
|
| 2729 |
<!-- The decision trace, BEFORE cockpit.js: applyEvent calls TDTrace.push
|
| 2730 |
on the very first event, and both are `defer`, so document order is
|
| 2731 |
load order. Loading it after would drop the opening events of a
|
tests/test_dna_endpoints.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""/api/dna/score and /api/dna/generate β the DNA view's only way in.
|
| 2 |
+
|
| 3 |
+
Before these existed, the ONLY route to Evo 2 was the agent choosing to call
|
| 4 |
+
score_or_generate_dna mid-conversation: /api/models computed `dna_models` and
|
| 5 |
+
no frontend read it, so a 7B genome model was unreachable by clicking.
|
| 6 |
+
|
| 7 |
+
What these pin is the same contract the rest of the tool endpoints keep:
|
| 8 |
+
never run anonymously, never pretend a tier can do something it cannot, and
|
| 9 |
+
never dress an unavailable backend as a scientific result.
|
| 10 |
+
"""
|
| 11 |
+
import pytest
|
| 12 |
+
|
| 13 |
+
from dee import server
|
| 14 |
+
from dee.core import dna_scoring
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@pytest.fixture
|
| 18 |
+
def client():
|
| 19 |
+
app = server.create_app()
|
| 20 |
+
app.config.update(TESTING=True)
|
| 21 |
+
return app.test_client()
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
@pytest.fixture
|
| 25 |
+
def signed_in(monkeypatch):
|
| 26 |
+
"""Every DNA call is a metered GPU call, so both routes are account-gated
|
| 27 |
+
the same way CRISPR and primers are."""
|
| 28 |
+
class _Auth:
|
| 29 |
+
anonymous = False
|
| 30 |
+
user_id = "11111111-1111-1111-1111-111111111111"
|
| 31 |
+
monkeypatch.setattr(server._auth, "get_auth", lambda *a, **k: _Auth())
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
REF = "TTTACACTTTATGCTTCCGGCTCGTATGTTGTGTGGAATTGTGAGCGGATAACAATTTCA"
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
# ββ the account gate βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 38 |
+
def test_scoring_refuses_anonymous_callers(client, monkeypatch):
|
| 39 |
+
class _Anon:
|
| 40 |
+
anonymous = True
|
| 41 |
+
user_id = None
|
| 42 |
+
monkeypatch.setattr(server._auth, "get_auth", lambda *a, **k: _Anon())
|
| 43 |
+
r = client.post("/api/dna/score", json={"reference": REF, "variants": ["T10G"]})
|
| 44 |
+
assert r.status_code == 403
|
| 45 |
+
assert r.get_json()["kind"] == "signin_required"
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def test_generation_refuses_anonymous_callers(client, monkeypatch):
|
| 49 |
+
class _Anon:
|
| 50 |
+
anonymous = True
|
| 51 |
+
user_id = None
|
| 52 |
+
monkeypatch.setattr(server._auth, "get_auth", lambda *a, **k: _Anon())
|
| 53 |
+
r = client.post("/api/dna/generate", json={"prompt": REF})
|
| 54 |
+
assert r.status_code == 403
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
# ββ input validation, before anything is billed ββββββββββββββββββββββββββ
|
| 58 |
+
def test_a_missing_reference_is_a_400_not_a_gpu_call(client, signed_in, monkeypatch):
|
| 59 |
+
def boom(*a, **k):
|
| 60 |
+
raise AssertionError("called the GPU for an empty request")
|
| 61 |
+
monkeypatch.setattr(dna_scoring, "score_dna_variants", boom)
|
| 62 |
+
assert client.post("/api/dna/score", json={"variants": ["T10G"]}).status_code == 400
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def test_empty_variants_is_a_400_not_a_gpu_call(client, signed_in, monkeypatch):
|
| 66 |
+
def boom(*a, **k):
|
| 67 |
+
raise AssertionError("called the GPU for an empty request")
|
| 68 |
+
monkeypatch.setattr(dna_scoring, "score_dna_variants", boom)
|
| 69 |
+
r = client.post("/api/dna/score", json={"reference": REF, "variants": []})
|
| 70 |
+
assert r.status_code == 400
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def test_whitespace_and_case_are_normalised(client, signed_in, monkeypatch):
|
| 74 |
+
"""Users paste FASTA-ish text with newlines; the model wants bases."""
|
| 75 |
+
seen = {}
|
| 76 |
+
|
| 77 |
+
def fake(reference, variants, tier="achilles"):
|
| 78 |
+
seen["reference"] = reference
|
| 79 |
+
seen["variants"] = variants
|
| 80 |
+
return {"ok": True, "scores": [], "skipped": [], "reference_ll": -1.0}
|
| 81 |
+
|
| 82 |
+
monkeypatch.setattr(dna_scoring, "score_dna_variants", fake)
|
| 83 |
+
client.post("/api/dna/score", json={
|
| 84 |
+
"reference": "ttta cact\ntta", "variants": [" t10g "]})
|
| 85 |
+
assert seen["reference"] == "TTTACACTTTA"
|
| 86 |
+
assert seen["variants"] == ["T10G"]
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
# ββ the honesty contract βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 90 |
+
def test_an_unavailable_backend_is_a_503_not_an_empty_result(client, signed_in,
|
| 91 |
+
monkeypatch):
|
| 92 |
+
"""The failure this whole module exists to prevent: reporting "no variants
|
| 93 |
+
scored" when the truth is "nothing was asked"."""
|
| 94 |
+
def unavailable(*a, **k):
|
| 95 |
+
raise dna_scoring.DnaModelUnavailable("DNA scoring isn't switched on.")
|
| 96 |
+
monkeypatch.setattr(dna_scoring, "score_dna_variants", unavailable)
|
| 97 |
+
r = client.post("/api/dna/score", json={"reference": REF, "variants": ["T10G"]})
|
| 98 |
+
assert r.status_code == 503
|
| 99 |
+
assert r.get_json()["kind"] == "dna_unavailable"
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def test_generation_on_the_achilles_tier_is_refused(client, signed_in):
|
| 103 |
+
"""Prometheus-only, enforced here AND in dna_scoring AND again in
|
| 104 |
+
modal/evo2_scoring β a caller that skips a layer still gets refused."""
|
| 105 |
+
r = client.post("/api/dna/generate", json={"prompt": REF, "tier": "achilles"})
|
| 106 |
+
assert r.status_code == 503
|
| 107 |
+
assert "Prometheus" in r.get_json()["error"]
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def test_a_backend_crash_never_reports_a_scientific_result(client, signed_in,
|
| 111 |
+
monkeypatch):
|
| 112 |
+
def boom(*a, **k):
|
| 113 |
+
raise RuntimeError("connection reset")
|
| 114 |
+
monkeypatch.setattr(dna_scoring, "score_dna_variants", boom)
|
| 115 |
+
r = client.post("/api/dna/score", json={"reference": REF, "variants": ["T10G"]})
|
| 116 |
+
assert r.status_code == 502
|
| 117 |
+
body = r.get_json()
|
| 118 |
+
assert body["kind"] == "dna_error"
|
| 119 |
+
assert "connection reset" not in body["error"], "no raw exception text to users"
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def test_a_successful_score_is_passed_through_whole(client, signed_in, monkeypatch):
|
| 123 |
+
"""Including `skipped`. A variant whose WT base disagrees with the
|
| 124 |
+
reference is REFUSED, not scored β dropping that list would turn a
|
| 125 |
+
refusal into a silently shorter table."""
|
| 126 |
+
payload = {"ok": True, "reference_ll": -147.9081,
|
| 127 |
+
"scores": [{"label": "T10G", "delta_ll": -5.5079}],
|
| 128 |
+
"skipped": [{"label": "A10G", "why": "reference base is T"}],
|
| 129 |
+
"tier": "achilles", "checkpoint": "evo2_7b"}
|
| 130 |
+
monkeypatch.setattr(dna_scoring, "score_dna_variants", lambda *a, **k: payload)
|
| 131 |
+
r = client.post("/api/dna/score",
|
| 132 |
+
json={"reference": REF, "variants": ["T10G", "A10G"]})
|
| 133 |
+
assert r.status_code == 200
|
| 134 |
+
got = r.get_json()
|
| 135 |
+
assert got["scores"][0]["delta_ll"] == -5.5079
|
| 136 |
+
assert got["skipped"], "the refusal list must survive to the UI"
|
| 137 |
+
assert got["reference_ll"] == -147.9081
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def test_generation_forwards_its_sampling_knobs(client, signed_in, monkeypatch):
|
| 141 |
+
seen = {}
|
| 142 |
+
|
| 143 |
+
def fake(prompt, n_tokens=200, tier="prometheus", temperature=1.0, top_k=4):
|
| 144 |
+
seen.update(prompt=prompt, n_tokens=n_tokens, tier=tier,
|
| 145 |
+
temperature=temperature, top_k=top_k)
|
| 146 |
+
return {"ok": True, "sequence": "ACGT"}
|
| 147 |
+
|
| 148 |
+
monkeypatch.setattr(dna_scoring, "generate_dna_sequence", fake)
|
| 149 |
+
client.post("/api/dna/generate", json={
|
| 150 |
+
"prompt": REF, "n_tokens": 64, "temperature": 0.8, "top_k": 3})
|
| 151 |
+
assert seen["n_tokens"] == 64 and seen["top_k"] == 3
|
| 152 |
+
assert seen["temperature"] == 0.8
|
| 153 |
+
assert seen["tier"] == "prometheus"
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def test_non_numeric_sampling_knobs_are_a_400(client, signed_in, monkeypatch):
|
| 157 |
+
def boom(*a, **k):
|
| 158 |
+
raise AssertionError("called the GPU with junk parameters")
|
| 159 |
+
monkeypatch.setattr(dna_scoring, "generate_dna_sequence", boom)
|
| 160 |
+
r = client.post("/api/dna/generate", json={"prompt": REF, "n_tokens": "lots"})
|
| 161 |
+
assert r.status_code == 400
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
# ββ the cost guard βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 165 |
+
def test_dna_routes_carry_their_own_tighter_rate_limits():
|
| 166 |
+
"""Evo 2 7B on a rented GPU is the most expensive call in the product and
|
| 167 |
+
has no free local path. Its bucket must be tighter than the tool default,
|
| 168 |
+
and generation tighter still."""
|
| 169 |
+
rules = dict(server._RL_RULES)
|
| 170 |
+
assert rules["/api/dna/generate"][0] < rules["/api/dna"][0]
|
| 171 |
+
assert rules["/api/dna"][0] < rules["/api/crispr"][0]
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def test_dna_calls_are_logged_as_their_own_event_kinds():
|
| 175 |
+
assert server._EVENT_KINDS["/api/dna/score"] == "dna_score"
|
| 176 |
+
assert server._EVENT_KINDS["/api/dna/generate"] == "dna_generate"
|