Tengo Gzirishvili commited on
Commit
1f7de2a
·
1 Parent(s): 7b48a9b

Auto-opening Structure tab: name a protein, see it fold

Browse files

Say "I want to engineer MC1R" and its 3-D structure now appears on the Bench
while Turing keeps working — no click, no wait.

- New fold_structure agent tool (dee/core/agent_tools.py) resolving a named
protein to its PUBLIC AlphaFold-DB model via the existing
resolve.resolve_uniprot(). The system prompt tells Turing to call it
proactively the moment the user names their target.
- New Structure tab + view on the Bench, first in the tab order, mounting the
existing resilient Mol* loader (mountAlphaFoldViewer) — so it inherits the
EBI-outage retry ladder and version fallback already built there.
- assistant.js relays the tool result to the parent app by postMessage;
TDStructure receives it (origin-checked) and mounts.

PRIVACY — the reason this is split the way it is: fold_structure sends ONLY
the gene symbol + organism to UniProt and returns a public precomputed model,
so nothing of the user's leaves the engine and it's safe to fire automatically.
De-novo folding of a user's own VARIANT would POST their sequence to a third
party (ESMFold), so it is deliberately NOT reachable from this tool and stays
the existing explicit, separately-consented action. The tool takes no
'sequence' parameter at all, and a test asserts that.
TDStructure hard-allowlists alphafold.ebi.ac.uk before handing any URL to the
loader, since it's reachable via postMessage.

Also fixes a real deep-link bug found while verifying: booting straight onto
#turing (the landing's "Meet Turing" link) ran showRoute BEFORE the TDBench
IIFE defined window.TDBench, so the "open a conversation, not a cold chat"
intercept silently no-opped and rendered the standalone chat anyway. Corrected
on the next tick, after boot routing settles.

469 tests green (4 new), verified end-to-end in the browser with the real
MC1R model rendering, no console errors.

dee/core/agent.py CHANGED
@@ -139,13 +139,19 @@ _SYSTEM_PROMPT_TEMPLATE = (
139
  "You are Turing, the conversational orchestrator for TuringDNA, a "
140
  "directed-evolution workbench (design variant libraries with ESM-2, "
141
  "build/map plasmids, edit with CRISPR, learn from bench results). "
142
- "Current loop phase: {phase}. You have five tools available right now: "
143
- "fetch_sequence, design_crispr_guides, design_primers, "
144
  "design_variant_library, and recommend_promoter. If a request could be "
145
  "answered by calling one "
146
  "of these, call it — do not describe what you would do, and do not "
147
  "answer a design/analysis question from your own knowledge instead of "
148
- "running the real tool. If the user names a gene, protein, or "
 
 
 
 
 
 
149
  "accession instead of pasting a sequence (e.g. \"human GFP\", "
150
  "\"NM_001301717\"), call fetch_sequence first to resolve it, then feed "
151
  "the sequence it returns into whichever design tool the request "
 
139
  "You are Turing, the conversational orchestrator for TuringDNA, a "
140
  "directed-evolution workbench (design variant libraries with ESM-2, "
141
  "build/map plasmids, edit with CRISPR, learn from bench results). "
142
+ "Current loop phase: {phase}. You have six tools available right now: "
143
+ "fetch_sequence, fold_structure, design_crispr_guides, design_primers, "
144
  "design_variant_library, and recommend_promoter. If a request could be "
145
  "answered by calling one "
146
  "of these, call it — do not describe what you would do, and do not "
147
  "answer a design/analysis question from your own knowledge instead of "
148
+ "running the real tool. As soon as the user names the protein they want "
149
+ "to work on (human or mouse), ALSO call fold_structure for it — it is a "
150
+ "fast public AlphaFold-DB lookup that puts the 3-D structure on their "
151
+ "Bench while you keep working, and it sends only the gene symbol, never "
152
+ "a sequence. Do this once per protein, alongside whatever else the "
153
+ "request needs; don't announce it as a separate step or ask permission. "
154
+ "If the user names a gene, protein, or "
155
  "accession instead of pasting a sequence (e.g. \"human GFP\", "
156
  "\"NM_001301717\"), call fetch_sequence first to resolve it, then feed "
157
  "the sequence it returns into whichever design tool the request "
dee/core/agent_tools.py CHANGED
@@ -73,6 +73,46 @@ def _tool_fetch_sequence(args: Dict[str, Any]) -> Dict[str, Any]:
73
  }
