pen-stack 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pen_stack/__init__.py +2 -0
- pen_stack/_resources.py +34 -0
- pen_stack/active/__init__.py +20 -0
- pen_stack/active/acquire.py +165 -0
- pen_stack/active/brains.py +74 -0
- pen_stack/active/campaign.py +109 -0
- pen_stack/active/design.py +66 -0
- pen_stack/active/validate.py +104 -0
- pen_stack/adapt/__init__.py +14 -0
- pen_stack/adapt/finetune.py +33 -0
- pen_stack/adapt/ingest.py +86 -0
- pen_stack/adapt/pipeline.py +101 -0
- pen_stack/adapt/recalibrate.py +58 -0
- pen_stack/adapt/report.py +130 -0
- pen_stack/agent/__init__.py +1 -0
- pen_stack/agent/cite.py +175 -0
- pen_stack/agent/co_scientist.py +262 -0
- pen_stack/agent/epistemic.py +102 -0
- pen_stack/agent/guardrails.py +67 -0
- pen_stack/agent/mcp_server.py +215 -0
- pen_stack/agent/orchestrator.py +112 -0
- pen_stack/agent/orchestrator_live.py +56 -0
- pen_stack/agent/pen_agent.py +242 -0
- pen_stack/agent/scope.py +60 -0
- pen_stack/agent/tools.py +130 -0
- pen_stack/api/__init__.py +12 -0
- pen_stack/api/manifest.py +160 -0
- pen_stack/atlas/__init__.py +1 -0
- pen_stack/atlas/atlas.parquet +0 -0
- pen_stack/atlas/build_wtkb.py +80 -0
- pen_stack/atlas/crosslink.py +179 -0
- pen_stack/atlas/expand.py +190 -0
- pen_stack/atlas/guide_design.py +178 -0
- pen_stack/atlas/schema.py +59 -0
- pen_stack/atlas/scorecard.py +134 -0
- pen_stack/atlas/scorecard_v3.parquet +0 -0
- pen_stack/atlas/universe.py +75 -0
- pen_stack/atlas/universe_v3.parquet +0 -0
- pen_stack/atlas/variant_propose.py +155 -0
- pen_stack/atlas/writer_efficiency.py +184 -0
- pen_stack/atlas/writer_predict.py +229 -0
- pen_stack/atlas/writer_recommend.py +170 -0
- pen_stack/atlas/writer_verify.py +167 -0
- pen_stack/atlas/wtkb.parquet +0 -0
- pen_stack/bridge/__init__.py +1 -0
- pen_stack/bridge/activity.py +52 -0
- pen_stack/bridge/cli.py +65 -0
- pen_stack/bridge/fold_qc.py +53 -0
- pen_stack/bridge/guide_qc.py +87 -0
- pen_stack/bridge/ingest.py +139 -0
- pen_stack/bridge/offtarget.py +191 -0
- pen_stack/bridge/offtarget_energetics.py +105 -0
- pen_stack/bridge/ortholog_screen.py +73 -0
- pen_stack/bridge/pipeline.py +83 -0
- pen_stack/build/__init__.py +16 -0
- pen_stack/build/cloudlab.py +74 -0
- pen_stack/build/ingest.py +47 -0
- pen_stack/build/protocol.py +82 -0
- pen_stack/build/simlab.py +30 -0
- pen_stack/cli.py +126 -0
- pen_stack/data/__init__.py +1 -0
- pen_stack/data/encode.py +84 -0
- pen_stack/data/genome.py +71 -0
- pen_stack/data/ingest_chromatin.py +119 -0
- pen_stack/data/ingest_integration.py +112 -0
- pen_stack/data/ingest_safety_annot.py +201 -0
- pen_stack/data/ingest_trip.py +76 -0
- pen_stack/design/__init__.py +14 -0
- pen_stack/design/capsid_generate.py +62 -0
- pen_stack/design/generate.py +70 -0
- pen_stack/design/pareto.py +70 -0
- pen_stack/design/space.py +137 -0
- pen_stack/design/writer_variants.py +121 -0
- pen_stack/env/__init__.py +1 -0
- pen_stack/env/genome_writing_env.py +248 -0
- pen_stack/env/policies.py +94 -0
- pen_stack/graph/__init__.py +21 -0
- pen_stack/graph/build.py +133 -0
- pen_stack/graph/cell_types.py +58 -0
- pen_stack/graph/ingest.py +132 -0
- pen_stack/graph/query.py +148 -0
- pen_stack/graph/schema.py +100 -0
- pen_stack/loop/__init__.py +15 -0
- pen_stack/loop/continual.py +61 -0
- pen_stack/loop/cycle.py +84 -0
- pen_stack/loop/drift.py +41 -0
- pen_stack/mech/__init__.py +1 -0
- pen_stack/mech/classify_atlas.py +71 -0
- pen_stack/mech/pfam_whitelist.yaml +247 -0
- pen_stack/mech/whitelist.py +66 -0
- pen_stack/monitor/__init__.py +1 -0
- pen_stack/monitor/europepmc.py +32 -0
- pen_stack/monitor/run.py +57 -0
- pen_stack/monitor/triage.py +63 -0
- pen_stack/oracles/__init__.py +65 -0
- pen_stack/oracles/affinity.py +116 -0
- pen_stack/oracles/cache.py +53 -0
- pen_stack/oracles/energetics.py +33 -0
- pen_stack/oracles/genome.py +167 -0
- pen_stack/oracles/protein_design.py +136 -0
- pen_stack/oracles/reliability.py +64 -0
- pen_stack/oracles/rna.py +28 -0
- pen_stack/oracles/schema.py +77 -0
- pen_stack/oracles/status.py +123 -0
- pen_stack/oracles/structure.py +42 -0
- pen_stack/oracles/structure_run.py +76 -0
- pen_stack/oracles/vcell.py +74 -0
- pen_stack/planner/__init__.py +1 -0
- pen_stack/planner/ada_risk.py +64 -0
- pen_stack/planner/antipeg_oracle.py +75 -0
- pen_stack/planner/capsid_epitope_oracle.py +135 -0
- pen_stack/planner/cargo.py +56 -0
- pen_stack/planner/cargo_polish.py +146 -0
- pen_stack/planner/chromosome.py +106 -0
- pen_stack/planner/delivery.py +55 -0
- pen_stack/planner/delivery_constraints.py +110 -0
- pen_stack/planner/delivery_immune.py +61 -0
- pen_stack/planner/delivery_immunology.py +222 -0
- pen_stack/planner/delivery_predict.py +196 -0
- pen_stack/planner/delivery_vehicles.py +37 -0
- pen_stack/planner/genotoxicity_oracle.py +112 -0
- pen_stack/planner/immune_mhc2.py +154 -0
- pen_stack/planner/immune_profile.py +292 -0
- pen_stack/planner/innate_sensing.py +135 -0
- pen_stack/planner/multiplex.py +110 -0
- pen_stack/planner/optimize.py +278 -0
- pen_stack/planner/pipeline.py +87 -0
- pen_stack/planner/report.py +26 -0
- pen_stack/planner/router.py +57 -0
- pen_stack/planner/seroprevalence_oracle.py +92 -0
- pen_stack/planner/target_site.py +118 -0
- pen_stack/rag/__init__.py +1 -0
- pen_stack/rag/corpus.py +133 -0
- pen_stack/rag/embed.py +98 -0
- pen_stack/rag/ground.py +131 -0
- pen_stack/rag/index.py +53 -0
- pen_stack/rag/llm.py +215 -0
- pen_stack/rag/qa.py +105 -0
- pen_stack/rag/retrieve.py +48 -0
- pen_stack/rules/__init__.py +9 -0
- pen_stack/rules/evaluators.py +318 -0
- pen_stack/rules/loader.py +31 -0
- pen_stack/rules/schema.py +99 -0
- pen_stack/rules/solver.py +43 -0
- pen_stack/rules/spec.py +78 -0
- pen_stack/safety/__init__.py +21 -0
- pen_stack/safety/audit.py +90 -0
- pen_stack/safety/gate.py +58 -0
- pen_stack/safety/pfam_scan.py +157 -0
- pen_stack/safety/policy.py +69 -0
- pen_stack/safety/redteam.py +71 -0
- pen_stack/safety/registry.py +255 -0
- pen_stack/safety/screen.py +59 -0
- pen_stack/safety/standards.py +141 -0
- pen_stack/score/__init__.py +1 -0
- pen_stack/score/recalibrate.py +77 -0
- pen_stack/score/therapeutic.py +85 -0
- pen_stack/server/__init__.py +1 -0
- pen_stack/server/api.py +647 -0
- pen_stack/spec/__init__.py +18 -0
- pen_stack/spec/clarify.py +42 -0
- pen_stack/spec/extract.py +406 -0
- pen_stack/spec/resolvers/__init__.py +17 -0
- pen_stack/spec/resolvers/cell.py +51 -0
- pen_stack/spec/resolvers/chem.py +32 -0
- pen_stack/spec/resolvers/feature.py +36 -0
- pen_stack/spec/resolvers/gene.py +43 -0
- pen_stack/spec/resolvers/locus.py +29 -0
- pen_stack/spec/resolvers/phenotype.py +37 -0
- pen_stack/spec/satisfy.py +114 -0
- pen_stack/spec/service.py +33 -0
- pen_stack/spec/writespec.py +252 -0
- pen_stack/twin/__init__.py +14 -0
- pen_stack/twin/calibrate.py +61 -0
- pen_stack/twin/data/__init__.py +12 -0
- pen_stack/twin/data/position_effect.py +245 -0
- pen_stack/twin/mechanistic.py +147 -0
- pen_stack/twin/outcome.py +132 -0
- pen_stack/twin/position_effect.py +454 -0
- pen_stack/ui/__init__.py +1 -0
- pen_stack/ui/app.py +713 -0
- pen_stack/validate/__init__.py +1 -0
- pen_stack/validate/adapt_demo.py +69 -0
- pen_stack/validate/agent_eval.py +117 -0
- pen_stack/validate/bench_adversarial_tasks.py +118 -0
- pen_stack/validate/bench_coscientist_tasks.py +60 -0
- pen_stack/validate/bench_graph_tasks.py +64 -0
- pen_stack/validate/bench_rule_tasks.py +84 -0
- pen_stack/validate/bench_trust_tasks.py +92 -0
- pen_stack/validate/bench_writetype_tasks.py +101 -0
- pen_stack/validate/blind_gsh_discovery.py +261 -0
- pen_stack/validate/cargo_directionality.py +57 -0
- pen_stack/validate/closed_loop.py +63 -0
- pen_stack/validate/durability_baselines.py +185 -0
- pen_stack/validate/experiment_design.py +65 -0
- pen_stack/validate/expr_controls.py +39 -0
- pen_stack/validate/forward_hypotheses.py +104 -0
- pen_stack/validate/generative_design.py +62 -0
- pen_stack/validate/guide_qc_demo.py +69 -0
- pen_stack/validate/heldout_celltype_expr.py +32 -0
- pen_stack/validate/immune_calibration.py +133 -0
- pen_stack/validate/intent_specification.py +82 -0
- pen_stack/validate/known_biology_expr.py +38 -0
- pen_stack/validate/offtarget_energetics_eval.py +144 -0
- pen_stack/validate/out_of_scope_refusal.py +82 -0
- pen_stack/validate/outcome_calibration.py +194 -0
- pen_stack/validate/outcome_prediction.py +76 -0
- pen_stack/validate/paper3_benchmark.py +165 -0
- pen_stack/validate/paper4_real_validation.py +144 -0
- pen_stack/validate/paper4_validation.py +82 -0
- pen_stack/validate/protocol_safety.py +62 -0
- pen_stack/validate/safety_screening.py +72 -0
- pen_stack/validate/selective_prediction.py +104 -0
- pen_stack/validate/seq_vs_measured.py +134 -0
- pen_stack/validate/target_site_controls.py +65 -0
- pen_stack/validate/uncertainty_eval.py +244 -0
- pen_stack/validate/ungrounded_baseline.py +234 -0
- pen_stack/validate/within_locus_ranking.py +84 -0
- pen_stack/validate/writer_recovery.py +91 -0
- pen_stack/verify/__init__.py +5 -0
- pen_stack/verify/proof.py +206 -0
- pen_stack/verify/schema.py +53 -0
- pen_stack/verify/service.py +191 -0
- pen_stack/web/__init__.py +18 -0
- pen_stack/web/guide.py +110 -0
- pen_stack/web/llm.py +393 -0
- pen_stack/web/llm_provider.py +119 -0
- pen_stack/web/router.py +119 -0
- pen_stack/web/server.py +96 -0
- pen_stack/web/tools.py +197 -0
- pen_stack/wgenome/__init__.py +1 -0
- pen_stack/wgenome/chromatin_seq.py +83 -0
- pen_stack/wgenome/durability.py +108 -0
- pen_stack/wgenome/export_tracks.py +52 -0
- pen_stack/wgenome/features.py +82 -0
- pen_stack/wgenome/genotoxic_blocklist.py +88 -0
- pen_stack/wgenome/gsh_baseline.py +154 -0
- pen_stack/wgenome/mesh_features.py +61 -0
- pen_stack/wgenome/offtarget_assay.py +80 -0
- pen_stack/wgenome/offtarget_bridge.py +47 -0
- pen_stack/wgenome/offtarget_cast.py +97 -0
- pen_stack/wgenome/offtarget_data.py +148 -0
- pen_stack/wgenome/offtarget_enumerate.py +274 -0
- pen_stack/wgenome/offtarget_integrase.py +155 -0
- pen_stack/wgenome/offtarget_nuclease.py +123 -0
- pen_stack/wgenome/offtarget_paste.py +41 -0
- pen_stack/wgenome/offtarget_predict.py +282 -0
- pen_stack/wgenome/ood.py +135 -0
- pen_stack/wgenome/providers.py +278 -0
- pen_stack/wgenome/safety.py +69 -0
- pen_stack/wgenome/structure3d.py +212 -0
- pen_stack/wgenome/uncertainty.py +250 -0
- pen_stack/wgenome/writability.py +72 -0
- pen_stack-0.1.0.dist-info/METADATA +401 -0
- pen_stack-0.1.0.dist-info/RECORD +259 -0
- pen_stack-0.1.0.dist-info/WHEEL +5 -0
- pen_stack-0.1.0.dist-info/entry_points.txt +3 -0
- pen_stack-0.1.0.dist-info/licenses/LICENSE +21 -0
- pen_stack-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""The co-scientist, deliberative, multi-strategy, grounded design.
|
|
2
|
+
|
|
3
|
+
The reasoning ceiling rises while the grounding floor stays fixed (Principle 1): the co-scientist
|
|
4
|
+
*deliberates* over alternative design paths and returns a small set of **materially distinct** strategies, but
|
|
5
|
+
every number still comes from the rule-grounded verifier / oracles, it can propose and rank, never source a
|
|
6
|
+
quantity (the no-fabrication gate holds by construction, asserted by test).
|
|
7
|
+
|
|
8
|
+
`propose_strategies(goal)` returns 2-3 strategies that differ on real design axes (write-type / writer /
|
|
9
|
+
delivery / edit-intent), each independently **verified** (legal) and **confidence-tagged**, with its tradeoffs
|
|
10
|
+
surfaced. A distinctness metric proves they are materially different, not reworded variants (Principle 2).
|
|
11
|
+
The deterministic planner remains the baseline/fallback; `deliberate()` benchmarks the two head-to-head.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from itertools import combinations
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
# candidate strategy templates - each a MATERIALLY different approach to installing a payload at a locus.
|
|
20
|
+
# (write_type, writer_family, delivery_vehicle, edit_intent, label, tradeoff)
|
|
21
|
+
_STRATEGY_TEMPLATES = [
|
|
22
|
+
("insertion", "bridge_IS110", "AAV_single", "safe_harbour_insertion",
|
|
23
|
+
"safe-harbour insertion", "DSB-free, AAV-deliverable, off-target-screened; cargo <=4.7 kb"),
|
|
24
|
+
("landing_pad_install", "PE_integrase", "AAV_single", "high_durability_insertion",
|
|
25
|
+
"landing-pad install", "prime-edited att beacon then integrase; two-step, durable, broadly reachable"),
|
|
26
|
+
("insertion", "Cas9", "electroporation", "knock_in_with_disruption",
|
|
27
|
+
"in-locus RNP knock-in", "RNP electroporation (transient, low immunogenicity); DSB-based, ex-vivo"),
|
|
28
|
+
("multiplex", "bridge_IS110", "electroporation", "safe_harbour_insertion",
|
|
29
|
+
"multiplex DSB-free", "concurrent edits, DSB-free -> ~zero translocation risk; ex-vivo"),
|
|
30
|
+
]
|
|
31
|
+
_AXES = ("write_type", "writer_family", "delivery_vehicle", "edit_intent")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class Strategy:
|
|
36
|
+
label: str
|
|
37
|
+
design: dict[str, Any]
|
|
38
|
+
legal: bool | None
|
|
39
|
+
confidence: float | None
|
|
40
|
+
interval: list[float] | None
|
|
41
|
+
epistemic_status: str
|
|
42
|
+
violations: list[dict]
|
|
43
|
+
tradeoff: str
|
|
44
|
+
no_fabrication: bool
|
|
45
|
+
provenance: dict[str, Any] = field(default_factory=dict)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _verify_design(design: dict) -> Strategy | None:
|
|
49
|
+
from pen_stack.verify import verify
|
|
50
|
+
v = verify(design)
|
|
51
|
+
if v.deferred:
|
|
52
|
+
return None
|
|
53
|
+
return Strategy(label=design.get("_label", ""), design={k: v2 for k, v2 in design.items()
|
|
54
|
+
if not k.startswith("_")}, legal=v.legal, confidence=v.confidence, interval=v.interval,
|
|
55
|
+
epistemic_status=v.epistemic_status, violations=v.violations,
|
|
56
|
+
tradeoff=design.get("_tradeoff", ""), no_fabrication=v.no_fabrication,
|
|
57
|
+
provenance=v.provenance)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def propose_strategies(gene: str = "AAVS1", cargo_bp: int = 3000, cell_type: str = "K562",
|
|
61
|
+
n: int = 3) -> dict:
|
|
62
|
+
"""Return up to `n` materially-distinct, verified, confidence-tagged strategies for a write goal.
|
|
63
|
+
Numbers come only from the verifier (no fabrication); strategies are ranked legal-first then by confidence."""
|
|
64
|
+
strategies: list[Strategy] = []
|
|
65
|
+
for wt, fam, veh, intent, label, tradeoff in _STRATEGY_TEMPLATES:
|
|
66
|
+
design = {"write_type": wt, "writer_family": fam, "delivery_vehicle": veh, "edit_intent": intent,
|
|
67
|
+
"cargo_bp": cargo_bp, "cell_type": cell_type, "gene": gene,
|
|
68
|
+
# per-axis scores let the verifier attach a CALIBRATED confidence (else it abstains) - tool-sourced
|
|
69
|
+
"safety": 0.8, "p_durable": 0.75, "writer_activity": 0.7,
|
|
70
|
+
"edits": [{"site": "A"}, {"site": "B"}] if wt == "multiplex" else [],
|
|
71
|
+
"_label": label, "_tradeoff": tradeoff}
|
|
72
|
+
s = _verify_design(design)
|
|
73
|
+
if s is not None:
|
|
74
|
+
strategies.append(s)
|
|
75
|
+
legal = [s for s in strategies if s.legal]
|
|
76
|
+
legal.sort(key=lambda s: (s.confidence if s.confidence is not None else -1), reverse=True)
|
|
77
|
+
chosen = legal[:n]
|
|
78
|
+
dist = distinctness(chosen)
|
|
79
|
+
return {"goal": {"gene": gene, "cargo_bp": cargo_bp, "cell_type": cell_type},
|
|
80
|
+
"n_strategies": len(chosen),
|
|
81
|
+
"strategies": [s.__dict__ for s in chosen],
|
|
82
|
+
"distinctness": dist,
|
|
83
|
+
"all_legal": all(s.legal for s in chosen),
|
|
84
|
+
"all_confidence_tagged": all(s.confidence is not None for s in chosen),
|
|
85
|
+
"no_fabrication": all(s.no_fabrication for s in chosen),
|
|
86
|
+
"note": "multiple materially-distinct strategies; every number is verifier-sourced (no fabrication)"}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def distinctness(strategies: list[Strategy]) -> dict:
|
|
90
|
+
"""Materially-distinct = every pair differs on >=2 design axes (not a reworded variant). Measured."""
|
|
91
|
+
if len(strategies) < 2:
|
|
92
|
+
return {"materially_distinct": len(strategies) <= 1, "min_pairwise_axis_diff": None, "n": len(strategies)}
|
|
93
|
+
diffs = []
|
|
94
|
+
for a, b in combinations(strategies, 2):
|
|
95
|
+
d = sum(1 for ax in _AXES if a.design.get(ax) != b.design.get(ax))
|
|
96
|
+
diffs.append(d)
|
|
97
|
+
return {"materially_distinct": min(diffs) >= 2, "min_pairwise_axis_diff": min(diffs),
|
|
98
|
+
"mean_pairwise_axis_diff": round(sum(diffs) / len(diffs), 2), "n": len(strategies),
|
|
99
|
+
"axes": list(_AXES)}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# self-critique / revise loop. The critic can ONLY flag/reject + suggest a design-level swap;
|
|
103
|
+
# it never invents a number. A deterministic fix is applied and the plan is RE-VERIFIED (falsifiable: the
|
|
104
|
+
# revision must measurably improve plan quality, else it is reported as not-yet-useful).
|
|
105
|
+
# DNA-cargo vehicles by ascending capacity, for the "oversize cargo" deterministic revision.
|
|
106
|
+
_DNA_VEHICLES = [("AAV_single", 4700), ("AAV_dual", 9000), ("helper_dependent_adenovirus", 35000),
|
|
107
|
+
("hsv_amplicon", 100000)]
|
|
108
|
+
_RNP_VEHICLE = "electroporation"
|
|
109
|
+
_RNP_WRITERS = {"Cas9", "Cas12a"}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def critique(design: dict) -> dict:
|
|
113
|
+
"""Flag issues in a design via the verifier (hard violations / soft flags / scope) + categorical
|
|
114
|
+
cross-checks. Returns flags + a suggested design-level revision. NEVER invents a number."""
|
|
115
|
+
from pen_stack.verify import verify
|
|
116
|
+
v = verify(design)
|
|
117
|
+
flags = []
|
|
118
|
+
revision: dict | None = None
|
|
119
|
+
for viol in v.violations:
|
|
120
|
+
rid = viol["rule_id"]
|
|
121
|
+
flags.append({"kind": "hard", "rule_id": rid, "reason": viol["reason"]})
|
|
122
|
+
if rid == "payload.cargo_within_capacity": # oversize cargo -> bigger DNA vehicle
|
|
123
|
+
cap_ok = next((name for name, cap in _DNA_VEHICLES if (design.get("cargo_bp") or 0) <= cap), None)
|
|
124
|
+
if cap_ok:
|
|
125
|
+
revision = {**design, "delivery_vehicle": cap_ok}
|
|
126
|
+
elif rid == "delivery.cargo_form_compatible": # RNP into a DNA-only vehicle -> physical delivery
|
|
127
|
+
revision = {**design, "delivery_vehicle": _RNP_VEHICLE}
|
|
128
|
+
for s in v.soft_flags:
|
|
129
|
+
flags.append({"kind": "soft", "rule_id": s["rule_id"], "reason": s["reason"]})
|
|
130
|
+
for sc in v.scope_flags:
|
|
131
|
+
flags.append({"kind": "scope", "reason": sc.get("reason", sc.get("kind"))})
|
|
132
|
+
return {"legal": v.legal, "confidence": v.confidence, "flags": flags, "n_hard": len(v.violations),
|
|
133
|
+
"n_soft": len(v.soft_flags), "suggested_revision": revision, "no_fabrication": v.no_fabrication}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def critique_and_revise(design: dict) -> dict:
|
|
137
|
+
"""One critique→revise→re-verify cycle. Returns before/after with whether plan quality IMPROVED
|
|
138
|
+
(illegal→legal, or fewer soft flags). The critic only swaps design choices; numbers stay tool-sourced."""
|
|
139
|
+
before = critique(design)
|
|
140
|
+
if before["suggested_revision"] is None:
|
|
141
|
+
return {"revised": False, "before": before, "after": before, "improved": False,
|
|
142
|
+
"note": "no deterministic fix available (critique not-yet-useful on this design)"}
|
|
143
|
+
after = critique(before["suggested_revision"])
|
|
144
|
+
improved = (bool(after["legal"]) and not before["legal"]) or (after["n_soft"] < before["n_soft"])
|
|
145
|
+
return {"revised": True, "revised_design": before["suggested_revision"], "before": before,
|
|
146
|
+
"after": after, "improved": bool(improved),
|
|
147
|
+
"no_fabrication": before["no_fabrication"] and after["no_fabrication"]}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# frozen falsifiability panel: FLAWED designs (a real fixable flaw) + CLEAN designs (no fixable flaw).
|
|
151
|
+
_FLAWED = [
|
|
152
|
+
{"write_type": "insertion", "writer_family": "bridge_IS110", "cargo_bp": 30000,
|
|
153
|
+
"delivery_vehicle": "AAV_single"}, # oversize -> AAV_dual/HDAd
|
|
154
|
+
{"write_type": "insertion", "writer_family": "Cas9", "cargo_bp": 1000,
|
|
155
|
+
"delivery_vehicle": "AAV_single"}, # RNP into DNA-only AAV -> electroporation
|
|
156
|
+
]
|
|
157
|
+
_CLEAN = [
|
|
158
|
+
{"write_type": "insertion", "writer_family": "bridge_IS110", "cargo_bp": 3000,
|
|
159
|
+
"delivery_vehicle": "AAV_single", "safety": 0.8, "p_durable": 0.7, "writer_activity": 0.7},
|
|
160
|
+
]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def critique_falsifiability() -> dict:
|
|
164
|
+
"""Falsifiability test (Principle 3): on FLAWED designs the critique→revise loop must IMPROVE plan
|
|
165
|
+
quality (illegal→legal); on CLEAN designs it must NOT spuriously change them. Reported."""
|
|
166
|
+
flawed = [critique_and_revise(d) for d in _FLAWED]
|
|
167
|
+
clean = [critique_and_revise(d) for d in _CLEAN]
|
|
168
|
+
improved = sum(int(r["improved"]) for r in flawed)
|
|
169
|
+
spurious = sum(int(r["revised"] and not r["improved"]) for r in clean)
|
|
170
|
+
return {"available": True, "n_flawed": len(_FLAWED), "n_clean": len(_CLEAN),
|
|
171
|
+
"flawed_improved": improved, "flawed_improve_rate": round(improved / len(_FLAWED), 3),
|
|
172
|
+
"clean_spurious_revisions": spurious,
|
|
173
|
+
"useful": improved == len(_FLAWED) and spurious == 0,
|
|
174
|
+
"no_fabrication": all(r.get("no_fabrication", True) for r in flawed + clean),
|
|
175
|
+
"note": "self-critique improves held-out FLAWED plans (illegal->legal) without touching CLEAN ones; "
|
|
176
|
+
"reported (Principle 3: falsifiable, not assumed beneficial)."}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# a fine-grained per-recommendation scope ledger: what WAS assessed vs what was NOT.
|
|
180
|
+
def scope_ledger(design: dict) -> dict:
|
|
181
|
+
"""Itemise, per recommendation, what the substrate ASSESSED (with its verdict/confidence) and what it
|
|
182
|
+
did NOT (the standing known-unknowns + any verifier scope flags). Out-of-scope is never silently omitted."""
|
|
183
|
+
import yaml
|
|
184
|
+
|
|
185
|
+
from pen_stack._resources import resource
|
|
186
|
+
from pen_stack.verify import verify
|
|
187
|
+
v = verify(design)
|
|
188
|
+
assessed = [
|
|
189
|
+
{"dimension": "rule_legality", "verdict": v.legal, "source": "rules.solver"},
|
|
190
|
+
{"dimension": "reachability", "verdict": not any(x["rule_id"].startswith("reachability.")
|
|
191
|
+
for x in v.violations), "source": "target_site rule"},
|
|
192
|
+
{"dimension": "delivery_compatibility", "verdict": not any(x["rule_id"].startswith("delivery.")
|
|
193
|
+
for x in v.violations), "source": "delivery rules"},
|
|
194
|
+
{"dimension": "payload_capacity", "verdict": not any(x["rule_id"].startswith("payload.")
|
|
195
|
+
for x in v.violations), "source": "payload rule"},
|
|
196
|
+
{"dimension": "calibrated_confidence",
|
|
197
|
+
"verdict": v.confidence, "source": "L4 uncertainty (abstains when unscored)"},
|
|
198
|
+
]
|
|
199
|
+
ku = yaml.safe_load(resource("configs/known_unknowns.yaml").read_text(encoding="utf-8"))["known_unknowns"]
|
|
200
|
+
not_assessed = [{"id": k["id"], "title": k.get("title"), "why": k.get("why")} for k in ku]
|
|
201
|
+
not_assessed += [{"id": "rule_scope", "title": sc.get("rule_id", sc.get("kind")),
|
|
202
|
+
"why": sc.get("reason")} for sc in v.scope_flags]
|
|
203
|
+
return {"design": {k: val for k, val in design.items() if not str(k).startswith("_")},
|
|
204
|
+
"assessed": assessed, "not_assessed": not_assessed,
|
|
205
|
+
"n_assessed": len(assessed), "n_not_assessed": len(not_assessed),
|
|
206
|
+
"complete": True, "no_fabrication": v.no_fabrication,
|
|
207
|
+
"note": "every recommendation carries a complete scope ledger; out-of-scope dimensions are "
|
|
208
|
+
"itemised (the known-unknowns), never silently omitted."}
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def deliberate(gene: str = "AAVS1", cargo_bp: int = 3000, cell_type: str = "K562") -> dict:
|
|
212
|
+
"""Head-to-head: the deliberative co-scientist (best of the distinct strategies) vs the
|
|
213
|
+
deterministic baseline (pen_agent state machine). Reports both; no-fabrication holds for both."""
|
|
214
|
+
delib = propose_strategies(gene, cargo_bp, cell_type, n=3)
|
|
215
|
+
best = delib["strategies"][0] if delib["strategies"] else None
|
|
216
|
+
baseline = {"available": False, "note": "deterministic pen_agent baseline needs the writability atlas (VM/local)"}
|
|
217
|
+
try:
|
|
218
|
+
from pen_stack.agent.pen_agent import plan_write_session
|
|
219
|
+
r = plan_write_session(gene, "safe_harbour_insertion", cargo_bp=cargo_bp, ct=cell_type.lower())
|
|
220
|
+
baseline = {"available": True, "no_fabrication": r.get("no_fabrication"),
|
|
221
|
+
"plan_confidence": r.get("plan_confidence"), "completed": r.get("completed")}
|
|
222
|
+
except Exception as e: # noqa: BLE001 - atlas absent -> baseline deferred, never fabricated
|
|
223
|
+
baseline["error"] = f"{type(e).__name__}"
|
|
224
|
+
return {"deliberative_best": best, "deliberative_n": delib["n_strategies"],
|
|
225
|
+
"distinctness": delib["distinctness"], "baseline": baseline,
|
|
226
|
+
"no_fabrication": delib["no_fabrication"] and baseline.get("no_fabrication", True),
|
|
227
|
+
"note": "deliberative planner explores distinct verified strategies; deterministic planner is the "
|
|
228
|
+
"baseline/fallback; both are grounded (no fabrication). Plan quality reported."}
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# the co-scientist drives the WHOLE loop for a working scientist.
|
|
232
|
+
# Every output is safe + legal + calibrated + cited + scope-ledgered + IMMUNE-PROFILED, and never
|
|
233
|
+
# fabricated. The scientist/lab decides; the co-scientist drives and presents.
|
|
234
|
+
def co_scientist_session(goal: dict, cell_state: str, *, candidates: list[dict] | None = None,
|
|
235
|
+
actor: str = "scientist") -> dict:
|
|
236
|
+
"""End-to-end, human-facing: safe legal designs -> predicted outcomes -> suggested experiments ->
|
|
237
|
+
exportable protocols. Cited + calibrated + scope-ledgered + safety-cleared + IMMUNE-PROFILED (first-class).
|
|
238
|
+
The scientist decides; the co-scientist drives. No number is fabricated."""
|
|
239
|
+
from pen_stack.active.design import select_batch
|
|
240
|
+
from pen_stack.agent.cite import cited_rationale
|
|
241
|
+
from pen_stack.design.generate import generate_designs
|
|
242
|
+
from pen_stack.design.pareto import pareto_front
|
|
243
|
+
from pen_stack.twin.outcome import predict_outcome
|
|
244
|
+
|
|
245
|
+
designs = generate_designs(goal, candidates=candidates, keep=8, actor=actor) # safe+legal+calibrated+immune
|
|
246
|
+
enriched = [{**d, "outcome": predict_outcome(d, cell_state)} for d in designs]
|
|
247
|
+
experiments = select_batch(enriched, cell_state, {}, k=4) if enriched else []
|
|
248
|
+
return {
|
|
249
|
+
"goal": goal,
|
|
250
|
+
"strategies": pareto_front(designs), # Pareto (incl. immune axis)
|
|
251
|
+
"predicted_outcomes": [e["outcome"] for e in enriched], # calibrated + scope
|
|
252
|
+
"immune_profiles": [d.get("immune_profile") for d in designs], # first-class, per-axis
|
|
253
|
+
"suggested_experiments": experiments, # info + immune-VOI
|
|
254
|
+
"protocols_available": True, # safety-gated on request
|
|
255
|
+
"citations": cited_rationale(designs[0]) if designs else {"available": False}, # cite (resolve-by-construction)
|
|
256
|
+
"scope_ledger": scope_ledger(designs[0]) if designs else {"available": False}, # assessed vs not
|
|
257
|
+
"safety": [d.get("safety_decision") for d in designs], # cleared/flagged
|
|
258
|
+
"n_designs": len(designs),
|
|
259
|
+
"no_fabrication": True,
|
|
260
|
+
"note": "the co-scientist DRIVES and PRESENTS (incl. the immune-risk profile with its known-unknowns); "
|
|
261
|
+
"the scientist/lab DECIDES. Every output is safe + legal + calibrated + cited + scope-ledgered.",
|
|
262
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Epistemic scoping, first-class "I don't know".
|
|
2
|
+
|
|
3
|
+
A thin, high-trust layer over the uncertainty-quantification signals (conformal confidence + OOD) and the existing grounding
|
|
4
|
+
machinery (provenance, refusals, scope matcher). It assigns every agent output **exactly one** of three
|
|
5
|
+
epistemic statuses, *driven by the signals, never hand-set*:
|
|
6
|
+
|
|
7
|
+
* **grounded-confident**, tool-grounded, in-distribution (low OOD), tight/calibrated, above the
|
|
8
|
+
abstention threshold.
|
|
9
|
+
* **grounded-extrapolating**, tool-grounded but the OOD detector flags the query as far from training
|
|
10
|
+
data, or the conformal interval is wide / confidence low. The number is
|
|
11
|
+
real but the model is extrapolating, trust it less.
|
|
12
|
+
* **not-computable**, no tool can ground it: the step refused, the query is out of scope (a
|
|
13
|
+
known-unknown), or the agent abstained. The "I don't know."
|
|
14
|
+
|
|
15
|
+
This makes trustworthiness *legible*: a reader sees not just the number but how much the system stands
|
|
16
|
+
behind it. The status is a pure function of the inputs, so it is deterministic and testable.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
|
|
22
|
+
GROUNDED_CONFIDENT = "grounded-confident"
|
|
23
|
+
GROUNDED_EXTRAPOLATING = "grounded-extrapolating"
|
|
24
|
+
NOT_COMPUTABLE = "not-computable"
|
|
25
|
+
|
|
26
|
+
# OOD widen-factor at/above which a grounded answer is treated as extrapolating (matches the OODDetector
|
|
27
|
+
# widen_factor scale: 1.0 = in-distribution, rising to the cap). 1.5 = "halfway to the cap", the same
|
|
28
|
+
# threshold the planner's attach_uncertainty uses, so the two layers agree.
|
|
29
|
+
OOD_EXTRAPOLATE_FACTOR = 1.5
|
|
30
|
+
# plan/answer confidence below this abstains (matches planner attach_uncertainty abstain_below default).
|
|
31
|
+
ABSTAIN_CONFIDENCE = 0.5
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class EpistemicVerdict:
|
|
36
|
+
status: str
|
|
37
|
+
confidence: float | None
|
|
38
|
+
reason: str
|
|
39
|
+
grounded: bool
|
|
40
|
+
ood_factor: float | None = None
|
|
41
|
+
out_of_scope: bool = False
|
|
42
|
+
|
|
43
|
+
def to_dict(self) -> dict:
|
|
44
|
+
return {"epistemic_status": self.status, "confidence": self.confidence, "reason": self.reason,
|
|
45
|
+
"grounded": self.grounded, "ood_factor": self.ood_factor,
|
|
46
|
+
"out_of_scope": self.out_of_scope}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def classify(grounded: bool, confidence: float | None = None, ood_factor: float | None = None,
|
|
50
|
+
abstained: bool = False, out_of_scope: bool = False,
|
|
51
|
+
refused: bool = False) -> EpistemicVerdict:
|
|
52
|
+
"""Assign exactly one epistemic status from the (UQ/OOD/grounding/scope) signals.
|
|
53
|
+
|
|
54
|
+
Precedence (most-specific-wins): out-of-scope / refused / abstained → not-computable; else if grounded but
|
|
55
|
+
OOD-flagged or low-confidence → grounded-extrapolating; else grounded-confident. An ungrounded answer
|
|
56
|
+
that is none of the above is still not-computable (we never label an ungrounded number 'confident').
|
|
57
|
+
"""
|
|
58
|
+
if out_of_scope:
|
|
59
|
+
return EpistemicVerdict(NOT_COMPUTABLE, None,
|
|
60
|
+
"out of scope, a known-unknown PEN-STACK does not model", grounded=False,
|
|
61
|
+
ood_factor=ood_factor, out_of_scope=True)
|
|
62
|
+
if refused or not grounded:
|
|
63
|
+
return EpistemicVerdict(NOT_COMPUTABLE, None,
|
|
64
|
+
"no validated tool can ground this value", grounded=False,
|
|
65
|
+
ood_factor=ood_factor)
|
|
66
|
+
if abstained or (confidence is not None and confidence < ABSTAIN_CONFIDENCE):
|
|
67
|
+
reason = ("abstained, no calibrated confidence was computed" if confidence is None
|
|
68
|
+
else f"abstained, confidence {confidence} below {ABSTAIN_CONFIDENCE}")
|
|
69
|
+
return EpistemicVerdict(NOT_COMPUTABLE, confidence, reason,
|
|
70
|
+
grounded=True, ood_factor=ood_factor)
|
|
71
|
+
if ood_factor is not None and ood_factor >= OOD_EXTRAPOLATE_FACTOR:
|
|
72
|
+
return EpistemicVerdict(GROUNDED_EXTRAPOLATING, confidence,
|
|
73
|
+
f"grounded but extrapolating, OOD factor {round(ood_factor, 3)} ≥ "
|
|
74
|
+
f"{OOD_EXTRAPOLATE_FACTOR} (query far from training data)",
|
|
75
|
+
grounded=True, ood_factor=ood_factor)
|
|
76
|
+
reason = ("grounded, in-distribution, calibrated" if confidence is not None
|
|
77
|
+
else "grounded in a validated tool output, in-distribution")
|
|
78
|
+
return EpistemicVerdict(GROUNDED_CONFIDENT, confidence, reason, grounded=True, ood_factor=ood_factor)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def classify_step(step_status: str, confidence: float | None = None, ood_factor: float | None = None,
|
|
82
|
+
out_of_scope: bool = False) -> dict:
|
|
83
|
+
"""Map a PEN-Agent step (`ok` | `degraded` | `refused`) + UQ/OOD signals → an epistemic verdict dict.
|
|
84
|
+
|
|
85
|
+
`ok` steps are grounded; `degraded`/`refused` steps are not-computable. This is the bridge the agent
|
|
86
|
+
uses to tag every step (see agent.pen_agent)."""
|
|
87
|
+
grounded = step_status == "ok"
|
|
88
|
+
refused = step_status in ("refused", "degraded")
|
|
89
|
+
return classify(grounded=grounded, confidence=confidence, ood_factor=ood_factor,
|
|
90
|
+
out_of_scope=out_of_scope, refused=refused).to_dict()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def summarize(verdicts: list[dict]) -> dict:
|
|
94
|
+
"""Roll up per-output verdicts into a session-level epistemic summary (counts per status)."""
|
|
95
|
+
counts = {GROUNDED_CONFIDENT: 0, GROUNDED_EXTRAPOLATING: 0, NOT_COMPUTABLE: 0}
|
|
96
|
+
for v in verdicts:
|
|
97
|
+
counts[v.get("epistemic_status", NOT_COMPUTABLE)] = counts.get(
|
|
98
|
+
v.get("epistemic_status", NOT_COMPUTABLE), 0) + 1
|
|
99
|
+
n = len(verdicts) or 1
|
|
100
|
+
return {"counts": counts, "n": len(verdicts),
|
|
101
|
+
"fraction_grounded_confident": round(counts[GROUNDED_CONFIDENT] / n, 4),
|
|
102
|
+
"all_tagged": all("epistemic_status" in v for v in verdicts)}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""LLM guardrails for PEN-STACK platform services.
|
|
2
|
+
|
|
3
|
+
The contract every service obeys: **grounded** (answers from the curated atlas + indexed literature),
|
|
4
|
+
**cited** (every factual claim carries a source), **defer-to-models** (any quantitative claim is produced
|
|
5
|
+
by a validated tool call, never guessed by the LLM), **decision-support** (never a clinical directive),
|
|
6
|
+
**budget-aware**, **auditable** (a provenance block accompanies every answer).
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
DISCLAIMER = ("Decision-support only - PEN-STACK returns calibrated risk/durability/reachability "
|
|
13
|
+
"estimates, not clinical directives. Tier-2/3 reachability is candidate and requires "
|
|
14
|
+
"experimental validation. Verify all designs experimentally.")
|
|
15
|
+
|
|
16
|
+
# Questions PEN-STACK must refuse: clinical directives, diagnosis, dosing, treatment decisions for a
|
|
17
|
+
# specific patient. (Scientific questions about loci/writers/safety are in scope.)
|
|
18
|
+
_REFUSE_PATTERNS = [
|
|
19
|
+
r"\bshould i (treat|inject|dose|administer|give)\b",
|
|
20
|
+
r"\b(diagnos|prescrib|dosage|dosing)\w*\b",
|
|
21
|
+
r"\b(my|this|the) patient\b",
|
|
22
|
+
r"\bdose\b.{0,40}\b(child|patient|human|person|kid|baby|infant)\b", # dosing for a person = clinical
|
|
23
|
+
r"\b(what|which) dose\b", # dosing questions are clinical
|
|
24
|
+
r"\bis it safe (to|for) (a |the |my )?(patient|human|person|child)\b",
|
|
25
|
+
r"\bclinical (decision|recommendation|advice) for\b",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def out_of_scope(question: str) -> str | None:
|
|
30
|
+
"""Return a refusal reason if the question is a clinical directive, else None."""
|
|
31
|
+
q = question.lower()
|
|
32
|
+
for pat in _REFUSE_PATTERNS:
|
|
33
|
+
if re.search(pat, q):
|
|
34
|
+
return ("This is a clinical-directive question. PEN-STACK is decision-support "
|
|
35
|
+
"infrastructure for genome-writing design and does not give clinical advice.")
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def check_question(question: str) -> dict | None:
|
|
40
|
+
"""Combined gate for a free-text question. Returns a structured deferral/refusal or None.
|
|
41
|
+
|
|
42
|
+
Two distinct out-of-scope arms, both ending in zero fabrication: (1) clinical-directive REFUSAL
|
|
43
|
+
(`out_of_scope`), and (2) known-unknown DEFERRAL via the scope matcher (biology beyond any tool here).
|
|
44
|
+
Clinical refusal takes precedence (a clinical question is refused even if it also names a known-unknown).
|
|
45
|
+
"""
|
|
46
|
+
clinical = out_of_scope(question)
|
|
47
|
+
if clinical:
|
|
48
|
+
return {"kind": "clinical_refusal", "epistemic_status": "not-computable", "message": clinical}
|
|
49
|
+
from pen_stack.agent.scope import match_scope
|
|
50
|
+
oos = match_scope(question)
|
|
51
|
+
if oos:
|
|
52
|
+
return {"kind": "out_of_scope", "epistemic_status": "not-computable",
|
|
53
|
+
"message": oos["deferral"], **oos}
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def enforce_grounded(answer: dict) -> dict:
|
|
58
|
+
"""Assert the auditable contract on a finished answer: numeric claims must trace to a tool call."""
|
|
59
|
+
answer.setdefault("disclaimer", DISCLAIMER)
|
|
60
|
+
answer.setdefault("provenance", [])
|
|
61
|
+
answer.setdefault("citations", [])
|
|
62
|
+
# if the answer reports numbers, there must be a tool-call provenance entry backing them
|
|
63
|
+
has_number = bool(re.search(r"\d", str(answer.get("answer", ""))))
|
|
64
|
+
if has_number and not answer["provenance"]:
|
|
65
|
+
answer["warning"] = "numeric claim without tool provenance - suppressed"
|
|
66
|
+
answer["answer"] = "(suppressed: a number was produced without a backing tool call)"
|
|
67
|
+
return answer
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""PEN-STACK MCP server: expose the validated capabilities to any agent.
|
|
2
|
+
|
|
3
|
+
Wraps the validated tools as a Model Context Protocol server (fastmcp) so any MCP client (Claude, etc.)
|
|
4
|
+
can call ``writability``, ``reachable_writers``, ``writer_axes``, ``plan_write``, ``ask_literature`` and the
|
|
5
|
+
grounded ``plan_write_session`` (the full PEN-Agent state machine) and receive correct, provenance-tagged
|
|
6
|
+
results - turning PEN-STACK into shared agentic infrastructure.
|
|
7
|
+
|
|
8
|
+
Run: ``python -m pen_stack.agent.mcp_server`` (needs the ``services`` extra: ``pip install fastmcp``).
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from pen_stack.agent import pen_agent, tools
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from fastmcp import FastMCP
|
|
16
|
+
except ImportError as e: # pragma: no cover - services extra optional
|
|
17
|
+
raise ImportError("fastmcp not installed: pip install 'pen-stack[services]'") from e
|
|
18
|
+
|
|
19
|
+
mcp = FastMCP("pen-stack")
|
|
20
|
+
|
|
21
|
+
# register each validated tool (the same functions the in-process agent and the eval harness use)
|
|
22
|
+
mcp.tool()(tools.writability)
|
|
23
|
+
mcp.tool()(tools.reachable_writers)
|
|
24
|
+
mcp.tool()(tools.writer_axes)
|
|
25
|
+
mcp.tool()(tools.plan_write)
|
|
26
|
+
mcp.tool()(tools.ask_literature)
|
|
27
|
+
mcp.tool()(tools.multiplex_translocation_risk) # multiplex translocation-risk screen
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@mcp.tool()
|
|
31
|
+
def plan_write_session(gene: str, intent: str, cargo_bp: int = 2000, ct: str = "k562",
|
|
32
|
+
payload_seq: str | None = None, mode: str = "automatic") -> dict:
|
|
33
|
+
"""PEN-Agent: grounded write-planning state machine (site -> writer -> cargo+polish -> off-target -> 3D).
|
|
34
|
+
|
|
35
|
+
Every number is copied from a tool result with provenance; ungrounded steps degrade/refuse, never
|
|
36
|
+
fabricate. Modes: automatic | guided | qa."""
|
|
37
|
+
return pen_agent.plan_write_session(gene, intent, cargo_bp=cargo_bp, ct=ct,
|
|
38
|
+
payload_seq=payload_seq, mode=mode)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@mcp.tool()
|
|
42
|
+
def verify_write(design: dict) -> dict:
|
|
43
|
+
"""Verifier: submit a proposed genomic write as a dict (write_type, writer_family, site_seq,
|
|
44
|
+
cargo_bp, delivery_vehicle, cell_type, edit_intent, no_integration, target_guide/donor_guide, edits, ...)
|
|
45
|
+
and get back a Verdict: legal/illegal + the named violated rule(s) + citation, a calibrated confidence on
|
|
46
|
+
the soft components, an epistemic status, and any out-of-scope flags. Legality and confidence are distinct
|
|
47
|
+
axes; every number traces to a tool (no fabrication). Unsupported write types defer."""
|
|
48
|
+
from pen_stack.verify import verify
|
|
49
|
+
return verify(design).model_dump()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@mcp.tool()
|
|
53
|
+
def verify_proof(design: dict) -> dict:
|
|
54
|
+
"""Verifier: the repair-oriented proof object for a proposed write. Returns the three
|
|
55
|
+
axes (legality, confidence, biosecurity) reported separately, each with a status, the rule or signature
|
|
56
|
+
that fired, evidence, and a repair hint; the collapsed verdict is None. An agent fixes a failed-on-legality
|
|
57
|
+
design from the legality axis's repair hint and re-verifies. Biosecurity hazards are acknowledged and
|
|
58
|
+
routed to human review, never given an actionable repair."""
|
|
59
|
+
from pen_stack.verify.proof import verify_proof as _vp
|
|
60
|
+
return _vp(design).model_dump()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@mcp.tool()
|
|
64
|
+
def validation_campaign() -> dict:
|
|
65
|
+
"""The validation-campaign engine. Returns the expression-validation campaign: the
|
|
66
|
+
next batch of (cassette x locus x cell type) measurements ordered by expected information gain, the
|
|
67
|
+
calibrate_axis gate it targets (the path to the program's first outcome-validated axis), and the
|
|
68
|
+
active-vs-random result reported verbatim. Cloud-lab-executable; Level 3, human in control; the experiments
|
|
69
|
+
are candidates and the wet run is the standing bottleneck."""
|
|
70
|
+
from pen_stack.active.campaign import design_campaign
|
|
71
|
+
return design_campaign()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@mcp.tool()
|
|
75
|
+
def cloudlab_submit(design: dict, experiment: dict | None = None, provider: str = "mock") -> dict:
|
|
76
|
+
"""Safety-gated cloud-lab submission. The biosecurity gate runs BEFORE submission; a flagged
|
|
77
|
+
design returns a structured refusal (blocked=True) and NO protocol is emitted. A cleared design returns a
|
|
78
|
+
mock / dry-run job receipt (a real run needs a cloud-lab partner + budget). Level 3, human in control."""
|
|
79
|
+
from pen_stack.build.cloudlab import submit_gated
|
|
80
|
+
return submit_gated(design, experiment or {}, provider=provider, actor="mcp")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@mcp.tool()
|
|
84
|
+
def writespec_parse(prose: str, check_feasibility: bool = True) -> dict:
|
|
85
|
+
"""Parse a plain-language genome-writing request into a typed, ontology-backed WriteSpec (an
|
|
86
|
+
SBOL3 profile). Returns the typed spec with per-field provenance (explicit / inferred / user / unresolved),
|
|
87
|
+
the assumptions behind every inferred field, clarifying questions for anything underspecified or ambiguous,
|
|
88
|
+
the unresolved terms (kept null, never invented), the downstream design adapter, and a feasibility verdict
|
|
89
|
+
(reachability + deliverability + legality, with named blocking constraints + repair hints). A WriteSpec is a
|
|
90
|
+
REQUEST, not a claim; the extractor never fabricates intent."""
|
|
91
|
+
from pen_stack.spec.service import parse_request
|
|
92
|
+
return parse_request(prose, check_feasibility=check_feasibility)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@mcp.tool()
|
|
96
|
+
def oracle_query(oracle: str | None = None, protein_seq: str | None = None, ligand_smiles: str | None = None,
|
|
97
|
+
pair_type: str = "ligand", ligand_name: str | None = None) -> dict:
|
|
98
|
+
"""Query the oracle mesh under one contract. With no arguments, returns every oracle's
|
|
99
|
+
execution + latency + live status + PUBLISHED reliability (reported verbatim from public benchmarks, with
|
|
100
|
+
citation; not a claim about this stack's accuracy) and the disagreement-to-interval check. With `oracle` set,
|
|
101
|
+
returns that oracle's status + reliability. With `protein_seq` + `ligand_smiles`, returns a CANDIDATE
|
|
102
|
+
binding-affinity prediction (Boltz-2 head): a binder probability + a predicted value with native uncertainty,
|
|
103
|
+
cache-or-abstain; protein-protein/protein-DNA pair types are flagged extrapolating (the head is
|
|
104
|
+
protein-ligand only). The long GPU job never runs on the request path."""
|
|
105
|
+
from pen_stack.oracles.status import oracle_status, summary
|
|
106
|
+
if protein_seq and ligand_smiles:
|
|
107
|
+
from pen_stack.oracles.affinity import predict_affinity
|
|
108
|
+
return predict_affinity(protein_seq, ligand_smiles, pair_type=pair_type, ligand_name=ligand_name).model_dump()
|
|
109
|
+
st = oracle_status()
|
|
110
|
+
if oracle:
|
|
111
|
+
return {"oracle": oracle, "status": st.get(oracle), "found": oracle in st}
|
|
112
|
+
return {"summary": summary(), "oracles": st}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@mcp.tool()
|
|
116
|
+
def graph_query(locus: str, cargo_form: str | None = None) -> dict:
|
|
117
|
+
"""World-model graph: a multi-hop query. Returns the writer families that REACH `locus` AND
|
|
118
|
+
are DELIVERABLE by a vehicle carrying `cargo_form` (optional), each answer with its provenanced edge path
|
|
119
|
+
(the answer IS the path, no fabrication). The graph nodes/edges carry evidence kind + scope + provenance."""
|
|
120
|
+
from pen_stack.graph import writers_reaching_and_deliverable
|
|
121
|
+
return writers_reaching_and_deliverable(locus, cargo_form=cargo_form)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ---- the AI Integration Surface: self-describing resources + the engine tools ----
|
|
125
|
+
@mcp.resource("pen-stack://capabilities")
|
|
126
|
+
def capabilities_resource() -> dict:
|
|
127
|
+
"""WHAT PEN-STACK can do (machine-readable). An agent reads this and routes on it, not on prose."""
|
|
128
|
+
from pen_stack.api.manifest import capability_manifest
|
|
129
|
+
return capability_manifest()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@mcp.resource("pen-stack://scope")
|
|
133
|
+
def scope_resource() -> dict:
|
|
134
|
+
"""WHAT PEN-STACK REFUSES to answer: the known-unknowns + the oracle scope cards. The contract that makes
|
|
135
|
+
depending on PEN-STACK safe, outputs outside scope are out_of_scope/extrapolating, never asserted."""
|
|
136
|
+
from pen_stack.api.manifest import scope_manifest
|
|
137
|
+
return scope_manifest()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@mcp.tool()
|
|
141
|
+
def safety_screen(design: dict) -> dict:
|
|
142
|
+
"""Guardian: biosecurity / dual-use screen -> SafetyVerdict (clear/flag/escalate/refuse) + reason. A
|
|
143
|
+
hazardous design returns a STRUCTURED refusal an agent can branch on (decision == 'refuse')."""
|
|
144
|
+
from pen_stack.safety import safety_gate
|
|
145
|
+
return safety_gate(design, actor=str(design.get("actor", "mcp"))).model_dump()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@mcp.tool()
|
|
149
|
+
def immune_profile(design: dict) -> dict:
|
|
150
|
+
"""Immune-risk profile: a per-axis screen (genotox/CD8/innate/NAb/anti-PEG), each with its own
|
|
151
|
+
uncertainty + validation label. Never collapsed into one number (collapsed_score is None); magnitude is a
|
|
152
|
+
declared known-unknown."""
|
|
153
|
+
from pen_stack.planner.immune_profile import immune_profile as _ip
|
|
154
|
+
return _ip(design)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@mcp.tool()
|
|
158
|
+
def offtarget_scan(writer_family: str, guide: str | None = None, candidate_sites: list | None = None,
|
|
159
|
+
sequence: str | None = None, assay: str = "guideseq", enzyme: str | None = None,
|
|
160
|
+
max_mismatch: int = 5) -> dict:
|
|
161
|
+
"""Genome-wide, per-mechanism off-target FINDER (NOT a clearance). For a nuclease guide it
|
|
162
|
+
enumerates the genome-wide off-target set over GRCh38 (Cas-OFFinder, cached) + real CRISOT + risk + chromatin
|
|
163
|
+
(validated); serine integrase = genome-wide pseudo-attP scan (semi-validated); bridge = DMS-scored scan
|
|
164
|
+
(mechanism-based, unvalidated); CAST = guide-directed + untargeted-transposition background (unvalidated);
|
|
165
|
+
PASTE = nuclease + integrase composition. Each carries a truthful status label + the confirming assay.
|
|
166
|
+
Abstains for a novel input (VM scan); never fabricates sites."""
|
|
167
|
+
from pen_stack.wgenome.offtarget_predict import nominate_offtargets
|
|
168
|
+
return nominate_offtargets(writer_family, guide=guide, candidate_sites=candidate_sites,
|
|
169
|
+
sequence=sequence, assay=assay, enzyme=enzyme, max_mismatch=max_mismatch)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@mcp.tool()
|
|
173
|
+
def delivery_recommend(cargo_form: str, cargo_bp: int | None = None, target_tissue: str | None = None,
|
|
174
|
+
safety_weight: float = 0.5, in_vivo: bool | None = None) -> dict:
|
|
175
|
+
"""Cross-modality delivery recommender: rank vehicles by cargo-form + safety<->efficacy + a
|
|
176
|
+
GROUNDED serotype->tissue tropism prior (from approved AAV therapies; a known-unknown for novel capsids), plus
|
|
177
|
+
the learned FLIP-AAV capsid-fitness capability. Never fabricates tropism; abstains without inputs."""
|
|
178
|
+
from pen_stack.planner.delivery_predict import recommend_delivery_plus
|
|
179
|
+
return recommend_delivery_plus(cargo_form, cargo_bp, target_tissue, safety_weight=safety_weight, in_vivo=in_vivo)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@mcp.tool()
|
|
183
|
+
def generate_designs(goal: dict | None = None, candidates: list | None = None, keep: int = 25) -> dict:
|
|
184
|
+
"""Generative designer (verifier-as-discriminator): hazardous/illegal candidates are DISCARDED;
|
|
185
|
+
survivors are calibrated + immune-profiled CANDIDATES (never asserted to work)."""
|
|
186
|
+
from pen_stack.design import generate_designs as _gd
|
|
187
|
+
return {"survivors": _gd(goal, candidates=candidates, keep=keep, actor="mcp")}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@mcp.tool()
|
|
191
|
+
def predict_outcome(design: dict, cell_state: str = "k562") -> dict:
|
|
192
|
+
"""Digital twin: a calibrated, OOD-gated, phenotype-bounded outcome (interval + scope flags). A
|
|
193
|
+
candidate prediction, never the truth; phenotype/in-vivo magnitude stay out of scope."""
|
|
194
|
+
from pen_stack.twin import predict_outcome as _po
|
|
195
|
+
return _po(design, cell_state)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@mcp.tool()
|
|
199
|
+
def suggest_experiment(candidates: list, cell_state: str = "k562", k: int = 8) -> dict:
|
|
200
|
+
"""Experiment designer: a diverse, informative next-experiment batch (EIG + immune-VOI), each with
|
|
201
|
+
its expected information gain."""
|
|
202
|
+
from pen_stack.active import select_batch
|
|
203
|
+
return {"batch": select_batch(candidates, cell_state, {}, k=k)}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@mcp.tool()
|
|
207
|
+
def co_scientist_session(goal: dict, cell_state: str = "k562") -> dict:
|
|
208
|
+
"""Co-scientist: drive the full loop -> Pareto strategies + calibrated outcomes + per-axis immune
|
|
209
|
+
profiles + suggested experiments + citations + scope ledger + safety. The scientist decides; this drives."""
|
|
210
|
+
from pen_stack.agent.co_scientist import co_scientist_session as _cs
|
|
211
|
+
return _cs(goal, cell_state)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
if __name__ == "__main__": # pragma: no cover
|
|
215
|
+
mcp.run()
|