74
 
75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  def _tool_design_crispr_guides(args: Dict[str, Any]) -> Dict[str, Any]:
77
  from dee.core.crispr import find_guides
78
 
@@ -404,6 +444,7 @@ def _tool_recommend_promoter(args: Dict[str, Any]) -> Dict[str, Any]:
404
  # anonymous-trial quota /api/agent/step itself is gated by.
405
  _TOOLS: Dict[str, Dict[str, Any]] = {
406
  "fetch_sequence": {"fn": _tool_fetch_sequence, "requires_signin": True},
 
407
  "design_crispr_guides": {"fn": _tool_design_crispr_guides, "requires_signin": True},
408
  "design_primers": {"fn": _tool_design_primers, "requires_signin": True},
409
  "design_variant_library": {"fn": _tool_design_variant_library, "requires_signin": True},
@@ -445,6 +486,41 @@ TOOL_SPECS: List[Dict[str, Any]] = [
445
  },
446
  },
447
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
448
  {
449
  "type": "function",
450
  "function": {
 
73
  }
74
 
75
 
76
+ def _tool_fold_structure(args: Dict[str, Any]) -> Dict[str, Any]:
77
+ """Resolve a named protein to its AlphaFold-DB predicted structure so the
78
+ Bench can show it the moment the user says what they're engineering.
79
+
80
+ PRIVACY, deliberately: this only ever sends (gene_symbol, organism) to
81
+ UniProt — never a sequence. The returned AlphaFold URL is a PUBLIC,
82
+ precomputed model fetched straight by the browser, so nothing of the
83
+ user's leaves the Space to get a wild-type structure on screen. De-novo
84
+ folding of a user's own VARIANT is a different thing entirely: it means
85
+ POSTing their amino-acid sequence to a third party (ESMFold /
86
+ api.esmatlas.com), so that stays an explicit, separately-consented
87
+ client-side action and is intentionally NOT reachable from this tool.
88
+ """
89
+ from dee.core import resolve as _resolve
90
+
91
+ gene = str(args.get("gene_symbol") or "").strip()
92
+ organism = str(args.get("organism") or "").lower().strip()
93
+ if not gene:
94
+ return {"ok": False, "error": "missing 'gene_symbol'"}
95
+ if organism not in ("human", "mouse"):
96
+ return {"ok": False,
97
+ "error": "AlphaFold lookup needs 'organism' set to human or mouse."}
98
+ try:
99
+ result = _resolve.resolve_uniprot(organism, gene)
100
+ except Exception: # noqa: BLE001
101
+ logger.exception("fold_structure uniprot resolve failed")
102
+ return {"ok": False, "error": "Structure lookup failed — try again shortly."}
103
+ if not result.get("ok"):
104
+ return {"ok": False, "error": result.get("error") or "No structure found."}
105
+ return {
106
+ "ok": True,
107
+ "gene_symbol": gene,
108
+ "organism": organism,
109
+ "uniprot": result.get("uniprot", ""),
110
+ "alphafold_url": result.get("alphafold_url", ""),
111
+ "alphafold_page": result.get("alphafold_page", ""),
112
+ "source": "AlphaFold DB",
113
+ }
114
+
115
+
116
  def _tool_design_crispr_guides(args: Dict[str, Any]) -> Dict[str, Any]:
117
  from dee.core.crispr import find_guides
118
 
 
444
  # anonymous-trial quota /api/agent/step itself is gated by.
445
  _TOOLS: Dict[str, Dict[str, Any]] = {
446
  "fetch_sequence": {"fn": _tool_fetch_sequence, "requires_signin": True},
447
+ "fold_structure": {"fn": _tool_fold_structure, "requires_signin": True},
448
  "design_crispr_guides": {"fn": _tool_design_crispr_guides, "requires_signin": True},
449
  "design_primers": {"fn": _tool_design_primers, "requires_signin": True},
450
  "design_variant_library": {"fn": _tool_design_variant_library, "requires_signin": True},
 
486
  },
487
  },
488
  },
489
+ {
490
+ "type": "function",
491
+ "function": {
492
+ "name": "fold_structure",
493
+ "description": (
494
+ "Look up a named protein's experimentally-validated predicted "
495
+ "3-D structure in AlphaFold DB and show it on the user's Bench. "
496
+ "Call this proactively, as soon as the user names the protein "
497
+ "they want to engineer (e.g. 'I want to engineer MC1R') — it is "
498
+ "fast (a public precomputed model, no folding is run) and it "
499
+ "puts the structure on screen while you keep talking. It sends "
500
+ "ONLY the gene symbol and organism to UniProt, never a "
501
+ "sequence, so it is always safe to call. Human and mouse only. "
502
+ "NOTE: this does NOT fold a mutated variant — predicting a "
503
+ "user's own variant structure de novo would send their sequence "
504
+ "to a third party, so that stays a separate explicit action the "
505
+ "user takes themselves in the UI. Requires the user to be signed in."
506
+ ),
507
+ "parameters": {
508
+ "type": "object",
509
+ "properties": {
510
+ "gene_symbol": {
511
+ "type": "string",
512
+ "description": "Gene/protein symbol, e.g. MC1R, TP53, BRCA1.",
513
+ },
514
+ "organism": {
515
+ "type": "string",
516
+ "enum": ["human", "mouse"],
517
+ "description": "AlphaFold lookup is supported for human and mouse.",
518
+ },
519
+ },
520
+ "required": ["gene_symbol", "organism"],
521
+ },
522
+ },
523
+ },
524
  {
525
  "type": "function",
526
  "function": {
dee/static/app.css CHANGED
@@ -6809,6 +6809,28 @@ body[data-ui="bench"] #navTuring { display: none; }
6809
  .mc-grid { grid-template-columns: 1fr; }
6810
  }
6811
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6812
  /* ═══════════════════════════════════════════════════════════════════════
6813
  THE BENCH (construct workspace) — Phase 2, 2026-07-13. Active only when
6814
  data-ui="bench" AND data-bench="open". Repositions the EXISTING Turing
 
6809
  .mc-grid { grid-template-columns: 1fr; }
6810
  }
6811
 
6812
+ /* ═══════════════════════════════════════════════════════════════════════
6813
+ STRUCTURE TAB (2026-07-18) — the Bench's 3-D view. Auto-filled from the
6814
+ fold_structure tool with a public AlphaFold-DB model. Reuses the existing
6815
+ .alphafold-loading overlay styling for the load/error states.
6816
+ ═══════════════════════════════════════════════════════════════════════ */
6817
+ .view--structure { padding: 28px 32px 40px; overflow: auto; }
6818
+ .struct { max-width: 1080px; margin: 0 auto; }
6819
+ .struct-head { display: flex; align-items: flex-end; justify-content: space-between;
6820
+ gap: 20px; margin-bottom: 18px; }
6821
+ .struct-head h2 { font-weight: 500; letter-spacing: -0.012em; margin: 2px 0 0; }
6822
+ .struct-viewer { position: relative; width: 100%; height: min(62vh, 560px);
6823
+ border: 1px solid var(--line-strong); background: #0E141B; overflow: hidden; }
6824
+ .struct-empty { position: absolute; inset: 0; display: flex; align-items: center;
6825
+ justify-content: center; text-align: center; padding: 28px; color: rgba(255,255,255,.62);
6826
+ font-size: 13.5px; line-height: 1.6; max-width: 460px; margin: auto; }
6827
+ .struct-note { margin-top: 12px; font-size: 12px; color: var(--ink-faint); line-height: 1.6; }
6828
+ @media (max-width: 860px) {
6829
+ .view--structure { padding: 20px 18px 32px; }
6830
+ .struct-head { flex-direction: column; align-items: flex-start; }
6831
+ .struct-viewer { height: 52vh; }
6832
+ }
6833
+
6834
  /* ═══════════════════════════════════════════════════════════════════════
6835
  THE BENCH (construct workspace) — Phase 2, 2026-07-13. Active only when
6836
  data-ui="bench" AND data-bench="open". Repositions the EXISTING Turing
dee/static/app.js CHANGED
@@ -368,7 +368,7 @@ renderGutter();
368
  // existed): 'turing' leads the list and is the no-hash fallback. The other
369
  // four tools stay one click away for hands-on work — Turing just isn't a
370
  // nav-rail peer anymore, it's what a session starts with.
371
- const ROUTES = ['mission', 'turing', 'plasmid', 'design', 'crispr', 'primers', 'docs'];
372
 
373
  // ── UI mode flag (Mission-Control-+-Bench re-architecture, 2026-07-13) ──
374
  // 'bench' is now the default UI; ?ui=classic remains as a rollback escape
@@ -438,6 +438,7 @@ function showRoute(name) {
438
  // the old static "Engine" title).
439
  const TOPBAR = {
440
  mission: ['Mission control', 'Your constructs and the loop'],
 
441
  plasmid: ['Plasmid Editor', 'Map, annotate & clone your construct'],
442
  design: ['Directed Evolution', 'ESM-2 variant libraries from a wild-type'],
443
  crispr: ['CRISPR', 'Guide RNA design — knockout & base editing'],
@@ -9003,7 +9004,7 @@ function runOracle(opts){
9003
  // keep working. Only ever active in the opt-in bench UI.
9004
  // ═══════════════════════════════════════════════════════════════════════
9005
  (function () {
9006
- const TAB_ROUTES = ['design', 'plasmid', 'crispr', 'primers'];
9007
  let current = null; // { name, sub, phaseIdx, route }
9008
 
9009
  function el(id) { return document.getElementById(id); }
@@ -9091,6 +9092,105 @@ function runOracle(opts){
9091
 
9092
  wire();
9093
  window.TDBench = { openConstruct: openConstruct, openConversation: openConversation, close: close, onRoute: onRoute };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9094
  })();
9095
 
9096
  // ═══════════════════════════════════════════════════════════════════════
 
368
  // existed): 'turing' leads the list and is the no-hash fallback. The other
369
  // four tools stay one click away for hands-on work — Turing just isn't a
370
  // nav-rail peer anymore, it's what a session starts with.
371
+ const ROUTES = ['mission', 'turing', 'structure', 'plasmid', 'design', 'crispr', 'primers', 'docs'];
372
 
373
  // ── UI mode flag (Mission-Control-+-Bench re-architecture, 2026-07-13) ──
374
  // 'bench' is now the default UI; ?ui=classic remains as a rollback escape
 
438
  // the old static "Engine" title).
439
  const TOPBAR = {
440
  mission: ['Mission control', 'Your constructs and the loop'],
441
+ structure: ['Structure', 'Predicted 3-D model of your target'],
442
  plasmid: ['Plasmid Editor', 'Map, annotate & clone your construct'],
443
  design: ['Directed Evolution', 'ESM-2 variant libraries from a wild-type'],
444
  crispr: ['CRISPR', 'Guide RNA design — knockout & base editing'],
 
9004
  // keep working. Only ever active in the opt-in bench UI.
9005
  // ═══════════════════════════════════════════════════════════════════════
9006
  (function () {
9007
+ const TAB_ROUTES = ['structure', 'design', 'plasmid', 'crispr', 'primers'];
9008
  let current = null; // { name, sub, phaseIdx, route }
9009
 
9010
  function el(id) { return document.getElementById(id); }
 
9092
 
9093
  wire();
9094
  window.TDBench = { openConstruct: openConstruct, openConversation: openConversation, close: close, onRoute: onRoute };
9095
+
9096
+ // The top-level showRoute(currentRoute()) runs during initial script
9097
+ // execution — BEFORE this IIFE defines window.TDBench — so booting straight
9098
+ // onto #turing (the landing's "Meet Turing" deep-link) missed showRoute's
9099
+ // "open a conversation, not a cold chat" intercept and rendered the
9100
+ // standalone full-screen chat anyway. Catch it on the next tick rather than
9101
+ // inline: other boot code still routes after this IIFE evaluates, and doing
9102
+ // it synchronously here got undone by whatever ran later. Deferring one
9103
+ // turn lets all boot-time routing settle, then we correct it once.
9104
+ setTimeout(function () {
9105
+ try {
9106
+ if (typeof currentRoute === 'function' && currentRoute() === 'turing'
9107
+ && typeof uiMode === 'function' && uiMode() === 'bench'
9108
+ && document.body.getAttribute('data-bench') !== 'open') {
9109
+ openConversation();
9110
+ }
9111
+ } catch (e) {}
9112
+ }, 0);
9113
+ })();
9114
+
9115
+ // ═══════════════════════════════════════════════════════════════════════
9116
+ // STRUCTURE TAB (2026-07-18) — the Bench's 3-D view, filled automatically.
9117
+ // When Turing resolves a named protein it calls the fold_structure tool,
9118
+ // which returns a PUBLIC AlphaFold-DB model reference (only the gene symbol
9119
+ // was sent to look it up — never a sequence). The chat panel relays that to
9120
+ // this controller, which mounts the same resilient Mol* viewer the identify
9121
+ // embed uses, so the structure is on screen while Turing keeps working.
9122
+ //
9123
+ // De-novo folding of a user's own VARIANT is deliberately NOT here: that
9124
+ // POSTs their amino-acid sequence to a third party (ESMFold), so it stays an
9125
+ // explicit, separately-consented action via the existing fold modal.
9126
+ // ═══════════════════════════════════════════════════════════════════════
9127
+ (function () {
9128
+ let mountedAcc = null; // don't remount the structure already showing
9129
+
9130
+ function el(id) { return document.getElementById(id); }
9131
+
9132
+ // Hard allowlist: this controller is reachable via postMessage, so it must
9133
+ // never hand an attacker-supplied URL to the structure loader.
9134
+ function safeAfUrl(u) {
9135
+ try {
9136
+ const p = new URL(String(u), window.location.origin);
9137
+ return (p.protocol === 'https:' && p.hostname === 'alphafold.ebi.ac.uk') ? p.href : '';
9138
+ } catch (_) { return ''; }
9139
+ }
9140
+
9141
+ function show(info) {
9142
+ info = info || {};
9143
+ const url = safeAfUrl(info.alphafold_url);
9144
+ const host = el('structViewer');
9145
+ if (!url || !host) return;
9146
+
9147
+ const acc = String(info.uniprot || '').replace(/[^A-Za-z0-9]/g, '').slice(0, 20);
9148
+ const gene = String(info.gene_symbol || '').slice(0, 40);
9149
+ const org = String(info.organism || '').slice(0, 20);
9150
+
9151
+ const title = el('structTitle'), sub = el('structSub');
9152
+ const link = el('structEntryLink'), note = el('structNote');
9153
+ if (title) title.textContent = gene || 'Predicted structure';
9154
+ if (sub) {
9155
+ sub.textContent = 'AlphaFold DB predicted model'
9156
+ + (acc ? ' · UniProt ' + acc : '') + (org ? ' · ' + org : '');
9157
+ }
9158
+ const page = safeAfUrl(info.alphafold_page);
9159
+ if (link) { if (page) { link.href = page; link.hidden = false; } else { link.hidden = true; } }
9160
+ if (note) {
9161
+ note.hidden = false;
9162
+ note.textContent = 'Public precomputed model — only the gene symbol was sent to '
9163
+ + 'look this up. Your sequences never left the engine.';
9164
+ }
9165
+
9166
+ if (mountedAcc && mountedAcc === acc) return; // already showing this one
9167
+ mountedAcc = acc;
9168
+ host.innerHTML = '<div class="alphafold-loading">Loading predicted structure…</div>';
9169
+ host.dataset.pdbUrl = url;
9170
+ try { mountAlphaFoldViewer({ alphafold_url: url }, host); } catch (_) {}
9171
+ }
9172
+
9173
+ // Show it AND bring the Structure tab forward (only inside an open bench —
9174
+ // we never yank a user out of whatever else they're doing).
9175
+ function open(info) {
9176
+ show(info);
9177
+ if (document.body.getAttribute('data-bench') === 'open'
9178
+ && location.hash !== '#structure') {
9179
+ location.hash = '#structure';
9180
+ }
9181
+ }
9182
+
9183
+ // Relay from the Turing panel (assistant.js) after a fold_structure call.
9184
+ // Origin-checked: the only legitimate sender is our own iframe.
9185
+ window.addEventListener('message', function (e) {
9186
+ if (e.origin !== window.location.origin) return;
9187
+ const d = e.data;
9188
+ if (!d || d.type !== 'td-structure') return;
9189
+ open({ gene_symbol: d.gene_symbol, organism: d.organism, uniprot: d.uniprot,
9190
+ alphafold_url: d.alphafold_url, alphafold_page: d.alphafold_page });
9191
+ });
9192
+
9193
+ window.TDStructure = { show: show, open: open };
9194
  })();
9195
 
9196
  // ═══════════════════════════════════════════════════════════════════════
dee/static/assistant.js CHANGED
@@ -312,6 +312,12 @@
312
  return [p.name || "?", p.tier || "?"];
313
  }) };
314
  }
 
 
 
 
 
 
315
  if (name === "fetch_sequence") {
316
  // No `items` here on purpose — the resolved sequence itself
317
  // (which can be thousands of nt) goes back to the model to
@@ -601,6 +607,24 @@
601
  if (j.phase) c.phase = j.phase;
602
  if (j.context_tokens_limit) { c.contextTokensUsed = j.context_tokens_used || 0; c.contextTokensLimit = j.context_tokens_limit; }
603
  if (j.tool_result) c.messages.push({ role: "tool", tool: shapeToolResult(j.tool_name, j.tool_result) });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
604
  pendingReply = { chat: c, index: c.messages.length, text: j.assistant_message || "(no reply)" };
605
  c.messages.push({ role: "assistant", text: "" });
606
  }
 
312
  return [p.name || "?", p.tier || "?"];
313
  }) };
314
  }
315
+ if (name === "fold_structure") {
316
+ return { name: label, status: "done", result: result,
317
+ rows: [["protein", String(result.gene_symbol || "?")],
318
+ ["uniprot", String(result.uniprot || "?")],
319
+ ["source", String(result.source || "AlphaFold DB")]] };
320
+ }
321
  if (name === "fetch_sequence") {
322
  // No `items` here on purpose — the resolved sequence itself
323
  // (which can be thousands of nt) goes back to the model to
 
607
  if (j.phase) c.phase = j.phase;
608
  if (j.context_tokens_limit) { c.contextTokensUsed = j.context_tokens_used || 0; c.contextTokensLimit = j.context_tokens_limit; }
609
  if (j.tool_result) c.messages.push({ role: "tool", tool: shapeToolResult(j.tool_name, j.tool_result) });
610
+ // fold_structure resolved a PUBLIC AlphaFold-DB model for the
611
+ // protein the user named — hand it to the parent app so the
612
+ // Bench's Structure tab fills in while Turing keeps talking.
613
+ // Only the model reference crosses; no sequence is involved.
614
+ // Same-origin target, mirroring app.js's td-theme post.
615
+ if (j.tool_name === "fold_structure" && j.tool_result && j.tool_result.ok
616
+ && window.parent && window.parent !== window) {
617
+ try {
618
+ window.parent.postMessage({
619
+ type: "td-structure",
620
+ gene_symbol: j.tool_result.gene_symbol || "",
621
+ organism: j.tool_result.organism || "",
622
+ uniprot: j.tool_result.uniprot || "",
623
+ alphafold_url: j.tool_result.alphafold_url || "",
624
+ alphafold_page: j.tool_result.alphafold_page || "",
625
+ }, window.location.origin);
626
+ } catch (err) {}
627
+ }
628
  pendingReply = { chat: c, index: c.messages.length, text: j.assistant_message || "(no reply)" };
629
  c.messages.push({ role: "assistant", text: "" });
630
  }
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=20260718-onehome" />
116
  <link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=2" />
117
  <link rel="apple-touch-icon" href="/static/favicon.svg?v=2" />
118
  <!-- Mol* (PDBe) 3-D viewer is ~4.9 MB. We do NOT eager-load it on every
@@ -444,6 +444,7 @@
444
 
445
  <div class="bench-canvashead" id="benchCanvasHead" hidden>
446
  <nav class="bench-tabs" id="benchTabs" aria-label="Construct artifacts">
 
447
  <button class="bench-tab" data-route="design" type="button">Library</button>
448
  <button class="bench-tab" data-route="plasmid" type="button">Map</button>
449
  <button class="bench-tab" data-route="crispr" type="button">Guides</button>
@@ -507,6 +508,34 @@
507
  title="Turing — conversational engine" loading="lazy"></iframe>
508
  </section>
509
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
510
  <!-- =============================== DESIGN view (default) -->
511
  <section class="view view--design" data-view="design">
512
  <!--
@@ -2272,7 +2301,7 @@
2272
  <!-- Cloning reference data must load before app.js so the Designer
2273
  can read VECTORS / ENZYMES / CLONING_METHODS / TAGS / LINKERS. -->
2274
  <script src="/static/cloning_db.js?v=20260530-ui-polish" defer></script>
2275
- <script src="/static/app.js?v=20260718-onehome" defer></script>
2276
  <!-- Dwell-time heartbeat. Loads after auth.js so its /api/ping calls go
2277
  through the JWT-attaching fetch wrapper (signed-in attribution). -->
2278
  <script src="/static/telemetry.js?v=20260622-analytics" defer></script>
 
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=20260718-structure2" />
116
  <link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=2" />
117
  <link rel="apple-touch-icon" href="/static/favicon.svg?v=2" />
118
  <!-- Mol* (PDBe) 3-D viewer is ~4.9 MB. We do NOT eager-load it on every
 
444
 
445
  <div class="bench-canvashead" id="benchCanvasHead" hidden>
446
  <nav class="bench-tabs" id="benchTabs" aria-label="Construct artifacts">
447
+ <button class="bench-tab" data-route="structure" type="button">Structure</button>
448
  <button class="bench-tab" data-route="design" type="button">Library</button>
449
  <button class="bench-tab" data-route="plasmid" type="button">Map</button>
450
  <button class="bench-tab" data-route="crispr" type="button">Guides</button>
 
508
  title="Turing — conversational engine" loading="lazy"></iframe>
509
  </section>
510
 
511
+ <!-- =============================== STRUCTURE view
512
+ The Bench's Structure tab. Populated automatically when
513
+ Turing resolves a named protein (fold_structure → a PUBLIC
514
+ AlphaFold-DB model; only the gene symbol ever leaves the
515
+ Space, never a sequence). Mounts the same resilient Mol*
516
+ viewer the identify/AlphaFold embeds use. De-novo folding of
517
+ a user's own VARIANT stays an explicit, separately-consented
518
+ action (it would POST their sequence to a third party). -->
519
+ <section class="view view--structure2" data-view="structure" hidden aria-label="Predicted structure">
520
+ <div class="struct">
521
+ <header class="struct-head">
522
+ <div>
523
+ <p class="card-kicker" id="structKicker">&sect; Structure</p>
524
+ <h2 id="structTitle">Predicted structure</h2>
525
+ <p class="card-sub" id="structSub">Turing loads the wild-type model as soon as you name a protein.</p>
526
+ </div>
527
+ <a class="ghost" id="structEntryLink" target="_blank" rel="noopener" hidden>Open AlphaFold entry</a>
528
+ </header>
529
+ <div class="struct-viewer" id="structViewer">
530
+ <div class="struct-empty" id="structEmpty">
531
+ Name a protein in the conversation — “I want to engineer MC1R” — and its
532
+ predicted structure appears here.
533
+ </div>
534
+ </div>
535
+ <p class="struct-note" id="structNote" hidden></p>
536
+ </div>
537
+ </section>
538
+
539
  <!-- =============================== DESIGN view (default) -->
540
  <section class="view view--design" data-view="design">
541
  <!--
 
2301
  <!-- Cloning reference data must load before app.js so the Designer
2302
  can read VECTORS / ENZYMES / CLONING_METHODS / TAGS / LINKERS. -->
2303
  <script src="/static/cloning_db.js?v=20260530-ui-polish" defer></script>
2304
+ <script src="/static/app.js?v=20260718-structure2" defer></script>
2305
  <!-- Dwell-time heartbeat. Loads after auth.js so its /api/ping calls go
2306
  through the JWT-attaching fetch wrapper (signed-in attribution). -->
2307
  <script src="/static/telemetry.js?v=20260622-analytics" defer></script>
tests/test_fold_structure_tool.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for the fold_structure agent tool — the Bench's auto-opening 3-D view.
2
+
3
+ The privacy contract is the point of this tool and is asserted here: it must
4
+ resolve a NAMED protein to a PUBLIC AlphaFold-DB model by sending only the gene
5
+ symbol + organism, and must never accept or forward a sequence (de-novo folding
6
+ of a user's own variant goes to a third party, so it stays an explicit,
7
+ separately-consented client-side action).
8
+ """
9
+ import pytest
10
+
11
+ from dee.core import agent_tools as t
12
+
13
+
14
+ def test_fold_structure_is_registered_and_specced():
15
+ assert "fold_structure" in t._TOOLS
16
+ spec = next(s for s in t.TOOL_SPECS if s["function"]["name"] == "fold_structure")
17
+ params = spec["function"]["parameters"]
18
+ assert set(params["required"]) == {"gene_symbol", "organism"}
19
+ # It must NOT take a sequence — that's the whole privacy boundary.
20
+ assert "sequence" not in params["properties"]
21
+ assert params["properties"]["organism"]["enum"] == ["human", "mouse"]
22
+
23
+
24
+ def test_fold_structure_requires_gene_and_supported_organism():
25
+ assert t._tool_fold_structure({"organism": "human"})["ok"] is False
26
+ bad = t._tool_fold_structure({"gene_symbol": "MC1R", "organism": "zebrafish"})
27
+ assert bad["ok"] is False
28
+ assert "human or mouse" in bad["error"]
29
+
30
+
31
+ def test_fold_structure_returns_public_model_reference(monkeypatch):
32
+ seen = {}
33
+
34
+ def fake_resolve_uniprot(organism, gene_symbol):
35
+ seen["args"] = (organism, gene_symbol)
36
+ return {"ok": True, "uniprot": "Q01726",
37
+ "alphafold_url": "https://alphafold.ebi.ac.uk/files/AF-Q01726-F1-model_v6.pdb",
38
+ "alphafold_page": "https://alphafold.ebi.ac.uk/entry/Q01726"}
39
+
40
+ from dee.core import resolve as _resolve
41
+ monkeypatch.setattr(_resolve, "resolve_uniprot", fake_resolve_uniprot)
42
+
43
+ out = t._tool_fold_structure({"gene_symbol": "MC1R", "organism": "human"})
44
+ assert out["ok"] is True
45
+ assert out["uniprot"] == "Q01726"
46
+ assert out["alphafold_url"].startswith("https://alphafold.ebi.ac.uk/")
47
+ assert out["source"] == "AlphaFold DB"
48
+ # ONLY (organism, gene) was sent onward — no sequence anywhere in the call.
49
+ assert seen["args"] == ("human", "MC1R")
50
+
51
+
52
+ def test_fold_structure_surfaces_lookup_failure_honestly(monkeypatch):
53
+ from dee.core import resolve as _resolve
54
+ monkeypatch.setattr(_resolve, "resolve_uniprot",
55
+ lambda o, g: {"ok": False, "error": "No reviewed UniProt entry found for ZZZ (human)."})
56
+ out = t._tool_fold_structure({"gene_symbol": "ZZZ", "organism": "human"})
57
+ assert out["ok"] is False
58
+ assert "ZZZ" in out["error"]